Skip to main content
LeanCode Lint provides an opinionated set of high-quality lint rules for Flutter and Dart projects. This guide covers installation, configuration, and creating custom lint plugins with project-specific rules.

What is LeanCode Lint?

LeanCode Lint is a collection of:
  • Static analysis rules - Enforce code quality and consistency
  • Custom lint rules - Project-specific best practices (e.g., design system enforcement, hook usage)
  • Code assists - IDE refactorings for common patterns
  • Analyzer plugin - Configurable rules beyond YAML settings

Installation

1

Add the dependency

Add leancode_lint as a dev dependency:
dart pub add leancode_lint --dev
2

Include lint rules

Create or update analysis_options.yaml in your project root:
include: package:leancode_lint/analysis_options.yaml
3

Enable the analyzer plugin

Add the plugin to analysis_options.yaml:
include: package:leancode_lint/analysis_options.yaml

plugins:
  leancode_lint: ^20.0.0

analyzer:
  exclude:
    - '**/*.g.dart'
    - '**/*.freezed.dart'
4

Restart the analysis server

Run this command and restart your IDE:
flutter pub get
You now have LeanCode Lint enabled with default configuration!

Configuration Options

Default Configuration (As-Is)

Use the built-in rules with default settings:
include: package:leancode_lint/analysis_options.yaml

plugins:
  leancode_lint: ^20.0.0

analyzer:
  exclude:
    - '**/*.g.dart'
    - '**/*.freezed.dart'
This gives you all LeanCode Lint rules with sensible defaults.

Library/Package Configuration

If you’re building a library (not an app), use the package-optimized rules:
include: package:leancode_lint/analysis_options_package.yaml

plugins:
  leancode_lint: ^20.0.0
The package variant includes rules for public API documentation and stricter visibility checks.

Disabling Specific Rules

Disable individual rules in analysis_options.yaml:
include: package:leancode_lint/analysis_options.yaml

plugins:
  leancode_lint:
    version: ^20.0.0
    diagnostics:
      # Disable specific custom lint rules
      prefix_widgets_returning_slivers: false
      use_design_system_item: false
      avoid_conditional_hooks: false

Custom Plugin Package

For advanced configuration, create your own analyzer plugin package that extends LeanCode Lint.

Why Create a Custom Plugin?

Custom plugins allow you to:
  • Configure rules programmatically (not just enable/disable)
  • Enforce design system conventions
  • Customize error messages and parameter names
  • Share configuration across multiple projects

Creating a Custom Plugin

1

Create the plugin package

Create a new Dart package for your custom lints:
mkdir my_lints
cd my_lints
dart create -t package .
2

Add leancode_lint dependency

Update my_lints/pubspec.yaml:
name: my_lints
description: Custom lint rules for my project
version: 1.0.0

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  leancode_lint: ^20.0.0
3

Create the plugin

Create my_lints/lib/main.dart:
import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: const LeanCodeLintConfig(
    applicationPrefix: 'App',
    catchParameterNames: CatchParameterNamesConfig(
      exception: 'error',
      stackTrace: 'stackTrace',
    ),
    designSystemItemReplacements: {
      'AppText': [
        DesignSystemForbiddenItem(name: 'Text', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'RichText', packageName: 'flutter'),
      ],
      'AppScaffold': [
        DesignSystemForbiddenItem(name: 'Scaffold', packageName: 'flutter'),
      ],
    },
  ),
);
4

Use the custom plugin

In your app’s analysis_options.yaml:
include: package:leancode_lint/analysis_options.yaml

plugins:
  my_lints:
    path: ./path/to/my_lints
When using a custom plugin, enable YOUR plugin (e.g., my_lints), not leancode_lint!

Configuration Parameters

Application Prefix

Set a prefix for your app-specific widgets:
LeanCodeLintConfig(
  applicationPrefix: 'Lncd',
)
Effect: Allows widgets like LncdSliverList to satisfy the prefix_widgets_returning_slivers rule.

Catch Parameter Names

Enforce consistent naming for exception handling:
LeanCodeLintConfig(
  catchParameterNames: CatchParameterNamesConfig(
    exception: 'error',      // Default: 'err'
    stackTrace: 'stackTrace', // Default: 'st'
  ),
)
// With default config
try {
  riskyOperation();
} catch (err, st) {
  logger.error('Failed', err, st);
}

Design System Item Replacements

Enforce custom design system widgets:
LeanCodeLintConfig(
  designSystemItemReplacements: {
    'AppText': [
      DesignSystemForbiddenItem(name: 'Text', packageName: 'flutter'),
      DesignSystemForbiddenItem(name: 'RichText', packageName: 'flutter'),
    ],
    'AppButton': [
      DesignSystemForbiddenItem(name: 'ElevatedButton', packageName: 'flutter'),
      DesignSystemForbiddenItem(name: 'TextButton', packageName: 'flutter'),
    ],
    'AppScaffold': [
      DesignSystemForbiddenItem(name: 'Scaffold', packageName: 'flutter'),
    ],
  },
)
Effect:
// ❌ Error: Use AppText instead of Text
Widget build(BuildContext context) {
  return Text('Hello');
}

// ✅ Correct
Widget build(BuildContext context) {
  return AppText('Hello');
}
Configure naming conventions for BLoC classes:
LeanCodeLintConfig(
  blocRelatedClassNaming: BlocRelatedClassNamingConfig(
    stateSuffix: 'State',
    eventSuffix: 'Event',
    presentationEventSuffix: 'PresentationEvent',
  ),
)
Effect:
// For CounterBloc, these classes must be named:
class CounterEvent {}   // Event class
class CounterState {}   // State class
class CounterPresentationEvent {}  // Presentation event (if using bloc_presentation)

Complete Configuration Example

import 'package:leancode_lint/plugin.dart';

/// Custom LeanCode Lint plugin for MyCompany projects.
///
/// Configuration:
/// - Application prefix: "Mc" (for MyCompany)
/// - Catch parameters: error/stackTrace
/// - Design system: Enforces Mc* widgets
final plugin = LeanCodeLintPlugin(
  name: 'my_company_lints',
  config: const LeanCodeLintConfig(
    applicationPrefix: 'Mc',
    
    catchParameterNames: CatchParameterNamesConfig(
      exception: 'error',
      stackTrace: 'stackTrace',
    ),
    
    blocRelatedClassNaming: BlocRelatedClassNamingConfig(
      stateSuffix: 'State',
      eventSuffix: 'Event',
      presentationEventSuffix: 'PresentationEvent',
    ),
    
    designSystemItemReplacements: {
      // Typography
      'McText': [
        DesignSystemForbiddenItem(name: 'Text', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'RichText', packageName: 'flutter'),
      ],
      
      // Buttons
      'McButton': [
        DesignSystemForbiddenItem(name: 'ElevatedButton', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'TextButton', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'OutlinedButton', packageName: 'flutter'),
      ],
      
      // Layout
      'McScaffold': [
        DesignSystemForbiddenItem(name: 'Scaffold', packageName: 'flutter'),
      ],
      
      'McAppBar': [
        DesignSystemForbiddenItem(name: 'AppBar', packageName: 'flutter'),
      ],
    },
  ),
);

Custom Lint Rules Reference

Here are the key custom lint rules provided by LeanCode Lint:
DO add a ‘Cubit’ suffix to your cubit names.
// ❌ Bad
class Counter extends Cubit<int> {}

// ✅ Good
class CounterCubit extends Cubit<int> {}
Configuration: None
AVOID using hooks conditionally or after early returns.
// ❌ Bad
Widget build(BuildContext context) {
  if (condition) {
    useEffect(() { /* ... */ }, []);
  }
}

// ✅ Good
Widget build(BuildContext context) {
  useEffect(() {
    if (condition) { /* ... */ }
  }, []);
}
Configuration: None
DO name catch clause parameters consistently.
// ❌ Bad (with default config)
try {} catch (e, s) {}

// ✅ Good (with default config)
try {} catch (err, st) {}
Configuration: CatchParameterNamesConfig
AVOID using items disallowed by the design system.
// ❌ Bad (if AppText is configured)
return Text('Hello');

// ✅ Good
return AppText('Hello');
Configuration: designSystemItemReplacements
DO prefix widget names with ‘Sliver’ if the widget returns slivers.
// ❌ Bad
class MyList extends StatelessWidget {
  Widget build(BuildContext context) => SliverList(...);
}

// ✅ Good
class SliverMyList extends StatelessWidget {
  Widget build(BuildContext context) => SliverList(...);
}
Configuration: applicationPrefix (allows AppSliverMyList format)
AVOID using MediaQuery.of for single properties.
// ❌ Bad
final size = MediaQuery.of(context).size;

// ✅ Good
final size = MediaQuery.sizeOf(context);
Configuration: None
DO mix in EquatableMixin instead of extending Equatable.
// ❌ Bad
class User extends Equatable {
  final String name;
  List<Object?> get props => [name];
}

// ✅ Good
class User with EquatableMixin {
  final String name;
  List<Object?> get props => [name];
}
Configuration: None
See the LeanCode Lint README for the complete list of rules.

Code Assists

LeanCode Lint provides IDE refactorings (code actions):

Available Assists

  1. Convert positional to named formal - Convert positional parameters to named
  2. Convert record into nominal type - Extract record into a class
  3. Convert iterable map to collection-for - Refactor .map() to for loops

Using Assists

In your IDE:
  • VSCode: Place cursor on code, press Ctrl+. or ⌘+.
  • Android Studio/IntelliJ: Place cursor on code, press Alt+Enter

Real-World Examples

Example 1: Enforcing Design System

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_design_lints',
  config: const LeanCodeLintConfig(
    designSystemItemReplacements: {
      'AppText': [
        DesignSystemForbiddenItem(name: 'Text', packageName: 'flutter'),
      ],
      'AppButton': [
        DesignSystemForbiddenItem(name: 'ElevatedButton', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'TextButton', packageName: 'flutter'),
      ],
    },
  ),
);

Example 2: Team-Wide Conventions

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'team_lints',
  config: const LeanCodeLintConfig(
    // All team members use 'error' and 'stackTrace'
    catchParameterNames: CatchParameterNamesConfig(
      exception: 'error',
      stackTrace: 'stackTrace',
    ),
    
    // All widgets start with 'App'
    applicationPrefix: 'App',
    
    // Enforce consistent BLoC naming
    blocRelatedClassNaming: BlocRelatedClassNamingConfig(
      stateSuffix: 'State',
      eventSuffix: 'Event',
      presentationEventSuffix: 'PresentationEvent',
    ),
  ),
);

Sharing Configuration Across Projects

Option 1: Local Path

For monorepos, use a path dependency:
plugins:
  my_lints:
    path: ../../shared/my_lints

Option 2: Git Dependency

Host your lint package in a Git repository:
plugins:
  my_lints:
    git:
      url: https://github.com/mycompany/my_lints.git
      ref: main

Option 3: Publish to pub.dev

Publish your custom lint package:
plugins:
  my_company_lints: ^1.0.0

Debugging Lint Issues

Check Plugin Status

Verify the analyzer plugin is running:
dart analyze

View Analysis Server Logs

In VSCode:
  1. Open Command Palette (Cmd+Shift+P)
  2. Run “Dart: Open DevTools”
  3. Check “Logging” tab

Common Issues

Solution: Ensure you’ve run flutter pub get and restarted your IDE.
flutter pub get
# Then restart your IDE
Solution: Check that you’re enabling the correct plugin name:
# If using custom plugin
plugins:
  my_lints:  # ← Your plugin name, not 'leancode_lint'
    path: ./my_lints
Solution: LeanCode Lint rules take precedence. Disable conflicting rules:
linter:
  rules:
    # Disable if it conflicts with leancode_lint
    prefer_const_constructors: false

Best Practices

Begin with default configuration and add custom rules as your team identifies patterns:
# Week 1: Start simple
include: package:leancode_lint/analysis_options.yaml
plugins:
  leancode_lint: ^20.0.0

# Week 4: Add custom plugin as patterns emerge
plugins:
  my_lints:
    path: ./my_lints
Add comments explaining custom rules:
final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: const LeanCodeLintConfig(
    // We use 'error' instead of 'err' to match our logging framework
    catchParameterNames: CatchParameterNamesConfig(
      exception: 'error',
      stackTrace: 'stackTrace',
    ),
  ),
);
Add to your CI pipeline:
# .github/workflows/lint.yml
- name: Run analyzer
  run: dart analyze --fatal-infos

Next Steps

Build docs developers (and LLMs) love