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

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.
Insights from Our Experts

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.

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.
%20(1).png)
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.
