mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter): implement eslint/no-multi-assign (#8158)
Implements [eslint/no-multi-assign](https://eslint.org/docs/latest/rules/no-multi-assign) rule.
This commit is contained in:
parent
384858b9ed
commit
ef76e288ca
3 changed files with 388 additions and 0 deletions
|
|
@ -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_negated_condition;
|
||||
pub mod no_nested_ternary;
|
||||
|
|
@ -539,6 +540,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,
|
||||
|
|
|
|||
226
crates/oxc_linter/src/rules/eslint/no_multi_assign.rs
Normal file
226
crates/oxc_linter/src/rules/eslint/no_multi_assign.rs
Normal file
|
|
@ -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();
|
||||
}
|
||||
160
crates/oxc_linter/src/snapshots/eslint_no_multi_assign.snap
Normal file
160
crates/oxc_linter/src/snapshots/eslint_no_multi_assign.snap
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue