diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f73fb0164f..f68a921681 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,7 +96,9 @@ jobs: - uses: Swatinem/rust-cache@v1 - run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint - run: cargo check - - run: ./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ + - run: | + ./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST + ./scripts/add_rule.py --name FirstRule --code TST001 --linter test - run: cargo check # TODO(charlie): Re-enable the `wasm-pack` tests. diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index 3a7cc80fbc..716ef0eb08 100755 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -6,6 +6,7 @@ Example usage: python scripts/add_plugin.py \ flake8-pie \ --url https://pypi.org/project/flake8-pie/0.16.0/ + --prefix PIE """ import argparse @@ -14,19 +15,18 @@ import os from _utils import ROOT_DIR, dir_name, get_indent, pascal_case -def main(*, plugin: str, url: str) -> None: +def main(*, plugin: str, url: str, prefix_code: str) -> None: # Create the test fixture folder. os.makedirs( ROOT_DIR / "resources/test/fixtures" / dir_name(plugin), exist_ok=True, ) - # Create the Rust module. - rust_module = ROOT_DIR / "src/rules" / dir_name(plugin) - os.makedirs(rust_module, exist_ok=True) - with open(rust_module / "rules.rs", "w+") as fp: - fp.write("use crate::checkers::ast::Checker;\n") - with open(rust_module / "mod.rs", "w+") as fp: + # Create the Plugin rules module. + plugin_dir = ROOT_DIR / "src/rules" / dir_name(plugin) + plugin_dir.mkdir(exist_ok=True) + + with (plugin_dir / "mod.rs").open("w+") as fp: fp.write(f"//! Rules from [{plugin}]({url}).\n") fp.write("pub(crate) mod rules;\n") fp.write("\n") @@ -59,14 +59,24 @@ mod tests { % dir_name(plugin) ) + # Create a subdirectory for rules and create a `mod.rs` placeholder + rules_dir = plugin_dir / "rules" + rules_dir.mkdir(exist_ok=True) + + with (rules_dir / "mod.rs").open("w+") as fp: + fp.write("\n\n") + + # Create the snapshots subdirectory + (plugin_dir / "snapshots").mkdir(exist_ok=True) + # Add the plugin to `rules/mod.rs`. - with open(ROOT_DIR / "src/rules/mod.rs", "a") as fp: + with (ROOT_DIR / "src/rules/mod.rs").open("a") as fp: fp.write(f"pub mod {dir_name(plugin)};") # Add the relevant sections to `src/registry.rs`. content = (ROOT_DIR / "src/registry.rs").read_text() - with open(ROOT_DIR / "src/registry.rs", "w") as fp: + with (ROOT_DIR / "src/registry.rs").open("w") as fp: for line in content.splitlines(): indent = get_indent(line) @@ -75,33 +85,19 @@ mod tests { fp.write("\n") elif line.strip() == '#[prefix = "RUF"]': - fp.write(f'{indent}#[prefix = "TODO"]\n') + fp.write(f'{indent}#[prefix = "{prefix_code}"]\n') fp.write(f"{indent}{pascal_case(plugin)},") fp.write("\n") elif line.strip() == "Linter::Ruff => Prefixes::Single(RuleSelector::RUF),": - prefix = 'todo!("Fill-in prefix after generating codes")' fp.write( - f"{indent}Linter::{pascal_case(plugin)} => Prefixes::Single({prefix})," + f"{indent}Linter::{pascal_case(plugin)} => Prefixes::Single(RuleSelector::{prefix_code})," ) fp.write("\n") fp.write(line) fp.write("\n") - # Add the relevant section 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(): - if line.strip() == "// Ruff": - indent = get_indent(line) - fp.write(f"{indent}// {plugin}") - fp.write("\n") - - fp.write(line) - fp.write("\n") - if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -122,6 +118,13 @@ if __name__ == "__main__": type=str, help="The URL of the latest release in PyPI.", ) + parser.add_argument( + "--prefix", + required=False, + default="TODO", + type=str, + help="Prefix code for the plugin. Leave empty to manually fill.", + ) args = parser.parse_args() - main(plugin=args.plugin, url=args.url) + main(plugin=args.plugin, url=args.url, prefix_code=args.prefix) diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 149475b30e..d80a8b58bc 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -21,47 +21,58 @@ def snake_case(name: str) -> str: def main(*, name: str, code: str, linter: str) -> None: # Create a test fixture. - with open( - ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py", - "a", - ): + with (ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py").open("a"): pass + plugin_module = ROOT_DIR / "src/rules" / dir_name(linter) + rule_name_snake = snake_case(name) + # Add the relevant `#testcase` macro. - mod_rs = ROOT_DIR / "src/rules" / dir_name(linter) / "mod.rs" + mod_rs = plugin_module / "mod.rs" content = mod_rs.read_text() - with open(mod_rs, "w") as fp: + with mod_rs.open("w") as fp: for line in content.splitlines(): if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {": indent = get_indent(line) - fp.write(f'{indent}#[test_case(Rule::{code}, Path::new("{code}.py"); "{code}")]') + fp.write(f'{indent}#[test_case(Rule::{name}, 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(linter) / (snake_case(name) + ".rs"), "w") as fp: - fp.write( - f""" -/// {code} -pub fn {snake_case(name)}(checker: &mut Checker) {{}} -""" - ) - fp.write("\n") + # Add the exports + rules_dir = plugin_module / "rules" + rules_mod = rules_dir / "mod.rs" - # 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) + contents = rules_mod.read_text() + parts = contents.split("\n\n") + if len(parts) == 2: + new_contents = parts[0] + "\n" + new_contents += f"pub use {rule_name_snake}::{{{rule_name_snake}, {name}}};" + new_contents += "\n" + new_contents += "\n" + new_contents += parts[1] + new_contents += f"mod {rule_name_snake};" + new_contents += "\n" + rules_mod.write_text(new_contents) + else: + with rules_mod.open("a") as fp: + fp.write(f"pub use {rule_name_snake}::{{{rule_name_snake}, {name}}};") + fp.write("\n") + fp.write(f"mod {rule_name_snake};") fp.write("\n") - if line.startswith(f"// {linter}"): - fp.write( - """define_violation!( + # Add the relevant rule function. + with (rules_dir / f"{rule_name_snake}.rs").open("w") as fp: + fp.write( + """use ruff_macros::derive_message_formats; + +use crate::define_violation; +use crate::violation::Violation; +use crate::checkers::ast::Checker; + +define_violation!( pub struct %s; ); impl Violation for %s { @@ -72,16 +83,23 @@ impl Violation for %s { } } """ - % (name, name) - ) - fp.write("\n") + % (name, name) + ) + fp.write("\n") + fp.write( + f""" +/// {code} +pub fn {rule_name_snake}(checker: &mut Checker) {{}} +""" + ) + 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: + with (ROOT_DIR / "src/registry.rs").open("w") as fp: for line in content.splitlines(): fp.write(line) fp.write("\n") @@ -98,7 +116,7 @@ impl Violation for %s { if line.strip() == f"// {linter}": indent = get_indent(line) - fp.write(f"{indent}{code} => violations::{name},") + fp.write(f"{indent}{code} => rules::{linter}::rules::{name},") fp.write("\n") has_written = True