Skip to content

8.x Register CriteriaTypeCheckingExtension and cover createCriteria().<terminal>{} closures#15720

Open
codeconsole wants to merge 2 commits into
apache:8.0.xfrom
codeconsole:criteria-typecheck-chained-8
Open

8.x Register CriteriaTypeCheckingExtension and cover createCriteria().<terminal>{} closures#15720
codeconsole wants to merge 2 commits into
apache:8.0.xfrom
codeconsole:criteria-typecheck-chained-8

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

What

CriteriaTypeCheckingExtension exists in grails-core but is not registered in @GrailsCompileStatic's extension list, and isCriteriaCall only recognizes the Domain.withCriteria { … } / Domain.createCriteria() forms (closure as the direct argument). The chained Domain.createCriteria().<terminal> { … } form — where the closure is the argument to a terminal called on the builder — is never matched, so a criteria scope is never opened for it.

Why it matters

Most chained terminals (list, listDistinct, get, scroll) still type-check today because BuildableCriteria declares them with a Closure parameter and @DelegatesTo resolves the closure body. But count(Closure) is not declared on BuildableCriteria, so:

@GrailsCompileStatic
class Foo {
    def c() { Person.createCriteria().count { eq 'name', 'Anakin' } }
}

fails static compilation on stock 8.0.x:

[Static type checking] - Cannot find matching method
org.grails.datastore.mapping.query.api.BuildableCriteria#count(groovy.lang.Closure)

forcing a @CompileDynamic escape hatch.

Change

  1. Extend isCriteriaCall to also open a criteria scope when the call is a terminal (list, listDistinct, get, count, scroll) chained directly on Domain.createCriteria(). Registration alone is not enough — the createCriteria() scope is exited (via afterMethodCall) before the terminal call is visited, so the terminal needs to be recognized in its own right.
  2. Add org.grails.compiler.CriteriaTypeCheckingExtension to the @CompileStatic(extensions=[…]) list on @GrailsCompileStatic.

Test

Adds 'Test compiling a class which invokes a chained createCriteria() terminal on a domain class' to GrailsCompileStaticCompilationErrorsSpec, covering all six chained terminals. It fails on stock 8.0.x (on the count terminal) and passes with this change. The existing GRAILS-11255 criteria spec continues to pass.

Scope / limits

The extension makes the direct children of the criteria closure dynamic. Deeply nested builder closures (e.g. projections { property '…' }) and post-processing that indexes the result (withCriteria { … }[0]) still require @CompileDynamic — out of scope here.

…rminal>{} closures

CriteriaTypeCheckingExtension existed but was never added to the
@GrailsCompileStatic extension list, and isCriteriaCall only recognized the
Domain.withCriteria {} / Domain.createCriteria() forms where the closure is
the direct argument. The chained Domain.createCriteria().<terminal> { } form
(closure as the argument to a terminal called on the builder) was not
matched, so its scope was never opened.

Most chained terminals (list/listDistinct/get/scroll) still type-checked
because BuildableCriteria declares them with a Closure parameter and
@DelegatesTo resolves the closure body. count(Closure) is not declared on
BuildableCriteria, so Domain.createCriteria().count {} failed static
compilation with 'Cannot find matching method BuildableCriteria#count(Closure)'.

Extend isCriteriaCall to also open a criteria scope for a terminal
(list/listDistinct/get/count/scroll) chained directly on createCriteria(),
and register the extension in @GrailsCompileStatic so criteria-builder
closures type-check under static compilation. Adds a test covering all six
chained terminals; count is the case that fails without this change.

git log --oneline -1
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.3627%. Comparing base (41f9fb9) to head (b10f36f).
⚠️ Report is 68 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...ails/compiler/CriteriaTypeCheckingExtension.groovy 0.0000% 14 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15720        +/-   ##
==================================================
+ Coverage     48.3483%   48.3627%   +0.0144%     
- Complexity      15104      15233       +129     
==================================================
  Files            1870       1877         +7     
  Lines           85459      85905       +446     
  Branches        14901      14974        +73     
==================================================
+ Hits            41318      41546       +228     
- Misses          37805      37986       +181     
- Partials         6336       6373        +37     
Files with missing lines Coverage Δ
...g/tck/support/StaticCompiledCriteriaQueries.groovy 100.0000% <100.0000%> (ø)
...esting/tck/tests/StaticCompiledCriteriaSpec.groovy 100.0000% <100.0000%> (ø)
...ails/compiler/CriteriaTypeCheckingExtension.groovy 0.0000% <0.0000%> (ø)

... and 25 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codeconsole codeconsole marked this pull request as draft June 5, 2026 18:53
@codeconsole codeconsole marked this pull request as ready for review June 7, 2026 06:38
'org.grails.compiler.WhereQueryTypeCheckingExtension',
'org.grails.compiler.DynamicFinderTypeCheckingExtension',
'org.grails.compiler.DomainMappingTypeCheckingExtension',
'org.grails.compiler.CriteriaTypeCheckingExtension',

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.

To change the default, we need to expand the test coverage so that there are tests in the TCK - this way we know we haven't broken it in hibernate 7.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in b10f36f. The TCK now executes criteria queries that were compiled with @GrailsCompileStatic, covering both dispatch paths the extension produces: statically-resolved Criteria API calls and the dynamic fallback (projections, min/countDistinct/property, maxResults, and the chained count { } terminal, which is not declared on BuildableCriteria). Verified green against the in-memory, Hibernate 5, Hibernate 7, and MongoDB implementations locally.

@bito-code-review

Copy link
Copy Markdown

The pull request already includes a new test case, 'Test compiling a class which invokes a chained createCriteria() terminal on a domain class', within GrailsCompileStaticCompilationErrorsSpec.groovy. This test verifies that chained createCriteria() calls (e.g., .list, .get, .count) are correctly type-checked under @GrailsCompileStatic. If further TCK (Technology Compatibility Kit) coverage is required, you should add similar integration tests to the dedicated TCK test suite modules, as the current tests are located in the grails-test-suite-uber module.

grails-test-suite-uber/src/test/groovy/grails/compiler/GrailsCompileStaticCompilationErrorsSpec.groovy

void 'Test compiling a class which invokes a chained createCriteria() terminal on a domain class'() {
        given:
        def gcl = new GroovyClassLoader()

        when: 'the criteria builder closure is the argument to a terminal chained directly on createCriteria()'
        def c = gcl.parseClass('''
            package grails.compiler

            @GrailsCompileStatic
            class SomeClass {

                def someMethod() {
                    Person.createCriteria().list {
                        cache true
                        eq 'name', 'Anakin'
                    }
                    // ... other terminals
                }
            }
        '''.stripIndent())

        then: 'no errors are thrown'
        c
    }

Registering CriteriaTypeCheckingExtension changes the default compile
semantics of every @GrailsCompileStatic class, so the TCK now runs
criteria queries that were compiled statically against each GORM
implementation. The queries mix calls that resolve statically against
the Criteria API (eq, like, or, order) with calls that only compile
through the extension's dynamic-dispatch fallback (projections, min,
countDistinct, property, maxResults and the count terminal chained on
createCriteria(), which is not declared on BuildableCriteria), proving
that dispatch behaves identically to dynamic Groovy on the in-memory,
Hibernate 5, Hibernate 7 and MongoDB implementations.
@testlens-app

testlens-app Bot commented Jun 11, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

⚠️ TestLens detected flakiness ⚠️

Test Summary

Check Project/Task Test Runs
CI / Functional Tests (Java 21, indy=false) :grails-test-examples-scaffolding:integrationTest UserControllerSpec > User list ❌ ✅

🏷️ Commit: b10f36f
▶️ Tests: 5719 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

@codeconsole codeconsole changed the title Register CriteriaTypeCheckingExtension and cover createCriteria().<terminal>{} closures 8.x Register CriteriaTypeCheckingExtension and cover createCriteria().<terminal>{} closures Jun 12, 2026
@codeconsole codeconsole requested a review from jdaugherty June 12, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants