mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 22:55:08 +00:00

Update to [Rust 1.74](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) and use the new clippy lints table. The update itself introduced a new clippy lint about superfluous hashes in raw strings, which got removed. I moved our lint config from `rustflags` to the newly stabilized [workspace.lints](https://doc.rust-lang.org/stable/cargo/reference/workspaces.html#the-lints-table). One consequence is that we have to `unsafe_code = "warn"` instead of "forbid" because the latter now actually bans unsafe code: ``` error[E0453]: allow(unsafe_code) incompatible with previous forbid --> crates/ruff_source_file/src/newlines.rs:62:17 | 62 | #[allow(unsafe_code)] | ^^^^^^^^^^^ overruled by previous forbid | = note: `forbid` lint level was set on command line ``` --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
170 lines
5.7 KiB
Rust
170 lines
5.7 KiB
Rust
use std::cmp::Reverse;
|
|
use std::collections::HashSet;
|
|
|
|
use quote::quote;
|
|
use syn::spanned::Spanned;
|
|
use syn::{Attribute, Data, DataEnum, DeriveInput, Error, ExprLit, Lit, Meta, MetaNameValue};
|
|
|
|
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|
let DeriveInput {
|
|
ident,
|
|
data: Data::Enum(DataEnum { variants, .. }),
|
|
..
|
|
} = input
|
|
else {
|
|
return Err(Error::new(
|
|
input.ident.span(),
|
|
"only named fields are supported",
|
|
));
|
|
};
|
|
|
|
let mut parsed = Vec::new();
|
|
|
|
let mut common_prefix_match_arms = quote!();
|
|
let mut name_match_arms =
|
|
quote!(Self::Ruff => "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", );
|
|
let mut url_match_arms = quote!(Self::Ruff => None, Self::Numpy => None, );
|
|
|
|
let mut all_prefixes = HashSet::new();
|
|
|
|
for variant in variants {
|
|
let mut first_chars = HashSet::new();
|
|
let prefixes: Result<Vec<_>, _> = variant
|
|
.attrs
|
|
.iter()
|
|
.filter(|attr| attr.path().is_ident("prefix"))
|
|
.map(|attr| {
|
|
let Meta::NameValue(MetaNameValue{value: syn::Expr::Lit (ExprLit { lit: Lit::Str(lit), ..}), ..}) = &attr.meta else {
|
|
return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#));
|
|
};
|
|
let str = lit.value();
|
|
match str.chars().next() {
|
|
None => return Err(Error::new(lit.span(), "expected prefix string to be non-empty")),
|
|
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"));
|
|
}
|
|
Ok(str)
|
|
})
|
|
.collect();
|
|
let prefixes = prefixes?;
|
|
|
|
if prefixes.is_empty() {
|
|
return Err(Error::new(
|
|
variant.span(),
|
|
r#"Missing #[prefix = "..."] attribute"#,
|
|
));
|
|
}
|
|
|
|
let Some(doc_attr) = variant
|
|
.attrs
|
|
.iter()
|
|
.find(|attr| attr.path().is_ident("doc"))
|
|
else {
|
|
return Err(Error::new(variant.span(), "expected a doc comment"));
|
|
};
|
|
|
|
let variant_ident = variant.ident;
|
|
|
|
if variant_ident != "Ruff" && variant_ident != "Numpy" {
|
|
let (name, url) = parse_doc_attr(doc_attr)?;
|
|
name_match_arms.extend(quote! {Self::#variant_ident => #name,});
|
|
url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),});
|
|
}
|
|
|
|
for lit in &prefixes {
|
|
parsed.push((
|
|
lit.clone(),
|
|
variant_ident.clone(),
|
|
match prefixes.len() {
|
|
1 => ParseStrategy::SinglePrefix,
|
|
_ => ParseStrategy::MultiplePrefixes,
|
|
},
|
|
));
|
|
}
|
|
|
|
if let [prefix] = &prefixes[..] {
|
|
common_prefix_match_arms.extend(quote! { Self::#variant_ident => #prefix, });
|
|
} 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()));
|
|
|
|
let mut if_statements = quote!();
|
|
|
|
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, #ret_str));
|
|
}});
|
|
}
|
|
|
|
Ok(quote! {
|
|
#[automatically_derived]
|
|
impl crate::registry::RuleNamespace for #ident {
|
|
fn parse_code(code: &str) -> Option<(Self, &str)> {
|
|
#if_statements
|
|
None
|
|
}
|
|
|
|
fn common_prefix(&self) -> &'static str {
|
|
match self { #common_prefix_match_arms }
|
|
}
|
|
|
|
fn name(&self) -> &'static str {
|
|
match self { #name_match_arms }
|
|
}
|
|
|
|
fn url(&self) -> Option<&'static str> {
|
|
match self { #url_match_arms }
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Parses an attribute in the form of `#[doc = " [name](https://example.com/)"]`
|
|
/// into a tuple of link label and URL.
|
|
fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> {
|
|
let Meta::NameValue(MetaNameValue {
|
|
value:
|
|
syn::Expr::Lit(ExprLit {
|
|
lit: Lit::Str(doc_lit),
|
|
..
|
|
}),
|
|
..
|
|
}) = &doc_attr.meta
|
|
else {
|
|
return Err(Error::new(
|
|
doc_attr.span(),
|
|
r#"expected doc attribute to be in the form of #[doc = "..."]"#,
|
|
));
|
|
};
|
|
parse_markdown_link(doc_lit.value().trim())
|
|
.map(|(name, url)| (name.to_string(), url.to_string()))
|
|
.ok_or_else(|| {
|
|
Error::new(
|
|
doc_lit.span(),
|
|
"expected doc comment to be in the form of `/// [name](https://example.com/)`",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn parse_markdown_link(link: &str) -> Option<(&str, &str)> {
|
|
link.strip_prefix('[')?.strip_suffix(')')?.split_once("](")
|
|
}
|
|
|
|
enum ParseStrategy {
|
|
SinglePrefix,
|
|
MultiplePrefixes,
|
|
}
|