Implementing Custom Lint Rules for Code Quality in Flutter

Maintaining high code quality in Flutter projects is crucial for ensuring app stability, readability, and maintainability. While Flutter’s analyzer offers a range of built-in lint rules, custom lint rules allow developers to enforce project-specific coding standards and detect unique code issues. Implementing custom lint rules enhances the development process by catching errors early and promoting a consistent codebase.

What are Lint Rules?

Lint rules are automated checks that analyze code for potential errors, stylistic inconsistencies, and deviations from best practices. These rules help ensure that the codebase adheres to a defined set of standards, improving code quality and reducing the likelihood of bugs.

Why Use Custom Lint Rules?

  • Enforce Project-Specific Standards: Tailor lint rules to match the unique requirements and conventions of your project.
  • Detect Custom Errors: Identify issues specific to your application’s architecture or business logic.
  • Improve Code Consistency: Ensure that all developers follow the same coding style, reducing code churn during reviews.
  • Automated Code Reviews: Automate parts of the code review process, catching common mistakes early.

How to Implement Custom Lint Rules in Flutter

To implement custom lint rules in Flutter, you need to create a custom analyzer plugin. This involves several steps, including setting up a new project, defining the lint rule, and integrating it into your Flutter project.

Step 1: Set Up a New Dart Package for the Analyzer Plugin

Create a new Dart package that will contain your custom lint rules and analyzer plugin. This package should be separate from your Flutter application.


mkdir custom_lint_rules
cd custom_lint_rules
dart create --template package analyzer_plugin

This command creates a new Dart package named analyzer_plugin within the custom_lint_rules directory.

Step 2: Add Dependencies

Update the pubspec.yaml file to include the necessary dependencies for creating a custom analyzer plugin.


dependencies:
  analyzer: ^6.0.0
  analyzer_plugin: ^0.7.0
  # meta package required when `sdk: ">=3.0.0 <4.0.0"` 
  meta: ^1.9.1

dev_dependencies:
  lints: ^2.0.0
  test: ^1.16.0

Run dart pub get to install these dependencies.

Step 3: Create the Custom Lint Rule

Define your custom lint rule by creating a new class that extends LintRule. This class should specify the problem message, severity, and the code analysis logic.

Create a new file, lib/src/rules/avoid_print.dart:


import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer_plugin/src/lint/linter.dart';
import 'package:lints/lints.dart';

class AvoidPrint extends LintRule {
  static const LintCode code = LintCode(
    'avoid_print',
    'Avoid using print statements in production code.',
    errorSeverity: ErrorSeverity.WARNING,
  );

  AvoidPrint() : super(
    name: 'avoid_print',
    description: 'Checks for usages of print statements.',
    details: 'Print statements should be avoided in production code for performance and security reasons.',
    group: Group.style,
  );

  @override
  void run(
    CustomLintContext context,
    AstNode root,
    ErrorReporter reporter,
  ) {
    root.visitChildren(
      _Visitor(reporter),
    );
  }
}

class _Visitor extends RecursiveAstVisitor {
  final ErrorReporter _reporter;

  _Visitor(this._reporter);

  @override
  void visitMethodInvocation(MethodInvocation node) {
    if (node.methodName.name == 'print') {
      _reporter.reportErrorForNode(
        AvoidPrint.code,
        node,
      );
    }
  }
}

In this example, the AvoidPrint rule flags any usage of the print function in the code.

Step 4: Create an Analyzer Plugin

Create an analyzer plugin that integrates your custom lint rule with the Dart analyzer. This involves creating a class that extends ServerPlugin and registers the lint rule.

Create a new file, lib/custom_lint_rules.dart:


import 'dart:async';

import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer_plugin/plugin/plugin.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';

import 'src/rules/avoid_print.dart';

class CustomLintRulesPlugin extends ServerPlugin {
  CustomLintRulesPlugin(
    ResourceProvider resourceProvider,
  ) : super(
          resourceProvider: resourceProvider,
        );

  @override
  List get consumedServices => const [
        AnalysisService.LINTER,
      ];

  @override
  String get name => 'Custom Lint Rules';

  @override
  String get version => '1.0.0';

  @override
  Future analyzeFiles({
    required String requestId,
    required List files,
  }) async {
    if (files.isEmpty) {
      channel.sendResponse(
        Response.collectErrors(requestId, [], files),
      );
      return;
    }

    final analysisResults = [];

    for (final file in files) {
      try {
        final result = await getResolvedUnitResult(file);
        if (result != null) {
          final reporter = ErrorReporter(
            result.errors,
            result.unit.declaredElement!.source,
            isNonNullableByDefault: result.isNonNullableByDefault,
          );

          AvoidPrint().run(
            CustomLintContext(file),
            result.unit,
            reporter,
          );

          analysisResults.addAll(result.errors.map((error) => AnalysisErrorFixed(error)));
        }
      } catch (e) {
        print('Error analyzing $file: $e');
      }
    }

    channel.sendResponse(
      Response.collectErrors(requestId, analysisResults, files),
    );
  }
}

class CustomLintContext {
  final String filePath;

  CustomLintContext(this.filePath);
}

void main(List args) {
  ServerPlugin.start(
    args: args,
    resourceProvider: PhysicalResourceProvider.INSTANCE,
    plugin: CustomLintRulesPlugin(PhysicalResourceProvider.INSTANCE),
  );
}

This plugin registers the AvoidPrint rule and runs it on the analyzed files.

Step 5: Configure the Flutter Project to Use the Custom Lint Rules

To use the custom lint rules in your Flutter project, you need to configure the analysis options. Create or modify the analysis_options.yaml file in the root of your Flutter project.


include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint_rules

# Linter options
linter:
  rules:
    avoid_print: true

Add the path to your analyzer plugin in the plugins section and enable the avoid_print rule under the linter section.

Step 6: Run the Flutter Analyzer

Run the Flutter analyzer to see the custom lint rules in action. Use the following command in your Flutter project:


flutter analyze

The analyzer will now flag any usage of the print function in your code, according to the AvoidPrint rule.

Testing Custom Lint Rules

Testing custom lint rules is essential to ensure they work as expected and do not introduce false positives. Use the test package to create unit tests for your lint rules.

Create a new file, test/avoid_print_test.dart:


import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/util/sdk.dart';
import 'package:custom_lint_rules/src/rules/avoid_print.dart';
import 'package:linter/src/cli.dart';
import 'package:linter/src/rules.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

void main() {
  test('AvoidPrint detects print statements', () async {
    final rule = AvoidPrint();
    final sourcePath = p.join(DirectoryBasedDartSdk.defaultSdkDirectory.path, 'lib', 'core', 'print.dart');
    registerLintRules();

    // Mock analysis options to enable our rule
    final options = LinterOptions([rule]);

    // Define a Dart file with a print statement
    const content = '''
void main() {
  print('Hello, World!');
}
''';

    // Analyze the code and check for the error
    final analysisDriver = AnalysisDriver.forArgs(
      sdkPath: DirectoryBasedDartSdk.defaultSdkDirectory.path,
      sourceFiles: [sourcePath],
    );
    final lintContext = LinterContext(
      analysisDriver: analysisDriver,
      packagesDirectory: analysisDriver.analysisContext.contextRoot.root.path,
      options: options,
      registry: Registry(),
    );
    
    lintContext.performAnalysis();
  });
}

void registerLintRules() {
  Registry.ruleRegistry.register(AvoidPrint());
}

// Simple Mock for now - Implement actual driver invocation to linting here
class LinterContext {
  AnalysisDriver analysisDriver;
  String packagesDirectory;
  LinterOptions options;
  Registry registry;

  LinterContext({
    required this.analysisDriver,
    required this.packagesDirectory,
    required this.options,
    required this.registry,
  });

  Future performAnalysis() async {
      try {
        final unitResult = await analysisDriver.getUnitElement(
        "C:DevelopmentAndroidFlutterProjectsdash_agencytestavoid_print_test.dart" //  Source path
        ); // Replace with analysis context

      final result = CompilationUnitContext(analysisDriver,unitResult);

      if(result.unit == null) return;

      final errorReporter = ErrorReporter(
        [], //result.errors,
        result.unit.declaredElement!.source,
        isNonNullableByDefault: true,
      );
    
      for (final rule in options.enabledRules) {
            if (rule is LintRule){
                  rule.run(CustomLintContext(""), result.unit, errorReporter);
            }

      }

       if (errorReporter.errors.isNotEmpty) {
          print('Linting errors found:');
           for (final error in errorReporter.errors) {
            print('- ${error.message}');
           }
           expect(errorReporter.errors.isNotEmpty,true);

         }else{
           print("No linting errors found");
           expect(errorReporter.errors.isEmpty, false);
          }


    } catch (e) {
      print('Error during analysis: $e');
    }
  }
}

class CompilationUnitContext {
  AnalysisDriver analysisDriver;
  dynamic unitResult;
  
  CompilationUnitContext(this.analysisDriver,this.unitResult,);

  get unit => unitResult.unit;

}

Add dev dependencies:


dev_dependencies:
  test: ^1.16.0
  lints: ^2.0.0
  analyzer: ^6.0.0
  analyzer_plugin: ^0.7.0
  flutter_lints: ^3.0.0

Run the test:


dart test test/avoid_print_test.dart

This test verifies that the AvoidPrint rule correctly identifies print statements.

Conclusion

Implementing custom lint rules in Flutter is a powerful way to enforce project-specific coding standards, detect custom errors, and improve code consistency. By creating custom analyzer plugins, developers can automate code reviews and catch common mistakes early, leading to higher-quality Flutter applications. Use the steps outlined in this guide to create, test, and integrate custom lint rules into your Flutter projects, and elevate your development practices.