Skip to content

Commit

Permalink
fix: failure on anonymous class with a method (#1824)
Browse files Browse the repository at this point in the history
`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>
(cherry picked from commit dbd4506)
  • Loading branch information
rix0rrr committed Mar 10, 2025
1 parent 1840c41 commit 9a26124
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 17 deletions.
50 changes: 50 additions & 0 deletions fixtures/jsii-calc/lib/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
type Constructor = { new (...args: any[]): {} };

export function functionReturnsClasstype<T extends Constructor>(ctr: T) {
return class extends ctr {
};
}

/**
* A class decorator that changes inherited state and adds a readonly field to the class.
*
* This wasn't the thing that was exploding, see `function-returning-anonymous-class.ts` for that.
* Nevertheless, this makes for a good class decorator demo.
*/
export function classDecorator(x: typeof SomeDecoratedClass): typeof SomeDecoratedClass {
const ret = class extends x {
constructor() {
super();
this.state = this.state + this.state;
}
};

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

return ret;
}

@classDecorator
export class SomeDecoratedClass {
protected state = 'state';

public accessState() {
return this.state;
}
}

export interface SomeDecoratedClass {
readonly field: string;
}

/**
* Exercise the above code
*/
function tryDecoratedClass() {
const instance = new SomeDecoratedClass();
return instance.field;
}
// Suppress unused locals warnings
void tryDecoratedClass;
17 changes: 17 additions & 0 deletions fixtures/jsii-calc/lib/function-returning-anonymous-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type Constructor = { new (...args: any[]): {} };

/**
* Just the mere presence of this function is enough to break jsii, even if it's not exported from
* the jsii root module.
*
* The reason is that when we add deprecation warnings we visit all functions in all files.
*/
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';
}
};
}
1 change: 1 addition & 0 deletions fixtures/jsii-calc/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './stability';
export * from './submodules';
export * from './container-types';
export * from './indirect-implementation';
export * from './decorators';

export * as submodule from './submodule';
export * as onlystatic from './only-static';
Expand Down
6 changes: 5 additions & 1 deletion src/common/symbol-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ interface SymbolIdOptions {
*/
export function symbolIdentifier(
typeChecker: ts.TypeChecker,
sym: ts.Symbol,
sym: ts.Symbol | undefined,
options: SymbolIdOptions = {},
): string | undefined {
if (!sym) {
return undefined;
}

// If this symbol happens to be an alias, resolve it first
// eslint-disable-next-line no-bitwise
while ((sym.flags & ts.SymbolFlags.Alias) !== 0) {
Expand Down
98 changes: 82 additions & 16 deletions test/__snapshots__/integration.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9a26124

Please # to comment.