diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index e21dc30e2899de..95075ac8e1ae7b 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -334,6 +334,7 @@ mod unicorn { pub mod prefer_string_slice; pub mod prefer_string_starts_ends_with; pub mod prefer_string_trim_start_end; + pub mod prefer_structured_clone; pub mod prefer_type_error; pub mod require_array_join_separator; pub mod require_number_to_fixed_digits_argument; @@ -727,6 +728,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::prefer_string_slice, unicorn::prefer_string_starts_ends_with, unicorn::prefer_string_trim_start_end, + unicorn::prefer_structured_clone, unicorn::prefer_type_error, unicorn::require_array_join_separator, unicorn::require_number_to_fixed_digits_argument, diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs b/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs new file mode 100644 index 00000000000000..ee03d256096dcd --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs @@ -0,0 +1,202 @@ +use std::ops::Deref; + +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ast_util::is_method_call, context::LintContext, rule::Rule, AstNode}; + +fn prefer_structured_clone_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Use `structuredClone(…)` to create a deep clone.") + .with_help("Switch to `structuredClone(…)`.") + .with_label(span0) +} + +#[derive(Debug, Default, Clone)] +pub struct PreferStructuredClone(Box); + +#[derive(Debug, Default, Clone)] +pub struct PreferStructuredCloneConfig { + allowed_functions: Vec, +} + +impl Deref for PreferStructuredClone { + type Target = PreferStructuredCloneConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefer using structuredClone to create a deep clone. + /// + /// ### Why is this bad? + /// + /// structuredClone is the modern way to create a deep clone of a value. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// const clone = JSON.parse(JSON.stringify(foo)); + /// + /// const clone = _.cloneDeep(foo); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// const clone = structuredClone(foo); + /// ``` + PreferStructuredClone, + style, + pending, +); + +impl Rule for PreferStructuredClone { + fn from_configuration(value: serde_json::Value) -> Self { + let config = value.get(0); + + let allowed_functions = config + .and_then(|config| config.get("functions")) + .and_then(serde_json::Value::as_array) + .map(|v| { + v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() + }) + .unwrap_or(vec![String::from("cloneDeep"), String::from("utils.clone")]); + + Self(Box::new(PreferStructuredCloneConfig { allowed_functions })) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr): oxc_ast::AstKind<'a> = node.kind() else { + return; + }; + + if call_expr.arguments.len() != 1 { + return; + } + + if call_expr.optional { + return; + } + + // `JSON.parse(JSON.stringify(…)) + if is_method_call(call_expr, Some(&["JSON"]), Some(&["parse"]), Some(1), Some(1)) { + let Some(first_argument) = call_expr.arguments[0].as_expression() else { + return; + }; + + let Expression::CallExpression(inner_call_expr) = + first_argument.without_parenthesized() + else { + return; + }; + + if inner_call_expr.optional { + return; + } + + if !is_method_call( + inner_call_expr, + Some(&["JSON"]), + Some(&["stringify"]), + Some(1), + Some(1), + ) { + return; + } + + if inner_call_expr.arguments[0].is_spread() { + return; + } + + let span = Span::new(call_expr.span.start, inner_call_expr.span.end); + ctx.diagnostic(prefer_structured_clone_diagnostic(span)); + } else if !call_expr.arguments[0].is_spread() { + for function in &self.allowed_functions { + if let Some((object, method)) = function.split_once('.') { + if is_method_call(call_expr, Some(&[object]), Some(&[method]), None, None) { + ctx.diagnostic(prefer_structured_clone_diagnostic(call_expr.span)); + } + } else if is_method_call(call_expr, None, Some(&[function]), None, None) + || is_method_call(call_expr, Some(&[function]), None, None, None) + || call_expr.callee.is_specific_id(function) + { + ctx.diagnostic(prefer_structured_clone_diagnostic(call_expr.span)); + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("structuredClone(foo)", None), + ("JSON.parse(new JSON.stringify(foo))", None), + ("new JSON.parse(JSON.stringify(foo))", None), + ("JSON.parse(JSON.stringify())", None), + ("JSON.parse(JSON.stringify(...foo))", None), + ("JSON.parse(JSON.stringify(foo, extraArgument))", None), + ("JSON.parse(...JSON.stringify(foo))", None), + ("JSON.parse(JSON.stringify(foo), extraArgument)", None), + ("JSON.parse(JSON.stringify?.(foo))", None), + ("JSON.parse(JSON?.stringify(foo))", None), + ("JSON.parse?.(JSON.stringify(foo))", None), + // ("JSON?.parse(JSON.stringify(foo))", None), + ("JSON.parse(JSON.not_stringify(foo))", None), + ("JSON.parse(not_JSON.stringify(foo))", None), + ("JSON.not_parse(JSON.stringify(foo))", None), + ("not_JSON.parse(JSON.stringify(foo))", None), + ("JSON.stringify(JSON.parse(foo))", None), + ("JSON.parse(JSON.stringify(foo, undefined, 2))", None), + ("new _.cloneDeep(foo)", None), + ("notMatchedFunction(foo)", None), + ("_.cloneDeep()", None), + ("_.cloneDeep(...foo)", None), + ("_.cloneDeep(foo, extraArgument)", None), + // ("_.cloneDeep?.(foo)", None), + // ("_?.cloneDeep(foo)", None), + ]; + + let fail = vec![ + ("JSON.parse((JSON.stringify((foo))))", None), + ("JSON.parse(JSON.stringify(foo))", None), + ("JSON.parse(JSON.stringify(foo),)", None), + ("JSON.parse(JSON.stringify(foo,))", None), + ("JSON.parse(JSON.stringify(foo,),)", None), + ("JSON.parse( ((JSON.stringify)) (foo))", None), + ("(( JSON.parse)) (JSON.stringify(foo))", None), + ("JSON.parse(JSON.stringify( ((foo)) ))", None), + ( + " + function foo() { + return JSON + .parse( + JSON. + stringify( + bar, + ), + ); + } + ", + None, + ), + ("_.cloneDeep(foo)", None), + ("lodash.cloneDeep(foo)", None), + ("lodash.cloneDeep(foo,)", None), + ( + "myCustomDeepCloneFunction(foo,)", + Some(serde_json::json!([{"functions": ["myCustomDeepCloneFunction"]}])), + ), + ("my.cloneDeep(foo,)", Some(serde_json::json!([{"functions": ["my.cloneDeep"]}]))), + ]; + + Tester::new(PreferStructuredClone::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_structured_clone.snap b/crates/oxc_linter/src/snapshots/prefer_structured_clone.snap new file mode 100644 index 00000000000000..e7d09df4bc01f4 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_structured_clone.snap @@ -0,0 +1,106 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse((JSON.stringify((foo)))) + · ───────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse(JSON.stringify(foo)) + · ────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse(JSON.stringify(foo),) + · ────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse(JSON.stringify(foo,)) + · ─────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse(JSON.stringify(foo,),) + · ─────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse( ((JSON.stringify)) (foo)) + · ──────────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ (( JSON.parse)) (JSON.stringify(foo)) + · ──────────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ JSON.parse(JSON.stringify( ((foo)) )) + · ──────────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:3:28] + 2 │ function foo() { + 3 │ ╭─▶ return JSON + 4 │ │ .parse( + 5 │ │ JSON. + 6 │ │ stringify( + 7 │ │ bar, + 8 │ ╰─▶ ), + 9 │ ); + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ _.cloneDeep(foo) + · ──────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ lodash.cloneDeep(foo) + · ───────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ lodash.cloneDeep(foo,) + · ────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ myCustomDeepCloneFunction(foo,) + · ─────────────────────────────── + ╰──── + help: Switch to `structuredClone(…)`. + + ⚠ eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone. + ╭─[prefer_structured_clone.tsx:1:1] + 1 │ my.cloneDeep(foo,) + · ────────────────── + ╰──── + help: Switch to `structuredClone(…)`.