Add @Interactions for declaring interactions on passed-in mocks#2376
Conversation
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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
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.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 63a28b9 Learn more about TestLens at testlens.app. |
| List<MethodNode> methods = new ArrayList<>(clazz.getMethods()); | ||
| if (methods.stream().noneMatch(this::hasInteractions)) return; |
There was a problem hiding this comment.
Why not do the bail out on the Array first, before allocating the List, because it would be the common case:
| 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; | ||
| } |
| // 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); |
There was a problem hiding this comment.
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; |
| 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); |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Now here no ..., I do not get it.
When do you add dots and when not?
What is the right thing?
| 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. |
There was a problem hiding this comment.
Maybe a list * instead of the ,

Build on the external-interaction SPI with a second mechanism. Annotate a
helper method with
spock.lang.Interactionsto declare interactions onmocks 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
Specificationas an explicit leading parameter and rewrites the originalmethod body into a diagnostic-throwing stub. When the helper is called from a
spec on a strongly-typed receiver,
DeepBlockRewriterrewrites the call to passthe spec to the companion; a
def-typed receiver falls through to the throwingoriginal 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
MockInteractionSupportmethod 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
@Interactionscall-site handling inDeepBlockRewriter, and Pass 1 ofSpockTransform(companion synthesis across the unit). Tests cover companionsynthesis, 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.