mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
# This commit was automatically generated by running the following
# script (followed by `cargo +nightly fmt`):
import glob
import re
from typing import NamedTuple
class Rule(NamedTuple):
code: str
name: str
path: str
def rules() -> list[Rule]:
"""Returns all the rules defined in `src/registry.rs`."""
file = open('src/registry.rs')
rules = []
while next(file) != 'ruff_macros::define_rule_mapping!(\n':
continue
while (line := next(file)) != ');\n':
line = line.strip().rstrip(',')
if line.startswith('//'):
continue
code, path = line.split(' => ')
name = path.rsplit('::')[-1]
rules.append(Rule(code, name, path))
return rules
code2name = {r.code: r.name for r in rules()}
for pattern in ('src/**/*.rs', 'ruff_cli/**/*.rs', 'ruff_dev/**/*.rs', 'scripts/add_*.py'):
for name in glob.glob(pattern, recursive=True):
with open(name) as f:
text = f.read()
text = re.sub('Rule(?:Code)?::([A-Z]\w+)', lambda m: 'Rule::' + code2name[m.group(1)], text)
text = re.sub(r'(?<!"<FilePattern>:<)RuleCode\b', 'Rule', text)
text = re.sub('(use crate::registry::{.*, Rule), Rule(.*)', r'\1\2', text) # fix duplicate import
with open(name, 'w') as f:
f.write(text)
145 lines
4.0 KiB
Python
145 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate boilerplate for a new rule.
|
|
|
|
Example usage:
|
|
|
|
python scripts/add_rule.py \
|
|
--name PreferListBuiltin \
|
|
--code PIE807 \
|
|
--origin flake8-pie
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
|
|
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
def dir_name(origin: str) -> str:
|
|
return origin.replace("-", "_")
|
|
|
|
|
|
def pascal_case(origin: str) -> str:
|
|
"""Convert from snake-case to PascalCase."""
|
|
return "".join(word.title() for word in origin.split("-"))
|
|
|
|
|
|
def snake_case(name: str) -> str:
|
|
"""Convert from PascalCase to snake_case."""
|
|
return "".join(f"_{word.lower()}" if word.isupper() else word for word in name).lstrip("_")
|
|
|
|
|
|
def main(*, name: str, code: str, origin: str) -> None:
|
|
# Create a test fixture.
|
|
with open(
|
|
ROOT_DIR / "resources/test/fixtures" / dir_name(origin) / f"{code}.py",
|
|
"a",
|
|
):
|
|
pass
|
|
|
|
# Add the relevant `#testcase` macro.
|
|
mod_rs = ROOT_DIR / "src/rules" / dir_name(origin) / "mod.rs"
|
|
content = mod_rs.read_text()
|
|
|
|
with open(mod_rs, "w") as fp:
|
|
for line in content.splitlines():
|
|
if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
|
|
indent = line.split("fn rules(rule_code: Rule, path: &Path) -> Result<()> {")[0]
|
|
fp.write(f'{indent}#[test_case(Rule::{code}, Path::new("{code}.py"); "{code}")]')
|
|
fp.write("\n")
|
|
|
|
fp.write(line)
|
|
fp.write("\n")
|
|
|
|
# Add the relevant rule function.
|
|
with open(ROOT_DIR / "src/rules" / dir_name(origin) / "rules.rs", "a") as fp:
|
|
fp.write(
|
|
f"""
|
|
/// {code}
|
|
pub fn {snake_case(name)}(checker: &mut Checker) {{}}
|
|
"""
|
|
)
|
|
fp.write("\n")
|
|
|
|
# Add the relevant struct to `src/violations.rs`.
|
|
content = (ROOT_DIR / "src/violations.rs").read_text()
|
|
|
|
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
|
|
for line in content.splitlines():
|
|
fp.write(line)
|
|
fp.write("\n")
|
|
|
|
if line.startswith(f"// {origin}"):
|
|
fp.write(
|
|
"""define_violation!(
|
|
pub struct %s;
|
|
);
|
|
impl Violation for %s {
|
|
fn message(&self) -> String {
|
|
todo!("Implement message")
|
|
}
|
|
|
|
fn placeholder() -> Self {
|
|
%s
|
|
}
|
|
}
|
|
"""
|
|
% (name, name, name)
|
|
)
|
|
fp.write("\n")
|
|
|
|
# Add the relevant code-to-violation pair to `src/registry.rs`.
|
|
content = (ROOT_DIR / "src/registry.rs").read_text()
|
|
|
|
seen_macro = False
|
|
has_written = False
|
|
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
|
|
for line in content.splitlines():
|
|
fp.write(line)
|
|
fp.write("\n")
|
|
|
|
if has_written:
|
|
continue
|
|
|
|
if line.startswith("define_rule_mapping!"):
|
|
seen_macro = True
|
|
continue
|
|
|
|
if not seen_macro:
|
|
continue
|
|
|
|
if line.strip() == f"// {origin}":
|
|
indent = line.split("//")[0]
|
|
fp.write(f"{indent}{code} => violations::{name},")
|
|
fp.write("\n")
|
|
has_written = True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate boilerplate for a new rule.",
|
|
epilog="python scripts/add_rule.py --name PreferListBuiltin --code PIE807 --origin flake8-pie",
|
|
)
|
|
parser.add_argument(
|
|
"--name",
|
|
type=str,
|
|
required=True,
|
|
help="The name of the check to generate, in PascalCase (e.g., 'LineTooLong').",
|
|
)
|
|
parser.add_argument(
|
|
"--code",
|
|
type=str,
|
|
required=True,
|
|
help="The code of the check to generate (e.g., 'A001').",
|
|
)
|
|
parser.add_argument(
|
|
"--origin",
|
|
type=str,
|
|
required=True,
|
|
help="The source with which the check originated (e.g., 'flake8-builtins').",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
main(name=args.name, code=args.code, origin=args.origin)
|