Various bugs with class expressions and JavaScript decorators (code run twice, internal compiler crash)
🔎 Search Terms
javascript decorators proposal class expressions crash debug failure error
🕗 Version & Regression Information
- This is a crash
⏯ Playground Link
No response
💻 Code
Case 1: (playground link)
const log: string[] = []
const dec = (x: number, y: number, z: number, t: number) => {
log.push('x' + x)
return (_: any, ctx: DecoratorContext): any => {
log.push('y' + y)
ctx.addInitializer(() => {
log.push('z' + z)
})
if (ctx.kind === 'field') return () => { log.push('f' + t) }
if (ctx.kind === 'accessor') return { init: () => { log.push('a' + t) } }
}
}
const Foo = @dec(0, 4, 2, -1) class {
@dec(1, 3, 3, 1) field: undefined
@dec(2, 2, 0, 0) static field: undefined
@dec(3, 1, 4, 1) accessor accessor: undefined
@dec(4, 0, 1, 0) static accessor accessor: undefined
}
new Foo
const expected = 'x0,x1,x2,x3,x4,y0,y1,y2,y3,y4,f0,z0,a0,z1,z2,f1,z3,a1,z4'
const success = log + '' === expected
console.log('success: ' + success)
if (!success) {
console.log('observed: ' + log)
console.log('expected: ' + expected)
}
Case 2 (playground link):
This is case 1 with the decorator on the class expression removed.
const dec = (x: number, y: number, z: number, t: number) => {
return (_: any, ctx: DecoratorContext): any => {
}
}
const Foo = class {
@dec(1, 3, 3, 1) field: undefined
@dec(2, 2, 0, 0) static field: undefined
@dec(3, 1, 4, 1) accessor accessor: undefined
@dec(4, 0, 1, 0) static accessor accessor: undefined
}
Case 3 (playground link):
This is case 1 with the two instance elements removed.
const dec = (x: number, y: number, z: number, t: number) => {
return (_: any, ctx: DecoratorContext): any => {
}
}
const Foo = @dec(0, 4, 2, -1) class {
@dec(2, 2, 0, 0) static field: undefined
@dec(4, 0, 1, 0) static accessor accessor: undefined
}
🙁 Actual behavior
Case 1:
When run, this prints the following:
[LOG]: "success: false"
[LOG]: "observed: x0,x1,x2,x3,x4,y0,y1,y2,y3,y4,f0,z0,a0,z1,z2,f0,z0,a0,f1,z3,a1,z4"
[LOG]: "expected: x0,x1,x2,x3,x4,y0,y1,y2,y3,y4,f0,z0,a0,z1,z2,f1,z3,a1,z4"
That means:
-
TypeScript's behavior for decorators on class expressions diverges from TypeScript's behavior for class statements. The
expectedoutput was taken from TypeScript's transformation of the same code but as a class statement instead of a class expression, and appears to be correct. -
The code that TypeScript generates evaluates several initializers multiple times. Specifically, static field and static accessor initializers are run twice, which corresponds to duplicated calls to
__runInitializers(_classThis, _static_field_extraInitializers)and__runInitializers(_classThis, _static_accessor_initializers)in the generated code.
Case 2:
This crashes the playground compiler with Error: Debug Failure. and a minified stack trace.
Case 3:
This crashes the playground compiler with Error: Debug Failure. False expression: Undeclared private name for property declaration. and a minified stack trace.
🙂 Expected behavior
TypeScript should transform decorators into code that behaves the same whether or not the original class was an expression or a statement. The compiler should also not crash with debug assertion failures.
For context, I discovered these issues while expanding the coverage of my decorator test suite: https://github.com/evanw/decorator-tests. I intend to use these test cases when adding support for transforming decorators to esbuild.
Additional information about the issue
No response
For Case 1, it looks like the static field is being emit twice, which is triggering initializers twice.
For Case 2 and Case 3, it looks like a combination of target: esnext and useDefineForClassFields: false playing poorly with decorators.