mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
fix(core): DartExtractor — call graph coverage for codex P2 findings
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.
This commit is contained in:
+103
@@ -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", () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user