From a555f4dd2a9c436b336a10f96c1453e431ec681d Mon Sep 17 00:00:00 2001 From: thejesh Date: Mon, 15 Jun 2026 02:50:06 -0700 Subject: [PATCH] =?UTF-8?q?fix(core):=20DartExtractor=20=E2=80=94=20call?= =?UTF-8?q?=20graph=20coverage=20for=20codex=20P2=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the call-graph walker, both flagged by codex on #435: 1. `const Foo(...)` / `new Foo(...)` constructor calls were silently dropped. The grammar emits these as `const_object_expression` / `new_expression` containing `arguments` directly — they bypass the `selector > argument_part` shape the walker relied on. Added a dedicated branch that records the inner `type_identifier` as the callee. Critical for Flutter widget trees where `runApp(const MyApp())` would otherwise lose the MyApp construction edge. 2. When a getter / setter / constructor / factory_constructor has a body, its `method_signature` wraps `getter_signature` / `setter_signature` / `constructor_signature` / `factory_constructor_signature` instead of `function_signature`. The walker only looked for `function_signature`, so `pendingName` stayed null and the sibling `function_body` was walked with an empty stack — calls inside ctor/factory/getter/setter bodies were silently dropped even though those members were already extracted as functions. Now dispatch across all five signature variants, using `constructorName` for the (factory) constructor pair to match what `collectClassBody` pushes. Tests: 41 → 47 dart cases (+6); full core 733 → 739; no regressions. --- .../__tests__/dart-extractor.test.ts | 103 ++++++++++++++++++ .../src/plugins/extractors/dart-extractor.ts | 49 ++++++++- 2 files changed, 149 insertions(+), 3 deletions(-) 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.