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
+27
View File
@@ -2376,6 +2376,33 @@ export class TreeSitterExtractor {
return;
}
// Java static-factory / fluent chain: `Foo.getInstance().bar()` — the
// receiver is itself a method call, so resolution must infer bar's class
// from what `Foo.getInstance` RETURNS (its declared return type), the
// #645/#608 mechanism. Encode `<inner-receiver>.<inner-method>().<method>`;
// the `().` marker lets the Java chain resolver split it, and normalizing to
// empty parens drops any factory args (`Foo.create(cfg).bar()`) that would
// otherwise leave a `(cfg)` in the receiver text and break the split.
if (
methodName &&
this.language === 'java' &&
objectField.type === 'method_invocation'
) {
const innerObj = getChildByField(objectField, 'object');
const innerName = getChildByField(objectField, 'name');
if (innerObj && innerName) {
calleeName = `${getNodeText(innerObj, this.source)}.${getNodeText(innerName, this.source)}().${methodName}`;
this.unresolvedReferences.push({
fromNodeId: callerId,
referenceName: calleeName,
referenceKind: 'calls',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
return;
}
}
let receiverName: string;
if (objectField.type === 'field_access') {
const inner = getChildByField(objectField, 'object');