Skip to content

Add @Interactions for declaring interactions on passed-in mocks#2376

Draft
leonard84 wants to merge 1 commit into
external-mock-interactions-1-mock-interaction-supportfrom
external-mock-interactions-2-interactions-annotation
Draft

Add @Interactions for declaring interactions on passed-in mocks#2376
leonard84 wants to merge 1 commit into
external-mock-interactions-1-mock-interaction-supportfrom
external-mock-interactions-2-interactions-annotation

Conversation

@leonard84

Copy link
Copy Markdown
Member

Build on the external-interaction SPI with a second mechanism. Annotate a
helper method with spock.lang.Interactions to declare interactions on
mocks received as parameters; the method needs no spec parameter and is written
naturally. Mock creation is not allowed (pass the mocks in, or use
MockInteractionSupport).

The compiler synthesizes a companion overload that takes the owning
Specification as an explicit leading parameter and rewrites the original
method body into a diagnostic-throwing stub. When the helper is called from a
spec on a strongly-typed receiver, DeepBlockRewriter rewrites the call to pass
the spec to the companion; a def-typed receiver falls through to the throwing
original with a clear message. Static helpers are supported when called
class-qualified (the spec arrives as the injected parameter).

Helpers compose: calls to other @interactions helpers inside a companion or a
MockInteractionSupport method are rewritten to pass the located spec along
(the $spec parameter has no user-visible name, so this is the only way to
chain helpers).

Invalid declarations are rejected with compile errors: a leading Specification
parameter (the compiler adds that parameter to the companion), an abstract
method, a trait method, and a method name where only some overloads carry the
annotation (calls are matched by name, so an unannotated overload would
mis-dispatch after spec injection). The null-spec guard message is
mechanism-specific, so a null $spec no longer reports MockInteractionSupport
advice.

Adds Interactions, CompanionMethodBuilder, InteractionsCallDetector,
the @Interactions call-site handling in DeepBlockRewriter, and Pass 1 of
SpockTransform (companion synthesis across the unit). Tests cover companion
synthesis, typed/def receivers, the explicit-spec overload, static + static-import
behaviour, cross-compilation-unit detection, @CompileStatic resolution,
composition, the validation errors, and AST snapshots of the generated code.

Build on the external-interaction SPI with a second mechanism. Annotate a
helper method with `spock.lang.Interactions` to declare interactions on
mocks received as parameters; the method needs no spec parameter and is written
naturally. Mock creation is not allowed (pass the mocks in, or use
`MockInteractionSupport`).

The compiler synthesizes a companion overload that takes the owning
`Specification` as an explicit leading parameter and rewrites the original
method body into a diagnostic-throwing stub. When the helper is called from a
spec on a strongly-typed receiver, `DeepBlockRewriter` rewrites the call to pass
the spec to the companion; a `def`-typed receiver falls through to the throwing
original with a clear message. Static helpers are supported when called
class-qualified (the spec arrives as the injected parameter).

Helpers compose: calls to other @interactions helpers inside a companion or a
`MockInteractionSupport` method are rewritten to pass the located spec along
(the $spec parameter has no user-visible name, so this is the only way to
chain helpers).

Invalid declarations are rejected with compile errors: a leading Specification
parameter (the compiler adds that parameter to the companion), an abstract
method, a trait method, and a method name where only some overloads carry the
annotation (calls are matched by name, so an unannotated overload would
mis-dispatch after spec injection). The null-spec guard message is
mechanism-specific, so a null $spec no longer reports MockInteractionSupport
advice.

Adds `Interactions`, `CompanionMethodBuilder`, `InteractionsCallDetector`,
the `@Interactions` call-site handling in `DeepBlockRewriter`, and Pass 1 of
`SpockTransform` (companion synthesis across the unit). Tests cover companion
synthesis, typed/def receivers, the explicit-spec overload, static + static-import
behaviour, cross-compilation-unit detection, @CompileStatic resolution,
composition, the validation errors, and AST snapshots of the generated code.
@coderabbitai

coderabbitai Bot commented Jun 13, 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: fdbe84ea-74bc-458e-b082-3021a0c8ecbb

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 and usage tips.

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.10345% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.53%. Comparing base (4ebfc49) to head (63a28b9).

Files with missing lines Patch % Lines
...va/org/spockframework/compiler/SpockTransform.java 89.85% 2 Missing and 5 partials ⚠️
...ckframework/compiler/InteractionsCallDetector.java 88.00% 0 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                                     Coverage Diff                                     @@
##             external-mock-interactions-1-mock-interaction-support    #2376      +/-   ##
===========================================================================================
+ Coverage                                                    82.42%   82.53%   +0.11%     
- Complexity                                                    4910     4936      +26     
===========================================================================================
  Files                                                          478      481       +3     
  Lines                                                        15314    15454     +140     
  Branches                                                      1954     1990      +36     
===========================================================================================
+ Hits                                                         12622    12755     +133     
  Misses                                                        1993     1993              
- Partials                                                       699      706       +7     
Files with missing lines Coverage Δ
...java/org/spockframework/compiler/AstNodeCache.java 100.00% <100.00%> (ø)
...pockframework/compiler/CompanionMethodBuilder.java 100.00% <100.00%> (ø)
...org/spockframework/compiler/DeepBlockRewriter.java 98.32% <100.00%> (+0.09%) ⬆️
...ramework/compiler/ExternalInteractionRewriter.java 98.07% <100.00%> (+7.83%) ⬆️
...ckframework/compiler/ExternalRewriteResources.java 78.94% <100.00%> (+2.47%) ⬆️
...org/spockframework/compiler/IRewriteResources.java 100.00% <100.00%> (ø)
...g/spockframework/compiler/InteractionRewriter.java 91.79% <100.00%> (ø)
...ckframework/compiler/InteractionsCallDetector.java 88.00% <88.00%> (ø)
...va/org/spockframework/compiler/SpockTransform.java 90.35% <89.85%> (-0.77%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@testlens-app

testlens-app Bot commented Jun 13, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 63a28b9
▶️ Tests: 105001 executed
⚪️ Checks: 33/33 completed


Learn more about TestLens at testlens.app.

@leonard84 leonard84 self-assigned this Jun 13, 2026
Comment on lines +104 to +105
List<MethodNode> methods = new ArrayList<>(clazz.getMethods());
if (methods.stream().noneMatch(this::hasInteractions)) return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not do the bail out on the Array first, before allocating the List, because it would be the common case:

Suggested change
List<MethodNode> methods = new ArrayList<>(clazz.getMethods());
if (methods.stream().noneMatch(this::hasInteractions)) return;
Method[] methodArray = clazz.getMethods();
if Arrays.stream(methodArray).noneMatch(this::hasInteractions)) return;
List<MethodNode> methods = new ArrayList<>(methodArray);

for (int i = 0; i < a.length; i++)
if (!a[i].getType().equals(b[i].getType())) return false;
return true;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Move to AstUtils.

// an explicit-spec overload call inside a spec passes `this`/`super`, or a
// variable whose static type is a Specification
return AstUtil.isThisOrSuperExpression(first)
|| first.getType().isDerivedFrom(nodeCache.Specification);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You use the same line first.getType().isDerivedFrom(nodeCache.Specification).
Maybe add a help to AstUtil.isSpecification().

private final AstNodeCache nodeCache;

public CompanionMethodBuilder(AstNodeCache nodeCache) {
this.nodeCache = nodeCache;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Null-check

Comment on lines +48 to +51
Parameter[] originalParams = original.getParameters();
Parameter[] params = new Parameter[originalParams.length + 1];
params[0] = new Parameter(nodeCache.Specification, SPEC_PARAM_NAME);
System.arraycopy(originalParams, 0, params, 1, originalParams.length);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You are doing similar thing in another place.
How about a helper method?


[source,groovy,indent=0]
----
include::{sourcedir}/interaction/ExternalMockInteractionsDocSpec.groovy[tags=interactions-usage]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now here no ..., I do not get it.
When do you add dots and when not?
What is the right thing?

Comment on lines +1315 to +1318
A few constraints apply, each enforced with a compile error:
the method must have a body (it cannot be abstract or declared in a trait),
it must not declare a leading `Specification` parameter (the compiler adds that parameter to the synthesized overload),
and if one overload of a method name is annotated, all overloads of that name must be annotated, because calls to `@Interactions` helpers are matched by method name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe a list * instead of the ,

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.

2 participants