Skip to content

Support statically compiled specifications - #2397

Draft
leonard84 wants to merge 7 commits into
masterfrom
compile-static-support
Draft

Support statically compiled specifications#2397
leonard84 wants to merge 7 commits into
masterfrom
compile-static-support

Conversation

@leonard84

Copy link
Copy Markdown
Member

Summary

Specs annotated with @CompileStatic or @TypeChecked (on the class or on individual feature methods) now compile and run. Conditions containing method calls, property access, comparisons, and indexing, old(), interactions including interaction {} blocks and mock init closures, and data tables referencing previous columns all work, with condition rendering fully intact.

Code generated for dynamic specs is byte-identical to master except for two snapshot-pinned changes (synthesized method names are no longer record-wrapped), so existing dispatch semantics are preserved, including the behaviors pinned by #396 and #1076.

Changes, one commit each

  • ValueRecorder.record()/realizeNas() are now generic (<T> T, same erasure), so recorded sub-expressions keep their static type.
  • Method names synthesized by earlier rewrite steps (oldImpl, matchCollections*) are no longer wrapped in record(), which turned the call into a dynamic method name call and broke old() under static compilation. Their value slot is still allocated, keeping rendering intact.
  • Previous-column extractors in generated data providers use the declared feature parameter type with a coercion cast, mirroring the data processor. Untyped tables generate unchanged code.
  • Generated code no longer shares the VariableExpression.THIS_EXPRESSION singleton between call sites. The static type checker stores inferred types as per-node metadata, so the shared node accumulated conflicting types and produced contradictory checkcasts, causing a ClassCastException in interaction {} blocks and mock init closures.
  • In statically type checked code (detected via the annotations, with @CompileDynamic/TypeCheckingMode.SKIP opting out), constant method names in conditions stay in place and the rewritten call keeps its original implicit-this flag. Dynamic code is rewritten exactly as before.
  • New docs section "Statically Compiled Specifications" in the primer, plus a release notes entry.
  • Test coverage: CompileStaticSupport (compile, run, and exact failure rendering per fix) and CompileStaticConditionRendering (rendering cases mirrored from ConditionRendering, verifying identical output).

Known limitations (documented)

  • Global Groovy mocks cannot intercept calls from statically compiled code (meta-object protocol is bypassed), related to GroovyMock object on a final class doesn't work properly inside statically compiled service #765.
  • Static compilation applied via a compiler configuration script instead of annotations is not detected.
  • One deliberate rendering difference: the target class of a static method call (e.g. the class java.lang.Math line) is not rendered, because statically compiled code never evaluates the receiver of a static method call, so there is no value to record.

Validation

Full :spock-specs:test on all Groovy variants (2.5, 3.0, 4.0, 5.0) per commit, plus an all-modules test sweep (4028 tests) and the asciidoctor build.

🤖 Prepared with Claude Code

record() and realizeNas() wrap sub-expressions of rewritten conditions
but returned Object, so member access, comparisons, and indexing on a
recorded value failed static type checking. Preserving the argument type
via a type parameter (same erasure, binary compatible) lets such
conditions in @CompileStatic specs compile.
Earlier rewrite steps synthesize method calls inside conditions, e.g.
old() becomes oldImpl() and the collection conditions =~ and ==~ become
matchCollections*() calls. Wrapping such a synthesized name in record()
turns the call into a dynamic method name call, which fails static
compilation with "Target method for method call expression hasn't been
set". Names without a plausible source position now only allocate their
value slot via recordNa(), keeping slot numbering and rendering intact.
This makes old() usable in @CompileStatic specs.
Data table cells referencing previous columns are wrapped in a closure
that extracts the referenced row values into untyped locals, so such
cells failed static type checking even when the feature method declared
typed parameters. The extractor locals now use the declared parameter
type, coercing the row value like the data processor already does.
Without a declared type the generated code is unchanged.
Generated calls like getSpecificationContext() shared the
VariableExpression.THIS_EXPRESSION singleton between all use sites. The
static type checker stores inferred types as per-node metadata, so a
node used both inside and outside of closures accumulated conflicting
types, producing contradictory checkcasts and a ClassCastException at
runtime. This broke interaction blocks and mock init closures in
@CompileStatic specs. Each generated call site now gets its own node.
Conditions record the method name of each call for value tracking,
which turns the call into a dynamic method name call. Static type
checking rejects those, so any condition containing a method call
failed to compile in @CompileStatic or @TypeChecked specs.

For methods subject to static type checking, detected via the
CompileStatic and TypeChecked annotations on the method or its class,
constant method names now stay in place and only their value slot is
allocated, and the rewritten call keeps the original implicit-this
flag so qualified calls are not dispatched to local closure variables
of the same name.

Dynamic code is rewritten exactly as before, preserving its dispatch
semantics, e.g. calling closures in data variables with method syntax
and stubbing getProperty() on Java mocks.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6774167-9fac-40a4-bdc3-73cb2abfdb11

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Adds a primer section describing how to use @CompileStatic and
@TypeChecked with specs, the typing rules that apply, and the
remaining limitations, plus a release notes entry.
Mirrors representative cases from ConditionRendering with typed locals
in a @CompileStatic spec, verifying that failed conditions render the
same as in dynamic code. One deliberate difference is pinned and
documented: the target class of a static method call is not rendered,
because statically compiled code never evaluates the receiver of a
static method call, so its value is never recorded.

Also imports CompileStatic in CompileStaticSupport instead of using
the fully qualified annotation in the embedded sources.
@leonard84
leonard84 force-pushed the compile-static-support branch from 1c633eb to e1bb1c3 Compare July 20, 2026 20:16
@testlens-app

testlens-app Bot commented Jul 20, 2026

Copy link
Copy Markdown

🚨 TestLens detected 1 failed test 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

Verify Branches and PRs / Build and Verify (2.5, 8, windows-latest) > :spock-specs:test

Test Runs Flakiness
AsyncConditionsSpec > multiple passing evals 1% 🟡

🏷️ Commit: e1bb1c3
▶️ Tests: 76351 executed
⚪️ Checks: 61/61 completed

Test Failures

AsyncConditionsSpec > multiple passing evals (:spock-specs:test in Verify Branches and PRs / Build and Verify (2.5, 8, windows-latest))
Async conditions timed out after 1.00 seconds; 2 out of 3 evaluate blocks did not complete in time
	at spock.util.concurrent.AsyncConditions.await(AsyncConditions.java:144)
	at spock.util.concurrent.AsyncConditions.await(AsyncConditions.java:122)
	at spock.util.concurrent.AsyncConditionsSpec.multiple passing evals(AsyncConditionsSpec.groovy:112)

Muted Tests

Select tests to mute in this pull request:

  • AsyncConditionsSpec > multiple passing evals

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.47619% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.31%. Comparing base (d7afcdc) to head (e1bb1c3).

Files with missing lines Patch % Lines
...main/java/org/spockframework/compiler/AstUtil.java 86.66% 0 Missing and 2 partials ⚠️
...org/spockframework/compiler/ConditionRewriter.java 90.90% 0 Missing and 1 partial ⚠️
...rg/spockframework/compiler/WhereBlockRewriter.java 92.30% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #2397      +/-   ##
============================================
+ Coverage     82.29%   82.31%   +0.01%     
- Complexity     4882     4902      +20     
============================================
  Files           474      474              
  Lines         15258    15292      +34     
  Branches       1961     1973      +12     
============================================
+ Hits          12557    12588      +31     
  Misses         2002     2002              
- Partials        699      702       +3     
Files with missing lines Coverage Δ
...java/org/spockframework/compiler/SpecRewriter.java 94.19% <100.00%> (+0.01%) ⬆️
...org/spockframework/compiler/SpecialMethodCall.java 93.80% <100.00%> (ø)
...java/org/spockframework/runtime/ValueRecorder.java 73.07% <ø> (ø)
...org/spockframework/compiler/ConditionRewriter.java 89.87% <90.90%> (-0.03%) ⬇️
...rg/spockframework/compiler/WhereBlockRewriter.java 95.15% <92.30%> (-0.10%) ⬇️
...main/java/org/spockframework/compiler/AstUtil.java 79.04% <86.66%> (+0.75%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant