🦀 Rigid Dart
Rust-grade guardrails for Dart & Flutter.
A custom_lint plugin that turns analyzer warnings into walls.
The Problem
Dart's compiler is lenient. It lets you ship layout crashes, untyped state,
hardcoded colors, deprecated APIs, and dynamic casts that Rust would
catch at compile time. AI agents make this worse — they generate code that
works but violates every architecture principle you've established.
The Solution
Rigid Dart is a custom_lint plugin that enforces 23 rules as hard
analyzer errors. Every rule uses TypeChecker-based type resolution —
it catches aliases, subclasses, and reexports, not just string names.
It also ships 3 quick fixes, a strict analysis_options.yaml base,
a configurable presets system, and an optional PATH wrapper that
blocks flutter run until your code is clean.
🦀 Rigid Dart gate -- analyzing before compile...
error - lib/home_page.dart:42:9
Colors.blue is banned. Use Theme.of(context).colorScheme.* instead.
rigid_no_hardcoded_colors
════════════════════════════════════════════════════════
❌ RIGID DART: Analysis failed. Fix violations above.
════════════════════════════════════════════════════════
The agent sees a build failure. It fixes the code. It retries. That's the loop.
Rules
Phase 1: Layout Safety
Catches runtime crashes before they happen.
| Rule | Sev | What it catches |
|---|---|---|
rigid_no_expanded_outside_flex |
🔴 | Expanded/Flexible outside Row/Column/Flex |
rigid_no_unbounded_column |
🟡 | Nested Column/ListView in scrollables without constraints |
rigid_constrained_text_field |
🔴 | TextField in Row without width constraint |
Phase 2: State Discipline
Bans anti-patterns. Prevents memory leaks and async crashes.
| Rule | Sev | What it catches |
|---|---|---|
rigid_no_set_state |
🔴 | Any setState() call (type-resolved: checks receiver is State<T>) |
rigid_no_change_notifier |
🔴 | ChangeNotifier subclass/mixin (catches via isAssignableFrom) |
rigid_exhaustive_async |
🔴 | Direct .value on async types without exhaustive handling |
rigid_no_build_context_across_async |
🔴 | BuildContext used after await without mounted check |
rigid_dispose_required |
🔴 | Disposable controllers (AnimationController, FocusNode, etc.) not disposed |
Phase 3: Architecture
Enforces design system usage. Bans magic values.
| Rule | Sev | What it catches |
|---|---|---|
rigid_no_hardcoded_colors |
🔴 | Color(0xFF...) or Colors.* outside theme definitions |
rigid_no_hardcoded_text_style |
🟡 | Raw TextStyle(fontSize: N) outside theme definitions |
rigid_no_magic_numbers |
🟡 | Literal numbers in layout params (padding, margin, etc.) |
rigid_require_tests |
🟡 | lib/ files without corresponding test/ files |
rigid_layer_boundaries |
🔴 | Cross-layer imports violating user-defined architecture |
rigid_no_direct_instantiation |
🔴 | Direct Repository/Service/Api/Client instantiation in widgets |
Phase 4: Freshness
Bans deprecated APIs. Enforces modern Dart.
| Rule | Sev | What it catches |
|---|---|---|
rigid_no_will_pop_scope |
🔴 | Deprecated WillPopScope → quick fix: PopScope |
rigid_no_with_opacity |
🔴 | Deprecated .withOpacity() → quick fix: .withValues(alpha:) |
rigid_no_dynamic |
🔴 | Explicit dynamic type annotations → quick fix: Object? |
rigid_no_print |
🟡 | print() calls in non-test code |
Phase 5: Quality
Prevents sloppy agent patterns.
| Rule | Sev | What it catches |
|---|---|---|
rigid_max_widget_lines |
🟡 | Widget classes exceeding configurable line threshold (default 250) |
rigid_no_raw_async |
🟡 | Async functions with await but no try/catch |
rigid_min_test_assertions |
🟡 | Test files with test blocks but zero expect() calls |
rigid_require_key_in_list |
🟡 | Widgets in list builders without explicit Key parameter |
rigid_no_hardcoded_strings |
🟡 | Hardcoded string literals in Text(), Tooltip(), InputDecoration |
Quick Start
1. Install
# pubspec.yaml
dev_dependencies:
custom_lint: ^0.8.1
rigid_dart: ^0.1.0
flutter pub get
2. Configure
# analysis_options.yaml
include: package:rigid_dart/analysis_options.yaml
analyzer:
plugins:
- custom_lint
3. Analyze
# Standard analysis (strict options from rigid_dart)
dart analyze --fatal-infos
# Custom rules (the 23 rigid_* rules)
dart run custom_lint
Quick Fixes
Three rules offer IDE quick fixes (lightbulb menu):
| Rule | Quick fix |
|---|---|
rigid_no_with_opacity |
Replace .withOpacity(x) → .withValues(alpha: x) |
rigid_no_will_pop_scope |
Replace WillPopScope → PopScope |
rigid_no_dynamic |
Replace dynamic → Object? |
Enforcement Tiers
| Tier | Name | Blocks | Agent can bypass? |
|---|---|---|---|
| 1 | Advisor | Nothing — IDE warnings only | ✅ Yes |
| 2 | Gatekeeper | git commit |
🔶 Can still run locally |
| 3 | Compiler | flutter run/build/test |
❌ No |
See AGENT.md for full setup instructions for each tier.
Suppressing Rules
Per-line only. Global suppression is not supported by design.
// ignore: rigid_no_hardcoded_colors
final debugColor = Colors.red; // Emergency escape hatch
For multi-line blocks:
// ignore_for_file: rigid_no_magic_numbers
// Only in spacing_tokens.dart where constants are DEFINED
const kCardPadding = 16.0;
const kSectionSpacing = 24.0;
Modifying Rules
Adding a new rule
- Create a file in
lib/rules/<phase>/(e.g.,lib/rules/state/no_global_keys.dart) - Extend
DartLintRulefromcustom_lint_builder - Define a
LintCodewith arigid_prefix and appropriate severity - Implement the
runmethod usingcontext.registry.add*callbacks - Register the rule in
lib/rigid_dart.dart
Example skeleton:
import 'package:analyzer/error/error.dart' show DiagnosticSeverity;
import 'package:analyzer/error/listener.dart' show DiagnosticReporter;
import 'package:custom_lint_builder/custom_lint_builder.dart';
class NoGlobalKeys extends DartLintRule {
const NoGlobalKeys() : super(code: _code);
static const _code = LintCode(
name: 'rigid_no_global_keys',
problemMessage: 'GlobalKey is banned. Use ValueKey or UniqueKey.',
errorSeverity: DiagnosticSeverity.ERROR,
);
@override
void run(
CustomLintResolver resolver,
DiagnosticReporter reporter,
CustomLintContext context,
) {
context.registry.addInstanceCreationExpression((node) {
// Your detection logic here
reporter.atNode(node, code);
});
}
}
Then add to lib/rigid_dart.dart:
import 'package:rigid_dart/rules/state/no_global_keys.dart';
// ...
const NoGlobalKeys(),
Changing severity
Edit the errorSeverity parameter in the rule's LintCode:
DiagnosticSeverity.ERROR— hard error (red squiggly)DiagnosticSeverity.WARNING— warning (yellow squiggly)DiagnosticSeverity.INFO— informational (blue squiggly)
Disabling a rule
Remove it from the getLintRules list in lib/rigid_dart.dart. Do not
comment it out — unused imports trigger their own warnings.
Testing changes
cd packages/rigid_dart
dart analyze # Must show 0 issues
Then test against a consumer project:
cd apps/your_app
flutter pub get # Picks up local changes via path dep
dart run custom_lint # Verify your rule fires
Shared Analysis Options
package:rigid_dart/analysis_options.yaml includes:
analyzer:
language:
strict-casts: true # No implicit dynamic downcasts
strict-inference: true # No implicit dynamic in generics
linter:
rules:
avoid_dynamic_calls: true
always_declare_return_types: true
prefer_final_locals: true
prefer_const_constructors: true
prefer_const_declarations: true
unawaited_futures: true
# ... and more
Projects that include: this file inherit all settings. Override
specific rules in your project's analysis_options.yaml under
linter: rules:.
For AI Agents
This repo includes AGENT.md — a machine-readable playbook for IDE agents (Cursor, Windsurf, Claude Code, GitHub Copilot, etc.). Give an agent this repo URL and it can install, configure, and verify rigid_dart with zero guesswork.
License
MIT — see LICENSE.
Libraries
- rigid_dart
- Rigid Dart — Rust-grade guardrails for Dart/Flutter.
- rules/architecture/layer_boundaries
- rules/architecture/no_direct_instantiation
- rules/architecture/no_hardcoded_colors
- rules/architecture/no_hardcoded_text_style
- rules/architecture/no_magic_numbers
- rules/architecture/require_tests
- rules/freshness/no_dynamic
- rules/freshness/no_print
- rules/freshness/no_will_pop_scope
- rules/freshness/no_with_opacity
- rules/layout/constrained_text_field
- rules/layout/no_expanded_outside_flex
- rules/layout/no_unbounded_column
- rules/quality/max_widget_lines
- rules/quality/min_test_assertions
- rules/quality/no_hardcoded_strings
- rules/quality/no_raw_async
- rules/quality/require_key_in_list
- rules/state/dispose_required
- rules/state/exhaustive_async
- rules/state/no_build_context_across_async
- rules/state/no_change_notifier
- rules/state/no_set_state