clean_architecture_linter 2.4.0
clean_architecture_linter: ^2.4.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 (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 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(usedart analyze, notflutter analyzeβ see the warning below). 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 too β use `dart analyze`, NOT `flutter analyze`
That's it! The 34 rules are reported directly in your dart analyze output.
β οΈ 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.
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 13 / Riverpod 3+ #
v2.4 runs on the official analysis_server_plugin (>=0.3.15 <0.3.16) and supports analyzer >=13.0.0 <14.0.0, matching the current riverpod_lint 3.1.x analyzer line while avoiding analyzer 14-only plugin-host versions. Consumer smoke keeps the ASP range pinned to 0.3.15 because 0.3.18 still hangs during plugin analysis under analyzer 13.
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.7
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