feat(ivy): port the static resolver to use the ReflectionHost (#24862)

Previously, the static resolver did its own interpretation of statements
in the TypeScript AST, which only functioned on TypeScript code. ES5
code in particular would not work with the resolver as it had hard-coded
assumptions about AST structure.

This commit changes the resolver to use a ReflectionHost instead, which
abstracts away understanding of the structural side of the AST. It adds 3
new methods to the ReflectionHost in support of this functionality:

* getDeclarationOfIdentifier
* getExportsOfModule
* isClass

PR Close #24862
This commit is contained in:
Alex Rickabaugh
2018-07-12 12:11:18 -07:00
committed by Victor Berchet
parent 2e724ec68b
commit 5d7005eef5
9 changed files with 353 additions and 121 deletions

View File

@ -43,7 +43,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
const component = reflectObjectLiteral(meta); const component = reflectObjectLiteral(meta);
if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) { if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) {
const templateUrl = staticallyResolve(component.get('templateUrl') !, this.checker); const templateUrl =
staticallyResolve(component.get('templateUrl') !, this.reflector, this.checker);
if (typeof templateUrl !== 'string') { if (typeof templateUrl !== 'string') {
throw new Error(`templateUrl should be a string`); throw new Error(`templateUrl should be a string`);
} }
@ -73,7 +74,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
let templateStr: string|null = null; let templateStr: string|null = null;
if (component.has('templateUrl')) { if (component.has('templateUrl')) {
const templateUrl = staticallyResolve(component.get('templateUrl') !, this.checker); const templateUrl =
staticallyResolve(component.get('templateUrl') !, this.reflector, this.checker);
if (typeof templateUrl !== 'string') { if (typeof templateUrl !== 'string') {
throw new Error(`templateUrl should be a string`); throw new Error(`templateUrl should be a string`);
} }
@ -81,7 +83,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
templateStr = this.resourceLoader.load(url); templateStr = this.resourceLoader.load(url);
} else if (component.has('template')) { } else if (component.has('template')) {
const templateExpr = component.get('template') !; const templateExpr = component.get('template') !;
const resolvedTemplate = staticallyResolve(templateExpr, this.checker); const resolvedTemplate = staticallyResolve(templateExpr, this.reflector, this.checker);
if (typeof resolvedTemplate !== 'string') { if (typeof resolvedTemplate !== 'string') {
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`); throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
} }
@ -92,7 +94,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
let preserveWhitespaces: boolean = false; let preserveWhitespaces: boolean = false;
if (component.has('preserveWhitespaces')) { if (component.has('preserveWhitespaces')) {
const value = staticallyResolve(component.get('preserveWhitespaces') !, this.checker); const value =
staticallyResolve(component.get('preserveWhitespaces') !, this.reflector, this.checker);
if (typeof value !== 'boolean') { if (typeof value !== 'boolean') {
throw new Error(`preserveWhitespaces must resolve to a boolean if present`); throw new Error(`preserveWhitespaces must resolve to a boolean if present`);
} }
@ -116,9 +119,11 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// Construct the list of view queries. // Construct the list of view queries.
const coreModule = this.isCore ? undefined : '@angular/core'; const coreModule = this.isCore ? undefined : '@angular/core';
const viewChildFromFields = queriesFromFields( const viewChildFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.checker); filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.reflector,
this.checker);
const viewChildrenFromFields = queriesFromFields( const viewChildrenFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.checker); filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.reflector,
this.checker);
const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields]; const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields];
if (component.has('queries')) { if (component.has('queries')) {

View File

@ -90,19 +90,21 @@ export function extractDirectiveMetadata(
// Construct the map of inputs both from the @Directive/@Component // Construct the map of inputs both from the @Directive/@Component
// decorator, and the decorated // decorator, and the decorated
// fields. // fields.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker); const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', reflector, checker);
const inputsFromFields = parseDecoratedFields( const inputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), checker); filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker);
// And outputs. // And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker); const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', reflector, checker);
const outputsFromFields = parseDecoratedFields( const outputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), checker); filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), reflector, checker);
// Construct the list of queries. // Construct the list of queries.
const contentChildFromFields = queriesFromFields( const contentChildFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), checker); filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
checker);
const contentChildrenFromFields = queriesFromFields( const contentChildrenFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), checker); filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), reflector,
checker);
const queries = [...contentChildFromFields, ...contentChildrenFromFields]; const queries = [...contentChildFromFields, ...contentChildrenFromFields];
@ -115,14 +117,14 @@ export function extractDirectiveMetadata(
// Parse the selector. // Parse the selector.
let selector = ''; let selector = '';
if (directive.has('selector')) { if (directive.has('selector')) {
const resolved = staticallyResolve(directive.get('selector') !, checker); const resolved = staticallyResolve(directive.get('selector') !, reflector, checker);
if (typeof resolved !== 'string') { if (typeof resolved !== 'string') {
throw new Error(`Selector must be a string`); throw new Error(`Selector must be a string`);
} }
selector = resolved; selector = resolved;
} }
const host = extractHostBindings(directive, decoratedElements, checker, coreModule); const host = extractHostBindings(directive, decoratedElements, reflector, checker, coreModule);
// Determine if `ngOnChanges` is a lifecycle hook defined on the component. // Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = members.find( const usesOnChanges = members.find(
@ -148,12 +150,12 @@ export function extractDirectiveMetadata(
export function extractQueryMetadata( export function extractQueryMetadata(
name: string, args: ReadonlyArray<ts.Expression>, propertyName: string, name: string, args: ReadonlyArray<ts.Expression>, propertyName: string,
checker: ts.TypeChecker): R3QueryMetadata { reflector: ReflectionHost, checker: ts.TypeChecker): R3QueryMetadata {
if (args.length === 0) { if (args.length === 0) {
throw new Error(`@${name} must have arguments`); throw new Error(`@${name} must have arguments`);
} }
const first = name === 'ViewChild' || name === 'ContentChild'; const first = name === 'ViewChild' || name === 'ContentChild';
const arg = staticallyResolve(args[0], checker); const arg = staticallyResolve(args[0], reflector, checker);
// Extract the predicate // Extract the predicate
let predicate: Expression|string[]|null = null; let predicate: Expression|string[]|null = null;
@ -182,7 +184,7 @@ export function extractQueryMetadata(
} }
if (options.has('descendants')) { if (options.has('descendants')) {
const descendantsValue = staticallyResolve(options.get('descendants') !, checker); const descendantsValue = staticallyResolve(options.get('descendants') !, reflector, checker);
if (typeof descendantsValue !== 'boolean') { if (typeof descendantsValue !== 'boolean') {
throw new Error(`@${name} options.descendants must be a 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`); 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')) { if (type.name.startsWith('Content')) {
content.push(query); content.push(query);
} else { } else {
@ -248,14 +251,14 @@ function isStringArrayOrDie(value: any, name: string): value is string[] {
* correctly shaped metadata object. * correctly shaped metadata object.
*/ */
function parseFieldToPropertyMapping( function parseFieldToPropertyMapping(
directive: Map<string, ts.Expression>, field: string, directive: Map<string, ts.Expression>, field: string, reflector: ReflectionHost,
checker: ts.TypeChecker): {[field: string]: string} { checker: ts.TypeChecker): {[field: string]: string} {
if (!directive.has(field)) { if (!directive.has(field)) {
return EMPTY_OBJECT; return EMPTY_OBJECT;
} }
// Resolve the field of interest from the directive metadata to a string[]. // 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)) { if (!isStringArrayOrDie(metaValues, field)) {
throw new Error(`Failed to resolve @Directive.${field}`); throw new Error(`Failed to resolve @Directive.${field}`);
} }
@ -276,7 +279,7 @@ function parseFieldToPropertyMapping(
* object. * object.
*/ */
function parseDecoratedFields( function parseDecoratedFields(
fields: {member: ClassMember, decorators: Decorator[]}[], fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost,
checker: ts.TypeChecker): {[field: string]: string} { checker: ts.TypeChecker): {[field: string]: string} {
return fields.reduce( return fields.reduce(
(results, field) => { (results, field) => {
@ -287,7 +290,7 @@ function parseDecoratedFields(
if (decorator.args == null || decorator.args.length === 0) { if (decorator.args == null || decorator.args.length === 0) {
results[fieldName] = fieldName; results[fieldName] = fieldName;
} else if (decorator.args.length === 1) { } 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') { if (typeof property !== 'string') {
throw new Error(`Decorator argument must resolve to a string`); throw new Error(`Decorator argument must resolve to a string`);
} }
@ -304,7 +307,7 @@ function parseDecoratedFields(
} }
export function queriesFromFields( export function queriesFromFields(
fields: {member: ClassMember, decorators: Decorator[]}[], fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost,
checker: ts.TypeChecker): R3QueryMetadata[] { checker: ts.TypeChecker): R3QueryMetadata[] {
return fields.map(({member, decorators}) => { return fields.map(({member, decorators}) => {
if (decorators.length !== 1) { if (decorators.length !== 1) {
@ -313,7 +316,8 @@ export function queriesFromFields(
throw new Error(`Query decorator must go on a property-type member`); throw new Error(`Query decorator must go on a property-type member`);
} }
const decorator = decorators[0]; 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( function extractHostBindings(
metadata: Map<string, ts.Expression>, members: ClassMember[], checker: ts.TypeChecker, metadata: Map<string, ts.Expression>, members: ClassMember[], reflector: ReflectionHost,
coreModule: string | undefined): { checker: ts.TypeChecker, coreModule: string | undefined): {
attributes: StringMap, attributes: StringMap,
listeners: StringMap, listeners: StringMap,
properties: StringMap, properties: StringMap,
} { } {
let hostMetadata: StringMap = {}; let hostMetadata: StringMap = {};
if (metadata.has('host')) { if (metadata.has('host')) {
const hostMetaMap = staticallyResolve(metadata.get('host') !, checker); const hostMetaMap = staticallyResolve(metadata.get('host') !, reflector, checker);
if (!(hostMetaMap instanceof Map)) { if (!(hostMetaMap instanceof Map)) {
throw new Error(`Decorator host metadata must be an object`); 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`); 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') { if (typeof resolved !== 'string') {
throw new Error(`@HostBinding()'s argument must be a 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`); 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') { if (typeof resolved !== 'string') {
throw new Error(`@HostListener()'s event name argument must be a string`); throw new Error(`@HostListener()'s event name argument must be a string`);
} }
@ -388,7 +392,7 @@ function extractHostBindings(
eventName = resolved; eventName = resolved;
if (decorator.args.length === 2) { 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')) { if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args')) {
throw new Error(`@HostListener second argument must be a string array`); throw new Error(`@HostListener second argument must be a string array`);
} }

View File

@ -59,20 +59,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
// Extract the module declarations, imports, and exports. // Extract the module declarations, imports, and exports.
let declarations: Reference[] = []; let declarations: Reference[] = [];
if (ngModule.has('declarations')) { if (ngModule.has('declarations')) {
const declarationMeta = staticallyResolve(ngModule.get('declarations') !, this.checker); const declarationMeta =
staticallyResolve(ngModule.get('declarations') !, this.reflector, this.checker);
declarations = resolveTypeList(declarationMeta, 'declarations'); declarations = resolveTypeList(declarationMeta, 'declarations');
} }
let imports: Reference[] = []; let imports: Reference[] = [];
if (ngModule.has('imports')) { if (ngModule.has('imports')) {
const importsMeta = staticallyResolve( const importsMeta = staticallyResolve(
ngModule.get('imports') !, this.checker, ngModule.get('imports') !, this.reflector, this.checker,
node => this._extractModuleFromModuleWithProvidersFn(node)); node => this._extractModuleFromModuleWithProvidersFn(node));
imports = resolveTypeList(importsMeta, 'imports'); imports = resolveTypeList(importsMeta, 'imports');
} }
let exports: Reference[] = []; let exports: Reference[] = [];
if (ngModule.has('exports')) { if (ngModule.has('exports')) {
const exportsMeta = staticallyResolve( const exportsMeta = staticallyResolve(
ngModule.get('exports') !, this.checker, ngModule.get('exports') !, this.reflector, this.checker,
node => this._extractModuleFromModuleWithProvidersFn(node)); node => this._extractModuleFromModuleWithProvidersFn(node));
exports = resolveTypeList(exportsMeta, 'exports'); exports = resolveTypeList(exportsMeta, 'exports');
} }

View File

@ -44,7 +44,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
if (!pipe.has('name')) { if (!pipe.has('name')) {
throw new Error(`@Pipe decorator is missing name field`); 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') { if (typeof pipeName !== 'string') {
throw new Error(`@Pipe.name must be a string`); throw new Error(`@Pipe.name must be a string`);
} }
@ -52,7 +52,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
let pure = true; let pure = true;
if (pipe.has('pure')) { 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') { if (typeof pureValue !== 'boolean') {
throw new Error(`@Pipe.pure must be a boolean`); throw new Error(`@Pipe.pure must be a boolean`);
} }

View File

@ -83,10 +83,57 @@ export interface ClassMember {
nameNode: ts.Identifier|null; nameNode: ts.Identifier|null;
/** /**
* TypeScript `ts.Expression` which initializes this member, if the member is a property, or * TypeScript `ts.Expression` which represents the value of the member.
* `null` otherwise. *
* 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. * Whether the member is static or not.
@ -151,6 +198,24 @@ export interface Import {
from: string; 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. * 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. * `null` if the identifier doesn't resolve to an import but instead is locally defined.
*/ */
getImportOfIdentifier(id: ts.Identifier): Import|null; 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<string, Declaration>|null;
/**
* Check whether the given declaration node actually represents a class.
*/
isClass(node: ts.Declaration): boolean;
} }

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; 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`. * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
@ -104,7 +104,94 @@ export class TypeScriptReflectionHost implements ReflectionHost {
return {from, name}; return {from, name};
} }
isClass(node: ts.Node): node is ts.Declaration { return ts.isClassDeclaration(node); } getExportsOfModule(node: ts.Node): Map<string, Declaration>|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<string, Declaration>();
// 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 { private _reflectDecorator(node: ts.Decorator): Decorator|null {
// Attempt to resolve the decorator expression into a reference to a concrete Identifier. The // 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 { private _reflectMember(node: ts.ClassElement): ClassMember|null {
let kind: ClassMemberKind|null = null; let kind: ClassMemberKind|null = null;
let initializer: ts.Expression|null = null; let value: ts.Expression|null = null;
let name: string|null = null; let name: string|null = null;
let nameNode: ts.Identifier|null = null; let nameNode: ts.Identifier|null = null;
if (ts.isPropertyDeclaration(node)) { if (ts.isPropertyDeclaration(node)) {
kind = ClassMemberKind.Property; kind = ClassMemberKind.Property;
initializer = node.initializer || null; value = node.initializer || null;
} else if (ts.isGetAccessorDeclaration(node)) { } else if (ts.isGetAccessorDeclaration(node)) {
kind = ClassMemberKind.Getter; kind = ClassMemberKind.Getter;
} else if (ts.isSetAccessorDeclaration(node)) { } else if (ts.isSetAccessorDeclaration(node)) {
@ -169,8 +256,8 @@ export class TypeScriptReflectionHost implements ReflectionHost {
return { return {
node, node,
kind, implementation: node, kind,
type: node.type || null, name, nameNode, decorators, initializer, isStatic, type: node.type || null, name, nameNode, decorators, value, isStatic,
}; };
} }
} }

View File

@ -15,6 +15,8 @@ import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@ang
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassMemberKind, ReflectionHost} from '../../host';
const TS_DTS_EXTENSION = /(\.d)?\.ts$/; const TS_DTS_EXTENSION = /(\.d)?\.ts$/;
/** /**
@ -188,10 +190,10 @@ export class AbsoluteReference extends Reference {
* @returns a `ResolvedValue` representing the resolved value * @returns a `ResolvedValue` representing the resolved value
*/ */
export function staticallyResolve( export function staticallyResolve(
node: ts.Expression, checker: ts.TypeChecker, node: ts.Expression, host: ReflectionHost, checker: ts.TypeChecker,
foreignFunctionResolver?: (node: ts.FunctionDeclaration | ts.MethodDeclaration) => foreignFunctionResolver?: (node: ts.FunctionDeclaration | ts.MethodDeclaration) =>
ts.Expression | null): ResolvedValue { ts.Expression | null): ResolvedValue {
return new StaticInterpreter(checker).visit(node, { return new StaticInterpreter(host, checker).visit(node, {
absoluteModuleName: null, absoluteModuleName: null,
scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver, scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver,
}); });
@ -243,7 +245,7 @@ interface Context {
} }
class StaticInterpreter { class StaticInterpreter {
constructor(private checker: ts.TypeChecker) {} constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
visit(node: ts.Expression, context: Context): ResolvedValue { visit(node: ts.Expression, context: Context): ResolvedValue {
return this.visitExpression(node, context); return this.visitExpression(node, context);
@ -286,7 +288,7 @@ class StaticInterpreter {
return this.visitExpression(node.expression, context); return this.visitExpression(node.expression, context);
} else if (ts.isNonNullExpression(node)) { } else if (ts.isNonNullExpression(node)) {
return this.visitExpression(node.expression, context); return this.visitExpression(node.expression, context);
} else if (ts.isClassDeclaration(node)) { } else if (isPossibleClassDeclaration(node) && this.host.isClass(node)) {
return this.visitDeclaration(node, context); return this.visitDeclaration(node, context);
} else { } else {
return DYNAMIC_VALUE; return DYNAMIC_VALUE;
@ -356,14 +358,6 @@ class StaticInterpreter {
return map; 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 { private visitTemplateExpression(node: ts.TemplateExpression, context: Context): ResolvedValue {
const pieces: string[] = [node.head.text]; const pieces: string[] = [node.head.text];
for (let i = 0; i < node.templateSpans.length; i++) { for (let i = 0; i < node.templateSpans.length; i++) {
@ -380,48 +374,19 @@ class StaticInterpreter {
return pieces.join(''); return pieces.join('');
} }
private visitSymbol(symbol: ts.Symbol, context: Context): ResolvedValue { private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue {
let absoluteModuleName = context.absoluteModuleName; const decl = this.host.getDeclarationOfIdentifier(node);
if (symbol.declarations !== undefined && symbol.declarations.length > 0) { if (decl === null) {
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) {
return DYNAMIC_VALUE; return DYNAMIC_VALUE;
} }
return this.visitDeclaration(
if (symbol.valueDeclaration !== undefined) { decl.node, {...context, absoluteModuleName: decl.viaModule || context.absoluteModuleName});
return this.visitDeclaration(symbol.valueDeclaration, newContext);
}
return symbol.declarations.reduce<ResolvedValue>((prev, decl) => {
if (!(isDynamicValue(prev) || prev instanceof Reference)) {
return prev;
}
return this.visitDeclaration(decl, newContext);
}, DYNAMIC_VALUE);
} }
private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue { 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) { if (!node.initializer) {
return undefined; return undefined;
} }
@ -471,14 +436,18 @@ class StaticInterpreter {
} }
private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue { private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue {
const map = new Map<string, ResolvedValue>(); const declarations = this.host.getExportsOfModule(node);
const symbol = this.checker.getSymbolAtLocation(node); if (declarations === null) {
if (symbol === undefined) {
return DYNAMIC_VALUE; return DYNAMIC_VALUE;
} }
const exports = this.checker.getExportsOfModule(symbol); const map = new Map<string, ResolvedValue>();
exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol, context))); declarations.forEach((decl, name) => {
const value = this.visitDeclaration(decl.node, {
...context,
absoluteModuleName: decl.viaModule || context.absoluteModuleName,
});
map.set(name, value);
});
return map; return map;
} }
@ -503,22 +472,21 @@ class StaticInterpreter {
return lhs[rhs]; return lhs[rhs];
} else if (lhs instanceof Reference) { } else if (lhs instanceof Reference) {
const ref = lhs.node; const ref = lhs.node;
if (ts.isClassDeclaration(ref)) { if (isPossibleClassDeclaration(ref) && this.host.isClass(ref)) {
let absoluteModuleName = context.absoluteModuleName; let absoluteModuleName = context.absoluteModuleName;
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) {
absoluteModuleName = lhs.moduleName || absoluteModuleName; absoluteModuleName = lhs.moduleName || absoluteModuleName;
} }
let value: ResolvedValue = undefined; let value: ResolvedValue = undefined;
const member = const member = this.host.getMembersOfClass(ref).find(
ref.members.filter(member => isStatic(member)) member => member.isStatic && member.name === strIndex);
.find(
member => member.name !== undefined &&
this.stringNameFromPropertyName(member.name, context) === strIndex);
if (member !== undefined) { if (member !== undefined) {
if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) { if (member.value !== null) {
value = this.visitExpression(member.initializer, context); value = this.visitExpression(member.value, context);
} else if (ts.isMethodDeclaration(member)) { } else if (member.implementation !== null) {
value = new NodeReference(member, absoluteModuleName); value = new NodeReference(member.implementation, absoluteModuleName);
} else {
value = new NodeReference(member.node, absoluteModuleName);
} }
} }
return value; 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| function isFunctionOrMethodDeclaration(node: ts.Node): node is ts.FunctionDeclaration|
ts.MethodDeclaration { ts.MethodDeclaration {
return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node); return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node);
@ -691,3 +654,7 @@ function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined
return undefined; return undefined;
} }
} }
function isPossibleClassDeclaration(node: ts.Node): node is ts.Declaration {
return ts.isClassDeclaration(node) || ts.isVariableDeclaration(node);
}

View File

@ -120,6 +120,48 @@ describe('reflector', () => {
expectParameter(args[1], 'otherBar', 'star.Bar'); 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( function expectParameter(

View File

@ -9,6 +9,7 @@
import {ExternalExpr} from '@angular/compiler'; import {ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {TypeScriptReflectionHost} from '..';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {Reference, ResolvedValue, staticallyResolve} from '../src/resolver'; import {Reference, ResolvedValue, staticallyResolve} from '../src/resolver';
@ -30,7 +31,8 @@ function makeExpression(
function evaluate<T extends ResolvedValue>(code: string, expr: string): T { function evaluate<T extends ResolvedValue>(code: string, expr: string): T {
const {expression, checker} = makeExpression(code, expr); 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', () => { describe('ngtsc metadata', () => {
@ -52,8 +54,9 @@ describe('ngtsc metadata', () => {
} }
]); ]);
const decl = getDeclaration(program, 'entry.ts', 'X', ts.isVariableDeclaration); 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'); expect(value).toEqual('test');
}); });
@ -132,9 +135,10 @@ describe('ngtsc metadata', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
const resolved = staticallyResolve(expr, checker); const resolved = staticallyResolve(expr, host, checker);
if (!(resolved instanceof Reference)) { if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference'); return fail('Expected expression to resolve to a reference');
} }
@ -160,9 +164,10 @@ describe('ngtsc metadata', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
const resolved = staticallyResolve(expr, checker); const resolved = staticallyResolve(expr, host, checker);
if (!(resolved instanceof Reference)) { if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference'); return fail('Expected expression to resolve to a reference');
} }
@ -188,9 +193,10 @@ describe('ngtsc metadata', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
expect(staticallyResolve(expr, checker)).toEqual('test'); expect(staticallyResolve(expr, host, checker)).toEqual('test');
}); });
it('reads values from named exports', () => { it('reads values from named exports', () => {
@ -205,9 +211,10 @@ describe('ngtsc metadata', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
expect(staticallyResolve(expr, checker)).toEqual('test'); expect(staticallyResolve(expr, host, checker)).toEqual('test');
}); });
it('chain of re-exports works', () => { it('chain of re-exports works', () => {
@ -222,9 +229,10 @@ describe('ngtsc metadata', () => {
}, },
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !; const expr = result.initializer !;
expect(staticallyResolve(expr, checker)).toEqual('test'); expect(staticallyResolve(expr, host, checker)).toEqual('test');
}); });
it('map spread works', () => { it('map spread works', () => {