Skip to content

Commands analyze

albertoodev edited this page Aug 19, 2026 · 7 revisions

analyze: static analysis

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.

Usage

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

Options and flags

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.
<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.

How it works

  1. Resolved AST analysis. SPM uses the analyzer package to resolve each Dart compilation unit. Files with error-level diagnostics are skipped and reported in the summary.
  2. Rebuild-scope discovery. RebuildScopeAnalysisVisitor identifies every Flutter State<T> subclass and consumer widget (locating each one's build method), plus every builder callback passed inline to a state-management widget. Scopes nest: a BlocBuilder callback inside a State.build() produces its own row, and the enclosing State row still counts the widgets the callback builds, because a parent rebuild does re-run it.
  3. Build-tree extraction. TreeExtractor follows non-const custom widgets transitively, including the State build of custom StatefulWidget children and declarations in part files.
  4. Metric collection. BuildMetricsVisitor records widgets, helper references, iterations, allocations, list strategy, and layout-dependent builders. Helper bodies are resolved transitively, both those returning a Widget and 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.
  5. 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: its types come back null, so its widgets classify as value objects. The row carries both lists.
  6. Normalization and streaming. Metrics are aggregated into an AnalysisResultModel and emitted as one JSONL row per discovered rebuild scope (filtered by --scope-types), followed internally by a summary event.

Output

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.

Working with the results

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.

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.

Clone this wiki locally