Announcing Linting for Bazel

Announcing Linting for Bazel

Aspect's mission is to make developers productive in large-scale polyglot repositories. We largely rely on the Bazel build system to power that productivity gain. But what happens when Bazel has a major missing feature that all developers need? There is a bazel coverage command for collecting Test Coverage, but no bazel lint command for running code analysis tools. Why?

At Google, we built a separate system for this called Tricorder which integrates with the code review tool, Critique. You can read an academic paper about it:

However, most engineers and team leads aren't looking for research; they need solutions which are ready to deploy. When Bazel was open-sourced, this ecosystem for running linters wasn't included. Aspect has put these parts back together and we're very excited to share what we've built.

The solution is in several layers, so I'll explore them one at a time.

rules_lint

rules_lint is the lowest layer in Aspect's linting support is a "ruleset" which you can think of as a plugin for Bazel. It's open-sourced under an Apache 2.0 license, because we want everyone in the Bazel ecosystem to be able to use and contribute to it!

First we split out formatting as a special case. Formatters run on individual files, and the modifications they make are always safe to apply. In fact, engineers shouldn't even need to think about formatting: because they're guaranteed to be fast and only run on modified files, they can be in a pre-commit version control hook so they're run automatically by git commit. rules_lint includes an aggregator rule, format_multirun that gives you a simple runnable command that formats any files you pass to it, regardless of language.

Remaining linters may need to operate on a whole program, and their suggested fixes may require human review. For these we use Bazel's aspect feature, which is like Aspect-oriented programming: you can apply some logic across an existing object model. In Bazel's case this means running some tools over the existing dependency graph. That's perfect for linting, since you already declared your "library targets" to Bazel. We just need to visit them.

The 1.0 release of rules_lint includes a TON of tool integrations already, and more are added all the time, thanks to our design that requires a minimal layer of Bazel idioms on top of the tools themselves:

The rules_lint layer concludes with a bare-bones developer experience for library code in all languages:

  1. Run bazel build --config=lint //... to produce lint reports.
    (See the build:lint lines in this .bazelrc for the flags this config option expands to.)

  2. Print the resulting reports, for example a simplistic one-liner using find looks like: find $(bazel info bazel-bin) -name "*AspectRulesLint*report" -exec cat {} \;

A slightly better way to view just the newly-produced reports is to make a script like https://github.com/aspect-build/rules_lint/blob/main/example/lint.sh. However we can do much better, so keep reading.

aspect lint

Aspect CLI is a replacement for the Bazel command-line interface. It has better usability, and a plugin model. It is available with an Apache-2 license, and the behavior matches bazel so it's safe to simply drop into your .bazeliskrc file to switch over for your whole team. In fact, our Homebrew formula installs the Aspect CLI as bazel on your path, just like the officially recommended Bazelisk package which we modeled after.

It also lets us add the lint command that Bazel is missing!

aspect lint picks up where we left off in rules_lint. It can read the report files similarly to the lint.sh wrapper I linked to - but it can also read the suggested fixes produced by linter tools. When lint is running in an interactive terminal, it will prompt the user to accept the proposed patches from the linter tool (they can be previewed before apply if desired.) Alternatively like most linter tools, you can run lint --fix to request the fixes be applied.

asciicast

Try it

The easiest way to try rules_lint and the lint command is to run aspect init to get a blank Bazel repository with formatting and linting already configured for the language(s) you code in.

Lint in Code Review

The two layers described so far give developers a local experience for linting their code and applying fixes. However most of us don't remember to run lint on our changes before we send them for review. In fact, it's often a waste of time, because there's no reason to polish code that's still a work-in-progress: we often refactor several times before getting to a shape that's ready to request feedback.

The typical options for integrating linting into the code review process are terrible:

  1. Print warnings to the terminal during the build and test step on CI. The developer ignores them, especially when existing warnings are mixed with new ones. The code reviewer is unlikely to click into the CI logs to discover that warnings were printed. New warnings continue to be added to the codebase. I've been writing about this since 2016.

  2. Promote all lint warnings to errors. Developers are now forced to fix any linting violation, even if it is trivial, regardless of whether their code reviewer agrees. They quickly learn how to suppress the error by adding //ignore lines in their code. When you want to enable a new linter check, you're also forced to //ignore the existing violations in the codebase, or take the risk of editing code you don't understand. Linting is now equivalent to tests, which block merge when red.

What if I told you there's a third way: the way we did it at Google. The linter should actually be re-thought, not as warnings, not as tests, but as code review comments.

Of course, you might still promote certain lint rules to errors, such as data tainting rules for security, and our tooling respects that and blocks the developer. Our goal is to support both warnings AND errors well.

Marvin

Marvin is adorable, and he's your Bazel buddy. For linting, he acts like a code review bot. He uses rules_lint but requests the machine-readable outputs. Then he shows up in your code review and presents the lint warnings on the lines you've changed, and suggests the fixes offered by the linter tool. Deciding how to act (or maybe not act) on the warnings is now a human task for the author and reviewer to perform, along with other things that come up during a review.

This works for any language rules_lint supports! Follow the links to the live PR on our examples repo where I made the code change.

Java, using PMD:

TypeScript, using ESLint:
This demonstrates what happens if the user configures this rule as an error in the eslint config.

Python, using Ruff:

C++, using Clang-Tidy:

Try it

The screenshots above are from our "kitchen sink" monorepo, github.com/aspect-build/bazel-examples. Try editing some code to produce linter warnings. You can interact with Marvin by sending a PR to the repository, and watch the automation run!

Sign up today!

We offer a free trial of our Workflows solution for Bazel CI/CD which includes Marvin in the latest release.