mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 00:01:39 +08:00
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:
co-authored by
Claude Opus 5.5
parent
1941caf7bc
commit
2514467801
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user