FP2: Local Lint Rules¶
By default, Fixit expects rules to be referenced by their fully-qualified
module and/or class name. For example, fixit.rules
refers to the bundled set
if lint rules shipped with Fixit. Third party rules available in the environment
can similarly be referenced by their module name, as long as they are installed
and importable by Fixit at runtime. These will be collectively referred to as
“global rules” here for sake of clarity.
However, in many cases, it is also useful for projects using Fixit to build custom lint rules specific to the project being linted. It is beneficial for those rules to be defined within the codebase being linted, without needing to first install those lint rules into the environment before running Fixit.
These are defined as “local rules”, and will require dedicated behavior from Fixit to discover and import these rules at runtime, as well as special configuration syntax for enabling these local rules in a project.
Configuration¶
When configuring the set of enabled or disabled rules, any local rules must be marked with a single leading period, and referred to using their path relative to the configuration file’s directory.
For example, a configuration file at project/fixit.toml
could include
rules defined in project/some/local/rules.py
with the following:
[tool.fixit]
enable = [".some.local.rules"]
References to local rules should be accepted in either enable
or disable
options, including overrides, to provide the same selection criteria available
to global rules:
[tool.fixit]
enable = [".rules"]
[[tool.fixit.overrides]]
path = "project1"
disable = [".rules:PickyRule"]
enable = [".project1.rules"]
Implementation¶
When gathering enabled/disabled rules from configuration, Fixit currently just records the module name (qualname) and does set operations when merging configs/overrides. In order to differentiate and manage local rules correctly, without affecting the behavior of merging/overrides, one possible option is to replace the simple string with a tuple of path and module.
For example, where a global rule would be tracked as just "fixit.rules"
,
local rules from foo/bar/fixit.toml
could be tracked as
(Path('foo/bar'), ".local.rules"))
, allowing set operations while still
tracking their origin, and preventing collisions from different directories.
In this implementation, the path object should match the parent directory
containing the configuration file that enables or disables these local rules.
Options in both a pyproject.toml
and fixit.toml
from the same exact
directory would use the same path object, as would any local rules referenced
by overrides in those files.
—
When discovering and loading rules, the system should attempt to make sure that it is loading rules from the local path, rather than accidentally loading rules from the outside environment. This can be done either by a temporarily restricted path when importing, or with a custom import loader.
The former may be a simpler starting point, and can be handled with a temporary
override of sys.path
to include the local directory — and nothing else? —
and then importing the module as normal, with the leading period removed.
This could look something like:
with temporary_sys_path(parent_path):
name = name.lstrip(".")
module = importlib.import_module(name)
Once loaded, local modules and rules can be handled and traversed the same as for global rules, though the logic for filtering out disabled local rules may require more nuance.
Long term, we almost certainly need a custom loader, to prevent potential
conflicts between local namespaces. For example, project1 and project2 both
referece a local .rules
module, which would clash in sys.modules
.
Before a final public release, the following considerations must be handled:
Importing multiple local rules with the same local path and/or module name, or names that conflict with system modules. Eg, two different configs specifying
".local.rules"
, or loading a module named".sys"
.Relative imports from within local rules modules, such also
from .foo import Bar
or worsefrom ..foo import Bar
.
Limitations¶
For simplicity of implementation, it makes sense to disallow local rules from
outside of the configuration file’s parent directory. In other words,
project/subdir/fixit.toml
can not reference local rules from
project/other/rules.py
.
Enabling (or disabling) local rules with more than one preceding period,
such as ..rules
, is not supported, and should be rejected during config
validation.
To apply rules from a different subdirectory to another subdirectory,
a configuration located in a common parent can use
configuration overrides.
For instance, project/fixit.toml
could specify an override for the
subdir
path to enable ".local.rules"
.
Also for simplicity of implementation (and explanation to users), it makes sense to disallow filtering of local rules from outside the file (or exact parent directory) that originally enabled them.
For example, this would be considered invalid, or at least would not accomplish what the user may expect:
# foo/fixit.toml
[tool.fixit]
enable = [".local.rules"]
# foo/bar/fixit.toml
[tool.fixit]
disable = [".local.rules"]
Rather, the expected way to make this work would be with subpath overrides
in the parent directory’s fixit.toml
file:
# foo/fixit.toml
[tool.fixit]
enable = [".local.rules"]
[[tool.fixit.overrides]]
path = "bar"
disable = [".local.rules"]