Implementing Custom Lint Rules with Dart Linter in Flutter

In Flutter development, maintaining a consistent and high-quality codebase is crucial for the long-term success of any project. Linting tools play a vital role in this process by enforcing coding standards, detecting potential errors, and promoting best practices. While Flutter comes with a default set of lint rules, creating custom lint rules tailored to your specific project needs can significantly enhance code quality and maintainability. This article delves into how to implement custom lint rules using the Dart linter in a Flutter project.

Why Use Custom Lint Rules?

  • Enforce Project-Specific Standards: Ensure all developers adhere to the coding conventions and practices specific to your project.
  • Detect Anti-Patterns: Identify and prevent the use of code patterns that may lead to performance issues, bugs, or maintainability problems.
  • Improve Code Consistency: Maintain a uniform codebase across the entire project, making it easier to read, understand, and modify.
  • Automated Code Review: Automate parts of the code review process, catching common mistakes and enforcing best practices automatically.

Prerequisites

Before you begin, make sure you have the following:

  • A Flutter project.
  • Basic knowledge of Dart and Flutter.
  • Familiarity with the Dart linter.

Step 1: Set Up Your Project

Ensure your Flutter project has a analysis_options.yaml file in the root directory. If you don’t have one, create it. This file configures the Dart analyzer and linter.

include: package:flutter_lints/flutter.yaml

analyzer:
  exclude: [build/**]

linter:
  rules:
    avoid_print: true # Example built-in rule

Step 2: Create a Custom Lint Rule Project

To create custom lint rules, you need to create a separate Dart package dedicated to these rules. This helps keep your main project clean and modular.

Create a New Package

Create a new Dart package named custom_lint_rules using the following command:

flutter create --template=package custom_lint_rules

Configure pubspec.yaml

Modify the pubspec.yaml file of your custom_lint_rules package to include dependencies on analyzer and linter:

name: custom_lint_rules
description: A package for custom Dart lint rules.
version: 0.0.1

environment:
  sdk: ">=3.0.0 =6.0.0 =2.0.0 <4.0.0"

dev_dependencies:
  lints: ^2.0.0
  test: ^1.21.0

Run flutter pub get to install the dependencies.

Step 3: Implement Your Custom Lint Rule

Create a new Dart file inside the lib directory of your custom_lint_rules package to define your custom lint rule. Let’s create a rule that prevents the use of magic numbers (i.e., unnamed numeric literals) in the code.

Create avoid_magic_number_rule.dart

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/listener.dart';
import 'package:linter/src/analyzer.dart';

class AvoidMagicNumber extends LintRule {
  static const LintCode code = LintCode(
    'avoid_magic_number',
    'Avoid using magic numbers. Define constants instead.',
    correctionMessage: 'Replace the magic number with a named constant.',
  );

  AvoidMagicNumber() : super(
      code: code,
      documentationDescription: 'Avoid using unnamed numeric literals (magic numbers) directly in the code.  Define a constant for it instead.'
  );

  @override
  void registerNodeProcessors(
      NodeLintContext context,
      ) {
    context.visitIntegerLiteral(this as dynamic);
    context.visitDoubleLiteral(this as dynamic);
  }

  @override
  void visitIntegerLiteral(IntegerLiteral node) {
    _checkLiteral(node, node.value.toString(), node.literal.toString(), node.offset, node.length, node.end);
  }

  @override
  void visitDoubleLiteral(DoubleLiteral node) {
    _checkLiteral(node, node.value.toString(), node.literal.toString(), node.offset, node.length, node.end);
  }

  void _checkLiteral(AstNode node, String value, String lexeme, int offset, int length, int end) {
    if (value == '0' || value == '1') {
      // allow 0 and 1 by default; can configure in the options later.
      return;
    }

    final ErrorReporter errorReporter = node.root.unit.errorReporter;
    errorReporter.reportErrorForOffset(code, offset, length, [value]);
  }
}

This code defines a lint rule that checks for integer and double literals and reports an error if a magic number (other than 0 or 1) is found. LintCode is used to define the error message and correction hint.

Step 4: Register Your Custom Lint Rule

To make the Dart analyzer aware of your custom lint rule, you need to register it. Create a file named rules.dart in the lib directory of your custom_lint_rules package:

import 'package:custom_lint_rules/avoid_magic_number_rule.dart';

final List rules = [
  AvoidMagicNumber(),
];

Step 5: Update analysis_options.yaml in Your Main Project

To use the custom lint rule in your Flutter project, update the analysis_options.yaml file to include the custom_lint_rules package and enable the rule.

include: package:flutter_lints/flutter.yaml

analyzer:
  exclude: [build/**]
  plugins:
    - custom_lint_rules

linter:
  rules:
    avoid_print: true
    # Enable custom lint rules
    avoid_magic_number: true
    
  plugins:
    - custom_lint_rules

Add the `plugins` entry in `analyzer` and add custom rules in the linter section to use these plugin. Also, add the below dependency path to custom_lint_rules to use them.

dependencies:
  flutter:
    sdk: flutter
  custom_lint_rules:
    path: ../custom_lint_rules

Also to prevent magic_number from using same directory update the rule in the same anaysis options

Step 6: Test Your Custom Lint Rule

To test your custom lint rule, run the Dart analyzer in your Flutter project:

flutter analyze

Create a Dart file in your Flutter project that uses magic numbers to see the custom lint rule in action:

void main() {
  int age = 25; // Magic number
  double pi = 3.14; // Magic number
  print('Age: $age, PI: $pi');
}

You should see the following lint errors:

info • Avoid using magic numbers. Define constants instead. • lib/main.dart:2:13 • avoid_magic_number
info • Avoid using magic numbers. Define constants instead. • lib/main.dart:3:14 • avoid_magic_number

Step 7: Customize Your Lint Rule (Optional)

You can customize your lint rule by adding options in the analysis_options.yaml file. For example, you might want to allow specific magic numbers or ignore certain files.

linter:
  rules:
    avoid_magic_number:
      allowed_numbers: [0, 1, 100] # Allow 100
      ignore_files: [lib/utils.dart]

Update your lint rule to read and use these options.

Advanced Usage

  • Custom Documentation: Provide detailed documentation for each custom rule, explaining why it’s important and how to fix violations.
  • Integration with CI/CD: Integrate the linting process into your CI/CD pipeline to automatically check code for violations.
  • Configurable Severity: Allow configuration of rule severity (e.g., info, warning, error) to match project needs.

Conclusion

Implementing custom lint rules with the Dart linter in Flutter can significantly improve the quality and consistency of your codebase. By enforcing project-specific standards, detecting anti-patterns, and automating code reviews, you can create a more maintainable and robust application. Follow the steps outlined in this article to create your own custom lint rules and elevate your Flutter development process.