clean_architecture_linter 2.7.2
clean_architecture_linter: ^2.7.2 copied to clipboard
A comprehensive analysis_server_plugin that automatically enforces Clean Architecture principles in Flutter projects with Riverpod state management.
Clean Architecture Linter #
π°π· νκ΅μ΄ README | πΊπΈ English README
A comprehensive custom lint package that automatically enforces Clean Architecture principles in Flutter/Dart projects with Riverpod state management. Write code naturally while the linter guides you toward perfect Clean Architecture compliance with real-time feedback and actionable corrections.
β οΈ Note: This package is designed for projects using Riverpod for state management. Some presentation layer rules specifically validate Riverpod patterns.
β¨ Key Features #
- π‘οΈ Automatic Clean Architecture Protection - Write code freely, linter catches violations
- π― 34 Specialized Rules - Comprehensive coverage of all Clean Architecture layers
- π Flutter-Optimized - Built specifically for Flutter development patterns
- π¨ Riverpod State Management - Enforces 3-tier provider architecture (Entity β UI β Computed)
- π Educational - Learn Clean Architecture through guided corrections
- β‘ Real-time Feedback - Immediate warnings with actionable solutions
- π§ Zero Configuration - Works out of the box with sensible defaults
- π§ͺ Test-Aware - Smart exceptions for test files and development contexts
π Rules Overview (34 Rules) #
π Core Clean Architecture Principles (6 rules) #
- Layer Dependency - Enforces dependency direction (inward only)
- Domain Purity - Prevents external framework dependencies in domain layer
- Dependency Inversion - Validates abstraction-based dependencies
- Repository Interface - Ensures proper repository abstractions
- Circular Dependency - Prevents circular dependencies between layers
- Boundary Crossing - Validates proper layer boundary crossing
π― Domain Layer Rules (2 rules) #
- UseCase No Result Return - UseCases should return entities directly (pass-through pattern)
- Exception Naming Convention - Feature prefix for domain exceptions
πΎ Data Layer Rules (10 rules) #
- Model Structure - Freezed models with entity composition
- Model Field Duplication - No duplicate entity fields in models
- Model Conversion Methods - Required
toEntity()method in extensions - Model Naming Convention - Models must end with
Modelsuffix - DataSource Abstraction - Abstract interfaces for data sources
- DataSource No Result Return - DataSources throw exceptions
- Repository Implementation - RepositoryImpl must implement domain interface
- Repository Pass Through - Repositories return
Future<Entity>(warns on Result pattern) - Repository No Throw - Repositories use pass-through pattern (AppException types allowed)
- DataSource Exception Types - Use defined data layer exceptions only
- Model Entity Direct Access - Use
.toEntity()instead of direct.entityaccess
π¨ Presentation Layer Rules (14 rules) #
- No Presentation Models - Use Freezed State instead of ViewModels
- Extension Location - Extensions in same file as the class
- Freezed Usage - Use Freezed instead of Equatable
- Riverpod Generator - Use
@riverpodannotation - Presentation No Data Exceptions - Use domain exceptions only
- Presentation Use AsyncValue - Use AsyncValue for error handling (3-tier architecture)
- Presentation No Throw - No exception throwing in Presentation layer
- Widget No UseCase Call - Widgets should not call UseCases directly (use Providers)
- Widget Ref Read Then When - Avoid using .when() after ref.read() (anti-pattern)
- Riverpod Ref Usage - Use ref.watch() in build(), ref.read() in methods (with UseCase detection)
- Riverpod Ref After Async Gap - Advisory warning for ref.read/watch/listen/invalidate/refresh after await in provider methods (a preceding
if (!ref.mounted) return;guard suppresses it) - Riverpod Provider Naming - Provider functions must include type suffix (repository/usecase/datasource)
- Ref Mounted Usage - Avoid
ref.mountedin the UI layer (widgets/pages); inside a Notifier it is the recommended disposal guard and is not reported - Riverpod Keep Alive - Only use
keepAlive: truefor app-wide or app-lifetime state (auth, settings, cache, startup, listener, ...). Name/path heuristic; full keyword list and the// ignore+ reason convention for generic-noun caches in doc/EXAMPLES.md Β§9
π§ Cross-Layer Rules (1 rule) #
- Allowed Instance Variables - Enforces stateless architecture (UseCase/Repository/DataSource)
π Opt-in: Riverpod State After Async Gap #
Riverpod State After Async Gap (riverpod_state_after_async_gap) - Reports an unguarded state = β¦ write or state read after an await in provider methods, including state = await β¦, same-statement reads that run after an await (await foo() ?? state), and reads in bodies that run after a control-flow await (await for, if (await β¦), a pattern when guard that awaits). Receiver-first forms (state.foo(await x)) and locals/parameters named state are not reported; this.state and super.state still are (reads and writes alike). Riverpod 3 throws UnmountedRefException from both the state setter and getter once the provider is disposed, so the guard belongs right after the await: await β¦; if (!ref.mounted) return; (or final next = await β¦; if (!ref.mounted) return; state = next;). Disabled by default because the state = await idiom is common in existing apps; enable it per project:
# analysis_options.yaml
plugins:
clean_architecture_linter:
diagnostics:
riverpod_state_after_async_gap: true
π§ͺ Optional: Test Coverage Rule #
Test Coverage - Enforces test files for UseCases, Repositories, DataSources, and Notifiers (disabled by default)
π Implementation Guide: See CLEAN_ARCHITECTURE_GUIDE.md for detailed patterns and examples.
π¨ Riverpod State Management: See CLAUDE.md Β§ Riverpod State Management Patterns for 3-tier provider architecture guide.
π Quick Start #
π v2.0: Starting with
2.0.0-dev.1, this package runs on the officialanalysis_server_pluginβ nocustom_lintdependency, nopubspec_overrides.yamlworkaround. Lint runs directly viadart analyze(usedart analyze, notflutter analyzeβ see the warning below). Upgrading from a v1 (custom_lint) setup? Follow MIGRATION.md.
π Requirements #
- Dart SDK: 3.11.0+ (analyzer 14.3.0); 3.13.0+ when
riverpod_lintis enabled alongside - Flutter: 3.0+ (optional, for Flutter projects)
- Riverpod: Required for presentation layer rules (riverpod_generator recommended)
1. Enable the plugin #
# analysis_options.yaml
plugins:
clean_architecture_linter: ^2.0.0-dev.1
analyzer:
exclude:
- test/**
- "**/*.test.dart" # Exclude test files
- "**/*.g.dart" # Exclude generated files
- "**/*.freezed.dart" # Exclude Freezed files
- "**/*.mocks.dart" # Exclude mock files
Do not also add clean_architecture_linter to dev_dependencies when your
project uses analyzer-bound tools such as riverpod_lint. The ASP plugin is
resolved in its own synthetic package from the plugins: section, which avoids
forcing its analyzer constraints into your app's pub solve.
2. Run the linter #
dart pub get
dart analyze # Flutter projects too β use `dart analyze`, NOT `flutter analyze`
Locally, dart analyze reports the 34 default rules in its output (plus the
opt-in riverpod_state_after_async_gap, see the rule list above). In CI, a
single No issues found! is not proof the plugin answered β use the sentinel
gate in the warning below.
β οΈ Use
dart analyze, notflutter analyze.flutter analyzesilently dropsanalysis_server_plugindiagnostics (it stops collecting before the plugin publishes its results) and printsNo issues found!even when violations exist. This applies to CI and local runs alike. Root cause and a regression guard: docs/analysis/FLUTTER_ANALYZE_PLUGIN_LOSS.md.
dart analyze(Dart 3.13.2) has a milder form of the same race: it can return before the plugin isolate publishes, so a singleNo issues found!is not proof. For CI, gate on a sentinel violation that must always be reported: tools/lint_sentinel/README.md.
3. See it in action #
example/ is a runnable Dart project with a good_examples/ (0 warnings) and a bad_examples/ folder (2 intentional violations). Clone this repo, then:
cd example
dart pub get
dart analyze
Real output:
warning - lib/bad_examples/features/todo/data/models/todo_remote_model.dart:12:1 - Model name "TodoRemoteModel" should not include DataSource implementation "remote". This violates implementation independence. Rename to "TodoModel". Models should be independent of DataSource implementation. - model_naming_convention
warning - lib/bad_examples/features/todo/data/repositories/todo_repository_impl.dart:25:3 - Repository should NOT use Result pattern. Use pass-through pattern instead. Return Future<Entity> directly. Let errors pass through to AsyncValue.guard(). - repository_pass_through
In VS Code / Android Studio / IntelliJ, the same two warnings appear as inline squiggles in the editor and as entries in the Problems panel β no extra setup beyond the Dart/Flutter extension. Hovering a squiggle shows the problem message; the correction message underneath tells you the fix (there is no auto-fix quick action yet, since these rules don't register an analyzer CorrectionProducer). Each file under example/lib/bad_examples/ links to its fixed counterpart in example/lib/good_examples/ in a header comment.
Recommended team profile #
- Local:
docs/config/lint_profile_balanced.yaml - CI:
docs/config/lint_profile_strict.yaml
See docs/config/RECOMMENDED_SETUP.md for details.
π§© Compatibility β analyzer 14 / Riverpod 3+ #
Verified consumer matrix (2026-09-04, maintainer SDK + flagship ittae lockfile):
| Line | Version |
|---|---|
| Flutter | 3.47.2 (Dart 3.13.2) |
riverpod / flutter_riverpod |
3.3.2 |
riverpod_generator / riverpod_annotation |
4.0.4 / 4.0.3 |
riverpod_lint (resolved in the plugin host from ^3.1.3) |
3.1.9 |
pub.flutter-io.cn latest Riverpod 3.4.2 is not in this verified set.
The package runs on the official analysis_server_plugin (>=0.3.22 <0.3.23), which pins analyzer 14.3.0; the declared analyzer range >=14.3.0 <15.0.0 therefore resolves to 14.3.0 today and is kept open for the next ASP bump. analysis_server_plugin 0.3.20 fixed the "dart analyze could spin indefinitely" hang that kept 2.4.x on 0.3.15 / analyzer 13. Coexistence with riverpod_lint in one plugin synthetic package requires riverpod_lint 3.1.9 (analyzer >=13.0.0 <15.0.0, Dart >=3.13.0); on Dart 3.12.x a consumer whose riverpod_lint constraint still admits 3.1.8 or older (for example ^3.1.3) is silently resolved back to clean_architecture_linter 2.4.0, while ^3.1.9 fails to solve outright below Dart 3.13 because of its SDK bound. Verified 2026-09-04 on Dart 3.13.2: the flagship ittae tree (with riverpod_lint ^3.1.3) analyzes in 29β40 s (2.4.0: 20β38 s) with no hang across three consecutive runs.
This line also closes a silent-loss bug in the 2.4.x line. With analysis_server_plugin 0.3.15 the plugin reported idle to the analysis server between its first file and its full pass, so dart analyze (which stops collecting at the first idle status) returned with zero or partial plugin diagnostics and exit code 0: on this package's fixtures 11 of 20 runs (2-row fixture) and 7 of 10 runs (16-row fixture) lost rows, and 15 of 15 did under parallel load. analysis_server_plugin 0.3.16 reworked the plugin server so its status follows the analysis driver; on 0.3.22 the same fixtures delivered every row in 55 of 55 dart analyze runs, including 15 under the same load. See tools/verify_analyze_parity.sh for the CI guard that still verifies delivery.
riverpod_lint 3.1.x carries its own analyzer constraints. Keep analyzer plugins out of dev_dependencies and enable both tools through top-level plugins: when you need them in one consumer project. The analyzer plugin manager resolves all enabled plugins in one synthetic package, so this package keeps its analyzer range aligned with riverpod_lint:
plugins:
clean_architecture_linter: ^2.4.0
riverpod_lint: ^3.1.9
The v1
custom_lintupstream (invertase/dart_custom_lint) was archived in May 2026. v2.0 moves fully to the official plugin, so the oldpubspec_overrides.yamlbridge is no longer needed β delete it when upgrading.
ποΈ Configuration #
Optional: Test Coverage #
In v2.0, rule severity is controlled with the standard analyzer errors: map, keyed by each rule's diagnostic name. Promote a rule to an error, downgrade it to a hint, or silence it:
# analysis_options.yaml
analyzer:
errors:
repository_interface: error # treat as build-breaking
riverpod_keep_alive: ignore # silence
The opt-in
clean_architecture_linter_require_test(test coverage) rule is not bundled in2.0.0-dev.1. It will be re-introduced in a later v2 pre-release; track the CHANGELOG.
π¦ Usage #
Folder Structure #
Organize your Flutter project following Clean Architecture:
lib/
βββ {feature_name}/
β βββ domain/
β β βββ entities/
β β βββ repositories/
β β βββ usecases/
β βββ data/
β β βββ datasources/
β β βββ models/
β β βββ repositories/
β βββ presentation/
β βββ providers/
β βββ widgets/
β βββ pages/
Running the Linter #
# Run the linter (rules are included in the analyzer output)
dart analyze # Flutter projects too β NOT `flutter analyze` (drops plugin diagnostics)
β οΈ In Flutter projects run
dart analyze, notflutter analyze. See docs/analysis/FLUTTER_ANALYZE_PLUGIN_LOSS.md.
IDE Integration #
The linter works automatically in:
- VS Code with the Dart/Flutter extensions
- IntelliJ IDEA / Android Studio with Flutter plugin
π Examples #
β Good Examples #
Domain Entity (Immutable)
// lib/domain/entities/user_entity.dart
class UserEntity {
final String id;
final String name;
final String email;
const UserEntity({
required this.id,
required this.name,
required this.email,
});
bool isValidEmail() {
return email.contains('@');
}
}
Data Model with Database (ObjectBox Example)
// lib/data/models/user_model.dart
import 'package:objectbox/objectbox.dart'; // β
Allowed
@Entity() // β
Database annotation instead of @freezed
class UserModel {
@Id()
int id = 0;
String name;
String email;
UserModel({required this.name, required this.email});
// β
Private database access is allowed
static Box<UserModel> get _box => objectBoxService.store.box<UserModel>();
// Conversion method
UserEntity toEntity() => UserEntity(
id: id.toString(),
name: name,
email: email,
);
}
Note: When using database libraries (ObjectBox, Realm, Isar, Drift), Models are mutable and use database-specific annotations instead of
@freezed. This is an exception to the standard Freezed pattern.
Repository Interface
// lib/domain/repositories/user_repository.dart
abstract class UserRepository {
Future<UserEntity> getUser(String id);
Future<void> saveUser(UserEntity user);
}
UseCase with Single Responsibility
// lib/domain/usecases/get_user_usecase.dart
class GetUserUseCase {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<UserEntity> call(String userId) {
return repository.getUser(userId);
}
}
β Bad Examples (Will be flagged) #
Mutable Domain Entity
// β This will be flagged by entity_immutability
class UserEntity {
String name; // Non-final field
void setName(String newName) { // Setter in entity
name = newName;
}
}
Domain Layer with External Dependencies
// β This will be flagged by domain_purity
import 'package:http/http.dart'; // External framework import
class UserEntity {
final String name;
}
UI with Direct Business Logic
// β This will be flagged by business_logic_isolation
class UserWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Business logic in UI layer - WRONG!
final isValid = email.contains('@') && email.length > 5;
return Text(isValid ? 'Valid' : 'Invalid');
}
}
Repository Using Result Pattern
// β This will be flagged - use pass-through pattern instead
class UserRepositoryImpl implements UserRepository {
@override
Future<Result<UserEntity, Failure>> getUser(String id) async {
try {
final model = await dataSource.getUser(id);
return Success(model.toEntity());
} catch (e) {
return Failure(UserFailure.fromException(e));
}
}
}
// β
Correct: Pass-through pattern
class UserRepositoryImpl implements UserRepository {
@override
Future<UserEntity> getUser(String id) async {
final model = await dataSource.getUser(id); // Errors pass through
return model.toEntity();
}
}
Layer Dependency Violation
// β This will be flagged by avoid_layer_dependency_violation
// In domain layer file:
import 'package:myapp/data/models/user_model.dart'; // Domain importing Data!
class UserEntity extends UserModel { // Wrong dependency direction
// ...
}
Missing Exception Prefix
// β This will be flagged by ensure_exception_prefix
class NetworkException extends Exception { // Should be UserNetworkException
// ...
}
π Common Patterns #
Pass-through Error Handling (Recommended)
// β
Good: Pass-through pattern
// DataSource throws AppException
class UserRemoteDataSource {
Future<UserModel> getUser(String id) async {
try {
final response = await client.get('/users/$id');
return UserModel.fromJson(response.data);
} on DioException catch (e) {
throw e.toAppException(); // Convert to AppException
}
}
}
// Repository passes through (no try-catch)
class UserRepositoryImpl implements UserRepository {
@override
Future<UserEntity> getUser(String id) async {
final model = await dataSource.getUser(id); // Errors pass through
return model.toEntity();
}
}
// UseCase adds business validation
class GetUserUseCase {
Future<UserEntity> call(String id) {
if (id.isEmpty) {
throw const InvalidInputException.withCode('errorValidationIdRequired');
}
return repository.getUser(id); // Pass-through
}
}
// Presentation uses AsyncValue.guard()
@riverpod
class UserNotifier extends _$UserNotifier {
@override
Future<User> build(String id) => ref.read(getUserUseCaseProvider)(id);
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(getUserUseCaseProvider)(id));
}
}
Proper Exception Naming
// β
Good: Proper exception prefixes
class UserNetworkException extends Exception {
final String message;
UserNetworkException(this.message);
}
class UserValidationException extends Exception {
final String field;
UserValidationException(this.field);
}
For more detailed examples and explanations, see our comprehensive Examples Guide.
π οΈ Development #
Project Structure #
clean_architecture_linter/
βββ lib/
β βββ src/
β β βββ rules/
β β βββ domain_rules/
β β βββ data_rules/
β β βββ presentation_rules/
β βββ clean_architecture_linter.dart
βββ example/
βββ test/
βββ README.md
Contributing #
- Fork the repository
- Create a feature branch
- Add tests for new rules
- Format your code:
dart format . - Ensure all tests pass
- Submit a pull request
See CONTRIBUTING.md for detailed guidelines.
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π Support #
- β Star this repository if it helped you!
- π Report bugs
- π‘ Request features
- π Read the documentation
π― Roadmap #
- β Configuration system for custom naming patterns
- β Support for multiple state management solutions
- β Integration with CI/CD workflows
- β Custom rule creation guide
- β Performance optimizations
Made with β€οΈ for the Flutter community