Skip to content

Add hedgehoglab_lints analyzer plugin (STATE rules and metric limits) - #2

Open
hedgehogowainbrown wants to merge 12 commits into
mainfrom
feature/analyzer-plugin
Open

hedgehogowainbrown wants to merge 12 commits into
mainfrom
feature/analyzer-plugin

Conversation

@hedgehogowainbrown

@hedgehogowainbrown hedgehogowainbrown commented Oct 9, 2026 •

Copy link
Copy Markdown

What and why

Adds plugin/hedgehoglab_lints/, an analyzer plugin (analysis_server_plugin 0.3.24, Dart 3.10+) so dart analyze itself enforces code-metric limits and the first mechanically checkable rules from flutter-standards 04-state-management.md. Nothing is read into an agent's context until a rule is violated. Seven warning rules, all prefixed hl_:

  • Standards: STATE-005 hl_notifier_suffix, STATE-007 hl_notifier_void_methods, STATE-002 hl_no_static_singleton (conservative heuristic, documented).
  • Metrics: hl_max_function_length, hl_max_cyclomatic_complexity (20), hl_max_nesting (5), hl_max_parameters (4).

Metrics are reimplemented because dart_code_linter 4.4.2 exposes its metrics only through its own CLI, not dart analyze (a function breaking every limit gave no warnings under dart analyze). Its lint rules can still run alongside.

Owner decisions applied

  1. Function length: a widget build keeps 50 source lines; every other function or method gets 30. A widget build is a non-static method named build whose resolved return type is Widget (from package:flutter) or a subtype (covers StatelessWidget, State, ConsumerWidget). A Riverpod notifier build() returns state and gets 30. Messages say which limit applied ("over the widget build limit of 50" / "over the function limit of 30").
  2. Exceptions: // ignore: hl_<rule> stays the mechanism. The README states that in projects using the agentic toolkit an added // ignore: fails the switch-off check unless a human applies the rule-exception label to the PR (implemented in the toolkit, not here).
  3. Policy (README, Versioning): violations are fixed when a project adopts the plugin or a rule or limit changes; no baselines, no tolerated trailing warnings. A new rule or tighter limit is a minor bump and its release notes must say adopters fix all resulting violations in the upgrade PR.
  4. Quilltrail-matching IDs and pinning: standards.lock.json records the catalogue (flutter-standards 67839f58, tag feat/quilltrail-catalog-reference, policy 2026.09.2) and, per rule, {file, title, content_hash, lints} for STATE-002/005/007. The README now says IDs are Quilltrail catalogue IDs at the pinned revision and each lint is pinned to the rule's content hash.
    • Quilltrail's content_hash is SHA-256 of title\ntag\ntrimmed body (resolve_standards.dart, hashRuleContent). I reproduced all three hashes from the pinned commit's markdown and they match bnf-flutter's provenance.json.
    • tool/verify_standards_pin.dart recomputes each hash and exits 1 on a mismatch (a standard changed, so re-review the lint). Modes: default fetches the pinned commit with gh; --standards-dir DIR; --provenance FILE (also checks commit and policy version).
    • test/standards_lock_test.dart (offline): every standards rule class ID is in the lock, every lock entry names registered lints, plus unit tests of the hash algorithm.

Adoption cost (new limits, plugin run over the exported BNF PR trees)

Rule bnf PR 16 bnf PR 17
hl_max_function_length, widget build over 50 5 2
hl_max_function_length, other function over 30 8 5
hl_notifier_suffix 0 2
hl_notifier_void_methods, hl_no_static_singleton, complexity, nesting, parameters 0 0

No notifier build() was over 30 in either tree. Hits of the 30-line limit:

  • PR 16: catalogue/lib/catalogue/components.dart:componentsCategory (36), components/monograph.dart:monographComponents (63), components/navigation.dart:navigationComponents (58), foundations.dart:foundationsCategory (32), pages.dart:pagesCategory (59), catalogue/lib/synthetic_content_styles.dart:forElement (82), lib/core/firebase/firebase_bootstrap.dart:bootstrapFirebase (34), lib/features/content/data/schema/content_database.dart:open (34).
  • PR 17: firebase_bootstrap.dart:bootstrapFirebase (34), content_database.dart:open (34), features/app/routing/go_router_tab_navigator.dart:_pagesForMatches (35), features/navigation/data/demo_navigation_repository_impl.dart:_resolve (31) and _content (49).

Verification

  • dart format --set-exit-if-changed . (plugin and repo root), dart analyze: clean. CI: both jobs pass.
  • dart test: 48 passed. Function length has positive/negative/edge cases for both limits (30 and 31 lines; widget build at 50 and 51; State and indirect-ConsumerWidget builds; notifier build() at 30 and 31; _buildBody() helper; non-widget build).
  • Example project: dart analyze reports exactly 8 warnings in violations*.dart (one per rule, plus a 31-line notifier build() under the 30 limit); clean.dart and stubs.dart report none. CI greps both length messages.
  • verify_standards_pin.dart passes in all three modes against the pinned commit and bnf-flutter's provenance.json, and fails with a clear message on an altered hash.

Findings that decide adoption

  • A plugins: block in an include:d file was honoured by dart analyze on Dart 3.13, but the docs say it is unsupported: put the block in each project's own analysis_options.yaml.
  • flutter analyze is unreliable with plugins on 3.47.0 (plugin warnings in 3 of 6 runs); gate on dart analyze.
  • riverpod_lint: ^3.1.9 loads alongside.

Open points

  • The CI "Standards pin" step runs only when a STANDARDS_READ_TOKEN secret (read access to flutter-standards) exists; without it the lock is checked by dart test only.
  • Plugin version stays 0.1.0 (nothing tagged yet); the first release note must carry the fix-all-violations line.

Out of scope

Per-project thresholds (the plugin API offers on/off only), quick fixes, further standards, tagging a release, and the toolkit-side rule-exception check.

Trial adoption fixes

Defects found adopting this in bnf-flutter, fixed in separate commits:

  • The plugin analysed files outside the package: isInLibDir accepts any lib segment, so a Swift Package Manager checkout under build/ produced 18 hl_max_parameters warnings. It now analyses only <package root>/lib/**, and skips build/, .dart_tool/ and packages nested inside them. Tests added (test/scope_test.dart). This also fixed the generated-file skip, which never applied because it read currentUnit (null while rules register).
  • Flutter 3.47's flutter pub get rewrites a project's analysis_options.yaml unless the project's own file lists seven excludes (build/** and the six platform directories); it does not follow include:. The README now shows the exact recommended project file.
  • The README recommends dart analyze --fatal-infos throughout, since flutter analyze 3.47 drops plugin diagnostics intermittently, and says the plugin tag is created on release.
  • The README notes that riverpod_lint is removed from pubspec.yaml once listed under plugins:.

🤖 Generated with Claude Code

hedgehogowainbrown and others added 4 commits October 9, 2026 14:47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hedgehogowainbrown
hedgehogowainbrown requested a review from a team as a code owner October 9, 2026 15:22
hedgehogowainbrown and others added 8 commits October 9, 2026 16:23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root job ran dart analyze over the whole repository, which now
includes the plugin package; without its dependencies resolved that
reported 380 unresolved-type issues. The plugin has its own workflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atalogue

Widget build methods keep 50 source lines; every other function gets 30.
Add standards.lock.json (catalogue revision and per-rule content hash), a
offline test that the lock and registered rules agree, and
tool/verify_standards_pin.dart, which recomputes Quilltrail's content_hash.
Document exceptions and the fix-on-change versioning policy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ersions

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
shouldAnalyse used RuleContext.isInLibDir, which accepts any path with a lib
segment, so a Swift Package Manager checkout under build/ was reported. Resolve
against the package root and skip build/ and .dart_tool/ (including a package
nested inside another's output). Read the defining unit: currentUnit is null
while processors register, so the generated-file skip never applied.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ject file

flutter pub get rewrites analysis_options.yaml to add them unless the project's
own file lists them; it does not follow include:. Show the exact recommended
project file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s created on release

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant