1818import io .micrometer .core .instrument .Metrics ;
1919import io .micrometer .core .instrument .Timer ;
2020import io .quarkus .gizmo .*;
21+ import org .jspecify .annotations .Nullable ;
2122import org .openrewrite .Cursor ;
2223import org .openrewrite .Tree ;
2324import org .openrewrite .TreeVisitor ;
2425
26+ import java .io .BufferedReader ;
27+ import java .io .IOException ;
28+ import java .io .InputStreamReader ;
2529import java .lang .reflect .*;
30+ import java .net .URL ;
31+ import java .nio .charset .StandardCharsets ;
32+ import java .util .Arrays ;
33+ import java .util .Enumeration ;
2634import java .util .IdentityHashMap ;
35+ import java .util .LinkedHashMap ;
2736import java .util .Map ;
37+ import java .util .Optional ;
2838
2939public class TreeVisitorAdapter {
3040 private static final Integer classCreationLock = 1 ;
@@ -106,28 +116,30 @@ public static <T extends Tree, Adapted> Adapted adapt(TreeVisitor<T, ?> delegate
106116 );
107117 setCursor .returnValue (null );
108118
109- // Walk delegate's class hierarchy (down to but excluding TreeVisitor)
110- // and collect every visit-like method declared at each level — typed
111- // overrides AND synthetic bridges. JVM virtual dispatch picks based on
112- // erased descriptor; e.g., J.Annotation.acceptJava emits
113- // invokevirtual JavaVisitor.visitAnnotation(J.Annotation, Object) → J,
114- // and the proxy must define a method with that exact descriptor
115- // (not just the typed RemoveAnnotationVisitor.visitAnnotation(..., ExecutionContext))
116- // for the forwarding to actually be the dispatch target.
117- java .util .LinkedHashMap <MethodKey , Method > methodsByKey = new java .util .LinkedHashMap <>();
118- for (Method m : delegate .getClass ().getDeclaredMethods ()) {
119- if (!isVisitLike (m ) || Modifier .isStatic (m .getModifiers ()) || Modifier .isPrivate (m .getModifiers ())) {
120- continue ;
119+ // Collect the user's visit-like overrides from the leaf up to (but not into) the
120+ // iso-visitor, so overrides declared on a shared superclass are forwarded too.
121+ // Leaf-first, so the most-derived override wins.
122+ Map <MethodKey , Method > methodsByKey = new LinkedHashMap <>();
123+ Class <?> leafClass = delegate .getClass ();
124+ for (Class <?> c = leafClass ;
125+ c != null && c != Object .class && !TreeVisitor .class .equals (c );
126+ c = c .getSuperclass ()) {
127+ if (c == delegateType && c != leafClass ) {
128+ break ;
129+ }
130+ for (Method m : c .getDeclaredMethods ()) {
131+ if (!isVisitLike (m ) || Modifier .isStatic (m .getModifiers ()) || Modifier .isPrivate (m .getModifiers ())) {
132+ continue ;
133+ }
134+ methodsByKey .putIfAbsent (new MethodKey (m .getName (), m .getParameterTypes (), m .getReturnType ()), m );
135+ }
136+ if (c == delegateType ) {
137+ break ;
121138 }
122- MethodKey key = new MethodKey (m .getName (), m .getParameterTypes (), m .getReturnType ());
123- methodsByKey .putIfAbsent (key , m );
124139 }
125140
126- // For visit*(SpecificNode) override-skip: name+arity match against any
127- // user-declared (non-synthetic) mixin method. If the mixin overrides
128- // visitClassDeclaration at one signature, skip generating proxy methods
129- // for ALL same-name same-arity delegate methods (typed + bridges), so
130- // dispatch falls through to the mixin via inheritance.
141+ // If the mixin declares its own visitX override, don't proxy the delegate's
142+ // same-name/arity methods (typed or bridge) — let dispatch fall through to it.
131143 for (Method method : methodsByKey .values ()) {
132144 boolean isPreOrPost = "preVisit" .equals (method .getName ()) || "postVisit" .equals (method .getName ());
133145 boolean mixinOverridesByName = mixinClass != null && !isPreOrPost &&
@@ -206,11 +218,8 @@ public static <T extends Tree, Adapted> Adapted adapt(TreeVisitor<T, ?> delegate
206218 }
207219
208220 private static boolean declaresUserOverrideByNameAndArity (Class <?> mixinClass , Class <?> adaptTo , Method baseMethod ) {
209- // Only consider methods declared on the immediate mixin class —
210- // that is, what the user wrote. Methods inherited from the language
211- // iso-visitor (e.g., JavaIsoVisitor declares visitIdentifier) or from
212- // adaptTo are ambient, not real overrides, and would otherwise cause
213- // us to skip generating forwarding for nearly every visit method.
221+ // Only methods the user actually declared on the mixin count; inherited iso-visitor/adaptTo
222+ // methods are ambient, not real overrides.
214223 for (Method m : mixinClass .getDeclaredMethods ()) {
215224 if (m .isSynthetic () || m .isBridge ()) {
216225 continue ;
@@ -242,12 +251,12 @@ public boolean equals(Object o) {
242251 MethodKey other = (MethodKey ) o ;
243252 return name .equals (other .name ) &&
244253 returnType .equals (other .returnType ) &&
245- java . util . Arrays .equals (paramTypes , other .paramTypes );
254+ Arrays .equals (paramTypes , other .paramTypes );
246255 }
247256
248257 @ Override
249258 public int hashCode () {
250- return name .hashCode () * 31 + returnType .hashCode () + java . util . Arrays .hashCode (paramTypes );
259+ return name .hashCode () * 31 + returnType .hashCode () + Arrays .hashCode (paramTypes );
251260 }
252261 }
253262
@@ -324,13 +333,11 @@ private static void copyMixinInstanceFields(TreeVisitor<?, ?> mixin, Object prox
324333 }
325334 }
326335
327- private static @ org . jspecify . annotations . Nullable TreeVisitor <?, ?> discoverRegisteredMixin (
336+ private static @ Nullable TreeVisitor <?, ?> discoverRegisteredMixin (
328337 TreeVisitorAdapterClassLoader cache , TreeVisitor <?, ?> delegate , Class <?> adaptTo ) {
329- // adapt() is invoked once per node of a foreign-language subtree, so the
330- // classpath scan in discoverRegisteredMixinClass would otherwise run per node.
331- // The cache resolves the mixin class (or "none") keyed by delegate + adaptTo,
332- // then we instantiate a fresh mixin per call (the proxy copies the mixin's fields).
333- java .util .Optional <Class <?>> mixinClass = cache .mixinClass (delegate .getClass (), adaptTo );
338+ // Cache the per-(delegate, adaptTo) classpath scan — adapt() runs once per node — and
339+ // instantiate a fresh mixin each call (the proxy copies its fields).
340+ Optional <Class <?>> mixinClass = cache .mixinClass (delegate .getClass (), adaptTo );
334341 if (!mixinClass .isPresent ()) {
335342 return null ;
336343 }
@@ -342,34 +349,25 @@ private static void copyMixinInstanceFields(TreeVisitor<?, ?> mixin, Object prox
342349 }
343350 }
344351
345- static java .util .Optional <Class <?>> discoverRegisteredMixinClass (Class <?> delegateClass , Class <?> adaptTo ) {
346- // Each language module ships per-base-visitor registry files at
347- // META-INF/rewrite/mixins/<base-visitor-fqn>
348- // listing mixin classes that should compose with that base. Same
349- // namespace already used by rewrite-YAML resources, not the JDK
350- // ServiceLoader namespace — we don't use {@link ServiceLoader} because
351- // its subtype check rejects mixins (a mixin extends the language
352- // module's iso-visitor, e.g. KotlinIsoVisitor, not the base visitor
353- // it composes with, e.g. RemoveAnnotationVisitor). The visitor role
354- // is shared structurally, not by inheritance.
355- //
356- // Anchor resource lookup to adaptTo's classloader so the language
357- // module's own registry files (shipped in the language module's JAR)
358- // are visible.
352+ static Optional <Class <?>> discoverRegisteredMixinClass (Class <?> delegateClass , Class <?> adaptTo ) {
353+ // Mixins are registered in META-INF/rewrite/mixins/<base-visitor-fqn>. Not ServiceLoader:
354+ // its subtype check rejects mixins, which extend the language iso-visitor (e.g.
355+ // KotlinIsoVisitor) rather than the base visitor they compose with. Anchor lookup to
356+ // adaptTo's classloader so the language module's own registry files are visible.
359357 ClassLoader cl = adaptTo .getClassLoader ();
360358 if (cl == null ) {
361359 cl = Thread .currentThread ().getContextClassLoader ();
362360 }
363361 if (cl == null ) {
364- return java . util . Optional .empty ();
362+ return Optional .empty ();
365363 }
366364 String resource = "META-INF/rewrite/mixins/" + delegateClass .getName ();
367365 try {
368- java . util . Enumeration <java . net . URL > urls = cl .getResources (resource );
366+ Enumeration <URL > urls = cl .getResources (resource );
369367 while (urls .hasMoreElements ()) {
370- java . net . URL url = urls .nextElement ();
371- try (java . io . BufferedReader reader = new java . io . BufferedReader (
372- new java . io . InputStreamReader (url .openStream (), java . nio . charset . StandardCharsets .UTF_8 ))) {
368+ URL url = urls .nextElement ();
369+ try (BufferedReader reader = new BufferedReader (
370+ new InputStreamReader (url .openStream (), StandardCharsets .UTF_8 ))) {
373371 String line ;
374372 while ((line = reader .readLine ()) != null ) {
375373 int hash = line .indexOf ('#' );
@@ -389,18 +387,18 @@ static java.util.Optional<Class<?>> discoverRegisteredMixinClass(Class<?> delega
389387 if (!adaptTo .isAssignableFrom (mixinClass )) {
390388 continue ;
391389 }
392- return java . util . Optional .of (mixinClass );
390+ return Optional .of (mixinClass );
393391 } catch (ClassNotFoundException e ) {
394392 throw new IllegalStateException (
395393 "Failed to load registered mixin " + line + " (from " + url + ")." , e );
396394 }
397395 }
398396 }
399397 }
400- } catch (java . io . IOException e ) {
398+ } catch (IOException e ) {
401399 // Couldn't enumerate mixin registry resources; treat as no registration.
402400 }
403- return java . util . Optional .empty ();
401+ return Optional .empty ();
404402 }
405403
406404 @ SuppressWarnings ("rawtypes" )
0 commit comments