chore(tools): Remove use of TypeChecker from metadata collector.
The metadata collector was modified to look up references in the import list instead of resolving the symbol using the TypeChecker making the use of the TypeChecker vestigial. This change removes all uses of the TypeChecker. Modified the schema to be able to record global and local (non-module specific references). Added error messages to the schema and errors are recorded in the metadata file allowing the static reflector to throw errors if an unsupported construct is referenced by metadata. Closes #8966 Fixes #8893 Fixes #8894
This commit is contained in:
@ -1,81 +1,28 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Evaluator, ImportMetadata, ImportSpecifierMetadata} from './evaluator';
|
||||
import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata} from './schema';
|
||||
import {Evaluator, ImportMetadata, ImportSpecifierMetadata, isPrimitive} from './evaluator';
|
||||
import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, isMetadataError, isMetadataSymbolicReferenceExpression,} from './schema';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
|
||||
/**
|
||||
* Collect decorator metadata from a TypeScript module.
|
||||
*/
|
||||
export class MetadataCollector {
|
||||
constructor() {}
|
||||
|
||||
collectImports(sourceFile: ts.SourceFile) {
|
||||
let imports: ImportMetadata[] = [];
|
||||
const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, '');
|
||||
function visit(node: ts.Node) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ImportDeclaration:
|
||||
const importDecl = <ts.ImportDeclaration>node;
|
||||
const from = stripQuotes(importDecl.moduleSpecifier.getText());
|
||||
const newImport = {from};
|
||||
if (!importDecl.importClause) {
|
||||
// Bare imports do not bring symbols into scope, so we don't need to record them
|
||||
break;
|
||||
}
|
||||
if (importDecl.importClause.name) {
|
||||
(<any>newImport)['defaultName'] = importDecl.importClause.name.text;
|
||||
}
|
||||
const bindings = importDecl.importClause.namedBindings;
|
||||
if (bindings) {
|
||||
switch (bindings.kind) {
|
||||
case ts.SyntaxKind.NamedImports:
|
||||
const namedImports: ImportSpecifierMetadata[] = [];
|
||||
(<ts.NamedImports>bindings).elements.forEach(i => {
|
||||
const namedImport = {name: i.name.text};
|
||||
if (i.propertyName) {
|
||||
(<any>namedImport)['propertyName'] = i.propertyName.text;
|
||||
}
|
||||
namedImports.push(namedImport);
|
||||
});
|
||||
(<any>newImport)['namedImports'] = namedImports;
|
||||
break;
|
||||
case ts.SyntaxKind.NamespaceImport:
|
||||
(<any>newImport)['namespace'] = (<ts.NamespaceImport>bindings).name.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
imports.push(newImport);
|
||||
break;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
ts.forEachChild(sourceFile, visit);
|
||||
return imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON.stringify friendly form describing the decorators of the exported classes from
|
||||
* the source file that is expected to correspond to a module.
|
||||
*/
|
||||
public getMetadata(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): ModuleMetadata {
|
||||
const locals = new Symbols();
|
||||
const evaluator = new Evaluator(typeChecker, locals, this.collectImports(sourceFile));
|
||||
public getMetadata(sourceFile: ts.SourceFile): ModuleMetadata {
|
||||
const locals = new Symbols(sourceFile);
|
||||
const evaluator = new Evaluator(locals);
|
||||
let metadata: {[name: string]: MetadataValue | ClassMetadata}|undefined;
|
||||
|
||||
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
|
||||
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression);
|
||||
}
|
||||
|
||||
function referenceFromType(type: ts.Type): MetadataSymbolicReferenceExpression {
|
||||
if (type) {
|
||||
let symbol = type.getSymbol();
|
||||
if (symbol) {
|
||||
return evaluator.symbolReference(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
|
||||
let result: ClassMetadata = {__symbolic: 'class'};
|
||||
|
||||
@ -85,6 +32,15 @@ export class MetadataCollector {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function referenceFrom(node: ts.Node): MetadataSymbolicReferenceExpression|MetadataError {
|
||||
const result = evaluator.evaluateNode(node);
|
||||
if (isMetadataError(result) || isMetadataSymbolicReferenceExpression(result)) {
|
||||
return result;
|
||||
} else {
|
||||
return {__symbolic: 'error', message: 'Symbol reference expected'};
|
||||
}
|
||||
}
|
||||
|
||||
// Add class decorators
|
||||
if (classDeclaration.decorators) {
|
||||
result.decorators = getDecorators(classDeclaration.decorators);
|
||||
@ -108,8 +64,9 @@ export class MetadataCollector {
|
||||
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
|
||||
const methodDecorators = getDecorators(method.decorators);
|
||||
const parameters = method.parameters;
|
||||
const parameterDecoratorData: MetadataSymbolicExpression[][] = [];
|
||||
const parametersData: MetadataSymbolicReferenceExpression[] = [];
|
||||
const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
|
||||
const parametersData: (MetadataSymbolicReferenceExpression | MetadataError | null)[] =
|
||||
[];
|
||||
let hasDecoratorData: boolean = false;
|
||||
let hasParameterData: boolean = false;
|
||||
for (const parameter of parameters) {
|
||||
@ -117,8 +74,11 @@ export class MetadataCollector {
|
||||
parameterDecoratorData.push(parameterData);
|
||||
hasDecoratorData = hasDecoratorData || !!parameterData;
|
||||
if (isConstructor) {
|
||||
const parameterType = typeChecker.getTypeAtLocation(parameter);
|
||||
parametersData.push(referenceFromType(parameterType) || null);
|
||||
if (parameter.type) {
|
||||
parametersData.push(referenceFrom(parameter.type));
|
||||
} else {
|
||||
parametersData.push(null);
|
||||
}
|
||||
hasParameterData = true;
|
||||
}
|
||||
}
|
||||
@ -133,7 +93,9 @@ export class MetadataCollector {
|
||||
if (hasParameterData) {
|
||||
(<ConstructorMetadata>data).parameters = parametersData;
|
||||
}
|
||||
recordMember(name, data);
|
||||
if (!isMetadataError(name)) {
|
||||
recordMember(name, data);
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.PropertyDeclaration:
|
||||
case ts.SyntaxKind.GetAccessor:
|
||||
@ -141,9 +103,10 @@ export class MetadataCollector {
|
||||
const property = <ts.PropertyDeclaration>member;
|
||||
const propertyDecorators = getDecorators(property.decorators);
|
||||
if (propertyDecorators) {
|
||||
recordMember(
|
||||
evaluator.nameOf(property.name),
|
||||
{__symbolic: 'property', decorators: propertyDecorators});
|
||||
let name = evaluator.nameOf(property.name);
|
||||
if (!isMetadataError(name)) {
|
||||
recordMember(name, {__symbolic: 'property', decorators: propertyDecorators});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -155,36 +118,94 @@ export class MetadataCollector {
|
||||
return result.decorators || members ? result : undefined;
|
||||
}
|
||||
|
||||
let metadata: {[name: string]: (ClassMetadata | MetadataValue)};
|
||||
const symbols = typeChecker.getSymbolsInScope(sourceFile, ts.SymbolFlags.ExportValue);
|
||||
for (var symbol of symbols) {
|
||||
for (var declaration of symbol.getDeclarations()) {
|
||||
switch (declaration.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
const classDeclaration = <ts.ClassDeclaration>declaration;
|
||||
// Predeclare classes
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
const classDeclaration = <ts.ClassDeclaration>node;
|
||||
const className = classDeclaration.name.text;
|
||||
if (node.flags & ts.NodeFlags.Export) {
|
||||
locals.define(className, {__symbolic: 'reference', name: className});
|
||||
} else {
|
||||
locals.define(
|
||||
className,
|
||||
{__symbolic: 'error', message: `Reference to non-exported class ${className}`});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
const classDeclaration = <ts.ClassDeclaration>node;
|
||||
const className = classDeclaration.name.text;
|
||||
if (node.flags & ts.NodeFlags.Export) {
|
||||
if (classDeclaration.decorators) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[classDeclaration.name.text] = classMetadataOf(classDeclaration);
|
||||
metadata[className] = classMetadataOf(classDeclaration);
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.VariableDeclaration:
|
||||
const variableDeclaration = <ts.VariableDeclaration>declaration;
|
||||
if (variableDeclaration.initializer) {
|
||||
const value = evaluator.evaluateNode(variableDeclaration.initializer);
|
||||
if (value !== undefined) {
|
||||
if (evaluator.isFoldable(variableDeclaration.initializer)) {
|
||||
// Record the value for use in other initializers
|
||||
locals.set(symbol, value);
|
||||
}
|
||||
if (!metadata) metadata = {};
|
||||
metadata[evaluator.nameOf(variableDeclaration.name)] =
|
||||
evaluator.evaluateNode(variableDeclaration.initializer);
|
||||
}
|
||||
// Otherwise don't record metadata for the class.
|
||||
break;
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
const variableStatement = <ts.VariableStatement>node;
|
||||
for (let variableDeclaration of variableStatement.declarationList.declarations) {
|
||||
if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) {
|
||||
let nameNode = <ts.Identifier>variableDeclaration.name;
|
||||
let varValue: MetadataValue;
|
||||
if (variableDeclaration.initializer) {
|
||||
varValue = evaluator.evaluateNode(variableDeclaration.initializer);
|
||||
} else {
|
||||
varValue = {
|
||||
__symbolic: 'error',
|
||||
message: 'Only intialized variables and constants can be referenced statically'
|
||||
};
|
||||
}
|
||||
if (variableStatement.flags & ts.NodeFlags.Export ||
|
||||
variableDeclaration.flags & ts.NodeFlags.Export) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[nameNode.text] = varValue;
|
||||
}
|
||||
if (isPrimitive(varValue)) {
|
||||
locals.define(nameNode.text, varValue);
|
||||
}
|
||||
} else {
|
||||
// Destructuring (or binding) declarations are not supported,
|
||||
// var {<identifier>[, <identifer>]+} = <expression>;
|
||||
// or
|
||||
// var [<identifier>[, <identifier}+] = <expression>;
|
||||
// are not supported.
|
||||
let varValue = {
|
||||
__symbolc: 'error',
|
||||
message: 'Destructuring declarations cannot be referenced statically'
|
||||
};
|
||||
const report = (nameNode: ts.Node) => {
|
||||
switch (nameNode.kind) {
|
||||
case ts.SyntaxKind.Identifier:
|
||||
const name = <ts.Identifier>nameNode;
|
||||
locals.define(name.text, varValue);
|
||||
if (node.flags & ts.NodeFlags.Export) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[name.text] = varValue;
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.BindingElement:
|
||||
const bindingElement = <ts.BindingElement>nameNode;
|
||||
report(bindingElement.name);
|
||||
break;
|
||||
case ts.SyntaxKind.ObjectBindingPattern:
|
||||
case ts.SyntaxKind.ArrayBindingPattern:
|
||||
const bindings = <ts.BindingPattern>nameNode;
|
||||
bindings.elements.forEach(report);
|
||||
break;
|
||||
}
|
||||
};
|
||||
report(variableDeclaration.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return metadata && {__symbolic: 'module', metadata};
|
||||
}
|
||||
|
@ -67,8 +67,7 @@ export class MetadataWriterHost extends DelegatingHost {
|
||||
// released
|
||||
if (/*DTS*/ /\.js$/.test(emitFilePath)) {
|
||||
const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json');
|
||||
const metadata =
|
||||
this.metadataCollector.getMetadata(sourceFile, this.program.getTypeChecker());
|
||||
const metadata = this.metadataCollector.getMetadata(sourceFile);
|
||||
if (metadata && metadata.metadata) {
|
||||
const metadataText = JSON.stringify(metadata);
|
||||
writeFileSync(path, metadataText, {encoding: 'utf-8'});
|
||||
|
@ -1,21 +1,8 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {MetadataValue, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression} from './schema';
|
||||
import {MetadataValue, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression, MetadataError, isMetadataError, isMetadataModuleReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataGlobalReferenceExpression,} from './schema';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
|
||||
// TOOD: Remove when tools directory is upgraded to support es6 target
|
||||
interface Map<K, V> {
|
||||
has(k: K): boolean;
|
||||
set(k: K, v: V): void;
|
||||
get(k: K): V;
|
||||
delete (k: K): void;
|
||||
}
|
||||
interface MapConstructor {
|
||||
new<K, V>(): Map<K, V>;
|
||||
}
|
||||
declare var Map: MapConstructor;
|
||||
|
||||
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
|
||||
const expression = callExpression.expression;
|
||||
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
||||
@ -46,7 +33,7 @@ function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
|
||||
return !ts.forEachChild(node, node => !cb(node));
|
||||
}
|
||||
|
||||
function isPrimitive(value: any): boolean {
|
||||
export function isPrimitive(value: any): boolean {
|
||||
return Object(value) !== value;
|
||||
}
|
||||
|
||||
@ -72,63 +59,21 @@ export interface ImportMetadata {
|
||||
* possible.
|
||||
*/
|
||||
export class Evaluator {
|
||||
constructor(
|
||||
private typeChecker: ts.TypeChecker, private symbols: Symbols,
|
||||
private imports: ImportMetadata[]) {}
|
||||
constructor(private symbols: Symbols) {}
|
||||
|
||||
symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression {
|
||||
if (symbol) {
|
||||
let module: string;
|
||||
let name = symbol.name;
|
||||
for (const eachImport of this.imports) {
|
||||
if (symbol.name === eachImport.defaultName) {
|
||||
module = eachImport.from;
|
||||
name = undefined;
|
||||
}
|
||||
if (eachImport.namedImports) {
|
||||
for (const named of eachImport.namedImports) {
|
||||
if (symbol.name === named.name) {
|
||||
name = named.propertyName ? named.propertyName : named.name;
|
||||
module = eachImport.from;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {__symbolic: 'reference', name, module};
|
||||
}
|
||||
}
|
||||
|
||||
private findImportNamespace(node: ts.Node) {
|
||||
if (node.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
||||
const lhs = (<ts.PropertyAccessExpression>node).expression;
|
||||
if (lhs.kind === ts.SyntaxKind.Identifier) {
|
||||
// TOOD: Use Array.find when tools directory is upgraded to support es6 target
|
||||
for (const eachImport of this.imports) {
|
||||
if (eachImport.namespace === (<ts.Identifier>lhs).text) {
|
||||
return eachImport;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression {
|
||||
const importNamespace = this.findImportNamespace(node);
|
||||
if (importNamespace) {
|
||||
const result = this.symbolReference(
|
||||
this.typeChecker.getSymbolAtLocation((<ts.PropertyAccessExpression>node).name));
|
||||
result.module = importNamespace.from;
|
||||
return result;
|
||||
}
|
||||
return this.symbolReference(this.typeChecker.getSymbolAtLocation(node));
|
||||
}
|
||||
|
||||
nameOf(node: ts.Node): string {
|
||||
nameOf(node: ts.Node): string|MetadataError {
|
||||
if (node.kind == ts.SyntaxKind.Identifier) {
|
||||
return (<ts.Identifier>node).text;
|
||||
}
|
||||
return <string>this.evaluateNode(node);
|
||||
const result = this.evaluateNode(node);
|
||||
if (isMetadataError(result) || typeof result === 'string') {
|
||||
return result;
|
||||
} else {
|
||||
return {
|
||||
__symbolic: 'error',
|
||||
message: `Name expected a string or an identifier but received "${node.getText()}""`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,25 +159,10 @@ export class Evaluator {
|
||||
return this.isFoldableWorker(elementAccessExpression.expression, folding) &&
|
||||
this.isFoldableWorker(elementAccessExpression.argumentExpression, folding);
|
||||
case ts.SyntaxKind.Identifier:
|
||||
let symbol = this.typeChecker.getSymbolAtLocation(node);
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = this.typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
if (this.symbols.has(symbol)) return true;
|
||||
|
||||
// If this is a reference to a foldable variable then it is foldable too.
|
||||
const variableDeclaration = <ts.VariableDeclaration>(
|
||||
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
|
||||
if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) {
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (folding.has(initializer)) {
|
||||
// A recursive reference is not foldable.
|
||||
return false;
|
||||
}
|
||||
folding.set(initializer, true);
|
||||
const result = this.isFoldableWorker(initializer, folding);
|
||||
folding.delete(initializer);
|
||||
return result;
|
||||
let identifier = <ts.Identifier>node;
|
||||
let reference = this.symbols.resolve(identifier.text);
|
||||
if (isPrimitive(reference)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -245,46 +175,43 @@ export class Evaluator {
|
||||
* tree are folded. For example, a node representing `1 + 2` is folded into `3`.
|
||||
*/
|
||||
public evaluateNode(node: ts.Node): MetadataValue {
|
||||
let error: MetadataError|undefined;
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||
let obj: MetadataValue = {};
|
||||
let allPropertiesDefined = true;
|
||||
let obj: {[name: string]: any} = {};
|
||||
ts.forEachChild(node, child => {
|
||||
switch (child.kind) {
|
||||
case ts.SyntaxKind.PropertyAssignment:
|
||||
const assignment = <ts.PropertyAssignment>child;
|
||||
const propertyName = this.nameOf(assignment.name);
|
||||
if (isMetadataError(propertyName)) {
|
||||
return propertyName;
|
||||
}
|
||||
const propertyValue = this.evaluateNode(assignment.initializer);
|
||||
(<any>obj)[propertyName] = propertyValue;
|
||||
allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined;
|
||||
if (isMetadataError(propertyValue)) {
|
||||
error = propertyValue;
|
||||
return true; // Stop the forEachChild.
|
||||
} else {
|
||||
obj[<string>propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (allPropertiesDefined) return obj;
|
||||
break;
|
||||
if (error) return error;
|
||||
return obj;
|
||||
case ts.SyntaxKind.ArrayLiteralExpression:
|
||||
let arr: MetadataValue[] = [];
|
||||
let allElementsDefined = true;
|
||||
ts.forEachChild(node, child => {
|
||||
const value = this.evaluateNode(child);
|
||||
if (isMetadataError(value)) {
|
||||
error = value;
|
||||
return true; // Stop the forEachChild.
|
||||
}
|
||||
arr.push(value);
|
||||
allElementsDefined = isDefined(value) && allElementsDefined;
|
||||
});
|
||||
if (allElementsDefined) return arr;
|
||||
break;
|
||||
if (error) return error;
|
||||
return arr;
|
||||
case ts.SyntaxKind.CallExpression:
|
||||
const callExpression = <ts.CallExpression>node;
|
||||
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
|
||||
if (this.isFoldable(callExpression)) {
|
||||
if (isMethodCallOf(callExpression, 'concat')) {
|
||||
const arrayValue = <MetadataValue[]>this.evaluateNode(
|
||||
(<ts.PropertyAccessExpression>callExpression.expression).expression);
|
||||
return arrayValue.concat(args[0]);
|
||||
}
|
||||
}
|
||||
// Always fold a CONST_EXPR even if the argument is not foldable.
|
||||
if (isCallOf(callExpression, 'CONST_EXPR') && callExpression.arguments.length === 1) {
|
||||
return args[0];
|
||||
}
|
||||
if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) {
|
||||
const firstArgument = callExpression.arguments[0];
|
||||
if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) {
|
||||
@ -292,72 +219,136 @@ export class Evaluator {
|
||||
return this.evaluateNode(arrowFunction.body);
|
||||
}
|
||||
}
|
||||
const expression = this.evaluateNode(callExpression.expression);
|
||||
if (isDefined(expression) && args.every(isDefined)) {
|
||||
const result:
|
||||
MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
|
||||
if (args && args.length) {
|
||||
result.arguments = args;
|
||||
}
|
||||
return result;
|
||||
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
|
||||
if (args.some(isMetadataError)) {
|
||||
return args.find(isMetadataError);
|
||||
}
|
||||
break;
|
||||
if (this.isFoldable(callExpression)) {
|
||||
if (isMethodCallOf(callExpression, 'concat')) {
|
||||
const arrayValue = <MetadataValue[]>this.evaluateNode(
|
||||
(<ts.PropertyAccessExpression>callExpression.expression).expression);
|
||||
if (isMetadataError(arrayValue)) return arrayValue;
|
||||
return arrayValue.concat(args[0]);
|
||||
}
|
||||
}
|
||||
// Always fold a CONST_EXPR even if the argument is not foldable.
|
||||
if (isCallOf(callExpression, 'CONST_EXPR') && callExpression.arguments.length === 1) {
|
||||
return args[0];
|
||||
}
|
||||
const expression = this.evaluateNode(callExpression.expression);
|
||||
if (isMetadataError(expression)) {
|
||||
return expression;
|
||||
}
|
||||
let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
|
||||
if (args && args.length) {
|
||||
result.arguments = args;
|
||||
}
|
||||
return result;
|
||||
case ts.SyntaxKind.NewExpression:
|
||||
const newExpression = <ts.NewExpression>node;
|
||||
const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg));
|
||||
const newTarget = this.evaluateNode(newExpression.expression);
|
||||
if (isDefined(newTarget) && newArgs.every(isDefined)) {
|
||||
const result: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget};
|
||||
if (newArgs.length) {
|
||||
result.arguments = newArgs;
|
||||
}
|
||||
return result;
|
||||
if (newArgs.some(isMetadataError)) {
|
||||
return newArgs.find(isMetadataError);
|
||||
}
|
||||
break;
|
||||
const newTarget = this.evaluateNode(newExpression.expression);
|
||||
if (isMetadataError(newTarget)) {
|
||||
return newTarget;
|
||||
}
|
||||
const call: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget};
|
||||
if (newArgs.length) {
|
||||
call.arguments = newArgs;
|
||||
}
|
||||
return call;
|
||||
case ts.SyntaxKind.PropertyAccessExpression: {
|
||||
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
||||
const expression = this.evaluateNode(propertyAccessExpression.expression);
|
||||
if (isMetadataError(expression)) {
|
||||
return expression;
|
||||
}
|
||||
const member = this.nameOf(propertyAccessExpression.name);
|
||||
if (this.isFoldable(propertyAccessExpression.expression)) return (<any>expression)[member];
|
||||
if (this.findImportNamespace(propertyAccessExpression)) {
|
||||
return this.nodeSymbolReference(propertyAccessExpression);
|
||||
if (isMetadataError(member)) {
|
||||
return member;
|
||||
}
|
||||
if (isDefined(expression)) {
|
||||
return {__symbolic: 'select', expression, member};
|
||||
if (this.isFoldable(propertyAccessExpression.expression))
|
||||
return (<any>expression)[<string>member];
|
||||
if (isMetadataModuleReferenceExpression(expression)) {
|
||||
// A select into a module refrence and be converted into a reference to the symbol
|
||||
// in the module
|
||||
return {__symbolic: 'reference', module: expression.module, name: member};
|
||||
}
|
||||
break;
|
||||
return {__symbolic: 'select', expression, member};
|
||||
}
|
||||
case ts.SyntaxKind.ElementAccessExpression: {
|
||||
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
||||
const expression = this.evaluateNode(elementAccessExpression.expression);
|
||||
if (isMetadataError(expression)) {
|
||||
return expression;
|
||||
}
|
||||
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
|
||||
if (isMetadataError(expression)) {
|
||||
return expression;
|
||||
}
|
||||
if (this.isFoldable(elementAccessExpression.expression) &&
|
||||
this.isFoldable(elementAccessExpression.argumentExpression))
|
||||
return (<any>expression)[<string|number>index];
|
||||
if (isDefined(expression) && isDefined(index)) {
|
||||
return {__symbolic: 'index', expression, index};
|
||||
}
|
||||
break;
|
||||
return {__symbolic: 'index', expression, index};
|
||||
}
|
||||
case ts.SyntaxKind.Identifier:
|
||||
let symbol = this.typeChecker.getSymbolAtLocation(node);
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = this.typeChecker.getAliasedSymbol(symbol);
|
||||
const identifier = <ts.Identifier>node;
|
||||
const name = identifier.text;
|
||||
const reference = this.symbols.resolve(name);
|
||||
if (reference === undefined) {
|
||||
// Encode as a global reference. StaticReflector will check the reference.
|
||||
return { __symbolic: 'reference', name }
|
||||
}
|
||||
if (this.symbols.has(symbol)) return this.symbols.get(symbol);
|
||||
if (this.isFoldable(node)) {
|
||||
// isFoldable implies, in this context, symbol declaration is a VariableDeclaration
|
||||
const variableDeclaration = <ts.VariableDeclaration>(
|
||||
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
|
||||
return this.evaluateNode(variableDeclaration.initializer);
|
||||
return reference;
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
const typeReferenceNode = <ts.TypeReferenceNode>node;
|
||||
const typeNameNode = typeReferenceNode.typeName;
|
||||
if (typeNameNode.kind != ts.SyntaxKind.Identifier) {
|
||||
return { __symbolic: 'error', message: 'Qualified type names not supported' }
|
||||
}
|
||||
return this.nodeSymbolReference(node);
|
||||
const typeNameIdentifier = <ts.Identifier>typeReferenceNode.typeName;
|
||||
const typeName = typeNameIdentifier.text;
|
||||
const typeReference = this.symbols.resolve(typeName);
|
||||
if (!typeReference) {
|
||||
return {__symbolic: 'error', message: `Could not resolve type ${typeName}`};
|
||||
}
|
||||
if (typeReferenceNode.typeArguments && typeReferenceNode.typeArguments.length) {
|
||||
const args = typeReferenceNode.typeArguments.map(element => this.evaluateNode(element));
|
||||
if (isMetadataImportedSymbolReferenceExpression(typeReference)) {
|
||||
return {
|
||||
__symbolic: 'reference',
|
||||
module: typeReference.module,
|
||||
name: typeReference.name,
|
||||
arguments: args
|
||||
};
|
||||
} else if (isMetadataGlobalReferenceExpression(typeReference)) {
|
||||
return {__symbolic: 'reference', name: typeReference.name, arguments: args};
|
||||
}
|
||||
}
|
||||
return typeReference;
|
||||
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
||||
return (<ts.LiteralExpression>node).text;
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
return (<ts.StringLiteral>node).text;
|
||||
case ts.SyntaxKind.NumericLiteral:
|
||||
return parseFloat((<ts.LiteralExpression>node).text);
|
||||
case ts.SyntaxKind.AnyKeyword:
|
||||
return {__symbolic: 'reference', name: 'any'};
|
||||
case ts.SyntaxKind.StringKeyword:
|
||||
return {__symbolic: 'reference', name: 'string'};
|
||||
case ts.SyntaxKind.NumberKeyword:
|
||||
return {__symbolic: 'reference', name: 'number'};
|
||||
case ts.SyntaxKind.BooleanKeyword:
|
||||
return {__symbolic: 'reference', name: 'boolean'};
|
||||
case ts.SyntaxKind.ArrayType:
|
||||
const arrayTypeNode = <ts.ArrayTypeNode>node;
|
||||
return {
|
||||
__symbolic: 'reference',
|
||||
name: 'Array',
|
||||
arguments: [this.evaluateNode(arrayTypeNode.elementType)]
|
||||
};
|
||||
case ts.SyntaxKind.NullKeyword:
|
||||
return null;
|
||||
case ts.SyntaxKind.TrueKeyword:
|
||||
@ -462,6 +453,6 @@ export class Evaluator {
|
||||
}
|
||||
break;
|
||||
}
|
||||
return undefined;
|
||||
return {__symbolic: 'error', message: 'Expression is too complex to resolve statically'};
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export function isModuleMetadata(value: any): value is ModuleMetadata {
|
||||
|
||||
export interface ClassMetadata {
|
||||
__symbolic: 'class';
|
||||
decorators?: MetadataSymbolicExpression[];
|
||||
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
||||
members?: MetadataMap;
|
||||
}
|
||||
export function isClassMetadata(value: any): value is ClassMetadata {
|
||||
@ -19,7 +19,7 @@ export interface MetadataMap { [name: string]: MemberMetadata[]; }
|
||||
|
||||
export interface MemberMetadata {
|
||||
__symbolic: 'constructor'|'method'|'property';
|
||||
decorators?: MetadataSymbolicExpression[];
|
||||
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
||||
}
|
||||
export function isMemberMetadata(value: any): value is MemberMetadata {
|
||||
if (value) {
|
||||
@ -35,7 +35,7 @@ export function isMemberMetadata(value: any): value is MemberMetadata {
|
||||
|
||||
export interface MethodMetadata extends MemberMetadata {
|
||||
__symbolic: 'constructor'|'method';
|
||||
parameterDecorators?: MetadataSymbolicExpression[][];
|
||||
parameterDecorators?: (MetadataSymbolicExpression|MetadataError)[][];
|
||||
}
|
||||
export function isMethodMetadata(value: any): value is MemberMetadata {
|
||||
return value && (value.__symbolic === 'constructor' || value.__symbolic === 'method');
|
||||
@ -43,14 +43,14 @@ export function isMethodMetadata(value: any): value is MemberMetadata {
|
||||
|
||||
export interface ConstructorMetadata extends MethodMetadata {
|
||||
__symbolic: 'constructor';
|
||||
parameters?: MetadataSymbolicExpression[];
|
||||
parameters?: (MetadataSymbolicExpression|MetadataError|null)[];
|
||||
}
|
||||
export function isConstructorMetadata(value: any): value is ConstructorMetadata {
|
||||
return value && value.__symbolic === 'constructor';
|
||||
}
|
||||
|
||||
export type MetadataValue =
|
||||
string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression;
|
||||
export type MetadataValue = string | number | boolean | MetadataObject | MetadataArray |
|
||||
MetadataSymbolicExpression | MetadataError;
|
||||
|
||||
export interface MetadataObject { [name: string]: MetadataValue; }
|
||||
|
||||
@ -117,11 +117,51 @@ export function isMetadataSymbolicPrefixExpression(value: any):
|
||||
return value && value.__symbolic === 'pre';
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicReferenceExpression extends MetadataSymbolicExpression {
|
||||
export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
name: string;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataGlobalReferenceExpression(value: any):
|
||||
value is MetadataGlobalReferenceExpression {
|
||||
return isMetadataSymbolicReferenceExpression(value) && value.name && !value.module;
|
||||
}
|
||||
|
||||
export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
}
|
||||
export function isMetadataModuleReferenceExpression(value: any):
|
||||
value is MetadataModuleReferenceExpression {
|
||||
return isMetadataSymbolicReferenceExpression(value) && value.module && !value.name &&
|
||||
!value.default;
|
||||
}
|
||||
|
||||
export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
name: string;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataImportedSymbolReferenceExpression(value: any):
|
||||
value is MetadataImportedSymbolReferenceExpression {
|
||||
return isMetadataSymbolicReferenceExpression(value) && value.module && !!value.name;
|
||||
}
|
||||
|
||||
export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
default:
|
||||
boolean;
|
||||
}
|
||||
export function isMetadataImportDefaultReference(value: any):
|
||||
value is MetadataImportedDefaultReferenceExpression {
|
||||
return isMetadataSymbolicReferenceExpression(value) && value.module && value.default;
|
||||
}
|
||||
|
||||
export type MetadataSymbolicReferenceExpression = MetadataGlobalReferenceExpression |
|
||||
MetadataModuleReferenceExpression | MetadataImportedSymbolReferenceExpression |
|
||||
MetadataImportedDefaultReferenceExpression;
|
||||
export function isMetadataSymbolicReferenceExpression(value: any):
|
||||
value is MetadataSymbolicReferenceExpression {
|
||||
return value && value.__symbolic === 'reference';
|
||||
@ -136,3 +176,11 @@ export function isMetadataSymbolicSelectExpression(value: any):
|
||||
value is MetadataSymbolicSelectExpression {
|
||||
return value && value.__symbolic === 'select';
|
||||
}
|
||||
|
||||
export interface MetadataError {
|
||||
__symbolic: 'error';
|
||||
message: string;
|
||||
}
|
||||
export function isMetadataError(value: any): value is MetadataError {
|
||||
return value && value.__symbolic === 'error';
|
||||
}
|
@ -1,36 +1,99 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
// TOOD: Remove when tools directory is upgraded to support es6 target
|
||||
interface Map<K, V> {
|
||||
has(v: V): boolean;
|
||||
set(k: K, v: V): void;
|
||||
get(k: K): V;
|
||||
}
|
||||
interface MapConstructor {
|
||||
new<K, V>(): Map<K, V>;
|
||||
}
|
||||
declare var Map: MapConstructor;
|
||||
import {MetadataValue} from './schema';
|
||||
|
||||
var a: Array<number>;
|
||||
|
||||
/**
|
||||
* A symbol table of ts.Symbol to a folded value used during expression folding in Evaluator.
|
||||
*
|
||||
* This is a thin wrapper around a Map<> using the first declaration location instead of the symbol
|
||||
* itself as the key. In the TypeScript binder and type checker, mulitple symbols are sometimes
|
||||
* created for a symbol depending on what scope it is in (e.g. export vs. local). Using the
|
||||
* declaration node as the key results in these duplicate symbols being treated as identical.
|
||||
*/
|
||||
export class Symbols {
|
||||
private map = new Map<ts.Node, any>();
|
||||
private _symbols: Map<string, MetadataValue>;
|
||||
|
||||
public has(symbol: ts.Symbol): boolean { return this.map.has(symbol.getDeclarations()[0]); }
|
||||
constructor(private sourceFile: ts.SourceFile) {}
|
||||
|
||||
public set(symbol: ts.Symbol, value: any): void {
|
||||
this.map.set(symbol.getDeclarations()[0], value);
|
||||
resolve(name: string): MetadataValue|undefined { return this.symbols.get(name); }
|
||||
|
||||
define(name: string, value: MetadataValue) { this.symbols.set(name, value); }
|
||||
|
||||
has(name: string): boolean { return this.symbols.has(name); }
|
||||
|
||||
private get symbols(): Map<string, MetadataValue> {
|
||||
let result = this._symbols;
|
||||
if (!result) {
|
||||
result = this._symbols = new Map();
|
||||
populateBuiltins(result);
|
||||
this.buildImports();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public get(symbol: ts.Symbol): any { return this.map.get(symbol.getDeclarations()[0]); }
|
||||
|
||||
static empty: Symbols = new Symbols();
|
||||
private buildImports(): void {
|
||||
let symbols = this._symbols;
|
||||
// Collect the imported symbols into this.symbols
|
||||
const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, '');
|
||||
const visit = (node: ts.Node) => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ImportEqualsDeclaration:
|
||||
const importEqualsDeclaration = <ts.ImportEqualsDeclaration>node;
|
||||
if (importEqualsDeclaration.moduleReference.kind ===
|
||||
ts.SyntaxKind.ExternalModuleReference) {
|
||||
const externalReference =
|
||||
<ts.ExternalModuleReference>importEqualsDeclaration.moduleReference;
|
||||
// An `import <identifier> = require(<module-specifier>);
|
||||
const from = stripQuotes(externalReference.expression.getText());
|
||||
symbols.set(importEqualsDeclaration.name.text, {__symbolic: 'reference', module: from});
|
||||
} else {
|
||||
symbols.set(
|
||||
importEqualsDeclaration.name.text,
|
||||
{__symbolic: 'error', message: `Unsupported import syntax`});
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.ImportDeclaration:
|
||||
const importDecl = <ts.ImportDeclaration>node;
|
||||
if (!importDecl.importClause) {
|
||||
// An `import <module-specifier>` clause which does not bring symbols into scope.
|
||||
break;
|
||||
}
|
||||
const from = stripQuotes(importDecl.moduleSpecifier.getText());
|
||||
if (importDecl.importClause.name) {
|
||||
// An `import <identifier> form <module-specifier>` clause. Record the defualt symbol.
|
||||
symbols.set(
|
||||
importDecl.importClause.name.text,
|
||||
{__symbolic: 'reference', module: from, default: true});
|
||||
}
|
||||
const bindings = importDecl.importClause.namedBindings;
|
||||
if (bindings) {
|
||||
switch (bindings.kind) {
|
||||
case ts.SyntaxKind.NamedImports:
|
||||
// An `import { [<identifier> [, <identifier>] } from <module-specifier>` clause
|
||||
for (let binding of (<ts.NamedImports>bindings).elements) {
|
||||
symbols.set(binding.name.text, {
|
||||
__symbolic: 'reference',
|
||||
module: from,
|
||||
name: binding.propertyName ? binding.propertyName.text : binding.name.text
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.NamespaceImport:
|
||||
// An `input * as <identifier> from <module-specifier>` clause.
|
||||
symbols.set(
|
||||
(<ts.NamespaceImport>bindings).name.text,
|
||||
{__symbolic: 'reference', module: from});
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
if (this.sourceFile) {
|
||||
ts.forEachChild(this.sourceFile, visit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateBuiltins(symbols: Map<string, MetadataValue>) {
|
||||
// From lib.core.d.ts (all "define const")
|
||||
['Object', 'Function', 'String', 'Number', 'Array', 'Boolean', 'Map', 'NaN', 'Infinity', 'Math',
|
||||
'Date', 'RegExp', 'Error', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError',
|
||||
'TypeError', 'URIError', 'JSON', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array',
|
||||
'Uint8ClampedArray', 'Uint16Array', 'Int16Array', 'Int32Array', 'Uint32Array', 'Float32Array',
|
||||
'Float64Array']
|
||||
.forEach(name => symbols.set(name, {__symbolic: 'reference', name}));
|
||||
}
|
Reference in New Issue
Block a user