From 65cc0c8bd6ac828a1dde1d5279066f9ae5b7ad0c Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Fri, 24 Jul 2020 13:22:28 -0700 Subject: [PATCH] fix(compiler-cli): Add support for string literal class members (#38226) The current implementation of the TypeScriptReflectionHost does not account for members that are string literals, i.e. `class A { 'string-literal-prop': string; }` PR Close #38226 --- .../src/ngtsc/reflection/src/host.ts | 6 +- .../src/ngtsc/reflection/src/typescript.ts | 5 +- .../src/ngtsc/reflection/test/ts_host_spec.ts | 91 ++++++++++++++++++- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index b9174520b6..bb46477213 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -152,13 +152,13 @@ export interface ClassMember { name: string; /** - * TypeScript `ts.Identifier` representing the name of the member, or `null` if no such node - * is present. + * TypeScript `ts.Identifier` or `ts.StringLiteral` representing the name of the member, or `null` + * if no such node is present. * * The `nameNode` is useful in writing references to this member that will be correctly source- * mapped back to the original file. */ - nameNode: ts.Identifier|null; + nameNode: ts.Identifier|ts.StringLiteral|null; /** * TypeScript `ts.Expression` which represents the value of the member. diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index 2bdcb11404..56b54b24dc 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -363,7 +363,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { let kind: ClassMemberKind|null = null; let value: ts.Expression|null = null; let name: string|null = null; - let nameNode: ts.Identifier|null = null; + let nameNode: ts.Identifier|ts.StringLiteral|null = null; if (ts.isPropertyDeclaration(node)) { kind = ClassMemberKind.Property; @@ -385,6 +385,9 @@ export class TypeScriptReflectionHost implements ReflectionHost { } else if (ts.isIdentifier(node.name)) { name = node.name.text; nameNode = node.name; + } else if (ts.isStringLiteral(node.name)) { + name = node.name.text; + nameNode = node.name; } else { return null; } diff --git a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts index 1093fe257d..54ce4125d2 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {getDeclaration, makeProgram} from '../../testing'; -import {CtorParameter} from '../src/host'; +import {ClassMember, ClassMemberKind, CtorParameter} from '../src/host'; import {TypeScriptReflectionHost} from '../src/typescript'; import {isNamedClassDeclaration} from '../src/util'; @@ -450,6 +450,95 @@ runInEachFileSystem(() => { ]); }); }); + + describe('getMembersOfClass()', () => { + it('should get string literal members of class', () => { + const {program} = makeProgram([{ + name: _('/entry.ts'), + contents: ` + class Foo { + 'string-literal-property-member' = 'my value'; + } + ` + }]); + const members = getMembers(program); + expect(members.length).toBe(1); + expectMember(members[0], 'string-literal-property-member', ClassMemberKind.Property); + }); + + it('should retrieve method members', () => { + const {program} = makeProgram([{ + name: _('/entry.ts'), + contents: ` + class Foo { + myMethod(): void { + } + } + ` + }]); + const members = getMembers(program); + expect(members.length).toBe(1); + expectMember(members[0], 'myMethod', ClassMemberKind.Method); + }); + + it('should retrieve constructor as member', () => { + const {program} = makeProgram([{ + name: _('/entry.ts'), + contents: ` + class Foo { + constructor() {} + } + ` + }]); + const members = getMembers(program); + expect(members.length).toBe(1); + expectMember(members[0], 'constructor', ClassMemberKind.Constructor); + }); + + it('should retrieve decorators of member', () => { + const {program} = makeProgram([{ + name: _('/entry.ts'), + contents: ` + declare var Input; + + class Foo { + @Input() + prop: string; + } + ` + }]); + const members = getMembers(program); + expect(members.length).toBe(1); + expect(members[0].decorators).not.toBeNull(); + expect(members[0].decorators![0].name).toBe('Input'); + }); + + it('identifies static members', () => { + const {program} = makeProgram([{ + name: _('/entry.ts'), + contents: ` + class Foo { + static staticMember = ''; + } + ` + }]); + const members = getMembers(program); + expect(members.length).toBe(1); + expect(members[0].isStatic).toBeTrue(); + }); + + function getMembers(program: ts.Program) { + const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + return host.getMembersOfClass(clazz); + } + + function expectMember(member: ClassMember, name: string, kind: ClassMemberKind) { + expect(member.name).toEqual(name); + expect(member.kind).toEqual(kind); + } + }); }); function expectParameter(