Creating Your First Dart Analyzer Plugin with the New Plugin System

Here’s how VGV is using Flutter 3.38 and Dart 3.10’s new analyzer plugin system to automate best practices

Óscar Martín
Óscar Martín
December 1, 2025
3min
News
Creating Your First Dart Analyzer Plugin with the New Plugin System

At Very Good Ventures (VGV), we believe that code quality isn’t just about making things work—it’s about building in a consistent, scalable way. That’s why we rely heavily on our Very Good Analysis package and document our engineering standards publicly through Very Good Engineering

With the new release of Flutter 3.38 and Dart 3.10, the ecosystem gained a much-needed, modernized analyzer plugin system.

To recap: The analyzer plugin system is a feature that allows developers to create additional checks and rules that don't exist out of the box in the Dart analyzer, effectively allowing them to expand that tool.

A plugin system in the dart analyzer isn’t something totally new. A plugin system already existed, but it presented two main issues:

  • Memory Usage: The need of having one isolate per plugin meant running multiple plugins could cause significant memory issues.

  • Not a Single Command: Using different custom lint libraries required running different commands (not just dart analyze).

The new plugin system solves these issues and makes it much easier for developers to build plugins effectively. It also brings many considerations overall, so if you’re curious about the decision process behind it, make sure to read the original proposal

Now, let’s deep dive into this new exciting change for the Dart ecosystem.

How to Create a Package

Once you have a package created (you can do so with Very Good CLI), the very first step is to declare a new plugin using the dependency analysis_server_plugin.

class YourPlugin extends Plugin {
 @override
 void register(PluginRegistry registry) {
   // Here we will register all our rules
 }


 @override
 String get name => 'your_plugin_name';
}

You must create a new entry point for your plugin. This entry point needs to be in lib/main.dart:

final plugin = YourPlugin();


And you must declare in the pubspec.yaml of your package that it contains a Plugin:

plugin:
 platforms:
   dart:
     pluginClass: YourPlugin
     fileName: main.dart

Now, to start using this plugin in your application you just need to have an analysis_options.yaml file with those lines:

plugins:
 your_plugin_name:
   path: yourPath

Creating Your First Rule

Once you’re all set up, the fun part starts: creating your own rules!

Let’s say your team has a best practice that requires using the isTrue/isFalse matchers used during testing instead of manually checking “true”/”false” primitive values.

Something like this:

 // Bad
 expect(myVariable, true);
 // Good
 expect(myVariable, isTrue);

Let's create a rule that will enforce that practice. First, we will add the analyzer dependency.

Next, we create a class to represent that rule. The basic structure of any rule will be: A class extending AnalysisRule which defines the rule name, description, and the correction message: 

class PreferBoolMatcher extends AnalysisRule {
 PreferBoolMatcher()
   : super(
       name: rule,
       description: _description,
     );


 static const rule = 'prefer_bool_matcher';


 static const _description =
     'Prefers using `isTrue`/`isFalse` matchers in `expect(...)` calls.';


 static const _correctionMessage =
     'Try using the `isTrue` or `isFalse` matcher instead of the raw '
     'boolean literal.';
. . .
}

Then, we define the DiagnosticCode, which basically will help the developer once it has the issue on the code with the appropriate message and a way to fix it:

 /// The code for the lint rule.
 static const LintCode code = LintCode(
   rule,
   _description,
   correctionMessage: _correctionMessage,
 );


 @override
 DiagnosticCode get diagnosticCode => code;

To implement the code that’ll do the actual checking, we use a Visitor.

The Visitor's job is to "walk through" the code and make sure it follows a specific rule. Here's a closer look:

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


 final AnalysisRule rule;
 final RuleContext context;


...  
}

And then we override the most convenient method for us to “visit pieces of code” and perform the checking.

In this case, we use visitMethodInvocation since we need to access the way we’re invoking a method (“expect”). Basically, we’re checking whether the method invoked is expect, and if the second argument is a BooleanLiteral (true/false):



 @override
 void visitMethodInvocation(MethodInvocation node) {
   if (node.methodName.name != 'expect') return;


   final arguments = node.argumentList.arguments;


   if (arguments.length < 2) return;


   final matcherArgument = arguments[1];


   if (matcherArgument is BooleanLiteral) {
     rule.reportAtNode(matcherArgument);
   }
 }

Once we have the visitor defined, we need to register it in the rule we created above (PreferBoolMatcher).

@override
 void registerNodeProcessors(
   RuleVisitorRegistry registry,
   RuleContext context,
 ) {
   final visitor = _Visitor(this, context);
   registry.addMethodInvocation(this, visitor);
 }

The easiest way to verify if your new lint rule is working is by forcing the issue with an example:

void main() {
 const myVariable = true;
 // Bad - It should give you the warning
 expect(myVariable, true);
}

Running dart analyze should throw this error:

Creating Your First Fix

Once your first lint rule is in place, you might wonder if that’s enough. While validating your code follows certain rules, it’s even more useful when those issues can be automatically fixed.

Assuming the previous example, we would just need to move from this:

expect(myVariable, true);


To this:

expect(myVariable, isTrue);

To do so, we just need to create a class that extends ResolvedCorrectionProducer, define the scope where the fix can be safely applied (CorrectionApplicability), and specify different properties such as the IDE-facing description and the fix’s unique identifier. 

class LiteralBoolInExpectFix extends ResolvedCorrectionProducer {
 /// {@macro matchers_in_expect_fix}
 LiteralBoolInExpectFix({required super.context});


 @override
 CorrectionApplicability get applicability =>
     CorrectionApplicability.singleLocation;


 @override
 FixKind get fixKind => const FixKind(
   'dart.fix.literalBoolInExpect',
   DartFixKindPriority.standard,
   "Use 'isTrue' or 'isFalse' matcher",
 );
. . .
}

After that, we’ll override the compute method, which will take care of the fix itself. In this case, we’ll be replacing the bool expression true/false with isTrue/isFalse:

 @override
 Future<void> compute(ChangeBuilder builder) async {
   final matcherArgument = node;


   if (matcherArgument is! BooleanLiteral) return;


   final expectedMatcher = matcherArgument.value ? 'isTrue' : 'isFalse';


   await builder.addDartFileEdit(file, (builder) {
     builder.addSimpleReplacement(
       range.node(matcherArgument),
       expectedMatcher,
     );
   });
 }

If everything worked fine, on VSC for example, you’ll see something like this:

Wrapping Up

We still have plenty to explore with the new plugin system, but it’s already clear how promising it is. At VGV, we’ve constantly proven how those engineering best practices pay off in the long term—and the ability to automate lint rules that reinforce those practices is especially exciting!

This is only a small glimpse of what’s possible. The new plugin system unlocks much more, and we’re excited to keep exploring it.

A special shout-out to Erick Zanardo, who helped me writing this blog, and Ricardo Dalarme, whose work on our early POCs helped shape many of the insights shared here.

Name of the heading

Category
Backend
Best Practices
Dart
Flutter
Share

Insights from Our Experts

View All Insights
How We Efficiently Onboard Engineers at Very Good Ventures

How We Efficiently Onboard Engineers at Very Good Ventures

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

Marcus Twichel
Marcus Twichel
November 20, 2025
How We Efficiently Onboard Engineers at Very Good Ventures
From Code to Community: How puf Inspires More People to Build More Apps

From Code to Community: How puf Inspires More People to Build More Apps

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

VGV Team
VGV Team
November 19, 2025
From Code to Community: How puf Inspires More People to Build More Apps
What It Takes to Modernize Without Breaking Trust

What It Takes to Modernize Without Breaking Trust

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

VGV Team
VGV Team
November 5, 2025
What It Takes to Modernize Without Breaking Trust