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);
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') {
throw new Error(`templateUrl should be a string`);
}
@ -73,7 +74,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
let templateStr: string|null = null;
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') {
throw new Error(`templateUrl should be a string`);
}
@ -81,7 +83,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
templateStr = this.resourceLoader.load(url);
} else if (component.has('template')) {
const templateExpr = component.get('template') !;
const resolvedTemplate = staticallyResolve(templateExpr, this.checker);
const resolvedTemplate = staticallyResolve(templateExpr, this.reflector, this.checker);
if (typeof resolvedTemplate !== 'string') {
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;
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') {
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.
const coreModule = this.isCore ? undefined : '@angular/core';
const viewChildFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.checker);
filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.reflector,
this.checker);
const viewChildrenFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.checker);
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.reflector,
this.checker);
const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields];
if (component.has('queries')) {

View File

@ -90,19 +90,21 @@ export function extractDirectiveMetadata(
// Construct the map of inputs both from the @Directive/@Component
// decorator, and the decorated
// fields.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker);
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', reflector, checker);
const inputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), checker);
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker);
// And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', reflector, checker);
const outputsFromFields = parseDecoratedFields(
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), checker);
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), reflector, checker);
// Construct the list of queries.
const contentChildFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), checker);
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
checker);
const contentChildrenFromFields = queriesFromFields(
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), checker);
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), reflector,
checker);
const queries = [...contentChildFromFields, ...contentChildrenFromFields];
@ -115,14 +117,14 @@ export function extractDirectiveMetadata(
// Parse the selector.
let selector = '';
if (directive.has('selector')) {
const resolved = staticallyResolve(directive.get('selector') !, checker);
const resolved = staticallyResolve(directive.get('selector') !, reflector, checker);
if (typeof resolved !== 'string') {
throw new Error(`Selector must be a string`);
}
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.
const usesOnChanges = members.find(
@ -148,12 +150,12 @@ export function extractDirectiveMetadata(
export function extractQueryMetadata(
name: string, args: ReadonlyArray<ts.Expression>, 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<string, ts.Expression>, field: string,
directive: Map<string, ts.Expression>, 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<string, ts.Expression>, members: ClassMember[], checker: ts.TypeChecker,
coreModule: string | undefined): {
metadata: Map<string, ts.Expression>, 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`);
}

View File

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

View File

@ -44,7 +44,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
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<R3PipeMetadata> {
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`);
}

View File

@ -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<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 {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<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 {
// 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,
};
}
}

View File

@ -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<ts.ParameterDeclaration, ResolvedValue>(), 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<ResolvedValue>((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<string, ResolvedValue>();
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<string, ResolvedValue>();
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);
}

View File

@ -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(

View File

@ -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<T extends ResolvedValue>(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', () => {