Skip to content

Commit 1b41cb3

Browse files
authoredMar 24, 2025··
feat(linter): add suggested fix to unicorn/prefer-structured-clone (#9994)
I added the suggested fix to `unicorn/prefer-structured-clone`. Also fixed the incorrect reported spans, cleaned up the code and added a new fail & fix test with a more complex first argument.
1 parent 41fffed commit 1b41cb3

File tree

2 files changed

+116
-30
lines changed

2 files changed

+116
-30
lines changed
 

‎crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs

+91-13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
use std::ops::Deref;
22

3-
use oxc_ast::{AstKind, ast::Expression};
3+
use oxc_ast::{
4+
AstKind,
5+
ast::{CallExpression, Expression},
6+
};
47
use oxc_diagnostics::OxcDiagnostic;
58
use oxc_macros::declare_oxc_lint;
69
use oxc_span::Span;
710

8-
use crate::{AstNode, ast_util::is_method_call, context::LintContext, rule::Rule};
11+
use crate::{
12+
AstNode,
13+
ast_util::is_method_call,
14+
context::LintContext,
15+
fixer::{RuleFix, RuleFixer},
16+
rule::Rule,
17+
};
918

1019
fn prefer_structured_clone_diagnostic(span: Span) -> OxcDiagnostic {
1120
OxcDiagnostic::warn("Use `structuredClone(…)` to create a deep clone.")
@@ -54,7 +63,7 @@ declare_oxc_lint!(
5463
PreferStructuredClone,
5564
unicorn,
5665
style,
57-
pending,
66+
suggestion,
5867
);
5968

6069
impl Rule for PreferStructuredClone {
@@ -73,7 +82,7 @@ impl Rule for PreferStructuredClone {
7382
}
7483

7584
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
76-
let AstKind::CallExpression(call_expr): oxc_ast::AstKind<'a> = node.kind() else {
85+
let AstKind::CallExpression(call_expr) = node.kind() else {
7786
return;
7887
};
7988

@@ -85,7 +94,6 @@ impl Rule for PreferStructuredClone {
8594
return;
8695
}
8796

88-
// `JSON.parse(JSON.stringify(…))
8997
if is_method_call(call_expr, Some(&["JSON"]), Some(&["parse"]), Some(1), Some(1)) {
9098
let Some(first_argument) = call_expr.arguments[0].as_expression() else {
9199
return;
@@ -110,29 +118,49 @@ impl Rule for PreferStructuredClone {
110118
return;
111119
}
112120

113-
if inner_call_expr.arguments[0].is_spread() {
121+
let Some(first_argument) = inner_call_expr.arguments[0].as_expression() else {
114122
return;
115-
}
123+
};
116124

117-
let span = Span::new(call_expr.span.start, inner_call_expr.span.end);
118-
ctx.diagnostic(prefer_structured_clone_diagnostic(span));
119-
} else if !call_expr.arguments[0].is_spread() {
125+
ctx.diagnostic_with_suggestion(
126+
prefer_structured_clone_diagnostic(call_expr.span),
127+
|fixer| replace_with_structured_clone(fixer, call_expr, first_argument),
128+
);
129+
} else if let Some(first_argument) = call_expr.arguments[0].as_expression() {
120130
for function in &self.allowed_functions {
121131
if let Some((object, method)) = function.split_once('.') {
122132
if is_method_call(call_expr, Some(&[object]), Some(&[method]), None, None) {
123-
ctx.diagnostic(prefer_structured_clone_diagnostic(call_expr.span));
133+
ctx.diagnostic_with_suggestion(
134+
prefer_structured_clone_diagnostic(call_expr.span),
135+
|fixer| replace_with_structured_clone(fixer, call_expr, first_argument),
136+
);
124137
}
125138
} else if is_method_call(call_expr, None, Some(&[function]), None, None)
126139
|| is_method_call(call_expr, Some(&[function]), None, None, None)
127140
|| call_expr.callee.is_specific_id(function)
128141
{
129-
ctx.diagnostic(prefer_structured_clone_diagnostic(call_expr.span));
142+
ctx.diagnostic_with_suggestion(
143+
prefer_structured_clone_diagnostic(call_expr.span),
144+
|fixer| replace_with_structured_clone(fixer, call_expr, first_argument),
145+
);
130146
}
131147
}
132148
}
133149
}
134150
}
135151

152+
fn replace_with_structured_clone<'a>(
153+
fixer: RuleFixer<'_, 'a>,
154+
call_expr: &CallExpression<'_>,
155+
first_argument: &Expression<'_>,
156+
) -> RuleFix<'a> {
157+
let mut codegen = fixer.codegen();
158+
codegen.print_str("structuredClone(");
159+
codegen.print_expression(first_argument);
160+
codegen.print_str(")");
161+
fixer.replace(call_expr.span, codegen)
162+
}
163+
136164
#[test]
137165
fn test() {
138166
use crate::tester::Tester;
@@ -174,6 +202,7 @@ fn test() {
174202
("JSON.parse( ((JSON.stringify)) (foo))", None),
175203
("(( JSON.parse)) (JSON.stringify(foo))", None),
176204
("JSON.parse(JSON.stringify( ((foo)) ))", None),
205+
("JSON.parse(JSON.stringify( ((foo.bar['hello'])) ))", None),
177206
(
178207
"
179208
function foo() {
@@ -185,7 +214,7 @@ fn test() {
185214
),
186215
);
187216
}
188-
",
217+
",
189218
None,
190219
),
191220
("_.cloneDeep(foo)", None),
@@ -198,6 +227,55 @@ fn test() {
198227
("my.cloneDeep(foo,)", Some(serde_json::json!([{"functions": ["my.cloneDeep"]}]))),
199228
];
200229

230+
let fix = vec![
231+
("JSON.parse((JSON.stringify((foo))))", "structuredClone(foo)", None),
232+
("JSON.parse(JSON.stringify(foo))", "structuredClone(foo)", None),
233+
("JSON.parse(JSON.stringify(foo),)", "structuredClone(foo)", None),
234+
("JSON.parse(JSON.stringify(foo,))", "structuredClone(foo)", None),
235+
("JSON.parse(JSON.stringify(foo,),)", "structuredClone(foo)", None),
236+
("JSON.parse( ((JSON.stringify)) (foo))", "structuredClone(foo)", None),
237+
("(( JSON.parse)) (JSON.stringify(foo))", "structuredClone(foo)", None),
238+
("JSON.parse(JSON.stringify( ((foo)) ))", "structuredClone(foo)", None),
239+
(
240+
"JSON.parse(JSON.stringify( ((foo.bar['hello'])) ))",
241+
"structuredClone(foo.bar['hello'])",
242+
None,
243+
),
244+
(
245+
"
246+
function foo() {
247+
return JSON
248+
.parse(
249+
JSON.
250+
stringify(
251+
bar,
252+
),
253+
);
254+
}
255+
",
256+
"
257+
function foo() {
258+
return structuredClone(bar);
259+
}
260+
",
261+
None,
262+
),
263+
("_.cloneDeep(foo)", "structuredClone(foo)", None),
264+
("lodash.cloneDeep(foo)", "structuredClone(foo)", None),
265+
("lodash.cloneDeep(foo,)", "structuredClone(foo)", None),
266+
(
267+
"myCustomDeepCloneFunction(foo,)",
268+
"structuredClone(foo)",
269+
Some(serde_json::json!([{"functions": ["myCustomDeepCloneFunction"]}])),
270+
),
271+
(
272+
"my.cloneDeep(foo,)",
273+
"structuredClone(foo)",
274+
Some(serde_json::json!([{"functions": ["my.cloneDeep"]}])),
275+
),
276+
];
277+
201278
Tester::new(PreferStructuredClone::NAME, PreferStructuredClone::PLUGIN, pass, fail)
279+
.expect_fix(fix)
202280
.test_and_snapshot();
203281
}

‎crates/oxc_linter/src/snapshots/unicorn_prefer_structured_clone.snap

+25-17
Original file line numberDiff line numberDiff line change
@@ -4,72 +4,80 @@ source: crates/oxc_linter/src/tester.rs
44
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
55
╭─[prefer_structured_clone.tsx:1:1]
66
1JSON.parse((JSON.stringify((foo))))
7-
· ─────────────────────────────────
7+
· ───────────────────────────────────
88
╰────
99
help: Switch to `structuredClone(…)`.
1010

1111
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
1212
╭─[prefer_structured_clone.tsx:1:1]
1313
1JSON.parse(JSON.stringify(foo))
14-
· ──────────────────────────────
14+
· ──────────────────────────────
1515
╰────
1616
help: Switch to `structuredClone(…)`.
1717

1818
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
1919
╭─[prefer_structured_clone.tsx:1:1]
2020
1JSON.parse(JSON.stringify(foo),)
21-
· ──────────────────────────────
21+
· ────────────────────────────────
2222
╰────
2323
help: Switch to `structuredClone(…)`.
2424

2525
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
2626
╭─[prefer_structured_clone.tsx:1:1]
2727
1JSON.parse(JSON.stringify(foo,))
28-
· ───────────────────────────────
28+
· ───────────────────────────────
2929
╰────
3030
help: Switch to `structuredClone(…)`.
3131

3232
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
3333
╭─[prefer_structured_clone.tsx:1:1]
3434
1JSON.parse(JSON.stringify(foo,),)
35-
· ───────────────────────────────
35+
· ─────────────────────────────────
3636
╰────
3737
help: Switch to `structuredClone(…)`.
3838

3939
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
4040
╭─[prefer_structured_clone.tsx:1:1]
4141
1JSON.parse( ((JSON.stringify)) (foo))
42-
· ────────────────────────────────────
42+
· ────────────────────────────────────
4343
╰────
4444
help: Switch to `structuredClone(…)`.
4545

4646
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
4747
╭─[prefer_structured_clone.tsx:1:1]
4848
1 │ (( JSON.parse)) (JSON.stringify(foo))
49-
· ────────────────────────────────────
49+
· ────────────────────────────────────
5050
╰────
5151
help: Switch to `structuredClone(…)`.
5252

5353
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
5454
╭─[prefer_structured_clone.tsx:1:1]
5555
1JSON.parse(JSON.stringify( ((foo)) ))
56-
· ────────────────────────────────────
56+
· ────────────────────────────────────
5757
╰────
5858
help: Switch to `structuredClone(…)`.
5959

6060
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
61-
╭─[prefer_structured_clone.tsx:3:28]
62-
2function foo() {
63-
3 │ ╭─▶ return JSON
64-
4 │ │ .parse(
65-
5 │ │ JSON.
66-
6 │ │ stringify(
67-
7 │ │ bar,
68-
8 │ ╰─▶ ),
69-
9 │ );
61+
╭─[prefer_structured_clone.tsx:1:1]
62+
1JSON.parse(JSON.stringify( ((foo.bar['hello'])) ))
63+
· ──────────────────────────────────────────────────
7064
╰────
7165
help: Switch to `structuredClone(…)`.
7266

67+
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
68+
╭─[prefer_structured_clone.tsx:3:28]
69+
2function foo() {
70+
3 │ ╭─▶ return JSON
71+
4 │ │ .parse(
72+
5 │ │ JSON.
73+
6 │ │ stringify(
74+
7 │ │ bar,
75+
8 │ │ ),
76+
9 │ ╰─▶ );
77+
10 │ }
78+
╰────
79+
help: Switch to `structuredClone(…)`.
80+
7381
eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
7482
╭─[prefer_structured_clone.tsx:1:1]
7583
1_.cloneDeep(foo)

0 commit comments

Comments
 (0)
Please sign in to comment.