Fixit is dead! Long live Fixit 2 – the latest version of our open-source auto-fixing linter.
Fixit 2 allows developers to efficiently build custom lint rules and perform auto-fixes for their codebases.
Fixit 2 is available today on PyPI.
Python is one of the most popular languages in use at Meta. Meta’s production engineers (PEs) are specialized software engineers (SWEs) who focus on reliability, efficiency, and scalability. They work on various projects, including debugging production services, rewriting inefficient libraries, orchestrating project deployments at scale, or capacity planning and scheduling. And Python is often one of the first tools that PEs reach for, as it offers rapid development, easy to read syntax, and a massive array of open source libraries.
Meta’s Python Language Foundation team — a hybrid team of both PEs and traditional SWEs — helps own and maintain the infrastructure and tooling for Python at Meta. The team supports engineers, data scientists, researchers, and anyone else at Meta using Python to get their work done.
One of the ways we accomplish this is building tools that enable Python developers to write better, and more reliable code more efficiently. This includes tools like automatic formatting and import sorting that eliminate tedium, or linters that guide engineers toward maintainable code with fewer bugs.
This year, we have been building a new linter, Fixit 2, designed from the ground up to make developers more efficient and capable, both in open source projects and the diverse landscape of our internal monorepo. At Meta, we are using Fixit 2 with a few early adopters, and plan to roll it out to the rest of our monorepo soon. But any developer can use it to perform auto-fixing more efficiently and make faster improvements to their own codebases.
Why a new linter? (why not X?)
There are a variety of excellent linters in the Python ecosystem, many of which have a large community of third-party plugins providing a diverse array of lint rules. We have used Flake8 internally at Meta since 2016, and it has been very successful in helping developers reduce bugs and keep a clean codebase. The popular flake8-bugbear plugin was even created by Łukasz Langa (author of Black, PSF developer-in-residence, and release manager for Python 3.8 and 3.9) while working at Meta (then Facebook), as a home for more opinionated lint rules that we could both use internally and share with the rest of the Python developer community.
We also have a large number of internal plugins built by various teams, and Flake8 allows them to write and enable custom lint rules directly in the codebase without getting sign-off from a central gatekeeper, and without waiting for a new deployment of Flake8 to roll out.
But while Flake8 has long been a cornerstone of our linting solution, it also has some rough edges. Writing new lint rules requires building entire plugins (each claiming a portion of the “namespace” for error codes) and encourages developers to build complicated plugins covering multiple classes of errors. When those lint errors are found, Flake8 can only point to a line and column number where it occurred, but has no way of suggesting changes to the developer looking at a list of lint results, leaving them in a state of trial and error to find changes that make the linter happy. Also, Flake8 uses the stdlib ast module, making it unable to parse future syntax features and forcing developers to wait for tools to upgrade before they can use the shiny new hotness.
There are alternatives to Flake8 of course, but many of them suffer from one or more drawbacks:
A lack of support for “local” in-repo plugins or custom lint rules.
Limited or no support for hierarchical configuration for different projects within a monorepo.
No option for auto-fixes when errors are found.
Slow performance on large codebases.
While some of those features aren’t critical, the most important for developer efficiency is offering auto-fixes – automatic suggested changes that would satisfy the lint rule. This takes the guesswork out of using a linter, and allows users to quickly review and accept those changes when possible, eliminating the need to re-run the linter until the code is finally clean. Combining these auto-fixes with in-repo, custom lint rules provides a level of tailored code quality improvements that is hard to beat.
Unfortunately, even Fixit, the auto-fixing linter that we built for Instagram and open sourced, did not support local lint rules or hierarchical configuration – core requirements for our monorepo that is home to thousands of projects, many of which are themselves open source projects with their own distinct needs for linting and CI. We received many requests from developers to support Fixit in our monorepo, but there were enough hurdles that we were only able to support a small set of security lint rules, reducing the direct benefits to our Python codebase.
Meet Fixit 2
After discussions with other teams, especially in the rapidly growing AI/ML space, we considered our options and decided upon a partial rewrite of Fixit. We intentionally designed the new version with an open source-first mindset, while incorporating the needs and requirements of our own monorepos and open source projects from day one.
The framework and linting engine would be rebuilt from the ground up while leaving the core design of lint rules largely untouched. The new system provides a hierarchical configuration based on the TOML format; support for local, in-repo lint rules similar to Flake8; and a much improved CLI and API for integration with other tools and automation.
Fixit itself builds on top of another Instagram open source project, LibCST, a concrete syntax tree for Python with a tree and node API following the patterns of the ast module in the standard library. The “concrete” part of CST means that LibCST includes every part of the source file in the resulting tree after parsing, including whitespace, comments, and formatting elements that are ignored by the ast module. This is what allows Fixit (and other tools we built, like µsort) to safely modify source files, without using regular expressions or the risk of producing broken syntax, and provides the foundation for Fixit to offer auto-fixes suggested by the lint rules themselves.
Writing a new lint rule can be done with less than a dozen lines of code, and test cases are defined inline. You can even place it right next to the code that it will be linting:
# teambread/rules/hollywood.py
import fixit
import libcst
class HollywoodName(fixit.LintRule):
VALID = […] # no lint errors here
INVALID = […] # bad code samples here
def visit_SimpleString(self, node: libcst.SimpleString):
if node.value in “‘Paul'” or ‘”Paul”‘:
self.report(node, “It’s underbaked!”)
Suggesting auto-fixes for the user is as easy as including a new CST node when reporting an error:
def visit_SimpleString(self, node: libcst.SimpleString):
if node.value in “‘Paul'” or ‘”Paul”‘:
new_node = libcst.SimpleString(‘”Mary”‘)
self.report(node, replacement=new_node)
Enabling this new rule within the project’s codebase can be done with a simple config change:
# teambread/sourdough/fixit.toml
[tool.fixit]
enable = [“.rules.hollywood”]
Now we can run our linter against our project:
# teambread/sourdough/baker.py
name = “Paul”
print(f”hello {name}!”)
$ fixit lint –diff sourdough/baker.py
sourdough/baker.py@7:11 HollywoodName: It’s underbaked! (has autofix)
— a/baker.py
+++ b/baker.py
@@ -6,3 +6,3 @@
def main():
– name = “Paul”
+ name = “Mary”
print(f”hello {name}”)
1 file checked, 1 file with errors, 1 auto-fix available
[1]
The `lint` command only shows errors and suggested changes. If we use the `fix` command, we can apply those suggested changes back to the codebase:
$ fixit fix –automatic sourdough/baker.py
sourdough/baker.py@7:11 HollywoodName: It’s underbaked! (has autofix)
1 file checked, 1 file with errors, 1 auto-fix available, 1 fix applied
Now that our auto-fixes have been applied, we can confirm that the project is now clean and lint-free:
$ fixit lint sourdough/baker.py
1 file clean
When running Fixit 2 with auto-fixing lint rules, any code that triggers the lint rule is an opportunity to get an automatic replacement, improving the codebase with less effort from the developer. Applied more broadly, Fixit 2 can even be used as a tool to enact sweeping codemods against a large codebase, while leaving a lint rule in place to handle any matching code in the future.
Try Fixit 2
Fixit 2 is available today on PyPI. You can install and test Fixit 2 with pip install fixit.
We have a roadmap with plans for future improvements and features, and a rich set of documentation and user guides to help you get started with Fixit 2 in your own projects or repositories. We hope it proves useful in your projects, and we look forward to hearing your feedback!
The post Fixit 2: Meta’s next-generation auto-fixing linter appeared first on Engineering at Meta.
Engineering at Meta