loam 0.1.13
loam: ^0.1.13 copied to clipboard
Codebase intelligence for Dart & Flutter: unused public API, circular deps, code duplicates, complexity hotspots, empty catch, unjustified ignores, narrative comments and a11y.
Changelog #
0.1.13 #
- Fix:
ignoreglobs and the gate now suppress correctly no matter which directory you run loam from. Path-based suppression matched the ignore globs against a path that was accidentally resolved relative to the current working directory, so running loam from a directory other than the analysed project root — e.g.loam gate --project-root packages/appfrom a repo root, or any CI job whose working directory differs from--project-root— silently dropped allignore-glob suppressions. Findings you had deliberately excluded could reappear and fail the gate. Suppression is now matched against each finding's project-relative path directly and is independent of the process working directory. Inline// loam-ignore:suppression was unaffected.
0.1.12 #
loam.yamlis now discovered by walking up to your git repository root — and monorepo configs layer. Previously loam only read aloam.yamlsitting exactly at the analysed project root. Now it searches upward from that root to the enclosing git repository root (the directory holding.git), just like Dart's ownanalysis_options.yaml— so a config at your repo root is honoured even when you analyse a sub-package. Everyloam.yamlon the way is layered farthest→nearest: a repo-root file provides shared defaults and a nearer (per-package) file builds on top of it. Merge rules, nearer wins:rulesare merged per ruleId,ignorelists are concatenated (farther first, de-duplicated), andsource_dirs/update_check/a11yoverride while an omitted key inherits the farther value.ignoreglobs are still matched relative to the analysed project root, regardless of which layer declared them. The walk is bounded at the git root, so discovery stays repo-relative and reproducible across machines and CI — aloam.yamlabove the repository (e.g. in$HOME) is never picked up. With no git root, only<projectRoot>/loam.yamlis read. Fully backward compatible: a singleloam.yamlat the project root yields exactly the previous result.
0.1.11 #
- New: accessibility audits for Flutter — the
loam a11ycommand and four WCAG-grounded rules. loam.dev now ships ana11yrule category and a dedicatedloam a11ycommand (plus a--no-a11yopt-out on the broader scans) that flag missing accessible names and labels in Flutter widget trees. Every rule analyses the resolved element model — local classes that merely share a name with a Flutter widget are never flagged — and stays deliberately AST-only (runtime contrast, focus order and touch-target sizes are out of scope):a11y-image-label(WCAG 1.1.1) —Image/Image.asset/Image.network/.file/.memorywith nosemanticLabeland noexcludeFromSemantics: true; an explicit empty label still counts as missing.a11y-icon-button-label(WCAG 1.1.1 / 4.1.2) —IconButtonwith notooltipand no enclosingSemantics(label: …), plusGestureDetector/InkWellwhose direct child is a bareIcon.a11y-form-field-label(WCAG 1.3.1 / 3.3.2 / 4.1.2) —TextField/TextFormFieldwith neither anInputDecoration(labelText: …)(norhintTextas a fallback) nor an enclosingSemantics(label: …).a11y-interactive-semantics(WCAG 4.1.2) — generic and custom interactive widgets carryingonTap/onPressed/onLongPressbut noSemantics(label: …)ancestor. Flutter built-ins already covered by the sibling rules are excluded, so the two never double-report.
- New: the
loam slopcommand and the first three deterministic anti-AI-slop rules. A newsloprule category and aloam slopcommand surface the low-effort patterns AI agents commonly leave behind. All three are deterministic AST/token checks (no LLM, no token cost), skip generated files automatically, and are suppressible per project vialoam.yamlor inline with// loam-ignore: <rule> – <reason>:slop-unjustified-ignore(info) —// ignore:and// ignore_for_file:directives with no written justification, inline or on the preceding line.slop-empty-catch(warning) —catchblocks whose body is empty or contains only comments. Extends the built-inempty_catcheslint to the comment-only variant; bodies with arethrow,throwor any logging call are never flagged.slop-narrative-comment(info) —//comments immediately before a declaration that merely restate its name (// buildbeforevoid build()) or use a fixed narrative phrase (constructor,getter,setter,build method). Deliberately narrow:///Dartdoc and informative comments are never flagged — precision over recall.
- All seven new rules run in
loam scan,gateandbaselineand are visible in every reporter (human,json,sarif,markdown,html), with stable, line-shift-robust fingerprints so the baseline only reacts to genuinely new findings.
0.1.10 #
- New rule:
code-duplicates— loam.dev now finds copy-pasted and structurally identical code across your whole project. It enumerates code blocks over the resolved AST, normalises each into a token sequence, and clusters recurring blocks project-wide with a Rabin-Karp rolling hash. Every cluster becomes exactly one Finding (Severity.warning) that lists all locations and points at the first occurrence, with a stable Fingerprint so the Baseline survives line shifts and only reacts when a copy is added or removed. Generated files are excluded, and a conservative minimum-token threshold keeps short boilerplate out. Likecomplexity-hotspots, it scanslib/andbin/by default —test/,tool/andexample/duplicates are not reported unless you widensource_dirsinloam.yaml. It catches Type-1 (exact, whitespace- and comment-independent) and Type-2 (renamed identifiers and literals) duplicates; gapped duplicates are out of scope. The rule runs inloam scan,gateandbaselineand is visible in every reporter (human,json,sarif,markdown,html); suppress it per project vialoam.yamlor inline with// loam-ignore: code-duplicates. - Fewer false positives on Flutter widget code. The detector uses consistent (alpha) renaming — each distinct identifier and literal gets a stable per-block slot, preserving the repetition pattern — plus a distinct-token-type gate. Together they stop reporting structurally identical but semantically distinct widget boilerplate (parallel factories, builders and state widgets) as duplicates, while still catching genuine renamed copies. The hardening was validated on a real-world Flutter codebase: precision in production code improved with zero new missed duplicates.
0.1.9 #
unused-public-exportsnow reads the stack it runs on — far fewer false positives on real Dart and Flutter projects. The rule used to judge a throwaway app and a published package the same way, and it recognised only a handful of code-gen frameworks, so on real codebases it flagged deliberately public API and generated or annotated symbols as "dead". Three changes fix that. All of them read from the resolved element model (never source text), and each one only ever removes a false positive — upgrading can lower your finding count but never raises it, so the baseline/ratchet gate never paints a project red:- App vs. publishable package. loam reads the target's
pubspec.yamlonce at load. On a publishable package (anything withoutpublish_to: none) the deliberately public top-levellib/API is no longer reported as unused — that surface exists to be consumed from outside — while genuinely deadlib/src/internals are still reported. An app (publish_to: none) behaves exactly as it did before. Publishability is conservative: when it cannot be determined, loam assumes publishable. - Two more visibility annotations. Public members marked
@internal(package:meta) or@visibleForOverridingare no longer reported, joining the existing@visibleForTestingand@pragma('vm:entry-point')exemptions. - A wider code-gen registry. Symbols produced or annotated by six more
generators count as code-gen input instead of dead API:
injectable/get_it,auto_route,mockito, Isar, ObjectBox / floor and Hive — plus the generated suffixes*.gr.dart,*.config.dart,*.mocks.dartand*.pb.dart, on top of the existing*.g.dart/*.freezed.dart.
- App vs. publishable package. loam reads the target's
- New: a
stack:line inloam scan. Human-format scans print a one-line stack profile — the detected code-gen generators, whether it is a Flutter project, and publishability — so you can see at a glance what loam inferred about your project. It is purely diagnostic: it feeds the report and the publishable-vs-app decision but never makes a suppression call on its own. JSON, SARIF, HTML and Markdown output are unchanged.
0.1.8 #
- Complexity now scans
bin/too, and the scope is configurable. Thecomplexity-hotspotsrule andloam healthmeasured onlylib/, silently skippingbin/entrypoint logic (real, shipping code). They now measurelib/andbin/by default. Configure it per project withsource_dirsinloam.yaml— e.g. addtest/toolto widen, or narrow tolibonly. Generated files stay excluded regardless;test/,example/andtool/remain off by default (intentionally high or low complexity = noise). The structural rulescircular-dependenciesandunused-public-exportsare an inherentlib/concept and are deliberately unaffected. - Channel-aware update notice. The once-a-day update line now prints the
upgrade command that matches how loam was actually installed —
brew upgrade loamfor a Homebrew binary,dart pub global activate loamotherwise. This prevents the trap where a Homebrew user follows the generic pub.dev advice and installs a second, unreachable~/.pub-cachecopy that never shadows the Homebrew binary, so the "update" silently does nothing. - New:
loam --version. Prints the running version, the install channel and the resolved executable path (e.g.install: homebrew · …/Cellar/loam/…). Use it to confirm an upgrade actually landed on the binary on yourPATH. The version comes from the same constant the report footer uses, so the two can never disagree. The Homebrew formula'sbrew testnow asserts this version, catching a stale or mis-rendered formula. - Reports now surface suppression and scan scope. A
0 findings — cleanresult was indistinguishable from "nothing scanned" or "real findings knowingly suppressed". Every reporter now shows how many findings were suppressed (// loam-ignore:+loam.yamlglobs) and a scope line — files analysed (and how many underlib/), lines, and which rules ran — so a clean scan can be trusted as comprehensive. JSON is nowschemaVersion: 3with asummary.suppressedcount and ascanobject; SARIF carries the same in a run-level property bag; human/Markdown gain a scope line; the HTML report shows scope rows and a suppressed note. Findings and fingerprints are unchanged, so baselines do not churn.
0.1.7 #
- Fixed: Flutter SDK no longer crashes the scan. On a standard Flutter
install only
flutter/binis onPATH, wheredartis a wrapper script; loam resolved the SDK two levels up to the Flutter checkout root (which has nolib/_internal) and the analyzer crashed with a rawPathNotFoundException. loam now redirects into<flutterRoot>/bin/cache/dart-sdk. When no usable SDK can be found at all, loam prints an actionable message (setDART_SDK=…) and exits 78 instead of dumping a stack trace. - Agent-proof findings: every finding now carries a
kindand aremedy. loam's output is read by AI agents as well as humans, and a bare fact invites mis-triage.complexity-hotspotsnow distinguishesflutter-widget-buildfromlogic;circular-dependenciesstates the cycle is real regardless of any platform/strategy factory layered on top; every finding names the concrete next action. JSON output is nowschemaVersion: 2withkindandremedyas first-class fields; SARIF carries them in the property bag; Markdown gains a Remedy column. Fingerprints are unchanged, so baselines do not churn.
0.1.6 #
- New rule:
complexity-hotspots. Measures cyclomatic and cognitive complexity per executable over the resolved AST and flags hotspots that breach documented, conservative defaults (cyclomatic > 20 or cognitive > 30; a cyclomatic-only breach also requires cognitive > 5, so flat lookup tables and generated-style boilerplate stay quiet). Findings carry a stable fingerprint anchored on the qualified symbol name, so line shifts don't churn the baseline. Suppress individually with// loam-ignore: complexity-hotspots – <reason>. - New command:
loam health. A diagnostic aggregate view — Health-Score (0–100), Grade (A–F) and a descending hotspot table — rendered by its own terminal renderer. It always measures (independent of thecomplexity-hotspotsrule toggle) and is a report, not a gate: exit 0 even on a low score, exit 64 on a usage error. - New rule:
circular-dependencies. Detects import cycles across first-party Dart libraries (Tarjan strongly-connected components over the import graph); export-only re-exports are excluded so barrel files don't false-positive. - Positional project path.
scan,gate,health,initandbaselinenow accept the project root as a positional[path];-p/--project-rootoverrides it. - Fewer false positives. Flutter
gen-l10noutput is treated as generated and excluded from analysis. - Update notice (opt-out). Prints a one-line stderr notice (at most once a
day) when a newer release is on pub.dev; silence it with
--no-update-check, theLOAM_NO_UPDATE_CHECKenv var, orupdate_check: falseinloam.yaml. CI is always silent. - HTML report shows a Health-Score badge. When
loam scanemits HTML, a Score + Grade badge is rendered in the report head via a sidecar; the other formats and theReportPayloadcontract are unchanged. --format htmlnow writes a file and opens it. Instead of streaming to stdout, the HTML report is written toloam-report.html(override with--output <file>) and opened in the default browser on interactive runs. Auto-open is suppressed for piped/redirected output and under CI;--no-openskips it explicitly. The other formats (human/sarif/json/markdown) still stream to stdout unchanged.- Redesigned HTML report. Two-column layout with a sticky side panel that keeps the severity summary, the toolchain facts and the always-visible Fix-Prompt in view — no more scrolling to the page foot. Adds an inline brand mark, a terminal-styled masthead, a live search/filter across file, rule and message, and per-finding rule IDs that deep-link to the matching rule on getloam.dev. Footer and masthead link to the website, the repository and the sponsor page. The reporter stays a pure, byte-identical renderer.
0.1.4 #
- Documentation release (no functional changes). Adds an
example/so the pub.dev package page renders a usage example, and documents the full public API surface (Finding,Severity,Rule,ProjectLoader, …). Thepublic_member_api_docslint now keeps public-API documentation complete.
0.1.3 #
--format markdownis now available: a portable Markdown report alongsidehuman,sarifandjson.--format htmlis now available: a single self-contained, offlineloam-report.html(inline CSS/JS, no server, no CDN). Browse findings by rule, severity and file; select findings and copy a deterministic, versioned (prompt@ver) fix-prompt for an AI coding agent. The report is a pure renderer — no thresholds, no LLM calls. Redirect stdout:loam scan --format html > loam-report.html.- User-driven suppression via
loam.yaml. A project-rootloam.yamlnow drives per-rule toggles (rules:) and path-basedignore:globs (project- relative). A disabled rule is not run at all and changes therulesetVersion; glob-ignored files drop out of the audit entirely. Suppression acts before the baseline/gate and never fills the baseline or trips the gate. - Inline suppression
// loam-ignore: <ruleId> – reason. Suppress a single finding at its source on the same or immediately preceding line, via the analyzer comment model (not text regex). The reason is mandatory; only the named rule is suppressed. Distinct from the existing automatic codegen-input handling. loam initscaffolds a commentedloam.yaml(therules:+ignore:schema) and refuses to overwrite an existing file (exit 1).
0.1.2 #
--format jsonis now available: a stable, machine-readable JSON report (schema-versioned, with tool/ruleset metadata, summary counts and findings) for agent and tooling integration.markdownandhtmlremain on the roadmap.- Correct version everywhere. The tool version is now sourced from the
pubspec via a single in-code constant (
loamVersion) instead of a hard-coded string, so the scan footer and SARIF/JSON tool block always match the release. Adocs-attestcheck makes version drift across code and docs impossible.
0.1.1 #
- Run as a compiled binary. The analyzer needs a real Dart SDK at runtime;
as an AOT executable (Homebrew) none sits beside the binary, so
loam scancrashed with aPathNotFoundException. loam now resolves the SDK explicitly (DART_SDK, the Dart VM, or adartonPATH) and passes it to the analyzer — works both as a pub global binary and via the Homebrew tap. - Homebrew install (Apple Silicon macOS & Linux):
brew install silvio-l/loam/loam.
0.1.0 #
First functional preview. The walking-skeleton pipeline is real end to end:
loam scan— full audit via the tracer ruleunused-public-exports: finds project-wide unused public API (exports, classes, methods, getters/setters, fields) on the resolved Dart element model, not regex.loam baseline --write— freeze the accepted state tobaseline.json.loam gate— baseline/ratchet gate (default) and--absolutemode, with exit codes for CI.- Reporters —
human(default) andsarif(valid 2.1 for code-scanning). - Hardened against false positives on real Riverpod/Drift codebases (part-file references, extensions, setter writes, pattern destructuring).
Not yet implemented (wired in --help as "coming soon"): health, slop,
init, fix, and the json / markdown / html reporters. See the roadmap
in the repository.
0.0.2 #
- Polished pub.dev page: logo, badges and clearer sections in the README. No functional changes (still early development).
0.0.1 #
- Initial preview release (name reservation). Walking-skeleton CLI:
loam scan,loam gate,loam baselineare wired as stubs; the analysis pipeline and the tracer rule (unused-public-exports) are in active development. Not yet functional — see the roadmap in the repository.