fix(linter/eslint/no-useless-assignment): report dead stores to function parameters (#27735)

Track function parameter bindings so `function f(p) { p = 5; p = 6; return p; }` reports `p = 5`, matching ESLint. This also covers defaulted, destructured, and rest parameters.

Keep writes during parameter initialization excluded: the CFG does not model skipped defaults or their destructuring evaluation order reliably. Reads in defaults still count as uses.

Fixes #27720
This commit is contained in:
camc314 committed 2026-10-15 13:33:08 +00:00
1 parent ee5e730ca9
commit 9ff25c3a14
2 files changed
+200 -21

No files matched your search

@@ -159,22 +159,30 @@ impl Rule for NoUselessAssignment {
for symbol_id in ctx.scoping().symbol_ids() {
let decl_node = ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(var_decl) = decl_node.kind() else { continue };
if let AstKind::VariableDeclaration(var_declaration) =
ctx.nodes().parent_node(decl_node.id()).kind()
&& var_declaration.kind == VariableDeclarationKind::Const
{
continue;
}
if matches!(
&var_decl.init,
Some(Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_))
) {
continue;
}
let var_decl = match decl_node.kind() {
AstKind::VariableDeclarator(var_decl) => Some(var_decl),
AstKind::FormalParameter(_) | AstKind::FormalParameterRest(_) => None,
_ => continue,
};
if let Some(var_decl) = var_decl {
if let AstKind::VariableDeclaration(var_declaration) =
ctx.nodes().parent_node(decl_node.id()).kind()
&& var_declaration.kind == VariableDeclarationKind::Const
{
continue;
}
if matches!(
&var_decl.init,
Some(
Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)
)
) {
continue;
}
if Self::is_exported(ctx, symbol_id, decl_node) {
continue;
if Self::is_exported(ctx, symbol_id, decl_node) {
continue;
}
}
#[expect(clippy::cast_possible_truncation)]
@@ -187,7 +195,7 @@ impl Rule for NoUselessAssignment {
});
// Collect ops for this symbol (formerly Pass 2)
if var_decl.init.is_some() {
if var_decl.is_some_and(|var_decl| var_decl.init.is_some()) {
Self::push_op(
ctx,
graph,
@@ -533,7 +541,7 @@ impl NoUselessAssignment {
cfg_ops: &mut CfgOps,
reference: &Reference,
compact_idx: u32,
var_decl: &oxc_ast::ast::VariableDeclarator,
var_decl: Option<&oxc_ast::ast::VariableDeclarator>,
decl_node: &oxc_semantic::AstNode,
tracked_symbol: &mut TrackedSymbol,
previous_value_read: bool,
@@ -555,6 +563,17 @@ impl NoUselessAssignment {
}
if reference.is_write() {
// Parameter initialization lacks conditional CFG edges and ordered destructuring writes.
if var_decl.is_none()
&& ctx
.nodes()
.parent_node(decl_node.id())
.span()
.contains_inclusive(ctx.nodes().get_node(op_node).span())
{
return;
}
if previous_value_read && !reference.is_read() {
// Model RHS self-reads before the deferred assignment write.
// This keeps the previous value live without making nested
@@ -562,10 +581,12 @@ impl NoUselessAssignment {
Self::push_op(ctx, graph, cfg_ops, op_node, Operation::Read, compact_idx);
}
if matches!(
&var_decl.id,
BindingPattern::ObjectPattern(_) | BindingPattern::ArrayPattern(_)
) && decl_node
if var_decl.is_some_and(|var_decl| {
matches!(
&var_decl.id,
BindingPattern::ObjectPattern(_) | BindingPattern::ArrayPattern(_)
)
}) && decl_node
.span()
.contains_inclusive(ctx.nodes().get_node(reference.node_id()).span())
{
@@ -1520,6 +1541,18 @@ function useResource(unsafe: (resource: { readonly release: () => void }) => voi
published = (local = 1);
continue;
}",
"function f(p) { p = 1; return p; }",
"function f(p) { p = 1; }",
"function f(p = 0) { p = 1; return p; }",
"function f({p = 0}) { p = 1; return p; }",
"function f([p = 0]) { p = 1; return p; }",
"function f(...p) { p = []; return p; }",
"function f(p) { p = 1; p = 2; return () => p; }",
"function f(p) { p = p + 1; return p; }",
"function f(p) { p = 1; if (condition) { use(p); } p = 2; return p; }",
"function f(p, q = (p = 1), r = (p = 2)) { return p; }",
"function f(p, { [p = 1]: q } = (p = 2, {})) { return p; }",
"function f(p, { a = (p = 1) } = (p = 2, {})) { return p; }",
];
let fail = vec![
@@ -1864,6 +1897,24 @@ function useResource(unsafe: (resource: { readonly release: () => void }) => voi
"let local = 0;
export let exported = (local = 1);
use(local);",
"export function reassignedParam(p) { p = 5; p = 6; return p; }
export function reassignedLocal() { let x = 1; x = 2; return x; }",
"const f = function(p) { p = 1; p = 2; return p; };",
"const f = p => { p = 1; p = 2; return p; };",
"const obj = { f(p) { p = 1; p = 2; return p; } };",
"class C { f(p) { p = 1; p = 2; return p; } }",
"function f(p = 0) { p = 1; p = 2; return p; }",
"function f({p}) { p = 1; p = 2; return p; }",
"function f([p]) { p = 1; p = 2; return p; }",
"function f({nested: [p = 0]}) { p = 1; p = 2; return p; }",
"function f(...p) { p = []; p = [1]; return p; }",
"function f(...[p]) { p = 1; p = 2; return p; }",
"function f({a, ...p}) { p = {}; p = {a}; return p; }",
"export const p = 0; function f(p) { p = 1; p = 2; return p; }",
"function f(p) { p++; p = 2; return p; }",
"function f(p) { use(p); p = 1; }",
"function f(p, q = p) { p = 1; return q; }",
"function f(cb = p => { p = 1; p = 2; return p; }) { return cb; }",
];
Tester::new(NoUselessAssignment::NAME, NoUselessAssignment::PLUGIN, pass, fail)
@@ -532,3 +532,131 @@ source: crates/oxc_linter/src/tester.rs
2 │ export let exported = (local = 1);
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:2:50]
1 │ export function reassignedParam(p) { p = 5; p = 6; return p; }
2 │ export function reassignedLocal() { let x = 1; x = 2; return x; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:38]
1 │ export function reassignedParam(p) { p = 5; p = 6; return p; }
· ─
2 │ export function reassignedLocal() { let x = 1; x = 2; return x; }
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:25]
1 │ const f = function(p) { p = 1; p = 2; return p; };
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:18]
1 │ const f = p => { p = 1; p = 2; return p; };
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:22]
1 │ const obj = { f(p) { p = 1; p = 2; return p; } };
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:18]
1 │ class C { f(p) { p = 1; p = 2; return p; } }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:21]
1 │ function f(p = 0) { p = 1; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:19]
1 │ function f({p}) { p = 1; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:19]
1 │ function f([p]) { p = 1; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:33]
1 │ function f({nested: [p = 0]}) { p = 1; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:20]
1 │ function f(...p) { p = []; p = [1]; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:22]
1 │ function f(...[p]) { p = 1; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:25]
1 │ function f({a, ...p}) { p = {}; p = {a}; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:37]
1 │ export const p = 0; function f(p) { p = 1; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:17]
1 │ function f(p) { p++; p = 2; return p; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:25]
1 │ function f(p) { use(p); p = 1; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:24]
1 │ function f(p, q = p) { p = 1; return q; }
· ─
╰────
help: Consider removing or reusing the assigned value.
⚠ eslint(no-useless-assignment): This assigned value is not used in subsequent statements.
╭─[no_useless_assignment.tsx:1:24]
1 │ function f(cb = p => { p = 1; p = 2; return p; }) { return cb; }
· ─
╰────
help: Consider removing or reusing the assigned value.