-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commands analyze
analyze resolves the Dart AST for a Flutter project and finds its rebuild scopes: State
subclasses, ConsumerWidget and HookConsumerWidget classes, and callbacks passed to supported
state-management builders. It writes one JSONL row of build-tree metrics per scope.
The same output serves as the manifest for inject and run.
spm analyze --output results.jsonl /path/to/flutter/project
spm analyze -o results.jsonl /path/to/project1 /path/to/project2
spm analyze -v -o results.jsonl /path/to/project # verbose
spm analyze -o results.jsonl -s State -s BlocBuilder /path/to/project| Flag / Option | Short | Description |
|---|---|---|
--output <path> |
-o |
Required. Output JSONL file path. |
--verbose |
-v |
Print per-instance progress to stderr. |
--scope-types <type> |
-s |
Rebuild scope types to extract; repeatable. Defaults to all of State, ConsumerWidget, Consumer, Selector, BlocBuilder, BlocSelector, BlocConsumer, Obx, GetX, GetBuilder, Observer. -s State reproduces the pre-rebuild-scope output. |
--package-config <path> |
The resolved .dart_tool/package_config.json to read every library outside the analysed directories against. Defaults to the one above the first directory. |
|
<dir> [dir…] |
One or more Flutter project roots to analyze. |
On completion it prints a summary to stdout:
Scanned 142 files (3 skipped with compile errors); found 47 rebuild scopes (State: 31, BlocBuilder: 12, ConsumerWidget: 4); kept 47 rows.
-
Resolved AST analysis. SPM uses the
analyzerpackage to resolve each Dart compilation unit. Files with error-level diagnostics are skipped and reported in the summary. -
Rebuild-scope discovery.
RebuildScopeAnalysisVisitoridentifies every FlutterState<T>subclass and consumer widget (locating each one'sbuildmethod), plus every builder callback passed inline to a state-management widget. Scopes nest: aBlocBuildercallback inside aState.build()produces its own row, and the enclosingStaterow still counts the widgets the callback builds, because a parent rebuild does re-run it. -
Build-tree extraction.
TreeExtractorfollows non-const custom widgets transitively, including the State build of customStatefulWidgetchildren and declarations inpartfiles. A widget from a package is followed too. It resolves outside the analysed directories, whereAnalysisContextCollection.contextForthrows, so SPM builds one collection per package root against the project's own package config. Without that config a pub-cache package cannot resolve its ownpackage:flutter, the index comes back with null types, and every widget in it would classify as a value object. -
Interaction callbacks. A closure a rebuild cannot run is not entered, so nothing inside it
reaches any feature and a custom widget built there does not seed step 3. The slots are named
under Extracted Features, rule 8: handler labels such as
onPressedandonChanged, thevalidatorfamily, and the arguments of deferred hosts such asthenandaddPostFrameCallback.isolateasks the same predicate through--[no-]prune-non-rebuild, so the isolated file and the in-place row agree on what a rebuild runs. -
Metric collection.
BuildMetricsVisitorrecords widgets, helper references, iterations, allocations, list strategy, and layout-dependent builders. Helper bodies are resolved transitively, both those returning aWidgetand those returning a collection of widgets. Local functions are read at their call site, so widgets they build inside a loop are counted per element. The AST complexity extension computes build and helper decision points. -
Closure recording. Every library the extraction reads is recorded against the scope that
asked for it, cache hits included, and so is the verdict on whether it could be read. A library
that resolves while carrying an error-severity diagnostic counts as unreadable and is refused
rather than read: its types come back null, so its widgets would classify as value objects and
the row would be wrong instead of short. The row carries both lists, the resolved version of
every package the closure entered (
packageVersions), and the non-SDK classes whose build bodies were walked (walkedWidgetClasses). -
Normalization and streaming. Metrics are aggregated into an
AnalysisResultModeland emitted as one JSONL row per discovered rebuild scope (filtered by--scope-types), followed internally by a summary event.
One JSON object is written per rebuild scope and tagged with its scopeType. Output
Formats gives the field reference and an example record. Extracted
Features defines each metric.
Each row also names the files its metrics came from (dependencyFiles) and any that could not be
read (unresolvedDependencies, summarized by closureResolved). The step-1 skip count in the
summary is not the same guard: it covers the file being scanned, while a scope's metrics are read
from a closure of files that reaches well beyond it. See closure
fields.
Pass one or more feature directories when a whole-project result would be too broad. Use
--scope-types State when the output will be used only for SpmState injection; inject can also
accept a full manifest and skip the other scope kinds itself.
Drop or flag rows whose closureResolved is 0 before using the metrics quantitatively. Such a row
is short by however much its unreadable subtree would have contributed, and nothing else in the
output reveals the shortfall.
Pin --package-config when the same project is analysed more than once. Package source contributes
to the metrics, so two runs whose pubspec.lock moved between them would otherwise differ with no
source edit to explain it. Pinning one resolved config removes that by construction; the cost is
that a subtree may be counted against a package version the checkout did not ship with.
packageVersions makes the pin auditable, so two runs that somehow read different versions are
visible rather than silent.
The framework itself is never walked. dart: and package:flutter/ stay outside the traversal, and
that is a correctness decision rather than a cost one: the visitor counts every branch of a build
body rather than the branch that ran, so walking _ScaffoldState.build would make a Scaffold
carrying only a body: count identically to one carrying an appBar, a drawer and a snackbar. A
third-party application-level widget does not have that shape, which is why the walk enters one and
not the other.
Rows from 0.6.0 or earlier are larger than rows from 0.7.0 for any scope holding a handler, because handler bodies were counted then and are not now. Do not pool the two.
Because scopes can nest, summing every row in a file double-counts work shared by a parent scope and
its nested builder callback. Filter or group by scopeType according to the question being asked.
JSONL tools such as jq can process the file without loading it as one large JSON array.
Commands
Reference
Internals
Contributing