mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
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:
committed by
Colby Mchenry
co-authored by
Claude Opus 5.5
parent
72f398af6e
commit
7973df0d09
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user