fix(java): resolve chained static-factory calls Foo.getInstance().bar() (#750) (#751)

A Java method called through a static factory or fluent chain — `Foo.getInstance().bar()`,
`Config.create(opts).build()` — lost the receiver's type, so the chained method either
didn't resolve at all or (when a same-named method existed on an unrelated class) attached
to whichever class was indexed first. Ports the #645 (C++) / #608 (PHP) 3-part mechanism:

- Part 1: capture Java return types in the extractor (skip void/primitives/arrays,
  unwrap generics, strip package qualifier).
- Part 2: encode a chained-call receiver as `inner().method` with normalized empty
  parens, so factory calls that take arguments still split.
- Part 3: matchJavaCallChain resolves the chained method on the factory's return type,
  validated via resolveMethodOnType so a wrong inference yields NO edge (never a wrong one).

Validated: synthetic decoy + absent-method safety tests; real-repo A/B on google/guava
(3,227 files) — node count identical (no explosion), 0 edges lost, +1,507 unique chained
edges recovered, precision spot-checked verbatim (Splitter.on().split(),
CacheBuilder.newBuilder().recordStats(), GraphBuilder.directed().build(), nested
MultimapBuilder.linkedHashKeys().arrayListValues()). EXTRACTION_VERSION 5 -> 6.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-08 23:43:17 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent eb5960b535
commit 7f6bdf7ad1
6 changed files with 173 additions and 1 deletions
+41
View File
@@ -576,6 +576,39 @@ export function matchPhpCallChain(
return resolveMethodOnType(resolvedClass, method, ref, context, 0.85, 'instance-method');
}
/**
* Resolve a Java chained call whose receiver is a static factory / fluent call —
* `Foo.getInstance().bar()`, encoded by the extractor as `Foo.getInstance().bar`
* (#645/#608 mechanism). The receiver's type is what `Foo.getInstance` returns
* (its declared return type); the outer method is then resolved and VALIDATED on
* it (resolveMethodOnType requires `Type::method` to exist), so a wrong inference
* yields no edge rather than a wrong one (e.g. a same-named `bar()` on an
* unrelated class is never matched).
*/
export function matchJavaCallChain(
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null {
const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/);
if (!m || !m[1] || !m[2]) return null;
const inner = m[1]; // `Foo.getInstance`
const method = m[2]; // `bar`
// Require an explicit receiver (`Receiver.factory`) — a bare `factory().bar`
// chain (a method on `this`) isn't handled here.
const lastDot = inner.lastIndexOf('.');
if (lastDot <= 0) return null;
const factoryClass = inner.slice(0, lastDot).split('.').pop(); // simple class name
const factoryMethod = inner.slice(lastDot + 1);
if (!factoryClass || !factoryMethod) return null;
const ret = lookupCalleeReturnType(`${factoryClass}::${factoryMethod}`, ref, context);
if (!ret) return null;
// When several classes share the returned simple name, the caller file's
// import of that type is the only signal that names WHICH one (#314).
const imports = context.getImportMappings(ref.filePath, ref.language);
const importedFqn = imports.find((i) => i.localName === ret)?.source;
return resolveMethodOnType(ret, method, ref, context, 0.85, 'instance-method', importedFqn);
}
/**
* Java/Kotlin: infer a receiver's declared type by walking field declarations
* in the class enclosing the call site. The field's `signature` is already in
@@ -1006,6 +1039,14 @@ export function matchReference(
if (result) return result;
}
// 1d. Java chained static-factory / fluent call — `Foo.getInstance().bar()`
// encoded as `Foo.getInstance().bar` (#645/#608 mechanism). Resolve bar's class
// from getInstance's declared return type, then validate the method on it.
if (ref.language === 'java') {
result = matchJavaCallChain(ref, context);
if (result) return result;
}
// 2. Method call pattern
result = matchMethodCall(ref, context);
if (result) return result;