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,21 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "partial_evaluator",
srcs = glob([
"index.ts",
"src/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/partial_evaluator",
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//@types/node",
"@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 {ForeignFunctionResolver, PartialEvaluator} from './src/interface';
export {BuiltinFn, DynamicValue, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './src/result';

View File

@ -0,0 +1,21 @@
/**
* @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 {BuiltinFn, DYNAMIC_VALUE, ResolvedValue, ResolvedValueArray} from './result';
export class ArraySliceBuiltinFn extends BuiltinFn {
constructor(private lhs: ResolvedValueArray) { super(); }
evaluate(args: ResolvedValueArray): ResolvedValue {
if (args.length === 0) {
return this.lhs;
} else {
return DYNAMIC_VALUE;
}
}
}

View File

@ -0,0 +1,31 @@
/**
* @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 {Reference} from '../../imports';
import {ReflectionHost} from '../../reflection';
import {StaticInterpreter} from './interpreter';
import {ResolvedValue} from './result';
export type ForeignFunctionResolver =
(node: Reference<ts.FunctionDeclaration|ts.MethodDeclaration>, args: ts.Expression[]) =>
ts.Expression | null;
export class PartialEvaluator {
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue {
const interpreter = new StaticInterpreter(this.host, this.checker);
return interpreter.visit(expr, {
absoluteModuleName: null,
scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver,
});
}
}

View File

@ -0,0 +1,534 @@
/**
* @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 {AbsoluteReference, NodeReference, Reference, ResolvedReference} from '../../imports';
import {ReflectionHost} from '../../reflection';
import {ArraySliceBuiltinFn} from './builtin';
import {BuiltinFn, DYNAMIC_VALUE, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './result';
/**
* Tracks the scope of a function body, which includes `ResolvedValue`s for the parameters of that
* body.
*/
type Scope = Map<ts.ParameterDeclaration, ResolvedValue>;
interface BinaryOperatorDef {
literal: boolean;
op: (a: any, b: any) => ResolvedValue;
}
function literalBinaryOp(op: (a: any, b: any) => any): BinaryOperatorDef {
return {op, literal: true};
}
function referenceBinaryOp(op: (a: any, b: any) => any): BinaryOperatorDef {
return {op, literal: false};
}
const BINARY_OPERATORS = new Map<ts.SyntaxKind, BinaryOperatorDef>([
[ts.SyntaxKind.PlusToken, literalBinaryOp((a, b) => a + b)],
[ts.SyntaxKind.MinusToken, literalBinaryOp((a, b) => a - b)],
[ts.SyntaxKind.AsteriskToken, literalBinaryOp((a, b) => a * b)],
[ts.SyntaxKind.SlashToken, literalBinaryOp((a, b) => a / b)],
[ts.SyntaxKind.PercentToken, literalBinaryOp((a, b) => a % b)],
[ts.SyntaxKind.AmpersandToken, literalBinaryOp((a, b) => a & b)],
[ts.SyntaxKind.BarToken, literalBinaryOp((a, b) => a | b)],
[ts.SyntaxKind.CaretToken, literalBinaryOp((a, b) => a ^ b)],
[ts.SyntaxKind.LessThanToken, literalBinaryOp((a, b) => a < b)],
[ts.SyntaxKind.LessThanEqualsToken, literalBinaryOp((a, b) => a <= b)],
[ts.SyntaxKind.GreaterThanToken, literalBinaryOp((a, b) => a > b)],
[ts.SyntaxKind.GreaterThanEqualsToken, literalBinaryOp((a, b) => a >= b)],
[ts.SyntaxKind.LessThanLessThanToken, literalBinaryOp((a, b) => a << b)],
[ts.SyntaxKind.GreaterThanGreaterThanToken, literalBinaryOp((a, b) => a >> b)],
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, literalBinaryOp((a, b) => a >>> b)],
[ts.SyntaxKind.AsteriskAsteriskToken, literalBinaryOp((a, b) => Math.pow(a, b))],
[ts.SyntaxKind.AmpersandAmpersandToken, referenceBinaryOp((a, b) => a && b)],
[ts.SyntaxKind.BarBarToken, referenceBinaryOp((a, b) => a || b)]
]);
const UNARY_OPERATORS = new Map<ts.SyntaxKind, (a: any) => any>([
[ts.SyntaxKind.TildeToken, a => ~a], [ts.SyntaxKind.MinusToken, a => -a],
[ts.SyntaxKind.PlusToken, a => +a], [ts.SyntaxKind.ExclamationToken, a => !a]
]);
interface Context {
absoluteModuleName: string|null;
scope: Scope;
foreignFunctionResolver?
(ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>,
args: ReadonlyArray<ts.Expression>): ts.Expression|null;
}
export class StaticInterpreter {
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
visit(node: ts.Expression, context: Context): ResolvedValue {
return this.visitExpression(node, context);
}
private visitExpression(node: ts.Expression, context: Context): ResolvedValue {
if (node.kind === ts.SyntaxKind.TrueKeyword) {
return true;
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
return false;
} else if (ts.isStringLiteral(node)) {
return node.text;
} else if (ts.isNoSubstitutionTemplateLiteral(node)) {
return node.text;
} else if (ts.isTemplateExpression(node)) {
return this.visitTemplateExpression(node, context);
} else if (ts.isNumericLiteral(node)) {
return parseFloat(node.text);
} else if (ts.isObjectLiteralExpression(node)) {
return this.visitObjectLiteralExpression(node, context);
} else if (ts.isIdentifier(node)) {
return this.visitIdentifier(node, context);
} else if (ts.isPropertyAccessExpression(node)) {
return this.visitPropertyAccessExpression(node, context);
} else if (ts.isCallExpression(node)) {
return this.visitCallExpression(node, context);
} else if (ts.isConditionalExpression(node)) {
return this.visitConditionalExpression(node, context);
} else if (ts.isPrefixUnaryExpression(node)) {
return this.visitPrefixUnaryExpression(node, context);
} else if (ts.isBinaryExpression(node)) {
return this.visitBinaryExpression(node, context);
} else if (ts.isArrayLiteralExpression(node)) {
return this.visitArrayLiteralExpression(node, context);
} else if (ts.isParenthesizedExpression(node)) {
return this.visitParenthesizedExpression(node, context);
} else if (ts.isElementAccessExpression(node)) {
return this.visitElementAccessExpression(node, context);
} else if (ts.isAsExpression(node)) {
return this.visitExpression(node.expression, context);
} else if (ts.isNonNullExpression(node)) {
return this.visitExpression(node.expression, context);
} else if (this.host.isClass(node)) {
return this.visitDeclaration(node, context);
} else {
return DYNAMIC_VALUE;
}
}
private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression, context: Context):
ResolvedValue {
const array: ResolvedValueArray = [];
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (ts.isSpreadElement(element)) {
const spread = this.visitExpression(element.expression, context);
if (isDynamicValue(spread)) {
return DYNAMIC_VALUE;
}
if (!Array.isArray(spread)) {
throw new Error(`Unexpected value in spread expression: ${spread}`);
}
array.push(...spread);
} else {
const result = this.visitExpression(element, context);
if (isDynamicValue(result)) {
return DYNAMIC_VALUE;
}
array.push(result);
}
}
return array;
}
private visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: Context):
ResolvedValue {
const map: ResolvedValueMap = new Map<string, ResolvedValue>();
for (let i = 0; i < node.properties.length; i++) {
const property = node.properties[i];
if (ts.isPropertyAssignment(property)) {
const name = this.stringNameFromPropertyName(property.name, context);
// Check whether the name can be determined statically.
if (name === undefined) {
return DYNAMIC_VALUE;
}
map.set(name, this.visitExpression(property.initializer, context));
} else if (ts.isShorthandPropertyAssignment(property)) {
const symbol = this.checker.getShorthandAssignmentValueSymbol(property);
if (symbol === undefined || symbol.valueDeclaration === undefined) {
return DYNAMIC_VALUE;
}
map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration, context));
} else if (ts.isSpreadAssignment(property)) {
const spread = this.visitExpression(property.expression, context);
if (isDynamicValue(spread)) {
return DYNAMIC_VALUE;
}
if (!(spread instanceof Map)) {
throw new Error(`Unexpected value in spread assignment: ${spread}`);
}
spread.forEach((value, key) => map.set(key, value));
} else {
return DYNAMIC_VALUE;
}
}
return map;
}
private visitTemplateExpression(node: ts.TemplateExpression, context: Context): ResolvedValue {
const pieces: string[] = [node.head.text];
for (let i = 0; i < node.templateSpans.length; i++) {
const span = node.templateSpans[i];
const value = this.visit(span.expression, context);
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ||
value == null) {
pieces.push(`${value}`);
} else {
return DYNAMIC_VALUE;
}
pieces.push(span.literal.text);
}
return pieces.join('');
}
private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue {
const decl = this.host.getDeclarationOfIdentifier(node);
if (decl === null) {
return DYNAMIC_VALUE;
}
const result = this.visitDeclaration(
decl.node, {...context, absoluteModuleName: decl.viaModule || context.absoluteModuleName});
if (result instanceof Reference) {
result.addIdentifier(node);
}
return result;
}
private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue {
if (this.host.isClass(node)) {
return this.getReference(node, context);
} else if (ts.isVariableDeclaration(node)) {
return this.visitVariableDeclaration(node, context);
} else if (ts.isParameter(node) && context.scope.has(node)) {
return context.scope.get(node) !;
} else if (ts.isExportAssignment(node)) {
return this.visitExpression(node.expression, context);
} else if (ts.isEnumDeclaration(node)) {
return this.visitEnumDeclaration(node, context);
} else if (ts.isSourceFile(node)) {
return this.visitSourceFile(node, context);
} else {
return this.getReference(node, context);
}
}
private visitVariableDeclaration(node: ts.VariableDeclaration, context: Context): ResolvedValue {
const value = this.host.getVariableValue(node);
if (value !== null) {
return this.visitExpression(value, context);
} else if (isVariableDeclarationDeclared(node)) {
return this.getReference(node, context);
} else {
return undefined;
}
}
private visitEnumDeclaration(node: ts.EnumDeclaration, context: Context): ResolvedValue {
const enumRef = this.getReference(node, context) as Reference<ts.EnumDeclaration>;
const map = new Map<string, EnumValue>();
node.members.forEach(member => {
const name = this.stringNameFromPropertyName(member.name, context);
if (name !== undefined) {
map.set(name, new EnumValue(enumRef, name));
}
});
return map;
}
private visitElementAccessExpression(node: ts.ElementAccessExpression, context: Context):
ResolvedValue {
const lhs = this.visitExpression(node.expression, context);
if (node.argumentExpression === undefined) {
throw new Error(`Expected argument in ElementAccessExpression`);
}
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
const rhs = this.visitExpression(node.argumentExpression, context);
if (isDynamicValue(rhs)) {
return DYNAMIC_VALUE;
}
if (typeof rhs !== 'string' && typeof rhs !== 'number') {
throw new Error(
`ElementAccessExpression index should be string or number, got ${typeof rhs}: ${rhs}`);
}
return this.accessHelper(lhs, rhs, context);
}
private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, context: Context):
ResolvedValue {
const lhs = this.visitExpression(node.expression, context);
const rhs = node.name.text;
// TODO: handle reference to class declaration.
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
return this.accessHelper(lhs, rhs, context);
}
private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue {
const declarations = this.host.getExportsOfModule(node);
if (declarations === null) {
return DYNAMIC_VALUE;
}
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;
}
private accessHelper(lhs: ResolvedValue, rhs: string|number, context: Context): ResolvedValue {
const strIndex = `${rhs}`;
if (lhs instanceof Map) {
if (lhs.has(strIndex)) {
return lhs.get(strIndex) !;
} else {
throw new Error(`Invalid map access: [${Array.from(lhs.keys())}] dot ${rhs}`);
}
} else if (Array.isArray(lhs)) {
if (rhs === 'length') {
return lhs.length;
} else if (rhs === 'slice') {
return new ArraySliceBuiltinFn(lhs);
}
if (typeof rhs !== 'number' || !Number.isInteger(rhs)) {
return DYNAMIC_VALUE;
}
if (rhs < 0 || rhs >= lhs.length) {
throw new Error(`Index out of bounds: ${rhs} vs ${lhs.length}`);
}
return lhs[rhs];
} else if (lhs instanceof Reference) {
const ref = lhs.node;
if (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 = this.host.getMembersOfClass(ref).find(
member => member.isStatic && member.name === strIndex);
if (member !== undefined) {
if (member.value !== null) {
value = this.visitExpression(member.value, context);
} else if (member.implementation !== null) {
value = new NodeReference(member.implementation, absoluteModuleName);
} else if (member.node) {
value = new NodeReference(member.node, absoluteModuleName);
}
}
return value;
}
}
throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`);
}
private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue {
const lhs = this.visitExpression(node.expression, context);
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
// If the call refers to a builtin function, attempt to evaluate the function.
if (lhs instanceof BuiltinFn) {
return lhs.evaluate(node.arguments.map(arg => this.visitExpression(arg, context)));
}
if (!(lhs instanceof Reference)) {
throw new Error(`attempting to call something that is not a function: ${lhs}`);
} else if (!isFunctionOrMethodReference(lhs)) {
throw new Error(
`calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]} (${node.getText()})`);
}
const fn = this.host.getDefinitionOfFunction(lhs.node);
// If the function is foreign (declared through a d.ts file), attempt to resolve it with the
// foreignFunctionResolver, if one is specified.
if (fn.body === null) {
let expr: ts.Expression|null = null;
if (context.foreignFunctionResolver) {
expr = context.foreignFunctionResolver(lhs, node.arguments);
}
if (expr === null) {
throw new Error(
`could not resolve foreign function declaration: ${node.getSourceFile().fileName} ${(lhs.node.name as ts.Identifier).text}`);
}
// If the function is declared in a different file, resolve the foreign function expression
// using the absolute module name of that file (if any).
let absoluteModuleName: string|null = context.absoluteModuleName;
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) {
absoluteModuleName = lhs.moduleName || absoluteModuleName;
}
return this.visitExpression(expr, {...context, absoluteModuleName});
}
const body = fn.body;
if (body.length !== 1 || !ts.isReturnStatement(body[0])) {
throw new Error('Function body must have a single return statement only.');
}
const ret = body[0] as ts.ReturnStatement;
const newScope: Scope = new Map<ts.ParameterDeclaration, ResolvedValue>();
fn.parameters.forEach((param, index) => {
let value: ResolvedValue = undefined;
if (index < node.arguments.length) {
const arg = node.arguments[index];
value = this.visitExpression(arg, context);
}
if (value === undefined && param.initializer !== null) {
value = this.visitExpression(param.initializer, context);
}
newScope.set(param.node, value);
});
return ret.expression !== undefined ?
this.visitExpression(ret.expression, {...context, scope: newScope}) :
undefined;
}
private visitConditionalExpression(node: ts.ConditionalExpression, context: Context):
ResolvedValue {
const condition = this.visitExpression(node.condition, context);
if (isDynamicValue(condition)) {
return condition;
}
if (condition) {
return this.visitExpression(node.whenTrue, context);
} else {
return this.visitExpression(node.whenFalse, context);
}
}
private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression, context: Context):
ResolvedValue {
const operatorKind = node.operator;
if (!UNARY_OPERATORS.has(operatorKind)) {
throw new Error(`Unsupported prefix unary operator: ${ts.SyntaxKind[operatorKind]}`);
}
const op = UNARY_OPERATORS.get(operatorKind) !;
const value = this.visitExpression(node.operand, context);
return isDynamicValue(value) ? DYNAMIC_VALUE : op(value);
}
private visitBinaryExpression(node: ts.BinaryExpression, context: Context): ResolvedValue {
const tokenKind = node.operatorToken.kind;
if (!BINARY_OPERATORS.has(tokenKind)) {
throw new Error(`Unsupported binary operator: ${ts.SyntaxKind[tokenKind]}`);
}
const opRecord = BINARY_OPERATORS.get(tokenKind) !;
let lhs: ResolvedValue, rhs: ResolvedValue;
if (opRecord.literal) {
lhs = literal(this.visitExpression(node.left, context));
rhs = literal(this.visitExpression(node.right, context));
} else {
lhs = this.visitExpression(node.left, context);
rhs = this.visitExpression(node.right, context);
}
return isDynamicValue(lhs) || isDynamicValue(rhs) ? DYNAMIC_VALUE : opRecord.op(lhs, rhs);
}
private visitParenthesizedExpression(node: ts.ParenthesizedExpression, context: Context):
ResolvedValue {
return this.visitExpression(node.expression, context);
}
private stringNameFromPropertyName(node: ts.PropertyName, context: Context): string|undefined {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else { // ts.ComputedPropertyName
const literal = this.visitExpression(node.expression, context);
return typeof literal === 'string' ? literal : undefined;
}
}
private getReference(node: ts.Declaration, context: Context): Reference {
const id = identifierOfDeclaration(node);
if (id === undefined) {
throw new Error(`Don't know how to refer to ${ts.SyntaxKind[node.kind]}`);
}
if (context.absoluteModuleName !== null) {
// TODO(alxhub): investigate whether this can get symbol names wrong in the event of
// re-exports under different names.
return new AbsoluteReference(node, id, context.absoluteModuleName, id.text);
} else {
return new ResolvedReference(node, id);
}
}
}
function isFunctionOrMethodReference(ref: Reference<ts.Node>):
ref is Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression> {
return ts.isFunctionDeclaration(ref.node) || ts.isMethodDeclaration(ref.node) ||
ts.isFunctionExpression(ref.node);
}
function literal(value: ResolvedValue): any {
if (value === null || value === undefined || typeof value === 'string' ||
typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (isDynamicValue(value)) {
return DYNAMIC_VALUE;
}
throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
}
function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined {
if (ts.isClassDeclaration(decl)) {
return decl.name;
} else if (ts.isEnumDeclaration(decl)) {
return decl.name;
} else if (ts.isFunctionDeclaration(decl)) {
return decl.name;
} else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) {
return decl.name;
} else if (ts.isShorthandPropertyAssignment(decl)) {
return decl.name;
} else {
return undefined;
}
}
function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean {
if (node.parent === undefined || !ts.isVariableDeclarationList(node.parent)) {
return false;
}
const declList = node.parent;
if (declList.parent === undefined || !ts.isVariableStatement(declList.parent)) {
return false;
}
const varStmt = declList.parent;
return varStmt.modifiers !== undefined &&
varStmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword);
}

View File

@ -0,0 +1,79 @@
/**
* @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 {Reference} from '../../imports';
/**
* A value resulting from static resolution.
*
* This could be a primitive, collection type, reference to a `ts.Node` that declares a
* non-primitive value, or a special `DynamicValue` type which indicates the value was not
* available statically.
*/
export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue |
ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue;
/**
* Represents a value which cannot be determined statically.
*
* Use `isDynamicValue` to determine whether a `ResolvedValue` is a `DynamicValue`.
*/
export class DynamicValue {
/**
* This is needed so the "is DynamicValue" assertion of `isDynamicValue` actually has meaning.
*
* Otherwise, "is DynamicValue" is akin to "is {}" which doesn't trigger narrowing.
*/
private _isDynamic = true;
}
/**
* An internal flyweight for `DynamicValue`. Eventually the dynamic value will carry information
* on the location of the node that could not be statically computed.
*/
export const DYNAMIC_VALUE: DynamicValue = new DynamicValue();
/**
* Used to test whether a `ResolvedValue` is a `DynamicValue`.
*/
export function isDynamicValue(value: any): value is DynamicValue {
return value === DYNAMIC_VALUE;
}
/**
* An array of `ResolvedValue`s.
*
* This is a reified type to allow the circular reference of `ResolvedValue` -> `ResolvedValueArray`
* ->
* `ResolvedValue`.
*/
export interface ResolvedValueArray extends Array<ResolvedValue> {}
/**
* A map of strings to `ResolvedValue`s.
*
* This is a reified type to allow the circular reference of `ResolvedValue` -> `ResolvedValueMap` ->
* `ResolvedValue`.
*/ export interface ResolvedValueMap extends Map<string, ResolvedValue> {}
/**
* A value member of an enumeration.
*
* Contains a `Reference` to the enumeration itself, and the name of the referenced member.
*/
export class EnumValue {
constructor(readonly enumRef: Reference<ts.EnumDeclaration>, readonly name: string) {}
}
/**
* An implementation of a builtin function, such as `Array.prototype.slice`.
*/
export abstract class BuiltinFn { abstract evaluate(args: ResolvedValueArray): ResolvedValue; }

View File

@ -0,0 +1,29 @@
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",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//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,313 @@
/**
* @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 {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteReference, Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {PartialEvaluator} from '../src/interface';
import {EnumValue, ResolvedValue} from '../src/result';
function makeSimpleProgram(contents: string): ts.Program {
return makeProgram([{name: 'entry.ts', contents}]).program;
}
function makeExpression(
code: string, expr: string): {expression: ts.Expression, checker: ts.TypeChecker} {
const {program} =
makeProgram([{name: 'entry.ts', contents: `${code}; const target$ = ${expr};`}]);
const checker = program.getTypeChecker();
const decl = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
return {
expression: decl.initializer !,
checker,
};
}
function evaluate<T extends ResolvedValue>(code: string, expr: string): T {
const {expression, checker} = makeExpression(code, expr);
const host = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(host, checker);
return evaluator.evaluate(expression) as T;
}
describe('ngtsc metadata', () => {
it('reads a file correctly', () => {
const {program} = makeProgram([
{
name: 'entry.ts',
contents: `
import {Y} from './other';
const A = Y;
export const X = A;
`
},
{
name: 'other.ts',
contents: `
export const Y = 'test';
`
}
]);
const decl = getDeclaration(program, 'entry.ts', 'X', ts.isVariableDeclaration);
const host = new TypeScriptReflectionHost(program.getTypeChecker());
const evaluator = new PartialEvaluator(host, program.getTypeChecker());
const value = evaluator.evaluate(decl.initializer !);
expect(value).toEqual('test');
});
it('map access works',
() => { expect(evaluate('const obj = {a: "test"};', 'obj.a')).toEqual('test'); });
it('function calls work', () => {
expect(evaluate(`function foo(bar) { return bar; }`, 'foo("test")')).toEqual('test');
});
it('conditionals work', () => {
expect(evaluate(`const x = false; const y = x ? 'true' : 'false';`, 'y')).toEqual('false');
});
it('addition works', () => { expect(evaluate(`const x = 1 + 2;`, 'x')).toEqual(3); });
it('static property on class works',
() => { expect(evaluate(`class Foo { static bar = 'test'; }`, 'Foo.bar')).toEqual('test'); });
it('static property call works', () => {
expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")'))
.toEqual('test');
});
it('indirected static property call works', () => {
expect(
evaluate(
`class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")'))
.toEqual('test');
});
it('array works', () => {
expect(evaluate(`const x = 'test'; const y = [1, x, 2];`, 'y')).toEqual([1, 'test', 2]);
});
it('array spread works', () => {
expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c'))
.toEqual([1, 2, 3, 4, 5]);
});
it('&& operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a && b')).toEqual('world');
expect(evaluate(`const a = false, b = 'world';`, 'a && b')).toEqual(false);
expect(evaluate(`const a = 'hello', b = 0;`, 'a && b')).toEqual(0);
});
it('|| operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a || b')).toEqual('hello');
expect(evaluate(`const a = false, b = 'world';`, 'a || b')).toEqual('world');
expect(evaluate(`const a = 'hello', b = 0;`, 'a || b')).toEqual('hello');
});
it('parentheticals work',
() => { expect(evaluate(`const a = 3, b = 4;`, 'a * (a + b)')).toEqual(21); });
it('array access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[1] + a[0]')).toEqual(3); });
it('array `length` property access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); });
it('array `slice` function works', () => {
expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]);
});
it('negation works', () => {
expect(evaluate(`const x = 3;`, '!x')).toEqual(false);
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
});
it('imports work', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export function foo(bar) { return bar; }'},
{
name: 'entry.ts',
contents: `
import {foo} from './second';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = new PartialEvaluator(host, checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof WrappedNodeExpr)) {
return fail('Expected expression reference to be a wrapped node');
}
if (!ts.isIdentifier(reference.node)) {
return fail('Expected expression to be an Identifier');
}
expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
});
it('absolute imports work', () => {
const {program} = makeProgram([
{name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'},
{
name: 'entry.ts',
contents: `
import {foo} from 'some_library';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = new PartialEvaluator(host, checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof AbsoluteReference)) {
return fail('Expected expression to resolve to an absolute reference');
}
expect(resolved.moduleName).toBe('some_library');
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof WrappedNodeExpr)) {
return fail('Expected expression reference to be a wrapped node');
}
if (!ts.isIdentifier(reference.node)) {
return fail('Expected expression to be an Identifier');
}
expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !);
});
it('reads values from default exports', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export default {property: "test"}'},
{
name: 'entry.ts',
contents: `
import mod from './second';
const target$ = mod.property;
`
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = new PartialEvaluator(host, checker);
expect(evaluator.evaluate(expr)).toEqual('test');
});
it('reads values from named exports', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export const a = {property: "test"};'},
{
name: 'entry.ts',
contents: `
import * as mod from './second';
const target$ = mod.a.property;
`
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = new PartialEvaluator(host, checker);
expect(evaluator.evaluate(expr)).toEqual('test');
});
it('chain of re-exports works', () => {
const {program} = makeProgram([
{name: 'const.ts', contents: 'export const value = {property: "test"};'},
{name: 'def.ts', contents: `import {value} from './const'; export default value;`},
{name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`},
{name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`},
{
name: 'entry.ts',
contents: `import * as mod from './direct-reexport'; const target$ = mod.value.property;`
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const evaluator = new PartialEvaluator(host, checker);
expect(evaluator.evaluate(expr)).toEqual('test');
});
it('map spread works', () => {
const map: Map<string, number> = evaluate<Map<string, number>>(
`const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c');
const obj: {[key: string]: number} = {};
map.forEach((value, key) => obj[key] = value);
expect(obj).toEqual({
a: 1,
b: 2,
c: 3,
});
});
it('indirected-via-object function call works', () => {
expect(evaluate(
`
function fn(res) { return res; }
const obj = {fn};
`,
'obj.fn("test")'))
.toEqual('test');
});
it('template expressions work',
() => { expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345'); });
it('enum resolution works', () => {
const result = evaluate(
`
enum Foo {
A,
B,
C,
}
const r = Foo.B;
`,
'r');
if (!(result instanceof EnumValue)) {
return fail(`result is not an EnumValue`);
}
expect(result.enumRef.node.name.text).toBe('Foo');
expect(result.name).toBe('B');
});
it('variable declaration resolution works', () => {
const {program} = makeProgram([
{name: 'decl.d.ts', contents: 'export declare let value: number;'},
{name: 'entry.ts', contents: `import {value} from './decl'; const target$ = value;`},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const evaluator = new PartialEvaluator(host, checker);
const res = evaluator.evaluate(result.initializer !);
expect(res instanceof Reference).toBe(true);
});
});