fix(java): a call resolves on its receiver's declared type, and a library type gets no guess

Java typed a call receiver from a local declaration or a plain field of
the enclosing class only. A field of an outer class read inside an inner
or anonymous class (mHandler.post(...) inside new Runnable() {...}),
Outer.this.f, a chain of fields (this.mOwner.mRepo.save()) and a local
whose type has type arguments fell through to the name-only strategies,
and so did every receiver whose declared type is a library class:
parcel.readInt() on an android.os.Parcel went to a project
VersionedParcel.readInt, list.size() to any project size().

The declared type is now read for each of those shapes, a chain one hop
at a time with each hop's own imports, keeping a written package or
outer class (Map.Entry, play.api.mvc.BodyParser). A receiver whose
declared type the project does not declare, or that an import or a
written package pins outside the project, gets no edge. When no import
decides between same-named types, a Java call prefers the Java
declaration over a Scala or Kotlin one.

Also, from review: a field the class around the call declares is read
as written before the member reader, which keeps only the last name
(PauseTest.Suite, a field written with its package); a local or loop
variable keeps the package its declaration writes; a field typed by a
type parameter goes to its bound, and a class the class head names as
a type argument (Comparable<ApplicationModule>) is not taken for one; a
nested type a superclass declares, or one an on-demand import of its
class names (import play.mvc.Http.*), counts as named; and a typed miss
on a type no project class or interface is named (a URI, an enum's
name()) gets no guess, as on main.

Validation:
- java-receiver-type.test.ts: 19 tests, the PR's 5 red on main; the
  other 14 pass on main, and our 8 each fail without their change.
- A/B against main 25776014 (with #2942 and #2950; the same sites
  change against 7cceba52), sites added/removed/retargeted: jextract
  +2/-1/106, spring-modulith +1/-3/2, Minestom +52/-91/71, SystemUI
  +53/-319/96, spring-ai +69/-659/532, kestra-java +5/-504/162,
  playframework +0/-196/599; click and gin identical. Of the removed
  edges main had at 0.9, 1,004 were wrong (an import oracle plus 16
  read by hand) and 4 right (a lambda parameter, two nested types of a
  Scala object, a field read under a static import of a same-named
  member). Retargets land on the declared type or the supertype
  declaring the method, except about 16 build()/clone() calls on
  spring-ai's nested option builders, which move from one wrong target
  to another.
- Cost (3 interleaved rounds): spring-ai wall 27.9 s on main vs
  25.9 s, CPU 90.2 s vs 88.1 s. SystemUI read 71.3 vs 79.4 s wall
  under other jobs' load; CPU profiles of both arms put the new code at
  about 0.6 s of 115 s.

Original PR #1942 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-28 01:30:24 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 72f398af6e
commit 7973df0d09
3 changed files with 889 additions and 19 deletions
+1
View File
@@ -251,6 +251,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Java and TypeScript, a class that implements or extends a parameterized type, like `implements Store<String>`, `extends Base<Integer>`, an interface's `extends Store<T>` or TypeScript's `implements Store<string>`, is now linked to that type, so its methods get their interface-to-implementation links and `codegraph_explore` follows a call on the interface into the class. Before, such a supertype was read with its type arguments and matched nothing. Re-index Java and TypeScript projects after upgrading. Thanks @danusha2345, and @paiyahooJay for the earlier pull request. (#2705)
- In Java and C#, a method called on a newly constructed object, like `new SqlSessionFactoryBuilder().build(reader)` or `new JsonSerializer().Serialize(writer, value)`, now links to that class's own or inherited method. When several packages or namespaces declare a class of that name, it links to the one the file imports, and to none when the file could mean either. Re-index Java and C# projects after upgrading.
- In Java, a `record` is now indexed as a class, with its methods, its constructors, a field for each component and the accessor each component gets, like `info.remoteAddress()`. Before, a record had no symbol of its own and its methods were filed under the package or the class around it, so a call on a record-typed value linked to nothing or to the wrong method, and records were missing from callers, impact and implementations. A call like `Circle.unit()` on a type that a sealed interface's `permits` list names first now links too. Re-index Java projects after upgrading. Thanks @danusha2345.
- In Java, a call now resolves on the type its receiver is declared with in more cases: a field of an outer class read inside an inner or anonymous class, `Outer.this.field`, a chain of fields like `this.owner.repo.save()`, a local declared with type arguments, a for-each variable, a variable declared with its package, like `org.app.Message message`, and a nested class that a parent class declares. When that type comes from a library, like Android's `Parcel`, `Handler` or `Notification.Builder`, or `java.util.List`, the call no longer links to a same-named method of your own. A type the file imports from a library (`import java.util.Iterator`) or as a nested class (`import p.Http.Cookie`) is no longer mistaken for a same-named type elsewhere in the project, and a Java type wins over a same-named Scala one. Re-index Java projects after upgrading. Thanks @danusha2345.
- In Kotlin, a call on what one of your functions returns, like `repository().pending(1)` or `val repo = repository(); repo.pending(1)` with a `private fun repository(): RealRepo` in a test class, now links to the method of the type that function declares, including one the type inherits or an extension on it. Before, the chained call went to whichever `pending` the name matched first, like a test double's in another package, and the local was guessed from its name, which reached the interface. The function is found in the calling class, a class it inherits from or one around it, or at the top level, and a nested type like `Request.Builder` is that type, not another class's `Builder`. When the declared type has no such method, the call now links to nothing rather than a method of the same name elsewhere; a type from outside your project is matched by name as before. Re-index Kotlin projects after upgrading. Thanks @mindcipherrr for the report. (#2805)
- 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)
+523
View File
@@ -0,0 +1,523 @@
/**
* A Java call through a field or a local resolves on the receiver's declared
* type, and a receiver of a library type gets no edge.
*
* The field lookup only read the tightest enclosing class and only a plain
* name, so `this.mRepo.save()`, `Outer.this.mRepo.save()`, a field read from
* inside an anonymous class and a field chain `mOwner.repo.save()` were all
* left to name-only guessing. And when the declared type WAS known but was a
* library class (`Parcel`, `Handler`, `List`), the call fell through to the
* same guessing, which bound it to whichever project class declares a method
* of that name (`parcel.readInt()` → a project `VersionedParcel.readInt`).
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
describe('Java receivers resolve on the declared type', () => {
let dir: string;
let cg: CodeGraph;
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-java-recv-'));
const src = path.join(dir, 'src', 'p');
fs.mkdirSync(src, { recursive: true });
fs.writeFileSync(path.join(src, 'Repo.java'), `package p;
public class Repo {
public void save() {}
public void close() {}
}
`);
fs.writeFileSync(path.join(src, 'Owner.java'), `package p;
public class Owner {
public Repo repo = new Repo();
public android.content.Context ctx;
}
`);
fs.writeFileSync(path.join(src, 'Hook.java'), `package p;
public abstract class Hook {
public abstract void fire();
}
`);
fs.writeFileSync(path.join(src, 'Box.java'), `package p;
public class Box<T> {
public void seal() {}
}
`);
// Same-named methods on unrelated project classes: what a name-only
// guess lands on. The decoy sits in the callers' own file below too.
fs.writeFileSync(path.join(src, 'VersionedParcel.java'), `package p;
public class VersionedParcel {
public int readInt() { return 0; }
public void writeInt(int v) {}
}
`);
// Project types nested in other packages that share a simple name with
// a JDK type the callers use.
fs.mkdirSync(path.join(dir, 'src', 'q'));
fs.writeFileSync(path.join(dir, 'src', 'q', 'Bytes.java'), `package q;
public class Bytes {
public static class Iterator { public boolean hasNext() { return false; } }
}
`);
fs.writeFileSync(path.join(dir, 'src', 'q', 'Table.java'), `package q;
public class Table {
public static class Entry { public Object getKey() { return null; } }
}
`);
// A Scala type of the same simple name as a Java one, reached from a
// Java file through a wildcard import.
fs.writeFileSync(path.join(src, 'Form.java'), `package p;
public class Form<T> {
public T get() { return null; }
}
`);
fs.mkdirSync(path.join(dir, 'src', 'a'));
fs.writeFileSync(path.join(dir, 'src', 'a', 'Form.scala'), `package api
class Form(value: Int) {
def get(): Int = value
}
`);
// A Scala trait in a file not named after it, imported explicitly.
fs.mkdirSync(path.join(dir, 'src', 'api', 'mvc'), { recursive: true });
fs.writeFileSync(path.join(dir, 'src', 'api', 'mvc', 'BodyParsers.scala'), `package api.mvc
trait PlayBodyParsers {
def raw(): Int
}
`);
// Same simple names in another package, indexed first: what an unpinned
// lookup would land on.
fs.mkdirSync(path.join(dir, 'src', 'b'));
fs.writeFileSync(path.join(dir, 'src', 'b', 'Cookie.java'), `package b;
public class Cookie { public String value() { return null; } }
`);
fs.writeFileSync(path.join(dir, 'src', 'b', 'PlayBodyParsers.java'), `package b;
public class PlayBodyParsers { public int raw() { return 0; } }
`);
fs.writeFileSync(path.join(src, 'Http.java'), `package p;
public class Http {
public static class Cookie { public String value() { return null; } }
}
`);
fs.mkdirSync(path.join(dir, 'src', 'r'));
fs.writeFileSync(path.join(dir, 'src', 'r', 'Controller.java'), `package r;
import api.mvc.PlayBodyParsers;
import p.*;
import p.Http.Cookie;
public class Controller {
void submit(Form<String> form) { form.get(); }
void parse(PlayBodyParsers parsers) { parsers.raw(); }
void written(api.mvc.PlayBodyParsers other) { other.raw(); }
void writtenLocal() {
api.mvc.PlayBodyParsers local = null;
local.raw();
}
void cookie(Cookie crumb) { crumb.value(); }
}
`);
fs.writeFileSync(path.join(src, 'Screen.java'), `package p;
import android.os.Handler;
import android.os.Parcel;
import android.util.Log;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import q.Bytes;
class Decoy {
void save() {}
void post(Runnable r) {}
void add(Object o) {}
void put(Object k, Object v) {}
void d(String t, String m) {}
void getResources() {}
void seal() {}
}
// Decoys whose names share words with the receivers, so a receiver-name
// guess prefers them over the declared type.
class StoreCache { void save() {} void close() {} }
class SharedCache { void close() {} }
class OwnerRepoCache { void save() {} }
class BoxCache { void seal() {} }
public class Screen {
private Repo mStore;
private Handler mHandler;
private Owner mOwner;
private static final Repo sShared = new Repo();
void viaField() { mStore.save(); }
void viaThis() { this.mStore.save(); }
void viaConstant() { sShared.close(); }
void viaChain() { mOwner.repo.save(); }
void viaThisChain() { this.mOwner.repo.save(); }
void viaLocalChain(Owner owner) { owner.repo.save(); }
void libraryField() { mHandler.post(null); }
void libraryChain() { mOwner.ctx.getResources(); }
void libraryParam(Parcel parcel) { parcel.readInt(); parcel.writeInt(1); }
void libraryGenericLocal() {
List<String> items = null;
items.add("x");
Map<String, List<Repo>> byName = null;
byName.put("k", null);
}
void libraryForEach(List<Parcel> parcels) {
for (Parcel p : parcels) { p.readInt(); }
}
void projectGenericLocal() {
Box<Repo> crate = make();
crate.seal();
}
void iterate(List<Repo> list) {
Iterator<Repo> it = list.iterator();
it.hasNext();
}
void entries(Map<String, String> map) {
for (Map.Entry<String, String> e : map.entrySet()) { e.getKey(); }
}
void nested(Bytes.Iterator bytes) { bytes.hasNext(); }
static Box<Repo> make() { return null; }
void staticLibrary() { Log.d("t", "m"); }
Runnable anonymous() {
return new Runnable() {
public void run() { mStore.save(); }
};
}
class Inner {
private Box<Repo> mStore;
void viaOuter() { Screen.this.mStore.close(); }
}
}
class Holder<T extends Hook> {
T hook;
void go() { hook.fire(); }
}
`);
cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
});
afterAll(() => {
cg?.destroy();
fs.rmSync(dir, { recursive: true, force: true });
});
/** Qualified names of what `owner.method` calls (`owner` may name an anonymous class's supertype). */
function callees(owner: string, method: string): string[] {
const node = cg.searchNodes(method).map((r) => r.node)
.find((n) => n.qualifiedName.endsWith(`${owner}::${method}`) || (n.qualifiedName.includes(`${owner}$anon`) && n.qualifiedName.endsWith(`::${method}`)));
expect(node, `${owner}::${method}`).toBeDefined();
return cg.getCallees(node!.id)
.filter((c) => c.edge.kind === 'calls')
.map((c) => c.node.qualifiedName)
.sort();
}
it('a field, `this.field` and a static final field resolve on the declared type', () => {
expect(callees('Screen', 'viaField')).toEqual(['p::Repo::save']);
expect(callees('Screen', 'viaThis')).toEqual(['p::Repo::save']);
expect(callees('Screen', 'viaConstant')).toEqual(['p::Repo::close']);
});
it('an anonymous class reads its outer class field, and `Outer.this.field` names the class', () => {
expect(callees('Runnable', 'run')).toEqual(['p::Repo::save']);
expect(callees('Inner', 'viaOuter')).toEqual(['p::Repo::close']);
});
it('a chain of fields resolves through each declared type', () => {
expect(callees('Screen', 'viaChain')).toEqual(['p::Repo::save']);
expect(callees('Screen', 'viaThisChain')).toEqual(['p::Repo::save']);
expect(callees('Screen', 'viaLocalChain')).toEqual(['p::Repo::save']);
});
it('a field, a chain hop, a parameter or a local of a library type gets no edge', () => {
expect(callees('Screen', 'libraryField')).toEqual([]);
expect(callees('Screen', 'libraryChain')).toEqual([]);
expect(callees('Screen', 'libraryParam')).toEqual([]);
expect(callees('Screen', 'libraryGenericLocal')).toEqual([]);
expect(callees('Screen', 'libraryForEach')).toEqual([]);
// A static call on an imported library class (`Log.d` beside a project `Decoy.d`).
expect(callees('Screen', 'staticLibrary')).toEqual([]);
});
it('a JDK type is not read as a same-named type nested in another package', () => {
// An explicit `import java.util.Iterator`, and a qualified `Map.Entry`.
expect(callees('Screen', 'iterate')).toEqual([]);
expect(callees('Screen', 'entries')).toEqual([]);
});
it('a nested project type written with its outer class resolves on it', () => {
expect(callees('Screen', 'nested')).toEqual(['q::Bytes::Iterator::hasNext']);
});
it('a generic local of a project type resolves on that type', () => {
expect(callees('Screen', 'projectGenericLocal')).toEqual(['p::Box::seal', 'p::Screen::make']);
});
it('a Java receiver type prefers the Java declaration over a same-named Scala one', () => {
expect(callees('Controller', 'submit')).toEqual(['p::Form::get']);
});
it('an explicitly imported Scala type is a project type wherever its file is named', () => {
expect(callees('Controller', 'parse')).toEqual(['PlayBodyParsers::raw']);
expect(callees('Controller', 'written')).toEqual(['PlayBodyParsers::raw']);
expect(callees('Controller', 'writtenLocal')).toEqual(['PlayBodyParsers::raw']);
});
it('an imported nested type is the one the import names', () => {
expect(callees('Controller', 'cookie')).toEqual(['p::Http::Cookie::value']);
});
it('a type-parameter receiver is not treated as a library type', () => {
expect(callees('Holder', 'go')).toEqual(['p::Hook::fire']);
});
});
describe('Java receivers keep the type their declaration names', () => {
let dir: string;
let cg: CodeGraph;
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-java-field-written-'));
const write = (rel: string, body: string) => {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), body);
};
write('src/a/Pause.java', `package a;
public class Pause {
public static class Suite {
public void runDelay(String id) {}
}
}
`);
write('src/a/http/Request.java', `package a.http;
public class Request {
public String getBody() { return ""; }
}
`);
// kestra's AbstractRunnerTest: `protected PauseTest.Suite pauseTest;` under
// `import io.kestra.plugin.core.flow.*;`, and RequestTest's field written
// with the project package beside an import of a library `HttpRequest`.
write('src/b/Runner.java', `package b;
import a.*;
import io.lib.Request;
public class Runner {
protected Pause.Suite pause;
private a.http.Request request;
public void delay() {
pause.runDelay("x");
}
public void body() {
request.getBody();
}
// spring-ai's \`org.springframework.ai.chat.messages.Message message\` beside
// an import of the Anthropic SDK's \`Message\`: a local or loop variable
// written with its package.
public void localBody(java.util.List<a.http.Request> all) {
a.http.Request first = all.get(0);
first.getBody();
}
public void eachBody(java.util.List<a.http.Request> all) {
for (a.http.Request each : all) {
each.getBody();
}
}
}
`);
// SystemUI's QSTileImpl<TState extends State>: a field typed by the class's
// type parameter, beside another class's same-named method.
write('src/a/State.java', `package a;
public class State {
public void copyTo(State other) {}
}
`);
write('src/a/Other.java', `package a;
public class Other {
public void copyTo(Other other) {}
}
`);
write('src/b/Tile.java', `package b;
import a.State;
public abstract class Tile<TState extends State> {
private TState mTmpState;
public void refresh() {
mTmpState.copyTo(null);
}
}
`);
// SystemUI's HeadsUpManagerPhone: `AlertEntry`, nested in a class two
// levels up its hierarchy, named without an import.
write('src/a/Alerts.java', `package a;
public abstract class Alerts {
public static class Entry {
public void update() {}
}
}
`);
write('src/a/HeadsUp.java', `package a;
public abstract class HeadsUp extends Alerts {}
`);
write('src/c/Store.java', `package c;
public class Store {
public void update() {}
}
`);
write('src/b/Phone.java', `package b;
import a.HeadsUp;
public class Phone extends HeadsUp {
public void refresh(java.util.List<Entry> entries) {
for (Entry entry : entries) {
entry.update();
}
}
}
`);
// kestra: a `java.net.URI` (short enough to read as a type parameter) and
// a project enum, whose `name()` is Java's, beside project methods the
// receivers' names match.
write('src/a/FlowWithPath.java', `package a;
public class FlowWithPath {
public String getPath() { return ""; }
}
`);
write('src/a/Type.java', `package a;
public class Type {
public String name() { return ""; }
}
`);
write('src/a/ServiceType.java', `package a;
public enum ServiceType { WORKER, EXECUTOR }
`);
write('src/b/Storage.java', `package b;
import java.net.URI;
import a.ServiceType;
public class Storage {
private URI base;
private ServiceType kind;
public void local(URI path, ServiceType type) {
path.getPath();
type.name();
}
public void field() {
base.getPath();
kind.name();
}
}
`);
// playframework's javaguide: `import play.mvc.Http.*;` names Http's
// nested `RequestBody`.
write('src/a/Web.java', `package a;
public class Web {
public static class Body {
public String asText() { return ""; }
}
}
`);
write('src/b/Parsers.java', `package b;
import a.Web.*;
public class Parsers {
public void read(Body body) {
body.asText();
}
}
`);
// spring-modulith's ApplicationModule: `Comparable<ApplicationModule>` in
// the class head names the class, not a type parameter, and a nested
// class's field of that type types the call on a local named like it.
write('src/a/AppModule.java', `package a;
public class AppModule implements Comparable<AppModule> {
public java.util.List<String> getNames() { return null; }
public int compareTo(AppModule other) { return 0; }
public static class Link {
private final AppModule target;
Link(AppModule target) { this.target = target; }
static Link of(AppModule source) {
var target = source.find();
var names = target.getNames();
return null;
}
}
public AppModule find() { return this; }
}
`);
write('src/c/Other.java', `package c;
public class Other {
public java.util.List<String> getNames() { return null; }
}
`);
cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
});
afterAll(() => {
cg?.destroy();
fs.rmSync(dir, { recursive: true, force: true });
});
function callees(owner: string, method: string): string[] {
const node = cg.searchNodes(method).map((r) => r.node).find((n) => n.qualifiedName.endsWith(`${owner}::${method}`));
expect(node, `${owner}::${method}`).toBeDefined();
return cg.getCallees(node!.id).filter((c) => c.edge.kind === 'calls').map((c) => c.node.qualifiedName).sort();
}
it('a field typed with its outer class, through an on-demand import, resolves on the nested type', () => {
expect(callees('Runner', 'delay')).toEqual(['a::Pause::Suite::runDelay']);
});
it('a field written with its package resolves on that type, whatever the file imports by the name', () => {
expect(callees('Runner', 'body')).toEqual(['a.http::Request::getBody']);
});
it('a local or loop variable written with its package resolves on that type too', () => {
expect(callees('Runner', 'localBody')).toEqual(['a.http::Request::getBody']);
expect(callees('Runner', 'eachBody')).toEqual(['a.http::Request::getBody']);
});
it('a field typed by the class type parameter resolves on its bound', () => {
expect(callees('Tile', 'refresh')).toEqual(['a::State::copyTo']);
});
it('a nested type a superclass declares is named without an import', () => {
expect(callees('Phone', 'refresh')).toEqual(['a::Alerts::Entry::update']);
});
it('a library type with a short name, or a project enum lacking the method, gets no guess', () => {
expect(callees('Storage', 'local')).toEqual([]);
expect(callees('Storage', 'field')).toEqual([]);
});
it('an on-demand import of a class names its nested types', () => {
expect(callees('Parsers', 'read')).toEqual(['a::Web::Body::asText']);
});
it('a class the head names as a type argument is no type parameter', () => {
expect(callees('Link', 'of')).toEqual(['a::AppModule::find', 'a::AppModule::getNames']);
});
});
+365 -19
View File
@@ -13588,10 +13588,12 @@ export function resolveMethodOnType(
// intentionally in ANOTHER file (#314) — is unaffected: those blocks return
// early whenever an import pins the class.
if (ref.referenceKind === 'function_ref' && matches.length !== 1) return null;
// A Ruby receiver is typed only by `x = Widget.new`, so it is an instance:
// `x.build` reaches `def build`, not `def self.build`, wherever each is.
// Java: a same-named type in another JVM language (Scala's `play.api.data.Form`
// beside Java's `play.data.Form`) is the weaker candidate when no import
// pins one — a wildcard `import play.data.*` or the same package names none.
const sameLanguage = ref.language === 'java' ? matches.filter((m) => m.language === 'java') : [];
const ordered = rubyMethodsOfKindFirst(
preferCallSiteFile(withinCppTranslationUnit(cppVisibleFirst(matches, ref, context), ref, context), ref.filePath),
preferCallSiteFile(withinCppTranslationUnit(cppVisibleFirst(sameLanguage.length > 0 ? sameLanguage : matches, ref, context), ref, context), ref.filePath),
ref.language === 'ruby' ? 'instance' : undefined);
return {
original: ref,
@@ -18322,6 +18324,7 @@ function inferJavaFieldReceiverType(
): string | null {
const inFile = context.getNodesInFile(ref.filePath);
if (inFile.length === 0) return null;
if (ref.language === 'java') return javaEnclosingFieldType(receiverName, ref, inFile);
// Find the class enclosing the call line (tightest match by latest start).
let enclosing: Node | null = null;
@@ -18344,16 +18347,21 @@ function inferJavaFieldReceiverType(
n.startLine >= enclosing.startLine &&
(n.endLine ?? n.startLine) <= enclosingEnd,
);
if (!field || !field.signature) return null;
return field ? fieldSignatureType(field) : null;
}
// Signature shape: "<TypeName> <fieldName>" (extractField). Pull the type,
// strip generics + dotted package, drop array/varargs markers.
const beforeName = field.signature.slice(
0,
field.signature.lastIndexOf(field.name),
);
const typeRaw = beforeName.trim();
/**
* The declared type of a Java/Kotlin field node, read from its signature
* ("<TypeName> <fieldName>", set by tree-sitter.ts extractField): generics,
* a dotted package and array/varargs markers stripped — for Java, in
* javaTypeName's form, which keeps a written package and outer class. Null
* for a primitive or a field without a signature.
*/
function fieldSignatureType(field: Node): string | null {
if (!field.signature) return null;
const typeRaw = field.signature.slice(0, field.signature.lastIndexOf(field.name)).trim();
if (!typeRaw) return null;
if (field.language === 'java') return javaTypeName(typeRaw);
const typeNoGenerics = typeRaw.replace(/<[^>]*>/g, '').trim();
const typeNoArray = typeNoGenerics.replace(/\[\s*\]/g, '').replace(/\.\.\.$/, '').trim();
@@ -18364,6 +18372,301 @@ function inferJavaFieldReceiverType(
return lastPart;
}
/**
* A Java type as written (`Foo`, `java.util.List<Foo>`, `Map.Entry<K, V>`,
* `play.api.mvc.BodyParser<B>`, `Foo[]`), type arguments and array markers
* dropped. The class chain is joined with `::` (`Map::Entry`), so a nested
* type is never taken for a same-named type nested elsewhere, and a package
* written in source stays in front of it, dotted (`play.api.mvc.BodyParser`),
* for javaTypeRef to pin the declaration with. Null for a primitive.
*/
function javaTypeName(raw: string): string | null {
let type = raw;
for (let prev = ''; prev !== type; ) {
prev = type;
type = type.replace(/<[^<>]*>/g, '');
}
type = type.replace(/\[\s*\]/g, '').replace(/\.\.\.$/, '').replace(/\s+/g, '');
const segments = type.split('.').filter(Boolean);
let first = segments.length;
while (first > 0 && /^[A-Z][\w$]*$/.test(segments[first - 1]!)) first--;
const chain = segments.slice(first).join('::');
if (!chain || NON_TYPE_RECEIVER_TOKENS.has(chain)) return null;
const pkg = segments.slice(0, first).join('.');
return pkg ? `${pkg}.${chain}` : chain;
}
/**
* Java: the declared type of the field a receiver names — `mHandler`,
* `this.mHandler`, or `Outer.this.mHandler`. The field is looked up in the
* class enclosing the call and then in each class around it, tightest first,
* because an inner or anonymous class reads its outer classes' fields
* (`mHandler.post(...)` inside a `new Runnable() {…}`); `Outer.this.f` names
* the class to read. A `static final` field is indexed as a `constant`.
*/
function javaEnclosingFieldType(receiverName: string, ref: UnresolvedRef, inFile: Node[]): string | null {
const m = receiverName.match(/^(?:(?:(\w+)\.)?this\.)?(\w+)$/);
if (!m) return null;
const [, outer, fieldName] = m;
const enclosing = inFile
.filter(
(n) =>
(n.kind === 'class' || n.kind === 'interface' || n.kind === 'enum') &&
n.language === 'java' &&
n.startLine <= ref.line &&
(n.endLine ?? n.startLine) >= ref.line,
)
.sort((a, b) => b.startLine - a.startLine);
for (const cls of enclosing) {
if (outer && cls.name !== outer) continue;
const field = inFile.find(
(n) =>
(n.kind === 'field' || n.kind === 'constant') &&
n.name === fieldName &&
n.qualifiedName === `${cls.qualifiedName}::${fieldName}`,
);
if (field) return fieldSignatureType(field);
if (outer) return null;
}
return null;
}
/**
* Stands for "a type the project does not declare" — a library class, or a
* field reached through one — so the caller can refuse a name-only guess.
*/
const JAVA_EXTERNAL_TYPE = '\u0000external';
const JVM_DECLARED_TYPE_KINDS = new Set<Node['kind']>(['class', 'interface', 'enum', 'struct', 'trait']);
/** JVM type declarations named `name`. */
function jvmTypeDecls(name: string, context: ResolutionContext): Node[] {
return context
.getNodesByName(name)
.filter((n) => JVM_DECLARED_TYPE_KINDS.has(n.kind) && sameLanguageFamily(n.language, 'java'));
}
/**
* Whether `decl` is the type one of `fqns` (an import, or a package written
* in source) names: the Java declaration of that qualified name or file, or
* a Scala / Kotlin one in that package's directory (their qualified names
* carry no package, and their files need not be named after the type).
*/
function declaresJvmFqn(decl: Node, fqns: string[]): boolean {
const dotted = decl.qualifiedName.replace(/::/g, '.');
const declPath = decl.filePath.replace(/\\/g, '/');
const declFile = declPath.replace(/\.\w+$/, '');
const declDir = declPath.slice(0, declPath.lastIndexOf('/') + 1);
return fqns.some((fqn) => {
const fqnPath = fqn.replace(/\./g, '/');
if (dotted === fqn || declFile === fqnPath || declFile.endsWith('/' + fqnPath)) return true;
const pkgPath = fqnPath.slice(0, fqnPath.lastIndexOf('/') + 1);
return decl.language !== 'java' && pkgPath !== '' && (declDir === pkgPath || declDir.endsWith('/' + pkgPath));
});
}
/**
* Whether Java file `filePath` can name `decl` by its simple name without
* importing it — for a type read from a declaration (a field's, a local's),
* where isJavaTypeVisible judges a name written at a reference. A Java
* qualified name reads `package::Outer::Inner`; a nested type is named that
* way only in its own file, in its own package (as `Outer.Inner`), or where
* its top-level class is imported. A top-level type of another package is
* also reachable through a wildcard import, which the import mappings do not
* list, so it counts as visible, and so does a Scala or Kotlin declaration,
* whose qualified name carries no package to check. A class also names the
* nested types of the classes it extends or implements (SystemUI's
* `AlertEntry` in a subclass of `AlertingNotificationManager`).
*/
function isJavaTypeNameable(decl: Node, filePath: string, context: ResolutionContext): boolean {
if (decl.language !== 'java' || decl.filePath === filePath) return true;
const segments = decl.qualifiedName.split('::');
const hasPackage = segments.length > 1 && /^[a-z]/.test(segments[0]!);
const classChain = hasPackage ? segments.slice(1) : segments;
if (classChain.length <= 1) return true;
const pkg = hasPackage ? segments[0]! : '';
if (pkg === javaFileScope(filePath, context).pkg) return true;
const outer = pkg ? `${pkg}.${classChain[0]}` : classChain[0]!;
if (context.getImportMappings(filePath, 'java').some((i) => i.source === outer || i.source.startsWith(outer + '.'))) return true;
// `import play.mvc.Http.*;` names Http's nested types.
if (javaFileScope(filePath, context).demand.has(segments.slice(0, -1).join('.'))) return true;
const ancestors = javaFileAncestors(filePath, context);
return context.getNodesByQualifiedName(segments.slice(0, -1).join('::')).some((o) => ancestors.has(o.id));
}
/** Per-context memo: Java file → ids of the classes its classes extend or implement. */
const JAVA_FILE_ANCESTORS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
/** The Java classes and interfaces the classes of `filePath` extend or implement, up to five levels up. */
function javaFileAncestors(filePath: string, context: ResolutionContext): Set<string> {
let memo = JAVA_FILE_ANCESTORS.get(context);
if (!memo) JAVA_FILE_ANCESTORS.set(context, (memo = new Map()));
const hit = memo.get(filePath);
if (hit) return hit;
const classes = context.getNodesInFile(filePath).filter((n) => n.language === 'java' && isMethodOwnerKind(n));
const seen = new Set<string>(classes.map((c) => c.id));
const ancestors = new Set<string>();
let frontier = classes;
for (let depth = 0; depth < 5 && frontier.length > 0; depth++) {
const next: Node[] = [];
for (const cls of frontier) {
for (const sup of declaredSupertypes(cls, context)) {
for (const decl of context.getNodesByName(sup)) {
if (decl.language !== 'java' || !isMethodOwnerKind(decl) || seen.has(decl.id)) continue;
seen.add(decl.id);
ancestors.add(decl.id);
next.push(decl);
}
}
}
frontier = next;
}
memo.set(filePath, ancestors);
return ancestors;
}
/** A Java receiver type the project declares. */
type JavaTypeRef = {
/** The class chain resolveMethodOnType matches (`Http::Cookie`). */
name: string;
/** The declaration an import or a written package pins, when one does. */
decl?: Node;
};
/**
* What a Java type written in `filePath` (javaTypeName's form) is: a project
* type, or null for a library one (`Parcel`, `Context`, `List`), which none
* of the project's methods belongs to. It is a library type when the project
* declares no JVM type of its outer name; when an import, or a package
* written in source, names a type the project does not declare (`import
* java.util.Iterator` beside a project `UByteArray.Iterator`); when every
* project type of that name is one the file cannot name that way (a nested
* type of another package); or when a project class declares no nested type
* of the written name (`MyLayout.LayoutParams`, inherited from a library
* class). A short all-caps name (`T`, `VH`) is taken for a type parameter,
* whose bound may be a project type, and left alone.
*/
function javaTypeRef(written: string, filePath: string, context: ResolutionContext): JavaTypeRef | null {
const chainStart = written.indexOf('::');
const head = chainStart < 0 ? written : written.slice(0, chainStart);
const lastDot = head.lastIndexOf('.');
const pkg = lastDot < 0 ? '' : head.slice(0, lastDot);
const chain = written.slice(lastDot + 1).split('::');
const outer = chain[0]!;
if (!pkg && chain.length === 1 && /^[A-Z][A-Z0-9]{0,2}$/.test(outer)) return { name: outer };
const decls = jvmTypeDecls(outer, context);
if (decls.length === 0) return null;
const pins = pkg
? [`${pkg}.${outer}`]
: context.getImportMappings(filePath, 'java').filter((i) => i.localName === outer).map((i) => i.source);
if (pins.length === 0) {
if (!decls.some((d) => isJavaTypeNameable(d, filePath, context))) return null;
if (chain.length === 1) return { name: outer };
const name = chain.join('::');
const nested = jvmTypeDecls(chain[chain.length - 1]!, context).some(
(n) => n.qualifiedName === name || n.qualifiedName.endsWith(`::${name}`),
);
return nested ? { name } : null;
}
let decl = decls.find((d) => declaresJvmFqn(d, pins));
for (const segment of chain.slice(1)) {
if (!decl) break;
const owner: Node = decl;
decl = jvmTypeDecls(segment, context).find((n) => n.qualifiedName === `${owner.qualifiedName}::${segment}`);
}
if (!decl) return null;
const segments = decl.qualifiedName.split('::');
const hasPackage = decl.language === 'java' && segments.length > 1 && /^[a-z]/.test(segments[0]!);
return { name: (hasPackage ? segments.slice(1) : segments).join('::'), decl };
}
/**
* Resolve a method on a Java receiver type: the pinned declaration's own
* method first, then resolveMethodOnType on the class chain, then — for a
* nested chain, which the supertype walk there cannot look up — the
* supertypes of its simple name. A Scala or Kotlin declaration pinned by an
* import is not followed further than its own methods: the class chain alone
* matches a same-named Java type as well.
*/
function resolveOnJavaType(
type: JavaTypeRef,
methodName: string,
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null {
if (type.decl) {
const decl = type.decl;
const own = context
.getNodesByName(methodName)
.find((n) => n.kind === 'method' && n.qualifiedName === `${decl.qualifiedName}::${methodName}`);
if (own) return { original: ref, targetNodeId: own.id, confidence: 0.9, resolvedBy: 'instance-method' };
if (decl.language !== 'java') return null;
}
const fqn = type.decl ? type.decl.qualifiedName.replace(/::/g, '.') : undefined;
const direct = resolveMethodOnType(type.name, methodName, ref, context, 0.9, 'instance-method', fqn);
if (direct || !type.name.includes('::') || !context.getSupertypes) return direct;
for (const supertype of context.getSupertypes(type.name.split('::').pop()!, 'java')) {
const via = resolveMethodOnType(supertype, methodName, ref, context, 0.9, 'instance-method', undefined, 1);
if (via) return via;
}
return null;
}
/**
* Java: the declared type of a receiver the field lookup reaches — a field
* (`mRepo`, `this.mRepo`, `Outer.this.mRepo`) or a chain of up to three
* fields after it (`mState.menu`, `this.mOwner.mRepo`), whose head may also
* be a local. Each hop reads the field declared on the previous hop's type,
* and reads that field's type with its own file's imports.
* JAVA_EXTERNAL_TYPE as soon as a hop's type is a library one; null when a
* hop cannot be typed (an inherited field, an untyped local).
*/
function javaReceiverDeclaredType(
receiver: string,
ref: UnresolvedRef,
context: ResolutionContext,
): JavaTypeRef | typeof JAVA_EXTERNAL_TYPE | null {
const m = receiver.match(/^((?:\w+\.)?this\.\w+|[A-Za-z_$][\w$]*)((?:\.[a-zA-Z_$][\w$]*){0,3})$/);
if (!m) return null;
const [, head, rest] = m;
const segments = rest ? rest.slice(1).split('.').filter(Boolean) : [];
const headType =
// A plain receiver's local declaration was already read by the caller.
(segments.length > 0 && !head!.includes('.')
? inferLocalReceiverType(head!, ref, context)
: null) ?? inferJavaFieldReceiverType(head!, ref, context);
if (!headType) return null;
// A type parameter of the class or method around the call (`TState
// mTmpState` in `QSTileImpl<TState extends State>`) is no library type: its
// bound decides, which matchMethodCall reads when this answers null. As
// there, only a name no project class has: typeParameterBound also reads a
// type argument (`Comparable<ApplicationModule>`) as a parameter.
if (/^[A-Z][\w$]*$/.test(headType) && !context.getNodesByName(headType).some(isMethodOwnerKind) &&
typeParameterBound(headType, ref, context) !== undefined) return null;
let written = headType;
let filePath = ref.filePath;
for (const segment of segments) {
const type = javaTypeRef(written, filePath, context);
if (!type) return JAVA_EXTERNAL_TYPE;
const owner = type.decl?.qualifiedName;
const field = context
.getNodesByName(segment)
.find(
(n) =>
(n.kind === 'field' || n.kind === 'constant') &&
n.language === 'java' &&
(owner
? n.qualifiedName === `${owner}::${segment}`
: n.qualifiedName.endsWith(`::${type.name}::${segment}`) || n.qualifiedName === `${type.name}::${segment}`),
);
const fieldType = field ? fieldSignatureType(field) : null;
if (!fieldType) return null;
written = fieldType;
filePath = field!.filePath;
}
return javaTypeRef(written, filePath, context) ?? JAVA_EXTERNAL_TYPE;
}
// ── Local-variable receiver-type inference (#1108) ──────────────────────────
//
// Instance calls through a local variable (`const lg = new Logger(); lg.log()`)
@@ -18548,6 +18851,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
CPP_CALLERS_MEMBERS.delete(context);
SOLIDITY_SUPERS.delete(context);
DECLARED_SUPERS.delete(context);
JAVA_FILE_ANCESTORS.delete(context);
INHERITED_METHODS.delete(context);
MEMBER_SHADOWS.delete(context);
MEMBER_WALKS.delete(context);
@@ -18709,8 +19013,10 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[]
case 'java':
return [
new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`), // = new Logger()
new RegExp(`\\b([A-Z][\\w.]*)\\s*${TYPE_ARGS}\\s+${r}\\b\\s*[=;,)]`), // Logger lg; / Pair<L, R> pair / String[] args
new RegExp(`\\bfor\\s*\\(\\s*(?:final\\s+)?([A-Z][\\w.]*)\\s*${TYPE_ARGS}\\s+${r}\\s*:`), // for (Element el : els)
// A package written before the type is kept (`org.app.Message message`):
// it, not a same-named import, says which type the local is.
new RegExp(`\\b((?:[a-z_$][\\w$]*\\.)*[A-Z][\\w.]*)\\s*${TYPE_ARGS}\\s+${r}\\b\\s*[=;,)]`), // Logger lg; / Pair<L, R> pair / String[] args
new RegExp(`\\bfor\\s*\\(\\s*(?:final\\s+)?((?:[a-z_$][\\w$]*\\.)*[A-Z][\\w.]*)\\s*${TYPE_ARGS}\\s+${r}\\s*:`), // for (Element el : els)
];
case 'kotlin':
return [
@@ -19388,7 +19694,7 @@ export function inferLocalReceiverType(
for (const re of patterns) {
const m = line.match(re);
if (m && m[1]) {
const type = normalizeInferredTypeName(m[1]);
const type = ref.language === 'java' ? javaTypeName(m[1]) : normalizeInferredTypeName(m[1]);
if (type) {
lastRaw = m[1];
return type;
@@ -20279,6 +20585,9 @@ export function matchMethodCall(
// dedicated inferrer (header scan + `auto`); every other language uses the
// shared source-based inferrer. resolveMethodOnType validates the method
// exists on the inferred type, so a mis-inference produces no edge.
let localInferred = false;
// Java: what javaReceiverDeclaredType read for the receiver, once it ran.
let javaDeclared: ReturnType<typeof javaReceiverDeclaredType> | undefined;
if (inferableReceiver) {
// A VB.NET receiver's declared type decides the call, or that there is no
// project method to call: SCrawler's `ThumbnailFile.Delete(…)` on an
@@ -20378,7 +20687,12 @@ export function matchMethodCall(
const from = goLocalDecl(objectOrClass!, ref, context)?.from;
if (from && isGoOutsidePackage(from, ref.filePath, context)) return null;
}
if (!inferredType && MEMBER_TYPED_LANGUAGES.has(ref.language) && dotMatch) {
// A Java field the class around the call declares is read below as its
// declaration writes it, with the package or outer class the member
// reader drops (`PauseTest.Suite`, `io.kestra.core.http.HttpRequest`);
// the member reader still types a field the class inherits.
if (!inferredType && MEMBER_TYPED_LANGUAGES.has(ref.language) && dotMatch &&
(ref.language !== 'java' || (javaDeclared = javaReceiverDeclaredType(objectOrClass!, ref, context)) === null)) {
inferredType = nmTimedT('mc-member', ref, () => inferMemberReceiverType(objectOrClass!, ref, context));
// A field of a built-in type (`string _name`, `int count`) has no project method.
if (inferredType && /^[a-z]/.test(inferredType)) return null;
@@ -20452,7 +20766,23 @@ export function matchMethodCall(
if (ref.language === 'rust' && inferredType === 'Pin' && !isRustProjectNamed('Pin', RUST_DECLARED_TYPE_KINDS, context)) {
return matchRustPinnedCall(objectOrClass!, methodName!, ref, context);
}
if (inferredType) {
if (inferredType && ref.language === 'java') {
localInferred = true;
// Java: a local of a library type (`Parcel parcel`, `List<Foo> items`)
// has none of the project's methods — neither a same-named nested
// project type's (`Iterator<T> it` beside `UByteArray.Iterator`) nor
// whatever the name-only strategies below would bind the call to.
const type = javaTypeRef(inferredType, ref.filePath, context);
if (!type) return null;
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveOnJavaType(type, methodName!, ref, context));
if (typedMatch) return typedMatch;
// As for every other language below: a type no project class or
// interface is named — a library `URI` the short-name rule took for a
// type parameter, an enum whose `name()` / `equals()` are Java's — is
// past guessing (kestra's `URI file; file.toString()`).
if (isUndeclaredTypeName(inferredType.split(/[.:]+/).pop()!, ref, context)) return null;
} else if (inferredType) {
localInferred = true;
// A Go local bound from a type assertion calls a method of the type Go
// finds for it, or nothing the project declares: `flusher.Flush()` after
// `flusher, ok := w.(http.Flusher)` went to a project type named after it.
@@ -20669,12 +20999,28 @@ export function matchMethodCall(
return guess && read.size > 0 ? { ...guess, metadata: { ...guess.metadata, typeFrom: [...read].sort() } } : guess;
}
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
// Java: a field receiver — `mRepo`, `this.mRepo`, `Outer.this.mRepo`, or a
// chain of fields `mState.menu` — resolves on its declared type, and a
// library type (`Handler`, `Context`) gets NO edge rather than the
// name-only guesses below.
if (ref.language === 'java' && dotMatch && !localInferred) {
const declared = javaDeclared !== undefined ? javaDeclared : nmTimedT('mc-java-declared', ref, () =>
javaReceiverDeclaredType(objectOrClass!, ref, context));
if (declared === JAVA_EXTERNAL_TYPE) return null;
if (declared) {
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveOnJavaType(declared, methodName!, ref, context));
if (typedMatch) return typedMatch;
if (isUndeclaredTypeName(declared.name.split('::').pop()!, ref, context)) return null;
}
}
// Kotlin (Java's fields are read in the branch above): receiver may be a
// field whose name doesn't match the type by Java naming convention
// (`userbo` → class `UserBO`, abbreviated). Look up
// the field in the enclosing class to get its declared type, then resolve
// the method on that type. Covers Spring `@Resource`/`@Autowired` field
// injection where the field type is the concrete bean class.
if ((ref.language === 'java' || ref.language === 'kotlin') && dotMatch) {
if (ref.language === 'kotlin' && dotMatch) {
const inferredType = inferJavaFieldReceiverType(objectOrClass!, ref, context);
if (inferredType) {
// When two classes share the same simple name, the caller file's