Easily ensure your code is always formatted, with consistent tooling across everyone's machines.
bazel-super-formatter is an aggregator tool that runs many formatters, in the spirit of https://github.com/github/super-linter.
Features:
- Don't need to add Bazel targets to include the source files, or even adopt Bazel at all.
- Managed, fully hermetic tools and runtimes (except as noted below).
- Honors formatter configuration files.
Supported languages:
Supported | Language | Tool |
---|---|---|
✓ | Python | Black |
✓ | Java | google-java-format |
✓ | JavaScript/TypeScript/TSX | Prettier |
✓ | CSS/HTML | Prettier |
✓ | JSON | Prettier |
✓ | Markdown | Prettier |
✓ | Bash | prettier-plugin-sh |
✓ | SQL | prettier-plugin-sql |
✓ | Starlark (Bazel) | Buildifier |
✓ | Swift | SwiftFormat (1) |
✓ | Go | gofmt |
✓ | Protobuf | buf |
✓ | Terraform | terraform fmt |
C/C++/C# | clang-format | |
Rust | rustfmt | |
YAML | yamlfmt |
More languages TODO: aspect-build#6
- Non-hermetic: requires that a swift toolchain is installed on the machine. See https://github.com/bazelbuild/rules_swift#1-install-swift
Install Bazel: https://bazel.build/install/bazelisk
From the release you wish to use:
https://github.com/aspect-build/bazel-super-formatter/releases
copy the WORKSPACE snippet into your WORKSPACE
file.
bazel run @aspect_rules_format//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-revs
file to avoid polluting the blame layer.
bazel run @aspect_rules_format//format some/file.md other/file.json
If you use pre-commit.com, add this in your .pre-commit-config.yaml
:
- repo: local
hooks:
- id: bazel-super-formatter
name: Format
language: system
entry: bazel run @aspect_rules_format//format
files: .*
Note that pre-commit is silent while Bazel is fetching the tooling, 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, however this option will always run the formatter over all files, not just changed files.
$ echo "bazel run @aspect_rules_format//format" >> .git/hooks/pre-commit
$ chmod u+x .git/hooks/pre-commit
This will exit non-zero if formatting is needed. You would typically run the check mode on CI.
bazel run @aspect_rules_format//format -- --mode check
When you enable a language, it causes the super-formatter to fetch additional tooling as a runtime dependency of the format binary.
Add some of these lines to .bazelrc
:
# Enable fetching formatter toolchains
run --@aspect_rules_format//format:java_enabled
run --@aspect_rules_format//format:proto_enabled
run --@aspect_rules_format//format:python_enabled
run --@aspect_rules_format//format:swift_enabled
run --@aspect_rules_format//format:terraform_enabled
Look in our format/repositories.bzl
file and copy the http_*
rule you want to modify into your WORKSPACE, above the rules_format_dependencies()
call.
You can specify your own local binary, for example you might use black
for Python with a custom requirements.txt
file that adds plugins such as typing_extensions
.
You can do this in WORKSPACE
before installing aspect_rules_format:
pip_parse(
name = "aspect_rules_format_pypi",
requirements_lock = "//tools/black:requirements.txt",
)
load("@aspect_rules_format_pypi//:requirements.bzl", _aspect_rules_format_pypi_install_deps = "install_deps")
_aspect_rules_format_pypi_install_deps()
See aspect-build#13
We honor the .gitignore
file. Otherwise use the affordance provided by the formatter tool, for example .prettierignore
for files to be ignored by Prettier.
Sometimes engineers want to ignore a file with a certain extension because the content isn't actually valid syntax for the corresponding language. For example, you might write a template for YAML and name it my-template.yaml
even though it needs to have some interpolated values inserted before it's syntactically valid. We recommend instead fixing the file extension. In this example, my.yaml.tmpl
or my-template.yaml_
might be better.
Some engineers are used to setting up a formatter tool in their editor/IDE. Typically there is an editor extension, for example Prettier documents a number of them on https://prettier.io/docs/en/editors.html.
The authors of bazel-super-formatter believe that it's a waste of time for developers to setup their workflow this way. It's redundant. Since not all engineers will configure their editor, you need to have a pre-commit CI check anyway. At best, the users are instructed how to carefully reproduce the versions of the tools and their configuration files, so that the format operation they perform matches what will happen automatically as they send their change off for review.
Not everyone will agree, and it may not be beneficial to convince developers to change their behavior. Since this tool uses the same configuration files as the tool expects, you can always setup the editor extensions for all the underlying formatters yourself. Or you could probably configure the editor to always run one of the commands above in the Usage section, any time a file is changed. The instructions to do this are out-of-scope for this repo, particularly since they have to be formulated and updated for so many editors.
Generally, you should just allow code generators to make messy files.
You can exclude them from formatting by changing the file extension, adding a suppression comment at the top (following the formatter's docs) or adding to the formatter's ignore file (e.g. .prettierignore
).
However there are some valid cases where you really want to run a formatter as a build step. You can just reach into the external repository where we've installed them. For example, to run Prettier:
load("@aspect_rules_format_npm//:prettier/package_json.bzl", prettier = "bin")
prettier.prettier_binary(name = "prettier")
js_run_binary(
name = "fmt",
srcs = ["raw_file.md"],
args = ["raw_file.md"],
chdir = package_name(),
stdout = "formatted_file.md",
tool = "prettier",
)
See https://hackmd.io/0UgIb6gyTvSVX9N2vTPGug
This project just covers the "formatting" use case. Linting is a totally different developer experience as explained in the design doc above:
Formatter | Linter |
---|---|
Only one per language, since they could conflict with each other. | Many per language is fine; results compose. |
Invariant: program's behavior is never changed. | Suggested fixes may change behavior. |
Developer has no choices. Always blindly accept result. | Fix may be manual, or select from multiple auto-fixes. |
Changes must be applied. | Violations can be suppressed. |
Operates on a single file at a time. | Can require the dependency graph. |
Can always format just changed files / regions | New violations might be introduced in unchanged files. |
Fast enough to put in a pre-commit workflow. | Some are slow. |
We do intend to have an Aspect-recommended solution for linting, but the project isn't funded yet. Please let us know if your organization is interested in sponsoring that work.