diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 053fec48b501e..37954db6dac1a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -92,6 +92,7 @@ mod eslint { pub mod no_labels; pub mod no_loss_of_precision; pub mod no_magic_numbers; + pub mod no_multi_assign; pub mod no_multi_str; pub mod no_nested_ternary; pub mod no_new; @@ -536,6 +537,7 @@ oxc_macros::declare_all_lint_rules! { eslint::max_classes_per_file, eslint::max_lines, eslint::max_params, + eslint::no_multi_assign, eslint::no_nested_ternary, eslint::no_labels, eslint::no_restricted_imports, diff --git a/crates/oxc_linter/src/rules/eslint/no_multi_assign.rs b/crates/oxc_linter/src/rules/eslint/no_multi_assign.rs new file mode 100644 index 0000000000000..72a1b548ed5cf --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_multi_assign.rs @@ -0,0 +1,226 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_multi_assign_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Do not use chained assignment") + .with_help("Separate each assignment into its own statement") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoMultiAssign { + ignore_non_declaration: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow use of chained assignment expressions. + /// + /// ### Why is this bad? + /// + /// Chaining the assignment of variables can lead to unexpected results and be difficult to read. + /// ```js + /// (function() { + /// const foo = bar = 0; // Did you mean `foo = bar == 0`? + /// bar = 1; // This will not fail since `bar` is not constant. + /// })(); + /// console.log(bar); // This will output 1 since `bar` is not scoped. + /// ``` + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// var a = b = c = 5; + /// + /// const foo = bar = "baz"; + /// + /// let d = + /// e = + /// f; + /// + /// class Foo { + /// a = b = 10; + /// } + /// + /// a = b = "quux"; + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// var a = 5; + /// var b = 5; + /// var c = 5; + /// + /// const foo = "baz"; + /// const bar = "baz"; + /// + /// let d = c; + /// let e = c; + /// + /// class Foo { + /// a = 10; + /// b = 10; + /// } + /// + /// a = "quux"; + /// b = "quux"; + /// ``` + /// + /// ### Options + /// + /// This rule has an object option: + /// * `"ignoreNonDeclaration"`: When set to `true`, the rule allows chains that don't include initializing a variable in a declaration or initializing a class field. Default is `false`. + /// + /// #### ignoreNonDeclaration + /// + /// Examples of **correct** code for the `{ "ignoreNonDeclaration": true }` option: + /// ```js + /// let a; + /// let b; + /// a = b = "baz"; + /// + /// const x = {}; + /// const y = {}; + /// x.one = y.one = 1; + /// ``` + /// + /// Examples of **incorrect** code for the `{ "ignoreNonDeclaration": true }` option: + /// ```js + /// let a = b = "baz"; + /// + /// const foo = bar = 1; + /// + /// class Foo { + /// a = b = 10; + /// } + /// ``` + NoMultiAssign, + style, +); + +impl Rule for NoMultiAssign { + fn from_configuration(value: serde_json::Value) -> Self { + let ignore_non_declaration = value + .get(0) + .and_then(|config| config.get("ignoreNonDeclaration")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + Self { ignore_non_declaration } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + // e.g. `var a = b = c;` + if let AstKind::VariableDeclarator(declarator) = node.kind() { + let Some(Expression::AssignmentExpression(assign_expr)) = &declarator.init else { + return; + }; + ctx.diagnostic(no_multi_assign_diagnostic(assign_expr.span)); + } + + // e.g. `class A { a = b = 1; }` + if let AstKind::PropertyDefinition(prop_def) = node.kind() { + let Some(Expression::AssignmentExpression(assign_expr)) = &prop_def.value else { + return; + }; + ctx.diagnostic(no_multi_assign_diagnostic(assign_expr.span)); + } + + // e.g. `let a; let b; a = b = 1;` + if !self.ignore_non_declaration { + if let AstKind::AssignmentExpression(parent_expr) = node.kind() { + let Expression::AssignmentExpression(expr) = &parent_expr.right else { + return; + }; + ctx.diagnostic(no_multi_assign_diagnostic(expr.span)); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + "var a, b, c, + d = 0;", + None, + ), + ( + "var a = 1; var b = 2; var c = 3; + var d = 0;", + None, + ), + ("var a = 1 + (b === 10 ? 5 : 4);", None), + ("const a = 1, b = 2, c = 3;", None), // { "ecmaVersion": 6 }, + ( + "const a = 1; + const b = 2; + const c = 3;", + None, + ), // { "ecmaVersion": 6 }, + ("for(var a = 0, b = 0;;){}", None), + ("for(let a = 0, b = 0;;){}", None), // { "ecmaVersion": 6 }, + ("for(const a = 0, b = 0;;){}", None), // { "ecmaVersion": 6 }, + ("export let a, b;", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "export let a, + b = 0;", + None, + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "const x = {};const y = {};x.one = y.one = 1;", + Some(serde_json::json!([{ "ignoreNonDeclaration": true }])), + ), // { "ecmaVersion": 6 }, + ("let a, b;a = b = 1", Some(serde_json::json!([{ "ignoreNonDeclaration": true }]))), // { "ecmaVersion": 6 }, + ("class C { [foo = 0] = 0 }", None), // { "ecmaVersion": 2022 } + ]; + + let fail = vec![ + ("var a = b = c;", None), + ("var a = b = c = d;", None), + ("let foo = bar = cee = 100;", None), // { "ecmaVersion": 6 }, + ("a=b=c=d=e", None), + ("a=b=c", None), + ( + "a + =b + =c", + None, + ), + ("var a = (b) = (((c)))", None), + ("var a = ((b)) = (c)", None), + ("var a = b = ( (c * 12) + 2)", None), + ( + "var a = + ((b)) + = (c)", + None, + ), + ("a = b = '=' + c + 'foo';", None), + ("a = b = 7 * 12 + 5;", None), + ( + "const x = {}; + const y = x.one = 1;", + Some(serde_json::json!([{ "ignoreNonDeclaration": true }])), + ), // { "ecmaVersion": 6 }, + ("let a, b;a = b = 1", Some(serde_json::json!([{}]))), // { "ecmaVersion": 6 }, + ("let x, y;x = y = 'baz'", Some(serde_json::json!([{ "ignoreNonDeclaration": false }]))), // { "ecmaVersion": 6 }, + ("const a = b = 1", Some(serde_json::json!([{ "ignoreNonDeclaration": true }]))), // { "ecmaVersion": 6 }, + ("class C { field = foo = 0 }", None), // { "ecmaVersion": 2022 }, + ( + "class C { field = foo = 0 }", + Some(serde_json::json!([{ "ignoreNonDeclaration": true }])), + ), // { "ecmaVersion": 2022 } + ]; + + Tester::new(NoMultiAssign::NAME, NoMultiAssign::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/eslint_no_multi_assign.snap b/crates/oxc_linter/src/snapshots/eslint_no_multi_assign.snap new file mode 100644 index 0000000000000..d697530ebc474 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/eslint_no_multi_assign.snap @@ -0,0 +1,160 @@ +--- +source: crates/oxc_linter/src/tester.rs +snapshot_kind: text +--- + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:9] + 1 │ var a = b = c; + · ───── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:9] + 1 │ var a = b = c = d; + · ───────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:13] + 1 │ var a = b = c = d; + · ───── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:11] + 1 │ let foo = bar = cee = 100; + · ─────────────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:17] + 1 │ let foo = bar = cee = 100; + · ───────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:3] + 1 │ a=b=c=d=e + · ─────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:5] + 1 │ a=b=c=d=e + · ───── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:7] + 1 │ a=b=c=d=e + · ─── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:3] + 1 │ a=b=c + · ─── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:2:5] + 1 │ a + 2 │ ╭─▶ =b + 3 │ ╰─▶ =c + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:9] + 1 │ var a = (b) = (((c))) + · ───────────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:9] + 1 │ var a = ((b)) = (c) + · ─────────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:9] + 1 │ var a = b = ( (c * 12) + 2) + · ─────────────────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:2:4] + 1 │ var a = + 2 │ ╭─▶ ((b)) + 3 │ ╰─▶ = (c) + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:5] + 1 │ a = b = '=' + c + 'foo'; + · ─────────────────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:5] + 1 │ a = b = 7 * 12 + 5; + · ────────────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:2:14] + 1 │ const x = {}; + 2 │ const y = x.one = 1; + · ───────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:14] + 1 │ let a, b;a = b = 1 + · ───── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:14] + 1 │ let x, y;x = y = 'baz' + · ───────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:11] + 1 │ const a = b = 1 + · ───── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:19] + 1 │ class C { field = foo = 0 } + · ─────── + ╰──── + help: Separate each assignment into its own statement + + ⚠ eslint(no-multi-assign): Do not use chained assignment + ╭─[no_multi_assign.tsx:1:19] + 1 │ class C { field = foo = 0 } + · ─────── + ╰──── + help: Separate each assignment into its own statement