Ask or search…
K

Custom Linters

Custom linter configuration overview
Trunk Check's linter integrations are fully configurable. This means that you can easily tune existing linters or leverage our caching and hold-the-line solution with your own custom linters. Let's walk through the steps of setting up your own linter. For a full walkthrough, see our blog.

Execution Model

Running trunk check tells trunk to do the following:
  • compute the set of modified files (by comparing the current working tree and upstream-ref, usually your main or master branch)
  • compute the set of lint actions to run based on the modified files
    • each enabled linter is invoked once per applicable modified file; for example, if pylint and flake8 are enabled, they will both be run on every modified python file but not on any modified markdown files
    • every lint action also will have a corresponding upstream lint action (i.e. the linter will also be run on the upstream version of the file, so that we can determine which issues already exist in your repository)
  • download and install any newly enabled linters/formatters
  • execute uncached lint actions
  • parse linter outputs into configurable output types
  • determine which lint issues are new, existing, or fixed
Trunk currently supports the following types of additional/proprietary linters:
Linter Type
Autofix support
Description
sarif
Produces diagnostics as Static Analysis Results Interchange Format JSON.
lsp_json
Produces diagnostics as Language Server Protocol JSON.
pass_fail
Writes a single file-level diagnostic to stdout.
regex
Produces diagnostics using a custom regex format.
arcanist
Produces diagnostics as Arcanist JSON.
rewrite
Writes the formatted version of a file to stdout.
If your linter produces a different output type, you can also write a parser to transform the linter's output into something Trunk can understand.
To set up a custom linter, add it to trunk.yaml under lint.definitions and enable it:
lint:
definitions:
- name: foo
files: [ALL]
commands:
- name: lint
output: sarif
success_codes: [0, 1]
run: ${workspace}/bin/foo --file ${target}
read_output_from: stdout
enabled:
# ...
- foo@SYSTEM
The @SYSTEM is a special identifier that indicates that we will forward the PATH environment variable to the custom linter when we invoke it.
Every custom linter must specify a name, the types of files it will run on, at least one command, and success_codes or error_codes.
Info: Entries in enabled must specify both a linter name and a version. If you commit your linter into your repository, you should simply use @SYSTEM, which will run the linter with your shell's PATH. If you have a versioned release pipeline for your linter, though, you'll want to define your custom linter using a download and specify the download version to use.

Configuration Options

When defining a custom linter, Trunk not only needs to know the output of the linter command but also details such as how to invoke it, how to specify the file to check, and more; the next few sections explain what options you use to configure these and how:
For even more details, you can refer to the JSON schema for trunk.yaml.

Applicable filetypes

To determine which linters to run on which files (i.e. compute the set of lint actions), Trunk requires that every linter define the set of filetypes it applies to in files.
We have a number of pre-defined filetypes (e.g. c++-header, gemspec, rust; see our plugins repo for an up-to-date list), but you can also define your own filetypes. Here's how we define the python filetype:
lint:
files:
- name: python
extensions:
- py
- py2
- py3
shebangs:
- python
- python3
This tells Trunk that files matching either of the following criteria should be considered python files:
  • the extension is any of .py, .py2, or .py3 (e.g. lib.py)
  • the shebang is any of python or python3 (e.g. #!/usr/bin/env python3)

Command

Once Trunk has figured out which linters it will run on which files, Trunk expands the template provided in the run field to determine the arguments it will invoke the linter with. Here's what that looks like for detekt, one of our Kotlin linters:
lint:
definitions:
- name: detekt
# ...
commands:
- output: sarif
run:
detekt-cli --build-upon-default-config --config .detekt.yaml --input ${target} --report
sarif:${tmpfile}
This command template contains all the information Trunk needs to execute detekt in a way where Trunk will be able to understand detekt's output.
Note that some of the fields in this command template contain ${} tokens: these tokens are why command is a template and are replaced at execution time with the value of that variable within the context of the lint action being executed.
Variable
Description
${workspace}
Path to the root of the repository
${target}
Path to the file to check, relative to ${workspace}
${linter}
Path to the directory the linter was downloaded to
${runtime}
Path to the directory the runtime (e.g. node) was downloaded to
${upstream-ref}
Upstream git commit that is being used to calculate new/existing/fixed issues
${plugin}
Path to the root of the plugin's repository

Input

The target field specifies what paths this linter will run on given an input file. It may be a literal such a . which will run the linter on the whole repository. It also supports various substitutions:
Variable
Description
${file}
The input file.
${parent}
The folder containing the file.
${parent_with(<name>)}
Walks up toward the repository root looking for the first folder containing <name>. If <name> is not found, do not run any linter.
${root_or_parent_with(<name>)}
Walks up toward the repository root looking for the first folder containing <name>. If <name> is not found, evaluate to the repository root.
If target is not specified it will default to ${file}.
This target may be referenced in the run field as ${target}.
lint:
definitions:
- name: noop
files: [ALL]
commands:
- name: format
output: rewrite
formatter: true
run: cat ${target}
or via stdin, by specifying stdin: true:
lint:
definitions:
- name: noop
files: [ALL]
commands:
- name: format
output: rewrite
formatter: true
run: cat -
stdin: true
Note: Linters that take their input via stdin may still want to know the file's path so that they can, say, generate diagnostics with the file's path. In these cases you can still use ${target} in run.

Output

The output format that Trunk expects from a linter is determined by its output type.
stdout, stderr or tmp_file
trunk generally expects a linter to output its findings to stdout, but does support other output mechanisms:
read_output_from
Description
stdout
Standard output.
stderr
Standard error.
tmp_file
If ${tmpfile} was specified in command, the path of the created ${tmpfile}.

Exit codes

Linters often use different exit codes to categorize the outcome. For instance, markdownlint uses 0 to indicate that no issues were found, 1 to indicate that the tool ran successfully but issues were found, and 2, 3, and 4 for tool execution failures.
Trunk supports specifying either success_codes or error_codes for a linter:
  • if success_codes are specified, Trunk expects a successful linter invocation (which may or may not find issues) to return one of the specified success_codes;
  • if error_codes are specified, Trunk expects a successful linter invocation to return any exit code which is not one of the specified error_codes.
markdownlint, for example, has success_codes: [0, 1] in its configuration.

Working directory

run_from determines what directory a linter command is run from.
run_from
Description
<path> (. by default)
Explicit path to run from
${parent}
Parent of the target file; e.g. would be foo/bar for foo/bar/hello.txt
${root_or_parent_with(<file>)}
Nearest parent directory containing the specified file
${root_or_parent_with_dir(<dir>)}
Nearest parent directory containing the specified directory
${root_or_parent_with_regex(<regex>)}
Nearest parent directory containing a file or directory matching specified regex
${root_or_parent_with_direct_config}
Nearest parent directory containing a file from direct_configs
${root_or_parent_with_any_config}
Nearest parent directory containing a file from affects_cache or direct_configs
${target_directory}
Run the linter from the same directory as the target file, and change the target to be .
${compile_command}
Run from the directory where compile_commands.json is located

Limiting concurrency

If you would like to limit the number of times trunk will invoke a linter concurrently, then you can use the maximum_concurrency option. For example, setting maximum_concurrency: 1 will limit Trunk from running more than one instance of the linter simultaneously.

Environment variables

Trunk by default runs linters without environment variables from the parent shell; however, most linters need at least some such variables to be set, so Trunk allows specifying them using environment; for example, the environment for ktlint looks like this:
lint:
definitions:
name: ktlint
# ...
environment:
- name: PATH
list: ["${linter}"]
- name: LANG
value: en_US.UTF-8
Most environment entries are maps with name and value keys; these become name=value environment variables. For PATH, we allow specifying list, in which case we concatenate the entries with :.
We use the same template syntax for environment as we do for command.

Hermetic Installs

You can use the tools section to specify trunk-configured binaries that the linter uses to run. The tools key should specify a list of tool names. We have two kinds of tool dependencies - they are described in turn below. See the Tools Configuration page for more details on how to set up your tools.
Using tools is the preferred way of defining and versioning a linter, as it also allows repo users to conveniently run the linter binary outside of the trunk check context.

Eponymous Tool Dependencies

Here is an example of where the tool matches the linter name:
tools:
definitions:
- name: pylint
runtime: python
package: pylint
shims: [pylint]
known_good_version: 2.11.1
lint:
definitions:
- name: pylint
files: [python]
commands:
- name: lint
# Custom parser type defined in the trunk cli to handle pylint's JSON output.
output: pylint
run: pylint --exit-zero --output ${tmpfile} --output-format json ${target}
success_codes: [0]
read_output_from: tmp_file
batch: true
cache_results: true
tools: [pylint]
suggest_if: config_present
direct_configs:
- pylintrc
- .pylintrc
affects_cache:
- pyproject.toml
- setup.cfg
issue_url_format: http://pylint-messages.wikidot.com/messages:{}
known_good_version: 2.11.1
version_command:
parse_regex: pylint ${semver}
run: pylint --version
In this case, the tool name (pylint) matches that of the linter, making it an eponymous tool. Eponymous tools need to be defined separately from the linter but implicitly enabled with the linter's version. You may explicitly enable the eponymous tool if you wish, but note that its version needs to be synced to that of the linter.

Additional Tool Dependencies

You can also have a scenario where a linter depends on a tool that is not identically named - an additional tool dependency. We give an example below:
tools:
definitions:
- name: terragrunt
known_good_version: 0.45.8
download: terragrunt
shims:
- name: terragrunt
target: terragrunt
lint:
definitions:
- name: terragrunt
tools: [terragrunt, terraform]
known_good_version: 0.45.8
files: [hcl]
suggest_if: never
environment:
- name: PATH
list: ["${linter}"]
commands:
- name: format
output: rewrite
run: terragrunt hclfmt ${target}
success_codes: [0]
sandbox_type: copy_targets
in_place: true
formatter: true
batch: true
version_command:
parse_regex: terragrunt v${semver}
run: terragrunt -version
In this scenario, terraform is an additional tool dependency - terragrunt requires it to be in $PATH. If the tool is an additional dependency, it must be enabled explicitly and versioned independently of the linter - that is, it must be listed in the tools.enabled section.

Downloads

(NOTE: This method of specifying linters is still supported, but using tools like specified above is recommended going forward. Tools support referencing downloads from the top-level downloads section)
If your custom linter has a separate release process (i.e. is not committed in your repo), then you can tell Trunk how to download it like so:
lint:
downloads:
- name: lorem-linter
# the default version to download; overridden by the version in `enabled`
version: 4.0.1
executable: true
downloads:
- os: linux
cpu: x86_64
url: https://github.com/my-org/my-repo/releases/download/${version}/lorem-darwin-x86-64
- os: macos
cpu: x86_64
url: https://github.com/my-org/my-repo/releases/download/${version}/lorem-linux-x86-64
- name: ipsum-linter
# the default version to download; overridden by the version in `enabled`
version: 0.1.1
downloads:
- os: linux
cpu: x86_64
url: https://github.com/my-org/my-repo/releases/download/${version}/ipsum-darwin-x86-64.tar.gz
strip_components: 2
- os: macos
cpu: x86_64
url: https://github.com/my-org/my-repo/releases/download/${version}/ipsum-linux-x86-64.tar.gz
strip_components: 2
definitions:
- name: lorem-linter
files: [javascript, typescript]
download: lorem-linter
...
- name: ipsum-linter
files: [rust]
download: ipsum-linter
...
enabled:
This tells Trunk that, for lorem-linter:
  • you want to run version 4.0.2 on javascript and typescript files,
  • it is available for macOS and Linux at the specified URLs (expanded by replacing ${version} with 4.0.2), and
  • the download consists of a single executable binary, since executable: true is set;
for ipsum-linter:
  • you want to run version 0.1.6 on rust files,
  • it is available for macOS and Linux at the specified URLs (expanded by replacing ${version} with 0.1.6), and
  • the download is a compressed archive, the linter binary is strip_components: 2 directories deep inside the uncompressed archive, and trunk should automatically extract and unpack the linter from the archive.

Download via package manager

If your linter can be downloaded via gem install, go get, npm install, or pip install, you can specify a runtime and the package key:
lint:
definitions:
- name: fizz-buzz
files: [javascript]
# npm install fizz-buzz
runtime: node
package: fizz-buzz
This will now create a hermetic directory in ~/.cache/trunk/linters/fizz-buzz and npm install fizz-buzz there. You can refer to different versions of your package in trunk.yaml as normal, via [email protected].
Note: Such downloads will use the hermetic version of the specified runtime that trunk installs, not the one >you've installed on your machine.

run_when

Lint sessions are always associated with exactly one of the following session types:
  • ci: triggered by a CI run, i.e. trunk check --ci or when running inside the GitHub action,
  • cli: triggered by a human or script running trunk check,
  • lsp: triggered by VSCode, or
  • monitor: triggered by background linting.
You can use run_when to specify which session types you want to run a linter in; for example, to always disable a linter during CI:
lint:
definitions:
- name: fizz-buzz
run_when: [cli, lsp, monitor]
Last modified 10d ago