Newsletter
Subscribe if you want to know how to build digital products
By submitting your email you agree to receive the content requested and to LeanCode's Privacy Policy
Case studies
CareerAbout us

Lint Smarter, Not Harder: Migrate to the New Dart Analyzer Plugin System

Piotr Rogulski - Flutter Developer at LeanCode
Piotr Rogulski - Flutter Developer at LeanCode
May 6, 2025 • 8 min
Piotr Rogulski - Flutter Developer at LeanCode
Piotr Rogulski
Flutter Developer at LeanCode

Rating: 5.00 / 5 Based on 9 reviews

Rating: 5.00 / 5 Based on 9 reviews

The Dart ecosystem is constantly evolving, and a significant change is on the horizon for developers who rely on custom static analysis: a new, first-party analyzer plugin system. This upcoming feature, tracked in GitHub issue #53402, is poised to become the official, integrated way to write and distribute custom lint rules and fixes, effectively replacing the popular third-party custom_lint package. Crucially, this new system is also expected to be more performant by integrating custom analysis directly into the main dart analyze process.

One of the most immediate and welcome changes this new system brings is streamlining the analysis process. Currently, developers using custom lint rules (such as leancode_lint) face a two-step verification process: running the standard dart analyze for built-in diagnostics and executing a separate dart run custom_lint command to check their custom rules.

With the new integrated system, this friction disappears. Custom rules defined using the new plugin architecture will be automatically discovered and executed during the standard dart analyze command. This means a single command will provide a comprehensive view of all potential issues in your codebase, whether they originate from the Dart SDK, the core lints package, or your own custom plugins. This unification promises a smoother, faster, and less error-prone development workflow.

In this article I will show you how to migrate to the new Dart Analyzer System Plugin the LeanCode way.

Migration

Plugin definition

The first (and probably the most important) change is the way you register your plugin with the analyzer.

Before, you extended PluginBase and provided a createPlugin function.

PluginBase createPlugin() => _Linter();
class _Linter extends PluginBase {
  @override
  List<LintRule> getLintRules(CustomLintConfigs configs) => [
        const StartCommentsWithSpace(),
        ...UseDesignSystemItem.getRulesListFromConfigs(configs),
        PrefixWidgetsReturningSlivers.fromConfigs(configs),
        const AddCubitSuffixForYourCubits(),
        const CatchParameterNames(),
        const AvoidConditionalHooks(),
        const HookWidgetDoesNotUseHooks(),
        const ConstructorParametersAndFieldsShouldHaveTheSameOrder(),
        const AvoidSingleChildInMultiChildWidgets(),
      ];
 
  @override
  List<Assist> getAssists() => [
        ConvertRecordIntoNominalType(),
        ConvertPositionalToNamedFormal(),
        ConvertIterableMapToCollectionFor(),
      ];
}

Now, you extend the Plugin class and create a top-level plugin variable.

final plugin = _Linter();

class _Linter extends Plugin {
  @override
  void register(PluginRegistry registry) {
    registry
      ..registerWarningRule(StartCommentsWithSpace()..registerFixes(registry))
      ..registerWarningRule(AddCubitSuffixForYourCubits())
      ..registerWarningRule(CatchParameterNames())
      ..registerWarningRule(AvoidConditionalHooks())
      ..registerWarningRule(
        HookWidgetDoesNotUseHooks()..registerFixes(registry),
      )
      ..registerWarningRule(AvoidSingleChildInMultiChildWidgets());
  }
}

Keep in mind that the registerFixes method shown above is not part of the core API. It comes from a mixin that simplifies the management of lint fixes.

mixin AnalysisRuleWithFixes on AnalysisRule {
  List<ProducerGenerator> get fixes;

  void registerFixes(PluginRegistry registry) {
    for (final fix in fixes) {
      registry.registerFixForRule(lintCode, fix);
    }
  }
}

Lint definitions

The new system requires some changes to how you define your lints, but they are relatively minor.

Before:

class AvoidSingleChildInMultiChildWidgets extends DartLintRule {
  const AvoidSingleChildInMultiChildWidgets()
      : super(
          code: const LintCode(
            name: 'avoid_single_child_in_multi_child_widgets',
            problemMessage: 'Avoid using {0} with a single child.',
            correctionMessage:
                'Remove the {0} and achieve the same result using dedicated widgets.',
            errorSeverity: ErrorSeverity.WARNING,
          ),
        );
}

After:

class AvoidSingleChildInMultiChildWidgets extends AnalysisRule {
  AvoidSingleChildInMultiChildWidgets()
    : super(
        name: 'avoid_single_child_in_multi_child_widgets',
        description: 'Avoid using {0} with a single child.',
      );

  @override
  LintCode get lintCode => LintCode(
    name,
    description,
    correctionMessage:
        'Remove the {0} and achieve the same result using dedicated widgets.',
  );
}

Please note that currently, lint developers have no ability to define the default severity for their rules, which defaults to info.

Node processors

The most significant change from custom_lint is how you subscribe to specific parts of the Dart code. custom_lint uses a callback-based API, which allows you to define simple functions with your lint logic. The new system, however, uses a visitor pattern that is much more flexible and closer to the core of the Dart runtime.

Before:

@override
void run(
  CustomLintResolver resolver,
  ErrorReporter reporter,
  CustomLintContext context,
) {
  context.registry.addClassDeclaration((node) {
    // Inspect a class declaration
  });
}

After:

@override
void registerNodeProcessors(
  NodeLintRegistry registry,
  LinterContext context,
) {
  registry.addClassDeclaration(this, _Visitor(this, context));
}

class _Visitor extends SimpleAstVisitor<void> {
  _Visitor(this.rule, this.context);

  final LintRule rule;
  final LinterContext context;

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    // Inspect a class declaration
  }
}

Limitations

As with any significant new feature under development, the initial versions might not have full parity with existing solutions. Based on the current state of the new analyzer system, there are a couple of notable omissions compared to what’s offered with custom_lint.

User configuration

A significant feature missing in the current design is the ability for users of a custom lint package to configure the behavior of specific rules. For example, the use_design_system_item lint from leancode_lint heavily relies on users' configuration (e.g., specifying which widgets count as "design system items"). Without this capability, complex, configurable lints might be difficult or impossible to implement or migrate directly to the new system in its initial form.

Creating assists

The new system focuses on reporting lints (diagnostics) and providing fixes directly tied to those specific reported lints. It doesn't currently support assists – contextual actions that are always available in the IDE based on the cursor position, regardless of whether a lint is reported (e.g., “Wrap with Widget…”). However, the Dart team has this feature on the radar, and it will be implemented as part of “product excellence.”

Missing convenience utilities

The new system for defining lints is still a work in progress, so it’s not unexpected that certain quality-of-life improvements are yet to be implemented. The most significant omission is the lack of the TypeChecker, a utility that enables developers to check the type of inspected values easily. However, this class already exists in one of the core Dart packages, so it is likely to make its way into the new linter system.

Check out what LeanCode lints can do

Lints help enforce consistent code style, catch potential bugs early, and maintain high code quality across teams - especially in growing projects. At LeanCode, we've been building and using custom lint rules for years to ensure our Flutter apps meet the highest standards. We have released or own leancode_lint package that you can find on Pub.dev.

Here’s what LeanCode lints that are a part of this leancode_lint package can do to improve your development workflow.

add_cubit_suffix_for_your_cubits

This leancode_lint helps you keep your code clean by ensuring all your cubits have consistent names.

screen
circlecrossrectangletrianglesblock

use_design_system_item

This leancode_lint helps you keep your app’s UI consistent by ensuring you use your custom app components.

screen
circlecrossrectangletrianglesblock

avoid_conditional_hooks

This leancode_lint helps prevent bugs in your code by detecting hooks used inside conditional code.

screen
circlecrossrectangletrianglesblock

hook_widget_does_not_use_hooks

This leancode_lint helps you keep your app efficient by detecting unnecessary HookWidgets in your code.

screen
circlecrossrectangletrianglesblock

Summing up

The new first-party Dart analyzer plugin system is a significant step forward for Flutter developers, expected to replace custom_lint by integrating custom rules directly into the standard dart analyze command. This unification streamlines the development workflow, improves performance, and provides a single, comprehensive view of all codebase issues. At LeanCode, we believe this represents a vast improvement and opens up many new possibilities, offering a smoother and faster development experience, despite current limitations.

You may also like

How We Boosted Moving Flutter Widgets to Widgetbook

At LeanCode, while working on complex Flutter projects, we noticed that integrating widgets into Widgetbook was often repetitive and time-consuming. To address this, we created the Widgetbook Entries Generator - a VSCode extension that simplifies the process. See how it works.
Widgetbook Entries Generator by Leancode

No Macros in Dart, How to Replace Freezed?

Unfortunately, the Dart team has taken up the difficult decision of abandoning their work on the macros language feature. Although freezed is a powerful package, it comes with some costs. At some point, we decided to move forward and use alternative solutions for new code and projects. See our approach.
A replacement for freezed in Dart

Feature-Based Flutter App Architecture - LeanCode

Read about the LeanCode approach to Flutter architecture. We highlight some of the design decisions that should be made when developing a feature in mobile apps. This includes our approach to dependency injection, state management, widget lifecycle, and data fetching.
Flutter architecture by LeanCode