clean_architecture_linter 2.1.0 copy "clean_architecture_linter: ^2.1.0" to clipboard
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 #

pub package License: MIT

πŸ‡°πŸ‡· ν•œκ΅­μ–΄ 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) #

  1. Layer Dependency - Enforces dependency direction (inward only)
  2. Domain Purity - Prevents external framework dependencies in domain layer
  3. Dependency Inversion - Validates abstraction-based dependencies
  4. Repository Interface - Ensures proper repository abstractions
  5. Circular Dependency - Prevents circular dependencies between layers
  6. Boundary Crossing - Validates proper layer boundary crossing

🎯 Domain Layer Rules (2 rules) #

  1. UseCase No Result Return - UseCases should return entities directly (pass-through pattern)
  2. Exception Naming Convention - Feature prefix for domain exceptions

πŸ’Ύ Data Layer Rules (10 rules) #

  1. Model Structure - Freezed models with entity composition
  2. Model Field Duplication - No duplicate entity fields in models
  3. Model Conversion Methods - Required toEntity() method in extensions
  4. Model Naming Convention - Models must end with Model suffix
  5. DataSource Abstraction - Abstract interfaces for data sources
  6. DataSource No Result Return - DataSources throw exceptions
  7. Repository Implementation - RepositoryImpl must implement domain interface
  8. Repository Pass Through - Repositories return Future<Entity> (warns on Result pattern)
  9. Repository No Throw - Repositories use pass-through pattern (AppException types allowed)
  10. DataSource Exception Types - Use defined data layer exceptions only
  11. Model Entity Direct Access - Use .toEntity() instead of direct .entity access

🎨 Presentation Layer Rules (14 rules) #

  1. No Presentation Models - Use Freezed State instead of ViewModels
  2. Extension Location - Extensions in same file as the class
  3. Freezed Usage - Use Freezed instead of Equatable
  4. Riverpod Generator - Use @riverpod annotation
  5. Presentation No Data Exceptions - Use domain exceptions only
  6. Presentation Use AsyncValue - Use AsyncValue for error handling (3-tier architecture)
  7. Presentation No Throw - No exception throwing in Presentation layer
  8. Widget No UseCase Call - Widgets should not call UseCases directly (use Providers)
  9. Widget Ref Read Then When - Avoid using .when() after ref.read() (anti-pattern)
  10. Riverpod Ref Usage - Use ref.watch() in build(), ref.read() in methods (with UseCase detection)
  11. Riverpod Ref After Async Gap - Advisory warning for ref.read/watch/listen/invalidate/refresh after await in provider methods
  12. Riverpod Provider Naming - Provider functions must include type suffix (repository/usecase/datasource)
  13. Ref Mounted Usage - Avoid ref.mounted (use AsyncValue or complete async before navigation)
  14. Riverpod Keep Alive - Only use keepAlive: true for global state (auth, settings, cache)

πŸ”§ Cross-Layer Rules (1 rule) #

  1. 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 official analysis_server_plugin β€” no custom_lint dependency, no pubspec_overrides.yaml workaround. Lint runs directly via dart 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.

  • 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_lint upstream (invertase/dart_custom_lint) was archived in May 2026. v2.0 moves fully to the official plugin, so the old pubspec_overrides.yaml bridge 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 in 2.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 #

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new rules
  4. Format your code: dart format .
  5. Ensure all tests pass
  6. 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 #

🎯 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

2
likes
130
points
600
downloads

Documentation

API reference

Publisher

verified publisherittae.com

Weekly Downloads

A comprehensive analysis_server_plugin that automatically enforces Clean Architecture principles in Flutter projects with Riverpod state management.

Topics

#clean-architecture #lint #custom-lint #linter #static-analysis

License

MIT (license)

Dependencies

analysis_server_plugin, analyzer, path

More

Packages that depend on clean_architecture_linter