diff --git a/crates/oxc_linter/src/rules/eslint/prefer_const.rs b/crates/oxc_linter/src/rules/eslint/prefer_const.rs index 8ef0f03ff9bdf..881d66fb17f8b 100644 --- a/crates/oxc_linter/src/rules/eslint/prefer_const.rs +++ b/crates/oxc_linter/src/rules/eslint/prefer_const.rs @@ -290,10 +290,19 @@ impl PreferConst { return false; } + let references: Vec<_> = symbol_table.get_resolved_references(symbol_id).collect(); + let symbol_scope = symbol_table.symbol_scope_id(symbol_id); + let has_cross_scope_write = references + .iter() + .filter(|r| r.is_write()) + .any(|r| ctx.nodes().get_node(r.node_id()).scope_id() != symbol_scope); + if has_cross_scope_write { + return false; + } + // In "all" mode, check if this variable has more than one write // (one is the destructuring assignment itself) if matches!(self.0.destructuring, Destructuring::All) { - let references: Vec<_> = symbol_table.get_resolved_references(symbol_id).collect(); let write_count = references.iter().filter(|r| r.is_write()).count(); if write_count > 1 { return false; @@ -309,20 +318,65 @@ impl PreferConst { symbol_table: &oxc_semantic::Scoping, ctx: &LintContext<'_>, ) -> bool { - use oxc_ast::ast::AssignmentTargetMaybeDefault; - for target in array_target.elements.iter().flatten() { - if let AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(ident) = target { - // Get the symbol for this identifier + if !self.can_assignment_target_maybe_default_be_const(target, symbol_table, ctx) { + return false; + } + } + true + } + + fn can_assignment_target_maybe_default_be_const( + &self, + target: &AssignmentTargetMaybeDefault, + symbol_table: &oxc_semantic::Scoping, + ctx: &LintContext<'_>, + ) -> bool { + match target { + AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(ident) => { if let Some(symbol_id) = ctx.semantic().scoping().get_reference(ident.reference_id()).symbol_id() - && !self.can_identifier_be_const(symbol_id, symbol_table, ctx) { - return false; + return self.can_identifier_be_const(symbol_id, symbol_table, ctx); } + true + } + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(t) => { + self.can_assignment_target_be_const(&t.binding, symbol_table, ctx) + } + AssignmentTargetMaybeDefault::ArrayAssignmentTarget(array_target) => { + self.can_all_destructuring_identifiers_be_const(array_target, symbol_table, ctx) + } + AssignmentTargetMaybeDefault::ObjectAssignmentTarget(obj_target) => { + self.can_all_destructuring_identifiers_be_const_obj(obj_target, symbol_table, ctx) } + _ => true, + } + } + + fn can_assignment_target_be_const( + &self, + target: &AssignmentTarget, + symbol_table: &oxc_semantic::Scoping, + ctx: &LintContext<'_>, + ) -> bool { + match target { + AssignmentTarget::AssignmentTargetIdentifier(ident) => { + if let Some(symbol_id) = + ctx.semantic().scoping().get_reference(ident.reference_id()).symbol_id() + { + return self.can_identifier_be_const(symbol_id, symbol_table, ctx); + } + true + } + AssignmentTarget::ArrayAssignmentTarget(array_target) => { + self.can_all_destructuring_identifiers_be_const(array_target, symbol_table, ctx) + } + AssignmentTarget::ObjectAssignmentTarget(obj_target) => { + self.can_all_destructuring_identifiers_be_const_obj(obj_target, symbol_table, ctx) + } + _ => true, } - true } /// Check if all identifiers in an object destructuring assignment can be const @@ -350,9 +404,16 @@ impl PreferConst { return false; } } - AssignmentTargetProperty::AssignmentTargetPropertyProperty(_) => { - // For complex properties, we can't easily check, so allow it - // This is handled by the member expression check above + AssignmentTargetProperty::AssignmentTargetPropertyProperty(p) => { + // Handle property-style bindings like `{ key: value }`, + // including default values and nested patterns. + if !self.can_assignment_target_maybe_default_be_const( + &p.binding, + symbol_table, + ctx, + ) { + return false; + } } } } @@ -752,6 +813,44 @@ fn test() { ), ("var a; { var b; ({ a, b } = obj); }", None), ("let a; { let b; ({ a, b } = obj); }", None), + ( + " + let someOuterScopeVariable: string; + + async function generatorFunction() { + return { + inner: 'foo', + outer: 'bar', + }; + } + + async function myFunction() { + let innerScope: string; + ({inner: innerScope, outer: someOuterScopeVariable} = await generatorFunction()); + console.info(`${innerScope}, ${someOuterScopeVariable}`); + } + ", + Some(serde_json::json!([{ "destructuring": "all" }])), + ), + ( + " + let someOuterScopeVariable: string; + + function generatorFunction() { + return { + inner: 'foo', + outer: 'bar', + }; + } + + function myFunction() { + let innerScope: string; + ({inner: innerScope = 'x', outer: someOuterScopeVariable} = generatorFunction()); + console.info(`${innerScope}, ${someOuterScopeVariable}`); + } + ", + Some(serde_json::json!([{ "destructuring": "all" }])), + ), ("var a; { var b; ([ a, b ] = obj); }", None), ("let a; { let b; ([ a, b ] = obj); }", None), ("let x; { x = 0; foo(x); }", None),