Skip to content

Commit 1fb9d23

Browse files
authored
feat(linter): add fixer for no-useless-fallback-in-spread rule (#3544)
1 parent ff3f37d commit 1fb9d23

File tree

2 files changed

+64
-12
lines changed

2 files changed

+64
-12
lines changed

crates/oxc_linter/src/rules/unicorn/no_useless_fallback_in_spread.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ use oxc_ast::{ast::Expression, AstKind};
22
use oxc_diagnostics::OxcDiagnostic;
33

44
use oxc_macros::declare_oxc_lint;
5-
use oxc_span::Span;
5+
use oxc_span::{GetSpan, Span};
66
use oxc_syntax::operator::LogicalOperator;
77

8-
use crate::{ast_util::outermost_paren_parent, context::LintContext, rule::Rule, AstNode};
8+
use crate::{
9+
ast_util::outermost_paren_parent, context::LintContext, fixer::Fix, rule::Rule, AstNode,
10+
};
911

10-
fn no_useless_fallback_in_spread_diagnostic(span0: Span) -> OxcDiagnostic {
12+
fn no_useless_fallback_in_spread_diagnostic(span: Span) -> OxcDiagnostic {
1113
OxcDiagnostic::warn("eslint-plugin-unicorn(no-useless-fallback-in-spread): Disallow useless fallback when spreading in object literals")
1214
.with_help("Spreading falsy values in object literals won't add any unexpected properties, so it's unnecessary to add an empty object as fallback.")
13-
.with_labels([span0.into()])
15+
.with_labels([span.into()])
1416
}
1517

1618
#[derive(Debug, Default, Clone)]
@@ -75,7 +77,29 @@ impl Rule for NoUselessFallbackInSpread {
7577
return;
7678
}
7779

78-
ctx.diagnostic(no_useless_fallback_in_spread_diagnostic(spread_element.span));
80+
let diagnostic = no_useless_fallback_in_spread_diagnostic(spread_element.span);
81+
82+
if can_fix(&logical_expression.left) {
83+
ctx.diagnostic_with_fix(diagnostic, || {
84+
let left_text = logical_expression.left.span().source_text(ctx.source_text());
85+
Fix::new(format!("...{left_text}"), spread_element.span)
86+
});
87+
} else {
88+
ctx.diagnostic(diagnostic);
89+
}
90+
}
91+
}
92+
93+
fn can_fix(left: &Expression<'_>) -> bool {
94+
const BANNED_IDENTIFIERS: [&str; 3] = ["undefined", "NaN", "Infinity"];
95+
match left.without_parenthesized() {
96+
Expression::Identifier(ident) => !BANNED_IDENTIFIERS.contains(&ident.name.as_str()),
97+
Expression::LogicalExpression(expr) => can_fix(&expr.left),
98+
Expression::ObjectExpression(_)
99+
| Expression::CallExpression(_)
100+
| Expression::StaticMemberExpression(_)
101+
| Expression::ComputedMemberExpression(_) => true,
102+
_ => false,
79103
}
80104
}
81105

@@ -131,5 +155,15 @@ fn test() {
131155
r"const object = {...(document.all || {})}",
132156
];
133157

134-
Tester::new(NoUselessFallbackInSpread::NAME, pass, fail).test_and_snapshot();
158+
let fix = vec![
159+
//
160+
(r"const object = {...(foo || {})}", r"const object = {...foo}"),
161+
(r"const object = {...(foo() || {})}", r"const object = {...foo()}"),
162+
(r"const object = {...((foo && {}) || {})}", "const object = {...(foo && {})}"),
163+
(r"const object = {...(0 || {})}", r"const object = {...(0 || {})}"),
164+
(r"const object = {...(NaN || {})}", r"const object = {...(NaN || {})}"),
165+
(r"const object = {...(Infinity || {})}", r"const object = {...(Infinity || {})}"),
166+
];
167+
168+
Tester::new(NoUselessFallbackInSpread::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
135169
}

crates/oxc_linter/src/tester.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,30 @@ impl From<(&str, Option<Value>, Option<Value>, Option<PathBuf>)> for TestCase {
6565
}
6666
}
6767

68+
#[derive(Debug, Clone)]
69+
pub struct ExpectFix {
70+
/// Source code being tested
71+
source: String,
72+
/// Expected source code after fix has been applied
73+
expected: String,
74+
rule_config: Option<Value>,
75+
}
76+
impl<S: Into<String>> From<(S, S, Option<Value>)> for ExpectFix {
77+
fn from(value: (S, S, Option<Value>)) -> Self {
78+
Self { source: value.0.into(), expected: value.1.into(), rule_config: value.2 }
79+
}
80+
}
81+
impl<S: Into<String>> From<(S, S)> for ExpectFix {
82+
fn from(value: (S, S)) -> Self {
83+
Self { source: value.0.into(), expected: value.1.into(), rule_config: None }
84+
}
85+
}
6886
pub struct Tester {
6987
rule_name: &'static str,
7088
rule_path: PathBuf,
7189
expect_pass: Vec<TestCase>,
7290
expect_fail: Vec<TestCase>,
73-
expect_fix: Vec<(String, String, Option<Value>)>,
91+
expect_fix: Vec<ExpectFix>,
7492
snapshot: String,
7593
current_working_directory: Box<Path>,
7694
import_plugin: bool,
@@ -138,9 +156,8 @@ impl Tester {
138156
self
139157
}
140158

141-
pub fn expect_fix<S: Into<String>>(mut self, expect_fix: Vec<(S, S, Option<Value>)>) -> Self {
142-
self.expect_fix =
143-
expect_fix.into_iter().map(|(s1, s2, r)| (s1.into(), s2.into(), r)).collect::<Vec<_>>();
159+
pub fn expect_fix<F: Into<ExpectFix>>(mut self, expect_fix: Vec<F>) -> Self {
160+
self.expect_fix = expect_fix.into_iter().map(std::convert::Into::into).collect::<Vec<_>>();
144161
self
145162
}
146163

@@ -179,8 +196,9 @@ impl Tester {
179196
}
180197

181198
fn test_fix(&mut self) {
182-
for (test, expected, config) in self.expect_fix.clone() {
183-
let result = self.run(&test, config, &None, None, true);
199+
for fix in self.expect_fix.clone() {
200+
let ExpectFix { source, expected, rule_config: config } = fix;
201+
let result = self.run(&source, config, &None, None, true);
184202
if let TestResult::Fixed(fixed_str) = result {
185203
assert_eq!(expected, fixed_str);
186204
} else {

0 commit comments

Comments
 (0)