architecture_lints 0.1.5
architecture_lints: ^0.1.5 copied to clipboard
A configuration-driven, architecture-agnostic linter for Dart & Flutter.
Architecture Lints ποΈ #
A configuration-driven, architecture-agnostic linting engine for Dart and Flutter that transforms your architectural vision into enforceable code standards.
Unlike standard linters that enforce hardcoded opinions (e.g., "Always extend Bloc"),
architecture_lints reads a Policy Definition from an architecture.yaml file in your project
root. This allows you to define your own architectural rules, layers, and naming conventions.
It is the core engine powering packages like architecture_clean, but it can be used standalone to
enforce any architectural style (MVVM, MVC, DDD, Layer-First, Feature-First).
π¦ Installation #
Add the package to your dev_dependencies:
# pubspec.yaml
dev_dependencies:
custom_lint: ^0.8.0
architecture_lints: ^0.1.0
Enable the plugin in analysis_options.yaml:
# analysis_options.yaml
analyzer:
plugins:
- custom_lint
Create an architecture.yaml file in your project root (see Configuration below).
π Available Lint Rules #
These rules are generic but become specific based on your configuration.
| Error Code | Category | Trigger Logic |
|---|---|---|
arch_naming_pattern |
Naming | Class name does not match the configured pattern (e.g., must end in UseCase). |
arch_naming_antipattern |
Naming | Class name uses a forbidden term defined in antipattern (e.g., Manager in a Utils folder). |
arch_structure_kind |
Structure | Component is the wrong Dart kind (e.g., found enum, config required class). |
arch_structure_modifier |
Structure | Component is missing a required modifier (e.g., Interface must be abstract). |
arch_dep_component |
Boundaries | Layer A imports Layer B, but it is forbidden by the dependencies policy. |
arch_parity_missing |
Consistency | A required companion file is missing (e.g., every Port must have a UseCase). |
arch_safety_return_strict |
Type Safety | Method returns a raw type (e.g., Future) instead of a required wrapper (e.g., FutureEither). |
arch_safety_param_strict |
Type Safety | Method parameter uses a primitive (e.g., int) instead of a ValueObject (e.g., UserId). |
arch_exception_forbidden |
Exceptions | Layer performs a forbidden operation (e.g., throw in UI, or catch in Domain). |
arch_usage_global_access |
Usage | Direct access to global service locators (e.g., GetIt.I) is detected where banned. |
arch_usage_instantiation |
Usage | Direct instantiation of a dependency (new Repository()) instead of using injection. |
arch_annot_missing |
Annotations | Class is missing required metadata (e.g., @Injectable). |
arch_annot_forbidden |
Annotations | Usage of banned annotations (e.g., @JsonSerializable in Domain layer). |
βοΈ Configuration Manual (architecture.yaml) #
This file acts as the Domain Specific Language (DSL) for your architecture.
π Table of Contents #
- Concepts & Philosophies
- Core Declarations
- Auxiliary Declarations
- Policies (The Rules)
- Automation (Code Generation)
[1] π‘ Concepts & Philosophies #
To effectively lint a large project, we must understand its structure on two axes:
Modules (Horizontal Slicing) #
Modules represent the Features or high-level groupings of your application.
- Example:
Core,Shared,Profile,Auth. - A module usually contains multiple layers.
Components (Vertical Slicing) #
Components represent the Layers or technical roles within a module.
- Example:
Entity,Repository,UseCase,Widget. - A component is defined by what it is (Structure) and where it lives (Path).
The Linter combines these to identify a file:
lib/features/auth/domain/usecases/login.dart
- Module:
auth(Derived fromfeatures/{{name}})- Component:
domain.usecase(Derived from pathdomain/usecases)
[2] π― Core Declarations (The Configurations) #
The architecture.yaml file drives everything.
[2.1] Modules (modules) #
Modules represent the Features or high-level groupings of your application. The linter uses
these definitions to map codebase files to specific functional boundaries. For example Core,
Shared, Profile, Auth.
Relationship: A module usually acts as a container for multiple architectural layers.
Definitions
modules:
<module_key>: <module_value>
| Name | Type | Value | Description |
|---|---|---|---|
| <module_key> | String |
Definition | The unique identifier for the module. This ID is used when referencing modules in dependency rules. |
| <module_value> | String |
Shorthand |
<module_key>: '<path>'Simple path mapping for quick definitions. |
Map |
Longhand |
<module_key>: { path: '<path>', default: bool }Full configuration for advanced options. |
Note:
<module_value>can be only one of the two forms (shorthand or longhand).
Properties
| Name | Type | Value | Description |
|---|---|---|---|
| path | String |
Location + Token | The root directory for this module relative to the project root. |
Token |
{{name}} |
Dynamic module indicator. The folder name becomes the module instance name. | |
* |
Standard glob wildcard for ignoring intermediate folders. | ||
| default | Boolean |
Fallback |
If true, this module acts as the fallback for unmatched components.(Default: false)
|
Example
modules:
# [Dynamic] Feature modules under features/
# ID: 'feature', Instance: 'auth', 'payments', etc.
feature:
path: 'features/{{name}}'
default: true # Unmatched components belong here
# [Static] Core module
# ID: 'core', Path: 'lib/core'
core: 'core'
# [Static] Shared module
# ID: 'shared', Path: 'lib/shared'
shared: 'shared'
[2.2] Components (components) #
Components represent the Layers or technical roles within a module.
Maps your file system structure to architectural concepts. This is the core taxonomy of your project.
- Example:
Entity,Repository,UseCase,Widget. - A component is defined by what it is (Structure) and where it lives (Path).
Definitions
components:
<component_key>:
<component_property>
<component_property>
.<component_child>
.<component_child>
| Name | Type | Value | Description |
|---|---|---|---|
| <component_key> | String |
Hierarchy |
Keys starting with . are treated as children. Their ID is concatenated with
the parent (e.g., domain + .port = domain.port).
|
| Inheritance | Child components automatically inherit the path of their parent. |
||
| <component_value> | Map |
Structure | |
| <component_child> | Map |
Recursive Structure | Any thing that starts with . is treated as a child of the component. |
Properties
| Name | Type | Value | Description |
|---|---|---|---|
| mode | Enum |
file(default) |
Represents a specific code unit (e.g., a class in a file). Matches based on file name and content. |
part |
Represents a symbol defined *inside* a file (e.g., an Event class defined
within a Bloc file). Use this for detailed structural checks within a file.
|
||
namespace |
Represents a folder or layer container. Matches directories, never specific files.
Use this for parent keys (e.g., domain).
|
||
| path | String,List<String> |
Location | The directory name(s) relative to the parent component path. |
| kind | Enum,Set<Enum> |
class |
Enforces the specific Dart declaration type. Matches the language keyword. |
enum | |||
mixin | |||
extension | |||
typedef | |||
| modifier | Enum,Set<Enum> |
abstract |
Enforces specific Dart keywords on the declaration to control inheritance and visibility. |
sealed | |||
interface | |||
base | |||
final | |||
mixin | |||
| pattern, antipattern |
String,List<String> |
Regex + Tokens |
A required (pattern) naming pattern used to guide users to follow good naming
habits.
|
A forbidden (antipattern) naming pattern used to guide away from bad naming
habits.
|
|||
Token |
{{name}} |
PascalCase naming convention. | |
{{affix}} |
Wildcard naming convention. | ||
| grammar | String,List<String> |
Regex + Tokens | Semantic naming patterns using Natural Language Processing (NLP) parts of speech. |
Token |
{{noun}} |
Noun related Natural Language Processing (NLP) tokens. | |
{{noun.phrase}} | |||
{{noun.singular}} | |||
{{noun.singular.phrase}} | |||
{{noun.plural}} | |||
{{noun.plural.phrase}} | |||
{{verb}} |
Verb related Natural Language Processing (NLP) tokens. | ||
{{verb.present}} | |||
{{verb.present.phrase}} | |||
{{verb.past}} | |||
{{verb.past.phrase}} | |||
{{verb.gerund}} | |||
{{adjective}} |
Other Language Processing (NLP) tokens. | ||
{{adverb}} | |||
{{preposition}} | |||
{{conjunction}} |
Example
components:
# [Namespace] Domain Layer
# ID: 'domain'
# Path: 'domain'
# Mode: 'namespace' ensures this never matches a specific file, only the folder
.domain:
path: 'domain'
mode: namespace
# [Component] Domain Port
# ID: 'domain.port' (Concatenated)
# Path: 'domain/ports' (Inherited + Appended)
.port:
path: 'ports'
mode: file
# Structural Rules: Must be 'abstract interface class'
kind: class
modifier: [ abstract, interface ]
# Naming Rule: Must end in 'Port' (e.g. AuthPort)
pattern: '{{name}}Port'
antipattern: '{{name}}Interface' # Guide away from legacy naming
π§ Resolution Logic
The linter uses a Smart Resolution Logic to identify files by calculating a score. This allows an
Interface (AuthSource) and Implementation (AuthSourceImpl) in the same folder to be treated
differently.
Scoring Criteria:
- Path Match: Deeper directory matches get higher scores.
- Mode:
mode: filebeatsmode: part. - Naming: Matches configured
{{name}}Pattern. - Inheritance: Implements required base classes defined in
inheritances. - Structure: Matches required
kindandmodifier.
Example: A concrete class AuthImpl will fail to match a component requiring
modifier: abstract, forcing the resolver to pick the Implementation component instead.
[3] π§© Auxiliary Declarations #
[3.1] Types (definitions) #
Maps abstract concepts (like "Result Wrapper" or "Service Locator") to concrete Dart types. This decouples your rules from specific class names.
Definitions
definitions:
<group_key>:
<type_key>: <type_value>
| Name | Type | Value | Description |
|---|---|---|---|
| <group_key> | String |
Group | A logical grouping (e.g., 'usecase', 'result'). |
| <type_key> | String |
Key | The unique identifier within the group. |
| <type_value> | String |
Shorthand |
key: 'ClassName'. Inherits previous import.
|
Map |
Detailed |
key: { type: 'ClassName', import: '...' }. Explicit config.
|
Properties
| Name | Type | Value | Description |
|---|---|---|---|
| type | String |
Class | The raw Dart class name. |
| import | String |
URI | The package URI. If omitted, inherits from the previous entry. |
| argument | List<Map> |
Generics |
Expected generic type parameters (Recursive structure). '*' matches any.
|
Example
definitions:
# Domain Types
usecase:
.base:
type: 'Usecase'
import: 'package:my_app/core/usecase.dart'
# Inherits import from .base
.unary: 'UnaryUsecase'
# Result Wrappers
result:
.wrapper:
.future:
type: 'FutureEither'
import: 'package:my_app/core/types.dart'
argument: '*'
[3.2] Vocabularies (vocabularies) #
The linter uses Natural Language Processing (NLP) to check if class names make grammatical sense (e.g., "UseCases must be Verb-Noun").
Definitions
vocabularies:
nouns: <list>
verbs: <list>
| Name | Type | Value | Description |
|---|---|---|---|
| nouns | List<String> |
Terms | Domain-specific noun terms (e.g., 'auth', 'kyc'). |
| verbs | List<String> |
Actions | Domain-specific verb terms (e.g., 'upsert', 'rebase'). |
Example
vocabularies:
nouns: [ 'auth', 'todo', 'kyc' ]
verbs: [ 'upsert', 'rebase', 'unfriend' ]
[4] π Policies (Enforcing Behavior) #
Policies define what is required, allowed, or forbidden.
[4.1] Dependencies (dependencies) #
Enforce the Dependency Rule (Architecture Boundaries).
Definitions
dependencies:
- on: <component_id>
<allowed | forbidden>
| Name | Type | Value | Description |
|---|---|---|---|
| on | String,List<String> |
Target | The component or layer target. |
| allowed | Map |
Whitelist | If defined, the component may ONLY import from these sources. |
| forbidden | Map |
Blacklist | The component must NOT import from these sources. |
Properties
| Name | Type | Value | Description |
|---|---|---|---|
| component | String,List<String> |
Reference | List of architectural components to check against. |
| import | String,List<String> |
Pattern | List of URI patterns. Supports glob **. |
Example
dependencies:
# Domain is platform agnostic
- on: domain
forbidden:
import: [ 'package:flutter/**', 'dart:ui' ]
component: [ 'data', 'presentation' ]
# UseCases can only see Domain
- on: usecase
allowed:
component: [ 'entity', 'port' ]
[4.2] Type Safety (type_safeties) #
Enforce method signatures.
Definitions
type_safeties:
- on: <component_id>
<allowed | forbidden>
| Name | Type | Value | Description |
|---|---|---|---|
| on | String,List<String> |
Target | The component target. |
| allowed | Map |
Whitelist | Types MUST match one of these. |
| forbidden | Map |
Blacklist | Types MUST NOT match any of these. |
Properties
| Name | Type | Value | Description |
|---|---|---|---|
| kind | Enum |
return |
The context of the check (return type or parameter). |
parameter | |||
| identifier | String |
Param Name | (Only for kind: parameter) The parameter name to match. |
| definition | String,List<String> |
Ref | Reference to a key in the definitions config. |
| type | String,List<String> |
Raw | Raw class name string (e.g., 'int', 'Future'). |
| component | String |
Arch Ref | Reference to an architectural component. |
Example
type_safeties:
# Domain must return safe wrappers
- on: [ port, usecase ]
allowed:
kind: 'return'
definition: 'result.wrapper.future'
forbidden:
kind: 'return'
type: 'Future'
[4.3] Exceptions (exceptions) #
Enforce error handling flow.
Definitions
exceptions:
- on: <component_id>
role: <role>
<required | forbidden>
conversions: ...
| Name | Type | Value | Description |
|---|---|---|---|
| on | String,List<String> |
Target | The component target. |
| role | Enum |
producer |
The semantic role regarding errors. |
boundary | |||
consumer | |||
propagator | |||
| required | List<Map> |
Must | Required operations. |
| forbidden | List<Map> |
Must Not | Prohibited operations. |
| conversions | List<Map> |
Map | Exception-to-Failure mapping for boundaries. |
Properties (inside required/forbidden)
| Name | Type | Value | Description |
|---|---|---|---|
| operation | Enum,List<Enum> |
throw |
The control flow action. |
rethrow | |||
catch_return | |||
catch_throw | |||
try_return | |||
| definition | String |
Ref | Reference to a key in the definitions config. |
| type | String |
Raw | Raw class name (used if no definition key exists). |
Properties (inside conversions)
| Name | Type | Value | Description |
|---|---|---|---|
| from | String |
Exception | The exception type caught (reference to definitions). |
| to | String |
Failure | The failure type returned (reference to definitions). |
Example
exceptions:
# Repositories catch and return Failures
- on: repository
role: boundary
required:
- operation: 'catch_return'
definition: 'result.failure'
forbidden:
- operation: 'throw'
conversions:
- from: 'exception.server'
to: 'failure.server'
[4.4] Structure #
This section enforces internal class composition.
[4.4.1] Inheritances (inheritances)
Enforces base class requirements (extends, implements, with).
Definitions
inheritances:
- on: <component_id>
<required | forbidden>
| Name | Type | Value | Description |
|---|---|---|---|
| on | String,List<String> |
Target | Target component ID. |
| required | List<Map> |
Must | List of types the component MUST inherit. |
| forbidden | List<Map> |
Must Not | List of types the component MUST NOT inherit. |
Properties (inside required/forbidden)
| Name | Type | Value | Description |
|---|---|---|---|
| type | String,List<String> |
Class | Raw class name (e.g., 'Entity'). |
| import | String |
URI | The package URI. |
| definition | String,List<String> |
Ref | Reference to a definitions key. |
| component | String |
Arch | Reference to another architectural component ID. |
Example
inheritances:
# Entities must extend the base Entity class
- on: entity
required:
- type: 'Entity'
import: 'package:core/entity/entity.dart'
# Repositories must implement their corresponding Port
- on: repository
required:
- component: 'port'
[4.4.2] Members (members)
Enforces rules on class members (fields, methods, constructors).
Definitions
members:
- on: <component_id>
<required | allowed | forbidden>
| Name | Type | Value | Description |
|---|---|---|---|
| on | String,List<String> |
Target | Target component ID. |
| required | List<Map> |
Must | Members that must exist. |
| allowed | List<Map> |
Whitelist | Permitted members (whitelist). |
| forbidden | List<Map> |
Blacklist | Prohibited members. |
Properties
| Name | Type | Value | Description |
|---|---|---|---|
| kind | Enum,List<Enum> |
method |
The member type target. |
field | |||
getter | |||
setter | |||
constructor | |||
override | |||
| identifier | String,List<String> |
Name | Specific names or Regex patterns to match. |
| visibility | Enum |
public |
The access level. |
private | |||
| modifier | Enum,List<Enum> |
final |
Required keywords. |
const | |||
static | |||
late | |||
| action | String |
Fix | Quick Fix action ID if member is missing. |
Example
members:
# Entities must be immutable and have an 'id'
- on: entity
required:
- kind: field
identifier: 'id'
- kind: field
modifier: 'final'
forbidden:
- kind: setter
visibility: public
[4.4.3] Annotations (annotations)
Enforces metadata (Annotations) on classes.
Definitions
annotations:
- on: <component_id>
mode: <mode>
<required | forbidden>
| Name | Type | Value | Description |
|---|---|---|---|
| on | String |
Target | Target component ID. |
| mode | Enum |
strict |
Controls strictness regarding unlisted annotations. |
implicit | |||
| required | List<Map> |
Must | Annotations that MUST exist. |
| forbidden | List<Map> |
Must Not | Annotations that MUST NOT exist. |
Properties (inside required/forbidden)
| Name | Type | Value | Description |
|---|---|---|---|
| type | String,List<String> |
Class | Annotation class name. |
| import | String |
URI | Package URI. |
Example
annotations:
# UseCases must be injectable. No other framework annotations allowed.
- on: usecase
mode: strict
required:
- type: 'Injectable'
import: 'package:injectable/injectable.dart'
[4.5] Relationships (relationships) #
Enforce file parity (1-to-1 mappings).
Definitions
relationships:
- on: <component_id>
kind: <kind>
required: ...
| Name | Type | Value | Description |
|---|---|---|---|
| on | String |
Source | The source component. |
| kind | Enum |
class |
What to iterate over. |
method | |||
| visibility | Enum |
public |
Filter by visibility. |
| required | Map |
Target | Target component that must exist. |
Properties (inside required)
| Name | Type | Value | Description |
|---|---|---|---|
| component | String |
Target | The architectural component to look for. |
| action | String |
Fix | Quick Fix action ID if missing. |
Example
relationships:
# Every Port method needs a UseCase
- on: domain.port
kind: method
visibility: public
required:
component: domain.usecase
action: create_usecase
[4.6] Usage (usages) #
Bans specific coding patterns (e.g., global access).
Definitions
usages:
- on: <component_id>
forbidden: ...
| Name | Type | Value | Description |
|---|---|---|---|
| on | String,List<String> |
Target | The component target. |
| forbidden | List<Map> |
Blacklist | List of disallowed usage patterns. |
Properties (inside forbidden)
| Name | Type | Value | Description |
|---|---|---|---|
| kind | Enum |
access |
Type of usage. |
instantiation | |||
| definition | String |
Ref | Reference to service definition. |
| component | String,List<String> |
Arch | Reference to architectural component. |
Example
usages:
- on: domain
forbidden:
kind: access
definition: service.locator
[5] π€ Automation (Actions & Templates) #
The linter acts as a code generator when rules are broken.
[5.1] Actions (actions) #
Defines the logic for a Quick Fix. Uses a Dart-like Expression Language for variables.
Definitions
actions:
<action_id>:
<global_properties>
trigger: ...
source: ...
target: ...
write: ...
variables: ...
| Name | Type | Value | Description |
|---|---|---|---|
| <action_id> | String |
ID | Unique identifier for the action. |
| description | String |
Label | Human-readable name for the IDE. |
| template_id | String |
Ref | Reference to template key. |
| debug | Boolean |
Log | Enable debug logging. |
[5.1.1] Trigger Configuration
Determines when the action appears.
| Name | Type | Value | Description |
|---|---|---|---|
trigger |
Map |
Configuration block. | |
error_code |
String |
The lint rule triggering this. | |
component |
String |
The component scope. |
[5.1.2] Source & Target Context
Determines where data comes from and where code goes.
| Name | Type | Value | Description |
|---|---|---|---|
source |
Map |
Input context. | |
scope |
Enum |
current, related |
Context of the input data. |
element |
Enum |
class, method, field |
AST node to extract. |
target |
Map |
Output context. | |
scope |
Enum |
current, related |
Context for output. |
component |
String |
Destination component ID. |
[5.1.3] Write Strategy
How the generated code is saved.
| Name | Type | Value | Description |
|---|---|---|---|
write |
Map |
Write configuration block. | |
strategy |
Enum |
file, inject, replace |
Write mode. |
filename |
String |
Output filename template. | |
placement |
Enum |
start, end |
Where to insert (for inject). |
[5.1.4] Variables & Expressions
Maps data from the source to the template. This uses a Dart-like expression language.
| Name | Type | Description |
|---|---|---|
variables |
Map |
Map of keys to dynamic values used in the template. |
Common Variable Strategies:
- Simple References: Direct access to properties (e.g.,
className: '{{source.name.pascalCase}}'). - Conditional Switch Logic: Use a list of maps to handle "if/else" logic.
- Complex Mappings: Iterating over lists or mapping objects (e.g., extracting parameters).
- Common Filters:
| pascalCase,| snakeCase,| camelCase.
Example
actions:
create_usecase:
description: 'Generate UseCase'
template_id: 'usecase_template'
debug: true
trigger:
error_code: 'arch_parity_missing'
component: 'domain.port'
source:
scope: current
element: method
target:
scope: related
component: 'domain.usecase'
write:
strategy: file
filename: '{{source.name.snakeCase}}.dart'
variables:
# Switch logic
baseDef:
select:
- if: source.parameters.isEmpty
value: 'NullaryUsecase'
- else: 'UnaryUsecase'
# Simple reference
className: '{{source.name.pascalCase}}'
# List mapping
paramsList:
type: list
from: source.parameters
map:
name: item.name
[5.2] Templates (templates) #
Standard Mustache templates. Logic-less.
Definitions
templates:
<template_id>:
file: <path>
description: <text>
| Name | Type | Value | Description |
|---|---|---|---|
| <template_id> | String |
ID | Unique identifier. |
| file | String |
Path | Path to the .mustache file. |
| description | String |
Text | Human-readable description. |
Example
templates:
usecase_template:
file: 'templates/usecase.mustache'
Example template file (templates/usecase.mustache):
class {{className}} extends {{baseClass}} {
final {{repoType}} {{repoVar}};
const {{className}}(this.{{repoVar}});
@override
{{returnType}} call({{parameters}}) {
// TODO: Implement
}
}