Add opt-in to compile all controllers, services and tag libraries with @GrailsCompileStatic#15759
Conversation
a094915 to
0e5f617
Compare
…h @GrailsCompileStatic Introduce three Grails Gradle extension flags, compileStaticControllers, compileStaticServices and compileStaticTagLibs, that opt every controller, service and tag library into @GrailsCompileStatic at compile time without annotating each class, plus a compileStaticArtefacts shortcut that enables all three at once. All default to false and are independent. The flags are published to the Groovy compiler worker JVM as system properties by GrailsCompileStaticArtefactsProvider; CompileStaticArtefactInjector then stamps @GrailsCompileStatic onto matching artefacts via GrailsASTUtils.addGrailsCompileStaticAnnotation. A class that already declares @CompileStatic, @CompileDynamic, @TypeChecked, @GrailsCompileStatic or @GrailsTypeChecked keeps its own setting, so an explicit per-class choice always wins. To make tag libraries statically compilable, ControllerTagLibTypeCheckingExtension now treats *TagLib classes the same as *Controller classes for dynamic tag dispatch, and defers (rather than throwing) when there is no enclosing method so deprecated closure-field tags fail with a normal type error instead of crashing the compiler. Method-based tags are the supported form. Includes unit tests, Gradle plugin tests and documentation.
0e5f617 to
3f2de3e
Compare
|
The suggestion to use lazy properties and a nested configuration object is a valid architectural improvement for Grails configuration. This approach allows for more flexible, execution-time configuration rather than locking settings at configuration time, and the nested structure improves readability and organization. To implement this, you would define a nested configuration class within your extension and use a lazy-loading mechanism (such as a closure or a provider) to retrieve the values. This ensures that the configuration is only evaluated when needed. |
Address review feedback on the @GrailsCompileStatic opt-in: replace the flat compileStaticControllers/Services/TagLibs/Artefacts booleans on the grails extension with a nested compileStatic { } block backed by lazy Property<Boolean> flags, so the opt-ins are read when the Groovy compile task runs instead of at configuration time.
- Add GrailsCompileStaticOptions (all/controllers/services/tagLibs lazy properties; 'all' is the enable-everything shortcut, formerly compileStaticArtefacts)
- GrailsCompileStaticArtefactsProvider now consumes the lazy options instead of the whole extension
- Update GrailsExtension/GrailsGradlePlugin, tests and docs to the nested DSL
sbglasius
left a comment
There was a problem hiding this comment.
One minor thing to fix, otherwise ok
…eral Replace the hard-coded 'TagLib' suffix literal in ControllerTagLibTypeCheckingExtension with the shared CompileStaticArtefactInjector.TAGLIB_TYPE constant, the single source of truth for that suffix. Addresses review feedback on PR apache#15759.
…CheckingExtension Address review feedback: the extension now silences TagLibraryInvoker-trait tag dispatch for both *Controller and *TagLib classes, so the Controller-prefixed name is misleading. Rename it (and the spec) to reflect the generic scope, and update the fully-qualified references in GrailsCompileStatic and GrailsASTUtils plus the related specs and the what's-new doc.
When grails { compileStatic { tagLibs = true } } is enabled, CompileStaticArtefactInjector now detects tag libraries that still declare tags as closure fields (the deprecated form) and leaves them dynamically compiled, emitting a build warning, instead of failing the build. Detection matches how Grails identifies tags at runtime (public, non-static closure-valued properties/fields).
An explicit @GrailsCompileStatic on the class still forces static compilation (and still fails cleanly on closure-field tag dispatch via the type-checking extension) - the auto-skip only applies to the blanket build opt-in. Adds injector specs for the skip, plus guards proving static closures and static non-closure fields don't trip detection; docs updated.
✅ All tests passed ✅🏷️ Commit: 28f0e1c Learn more about TestLens at testlens.app. |
Summary
Adds an opt-in that compiles every controller, service and tag library with
@GrailsCompileStaticautomatically, without annotating each class. The opt-ins are exposed as a nestedcompileStatic { }block on thegrailsGradle extension:grails { compileStatic { controllers = true // every grails-app/controllers class services = true // every grails-app/services class tagLibs = true // every grails-app/taglib class } }…or the
all = trueshortcut to enable all three at once:grails { compileStatic { all = true } }Every flag is a lazy
Property<Boolean>that defaults tofalseand is read when the Groovy compile task runs (not at configuration time), so the values reflect thegrails { }block regardless of configuration ordering. This automates what the framework's own guidance already recommends doing per-class, while keeping it opt-in and reversible.Design
The configuration rides the same path PR #15118 established for the
grails { importJavaTime / starImports }DSL, but the annotation is applied through an artefact-aware AST injector rather than a globalImportCustomizer, because static compilation is selective (controllers/services/taglibs only), semantic (can fail the build), and must yield to an explicit per-class choice — none of which a blanket customizer can do.GrailsExtensiongains a nestedcompileStatic { }block backed byGrailsCompileStaticOptions(modeled on the existingViewCompileOptions), whoseall/controllers/services/tagLibsopt-ins are lazyProperty<Boolean>flags. AGrailsCompileStaticArtefactsProvider(aCommandLineArgumentProvider, modeled on the existingGrailsAppBaseDirProvider) reads those lazy options and publishes the enabled flags to the Groovy compiler worker JVM as-Dsystem properties. The property names live ingrails.util.BuildSettingsalongsideAPP_BASE_DIR.CompileStaticArtefactInjector(an@AstTransformerGrailsArtefactClassInjectorforController/Service/TagLib) reads the@Artefacttype, checks the corresponding flag, and applies@GrailsCompileStaticvia a newGrailsASTUtils.addGrailsCompileStaticAnnotation(ClassNode)helper. That helper reproduces@GrailsCompileStatic(i.e.@CompileStaticcarrying the Grails type-checking extensions) and registers the static-compile transform viaClassNode.addTransform, the same mechanism the existingaddCompileStaticAnnotationuses so a late-added annotation still fires. For thetagLibsopt-in the injector additionally skips tag libraries that still declare tags as closure fields (see below).Tag library type-checking
@GrailsCompileStaticalready silences tag dispatch in controllers, but the type-checking extension that does it was controller-only and namedControllerTagLibTypeCheckingExtension. This PR generalizes it to*TagLibclasses too (both carry theTagLibraryInvokertrait at runtime) and renames it toTagLibraryInvokerTypeCheckingExtensionto reflect the broader scope. It also makes the extension defer — rather than throw — when there is no enclosing method, so a deprecated closure-field tag that dispatches to other tags fails with a normal type error instead of crashing the compiler with an NPE.Method-based tags (
def myTag(Map attrs, Closure body)) are the supported form. Closure-field tags are already deprecated in Grails 8, so thetagLibsbuild opt-in detects a tag library that still uses them — a public, non-static closure-valued property, the same signal Grails uses to register tags — and skips it automatically, leaving it dynamically compiled and emitting a build warning rather than failing the build. An explicit@GrailsCompileStaticon the class still forces static compilation (and still fails cleanly on closure-field tag dispatch via the extension above); the auto-skip applies only to the blanket build opt-in.Opt-out always wins
A class that declares its own
@CompileDynamic(or@GrailsCompileStatic/@CompileStatic/@GrailsTypeChecked/@TypeChecked) keeps that setting; the opt-in never overrides an explicit choice:When the flags are unset (the default), the injector is fully inert.
Tests
CompileStaticArtefactInjectorSpec— controller/service/taglib compiled statically when its flag is on (a type error fails compilation), dynamically when off,@CompileDynamicopted out even when on, and each type unaffected by the others' flags; a legacy closure-field tag library is skipped (left dynamic) under thetagLibsopt-in, while a static closure and a static non-closure field do not trip that detection.TagLibraryInvokerTypeCheckingExtensionSpec— a@GrailsCompileStatictaglib with method-based tags can invoke tags onthisand via a namespace dispatcher; type errors are still caught; a deprecated closure-field tag fails cleanly (no compiler NPE); all existing controller cases still pass.GrailsASTUtilsSpec—addGrailsCompileStaticAnnotationapplies@CompileStaticwith the full Grails extension list;hasStaticCompilationAnnotationdetects every opt-out annotation; the extension list stays in sync with@GrailsCompileStatic.GrailsCompileStaticOptionsSpec— the nested options' lazy flags default to false, are independent, and support both theproperty = valueDSL convenience andproperty.set(...).GrailsCompileStaticArtefactsProviderSpec+GrailsExtensionSpec— thecompileStatic { }block (and theallshortcut) defaults to off, is configurable, is read lazily after construction, and is published as the expected-Dsystem properties.Full
grails-coresuite and thegrails-gradleplugins/model suites pass; checkstyle + codenarc clean on the changed modules.Docs
Documented under the GrailsCompileStatic guide section and the Grails 8 "what's new" page (public DSL only), including the method-based-tags guidance for tag libraries.