This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch test/document-datamapping-core-transformers in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 3b9909fe3384ef57b16a56fa9abdd36aca423cec Author: Walter Duque de Estrada <[email protected]> AuthorDate: Thu Aug 13 11:41:33 2026 -0500 Fix @Tenant compile-time crash and add coverage for TenantTransform's closure-resolver path Writing a test for @Tenant's closure-based tenant resolution (the one branch of TenantTransform#buildDelegatingMethodCall the existing CurrentTenant/WithoutTenant specs never exercised) surfaced a real bug: supplying a non-closure @Tenant value crashed the compiler with an internal NullPointerException wrapped as a GroovyBugError instead of a clean compile error. Root cause: the error path called the inherited AbstractASTTransformation#addError(String, ASTNode), which reads from that class's own sourceUnit field - never populated anywhere in this transform's call chain, since sourceUnit is threaded through as a method parameter instead. Fixed by reporting the error directly through that parameter's error collector, matching the pattern already used elsewhere in this codebase for AST transforms that don't rely on the inherited field. Also add tests for @Tenant applied at both method and class level, the now-fixed non-closure error path, getAnnotationType(), and the two hasTenantAnnotation branches (a method with @WithoutTenant, and being called directly with a bare ClassNode) that weren't reached by any existing spec. Coverage moves 65% -> 95% instruction, 67% -> 82% branch. Co-Authored-By: Claude Sonnet 5 <[email protected]> --- .../multitenancy/transform/TenantTransform.groovy | 8 +- .../multitenancy/TenantTransformSpec.groovy | 168 +++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy index 0e6b15ff60..358fe731b9 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/multitenancy/transform/TenantTransform.groovy @@ -33,6 +33,8 @@ import org.codehaus.groovy.ast.expr.VariableExpression import org.codehaus.groovy.ast.stmt.BlockStatement import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.control.messages.SyntaxErrorMessage +import org.codehaus.groovy.syntax.SyntaxException import org.codehaus.groovy.transform.GroovyASTTransformation import grails.gorm.multitenancy.CurrentTenant @@ -136,7 +138,11 @@ class TenantTransform extends AbstractDatastoreMethodDecoratingTransformation { return makeDelegatingClosureCall(tenantServiceVar, 'withId', args(tenantIdVar), params(param(serializableClassNode, VAR_TENANT_ID)), originalMethodCallExpr, variableScope) } else { - addError('@Tenant value should be a closure', annotationNode) + sourceUnit.getErrorCollector().addErrorAndContinue( + new SyntaxErrorMessage(new SyntaxException('@Tenant value should be a closure', + annotationNode.getLineNumber(), annotationNode.getColumnNumber(), + annotationNode.getLastLineNumber(), annotationNode.getLastColumnNumber()), sourceUnit) + ) return makeDelegatingClosureCall(tenantServiceVar, 'withCurrent', params(param(serializableClassNode, VAR_TENANT_ID)), originalMethodCallExpr, variableScope) } } diff --git a/grails-datamapping-core/src/test/groovy/grails/gorm/annotation/multitenancy/TenantTransformSpec.groovy b/grails-datamapping-core/src/test/groovy/grails/gorm/annotation/multitenancy/TenantTransformSpec.groovy new file mode 100644 index 0000000000..d2b10d0f51 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/grails/gorm/annotation/multitenancy/TenantTransformSpec.groovy @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.annotation.multitenancy + +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.MultipleCompilationErrorsException +import org.codehaus.groovy.control.Phases + +import spock.lang.Specification + +import org.grails.datastore.gorm.multitenancy.transform.TenantTransform + +/** + * {@link CurrentTenantTransformSpec} only exercises {@code @CurrentTenant} and + * {@code @WithoutTenant}, which both take {@code TenantTransform#buildDelegatingMethodCall}'s first + * two branches. The third branch - the actual {@code @Tenant} annotation, which resolves the tenant + * id from a closure supplied as the annotation value - and its own error path (a non-closure value) + * were unexercised. These specs cover that branch directly, plus the small standalone + * {@code getAnnotationType()}/{@code hasTenantAnnotation} surface not reached by any existing spec. + */ +class TenantTransformSpec extends Specification { + + void "test @Tenant with a closure value transforms a method to resolve the tenant id from the closure"() { + given: 'a service with @Tenant applied at the method level with a closure tenant resolver' + def bookService = new GroovyShell().evaluate(''' +import grails.gorm.multitenancy.Tenant + +class BookService { + @Tenant({ "someTenant" }) + List listBooks() { + return ["The Stand"] + } +} +new BookService() + +''') + when: 'the list books method is invoked' + bookService.listBooks() + + then: 'an exception was thrown because GORM is not setup, proving the delegating call was generated and reached' + thrown(IllegalStateException) + } + + void "test @Tenant applied at the class level transforms every method the same way"() { + given: 'a service with @Tenant applied at the class level with a closure tenant resolver' + def bookService = new GroovyShell().evaluate(''' +import grails.gorm.multitenancy.Tenant + +@Tenant({ "someTenant" }) +class BookService { + List listBooks() { + return ["The Stand"] + } +} +new BookService() + +''') + when: + bookService.listBooks() + + then: + thrown(IllegalStateException) + } + + void "test @Tenant with a non-closure value fails to compile"() { + when: + new GroovyClassLoader().parseClass(''' +import grails.gorm.multitenancy.Tenant + +class BookService { + @Tenant(String) + List listBooks() { + return ["The Stand"] + } +} +''') + + then: + MultipleCompilationErrorsException e = thrown() + e.message.contains('@Tenant value should be a closure') + } + + void "getAnnotationType returns the @Tenant annotation type"() { + expect: + new TenantTransform().getAnnotationType() == TenantTransform.TENANT_ANNOTATION_TYPE + } + + void "hasTenantAnnotation returns false for a method annotated with @WithoutTenant even though the class is annotated with @CurrentTenant"() { + given: + MethodNode methodNode = compileAndFindMethod(''' +import grails.gorm.multitenancy.CurrentTenant +import grails.gorm.multitenancy.WithoutTenant + +@CurrentTenant +class BookService { + @WithoutTenant + List listBooks() { + return [] + } +} +''', 'BookService', 'listBooks') + + expect: + !TenantTransform.hasTenantAnnotation(methodNode) + } + + void "hasTenantAnnotation returns true when called directly with a class node carrying @Tenant"() { + given: + ClassNode classNode = compileAndFindClass(''' +import grails.gorm.multitenancy.Tenant + +@Tenant({ "someTenant" }) +class BookService { + List listBooks() { + return [] + } +} +''', 'BookService') + + expect: + TenantTransform.hasTenantAnnotation(classNode) + } + + void "hasTenantAnnotation returns false for a plain class node with no tenant annotations"() { + given: + ClassNode classNode = compileAndFindClass(''' +class PlainService { + List listBooks() { + return [] + } +} +''', 'PlainService') + + expect: + !TenantTransform.hasTenantAnnotation(classNode) + } + + private static ClassNode compileAndFindClass(String source, String className) { + CompilationUnit unit = new CompilationUnit(new GroovyClassLoader()) + def sourceUnit = unit.addSource('Source.groovy', source) + unit.compile(Phases.CANONICALIZATION) + ModuleNode moduleNode = sourceUnit.getAST() + (ClassNode) moduleNode.classes.find { ClassNode cn -> cn.name == className } + } + + private static MethodNode compileAndFindMethod(String source, String className, String methodName) { + compileAndFindClass(source, className).methods.find { it.name == methodName } + } +}
