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
+68
View File
@@ -2195,4 +2195,72 @@ void wrong() { WidgetFactory::create().onlyOther(); }
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
});
});
describe('Java chained static-factory call resolution (#645/#608 mechanism)', () => {
function callerNamesOf(qualifiedName: string): string[] {
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
if (!target) return [];
const names = cg
.getIncomingEdges(target.id)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.source)?.name)
.filter((n): n is string => !!n);
return [...new Set(names)].sort();
}
it('resolves Foo.getInstance().bar() via the factory return type, never a same-named decoy', async () => {
// Aaa sorts first and has a same-named bar() — it must never win the chain.
fs.writeFileSync(
path.join(tempDir, 'Main.java'),
`class Aaa { void bar() {} }
class Foo {
static Foo getInstance() { return new Foo(); }
void bar() {}
}
class Caller {
void run() { Foo.getInstance().bar(); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Foo::bar')).toEqual(['run']);
expect(callerNamesOf('Aaa::bar')).toEqual([]);
});
it('resolves a factory chain that passes arguments — Foo.create(cfg).build()', async () => {
// The factory call carries an argument; the extractor must normalize the
// receiver to empty parens (`Foo.create().build`) so the chain still splits.
fs.writeFileSync(
path.join(tempDir, 'Main.java'),
`class Config {}
class Foo {
static Foo create(Config c) { return new Foo(); }
void build() {}
}
class Caller {
void run() { Foo.create(new Config()).build(); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Foo::build')).toEqual(['run']);
});
it('creates NO edge when the factory return type lacks the method (silent miss, not a wrong edge)', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.java'),
`class Foo {
static Foo getInstance() { return new Foo(); }
}
class Other { void onlyOther() {} }
class Caller {
void run() { Foo.getInstance().onlyOther(); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
// Foo has no onlyOther() — must not mis-attach to the same-named Other::onlyOther.
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
});
});
});