ruff/scripts/add_plugin.py
Micha Reiser f72ed255e5
chore: Use LF on all platforms (#3005)
I worked on #2993 and ran into issues that the formatter tests are failing on Windows because `writeln!` emits `\n` as line terminator on all platforms, but `git` on Windows converted the line endings in the snapshots to `\r\n`.

I then tried to replicate the issue on my Windows machine and was surprised that all linter snapshot tests are failing on my machine. I figured out after some time that it is due to my global git config keeping the input line endings rather than converting to `\r\n`. 

Luckily, I've been made aware of #2033 which introduced an "override" for the `assert_yaml_snapshot` macro that normalizes new lines, by splitting the formatted string using the platform-specific newline character. This is a clever approach and gives nice diffs for multiline fixes but makes assumptions about the setup contributors use and requires special care whenever we use line endings inside of tests. 

I recommend that we remove the special new line handling and use `.gitattributes` to enforce the use of `LF` on all platforms [guide](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). This gives us platform agnostic tests without having to worry about line endings in our tests or different git configurations.

## Note

It may be necessary for Windows contributors to run the following command to update the line endings of their files

```bash
git rm --cached -r .
git reset --hard
```
2023-02-20 20:13:37 +00:00

134 lines
3.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""Generate boilerplate for a new Flake8 plugin.
Example usage:
python scripts/add_plugin.py \
flake8-pie \
--url https://pypi.org/project/flake8-pie/
--prefix PIE
"""
import argparse
from _utils import ROOT_DIR, dir_name, get_indent, pascal_case
def main(*, plugin: str, url: str, prefix_code: str) -> None:
"""Generate boilerplate for a new plugin."""
# Create the test fixture folder.
(ROOT_DIR / "crates/ruff/resources/test/fixtures" / dir_name(plugin)).mkdir(
exist_ok=True,
)
# Create the Plugin rules module.
plugin_dir = ROOT_DIR / "crates/ruff/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")
fp.write(
"""#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use insta::assert_yaml_snapshot;
use test_case::test_case;
use crate::registry::Rule;
use crate::test::test_path;
use crate::settings;
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics =test_path(
Path::new("%s").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}
"""
% 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 (ROOT_DIR / "crates/ruff/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 / "crates/ruff/src/registry.rs").read_text()
with (ROOT_DIR / "crates/ruff/src/registry.rs").open("w") as fp:
for line in content.splitlines():
indent = get_indent(line)
if line.strip() == "// ruff":
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == "/// Ruff-specific rules":
fp.write(f"{indent}/// [{plugin}]({url})\n")
fp.write(f'{indent}#[prefix = "{prefix_code}"]\n')
fp.write(f"{indent}{pascal_case(plugin)},")
fp.write("\n")
fp.write(line)
fp.write("\n")
text = ""
with (ROOT_DIR / "crates/ruff/src/codes.rs").open("r") as fp:
while (line := next(fp)).strip() != "// ruff":
text += line
text += " "*8 + f"// {plugin}\n"
text += line
text += fp.read()
with (ROOT_DIR / "crates/ruff/src/codes.rs").open("w") as fp:
fp.write(text)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
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/"
),
)
parser.add_argument(
"plugin",
type=str,
help="The name of the plugin to generate.",
)
parser.add_argument(
"--url",
required=True,
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, prefix_code=args.prefix)