paulk-asert commented on PR #2645:
URL: https://github.com/apache/groovy/pull/2645#issuecomment-5086232622

   Thanks @daniellansun and @blackdrag for the thorough reviews — nearly 
everything has been actioned, and the preemption design has been reworked 
substantially in response. Details below; the branch is updated.
   
   ## Preemption redesign: intent + containment + consent
   
   The `classTagPreemptionTargets` name allowlist is gone. Daniel was right 
that a config-level name list was the wrong layer (names aren't unique, and a 
library couldn't express intent alone), and Jochen's mailing-list concern was 
that preemption changes the meaning of existing source at all. The replacement 
splits the decision across three parties, each owning the part that's theirs:
   
   1. **Intent (API author)** — `@ClassTag` gains `preempt() default false`. 
Only an overload explicitly marked `@ClassTag(preempt=true)` can ever displace 
a token-less incumbent; unmarked tags are strictly additive. A library now 
self-declares — no consumer configuration needed. DGM's checked `withDefault` 
overloads declare it; `asChecked` does not (nothing to preempt).
   
   2. **Containment (structural invariant, not configurable)** — a tagged 
overload may only preempt a token-less overload declared by the *same owner*: 
for an extension-method incumbent, tagged overloads of the same extension 
class; for an instance-method incumbent, the receiver's own class hierarchy. 
This closes what I think is the most important hole in author-declared 
preemption: without it, any jar on the compile classpath could ship e.g. 
`collect(Iterable, @ClassTag(preempt=true) Class, Closure)` and silently 
capture every statically-compiled `collect{}` call on the next rebuild. With 
the same-owner rule, a library can upgrade callers of *its own* lenient API but 
structurally cannot capture calls it doesn't own — the residual exposure 
reduces to "a library can change the behaviour of calls to itself", which it 
could already do by editing its method bodies.
   
   3. **Consent (consumer)** — 
`CompilerConfiguration.classTagPreemptionDisabled`, a veto set using the same 
selector forms as `groovy.extension.disable`: a bare name (`withDefault`), an 
exact signature in self-first static form (`withDefault(Map,Class,Closure)`), 
or `*` to disable all preemption. Seeded from 
`-Dgroovy.classtag.preemption.disable`, so no config script is needed for the 
common cases. Default empty = honour declared intent.
   
   For Jochen specifically: `*` (or simply never writing `preempt=true`) is now 
a first-class "no preemption ever" mode, and out of the box the only method 
that preempts anything is `withDefault` — whose lenient variant is the 
GROOVY-11807 bug this exists to route around.
   
   ## Daniel's review items
   
   - **#1 inject → re-select → rollback**: reworked as suggested. Matching is 
now a pure computation (`ClassTagSupport.matchAdditive`/`matchPreemption`); 
tagged argument types are computed by splicing token types into the args array, 
and the AST is rewritten only after the retried selection binds. The rollback 
path and the duplicated receiver loop are gone.
   - **#3 visitor growth + orphaned Javadoc**: all ClassTag logic extracted to 
a new package-private `ClassTagSupport` (~25-line call site remains in the 
visitor, net −230 lines); `findMethodsWithGenerated`'s Javadoc is reunited with 
its method.
   - **#4a arity-only matching**: subsumed by the pure-match refactor.
   - **#4b silent ambiguity**: still fail-soft by design (degrading to the call 
as written must not become an error), but I agree the additive-case "no 
matching method" error deserves a hint that injection was attempted and 
abandoned — follow-up.
   - **#4c receiver binding**: fixed. Additive matching walks receivers in 
delegate/owner priority order and constrains the retry to the matched receiver; 
preemptive matching reifies from the receiver the incumbent bound on, so an 
upgrade can change overload but never binding target.
   - **#4d docs**: annotation Javadoc no longer says "trailing" (tokens fill 
declared positions, anywhere in the list), and preemption + the veto config are 
now documented on the annotation itself.
   - **#4e non-`Class` parameter**: now a compile-time error on source methods, 
parallel to the override-typo check.
   
   ## Jochen's test questions
   
   1. *token-less overload alongside the tagged one* — covered by 
`testUserApiWithoutDeclaredIntentStaysLenient` / 
`testUserApiDeclaresPreemptionIntent` (and now also 
`testUserApiIntentVetoedByConsumer`).
   2. *switched `Class<V>`/`Class<K>` positions* — 
`testTokensReorderedByTypeVariableNotPosition` covers the swap; new 
`testTokensInterleavedWithSuppliedArguments` additionally covers a token 
declared *after* a supplied argument.
   3. *method-declared type parameters* — this was a genuinely good catch: 
chasing it surfaced that a method-declared type variable shadowing a 
class-level one (per Java scoping) could have reified the *wrong* class. Now: 
on instance methods compiled from source, tagging a method-declared variable is 
a compile-time error; a matching-side guard makes precompiled libraries degrade 
to no-injection rather than mis-inject; static self-style methods (the 
extension-authoring pattern) are exempt since there the method-level variable 
*is* the mechanism. Tests: `testMethodTypeParameterIsRejectedAtCompileTime`, 
`testShadowedTypeParameterIsRejectedAtCompileTime`, 
`testStaticSelfStyleMethodTypeParameterAccepted`.
   
   ## Status / open items
   
   `ClassTagStaticTest` 32/32, `CompilerConfigurationTest` 15/15, full 
`groovy.transform.stc.*` (2012 tests) and `classgen.asm.sc.*` (2295 tests) 
sweeps green, `Groovy11807` regression green.
   
   Two known follow-ups: (a) the preemption check now runs without the old O(1) 
name pre-gate — it's bounded (cached DGM lookup filtered by owner / 
receiver-hierarchy scan), but I'd like a compile-time benchmark on a large 
`@CompileStatic` codebase before merge, with an `ExtensionMethodCache`-level 
"names with preempt intent" cache as the optimization if needed; (b) the 
cross-module containment ("an extension module cannot capture DGM's methods") 
is implemented but wants an m12n-style fixture-jar integration test.
   
   Comments on the revised model welcome — particularly whether `preempt=true` 
+ same-owner containment + selector veto addresses the "why is one method 
better" and "wrong layer" concerns, respectively.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to