fix(dart): emit intra-file calls so .dart files are not stars - #2771
fix(dart): emit intra-file calls so .dart files are not stars#2771Saifitdin wants to merge 1 commit into
Conversation
The Dart extractor only ever linked members back to the file node, so every .dart file came out as a star: N members, N-1 `defines` edges, nothing between them. On a 32-file Flutter app that meant 0 `calls` edges from .dart against 167 from .ts in the same repo, and Louvain had no signal to cluster on beyond inheritance from framework base classes. The knock-on effect is that cohesion scores for Flutter communities sit at the mathematical floor for a tree (2(n-1)/n(n-1)), so GRAPH_REPORT.md reports every Flutter community as weakly interconnected and suggests splitting it - advice driven by the extractor, not by the code. Section 8 resolves call sites inside function bodies against declarations in the same file: - calls are attributed to the enclosing class, not the method. Member ids are (stem, name), so all 17 `build` methods in one file collapse onto one node and a method-level caller carries no information. - a constructor invocation of a class declared in the same file becomes `calls` with context="widget_composition". In Flutter this is the only edge that links sibling widgets into a tree. - only unqualified calls plus `this.`/`widget.` resolve. Receiver-qualified calls (`_controller.dispose()`), `super.` upcalls and cascades (`..cancel()`) are skipped - without type inference they invent edges that do not exist. - repeated call sites raise the edge weight instead of duplicating the edge. Names that resolve to nothing (imported symbols, calls into other libraries) are dropped rather than guessed, so the pass adds no cross-file speculation. Measured on a Flutter + TypeScript app (32 .dart files, 1691-line product screen): calls from .dart 0 -> 112, graph edges 2321 -> 2458, and the largest screen file now clusters by feature (alternatives subtree, score breakdown, actions/errors) instead of by StatefulWidget-vs-StatelessWidget. TypeScript extraction is untouched. Tests: two new cases in tests/test_dart.py covering composition, weights, enclosing-class attribution, no self-loops, and receiver rejection. Full suite is unchanged at 90 pre-existing failures, 4118 -> 4120 passed.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
This PR adds a new step (section 8) to the Dart extractor that builds an intra-file call graph. It scans function/method bodies for call sites and creates calls edges to other declarations in the same file, distinguishing constructor invocations (tagged widget_composition) from plain function calls (tagged call), attributing calls to the enclosing class where applicable, and dropping calls with foreign receivers or unresolved names. It also aggregates repeated calls into an edge weight and avoids self-loops. The accompanying test file additions cover widget composition edges, helper call attribution, weight counting, and the exclusion of receiver-qualified calls like super., cascades, and other-object calls.
Worth a look
- Cascade detection only checks single preceding '.', missing '..' cascade with whitespace —
graphify/extractors/dart.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- widget. receiver resolves against declared_funcs, inventing edges to other objects' members —
graphify/extractors/dart.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 30 functions depend on the 29 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_dart()— 10 callers, 7 callees
Verification — 30 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 30 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_dart.
The verifier did not have enough to check extract\_dart, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 more finding(s) on lines outside this diff (see the check run).
The problem
extract_dartlinks every member back to the file node and never to another member, so a.dartfile comes out as a star: N members, N−1definesedges, nothing in between. Section 7 does find invocations, but attributes them to the file node asreferences, so no call structure survives.On a Flutter + TypeScript app in one repo:
.dart.tsThe consequence is not just missing edges. A star's cohesion is
2(n−1)/n(n−1)— the mathematical floor — soGRAPH_REPORT.mdflags every Flutter community as weakly interconnected and suggests splitting it. On the app above, the 74-node community around a product screen scored 0.027 and was reported as the loosest thing in the project; it was simply the biggest file. Clustering also had no signal left except inheritance from framework base classes, so Louvain grouped "all StatefulWidgets in the project" and "all StatelessWidgets in the project" rather than anything about the product.The change
A self-contained section 8 in
graphify/extractors/dart.pythat resolves call sites inside function bodies against declarations in the same file.(stem, name), so all 17buildmethods in one file collapse onto a single node — a method-level caller would carry no information. Top-level functions still resolve to themselves.callswithcontext="widget_composition". In Flutter this is the only edge that assembles sibling widgets into a tree, and it is what turns a flat file into a readable structure.this./widget.resolve._controller.dispose()is not this widget'sdispose,super.initState()is a framework upcall, and..cancel()is a cascade onto some other object. Without type inference, matching those against local declarations invents edges. An earlier iteration did exactly that and produced_LoadingViewState → disposefrom_controller.dispose().weightinstead of duplicating the edge; self-loops are skipped.callsis already inSEMANTIC_RELATIONS, so no vocabulary change is needed.Result
Same app, after the change:
callsfrom.dartThe 1691-line product screen now splits by feature instead of by Flutter class kind:
_AlternativesSection(+State),_AlternativeTile,_AlternativesEmpty,_AlternativesSkeleton_BreakdownCard,_AdditivesCardState,_row,_factRow,_riskExplanation_snack,_addPhoto,_shareSheet,_favoriteButtons,_describeErrorWhat stays a star is the residue of plain fields and locals, which genuinely have no edges other than
defines.Tests
Two cases added to
tests/test_dart.py:test_intra_file_calls_and_widget_composition— composition edges, plain helper calls, enclosing-class attribution, weight counting, no self-loopstest_intra_file_calls_ignore_foreign_receivers—this./widget.resolve;super., receiver-qualified calls and cascades do nottests/test_dart.pypasses 7/7. Full suite: 90 failures before and after (pre-existing, intest_skillgen/test_terraform/test_tree_html, unrelated to this change), passing count 4118 → 4120 — exactly the two new tests. TypeScript extraction is byte-identical.🤖 Generated with Claude Code