Skip to content

Commit

Permalink
fix: look-up enum type by symbol expression
Browse files Browse the repository at this point in the history
  • Loading branch information
sean-perkins committed Sep 1, 2023
1 parent 40096d3 commit c729054
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const componentDecoratorToStatic = (
newMembers: ts.ClassElement[],
componentDecorator: ts.Decorator
) => {
const [componentOptions] = getDeclarationParameters<d.ComponentOptions>(componentDecorator);
const [componentOptions] = getDeclarationParameters<d.ComponentOptions>(componentDecorator, typeChecker);
if (!componentOptions) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 20 additions & 14 deletions src/compiler/transformers/decorators-to-static/decorator-utils.ts
Original file line number Diff line number Diff line change
@@ -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()}`);
Expand All @@ -43,7 +49,7 @@ export const isDecoratorNamed = (propName: string) => {
};

export interface GetDeclarationParameters {
<T>(decorator: ts.Decorator): [T];
<T, T1>(decorator: ts.Decorator): [T, T1];
<T, T1, T2>(decorator: ts.Decorator): [T, T1, T2];
<T>(decorator: ts.Decorator, typeChecker: ts.TypeChecker): [T];
<T, T1>(decorator: ts.Decorator, typeChecker: ts.TypeChecker): [T, T1];
<T, T1, T2>(decorator: ts.Decorator, typeChecker: ts.TypeChecker): [T, T1, T2];
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const parseEventDecorator = (
return null;
}

const [eventOpts] = getDeclarationParameters<d.EventOptions>(eventDecorator);
const [eventOpts] = getDeclarationParameters<d.EventOptions>(eventDecorator, typeChecker);
const symbol = typeChecker.getSymbolAtLocation(prop.name);
const eventName = getEventName(eventOpts, memberName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,32 @@ 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) {
newMembers.push(createStaticGetter('listeners', convertValueToLiteral(flatListeners)));
}
};

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 [];
}

return listenDecorators.map((listenDecorator) => {
const methodName = method.name.getText();
const [listenText, listenOptions] = getDeclarationParameters<string, d.ListenOptions>(listenDecorator);
const [listenText, listenOptions] = getDeclarationParameters<string, d.ListenOptions>(listenDecorator, typeChecker);

const eventNames = listenText.split(',');
if (eventNames.length > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const parsePropDecorator = (
return null;
}

const decoratorParams = getDeclarationParameters<d.PropOptions>(propDecorator);
const decoratorParams = getDeclarationParameters<d.PropOptions>(propDecorator, typeChecker);
const propOptions: d.PropOptions = decoratorParams[0] || {};

const propName = prop.name.getText();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export const watchDecoratorsToStatic = (
diagnostics: d.Diagnostic[],
decoratedProps: ts.ClassElement[],
watchable: Set<string>,
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);

Expand All @@ -26,11 +27,12 @@ const parseWatchDecorator = (
config: d.Config,
diagnostics: d.Diagnostic[],
watchable: Set<string>,
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<string>(decorator);
const [propName] = getDeclarationParameters<string>(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.
Expand Down
66 changes: 66 additions & 0 deletions src/compiler/transformers/test/decorator-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit c729054

Please sign in to comment.