mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:10:09 +00:00
feat: update scripts to new rules structure (#2078)
- optional `prefix` argument for `add_plugin.py` - rules directory instead of `rules.rs` - pathlib syntax - fix test case where code was added instead of name Example: ``` python scripts/add_plugin.py --url https://pypi.org/project/example/1.0.0/ example --prefix EXA python scripts/add_rule.py --name SecondRule --code EXA002 --linter example python scripts/add_rule.py --name FirstRule --code EXA001 --linter example python scripts/add_rule.py --name ThirdRule --code EXA003 --linter example ``` Note that it breaks compatibility with 'old style' plugins (generation works fine, but namespaces need to be changed): ``` python scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint ```
This commit is contained in:
parent
325faa8e18
commit
28f05aa6e7
3 changed files with 80 additions and 57 deletions
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
|
@ -96,7 +96,9 @@ jobs:
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
|
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
|
||||||
- run: cargo check
|
- 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
|
- run: cargo check
|
||||||
|
|
||||||
# TODO(charlie): Re-enable the `wasm-pack` tests.
|
# TODO(charlie): Re-enable the `wasm-pack` tests.
|
||||||
|
|
|
@ -6,6 +6,7 @@ Example usage:
|
||||||
python scripts/add_plugin.py \
|
python scripts/add_plugin.py \
|
||||||
flake8-pie \
|
flake8-pie \
|
||||||
--url https://pypi.org/project/flake8-pie/0.16.0/
|
--url https://pypi.org/project/flake8-pie/0.16.0/
|
||||||
|
--prefix PIE
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
@ -14,19 +15,18 @@ import os
|
||||||
from _utils import ROOT_DIR, dir_name, get_indent, pascal_case
|
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.
|
# Create the test fixture folder.
|
||||||
os.makedirs(
|
os.makedirs(
|
||||||
ROOT_DIR / "resources/test/fixtures" / dir_name(plugin),
|
ROOT_DIR / "resources/test/fixtures" / dir_name(plugin),
|
||||||
exist_ok=True,
|
exist_ok=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the Rust module.
|
# Create the Plugin rules module.
|
||||||
rust_module = ROOT_DIR / "src/rules" / dir_name(plugin)
|
plugin_dir = ROOT_DIR / "src/rules" / dir_name(plugin)
|
||||||
os.makedirs(rust_module, exist_ok=True)
|
plugin_dir.mkdir(exist_ok=True)
|
||||||
with open(rust_module / "rules.rs", "w+") as fp:
|
|
||||||
fp.write("use crate::checkers::ast::Checker;\n")
|
with (plugin_dir / "mod.rs").open("w+") as fp:
|
||||||
with open(rust_module / "mod.rs", "w+") as fp:
|
|
||||||
fp.write(f"//! Rules from [{plugin}]({url}).\n")
|
fp.write(f"//! Rules from [{plugin}]({url}).\n")
|
||||||
fp.write("pub(crate) mod rules;\n")
|
fp.write("pub(crate) mod rules;\n")
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
@ -59,14 +59,24 @@ mod tests {
|
||||||
% dir_name(plugin)
|
% 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`.
|
# 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)};")
|
fp.write(f"pub mod {dir_name(plugin)};")
|
||||||
|
|
||||||
# Add the relevant sections to `src/registry.rs`.
|
# Add the relevant sections to `src/registry.rs`.
|
||||||
content = (ROOT_DIR / "src/registry.rs").read_text()
|
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():
|
for line in content.splitlines():
|
||||||
indent = get_indent(line)
|
indent = get_indent(line)
|
||||||
|
|
||||||
|
@ -75,33 +85,19 @@ mod tests {
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
|
||||||
elif line.strip() == '#[prefix = "RUF"]':
|
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(f"{indent}{pascal_case(plugin)},")
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
|
||||||
elif line.strip() == "Linter::Ruff => Prefixes::Single(RuleSelector::RUF),":
|
elif line.strip() == "Linter::Ruff => Prefixes::Single(RuleSelector::RUF),":
|
||||||
prefix = 'todo!("Fill-in prefix after generating codes")'
|
|
||||||
fp.write(
|
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("\n")
|
||||||
|
|
||||||
fp.write(line)
|
fp.write(line)
|
||||||
fp.write("\n")
|
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__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
@ -122,6 +118,13 @@ if __name__ == "__main__":
|
||||||
type=str,
|
type=str,
|
||||||
help="The URL of the latest release in PyPI.",
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
main(plugin=args.plugin, url=args.url)
|
main(plugin=args.plugin, url=args.url, prefix_code=args.prefix)
|
||||||
|
|
|
@ -21,47 +21,58 @@ def snake_case(name: str) -> str:
|
||||||
|
|
||||||
def main(*, name: str, code: str, linter: str) -> None:
|
def main(*, name: str, code: str, linter: str) -> None:
|
||||||
# Create a test fixture.
|
# Create a test fixture.
|
||||||
with open(
|
with (ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py").open("a"):
|
||||||
ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py",
|
|
||||||
"a",
|
|
||||||
):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
plugin_module = ROOT_DIR / "src/rules" / dir_name(linter)
|
||||||
|
rule_name_snake = snake_case(name)
|
||||||
|
|
||||||
# Add the relevant `#testcase` macro.
|
# 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()
|
content = mod_rs.read_text()
|
||||||
|
|
||||||
with open(mod_rs, "w") as fp:
|
with mod_rs.open("w") as fp:
|
||||||
for line in content.splitlines():
|
for line in content.splitlines():
|
||||||
if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
|
if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
|
||||||
indent = get_indent(line)
|
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("\n")
|
||||||
|
|
||||||
fp.write(line)
|
fp.write(line)
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
|
||||||
|
# Add the exports
|
||||||
|
rules_dir = plugin_module / "rules"
|
||||||
|
rules_mod = rules_dir / "mod.rs"
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
# Add the relevant rule function.
|
# Add the relevant rule function.
|
||||||
with open(ROOT_DIR / "src/rules" / dir_name(linter) / (snake_case(name) + ".rs"), "w") as fp:
|
with (rules_dir / f"{rule_name_snake}.rs").open("w") as fp:
|
||||||
fp.write(
|
fp.write(
|
||||||
f"""
|
"""use ruff_macros::derive_message_formats;
|
||||||
/// {code}
|
|
||||||
pub fn {snake_case(name)}(checker: &mut Checker) {{}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
fp.write("\n")
|
|
||||||
|
|
||||||
# Add the relevant struct to `src/violations.rs`.
|
use crate::define_violation;
|
||||||
content = (ROOT_DIR / "src/violations.rs").read_text()
|
use crate::violation::Violation;
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
|
define_violation!(
|
||||||
for line in content.splitlines():
|
|
||||||
fp.write(line)
|
|
||||||
fp.write("\n")
|
|
||||||
|
|
||||||
if line.startswith(f"// {linter}"):
|
|
||||||
fp.write(
|
|
||||||
"""define_violation!(
|
|
||||||
pub struct %s;
|
pub struct %s;
|
||||||
);
|
);
|
||||||
impl Violation for %s {
|
impl Violation for %s {
|
||||||
|
@ -75,13 +86,20 @@ impl Violation for %s {
|
||||||
% (name, name)
|
% (name, name)
|
||||||
)
|
)
|
||||||
fp.write("\n")
|
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`.
|
# Add the relevant code-to-violation pair to `src/registry.rs`.
|
||||||
content = (ROOT_DIR / "src/registry.rs").read_text()
|
content = (ROOT_DIR / "src/registry.rs").read_text()
|
||||||
|
|
||||||
seen_macro = False
|
seen_macro = False
|
||||||
has_written = 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():
|
for line in content.splitlines():
|
||||||
fp.write(line)
|
fp.write(line)
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
|
@ -98,7 +116,7 @@ impl Violation for %s {
|
||||||
|
|
||||||
if line.strip() == f"// {linter}":
|
if line.strip() == f"// {linter}":
|
||||||
indent = get_indent(line)
|
indent = get_indent(line)
|
||||||
fp.write(f"{indent}{code} => violations::{name},")
|
fp.write(f"{indent}{code} => rules::{linter}::rules::{name},")
|
||||||
fp.write("\n")
|
fp.write("\n")
|
||||||
has_written = True
|
has_written = True
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue