Lint rules are a powerful way to improve the maintainability of a project. The more, the merrier! But while Dart offers a wide variety of lint rules by default, it cannot reasonably include every possible lint. For example, Dart does not include lints related to third-party packages or lints that are specific to your project and team.
I am pleased to announce the Custom Lint package, a powerful tool for building custom lint rules to allow package authors as well as Flutter and Dart developers to go beyond.
Motivation
If you have already tried to define custom lints, perhaps, you have seen analyzer_plugin. Dealing with this plugin is not as pleasant as you wish and comes with some constraints.
The custom_lint package is similar, however, it goes deeper and strives to provide a much better developer experience.
The package provides a lot of features, including but not limited to:
- A command line to obtain the list of lints in your CI without having to write a command line yourself.
- A simplified project setup. No need to deal with the
analyzer
server or error handling. custom_lint takes care of that for you, so that you can focus on writing lints. - Support for hot-restart.Updating the source code of a linter plugin will dynamically restart it, without having to restart your IDE/analyzer server.
- Built-in support for
// ignore:
and// ignore_for_file:
. - Support for
print(...)
and exceptions. If your plugin somehow throws or prints debug messages, custom_lint will generate a log file with the messages/errors.
Anatomy
You can open the custom_lint repository on Github and you’ll find the source code. Essentially, the repository comes with two major packages, custom_lint, and custom_lint_builder. Generally, the custom_lint package is being used in the application that you are going to leverage the defined custom lints and the custom_lint_builder is to be used in the package where you will define your own custom lint rules.
To understand better, let’s see how you can use them.
Usage
Generally, when you use custom_lint you need to define your task into two parts:
- how to define a custom_lint package
- how users can install our package in their application to see our newly defined lints
Let’s dive into it.
Creating a custom lint package
The first step towards creating custom lint rules is to create a package, to do that you need to follow two simple steps:
- Updating your
pubspec.yaml
to includecustom_lint_builder
as a dependency:
# pubspec.yaml
name: my_custom_lint_package
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
# we will use an analyzer for inspecting Dart files
analyzer:
# custom_lint_builder will give us tools for writing lints
custom_lint_builder:
- Create a
bin/custom_lint.dart
file in your project with the following:
void main(List<String> args, SendPort sendPort) {
startPlugin(sendPort, _ExampleLinter());
}
class _ExampleLinter extends PluginBase {
@override
Stream<Lint> getLints(ResolvedUnitResult resolvedUnitResult) async* {
yield Lint(
code: 'my_custom_lint_code',
message: 'This is the description of our custom lint',
location: resolvedUnitResult.lintLocationFromOffset(0, length: 10),
);
}
}
Let’s analyze the code in step 2 and understand what it does.
The entry point of your custom linter starts with main
function where it will have two parameters, List<String> args
and SendPort sendPort
You will need to call startPlugin
function by passing sendPort
and the custom Linter class you will define.
void main(List<String> args, SendPort sendPort) {
startPlugin(sendPort, _ExampleLinter());
}
Then, you need to create a custom class that is extending PluginBase
and write your own Lint
This is the class that will analyze Dart files and return lints.
class _ExampleLinter extends PluginBase {
@override
Stream<Lint> getLints(ResolvedUnitResult resolvedUnitResult) async* {
// A basic lint that shows at the top of the file.
yield Lint(
code: 'my_custom_lint_code',
message: 'This is the description of our custom lint',
location: resolvedUnitResult.lintLocationFromOffset(
0,
length: 10,
),
);
}
}
Let’s take a look at Lint
class. There are three mandatory parameters to fill in, code
, message
, and location
where your lint will appear within the Dart file. The example above will make it appear at the top of the file (offset 0), and be 10 characters long.
Let me give you a real-world example.
First folder structure
|__ my_awesome_lints
|_____ bin
|________ custom_lint.dart
|_____ analysis_options.yaml
|_____ pubspec.yaml
import 'dart:isolate';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
bool _isProvider(DartType type) {
final element = type.element! as ClassElement;
final source = element.librarySource.uri;
final isProviderBase = source.scheme == 'package' &&
source.pathSegments.first == 'riverpod' &&
element.name == 'ProviderBase';
return isProviderBase || element.allSupertypes.any(_isProvider);
}
void main(List<String> args, SendPort sendPort) {
startPlugin(sendPort, _RiverpodLint());
}
class _RiverpodLint extends PluginBase {
@override
Stream<Lint> getLints(ResolvedUnitResult resolvedUnitResult) async* {
final library = resolvedUnitResult.libraryElement;
print('This is a print');
final providers = library.topLevelElements
.whereType<VariableElement>()
.where((e) => !e.isFinal)
.where((e) => _isProvider(e.type))
.toList();
for (final provider in providers) {
if (provider.name == 'fail') throw StateError('Nani?');
yield Lint(
code: 'riverpod_final_provider',
message: 'Providers should always be declared as final',
location: provider.nameLintLocation!,
);
}
}
}
As you can see in the example above, I have now defined the riverpod_final_provider
rule to ensure that providers are defined with the final
keyword.
Let’s use it in an application now, the second step that I mentioned above.
Using our custom lint package in an application
Now that you have defined your package, you can use it in your application with only two steps:
- The application must contain an
analysis_options.yaml
with the following:
analyzer:
plugins:
- custom_lint
- The application also needs to add
custom_lint
and our package(s) as a dev dependency in their application:
# The pubspec.yaml of an application using your lints
name: example_app
environment:
sdk: ">=2.16.0 <3.0.0"
dev_dependencies:
custom_lint:
my_awesome_lints:
That’s all! After running pub get
(and possibly restarting their IDE), users should now see our custom lints in their Dart files:
Obtaining the list of lints in the CI
Unfortunately, running dart analyze
does not pick up our newly defined lints. We need a separate command for this.
To do that, users of our custom_lint package can run inside the application the following:
$ dart run custom_lint
lib/main.dart:0:0 • This is the description of your custom lint • my_awesome_lints
Debugging
As a developer, you spend so much time on debugging. The custom_lint comes with a neat feature that lets you easily click on the logs and jump into the custom lints you have defined. This could save time and make it easy to find out what and where things go wrong.
The custom_lint package also generates a custom_lint.log
file where you can find all the outputs, including what you have printed or any errors.
Advanced Use case
If you would like to learn more and see what you can do with the custom_lint package, you can watch the video below where I have implemented two custom lint rules along with corrections and quick fixes.
Conclusion
Adding lints, especially custom ones that perfectly match package requirements or further a project and teams needs, will boost code consistency across your projects and helps to improve code quality.
The Dart custom_lint package is here to help you define custom rules, enhance the developer experience, and provide a pleasant journey of defining such rules.
As always, Invertase provides tooling to make developers productive and aims to open source it in the hopes that others can leverage it to their benefit.
Do not hesitate to send us feedback by opening an issue on the repository or sharing it via social media.
Stay tuned for more examples and tutorials about custom lints and exciting news that we will share in the future about other tooling that we are working on. Follow us on Twitter, Linkedin, and Youtube, and subscribe to our monthly newsletter to always stay up-to-date.