Fixit: linting framework with auto-fixes

Documentation PyPI Changelog Project Roadmap MIT License

Fixit provides a highly configurable linting framework with support for auto-fixes, custom “local” lint rules, and hierarchical configuration, built on LibCST.

Fixit makes it quick and easy to write new lint rules and offer suggested changes for any errors found, which can then be accepted automatically, or presented to the user for consideration.

Fixit 2.0 has been rebuilt for better configuration and support for custom lint rules. If you are using Fixit 0.1.4 or older, take a look at the legacy documentation or the stable branch. See the upgrade guide for tools and information to migrate existing configuration and lint rules and ensure compatibility with the latest version of Fixit.

For more details, see the user guide.

Quick Start

Setup

Install Fixit from PyPI:

$ pip install --pre "fixit >1"

By default, Fixit enables all of the lint rules that ship with Fixit, all of which are part of the fixit.rules package.

If you want to customize the list of enabled rules, either to add new rules or disable others, see the Configuration Guide for details and options available.

If you are upgrading from previous versions of Fixit, look at the Upgrade Guide for a list of changes and tools to assist with migrating to the latest version.

Usage

See lints and suggested changes for a set of source files:

$ fixit lint <path>

Apply suggested changes on those same files automatically:

$ fixit fix <path>

If given directories, Fixit will automatically recurse them, finding any files with the .py extension, while obeying your repo’s global .gitignore.

See the Command Reference for more details.

Example

Given the following code:

# custom_object.py

class Foo(object):
    def bar(self, value: str) -> str:
        return "value is {}".format(value)

When running Fixit, we see two separate lint errors:

$ fixit lint custom_object.py
custom_object.py@4:15 UseFstring: Do not use printf style formatting or .format(). Use f-string instead to be more readable and efficient. See https://www.python.org/dev/peps/pep-0498/
custom_object.py@2:0 NoInheritFromObject: Inheriting from object is a no-op.  'class Foo:' is just fine =)

We can also see any suggested changes by passing --diff:

$ fixit lint --diff custom_object.py
custom_object.py@7:0 NoInheritFromObject: Inheriting from object is a no-op.  'class Foo:' is just fine =) (has autofix)
--- a/custom_object.py
+++ b/custom_object.py
@@ -6,3 +6,3 @@
# Triggers built-in lint rules
-class Foo(object):
+class Foo:
    def bar(self, value: str) -> str:
custom_object.py@9:15 UseFstring: Do not use printf style formatting or .format(). Use f-string instead to be more readable and efficient. See https://www.python.org/dev/peps/pep-0498/
🛠️  1 file checked, 1 file with errors, 1 auto-fix available 🛠️

Silencing Errors

For lint rules without autofixes, it may still be useful to silence individual errors. A simple # lint-ignore or # lint-fixme comment, either as a trailing inline comment, or as a dedicated comment line above the code that triggered the lint rule:

class Foo(object):  # lint-fixme: NoInheritFromObject
    ...

# lint-ignore: NoInheritFromObject
class Bar(object):
    ...

By providing one or more lint rule, separated by commas, Fixit can still report issues triggered by other lint rules that haven’t been listed in the comment, but this is not required.

If no rule name is listed, Fixit will silence all rules when reported on code associated with that comment:

class Foo(object):  # lint-ignore
    ...

“ignore” vs “fixme”

Both comment directives achieve the same result — silencing errors for a particular statement of code. The semantics of using either term is left to the user, though they are intended to be used with the following meanings:

  • # lint-fixme for errors that need to be corrected or reviewed at a later date, but where the lint rule should be silenced temporarily for the sake of CI or similar external circumstances.

  • # lint-ignore for errors that are false-positives (please report issues if this occurs with built-in lint rules) or the code is otherwise intentionally written or structured in a way that the lint error cannot be avoided.

Future versions of Fixit may offer reporting or similar tools that treat “fixme” directives differently from “ignore” directives.

Custom Rules

Fixit makes it easy to write and enable new lint rules, directly in your existing codebase alongside the code they will be linting.

Lint rules in Fixit are built on top of LibCST using a LintRule to combine visitors and tests together in a single unit. A (very) simple rule looks like this:

# teambread/rules/hollywood.py

from fixit import LintRule, InvalidTestCase, ValidTestCase
import libcst

class HollywoodNameRule(LintRule):
    # clean code samples
    VALID = [
        ValidTestCase('name = "Susan"'),
    ]
    # code that triggers this rule
    INVALID = [
        InvalidTestCase('name = "Paul"'),
    ]

    def visit_SimpleString(self, node: libcst.SimpleString) -> None:
        if node.value in ('"Paul"', "'Paul'"):
            self.report(node, "It's underproved!")

Rules can suggest auto-fixes for the user by including a replacement CST node when reporting an error:

def visit_SimpleString(self, node: libcst.SimpleString) -> None:
    if node.value in ('"Paul"', "'Paul'"):
        new_node = libcst.SimpleString('"Mary"')
        self.report(node, "It's underproved!", replacement=new_node)

The best lint rules will provide a clear error message, a suggested replacement, and multiple valid and invalid tests cases that exercise as many edge cases for the lint rule as possible.

Once written, the new lint rule can be enabled by adding it to the list of enabled lint rules in the project’s Configuration file:

# teambread/pyproject.toml

[tool.fixit]
enable = [
    ".rules.hollywood",  # enable just the rules in hollywood.py
    ".rules",  # enable rules from all files in the rules/ directory
]

Note

The leading . (period) is required when using in-repo, or “local”, lint rules, with a module path relative to the directory containing the config file. This allows Fixit to locate and import the lint rule without needing to install a plugin in the user’s environment.

However, be aware that if your custom lint rule needs to import other libraries from the repo, those libraries must be imported using relative imports, and must be contained within the same directory tree as the configuration file.

Once enabled, Fixit can run that new lint rule against the codebase:

# teambread/sourdough/baker.py

def main():
    name = "Paul"
    print(f"hello {name}")
$ fixit lint --diff sourdough/baker.py
sourdough/baker.py@7:11 HollywoodName: It's underproved! (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]

Note that the lint command only shows lint errors (and suggested changes). The fix command will apply these suggested changes to the codebase:

$ fixit fix --automatic sourdough/baker.py
sourdough/baker.py@7:11 HollywoodName: It's underproved! (has autofix)
🛠️  1 file checked, 1 file with errors, 1 auto-fix available, 1 fix applied 🛠️

By default, the fix command will interactively prompt the user for each suggested change available, which the user can then accept or decline.

Now that the suggested changes have been applied, the codebase is clean:

$ fixit lint sourdough/baker.py
🧼 1 file clean 🧼

License

Fixit is MIT licensed, as found in the LICENSE file.