Add method-based TagLib syntax with legacy compatibility, benchmarks, and docs#15465
Conversation
…pdate - implement method-defined tag handler support and invocation context - preserve closure-style behavior across property/direct and namespaced paths - convert built-in web/GSP taglibs to method syntax - add compile-time warning for closure-defined tag fields - add coverage and benchmark for method vs closure invocation - update guides and demo taglib samples to method syntax Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
- treat only Map parameter named attrs as full tag attributes map - allow other Map-typed parameters to bind from attribute key by parameter name - add regression tests for map-valued attribute binding and reserved attrs behavior Co-Authored-By: Oz <oz-agent@warp.dev>
- use private implementation helpers to avoid recursive dispatch in typed overloads - keep Map-based handlers for validation-safe fallback behavior - add regression test ensuring private/protected methods are not exposed as tag methods - document overload pattern for typed signatures with existing validation paths Co-Authored-By: Oz <oz-agent@warp.dev>
…gnment - keep typed overloads delegating to private implementation helpers - remove unnecessary attrs.name writes since typed args are sourced from attrs - preserve behavior validated by focused FormTagLib and method-tag test suites Co-Authored-By: Oz <oz-agent@warp.dev>
- add thread-safe ClassValue cache for invokable public tag methods by name - remove per-invocation getMethods scans in hasInvokableTagMethod/invokeTagMethod - optimize TagLibrary.propertyMissing by caching method fallback closures in non-dev mode - use resolved namespace for default-namespace fallback closures Co-Authored-By: Oz <oz-agent@warp.dev>
- restore attrs-reserved binding for paginate - route namespaced method tag calls via tag output capture - add fieldValue(Map) compatibility overload - harden form fields rendering/raw handling with method dispatch Co-Authored-By: Oz <oz-agent@warp.dev>
Doc ExamplesFiles: class SimpleTagLib {
static namespace = "my"
def example() { // ← new method added
def example = { attrs -> // ← old closure NOT removed
//...
}Same issue in
|
…egistered as tag methods Make helper methods private across all affected TagLib files to prevent TagMethodInvoker.isTagMethodCandidate() from matching them as tag methods. Remove convenience overloads (e.g. textField(String,Map)) entirely where Groovy 4's multimethod restriction forbids mixing private/public methods of the same name. Changes: - ApplicationTagLib: make renderResourceLink, doCreateLink private - FormatTagLib: make messageHelper private - UrlMappingTagLib: make appendClass private - ValidationTagLib: remove fieldValue(Map) overload, make formatValue private, remove formatValue from returnObjectForTags - FormTagLib: remove 5 typed convenience overloads, make renderNoSelectionOption private - FormFieldsTagLib: make 9 protected helper methods private - TagMethodInvoker: sort methods by descending param count to prefer (Map,Closure) over (Map) signatures - Checkstyle/CodeNarc fixes: alphabetical imports, blank lines before constructors, single-quoted strings
# Conflicts: # grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy # grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy # grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
|
@davydotcom I made some attempts at fixing the test pollution, but I think there is still more work required here. |
|
From my AI Results: |
|
Concerning the continued failures: Original CI failures (5 tests at maxTestParallel=3, 1 test at maxTestParallel=4) are reduced to occasional flakes on ReverseUrlMappingToDefaultActionTests.testLinkTagRendering. That remaining flake is a |
|
Update on this, from the AI: So the tag library state isn't being cleaned up correctly. |
# Conflicts: # DEVELOPMENT.md # grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy # grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
typed method tag arguments bind reliably in user applications. Narrow method tag discovery to avoid exposing public helper methods as tags by default. Conventional attrs/body signatures remain supported, while zero-arg and typed-parameter method tags require @tag. Preserve @NotATag opt-out behavior and add debug logging when tag dispatchers override existing methods. Expand unit, Gradle plugin, docs, and grails-test-examples coverage for the reviewed reproducer cases.
|
@jamesfredley I think i've addressed all of the issues you raised. |
|
This question isn’t related to the pull request. I can only help with questions about the PR’s code or comments. |
91be40c to
ddeb082
Compare
jamesfredley
left a comment
There was a problem hiding this comment.
I will push a small documentation commit in a few minutes. Looks good to go!!!
Adds three targeted upgrade-guide additions covering edge cases of the method-based TagLib handler work in this PR that are not currently surfaced to users: upgrading70x.adoc section 16.1 - Add diagnostic guidance for the overrideMethods=true behavior change. The TagLibraryMetaUtils dispatcher logs each override at DEBUG, but shadowed user methods are otherwise silent at runtime. Document how to enable the logger in logback.xml so users can audit which of their TagLib methods are being replaced by tag dispatchers. upgrading70x.adoc section 16.2 (new) - Document the now-symmetric parameter-name rule for Map and Closure parameters in method-based tag handlers. A Closure parameter named anything other than `body` is treated as a named attribute rather than the tag body, matching the existing behavior for Map parameters named other than `attrs`. Includes a worked rename example and a pointer to the preserveParameterNames Gradle extension for users that opt out of parameter-name preservation. upgrading80x.adoc section 21 - Clarify that the preserveParameterNames Gradle extension only governs Groovy compilation. Java sources receive -parameters transitively from the Spring Boot Gradle plugin (default since Spring Boot 3.2). - Add an explicit recipe for users that compile TagLib classes outside the Grails Gradle plugin (standalone Groovy modules, plain Spring Boot apps without the Boot plugin, or builds that disable the Boot plugin's compiler configuration), and call out the silent MissingMethodException failure mode that occurs without the flags. No code changes; documentation only. Assisted-by: claude-code:claude-opus-4-7
The method-based TagLib handler feature ships in Grails 8.0 and is the subject of this PR. Documentation for it was incorrectly placed in the 7.x upgrade guide (likely a carry-over from when this work originally targeted the 7.x line). Move the entire feature umbrella to the 8.x upgrade guide and consolidate the related sections. upgrading70x.adoc - Remove section 16 (Method-based TagLib Handlers) and its subsections 16.1 (Behavior change: tag method registration) and 16.2 (Behavior change: parameter-name binding for Map and Closure parameters). None of this content describes a change that landed in Grails 7. upgrading80x.adoc - Replace section 21 (formerly "Grails Gradle Extension Preserves Parameter Names By Default") with a consolidated section 21 (Method-based TagLib Handlers) covering the feature end to end: - 21. intro: feature overview, @tag annotation, @NotATag opt-out, closure-tag deprecation warning system property. - 21.1. Behavior change: tag method registration (overrideMethods default flipped to true, plus the DEBUG logging guidance for auditing shadowed user methods). - 21.2. Behavior change: parameter-name binding for Map and Closure parameters (symmetric reserved-name rule for `attrs` and `body`). - 21.3. Grails Gradle Extension Preserves Parameter Names By Default (the previous section 21 content, plus the Spring Boot -parameters note and the JavaCompile/GroovyCompile recipe for builds outside the Grails Gradle plugin). No code changes; documentation only. Assisted-by: claude-code:claude-opus-4-7
|
@codeconsole can you please confirm you are good to merge? James & I believe this is ready |
|
@codeconsole do you have time to review again? @jamesfredley do you agree his feedback is implemented? should we merge or wait for his second review? |
|
@jdaugherty I reviewed all feedback from everyone and believe it has been addressed. My two concerns on docs were handled in the two commits. I will merge 8.0.x in now so it actually runs the full CI. |
✅ All tests passed ✅🏷️ Commit: ac449c8 Learn more about TestLens at testlens.app. |
|
Asking @codeconsole again to confirm he's ok with these changes. I believe they've been addressed, and @jamesfredley I think agrees with me. My assumption is we'll discuss in the weekly if @codeconsole doesn't respond. |

Summary
This PR introduces method-based TagLib handlers as the recommended syntax, while preserving full backward compatibility with closure-based handlers and legacy invocation paths.
Rebased onto
8.0.xfrom the original PR #15459.What's included
attrsandbody) for method handlersdef greeting(String name)binds fromname="...")Performance
The method-vs-closure benchmark added in this change set shows an approximately 7–10% improvement for method-based invocation in the covered scenarios.
TagLib syntax examples
Recommended (method-based)
Usage:
Legacy-compatible (closure field)
Validation performed
FormTagLib2TestsFormTagLib3TestsSelectTagTestsNamespacedTagLibMethodTestsTagLibMethodMissingSpecMethodDefinedTagLibSpec:grails-gsp:test:grails-test-examples-app1:integrationTest --tests functionaltests.MiscFunctionalSpecCo-Authored-By: Oz oz-agent@warp.dev