From 1a09fff99187289c020e0670377723f80650604e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 9 Jan 2023 19:55:46 -0500 Subject: [PATCH] Update rule-generation `scripts` to match latest conventions (#1758) Resolves #1755. --- scripts/add_check.py | 139 ---------------------------------- scripts/add_plugin.py | 53 ++++++++----- scripts/add_rule.py | 172 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 159 deletions(-) delete mode 100644 scripts/add_check.py create mode 100644 scripts/add_rule.py diff --git a/scripts/add_check.py b/scripts/add_check.py deleted file mode 100644 index 6092e078fb..0000000000 --- a/scripts/add_check.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -"""Generate boilerplate for a new check. - -Example usage: - - python scripts/add_check.py \ - --name PreferListBuiltin \ - --code PIE807 \ - --plugin flake8-pie -""" - -import argparse -import os - -ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -def dir_name(plugin: str) -> str: - return plugin.replace("-", "_") - - -def pascal_case(plugin: str) -> str: - """Convert from snake-case to PascalCase.""" - return "".join(word.title() for word in plugin.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, plugin: str) -> None: - # Create a test fixture. - with open( - os.path.join(ROOT_DIR, f"resources/test/fixtures/{dir_name(plugin)}/{code}.py"), - "a", - ): - pass - - # Add the relevant `#testcase` macro. - with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "r") as fp: - content = fp.read() - - with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "w") as fp: - for line in content.splitlines(): - if line.strip() == "fn rules(check_code: RuleCode, path: &Path) -> Result<()> {": - indent = line.split("fn rules(check_code: RuleCode, path: &Path) -> Result<()> {")[0] - fp.write(f'{indent}#[test_case(RuleCode::{code}, Path::new("{code}.py"); "{code}")]') - fp.write("\n") - - fp.write(line) - fp.write("\n") - - # Add the relevant plugin function. - with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/plugins.rs"), "a") as fp: - fp.write( - f""" -/// {code} -pub fn {snake_case(name)}(checker: &mut Checker) {{}} -""" - ) - fp.write("\n") - - # Add the relevant sections to `src/registry.rs`. - with open(os.path.join(ROOT_DIR, "src/registry.rs"), "r") as fp: - content = fp.read() - - index = 0 - with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp: - for line in content.splitlines(): - fp.write(line) - fp.write("\n") - - if line.strip() == f"// {plugin}": - if index == 0: - # `RuleCode` definition - indent = line.split(f"// {plugin}")[0] - fp.write(f"{indent}{code},") - fp.write("\n") - - elif index == 1: - # `DiagnosticKind` definition - indent = line.split(f"// {plugin}")[0] - fp.write(f"{indent}{name},") - fp.write("\n") - - elif index == 2: - # `RuleCode#kind()` - indent = line.split(f"// {plugin}")[0] - fp.write(f"{indent}RuleCode::{code} => DiagnosticKind::{name},") - fp.write("\n") - - elif index == 3: - # `RuleCode#category()` - indent = line.split(f"// {plugin}")[0] - fp.write(f"{indent}RuleCode::{code} => CheckCategory::{pascal_case(plugin)},") - fp.write("\n") - - elif index == 4: - # `DiagnosticKind#code()` - indent = line.split(f"// {plugin}")[0] - fp.write(f"{indent}DiagnosticKind::{name} => &RuleCode::{code},") - fp.write("\n") - - elif index == 5: - # `RuleCode#body` - indent = line.split(f"// {plugin}")[0] - fp.write(f'{indent}DiagnosticKind::{name} => todo!("Write message body for {code}"),') - fp.write("\n") - - index += 1 - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Generate boilerplate for a new check.", - epilog="python scripts/add_check.py --name PreferListBuiltin --code PIE807 --plugin 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( - "--plugin", - type=str, - required=True, - help="The plugin with which the check is associated (e.g., 'flake8-builtins').", - ) - args = parser.parse_args() - - main(name=args.name, code=args.code, plugin=args.plugin) diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index ecddd97b3f..a8d4565498 100644 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate boilerplate for a new plugin. +"""Generate boilerplate for a new Flake8 plugin. Example usage: @@ -31,9 +31,9 @@ def main(*, plugin: str, url: str) -> None: # Create the Rust module. os.makedirs(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}"), exist_ok=True) - with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/rules"), "a"): - pass - with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/rules"), "w+") as fp: + with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/rules.rs"), "w+") as fp: + fp.write("use crate::checkers::ast::Checker;\n") + with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "w+") as fp: fp.write("pub mod rules;\n") fp.write("\n") fp.write( @@ -49,13 +49,13 @@ mod tests { use crate::linter::test_path; use crate::settings; - fn rules(check_code: RuleCode, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy()); + fn rules(rule_code: RuleCode, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics =test_path( Path::new("./resources/test/fixtures/%s") .join(path) .as_path(), - &settings::Settings::for_rule(check_code), + &settings::Settings::for_rule(rule_code), )?; insta::assert_yaml_snapshot!(snapshot, diagnostics); Ok(()) @@ -67,10 +67,10 @@ mod tests { # Add the plugin to `lib.rs`. with open(os.path.join(ROOT_DIR, "src/lib.rs"), "a") as fp: - fp.write(f"pub mod {dir_name(plugin)};") + fp.write(f"mod {dir_name(plugin)};") # Add the relevant sections to `src/registry.rs`. - with open(os.path.join(ROOT_DIR, "src/registry.rs"), "r") as fp: + with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp: content = fp.read() with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp: @@ -85,23 +85,37 @@ mod tests { fp.write(f"{indent}{pascal_case(plugin)},") fp.write("\n") - elif line.strip() == 'CheckCategory::Ruff => "Ruff-specific rules",': - indent = line.split('CheckCategory::Ruff => "Ruff-specific rules",')[0] - fp.write(f'{indent}CheckCategory::{pascal_case(plugin)} => "{plugin}",') + elif line.strip() == 'RuleOrigin::Ruff => "Ruff-specific rules",': + indent = line.split('RuleOrigin::Ruff => "Ruff-specific rules",')[0] + fp.write(f'{indent}RuleOrigin::{pascal_case(plugin)} => "{plugin}",') fp.write("\n") - elif line.strip() == "CheckCategory::Ruff => vec![RuleCodePrefix::RUF],": - indent = line.split("CheckCategory::Ruff => vec![RuleCodePrefix::RUF],")[0] + elif line.strip() == "RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],": + indent = line.split("RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],")[0] fp.write( - f"{indent}CheckCategory::{pascal_case(plugin)} => vec![\n" + f"{indent}RuleOrigin::{pascal_case(plugin)} => vec![\n" f'{indent} todo!("Fill-in prefix after generating codes")\n' f"{indent}]," ) fp.write("\n") - elif line.strip() == "CheckCategory::Ruff => None,": - indent = line.split("CheckCategory::Ruff => None,")[0] - fp.write(f"{indent}CheckCategory::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),') + elif line.strip() == "RuleOrigin::Ruff => None,": + indent = line.split("RuleOrigin::Ruff => None,")[0] + fp.write(f"{indent}RuleOrigin::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),') + fp.write("\n") + + fp.write(line) + fp.write("\n") + + # Add the relevant section to `src/violations.rs`. + with open(os.path.join(ROOT_DIR, "src/violations.rs")) as fp: + content = fp.read() + + with open(os.path.join(ROOT_DIR, "src/violations.rs"), "w") as fp: + for line in content.splitlines(): + if line.strip() == "// Ruff": + indent = line.split("// Ruff")[0] + fp.write(f"{indent}// {plugin}") fp.write("\n") fp.write(line) @@ -110,7 +124,7 @@ mod tests { if __name__ == "__main__": parser = argparse.ArgumentParser( - description="Generate boilerplate for a new plugin.", + description="Generate boilerplate for a new Flake8 plugin.", epilog=( "Example usage: python scripts/add_plugin.py flake8-pie " "--url https://pypi.org/project/flake8-pie/0.16.0/" @@ -118,7 +132,6 @@ if __name__ == "__main__": ) parser.add_argument( "plugin", - required=True, type=str, help="The name of the plugin to generate.", ) diff --git a/scripts/add_rule.py b/scripts/add_rule.py new file mode 100644 index 0000000000..8cec1782e6 --- /dev/null +++ b/scripts/add_rule.py @@ -0,0 +1,172 @@ +#!/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 + +ROOT_DIR = 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( + os.path.join(ROOT_DIR, f"resources/test/fixtures/{dir_name(origin)}/{code}.py"), + "a", + ): + pass + + # Add the relevant `#testcase` macro. + with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/mod.rs")) as fp: + content = fp.read() + + with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/mod.rs"), "w") as fp: + for line in content.splitlines(): + if line.strip() == "fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {": + indent = line.split("fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {")[0] + fp.write(f'{indent}#[test_case(RuleCode::{code}, Path::new("{code}.py"); "{code}")]') + fp.write("\n") + + fp.write(line) + fp.write("\n") + + # Add the relevant rule function. + with open(os.path.join(ROOT_DIR, f"src/{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`. + with open(os.path.join(ROOT_DIR, "src/violations.rs")) as fp: + content = fp.read() + + with open(os.path.join(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`. + with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp: + content = fp.read() + + seen_macro = False + has_written = False + with open(os.path.join(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 + + # Add the relevant code-to-origin pair to `src/registry.rs`. + with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp: + content = fp.read() + + seen_impl = False + has_written = False + with open(os.path.join(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("impl RuleCode"): + seen_impl = True + continue + + if not seen_impl: + continue + + if line.strip() == f"// {origin}": + indent = line.split("//")[0] + fp.write(f"{indent}RuleCode::{code} => RuleOrigin::{pascal_case(origin)},") + 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)