Build a Lint Rule
Example: NoInheritFromObjectRule
In Python 3, a class is inherited from object
by default.
Explicitly inheriting from object
is redundant, so removing it keeps the code simpler.
In this tutorial, we’d like to build a lint rule to identify cases when
a class inherit from object and add an autofix to remove it.
[2]:
# an example with unnecessary object inheritance
class C(object):
...
# the above example can be simplified as this
class C:
...
Pick a Good Name
Before starting creating a new lint rule, let’s figure out a good short name for it.
A good lint rule name should be short and actionable.
Instead of describing the issue, describe the action needs to be taken to fix it.
So developers can easily learn how to fix the issue by just reading the name.
A lint rule name is a class name in camel case and ends with Rule
.
For example, to suggest gather await calls in a loop, it’s better to name as GatherSequentialAwaitRule
instead of AwaitInLoopLintRule
(less actionable).
If the action need is to remove/cleanup something, it can be named as No...Rule
, e.g. NoAssertEqualsRule
.
In this example, we name the rule as NoInheritFromObjectRule.
Lint Rule Scaffolding
A lint rule is a subclass of CstLintRule
which inherits from CSTVisitor in LibCST.
LibCST provides visitors for traversing the syntax tree.
Defining a visit_
or leave_
functions for a specific types of CSTNode allows us to register a callback function to be called during the syntax tree traversal.
In this example, we can inspect all class definitions in a file by defining a visit_ClassDef
function, which will get called once per class definition encountered during syntax tree traversal.
To add types, we’ll need to import ClassDef from libcst, and annotate the function signature:
[3]:
from fixit import CstLintRule
import libcst as cst
class NoInheritFromObjectRule(CstLintRule):
def visit_ClassDef(self, node: cst.ClassDef) -> None:
...
We don’t need to perform any work after visiting our children, so we won’t define leave_ClassDef
.
While not needed for this lint rule, if we wanted to visit specific attributes of a given node type,
we could specify that attribute as part of the method name too:
[4]:
def visit_If(self, node: cst.If) -> None:
# called first
...
def visit_If_test(self, node: cst.If) -> None:
# called after visit_If, but before we visit the test attribute
# `leave_If_test` would be called next, followed by `leave_If`.
if check_something(node.test):
...
Iteration order of attributes is the same as the order they appear in the source code. In this case, that means visit_If_test is called before visit_If_body and visit_If_orelse.
Use fixit’s cli to generate a skeleton of adding a new rule file:
$ python -m fixit.cli.add_new_rule # Creates new.py at fixit/rules/new.py
$ python -m fixit.cli.add_new_rule --path fixit/rules/my_rule.py --name rule_name # Creates my_rule.py at path specified
This will generate a rule file used to create and add new rule to fixit module.
The fixit.cli.add_new_rule contains two argument, -path
and --name
--path
is used to create rule file at path given in--path
, defaults tofixit/rules/new.py
--name
is used to assign the name of the rule and should be in snake case, defaults to the rule file name ifpath
provided else new. Otherwise, considers the value specified in--name
Please provide the name of the rule in snake case without the suffix rule
as CLI will take care of adding Rule
to end of the rule name.
[6]:
! python -m fixit.cli.add_new_rule --path fixit/rules/my_rule.py --name abcd
All done! ✨ 🍰 ✨
1 file left unchanged.
Successfully created my_rule.py rule file at fixit/rules
[7]:
! cat fixit/rules/my_rule.py
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import libcst as cst
import libcst.matchers as m
from fixit import CstLintRule, InvalidTestCase as Invalid, ValidTestCase as Valid
"""
This is a model rule file for adding a new rule to fixit module
"""
class AbcdRule(CstLintRule):
"""
docstring or new_rule description
"""
MESSAGE = "Enter rule description message"
VALID = [Valid("'example'")]
INVALID = [Invalid("'example'")]
Now, you can add rule’s functionality on top of above generated file. Also, make sure to check the class name in generated file. Your class name should always be suffixed with Rule
.
The Declarative Matcher API
Once we have a ClassDef
node, we need to see if it contains a base class named object
.
We could implement by inspecting attributes of the node using equality and isinstance.
[8]:
# check if any of the base classes of this class def is "object"
def visit_ClassDef(self, node: cst.ClassDef):
has_object_base = any(
isinstance(arg.value, cst.Name) and arg.value.value == "object"
for arg in node.bases
)
Unfortunately, that imperative approach isn’t easy to read or write, especially when matching a more complex syntax tree structure. LibCST has a declarative matcher API which allows you to define the shape of an object to match. It’s like a regular expression, but for the CST.
[9]:
import libcst.matchers as m
def visit_ClassDef(self, node: cst.ClassDef):
has_object_base = m.matches(
node, m.ClassDef(bases=[m.AtLeastN(n=1, matcher=m.Arg(value=m.Name("object")))])
)
It makes the code easier to read and maintain.
Reporting Violations
To report a lint violation, simply call report()
with a CSTNode.
Define a lint message via the MESSAGE
attribute in your lint class.
Keep your lint descriptions brief but informative. Link to other documentation if you want to provide an extended explanation. Feedback that you provide to a developer should be clear and actionable.
Add a docstring to the rule class to provide more context and the docstring will be included in the generated document.
[10]:
class NoInheritFromObjectRule(CstLintRule):
"""
In Python 3, a class is inherited from ``object`` by default.
Explicitly inheriting from ``object`` is redundant, so removing it keeps the code simpler.
"""
MESSAGE = "Inheriting from object is a no-op. 'class Foo:' is just fine =)"
def visit_ClassDef(self, node: cst.ClassDef) -> None:
new_bases = tuple(
base for base in node.bases if not m.matches(base.value, m.Name("object"))
)
if tuple(node.bases) != new_bases:
self.report(node)
Adding an Autofix
Warning a user about a problem is nice, but offering to fix it for them is even better! That’s what you can do with an auto-fixer. Currently we support replacing a node (use with_changes to modify a CSTNode) or removing it (by passing a libcst.RemovalSentinel as the replacement). In our example, we want to remove any references to object in the base classes of a class definition:
[11]:
class NoInheritFromObjectRule(CstLintRule):
MESSAGE = "Inheriting from object is a no-op. 'class Foo:' is just fine =)"
def visit_ClassDef(self, node: cst.ClassDef) -> None:
new_bases = tuple(
base for base in node.bases if not m.matches(base.value, m.Name("object"))
)
if tuple(node.bases) != new_bases:
# reconstruct classdef, removing parens if bases and keywords are empty
new_classdef = node.with_changes(
bases=new_bases,
lpar=cst.MaybeSentinel.DEFAULT,
rpar=cst.MaybeSentinel.DEFAULT,
)
# report warning and autofix
self.report(node, replacement=new_classdef)
This example also makes use of libcst.MaybeSentinel to properly handle the rendering syntax. In this case, using a MaybeSentinel for the parens fields will remove the parentheses following the class name if removing object from bases means there are no other base classes or keywords in the class definition.
Skipping Files
Certain behaviors may be acceptable in a set of files, but not in others.
We can avoid running the linter on some files by overriding should_skip_file.
The properties provided by self.context
(BaseContext()
) are useful when implementing should_skip_file()
.
[12]:
class MyRule(CstLintRule):
def should_skip_file(self):
# Assert statements are okay for tests.
# We could check the self.context.file_path object (see pathlib.Path), but
# Context has a helper property for tests, since this is a common use-case
return self.context.in_tests
Your lint rule does something, but now you need to test it. Testing lint rules is easy, and every lint rule should include test cases. Continue to the next tutorial Test a Lint Rule
Reference
-
class
fixit.
CstLintRule
-
MESSAGE
a short message in one or two sentences show to user when the rule is violated.
-
METADATA_DEPENDENCIES
= (<class 'libcst.metadata.position_provider.PositionProvider'>,)
-
should_skip_file
() → bool
-
report
(node: libcst._nodes.base.CSTNode, message: Optional[str] = None, *, position: Optional[libcst._position.CodePosition] = None, replacement: Union[libcst._nodes.base.CSTNode, libcst._flatten_sentinel.FlattenSentinel, libcst._removal_sentinel.RemovalSentinel, None] = None) → None Report a lint violation for a given node. Optionally specify a custom position to report an error at or a replacement node for an auto-fix.
-
classmethod
requires_metadata_caches
() → bool
-
-
class
fixit.
CstContext