dmetrics — how to use and read it
dmetrics measures Dart code and judges each value against thresholds the project configured. It measures; it does not fix. Warnings are evidence to weigh, not orders to obey.
When to run
Once per task, after your edits, on the package's source roots:
dmetrics analyze lib bin (or the project's make check)
Do not run it after every edit. Run it on a single file only when you
want to compare before and after a refactor of that file.
Exit 0 clean • 1 violations • 2 analysis incomplete (fix those first:
parse errors, unreadable files, bad config) • 3 usage error.
Reading a report line
lib/a.dart:42:3 • warn • method Foo.bar • cognitive 18 [warn ≥ 15, fail ≥ 25] • if ×4, loop ×2, else ×3
where • verdict • scope kind and name • metric and value • thresholds that
applied • contributors: which constructs produced the score, in source
order. Only warn, fail and suppressed lines print; the last line is the
summary. --all prints every scope. --json gives the same data
structured. No thresholds configured means every scope is ok: nothing
is judged.
Counts are occurrences, not points. For cyclomatic they sum to value − 1.
For cognitive nested constructs cost more, so counts do not sum to the
value; the gap is the nesting.
if ×9 @2 means most of those ifs sit two levels down; no suffix means
the top of the body. Same count, different shape: a run of guards or a
nest. --json has the depth of every contributor.
Closures are scopes of their own: a warning on build.<closure#2> points
at the second closure inside build, and build itself may read ok.
Less obvious labels: if-case is if (x case p), when a case guard,
pattern-or a || inside a pattern, loop any for/while/do.
The metrics
cyclomatic Number of independent paths through a scope: every branch,
loop, case arm, &&, ||, ??, ?: and catch adds one. Measures
how much there is to test.
cognitive How hard the scope is to read: nesting makes each construct
cost more, sequences of the same construct cost less. A
switch counts once here but once per arm in cyclomatic, so
the same code reads switch ×1 and case ×15. Measures how
much there is to understand.
coupling Per library: how many other libraries of this package it
imports. dart: and other packages do not count; exports do
not count. Measures how much can break it when the package
changes. cycle of N on the line means an import cycle.
What to do about a warning
- Read the contributors before the value.
case ×15andif ×8, loop ×5at the same value are different problems. table-shaped: <kind>means one kind supplies most of the score: a dispatch switch, a field-wise ==, a copyWith, a parser step. Its size is the table's, not a tangle's.if@1means the ifs sit one level down, typically a switch whose arms each hold a run of ifs. Usually leave it; suppress if it must be silent.- A scope your task did not touch is not your problem. Mention it,
do not fix it, unless the user asked for a cleanup.
--changed main(orHEADfor uncommitted work) addstouchedto the tag of every scope your change hit, so you need not guess. - A scope your task touched and pushed over a threshold: the shape of
the contributors says which move fits, and
dmetrics agent refactormaps the shapes to the moves. If the honest shape is one long function, say so and suppress with the reason next to it. - Never lower a threshold or add an override to make a run pass. Thresholds are the project's decision; propose the change instead.
Suppressing // ignore: dmetrics_cognitive on the line before the declaration // ignore: dmetrics every metric // ignore_for_file: dmetrics_coupling A suppression on a method does not cover its closures. Suppressed scopes still print, so nothing is hidden.
Baseline
When the project has a baseline file (dmetrics_baseline.json next to
analysis_options.yaml), every line carries its status against it:
fail (new) a scope the baseline does not know: yours to fix
fail (worse, was 11) a known violation that got worse: yours to fix
fail (baselined) accepted debt; prints so nothing is hidden, does
not fail the run, not your problem unless asked
warn (was 6) moved but still under the fail line: drift
Exit 1 means new or worse. The Changed since baseline section lists
scopes that moved while staying ok, largest delta first: that is the
drift; report it, do not chase it. Never run dmetrics baseline to
make a run pass: refreshing the baseline accepts debt and is the
project's decision, like a threshold. Run it only when asked, or after
fixing violations so the file shrinks.
--baseline-ref main compares against main's tree instead of a file:
exit 1 means this branch made something new or worse. Nothing to
refresh; the clause reads baseline (main): ….
Other commands dmetrics stats [paths] Distribution, share above thresholds, a sweep over candidate thresholds, contributor mix. For calibrating thresholds, not for finding violations. Read p90/p95 against the warn line. dmetrics deps [paths] The package as a graph: cycles, fan-out and fan-in hubs with instability I = out/(in+out), and the graph folded onto directories with the edges that break the layering marked. Cycles and hubs are information, not violations; a barrel file is a normal fan-in hub. Complete only when the whole package is in the run.
dmetrics — refactoring a flagged scope
Read this once a scope your task touched crossed a threshold and you have
decided to change it. dmetrics agent comes first: it explains the report
line. This layer maps the shapes a report prints to the move each shape
usually supports. Hypotheses, not orders: the code decides.
The point of the change A refactor is not a fix to get the run green. It is the one chance the task gives you to leave this code in better shape than you found it: simpler to read, one responsibility per unit, in the terms the package already uses and the shape its architecture already has. Keep it simple, build nothing the task does not need, add no abstraction for its own sake. A helper that exists only to lower a number is worse than the number.
Before choosing
Run dmetrics analyze <file> --all. Only the warning line prints by
default, and several shapes below read cyclomatic against cognitive for
the same scope. Then read the contributors, not the value.
Shapes
table-shaped: case (or &&, ??) cyclomatic high, cognitive low
A table: a dispatch switch, a field-wise ==, a copyWith, a parser step.
Leave it, or a lookup map when the arms are data. Never split it into
helpers: its size is the domain's, and a helper per row hides that.
if ×N @2, @3 cognitive high, cyclomatic modest
Nesting, not branching: a ladder of conditions each waiting on the
last. Guard clauses and early returns; invert the condition that wraps
the rest of the body. Extract the deepest block only when it has a
name of its own.
switch ×1, if ×N @1 table-shaped: if@1, cognitive line
Arms that each hold a run of ifs. The switch is fine; the arms are the
units. One helper per busy arm, or a when guard that lifts the if
into the case.
&& ×N, || ×N
Predicates spelled inline. Name them: a local, a getter, a small
predicate method. Extract booleans, not blocks; the branch count stays
and the reading cost drops.
loop ×N, if ×M @1 or deeper
A loop body that filters and acts. When the ifs are filters, an
iterator pipeline (where, map, firstWhere); when the body does
one thing per element, extract the body with the element as its
argument.
ternary ×N in a build method
Presentation branching. Extract the widget the ternaries decide, or a
switch expression over the state when they all test the same thing.
catch ×N
One try per unit of failure: what fails together is caught together.
Catches that log or rethrow alike collapse into one handler.
<closure#N> on its own line, or a parent that fails only with its
closures folded in
The callback is the unit. Give it a name, a method or a top-level
function, and the parent reads as a sequence of named steps.
library … coupling N, cycle of M
Not a function problem, a module boundary problem. Read dmetrics deps
first. A cycle is two libraries that are one, or a type that belongs
to a third; a hub with high fan-out splits by what its importers
actually use.
Closing the loop
measure → change → re-measure → tests → baseline. Re-measure the file
with the same --all run as before: the flagged scope's value should
drop and the file's sum should not grow much. Complexity moved into
three helpers with the same total is not a refactor, it is the same
code with more names. The full run against the baseline then shows the
delta as a fact anyone can check, not as your opinion. If the honest
shape is one long function, say so and suppress with the reason next to
it. Never lower a threshold or refresh the baseline to make the run
pass.