Repository navigation
fix(jvm): a dotted supertype names its nested type, and a Kotlin object expression is an anonymous class - #3051
Merged
Conversation
…ct expression is an anonymous class
A supertype written through its enclosing type was lost in three ways.
Java and C# kept the dotted path (class Named extends Outer.Inner,
class N : Outer.IListener), but the qualified-name match compared it
verbatim with p::Outer::Inner, so it stayed unresolved; a Java anonymous
class (new Api.Listener() {...}) kept only the last segment, which no
nested type is found by; and Kotlin (both engines) took the FIRST
segment, so class K : Outer.Inner() extended Outer. A Kotlin object
expression, object : Listener {...}, made no class at all: its overrides
became local functions of the code around it, which a call through the
interface never reached, and bare calls elsewhere could be guessed onto
them.
A dotted Java, Kotlin or C# supertype now resolves to the type whose
qualified name ends with the path, when the file can name the path's
first type (its own file, package or namespace, an import; C#'s usings);
of several, the one closest in the tree, or none on a tie. The Java
anonymous class keeps the written path in its extends reference, and
Kotlin joins every segment of a supertype (type arguments left out), in
the wasm extractor and the kernel alike. A Kotlin object expression is
indexed as Java's anonymous class is: a class <Listener$anon@line> that
extends each supertype as written, which the code around it
instantiates (the class it builds, else its first supertype, also as
written and only ever a type that can be a supertype), with its body's
declarations as members; its header (constructor arguments, a
by-delegate) stays the enclosing code's. An anonymous class's members,
Java's and Kotlin's, are named only inside the declaration holding the
class: a bare block() elsewhere is never an object's override val block.
The walk up a Java or Kotlin class's supertypes for an inherited method
asks each supertype by the chain of types that names it
(AssistantMessage::Builder) before its simple name, so a builder's
build() is its own AbstractBuilder's and not an MCP annotation's;
the name still answers when an extends edge reached a namesake
(Minestom's Player extends LivingEntity).
The code in an object expression still sees the scope it is written in,
as when its overrides were local functions: a property of the class
around the object types a receiver there (okhttp's server.url() in an
object : Callback is MockWebServer's), an override is not what a call
of its own name in it means (original!!.contentLength(),
this@Outer.onScroll(), an extension receiver's onDataChanged()), and an
object's members out of reach leave a by-name guess as it was
(okhttp's it.isReady() is still the SerialTask interface's).
Validation:
- jvm-dotted-supertypes.test.ts: 13 tests, 11 fail on main 60b0d7a
(the other two guard a dotted supertype the file cannot name and the
calls inside an anonymous class), all pass here.
- Kernel parity, 0 files with diffs: torture.kt 1/1, Torture.java 1/1,
okhttp 588/619 Kotlin and 71/71 Java, mihon 891/940, nowinandroid
340/350, SystemUI 188/198 Kotlin and 1141/1141 Java, jextract 218/218,
Minestom 1859/1894 (deferrals are main's).
- A/B against main 60b0d7a, sites added/removed/retargeted: okhttp
+640/-53/149, SystemUI +435/-53/116, Minestom +165/-3/6, kestra
+108/-0/6, spring-ai +46/-0/38, playframework +27/-3/13, mihon
+22/-5/6, jextract +22/-0/1, spring-modulith +9/-0/10, nowinandroid
0/0/0; serilog, click, gin identical.
- Cost (median of 3 interleaved): okhttp CPU 27.8 -> 29.0 s, wall
8.6 -> 8.8 s (its 360 object classes and 1,491 more edges); mihon
23.6 -> 23.8 s, wall 7.8 -> 7.7 s.
Reimplements #1927 by @mixxer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
inth3shadows
added a commit
to inth3shadows/codegraph
that referenced
this pull request
Oct 30, 2026
Brings 9f379af..bc9291a (colbymchenry#3030, colbymchenry#3031, colbymchenry#3043, colbymchenry#3045, colbymchenry#3046, colbymchenry#3049, colbymchenry#3044, colbymchenry#3047, colbymchenry#3051, colbymchenry#3050, colbymchenry#3052, colbymchenry#3053, colbymchenry#3025, colbymchenry#3055). Clean auto-merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A supertype written through its enclosing type now links to that nested type: Java's
class Named extends Outer.Inner,implements Api.Listenerandnew Api.Listener() { … }, Kotlin'sclass K : Outer.Inner()and C#'sclass N : Outer.IListener, when the file can name the outer type. A Kotlin object expression,object : Listener { … }, is now indexed as an anonymous class, as Java'snew Listener() { … }is, so a call through the interface reaches its overrides. An anonymous class's members, Java's and Kotlin's, are now named only inside the declaration that holds the class, and a method a Java or Kotlin class inherits through a nested supertype is that type's (a builder'sbuild()from its ownAbstractBuilder), not a namesake's.Root cause
matchByQualifiedNamecompared it verbatim (Outer.Inneragainstp::Outer::Inner, whose scope boundary is::), so it stayed unresolved.extendsreference was cut to the last segment (Listenerfornew Api.Listener() {…}) in both engines (extractAnonymousClass,java.rs), a name no nested type is found by.kotlin.rs, and the wasm extractor), soclass K : Outer.Inner()extendedOuter.resolveMethodOnType's walk up the supertypes asks each by its simple name (context.getSupertypes), so a method inherited through a nested supertype was looked up on every type of that name: spring-ai'sOpenAiChatOptions.builder()…build()reached an MCP annotation'sAbstractBuilder.build.object_literal: an object expression's overrides became local functions of the code around it, which no call through the interface reached, and its properties made no nodes.What changed (our implementation)
matchDottedSupertype, name-matcher): a dotted Java, Kotlin or C# inheritance reference resolves to the type whose qualified name ends with the path (or equals it, written in full:com.x.Outer.Inner). Unless written in full, the path's first type must be one the file can name: Java its package, a single-type import or an on-demand import; Kotlin its package, an import or a star import; C# its namespaces and usings (isCsharpTypeVisible). Of several, the one closest in the tree, and none on a tie. Other languages are untouched, so MyBatis's mixed dotted names keep their matching. A Kotlin object expression'sinstantiatesgoes the same way, and like an extends reference takes only a type that can be a supertype (okhttp'sobject : CookieJaris noOverrideParam.CookieJarenum entry).jvmSupertypeChains, inresolveMethodOnType's supertype walk, Java and Kotlin): each supertype is asked first by the chain of types that names it, package dropped (AssistantMessage::Builder), then by its simple name as before, so anextendsedge resolved to a namesake (Minestom'sPlayer extends LivingEntityto a nestedEntityStatuses.LivingEntity) still reachesEntity.getPositionthrough the name.java.rs,kotlin.rs): a Java anonymous class'sextendsreference keeps the written path, type arguments cut; Kotlin joins every segment of a supertype'suser_type(Outer.Inner, type arguments left out); a Kotlinobject_literalwith a body becomes a class<Listener$anon@line>(named after the class it builds, else its first supertype), with anextendsreference per supertype, aninstantiatesreference from the code around it to that supertype as written (resolved bymatchDottedSupertypetoo, soobject : mockwebserver3.Dispatcher()is notokhttp3.Dispatcher), and its body's declarations as members. Its header (constructor arguments, abydelegate) is walked first, in the enclosing scope, by the walker that met it.isLexicallyReachable): a member of an anonymous class (its qualified name passes through<T$anon@N>) is reachable by name only from the declaration around line N that holds the class (found by its qualified name, else, for a Kotlin extension function named by its receiver likeCall::asObservable, as the innermost declaration around that line). Without this, mihon'sblock()calls on a lambda parameter reached anobject : MarkdownPadding'soverride val blockat 0.9.inferJavaFieldReceiverTypeandinferMemberReceiverTypewalk out past anonymous classes, an object's own property first;kotlinReceiverDeclaredTypereads inherited properties from the first named class. okhttp'sserver.url()in anobject : Callbackhad become a 0.65 guess at the deprecatedokhttp3.mockwebserver.MockWebServer.lexicalKotlinMembers):original!!.contentLength()inoverride fun contentLength(),this@MediaCarouselScrollHandler.onScroll(…), an extension receiver'sonDataChanged(…). It stays a candidate, so a call that lands on it is still declined as a collapsed receiver or asuper.call.scopedOutin the method-by-name strategy, Kotlin only): okhttp'sit.isReady()is still theSerialTaskinterface's.Reimplements #1927 by @mixxer, independently: the PR's diff and tests were read, not run or merged. We reused its cases (header calls in the enclosing scope,
bydelegates, generic outer and inner segments, nested object expressions) and the T5 triage fixtures (pr1927,pr1927b,anon-dispatch,dotted-super/k,dotted-super/cs). T5's "found while verifying" items 2 (Kotlin's first segment) and 3 (C#'s unresolved dotted base) are fixed here.Validation
Tests.
__tests__/jvm-dotted-supertypes.test.ts: 13 tests; 11 fail on main60b0d7a9(the other two guard what must not change: a dotted supertype the file cannot name, and calls inside an anonymous class), all 13 pass here on the wasm path and on the rebuilt kernel.Kernel parity (
scripts/kernel-parity.mjs, the rebuilt kernel against the wasm path), 0 files with diffs:torture.kt1/1 (137 nodes, with object expressions and dotted supertypes added),Torture.java1/1 (a dotted anonymous class added), okhttp Kotlin 588/619 and Java 71/71, mihon 891/940, nowinandroid 340/350, SystemUI Kotlin 188/198 and Java 1141/1141, jextract 218/218, Minestom 1859/1894. The deferred files are the ones main's kernel defers, a parse error in the file.A/B against main
60b0d7a9(its kernel's Java and Kotlin code is unchanged since787eafbf), both on their kernels,--no-refsites (a call's position, not its name, so a local function that became an object's method is a rename), every changed site sorted bytools/triage-1927.cjs:extendsorimplementsper supertype (okhttp 262, SystemUI 86 counting Java's anonymous classes, Minestom 40, mihon 6, jextract 3), theinstantiatesfrom the code around each object, which is that object's own supertype in every case (okhttp 261/261, SystemUI 62/62, mihon 6/6), and dispatch from an interface or base method to the object's override (okhttp 63, SystemUI 86). A named class's dotted supertype (SystemUI 64, Minestom 44, kestra 69, spring-ai 13, playframework 20, jextract 7, spring-modulith 1), its dispatch (okhttp 27, SystemUI 115, Minestom 68), and the calls a dotted supertype now types: SystemUI'ssuper.toString()/equals()/copyFrom()inMobileState extends SignalController.State, spring-modulith's 8 throughTargetEventPublication extends org.springframework.modulith.events.EventPublication, Minestom's 8 throughNetworkBuffer.Type. Wrong among them: 5 okhttp calls at 0.4 that surfaced because an object's member, formerly a local function that won the name match and was then declined, no longer competes (response.body.source()→ForwardingResponseBody.source×3, acontentLength, a fieldsource), andcacheBody.close()→ResponseBody.closeat 0.65 on an okio sink; 7 more of that kind (response.body.close()→ResponseBody.close) are right.extendsthat is now animplementsof the nested interface (okhttp 12, SystemUI 9, mihon 3, Minestom 2:new NetworkBuffer.Type<>() {…}had extended the enumScoreboardObjectivePacket.Type); dispatch a first-segment supertype had made up (okhttp 5:RealConnection : Http2Connection.Listener()"extended"Http2Connection; SystemUI 17:MediaDataManager.Listener,QSPanel.QSTileLayout); Java anonymous classes written with a library type's nested type, which had been guessed onto a projectCallbackorListener(SystemUI 14:new MediaController.Callback()); okhttp's 8toString()guesses onHttpUrl,Intand socket values. playframework's 3: two Java anonymous classes writtennew play.api.mvc.Filter() {…}andnew play.api.db.Database() {…}inside the Java types of the same name had "extended" those enclosing types (wrong; the Scala traits they name carry no package in their qualified names, so nothing links now), and one right edge is lost:new javaguide.testhelpers.MockJavaAction(…)names a Scala class the same way.new ScrimController.Callback(),new Interaction.Callback()); dispatch fan-outs that now include object overrides or lose a first-segment impostor (okhttp 101, SystemUI 72, Minestom 6, mihon 2); a bare call in an object's method to its sibling member (SystemUI'sMoveHelper.canMoveAfter, a localContentObserver'sonChange); jextract'stype.equals()on aType.Functionfield,Type.equalsnow (wasDeclaration.equals, through the namesakeDeclaration.Function); spring-modulith's 10 typed through the resolvedEventPublication(0.65 guesses → 0.9). spring-ai: 33 builder chains (OpenAiChatOptions.builder()…build(),.clone(),.combineWith(),LegacySubclass.builder().content(…)) and 5 dispatch fan-outs now reach the builder their class inherits (OpenAiChatOptions.AbstractBuilder,AssistantMessage.Builder) instead of a namesake in another module (AbstractMcpPromptListChangedMethodCallback.AbstractBuilder.build,OllamaApi.Message.Builder.content); kestra 6 and playframework 13 dispatch fan-outs gain implementers whose dotted supertype now links (Formatters.SimpleFormatter). Wrong to wrong: okhttp 24 guesses that went to a local function of another file now go to another candidate at the same confidence, 3close()on library streams; SystemUI 3setTitle(0.7 → 0.5).close()inRealWebSocketTest0.7 → 0.9 (a namesake local function left the ranking); spring-modulith 23 raised to 0.9 through the resolved supertype; SystemUI 62, mostly anonymous classes' supertypes now found through the import or the written path at the same or higher confidence, and 2 calls in a test object 0.7 → 0.65 on the same target.Cost (median of 3 interleaved index runs,
tools/cputime.ps1): okhttp CPU 27.8 → 29.0 s, wall 8.6 → 8.8 s (it gains 360 object classes, 702 methods and 1,491 edges); mihon CPU 23.6 → 23.8 s, wall 7.8 → 7.7 s.Suite: the full suite on this commit (
7c863e4b, main3a4cd3bf, kernel rebuilt there),CODEGRAPH_KERNEL_EXPECT=1,CODEGRAPH_NO_DAEMONunset,--maxWorkers=4: 792 test files passed, 6 skipped; 9,506 tests passed, 0 failed, 85 skipped (770 s). main has since moved to4e7a2210(#3044, dependency locks only);git merge-treewith it is clean.Follow-ups (not in this PR)
new Outer.Inner()still instantiates by the last name (Inner), as before; the Kotlin object expression's now uses the written path.new javaguide.testhelpers.MockJavaAction(…),new play.api.mvc.Filter() {…}) links nothing: Scala qualified names carry no package, so the written path matches none (playframework's one lost edge).<T$anon@line>), as Java's always have.class A { class B { fun f() {} } }) indexesfas a top-level function and dropsAandBin both engines (a grammar quirk).Reimplements #1927 by @mixxer.
🤖 Generated with Claude Code