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:

  1. Updating your pubspec.yaml to include custom_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:
  1. 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:

  1. The application must contain an analysis_options.yaml with the following:
analyzer:
  plugins:
    - custom_lint
  1. 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:

screenshot of our custom lints in the IDE

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.

Stay Up To Date!

Newsleter Form

Stay updated with our latest news, blogs and software updates, subscribe to our newsletter.

By subscribing, you agree with Revue's Terms of Service and Privacy Policy.