clean_architecture_linter 2.1.0
clean_architecture_linter: ^2.1.0 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
- Riverpod Provider Naming - Provider functions must include type suffix (repository/usecase/datasource)
- Ref Mounted Usage - Avoid
ref.mounted(use AsyncValue or complete async before navigation) - Riverpod Keep Alive - Only use
keepAlive: truefor global state (auth, settings, cache)
π§ Cross-Layer Rules (1 rule) #
- Allowed Instance Variables - Enforces stateless architecture (UseCase/Repository/DataSource)
π§ͺ 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/flutter analyze. Upgrading from a v1 (custom_lint) setup? Follow MIGRATION.md.
π Requirements #
- Dart SDK: 3.10.0+
- 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: flutter analyze
That's it! The 34 rules are reported directly in your dart analyze / flutter analyze output.
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 9-13 / Riverpod 3+ #
v2.0 runs on the official analysis_server_plugin (>=0.3.4 <0.4.0) and supports analyzer >=9.0.0 <14.0.0. This covers the analyzer bundled with Dart 3.10+, so the plugin loads inside your project's analysis server with no .dartServer or pubspec_overrides.yaml workaround.
riverpod_lint 3.1.x still carries its own analyzer constraints (^9.0.0 for stable 3.1.3 and ^12.0.0 for current dev releases). 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 broad enough to share that solve:
plugins:
clean_architecture_linter: ^2.0.0-dev.1
riverpod_lint: ^3.1.3
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: flutter analyze
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