Dart: extract dartdoc instead of discarding it - #2954
Conversation
extract_dart() strips comments before any pass runs, and `///` is just a
special case of `//`, so every doc comment in a Dart corpus is deleted
before extraction starts — 14,358 doc blocks in Flutter's own
lib/src/material alone, 97% of which carry a real summary sentence.
Recover the blocks from the raw source (the existing passes keep running
on the comment-free text) and turn them into graph signal:
- `doc`: the bounded lead paragraph, on every symbol declared in the file
and on the file node itself (a block above `library;`). Directives,
inline HTML, and `[ref]` brackets are stripped so it reads as prose.
- `See also:` entries -> `references` / context=dartdoc_see_also.
- `{@tool}` sample paths -> `references` / context=dartdoc_sample, the
runnable file that shows how to use the symbol.
- `{@template id}` -> `defines` a doc-fragment node, `{@macro id}` ->
`references` it, so dartdoc's transclusion is traversable across files
and packages.
Inline `[Foo]` mentions in prose are parsed but deliberately not emitted:
on src/material they add ~6.6k edges (+16%) that mostly restate relations
the AST passes already found. Doc-derived edges use the generic
`references` relation and a `dartdoc_*` context, so a doc-stated relation
stays distinguishable from one proven by code (Graphify-Labs#2270) and never downgrades
a specific relation on the same pair.
Measured on flutter/lib/src/material (182 files): 12,548 nodes gain a
summary, +6% nodes, +6% edges, extraction 3.05s -> 3.17s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds dartdoc (///) extraction to the Dart extractor via new _parse_dartdoc and _collect_dartdoc helpers, attaching a bounded lead-paragraph doc summary to file and declaration nodes in extract_dart. Recovers dartdoc from raw source before comment stripping, unwrapping [refs], stripping HTML/directives, and guarding external (source_file=None) nodes from inheriting a local symbol's doc on name collision. Emits curated cross-reference edges (See also:, {@tool} samples, {@template}/{@macro} transclusion) tagged dartdoc_*, while deliberately skipping inline [Foo] mentions.
Worth a look
- Dartdoc is lost when metadata annotation spans multiple lines —
graphify/extractors/dart.py:146· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 39 functions depend on the 38 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_dart()— 11 callers, 8 callees
Verification — 39 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: 39 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 grounded finding(s) anchored inline below.
| return library_doc, by_name | ||
|
|
||
|
|
||
| def extract_dart(path: Path) -> dict: |
There was a problem hiding this comment.
extract_dart()
fans out to 8 callees (efferent coupling); 11 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Two gaps in the first pass.
The doc text was the lead paragraph, truncated at 280 chars. The cap was
almost never the problem — it hit 16 of 13,869 blocks on src/material —
but keeping only the lead paragraph dropped half the prose (0.85 MB of
1.81 MB). `doc` is now the block's full text, every paragraph joined by
blank lines. Splitting it across nodes was the alternative and is worse
here: the median block is a single paragraph, so it would have minted
~14k nodes to hold one paragraph each. A consumer that wants the summary
takes doc.split("\n\n")[0], which is dartdoc's own convention.
Binding was by bare declared name, so a constructor's doc collapsed onto
its class (`MyFab(` and `class MyFab` share a name) and a documented
constructor parameter was dropped entirely — `_DARTDOC_MEMBER_DECL` never
matched `this.color,`. A block now attaches at the granularity of what it
sits above:
library; -> the file
class/mixin/enum/... -> that type
constructor -> that constructor (a new node, `contains` from
the class; `Foo()` keeps a label distinct from
`Foo`, and `Foo._()` gets an ID that does not
normalize onto the class or the file, Graphify-Labs#2738)
constructor parameter -> the field `this.x` forwards to, or the
parameter itself (`references` from the ctor)
anything else -> that member
Enclosing type comes from brace depth and the parameter list from paren
depth, both counted with strings and trailing comments blanked, so a
widget constructor CALL inside a build method is not read as a
declaration. Blocks also skip plain `//` comments between the doc and the
declaration, not just blanks and annotations.
Constructor and parameter nodes are minted only where a doc block points
at one, keeping this proportional to the documentation rather than to
every constructor in the corpus.
add_node now fills in a doc on a node an earlier pass already created
under a differently-normalized label. IDs strip leading underscores, so a
private field `_field` and a parameter `field` are one node; without this
the second label's doc was silently dropped (hit in cloud_firestore's
filters.dart).
flutter/lib/src/material: 25,207 nodes (13,042 with a doc, 1.67 MB of
text), 44,393 edges, 3.47s. lib/src/widgets: 737 documented constructors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds dartdoc (///) extraction to the Dart extractor: new _parse_dartdoc, _dartdoc_structure, _collect_dartdoc, constructor label/key helpers, and doc-node/edge wiring recover comment prose from raw source before the comment-stripping pass runs, binding each block to the file, type, constructor, forwarded field/parameter, or member it documents. Extends extract_dart to attach docs in add_node, emit add_dartdoc_edges, and surface see_also/samples/templates/macros. Covers behavior with new test_dart.py cases for binding, truncation, HTML/ref stripping, directive-only blocks, ID collisions, and constructor-vs-call disambiguation.
Worth a look
- add_node overwrites/misses node_by_id for pre-existing external nodes —
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
- Function-typed fields bind dartdoc to Function instead of the field —
graphify/extractors/dart.py:45· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Multi-line annotations stop dartdoc from reaching the declaration —
graphify/extractors/dart.py:250· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 53 functions depend on the 52 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_dart()— 15 callers, 9 callees
Verification — 53 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: 53 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 grounded finding(s) anchored inline below.
| return library_doc, by_label, constructors, parameters | ||
|
|
||
|
|
||
| def extract_dart(path: Path) -> dict: |
There was a problem hiding this comment.
extract_dart()
fans out to 9 callees (efferent coupling); 15 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
The gap
extract_dart()strips comments before any extraction pass runs:///is a special case of//, so every dartdoc comment in a Dart corpus is deleted before extraction starts. For a language whose convention puts the "what is this and how do you use it" statement in exactly that place, that is the single richest human-authored signal in the file, thrown away in the first ten lines.Measured on Flutter's own
lib/src/material(182 files):///doc blocks{@tool}blocks naming a runnable example{@macro}/{@template}All of it currently goes to
"".What this PR does
Collect doc blocks from the raw source before stripping (the existing passes keep operating on the comment-free text, unchanged), then bind each block to what it documents and turn the curated parts into edges.
Binding
A block attaches to the first line below it that is not more documentation — skipping blank lines, annotations and plain
//comments — at that declaration's own granularity:library;class/mixin/enum/extension/typedefthis.xforwards to, or the parameter itselfConstructors and documented parameters get nodes because nothing else mints them: the method pass skips every name starting uppercase, and parameter lists are never walked. They are created only where a doc block points at one, so the cost tracks the documentation rather than every constructor in the corpus.
Foo()/Foo.named()labels keep an unnamed constructor from colliding with its class.Foo._()gets an ID that does not normalize onto the class node — or, for the bare_, onto the file node (Dart private named constructor (Foo._()) collapses to an empty entity in make_id, colliding with the file node and disabling _is_file_node #2738).Padding(inside abuildmethod is read as a call, not a constructor declaration.Node attribute
doc= the block's full prose, every paragraph, joined by blank lines, with directives removed, inline HTML stripped,[refs]unwrapped and control characters dropped (#2897). Not truncated: a consumer that wants the one-line summary takesdoc.split("\n\n")[0], which is dartdoc's own convention for the first paragraph.Splitting long docs across nodes was the alternative and is worse for this shape of data — the median block is a single paragraph, so it would mint ~14k nodes to hold one paragraph each.
Edges
containsdartdoc_constructorreferencesdartdoc_parameterSee also:entriesreferencesdartdoc_see_also** See code in <path> **referencesdartdoc_sample{@template id}definesdartdoc_template{@macro id}referencesdartdoc_macro{@template}/{@macro}is dartdoc's own transclusion; linking the two ends makes reused documentation traversable across files and packages.On
FloatingActionButton:What it deliberately does not do
Inline
[Foo]mentions in prose are parsed but not emitted as edges. Onsrc/materialthey add ~6,600 edges (+16%) that mostly restate relations the AST passes already found, and they pile onto hub types ([ThemeData]alone appears in 423 blocks). Easy to add behind a flag — the parser already returns them.Doc-derived edges use the generic
referencesrelation, so_GENERIC_RELATIONSinbuild.pykeeps them from ever downgrading acalls/inheritson the same pair, and thedartdoc_*context keeps a doc-stated relation distinguishable from one proven by code — the confusion #2270 is about.Cost
src/materialnodessrc/materialedgessrc/widgets(186 files) for a second data point: 737 documented constructors, 1,709 see-also edges, 585 macro links.Notes / limits
///only. Legacy/** */dartdoc is still stripped — zero occurrences insrc/material, so it didn't seem worth the surface. Say the word and I'll add it.{@macro}is linked, not expanded. Resolving the text needs a global{@template}index across packages (many Material macros resolve intosrc/widgetsordart:ui), which is a bigger change than one per-file extractor. Linking the two ends gets the graph the relation without that pass.buildin two widget classes) already collapses to one node, so the first block wins.add_nodenow also fills in a doc on a node an earlier pass created under a differently-normalized label — IDs strip leading underscores, so a private field_fieldand a parameterfieldare one node, and without this the second label's doc was silently dropped (hit for real incloud_firestore/filters.dart).src/materialandsrc/widgets— it documents the fields instead), so that path is exercised by unit tests and verified againstcloud_firestore/filters.dart, which does.source_file=None) never inherit a same-named local symbol's doc.Testing
tests/test_dart.pycovering: docs on file/class/constructor/parameter/field/method nodes, full multi-paragraph text with no truncation, undocumented declarations staying clean, all six edge kinds,Foo._()not collapsing onto the class, a constructor call not being read as a declaration,//comments between doc and declaration, the normalized-ID collision, the lowercase-See alsoresolution rule, docs not leaking onto external nodes, directive-only blocks, and HTML stripping vs.<https://…>autolink preservation.4890 passed, 11 skippedon 3.10 with--all-extras.ruff check,pyright, andpython -m tools.skillgen --checkall clean.graphify <dir> --code-onlyconfirmsdocand everydartdoc_*edge survive intograph.json.🤖 Generated with Claude Code