diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts index 4d4a9fc..dd69c9b 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/dart-extractor.test.ts @@ -526,6 +526,109 @@ int caller() { tree.delete(); parser.delete(); }); + + it("records a `const Foo()` constructor as a call edge (Flutter widget shape)", () => { + const { tree, parser, root } = parse(`void main() { + runApp(const MyApp()); +} +`); + const entries = extractor.extractCallGraph(root); + + const callees = entries.map((e) => e.callee); + // Both the enclosing `runApp` call and the inner `MyApp` construction + // must surface — the latter is the dependency edge that motivates + // Flutter call-graph support. + expect(callees).toContain("runApp"); + expect(callees).toContain("MyApp"); + const myAppCall = entries.find((e) => e.callee === "MyApp"); + expect(myAppCall!.caller).toBe("main"); + tree.delete(); + parser.delete(); + }); + + it("records a `new Foo()` constructor as a call edge", () => { + const { tree, parser, root } = parse(`void main() { + var x = new Counter(1); +} +`); + const entries = extractor.extractCallGraph(root); + + const counterCall = entries.find((e) => e.callee === "Counter"); + expect(counterCall).toBeDefined(); + expect(counterCall!.caller).toBe("main"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a getter body to the getter", () => { + const { tree, parser, root } = parse(`class C { + int _v = 0; + int get value { + return helper(); + } +} +`); + const entries = extractor.extractCallGraph(root); + + const helperCall = entries.find((e) => e.callee === "helper"); + expect(helperCall).toBeDefined(); + expect(helperCall!.caller).toBe("value"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a setter body to the setter", () => { + const { tree, parser, root } = parse(`class C { + int _v = 0; + set value(int x) { + _v = clamp(x); + } +} +`); + const entries = extractor.extractCallGraph(root); + + const clampCall = entries.find((e) => e.callee === "clamp"); + expect(clampCall).toBeDefined(); + expect(clampCall!.caller).toBe("value"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a constructor body to the constructor", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo(this.x) { + validate(x); + } +} +`); + const entries = extractor.extractCallGraph(root); + + const validateCall = entries.find((e) => e.callee === "validate"); + expect(validateCall).toBeDefined(); + expect(validateCall!.caller).toBe("Foo"); + tree.delete(); + parser.delete(); + }); + + it("attributes calls inside a factory constructor body to `Class.named`", () => { + const { tree, parser, root } = parse(`class Foo { + int x; + Foo(this.x); + factory Foo.fromString(String s) { + return Foo(int.parse(s)); + } +} +`); + const entries = extractor.extractCallGraph(root); + + // Either the bare `Foo(...)` call inside the factory or the chained + // `int.parse(...)` must be attributed to the factory's qualified name. + const fromFactory = entries.filter((e) => e.caller === "Foo.fromString"); + expect(fromFactory.length).toBeGreaterThan(0); + tree.delete(); + parser.delete(); + }); }); describe("extractStructure - visibility", () => { diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts index ebe666f..5605e40 100644 --- a/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts +++ b/understand-anything-plugin/packages/core/src/plugins/extractors/dart-extractor.ts @@ -587,6 +587,29 @@ export class DartExtractor implements LanguageExtractor { }); } } + + // Constructor-call shapes that bypass the `selector > argument_part` + // pattern: + // const Foo(...) → `const_object_expression { const_builtin, type_identifier, arguments }` + // new Foo(...) → `new_expression { (unnamed `new`), type_identifier, arguments }` + // Both are extremely common in Flutter widget trees; without this branch + // the construction edge would be silently dropped. The callee is the + // `type_identifier` child. + if ( + (node.type === "const_object_expression" || + node.type === "new_expression") && + functionStack.length > 0 + ) { + const typeNode = findChild(node, "type_identifier"); + if (typeNode) { + entries.push({ + caller: functionStack[functionStack.length - 1], + callee: typeNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + walkSiblings(node); }; @@ -606,9 +629,29 @@ export class DartExtractor implements LanguageExtractor { // Recurse into signature (no calls expected, but stay complete). walkSiblings(child); } else if (child.type === "method_signature") { - // method_signature wraps function_signature; sibling function_body follows. - const inner = findChild(child, "function_signature"); - if (inner) pendingName = extractFunctionName(inner); + // method_signature wraps one of: + // function_signature → normal method + // getter_signature → getter (with body) + // setter_signature → setter (with body) + // constructor_signature → constructor (with body) + // factory_constructor_signature → factory (with body) + // All five carry the name as their first `identifier` child (factory + // ctors carry two — class + named — handled by `constructorName`). + // Without this dispatch, ctor/factory/getter/setter bodies were + // walked with an empty functionStack and their internal calls were + // dropped from the graph. + const fn = + findChild(child, "function_signature") ?? + findChild(child, "getter_signature") ?? + findChild(child, "setter_signature"); + if (fn) { + pendingName = extractFunctionName(fn); + } else { + const ctor = + findChild(child, "constructor_signature") ?? + findChild(child, "factory_constructor_signature"); + if (ctor) pendingName = constructorName(ctor); + } walkSiblings(child); } else if (child.type === "function_body") { // Consume pendingName: push for the duration of this body.