architecture_lints 0.1.5 copy "architecture_lints: ^0.1.5" to clipboard
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 #

  1. Concepts & Philosophies
  2. Core Declarations
  3. Auxiliary Declarations
  4. Policies (The Rules)
  5. 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 from features/{{name}})
  • Component: domain.usecase (Derived from path domain/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:

  1. Path Match: Deeper directory matches get higher scores.
  2. Mode: mode: file beats mode: part.
  3. Naming: Matches configured {{name}}Pattern.
  4. Inheritance: Implements required base classes defined in inheritances.
  5. Structure: Matches required kind and modifier.

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:

  1. Simple References: Direct access to properties (e.g., className: '{{source.name.pascalCase}}').
  2. Conditional Switch Logic: Use a list of maps to handle "if/else" logic.
  3. Complex Mappings: Iterating over lists or mapping objects (e.g., extracting parameters).
  4. 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
  }
}