refactor(ivy): split apart the 'metadata' package in the ngtsc compiler (#27743)

This refactoring moves code around between a few of the ngtsc subpackages,
with the goal of having a more logical package structure. Additional
interfaces are also introduced where they make sense.

The 'metadata' package formerly contained both the partial evaluator,
the TypeScriptReflectionHost as well as some other reflection functions,
and the Reference interface and various implementations. This package
was split into 3 parts.

The partial evaluator now has its own package 'partial_evaluator', and
exists behind an interface PartialEvaluator instead of a top-level
function. In the future this will be useful for reducing churn as the
partial evaluator becomes more complicated.

The TypeScriptReflectionHost and other miscellaneous functions have moved
into a new 'reflection' package. The former 'host' package which contained
the ReflectionHost interface and associated types was also merged into this
new 'reflection' package.

Finally, the Reference APIs were moved to the 'imports' package, which will
consolidate all import-related logic in ngtsc.

PR Close #27743
This commit is contained in:
Alex Rickabaugh
2018-12-18 09:48:15 -08:00
committed by Kara Erickson
parent 37b716b298
commit 2a6108af97
64 changed files with 534 additions and 433 deletions

View File

@ -0,0 +1,13 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "reflection",
srcs = glob([
"index.ts",
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/reflection",
deps = ["@ngdeps//typescript"],
)

View File

@ -0,0 +1,10 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './src/host';
export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration, typeNodeToValueExpr} from './src/typescript';

View File

@ -0,0 +1,463 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
/**
* Metadata extracted from an instance of a decorator on another declaration.
*/
export interface Decorator {
/**
* Name by which the decorator was invoked in the user's code.
*
* This is distinct from the name by which the decorator was imported (though in practice they
* will usually be the same).
*/
name: string;
/**
* Identifier which refers to the decorator in source.
*/
identifier: ts.Identifier;
/**
* `Import` by which the decorator was brought into the module in which it was invoked, or `null`
* if the decorator was declared in the same module and not imported.
*/
import : Import | null;
/**
* TypeScript reference to the decorator itself.
*/
node: ts.Node;
/**
* Arguments of the invocation of the decorator, if the decorator is invoked, or `null` otherwise.
*/
args: ts.Expression[]|null;
}
/**
* An enumeration of possible kinds of class members.
*/
export enum ClassMemberKind {
Constructor,
Getter,
Setter,
Property,
Method,
}
/**
* A member of a class, such as a property, method, or constructor.
*/
export interface ClassMember {
/**
* TypeScript reference to the class member itself, or null if it is not applicable.
*/
node: ts.Node|null;
/**
* Indication of which type of member this is (property, method, etc).
*/
kind: ClassMemberKind;
/**
* TypeScript `ts.TypeNode` representing the type of the member, or `null` if not present or
* applicable.
*/
type: ts.TypeNode|null;
/**
* Name of the class member.
*/
name: string;
/**
* TypeScript `ts.Identifier` representing the name of the member, or `null` if no such node
* is present.
*
* The `nameNode` is useful in writing references to this member that will be correctly source-
* mapped back to the original file.
*/
nameNode: ts.Identifier|null;
/**
* 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.
*/
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.
*/
isStatic: boolean;
/**
* Any `Decorator`s which are present on the member, or `null` if none are present.
*/
decorators: Decorator[]|null;
}
/**
* A parameter to a constructor.
*/
export interface CtorParameter {
/**
* Name of the parameter, if available.
*
* Some parameters don't have a simple string name (for example, parameters which are destructured
* into multiple variables). In these cases, `name` can be `null`.
*/
name: string|null;
/**
* TypeScript `ts.BindingName` representing the name of the parameter.
*
* The `nameNode` is useful in writing references to this member that will be correctly source-
* mapped back to the original file.
*/
nameNode: ts.BindingName;
/**
* TypeScript `ts.Expression` representing the type value of the parameter, if the type is a
* simple
* expression type that can be converted to a value.
*
* If the type is not present or cannot be represented as an expression, `type` is `null`.
*/
typeExpression: ts.Expression|null;
/**
* TypeScript `ts.TypeNode` representing the type node found in the type position.
*
* This field can be used for diagnostics reporting if `typeExpression` is `null`.
*
* Can be null, if the param has no type declared.
*/
typeNode: ts.TypeNode|null;
/**
* Any `Decorator`s which are present on the parameter, or `null` if none are present.
*/
decorators: Decorator[]|null;
}
/**
* Definition of a function or method, including its body if present and any parameters.
*
* In TypeScript code this metadata will be a simple reflection of the declarations in the node
* itself. In ES5 code this can be more complicated, as the default values for parameters may
* be extracted from certain body statements.
*/
export interface FunctionDefinition<T extends ts.MethodDeclaration|ts.FunctionDeclaration|
ts.FunctionExpression> {
/**
* A reference to the node which declares the function.
*/
node: T;
/**
* Statements of the function body, if a body is present, or null if no body is present.
*
* This list may have been filtered to exclude statements which perform parameter default value
* initialization.
*/
body: ts.Statement[]|null;
/**
* Metadata regarding the function's parameters, including possible default value expressions.
*/
parameters: Parameter[];
}
/**
* A parameter to a function or method.
*/
export interface Parameter {
/**
* Name of the parameter, if available.
*/
name: string|null;
/**
* Declaration which created this parameter.
*/
node: ts.ParameterDeclaration;
/**
* Expression which represents the default value of the parameter, if any.
*/
initializer: ts.Expression|null;
}
/**
* The source of an imported symbol, including the original symbol name and the module from which it
* was imported.
*/
export interface Import {
/**
* The name of the imported symbol under which it was exported (not imported).
*/
name: string;
/**
* The module from which the symbol was imported.
*
* This could either be an absolute module name (@angular/core for example) or a relative path.
*/
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.
*
* Depending on the format of the code being interpreted, different concepts are represented with
* different syntactical structures. The `ReflectionHost` abstracts over those differences and
* presents a single API by which the compiler can query specific information about the AST.
*
* All operations on the `ReflectionHost` require the use of TypeScript `ts.Node`s with binding
* information already available (that is, nodes that come from a `ts.Program` that has been
* type-checked, and are not synthetically created).
*/
export interface ReflectionHost {
/**
* Examine a declaration (for example, of a class or function) and return metadata about any
* decorators present on the declaration.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class or function over
* which to reflect. For example, if the intent is to reflect the decorators of a class and the
* source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5
* format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the
* result of an IIFE execution.
*
* @returns an array of `Decorator` metadata if decorators are present on the declaration, or
* `null` if either no decorators were present or if the declaration is not of a decorable type.
*/
getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null;
/**
* Examine a declaration which should be of a class, and return metadata about the members of the
* class.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class over which to
* reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the
* source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are
* represented as the result of an IIFE execution.
*
* @returns an array of `ClassMember` metadata representing the members of the class.
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getMembersOfClass(clazz: ts.Declaration): ClassMember[];
/**
* Reflect over the constructor of a class and return metadata about its parameters.
*
* This method only looks at the constructor of a class directly and not at any inherited
* constructors.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class over which to
* reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the
* source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are
* represented as the result of an IIFE execution.
*
* @returns an array of `Parameter` metadata representing the parameters of the constructor, if
* a constructor exists. If the constructor exists and has 0 parameters, this array will be empty.
* If the class has no constructor, this method returns `null`.
*/
getConstructorParameters(declaration: ts.Declaration): CtorParameter[]|null;
/**
* Reflect over a function and return metadata about its parameters and body.
*
* Functions in TypeScript and ES5 code have different AST representations, in particular around
* default values for parameters. A TypeScript function has its default value as the initializer
* on the parameter declaration, whereas an ES5 function has its default value set in a statement
* of the form:
*
* if (param === void 0) { param = 3; }
*
* This method abstracts over these details, and interprets the function declaration and body to
* extract parameter default values and the "real" body.
*
* A current limitation is that this metadata has no representation for shorthand assignment of
* parameter objects in the function signature.
*
* @param fn a TypeScript `ts.Declaration` node representing the function over which to reflect.
*
* @returns a `FunctionDefinition` giving metadata about the function definition.
*/
getDefinitionOfFunction<T extends ts.MethodDeclaration|ts.FunctionDeclaration|
ts.FunctionExpression>(fn: T): FunctionDefinition<T>;
/**
* Determine if an identifier was imported from another module and return `Import` metadata
* describing its origin.
*
* @param id a TypeScript `ts.Identifer` to reflect.
*
* @returns metadata about the `Import` if the identifier was imported from another module, or
* `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 node actually represents a class.
*/
isClass(node: ts.Node): node is ts.NamedDeclaration;
hasBaseClass(node: ts.Declaration): boolean;
/**
* Get the number of generic type parameters of a given class.
*
* @returns the number of type parameters of the class, if known, or `null` if the declaration
* is not a class or has an unknown number of type parameters.
*/
getGenericArityOfClass(clazz: ts.Declaration): number|null;
/**
* Find the assigned value of a variable declaration.
*
* Normally this will be the initializer of the declaration, but where the variable is
* not a `const` we may need to look elsewhere for the variable's value.
*
* @param declaration a TypeScript variable declaration, whose value we want.
* @returns the value of the variable, as a TypeScript expression node, or `undefined`
* if the value cannot be computed.
*/
getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null;
/**
* Take an exported declaration (maybe a class down-leveled to a variable) and look up the
* declaration of its type in a separate .d.ts tree.
*
* This function is allowed to return `null` if the current compilation unit does not have a
* separate .d.ts tree. When compiling TypeScript code this is always the case, since .d.ts files
* are produced only during the emit of such a compilation. When compiling .js code, however,
* there is frequently a parallel .d.ts tree which this method exposes.
*
* Note that the `ts.Declaration` returned from this function may not be from the same
* `ts.Program` as the input declaration.
*/
getDtsDeclaration(declaration: ts.Declaration): ts.Declaration|null;
}

View File

@ -0,0 +1,464 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost} from './host';
/**
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
*/
export class TypeScriptReflectionHost implements ReflectionHost {
constructor(protected checker: ts.TypeChecker) {}
getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null {
if (declaration.decorators === undefined || declaration.decorators.length === 0) {
return null;
}
return declaration.decorators.map(decorator => this._reflectDecorator(decorator))
.filter((dec): dec is Decorator => dec !== null);
}
getMembersOfClass(declaration: ts.Declaration): ClassMember[] {
const clazz = castDeclarationToClassOrDie(declaration);
return clazz.members.map(member => this._reflectMember(member))
.filter((member): member is ClassMember => member !== null);
}
getConstructorParameters(declaration: ts.Declaration): CtorParameter[]|null {
const clazz = castDeclarationToClassOrDie(declaration);
// First, find the constructor.
const ctor = clazz.members.find(ts.isConstructorDeclaration);
if (ctor === undefined) {
return null;
}
return ctor.parameters.map(node => {
// The name of the parameter is easy.
const name = parameterName(node.name);
const decorators = this.getDecoratorsOfDeclaration(node);
// It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter.
let typeValueExpr: ts.Expression|null = null;
let originalTypeNode = node.type || null;
let typeNode = originalTypeNode;
// Check if we are dealing with a simple nullable union type e.g. `foo: Foo|null`
// and extract the type. More complext union types e.g. `foo: Foo|Bar` are not supported.
// We also don't need to support `foo: Foo|undefined` because Angular's DI injects `null` for
// optional tokes that don't have providers.
if (typeNode && ts.isUnionTypeNode(typeNode)) {
let childTypeNodes = typeNode.types.filter(
childTypeNode => childTypeNode.kind !== ts.SyntaxKind.NullKeyword);
if (childTypeNodes.length === 1) {
typeNode = childTypeNodes[0];
} else {
typeNode = null;
}
}
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (typeNode) {
// It's only valid to convert a type reference to a value reference if the type actually has
// a value declaration associated with it.
let type: ts.Type|null = this.checker.getTypeFromTypeNode(typeNode);
if (type && type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) {
// The type points to a valid value declaration. Rewrite the TypeReference into an
// Expression
// which references the value pointed to by the TypeReference, if possible.
typeValueExpr = typeNodeToValueExpr(typeNode);
}
}
return {
name,
nameNode: node.name,
typeExpression: typeValueExpr,
typeNode: originalTypeNode, decorators,
};
});
}
getImportOfIdentifier(id: ts.Identifier): Import|null {
const symbol = this.checker.getSymbolAtLocation(id);
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
return null;
}
// Ignore decorators that are defined locally (not imported).
const decl: ts.Declaration = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
// Walk back from the specifier to find the declaration, which carries the module specifier.
const importDecl = decl.parent !.parent !.parent !;
// The module specifier is guaranteed to be a string literal, so this should always pass.
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
// Not allowed to happen in TypeScript ASTs.
return null;
}
// Read the module specifier.
const from = importDecl.moduleSpecifier.text;
// Compute the name by which the decorator was exported, not imported.
const name = (decl.propertyName !== undefined ? decl.propertyName : decl.name).text;
return {from, name};
}
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.Node): node is ts.NamedDeclaration {
// In TypeScript code, classes are ts.ClassDeclarations.
return ts.isClassDeclaration(node);
}
hasBaseClass(node: ts.Declaration): boolean {
return ts.isClassDeclaration(node) && node.heritageClauses !== undefined &&
node.heritageClauses.some(clause => clause.token === ts.SyntaxKind.ExtendsKeyword);
}
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);
}
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
return {
node,
body: node.body !== undefined ? Array.from(node.body.statements) : null,
parameters: node.parameters.map(param => {
const name = parameterName(param.name);
const initializer = param.initializer || null;
return {name, node: param, initializer};
}),
};
}
getGenericArityOfClass(clazz: ts.Declaration): number|null {
if (!ts.isClassDeclaration(clazz)) {
return null;
}
return clazz.typeParameters !== undefined ? clazz.typeParameters.length : 0;
}
getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null {
return declaration.initializer || null;
}
getDtsDeclaration(_: ts.Declaration): ts.Declaration|null { return null; }
/**
* 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
// expression may contain a call to a function which returns the decorator function, in which
// case we want to return the arguments.
let decoratorExpr: ts.Expression = node.expression;
let args: ts.Expression[]|null = null;
// Check for call expressions.
if (ts.isCallExpression(decoratorExpr)) {
args = Array.from(decoratorExpr.arguments);
decoratorExpr = decoratorExpr.expression;
}
// The final resolved decorator should be a `ts.Identifier` - if it's not, then something is
// wrong and the decorator can't be resolved statically.
if (!ts.isIdentifier(decoratorExpr)) {
return null;
}
const importDecl = this.getImportOfIdentifier(decoratorExpr);
return {
name: decoratorExpr.text,
identifier: decoratorExpr,
import: importDecl, node, args,
};
}
private _reflectMember(node: ts.ClassElement): ClassMember|null {
let kind: ClassMemberKind|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;
value = node.initializer || null;
} else if (ts.isGetAccessorDeclaration(node)) {
kind = ClassMemberKind.Getter;
} else if (ts.isSetAccessorDeclaration(node)) {
kind = ClassMemberKind.Setter;
} else if (ts.isMethodDeclaration(node)) {
kind = ClassMemberKind.Method;
} else if (ts.isConstructorDeclaration(node)) {
kind = ClassMemberKind.Constructor;
} else {
return null;
}
if (ts.isConstructorDeclaration(node)) {
name = 'constructor';
} else if (ts.isIdentifier(node.name)) {
name = node.name.text;
nameNode = node.name;
} else {
return null;
}
const decorators = this.getDecoratorsOfDeclaration(node);
const isStatic = node.modifiers !== undefined &&
node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
return {
node,
implementation: node, kind,
type: node.type || null, name, nameNode, decorators, value, isStatic,
};
}
}
export function reflectNameOfDeclaration(decl: ts.Declaration): string|null {
const id = reflectIdentifierOfDeclaration(decl);
return id && id.text || null;
}
export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identifier|null {
if (ts.isClassDeclaration(decl) || ts.isFunctionDeclaration(decl)) {
return decl.name || null;
} else if (ts.isVariableDeclaration(decl)) {
if (ts.isIdentifier(decl.name)) {
return decl.name;
}
}
return null;
}
export function reflectTypeEntityToDeclaration(
type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string | null} {
let realSymbol = checker.getSymbolAtLocation(type);
if (realSymbol === undefined) {
throw new Error(`Cannot resolve type entity ${type.getText()} to symbol`);
}
while (realSymbol.flags & ts.SymbolFlags.Alias) {
realSymbol = checker.getAliasedSymbol(realSymbol);
}
let node: ts.Declaration|null = null;
if (realSymbol.valueDeclaration !== undefined) {
node = realSymbol.valueDeclaration;
} else if (realSymbol.declarations !== undefined && realSymbol.declarations.length === 1) {
node = realSymbol.declarations[0];
} else {
throw new Error(`Cannot resolve type entity symbol to declaration`);
}
if (ts.isQualifiedName(type)) {
if (!ts.isIdentifier(type.left)) {
throw new Error(`Cannot handle qualified name with non-identifier lhs`);
}
const symbol = checker.getSymbolAtLocation(type.left);
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
throw new Error(`Cannot resolve qualified type entity lhs to symbol`);
}
const decl = symbol.declarations[0];
if (ts.isNamespaceImport(decl)) {
const clause = decl.parent !;
const importDecl = clause.parent !;
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
throw new Error(`Module specifier is not a string`);
}
return {node, from: importDecl.moduleSpecifier.text};
} else {
throw new Error(`Unknown import type?`);
}
} else {
return {node, from: null};
}
}
export function filterToMembersWithDecorator(members: ClassMember[], name: string, module?: string):
{member: ClassMember, decorators: Decorator[]}[] {
return members.filter(member => !member.isStatic)
.map(member => {
if (member.decorators === null) {
return null;
}
const decorators = member.decorators.filter(dec => {
if (dec.import !== null) {
return dec.import.name === name && (module === undefined || dec.import.from === module);
} else {
return dec.name === name && module === undefined;
}
});
if (decorators.length === 0) {
return null;
}
return {member, decorators};
})
.filter((value): value is {member: ClassMember, decorators: Decorator[]} => value !== null);
}
export function findMember(
members: ClassMember[], name: string, isStatic: boolean = false): ClassMember|null {
return members.find(member => member.isStatic === isStatic && member.name === name) || null;
}
export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
const map = new Map<string, ts.Expression>();
node.properties.forEach(prop => {
if (ts.isPropertyAssignment(prop)) {
const name = propertyNameToString(prop.name);
if (name === null) {
return;
}
map.set(name, prop.initializer);
} else if (ts.isShorthandPropertyAssignment(prop)) {
map.set(prop.name.text, prop.name);
} else {
return;
}
});
return map;
}
function castDeclarationToClassOrDie(declaration: ts.Declaration): ts.ClassDeclaration {
if (!ts.isClassDeclaration(declaration)) {
throw new Error(
`Reflecting on a ${ts.SyntaxKind[declaration.kind]} instead of a ClassDeclaration.`);
}
return declaration;
}
function parameterName(name: ts.BindingName): string|null {
if (ts.isIdentifier(name)) {
return name.text;
} else {
return null;
}
}
export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
function propertyNameToString(node: ts.PropertyName): string|null {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else {
return null;
}
}

View File

@ -0,0 +1,26 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"@ngdeps//typescript",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
deps = [
":test_lib",
"//tools/testing:node_no_angular",
],
)

View File

@ -0,0 +1,227 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {CtorParameter} from '../src/host';
import {TypeScriptReflectionHost} from '../src/typescript';
describe('reflector', () => {
describe('ctor params', () => {
it('should reflect a single argument', () => {
const {program} = makeProgram([{
name: 'entry.ts',
contents: `
class Bar {}
class Foo {
constructor(bar: Bar) {}
}
`
}]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', 'Bar');
});
it('should reflect a decorated argument', () => {
const {program} = makeProgram([
{
name: 'dec.ts',
contents: `
export function dec(target: any, key: string, index: number) {
}
`
},
{
name: 'entry.ts',
contents: `
import {dec} from './dec';
class Bar {}
class Foo {
constructor(@dec bar: Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
});
it('should reflect a decorated argument with a call', () => {
const {program} = makeProgram([
{
name: 'dec.ts',
contents: `
export function dec(target: any, key: string, index: number) {
}
`
},
{
name: 'entry.ts',
contents: `
import {dec} from './dec';
class Bar {}
class Foo {
constructor(@dec bar: Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
});
it('should reflect a decorated argument with an indirection', () => {
const {program} = makeProgram([
{
name: 'bar.ts',
contents: `
export class Bar {}
`
},
{
name: 'entry.ts',
contents: `
import {Bar} from './bar';
import * as star from './bar';
class Foo {
constructor(bar: Bar, otherBar: star.Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(2);
expectParameter(args[0], 'bar', 'Bar');
expectParameter(args[1], 'otherBar', 'star.Bar');
});
it('should reflect an nullable argument', () => {
const {program} = makeProgram([
{
name: 'bar.ts',
contents: `
export class Bar {}
`
},
{
name: 'entry.ts',
contents: `
import {Bar} from './bar';
class Foo {
constructor(bar: Bar|null) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', '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(
param: CtorParameter, name: string, type?: string, decorator?: string,
decoratorFrom?: string): void {
expect(param.name !).toEqual(name);
if (type === undefined) {
expect(param.typeExpression).toBeNull();
} else {
expect(param.typeExpression).not.toBeNull();
expect(argExpressionToString(param.typeExpression !)).toEqual(type);
}
if (decorator !== undefined) {
expect(param.decorators).not.toBeNull();
expect(param.decorators !.length).toBeGreaterThan(0);
expect(param.decorators !.some(
dec => dec.name === decorator && dec.import !== null &&
dec.import.from === decoratorFrom))
.toBe(true);
}
}
function argExpressionToString(name: ts.Node | null): string {
if (name == null) {
throw new Error('\'name\' argument can\'t be null');
}
if (ts.isIdentifier(name)) {
return name.text;
} else if (ts.isPropertyAccessExpression(name)) {
return `${argExpressionToString(name.expression)}.${name.name.text}`;
} else {
throw new Error(`Unexpected node in arg expression: ${ts.SyntaxKind[name.kind]}.`);
}
}