Skip to content

Commit

Permalink
Convert define_rule_mapping! to a procedural macro
Browse files Browse the repository at this point in the history
define_rule_mapping! was previously implemented as a declarative macro,
which was however partially relying on an origin_by_code! proc macro
because declarative macros cannot match on substrings of identifiers.

Currently all define_rule_mapping! lines look like the following:

    TID251 => violations::BannedApi,
    TID252 => violations::BannedRelativeImport,

We want to break up violations.rs, moving the violation definitions to
the respective rule modules. To do this we want to change the previous
lines to:

    TID251 => rules::flake8_tidy_imports::banned_api::BannedApi,
    TID252 => rules::flake8_tidy_imports::relative_imports::RelativeImport,

This however doesn't work because the define_rule_mapping! macro is
currently defined as:

    ($($code:ident => $mod:ident::$name:ident,)+) => { ... }

That is it only supported $module::$name but not longer paths with
multiple modules. While we could define `=> $path:path`[1] then we
could no longer access the last path segment, which we need because
we use it for the DiagnosticKind variant names. And
`$path:path::$last:ident` doesn't work either because it would be
ambiguous (Rust wouldn't know where the path ends ... so path fragments
have to be followed by some punctuation/keyword that may not be part of
paths). And we also cannot just introduce a procedural macro like
path_basename!(...) because the following is not valid Rust code:

    enum Foo { foo!(...), }

(macros cannot be called in the place where you define variants.)

So we have to convert define_rule_mapping! into a proc macro in order to
support paths of arbitrary length and this commit implements that.

[1]: https://doc.rust-lang.org/reference/macros-by-example.html#metavariables
  • Loading branch information
not-my-profile authored and charliermarsh committed Jan 15, 2023
1 parent e3cc918 commit 81996f1
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 120 deletions.
133 changes: 133 additions & 0 deletions ruff_macros/src/define_rule_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use proc_macro2::Span;
use quote::quote;
use syn::parse::Parse;
use syn::{Ident, Path, Token};

pub fn define_rule_mapping(mapping: Mapping) -> proc_macro2::TokenStream {
let mut rulecode_variants = quote!();
let mut diagkind_variants = quote!();
let mut rulecode_kind_match_arms = quote!();
let mut rulecode_origin_match_arms = quote!();
let mut diagkind_code_match_arms = quote!();
let mut diagkind_body_match_arms = quote!();
let mut diagkind_fixable_match_arms = quote!();
let mut diagkind_commit_match_arms = quote!();
let mut from_impls_for_diagkind = quote!();

for (code, path, name) in mapping.entries {
rulecode_variants.extend(quote! {#code,});
diagkind_variants.extend(quote! {#name(#path),});
rulecode_kind_match_arms.extend(
quote! {RuleCode::#code => DiagnosticKind::#name(<#path as Violation>::placeholder()),},
);
let origin = get_origin(&code);
rulecode_origin_match_arms.extend(quote! {RuleCode::#code => RuleOrigin::#origin,});
diagkind_code_match_arms.extend(quote! {DiagnosticKind::#name(..) => &RuleCode::#code, });
diagkind_body_match_arms
.extend(quote! {DiagnosticKind::#name(x) => Violation::message(x), });
diagkind_fixable_match_arms
.extend(quote! {DiagnosticKind::#name(x) => x.autofix_title_formatter().is_some(),});
diagkind_commit_match_arms.extend(
quote! {DiagnosticKind::#name(x) => x.autofix_title_formatter().map(|f| f(x)), },
);
from_impls_for_diagkind.extend(quote! {
impl From<#path> for DiagnosticKind {
fn from(x: #path) -> Self {
DiagnosticKind::#name(x)
}
}
});
}

quote! {
#[derive(
AsRefStr,
RuleCodePrefix,
EnumIter,
EnumString,
Debug,
Display,
PartialEq,
Eq,
Clone,
Serialize,
Deserialize,
Hash,
PartialOrd,
Ord,
)]
pub enum RuleCode { #rulecode_variants }

#[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticKind { #diagkind_variants }


impl RuleCode {
/// A placeholder representation of the `DiagnosticKind` for the diagnostic.
pub fn kind(&self) -> DiagnosticKind {
match self { #rulecode_kind_match_arms }
}

pub fn origin(&self) -> RuleOrigin {
match self { #rulecode_origin_match_arms }
}
}


impl DiagnosticKind {
/// A four-letter shorthand code for the diagnostic.
pub fn code(&self) -> &'static RuleCode {
match self { #diagkind_code_match_arms }
}

/// The body text for the diagnostic.
pub fn body(&self) -> String {
match self { #diagkind_body_match_arms }
}

/// Whether the diagnostic is (potentially) fixable.
pub fn fixable(&self) -> bool {
match self { #diagkind_fixable_match_arms }
}

/// The message used to describe the fix action for a given `DiagnosticKind`.
pub fn commit(&self) -> Option<String> {
match self { #diagkind_commit_match_arms }
}
}

#from_impls_for_diagkind
}
}

fn get_origin(ident: &Ident) -> Ident {
let ident = ident.to_string();
let mut iter = crate::prefixes::PREFIX_TO_ORIGIN.iter();
let origin = loop {
let (prefix, origin) = iter
.next()
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
if ident.starts_with(prefix) {
break origin;
}
};
Ident::new(origin, Span::call_site())
}
pub struct Mapping {
entries: Vec<(Ident, Path, Ident)>,
}

impl Parse for Mapping {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut entries = Vec::new();
while !input.is_empty() {
let code: Ident = input.parse()?;
let _: Token![=>] = input.parse()?;
let path: Path = input.parse()?;
let name = path.segments.last().unwrap().ident.clone();
let _: Token![,] = input.parse()?;
entries.push((code, path, name));
}
Ok(Mapping { entries })
}
}
25 changes: 5 additions & 20 deletions ruff_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@
)]
#![forbid(unsafe_code)]

use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident};
use syn::{parse_macro_input, DeriveInput};

mod config;
mod define_rule_mapping;
mod prefixes;
mod rule_code_prefix;

Expand All @@ -40,21 +39,7 @@ pub fn derive_rule_code_prefix(input: proc_macro::TokenStream) -> proc_macro::To
}

#[proc_macro]
pub fn origin_by_code(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ident = parse_macro_input!(item as Ident).to_string();
let mut iter = prefixes::PREFIX_TO_ORIGIN.iter();
let origin = loop {
let (prefix, origin) = iter
.next()
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
if ident.starts_with(prefix) {
break origin;
}
};
let prefix = Ident::new(origin, Span::call_site());

quote! {
RuleOrigin::#prefix
}
.into()
pub fn define_rule_mapping(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mapping = parse_macro_input!(item as define_rule_mapping::Mapping);
define_rule_mapping::define_rule_mapping(mapping).into()
}
101 changes: 1 addition & 100 deletions src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,106 +15,7 @@ use crate::fix::Fix;
use crate::violation::Violation;
use crate::violations;

macro_rules! define_rule_mapping {
($($code:ident => $mod:ident::$name:ident,)+) => {
#[derive(
AsRefStr,
RuleCodePrefix,
EnumIter,
EnumString,
Debug,
Display,
PartialEq,
Eq,
Clone,
Serialize,
Deserialize,
Hash,
PartialOrd,
Ord,
)]
pub enum RuleCode {
$(
$code,
)+
}

#[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticKind {
$(
$name($mod::$name),
)+
}

impl RuleCode {
/// A placeholder representation of the `DiagnosticKind` for the diagnostic.
pub fn kind(&self) -> DiagnosticKind {
match self {
$(
RuleCode::$code => DiagnosticKind::$name(<$mod::$name as Violation>::placeholder()),
)+
}
}

pub fn origin(&self) -> RuleOrigin {
match self {
$(
RuleCode::$code => ruff_macros::origin_by_code!($code),
)+
}
}
}

impl DiagnosticKind {
/// A four-letter shorthand code for the diagnostic.
pub fn code(&self) -> &'static RuleCode {
match self {
$(
DiagnosticKind::$name(..) => &RuleCode::$code,
)+
}
}

/// The body text for the diagnostic.
pub fn body(&self) -> String {
match self {
$(
DiagnosticKind::$name(x) => Violation::message(x),
)+
}
}

/// Whether the diagnostic is (potentially) fixable.
pub fn fixable(&self) -> bool {
match self {
$(
DiagnosticKind::$name(x) => x.autofix_title_formatter().is_some(),
)+
}
}

/// The message used to describe the fix action for a given `DiagnosticKind`.
pub fn commit(&self) -> Option<String> {
match self {
$(
DiagnosticKind::$name(x) => x.autofix_title_formatter().map(|f| f(x)),
)+
}
}
}

$(
impl From<$mod::$name> for DiagnosticKind {
fn from(x: $mod::$name) -> Self {
DiagnosticKind::$name(x)
}
}
)+

};
}

define_rule_mapping!(
ruff_macros::define_rule_mapping!(
// pycodestyle errors
E401 => violations::MultipleImportsOnOneLine,
E402 => violations::ModuleImportNotAtTopOfFile,
Expand Down

0 comments on commit 81996f1

Please sign in to comment.