dartrics 0.7.3
dartrics: ^0.7.3 copied to clipboard
Citation-anchored Dart code-quality metrics (CK, Halstead, McCabe, Martin, Cognitive) plus Periphery-style unused public-API detection, shaped for AI refactor loops.
Changelog #
0.7.3 #
Detection-fidelity and discoverability release driven by external agent feedback. No metric, threshold, or schema change.
- Unused detector now follows operator-method calls through
IndexExpression,BinaryExpression, and the operator element onPrefixExpression/PostfixExpression. User-definedoperator [], binary operators (including custom+/==), unary-, and the+reached viac++/c--were previously flagged as unused when their only caller was the textual operator form.==overrides were already kept alive by theObjectdunder auto-root, but theirsignals:fan-in / fan-out (anddartrics inspect ==upstream walk) now reflect the actual call sites. Stale-dismissal note: projects with// dartrics:dismiss unusedcomments suppressing the prior false-positives on those operators may now see those dismissals reported as stale — remove them. dartrics ai-looprepositioned as the operational entry point. Its--helpone-liner now reads "Operational playbook: commands, prompts, dismiss syntax (start here for AI agents).";dartrics --helpgains a footer pointing at it;doc/manual.mdopens with a cross-ref banner; and the README "AI agents — start here" admonition, Quick start block, Subcommands table, and Documentation list all lead withai-loopand reframemanualas the conceptual reference (lens design, decision tree, flag catalogue). Driven by an agent who skippedai-loopbecause the prior blurb read as a recap of the manual.
0.7.2 #
AI-consumer ergonomics release driven by a real dartrics session report from another agent. No metric, threshold, or schema change.
looksCosmetic→cosmeticSplitDetectedindartrics regression. The old name read as a verdict; the detector is a narrow opt-in signal (the cosmetic-split signature) parallel toanalyze'ssignals:block, not a refactor-quality verdict. All reporters now emit# narrow heuristic, not a global verdictalongside the boolean, and the manual reframes the block as a signal rather than a pass/fail.- Stray
// dartrics:dismisscomments no longer silently no-op. WhencommentSourceis off (the default — thedismissals:block is not yet authored) and the run is not--strict-dismiss,analyzescans for dismiss-shaped comments and emits a stderr WARN naming the affected files and the opt-in needed. - Operational protocol step 5 documents
--snapshot none. The snapshot cache rewrites itself every run, so two consecutiveanalyzeruns always reportchangedFiles: 0. The manual now flags this on the verify step. - README directs AI agents to
dartrics manualfirst. The subcommand was hidden behind--help; the README banner makes it the explicit entrypoint for AI consumers. - ACCEPT becomes a first-class decision in the loop diagram and manual. Borderline values on healthy code are now explicitly "no edit, no
// dartrics:dismiss, no punt — move to the next violation," distinct from dismiss (which is a tracked, recurring-reason commitment).
0.7.1 #
Documentation-only release. The 0.7.0 surface — dartrics inspect, the signals: reference block — shipped without matching updates to dartrics manual and dartrics ai-loop. 0.7.1 catches the in-binary walkthroughs up to the released CLI surface. No code, schema, or threshold change.
dartrics ai-loopsample report now carriessignals:andsnapshot:; documents what the agent reads out of it and clarifies thatviolations:/explain:go absent on a clean run whilesignals:keeps emitting. New "The unused-detector loop" section covers theread → inspect → --applyflow.dartrics manualgains a "Signals — reference information, not verdicts" section, an inlinedartrics inspectsubsection, and aninspectentry in the operational protocol flag map.
0.7.0 #
A call-graph release. The element-resolved reachability pass already powering dartrics unused now also surfaces per-declaration fan-in / fan-out reference signals and backs a new dartrics inspect <symbol> subcommand. Signals carry no threshold and no severity — they are reference information, not violations.
- Report schema bumps from
1.0to1.1. Three new top-level fields land in the JSON output:signals,explanations(previously AI / MD only), andstaleDismissals(previously AI only). All additive;additionalProperties: falseis now consistent withtoJsonagain. Seeschemas/dartrics-report.schema.json. - New
dartrics inspect <symbol> [--depth N] [--direction up|down|both]subcommand. Walks upstream callers and / or downstream callees within--depthhops; emits JSON or the YAML-ish AI shape. Homonym methods on different classes stay disambiguated as separate matches. - MD reporter gains
## Signals (reference)and## Stale Dismissalssections. The AI reporter'sunused:block is reframed in-output: entries may be leftover code OR unwired implementations — the framing comments instruct the loop to confirm against intent before acting.
0.6.6 #
A dartrics unused correctness fix. Detection now sees references that live inside machine-generated files. Output changes for any project whose source declarations are reached only from generated code (.g.dart, .freezed.dart, etc.). No CLI flag, exit code, schema, or threshold change.
dartrics unused — generated files participate in reachability #
AnalyzerRunner.includeGeneratedexisted but no call site flipped it on, so the unused command collected source files only and the reachability BFS never saw edges that live in generated files (e.g. ariverpod_generatorprovider's reference back to its source function).UnusedCommandnow constructs the runner withincludeGenerated: true. Snapshot hashing and the reported analyzed-file count still operate on the handwritten subset via the newAnalyzerRunner.isGeneratedDartPathhelper, so adart run build_runner buildre-emit does not churn the snapshot or inflate the count.dartrics analyze's unused output retains the same defect. Surfaced by dogfooding on a multi-moduleriverpod_generatorproject.
0.6.5 #
A README narrative simplification. No metric, threshold, exit-code, or wire-format change.
README.md — drop duplicated 'wager'/'lens' prose; surface manual premise in doc index #
- The two prose paragraphs under
## What it does(the "wager" and the lens framing) duplicated material already carried bydoc/manual.md's## The premise — multiple lenses on your own writing, with the same wager also gestured at by the README's### Designed for the AI loopbullets and the## Provided metricspreamble. They are dropped; the section now keeps the "what is computed" paragraph and the AI-loop feature bullets. The## Documentationentry fordartrics manualpreviously read as a flat list of operational topics; with the design premise no longer narrated in the README, the entry now leads with "the design premise (why lenses, why multiple at once)" so the navigation cue to the manual's premise section is explicit rather than inferred.
0.6.4 #
A dead-code cleanup surfaced by dogfooding dartrics on its own source. No metric, threshold, exit-code, or wire-format change.
lib/src/models/source_location.dart — drop unreachable toJson #
SourceLocation.toJsonhad no caller inlib/: every JSON serializer that holds aSourceLocation(ScopeRef.toJson,UnusedDeclaration.toJson, the SARIF reporter's location blocks, and theai/md/consolereporters' string interpolation) builds its own field set inline and bypasses the method, and the{path, line, column}shape it returned matched no emitted output schema. The method is removed along with the lone test that pinned its three-key return; the class stays a plain data holder, and the test file keeps theSourceLocationimport becauseScopeRef/UnusedDeclarationfixtures still instantiate it. Detected by runningdartrics analyzeon dartrics itself.
0.6.3 #
A dartrics unused --apply correctness fix plus two documentation additions. No metric, threshold, exit-code, or wire-format change.
dartrics unused --apply --filter field — constructor-formal coupling refused #
--applywould previously delete a field whose name still appeared as athis.<field>initializing formal on a constructor of the enclosing class. The formal stayed, the file no longer compiled, and the breakage was only discovered by runningdart analyzeafterwards (reported reproducibility: 27/27 fields on one user codebase). The detection now identifies that coupling before the deletion is committed and refuses the entry. A newApplyOutcome.coupledConstructorFormalcarries the(file, line, field, [Class.ctor...])detail; the CLI summary names every refused entry under askipped (constructor formal coupling) Nbucket so they can be resolved by hand. Auto-rewriting the formal and every named-argument call site is the kind of cascade that should stay in the user's hands —--applydoes not now and does not plan to do it. The field declaration is left as-is; no compilation regression.
dartrics unused --apply — adjacent-line indent leak #
- Deleting a declaration left the leading whitespace of its physical line in place, attaching it to the next line and doubling that line's indent (e.g. two adjacent
finalfields rendered asfinalafter the first was deleted). The deletion range now backs up through same-line spaces / tabs and stops at the previous newline, so siblings on the same physical line are unaffected.
doc/manual.md — Punt has no in-tree syntax #
- The "Punt when…" section described when to punt but never specified the output channel, leaving AI consumers to invent a format and discount the deliberate choice. The section now states explicitly that punt has no comment directive, YAML key, or JSON report field — it is the natural-language channel between AI and operator.
dartricsis a tool for both AI and human; raising the lens values verbatim doesn't transfer meaning to the operator the way it does to the AI. Translate, name the load-bearing hypothesis, and ask in the channel the harness uses to reach the human. The step 4 Apply row of the operational protocol gains the same option so the protocol matches the diagram's three branches.
doc/manual.md — Reporters section #
- AI agents running
dartricswere observed treating--reporter aioutput as the canonical record and discountingmd/json/sarif, then redirecting downstream conversations about extracting from json or shipping md into "how do I render ai output by hand". A new## Reporters — pick by audiencesection between the operational protocol and the flag map states the relationship explicitly: the four reporters are parallel projections of oneAnalysisReport, not stages of a pipeline; the metric IDs, exact threshold values, andcomplexityJustifiedsibling fields are bytes the renderers carry verbatim, so a result read out ofaimatches the bytes the other three would emit for the same run. The section closes with "re-run dartrics with the appropriate reporter flag, don't transcribe by hand" so the elevation pattern is named and the corrective instruction is concrete.
0.6.2 #
A documentation-only patch on top of 0.6.1. No metric, threshold, exit-code, or wire-format change.
Per-file Martin lens polarity framing #
efferent-coupling/afferent-coupling/instabilitywere grouped under thedownpolarity bullet indoc/manual.md's "## Polarity" section, butLibraryMetric.polaritydefaults to.neutraland the three subclasses don't override — thedownclaim contradicted the regression-diff classifier. The bullet now lists them underneutral, with a pointer to a new "Per-file Martin granularity" section indoc/calibration.mdthat explains why these three ship as change-impact rankings rather than Martin-frame Pain/Uselessness verdicts (a Dart library is one file; the release unit is the pub package, so the per-package granularity Pain/Uselessness assumes is absent). The "Library / file lenses (Martin 1994)" table indoc/manual.md(byte-mirrored inlib/src/cli/manual_text.dart) gains the same disclaimer directly above the rows so the framing is visible at table level.doc/calibration.md's "Selection principles" gains a bullet on informational polarity for the per-file Martin lenses.
README.md — Default warning column and per-file Martin caveat #
- The function- and class-level metric tables drop the
Notescolumn in favour of aDefault warningcolumn, matching the tighter "back of the box" format (descriptive notes already live indoc/manual.md's lens battery). Readers see at a glance that onlycyclomatic-complexity= 10,cognitive-complexity= 15, andnumber-of-parameters= 4 ship with a default warning; everything else is—(opt-in viadartrics: { metrics: { <id>: { warning: <n> } } }) oropt-in(off-by-default). The library-level table keeps itsNotescolumn (the section header carries the source attribution) and gains the sameDefault warningcolumn with—for all three Martin lenses;instabilityis explicitly tagged as informational. A "Per-file Martin granularity" bullet in the Limitations section surfaces the caveat at install-decision level rather than only deep indoc/calibration.md.
0.6.1 #
A small documentation-correctness patch on top of 0.6.0. No metric, threshold, exit-code, or wire-format change.
dartrics unused --apply summary message #
- The
--applysummary previously printed"unsupported kinds (method / field / enumValue) require range computation relative to a containing declaration and are not yet auto-deletable"when anApplyOutcome.unsupportedKindentry was present. That claim was stale: instance methods, fields, and enum constants have all been auto-deletable since the per-kind locator landed (covered by the existingapplyDeletionstest suite — seetest/unused/apply_test.dart). The only remainingunsupportedKindoutcome is "removing the last constant of an enum", which would leave invalid Dart. The message is rewritten to describe that single edge case accurately. Behaviour is unchanged.
0.6.0 #
A calibration release. Four metrics drop — two because their academic anchoring did not survive a re-audit against original sources, and two because Martin's "package = release unit" framing has no equivalent in Dart's language model. Four citations are corrected. The Dart-specific deviations from cited definitions consolidate into a single doc/calibration.md audit page. The README is restructured around the three things a "back of the box" surface should answer (philosophy / what it does / what metrics) with everything else moved to the dedicated docs.
Breaking changes #
maximum-nesting-levelremoved. A re-audit found the cited authority — "NIST SP 500-235 §4 reports a strong correlation between deep nesting and bug density" — to be incorrect: §4 of that document is "Simplified Complexity Calculation", not nesting research. With the misciting removed, the metric had no peer-reviewed source establishing a defect-correlated threshold for raw nesting depth — only PMD / Checkstyle / SonarLint convention. Dropped rather than shipped on tooling-convention backing. TheMaxNestingLevelcalculator, theMaximumNestingLevelRuleanalyzer-plugin rule, thedartrics_maximum_nesting_levellint id, themaximum-nesting-levelconfig key, and theMaxNestingLevelpublic-API export are all removed. CI configs that pinned amaximum-nesting-level: { warning: <n> }block must drop the entry — the schema'spropertyNames.enumno longer accepts the id anddartrics doctorwill flag it.boolean-trapremoved. A re-audit found the McConnell Code Complete and Bloch Effective Java item 36 attributions wrong: the term "boolean trap" was coined by Ariya Hidayat (2011) in a community blog post, and Bloch's item 36 in the 2nd edition is "Consistently use the Override annotation" — unrelated. With the academic backing reduced to a community blog post, and thedart-lang/linterruleavoid_positional_boolean_parametersalready covering the same ground with a stricter binary threshold (it flags any positional bool, not just two or more), the count-based dartrics framing was both weaker than the official lint and unanchored. Recommendation: enableavoid_positional_boolean_parametersinanalysis_options.yaml. TheBooleanTrapcalculator, theBooleanTrapRuleanalyzer-plugin rule, thedartrics_boolean_traplint id, theboolean-trapconfig key, and theBooleanTrappublic-API export are all removed.abstractnessanddistance-from-main-sequenceremoved. Martin (1994) definesAandDover a "package = release unit" — a named, bounded module of multiple types. Dart has no equivalent granularity in its language model: a Dart library is one file, and a pub package spans the whole project. ComputingA = abstract_count / total_countper file produces brittle values (a file with oneabstract class FooscoresA = 1.0regardless of design intent), andD = |A + I − 1|inherits that brittleness. Even at a synthetic "directory = module" granularity,Areduces Dart's abstraction story (which spansabstract class,mixin,interface class,sealed class, and extension types) to the ratio of one keyword. The metric is structurally misaligned with the language, not just under-implemented. Removed rather than kept off-by-default with a perpetual "until directory aggregation lands" caveat. The remaining Martin lenses (efferent-coupling,afferent-coupling,instability) stay on — they measure dependency direction, which holds at file granularity without the package assumption.
Citation corrections #
cognitive-complexity— Campbell / SonarSource white paper is 2017 (with revisions in 2018, 2021, 2023), not 2018. Title kept; year corrected. The rationale now flags the source as "industry white paper, not peer-reviewed" and notes that predictive value beyond McCabe's CC has not been independently validated.halstead-volume— Alfadel et al. is 2017, 9th IEEE-GCC Conference and Exhibition (GCCCE), pp. 1–9, not 2018 / "IEEE Conference on Computer and Information Technology". The "~0.9 mean correlation" specific figure is softened to "strong positive linear correlation" because the underlying paper's text could not be confirmed for the precise numeric claim during the re-audit.lcom4— Hitz & Montazeri (1995) introduced the connected-components cohesion variant; the "LCOM4" label is a later community convention (Henderson-Sellers' LCOM1–LCOM5 numbering). The rationale now reflects that distinction.martin 1994— full bibliographic citation expanded in the library-coupling family'sreferencesfrom the bare "Martin, R. C. (1994)" to "Self-published essay, August 14, 1994 (rev. June 20, 1995); cross-posted to comp.object and comp.lang.c++. Content later folded into Martin (2002), Agile Software Development: Principles, Patterns, and Practices, Ch. 28, Prentice Hall." The C++ Report attribution that some downstream sources use is incorrect — Martin became Editor-in-Chief of C++ Report in 1996, after the essay was already in circulation.
doc/calibration.md — new audit trail #
A single page documenting the relationship between dartrics' implementation and its cited sources: selection principles, counting-rule deviations from the literal source definitions (sealed-aware CC, positional-only NOP, declared-methods-only LCOM4), and off-by-default rationales. Threshold numbers (CC warn 10, CBO warn 14, …) follow the cited sources unchanged; the page enumerates only the cases where dartrics deviates from those sources, and why. README's "Provided metrics" preamble and doc/manual.md's "The lens battery" preamble link out to it instead of duplicating the rationale inline. Lenses not in the current catalogue are not enumerated — that history lives in this CHANGELOG.
README restructured around "back of the box" content #
- The README is roughly 58 % shorter (437 → 184 lines). Three priorities stay on the page: the lens-battery + AI-loop philosophy, what dartrics does at a glance, and the metric inventory with thresholds. Everything else moves to the dedicated docs that ship inside the binary (
dartrics manual,dartrics ai-loop) or to the JSON Schema files that already define the wire format. - Sections collapsed to a 1–2 line pointer: the long flag listing under "Subcommands" (replaced by a per-command
--helpreference + link todoc/manual.md), "AI integration" (token-budget knobs,--coverage,complexityJustified,--snapshot,--since,--strict-dismiss), "Generating the coverage data", "Deliberate dismissal" (comment + YAML examples), "Regression check", "Code-gen keep-alive annotations", "Public-API unused-code detection", "Flutter-aware mode", "Test-aware mode", "AI report schema (v1)", "JSON Schema files", and "Exit codes". All of that content already lives indoc/manual.md,doc/ai-loop.md, or theschemas/files; the README now links there instead of mirroring it. - A new "Documentation" section enumerates the four reading entry points (
dartrics manual,dartrics ai-loop,doc/calibration.md,schemas/) so first-time readers immediately see where the operator-level detail lives. - The Configuration section keeps only the minimum config example (yaml-language-server directive + a couple of metric thresholds + exclude); per-key reference moves to the schema file and
dartrics manual.
Analyzer plugin #
plugins: dartricsnow enables three function-level rules —dartrics_cyclomatic_complexity/_cognitive_complexity/_number_of_parameters. The previous_maximum_nesting_leveland_boolean_traprules are gone with their underlying metrics.
0.5.1 #
A maintenance release. No metric, threshold, exit-code, or wire-format change — # dartrics ai-report v1, every JSON / SARIF schema, and the public Dart API (FunctionMetric, FunctionMetricInput, MetricPolarity, dartricsVersion) stay byte-compatible with 0.5.0. The release modernises internals against the Dart 3.10 idiom now that the SDK floor (environment: sdk: ^3.10.0) has settled, plus two CI / docs hygiene items.
Internal: Dart 3 switch and pattern adoption #
- The function-level metric helpers in
lib/src/metrics/metric.dart(_bodyOf/_parametersOf/_scopeNameOf/_enclosingClassName) move fromif (x is FunctionDeclaration) ... else if (x is MethodDeclaration) ...chains toswitchexpressions with type patterns and destructuring.regression_diff.dart::_directionOrderbecomes a 1-statementswitchexpression overChangeDirection.metric_engine.dart::_buildViolationconsumes the sealedDismissalCheckvia an exhaustiveswitch; the previousfinal rejected = check as DismissalRejectedcast is gone, and adding a third sealed subclass is now a compile-time error rather than a runtime cast failure. - The string-format dispatchers (
pickReporter,resolveSnapshotConfig,_modeFromString, the void-returningRegressionReporter.report/RulesReporter.report) collapse into singleswitchexpressions or no-fallthroughswitchstatements. The'none' | 'off'synonym uses a single OR pattern. library_metric._countClasses,lcom4.ingest,wmc.compute,rfc.computewalk the analyzer's sealedClassMemberhierarchy with destructured patterns. The sealed walk inlcom4.ingestdeclares its no-op fallback case explicitly instead of relying onif-elseskip-through.
Internal: CallableDecl sealed wrapper #
- A new private
sealed class CallableDeclinlib/src/metrics/metric.dartconcentrates the analyzer-Declaration-to-callable type fan-out into a singleCallableDecl.from(Declaration)factory — a 3-lineif-chain plus a trailingas ConstructorDeclarationcast — and lets the rest of the metric layer dispatch through three subclasses that overridebody/parameters/scopeNamedirectly. The previousswitch-expression helpers shipped a_ => throw ArgumentError(...)wildcard arm in each, since analyzer'sDeclarationis open and the type system can't prove exhaustiveness over the three subtypes the engine actually passes in. With the wrapper, exhaustiveness arms are no longer needed, and analyzer version updates that introduce a fourthDeclarationsubtype only need a single factory amendment instead of four wildcard arms.FunctionMetricInput.body/parameters/scopeNamekeep their same shape —late finalfields delegating through the wrapper — so embedders see no API change.
Internal: dot-shorthand syntax #
- Dart 3.10's dot-shorthand is now used throughout
lib/src/wherever the target type is inferable from context — return types on the same line (MetricPolarity get polarity => .down;),switchsubjects (switch (config.mode) { .none => ... }), enum-to-enum equality (kind != .function), and named-arg positions where the parameter type names the enum (SnapshotConfig(mode: .cache),ScopeRef(kind: .library),MetricChange(polarity: polarity[id] ?? .neutral)). No semantic change; the same tokens reach the metric layer. A handful of sites stay in long form: ternaries whose target type is inferred from a sibling branch,Severity.values.byName(...)/ScopeKind.values.byName(...)static-collection access, and one default-value site where the type comes from a YAML coercion that names both branches.
Tooling: dartrics self-application CI gate #
- A new GitHub Actions job runs
dart run bin/dartrics.dart analyze --root . --snapshot none --reporter aion every push and PR and fails the build if dartrics' own source produces any warning-level violation. Codifies the dogfood loop principle — the lens battery only stays honest if the canonical idiomatic-Dart codebase the project itself ships clears it. Runs after theanalyze(static) job sodart analyzelints catch first, then dartrics gates on its own metric battery. Usesdart runagainst the in-tree source rather than installing the package so the gate works against unreleased states.
Docs: coverage data prerequisite #
- The README's
--coveragedocumentation, the in-binarydartrics manual, and thedartrics ai-loopwalkthrough now spell out how to generatecoverage/lcov.info(the file the auto-detection looks for) —dart pub global activate coverageplus the matchingdart run/flutter test --coveragecommand. Previously the docs assumed users already had the lcov file in place and only described the auto-detection step.
0.5.0 #
A pruning release. Five pieces of dead-or-deferred CLI / model surface come off — flags that advertised behavior they did not honor, a subcommand that overlapped with auto-explain, an enum value no lens emits — plus one payload addition (MetricChange.id) on the regression side.
Breaking changes — CLI #
dartrics explain <id>subcommand removed. Auto-explain has been default-on since 0.1.0 and inlines every fired metric's rationale + refactorHints + references next to the violation, so the AI loop already has the "why" without a second tool call. The subcommand's only remaining role was post-hoc lookup against a saved JSON report — but a saved report already carries the sameexplain:block, and looking the entry up directly in the JSON is oneMap<id, violation>build away with zero process-spawn overhead. Going through the CLI added a Dart VM cold-start (50–300 ms AOT, ~1–3 s underdart run) per lookup, which is wasteful inside an AI loop that may dereference dozens of ids. Drop any tooling that shells out todartrics explain; read the JSON report'sviolations:andexplain:blocks directly instead.--no-auto-explainflag removed. Auto-explain is now unconditional. The opt-out existed for "lean reports without rationale", but the entire reason an AI loop pipes dartrics into a model is to consume the rationale alongside the violation; the flag was a holdover from before auto-explain was the default. Theexplain:block still only appears when at least one metric fired (clean runs stay clean). Token-budget control belongs to--limit <n>instead, which trims violations rather than stripping their justification.--fatal-styleflag removed from every subcommand. The flag exited non-zero onSeverity.info, but no built-in lens has ever emitted that severity —MetricEngine._classifyThresholdonly returnsSeverity.error/Severity.warning, and unused / SARIF paths don't synthesize info either. The flag was a YAGNI hold-out for a hypothetical future info-tier and was indistinguishable in--helpfrom a working option. CI configs that pass--fatal-stylewill now fail withCould not find an option named "--fatal-style"; remove the flag.--fatal-warningsis unaffected.- Per-subcommand option scoping.
addCommonOptionswas a single dump of 13 flags onto every subcommand even thoughreportignored 9 of them andunusedignored 3. Same UX disease as--fatal-style:--helpadvertised behavior the command did not honor. The helper splits intoaddIoOptions(--reporter,--output,--limit,--verbose),addAnalysisOptions(--config,--root,--since,--snapshot,--concurrency,--fatal-warnings), andaddMetricsReadingOptions(--coverage,--strict-dismiss).analyzeopts into all three,unusedopts into the first two,reportopts into IO only. TheCommonOptionsclass likewise splits intoIoOptions/AnalysisOptions/MetricsReadingOptions. CI scripts that passed analysis-time flags todartrics report(e.g.dartrics report r.json --root .) will now fail withCould not find an option named "--root"; drop them — they were no-ops.
Breaking changes — model #
Severity.inforemoved from the enum. No built-in lens ever emitted it and no user-visible feature consumed it; the only reference was a SARIF reporter case-arm mapping it tonote. Removed in lockstep with--fatal-style. The SARIF reporter's_levelswitch is now an exhaustive switch expression on{warning, error}. The summary block in the markdown reporter likewise drops itsinforow. Custom embedder metrics that synthesizedSeverity.infowill fail to compile against 0.5.0; treat aswarning. The decoder inreport_command.dartwill throwArgumentErroron a saved JSON containinglevel: info— saved reports older than 0.5.0 that emitinfowill need to have those entries rewritten or filtered out before re-emission.
dartrics regression carries the violation id #
- Each
MetricChangenow exposes a stableidfield and emits it in the AI / JSON output. The id is the samesha256("<file>|<scope>|<metric>")[..16]thatMetricViolation.iduses, so a regressed row indartrics regression --reporter aiis one direct lookup away from the matching violation in the analyze report (or its SARIFpartialFingerprints.dartrics/v1). The id is computed from fields the row already carries — emitted unconditionally, including for sub-threshold deltas where no violation was recorded — so AI loops can correlate even sub-threshold drift across runs without rebuilding the triple by hand. The regression diff itself still keys on(file, scope.kind, scope.name)at the scope granularity (a record holds many metric values; an id would force per-metric matching and lose the kind disambiguation), so this is a payload addition, not a matching-key change.
Internal: auto-explain lookup moves into MetricEngine #
- The auto-explain pipeline used to round-trip through
buildExplanations(List<String>)inrules_command.dart, a helper that defended against duplicate / blank / unknown metric ids. Every one of those branches was unreachable in practice: violationmetricIds come from the samedefaultFunctionMetrics/defaultClassMetrics/defaultLibraryMetricslists thatfindRuleDescriptionwalks, and the upstream id collector already deduplicated. The helper and its test were keeping the dead branches alive without any production caller able to reach them. Embedders did not have a path to inject unknown ids either:MetricEngineis not exported fromlib/dartrics.dart(FunctionMetricis, but the engine isn't), so custom calculators cannot be registered through the public API. - Auto-explain now lives where the calculator set lives.
MetricEngine.firedExplanations(List<MetricRecord>) → List<ExplainEntry>iterates the engine's ownfunctionMetrics/classMetrics/libraryMetrics, filters by the set of metric ids that fired in the records, and emits oneExplainEntryper fired metric. The lookup is structural over the calculator set rather than a nullable Map indirection, so the previousfindRuleDescription(...)!bang at the call site disappears: the relationship "fired metric ⇒ has a rationale" is the type itself, not an unchecked invariant. Output order is calculator-declaration order (function, class, library) instead of first-violation order; AI consumers index bymetricIdrather than position, so this is informational.analyze_command.dartdrops_firedExplanationsand the now-unneededrules_commandimport;buildExplanationsand its test are gone with it.
0.4.0 #
Breaking changes #
- Three Dart-shape metrics removed:
widget-tree-depth,null-aware-chain-depth,async-chain-depth. All three were tool-originated lenses with no academic anchor and no Effective Dart consensus to point to — out of step with the catalogue's "every metric is anchored to its primary source" rule.widget-tree-depthwas lint-shaped (deepestInstanceCreationExpressionchain) and is better expressed as acustom_lintrule than as a metric.null-aware-chain-depthandasync-chain-depthmeasure call-shape patterns the language already gives clean rewrites for (?.short-circuit + early-return for the former;Future.wait/ hoisting for the latter), so the metric was re-deriving guidance the language semantics already imply. All three were off-by-default, so the impact is limited to projects that opted into them — drop the entries fromdartrics: { metrics: { ... } }. The schema'spropertyNames.enumno longer accepts the three ids. FunctionMetric.referencesandClassMetric.referencesare now abstract. With the three function-level lenses removed and citations added fornumber-of-methodsandclass-length, every remaining built-in metric overridesreferencesin both abstract classes, so the empty-default getters became dead code. Embedders implementing customFunctionMetrics orClassMetrics now need to declareList<String> get references => const [];explicitly when there is no primary citation — same shapeLibraryMetrichas always required. The three abstract bases are now consistent.
Class-metric citations #
number-of-methodsnow cites Lorenz & Kidd (1994) and Chidamber & Kemerer (1994). Lorenz & Kidd's Object-Oriented Software Metrics: A Practical Guide (Prentice Hall, ISBN 0-13-179292-X) includes the per-class method count in their 11-metric OO-size suite, catalogued as "Average number of methods per class"; CK's WMC reduces to NOM when each method is weighted at 1.class-lengthnow cites Beck (1996), Fowler (1999), and Lippert & Roock (2006). Beck's Smalltalk Best Practice Patterns and Fowler's Refactoring frame the underlying "large class" code smell; Lippert & Roock's Refactoring in Large Software Projects (Wiley, ISBN 0-470-85892-3) adds the threshold side via the "Rule of 30" — a class averaging more than 30 methods (~900 LOC) is highly likely to need decomposition.- README's class-level table and the manual's class-lens section (mirrored in
doc/manual.md+lib/src/cli/manual_text.dart) replace the placeholder—with the new citations.
README "Designed for the AI loop" #
- The differentiation pitch is promoted to a dedicated
### Designed for the AI loopsubsection inside "What it does", expanded into a 5-item feature list (auto-explain by default, stable IDs with reverse lookup, coverage-aware reading, output stability, docs in the binary). The previous "Who it's for" bullet is dropped — every other paragraph already pitches AI agents and the humans driving them as the audience. The "signals, not gates" framing remains in the lead paragraph where it belongs as operating philosophy rather than as a feature.
"Provided metrics" preamble #
- The off-by-default justification now names both Halstead Volume (strongly correlated with both cyclomatic complexity and SLOC: ~0.9 mean correlation per Alfadel et al. 2018; redundant rather than orthogonal signal) and Method Length (= SLOC + blank lines + comment-only lines by definition, so SLOC alone carries the same signal plus a known offset) with their distinct rationales. Pre-0.4.0 the preamble named only Halstead Volume; the three Dart-shape lenses being off too made the parenthetical read as a partial example, but with those gone the off-by-default group is just the two metrics, both nameable. The earlier wording asserted Halstead's "predictive value over cyclomatic complexity has not held up empirically" and Method Length is "highly correlated with SLOC in production code"; the first claim was wrong-direction (Alfadel et al. find correlation, not inferiority), the second was an uncited observational estimate. Both are reframed as overlap with simpler signals: Halstead via correlation evidence, Method Length via the definitional identity.
0.3.0 #
Breaking changes #
--explain <metric-id>removed fromdartrics analyzeanddartrics unused. Auto-explain has been the default since 0.1.0 and inlines every fired metric's rationale + refactorHints + references already; the only remaining role of--explainwas injecting a metric's rationale even when it didn't fire, which is better served by pipingdartrics rules --reporter aiinto the agent's context separately. For post-hoc lookup of a single violation, usedartrics explain <id> --input report.json.MetricPolarity.upremoved from the enum. No built-in metric ever used it (the onlyupmetric was the maintainability index, dropped in 0.1.0 as a derivation of CC + V + LOC). Custom embedder metrics that registered withMetricPolarity.upwill fail to compile against 0.3.0; treat the metric asneutral(regression diff surfaces deltas without classifying them as improvement / regression). The up-polarity arms ofregression_diff.dart::_directionByPolarityanddoctor_command.dart::checkThresholdOrderingare removed.dartrics: { unused: { presets: [...] } }removed. The field has been a no-op since 0.1.0 (every codegen preset is always honoured as a keep-alive root, regardless of the list). The loader, theConfig.UnusedConfig.presetsfield, theexpandPresetshelper, the doctor's "unknown unused preset" validator, and the corresponding entry indartrics-config.schema.jsonare all removed. Existinganalysis_options.yamlfiles that still listpresets:will fail validation against the schema; remove the key. For in-house codegen, list annotations underunused: { ignore-annotations: [...] }.
dartrics ai-loop subcommand #
dartrics ai-loopprints the four-station AI-loop walkthrough (setup → propose → apply → verify) to stdout. The body is embedded as a const string mirrored byte-for-byte fromdoc/ai-loop.md, so it travels withdart pub global activate dartrics— agents that have just installed dartrics can read the loop contract from the binary itself without a separate doc download. Pairs with the existingdartrics manual(the lens reference) so the manual + walkthrough are both reachable from inside an agent loop. A parity test enforces byte-equality so the two cannot drift.
Structured references field on every metric #
FunctionMetric/ClassMetric/LibraryMetricgainList<String> get references(defaultconst []so custom embedder metrics are unaffected). Built-in metrics now expose their primary source as a structured list — McCabe 1976 for cyclomatic complexity, Campbell / SonarSource 2018 for cognitive complexity, NIST SP 500-235 for maximum nesting level, Beck 1996 for method length, Halstead 1977, Boehm 1981 for SLOC, Fowler 1999 for number-of-parameters, McConnell 2004 + Bloch 2008 for boolean-trap, Chidamber & Kemerer 1994 for WMC / CBO / RFC, Hitz & Montazeri 1995 for LCOM4, Martin 1994 for the library-coupling family. Empty by default for metrics that don't trace to a published source (max-nesting was already cited; Dart-3-idiom lenses and the simpler size lenses stay empty).- Surfaces in
dartrics rules(ai / md / console / json),dartrics explain <id>(ai + json), the AI / md reporters' explain blocks, and SARIFhelp.text/help.markdownnext to the rationale + refactor hints. Each reporter gates the references block on isNotEmpty; uncited metrics produce byte-identical output to 0.2.x. JSON omits the field when empty so existing schemas / consumers see no shape change. RuleDescriptionandExplainEntrycarry the field through to every reporter; embedders consumingdartrics analyze --reporter jsonwill see newreferences: [...]arrays on rule descriptors when a metric is cited.
Manual drift gate #
test/cli/manual_command_test.dartnow asserts both directions against the live metric catalogue: every id fromcollectRuleDescriptions()appears as a back-ticked token in## The lens batteryofdoc/manual.md, and every back-ticked id at the start of a lens-table row resolves back to a registered metric. Region-scoped to that section so back-ticked tokens elsewhere (if,package:,MethodInvocation, …) aren't mistaken for metric ids. The byte-equality test alone caught prose drift but not metric-vs-doc drift.
Documentation cleanup #
- README rewritten and trimmed (545 → 429 lines, –21 %). 'In five lines' / 'Why dartrics' / 'Recommendation' folded into a single 'What it does' section; 'Honest limitations' + 'Who should not adopt yet' merged into 'Limitations' (dropped three bullets that duplicated information in the AI-integration knob list or in
doc/ai-loop.md); '--since (diff mode)' inlined into the AI-integration knob list; the AI-report YAML sample shortened (the full sample lives indoc/ai-loop.md); the per-package code-gen keep-alive annotation table replaced with a single paragraph pointing atdartrics rulesandlib/src/unused/keep_alive_presets.dart; Embedding section trimmed. - AI-leftover prose swept from tracked artifacts:
tmp/reference removed fromtest/analyzer_runner_test.dart; pre-0.1.0 / matches-0.1.0 wording removed from CLI help and rationale strings (CHANGELOG records 0.1.0 as the first public release); AI session-memory references ("Round 4's stable id", "Round 2-4 added", "Round 5") removed from explain_command, report_command_test, unused_apply_cli_test, coverage_cli_test; "in 0.1.0 / as of 0.1.0 / since 0.1.0" version stamps removed from ~25 sites acrosslib/,test/,doc/manual.md, and the byte-mirroredmanual_text.dart; test-justification dartdoc ("Exposed for tests", "Extracted so tests can …") trimmed from public dartdoc on entry_point, regression_diff, doctor_command, git_diff, unused_detector, lint_options, explain_command, analysis_report. None of these change runtime behaviour.
0.2.2 #
Bugfixes #
maximum-nesting-levelno longer counts named-argument closures (Widget builders, event handlers) as a nesting level.ListView.builder(itemBuilder: (...) {})andElevatedButton(onPressed: () {})were reporting1even when noif/forwas involved — directly contradicting the contract thatwidget_tree_depth.dartandflutter_aware.dartboth document ("a healthy Widget tree produces a nesting score of 0"). Closures still increment when passed positionally (xs.forEach((x) {}),xs.fold(0, (a, x) => …)) — those are higher-order calls, not declarative configuration. Innerif/forinside a named-argument closure still counts at the right depth.- The summary table now surfaces snapshot mode and the diff filter so the cache-mode default doesn't render as a regression. Without these rows the second run with no source changes filtered every violation through
_filterUnusedand rendered asunused declarations: 0indistinguishable from "really nothing fired". Addssnapshot mode: cache/files changed: 0 of 3 (no new findings)to the md summary, a top-levelsnapshot:block to the AI reporter, a[snapshot cache: 0 of 3 changed]tail tag to the console line, andsnapshotMode+changedFileCountfields at the JSON report root. Field additions only —# dartrics ai-report v1and the JSON1.0header stay valid.
0.2.1 #
Bugfix #
.pubignore'scoverage/pattern (no leading slash) matched at any depth, so the published0.1.0and0.2.0archives shipped withoutlib/src/coverage/coverage_loader.dartandlib/src/coverage/lcov_reader.darteven though both are imported bylib/src/cli/analyze_command.dartandlib/src/metrics/metric_engine.dart. Anyone who installed from pub.dev hit unresolved-import errors. The pattern is now/coverage/, matching.gitignore. No source-code changes; reinstall to recover a working package.
0.2.0 #
Public-API unused-code detection — element-resolution mode #
- The CLI's
dartrics analyzeanddartrics unusedpaths now run the public-API reachability analysis over the analyzer's resolved element graph instead of the simple-name reference graph that shipped in 0.1.0. The detector keys reachability on canonicalElement.ids of project-local declarations, so homonym methods on different classes are independent nodes (callingFoo.bar()no longer accidentally keepsBaz.bar()alive), prefixed imports keep distinct identities, and SDK / dependency symbols never pull through to project declarations they happen to share a name with. - Reachability is now tracked at member granularity. The detector reports unused instance methods, fields, getters, setters, and enum values in addition to the top-level kinds — same
UnusedKindenum as before, just with the per-class entries populated. Existing 0.1.0 callers will see new entries withkind: method | field | enumValuein the unused list once a class is reachable; pass--filter class,function,extension,typedef(or setunused: { filter: [...] }inanalysis_options.yaml) to restore the top-level-only shape. - New
--filter <kinds>CLI flag (and matchingunused: { filter: [...] }YAML key) narrows the report to a subset of declaration kinds. Accepted names:function,method,class,field,typedef,enum,extension.enumtargets individual enum constants; enum types are filtered withclass. Comma-separate or repeat the flag (--filter method,field). Unknown names exitExitCode.usagewith a did-you-mean style error. - Auto-rooting rules added to keep the per-member reports clean:
- Members marked
@overrideare rooted (covers interface / superclass overrides without us walking the supertype hierarchy). - Object dunder names —
toString,hashCode,==,noSuchMethod,runtimeType— are rooted; the language runtime calls them, not user source. - When a class carries any keep-alive annotation (
@JsonSerializable,@reflectiveTest, every codegen preset, …) every public member of that class is rooted too — these annotations signal generator / reflective consumers that read members by name.
- Members marked
- New
reflectiveTestkeep-alive preset added tokeep_alive_presets.dartso@reflectiveTestclasses frompackage:test_reflective_loaderkeep theirtest_*members alive. LibraryElement.exportNamespacenow drives theexcludeExportedroot set, so re-exportedlib/src/types (and every public method / field / getter / setter on them) survive without relying on textualshowmatching.- The parse-only
UnusedDetector.detectentry point stays as a fallback for tests / embedders that don't want a realAnalysisContextCollection.dartrics analyze/dartrics unusedroute through the newUnusedDetector.detectResolvedpath automatically — no caller changes required.
0.1.0 #
First public release. The CLI, the analyzer plugin, and the embeddable Dart API ship from a single package.
Design philosophy #
dartrics is built on the wager that the AI coding loop changes which software metrics are practically usable. The academic catalogue — McCabe 1976, CK 1994, LCOM4 1995, Martin 1994, Cognitive Complexity 2018 — has long been underused in everyday workflows because each of calculating the number, interpreting it, and acting on it was individually expensive for a human reviewer. An AI loop absorbs all three: the CLI computes in milliseconds, --auto-explain delivers the rationale, and the agent acts on it.
Each metric is treated as a single lens — one dimension of "hard to read." Lenses are independent and stackable: an agent can iterate through dozens in a session, refactor under each, then re-evaluate. dartrics surfaces what each lens reads; the accept / refactor / dismiss decision is first-class and stays in the loop.
The metric set, the thresholds, and the Flutter / test relaxations below are calibrated for Dart. The lens framing and the AI-loop contract are language-agnostic.
Metrics #
- Function / method: cyclomatic complexity (McCabe 1976), cognitive complexity (Sonar 2018), maximum nesting level (control-flow only —
if/for/while/switch/try/closure; widget-literal chains do not count), number of parameters (Fowler 1999 — positional-only; named parameters are weight-zero because the call sitefoo(a: …, b: …)carries each argument's name on the spot, dissolving the position-counting load Fowler's lens targets — same rule asboolean-trap), boolean-trap (McConnell Code Complete 2004; Bloch Effective Java item 36 — count ofbool-typed parameters, warning ≥ 2), source lines of code. Halstead Volume (Halstead 1977), method length, andwidget-tree-depth(deepest chain of nested constructor calls — Flutter community ~5–7 threshold) ship off-by-default — opt in withdartrics: { metrics: { <id>: { enabled: true } } }. Method length is default-off because its correlation with SLOC in production code is high enough (often > 0.95) that emitting both is redundant noise — opt in when you specifically want the "screen real estate" reading (counts blank lines + comments) on top of SLOC's "actual code volume" reading. Halstead Difficulty / Effort and the Maintainability Index (Oman 1992) were dropped because they are pure derivations of the underlying token counts andCC + V + LOCrespectively. - Class: number of methods, weighted methods per class (CK 1994), LCOM4 (Hitz & Montazeri 1995, connected-component variant), CBO and RFC (CK 1994), class length. CK's DIT and NOC are intentionally not provided — Dart's mixin / composition-over-inheritance culture keeps inheritance chains shallow, so they rarely produce signal.
- Library / file: efferent / afferent coupling, instability (Martin 1994). Abstractness and distance from main sequence ship default-off because Martin's framing assumes "package = release unit" and Dart's 1-file-1-library granularity makes the per-file values brittle (a single
abstract class Fooin its own file scores A=1.0 without saying anything about the design layer it participates in). Opt-in until the directory-level aggregation lands. - Each metric exposes
rationale(one-paragraph explanation anchored to the original paper),refactorHints(concrete moves), andpolarity(down/up/neutral) so AI loops know which direction is "healthier" for the regression diff.
Subcommands #
dartrics analyzeruns every metric and the public-API unused detector over the analysis root.dartrics unusedruns only the public-API reachability detector (fast path).dartrics report <input.json>re-emits a previously saved JSON report in a different format.dartrics rulescatalogues every metric with its rationale + refactor hints in--reporter ai|md|json|console.dartrics regression [--before <ref>] [--after <ref>]compares metrics between two git states (default:HEAD~1vs the working tree). Uses git worktrees for the historical side. Diff entries are classified asimproved/regressed/unchanged/added/removedperMetricPolarity. A built-in cosmetic-split heuristic flags refactors that look like AI just shuffled complexity into one-line helpers without actually reducing it.dartrics manualprints the AI-facing operator's manual to stdout. The content is a mirror ofdoc/manual.mdembedded as a const string in the executable, so it travels withdart pub global activate dartricsand is reachable from any agent loop without a separate doc download. A parity test enforces byte-equality with the markdown source so the two cannot drift.dartrics doctorvalidates thedartrics:block inanalysis_options.yaml. Surfaces unknown metric ids (with did-you-mean suggestions via Levenshtein distance ≤ 2), unknown unused presets, and threshold orderings inconsistent with each metric's polarity (e.g.cyclomatic-complexity: { warning: 20, error: 10 }is flagged because lower-is-better metrics neederror ≥ warning). Read-only — never edits the config. Exit codes: 0 clean, 1 warnings, 78 invalid YAML /ConfigException.dartrics explain <id>reverse-looks-up a violation by its stable 16-hex-char id and prints the matching entry plus the metric's rationale + refactor hints. Reads a JSON report (the format produced bydartrics analyze --reporter json) from stdin or--input <path>. AI agents that see the same id reappear across runs ("my fix didn't take") can retrieve full context for that id without re-reading the entire report.
AI integration (--reporter ai) #
- Token-efficient YAML-ish output starting with
# dartrics ai-report v1. The header is contractual; field renames or removals trigger a new header (v2). - Auto-explain (default on;
--no-auto-explainopts out) attaches each fired metric's rationale + refactor hints to the report'sexplain:block — AI loops no longer need to know to pass--explain <id>for every threshold they care about. --explain <metric-id>(repeatable) is still honoured and unions with auto-explain; explicit ids stay first in the order so authored prompts remain deterministic.- Stable violation
id— every violation carries a 16-hex-charsha256("<file>|<scope>|<metric>")so AI loops can correlate runs ("a3f1c4e9…showed up again ⇒ my fix didn't take"). Surfaces in the JSON / AI / md reporters and aspartialFingerprints.dartrics/v1in SARIF. Exported ascomputeViolationId(file, scope, metricId)for embedders. --limit <n>caps the violations + unused entries shown by the AI / md reporters after the priority sort. AI report records the dropped count in atruncated:block; md report appends_+ N more violation(s) hidden by --limit_. JSON / SARIF / console stay unlimited.--coverage <path>(auto-detectscoverage/lcov.info) attaches per-scope line and branch coverage to every emitted violation. The reporter sorts by a priority key that puts low-coverage entries first andcomplexityJustifiedones last so token budget lands on the most actionable items.complexityJustified: trueflags CC / Cognitive violations whose scope has branch coverage ≥ 0.8 (or line ≥ 0.95 when noBRDA:records are present) — earned complexity AI loops should leave alone. Two sibling fields surface the engine's decision:complexityJustifiedBy(branchorline, whichever rule won) andcomplexityJustifiedThreshold(the literal cutoff that rule used). Reporters pass the trio through verbatim — JSON, AI / YAML, MD, SARIF,dartrics explain.- Deliberate dismissal lets agents triage a specific
(file, scope, metric)triple via// dartrics:dismiss <metric> reason="…"comments or adartrics-dismissals.yamlsidecar. Both channels are opt-in throughdartrics: { dismissals: … }inanalysis_options.yaml. Validated entries decorate the violation withdismissed: true+ carriedreason/by/at; entries that failrequireReason/minReasonLength/requireAuthor/requireTimestampkeep the violation live and stamp it withdismissalRejected: <why>plus a stderr WARNING.--strict-dismissignores every dismissal for the run. Stale-entry detection (default-on,warnStale: true): dismissals that never matched a live violation in the analyzed file set surface as a stderr WARNING and as astaleDismissals:block on the AI / JSON reports, so AI loops can prune dead entries when scopes are renamed / deleted or metrics drop below threshold. Skipped for files filtered out by--since/ snapshot. --since <git-ref>filters output to declarations whose owning.dartfile changed between<ref>andHEAD. Cross-file analysis still resolves the full project so LCOM4 / library coupling / public-API reachability stay accurate; only the emitted records are filtered.- End-to-end loop walkthrough — setup → propose → apply → verify, with sample prompts and troubleshooting — lives in
doc/ai-loop.md. - AI-facing operator's manual — each metric framed as a lens on "hard to read" with the accept / refactor / dismiss decision step made first-class — lives in
doc/manual.md.
Reporters #
console— human-readable summary line + per-violation entries.json— stable schema forjqpipelines and SARIF transformation; carriesanalyzedFiles(sha256 list) when snapshot mode is engaged.md— Markdown for PR comments and issue bodies, formatted viapackage:dapper.formatMarkdown.ai— described above.sarif2.1.0 — GitHub Code Scanning / GitLab ingestion.tool.driver.rulesis populated for every metric that fired in the run, carrying the rationale infullDescription, the refactor hints inhelp.markdown, andhelpUrideep-linking back to the README anchor — so the platform surfaces the full lens explanation inline next to each result instead of an opaque rule id.
Public-API unused-code detection #
- Periphery-style BFS reachability over a name-based reference graph rooted at
main, declarations annotated with@pragma('vm:entry-point'), and (whenexcludeExportedis enabled)lib/exports outsidelib/src/. Followsexport ... show ...clauses so re-exportedlib/src/symbols stay reachable. Reports unused public functions, classes, mixins, extensions, typedefs, enums, and top-level fields. - Code-gen keep-alive annotations are always on:
freezed,json_serializable,dart_mappable,go_router_builder,auto_route,riverpod_generator,injectable,hive,drift. Listingpresets:inanalysis_options.yamlis no longer required (the field is still parsed for backward compatibility with older configs but no longer narrows the keep-alive set). The simple-name match means an annotation from a package you don't use simply never fires, so there's no per-project cost to leaving every preset on. - Generated Dart files (
*.g.dart,*.freezed.dart,*.gr.dart,*.config.dart,*.mocks.dart,*.pb*.dart,*.gen.dart) are skipped during file collection. Override withAnalyzerRunner(includeGenerated: true)if you really want them. - Private (underscore-prefixed) names are intentionally skipped —
dart analyze'sdead_codelint already covers them. dartrics unused --applydeletes detected top-level declarations from disk (analogous todart fix --apply). Refuses to run on a dirty git tree without--force. Files undertest/orintegration_test/are excluded by default; pass--include-teststo include them. Supports function / class / typedef / extension deletion; method / field / enum-value deletion is reported asunsupportedbecause the range computation needs containing-declaration awareness that is deferred. Imports left unused after deletion can be cleaned up withdart fix --apply.
Analyzer plugin #
plugins: dartricsinanalysis_options.yamlenables five function-level rules (dartrics_cyclomatic_complexity/_cognitive_complexity/_maximum_nesting_level/_number_of_parameters/_boolean_trap) inline indart analyzeand the IDE.- Rule thresholds are configurable through the same
dartrics:section the CLI uses (long form{ warning: <n>, error: <n> }or bare-integer short form). The plugin honoursflutter: truefor the same skip rules as the CLI. - Heavier metrics (LCOM4, CBO, RFC, library coupling) and the unused detector stay CLI-only — they need a project-wide index that an analysis-server plugin can't maintain efficiently per file.
- Diagnostics surface at INFO severity due to an upstream
analysis_server_plugin0.3.x constraint (non-INFOLintCodecrashes the plugin isolate).
Flutter-aware mode #
dartrics: { flutter: true }is the default. Its only effect in 0.1.0 is to skipnumber-of-parameterson widget constructors, which stays as a cushion for the rare positional-style widget constructor — in practice an idiomaticMyWidget({super.key, required this.title, ...})already scores 0 from NOP's positional-only semantic, so the skip is a no-op for typical Flutter code.Widget.build()is measured normally —maximum-nesting-levelonly counts control-flow constructs (if/for/while/switch/try/closure), so a healthy declarative tree produces a depth of 0 without any special-casing, andmethod-lengthis informative even on declarative trees.- Visual depth from chained Widget literals (
Container(child: Container(...))) is the responsibility of thewidget-tree-depthlens — opt-in for Flutter authors that want this signal, default warning 7 (matching Flutter community practice of ~5–7 before extracting a sub-widget). - Detection is AST-only across
StatelessWidget,StatefulWidget,State,ConsumerWidget,ConsumerStatefulWidget,HookWidget,HookConsumerWidget. Setflutter: falseto forcenumber-of-parameterson widget constructors too.
Test-aware mode #
dartrics: { test: true }is the default. When the file under analysis sits undertest/orintegration_test/and its basename ends in_test.dart(the conventionaldart testdiscovery marker),method-length/source-lines-of-code/maximum-nesting-levelare skipped at function level andclass-length/number-of-methodsare skipped at class level. AAA blocks and nestedgroup/setUp/testscaffolding don't dominate the diagnostic stream that way. Helpers undertest/whose basename does not end in_test.dart(e.g.test/helpers.dart) stay under the strict thresholds. Settest: falseto apply the production-grade thresholds to test files too.- Cyclomatic complexity, cognitive complexity, number-of-parameters, boolean-trap, LCOM4 / CBO / RFC, and the library-level lenses still apply — branchy or tangled tests are still hard to read.
CLI surface #
- Common options:
--config,--reporter,--output,--root,--since,--explain,--snapshot,--coverage,--strict-dismiss,--concurrency,--limit,--auto-explain/--no-auto-explain,--fatal-warnings,--fatal-style,-v. dartrics --versionprints the build's version. The same string is exported asdartricsVersionfrompackage:dartrics/dartrics.dart.- Exit codes are sysexits-aligned: 0 success, 1 violations (with
--fatal-warnings), 64 usage, 65 data, 70 internal, 78 config.
Embedding #
lib/dartrics.dartis a deliberately tight Dart API: the eight function-level metric calculators (CyclomaticComplexity,CognitiveComplexity,MaxNestingLevel,NumberOfParameters,BooleanTrap,MethodLength,SourceLinesOfCode,HalsteadVolume), the calculator interface (FunctionMetric,FunctionMetricInput,MetricPolarity), anddartricsVersion. That's it. Report assembly, regression diff, coverage attachment, dismissal validation, snapshot persistence, the unused detector, and class- / library-level metrics are CLI-only — the JSON reporter is the supported on-wire format for those scopes. Keeping the public Dart surface small means internal evolution doesn't break consumers we don't yet have; if a Dart-level handle is missing for a real use case, please file an issue rather than reaching intopackage:dartrics/src/.example/main.dartshows a 30-line standalone embedding.
Performance #
- File resolution in
AnalyzerRunner.resolveAllusespackage:poolto run up to--concurrencyresolves in flight at once. Default mirrors the host CPU count (clamped to 16). Output ordering remains alphabetical so reports stay deterministic across runs. The win on smaller trees (≈50 files) is ≈10 % wall-time; larger codebases get more.
Tooling #
.github/workflows/analyze.yamlruns format / analyze / test on Ubuntu for every push and PR, then uploadscoverage:test_with_coverageoutput to Codecov where per-PR coverage gating happens.- 100% line coverage on
lib/is treated as a correctness signal — uncovered lines are read as evidence of dead code, not as a coverage gap.