mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 22:54:42 +00:00
Update rule-generation scripts
to match latest conventions (#1758)
Resolves #1755.
This commit is contained in:
parent
b85105d2ec
commit
1a09fff991
3 changed files with 205 additions and 159 deletions
|
@ -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)
|
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Generate boilerplate for a new plugin.
|
"""Generate boilerplate for a new Flake8 plugin.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
|
@ -31,9 +31,9 @@ def main(*, plugin: str, url: str) -> None:
|
||||||
|
|
||||||
# Create the Rust module.
|
# Create the Rust module.
|
||||||
os.makedirs(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}"), exist_ok=True)
|
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"):
|
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/rules.rs"), "w+") as fp:
|
||||||
pass
|
fp.write("use crate::checkers::ast::Checker;\n")
|
||||||
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)}/mod.rs"), "w+") as fp:
|
||||||
fp.write("pub mod rules;\n")
|
fp.write("pub mod rules;\n")
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
fp.write(
|
fp.write(
|
||||||
|
@ -49,13 +49,13 @@ mod tests {
|
||||||
use crate::linter::test_path;
|
use crate::linter::test_path;
|
||||||
use crate::settings;
|
use crate::settings;
|
||||||
|
|
||||||
fn rules(check_code: RuleCode, path: &Path) -> Result<()> {
|
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||||
let diagnostics =test_path(
|
let diagnostics =test_path(
|
||||||
Path::new("./resources/test/fixtures/%s")
|
Path::new("./resources/test/fixtures/%s")
|
||||||
.join(path)
|
.join(path)
|
||||||
.as_path(),
|
.as_path(),
|
||||||
&settings::Settings::for_rule(check_code),
|
&settings::Settings::for_rule(rule_code),
|
||||||
)?;
|
)?;
|
||||||
insta::assert_yaml_snapshot!(snapshot, diagnostics);
|
insta::assert_yaml_snapshot!(snapshot, diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -67,10 +67,10 @@ mod tests {
|
||||||
|
|
||||||
# Add the plugin to `lib.rs`.
|
# Add the plugin to `lib.rs`.
|
||||||
with open(os.path.join(ROOT_DIR, "src/lib.rs"), "a") as fp:
|
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`.
|
# 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()
|
content = fp.read()
|
||||||
|
|
||||||
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
|
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(f"{indent}{pascal_case(plugin)},")
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
|
||||||
elif line.strip() == 'CheckCategory::Ruff => "Ruff-specific rules",':
|
elif line.strip() == 'RuleOrigin::Ruff => "Ruff-specific rules",':
|
||||||
indent = line.split('CheckCategory::Ruff => "Ruff-specific rules",')[0]
|
indent = line.split('RuleOrigin::Ruff => "Ruff-specific rules",')[0]
|
||||||
fp.write(f'{indent}CheckCategory::{pascal_case(plugin)} => "{plugin}",')
|
fp.write(f'{indent}RuleOrigin::{pascal_case(plugin)} => "{plugin}",')
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
|
||||||
elif line.strip() == "CheckCategory::Ruff => vec![RuleCodePrefix::RUF],":
|
elif line.strip() == "RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],":
|
||||||
indent = line.split("CheckCategory::Ruff => vec![RuleCodePrefix::RUF],")[0]
|
indent = line.split("RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],")[0]
|
||||||
fp.write(
|
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} todo!("Fill-in prefix after generating codes")\n'
|
||||||
f"{indent}],"
|
f"{indent}],"
|
||||||
)
|
)
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
|
||||||
elif line.strip() == "CheckCategory::Ruff => None,":
|
elif line.strip() == "RuleOrigin::Ruff => None,":
|
||||||
indent = line.split("CheckCategory::Ruff => None,")[0]
|
indent = line.split("RuleOrigin::Ruff => None,")[0]
|
||||||
fp.write(f"{indent}CheckCategory::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),')
|
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("\n")
|
||||||
|
|
||||||
fp.write(line)
|
fp.write(line)
|
||||||
|
@ -110,7 +124,7 @@ mod tests {
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Generate boilerplate for a new plugin.",
|
description="Generate boilerplate for a new Flake8 plugin.",
|
||||||
epilog=(
|
epilog=(
|
||||||
"Example usage: python scripts/add_plugin.py flake8-pie "
|
"Example usage: python scripts/add_plugin.py flake8-pie "
|
||||||
"--url https://pypi.org/project/flake8-pie/0.16.0/"
|
"--url https://pypi.org/project/flake8-pie/0.16.0/"
|
||||||
|
@ -118,7 +132,6 @@ if __name__ == "__main__":
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"plugin",
|
"plugin",
|
||||||
required=True,
|
|
||||||
type=str,
|
type=str,
|
||||||
help="The name of the plugin to generate.",
|
help="The name of the plugin to generate.",
|
||||||
)
|
)
|
||||||
|
|
172
scripts/add_rule.py
Normal file
172
scripts/add_rule.py
Normal file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue