Hey! These docs are for version 2.10, which is no longer officially supported. Click here for the latest version, 2.18!

This guide assumes that you are running a linter that already exists outside of Pants as a stand-alone binary, such as running Shellcheck, Pylint, Checkstyle, or ESLint.

If you are instead writing your own linting logic inline, you can skip Step 1. In Step 3, you will not need to use `Process`. You may find Pants's [`regex-lint` implementation](🔗) helpful for how to integrate custom linting logic into Pants.

## 1. Install your linter

There are several ways for Pants to install your linter. See [Installing tools](🔗). This example will use `ExternalTool` because there is already a pre-compiled binary for Shellcheck.

You will also likely want to register some options, like `--config`, `--skip`, and `--args`. Options are registered through a [`Subsystem`](🔗). If you are using `ExternalTool`, this is already a subclass of `Subsystem`. Otherwise, create a subclass of `Subsystem`. Then, set the class property `options_scope` to the name of the tool, e.g. `"shellcheck"` or `"eslint"`. Finally, add options via the class method `register_options()`.

Lastly, register your Subsystem with the engine:

## 2. Set up a `FieldSet` and `LintRequest`

As described in [Rules and the Target API](🔗), a `FieldSet` is a way to tell Pants which `Field`s you care about targets having for your plugin to work.

Usually, you should add a subclass of the `Sources` field to the class property `required_fields`, such as `BashSources` or `PythonSources`. This means that your linter will run on any target with that sources field or a subclass of it.

Create a new dataclass that subclasses `FieldSet`:

Then, hook this up to a new subclass of `LintTargetsRequest`:

Finally, register your new `LintTargetsRequest ` with a [`UnionRule`](🔗) so that Pants knows your linter exists:

## 3. Create a rule for your linter logic

Your rule should take as a parameter the `LintRequest` from step 2 and the `Subsystem` (or `ExternalTool`) from step 1. It should return `LintResults`:

The `LintTargetsRequest ` has a property called `.field_sets`, which stores a collection of the `FieldSet`s defined in step 2. Each `FieldSet` corresponds to a single target. Pants will have already validated that there is at least one valid `FieldSet`, so you can expect `LintRequest.field_sets` to have 1-n `FieldSet` instances.

The rule should return `LintResults`, which is a collection of multiple `LintResult` objects. Normally, you will only have one single `LintResult`. Sometimes, however, you may want to group your targets in a certain way and return a `LintResult` for each group, such as grouping Python targets by their interpreter compatibility.

If you have a `--skip` option, you should check if it was used at the beginning of your rule and, if so, to early return an empty `LintResults()`.

If you used `ExternalTool` in step 1, you will use `Get(DownloadedExternalTool, ExternalToolRequest)` to install the tool.

Typically, you will use `Get(SourceFiles, SourceFilesRequest)` to get all the sources you want to run your linter on.

If you have a `--config` option, you should use `Get(Digest, PathGlobs)` to find the config file and include it in the `input_digest`.

Use `Get(Digest, MergeDigests)` to combine the different inputs together, such as merging the source files, config file, and downloaded tool.

Usually, you will use `Get(FallibleProcessResult, Process)` to run a subprocess (see [Processes](🔗)). We use `Fallible` because Pants should not throw an exception if the linter returns a non-zero exit code. Then, you can use `LintResult.from_fallible_process_result()` to convert this into a `LintResult`.

Finally, update your plugin's `register.py` to activate this file's rules.

Now, when you run `./pants lint ::`, your new linter should run.

## 4. Add tests (optional)

Refer to [Testing rules](🔗). TODO