Adding a lint rule¶
Lint rules live in src/jellycell/lint/rules.py. Each rule is a function + metadata.
Pattern¶
# src/jellycell/lint/rules.py
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from jellycell.paths import Project
@dataclass(frozen=True)
class Violation:
rule: str
path: Path
line: int | None
message: str
fixable: bool
def rule_my_rule(project: Project) -> list[Violation]:
"""Check that <thing>. Return a list of violations."""
violations: list[Violation] = []
# scan project, append Violation(...) for each problem
return violations
def fix_my_rule(project: Project, violation: Violation) -> bool:
"""Apply auto-fix for a violation. Return True if applied, False if skipped."""
# ... mutate files, return True ...
return True
RULES: dict[str, Callable[[Project], list[Violation]]] = {
"my-rule": rule_my_rule,
# ... other rules ...
}
FIXERS: dict[str, Callable[[Project, Violation], bool]] = {
"my-rule": fix_my_rule,
# ... other fixers ...
}
Rules¶
Names are kebab-case. Match the ruff convention:
pep723-position,declared-deps,artifact-paths.Configurable via
jellycell.toml. If the rule can be opt-in/opt-out, checkproject.config.lint.<name>before scanning.Fixers are optional. Some rules can’t be auto-fixed; those violations are reported with
fixable=False.Test both paths. Unit test: rule detects + ignores correctly. Fix test: fix applies correctly and is idempotent.
Testing¶
# tests/unit/test_lint_rules.py
def test_my_rule_detects(sample_project_factory):
project = sample_project_factory(with_violation=True)
violations = rule_my_rule(project)
assert len(violations) == 1
assert violations[0].rule == "my-rule"
def test_my_rule_fix_is_idempotent(sample_project_factory):
project = sample_project_factory(with_violation=True)
violations = rule_my_rule(project)
assert fix_my_rule(project, violations[0]) is True
assert rule_my_rule(project) == [] # gone
# Second fix on a clean project is a no-op
assert fix_my_rule(project, violations[0]) is False