Ports the #645/#608 chained-receiver mechanism to Dart, plus makes Dart factory and named constructors first-class so their chains can resolve at all. A call whose receiver is itself a call — `Foo.create().bar()` (static factory or factory/named constructor) — used to drop the receiver to a bare `bar`, which name-matched a same-named method on an unrelated type (commonly a stdlib `Option`/`Iterator` `.map`/`.where` mis-tied to the project's own class). - dart.ts: extractBareCall now re-encodes `Foo.create().bar` when the chain starts with a capitalized type; getReturnType captures the return type (generic `List<Foo>` → `List`); factory (`factory Foo.create()`) and named (`Foo._()`) constructors are indexed as `Foo::create` / `Foo::_` with return type = the class (via resolveName + getReturnType + constructor_signature in methodTypes). - The UNNAMED ctor `Foo()` is deliberately NOT extracted (isMisparsedFunction), so plain construction stays an `instantiates` edge to the class rather than a call to a phantom `Foo::Foo` method. - dartCtorInfo validates a "constructor" against the enclosing class name, so a method tree-sitter MISPARSES as a constructor — `@override (A, B) m()`, where the annotation swallows the record return type and `m()` looks like a one-id constructor_signature — is still extracted as the method it is (regression found on localsend; covered by a new test). - name-matcher.ts / index.ts: `dart` joins the dotted-chain gate, CONSTRUCTS_VIA_BARE_CALL (case construction), and CHAIN_LANGUAGES (conformance for superclass/mixin methods). resolveMethodOnType validates, so a wrong inference yields no edge. Validation: 7 synthetic tests (static factory, factory/named ctor, construction, conformance, absent-method safety, the misparse regression, instantiation-not- hijacked). Real-repo A/B on localsend (368 Dart files): hand-written +17/-10 — all corrections (the -10 = 7 wrong stdlib/extension misattributions removed + 3 ctor source-renames), plus additive factory/named-ctor call resolution. Instantiation preserved; no node explosion. EXTRACTION_VERSION 13->14. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2f96f58cbb
commit
16c73e2b0e
@@ -2826,4 +2826,172 @@ object Main {
|
||||
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dart chained static-factory / factory-constructor 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 static-factory chain Foo.makeBar().doIt() to the return type, never a same-named decoy', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.dart'),
|
||||
`class Foo {
|
||||
static Bar makeBar() => Bar();
|
||||
}
|
||||
class Bar {
|
||||
void doIt() {}
|
||||
}
|
||||
class Decoy {
|
||||
void doIt() {}
|
||||
}
|
||||
void run() {
|
||||
Foo.makeBar().doIt();
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Bar::doIt')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::doIt')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a named factory-constructor chain Foo.create().ship() on the constructed class', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.dart'),
|
||||
`class Foo {
|
||||
Foo._();
|
||||
factory Foo.create() => Foo._();
|
||||
void ship() {}
|
||||
}
|
||||
class Decoy {
|
||||
void ship() {}
|
||||
}
|
||||
void run() {
|
||||
Foo.create().ship();
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// The factory constructor `Foo.create` is now a node whose return type is Foo,
|
||||
// so `ship` resolves on Foo, not the same-named Decoy.
|
||||
expect(callerNamesOf('Foo::ship')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::ship')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a constructor-receiver chain Bar().doIt() on the constructed class', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.dart'),
|
||||
`class Bar {
|
||||
void doIt() {}
|
||||
}
|
||||
class Decoy {
|
||||
void doIt() {}
|
||||
}
|
||||
void run() {
|
||||
Bar().doIt();
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Bar::doIt')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::doIt')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a chained method inherited from a superclass the return type extends (via conformance)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.dart'),
|
||||
`class Base {
|
||||
void render() {}
|
||||
}
|
||||
class Widget extends Base {
|
||||
static Widget make() => Widget();
|
||||
}
|
||||
class Decoy {
|
||||
void render() {}
|
||||
}
|
||||
void run() {
|
||||
Widget.make().render();
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Base::render')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::render')).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.dart'),
|
||||
`class Foo {
|
||||
static Bar makeBar() => Bar();
|
||||
}
|
||||
class Bar {
|
||||
}
|
||||
class Other {
|
||||
void onlyOther() {}
|
||||
}
|
||||
void run() {
|
||||
Foo.makeBar().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([]);
|
||||
});
|
||||
|
||||
it('still extracts a method tree-sitter misparses as a constructor (@override + record return)', async () => {
|
||||
// tree-sitter-dart misparses `@override (A, B) reduce()` — the annotation
|
||||
// swallows the record return type, so `reduce()` looks like a single-
|
||||
// identifier constructor_signature. It must NOT be skipped as an unnamed
|
||||
// ctor (its name doesn't match the class); its body call must attribute to
|
||||
// `reduce`, not the class.
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.dart'),
|
||||
`class Base {}
|
||||
class Action extends Base {
|
||||
Action({required int x});
|
||||
@override
|
||||
(int, String) reduce() {
|
||||
return (compute(), "y");
|
||||
}
|
||||
int compute() => 1;
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// reduce must be a node and its body call must resolve to Action::compute.
|
||||
expect(callerNamesOf('Action::compute')).toEqual(['reduce']);
|
||||
});
|
||||
|
||||
it('keeps plain construction Foo() as instantiation, not a Foo::Foo method call', async () => {
|
||||
// The unnamed constructor is intentionally NOT extracted as a `Foo::Foo`
|
||||
// method, so `Foo(...)` resolves to the class (an `instantiates` edge),
|
||||
// never hijacked into a call to a phantom constructor method.
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.dart'),
|
||||
`class Widget {
|
||||
final int x;
|
||||
Widget(this.x);
|
||||
}
|
||||
void run() {
|
||||
Widget(3);
|
||||
}
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// No Foo::Foo phantom method node.
|
||||
expect(cg.getNodesByKind('method').some((n) => n.qualifiedName === 'Widget::Widget')).toBe(false);
|
||||
// The construction resolves to the class as an `instantiates` edge.
|
||||
const widget = cg.getNodesByKind('class').find((n) => n.name === 'Widget')!;
|
||||
const incoming = cg.getIncomingEdges(widget.id);
|
||||
expect(incoming.some((e) => e.kind === 'instantiates')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user