architecture_lints 0.1.1 copy "architecture_lints: ^0.1.1" to clipboard
architecture_lints: ^0.1.1 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. Core Declarations
  2. Auxiliary Declarations
  3. Policies (The Rules)
  4. Automation (Code Generation)
  5. Reference: Available Options

[1] 🎯 Core Declarations (The Configurations) #

The architecture.yaml file drives everything.

[1.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.

Property 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 | Map Shorthand <module_key>: '<path>'
Simple path mapping for quick definitions.
Longhand <module_key>: { path: '<path>', default: bool }
Full configuration for advanced options.
path String Location + Token The root directory for this module relative to the project root.
{{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'

[1.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).
Property 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.
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 | List<Enum> class Enforces the specific Dart declaration type. Matches the language keyword.
enum
mixin
extension
typedef
modifier Enum | List<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) and forbidden (antipattern) naming pattern used to guide users to follow and away from bad naming habits respectively.
{{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.
{{noun}} Semantic naming patterns using Natural Language Processing (NLP) parts of speech.
{{noun.phrase}}
{{noun.singular}}
{{noun.plural}}
{{verb}} Semantic naming patterns using Natural Language Processing (NLP) parts of speech.
{{verb.present}}
{{verb.past}}
{{verb.gerund}}
{{adjective}} Semantic naming patterns using Natural Language Processing (NLP) parts of speech.
{{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.

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] 🧩 Auxiliary Declarations #

[2.1] Types (definitions) #

Maps abstract concepts (like "Result Wrapper") to concrete Dart types. This decouples your rules from specific class names.

Properties

[a] <group_key>: A logical grouping (e.g., usecase, result)

  • Type: Map<String, String | Map>

[a.1] <type_key>: The unique identifier within the group

  • Type: String | Map
  • Shorthand: key: 'ClassName' inherits previous import
  • Detailed: key: { type: 'ClassName', import: '...' }

[a.1.1] type: The raw Dart class name

  • Type: String

[a.1.2] import: The package URI (inherits from previous entry if omitted)

  • Type: String

[a.1.3] argument: Expected generic type parameters

  • Type: List<Map> (recursive structure)

Example

definitions:
  # Domain Types
  usecase:
    .base:
      type: 'Usecase'
      import: 'package:my_app/core/usecase.dart'
    .unary: 'UnaryUsecase' # Inherits import from .base
    
  # Result Wrappers
  result:
    .wrapper:
      .future:
        type: 'FutureEither'
        import: 'package:my_app/core/types.dart'
        argument: '*'

[2.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"). You can extend the dictionary with domain-specific terms.

Properties

[a] nouns: Domain-specific noun terms

  • Type: List<String>

[b] verbs: Domain-specific verb terms

  • Type: List<String>

Example

vocabularies:
  nouns: [ 'auth', 'todo', 'kyc' ]
  verbs: [ 'upsert', 'rebase', 'unfriend' ]

[3] 📜 Policies (Enforcing Behavior) #

Policies define what is required, allowed, or forbidden.

[3.1] Dependencies (dependencies) #

Purpose: Enforce the Dependency Rule (Architecture Boundaries)

Logic: Can Module A import Module B? Can Layer X import Layer Y?

Properties

[a] on: The component or layer target

  • Type: String | List<String>

[b] allowed | forbidden: Whitelist (if defined, component may ONLY import these) or blacklist (component must NOT import these) approach.

  • Type: Map

[b.1] component: List of architectural components or layers to check against

  • Type: String | List<String>

[b.2] import: List of URI patterns. Supports glob ** for wildcards

  • Type: String | List<String>

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' ]

[3.2] Type Safety (type_safeties) #

Purpose: Enforce method signatures

Logic: "Methods in this layer must return X" or "Parameters must not be Y"

Properties

[a] on: The component target

  • Type: String | List<String>

[b] allowed | forbidden: Whitelist of permitted types OR Blacklist of prohibited types

  • Type: Map

[b.1] kind: The context of the check

  • Type: String
  • Options: 'return' | 'parameter'

[b.2] identifier: (for parameters) The parameter name to match

  • Type: String

[b.3] definition: Reference to a key in the definitions config

  • Type: String | List<String>

[b.4] type: Raw class name string (e.g., 'int', 'Future')

  • Type: String | List<String>

[b.5] component: Reference to an architectural component

  • Type: String

Example

type_safeties:
  # Domain must return safe wrappers
  - on: [ port, usecase ]
    allowed:
      kind: 'return'
      definition: 'result.wrapper.future'
    forbidden:
      kind: 'return'
      type: 'Future'

[3.3] Exceptions (exceptions) #

Purpose: Enforce error handling flow

Logic: Who is a producer (throws), propagator (rethrows), or boundary (catches)?

Properties

[a] on: The component target

  • Type: String | List<String>

[b] role: The semantic role regarding errors

  • Type: String
  • Options: producer, boundary, consumer, propagator

[c] required | forbidden: Required operations and Prohibited operations

  • Type: List<Map>

[c.1] operation: The control flow action

  • Type: String | List<String>
  • Options: throw, rethrow, catch_return, catch_throw, try_return

[c.2] definition: Reference to a key in the definitions config

  • Type: String

[c.3] type: Raw class name (used if no definition key exists)

  • Type: String

[e] conversions: Exception-to-Failure mapping for boundaries

  • Type: List<Map>

[e.1] from: The exception type caught

  • Type: String

[e.2] to: The failure type returned

  • Type: String

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'

[3.4] Structure (members & annotations) #

Purpose: Enforce internal class structure

Members Properties

[a] on: The component target

  • Type: String | List<String>

[b] required | allowed | forbidden: Members that must exist, Permitted members (whitelist), and Prohibited members (blacklist)

  • Type: List<Map>

[b.1] kind: The member type target

  • Type: String | List<String>
  • Options: method, field, getter, setter, constructor, override

[b.2] identifier: Specific names or Regex patterns to match

  • Type: String | List<String>

[b.3] visibility: The access level

  • Type: String
  • Options: public, private

[b.4] modifier: Required keywords

  • Type: String
  • Options: final, const, static, late

[b.5] action: Quick Fix action if member is missing

  • Type: String

Example

members:
  # Entities must be immutable
  - on: entity
    required:
      - kind: 'field'
        identifier: 'id'
      - kind: 'field'
        modifier: 'final'
    forbidden:
      - kind: 'setter'
        visibility: 'public'

[3.5] Relationships (relationships) #

Purpose: Enforce file parity (1-to-1 mappings)

Logic: "For every Method in a Port, there must be a UseCase file"

Properties

[a] on: The source component

  • Type: String

[b] kind: What to iterate over

  • Type: String
  • Options: class, method

[c] visibility: Filter by visibility

  • Type: String
  • Options: public, private

[d] required: Target component that must exist

  • Type: Map

[d.1] component: The architectural component to look for

  • Type: String

[d.2] action: Quick Fix action if missing

  • Type: String

Example

relationships:
  # Every Port method needs a UseCase
  - on: 'domain.port'
    kind: 'method'
    visibility: 'public'
    required:
      component: 'domain.usecase'
      action: 'create_usecase'

[4] 🤖 Automation (Actions & Templates) #

The linter acts as a code generator when rules are broken.

[4.1] Actions (actions) #

Defines the logic for a Quick Fix. Uses a Mustache Like Expression Language for variables.

[4.1.1] Metadata

Basic configuration for the Quick Fix.

Property Type Description
description String Human-readable name.
template_id String Reference to template key.
debug Boolean Enable logging in the generated.
format Boolean Whether to format generated code
format_line_length Integer Line length for formatting

[4.1.2] Trigger, Source & Target Context

Determines when the action appears, where data is extracted from, and where the resulting code is injected.

Property Type Value Description
trigger.error_code String The lint rule triggering this.
trigger.component String The component scope.
source.scope Enum current The current file context.
related The related file context.
source.element Enum class The class definition node.
method The specific method node.
field The property or field node.
target.scope Enum current Write to the current file.
related Write to the related file.
target.component String Destination component name.

[4.1.3] Write Strategy

How the generated code is saved.

Property Type Value Description
write.strategy Enum file Sets the specific output filename.
inject Uses an existing file and identifies an insertion point.
replace Overwrites the output file entirely.
write.filename String The specific output filename (required for file strategy).
write.placement Enum start Places content at the beginning of the file/block.
end Places content at the end of the file/block.

[4.1.4] Expression Engine

Built-in methods and variables in actions.variables

  • source: The AST Node (Class, Method, Field)

    • .name: String (with .pascalCase, .snakeCase filters)
    • .parent: Parent node
    • .file.path: Absolute path
    • .returnType: TypeWrapper (methods)
    • .parameters: ListWrapper (methods)
  • config: The Architecture Config

    • .definitionFor('key'): Looks up type definition
    • .namesFor('componentId'): Looks up naming patterns

[4.1.5] Variables & Expressions

Maps data from the source to the template. This uses a Mustache-like expression language.

Simple References: Direct access to properties

variables:
  className: '{{source.name.pascalCase}}'
  repoName: '{{source.parent.name}}'

Conditional Switch Logic: Use a list of maps to handle "if/else" logic

variables:
  baseDef:
    select:
      - if: source.parameters.isEmpty
        value: config.definitionFor('usecase.nullary')
      - else: config.definitionFor('usecase.unary')

Complex Mappings & Lists: Iterating over lists or mapping objects

variables:
  params:
    type: list
    from: source.parameters
    map:
      .name: item.name.value
      .type: item.type.unwrapped.value

Common Filters: Available on string properties:

  • pascalCase
  • snakeCase
  • camelCase
  • extractGeneric(index=1)

Full Example

actions:
  create_usecase:
    description: 'Generate Functional UseCase'
    template_id: 'usecase_functional'
    format: true
    format_line_length: 100
    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:
      className: '{{source.name.pascalCase}}'
      repoVar: '_{{source.parent.name.camelCase}}'

[4.2] Templates (templates) #

Standard Mustache templates. Logic-less.

Properties

[a] file: Path to the Mustache template file

  • Type: String

[b] description: Human-readable description

  • Type: String

Example

templates:
  usecase_template:
    file: 'templates/usecase.mustache'
    description: 'Standard UseCase template'

Example template file (templates/usecase.mustache):

class {{className}} extends {{baseClass}} {
  final {{repoType}} {{repoVar}};
    
  const {{className}}(this.{{repoVar}});
    
  @override
  {{returnType}} call({{parameters}}) {
    // TODO: Implement
  }
}