This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch gsp-taglib-compilestatic in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit b0250102d2756b636cdba3126e4d7dd50675e3cf Author: James Daugherty <[email protected]> AuthorDate: Wed Mar 18 00:02:53 2026 -0400 #15506 - fix - @CompileStatic support for tag libs --- .../grails/core/gsp/DefaultGrailsTagLibClass.java | 17 ++++ .../core/gsp/DefaultGrailsTagLibClassSpec.groovy | 108 +++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java b/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java index 9719da62d9..87342d935a 100644 --- a/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java +++ b/grails-gsp/grails-taglib/src/main/groovy/org/grails/core/gsp/DefaultGrailsTagLibClass.java @@ -18,6 +18,7 @@ */ package org.grails.core.gsp; +import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.HashSet; @@ -70,6 +71,22 @@ public class DefaultGrailsTagLibClass extends AbstractInjectableGrailsClass impl } } + // Also scan declared fields via Java reflection to find Closure-typed tags + // that may not be reported by the metaclass (e.g., when @CompileStatic is applied + // at the class level, Groovy 4 may compile Closure properties differently so that + // MetaProperty.getType() no longer reports Closure). + for (Class<?> current = clazz; current != null && current != Object.class; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers)) { + continue; + } + if (Closure.class.isAssignableFrom(field.getType())) { + tags.add(field.getName()); + } + } + } + String ns = getStaticPropertyValue(NAMESPACE_FIELD_NAME, String.class); if (ns != null && !"".equals(ns.trim())) { namespace = ns.trim(); diff --git a/grails-gsp/grails-taglib/src/test/groovy/org/grails/core/gsp/DefaultGrailsTagLibClassSpec.groovy b/grails-gsp/grails-taglib/src/test/groovy/org/grails/core/gsp/DefaultGrailsTagLibClassSpec.groovy new file mode 100644 index 0000000000..422b06b5fe --- /dev/null +++ b/grails-gsp/grails-taglib/src/test/groovy/org/grails/core/gsp/DefaultGrailsTagLibClassSpec.groovy @@ -0,0 +1,108 @@ +/* + * 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 org.grails.core.gsp + +import groovy.transform.CompileStatic + +import spock.lang.Issue +import spock.lang.Specification + +class DefaultGrailsTagLibClassSpec extends Specification { + + def "tag discovery finds Closure properties in a dynamic TagLib"() { + when: + def tagLibClass = new DefaultGrailsTagLibClass(DynamicSampleTagLib) + + then: + tagLibClass.tagNames.contains('myTag') + tagLibClass.tagNames.contains('anotherTag') + !tagLibClass.tagNames.contains('nonClosureProperty') + !tagLibClass.tagNames.contains('someStaticClosure') + } + + @Issue('https://github.com/apache/grails-core/issues/15506') + def "tag discovery finds Closure properties in a @CompileStatic TagLib"() { + when: + def tagLibClass = new DefaultGrailsTagLibClass(CompileStaticSampleTagLib) + + then: + tagLibClass.tagNames.contains('staticTag') + tagLibClass.tagNames.contains('anotherStaticTag') + !tagLibClass.tagNames.contains('nonClosureField') + !tagLibClass.tagNames.contains('someStaticClosure') + } + + @Issue('https://github.com/apache/grails-core/issues/15506') + def "tag discovery finds Closure properties from both parent and child classes with @CompileStatic"() { + when: + def tagLibClass = new DefaultGrailsTagLibClass(ChildCompileStaticTagLib) + + then: + tagLibClass.tagNames.contains('parentTag') + tagLibClass.tagNames.contains('childTag') + } + + def "namespace is correctly read from TagLib"() { + when: + def tagLibClass = new DefaultGrailsTagLibClass(CustomNamespaceTagLib) + + then: + tagLibClass.namespace == 'custom' + } + + def "returnObjectForTags is correctly read from TagLib"() { + when: + def tagLibClass = new DefaultGrailsTagLibClass(DynamicSampleTagLib) + + then: + tagLibClass.tagNamesThatReturnObject.contains('myTag') + } +} + +class DynamicSampleTagLib { + static returnObjectForTags = ['myTag'] + + Closure myTag = { attrs -> "hello" } + Closure anotherTag = { attrs, body -> } + String nonClosureProperty = "not a tag" + static Closure someStaticClosure = { -> } +} + +@CompileStatic +class CompileStaticSampleTagLib { + Closure staticTag = { Map attrs -> "compiled" } + Closure anotherStaticTag = { Map attrs, body -> } + String nonClosureField = "not a tag" + static Closure someStaticClosure = { -> } +} + +@CompileStatic +class ParentCompileStaticTagLib { + Closure parentTag = { Map attrs -> "parent" } +} + +@CompileStatic +class ChildCompileStaticTagLib extends ParentCompileStaticTagLib { + Closure childTag = { Map attrs -> "child" } +} + +class CustomNamespaceTagLib { + static String namespace = 'custom' + Closure myTag = { attrs -> } +}
