diff --git a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts index eb7a0b4a8c..f6b44cc90f 100644 --- a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -115,7 +115,7 @@ export class ModuleWithProvidersAnalyzer { dtsNgModule.getText()}`); } - return {node: dtsNgModule, known: null, viaModule: null}; + return {node: dtsNgModule, known: null, viaModule: null, identity: null}; } } diff --git a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts index a2ba25cfbf..0ba357416b 100644 --- a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts +++ b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts @@ -156,7 +156,10 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { const reexports: ExportDeclaration[] = []; importedExports.forEach((decl, name) => { if (decl.node !== null) { - reexports.push({name, declaration: {node: decl.node, known: null, viaModule}}); + reexports.push({ + name, + declaration: {node: decl.node, known: null, viaModule, identity: decl.identity} + }); } else { reexports.push( {name, declaration: {node: null, known: null, expression: decl.expression, viaModule}}); @@ -184,7 +187,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { } const viaModule = !importInfo.from.startsWith('.') ? importInfo.from : null; - return {node: importedFile, known: getTsHelperFnFromIdentifier(id), viaModule}; + return {node: importedFile, known: getTsHelperFnFromIdentifier(id), viaModule, identity: null}; } private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 23fdfeacb0..064ece9fc6 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -8,13 +8,14 @@ import * as ts from 'typescript'; -import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, isDecoratorIdentifier, KnownDeclaration, reflectObjectLiteral, TypeScriptReflectionHost, TypeValueReference,} from '../../../src/ngtsc/reflection'; +import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection'; import {isWithinPackage} from '../analysis/util'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils'; import {ClassSymbol, isSwitchableVariableDeclaration, ModuleWithProvidersFunction, NgccClassSymbol, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration} from './ngcc_host'; +import {stripParentheses} from './utils'; export const DECORATORS = 'decorators' as ts.__String; export const PROP_DECORATORS = 'propDecorators' as ts.__String; @@ -344,13 +345,28 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { const superDeclaration = super.getDeclarationOfIdentifier(id); + // If no declaration was found or it's an inline declaration, return as is. + if (superDeclaration === null || superDeclaration.node === null) { + return superDeclaration; + } + + // If the declaration already has traits assigned to it, return as is. + if (superDeclaration.known !== null || superDeclaration.identity !== null) { + return superDeclaration; + } + // The identifier may have been of an additional class assignment such as `MyClass_1` that was // present as alias for `MyClass`. If so, resolve such aliases to their original declaration. - if (superDeclaration !== null && superDeclaration.node !== null && - superDeclaration.known === null) { - const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node); - if (aliasedIdentifier !== null) { - return this.getDeclarationOfIdentifier(aliasedIdentifier); + const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node); + if (aliasedIdentifier !== null) { + return this.getDeclarationOfIdentifier(aliasedIdentifier); + } + + // Variable declarations may represent an enum declaration, so attempt to resolve its members. + if (ts.isVariableDeclaration(superDeclaration.node)) { + const enumMembers = this.resolveEnumMembers(superDeclaration.node); + if (enumMembers !== null) { + superDeclaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers}; } } @@ -1763,10 +1779,189 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N // definition file. This requires default types to be enabled for the host program. return this.src.program.isSourceFileDefaultLibrary(node.getSourceFile()); } + + /** + * In JavaScript, enum declarations are emitted as a regular variable declaration followed by an + * IIFE in which the enum members are assigned. + * + * export var Enum; + * (function (Enum) { + * Enum["a"] = "A"; + * Enum["b"] = "B"; + * })(Enum || (Enum = {})); + * + * @param declaration A variable declaration that may represent an enum + * @returns An array of enum members if the variable declaration is followed by an IIFE that + * declares the enum members, or null otherwise. + */ + protected resolveEnumMembers(declaration: ts.VariableDeclaration): EnumMember[]|null { + // Initialized variables don't represent enum declarations. + if (declaration.initializer !== undefined) return null; + + const variableStmt = declaration.parent.parent; + if (!ts.isVariableStatement(variableStmt)) return null; + + const block = variableStmt.parent; + if (!ts.isBlock(block) && !ts.isSourceFile(block)) return null; + + const declarationIndex = block.statements.findIndex(statement => statement === variableStmt); + if (declarationIndex === -1 || declarationIndex === block.statements.length - 1) return null; + + const subsequentStmt = block.statements[declarationIndex + 1]; + if (!ts.isExpressionStatement(subsequentStmt)) return null; + + const iife = stripParentheses(subsequentStmt.expression); + if (!ts.isCallExpression(iife) || !isEnumDeclarationIife(iife)) return null; + + const fn = stripParentheses(iife.expression); + if (!ts.isFunctionExpression(fn)) return null; + + return this.reflectEnumMembers(fn); + } + + /** + * Attempts to extract all `EnumMember`s from a function that is according to the JavaScript emit + * format for enums: + * + * function (Enum) { + * Enum["MemberA"] = "a"; + * Enum["MemberB"] = "b"; + * } + * + * @param fn The function expression that is assumed to contain enum members. + * @returns All enum members if the function is according to the correct syntax, null otherwise. + */ + private reflectEnumMembers(fn: ts.FunctionExpression): EnumMember[]|null { + if (fn.parameters.length !== 1) return null; + + const enumName = fn.parameters[0].name; + if (!ts.isIdentifier(enumName)) return null; + + const enumMembers: EnumMember[] = []; + for (const statement of fn.body.statements) { + const enumMember = this.reflectEnumMember(enumName, statement); + if (enumMember === null) { + return null; + } + enumMembers.push(enumMember); + } + return enumMembers; + } + + /** + * Attempts to extract a single `EnumMember` from a statement in the following syntax: + * + * Enum["MemberA"] = "a"; + * + * or, for enum member with numeric values: + * + * Enum[Enum["MemberA"] = 0] = "MemberA"; + * + * @param enumName The identifier of the enum that the members should be set on. + * @param statement The statement to inspect. + * @returns An `EnumMember` if the statement is according to the expected syntax, null otherwise. + */ + protected reflectEnumMember(enumName: ts.Identifier, statement: ts.Statement): EnumMember|null { + if (!ts.isExpressionStatement(statement)) return null; + + const expression = statement.expression; + + // Check for the `Enum[X] = Y;` case. + if (!isEnumAssignment(enumName, expression)) { + return null; + } + const assignment = reflectEnumAssignment(expression); + if (assignment != null) { + return assignment; + } + + // Check for the `Enum[Enum[X] = Y] = ...;` case. + const innerExpression = expression.left.argumentExpression; + if (!isEnumAssignment(enumName, innerExpression)) { + return null; + } + return reflectEnumAssignment(innerExpression); + } } ///////////// Exported Helpers ///////////// +/** + * Checks whether the iife has the following call signature: + * + * (Enum || (Enum = {}) + * + * Note that the `Enum` identifier is not checked, as it could also be something + * like `exports.Enum`. Instead, only the structure of binary operators is checked. + * + * @param iife The call expression to check. + * @returns true if the iife has a call signature that corresponds with a potential + * enum declaration. + */ +function isEnumDeclarationIife(iife: ts.CallExpression): boolean { + if (iife.arguments.length !== 1) return false; + + const arg = iife.arguments[0]; + if (!ts.isBinaryExpression(arg) || arg.operatorToken.kind !== ts.SyntaxKind.BarBarToken || + !ts.isParenthesizedExpression(arg.right)) { + return false; + } + + const right = arg.right.expression; + if (!ts.isBinaryExpression(right) || right.operatorToken.kind !== ts.SyntaxKind.EqualsToken) { + return false; + } + + if (!ts.isObjectLiteralExpression(right.right) || right.right.properties.length !== 0) { + return false; + } + + return true; +} + +/** + * An enum member assignment that looks like `Enum[X] = Y;`. + */ +export type EnumMemberAssignment = ts.BinaryExpression&{left: ts.ElementAccessExpression}; + +/** + * Checks whether the expression looks like an enum member assignment targeting `Enum`: + * + * Enum[X] = Y; + * + * Here, X and Y can be any expression. + * + * @param enumName The identifier of the enum that the members should be set on. + * @param expression The expression that should be checked to conform to the above form. + * @returns true if the expression is of the correct form, false otherwise. + */ +function isEnumAssignment( + enumName: ts.Identifier, expression: ts.Expression): expression is EnumMemberAssignment { + if (!ts.isBinaryExpression(expression) || + expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isElementAccessExpression(expression.left)) { + return false; + } + + // Verify that the outer assignment corresponds with the enum declaration. + const enumIdentifier = expression.left.expression; + return ts.isIdentifier(enumIdentifier) && enumIdentifier.text === enumName.text; +} + +/** + * Attempts to create an `EnumMember` from an expression that is believed to represent an enum + * assignment. + * + * @param expression The expression that is believed to be an enum assignment. + * @returns An `EnumMember` or null if the expression did not represent an enum member after all. + */ +function reflectEnumAssignment(expression: EnumMemberAssignment): EnumMember|null { + const memberName = expression.left.argumentExpression; + if (!ts.isPropertyName(memberName)) return null; + + return {name: memberName, initializer: expression.right}; +} + export type ParamInfo = { decorators: Decorator[]|null, typeExpression: ts.Expression|null diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 17fd2134c8..43d3333293 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -13,6 +13,7 @@ import {getNameText, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, import {Esm2015ReflectionHost, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host'; import {NgccClassSymbol} from './ngcc_host'; +import {stripParentheses} from './utils'; /** @@ -849,7 +850,3 @@ function isUndefinedComparison(expression: ts.Expression): expression is ts.Expr expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken && ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left); } - -export function stripParentheses(node: ts.Node): ts.Node { - return ts.isParenthesizedExpression(node) ? node.expression : node; -} diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts index a9ef6bb565..09298082fc 100644 --- a/packages/compiler-cli/ngcc/src/host/umd_host.ts +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -15,7 +15,8 @@ import {BundleProgram} from '../packages/bundle_program'; import {FactoryMap, getTsHelperFnFromIdentifier, stripExtension} from '../utils'; import {ExportDeclaration, ExportStatement, findNamespaceOfIdentifier, findRequireCallReference, isExportStatement, isReexportStatement, isRequireCall, ReexportStatement} from './commonjs_umd_utils'; -import {Esm5ReflectionHost, stripParentheses} from './esm5_host'; +import {Esm5ReflectionHost} from './esm5_host'; +import {stripParentheses} from './utils'; export class UmdReflectionHost extends Esm5ReflectionHost { protected umdModules = @@ -179,7 +180,10 @@ export class UmdReflectionHost extends Esm5ReflectionHost { const reexports: ExportDeclaration[] = []; importedExports.forEach((decl, name) => { if (decl.node !== null) { - reexports.push({name, declaration: {node: decl.node, known: null, viaModule}}); + reexports.push({ + name, + declaration: {node: decl.node, known: null, viaModule, identity: decl.identity} + }); } else { reexports.push( {name, declaration: {node: null, known: null, expression: decl.expression, viaModule}}); @@ -211,7 +215,12 @@ export class UmdReflectionHost extends Esm5ReflectionHost { // We need to add the `viaModule` because the `getExportsOfModule()` call // did not know that we were importing the declaration. - return {node: importedFile, known: getTsHelperFnFromIdentifier(id), viaModule: importInfo.from}; + return { + node: importedFile, + known: getTsHelperFnFromIdentifier(id), + viaModule: importInfo.from, + identity: null + }; } private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile diff --git a/packages/compiler-cli/ngcc/src/host/utils.ts b/packages/compiler-cli/ngcc/src/host/utils.ts new file mode 100644 index 0000000000..bc3df0dae0 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/host/utils.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; + +export function stripParentheses(node: ts.Node): ts.Node { + return ts.isParenthesizedExpression(node) ? node.expression : node; +} diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index b4a4231371..bf8e27921f 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; @@ -1706,6 +1706,7 @@ exports.ExternalModule = ExternalModule; known: knownAs, node: getHelperDeclaration(helperName), viaModule, + identity: null, }); }; @@ -1740,6 +1741,7 @@ exports.ExternalModule = ExternalModule; expect(actualDeclaration).not.toBe(null); expect(actualDeclaration!.node).toBe(expectedDeclarationNode); expect(actualDeclaration!.viaModule).toBe(null); + expect((actualDeclaration as ConcreteDeclaration).identity).toBe(null); }); it('should return the correct declaration for an outer alias identifier', () => { @@ -2197,6 +2199,121 @@ exports.ExternalModule = ExternalModule; ['__unknownHelper', null], ]); }); + + it('should recognize enum declarations with string values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(exports.Enum || (exports.Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = + createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should recognize enum declarations with numeric values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + var Enum; + (function (Enum) { + Enum[Enum["ValueA"] = "1"] = "ValueA"; + Enum[Enum["ValueB"] = "2"] = "ValueB"; + })(exports.Enum || (exports.Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = + createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should not consider IIFEs that do no assign members to the parameter as an enum declaration', + () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + var Enum; + (function (E) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(exports.Enum || (exports.Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = + createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(declaration.identity).toBe(null); + }); + + it('should not consider IIFEs without call argument as an enum declaration', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = + createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(declaration.identity).toBe(null); + }); }); describe('getClassSymbol()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index f0b5956d1b..37c9f769a5 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; @@ -1558,6 +1558,7 @@ runInEachFileSystem(() => { expect(actualDeclaration).not.toBe(null); expect(actualDeclaration!.node).toBe(classNode); expect(actualDeclaration!.viaModule).toBe(null); + expect((actualDeclaration as ConcreteDeclaration).identity).toBe(null); }); it('should return the declaration of an externally defined identifier', () => { @@ -1618,6 +1619,119 @@ runInEachFileSystem(() => { expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier)!.node) .toBe(classDeclaration); }); + + it('should recognize enum declarations with string values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(Enum || (Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should recognize enum declarations with numeric values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (Enum) { + Enum[Enum["ValueA"] = "1"] = "ValueA"; + Enum[Enum["ValueB"] = "2"] = "ValueB"; + })(Enum || (Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should not consider IIFEs that do no assign members to the parameter as an enum declaration', + () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (E) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(Enum || (Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(declaration.identity).toBe(null); + }); + + it('should not consider IIFEs without call argument as an enum declaration', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(declaration.identity).toBe(null); + }); }); describe('getExportsOfModule()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 0f5190a3c2..bd28dd68d7 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, Decorator, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; @@ -1754,6 +1754,7 @@ runInEachFileSystem(() => { known: knownAs, node: getHelperDeclaration(helperName), viaModule, + identity: null, }); }; @@ -1786,6 +1787,7 @@ runInEachFileSystem(() => { expect(actualDeclaration).not.toBe(null); expect(actualDeclaration!.node).toBe(expectedDeclarationNode); expect(actualDeclaration!.viaModule).toBe(null); + expect((actualDeclaration as ConcreteDeclaration).identity).toBe(null); }); it('should return the declaration of an externally defined identifier', () => { @@ -2186,6 +2188,117 @@ runInEachFileSystem(() => { testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); }); + + it('should recognize enum declarations with string values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(Enum || (Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should recognize enum declarations with numeric values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (Enum) { + Enum[Enum["ValueA"] = "1"] = "ValueA"; + Enum[Enum["ValueB"] = "2"] = "ValueB"; + })(Enum || (Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should not consider IIFEs that do no assign members to the parameter as an enum declaration', + () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (E) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(Enum || (Enum = {})); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(declaration.identity).toBe(null); + }); + + it('should not consider IIFEs without call argument as an enum declaration', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + export var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(); + + var value = Enum;` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('export var Enum;'); + expect(declaration.identity).toBe(null); + }); }); describe('getExportsOfModule()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 5aea2a1f13..7183693594 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; @@ -1833,6 +1833,7 @@ runInEachFileSystem(() => { known: knownAs, node: getHelperDeclaration(factoryFn, helperName), viaModule, + identity: null, }); }; @@ -1874,6 +1875,7 @@ runInEachFileSystem(() => { expect(actualDeclaration).not.toBe(null); expect(actualDeclaration!.node).toBe(expectedDeclarationNode); expect(actualDeclaration!.viaModule).toBe(null); + expect((actualDeclaration as ConcreteDeclaration).identity).toBe(null); }); it('should return the correct declaration for an outer alias identifier', () => { @@ -2179,6 +2181,147 @@ runInEachFileSystem(() => { testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); }); + + it('should recognize enum declarations with string values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var Enum; + (function (Enum) { + Enum["ValueA"] = "1"; + Enum["ValueB"] = "2"; + })(exports.Enum || (exports.Enum = {})); + + var value = Enum; + }))); + ` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, _('/node_modules/test-package/some/file.js')) + .statements[0])!; + const valueDecl = getVariableDeclaration(factoryFn, 'value'); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should recognize enum declarations with numeric values', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var Enum; + (function (Enum) { + Enum[Enum["ValueA"] = "1"] = "ValueA"; + Enum[Enum["ValueB"] = "2"] = "ValueB"; + })(exports.Enum || (exports.Enum = {})); + + var value = Enum; + }))); + ` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, _('/node_modules/test-package/some/file.js')) + .statements[0])!; + const valueDecl = getVariableDeclaration(factoryFn, 'value'); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + const enumMembers = (declaration.identity as DownleveledEnum).enumMembers; + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(enumMembers!.length).toBe(2); + expect(enumMembers![0].name.getText()).toBe('"ValueA"'); + expect(enumMembers![0].initializer!.getText()).toBe('"1"'); + expect(enumMembers![1].name.getText()).toBe('"ValueB"'); + expect(enumMembers![1].initializer!.getText()).toBe('"2"'); + }); + + it('should not consider IIFEs that do no assign members to the parameter as an enum declaration', + () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var Enum; + (function (E) { + Enum["ValueA"] = "1"]; + Enum["ValueB"] = "2"]; + })(exports.Enum || (exports.Enum = {})); + + var value = Enum; + }))); + ` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(declaration.identity).toBe(null); + }); + + it('should not consider IIFEs without call argument as an enum declaration', () => { + const testFile: TestFile = { + name: _('/node_modules/test-package/some/file.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var Enum; + (function (Enum) { + Enum["ValueA"] = "1"]; + Enum["ValueB"] = "2"]; + })(); + + var value = Enum; + }))); + ` + }; + loadTestFiles([testFile]); + const bundle = makeTestBundleProgram(testFile.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const valueDecl = getDeclaration( + bundle.program, _('/node_modules/test-package/some/file.js'), 'value', + ts.isVariableDeclaration); + const declaration = host.getDeclarationOfIdentifier( + valueDecl.initializer as ts.Identifier) as ConcreteDeclaration; + + expect(declaration.node.parent.parent.getText()).toBe('var Enum;'); + expect(declaration.identity).toBe(null); + }); }); describe('getExportsOfModule()', () => { diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index a4c6ae2d31..7503eeb6e6 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -317,6 +317,46 @@ runInEachFileSystem(() => { expect(jsContents).toContain('imports: [ɵngcc1.MyOtherModule]'); }); + it('should be able to resolve enum values', () => { + compileIntoApf('test-package', { + '/index.ts': ` + import {Component, NgModule} from '@angular/core'; + + export enum StringEnum { + ValueA = "a", + ValueB = "b", + } + + export enum NumericEnum { + Value3 = 3, + Value4, + } + + @Component({ + template: \`\${StringEnum.ValueA} - \${StringEnum.ValueB} - \${NumericEnum.Value3} - \${NumericEnum.Value4}\`, + }) + export class FooCmp {} + + @NgModule({ + declarations: [FooCmp], + }) + export class FooModule {} + `, + }); + + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: 'test-package', + propertiesToConsider: ['esm2015', 'esm5'], + }); + + const es2015Contents = fs.readFile(_(`/node_modules/test-package/esm2015/src/index.js`)); + expect(es2015Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")'); + + const es5Contents = fs.readFile(_(`/node_modules/test-package/esm5/src/index.js`)); + expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")'); + }); + it('should add ɵfac but not duplicate ɵprov properties on injectables', () => { compileIntoFlatEs5Package('test-package', { '/index.ts': ` diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index ab12a398fb..83d067dc72 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {Reference} from '../../imports'; import {OwningModule} from '../../imports/src/references'; import {DependencyTracker} from '../../incremental/api'; -import {Declaration, InlineDeclaration, ReflectionHost} from '../../reflection'; +import {ConcreteDeclaration, Declaration, EnumMember, InlineDeclaration, ReflectionHost, SpecialDeclarationKind} from '../../reflection'; import {isDeclaration} from '../../util/src/typescript'; import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn} from './builtin'; @@ -225,6 +225,10 @@ export class StaticInterpreter { } if (decl.known !== null) { return resolveKnownDeclaration(decl.known); + } else if ( + isConcreteDeclaration(decl) && decl.identity !== null && + decl.identity.kind === SpecialDeclarationKind.DownleveledEnum) { + return this.getResolvedEnum(decl.node, decl.identity.enumMembers, context); } const declContext = {...context, ...joinModuleContext(context, node, decl)}; // The identifier's declaration is either concrete (a ts.Declaration exists for it) or inline @@ -279,7 +283,7 @@ export class StaticInterpreter { } private visitEnumDeclaration(node: ts.EnumDeclaration, context: Context): ResolvedValue { - const enumRef = this.getReference(node, context) as Reference; + const enumRef = this.getReference(node, context); const map = new Map(); node.members.forEach(member => { const name = this.stringNameFromPropertyName(member.name, context); @@ -572,7 +576,21 @@ export class StaticInterpreter { } } - private getReference(node: ts.Declaration, context: Context): Reference { + private getResolvedEnum(node: ts.Declaration, enumMembers: EnumMember[], context: Context): + ResolvedValue { + const enumRef = this.getReference(node, context); + const map = new Map(); + enumMembers.forEach(member => { + const name = this.stringNameFromPropertyName(member.name, context); + if (name !== undefined) { + const resolved = this.visit(member.initializer, context); + map.set(name, new EnumValue(enumRef, name, resolved)); + } + }); + return map; + } + + private getReference(node: T, context: Context): Reference { return new Reference(node, owningModule(context)); } } @@ -638,3 +656,11 @@ function owningModule(context: Context, override: OwningModule|null = null): Own return null; } } + +/** + * Helper type guard to workaround a narrowing limitation in g3, where testing for + * `decl.node !== null` would not narrow `decl` to be of type `ConcreteDeclaration`. + */ +function isConcreteDeclaration(decl: Declaration): decl is ConcreteDeclaration { + return decl.node !== null; +} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts index c4f817d043..7d6405d96e 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts @@ -73,7 +73,7 @@ export class ResolvedModule { */ export class EnumValue { constructor( - readonly enumRef: Reference, readonly name: string, + readonly enumRef: Reference, readonly name: string, readonly resolved: ResolvedValue) {} } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index 33e38e91a8..19b814efb5 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -11,7 +11,7 @@ import {absoluteFrom, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {Reference} from '../../imports'; import {DependencyTracker} from '../../incremental/api'; -import {Declaration, KnownDeclaration, TypeScriptReflectionHost} from '../../reflection'; +import {Declaration, KnownDeclaration, SpecialDeclarationKind, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {DynamicValue} from '../src/dynamic'; import {PartialEvaluator} from '../src/interface'; @@ -488,10 +488,23 @@ runInEachFileSystem(() => { if (!(result instanceof EnumValue)) { return fail(`result is not an EnumValue`); } - expect(result.enumRef.node.name.text).toBe('Foo'); + expect((result.enumRef.node as ts.EnumDeclaration).name.text).toBe('Foo'); expect(result.name).toBe('B'); }); + it('enum resolution works when recognized in reflection host', () => { + const {checker, expression} = makeExpression('var Foo;', 'Foo.ValueB'); + const reflectionHost = new DownleveledEnumReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker, null); + const result = evaluator.evaluate(expression); + if (!(result instanceof EnumValue)) { + return fail(`result is not an EnumValue`); + } + expect(result.enumRef.node.parent.parent.getText()).toBe('var Foo;'); + expect(result.name).toBe('ValueB'); + expect(result.resolved).toBe('b'); + }); + it('variable declaration resolution works', () => { const value = evaluate(`import {value} from './decl';`, 'value', [ {name: _('/decl.d.ts'), contents: 'export declare let value: number;'}, @@ -843,6 +856,20 @@ runInEachFileSystem(() => { }); }); + class DownleveledEnumReflectionHost extends TypeScriptReflectionHost { + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + const declaration = super.getDeclarationOfIdentifier(id); + if (declaration !== null && declaration.node !== null) { + const enumMembers = [ + {name: ts.createStringLiteral('ValueA'), initializer: ts.createStringLiteral('a')}, + {name: ts.createStringLiteral('ValueB'), initializer: ts.createStringLiteral('b')}, + ]; + declaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers}; + } + return declaration; + } + } + /** * Customizes the resolution of module exports and identifier declarations to recognize known * helper functions from `tslib`. Such functions are not handled specially in the default @@ -872,6 +899,7 @@ runInEachFileSystem(() => { known: tsHelperFn, node: id, viaModule: null, + identity: null, }; } diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index 55cff19104..e07d6eade8 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -404,6 +404,22 @@ export interface Import { from: string; } +/** + * A single enum member extracted from JavaScript when no `ts.EnumDeclaration` is available. + */ +export interface EnumMember { + /** + * The name of the enum member. + */ + name: ts.PropertyName; + + /** + * The initializer expression of the enum member. Unlike in TypeScript, this is always available + * in emitted JavaScript. + */ + initializer: ts.Expression; +} + /** * Base type for all `Declaration`s. */ @@ -434,6 +450,28 @@ export interface BaseDeclaration { export interface ConcreteDeclaration extends BaseDeclaration { node: T; + + /** + * Optionally represents a special identity of the declaration, or `null` if the declaration + * does not have a special identity. + */ + identity: SpecialDeclarationIdentity|null; +} + +export type SpecialDeclarationIdentity = DownleveledEnum; + +export const enum SpecialDeclarationKind { + DownleveledEnum, +} + +/** + * A special declaration identity that represents an enum. This is used in downleveled forms where + * a `ts.EnumDeclaration` is emitted in an alternative form, e.g. an IIFE call that declares all + * members. + */ +export interface DownleveledEnum { + kind: SpecialDeclarationKind.DownleveledEnum; + enumMembers: EnumMember[]; } /** diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index ec66a14014..07bf421ef7 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -315,12 +315,14 @@ export class TypeScriptReflectionHost implements ReflectionHost { node: symbol.valueDeclaration, known: null, viaModule, + identity: null, }; } else if (symbol.declarations !== undefined && symbol.declarations.length > 0) { return { node: symbol.declarations[0], known: null, viaModule, + identity: null, }; } 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 0c072f2451..5210047cbe 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 @@ -363,6 +363,7 @@ runInEachFileSystem(() => { node: targetDecl, known: null, viaModule: 'absolute', + identity: null, }); }); @@ -393,6 +394,7 @@ runInEachFileSystem(() => { node: targetDecl, known: null, viaModule: 'absolute', + identity: null, }); }); });