diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index 4cd54ce0d440f..c982cb9238ad0 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -434,6 +434,47 @@ impl<'a> LintContext<'a> { } } + /// Report a lint rule violation and provide multiple suggestions for fixing it. + /// + /// The second argument is an iterator of [`RuleFix`] values representing + /// the available suggestions. + /// + /// Use this when a rule violation can be fixed in multiple ways and the user + /// should choose which fix to apply. + pub fn diagnostic_with_suggestions(&self, diagnostic: OxcDiagnostic, suggestions: I) + where + I: IntoIterator, + { + let fixes_result: Vec = suggestions + .into_iter() + .filter_map(|rule_fix| { + #[cfg(debug_assertions)] + debug_assert!( + self.current_rule_fix_capabilities.supports_fix(rule_fix.kind()), + "Rule `{}` does not support this fix kind. Did you forget to update fix capabilities in declare_oxc_lint?.\n\tSupported fix kinds: {:?}\n\tAttempted fix kind: {:?}", + self.current_rule_name, + FixKind::from(self.current_rule_fix_capabilities), + rule_fix.kind() + ); + + if self.parent.fix.can_apply(rule_fix.kind()) && !rule_fix.is_empty() { + Some(rule_fix.into_fix(self.source_text())) + } else { + None + } + }) + .collect(); + + if fixes_result.is_empty() { + self.diagnostic(diagnostic); + } else { + self.add_diagnostic( + Message::new(diagnostic, PossibleFixes::Multiple(fixes_result)) + .with_section_offset(self.parent.current_sub_host().source_text_offset), + ); + } + } + fn create_fix( &self, fix_kind: FixKind, diff --git a/crates/oxc_linter/src/rules/typescript/prefer_enum_initializers.rs b/crates/oxc_linter/src/rules/typescript/prefer_enum_initializers.rs index dd9288873f5aa..32f407fd4a3c6 100644 --- a/crates/oxc_linter/src/rules/typescript/prefer_enum_initializers.rs +++ b/crates/oxc_linter/src/rules/typescript/prefer_enum_initializers.rs @@ -6,18 +6,14 @@ use oxc_span::Span; use crate::{ AstNode, context::{ContextHost, LintContext}, + fixer::RuleFixer, rule::Rule, }; -fn prefer_enum_initializers_diagnostic( - member_name: &str, - init: usize, - span: Span, -) -> OxcDiagnostic { +fn prefer_enum_initializers_diagnostic(member_name: &str, span: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!( "The value of the member {member_name:?} should be explicitly defined." )) - .with_help(format!("Can be fixed to {member_name:?} = {init:?}.")) .with_label(span) } @@ -54,7 +50,7 @@ declare_oxc_lint!( PreferEnumInitializers, typescript, pedantic, - pending + suggestion ); impl Rule for PreferEnumInitializers { @@ -67,11 +63,28 @@ impl Rule for PreferEnumInitializers { if member.initializer.is_none() && let TSEnumMemberName::Identifier(i) = &member.id { - ctx.diagnostic(prefer_enum_initializers_diagnostic( - i.name.as_str(), - index + 1, - member.span, - )); + let member_name = i.name.as_str(); + let name_span = i.span; + let fixer = RuleFixer::new(FixKind::Suggestion, ctx); + ctx.diagnostic_with_suggestions( + prefer_enum_initializers_diagnostic(member_name, member.span), + [ + fixer + .replace(name_span, format!("{member_name} = {index}")) + .with_message(format!("Initialize to `{index}` (the enum index).")), + fixer + .replace(name_span, format!("{member_name} = {}", index + 1)) + .with_message(format!( + "Initialize to `{}` (the enum index + 1).", + index + 1 + )), + fixer + .replace(name_span, format!("{member_name} = '{member_name}'")) + .with_message(format!( + "Initialize to `'{member_name}'` (the enum member name)." + )), + ], + ); } } } @@ -83,7 +96,7 @@ impl Rule for PreferEnumInitializers { #[test] fn test() { - use crate::tester::Tester; + use crate::tester::{ExpectFixTestCase, Tester}; let pass = vec![ " @@ -134,6 +147,48 @@ fn test() { ", ]; + // Each test case provides 3 suggestions: index, index+1, and member name as string + // When multiple members are uninitialized, all fixes for the same suggestion type are applied + let fix: Vec = vec![ + ( + "enum Direction { Up, }", + ( + "enum Direction { Up = 0, }", + "enum Direction { Up = 1, }", + "enum Direction { Up = 'Up', }", + ), + ) + .into(), + ( + "enum Direction { Up, Down, }", + ( + "enum Direction { Up = 0, Down = 1, }", + "enum Direction { Up = 1, Down = 2, }", + "enum Direction { Up = 'Up', Down = 'Down', }", + ), + ) + .into(), + ( + "enum Direction { Up = 'Up', Down, }", + ( + "enum Direction { Up = 'Up', Down = 1, }", + "enum Direction { Up = 'Up', Down = 2, }", + "enum Direction { Up = 'Up', Down = 'Down', }", + ), + ) + .into(), + ( + "enum Direction { Up, Down = 'Down', }", + ( + "enum Direction { Up = 0, Down = 'Down', }", + "enum Direction { Up = 1, Down = 'Down', }", + "enum Direction { Up = 'Up', Down = 'Down', }", + ), + ) + .into(), + ]; + Tester::new(PreferEnumInitializers::NAME, PreferEnumInitializers::PLUGIN, pass, fail) + .expect_fix(fix) .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/typescript_prefer_enum_initializers.snap b/crates/oxc_linter/src/snapshots/typescript_prefer_enum_initializers.snap index 479bf89f0dcd0..b0459a52709f7 100644 --- a/crates/oxc_linter/src/snapshots/typescript_prefer_enum_initializers.snap +++ b/crates/oxc_linter/src/snapshots/typescript_prefer_enum_initializers.snap @@ -8,7 +8,6 @@ source: crates/oxc_linter/src/tester.rs · ── 4 │ } ╰──── - help: Can be fixed to "Up" = 1. ⚠ typescript-eslint(prefer-enum-initializers): The value of the member "Up" should be explicitly defined. ╭─[prefer_enum_initializers.tsx:3:6] @@ -17,7 +16,6 @@ source: crates/oxc_linter/src/tester.rs · ── 4 │ Down, ╰──── - help: Can be fixed to "Up" = 1. ⚠ typescript-eslint(prefer-enum-initializers): The value of the member "Down" should be explicitly defined. ╭─[prefer_enum_initializers.tsx:4:6] @@ -26,7 +24,6 @@ source: crates/oxc_linter/src/tester.rs · ──── 5 │ } ╰──── - help: Can be fixed to "Down" = 2. ⚠ typescript-eslint(prefer-enum-initializers): The value of the member "Down" should be explicitly defined. ╭─[prefer_enum_initializers.tsx:4:6] @@ -35,7 +32,6 @@ source: crates/oxc_linter/src/tester.rs · ──── 5 │ } ╰──── - help: Can be fixed to "Down" = 2. ⚠ typescript-eslint(prefer-enum-initializers): The value of the member "Up" should be explicitly defined. ╭─[prefer_enum_initializers.tsx:3:6] @@ -44,4 +40,3 @@ source: crates/oxc_linter/src/tester.rs · ── 4 │ Down = 'Down', ╰──── - help: Can be fixed to "Up" = 1. diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index ab017acadfccc..38d467b206c79 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -174,6 +174,20 @@ impl> From<(S, (S, S))> for ExpectFixTestCase { } } +impl> From<(S, (S, S, S))> for ExpectFixTestCase { + fn from(value: (S, (S, S, S))) -> Self { + Self { + source: value.0.into(), + expected: vec![ + ExpectFix { expected: value.1.0.into(), kind: ExpectFixKind::Any }, + ExpectFix { expected: value.1.1.into(), kind: ExpectFixKind::Any }, + ExpectFix { expected: value.1.2.into(), kind: ExpectFixKind::Any }, + ], + rule_config: None, + } + } +} + impl From<(S, S, Option, F)> for ExpectFixTestCase where S: Into,