Skip to content

Commit

Permalink
feat(linter/unicorn): add prefer-structured-clone (#5095)
Browse files Browse the repository at this point in the history
  • Loading branch information
jelly authored Aug 23, 2024
1 parent ff7fa98 commit f7958c4
Show file tree
Hide file tree
Showing 3 changed files with 310 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
202 changes: 202 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/prefer_structured_clone.rs
Original file line number Diff line number Diff line change
@@ -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<PreferStructuredCloneConfig>);

#[derive(Debug, Default, Clone)]
pub struct PreferStructuredCloneConfig {
allowed_functions: Vec<String>,
}

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();
}
106 changes: 106 additions & 0 deletions crates/oxc_linter/src/snapshots/prefer_structured_clone.snap
Original file line number Diff line number Diff line change
@@ -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]
1JSON.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]
1JSON.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]
1JSON.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]
1JSON.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]
1JSON.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]
1JSON.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]
1JSON.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]
2function 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]
1lodash.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]
1lodash.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]
1myCustomDeepCloneFunction(foo,)
· ───────────────────────────────
╰────
help: Switch to `structuredClone(…)`.

eslint-plugin-unicorn(prefer-structured-clone): Use `structuredClone(…)` to create a deep clone.
╭─[prefer_structured_clone.tsx:1:1]
1my.cloneDeep(foo,)
· ──────────────────
╰────
help: Switch to `structuredClone(…)`.

0 comments on commit f7958c4

Please sign in to comment.