mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 03:55:09 +00:00 
			
		
		
		
	 9925910a29
			
		
	
	
		9925910a29
		
			
		
	
	
	
	
		
			
			Summary -- This PR adds a macro-generated method to retrieve the `Rule` associated with a given `Violation` struct, which makes it substantially cheaper than parsing from the rule name. The rule is then converted to a `NoqaCode` for storage on the `Message` (and eventually on the new diagnostic type). The `ViolationMetadata::rule_name` method was now unused, so the `rule` method replaces it. Several types had to be moved from the `ruff_diagnostics` crate to the `ruff_linter` crate to make this work, namely the `Violation` traits and the old `Diagnostic` type, which had a constructor generic over a `Violation`. It's actually a fairly small PR, minus the hundreds of import changes. The main changes are in these files: - [crates/ruff_linter/src/message/mod.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-139754ea310d75f28307008d21c771a190038bd106efe3b9267cc2d6c0fa0921) - [crates/ruff_diagnostics/src/lib.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-8e8ea5c586935bf21ea439f24253fcfd5955d2cb130f5377c2fa7bfee3ea3a81) - [crates/ruff_linter/src/diagnostic.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-1d0c9aad90d8f9446079c5be5f284150d97797158715bd9729e6f1f70246297a) - [crates/ruff_linter/src/lib.rs](https://github.com/astral-sh/ruff/pull/18234/files#diff-eb93ef7e78a612f5fa9145412c75cf6b1a5cefba1c2233e4a11a880a1ce1fbcc) Test Plan -- Existing tests
		
			
				
	
	
		
			196 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| """Generate boilerplate for a new rule.
 | |
| 
 | |
| Example usage:
 | |
| 
 | |
|     python scripts/add_rule.py \
 | |
|         --name PreferListBuiltin \
 | |
|         --prefix PIE \
 | |
|         --code 807 \
 | |
|         --linter flake8-pie
 | |
| """
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| import argparse
 | |
| import subprocess
 | |
| 
 | |
| from _utils import ROOT_DIR, dir_name, get_indent, pascal_case, snake_case
 | |
| 
 | |
| 
 | |
| def main(*, name: str, prefix: str, code: str, linter: str) -> None:
 | |
|     """Generate boilerplate for a new rule."""
 | |
|     # Create a test fixture.
 | |
|     filestem = f"{prefix}{code}" if linter != "pylint" else snake_case(name)
 | |
|     with (
 | |
|         ROOT_DIR
 | |
|         / "crates/ruff_linter/resources/test/fixtures"
 | |
|         / dir_name(linter)
 | |
|         / f"{filestem}.py"
 | |
|     ).open("a"):
 | |
|         pass
 | |
| 
 | |
|     plugin_module = ROOT_DIR / "crates/ruff_linter/src/rules" / dir_name(linter)
 | |
|     rule_name_snake = snake_case(name)
 | |
| 
 | |
|     # Add the relevant `#testcase` macro.
 | |
|     mod_rs = plugin_module / "mod.rs"
 | |
|     content = mod_rs.read_text()
 | |
| 
 | |
|     with mod_rs.open("w") as fp:
 | |
|         has_added_testcase = False
 | |
|         lines = []
 | |
|         for line in content.splitlines():
 | |
|             if not has_added_testcase and (
 | |
|                 line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {"
 | |
|             ):
 | |
|                 indent = get_indent(line)
 | |
|                 lines.append(
 | |
|                     f'{indent}#[test_case(Rule::{name}, Path::new("{filestem}.py"))]',
 | |
|                 )
 | |
|                 fp.write("\n".join(lines))
 | |
|                 fp.write("\n")
 | |
|                 lines.clear()
 | |
|                 has_added_testcase = True
 | |
| 
 | |
|             if has_added_testcase:
 | |
|                 fp.write(line)
 | |
|                 fp.write("\n")
 | |
|             elif line.strip() == "":
 | |
|                 fp.write("\n".join(lines))
 | |
|                 fp.write("\n\n")
 | |
|                 lines.clear()
 | |
|             else:
 | |
|                 lines.append(line)
 | |
| 
 | |
|     # Add the exports
 | |
|     rules_dir = plugin_module / "rules"
 | |
|     rules_mod = rules_dir / "mod.rs"
 | |
| 
 | |
|     contents = rules_mod.read_text()
 | |
|     parts = contents.split("\n\n")
 | |
| 
 | |
|     new_pub_use = f"pub(crate) use {rule_name_snake}::*"
 | |
|     new_mod = f"mod {rule_name_snake};"
 | |
| 
 | |
|     if len(parts) == 2:
 | |
|         new_contents = parts[0]
 | |
|         new_contents += "\n" + new_pub_use + ";"
 | |
|         new_contents += "\n\n"
 | |
|         new_contents += parts[1] + new_mod
 | |
|         new_contents += "\n"
 | |
| 
 | |
|         rules_mod.write_text(new_contents)
 | |
|     else:
 | |
|         with rules_mod.open("a") as fp:
 | |
|             fp.write(f"{new_pub_use};")
 | |
|             fp.write("\n\n")
 | |
|             fp.write(f"{new_mod}")
 | |
|             fp.write("\n")
 | |
| 
 | |
|     # Add the relevant rule function.
 | |
|     with (rules_dir / f"{rule_name_snake}.rs").open("w") as fp:
 | |
|         fp.write(
 | |
|             f"""\
 | |
| use ruff_macros::{{ViolationMetadata, derive_message_formats}};
 | |
| 
 | |
| use crate::Violation;
 | |
| use crate::checkers::ast::Checker;
 | |
| 
 | |
| /// ## What it does
 | |
| ///
 | |
| /// ## Why is this bad?
 | |
| ///
 | |
| /// ## Example
 | |
| /// ```python
 | |
| /// ```
 | |
| ///
 | |
| /// Use instead:
 | |
| /// ```python
 | |
| /// ```
 | |
| #[derive(ViolationMetadata)]
 | |
| pub(crate) struct {name};
 | |
| 
 | |
| impl Violation for {name} {{
 | |
|     #[derive_message_formats]
 | |
|     fn message(&self) -> String {{
 | |
|         format!("TODO: write message: {{}}", todo!("implement message"))
 | |
|     }}
 | |
| }}
 | |
| """,
 | |
|         )
 | |
|         fp.write(
 | |
|             f"""
 | |
| /// {prefix}{code}
 | |
| pub(crate) fn {rule_name_snake}(checker: &mut Checker) {{}}
 | |
| """,
 | |
|         )
 | |
| 
 | |
|     text = ""
 | |
|     with (ROOT_DIR / "crates/ruff_linter/src/codes.rs").open("r") as fp:
 | |
|         while (line := next(fp)).strip() != f"// {linter}":
 | |
|             text += line
 | |
|         text += line
 | |
| 
 | |
|         lines = []
 | |
|         while (line := next(fp)).strip() != "":
 | |
|             lines.append(line)
 | |
| 
 | |
|         variant = pascal_case(linter)
 | |
|         linter_name = linter.split(" ")[0].replace("-", "_")
 | |
|         rule = f"""rules::{linter_name}::rules::{name}"""
 | |
|         lines.append(
 | |
|             " " * 8 + f"""({variant}, "{code}") => (RuleGroup::Preview, {rule}),\n""",
 | |
|         )
 | |
|         lines.sort()
 | |
|         text += "".join(lines)
 | |
|         text += "\n"
 | |
|         text += fp.read()
 | |
|     with (ROOT_DIR / "crates/ruff_linter/src/codes.rs").open("w") as fp:
 | |
|         fp.write(text)
 | |
| 
 | |
|     _rustfmt(rules_mod)
 | |
| 
 | |
| 
 | |
| def _rustfmt(path: str) -> None:
 | |
|     subprocess.run(["rustfmt", path])
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     parser = argparse.ArgumentParser(
 | |
|         description="Generate boilerplate for a new rule.",
 | |
|         epilog=(
 | |
|             "python scripts/add_rule.py "
 | |
|             "--name PreferListBuiltin --code PIE807 --linter flake8-pie"
 | |
|         ),
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "--name",
 | |
|         type=str,
 | |
|         required=True,
 | |
|         help=(
 | |
|             "The name of the check to generate, in PascalCase "
 | |
|             "(e.g., 'PreferListBuiltin')."
 | |
|         ),
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "--prefix",
 | |
|         type=str,
 | |
|         required=True,
 | |
|         help="Prefix code for the plugin (e.g. 'PIE').",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "--code",
 | |
|         type=str,
 | |
|         required=True,
 | |
|         help="The code of the check to generate (e.g., '807').",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "--linter",
 | |
|         type=str,
 | |
|         required=True,
 | |
|         help="The source with which the check originated (e.g., 'flake8-pie').",
 | |
|     )
 | |
|     args = parser.parse_args()
 | |
| 
 | |
|     main(name=args.name, prefix=args.prefix, code=args.code, linter=args.linter)
 |