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"
+ );
+ }
+}