Skip to content
Open
Show file tree
Hide file tree
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
12 changes: 10 additions & 2 deletions src/js_parser/visit/visit_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
119 changes: 119 additions & 0 deletions test/bundler/transpiler/es-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading