diff --git a/Cargo.lock b/Cargo.lock index 0a009b3fe7bb..dae977c92c5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,7 @@ dependencies = [ name = "biome_analyze" version = "0.5.7" dependencies = [ + "biome_analyze_macros", "biome_console", "biome_deserialize", "biome_deserialize_macros", diff --git a/crates/biome_analyze/Cargo.toml b/crates/biome_analyze/Cargo.toml index bf09f589f532..1a728368dcef 100644 --- a/crates/biome_analyze/Cargo.toml +++ b/crates/biome_analyze/Cargo.toml @@ -14,6 +14,7 @@ publish = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +biome_analyze_macros = { workspace = true } biome_console = { workspace = true } biome_deserialize = { workspace = true, optional = true } biome_deserialize_macros = { workspace = true, optional = true } diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index 78b98962f763..86d547144d05 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -4,6 +4,7 @@ use crate::registry::{RegistryVisitor, RuleLanguage, RuleSuppressions}; use crate::{ Phase, Phases, Queryable, SourceActionKind, SuppressionAction, SuppressionCommentEmitterPayload, }; +use biome_analyze_macros::RuleSourceVariantIndex; use biome_console::fmt::{Display, Formatter}; use biome_console::{MarkupBuf, markup}; use biome_diagnostics::location::AsSpan; @@ -93,10 +94,11 @@ impl TryFrom for Applicability { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RuleSourceVariantIndex)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +/// Declaration order defines the sort order used for comparing rule sources. pub enum RuleSource<'a> { /// Rules from [Rust Clippy](https://rust-lang.github.io/rust-clippy/master/index.html) Clippy(&'a str), @@ -262,58 +264,6 @@ impl<'a> Ord for RuleSource<'a> { } impl<'a> RuleSource<'a> { - /// Returns the variant index for sorting and comparison across lifetimes. - pub const fn variant_index(&self) -> u16 { - match self { - Self::Clippy(_) => 0, - Self::DenoLint(_) => 1, - Self::Eslint(_) => 2, - Self::EslintBarrelFiles(_) => 3, - Self::EslintGraphql(_) => 4, - Self::EslintImport(_) => 5, - Self::EslintImportAccess(_) => 6, - Self::EslintJest(_) => 7, - Self::EslintJsDoc(_) => 8, - Self::EslintJsxA11y(_) => 9, - Self::EslintMysticatea(_) => 10, - Self::EslintN(_) => 11, - Self::EslintNext(_) => 12, - Self::EslintNoSecrets(_) => 13, - Self::EslintPackageJson(_) => 14, - Self::EslintPackageJsonDependencies(_) => 15, - Self::EslintPerfectionist(_) => 16, - Self::EslintPlaywright(_) => 17, - Self::EslintPromise(_) => 18, - Self::EslintQwik(_) => 19, - Self::EslintReact(_) => 20, - Self::EslintReactHooks(_) => 21, - Self::EslintReactPreferFunctionComponent(_) => 22, - Self::EslintReactRefresh(_) => 23, - Self::EslintReactX(_) => 24, - Self::EslintReactXyz(_) => 25, - Self::EslintRegexp(_) => 26, - Self::EslintSolid(_) => 27, - Self::EslintSonarJs(_) => 28, - Self::EslintStylistic(_) => 29, - Self::EslintTypeScript(_) => 30, - Self::EslintUnicorn(_) => 31, - Self::EslintUnusedImports(_) => 32, - Self::EslintVitest(_) => 33, - Self::EslintVueJs(_) => 34, - Self::GraphqlSchemaLinter(_) => 35, - Self::Stylelint(_) => 36, - Self::EslintTurbo(_) => 37, - Self::HtmlEslint(_) => 38, - Self::EslintE18e(_) => 39, - Self::EslintBetterTailwindcss(_) => 40, - Self::EslintJson(_) => 41, - Self::EslintMarkdown(_) => 42, - Self::EslintYml(_) => 43, - Self::EslintCss(_) => 44, - Self::EslintDrizzle(_) => 45, - } - } - const fn sort_key(&self) -> (u16, &'a str) { (self.variant_index(), self.as_rule_name()) } diff --git a/crates/biome_analyze_macros/src/lib.rs b/crates/biome_analyze_macros/src/lib.rs index 56be6443926f..e6882881d46a 100644 --- a/crates/biome_analyze_macros/src/lib.rs +++ b/crates/biome_analyze_macros/src/lib.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; mod group_macro; +mod rule_source_variant_index; /// Declares an analyzer group by reading rule files from the filesystem. /// @@ -27,3 +28,8 @@ mod group_macro; pub fn declare_group_from_fs(input: TokenStream) -> TokenStream { group_macro::declare_group_from_fs_impl(input) } + +#[proc_macro_derive(RuleSourceVariantIndex)] +pub fn rule_source_variant_index(input: TokenStream) -> TokenStream { + rule_source_variant_index::rule_source_variant_index_impl(input) +} diff --git a/crates/biome_analyze_macros/src/rule_source_variant_index.rs b/crates/biome_analyze_macros/src/rule_source_variant_index.rs new file mode 100644 index 000000000000..241e2edc627a --- /dev/null +++ b/crates/biome_analyze_macros/src/rule_source_variant_index.rs @@ -0,0 +1,105 @@ +use proc_macro::TokenStream; +use proc_macro2::{Literal, TokenStream as TokenStream2}; +use quote::quote; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; + +pub fn rule_source_variant_index_impl(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match generate_variant_index_impl(&input) { + Ok(tokens) => tokens.into(), + Err(err) => err.into_compile_error().into(), + } +} + +fn generate_variant_index_impl(input: &DeriveInput) -> syn::Result { + let Data::Enum(data_enum) = &input.data else { + return Err(syn::Error::new_spanned( + &input.ident, + "RuleSourceVariantIndex can only be derived for enums", + )); + }; + + if data_enum.variants.len() > usize::from(u16::MAX) + 1 { + return Err(syn::Error::new_spanned( + &input.ident, + "RuleSourceVariantIndex supports at most u16::MAX + 1 variants", + )); + } + + let ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let arms = data_enum + .variants + .iter() + .enumerate() + .map(|(index, variant)| { + let variant_ident = &variant.ident; + let index = Literal::u16_unsuffixed(index as u16); + let pattern = match &variant.fields { + Fields::Unit => quote!(Self::#variant_ident), + Fields::Unnamed(_) => quote!(Self::#variant_ident(..)), + Fields::Named(_) => quote!(Self::#variant_ident { .. }), + }; + + quote!(#pattern => #index,) + }); + + Ok(quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub const fn variant_index(&self) -> u16 { + match self { + #( #arms )* + } + } + } + }) +} + +#[cfg(test)] +mod tests { + use super::generate_variant_index_impl; + use quote::quote; + use syn::parse_quote; + + #[test] + fn generates_indices_for_all_variant_shapes() { + let input = parse_quote! { + enum Example<'a> { + Unit, + Tuple(&'a str), + Struct { name: &'a str }, + } + }; + + let tokens = generate_variant_index_impl(&input).unwrap(); + + assert_eq!( + tokens.to_string(), + quote! { + impl<'a> Example<'a> { + pub const fn variant_index(&self) -> u16 { + match self { + Self::Unit => 0, + Self::Tuple(..) => 1, + Self::Struct { .. } => 2, + } + } + } + } + .to_string() + ); + } + + #[test] + fn rejects_non_enum_input() { + let input = parse_quote! { + struct Example; + }; + + let error = generate_variant_index_impl(&input).unwrap_err(); + assert_eq!( + error.to_string(), + "RuleSourceVariantIndex can only be derived for enums" + ); + } +}