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 -> }
+}

Reply via email to