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 print
s are your best friends. They will show up in thecustom_lints.log
file (which appears at the same level as where yourpubspec.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 toList
from the fast_immutable_collections package. This package is awesome and will remove all the untraceable errors that classicList
s 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 Visitor
s, 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
:
You can then use this Visitor
to parse the current file inside the custom_lints getLints
method:
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 VariableDeclaration
s 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
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
:
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:
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 asourceChange
though aChangeBuilder
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:
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 afileEditBuilder
which you can use to make the changes in the file
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:
The last thing you might want to specify is a message letting the user know what the quick fix does:
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
orRecursiveElementVisitor
- Use
TypeChecker
from the source_gen package to check types - Get the
Element
you need and use itsnameOffset
andnameLength
to position your lint VariableDeclaration
is anAST node
which represents a variable (inVariableDeclaration.declaredElement
) and its associated expression (inVariableDeclaration.initializer
)- Use
ChangeBuilder
(by importingpackage:analyzer_plugin/utilities/change_builder/change_builder_core.dart
) to modify the file content through quick fixes
Lint implementation repo
https://github.com/lulupointu/lint_no_list_tutorial_solution