fix(scala): resolve chained static-factory/apply calls Foo.create().bar() (#750) (#761)

Ports the #645 (C++) / #608 (PHP) chained-receiver mechanism to Scala. A call
whose receiver is itself a call — `Foo.create().bar()` (companion factory),
`Builder(cfg).bar()` (case-class apply), or a fluent chain — used to drop the
receiver to a bare `bar`, which name-matched a same-named method on an unrelated
type. The most common wrong edge was a stdlib `Option`/`Iterator` `.map`/`.flatMap`/
`.foreach` mis-attributed onto the project's own same-named class.

- scala.ts: `getReturnType` reads the `return_type` field — generic `List[Foo]`
  → container `List`, qualified `pkg.Foo` → `Foo`, `this.type` left undefined.
- tree-sitter.ts: re-encode `Foo.create().bar` when the inner call's receiver chain
  starts with a capital (companion factory / case-class apply); instance chains
  (`list.map().filter()`) stay bare.
- name-matcher.ts: `scala` joins the dotted-chain gate + CONSTRUCTS_VIA_BARE_CALL
  (case-class `apply` constructs the class); resolveMethodOnType validates, so a
  non-conventional `apply` returning another type yields no edge, not a wrong one.
- index.ts: `scala` joins CHAIN_LANGUAGES so trait-inherited methods resolve via
  the conformance second pass.

Validation: 4 synthetic tests (factory+decoy, case-class apply, trait conformance,
absent-method safety). Real-repo A/B on gatling (750 Scala files): +14 / -59 unique
edges — all corrections. The +14 are retargets (e.g. `HttpProtocolBuilder(cfg).baseUrl`
now resolves to HttpProtocolBuilder::baseUrl, not the same-named private BaseUrlSupport
helper); the -59 are wrong edges removed (stdlib Option/Iterator monad calls
mis-tied to the project's Validation::*, self-loops, decoy collisions) — zero genuine
factory chains dropped (verified: gatling has no real Validation.success().map() chains).
db stable at 40 MB. EXTRACTION_VERSION 12→13. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-09 12:09:33 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent ccced9e358
commit 2f96f58cbb
7 changed files with 142 additions and 9 deletions
+98
View File
@@ -2728,4 +2728,102 @@ func caller() { engine().ServeHTTP() }
expect(rawCalls.length).toBeLessThan(5);
});
});
describe('Scala 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 a companion-factory chain Foo.create().doIt() to the return type, never a same-named decoy', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.scala'),
`object Foo {
def create(): Bar = new Bar()
}
class Bar {
def doIt(): Unit = {}
}
class Decoy {
def doIt(): Unit = {}
}
object Main {
def run(): Unit = { Foo.create().doIt() }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Bar::doIt')).toEqual(['run']);
expect(callerNamesOf('Decoy::doIt')).toEqual([]);
});
it('resolves a case-class apply construction Point(x).dist() on the constructed class', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.scala'),
`class Point(x: Int) {
def dist(): Int = x
}
class Other {
def dist(): Int = 0
}
object Main {
def run(): Unit = { Point(3).dist() }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Point::dist')).toEqual(['run']);
expect(callerNamesOf('Other::dist')).toEqual([]);
});
it('resolves a chained method provided by a trait the return type extends (via conformance)', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.scala'),
`trait Base {
def shared(): Unit = {}
}
class Widget extends Base
class Decoy {
def shared(): Unit = {}
}
object Factory {
def make(): Widget = new Widget()
}
object Main {
def run(): Unit = { Factory.make().shared() }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(callerNamesOf('Base::shared')).toEqual(['run']);
expect(callerNamesOf('Decoy::shared')).toEqual([]);
});
it('creates NO edge when neither the factory return type nor a supertype has the method (silent miss)', async () => {
fs.writeFileSync(
path.join(tempDir, 'Main.scala'),
`object Foo {
def create(): Bar = new Bar()
}
class Bar {
}
class Other {
def onlyOther(): Unit = {}
}
object Main {
def run(): Unit = { Foo.create().onlyOther() }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
// Bar has no onlyOther() — must not mis-attach to the same-named Other::onlyOther.
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
});
});
});