Skip to content

Add opt-in to compile all controllers, services and tag libraries with @GrailsCompileStatic#15759

Merged
codeconsole merged 5 commits into
apache:8.0.xfrom
codeconsole:feature/compile-static-artefacts
Jul 1, 2026
Merged

Add opt-in to compile all controllers, services and tag libraries with @GrailsCompileStatic#15759
codeconsole merged 5 commits into
apache:8.0.xfrom
codeconsole:feature/compile-static-artefacts

Conversation

@codeconsole

@codeconsole codeconsole commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in that compiles every controller, service and tag library with @GrailsCompileStatic automatically, without annotating each class. The opt-ins are exposed as a nested compileStatic { } block on the grails Gradle 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 = true shortcut to enable all three at once:

grails {
    compileStatic {
        all = true
    }
}

Every flag is a lazy Property<Boolean> that defaults to false and is read when the Groovy compile task runs (not at configuration time), so the values reflect the grails { } 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 global ImportCustomizer, 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.

  • Config + transport (grails-gradle): GrailsExtension gains a nested compileStatic { } block backed by GrailsCompileStaticOptions (modeled on the existing ViewCompileOptions), whose all / controllers / services / tagLibs opt-ins are lazy Property<Boolean> flags. A GrailsCompileStaticArtefactsProvider (a CommandLineArgumentProvider, modeled on the existing GrailsAppBaseDirProvider) reads those lazy options and publishes the enabled flags to the Groovy compiler worker JVM as -D system properties. The property names live in grails.util.BuildSettings alongside APP_BASE_DIR.
  • Application (grails-core): CompileStaticArtefactInjector (an @AstTransformer GrailsArtefactClassInjector for Controller/Service/TagLib) reads the @Artefact type, checks the corresponding flag, and applies @GrailsCompileStatic via a new GrailsASTUtils.addGrailsCompileStaticAnnotation(ClassNode) helper. That helper reproduces @GrailsCompileStatic (i.e. @CompileStatic carrying the Grails type-checking extensions) and registers the static-compile transform via ClassNode.addTransform, the same mechanism the existing addCompileStaticAnnotation uses so a late-added annotation still fires. For the tagLibs opt-in the injector additionally skips tag libraries that still declare tags as closure fields (see below).

Tag library type-checking

@GrailsCompileStatic already silences tag dispatch in controllers, but the type-checking extension that does it was controller-only and named ControllerTagLibTypeCheckingExtension. This PR generalizes it to *TagLib classes too (both carry the TagLibraryInvoker trait at runtime) and renames it to TagLibraryInvokerTypeCheckingExtension to 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 the tagLibs build 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 @GrailsCompileStatic on 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:

@CompileDynamic
class LegacyController {
    // compiled dynamically even when compileStatic { controllers = true } is enabled
}

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, @CompileDynamic opted out even when on, and each type unaffected by the others' flags; a legacy closure-field tag library is skipped (left dynamic) under the tagLibs opt-in, while a static closure and a static non-closure field do not trip that detection.
  • TagLibraryInvokerTypeCheckingExtensionSpec — a @GrailsCompileStatic taglib with method-based tags can invoke tags on this and 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.
  • GrailsASTUtilsSpecaddGrailsCompileStaticAnnotation applies @CompileStatic with the full Grails extension list; hasStaticCompilationAnnotation detects 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 the property = value DSL convenience and property.set(...).
  • GrailsCompileStaticArtefactsProviderSpec + GrailsExtensionSpec — the compileStatic { } block (and the all shortcut) defaults to off, is configurable, is read lazily after construction, and is published as the expected -D system properties.

Full grails-core suite and the grails-gradle plugins/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.

@codeconsole codeconsole force-pushed the feature/compile-static-artefacts branch from a094915 to 0e5f617 Compare June 24, 2026 05:42
@codeconsole codeconsole changed the title Add opt-in to compile all controllers and services with @GrailsCompileStatic Add opt-in to compile all controllers, services and tag libraries with @GrailsCompileStatic Jun 24, 2026
…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.
@codeconsole codeconsole force-pushed the feature/compile-static-artefacts branch from 0e5f617 to 3f2de3e Compare June 24, 2026 05:46
@bito-code-review

Copy link
Copy Markdown

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 sbglasius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@testlens-app

testlens-app Bot commented Jun 30, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 28f0e1c
▶️ Tests: 26203 executed
⚪️ Checks: 44/44 completed


Learn more about TestLens at testlens.app.

@codeconsole codeconsole merged commit 7150c7b into apache:8.0.x Jul 1, 2026
73 of 76 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants