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
+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