Skip to content

Commit dbd4506

Browse files
rix0rrriliapolo
andauthoredMar 5, 2025
fix: failure on anonymous class with a method (#1824)
`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`. Fixes aws/aws-cdk#33213 --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0 --------- Co-authored-by: Eli Polonsky <Eli.polonsky@gmail.com>
1 parent f44080e commit dbd4506

File tree

5 files changed

+155
-17
lines changed

5 files changed

+155
-17
lines changed
 

‎fixtures/jsii-calc/lib/decorators.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 {
15+
const ret = class extends x {
16+
constructor() {
17+
super();
18+
this.state = this.state + this.state;
19+
}
20+
};
21+
22+
// This adds a field to the class, but we can't reflect that in the type because of the limitations
23+
// of decorators. That's we advertise it through interface merging below.
24+
(ret.prototype as any)['field'] = 'some_added_field';
25+
26+
return ret;
27+
}
28+
29+
@classDecorator
30+
export class SomeDecoratedClass {
31+
protected state = 'state';
32+
33+
public accessState() {
34+
return this.state;
35+
}
36+
}
37+
38+
export interface SomeDecoratedClass {
39+
readonly field: string;
40+
}
41+
42+
/**
43+
* Exercise the above code
44+
*/
45+
function tryDecoratedClass() {
46+
const instance = new SomeDecoratedClass();
47+
return instance.field;
48+
}
49+
// Suppress unused locals warnings
50+
void tryDecoratedClass;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 from
5+
* the jsii root module.
6+
*
7+
* The reason is that when we add deprecation warnings we visit all functions in all files.
8+
*/
9+
export function propertyInjectionDecorator<T extends Constructor>(ctr: T) {
10+
// Important for the bug: the anonymous class extends something, *and*
11+
// declares a method.
12+
return class extends ctr {
13+
public someMethod(): string {
14+
return 'abc';
15+
}
16+
};
17+
}

‎fixtures/jsii-calc/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './stability';
99
export * from './submodules';
1010
export * from './container-types';
1111
export * from './indirect-implementation';
12+
export * from './decorators';
1213

1314
export * as submodule from './submodule';
1415
export * as onlystatic from './only-static';

‎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) {

‎test/__snapshots__/integration.test.ts.snap

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

0 commit comments

Comments
 (0)
Failed to load comments.