Skip to content

Commit

Permalink
feat(linter): support conditional fix capabilities (#4559)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac committed Jul 31, 2024
1 parent 16c7b98 commit ddd8b27
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 27 deletions.
33 changes: 19 additions & 14 deletions crates/oxc_linter/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand All @@ -135,49 +137,52 @@ 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",
(true, false) => "auto-fix is available for this rule",
(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() {
'a' | 'e' | 'i' | 'o' | 'u' => "An",
_ => "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<Self, Self::Error> {
impl From<RuleFixMeta> 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,
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion crates/oxc_macros/src/declare_all_lint_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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()),*
Expand Down
66 changes: 54 additions & 12 deletions crates/oxc_macros/src/declare_oxc_lint.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use convert_case::{Boundary, Case, Converter};
use itertools::Itertools as _;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Expand Down Expand Up @@ -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! {
Expand Down Expand Up @@ -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."),
}
}

0 comments on commit ddd8b27

Please sign in to comment.