In Pants, every formatter is (typically) also a linter, meaning that if you can run a tool with `./pants fmt
`, you can run the same tool in check-only mode with `./pants lint
`. Start by skimming [Add a linter](🔗) to familiarize yourself with how linters work.
Example repository
This guide walks through each step of adding shfmt. See [here](🔗) for the final result.
This guide assumes that you are running a formatter that already exists outside of Pants as a stand-alone binary, such as running Black or Prettier.
If you are instead writing your own formatting logic inline, you can skip Step 1. In Step 4, you will not need to use `Process
`.
## 1. Install your formatter
There are several ways for Pants to install your formatter. See [Installing tools](🔗). This example will use `ExternalTool
` because there is already a pre-compiled binary for shfmt.
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. `"shfmt"
` or `"prettier"
`. Finally, add options via the class method `register_options()
`.
## 2. Set up `LanguageFmtTargets
` and `FmtRequest
`
Unlike linters and test runners, formatters cannot safely run in parallel because they might overwrite each other. Instead, Pants runs formatters sequentially.
However, it is safe to run formatters belonging to different languages in parallel, such as a Java formatter at the same time as a Python formatter. So, Pants groups formatters by languages via the type `LanguageFmtTargets
`. Within each language, the formatters will run sequentially, but across languages, they may run in parallel.
New Python formatters can skip this step
Pants already defines `
PythonFmtTargets
` and `PythonFmtRequest
`, which are both defined in `pants.backend.python.lint.python_fmt
`.
To add support for a new language, copy the below code into a new file like `shell_formatters.py
`, but change the names of the `LanguageFmtTargets
`, `LanguageFmtRequest
`, and the rule to match your new language. Update the `LanguageFmtTargets.required_fields
` class property, and update the body of the rule to use your language.
Note that we use `await Get(FmtResult, ShellFmtRequest)
` in a `for
` loop. Normally, you should use `await MultiGet()
` instead of a `for
` loop, but we must use a `for
` loop here to run sequentially.
## 3. Set up a `FieldSet
` and `FmtRequest
`/`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 `SourcesField
` to the class property `required_fields
`, such as `ShellSourceField
` or `PythonSourceField
`. 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 both `LintRequest
` _and_ the `FmtRequest
` you set up in step 2 for your language, such as `ShellFmtRequest
` or `PythonFmtRequest
`:
Finally, register your new `LintRequest
`/`FmtRequest
` with two [`UnionRule
`s](🔗) so that Pants knows your formatter exists:
## 4. Create `fmt
` and `lint
` rules
You will need rules for both `fmt
` and `lint
`. Both rules should take the `LintRequest
`/`FmtRequest
` from step 3 (e.g. `ShfmtRequest
`) as a parameter. The `fmt
` rule should return `FmtResult
`, and the `lint
` rule should return `LintResults
`.
The `fmt
` and `lint
` rules will be very similar, except that a) the `argv
` to your `Process
` will be different, b) for `lint
`, you should use `await Get(FallibleProcessResult, Process)
` so that you tolerate failures, whereas `fmt
` should use `await Get(ProcessResult, Process)
`. To avoid duplication between the `fmt
` and `lint
` rules, you should set up a helper `setup
` rule, along with dataclasses for `SetupRequest
` and `Setup
`.
The `FmtRequest
`/`LintRequest
` 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 `ShfmtRequest.field_sets
` to have 1-n `FieldSet
` instances.
If you have a `--skip
` option, you should check if it was used at the beginning of your `fmt
` and `lint
` rules and, if so, to early return an empty `LintResults()
` and return `FmtResult.skip()
`.
Use `Get(SourceFiles, SourceFilesRequest)
` to get all the sources you want to run your linter on. However, you should check if the `FmtRequest.prior_formatter_result
` is set, and if so, use that value instead. This ensures that the result of any previous formatters is used, rather than the original source files.
If you used `ExternalTool
` in step 1, you will use `Get(DownloadedExternalTool, ExternalToolRequest)
` in the `setup
` rule to install the tool.
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.
Finally, update your plugin's `register.py
` to activate this file's rules. Note that we must register the rules added in Step 2, as well.
Now, when you run `./pants fmt ::
` or `./pants lint ::
`, your new formatter should run.
## 5. Add tests (optional)
Refer to [Testing rules](🔗).