Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 110 additions & 11 deletions crates/oxc_linter/src/rules/eslint/prefer_const.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down Expand Up @@ -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),
Expand Down
Loading