This explains how to write a simple analyzer plugin.
Create a Dart project called demo_plugin and add these dependencies to pubspec.yaml. The versions shown below should work with Dart 3.11. You might need to adapt them to your Dart version.
dependencies:
analysis_server_plugin: ^0.3.4
analyzer: ^9.0.0
analyzer_plugin: ^0.13.11
Next create a lib/main.dart file that defines a subclass of Plugin. Provide a name. The register method is used later.
class DemoPlugin extends Plugin {
@override
String get name => 'Demo plugin';
@override
void register(PluginRegistry registry) {}
}
Provide a singleton via a global variable called plugin:
final plugin = DemoPlugin();
Configure to your plugin in analysis_options.yaml. You could refer to a package from https://pub.dev or use a local package as I'll do here, using path to point to the plugin package, in my case . as I'm using the same package to implement and to use it.
plugins:
demo_plugin:
path: .
If you're using VisualStudio Code, run "Dart: Open Analyzer Diagnostics / Insights" via Cmd+Shift+P (or Alt+Shift+P) which opens a web page where you can click on Plugins to see registered "Demo Plugin". This might take 10 seconds to occur.
Currently, the plugin simply exists.
To make it useful, let's create a linter rule that forbids variable names that end with y (the first example that came to my mind, not really useful in practice) as in
int thingy = 10;
Here's my linter rule, implemented as a subclass of AnalysisRule:
class NoYRule extends AnalysisRule {
NoYRule() : super(name: code.name, description: code.problemMessage);
@override
DiagnosticCode get diagnosticCode => code;
static const code = LintCode(
'no_y_rule',
"Don't end variable names with 'y'",
severity: .WARNING,
correctionMessage: "Try removing the 'y'. Use 'ish' instead.",
);
}
Register the rule with the plugin in register:
class DemoPlugin extends Plugin {
...
@override
void register(PluginRegistry registry) {
registry.registerLintRule(NoYRule());
}
}
The rule has no behavior yet. To actually find the variables ending with y, we need to setup an AST visitor. Its main purpose is to create the warning.
But before we do that, I need to learn about the correct visitor method and AST node to use. So I'll create a tiny command line program to explore the AST:
void main() {
final result = parseString(
content: 'int thingy = 10; void testy() { print(thingy); }',
);
result.unit.accept(NoYVisitor());
}
class NoYVisitor extends RecursiveAstVisitor<void> {
@override
void visitVariableDeclaration(VariableDeclaration node) {
print(node.name);
super.visitVariableDeclaration(node);
}
}
This prints thingy once for the int variable declaration, but I'd like to find the variable refence, too, and that has a non obvious visitor method. But I can find the print() which is a method invocation and then ask its first argument for its runtime type:
@override
void visitMethodInvocation(MethodInvocation node) {
print(node.argumentList.arguments[0].runtimeType);
super.visitMethodInvocation(node);
}
That's a SimpleIdentifier node.
Unfortunately, print is also such an identifier and as I can't easily distinguish method names and variable names. This would cause false positives. I can ask a node for its parent, though, and ignore those identifiers which have a MethodInvocation node as parent. Better that nothing.
So, I've to look for two kinds of AST nodes and inspect the token that represents the variable name. According to the documentation, I must inherit from SimpleAstVisitor and then register the the visitor for each visited node. Note that I pass with rule to the visitor so I can report the warning:
class NoYVisitor extends SimpleAstVisitor<void> {
NoYVisitor(this.rule);
final AnalysisRule rule;
@override
void visitVariableDeclaration(VariableDeclaration node) {
_check(node.name);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (node.parent is! MethodDeclaration) {
_check(node.token);
}
}
void _check(Token token) {
final name = token.lexeme;
if (name.length > 1 && name.endsWith('y')) {
rule.reportAtToken(token);
}
}
}
The visitor needs to be registered within the rule for both kinds of nodes:
class NoYRule extends AnalysisRule {
...
@override
void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
final visitor = NoYVisitor(this);
registry.addVariableDeclaration(this, visitor);
registry.addSimpleIdentifier(this, visitor);
}
...
}
Once this has been all saved, the analysis server should restart the plugin and you should see this on the Plugins web page:
Demo plugin
Lint rules:
* no_y_rule
Last but not least, you have to activate the rule by adding no_y_rule: true in analysis_options.yaml below the diagnostics section:
plugins:
demo_plugin:
...
diagnostics:
no_y_rule: true
We're done.
A piece of code like this is now marked and a warning is issued:
final thingy = 10;
~~~~~~
^ Don't end variable names with 'y'
Try removing the 'y'. Use 'ish' instead.
The same should be true for
print(thingy);
~~~~~~
^ Don't end variable names with 'y'
Try removing the 'y'. Use 'ish' instead.
It's working!
If you want to disable the linter rule, you can use the usual // ignore comment, however, you've to add a demo_plugin/ prefix to the name. This isn't automatically generated by VisualStudio Code. I don't know whether that's a bug in that tool or whether I didn't configured the correct rule name.
Add this to main.dart to get rid of the warning @ registry:
// ignore_for_file: demo_plugin/no_y_rule
I'd like to offer a quix fix to replace the y with ish as already hinted in the problem description. This is called a quick fix, automatically available on the light-bubble context menu.
I need to create a rather complex ResolvedCorrectionProducer with a compute method that performs the change to the source code. Right now, I don't know how to rename all occurences of that variable, so I'm just renaming a single token.
class RenameY extends ResolvedCorrectionProducer {
RenameY({required super.context});
@override
FixKind? get fixKind => const FixKind(
'dart.fix.renameY',
DartFixKindPriority.standard,
"Rename variables ending in 'y' to '~ish'",
);
@override
CorrectionApplicability get applicability =>
CorrectionApplicability.automatically;
@override
Future<void> compute(ChangeBuilder builder) async {
if (node case VariableDeclaration node) {
final oldName = node.name.lexeme;
if (oldName.length > 1 && oldName.endsWith('y')) {
final newName = '${oldName.substring(0, oldName.length - 1)}ish';
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(range.token(token), newName);
});
}
}
}
}
Note that I just dealt with variable declarations. I leave the variable references to the reader. Also note that I just copied the id format from the documentation and I'm not sure whether it is good practice to start custom rules with dart.fix..
Add the RenameY fix to the plugin, refering the rule it fixes:
class DemoPlugin extends Plugin {
...
@override
void register(PluginRegistry registry) {
...
registry.registerFixForRule(NoYRule.code, RenameY.new);
}
}
Now reload the plugin by disabling and re-enabling the linter rule in analysis_options.yaml and the Plugins web page should display this:
Demo plugin
Lint rules:
* no_y_rule
Quick fixes:
* dart.fix.renameY: "Rename variables ending in 'y' to '~ish'" to fix no_y_rule
VisualStudio Code should offer that quick fix now, rename a variable name in a variable declaration. To actually rename not only a single occurence but all names, both definition and all occurences, studying this fix might help.
I'd like to also add a quick assist rule that is offered by the IDE independently of issues. To make this example at least slightly useful, the assist rule will add a void main() {} function if there isn't one.
I need to inspect the CompilationUnit to detect top-level function declarations that are named main. So, let's explode the AST again:
class AddMainVisitor extends RecursiveAstVisitor<void> {
@override
void visitCompilationUnit(CompilationUnit node) {
print(node.declarations.last.runtimeType);
super.visitCompilationUnit(node);
}
}
That's a FunctionDeclation, so this expression should do the trick:
final hasMain = node.declarations.any(
(node) => node is FunctionDeclaration && node.name.lexeme == 'main',
);
So here is my quick assist, again inheriting from ResolvedCorrectionProducer but this time, overriding assistKind instead of fixKind.
class AddMain extends ResolvedCorrectionProducer {
AddMain({required super.context});
@override
AssistKind? get assistKind => const AssistKind(
'dart.assist.addMain',
30, // there's no DartAssistKindPriority enum
"Add main() function to file",
);
@override
CorrectionApplicability get applicability => .singleLocation;
@override
Future<void> compute(ChangeBuilder builder) async {
if (node case CompilationUnit node) {
final hasMain = node.declarations.any(
(node) => node is FunctionDeclaration && node.name.lexeme == 'main',
);
if (!hasMain) {
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleInsertion(selectionOffset, 'void main() {\n \n}\n');
});
builder.setSelection(Position(file, selectionOffset + 16));
}
}
}
}
We should probably not hard-code a two-space-indentation, but feel free to change that. I like it this way and it is convenient to move the cursor just after the { in a new line, properly indented, even if the + 16 is a magic number anti pattern.
Next, add the new assist to the plugin:
class DemoPlugin extends Plugin {
...
@override
void register(PluginRegistry registry) {
...
registry.registerAssist(AddMain.new);
}
}
After reloading the plugin, you should now find a new code action in in the context menu if you're at top-level, not adjacent to any other declaration node. Try it.
To sumarize, custom linter rules, quick fixes and quick assists are surprisingly easy to implement. Working with the AST is trial and error because of poor documentation. But still, you could now implement a rule that reports an error if you import a flutter package in a file that is in a models folder or warn if you use a Scaffold widget in a widget that isn't called ~Page.