diff --git a/build.rs b/build.rs deleted file mode 100644 index ecafa65218..0000000000 --- a/build.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::fs; -use std::io::{BufRead, BufReader, BufWriter, Write}; -use std::path::{Path, PathBuf}; - -fn main() { - let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); - generate_linter_name_and_url(&out_dir); -} - -const RULES_SUBMODULE_DOC_PREFIX: &str = "//! Rules from "; - -/// The `src/rules/*/mod.rs` files are expected to have a first line such as the -/// following: -/// -/// //! Rules from [Pyflakes](https://pypi.org/project/pyflakes/). -/// -/// This function extracts the link label and url from these comments and -/// generates the `name` and `url` functions for the `Linter` enum -/// accordingly, so that they can be used by `ruff_dev::generate_rules_table`. -fn generate_linter_name_and_url(out_dir: &Path) { - println!("cargo:rerun-if-changed=src/rules/"); - - let mut name_match_arms: String = r#"Linter::Ruff => "Ruff-specific rules","#.into(); - let mut url_match_arms: String = r#"Linter::Ruff => None,"#.into(); - - for file in fs::read_dir("src/rules/") - .unwrap() - .flatten() - .filter(|f| f.file_type().unwrap().is_dir() && f.file_name() != "ruff") - { - let mod_rs_path = file.path().join("mod.rs"); - let mod_rs_path = mod_rs_path.to_str().unwrap(); - let first_line = BufReader::new(fs::File::open(mod_rs_path).unwrap()) - .lines() - .next() - .unwrap() - .unwrap(); - - let Some(comment) = first_line.strip_prefix(RULES_SUBMODULE_DOC_PREFIX) else { - panic!("expected first line in {mod_rs_path} to start with `{RULES_SUBMODULE_DOC_PREFIX}`") - }; - let md_link = comment.trim_end_matches('.'); - - let (name, url) = md_link - .strip_prefix('[') - .unwrap() - .strip_suffix(')') - .unwrap() - .split_once("](") - .unwrap(); - - let dirname = file.file_name(); - let dirname = dirname.to_str().unwrap(); - - let variant_name = dirname - .split('_') - .map(|part| match part { - "errmsg" => "ErrMsg".to_string(), - "mccabe" => "McCabe".to_string(), - "pep8" => "PEP8".to_string(), - _ => format!("{}{}", part[..1].to_uppercase(), &part[1..]), - }) - .collect::(); - - name_match_arms.push_str(&format!(r#"Linter::{variant_name} => "{name}","#)); - url_match_arms.push_str(&format!(r#"Linter::{variant_name} => Some("{url}"),"#)); - } - - write!( - BufWriter::new(fs::File::create(out_dir.join("linter.rs")).unwrap()), - " - impl Linter {{ - pub fn name(&self) -> &'static str {{ - match self {{ {name_match_arms} }} - }} - - pub fn url(&self) -> Option<&'static str> {{ - match self {{ {url_match_arms} }} - }} - }} - " - ) - .unwrap(); -} diff --git a/ruff_macros/src/rule_namespace.rs b/ruff_macros/src/rule_namespace.rs index 96ec6d4637..3e4e07ac26 100644 --- a/ruff_macros/src/rule_namespace.rs +++ b/ruff_macros/src/rule_namespace.rs @@ -13,6 +13,8 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result let mut parsed = Vec::new(); let mut prefix_match_arms = quote!(); + let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules",); + let mut url_match_arms = quote!(Self::Ruff => None,); for variant in variants { let prefix_attrs: Vec<_> = variant @@ -28,18 +30,34 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result )); } + let Some(doc_attr) = variant.attrs.iter().find(|a| a.path.is_ident("doc")) else { + return Err(Error::new(variant.span(), r#"expected a doc comment"#)) + }; + + let variant_ident = variant.ident; + + if variant_ident != "Ruff" { + let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(doc_lit), ..})) = doc_attr.parse_meta() else { + return Err(Error::new(doc_attr.span(), r#"expected doc attribute to be in the form of [#doc = "..."]"#)) + }; + let doc_lit = doc_lit.value(); + let Some((name, url)) = parse_markdown_link(doc_lit.trim()) else { + return Err(Error::new(doc_attr.span(), r#"expected doc comment to be in the form of `/// [name](https://example.com/)`"#)) + }; + name_match_arms.extend(quote! {Self::#variant_ident => #name,}); + url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),}); + } + let mut prefix_literals = Vec::new(); for attr in prefix_attrs { let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(lit), ..})) = attr.parse_meta() else { return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#)) }; - parsed.push((lit.clone(), variant.ident.clone())); + parsed.push((lit.clone(), variant_ident.clone())); prefix_literals.push(lit); } - let variant_ident = variant.ident; - prefix_match_arms.extend(quote! { Self::#variant_ident => &[#(#prefix_literals),*], }); @@ -82,6 +100,14 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result fn prefixes(&self) -> &'static [&'static str] { match self { #prefix_match_arms } } + + fn name(&self) -> &'static str { + match self { #name_match_arms } + } + + fn url(&self) -> Option<&'static str> { + match self { #url_match_arms } + } } impl IntoIterator for &#ident { @@ -98,3 +124,7 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result } }) } + +fn parse_markdown_link(link: &str) -> Option<(&str, &str)> { + link.strip_prefix('[')?.strip_suffix(')')?.split_once("](") +} diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index a26a4dd99d..19b247bdc7 100755 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -84,7 +84,8 @@ mod tests { fp.write(f"{indent}// {plugin}") fp.write("\n") - elif line.strip() == '#[prefix = "RUF"]': + elif line.strip() == '/// Ruff-specific rules': + fp.write(f"/// [{plugin}]({url})\n") fp.write(f'{indent}#[prefix = "{prefix_code}"]\n') fp.write(f"{indent}{pascal_case(plugin)},") fp.write("\n") diff --git a/src/registry.rs b/src/registry.rs index 4593cd10af..68f40ffadd 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -469,83 +469,122 @@ ruff_macros::define_rule_mapping!( #[derive(EnumIter, Debug, PartialEq, Eq, RuleNamespace)] pub enum Linter { + /// [Pyflakes](https://pypi.org/project/pyflakes/) #[prefix = "F"] Pyflakes, + /// [pycodestyle](https://pypi.org/project/pycodestyle/) #[prefix = "E"] #[prefix = "W"] Pycodestyle, + /// [mccabe](https://pypi.org/project/mccabe/) #[prefix = "C90"] McCabe, + /// [isort](https://pypi.org/project/isort/) #[prefix = "I"] Isort, + /// [pydocstyle](https://pypi.org/project/pydocstyle/) #[prefix = "D"] Pydocstyle, + /// [pyupgrade](https://pypi.org/project/pyupgrade/) #[prefix = "UP"] Pyupgrade, + /// [pep8-naming](https://pypi.org/project/pep8-naming/) #[prefix = "N"] PEP8Naming, + /// [flake8-2020](https://pypi.org/project/flake8-2020/) #[prefix = "YTT"] Flake82020, + /// [flake8-annotations](https://pypi.org/project/flake8-annotations/) #[prefix = "ANN"] Flake8Annotations, + /// [flake8-bandit](https://pypi.org/project/flake8-bandit/) #[prefix = "S"] Flake8Bandit, + /// [flake8-blind-except](https://pypi.org/project/flake8-blind-except/) #[prefix = "BLE"] Flake8BlindExcept, + /// [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/) #[prefix = "FBT"] Flake8BooleanTrap, + /// [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) #[prefix = "B"] Flake8Bugbear, + /// [flake8-builtins](https://pypi.org/project/flake8-builtins/) #[prefix = "A"] Flake8Builtins, + /// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) #[prefix = "C4"] Flake8Comprehensions, + /// [flake8-debugger](https://pypi.org/project/flake8-debugger/) #[prefix = "T10"] Flake8Debugger, + /// [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) #[prefix = "EM"] Flake8ErrMsg, + /// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/) #[prefix = "ISC"] Flake8ImplicitStrConcat, + /// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions) #[prefix = "ICN"] Flake8ImportConventions, + /// [flake8-print](https://pypi.org/project/flake8-print/) #[prefix = "T20"] Flake8Print, + /// [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) #[prefix = "PT"] Flake8PytestStyle, + /// [flake8-quotes](https://pypi.org/project/flake8-quotes/) #[prefix = "Q"] Flake8Quotes, + /// [flake8-return](https://pypi.org/project/flake8-return/) #[prefix = "RET"] Flake8Return, + /// [flake8-simplify](https://pypi.org/project/flake8-simplify/) #[prefix = "SIM"] Flake8Simplify, + /// [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) #[prefix = "TID"] Flake8TidyImports, + /// [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/) #[prefix = "ARG"] Flake8UnusedArguments, + /// [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) #[prefix = "DTZ"] Flake8Datetimez, + /// [eradicate](https://pypi.org/project/eradicate/) #[prefix = "ERA"] Eradicate, + /// [pandas-vet](https://pypi.org/project/pandas-vet/) #[prefix = "PD"] PandasVet, + /// [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) #[prefix = "PGH"] PygrepHooks, + /// [Pylint](https://pypi.org/project/pylint/) #[prefix = "PL"] Pylint, + /// [flake8-pie](https://pypi.org/project/flake8-pie/) #[prefix = "PIE"] Flake8Pie, + /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, + /// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) #[prefix = "INP"] Flake8NoPep420, + /// [flake8-executable](https://pypi.org/project/flake8-executable/) #[prefix = "EXE"] Flake8Executable, + /// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/) #[prefix = "TYP"] Flake8TypeChecking, + /// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) #[prefix = "TRY"] Tryceratops, + /// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/) #[prefix = "PTH"] Flake8UsePathlib, + /// Ruff-specific rules #[prefix = "RUF"] Ruff, } @@ -554,9 +593,11 @@ pub trait RuleNamespace: Sized { fn parse_code(code: &str) -> Option<(Self, &str)>; fn prefixes(&self) -> &'static [&'static str]; -} -include!(concat!(env!("OUT_DIR"), "/linter.rs")); + fn name(&self) -> &'static str; + + fn url(&self) -> Option<&'static str>; +} /// The prefix, name and selector for an upstream linter category. pub struct LinterCategory(pub &'static str, pub &'static str, pub RuleSelector);