Create a BUILD file that declares the formatter binary, typically at tools/format/BUILD.bazel
This file contains a format_multirun rule. To use the tools supplied by default in rules_lint,
just make a simple call to it like so:
load("@aspect_rules_lint//format:defs.bzl", "format_multirun")
format_multirun(name = "format")For more details, see the format_multirun API documentation and
the example/tools/format/BUILD.bazel file.
Finally, make it easy for developers to run on their changed files:
- With Aspect CLI, add the
formattask toMODULE.aspectby runningaspect axl add gh:aspect-build/rules_lint, then developers runaspect format - Add an alias in the root BUILD file, so that developers can type
bazel run format(assuming their working directory is the repository root):
alias(
name = "format",
actual = "//tools/format",
)Each formatter should be installed by Bazel. A formatter is just an executable target.
rules_lint provides some default tools at specific versions using
rules_multitool.
You may fetch alternate tools or versions instead.
To register the tools you fetch, supply them as values for that language attribute.
For example:
load("@aspect_rules_lint//format:defs.bzl", "format_multirun")
format_multirun(
name = "format",
python = ":ruff",
)File discovery for each language is based on file extension and shebang-based discovery is currently limited to shell.
Since the format target is a bazel run command, it already runs in the working directory alongside the sources.
Therefore the configuration instructions for the formatting tool should work as-is.
Whatever configuration files the formatter normally discovers will be used under Bazel as well.
As an example, if you want to change the indent level for Shell formatting, you can follow the
instructions for shfmt and create a .editorconfig file:
[[shell]]
indent_style = space
indent_size = 4
You can override the default command-line arguments passed to formatters by specifying custom arguments for each language and mode:
load("@aspect_rules_lint//format:defs.bzl", "format_multirun")
format_multirun(
name = "format",
kotlin = ":ktfmt",
kotlin_fix_args = ["--google-style"],
kotlin_check_args = ["--google-style", "--set-exit-if-changed", "--dry-run"],
java = ":java-format",
java_fix_args = ["--aosp", "--replace"],
python = ":ruff",
python_check_args = ["format", "--check", "--diff"],
)The custom argument attributes follow the pattern {language}_{mode}_args:
{language}_fix_args: Arguments used when runningbazel run //:format(fix mode){language}_check_args: Arguments used for bothbazel run //:format.check(check mode) andformat_test(test mode)
When custom arguments are specified, they completely replace the default arguments for that mode. If not specified, the built-in defaults for each formatter are used.
Assuming you installed with the typical layout:
bazel run //:format
Note that mass-reformatting can be disruptive in an active repo. You may want to instruct developers with in-flight changes to reformat their branches as well, to avoid merge conflicts. Also consider adding your re-format commit to the
.git-blame-ignore-revsfile to avoid polluting the blame layer.
bazel run //:format some/file.md other/file.json
Commonly, the underlying formatters that rules_lint invokes provide their own methods of excluding files (.prettierignore for example).
At times when that is not the case, rules_lint provides a means to exclude files from being formatted by using attributes specified via .gitattributes files.
If any of following attributes are set or have a value of true on a file it will be excluded:
gitlab-generated=truelinguist-generated=truerules-lint-ignored=true
Note that the first two attributes also have the side effect of preventing the generated files from being shown to code reviewers, and from being included in language stats, for GitLab and GitHub respectively. See GitHub docs.
Developers could choose to install pre-commit.com (note that it has a Python system dependency).
In this case you can add this in your .pre-commit-config.yaml:
- repo: local
hooks:
- id: aspect_rules_lint
name: Format
language: system
entry: bazel run //:format
files: .*Note that pre-commit is silent while Bazel is fetching the tools, which can make it appear hung on the first run. There is no way to avoid this; see pre-commit/pre-commit#1003
If you don't use pre-commit, you can just wire directly into the git hook. Here is a nice pattern to ensure your co-workers install the hook, and also to only format the added or modified files:
-
If you don't have a workspace status script, which Bazel runs on every execution, then create
githooks/check-config.sh, make it executable, and register in.bazelrcwithcommon --workspace_status_command=githooks/check-config.sh(note that a release build likely overrides theworkspace_status_commandto support stamping) -
Use a snippet like the following in that script:
#!/usr/bin/env bash
inside_work_tree=$(git rev-parse --is-inside-work-tree 2>/dev/null)
# Encourage developers to setup githooks
IFS='' read -r -d '' GITHOOKS_MSG <<"EOF"
cat <<EOF
It looks like the git config option core.hooksPath is not set.
This repository uses hooks stored in githooks/ to run tools such as formatters.
You can disable this warning by running:
echo "common --workspace_status_command=" >> ~/.bazelrc
To set up the hooks, please run:
git config core.hooksPath githooks
EOF
if [ "${inside_work_tree}" = "true" ] && [ "$EUID" -ne 0 ] && [ -z "$(git config core.hooksPath)" ]; then
echo >&2 "${GITHOOKS_MSG}"
fi- Finally, create the
githooks/pre-commitfile, make it executable and add a snippet like:
#!/usr/bin/env bash
# Get staged files and format them
git diff --cached --diff-filter=AM --name-only -z | xargs --null --no-run-if-empty bash -c '
if [ $# -gt 0 ]; then
# Avoid building the target if it was already placed in the bazel_env output
if [ -e "bazel-out/bazel_env-opt/bin/tools/bazel_env/bin/format" ]; then
bazel-out/bazel_env-opt/bin/tools/bazel_env/bin/format "$@"
else
bazel run //:format -- "$@"
fi
if ! git diff --quiet -- "$@"; then
echo "❌ Some staged files were modified by the pre-commit hook."
echo "Please stage the changes and try committing again:"
git diff --stat -- "$@"
exit 1
fi
fi
' _We recommend using Aspect Workflows to hook up the CI check to notify developers of formatting changes, and supply a patch file that can be locally applied.
To set this up manually, there are two supported methods:
This will exit non-zero if formatting is needed. You would typically run the check mode on CI.
bazel run //tools/format:format.check
Normally Bazel tests should be hermetic, declaring their inputs, and therefore have cacheable results.
This is possible with format_test and a list of srcs.
Note that developers may not remember to add format_test for their new source files, so this is quite brittle,
unless you also use a tool like Gazelle to automatically update BUILD files.
load("@aspect_rules_lint//format:defs.bzl", "format_test")
format_test(
name = "format_test",
# register languages, e.g.
# python = "//:ruff",
srcs = ["my_code.go"],
)Alternatively, you can give up on Bazel's hermeticity, and follow a similar pattern as buildifier_test which creates an intentionally non-hermetic, and not cacheable target.
This will always run the formatters over all files under bazel test, so this technique is only appropriate
when the formatters are fast enough, and/or the number of files in the repository are few enough.
To acknowledge this fact, this mode requires an additional opt-in attribute, no_sandbox.
load("@aspect_rules_lint//format:defs.bzl", "format_test")
format_test(
name = "format_test",
# register languages, e.g.
# python = "//:ruff",
no_sandbox = True,
workspace = "//:WORKSPACE.bazel",
)Then run bazel test //tools/format/... to check that all files are formatted.
