diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 435d712ead..989152dcea 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -43,7 +43,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler, propertyName: string, - checker: ts.TypeChecker): R3QueryMetadata { + reflector: ReflectionHost, checker: ts.TypeChecker): R3QueryMetadata { if (args.length === 0) { throw new Error(`@${name} must have arguments`); } const first = name === 'ViewChild' || name === 'ContentChild'; - const arg = staticallyResolve(args[0], checker); + const arg = staticallyResolve(args[0], reflector, checker); // Extract the predicate let predicate: Expression|string[]|null = null; @@ -182,7 +184,7 @@ export function extractQueryMetadata( } if (options.has('descendants')) { - const descendantsValue = staticallyResolve(options.get('descendants') !, checker); + const descendantsValue = staticallyResolve(options.get('descendants') !, reflector, checker); if (typeof descendantsValue !== 'boolean') { throw new Error(`@${name} options.descendants must be a boolean`); } @@ -220,7 +222,8 @@ export function extractQueriesFromDecorator( throw new Error(`query metadata must be an instance of a query type`); } - const query = extractQueryMetadata(type.name, queryExpr.arguments || [], propertyName, checker); + const query = extractQueryMetadata( + type.name, queryExpr.arguments || [], propertyName, reflector, checker); if (type.name.startsWith('Content')) { content.push(query); } else { @@ -248,14 +251,14 @@ function isStringArrayOrDie(value: any, name: string): value is string[] { * correctly shaped metadata object. */ function parseFieldToPropertyMapping( - directive: Map, field: string, + directive: Map, field: string, reflector: ReflectionHost, checker: ts.TypeChecker): {[field: string]: string} { if (!directive.has(field)) { return EMPTY_OBJECT; } // Resolve the field of interest from the directive metadata to a string[]. - const metaValues = staticallyResolve(directive.get(field) !, checker); + const metaValues = staticallyResolve(directive.get(field) !, reflector, checker); if (!isStringArrayOrDie(metaValues, field)) { throw new Error(`Failed to resolve @Directive.${field}`); } @@ -276,7 +279,7 @@ function parseFieldToPropertyMapping( * object. */ function parseDecoratedFields( - fields: {member: ClassMember, decorators: Decorator[]}[], + fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost, checker: ts.TypeChecker): {[field: string]: string} { return fields.reduce( (results, field) => { @@ -287,7 +290,7 @@ function parseDecoratedFields( if (decorator.args == null || decorator.args.length === 0) { results[fieldName] = fieldName; } else if (decorator.args.length === 1) { - const property = staticallyResolve(decorator.args[0], checker); + const property = staticallyResolve(decorator.args[0], reflector, checker); if (typeof property !== 'string') { throw new Error(`Decorator argument must resolve to a string`); } @@ -304,7 +307,7 @@ function parseDecoratedFields( } export function queriesFromFields( - fields: {member: ClassMember, decorators: Decorator[]}[], + fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost, checker: ts.TypeChecker): R3QueryMetadata[] { return fields.map(({member, decorators}) => { if (decorators.length !== 1) { @@ -313,7 +316,8 @@ export function queriesFromFields( throw new Error(`Query decorator must go on a property-type member`); } const decorator = decorators[0]; - return extractQueryMetadata(decorator.name, decorator.args || [], member.name, checker); + return extractQueryMetadata( + decorator.name, decorator.args || [], member.name, reflector, checker); }); } @@ -327,15 +331,15 @@ type StringMap = { }; function extractHostBindings( - metadata: Map, members: ClassMember[], checker: ts.TypeChecker, - coreModule: string | undefined): { + metadata: Map, members: ClassMember[], reflector: ReflectionHost, + checker: ts.TypeChecker, coreModule: string | undefined): { attributes: StringMap, listeners: StringMap, properties: StringMap, } { let hostMetadata: StringMap = {}; if (metadata.has('host')) { - const hostMetaMap = staticallyResolve(metadata.get('host') !, checker); + const hostMetaMap = staticallyResolve(metadata.get('host') !, reflector, checker); if (!(hostMetaMap instanceof Map)) { throw new Error(`Decorator host metadata must be an object`); } @@ -358,7 +362,7 @@ function extractHostBindings( throw new Error(`@HostBinding() can have at most one argument`); } - const resolved = staticallyResolve(decorator.args[0], checker); + const resolved = staticallyResolve(decorator.args[0], reflector, checker); if (typeof resolved !== 'string') { throw new Error(`@HostBinding()'s argument must be a string`); } @@ -380,7 +384,7 @@ function extractHostBindings( throw new Error(`@HostListener() can have at most two arguments`); } - const resolved = staticallyResolve(decorator.args[0], checker); + const resolved = staticallyResolve(decorator.args[0], reflector, checker); if (typeof resolved !== 'string') { throw new Error(`@HostListener()'s event name argument must be a string`); } @@ -388,7 +392,7 @@ function extractHostBindings( eventName = resolved; if (decorator.args.length === 2) { - const resolvedArgs = staticallyResolve(decorator.args[1], checker); + const resolvedArgs = staticallyResolve(decorator.args[1], reflector, checker); if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args')) { throw new Error(`@HostListener second argument must be a string array`); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index bd4153ca20..f0fe5b322d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -59,20 +59,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(node)); imports = resolveTypeList(importsMeta, 'imports'); } let exports: Reference[] = []; if (ngModule.has('exports')) { const exportsMeta = staticallyResolve( - ngModule.get('exports') !, this.checker, + ngModule.get('exports') !, this.reflector, this.checker, node => this._extractModuleFromModuleWithProvidersFn(node)); exports = resolveTypeList(exportsMeta, 'exports'); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 4331746afc..eabff8452d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -44,7 +44,7 @@ export class PipeDecoratorHandler implements DecoratorHandler { if (!pipe.has('name')) { throw new Error(`@Pipe decorator is missing name field`); } - const pipeName = staticallyResolve(pipe.get('name') !, this.checker); + const pipeName = staticallyResolve(pipe.get('name') !, this.reflector, this.checker); if (typeof pipeName !== 'string') { throw new Error(`@Pipe.name must be a string`); } @@ -52,7 +52,7 @@ export class PipeDecoratorHandler implements DecoratorHandler { let pure = true; if (pipe.has('pure')) { - const pureValue = staticallyResolve(pipe.get('pure') !, this.checker); + const pureValue = staticallyResolve(pipe.get('pure') !, this.reflector, this.checker); if (typeof pureValue !== 'boolean') { throw new Error(`@Pipe.pure must be a boolean`); } diff --git a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts index c6c809737b..d47f27c269 100644 --- a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts +++ b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts @@ -83,10 +83,57 @@ export interface ClassMember { nameNode: ts.Identifier|null; /** - * TypeScript `ts.Expression` which initializes this member, if the member is a property, or - * `null` otherwise. + * TypeScript `ts.Expression` which represents the value of the member. + * + * If the member is a property, this will be the property initializer if there is one, or null + * otherwise. */ - initializer: ts.Expression|null; + value: ts.Expression|null; + + /** + * TypeScript `ts.Declaration` which represents the implementation of the member. + * + * In TypeScript code this is identical to the node, but in downleveled code this should always be + * the Declaration which actually represents the member's runtime value. + * + * For example, the TS code: + * + * ``` + * class Clazz { + * static get property(): string { + * return 'value'; + * } + * } + * ``` + * + * Downlevels to: + * + * ``` + * var Clazz = (function () { + * function Clazz() { + * } + * Object.defineProperty(Clazz, "property", { + * get: function () { + * return 'value'; + * }, + * enumerable: true, + * configurable: true + * }); + * return Clazz; + * }()); + * ``` + * + * In this example, for the property "property", the node would be the entire + * Object.defineProperty ExpressionStatement, but the implementation would be this + * FunctionDeclaration: + * + * ``` + * function () { + * return 'value'; + * }, + * ``` + */ + implementation: ts.Declaration|null; /** * Whether the member is static or not. @@ -151,6 +198,24 @@ export interface Import { from: string; } +/** + * The declaration of a symbol, along with information about how it was imported into the + * application. + */ +export interface Declaration { + /** + * TypeScript reference to the declaration itself. + */ + node: ts.Declaration; + + /** + * The absolute module path from which the symbol was imported into the application, if the symbol + * was imported via an absolute module (even through a chain of re-exports). If the symbol is part + * of the application and was not imported from an absolute path, this will be `null`. + */ + viaModule: string|null; +} + /** * Abstracts reflection operations on a TypeScript AST. * @@ -220,4 +285,57 @@ export interface ReflectionHost { * `null` if the identifier doesn't resolve to an import but instead is locally defined. */ getImportOfIdentifier(id: ts.Identifier): Import|null; + + /** + * Trace an identifier to its declaration, if possible. + * + * This method attempts to resolve the declaration of the given identifier, tracing back through + * imports and re-exports until the original declaration statement is found. A `Declaration` + * object is returned if the original declaration is found, or `null` is returned otherwise. + * + * If the declaration is in a different module, and that module is imported via an absolute path, + * this method also returns the absolute path of the imported module. For example, if the code is: + * + * ``` + * import {RouterModule} from '@angular/core'; + * + * export const ROUTES = RouterModule.forRoot([...]); + * ``` + * + * and if `getDeclarationOfIdentifier` is called on `RouterModule` in the `ROUTES` expression, + * then it would trace `RouterModule` via its import from `@angular/core`, and note that the + * definition was imported from `@angular/core` into the application where it was referenced. + * + * If the definition is re-exported several times from different absolute module names, only + * the first one (the one by which the application refers to the module) is returned. + * + * This module name is returned in the `viaModule` field of the `Declaration`. If The declaration + * is relative to the application itself and there was no import through an absolute path, then + * `viaModule` is `null`. + * + * @param id a TypeScript `ts.Identifier` to trace back to a declaration. + * + * @returns metadata about the `Declaration` if the original declaration is found, or `null` + * otherwise. + */ + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null; + + /** + * Collect the declarations exported from a module by name. + * + * Iterates over the exports of a module (including re-exports) and returns a map of export + * name to its `Declaration`. If an exported value is itself re-exported from another module, + * the `Declaration`'s `viaModule` will reflect that. + * + * @param node a TypeScript `ts.Node` representing the module (for example a `ts.SourceFile`) for + * which to collect exports. + * + * @returns a map of `Declaration`s for the module's exports, by name. + */ + getExportsOfModule(module: ts.Node): Map|null; + + /** + * Check whether the given declaration node actually represents a class. + */ + isClass(node: ts.Declaration): boolean; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts index 61cab4e721..014abbae89 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassMember, ClassMemberKind, Decorator, Import, Parameter, ReflectionHost} from '../../host'; +import {ClassMember, ClassMemberKind, Declaration, Decorator, Import, Parameter, ReflectionHost} from '../../host'; /** * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`. @@ -104,7 +104,94 @@ export class TypeScriptReflectionHost implements ReflectionHost { return {from, name}; } - isClass(node: ts.Node): node is ts.Declaration { return ts.isClassDeclaration(node); } + getExportsOfModule(node: ts.Node): Map|null { + // In TypeScript code, modules are only ts.SourceFiles. Throw if the node isn't a module. + if (!ts.isSourceFile(node)) { + throw new Error(`getDeclarationsOfModule() called on non-SourceFile in TS code`); + } + const map = new Map(); + + // Reflect the module to a Symbol, and use getExportsOfModule() to get a list of exported + // Symbols. + const symbol = this.checker.getSymbolAtLocation(node); + if (symbol === undefined) { + return null; + } + this.checker.getExportsOfModule(symbol).forEach(exportSymbol => { + // Map each exported Symbol to a Declaration and add it to the map. + const decl = this._getDeclarationOfSymbol(exportSymbol); + if (decl !== null) { + map.set(exportSymbol.name, decl); + } + }); + return map; + } + + isClass(node: ts.Declaration): node is ts.ClassDeclaration { + // In TypeScript code, classes are ts.ClassDeclarations. + return ts.isClassDeclaration(node); + } + + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + // Resolve the identifier to a Symbol, and return the declaration of that. + let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(id); + if (symbol === undefined) { + return null; + } + return this._getDeclarationOfSymbol(symbol); + } + + /** + * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. + * + * @internal + */ + protected _getDeclarationOfSymbol(symbol: ts.Symbol): Declaration|null { + let viaModule: string|null = null; + // Look through the Symbol's immediate declarations, and see if any of them are import-type + // statements. + if (symbol.declarations !== undefined && symbol.declarations.length > 0) { + for (let i = 0; i < symbol.declarations.length; i++) { + const decl = symbol.declarations[i]; + if (ts.isImportSpecifier(decl) && decl.parent !== undefined && + decl.parent.parent !== undefined && decl.parent.parent.parent !== undefined) { + // Find the ImportDeclaration that imported this Symbol. + const importDecl = decl.parent.parent.parent; + // The moduleSpecifier should always be a string. + if (ts.isStringLiteral(importDecl.moduleSpecifier)) { + // Check if the moduleSpecifier is absolute. If it is, this symbol comes from an + // external module, and the import path becomes the viaModule. + const moduleSpecifier = importDecl.moduleSpecifier.text; + if (!moduleSpecifier.startsWith('.')) { + viaModule = moduleSpecifier; + break; + } + } + } + } + } + + // Now, resolve the Symbol to its declaration by following any and all aliases. + while (symbol.flags & ts.SymbolFlags.Alias) { + symbol = this.checker.getAliasedSymbol(symbol); + } + + // Look at the resolved Symbol's declarations and pick one of them to return. Value declarations + // are given precedence over type declarations. + if (symbol.valueDeclaration !== undefined) { + return { + node: symbol.valueDeclaration, + viaModule, + }; + } else if (symbol.declarations !== undefined && symbol.declarations.length > 0) { + return { + node: symbol.declarations[0], + viaModule, + }; + } else { + return null; + } + } private _reflectDecorator(node: ts.Decorator): Decorator|null { // Attempt to resolve the decorator expression into a reference to a concrete Identifier. The @@ -135,13 +222,13 @@ export class TypeScriptReflectionHost implements ReflectionHost { private _reflectMember(node: ts.ClassElement): ClassMember|null { let kind: ClassMemberKind|null = null; - let initializer: ts.Expression|null = null; + let value: ts.Expression|null = null; let name: string|null = null; let nameNode: ts.Identifier|null = null; if (ts.isPropertyDeclaration(node)) { kind = ClassMemberKind.Property; - initializer = node.initializer || null; + value = node.initializer || null; } else if (ts.isGetAccessorDeclaration(node)) { kind = ClassMemberKind.Getter; } else if (ts.isSetAccessorDeclaration(node)) { @@ -169,8 +256,8 @@ export class TypeScriptReflectionHost implements ReflectionHost { return { node, - kind, - type: node.type || null, name, nameNode, decorators, initializer, isStatic, + implementation: node, kind, + type: node.type || null, name, nameNode, decorators, value, isStatic, }; } } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts index a63ec59081..530406392c 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts @@ -15,6 +15,8 @@ import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@ang import * as path from 'path'; import * as ts from 'typescript'; +import {ClassMemberKind, ReflectionHost} from '../../host'; + const TS_DTS_EXTENSION = /(\.d)?\.ts$/; /** @@ -188,10 +190,10 @@ export class AbsoluteReference extends Reference { * @returns a `ResolvedValue` representing the resolved value */ export function staticallyResolve( - node: ts.Expression, checker: ts.TypeChecker, + node: ts.Expression, host: ReflectionHost, checker: ts.TypeChecker, foreignFunctionResolver?: (node: ts.FunctionDeclaration | ts.MethodDeclaration) => ts.Expression | null): ResolvedValue { - return new StaticInterpreter(checker).visit(node, { + return new StaticInterpreter(host, checker).visit(node, { absoluteModuleName: null, scope: new Map(), foreignFunctionResolver, }); @@ -243,7 +245,7 @@ interface Context { } class StaticInterpreter { - constructor(private checker: ts.TypeChecker) {} + constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} visit(node: ts.Expression, context: Context): ResolvedValue { return this.visitExpression(node, context); @@ -286,7 +288,7 @@ class StaticInterpreter { return this.visitExpression(node.expression, context); } else if (ts.isNonNullExpression(node)) { return this.visitExpression(node.expression, context); - } else if (ts.isClassDeclaration(node)) { + } else if (isPossibleClassDeclaration(node) && this.host.isClass(node)) { return this.visitDeclaration(node, context); } else { return DYNAMIC_VALUE; @@ -356,14 +358,6 @@ class StaticInterpreter { return map; } - private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue { - let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(node); - if (symbol === undefined) { - return DYNAMIC_VALUE; - } - return this.visitSymbol(symbol, context); - } - private visitTemplateExpression(node: ts.TemplateExpression, context: Context): ResolvedValue { const pieces: string[] = [node.head.text]; for (let i = 0; i < node.templateSpans.length; i++) { @@ -380,48 +374,19 @@ class StaticInterpreter { return pieces.join(''); } - private visitSymbol(symbol: ts.Symbol, context: Context): ResolvedValue { - let absoluteModuleName = context.absoluteModuleName; - if (symbol.declarations !== undefined && symbol.declarations.length > 0) { - for (let i = 0; i < symbol.declarations.length; i++) { - const decl = symbol.declarations[i]; - if (ts.isImportSpecifier(decl) && decl.parent !== undefined && - decl.parent.parent !== undefined && decl.parent.parent.parent !== undefined) { - const importDecl = decl.parent.parent.parent; - if (ts.isStringLiteral(importDecl.moduleSpecifier)) { - const moduleSpecifier = importDecl.moduleSpecifier.text; - if (!moduleSpecifier.startsWith('.')) { - absoluteModuleName = moduleSpecifier; - } - } - } - } - } - - const newContext = {...context, absoluteModuleName}; - - while (symbol.flags & ts.SymbolFlags.Alias) { - symbol = this.checker.getAliasedSymbol(symbol); - } - - if (symbol.declarations === undefined) { + private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue { + const decl = this.host.getDeclarationOfIdentifier(node); + if (decl === null) { return DYNAMIC_VALUE; } - - if (symbol.valueDeclaration !== undefined) { - return this.visitDeclaration(symbol.valueDeclaration, newContext); - } - - return symbol.declarations.reduce((prev, decl) => { - if (!(isDynamicValue(prev) || prev instanceof Reference)) { - return prev; - } - return this.visitDeclaration(decl, newContext); - }, DYNAMIC_VALUE); + return this.visitDeclaration( + decl.node, {...context, absoluteModuleName: decl.viaModule || context.absoluteModuleName}); } private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue { - if (ts.isVariableDeclaration(node)) { + if (this.host.isClass(node)) { + return this.getReference(node, context); + } else if (ts.isVariableDeclaration(node)) { if (!node.initializer) { return undefined; } @@ -471,14 +436,18 @@ class StaticInterpreter { } private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue { - const map = new Map(); - const symbol = this.checker.getSymbolAtLocation(node); - if (symbol === undefined) { + const declarations = this.host.getExportsOfModule(node); + if (declarations === null) { return DYNAMIC_VALUE; } - const exports = this.checker.getExportsOfModule(symbol); - exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol, context))); - + const map = new Map(); + declarations.forEach((decl, name) => { + const value = this.visitDeclaration(decl.node, { + ...context, + absoluteModuleName: decl.viaModule || context.absoluteModuleName, + }); + map.set(name, value); + }); return map; } @@ -503,22 +472,21 @@ class StaticInterpreter { return lhs[rhs]; } else if (lhs instanceof Reference) { const ref = lhs.node; - if (ts.isClassDeclaration(ref)) { + if (isPossibleClassDeclaration(ref) && this.host.isClass(ref)) { let absoluteModuleName = context.absoluteModuleName; if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { absoluteModuleName = lhs.moduleName || absoluteModuleName; } let value: ResolvedValue = undefined; - const member = - ref.members.filter(member => isStatic(member)) - .find( - member => member.name !== undefined && - this.stringNameFromPropertyName(member.name, context) === strIndex); + const member = this.host.getMembersOfClass(ref).find( + member => member.isStatic && member.name === strIndex); if (member !== undefined) { - if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) { - value = this.visitExpression(member.initializer, context); - } else if (ts.isMethodDeclaration(member)) { - value = new NodeReference(member, absoluteModuleName); + if (member.value !== null) { + value = this.visitExpression(member.value, context); + } else if (member.implementation !== null) { + value = new NodeReference(member.implementation, absoluteModuleName); + } else { + value = new NodeReference(member.node, absoluteModuleName); } } return value; @@ -657,11 +625,6 @@ class StaticInterpreter { } } -function isStatic(element: ts.ClassElement): boolean { - return element.modifiers !== undefined && - element.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); -} - function isFunctionOrMethodDeclaration(node: ts.Node): node is ts.FunctionDeclaration| ts.MethodDeclaration { return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node); @@ -691,3 +654,7 @@ function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined return undefined; } } + +function isPossibleClassDeclaration(node: ts.Node): node is ts.Declaration { + return ts.isClassDeclaration(node) || ts.isVariableDeclaration(node); +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts index 245dc8937b..47e05f0642 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts @@ -120,6 +120,48 @@ describe('reflector', () => { expectParameter(args[1], 'otherBar', 'star.Bar'); }); }); + + it('should reflect a re-export', () => { + const {program} = makeProgram([ + {name: '/node_modules/absolute/index.ts', contents: 'export class Target {}'}, + {name: 'local1.ts', contents: `export {Target as AliasTarget} from 'absolute';`}, + {name: 'local2.ts', contents: `export {AliasTarget as Target} from './local1';`}, { + name: 'entry.ts', + contents: ` + import {Target} from './local2'; + import {Target as DirectTarget} from 'absolute'; + + const target = Target; + const directTarget = DirectTarget; + ` + } + ]); + const target = getDeclaration(program, 'entry.ts', 'target', ts.isVariableDeclaration); + if (target.initializer === undefined || !ts.isIdentifier(target.initializer)) { + return fail('Unexpected initializer for target'); + } + const directTarget = + getDeclaration(program, 'entry.ts', 'directTarget', ts.isVariableDeclaration); + if (directTarget.initializer === undefined || !ts.isIdentifier(directTarget.initializer)) { + return fail('Unexpected initializer for directTarget'); + } + const Target = target.initializer; + const DirectTarget = directTarget.initializer; + + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const targetDecl = host.getDeclarationOfIdentifier(Target); + const directTargetDecl = host.getDeclarationOfIdentifier(DirectTarget); + if (targetDecl === null) { + return fail('No declaration found for Target'); + } else if (directTargetDecl === null) { + return fail('No declaration found for DirectTarget'); + } + expect(targetDecl.node.getSourceFile().fileName).toBe('/node_modules/absolute/index.ts'); + expect(ts.isClassDeclaration(targetDecl.node)).toBe(true); + expect(directTargetDecl.viaModule).toBe('absolute'); + expect(directTargetDecl.node).toBe(targetDecl.node); + }); }); function expectParameter( diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts index 740dd1ea0c..c4d1cc27a8 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts @@ -9,6 +9,7 @@ import {ExternalExpr} from '@angular/compiler'; import * as ts from 'typescript'; +import {TypeScriptReflectionHost} from '..'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {Reference, ResolvedValue, staticallyResolve} from '../src/resolver'; @@ -30,7 +31,8 @@ function makeExpression( function evaluate(code: string, expr: string): T { const {expression, checker} = makeExpression(code, expr); - return staticallyResolve(expression, checker) as T; + const host = new TypeScriptReflectionHost(checker); + return staticallyResolve(expression, host, checker) as T; } describe('ngtsc metadata', () => { @@ -52,8 +54,9 @@ describe('ngtsc metadata', () => { } ]); const decl = getDeclaration(program, 'entry.ts', 'X', ts.isVariableDeclaration); + const host = new TypeScriptReflectionHost(program.getTypeChecker()); - const value = staticallyResolve(decl.initializer !, program.getTypeChecker()); + const value = staticallyResolve(decl.initializer !, host, program.getTypeChecker()); expect(value).toEqual('test'); }); @@ -132,9 +135,10 @@ describe('ngtsc metadata', () => { }, ]); const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - const resolved = staticallyResolve(expr, checker); + const resolved = staticallyResolve(expr, host, checker); if (!(resolved instanceof Reference)) { return fail('Expected expression to resolve to a reference'); } @@ -160,9 +164,10 @@ describe('ngtsc metadata', () => { }, ]); const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - const resolved = staticallyResolve(expr, checker); + const resolved = staticallyResolve(expr, host, checker); if (!(resolved instanceof Reference)) { return fail('Expected expression to resolve to a reference'); } @@ -188,9 +193,10 @@ describe('ngtsc metadata', () => { }, ]); const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - expect(staticallyResolve(expr, checker)).toEqual('test'); + expect(staticallyResolve(expr, host, checker)).toEqual('test'); }); it('reads values from named exports', () => { @@ -205,9 +211,10 @@ describe('ngtsc metadata', () => { }, ]); const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - expect(staticallyResolve(expr, checker)).toEqual('test'); + expect(staticallyResolve(expr, host, checker)).toEqual('test'); }); it('chain of re-exports works', () => { @@ -222,9 +229,10 @@ describe('ngtsc metadata', () => { }, ]); const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - expect(staticallyResolve(expr, checker)).toEqual('test'); + expect(staticallyResolve(expr, host, checker)).toEqual('test'); }); it('map spread works', () => {