main gained six PRs (#1-#6) while the audit fixes and the state-map seam were in progress. Four of main's commits fixed the same bugs this branch did, independently; where they overlap, the more complete version wins: - Fingerprints: main's (a0844bb) - catchSite (`member#index` among all catches), union/owner as identity, a legacy fingerprint so version-1 baselines still match, baseline "version": 2. This branch's `catch` ordinal is dropped; its fingerprint_test still guards the result. - verify --rule: main's verifyAnalysis (cf9e0b7), which also reports findings in skipped files as NOT SCANNED. - Unreadable SDK: both kept - this branch's up-front probe gives the early, specific error; main's catch around the analyzer (d6b6bcb) is the backstop. - Riverpod: main's "a ref.listen is not a render" (f6411dc) and this branch's AsyncError patterns / stored-value tracking compose; the fixture keeps both sets of cases. Ported onto the model-only rules (the seam), since main edited code this branch had moved into front-end facts: - excerpt.dart's literal-free, bounded quoting (a prompt-injection guard) now also covers front-end facts: SwallowedFailure.handler and NonSubscribingRead.excerpt, and the C++ front end elides string literals in handler evidence the same way. - ScanCoverage: runChecks stays source-free; the model carries coverage (`scanCoverage` in the state JSON, written by both front ends), so `check --model` states coverage like a source check. - main's softened wording (rendered-not-emitted "probably unreachable", the remove-policy dead-handler text, notifier-field-unread) and the backticked flag message. Docs: main's README grew the Firestore query inventory and the output contract; the README stays short, the inventory moves to docs/queries.md, and the contract, exit codes, verify and baseline details join docs/usage.md. Checked against origin/main on resolved mirrors of the dogfood apps: nuwayze (102), dex (0) and word_alive (0) identical; tick differs only by the intended riverpod stored-value fix. On the fixture every difference is one of this branch's fixes. 214 tests pass.
4.7 KiB
Design
Motivation
The state bugs users feel (a spinner that never stops, an error never shown, a tap that does nothing) are whole-program facts: a state emitted in one file and never matched in any other, an event dispatched with no handler anywhere. A per-file linter cannot express them, so pipe resolves the whole project and reasons over all of it at once.
pipe is a CLI on package:analyzer, not a custom_lint or analyzer plugin:
plugins have no project-wide index, flutter analyze does not report
new-system plugin diagnostics, and a CLI can pin its own analyzer version.
Architecture
bin/pipe.dart entrypoint
lib/src/cli.dart commands and flags
lib/src/errors.dart PipeException (message + remedy)
lib/src/project.dart package validation, source discovery
lib/src/sdk.dart SDK discovery (Flutter-aware)
lib/src/analysis.dart analysis contexts, per-file resolution, scan
coverage, unresolved references
lib/src/pool.dart bounded-concurrency map over files
lib/src/excerpt.dart bounded, literal-free source excerpts
lib/src/index.dart resolution health, timing, diagnostics
lib/src/model.dart declaration model (`graph`)
lib/src/state_flow.dart Dart front end: the state model and its JSON
lib/src/dart_facts.dart Dart front end: loading and construction facts
lib/src/dart_provider_facts.dart Dart front end: ChangeNotifier / provider facts
lib/src/facts.dart language-neutral facts (JSON round-trip)
lib/src/rules.dart rules, findings, baselines (model only)
lib/src/provider_rules.dart ChangeNotifier / provider rules (model only)
lib/src/pipeline.dart analyse -> model -> rules
lib/src/verify.dart baseline verification
lib/src/suggest.dart fix order
lib/src/guide.dart agent brief, markdown report, yams emission
lib/src/recall.dart labelled-bug recall
lib/src/firestore_facts.dart Firestore query inventory (no rules)
lib/src/gateway_facts.dart adapters for declarative query APIs
tool/adjudicate.py precision adjudicator
tool/cpp_front.py experimental C++ front end
tool/build_release.sh native AOT build
Resolution mirrors Flutter's own tooling: the context collection is rebuilt
when package_config.json changes, file changes go through changeFile and
applyPendingFileChanges, and an InconsistentAnalysisException gets a fresh
session.
The state model
Rules never read a syntax tree. A front end turns code into a state model:
unions and variants, where each is emitted and matched, events dispatched and
handled, busy paths whose failure is swallowed, and observable objects with
whether their mutations are announced. runChecks takes only that model.
The model is plain JSON, and a model read back from JSON reports exactly what
the in-process one does (test/state_map_roundtrip_test.dart):
pipe state . --format json > model.json # or any front end that writes it
pipe check --model model.json # every rule, baseline and gate
A front end for another language only has to write that JSON.
C++ front end (experimental)
tool/cpp_front.py reads a compile database with libclang (Homebrew LLVM) and
maps each project enum class to a union:
- emission: a reference that produces the value (assignment, atomic
store, argument, return, initialiser); a cast from an integer is an indirect emission, which makes "never emitted" unprovable for that enum; - observation: a
case,==or!=; - swallowed failure: a busy enumerator written to the method's own state,
then a top-level
catchthat neither writes state nor rethrows.
Identity comes from clang's resolved AST, so two enums with the same name are never confused. Sources the build does not compile are recorded as unresolved.
tool/cpp_front.py <repo> --build-dir <repo>/build/debug -o model.json
pipe check --model model.json
For C++, read rendered-not-emitted as "branched on but never produced" (a
dead branch, or a state a state machine never enters) and emitted-not-rendered
as "produced but never branched on" (computed and ignored, or reaching output
only through a name table or reflection). Rule messages still say "UI".
On yams (375 translation units, about two minutes) a hand check of 13 findings
found 12 factually correct and 1 false positive, which the front end now
excludes. That sample bounds the false-positive rate; it does not measure it.
Controls live in test/fixtures/cpp_state and test/cpp_front_test.dart,
skipped when LLVM is absent.