diff --git a/src/js_parser/visit/visit_binary.rs b/src/js_parser/visit/visit_binary.rs index e86ba8da5398..5aac0f05ad35 100644 --- a/src/js_parser/visit/visit_binary.rs +++ b/src/js_parser/visit/visit_binary.rs @@ -120,8 +120,16 @@ impl BinaryExpressionVisitor { let was_anonymous_named_expr = e_.right.is_anonymous_named(); let prev_decorator_class_name = p.decorator_class_name; - // Propagate name for anonymous decorated class expressions in assignments - if e_.op == Op::Code::BinAssign && was_anonymous_named_expr { + // Only these four assignment operators perform NamedEvaluation of the right operand. + if was_anonymous_named_expr + && matches!( + e_.op, + Op::Code::BinAssign + | Op::Code::BinNullishCoalescingAssign + | Op::Code::BinLogicalOrAssign + | Op::Code::BinLogicalAndAssign + ) + { if let ExprData::EClass(class) = &e_.right.data { if class.should_lower_standard_decorators { if let ExprData::EIdentifier(ident) = e_.left.data { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index 41845a031c45..bc70cb6341c9 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -1396,4 +1396,123 @@ describe("ES Decorators", () => { expect(exitCode).toBe(0); }); }); + + describe("anonymous decorated class assigned with a logical assignment operator", () => { + // `x ??= class {}` names the class "x" exactly like `x = class {}` does (the + // same goes for `||=` and `&&=`). The lowering rewrites the class to + // `_class = class {}`, so the parser has to record the target name for these + // operators the way it already does for `=`. The expected `.name` values are + // what node prints for the same code with the decorators (and the `accessor` + // member, which node does not parse) removed. + test.concurrent("??=, ||= and &&= name the class after the identifier target", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + let Nullish; Nullish ??= class { @dec m() {} }; + let Or = null; Or ||= class { @dec m() {} }; + let And = 1; And &&= class { @dec m() {} }; + let Assigned; Assigned = class { @dec m() {} }; + let Outer, Inner; Outer = Inner ??= class { @dec m() {} }; + let InFunction; (() => { InFunction ||= class { @dec m() {} }; })(); + let AccessorOnly; AccessorOnly ??= class { accessor v = 1; }; + console.log(JSON.stringify({ + nullish: Nullish.name, + or: Or.name, + and: And.name, + assigned: Assigned.name, + chained: Outer.name, + inFunction: InFunction.name, + accessorOnly: [AccessorOnly.name, new AccessorOnly().v], + })); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + nullish: "Nullish", + or: "Or", + and: "And", + assigned: "Assigned", + chained: "Inner", + inFunction: "InFunction", + accessorOnly: ["AccessorOnly", 1], + }); + expect(exitCode).toBe(0); + }); + + test.concurrent("class decorator sees the target name as context.name", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const contextNames = []; + function dec(cls, ctx) { contextNames.push(ctx.name); } + let Nullish; Nullish ??= @dec class {}; + let Or = null; Or ||= @dec class {}; + let And = 1; And &&= @dec class {}; + console.log(JSON.stringify({ + contextNames, + classNames: [Nullish.name, Or.name, And.name], + })); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + contextNames: ["Nullish", "Or", "And"], + classNames: ["Nullish", "Or", "And"], + }); + expect(exitCode).toBe(0); + }); + + test.concurrent("the target name is only used for an anonymous class directly on the right", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const contextNames = []; + function dec(cls, ctx) { contextNames.push(ctx.name); } + let Explicit; Explicit ??= @dec class Named {}; + const holder = {}; holder.member ??= @dec class {}; + let Nested; Nested ||= [@dec class {}][0]; + let Concatenated = ""; Concatenated += @dec class {}; + console.log(JSON.stringify({ + classNames: [Explicit.name, holder.member.name, Nested.name], + contextNames, + })); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + classNames: ["Named", "", ""], + contextNames: ["Named", "", "", ""], + }); + expect(exitCode).toBe(0); + }); + + test.concurrent("the class body still refers to the variable being assigned", async () => { + // Recording the target name must not introduce a class binding of that + // name: `Or` inside the body is the outer variable, as in the source and + // as node prints for this program with the decorators removed. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec() {} + function replace(cls) { return class extends cls {}; } + let Or = null; + Or ||= class { static seenDuringDefinition = Or; @dec m() { return Or; } }; + const OrOriginal = Or; + Or = "reassigned"; + let And = 1; + And &&= class { static seenDuringDefinition = And; @dec m() {} }; + let Replaced; + Replaced ??= @replace class { static self() { return Replaced; } }; + let Assigned; + Assigned ??= class { @dec m() { Assigned = "assigned from the body"; } }; + new Assigned().m(); + console.log(JSON.stringify({ + names: [OrOriginal.name, And.name], + seenDuringDefinition: [OrOriginal.seenDuringDefinition === null, And.seenDuringDefinition === 1], + methodSeesReassignment: new OrOriginal().m(), + bodySeesReplacement: Replaced.self() === Replaced, + assigned: Assigned, + })); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + names: ["Or", "And"], + seenDuringDefinition: [true, true], + methodSeesReassignment: "reassigned", + bodySeesReplacement: true, + assigned: "assigned from the body", + }); + expect(exitCode).toBe(0); + }); + }); });