From c7290545a98cae394d60da0f08a34fd2d06c68ee Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 1 Sep 2023 15:52:51 -0400 Subject: [PATCH] fix: look-up enum type by symbol expression --- .../component-decorator.ts | 2 +- .../convert-decorators.ts | 4 +- .../decorators-to-static/decorator-utils.ts | 34 ++++++---- .../decorators-to-static/event-decorator.ts | 2 +- .../decorators-to-static/listen-decorator.ts | 11 +++- .../decorators-to-static/prop-decorator.ts | 2 +- .../decorators-to-static/watch-decorator.ts | 8 ++- .../transformers/test/decorator-utils.spec.ts | 66 +++++++++++++++++++ 8 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 src/compiler/transformers/test/decorator-utils.spec.ts diff --git a/src/compiler/transformers/decorators-to-static/component-decorator.ts b/src/compiler/transformers/decorators-to-static/component-decorator.ts index dfbf516bc1b4..3005059bd066 100644 --- a/src/compiler/transformers/decorators-to-static/component-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/component-decorator.ts @@ -13,7 +13,7 @@ export const componentDecoratorToStatic = ( newMembers: ts.ClassElement[], componentDecorator: ts.Decorator ) => { - const [componentOptions] = getDeclarationParameters(componentDecorator); + const [componentOptions] = getDeclarationParameters(componentDecorator, typeChecker); if (!componentOptions) { return; } diff --git a/src/compiler/transformers/decorators-to-static/convert-decorators.ts b/src/compiler/transformers/decorators-to-static/convert-decorators.ts index 798bb69c6cda..8c24b3200101 100644 --- a/src/compiler/transformers/decorators-to-static/convert-decorators.ts +++ b/src/compiler/transformers/decorators-to-static/convert-decorators.ts @@ -63,8 +63,8 @@ export const visitClassDeclaration = ( eventDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, newMembers); methodDecoratorsToStatic(config, diagnostics, classNode, decoratedMembers, typeChecker, newMembers); elementDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, newMembers); - watchDecoratorsToStatic(config, diagnostics, decoratedMembers, watchable, newMembers); - listenDecoratorsToStatic(diagnostics, decoratedMembers, newMembers); + watchDecoratorsToStatic(config, diagnostics, decoratedMembers, watchable, typeChecker, newMembers); + listenDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, newMembers); } validateMethods(diagnostics, classMembers); diff --git a/src/compiler/transformers/decorators-to-static/decorator-utils.ts b/src/compiler/transformers/decorators-to-static/decorator-utils.ts index ec3b7cacb09e..f3e9932cbe23 100644 --- a/src/compiler/transformers/decorators-to-static/decorator-utils.ts +++ b/src/compiler/transformers/decorators-to-static/decorator-utils.ts @@ -1,27 +1,33 @@ import { objectLiteralToObjectMap } from '../transform-utils'; import ts from 'typescript'; -export const getDeclarationParameters: GetDeclarationParameters = (decorator: ts.Decorator): any => { +export const getDeclarationParameters: GetDeclarationParameters = ( + decorator: ts.Decorator, + typeChecker: ts.TypeChecker +): any => { if (!ts.isCallExpression(decorator.expression)) { return []; } - return decorator.expression.arguments.map(getDeclarationParameter); + return decorator.expression.arguments.map((arg) => getDeclarationParameter(arg, typeChecker)); }; -const getDeclarationParameter = (arg: ts.Expression): any => { +const getDeclarationParameter = (arg: ts.Expression, typeChecker: ts.TypeChecker): any => { if (ts.isObjectLiteralExpression(arg)) { return objectLiteralToObjectMap(arg); } else if (ts.isStringLiteral(arg)) { return arg.text; } else if (ts.isPropertyAccessExpression(arg)) { - /** - * Enum members are property access expressions, so we can evaluate them - * to get the enum member value as a string. - * - * This enables developers to use enum members in decorators. - * e.g. @Watch(MyEnum.VALUE) - */ - return arg.name.getText(); + const symbol = typeChecker.getTypeAtLocation(arg); + if (symbol !== undefined && symbol.isLiteral()) { + /** + * Enum members are property access expressions, so we can evaluate them + * to get the enum member value as a string. + * + * This enables developers to use enum members in decorators. + * e.g. @Watch(MyEnum.VALUE) + */ + return symbol.value; + } } throw new Error(`invalid decorator argument: ${arg.getText()}`); @@ -43,7 +49,7 @@ export const isDecoratorNamed = (propName: string) => { }; export interface GetDeclarationParameters { - (decorator: ts.Decorator): [T]; - (decorator: ts.Decorator): [T, T1]; - (decorator: ts.Decorator): [T, T1, T2]; + (decorator: ts.Decorator, typeChecker: ts.TypeChecker): [T]; + (decorator: ts.Decorator, typeChecker: ts.TypeChecker): [T, T1]; + (decorator: ts.Decorator, typeChecker: ts.TypeChecker): [T, T1, T2]; } diff --git a/src/compiler/transformers/decorators-to-static/event-decorator.ts b/src/compiler/transformers/decorators-to-static/event-decorator.ts index 3a51cf717c54..d93b94d3e4a6 100644 --- a/src/compiler/transformers/decorators-to-static/event-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/event-decorator.ts @@ -43,7 +43,7 @@ const parseEventDecorator = ( return null; } - const [eventOpts] = getDeclarationParameters(eventDecorator); + const [eventOpts] = getDeclarationParameters(eventDecorator, typeChecker); const symbol = typeChecker.getSymbolAtLocation(prop.name); const eventName = getEventName(eventOpts, memberName); diff --git a/src/compiler/transformers/decorators-to-static/listen-decorator.ts b/src/compiler/transformers/decorators-to-static/listen-decorator.ts index ca53086d9449..17da5f699182 100644 --- a/src/compiler/transformers/decorators-to-static/listen-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/listen-decorator.ts @@ -7,11 +7,12 @@ import ts from 'typescript'; export const listenDecoratorsToStatic = ( diagnostics: d.Diagnostic[], decoratedMembers: ts.ClassElement[], + typeChecker: ts.TypeChecker, newMembers: ts.ClassElement[] ) => { const listeners = decoratedMembers .filter(ts.isMethodDeclaration) - .map((method) => parseListenDecorators(diagnostics, method)); + .map((method) => parseListenDecorators(diagnostics, method, typeChecker)); const flatListeners = flatOne(listeners); if (flatListeners.length > 0) { @@ -19,7 +20,11 @@ export const listenDecoratorsToStatic = ( } }; -const parseListenDecorators = (diagnostics: d.Diagnostic[], method: ts.MethodDeclaration) => { +const parseListenDecorators = ( + diagnostics: d.Diagnostic[], + method: ts.MethodDeclaration, + typeChecker: ts.TypeChecker +) => { const listenDecorators = method.decorators.filter(isDecoratorNamed('Listen')); if (listenDecorators.length === 0) { return []; @@ -27,7 +32,7 @@ const parseListenDecorators = (diagnostics: d.Diagnostic[], method: ts.MethodDec return listenDecorators.map((listenDecorator) => { const methodName = method.name.getText(); - const [listenText, listenOptions] = getDeclarationParameters(listenDecorator); + const [listenText, listenOptions] = getDeclarationParameters(listenDecorator, typeChecker); const eventNames = listenText.split(','); if (eventNames.length > 1) { diff --git a/src/compiler/transformers/decorators-to-static/prop-decorator.ts b/src/compiler/transformers/decorators-to-static/prop-decorator.ts index 3e70622d5747..fd3a0f354c24 100644 --- a/src/compiler/transformers/decorators-to-static/prop-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/prop-decorator.ts @@ -62,7 +62,7 @@ const parsePropDecorator = ( return null; } - const decoratorParams = getDeclarationParameters(propDecorator); + const decoratorParams = getDeclarationParameters(propDecorator, typeChecker); const propOptions: d.PropOptions = decoratorParams[0] || {}; const propName = prop.name.getText(); diff --git a/src/compiler/transformers/decorators-to-static/watch-decorator.ts b/src/compiler/transformers/decorators-to-static/watch-decorator.ts index 8efc3243ac35..8d4faa0e8ffc 100644 --- a/src/compiler/transformers/decorators-to-static/watch-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/watch-decorator.ts @@ -9,11 +9,12 @@ export const watchDecoratorsToStatic = ( diagnostics: d.Diagnostic[], decoratedProps: ts.ClassElement[], watchable: Set, + typeChecker: ts.TypeChecker, newMembers: ts.ClassElement[] ) => { const watchers = decoratedProps .filter(ts.isMethodDeclaration) - .map((method) => parseWatchDecorator(config, diagnostics, watchable, method)); + .map((method) => parseWatchDecorator(config, diagnostics, watchable, method, typeChecker)); const flatWatchers = flatOne(watchers); @@ -26,11 +27,12 @@ const parseWatchDecorator = ( config: d.Config, diagnostics: d.Diagnostic[], watchable: Set, - method: ts.MethodDeclaration + method: ts.MethodDeclaration, + typeChecker: ts.TypeChecker ): d.ComponentCompilerWatch[] => { const methodName = method.name.getText(); return method.decorators.filter(isDecoratorNamed('Watch')).map((decorator) => { - const [propName] = getDeclarationParameters(decorator); + const [propName] = getDeclarationParameters(decorator, typeChecker); if (!watchable.has(propName)) { const diagnostic = config.devMode ? buildWarn(diagnostics) : buildError(diagnostics); diagnostic.messageText = `@Watch('${propName}') is trying to watch for changes in a property that does not exist. diff --git a/src/compiler/transformers/test/decorator-utils.spec.ts b/src/compiler/transformers/test/decorator-utils.spec.ts new file mode 100644 index 000000000000..cc56e6071cc2 --- /dev/null +++ b/src/compiler/transformers/test/decorator-utils.spec.ts @@ -0,0 +1,66 @@ +import { getDeclarationParameters, isDecoratorNamed } from '../decorators-to-static/decorator-utils'; +import ts from 'typescript'; + +describe('decorator utils', () => { + describe('getDeclarationParameters', () => { + it('should return an empty array for decorator with no arguments', () => { + const decorator: ts.Decorator = { + expression: ts.factory.createIdentifier('DecoratorName'), + } as unknown as ts.Decorator; + + const typeCheckerMock = {} as ts.TypeChecker; + const result = getDeclarationParameters(decorator, typeCheckerMock); + + expect(result).toEqual([]); + }); + + it('should return correct parameters for decorator with multiple string arguments', () => { + const decorator: ts.Decorator = { + expression: ts.factory.createCallExpression(ts.factory.createIdentifier('DecoratorName'), undefined, [ + ts.factory.createStringLiteral('arg1'), + ts.factory.createStringLiteral('arg2'), + ]), + } as unknown as ts.Decorator; + + const typeCheckerMock = {} as ts.TypeChecker; + const result = getDeclarationParameters(decorator, typeCheckerMock); + + expect(result).toEqual(['arg1', 'arg2']); + }); + + it('should return enum value for enum member used in decorator', () => { + const typeCheckerMock = { + getTypeAtLocation: jest.fn(() => ({ + value: 'arg1', + isLiteral: () => true, + })), + } as unknown as ts.TypeChecker; + + const decorator: ts.Decorator = { + expression: ts.factory.createCallExpression(ts.factory.createIdentifier('DecoratorName'), undefined, [ + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('EnumName'), + ts.factory.createIdentifier('EnumMemberName') + ), + ]), + } as unknown as ts.Decorator; + + const result = getDeclarationParameters(decorator, typeCheckerMock); + + expect(result).toEqual(['arg1']); + }); + }); + + describe('isDecoratorNamed', () => { + it('should return false for non-CallExpression decorator', () => { + const decorator: ts.Decorator = { + expression: ts.factory.createIdentifier('DecoratorName'), + } as unknown as ts.Decorator; + + const decoratorCheck = isDecoratorNamed('DecoratorName'); + const result = decoratorCheck(decorator); + + expect(result).toBe(false); + }); + }); +});