From 46e1ecac65a65bc73a72dd419f088271b46c1439 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Tue, 30 Jul 2024 16:26:44 -0400 Subject: [PATCH] feat(linter): support conditional fix capabilities --- crates/oxc_linter/src/rule.rs | 33 ++++++---- .../oxc_macros/src/declare_all_lint_rules.rs | 9 ++- crates/oxc_macros/src/declare_oxc_lint.rs | 66 +++++++++++++++---- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 2ca25ecd8f3cb..2d240170fa6f4 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -125,6 +125,8 @@ pub enum RuleFixMeta { None, /// An auto-fix could be implemented, but it has not been yet. FixPending, + /// An auto-fix is available for some violations, but not all. + Conditional(FixKind), /// An auto-fix is available. Fixable(FixKind), } @@ -135,17 +137,22 @@ impl RuleFixMeta { /// Also returns `true` for suggestions. #[inline] pub fn has_fix(self) -> bool { - matches!(self, Self::Fixable(_)) + matches!(self, Self::Fixable(_) | Self::Conditional(_)) + } + + pub fn supports_fix(self, kind: FixKind) -> bool { + matches!(self, Self::Fixable(fix_kind) | Self::Conditional(fix_kind) if fix_kind.can_apply(kind)) } pub fn description(self) -> Cow<'static, str> { match self { Self::None => Cow::Borrowed("No auto-fix is available for this rule."), Self::FixPending => Cow::Borrowed("An auto-fix is still under development."), - Self::Fixable(kind) => { + Self::Fixable(kind) | Self::Conditional(kind) => { // e.g. an auto-fix is available for this rule // e.g. a suggestion is available for this rule // e.g. a dangerous auto-fix is available for this rule + // e.g. an auto-fix is available for this rule for some violations // e.g. an auto-fix and a suggestion are available for this rule let noun = match (kind.contains(FixKind::Fix), kind.contains(FixKind::Suggestion)) { (true, true) => "auto-fix and a suggestion are available for this rule", @@ -153,7 +160,7 @@ impl RuleFixMeta { (false, true) => "suggestion is available for this rule", _ => unreachable!(), }; - let message = + let mut message = if kind.is_dangerous() { format!("dangerous {noun}") } else { noun.into() }; let article = match message.chars().next().unwrap() { @@ -161,23 +168,21 @@ impl RuleFixMeta { _ => "A", }; - Cow::Owned(format!("{article} {message}")) + if matches!(self, Self::Conditional(_)) { + message += " for some violations"; + } + + Cow::Owned(format!("{article} {message}.")) } } } } -impl TryFrom<&str> for RuleFixMeta { - type Error = (); - fn try_from(value: &str) -> Result { +impl From for FixKind { + fn from(value: RuleFixMeta) -> Self { match value { - "none" => Ok(Self::None), - "pending" => Ok(Self::FixPending), - "fix" => Ok(Self::Fixable(FixKind::Fix)), - "fix-dangerous" => Ok(Self::Fixable(FixKind::DangerousFix)), - "suggestion" => Ok(Self::Fixable(FixKind::Suggestion)), - "suggestion-dangerous" => Ok(Self::Fixable(FixKind::Suggestion | FixKind::Dangerous)), - _ => Err(()), + RuleFixMeta::None | RuleFixMeta::FixPending => FixKind::None, + RuleFixMeta::Fixable(kind) | RuleFixMeta::Conditional(kind) => kind, } } } diff --git a/crates/oxc_macros/src/declare_all_lint_rules.rs b/crates/oxc_macros/src/declare_all_lint_rules.rs index 399155f105c31..93c29878c027a 100644 --- a/crates/oxc_macros/src/declare_all_lint_rules.rs +++ b/crates/oxc_macros/src/declare_all_lint_rules.rs @@ -53,7 +53,7 @@ pub fn declare_all_lint_rules(metadata: AllLintRulesMeta) -> TokenStream { let expanded = quote! { #(pub use self::#use_stmts::#struct_names;)* - use crate::{context::LintContext, rule::{Rule, RuleCategory, RuleMeta}, AstNode}; + use crate::{context::LintContext, rule::{Rule, RuleCategory, RuleFixMeta, RuleMeta}, AstNode}; use oxc_semantic::SymbolId; #[derive(Debug, Clone)] @@ -81,6 +81,13 @@ pub fn declare_all_lint_rules(metadata: AllLintRulesMeta) -> TokenStream { } } + /// This [`Rule`]'s auto-fix capabilities. + pub fn fix(&self) -> RuleFixMeta { + match self { + #(Self::#struct_names(_) => #struct_names::FIX),* + } + } + pub fn documentation(&self) -> Option<&'static str> { match self { #(Self::#struct_names(_) => #struct_names::documentation()),* diff --git a/crates/oxc_macros/src/declare_oxc_lint.rs b/crates/oxc_macros/src/declare_oxc_lint.rs index b7c73190f6178..fc75d6fb36602 100644 --- a/crates/oxc_macros/src/declare_oxc_lint.rs +++ b/crates/oxc_macros/src/declare_oxc_lint.rs @@ -1,4 +1,5 @@ use convert_case::{Boundary, Case, Converter}; +use itertools::Itertools as _; use proc_macro::TokenStream; use quote::quote; use syn::{ @@ -79,7 +80,7 @@ pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { let import_statement = if used_in_test { None } else { - Some(quote! { use crate::rule::{RuleCategory, RuleMeta, RuleFixMeta}; }) + Some(quote! { use crate::{rule::{RuleCategory, RuleMeta, RuleFixMeta}, fixer::FixKind}; }) }; let output = quote! { @@ -119,17 +120,58 @@ fn parse_attr<'a, const LEN: usize>( } fn parse_fix(s: &str) -> proc_macro2::TokenStream { + const SEP: char = '_'; + + match s { + "none" => { + return quote! { RuleFixMeta::None }; + } + "pending" => { return quote! { RuleFixMeta::FixPending }; } + "fix" => { + return quote! { RuleFixMeta::Fixable(FixKind::SafeFix) } + }, + "suggestion" => { + return quote! { RuleFixMeta::Fixable(FixKind::Suggestion) } + }, + // "fix-dangerous" => quote! { RuleFixMeta::Fixable(FixKind::Fix.union(FixKind::Dangerous)) }, + // "suggestion" => quote! { RuleFixMeta::Fixable(FixKind::Suggestion) }, + // "suggestion-dangerous" => quote! { RuleFixMeta::Fixable(FixKind::Suggestion.union(FixKind::Dangerous)) }, + "conditional" => panic!("Invalid fix capabilities: missing a fix kind. Did you mean 'fix-conditional'?"), + "None" => panic!("Invalid fix capabilities. Did you mean 'none'?"), + "Pending" => panic!("Invalid fix capabilities. Did you mean 'pending'?"), + "Fix" => panic!("Invalid fix capabilities. Did you mean 'fix'?"), + "Suggestion" => panic!("Invalid fix capabilities. Did you mean 'suggestion'?"), + invalid if !invalid.contains(SEP) => panic!("invalid fix capabilities: {invalid}. Valid capabilities are none, pending, fix, suggestion, or [fix|suggestion]_[conditional?]_[dangerous?]."), + _ => {} + } + + assert!(s.contains(SEP)); + + let mut is_conditional = false; + let fix_kinds = s + .split(SEP) + .filter(|seg| { + let conditional = *seg == "conditional"; + is_conditional = is_conditional || conditional; + !conditional + }) + .unique() + .map(parse_fix_kind) + .reduce(|acc, kind| quote! { #acc.union(#kind) }) + .expect("No fix kinds were found during parsing, but at least one is required."); + + if is_conditional { + quote! { RuleFixMeta::Conditional(#fix_kinds) } + } else { + quote! { RuleFixMeta::Fixable(#fix_kinds) } + } +} + +fn parse_fix_kind(s: &str) -> proc_macro2::TokenStream { match s { - "none" => quote! { RuleFixMeta::None }, - "pending" => quote! { RuleFixMeta::FixPending }, - "fix" => quote! { RuleFixMeta::Fixable(FixKind::Fix) }, - "fix-dangerous" => quote! { RuleFixMeta::Fixable(FixKind::Fix.union(FixKind::Dangerous)) }, - "suggestion" => quote! { RuleFixMeta::Fixable(FixKind::Suggestion) }, - "suggestion-dangerous" => quote! { RuleFixMeta::Fixable(FixKind::Suggestion.union(FixKind::Dangerous)) }, - "None" => panic!("Invalid fix kind. Did you mean 'none'?"), - "Pending" => panic!("Invalid fix kind. Did you mean 'pending'?"), - "Fix" => panic!("Invalid fix kind. Did you mean 'fix'?"), - "Suggestion" => panic!("Invalid fix kind. Did you mean 'suggestion'?"), - invalid => panic!("invalid fix kind: {invalid}. Valid kinds are none, pending, fix, fix-dangerous, suggestion, and suggestion-dangerous"), + "fix" => quote! { FixKind::Fix }, + "suggestion" => quote! { FixKind::Suggestion }, + "dangerous" => quote! { FixKind::Dangerous }, + _ => panic!("invalid fix kind: {s}. Valid fix kinds are fix, suggestion, or dangerous."), } }