diff --git a/CHANGELOG.md b/CHANGELOG.md index ee561ad18..9ed55a86e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -270,6 +270,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Kotlin, a call on a library type you import, like Jetpack Compose's `Modifier.pad()`, now links to the extension your project declares on that type, `fun Modifier.pad()`, when the calling file can see it: in the same package, or imported by name or with `.*`. The library type's own methods, like `Modifier.fillMaxSize()`, still never link to a project method that shares the name. Re-index Kotlin projects after upgrading. Thanks @danusha2345. - In Kotlin, when two packages each declare a class with the same name, a call on a value of that type now links to the class the file imports, even when that class lives in a file named for something else, like `FooConverter` in `Converters.kt`. Before, it went to whichever of the two came first. Java calls on such a class, like okhttp's Java tests calling the `mockwebserver3.MockWebServer` they import, now link through the type their variable is declared with (#1942). Re-index Kotlin projects after upgrading. Thanks @arttttt. - In Kotlin and Java, a call through an import of a class written in the other language now links to that class: Kotlin's `JUtil.make()`, `JUtil.Nested.deep()` or `Outer.Inner()` on an imported Java class, and Java's `KUtil.INSTANCE.kmake()` on a Kotlin object, `Factory.Companion.create()` on a companion object or `UtilsKt.helper()` on a Kotlin file's top-level function, whose import now links that file. Before, the import was only matched against files in the calling file's own language, so the call went to a same-named class in another package, or to nothing. A Kotlin extension declared in another package, like `fun Modes.hidden()`, is now reached only from a file that imports it or its package. Re-index Kotlin and Java projects after upgrading. Thanks @mixxer. +- In Java, Kotlin and C#, a supertype written through its enclosing type, like `class Named extends Outer.Inner`, `implements Api.Listener`, `new Api.Listener() { … }`, Kotlin's `class K : Outer.Inner()` or C#'s `class N : Outer.IListener`, now links to that nested type when the file can name the outer type, and a method a class inherits through a nested type, like a builder's `build()` from its `AbstractBuilder`, is that type's rather than a namesake's. Before, Java and C# left it unlinked, a Java anonymous class kept only the last name, and Kotlin linked the outer type instead. A Kotlin object expression, like `object : Listener { … }` passed as a callback or held in a property, is now indexed as an anonymous class, as Java's `new Listener() { … }` is: its overrides are its own methods, so a call through the interface reaches them and `codegraph_explore` follows a callback into its body. Re-index Java, Kotlin and C# projects after upgrading. Thanks @mixxer. - In JavaScript and TypeScript, a `super.run()` call now links to the method of the class's parent, or of the nearest ancestor that has it, like three.js's `super.copy(source)` reaching `Object3D.copy`, so `codegraph_explore`'s call path and impact follow an override into the method it builds on. Before, such a call inside a method of the same name was dropped, and elsewhere it went to whatever method had the name, often the class's own or a sibling class's in the same file. A call from a static method reaches the parent's static method, and a call to a parent from outside your project, like `HTMLElement`, `Set` or `Promise`, links nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2497) - In JavaScript and TypeScript, a named class expression, like `register(class Widget extends Base {…})`, `customElements.define('x-card', class XCard extends HTMLElement {…})` or `const Panel = class PanelImpl extends Base {…}`, is now indexed as a class with its own methods and the class it extends, so calls between its methods and its `super` calls link as a class declaration's do. Before, its methods were free functions or weren't indexed at all, and it extended nothing. Its name stays bound inside the expression alone, as JavaScript has it: `new Widget()` elsewhere links nothing, a constant, class or interface of the same name around it keeps its links, and code outside never reaches its methods by a guess. A class a CommonJS module exports, like webpack's `module.exports = class MultiCompiler {…}` or `exports.Store = class Store {…}`, is reached through `require` as a class declaration is, along with its static methods and the methods called on its instances. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2499) - In Go, a call through a struct field that holds a function, like prometheus's `d.refreshf(ctx)`, now reaches the functions put in that field, so `codegraph_explore` follows service discovery from `Discovery.Run` into each provider's `refresh` and names the line where each provider wires its method in, like `RefreshF: d.refresh`. The field can be set in a struct literal or by an assignment, copied from another struct's field (`refreshf: opts.RefreshF`), or given a method of an interface value, and two unrelated structs with a field of the same name, like `fn` or `handler`, stay apart. Before, such a flow stopped at the field. Re-index Go projects after upgrading. diff --git a/__tests__/fixtures/kernel-parity/Torture.java b/__tests__/fixtures/kernel-parity/Torture.java index 4d8c7c7ed..221f02b7a 100644 --- a/__tests__/fixtures/kernel-parity/Torture.java +++ b/__tests__/fixtures/kernel-parity/Torture.java @@ -131,3 +131,12 @@ interface Shape extends Comparable, Cloneable { @interface Marker { String value() default ""; } + +class DottedAnon { + Api.Listener listener = new Api.Listener() { + public void on() {} + }; + Object plain = new java.util.concurrent.Callable() { + public Integer call() { return 1; } + }; +} diff --git a/__tests__/fixtures/kernel-parity/torture.kt b/__tests__/fixtures/kernel-parity/torture.kt index a39e1ae69..81f554af9 100644 --- a/__tests__/fixtures/kernel-parity/torture.kt +++ b/__tests__/fixtures/kernel-parity/torture.kt @@ -311,3 +311,41 @@ class ReceiverChainK(private val engine: EngineK) { super.toString().length.toString() } } + +// Object expressions as anonymous classes: dotted and generic supertypes, a +// constructor and a `by` delegate in the header, nested and supertype-less ones. +// (Multi-line bodies: a one-line class body sets the grammar's phantom error.) +class ObjectExpressions { + val held = object : Outer.Listener, Runnable { + override fun on() { + helper() + } + override fun run() { + } + } + fun make(): Base = object : Base(compute()) { + override fun run() { + val inner = object : Runnable { + override fun run() { + helper() + } + } + } + } + fun delegated() = object : api.IFoo by makeDelegate(), Marker { + val x = 1 + } + fun generic() = object : pkg.Outer.Inner>() { + val y = 2 + } + fun bare() = object { + fun x() { + helper() + } + } + fun helper() { + } + fun compute(): Int = 1 +} + +class DottedSuper : Outer.Inner(), com.example.Api.Listener diff --git a/__tests__/jvm-dotted-supertypes.test.ts b/__tests__/jvm-dotted-supertypes.test.ts new file mode 100644 index 000000000..40c80b256 --- /dev/null +++ b/__tests__/jvm-dotted-supertypes.test.ts @@ -0,0 +1,390 @@ +/** + * A supertype written through its enclosing type, `Outer.Inner`, names the + * nested type: Java's `class Named extends Outer.Inner`, `implements + * Api.Listener` and `new Api.Listener() { … }`, Kotlin's `class K : + * Outer.Inner()`, C#'s `class N : Outer.IListener`. Before, Java and C# left + * the path unresolved, Kotlin took its FIRST segment (`Outer`), and a Java + * anonymous class kept only the last one. A Kotlin object expression, + * `object : Listener { … }`, is an anonymous class as Java's is: its members + * are its own, and a call through the interface reaches its override. + * + * Cases from @mixxer's #1927 and the T5 triage fixtures. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-jvm-dotted-super-')); + const files: Record = { + 'java/p/Api.java': `package p; +public interface Api { + interface Listener { void on(); } +} +`, + 'java/p/Outer.java': `package p; +public class Outer { + public static class Inner { public void run() {} } +} +`, + 'java/p/Users.java': `package p; +class Named extends Outer.Inner { + @Override public void run() {} +} +class Impl implements Api.Listener { + public void on() {} +} +class Host { + Api.Listener field = new Api.Listener() { + public void on() {} + }; +} +`, + 'java/a/Box.java': `package a; +public class Box { public static class Part { public void use() {} } } +`, + 'java/b/Box.java': `package b; +public class Box { public static class Part { public void use() {} } } +`, + 'java/c/Thing.java': `package c; +import a.Box; +class Thing extends Box.Part { + public void use() {} +} +`, + 'java/d/Stray.java': `package d; +class Stray extends Box.Part { + public void use() {} +} +`, + 'java/b2/Message.java': `package b2; +public class Message { + public static class Builder> { + public T content(String c) { return null; } + } +} +`, + 'java/b2/Use.java': `package b2; +class Other { + static class Builder { + Builder content(String c) { return this; } + } +} +class Legacy { + static LegacyBuilder builder() { return new LegacyBuilder(); } + static final class LegacyBuilder extends Message.Builder {} +} +class Use { + void run() { Legacy.builder().content("x"); } +} +`, + 'kotlin/k/Outer.kt': `package k + +open class KOuter { + open class KInner { open fun run() {} } + interface KListener { fun on() } +} +`, + 'kotlin/k/Users.kt': `package k + +class KNamed : KOuter.KInner() { + override fun run() {} +} + +class KImpl : KOuter.KListener { + override fun on() {} +} +`, + 'kotlin/k/Flow.kt': `package k + +interface Listener { fun onEvent() } + +abstract class Base(val n: Int) { abstract fun run() } + +class Bus(private val listener: Listener) { + fun fire() { listener.onEvent() } +} + +class Host { + fun wire(): Bus = Bus(object : Listener { + override fun onEvent() { handled() } + }) + + fun build(): Base = object : Base(compute()) { + override fun run() { other() } + } + + private val held = object : KOuter.KListener { + override fun on() { handled() } + } + + fun handled() {} + fun other() {} + fun compute(): Int = 1 +} +`, + 'kotlin/k/Client.kt': `package k + +class Server { + fun url(path: String): String = path +} +class Remote { + fun url(path: String): String = path +} +interface Callback { + fun onResponse() +} +class Caller(private val backup: Server) { + private val remote: Server = Server() + fun send(callback: Callback) {} + fun run() { + send(object : Callback { + override fun onResponse() { + remote.url("/a") + backup.url("/b") + } + }) + } +} +`, + 'kotlin/k/JarApi.kt': `package k + +interface Jar { + fun load() +} +`, + 'kotlin/k/Jar.kt': `package k + +enum class Override { + Jar, +} +class JarUser { + fun make(): Any = object : Jar { + override fun load() {} + } +} +`, + 'kotlin/k/Wrap.kt': `package k + +abstract class Body { + abstract fun measure(): Long +} + +fun wrap(original: Body?): Body = + object : Body() { + override fun measure(): Long = original!!.measure() + } + +class Tasks { + interface Task { + fun ready(): Boolean + } + private val queue = ArrayList() + fun add() { + queue.add(object : Task { + override fun ready() = true + }) + } + fun next(): Int = queue.indexOfFirst { it.ready() } +} + +interface ScrollListener { + fun onScroll(d: Int) +} +class Scroller { + fun onScroll(d: Int) {} + val listener = object : ScrollListener { + override fun onScroll(d: Int) = this@Scroller.onScroll(d) + } +} + +class Pipe { + fun close() {} + val stream = object : java.io.FilterInputStream(null) { + override fun close() { + super.close() + } + } +} +`, + 'kotlin/k/Ext.kt': `package k + +fun String.produce(): Runnable = object : Runnable { + override fun run() {} + fun request(n: Long) {} +} +fun consume(items: List) { + items.forEach { it.request(1) } +} +`, + 'kotlin/k/Padding.kt': `package k + +interface Padding { val block: Int } + +private val padding = object : Padding { + override val block = 1 +} +`, + 'kotlin/k/Runner.kt': `package k + +fun runIt(block: () -> Unit) { block() } +`, + 'java/v/Visit.java': `package v; +interface Visitor { void a(); void b(); } +class Walker { + static final Visitor VISITOR = new Visitor() { + public void a() { b(); } + public void b() {} + }; +} +`, + 'csharp/Outer.cs': `namespace Shop +{ + public class Outer + { + public class Inner { public virtual void Run() {} } + public interface IListener { void On(); } + } +} +`, + 'csharp/Users.cs': `namespace Shop +{ + public class Named : Outer.Inner + { + public override void Run() {} + } + public class Impl : Outer.IListener + { + public void On() {} + } +} +`, + }; + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** `kind qualified-name` of the supertypes the class named `name` in `file` links to. */ +function supertypes(file: string, name: string): string[] { + const types = cg.getNodesInFile(file).filter((n) => n.kind === 'class' || n.kind === 'interface'); + const from = types.find((n) => n.name === name) ?? types.find((n) => n.name.startsWith(name))!; + return cg + .getOutgoingEdgesFrom([from.id]) + .filter((e) => e.kind === 'extends' || e.kind === 'implements') + .map((e) => `${e.kind} ${cg.getNode(e.target)!.qualifiedName}`) + .sort(); +} + +/** Qualified names of what `qualifiedName` calls (or, with `kind`, links to by that kind). */ +function callees(qualifiedName: string, kind = 'calls'): string[] { + const from = cg.searchNodes(qualifiedName.split('::').pop()!).map((r) => r.node).find((n) => n.qualifiedName === qualifiedName)!; + return cg + .getOutgoingEdgesFrom([from.id]) + .filter((e) => e.kind === kind) + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +describe('Java: a dotted supertype', () => { + it('names the nested type it spells', () => { + expect(supertypes('java/p/Users.java', 'Named')).toEqual(['extends p::Outer::Inner']); + expect(supertypes('java/p/Users.java', 'Impl')).toEqual(['implements p::Api::Listener']); + }); + + it('is kept whole on an anonymous class', () => { + expect(supertypes('java/p/Users.java', ' { + expect(supertypes('java/b2/Use.java', 'LegacyBuilder')).toEqual(['extends b2::Message::Builder']); + expect(callees('b2::Use::run')).toContain('b2::Message::Builder::content'); + expect(callees('b2::Use::run')).not.toContain('b2::Other::Builder::content'); + }); + + it('starts at the type the file imports, or at none it cannot name', () => { + expect(supertypes('java/c/Thing.java', 'Thing')).toEqual(['extends a::Box::Part']); + expect(supertypes('java/d/Stray.java', 'Stray')).toEqual([]); + }); +}); + +describe('Kotlin: a dotted supertype', () => { + it('names the nested type, not its first segment', () => { + expect(supertypes('kotlin/k/Users.kt', 'KNamed')).toEqual(['extends k::KOuter::KInner']); + expect(supertypes('kotlin/k/Users.kt', 'KImpl')).toEqual(['implements k::KOuter::KListener']); + }); +}); + +describe('Kotlin: an object expression', () => { + it('is an anonymous class whose overrides are its own members', () => { + const anon = cg.getNodesInFile('kotlin/k/Flow.kt').filter((n) => n.kind === 'class' && n.name.includes('$anon@')); + expect(anon.map((n) => n.qualifiedName).sort()).toEqual([ + 'k::Host::build::', + 'k::Host::held::', + 'k::Host::wire::', + ]); + expect(supertypes('kotlin/k/Flow.kt', ' { + expect(callees('k::Host::held', 'instantiates')).toEqual(['k::KOuter::KListener']); + expect(callees('k::Host::wire', 'instantiates')).toEqual(['k::Bus', 'k::Listener']); + // Never an enum entry of the name, which no object extends. + expect(callees('k::JarUser::make', 'instantiates')).toEqual(['k::Jar']); + }); + + it('keeps its header in the code around it, and its body in its members', () => { + expect(callees('k::Host::build')).toContain('k::Host::compute'); + expect(callees('k::Host::build::::run')).toEqual(['k::Host::other']); + expect(callees('k::Host::wire::::onEvent')).toEqual(['k::Host::handled']); + }); + + it('is reached through the interface it implements', () => { + expect(callees('k::Listener::onEvent')).toEqual(['k::Host::wire::::onEvent']); + expect(callees('k::Base::run')).toEqual(['k::Host::build::::run']); + }); + + it('sees the properties of the class it is written in', () => { + expect(callees('k::Caller::run::::onResponse')).toEqual(['k::Server::url', 'k::Server::url']); + }); + + it('leaves the names it overrides to the code that calls them', () => { + // A call its override makes on another object is not the override itself. + expect(callees('k::wrap::::measure')).toEqual(['k::Body::measure']); + expect(callees('k::Scroller::listener::::onScroll')).toEqual(['k::Scroller::onScroll']); + // `super.close()` is the library parent's, not the class around the object. + expect(callees('k::Pipe::stream::::close')).toEqual([]); + // Its override outside the object's reach leaves the interface's method the one of that name. + expect(callees('k::Tasks::next')).toEqual(['k::Tasks::Task::ready']); + }); +}); + +describe('An anonymous class\'s members', () => { + it('are named only inside the declaration holding the class', () => { + expect(callees('k::runIt').filter((qn) => qn.includes('$anon@'))).toEqual([]); + // An extension function holds one too, though it is named by its receiver (`String::produce`). + expect(callees('k::consume').filter((qn) => qn.includes('$anon@'))).toEqual([]); + expect(callees('v::Walker::VISITOR::::a')).toEqual(['v::Walker::VISITOR::::b']); + }); +}); + +describe('C#: a dotted base type', () => { + it('names the nested type it spells', () => { + expect(supertypes('csharp/Users.cs', 'Named')).toEqual(['extends Shop::Outer::Inner']); + expect(supertypes('csharp/Users.cs', 'Impl')).toEqual(['implements Shop::Outer::IListener']); + }); +}); diff --git a/codegraph-kernel/src/java.rs b/codegraph-kernel/src/java.rs index 6a7735a20..eb8c7b70e 100644 --- a/codegraph-kernel/src/java.rs +++ b/codegraph-kernel/src/java.rs @@ -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() { diff --git a/codegraph-kernel/src/kotlin.rs b/codegraph-kernel/src/kotlin.rs index 5459e63c9..90eac788a 100644 --- a/codegraph-kernel/src/kotlin.rs +++ b/codegraph-kernel/src/kotlin.rs @@ -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 != "" { @@ -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 = 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 = 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::>().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 ``; + /// `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> = (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> = 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::>().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 diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 6afc7ba65..7203ed684 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -2022,6 +2022,12 @@ export class TreeSitterExtractor { skipChildren = true; } } + // Kotlin `object : Api.Listener { … }`: an anonymous class, as Java's + // `new Api.Listener() { … }` is (extractKotlinObjectLiteral). + else if (this.language === 'kotlin' && nodeType === 'object_literal' && + this.extractKotlinObjectLiteral(node, (header) => this.visitNode(header))) { + skipChildren = true; + } // (Decorator handling lives inside the symbol-creating extractors // — extractClass / extractFunction / extractProperty — because the // decorator node sits BEFORE the symbol in the AST and the walker @@ -7014,6 +7020,9 @@ export class TreeSitterExtractor { let typeName = typeNode ? getNodeText(typeNode, this.source) : 'Object'; const ltIdx = typeName.indexOf('<'); if (ltIdx > 0) typeName = typeName.slice(0, ltIdx); + // The supertype as written, `Api.Listener` for `new Api.Listener() {…}`: + // the last segment alone is a nested type nothing can find. + const writtenType = typeName.trim() || 'Object'; const lastDot = Math.max(typeName.lastIndexOf('.'), typeName.lastIndexOf('::')); if (lastDot >= 0) typeName = typeName.slice(lastDot + 1).replace(/^[:.]/, ''); typeName = typeName.trim() || 'Object'; @@ -7029,7 +7038,7 @@ export class TreeSitterExtractor { // bridge T's methods to the override names found in the anon body. this.unresolvedReferences.push({ fromNodeId: classNode.id, - referenceName: typeName, + referenceName: writtenType, referenceKind: 'extends', line: typeNode?.startPosition.row ?? node.startPosition.row, column: typeNode?.startPosition.column ?? node.startPosition.column, @@ -7045,6 +7054,72 @@ export class TreeSitterExtractor { this.nodeStack.pop(); } + /** + * A Kotlin object expression, `object : Api.Listener { … }` or `object : + * Base(x), Runnable { … }`: an anonymous class, as Java's `new T() { … }` + * is. The class, ``, extends each supertype as written + * (the dotted path, type arguments left out), the code around it + * instantiates the first, and its body's declarations are its members — an + * override inside is then reached through the interface it implements, + * instead of standing as a local function of the code around it. The + * header (a constructor's arguments, a `by` delegate) runs in the + * enclosing scope: `walkHeader` walks it there. False, with nothing done, + * for an object expression without a body. + */ + private extractKotlinObjectLiteral(node: SyntaxNode, walkHeader: (header: SyntaxNode) => void): boolean { + const body = node.namedChildren.find((c: SyntaxNode) => c.type === 'class_body'); + if (!body) return false; + const specs = node.namedChildren.filter((c: SyntaxNode) => c.type === 'delegation_specifier'); + const supertypes: Array<{ name: string; at: SyntaxNode; built: boolean }> = []; + for (const spec of specs) { + const holder = spec.namedChildren.find((c: SyntaxNode) => c.type === 'constructor_invocation' || c.type === 'explicit_delegation'); + const userType = (holder ?? spec).namedChildren.find((c: SyntaxNode) => c.type === 'user_type'); + const ids = userType?.namedChildren.filter((c: SyntaxNode) => c.type === 'type_identifier') ?? []; + if (ids.length > 0) { + supertypes.push({ + name: ids.map((c: SyntaxNode) => getNodeText(c, this.source)).join('.'), + at: ids[0]!, + built: holder?.type === 'constructor_invocation', + }); + } + } + for (const spec of specs) walkHeader(spec); + // The class it builds (`object : Base(), Runnable`), else its first + // supertype, names the object and is what the code around instantiates — + // as written (`mockwebserver3.Dispatcher`, `Detector.Listener`), which + // its last name alone would leave to a namesake. + const primary = supertypes.find((s) => s.built) ?? supertypes[0]; + const shortName = primary ? primary.name.slice(primary.name.lastIndexOf('.') + 1) : 'Object'; + const ownerId = this.nodeStack[this.nodeStack.length - 1]; + if (primary && ownerId) { + this.unresolvedReferences.push({ + fromNodeId: ownerId, + referenceName: primary.name, + referenceKind: 'instantiates', + line: primary.at.startPosition.row + 1, + column: primary.at.startPosition.column, + }); + } + const classNode = this.createNode('class', `<${shortName}$anon@${node.startPosition.row + 1}>`, node, {}); + if (!classNode) { + for (const child of body.namedChildren) walkHeader(child); + return true; + } + for (const supertype of supertypes) { + this.unresolvedReferences.push({ + fromNodeId: classNode.id, + referenceName: supertype.name, + referenceKind: 'extends', + line: supertype.at.startPosition.row + 1, + column: supertype.at.startPosition.column, + }); + } + this.nodeStack.push(classNode.id); + for (const child of body.namedChildren) this.visitNode(child); + this.nodeStack.pop(); + return true; + } + /** * Scan `declNode` and its preceding siblings (within the parent's * named children) for decorator nodes, emitting a `decorates` @@ -7651,6 +7726,15 @@ export class TreeSitterExtractor { if (ownerId) this.extractVariableTypeAnnotation(node, ownerId); } + // A Kotlin object expression in a body or an initializer, `object : + // Listener { … }`: an anonymous class with its own members, as Java's + // `new Listener() { … }` (extractKotlinObjectLiteral). Its header + // (constructor arguments, a delegate) is still this body's code. + if (this.language === 'kotlin' && nodeType === 'object_literal' && + this.extractKotlinObjectLiteral(node, (header) => visitForCallsAndStructure(header, node))) { + return; + } + // Nested NAMED functions inside a body — function declarations and named // function expressions like `.on('mount', function onmount(){})` — become // their own nodes so the graph can link to them (callback handlers, local @@ -8108,11 +8192,14 @@ export class TreeSitterExtractor { const constructorInvocation = child.namedChildren.find((c: SyntaxNode) => c.type === 'constructor_invocation'); const target = userType ?? constructorInvocation; if (target) { - const typeId = target.type === 'user_type' - ? target.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier') ?? target - : target.namedChildren.find((c: SyntaxNode) => c.type === 'user_type')?.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier') - ?? target.namedChildren.find((c: SyntaxNode) => c.type === 'user_type') ?? target; - const name = getNodeText(typeId, this.source); + // `class K : Outer.Inner()` names the nested type by its whole + // path (`Outer.Inner`), type arguments left out — not `Outer`. + const typeNode = target.type === 'user_type' + ? target + : target.namedChildren.find((c: SyntaxNode) => c.type === 'user_type'); + const ids = typeNode?.namedChildren.filter((c: SyntaxNode) => c.type === 'type_identifier') ?? []; + const typeId = ids[0] ?? typeNode ?? target; + const name = ids.length > 0 ? ids.map((c: SyntaxNode) => getNodeText(c, this.source)).join('.') : getNodeText(typeId, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 19a07d257..464dbc0bf 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1631,6 +1631,30 @@ export function isLexicallyReachable( ref: UnresolvedRef, context: ResolutionContext ): boolean { + // An anonymous class's member — Java's `new Listener() { … }`, Kotlin's + // `object : Listener { … }` — is named only inside the declaration holding + // the class: a bare `block()` elsewhere is never the `override val block` of + // an object a private property holds. Its overrides are reached through the + // type it implements. + const anon = candidate.qualifiedName.includes('$anon@') ? ANON_MEMBER_QN.exec(candidate.qualifiedName) : null; + if (anon) { + if (ref.filePath !== candidate.filePath) return false; + // The holder is the declaration of that name around the class's own line + // (overloads share the name). + const at = Number(anon[2]); + let holders = context.getNodesByQualifiedName(anon[1]!) + .filter((n) => n.filePath === candidate.filePath && n.startLine <= at && (n.endLine ?? n.startLine) >= at); + // A Kotlin extension function is named by its receiver (`Call::asObservable`), + // not by the path its object's members carry (`pkg::asObservable`): then + // the innermost declaration around the class's line holds it. + if (holders.length === 0) { + const around = context.getNodesInFile(candidate.filePath).filter((n) => + ANON_HOLDER_KINDS.has(n.kind) && n.startLine <= at && (n.endLine ?? n.startLine) >= at && !n.qualifiedName.includes('$anon@')); + const innermost = around.sort((x, y) => y.startLine - x.startLine)[0]; + if (innermost) holders = [innermost]; + } + return holders.length === 0 || holders.some((n) => ref.line >= n.startLine && ref.line <= (n.endLine ?? n.startLine)); + } // An object literal's member is reached through its object (#2300): a bare // `load()` or `setTimeout(load)` never means `App.load`, whatever shares // the name — see isObjectMemberReachableByName for the ways that do. @@ -1664,6 +1688,28 @@ export function isLexicallyReachable( return scope === null || (ref.filePath === candidate.filePath && ref.line >= scope.start && ref.line <= scope.end); } +/** A qualified name through an anonymous class: the declaration holding it, then `::`. */ +const ANON_MEMBER_QN = /^(.+?)::<[^<>:]*\$anon@(\d+)>(?:::|$)/; +/** Declarations an anonymous class can be written in. */ +const ANON_HOLDER_KINDS = new Set(['function', 'method', 'constant', 'field', 'variable', 'property']); + +/** An anonymous class — Java's `new T() { … }`, Kotlin's `object : T { … }` — named ``. */ +function isAnonymousClass(n: Node): boolean { + return n.name.startsWith('<') && n.name.includes('$anon@'); +} + +/** A member of an anonymous class: its qualified name passes through ``. */ +function isAnonymousMember(n: Node): boolean { + return n.qualifiedName.includes('$anon@') && ANON_MEMBER_QN.test(n.qualifiedName); +} + +/** How many anonymous classes a qualified name passes through. */ +function anonymousDepth(qualifiedName: string): number { + let count = 0; + for (let i = qualifiedName.indexOf('$anon@'); i >= 0; i = qualifiedName.indexOf('$anon@', i + 6)) count++; + return count; +} + /** Per context: node id → the function body (or Scala block) a declaration is local to. */ const LOCAL_DECL_MEMO = new WeakMap>(); @@ -6908,6 +6954,12 @@ function lexicalKotlinMembers(candidates: Node[], ref: UnresolvedRef, context: R if (hierarchy.has('*')) return candidates; const lexical = candidates.filter((n) => { if (n.kind !== 'method') return false; + // An object expression's override is not what a call of its own name in + // it means — `original!!.contentLength()` in `override fun + // contentLength()`, `this@Outer.onScroll(…)`, an extension receiver's + // `onDataChanged(…)` — as when its overrides were local functions: the + // scopes around it decide. + if (n.id === ref.fromNodeId && isAnonymousMember(n)) return false; const path = n.qualifiedName.slice(0, Math.max(0, n.qualifiedName.lastIndexOf('::'))).split(/::|\./); let owner = path.pop() ?? ''; if (owner === 'Companion') owner = path.pop() ?? ''; @@ -13176,7 +13228,9 @@ export function matchByExactName( // the real `trait`, and as the sole candidate was adopted outright by the // single-match shortcut. Restricting the pool BEFORE ranking lets the // legitimate supertype win instead of merely dropping the false edge. - (!inheritanceRef || isSupertypeTarget(n)) && + // So does a Kotlin object expression's `instantiates` (Kotlin's only): + // okhttp's `object : CookieJar { … }` is no `OverrideParam.CookieJar` entry. + (!(inheritanceRef || (ref.language === 'kotlin' && ref.referenceKind === 'instantiates')) || isSupertypeTarget(n)) && // And a supertype or type a TS/JS module names without importing it is // its own or a global, never another module's — ahead of the ranking, so // a global `interface Window` in a `declare global` still wins. @@ -13300,6 +13354,71 @@ export function matchByExactName( return null; } +/** Languages whose supertypes are written through their enclosing type with a dot: `Outer.Inner`. */ +const DOTTED_SUPERTYPE_LANGUAGES: ReadonlySet = new Set(['java', 'kotlin', 'csharp']); +const DOTTED_SUPERTYPE_KINDS: ReadonlySet = new Set(['class', 'interface', 'struct', 'trait', 'enum']); + +/** + * A supertype written through its enclosing type — Java's `class Named extends + * Outer.Inner`, `implements Api.Listener` or `new Api.Listener() {…}`, Kotlin's + * `class K : Outer.Inner()` or `object : Api.Listener`, C#'s `class N : + * Outer.IListener` — or in full (`com.x.Outer.Inner`). The index names a nested + * type `p::Outer::Inner`, so the written path is the tail of its qualified + * name. A path that is not written in full must start at a type the file can + * name: in its own file, package or namespace, or one it imports (C#: one its + * usings reach). Of several, the one closest to the file in the tree, or none + * when two are as close. Undefined when no project type has the path (a + * library supertype), for the other strategies to look at. + */ +function matchDottedSupertype(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + // A Kotlin object expression's `instantiates` names its supertype as written too. + if (!DOTTED_SUPERTYPE_LANGUAGES.has(ref.language) || + !(isInheritanceRef(ref) || (ref.language === 'kotlin' && ref.referenceKind === 'instantiates'))) return undefined; + const written = ref.referenceName.replace(/<[^<>]*(?:<[^<>]*>[^<>]*)*>/g, '').replace(/\s+/g, ''); + if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(written)) return undefined; + const parts = written.split('.'); + const candidates = context.getNodesByName(parts[parts.length - 1]!).filter((n) => { + if (!DOTTED_SUPERTYPE_KINDS.has(n.kind) || !sameLanguageFamily(n.language, ref.language) || n.id === ref.fromNodeId) return false; + const dotted = n.qualifiedName.replace(/::/g, '.'); + return dotted === written || dotted.endsWith(`.${written}`); + }); + if (candidates.length === 0) return undefined; + const visible = candidates.filter((n) => { + if (n.qualifiedName.replace(/::/g, '.') === written || n.filePath === ref.filePath) return true; + // The type the written path starts at: `p::Outer` for `p::Outer::Inner`. + const segments = n.qualifiedName.split('::'); + const rootQn = segments.slice(0, segments.length - (parts.length - 1)).join('::'); + const cut = rootQn.indexOf('::'); + const pkg = cut < 0 ? '' : rootQn.slice(0, cut); + const rootFqn = rootQn.replace(/::/g, '.'); + if (ref.language === 'java') { + const scope = javaFileScope(ref.filePath, context); + return scope.pkg === pkg || scope.single.has(rootFqn) || scope.demand.has(pkg); + } + if (ref.language === 'kotlin') { + const scope = kotlinFileScope(ref.filePath, context); + return scope.pkg === pkg || scope.imports.has(rootFqn) || scope.stars.has(pkg); + } + const root = context.getNodesByQualifiedName(rootQn).find((r) => DOTTED_SUPERTYPE_KINDS.has(r.kind)); + return root !== undefined && isCsharpTypeVisible(root, { ...ref, referenceName: parts[0]! }, context); + }); + if (visible.length === 0) return null; + let chosen = visible[0]!; + if (visible.length > 1) { + const fromDirs = ref.filePath.split('/').slice(0, -1); + const closeness = (n: Node): number => { + const dirs = n.filePath.split('/').slice(0, -1); + let shared = 0; + while (shared < Math.min(dirs.length, fromDirs.length) && dirs[shared] === fromDirs[shared]) shared++; + return shared; + }; + const ranked = visible.map((n) => ({ n, c: closeness(n) })).sort((a, b) => b.c - a.c); + if (ranked[0]!.c === ranked[1]!.c) return null; + chosen = ranked[0]!.n; + } + return { original: ref, targetNodeId: chosen.id, confidence: 0.9, resolvedBy: 'qualified-name' }; +} + /** * Try to resolve by qualified name */ @@ -14298,6 +14417,40 @@ function esClassSupertypes(typeName: string, language: Language, context: Resolu return [...names]; } +/** + * The supertypes of the Java / Kotlin types named `typeName` (`LegacyBuilder`, + * or a chain, `AssistantMessage::Builder`), in the order the walk by name + * reads them, each with the chain of types that names it, its package + * dropped: a nested supertype (`extends AssistantMessage.Builder`) is + * looked up as `AssistantMessage::Builder` before every `Builder` in the + * project is. Null without the edge reader. + */ +function jvmSupertypeChains( + typeName: string, + language: Language, + context: ResolutionContext, +): Array<{ chain: string; name: string }> | null { + if (!context.getSupertypeNodes) return null; + const simple = typeName.split('::').pop()!; + const out: Array<{ chain: string; name: string }> = []; + const seen = new Set(); + for (const n of context.getNodesByName(simple)) { + if (n.language !== language || !JVM_DECLARED_TYPE_KINDS.has(n.kind)) continue; + if (typeName !== simple && n.qualifiedName !== typeName && !n.qualifiedName.endsWith(`::${typeName}`)) continue; + for (const s of context.getSupertypeNodes(n.id)) { + if (!s.name || s.name === simple) continue; + const segments = s.qualifiedName.split('::'); + let first = 0; + while (first < segments.length - 1 && !/^[A-Z]/.test(segments[first]!)) first++; + const chain = segments.slice(first).join('::'); + if (seen.has(chain)) continue; + seen.add(chain); + out.push({ chain, name: s.name }); + } + } + return out; +} + // Exported for the precedence unit tests (#1079): they assert the // preferredFqn → same-file → matches[0] ordering directly. export function resolveMethodOnType( @@ -14379,6 +14532,24 @@ export function resolveMethodOnType( const inherited = esInheritedMethod(typeName, methodName, preferredFqn ?? ref.filePath, ref, context); if (inherited) return { original: ref, targetNodeId: inherited.id, confidence, resolvedBy }; } + // A Java / Kotlin supertype is asked by the chain that names it first + // (`AssistantMessage::Builder`, not every `Builder`), then — when the + // edge reached a namesake, Minestom's `Player extends LivingEntity` to a + // nested `EntityStatuses.LivingEntity` — by its name, as before. + const jvm = !es && (ref.language === 'java' || ref.language === 'kotlin') + ? jvmSupertypeChains(typeName, ref.language, context) : null; + if (jvm) { + const tried = new Set(); + for (const { chain, name } of jvm) { + for (const t of chain === name ? [chain] : [chain, name]) { + if (tried.has(t)) continue; + tried.add(t); + const via = resolveMethodOnType(t, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1); + if (via) return via; + } + } + return null; + } const supertypes = es ? esClassSupertypes(typeName, ref.language, context) : context.getSupertypes!(typeName, ref.language); for (const supertype of supertypes) { const via = resolveMethodOnType( @@ -19208,16 +19379,38 @@ function inferJavaFieldReceiverType( } if (!enclosing) return null; + // A Kotlin object expression (`object : Callback { … }`) sees the scope it + // is written in: a property it does not declare itself is one of the class + // around it — okhttp's `server.url(…)` in an `object : Callback`'s + // `onResponse` is the test class's `MockWebServer`. + while (ref.language === 'kotlin' && isAnonymousClass(enclosing)) { + const anon: Node = enclosing; + const own = inFile.find((n) => (n.kind === 'field' || n.kind === 'constant' || n.kind === 'variable') && + n.qualifiedName === `${anon.qualifiedName}::${receiverName}`); + if (own) return inferKotlinPropertyType(receiverName, own, anon, inFile, ref, context); + let outer: Node | null = null; + for (const n of inFile) { + if ((n.kind !== 'class' && n.kind !== 'interface') || n.language !== ref.language) continue; + if (n.startLine < anon.startLine && (n.endLine ?? n.startLine) >= ref.line && + (!outer || n.startLine >= outer.startLine)) outer = n; + } + if (!outer) return null; + enclosing = outer; + } + const enclosingEnd = enclosing.endLine ?? enclosing.startLine; + const enclosingAnon = anonymousDepth(enclosing.qualifiedName); // Kotlin indexes a `val` property as a `constant`, a `var` as a `field`, and - // a companion-object property as a `variable`. + // a companion-object property as a `variable`. One of an object expression + // in the class's code is that object's. const field = inFile.find( (n) => (n.kind === 'field' || (ref.language === 'kotlin' && (n.kind === 'constant' || n.kind === 'variable'))) && n.name === receiverName && n.language === ref.language && n.startLine >= enclosing.startLine && - (n.endLine ?? n.startLine) <= enclosingEnd, + (n.endLine ?? n.startLine) <= enclosingEnd && + !(ref.language === 'kotlin' && anonymousDepth(n.qualifiedName) > enclosingAnon), ); // Kotlin keeps no signature on a property, and a primary-constructor // property (`class A(private val repo: Repo)`) is no node at all, so the @@ -19900,9 +20093,12 @@ function kotlinReceiverDeclaredType( const param = inferLocalReceiverType(name, ref, context); if (param) return /^[A-Z]/.test(param) ? param : null; if (localDeclared) return null; - // A property of a superclass: `viewModel` declared in `BaseActivity`. - const owner = kotlinEnclosingClass(ref, context); - if (!owner || kotlinOwnPropertyType(name, owner, ref, context) !== undefined) return null; + // A property of a superclass: `viewModel` declared in `BaseActivity`. In an + // object expression, of the class it is written in, unless the object + // declares the name itself. + const around = kotlinEnclosingClasses(ref, context); + const owner = around[around.length - 1]; + if (!owner || around.some((c) => kotlinOwnPropertyType(name, c, ref, context) !== undefined)) return null; return kotlinInheritedPropertyType(name, owner, ref, context); } @@ -19914,6 +20110,24 @@ function kotlinEnclosingClass(ref: UnresolvedRef, context: ResolutionContext): N .sort((a, b) => b.startLine - a.startLine)[0]; } +/** + * The Kotlin classes enclosing `ref`'s line, tightest first: each object + * expression (`object : Callback { … }`) the line is in, whose code sees the + * scope it is written in, through the first named class. + */ +function kotlinEnclosingClasses(ref: UnresolvedRef, context: ResolutionContext): Node[] { + const around = context.getNodesInFile(ref.filePath) + .filter((n) => (n.kind === 'class' || n.kind === 'interface') && n.language === 'kotlin' && + n.startLine <= ref.line && (n.endLine ?? n.startLine) >= ref.line) + .sort((a, b) => b.startLine - a.startLine); + const out: Node[] = []; + for (const c of around) { + out.push(c); + if (!isAnonymousClass(c)) break; + } + return out; +} + /** * The 0-based lines a Kotlin local visible at `ref` may be declared on, * nearest first: the enclosing function above the call, then — when that @@ -20589,7 +20803,19 @@ function inferMemberReceiverType(receiver: string, ref: UnresolvedRef, context: else if ((n.kind === 'method' || n.kind === 'function') && (!fn || n.startLine >= fn.startLine)) fn = n; } if (!cls) return null; - const found = memberTypeThroughHierarchy(cls, name, context); + let found = memberTypeThroughHierarchy(cls, name, context); + // A Kotlin object expression's code sees the class it is written in, as + // inferJavaFieldReceiverType walks; its `this` is the object. + for (let inner: Node = cls; !found && ref.language === 'kotlin' && isAnonymousClass(inner) && receiver === name;) { + let outer: Node | undefined; + for (const n of inFile) { + if (MEMBER_CLASS_KINDS.has(n.kind) && n.startLine < inner.startLine && n.endLine >= ref.line && + (!outer || n.startLine >= outer.startLine)) outer = n; + } + if (!outer) break; + found = memberTypeThroughHierarchy(outer, name, context); + inner = outer; + } if (!found) return null; // A lambda parameter, `var` local, `out` variable or `foreach` binding in // the calling method shadows the field, and the local inference that ran @@ -22745,7 +22971,10 @@ export function matchMethodCall( // only method of the name: `cache.aadd()` is `BaseCache`'s, while // `value.split()` is no test model's `split`. const reachable = targetMethods.filter((m) => isLexicallyReachable(m, ref, context)); - const scopedOut = reachable.length !== targetMethods.length; + // A Kotlin object expression's member out of reach leaves no likelier a + // guess than before it was one (its overrides were local functions, never + // candidates): okhttp's `it.isReady()` is still the SerialTask interface's. + const scopedOut = targetMethods.some((m) => !reachable.includes(m) && !(m.language === 'kotlin' && isAnonymousMember(m))); targetMethods = reachable; // A Vue component's own method is reached as `this.m()` inside it — // never as `e.preventDefault()` on an event, nor `this.editor.setValue()` @@ -29925,7 +30154,11 @@ function matchReferenceInner( result = nmTimed('filePath', ref, () => matchByFilePath(ref, context)); if (result) return result; - // 1. Qualified name match (highest confidence) + // 1. Qualified name match (highest confidence). A dotted Java, Kotlin or C# + // supertype (`extends Outer.Inner`) is a nested type's path: the type it + // names in the file's reach, or nothing. + const dottedSupertype = nmTimedT('dottedSupertype', ref, () => matchDottedSupertype(ref, context)); + if (dottedSupertype !== undefined) return dottedSupertype; result = nmTimed('qualifiedName', ref, () => matchByQualifiedName(ref, context)); if (result) return result;