Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): support conditional fix capabilities #4559

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."),
}
}