refactor: Change RuleNamespace::prefixes to common_prefix

Previously Linter::parse_code("E401") returned
(Linter::Pycodestyle, "401") ... after this commit it returns
(Linter::Pycodestyle, "E401") instead, which is important
for the future implementation of the many-to-many mapping.
(The second value of the tuple isn't used currently.)
This commit is contained in:
Martin Fischer 2023-01-23 06:43:52 +01:00 committed by Charlie Marsh
parent 3ee6a90905
commit b532fce792
3 changed files with 65 additions and 27 deletions

View file

@ -1,6 +1,7 @@
//! Generate a Markdown-compatible table of supported lint rules.
use anyhow::Result;
use itertools::Itertools;
use ruff::registry::{Linter, LinterCategory, Rule, RuleNamespace};
use strum::IntoEnumIterator;
@ -47,7 +48,15 @@ pub fn main(args: &Args) -> Result<()> {
let mut table_out = String::new();
let mut toc_out = String::new();
for linter in Linter::iter() {
let codes_csv: String = linter.prefixes().join(", ");
let codes_csv: String = match linter.common_prefix() {
"" => linter
.categories()
.unwrap()
.iter()
.map(|LinterCategory(prefix, ..)| prefix)
.join(", "),
prefix => prefix.to_string(),
};
table_out.push_str(&format!("### {} ({codes_csv})", linter.name()));
table_out.push('\n');
table_out.push('\n');

View file

@ -15,13 +15,15 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
let mut parsed = Vec::new();
let mut prefix_match_arms = quote!();
let mut common_prefix_match_arms = quote!();
let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules",);
let mut url_match_arms = quote!(Self::Ruff => None,);
let mut into_iter_match_arms = quote!();
let mut all_prefixes = HashSet::new();
for variant in variants {
let mut first_chars = HashSet::new();
let prefixes: Result<Vec<_>, _> = variant
.attrs
.iter()
@ -33,7 +35,9 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
let str = lit.value();
match str.chars().next() {
None => return Err(Error::new(lit.span(), "expected prefix string to be non-empty")),
Some(_) => {},
Some(c) => if !first_chars.insert(c) {
return Err(Error::new(lit.span(), format!("this variant already has another prefix starting with the character '{c}'")))
}
}
if !all_prefixes.insert(str.clone()) {
return Err(Error::new(lit.span(), "prefix has already been defined before"));
@ -63,31 +67,43 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
}
for lit in &prefixes {
parsed.push((lit.clone(), variant_ident.clone()));
parsed.push((
lit.clone(),
variant_ident.clone(),
match prefixes.len() {
1 => ParseStrategy::SinglePrefix,
_ => ParseStrategy::MultiplePrefixes,
},
));
}
prefix_match_arms.extend(quote! {
Self::#variant_ident => &[#(#prefixes),*],
});
if let [prefix] = &prefixes[..] {
common_prefix_match_arms.extend(quote! { Self::#variant_ident => #prefix, });
let prefix_ident = Ident::new(prefix, Span::call_site());
into_iter_match_arms.extend(quote! {
#ident::#variant_ident => RuleCodePrefix::#prefix_ident.into_iter(),
});
} else {
// There is more than one prefix. We already previously asserted
// that prefixes of the same variant don't start with the same character
// so the common prefix for this variant is the empty string.
common_prefix_match_arms.extend(quote! { Self::#variant_ident => "", });
}
}
parsed.sort_by_key(|(prefix, _)| Reverse(prefix.len()));
parsed.sort_by_key(|(prefix, ..)| Reverse(prefix.len()));
let mut if_statements = quote!();
let mut into_iter_match_arms = quote!();
for (prefix, field) in parsed {
for (prefix, field, strategy) in parsed {
let ret_str = match strategy {
ParseStrategy::SinglePrefix => quote!(rest),
ParseStrategy::MultiplePrefixes => quote!(code),
};
if_statements.extend(quote! {if let Some(rest) = code.strip_prefix(#prefix) {
return Some((#ident::#field, rest));
return Some((#ident::#field, #ret_str));
}});
let prefix_ident = Ident::new(&prefix, Span::call_site());
if field != "Pycodestyle" {
into_iter_match_arms.extend(quote! {
#ident::#field => RuleCodePrefix::#prefix_ident.into_iter(),
});
}
}
into_iter_match_arms.extend(quote! {
@ -104,9 +120,8 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
None
}
fn prefixes(&self) -> &'static [&'static str] {
match self { #prefix_match_arms }
fn common_prefix(&self) -> &'static str {
match self { #common_prefix_match_arms }
}
fn name(&self) -> &'static str {
@ -152,3 +167,8 @@ fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> {
fn parse_markdown_link(link: &str) -> Option<(&str, &str)> {
link.strip_prefix('[')?.strip_suffix(')')?.split_once("](")
}
enum ParseStrategy {
SinglePrefix,
MultiplePrefixes,
}

View file

@ -615,9 +615,16 @@ pub enum Linter {
}
pub trait RuleNamespace: Sized {
fn parse_code(code: &str) -> Option<(Self, &str)>;
/// Returns the prefix that every single code that ruff uses to identify
/// rules from this linter starts with. In the case that multiple
/// `#[prefix]`es are configured for the variant in the `Linter` enum
/// definition this is the empty string.
fn common_prefix(&self) -> &'static str;
fn prefixes(&self) -> &'static [&'static str];
/// Attempts to parse the given rule code. If the prefix is recognized
/// returns the respective variant along with the code with the common
/// prefix stripped.
fn parse_code(code: &str) -> Option<(Self, &str)>;
fn name(&self) -> &'static str;
@ -766,10 +773,12 @@ mod tests {
}
#[test]
fn test_linter_prefixes() {
fn test_linter_parse_code() {
for rule in Rule::iter() {
Linter::parse_code(rule.code())
.unwrap_or_else(|| panic!("couldn't parse {:?}", rule.code()));
let code = rule.code();
let (linter, rest) =
Linter::parse_code(code).unwrap_or_else(|| panic!("couldn't parse {:?}", code));
assert_eq!(code, format!("{}{rest}", linter.common_prefix()));
}
}
}