fix(jvm): a dotted supertype names its nested type, and a Kotlin object expression is an anonymous class (#3051)

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 60b0d7a9
  (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 60b0d7a9, 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>
This commit is contained in:
Colby Mchenry
2026-10-30 07:29:52 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1941caf7bc
commit 2514467801
8 changed files with 917 additions and 39 deletions
+16 -3
View File
@@ -959,8 +959,21 @@ impl<'t> Walker<'t> {
.or_else(|| node.child_by_field_name("type"))
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.named_child(0));
let mut type_name = type_node.map(|t| self.text(t).to_string()).unwrap_or_else(|| "Object".to_string());
type_name = strip_generic_and_qualifier(&type_name);
let raw = type_node.map(|t| self.text(t).to_string()).unwrap_or_else(|| "Object".to_string());
// The supertype as written (`Api.Listener` for `new Api.Listener() {…}`),
// type arguments cut from the first `<` past index 0, trimmed — the
// `extends` ref's name. The anon class's own name keeps the last segment.
let mut written = raw.clone();
if let Some(lt) = written.find('<') {
if lt > 0 {
written.truncate(lt);
}
}
let mut written = written.trim().to_string();
if written.is_empty() {
written = "Object".to_string();
}
let mut type_name = strip_generic_and_qualifier(&raw);
if type_name.is_empty() {
type_name = "Object".to_string();
}
@@ -975,7 +988,7 @@ impl<'t> Walker<'t> {
Some(t) => (t.start_position().row as u32, self.col_of(t)),
None => (node.start_position().row as u32, self.col_of(node)),
};
self.push_ref(row, &type_name, edge_kind_index("extends").unwrap(), line, column);
self.push_ref(row, &written, edge_kind_index("extends").unwrap(), line, column);
self.stack.push(Scope { row, kind: "class", name: anon_name });
for i in 0..body.named_child_count() {
+128 -21
View File
@@ -895,10 +895,15 @@ impl<'t> Walker<'t> {
self.extract_call(node);
} else if kind == "infix_expression" {
self.extract_infix_call(node);
} else if kind == "object_literal" {
// `object : Api.Listener { … }` — an anonymous class.
if self.extract_object_literal(node, false) {
skip_children = true;
}
}
// companion_object, anonymous_initializer, secondary_constructor,
// getter/setter siblings, file_annotation, object_literal, if/when at
// top level: no branch — recursed (calls attribute to the stack top).
// getter/setter siblings, file_annotation, if/when at top level: no
// branch — recursed (calls attribute to the stack top).
if !skip_children {
for i in 0..node.named_child_count() {
@@ -930,6 +935,12 @@ impl<'t> Walker<'t> {
self.extract_static_member_ref(node);
// `object : Api.Listener { … }` in a body or an initializer — an
// anonymous class; its header is this body's code.
if kind == "object_literal" && self.extract_object_literal(node, true) {
return;
}
if kind == "function_declaration" {
let name = self.extract_name(node);
if name != "<anonymous>" {
@@ -1360,10 +1371,10 @@ impl<'t> Walker<'t> {
}
/// extractInheritance — delegation_specifier: user_type ?? its
/// constructor_invocation's user_type → FIRST type_identifier → ONE
/// `extends` ref at the typeId (interfaces ride extends too; qualified
/// supertypes take the FIRST segment — `com`; `by`-delegation emits
/// NOTHING).
/// constructor_invocation's user_type → ONE `extends` ref named by the
/// user_type's type_identifiers joined with `.` (`Outer.Inner`, type
/// arguments left out), at the first one (interfaces ride extends too;
/// `by`-delegation emits NOTHING).
fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
let extends_kind = edge_kind_index("extends").unwrap();
for i in 0..node.named_child_count() {
@@ -1379,29 +1390,125 @@ impl<'t> Walker<'t> {
.find(|c| c.kind() == "constructor_invocation");
let target = user_type.or(ctor_inv);
let Some(target) = target else { continue };
let type_id: Node = if target.kind() == "user_type" {
let type_node: Option<Node> = if target.kind() == "user_type" {
Some(target)
} else {
(0..target.named_child_count())
.filter_map(|j| target.named_child(j))
.find(|c| c.kind() == "type_identifier")
.unwrap_or(target)
.find(|c| c.kind() == "user_type")
};
let ids: Vec<Node> = match type_node {
Some(t) => (0..t.named_child_count())
.filter_map(|j| t.named_child(j))
.filter(|c| c.kind() == "type_identifier")
.collect(),
None => Vec::new(),
};
let type_id: Node = ids.first().copied().or(type_node).unwrap_or(target);
let name = if ids.is_empty() {
self.text(type_id).to_string()
} else {
// constructor_invocation → its user_type → first type_identifier
let ut = (0..target.named_child_count())
.filter_map(|j| target.named_child(j))
.find(|c| c.kind() == "user_type");
match ut {
Some(ut) => (0..ut.named_child_count())
.filter_map(|j| ut.named_child(j))
.find(|c| c.kind() == "type_identifier")
.unwrap_or(ut),
None => target,
}
ids.iter().map(|c| self.text(*c)).collect::<Vec<_>>().join(".")
};
let name = self.text(type_id).to_string();
self.push_ref_at(class_row, &name, extends_kind, type_id);
}
}
/// extractKotlinObjectLiteral — `object : Api.Listener { … }`: an anonymous
/// class. Headers (each delegation_specifier) walked first by the CURRENT
/// walker in the enclosing scope; an `instantiates` ref from the stack top
/// to the first supertype's last segment; a class `<Short$anon@line>`;
/// `extends` refs (dotted path) from it, at each path's first
/// type_identifier; its class_body children through visit_node. False
/// (nothing done) without a class_body.
fn extract_object_literal(&mut self, node: Node<'t>, body_walker: bool) -> bool {
stack_guard!();
let Some(body) = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "class_body")
else {
return false;
};
let specs: Vec<Node<'t>> = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.filter(|c| c.kind() == "delegation_specifier")
.collect();
// (name, first type_identifier, built by a constructor_invocation)
let mut supertypes: Vec<(String, Node<'t>, bool)> = Vec::new();
for spec in &specs {
let holder = (0..spec.named_child_count())
.filter_map(|j| spec.named_child(j))
.find(|c| c.kind() == "constructor_invocation" || c.kind() == "explicit_delegation");
let from = holder.unwrap_or(*spec);
let user_type = (0..from.named_child_count())
.filter_map(|j| from.named_child(j))
.find(|c| c.kind() == "user_type");
let ids: Vec<Node<'t>> = match user_type {
Some(t) => (0..t.named_child_count())
.filter_map(|j| t.named_child(j))
.filter(|c| c.kind() == "type_identifier")
.collect(),
None => Vec::new(),
};
if let Some(first) = ids.first() {
let name = ids.iter().map(|c| self.text(*c)).collect::<Vec<_>>().join(".");
let built = holder.map(|h| h.kind() == "constructor_invocation").unwrap_or(false);
supertypes.push((name, *first, built));
}
}
for spec in &specs {
if body_walker {
self.visit_for_calls_and_structure(*spec);
} else {
self.visit_node(*spec);
}
}
// The class it builds, else its first supertype: the object's name and,
// as written, what the code around instantiates.
let primary = supertypes.iter().find(|s| s.2).or(supertypes.first());
let short_name = match primary {
Some((name, _, _)) => match name.rfind('.') {
Some(dot) => name[dot + 1..].to_string(),
None => name.clone(),
},
None => "Object".to_string(),
};
if let Some((written, at, _)) = primary {
if !self.stack.is_empty() {
let owner = self.top_row();
let at = *at;
let written = written.clone();
self.push_ref_at(owner, &written, edge_kind_index("instantiates").unwrap(), at);
}
}
let anon_name = format!("<{short_name}$anon@{}>", node.start_position().row + 1);
let Some(row) = self.create_node("class", &anon_name, node, Extra::default()) else {
for i in 0..body.named_child_count() {
if let Some(c) = body.named_child(i) {
if body_walker {
self.visit_for_calls_and_structure(c);
} else {
self.visit_node(c);
}
}
}
return true;
};
let extends_kind = edge_kind_index("extends").unwrap();
for (name, at, _) in &supertypes {
let at = *at;
self.push_ref_at(row, name, extends_kind, at);
}
self.stack.push(Scope { row, kind: "class", name: anon_name });
for i in 0..body.named_child_count() {
if let Some(c) = body.named_child(i) {
self.visit_node(c);
}
}
self.stack.pop();
true
}
/// extractDecoratorsFor — kotlin annotations inside `modifiers`:
/// `@Marker` (user_type child) → decorates ref; `@Anno(args)`
/// (constructor_invocation) → NOTHING. Runs for functions/methods/classes