How to create a custom lint in Flutter with custom_lints

Getting started with custom_lints

When the custom_lints package did not exist, one had to painfully connect to the dart analyser: a terrible and tedious DX. Fortunately, that’s not the case anymore.

The easiest way to get start is to follow the README of custom_lints on pub.dev. You should have your first lint showing in less than 10 minutes !

Tips to work with the custom_lints package:

  • Once you make a change to your lint, if it does not restart in the project where you visualize your lint run flutter pub get and wait for them to appear
  • prints are your best friends. They will show up in the custom_lints.log file (which appears at the same level as where your pubspec.yaml is). I would advise you to never delete this file while working on your project since it can take some time before it first appears.

Create your first custom lint in Flutter

While the custom_lints package allows you to get started, it only provides you with a blank canvas. If you try to do anything, you will soon find that you have to learn to use paint, brushes and more - all things which are not included in the package. Let’s see how to start walking this new world of great possibilities through an example:

>  Let’s create a lint which warns the user not to assign List but to use IList instead.

IList is as immutable equivalent to List from the fast_immutable_collections package. This package is awesome and will remove all the untraceable errors that classic Lists lets slip by.

Note: The best way to learn the concept which follow is practice. I would advise you setup custom_lint and follow along. Here is a repo from which to start (where custom_lint is already setup). Every problem is separated in digestible steps with first the explanation of what we are trying to do and then the solution. The best way to read this article is to try each step before reading the solution.

Implementation plan:

  • Create and place the lint
  • What is the AST tree and how to get every variable declaration?
  • Check for variable type
  • Lint creation and placement from variable Element
  • Implement lint quick fix

Create and place the lint

In this first part, we will try to place the lint under every variable to which a List is assigned. Here is an example:

What is the AST tree and how to get every variable declaration?

The first step to creating a lint is to parse the code. In each file, the code is represented by two main trees:

  • The Element tree
  • The AST tree

The Element tree represents things that are declared with a name (e.g. classes, variables, methods, …) while the AST tree represents the structure of the code. Here is a concrete example:

What is important to remembers is that the AST tree is more complete but harder to parse. When you can, stick with the Element tree but if you really need, switch to AST.

In our case, we will need to have the Expression in order to be able to wrap it with IList (see next chapter). Therefore we will choose the AST tree.

In order to parse those trees, the easiest way is to use Visitors, which are classes with built-in method to detect what they are visiting. In particular, the RecursiveAstVisitor (or RecursiveElementVisitor) are particularly useful. In our case, we want to override the visitVariableDeclaration method of RecursiveAstVisitor:

++pre>++code>class RecursiveVariableDeclarationVisitor extends RecursiveAstVisitor { RecursiveVariableDeclarationVisitor({ required this.onVisitVariableDeclaration, }); void Function(VariableDeclaration node) onVisitVariableDeclaration; @override void visitVariableDeclaration(VariableDeclaration node) { onVisitVariableDeclaration(node); super.visitVariableDeclaration(node); } } ++/code>++/pre>

You can then use this Visitor to parse the current file inside the custom_lints getLints method:

++pre>++code>class DontUseListLinter extends PluginBase { @override Stream getLints(ResolvedUnitResult unit) async* { final variableDeclarations = []; unit.unit.visitChildren( RecursiveVariableDeclarationVisitor( onVisitVariableDeclaration: variableDeclarations.add, ), ); ... } } ++/code>++/pre>

The getLints method is generative. If this does not sound familiar, you might want to check out the video on this topic from the Flutter team

So far so good, we have gathered all the variable declaration in the file!


Check for variable type

Next step is to filter only  the VariableDeclarations that are associated to list. To do so we need to:

  • Use VariableDeclaration.declaredElement.type to get a special representation of the type of the variable
  • Import the source_gen package and use the TypeChecker class
++pre>++code>import 'package:source_gen/source_gen.dart'; ... final typeChecker = TypeChecker.fromRuntime(List); final variableDeclarationsOfLists = variableDeclarations.where( (variableDeclaration) { final type = variableDeclaration.declaredElement?.type; return type != null && typeChecker.isExactlyType(type); }, ).toList(); ++/code>++/pre>

Create the lint and place it at the right location

Finally, we can start building our Lint. The Lint class is fairly easy to use, the only challenge is to find specify the location of our lint. What you need to remember is that once you have any Element, you will be able to use its nameOffset and nameLength:

++pre>++code>for (final variableDeclaration in variableDeclarationsOfLists) { final declaredElement = variableDeclaration.declaredElement; if (declaredElement == null) { return; } final startLintOffset = declaredElement.nameOffset; final lintLength = declaredElement.nameLength; yield Lint( code: 'use_immutable_lists', message: 'Don\'t work with Lists directly, use IList instead', severity: LintSeverity.error, location: unit.lintLocationFromOffset( startLintOffset, length: lintLength, ), ); } ++/code>++/pre>

Here you go, now if we start the project in which our lint is imported we should see is appear where we want:


Now that you have successfully communicated the developers what not to do, let’s show them what to do with quick fixes.

Implement lint quick fix

Quick fixes are suggestions integrated in the editor which allows you to fix a lint with one click:

To implement quick fixes, you have to use the getAnalysisErrorFixes parameter of the Lint we just created:

++pre>++code>import 'package:analyzer_plugin/protocol/protocol_generated.dart'; ... Lint( code: 'use_immutable_lists', ... getAnalysisErrorFixes: (lint) async* { yield AnalysisErrorFixes( lint.asAnalysisError(), fixes: [ PrioritizedSourceChange( priority, sourceChange, ), ], ); }, ); ++/code>++/pre>

The two things we have to customize are:

  • priority: A number ≥ 0 which states how much the lint is pertinent (the lower the most pertinent)
  • sourceChange which indicated which change to actually perform when the quick fix is clicked. You can easily build a sourceChange though a ChangeBuilder which has aChangeBuilder.sourceChange property.

For some reason, change is not exported from the analyzer plugin so add an import to be able to export it:

++pre>++code>import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; // [unit] is the [ResolvedUnitResult] given by custom_lints final changeBuilder = ChangeBuilder(session: unit.session); ++/code>++/pre>

To indicate the change you want to perform, use the changeBuilder.addDartFileEdit method (which is async: don’t forget to await it!). This method takes two parameters:

  • path: the path to the file you want to edit, which will nearly always be the current one, accessible with: unit.libraryElement.source.fullName
  • buildFileEdit: a method which gives you a fileEditBuilder which you can use to make the changes in the file
++pre>++code>await changeBuilder.addDartFileEdit( unit.libraryElement.source.fullName, // Path to the current file (fileEditBuilder) { // Register your file changes }, ); ++/code>++/pre>

fileEditBuilder is quite simple to use. The only challenge, as for the lint, it to know the position of what you want to replace. In our example we want to wrap the expression used to define our variable: This won’t be perfect and could be improved but will work well in most cases. The expression can be obtained from VariableDeclaration which we got in the first part:

++pre>++code>(fileEditBuilder) { final expression = variableDeclaration.initializer; if (expression != null) { final startOffset = expression.offset; final endOffset = startOffset + expression.length; fileEditBuilder // Add "IList(" at the start of the expression ..addSimpleInsertion(startOffset, 'IList(') // Add ")" at the end of the expression ..addSimpleInsertion(endOffset, ')'); } } ++/code>++/pre>

The last thing you might want to specify is a message letting the user know what the quick fix does:

++pre>++code>final expression = variableDeclaration.initializer; final sourceChange = changeBuilder.sourceChange; sourceChange.message = "Replace expression with IList($expression)"; ++/code>++/pre>

Go back to the project where you imported the lint and you should see and be able to apply the quick fix:

Tada! We have created our first lint!


What you need to remember

  • Parse a file with RecursiveAstVisitor or RecursiveElementVisitor
  • Use TypeChecker from the source_gen package to check types
  • Get the Element you need and use its nameOffset and nameLength to position your lint
  • VariableDeclaration is an AST node  which represents a variable (in VariableDeclaration.declaredElement) and its associated expression (in VariableDeclaration.initializer)
  • Use ChangeBuilder (by importing package:analyzer_plugin/utilities/change_builder/change_builder_core.dart) to modify the file content through quick fixes

Lint implementation repo


Développeur Mobile ?

Rejoins nos équipes