Skip to content
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
41 changes: 41 additions & 0 deletions crates/oxc_linter/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I>(&self, diagnostic: OxcDiagnostic, suggestions: I)
where
I: IntoIterator<Item = RuleFix>,
{
let fixes_result: Vec<Fix> = 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<C, F>(
&self,
fix_kind: FixKind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -54,7 +50,7 @@ declare_oxc_lint!(
PreferEnumInitializers,
typescript,
pedantic,
pending
suggestion
);

impl Rule for PreferEnumInitializers {
Expand All @@ -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)."
)),
],
);
}
}
}
Expand All @@ -83,7 +96,7 @@ impl Rule for PreferEnumInitializers {

#[test]
fn test() {
use crate::tester::Tester;
use crate::tester::{ExpectFixTestCase, Tester};

let pass = vec![
"
Expand Down Expand Up @@ -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<ExpectFixTestCase> = 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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -44,4 +40,3 @@ source: crates/oxc_linter/src/tester.rs
· ──
4 │ Down = 'Down',
╰────
help: Can be fixed to "Up" = 1.
14 changes: 14 additions & 0 deletions crates/oxc_linter/src/tester.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@ impl<S: Into<String>> From<(S, (S, S))> for ExpectFixTestCase {
}
}

impl<S: Into<String>> 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<S, F> From<(S, S, Option<Value>, F)> for ExpectFixTestCase
where
S: Into<String>,
Expand Down
Loading