Repository: swc-project/swc
File: crates/swc_ecma_transformer/src/es2021/logical_assignment_operators.rs (Rust)

Code before:

use swc_common::{util::take::Take, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_hooks::VisitMutHook;
use swc_ecma_utils::alias_ident_for;

use crate::TraverseCtx;

pub fn hook() -> impl VisitMutHook<TraverseCtx> {
    LogicalAssignmentOperatorsPass::default()
}

#[derive(Default)]
struct LogicalAssignmentOperatorsPass {
    stmt_ptr: Option<*const Stmt>,
    stmt_ptr_stack: Vec<*const Stmt>,
}

impl VisitMutHook<TraverseCtx> for LogicalAssignmentOperatorsPass {
    fn enter_stmt(&mut self, node: &mut Stmt, _: &mut TraverseCtx) {
        self.stmt_ptr = Some(node as *const Stmt);
        self.stmt_ptr_stack.push(node as *const Stmt);
    }

    fn exit_stmt(&mut self, _: &mut Stmt, _: &mut TraverseCtx) {
        self.stmt_ptr = self.stmt_ptr_stack.pop();
    }

    fn enter_expr(&mut self, expr: &mut Expr, ctx: &mut TraverseCtx) {
        let Expr::Assign(assign_expr) = expr else {
            return;
        };

        // Check if this is a logical assignment operator
        let op = match assign_expr.op {
            AssignOp::AndAssign => BinaryOp::LogicalAnd,
            AssignOp::OrAssign => BinaryOp::LogicalOr,
            AssignOp::NullishAssign => BinaryOp::NullishCoalescing,
            _ => return,
        };

        // Only handle simple assignment targets
        let AssignTarget::Simple(left) = &mut assign_expr.left else {
            return;
        };

        let span = assign_expr.span;

        // Generate left expression and assignment target
        let (left_expr, assign_target) = match left {
            // `a &&= c` -> `a && (a = c)`

Review comment (suggestion):
In the `exit_stmt` method, `pop()` is called without checking if the stack is empty, but the returned value is assigned to `stmt_ptr`. If the stack becomes empty after popping, `stmt_ptr` will be set to `None`. However, in `enter_stmt`, the same pointer is both assigned to `stmt_ptr` and pushed to the stack. This creates a mismatch: when exiting the top-level statement, the stack becomes empty but `stmt_ptr` is set to `None` instead of the parent statement's pointer. This logic should either use the last element of the stack as `stmt_ptr` or pop and discard without reassigning to `stmt_ptr`.
```suggestion
        self.stmt_ptr_stack.pop();
        self.stmt_ptr = self.stmt_ptr_stack.last().copied();
```

Code after:

use swc_common::{util::take::Take, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_hooks::VisitMutHook;
use swc_ecma_utils::alias_ident_for;

use crate::TraverseCtx;

pub fn hook() -> impl VisitMutHook<TraverseCtx> {
    LogicalAssignmentOperatorsPass::default()
}

#[derive(Default)]
struct LogicalAssignmentOperatorsPass {
    stmt_ptr: Option<*const Stmt>,
    stmt_ptr_stack: Vec<*const Stmt>,
}

impl VisitMutHook<TraverseCtx> for LogicalAssignmentOperatorsPass {
    fn enter_stmt(&mut self, node: &mut Stmt, _: &mut TraverseCtx) {
        self.stmt_ptr = Some(node as *const Stmt);
        self.stmt_ptr_stack.push(node as *const Stmt);
    }

    fn exit_stmt(&mut self, _stmt: &mut Stmt, _ctx: &mut TraverseCtx) {
        self.stmt_ptr_stack.pop();
        self.stmt_ptr = self.stmt_ptr_stack.last().copied();
    }

    fn enter_expr(&mut self, expr: &mut Expr, ctx: &mut TraverseCtx) {
        let Expr::Assign(assign_expr) = expr else {
            return;
        };

        // Check if this is a logical assignment operator
        let op = match assign_expr.op {
            AssignOp::AndAssign => BinaryOp::LogicalAnd,
            AssignOp::OrAssign => BinaryOp::LogicalOr,
            AssignOp::NullishAssign => BinaryOp::NullishCoalescing,
            _ => return,
        };

        // Only handle simple assignment targets
        let AssignTarget::Simple(left) = &mut assign_expr.left else {
            return;
        };

        let span = assign_expr.span;

        // Generate left expression and assignment target
        let (left_expr, assign_target) = match left {