Skip to content

Commit c91e88b

Browse files
committed
fix: fails when a method returns an anonymous class with a method
`jsii` used to fail when trying to generate deprecation warnings and given a statement like this: ```ts export function propertyInjectionDecorator<T extends Constructor>(ctr: T) { // Important for the bug: the anonymous class extends something, *and* // declares a method. return class extends ctr { public someMethod(): string { return 'abc'; } }; } ``` The reason is that during generation of deprecation warnings we iterate over all classes and methods (both exported and unexported), and generate a symbol identifier for every method; but this class is anonymous so it doesn't have a symbol, and generating the identifier fails with the error: ``` TypeError: Cannot read properties of undefined (reading 'flags') at symbolIdentifier (.../jsii/lib/common/symbol-id.js:40:17) ``` Handle this by handling the case where we don't have a symbol and returning `undefined`.
1 parent 9157e28 commit c91e88b

File tree

7 files changed

+46
-14
lines changed

7 files changed

+46
-14
lines changed

fixtures/jsii-calc/lib/decorators.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
function classDecorator(x: typeof SomeDecoratedClass, _context: ClassDecoratorContext): typeof SomeDecoratedClass {
1+
type Constructor = { new (...args: any[]): {} };
2+
3+
export function functionReturnsClasstype<T extends Constructor>(ctr: T) {
4+
return class extends ctr {
5+
};
6+
}
7+
8+
/**
9+
* A class decorator that changes inherited state and adds a readonly field to the class.
10+
*
11+
* This wasn't the thing that was exploding, see `function-returning-anonymous-class.ts` for that.
12+
* Nevertheless, this makes for a good class decorator demo.
13+
*/
14+
export function classDecorator(x: typeof SomeDecoratedClass): typeof SomeDecoratedClass {
215
const ret = class extends x {
316
constructor() {
417
super();
@@ -8,27 +21,22 @@ function classDecorator(x: typeof SomeDecoratedClass, _context: ClassDecoratorCo
821

922
// This adds a field to the class, but we can't reflect that in the type because of the limitations
1023
// of decorators. That's we advertise it through interface merging below.
11-
ret.prototype['field'] = 'some_added_field';
24+
(ret.prototype as any)['field'] = 'some_added_field';
1225

1326
return ret;
1427
}
1528

16-
function methodDecorator<A extends Function>(x: A, _context: ClassMethodDecoratorContext): A {
17-
return x;
18-
}
19-
2029
@classDecorator
2130
export class SomeDecoratedClass {
2231
protected state = 'state';
2332

24-
@methodDecorator
2533
public accessState() {
2634
return this.state;
2735
}
2836
}
2937

3038
export interface SomeDecoratedClass {
31-
field: string;
39+
readonly field: string;
3240
}
3341

3442
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type Constructor = { new (...args: any[]): {} };
2+
3+
/**
4+
* Just the mere presence of this function is enough to break jsii, even if it's not exported
5+
*
6+
* The reason is that when we add deprecation warnings we visit all functions in all files.
7+
*/
8+
export function propertyInjectionDecorator<T extends Constructor>(ctr: T) {
9+
// Important for the bug: the anonymous class extends something, *and*
10+
// declares a method.
11+
return class extends ctr {
12+
public someMethod(): string {
13+
return 'abc';
14+
}
15+
};
16+
}

src/common/symbol-id.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,13 @@ interface SymbolIdOptions {
4949
*/
5050
export function symbolIdentifier(
5151
typeChecker: ts.TypeChecker,
52-
sym: ts.Symbol,
52+
sym: ts.Symbol | undefined,
5353
options: SymbolIdOptions = {},
5454
): string | undefined {
55+
if (!sym) {
56+
return undefined;
57+
}
58+
5559
// If this symbol happens to be an alias, resolve it first
5660
// eslint-disable-next-line no-bitwise
5761
while ((sym.flags & ts.SymbolFlags.Alias) !== 0) {

src/transforms/deprecation-warnings.ts

+3
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ class Transformer {
469469
private getStatementsForDeclaration(
470470
node: ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.ConstructorDeclaration,
471471
): ts.Statement[] {
472+
const printer = ts.createPrinter();
473+
console.log(printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()));
474+
472475
const klass = node.parent;
473476
const classSymbolId = symbolIdentifier(this.typeChecker, this.typeChecker.getTypeAtLocation(klass).symbol);
474477
if (classSymbolId && this.typeIndex.has(classSymbolId)) {

src/tsconfig/compiler-options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Case from '../case';
44
export const BASE_COMPILER_OPTIONS: ts.CompilerOptions = {
55
alwaysStrict: true,
66
declaration: true,
7+
experimentalDecorators: true,
78
incremental: true,
89
lib: ['lib.es2020.d.ts'],
910
module: ts.ModuleKind.CommonJS,

test/__snapshots__/integration.test.ts.snap

+5-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/tsconfig/validator.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,3 @@ describe('Object Validator', () => {
235235
function anythingExceptUndefined() {
236236
return fc.anything().filter((x) => x !== undefined);
237237
}
238-

0 commit comments

Comments
 (0)