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:
Chuck Jazdzewski 2016-05-31 11:00:39 -07:00
parent 13c39a52c6
commit 01dd7dde24
13 changed files with 656 additions and 353 deletions

View File

@ -27,7 +27,7 @@ describe("template codegen output", () => {
expect(fs.existsSync(metadataOutput)).toBeTruthy(); expect(fs.existsSync(metadataOutput)).toBeTruthy();
const output = fs.readFileSync(metadataOutput, {encoding: 'utf-8'}); const output = fs.readFileSync(metadataOutput, {encoding: 'utf-8'});
expect(output).toContain('"decorators":'); expect(output).toContain('"decorators":');
expect(output).toContain('"name":"Component","module":"@angular/core"'); expect(output).toContain('"module":"@angular/core","name":"Component"');
}); });
it("should write .d.ts files", () => { it("should write .d.ts files", () => {

View File

@ -62,17 +62,21 @@ export class CodeGenerator {
private readComponents(absSourcePath: string) { private readComponents(absSourcePath: string) {
const result: Promise<compiler.CompileDirectiveMetadata>[] = []; const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
const metadata = this.staticReflector.getModuleMetadata(absSourcePath); const moduleMetadata = this.staticReflector.getModuleMetadata(absSourcePath);
if (!metadata) { if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSourcePath}`); console.log(`WARNING: no metadata found for ${absSourcePath}`);
return result; return result;
} }
const metadata = moduleMetadata['metadata'];
const symbols = Object.keys(metadata['metadata']); const symbols = metadata && Object.keys(metadata);
if (!symbols || !symbols.length) { if (!symbols || !symbols.length) {
return result; return result;
} }
for (const symbol of symbols) { for (const symbol of symbols) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath); const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
let directive: compiler.CompileDirectiveMetadata; let directive: compiler.CompileDirectiveMetadata;
directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType); directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType);

View File

@ -153,7 +153,7 @@ export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator {
if (!sf) { if (!sf) {
throw new Error(`Source file ${filePath} not present in program.`); throw new Error(`Source file ${filePath} not present in program.`);
} }
const metadata = this.metadataCollector.getMetadata(sf, this.program.getTypeChecker()); const metadata = this.metadataCollector.getMetadata(sf);
return metadata; return metadata;
} }

View File

@ -359,6 +359,8 @@ export class StaticReflector implements ReflectorReader {
} else { } else {
return context; return context;
} }
case "error":
throw new Error(expression['message']);
} }
return null; return null;
} }

View File

@ -1,81 +1,28 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Evaluator, ImportMetadata, ImportSpecifierMetadata} from './evaluator'; import {Evaluator, ImportMetadata, ImportSpecifierMetadata, isPrimitive} from './evaluator';
import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata} from './schema'; import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, isMetadataError, isMetadataSymbolicReferenceExpression,} from './schema';
import {Symbols} from './symbols'; import {Symbols} from './symbols';
/** /**
* Collect decorator metadata from a TypeScript module. * Collect decorator metadata from a TypeScript module.
*/ */
export class MetadataCollector { export class MetadataCollector {
constructor() {} 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 * 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. * the source file that is expected to correspond to a module.
*/ */
public getMetadata(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): ModuleMetadata { public getMetadata(sourceFile: ts.SourceFile): ModuleMetadata {
const locals = new Symbols(); const locals = new Symbols(sourceFile);
const evaluator = new Evaluator(typeChecker, locals, this.collectImports(sourceFile)); const evaluator = new Evaluator(locals);
let metadata: {[name: string]: MetadataValue | ClassMetadata}|undefined;
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression); 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 { function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
let result: ClassMetadata = {__symbolic: 'class'}; let result: ClassMetadata = {__symbolic: 'class'};
@ -85,6 +32,15 @@ export class MetadataCollector {
return undefined; 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 // Add class decorators
if (classDeclaration.decorators) { if (classDeclaration.decorators) {
result.decorators = getDecorators(classDeclaration.decorators); result.decorators = getDecorators(classDeclaration.decorators);
@ -108,8 +64,9 @@ export class MetadataCollector {
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member; const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
const methodDecorators = getDecorators(method.decorators); const methodDecorators = getDecorators(method.decorators);
const parameters = method.parameters; const parameters = method.parameters;
const parameterDecoratorData: MetadataSymbolicExpression[][] = []; const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
const parametersData: MetadataSymbolicReferenceExpression[] = []; const parametersData: (MetadataSymbolicReferenceExpression | MetadataError | null)[] =
[];
let hasDecoratorData: boolean = false; let hasDecoratorData: boolean = false;
let hasParameterData: boolean = false; let hasParameterData: boolean = false;
for (const parameter of parameters) { for (const parameter of parameters) {
@ -117,8 +74,11 @@ export class MetadataCollector {
parameterDecoratorData.push(parameterData); parameterDecoratorData.push(parameterData);
hasDecoratorData = hasDecoratorData || !!parameterData; hasDecoratorData = hasDecoratorData || !!parameterData;
if (isConstructor) { if (isConstructor) {
const parameterType = typeChecker.getTypeAtLocation(parameter); if (parameter.type) {
parametersData.push(referenceFromType(parameterType) || null); parametersData.push(referenceFrom(parameter.type));
} else {
parametersData.push(null);
}
hasParameterData = true; hasParameterData = true;
} }
} }
@ -133,7 +93,9 @@ export class MetadataCollector {
if (hasParameterData) { if (hasParameterData) {
(<ConstructorMetadata>data).parameters = parametersData; (<ConstructorMetadata>data).parameters = parametersData;
} }
recordMember(name, data); if (!isMetadataError(name)) {
recordMember(name, data);
}
break; break;
case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.GetAccessor:
@ -141,9 +103,10 @@ export class MetadataCollector {
const property = <ts.PropertyDeclaration>member; const property = <ts.PropertyDeclaration>member;
const propertyDecorators = getDecorators(property.decorators); const propertyDecorators = getDecorators(property.decorators);
if (propertyDecorators) { if (propertyDecorators) {
recordMember( let name = evaluator.nameOf(property.name);
evaluator.nameOf(property.name), if (!isMetadataError(name)) {
{__symbolic: 'property', decorators: propertyDecorators}); recordMember(name, {__symbolic: 'property', decorators: propertyDecorators});
}
} }
break; break;
} }
@ -155,36 +118,94 @@ export class MetadataCollector {
return result.decorators || members ? result : undefined; return result.decorators || members ? result : undefined;
} }
let metadata: {[name: string]: (ClassMetadata | MetadataValue)}; // Predeclare classes
const symbols = typeChecker.getSymbolsInScope(sourceFile, ts.SymbolFlags.ExportValue); ts.forEachChild(sourceFile, node => {
for (var symbol of symbols) { switch (node.kind) {
for (var declaration of symbol.getDeclarations()) { case ts.SyntaxKind.ClassDeclaration:
switch (declaration.kind) { const classDeclaration = <ts.ClassDeclaration>node;
case ts.SyntaxKind.ClassDeclaration: const className = classDeclaration.name.text;
const classDeclaration = <ts.ClassDeclaration>declaration; 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 (classDeclaration.decorators) {
if (!metadata) metadata = {}; if (!metadata) metadata = {};
metadata[classDeclaration.name.text] = classMetadataOf(classDeclaration); metadata[className] = classMetadataOf(classDeclaration);
} }
break; }
case ts.SyntaxKind.VariableDeclaration: // Otherwise don't record metadata for the class.
const variableDeclaration = <ts.VariableDeclaration>declaration; break;
if (variableDeclaration.initializer) { case ts.SyntaxKind.VariableStatement:
const value = evaluator.evaluateNode(variableDeclaration.initializer); const variableStatement = <ts.VariableStatement>node;
if (value !== undefined) { for (let variableDeclaration of variableStatement.declarationList.declarations) {
if (evaluator.isFoldable(variableDeclaration.initializer)) { if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) {
// Record the value for use in other initializers let nameNode = <ts.Identifier>variableDeclaration.name;
locals.set(symbol, value); let varValue: MetadataValue;
} if (variableDeclaration.initializer) {
if (!metadata) metadata = {}; varValue = evaluator.evaluateNode(variableDeclaration.initializer);
metadata[evaluator.nameOf(variableDeclaration.name)] = } else {
evaluator.evaluateNode(variableDeclaration.initializer); 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}; return metadata && {__symbolic: 'module', metadata};
} }

View File

@ -67,8 +67,7 @@ export class MetadataWriterHost extends DelegatingHost {
// released // released
if (/*DTS*/ /\.js$/.test(emitFilePath)) { if (/*DTS*/ /\.js$/.test(emitFilePath)) {
const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json'); const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json');
const metadata = const metadata = this.metadataCollector.getMetadata(sourceFile);
this.metadataCollector.getMetadata(sourceFile, this.program.getTypeChecker());
if (metadata && metadata.metadata) { if (metadata && metadata.metadata) {
const metadataText = JSON.stringify(metadata); const metadataText = JSON.stringify(metadata);
writeFileSync(path, metadataText, {encoding: 'utf-8'}); writeFileSync(path, metadataText, {encoding: 'utf-8'});

View File

@ -1,21 +1,8 @@
import * as ts from 'typescript'; 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'; 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 { function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
const expression = callExpression.expression; const expression = callExpression.expression;
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) { 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)); return !ts.forEachChild(node, node => !cb(node));
} }
function isPrimitive(value: any): boolean { export function isPrimitive(value: any): boolean {
return Object(value) !== value; return Object(value) !== value;
} }
@ -72,63 +59,21 @@ export interface ImportMetadata {
* possible. * possible.
*/ */
export class Evaluator { export class Evaluator {
constructor( constructor(private symbols: Symbols) {}
private typeChecker: ts.TypeChecker, private symbols: Symbols,
private imports: ImportMetadata[]) {}
symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression { nameOf(node: ts.Node): string|MetadataError {
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 {
if (node.kind == ts.SyntaxKind.Identifier) { if (node.kind == ts.SyntaxKind.Identifier) {
return (<ts.Identifier>node).text; 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) && return this.isFoldableWorker(elementAccessExpression.expression, folding) &&
this.isFoldableWorker(elementAccessExpression.argumentExpression, folding); this.isFoldableWorker(elementAccessExpression.argumentExpression, folding);
case ts.SyntaxKind.Identifier: case ts.SyntaxKind.Identifier:
let symbol = this.typeChecker.getSymbolAtLocation(node); let identifier = <ts.Identifier>node;
if (symbol.flags & ts.SymbolFlags.Alias) { let reference = this.symbols.resolve(identifier.text);
symbol = this.typeChecker.getAliasedSymbol(symbol); if (isPrimitive(reference)) {
} return true;
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;
} }
break; break;
} }
@ -245,46 +175,43 @@ export class Evaluator {
* tree are folded. For example, a node representing `1 + 2` is folded into `3`. * tree are folded. For example, a node representing `1 + 2` is folded into `3`.
*/ */
public evaluateNode(node: ts.Node): MetadataValue { public evaluateNode(node: ts.Node): MetadataValue {
let error: MetadataError|undefined;
switch (node.kind) { switch (node.kind) {
case ts.SyntaxKind.ObjectLiteralExpression: case ts.SyntaxKind.ObjectLiteralExpression:
let obj: MetadataValue = {}; let obj: {[name: string]: any} = {};
let allPropertiesDefined = true;
ts.forEachChild(node, child => { ts.forEachChild(node, child => {
switch (child.kind) { switch (child.kind) {
case ts.SyntaxKind.PropertyAssignment: case ts.SyntaxKind.PropertyAssignment:
const assignment = <ts.PropertyAssignment>child; const assignment = <ts.PropertyAssignment>child;
const propertyName = this.nameOf(assignment.name); const propertyName = this.nameOf(assignment.name);
if (isMetadataError(propertyName)) {
return propertyName;
}
const propertyValue = this.evaluateNode(assignment.initializer); const propertyValue = this.evaluateNode(assignment.initializer);
(<any>obj)[propertyName] = propertyValue; if (isMetadataError(propertyValue)) {
allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined; error = propertyValue;
return true; // Stop the forEachChild.
} else {
obj[<string>propertyName] = propertyValue;
}
} }
}); });
if (allPropertiesDefined) return obj; if (error) return error;
break; return obj;
case ts.SyntaxKind.ArrayLiteralExpression: case ts.SyntaxKind.ArrayLiteralExpression:
let arr: MetadataValue[] = []; let arr: MetadataValue[] = [];
let allElementsDefined = true;
ts.forEachChild(node, child => { ts.forEachChild(node, child => {
const value = this.evaluateNode(child); const value = this.evaluateNode(child);
if (isMetadataError(value)) {
error = value;
return true; // Stop the forEachChild.
}
arr.push(value); arr.push(value);
allElementsDefined = isDefined(value) && allElementsDefined;
}); });
if (allElementsDefined) return arr; if (error) return error;
break; return arr;
case ts.SyntaxKind.CallExpression: case ts.SyntaxKind.CallExpression:
const callExpression = <ts.CallExpression>node; 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) { if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) {
const firstArgument = callExpression.arguments[0]; const firstArgument = callExpression.arguments[0];
if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) { if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) {
@ -292,72 +219,136 @@ export class Evaluator {
return this.evaluateNode(arrowFunction.body); return this.evaluateNode(arrowFunction.body);
} }
} }
const expression = this.evaluateNode(callExpression.expression); const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
if (isDefined(expression) && args.every(isDefined)) { if (args.some(isMetadataError)) {
const result: return args.find(isMetadataError);
MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
if (args && args.length) {
result.arguments = args;
}
return result;
} }
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: case ts.SyntaxKind.NewExpression:
const newExpression = <ts.NewExpression>node; const newExpression = <ts.NewExpression>node;
const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg)); const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg));
const newTarget = this.evaluateNode(newExpression.expression); if (newArgs.some(isMetadataError)) {
if (isDefined(newTarget) && newArgs.every(isDefined)) { return newArgs.find(isMetadataError);
const result: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget};
if (newArgs.length) {
result.arguments = newArgs;
}
return result;
} }
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: { case ts.SyntaxKind.PropertyAccessExpression: {
const propertyAccessExpression = <ts.PropertyAccessExpression>node; const propertyAccessExpression = <ts.PropertyAccessExpression>node;
const expression = this.evaluateNode(propertyAccessExpression.expression); const expression = this.evaluateNode(propertyAccessExpression.expression);
if (isMetadataError(expression)) {
return expression;
}
const member = this.nameOf(propertyAccessExpression.name); const member = this.nameOf(propertyAccessExpression.name);
if (this.isFoldable(propertyAccessExpression.expression)) return (<any>expression)[member]; if (isMetadataError(member)) {
if (this.findImportNamespace(propertyAccessExpression)) { return member;
return this.nodeSymbolReference(propertyAccessExpression);
} }
if (isDefined(expression)) { if (this.isFoldable(propertyAccessExpression.expression))
return {__symbolic: 'select', expression, member}; 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: { case ts.SyntaxKind.ElementAccessExpression: {
const elementAccessExpression = <ts.ElementAccessExpression>node; const elementAccessExpression = <ts.ElementAccessExpression>node;
const expression = this.evaluateNode(elementAccessExpression.expression); const expression = this.evaluateNode(elementAccessExpression.expression);
if (isMetadataError(expression)) {
return expression;
}
const index = this.evaluateNode(elementAccessExpression.argumentExpression); const index = this.evaluateNode(elementAccessExpression.argumentExpression);
if (isMetadataError(expression)) {
return expression;
}
if (this.isFoldable(elementAccessExpression.expression) && if (this.isFoldable(elementAccessExpression.expression) &&
this.isFoldable(elementAccessExpression.argumentExpression)) this.isFoldable(elementAccessExpression.argumentExpression))
return (<any>expression)[<string|number>index]; return (<any>expression)[<string|number>index];
if (isDefined(expression) && isDefined(index)) { return {__symbolic: 'index', expression, index};
return {__symbolic: 'index', expression, index};
}
break;
} }
case ts.SyntaxKind.Identifier: case ts.SyntaxKind.Identifier:
let symbol = this.typeChecker.getSymbolAtLocation(node); const identifier = <ts.Identifier>node;
if (symbol.flags & ts.SymbolFlags.Alias) { const name = identifier.text;
symbol = this.typeChecker.getAliasedSymbol(symbol); 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); return reference;
if (this.isFoldable(node)) { case ts.SyntaxKind.TypeReference:
// isFoldable implies, in this context, symbol declaration is a VariableDeclaration const typeReferenceNode = <ts.TypeReferenceNode>node;
const variableDeclaration = <ts.VariableDeclaration>( const typeNameNode = typeReferenceNode.typeName;
symbol.declarations && symbol.declarations.length && symbol.declarations[0]); if (typeNameNode.kind != ts.SyntaxKind.Identifier) {
return this.evaluateNode(variableDeclaration.initializer); 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: case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
return (<ts.LiteralExpression>node).text; return (<ts.LiteralExpression>node).text;
case ts.SyntaxKind.StringLiteral: case ts.SyntaxKind.StringLiteral:
return (<ts.StringLiteral>node).text; return (<ts.StringLiteral>node).text;
case ts.SyntaxKind.NumericLiteral: case ts.SyntaxKind.NumericLiteral:
return parseFloat((<ts.LiteralExpression>node).text); 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: case ts.SyntaxKind.NullKeyword:
return null; return null;
case ts.SyntaxKind.TrueKeyword: case ts.SyntaxKind.TrueKeyword:
@ -462,6 +453,6 @@ export class Evaluator {
} }
break; break;
} }
return undefined; return {__symbolic: 'error', message: 'Expression is too complex to resolve statically'};
} }
} }

View File

@ -8,7 +8,7 @@ export function isModuleMetadata(value: any): value is ModuleMetadata {
export interface ClassMetadata { export interface ClassMetadata {
__symbolic: 'class'; __symbolic: 'class';
decorators?: MetadataSymbolicExpression[]; decorators?: (MetadataSymbolicExpression|MetadataError)[];
members?: MetadataMap; members?: MetadataMap;
} }
export function isClassMetadata(value: any): value is ClassMetadata { export function isClassMetadata(value: any): value is ClassMetadata {
@ -19,7 +19,7 @@ export interface MetadataMap { [name: string]: MemberMetadata[]; }
export interface MemberMetadata { export interface MemberMetadata {
__symbolic: 'constructor'|'method'|'property'; __symbolic: 'constructor'|'method'|'property';
decorators?: MetadataSymbolicExpression[]; decorators?: (MetadataSymbolicExpression|MetadataError)[];
} }
export function isMemberMetadata(value: any): value is MemberMetadata { export function isMemberMetadata(value: any): value is MemberMetadata {
if (value) { if (value) {
@ -35,7 +35,7 @@ export function isMemberMetadata(value: any): value is MemberMetadata {
export interface MethodMetadata extends MemberMetadata { export interface MethodMetadata extends MemberMetadata {
__symbolic: 'constructor'|'method'; __symbolic: 'constructor'|'method';
parameterDecorators?: MetadataSymbolicExpression[][]; parameterDecorators?: (MetadataSymbolicExpression|MetadataError)[][];
} }
export function isMethodMetadata(value: any): value is MemberMetadata { export function isMethodMetadata(value: any): value is MemberMetadata {
return value && (value.__symbolic === 'constructor' || value.__symbolic === 'method'); 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 { export interface ConstructorMetadata extends MethodMetadata {
__symbolic: 'constructor'; __symbolic: 'constructor';
parameters?: MetadataSymbolicExpression[]; parameters?: (MetadataSymbolicExpression|MetadataError|null)[];
} }
export function isConstructorMetadata(value: any): value is ConstructorMetadata { export function isConstructorMetadata(value: any): value is ConstructorMetadata {
return value && value.__symbolic === 'constructor'; return value && value.__symbolic === 'constructor';
} }
export type MetadataValue = export type MetadataValue = string | number | boolean | MetadataObject | MetadataArray |
string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression; MetadataSymbolicExpression | MetadataError;
export interface MetadataObject { [name: string]: MetadataValue; } export interface MetadataObject { [name: string]: MetadataValue; }
@ -117,11 +117,51 @@ export function isMetadataSymbolicPrefixExpression(value: any):
return value && value.__symbolic === 'pre'; return value && value.__symbolic === 'pre';
} }
export interface MetadataSymbolicReferenceExpression extends MetadataSymbolicExpression { export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression {
__symbolic: 'reference'; __symbolic: 'reference';
name: string; 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; 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): export function isMetadataSymbolicReferenceExpression(value: any):
value is MetadataSymbolicReferenceExpression { value is MetadataSymbolicReferenceExpression {
return value && value.__symbolic === 'reference'; return value && value.__symbolic === 'reference';
@ -136,3 +176,11 @@ export function isMetadataSymbolicSelectExpression(value: any):
value is MetadataSymbolicSelectExpression { value is MetadataSymbolicSelectExpression {
return value && value.__symbolic === 'select'; 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';
}

View File

@ -1,36 +1,99 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
// TOOD: Remove when tools directory is upgraded to support es6 target import {MetadataValue} from './schema';
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;
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 { 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 { resolve(name: string): MetadataValue|undefined { return this.symbols.get(name); }
this.map.set(symbol.getDeclarations()[0], value);
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]); } private buildImports(): void {
let symbols = this._symbols;
static empty: Symbols = new 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}));
} }

View File

@ -1,6 +1,6 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {MetadataCollector} from '../src/collector'; import {MetadataCollector} from '../src/collector';
import {ClassMetadata, ModuleMetadata} from '../src/schema'; import {ClassMetadata, ConstructorMetadata, ModuleMetadata} from '../src/schema';
import {Directory, expectValidSources, Host} from './typescript.mocks'; import {Directory, expectValidSources, Host} from './typescript.mocks';
@ -8,16 +8,15 @@ describe('Collector', () => {
let host: ts.LanguageServiceHost; let host: ts.LanguageServiceHost;
let service: ts.LanguageService; let service: ts.LanguageService;
let program: ts.Program; let program: ts.Program;
let typeChecker: ts.TypeChecker;
let collector: MetadataCollector; let collector: MetadataCollector;
beforeEach(() => { beforeEach(() => {
host = new Host( host = new Host(FILES, [
FILES, '/app/app.component.ts', '/app/cases-data.ts', '/app/error-cases.ts', '/promise.ts',
['/app/app.component.ts', '/app/cases-data.ts', '/app/cases-no-data.ts', '/promise.ts']); '/unsupported-1.ts', '/unsupported-2.ts'
]);
service = ts.createLanguageService(host); service = ts.createLanguageService(host);
program = service.getProgram(); program = service.getProgram();
typeChecker = program.getTypeChecker();
collector = new MetadataCollector(); collector = new MetadataCollector();
}); });
@ -25,27 +24,13 @@ describe('Collector', () => {
it('should return undefined for modules that have no metadata', () => { it('should return undefined for modules that have no metadata', () => {
const sourceFile = program.getSourceFile('app/hero.ts'); const sourceFile = program.getSourceFile('app/hero.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker); const metadata = collector.getMetadata(sourceFile);
expect(metadata).toBeUndefined(); expect(metadata).toBeUndefined();
}); });
it('should be able to collect import statements', () => {
const sourceFile = program.getSourceFile('app/app.component.ts');
expect(collector.collectImports(sourceFile)).toEqual([
{
from: 'angular2/core',
namedImports: [{name: 'MyComponent', propertyName: 'Component'}, {name: 'OnInit'}]
},
{from: 'angular2/common', namespace: 'common'},
{from: './hero', namedImports: [{name: 'Hero'}]},
{from: './hero-detail.component', namedImports: [{name: 'HeroDetailComponent'}]},
{from: './hero.service', defaultName: 'HeroService'}
]);
});
it('should be able to collect a simple component\'s metadata', () => { it('should be able to collect a simple component\'s metadata', () => {
const sourceFile = program.getSourceFile('app/hero-detail.component.ts'); const sourceFile = program.getSourceFile('app/hero-detail.component.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker); const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({ expect(metadata).toEqual({
__symbolic: 'module', __symbolic: 'module',
metadata: { metadata: {
@ -53,7 +38,7 @@ describe('Collector', () => {
__symbolic: 'class', __symbolic: 'class',
decorators: [{ decorators: [{
__symbolic: 'call', __symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'}, expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
arguments: [{ arguments: [{
selector: 'my-hero-detail', selector: 'my-hero-detail',
template: ` template: `
@ -74,7 +59,7 @@ describe('Collector', () => {
decorators: [{ decorators: [{
__symbolic: 'call', __symbolic: 'call',
expression: expression:
{__symbolic: 'reference', name: 'Input', module: 'angular2/core'} {__symbolic: 'reference', module: 'angular2/core', name: 'Input'}
}] }]
}] }]
} }
@ -85,7 +70,7 @@ describe('Collector', () => {
it('should be able to get a more complicated component\'s metadata', () => { it('should be able to get a more complicated component\'s metadata', () => {
const sourceFile = program.getSourceFile('/app/app.component.ts'); const sourceFile = program.getSourceFile('/app/app.component.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker); const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({ expect(metadata).toEqual({
__symbolic: 'module', __symbolic: 'module',
metadata: { metadata: {
@ -93,7 +78,7 @@ describe('Collector', () => {
__symbolic: 'class', __symbolic: 'class',
decorators: [{ decorators: [{
__symbolic: 'call', __symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'}, expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
arguments: [{ arguments: [{
selector: 'my-app', selector: 'my-app',
template: ` template: `
@ -110,22 +95,22 @@ describe('Collector', () => {
directives: [ directives: [
{ {
__symbolic: 'reference', __symbolic: 'reference',
module: './hero-detail.component',
name: 'HeroDetailComponent', name: 'HeroDetailComponent',
module: './hero-detail.component'
}, },
{__symbolic: 'reference', name: 'NgFor', module: 'angular2/common'} {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'}
], ],
providers: [{__symbolic: 'reference', name: undefined, module: './hero.service'}], providers: [{__symbolic: 'reference', module: './hero.service', default: true}],
pipes: [ pipes: [
{__symbolic: 'reference', name: 'LowerCasePipe', module: 'angular2/common'}, {__symbolic: 'reference', module: 'angular2/common', name: 'LowerCasePipe'},
{__symbolic: 'reference', name: 'UpperCasePipe', module: 'angular2/common'} {__symbolic: 'reference', module: 'angular2/common', name: 'UpperCasePipe'}
] ]
}] }]
}], }],
members: { members: {
__ctor__: [{ __ctor__: [{
__symbolic: 'constructor', __symbolic: 'constructor',
parameters: [{__symbolic: 'reference', name: undefined, module: './hero.service'}] parameters: [{__symbolic: 'reference', module: './hero.service', default: true}]
}], }],
onSelect: [{__symbolic: 'method'}], onSelect: [{__symbolic: 'method'}],
ngOnInit: [{__symbolic: 'method'}], ngOnInit: [{__symbolic: 'method'}],
@ -138,7 +123,7 @@ describe('Collector', () => {
it('should return the values of exported variables', () => { it('should return the values of exported variables', () => {
const sourceFile = program.getSourceFile('/app/mock-heroes.ts'); const sourceFile = program.getSourceFile('/app/mock-heroes.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker); const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({ expect(metadata).toEqual({
__symbolic: 'module', __symbolic: 'module',
metadata: { metadata: {
@ -153,11 +138,11 @@ describe('Collector', () => {
}); });
}); });
it('should have no data produced for the no data cases', () => { it('should return undefined for modules that have no metadata', () => {
const sourceFile = program.getSourceFile('/app/cases-no-data.ts'); const sourceFile = program.getSourceFile('/app/error-cases.ts');
expect(sourceFile).toBeTruthy(sourceFile); expect(sourceFile).toBeTruthy(sourceFile);
const metadata = collector.getMetadata(sourceFile, typeChecker); const metadata = collector.getMetadata(sourceFile);
expect(metadata).toBeFalsy(); expect(metadata).toBeUndefined();
}); });
let casesFile: ts.SourceFile; let casesFile: ts.SourceFile;
@ -165,14 +150,15 @@ describe('Collector', () => {
beforeEach(() => { beforeEach(() => {
casesFile = program.getSourceFile('/app/cases-data.ts'); casesFile = program.getSourceFile('/app/cases-data.ts');
casesMetadata = collector.getMetadata(casesFile, typeChecker); casesMetadata = collector.getMetadata(casesFile);
}); });
it('should provide null for an any ctor pameter type', () => { it('should provide any reference for an any ctor parameter type', () => {
const casesAny = <ClassMetadata>casesMetadata.metadata['CaseAny']; const casesAny = <ClassMetadata>casesMetadata.metadata['CaseAny'];
expect(casesAny).toBeTruthy(); expect(casesAny).toBeTruthy();
const ctorData = casesAny.members['__ctor__']; const ctorData = casesAny.members['__ctor__'];
expect(ctorData).toEqual([{__symbolic: 'constructor', parameters: [null]}]); expect(ctorData).toEqual(
[{__symbolic: 'constructor', parameters: [{__symbolic: 'reference', name: 'any'}]}]);
}); });
it('should record annotations on set and get declarations', () => { it('should record annotations on set and get declarations', () => {
@ -193,6 +179,67 @@ describe('Collector', () => {
const caseFullProp = <ClassMetadata>casesMetadata.metadata['FullProp']; const caseFullProp = <ClassMetadata>casesMetadata.metadata['FullProp'];
expect(caseFullProp.members).toEqual(propertyData); expect(caseFullProp.members).toEqual(propertyData);
}); });
it('should record references to parameterized types', () => {
const casesForIn = <ClassMetadata>casesMetadata.metadata['NgFor'];
expect(casesForIn).toEqual({
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Injectable'}
}],
members: {
__ctor__: [{
__symbolic: 'constructor',
parameters: [{
__symbolic: 'reference',
name: 'ClassReference',
arguments: [{__symbolic: 'reference', name: 'NgForRow'}]
}]
}]
}
});
});
it('should report errors for destructured imports', () => {
let unsupported1 = program.getSourceFile('/unsupported-1.ts');
let metadata = collector.getMetadata(unsupported1);
expect(metadata).toEqual({
__symbolic: 'module',
metadata: {
a: {
__symbolc: 'error',
message: 'Destructuring declarations cannot be referenced statically'
},
b: {
__symbolc: 'error',
message: 'Destructuring declarations cannot be referenced statically'
},
c: {
__symbolc: 'error',
message: 'Destructuring declarations cannot be referenced statically'
},
d: {
__symbolc: 'error',
message: 'Destructuring declarations cannot be referenced statically'
},
e: {
__symbolic: 'error',
message: 'Only intialized variables and constants can be referenced statically'
}
}
});
});
it('should report an error for refrences to unexpected types', () => {
let unsupported1 = program.getSourceFile('/unsupported-2.ts');
let metadata = collector.getMetadata(unsupported1);
let barClass = <ClassMetadata>metadata.metadata['Bar'];
let ctor = <ConstructorMetadata>barClass.members['__ctor__'][0];
let parameter = ctor.parameters[0];
expect(parameter).toEqual(
{__symbolic: 'error', message: 'Reference to non-exported class Foo'});
});
}); });
// TODO: Do not use \` in a template literal as it confuses clang-format // TODO: Do not use \` in a template literal as it confuses clang-format
@ -344,8 +391,18 @@ const FILES: Directory = {
this._name = value; this._name = value;
} }
} }
export class ClassReference<T> { }
export class NgForRow {
}
@Injectable()
export class NgFor {
constructor (public ref: ClassReference<NgForRow>) {}
}
`, `,
'cases-no-data.ts': ` 'error-cases.ts': `
import HeroService from './hero.service'; import HeroService from './hero.service';
export class CaseCtor { export class CaseCtor {
@ -377,7 +434,21 @@ const FILES: Directory = {
declare var Promise: PromiseConstructor; declare var Promise: PromiseConstructor;
`, `,
'unsupported-1.ts': `
export let {a, b} = {a: 1, b: 2};
export let [c, d] = [1, 2];
export let e;
`,
'unsupported-2.ts': `
import {Injectable} from 'angular2/core';
class Foo {}
@Injectable()
export class Bar {
constructor(private f: Foo) {}
}
`,
'node_modules': { 'node_modules': {
'angular2': { 'angular2': {
'core.d.ts': ` 'core.d.ts': `

View File

@ -15,14 +15,15 @@ describe('Evaluator', () => {
let evaluator: Evaluator; let evaluator: Evaluator;
beforeEach(() => { beforeEach(() => {
host = new Host( host = new Host(FILES, [
FILES, 'expressions.ts', 'consts.ts', 'const_expr.ts', 'forwardRef.ts', 'classes.ts',
['expressions.ts', 'const_expr.ts', 'forwardRef.ts', 'classes.ts', 'newExpression.ts']); 'newExpression.ts'
]);
service = ts.createLanguageService(host); service = ts.createLanguageService(host);
program = service.getProgram(); program = service.getProgram();
typeChecker = program.getTypeChecker(); typeChecker = program.getTypeChecker();
symbols = new Symbols(); symbols = new Symbols(null);
evaluator = new Evaluator(typeChecker, symbols, []); evaluator = new Evaluator(symbols);
}); });
it('should not have typescript errors in test data', () => { it('should not have typescript errors in test data', () => {
@ -59,8 +60,14 @@ describe('Evaluator', () => {
it('should be able to evaluate expressions', () => { it('should be able to evaluate expressions', () => {
var expressions = program.getSourceFile('expressions.ts'); var expressions = program.getSourceFile('expressions.ts');
symbols.define('someName', 'some-name');
symbols.define('someBool', true);
symbols.define('one', 1);
symbols.define('two', 2);
expect(evaluator.evaluateNode(findVar(expressions, 'three').initializer)).toBe(3); expect(evaluator.evaluateNode(findVar(expressions, 'three').initializer)).toBe(3);
symbols.define('three', 3);
expect(evaluator.evaluateNode(findVar(expressions, 'four').initializer)).toBe(4); expect(evaluator.evaluateNode(findVar(expressions, 'four').initializer)).toBe(4);
symbols.define('four', 4);
expect(evaluator.evaluateNode(findVar(expressions, 'obj').initializer)) expect(evaluator.evaluateNode(findVar(expressions, 'obj').initializer))
.toEqual({one: 1, two: 2, three: 3, four: 4}); .toEqual({one: 1, two: 2, three: 3, four: 4});
expect(evaluator.evaluateNode(findVar(expressions, 'arr').initializer)).toEqual([1, 2, 3, 4]); expect(evaluator.evaluateNode(findVar(expressions, 'arr').initializer)).toEqual([1, 2, 3, 4]);
@ -102,9 +109,9 @@ describe('Evaluator', () => {
it('should report recursive references as symbolic', () => { it('should report recursive references as symbolic', () => {
var expressions = program.getSourceFile('expressions.ts'); var expressions = program.getSourceFile('expressions.ts');
expect(evaluator.evaluateNode(findVar(expressions, 'recursiveA').initializer)) expect(evaluator.evaluateNode(findVar(expressions, 'recursiveA').initializer))
.toEqual({__symbolic: 'reference', name: 'recursiveB', module: undefined}); .toEqual({__symbolic: 'reference', name: 'recursiveB'});
expect(evaluator.evaluateNode(findVar(expressions, 'recursiveB').initializer)) expect(evaluator.evaluateNode(findVar(expressions, 'recursiveB').initializer))
.toEqual({__symbolic: 'reference', name: 'recursiveA', module: undefined}); .toEqual({__symbolic: 'reference', name: 'recursiveA'});
}); });
it('should correctly handle special cases for CONST_EXPR', () => { it('should correctly handle special cases for CONST_EXPR', () => {
@ -120,8 +127,8 @@ describe('Evaluator', () => {
}); });
it('should return new expressions', () => { it('should return new expressions', () => {
evaluator = symbols.define('Value', {__symbolic: 'reference', module: './classes', name: 'Value'});
new Evaluator(typeChecker, symbols, [{from: './classes', namedImports: [{name: 'Value'}]}]); evaluator = new Evaluator(symbols);
var newExpression = program.getSourceFile('newExpression.ts'); var newExpression = program.getSourceFile('newExpression.ts');
expect(evaluator.evaluateNode(findVar(newExpression, 'someValue').initializer)).toEqual({ expect(evaluator.evaluateNode(findVar(newExpression, 'someValue').initializer)).toEqual({
__symbolic: 'new', __symbolic: 'new',
@ -154,7 +161,10 @@ const FILES: Directory = {
export var two = 2; export var two = 2;
`, `,
'expressions.ts': ` 'expressions.ts': `
import {someName, someBool, one, two} from './consts'; export var someName = 'some-name';
export var someBool = true;
export var one = 1;
export var two = 2;
export var three = one + two; export var three = one + two;
export var four = two * two; export var four = two * two;

View File

@ -1,29 +1,125 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {isMetadataGlobalReferenceExpression} from '../src/schema';
import {Symbols} from '../src/symbols'; import {Symbols} from '../src/symbols';
import {MockSymbol, MockVariableDeclaration} from './typescript.mocks';
import {Directory, Host, expectNoDiagnostics} from './typescript.mocks';
describe('Symbols', () => { describe('Symbols', () => {
let symbols: Symbols; let symbols: Symbols;
const someValue = 'some-value'; const someValue = 'some-value';
const someSymbol = MockSymbol.of('some-symbol');
const aliasSymbol = new MockSymbol('some-symbol', someSymbol.getDeclarations()[0]);
const missingSymbol = MockSymbol.of('some-other-symbol');
beforeEach(() => symbols = new Symbols()); beforeEach(() => symbols = new Symbols(null));
it('should be able to add a symbol', () => symbols.set(someSymbol, someValue)); it('should be able to add a symbol', () => symbols.define('someSymbol', someValue));
beforeEach(() => symbols.set(someSymbol, someValue)); beforeEach(() => symbols.define('someSymbol', someValue));
it('should be able to `has` a symbol', () => expect(symbols.has(someSymbol)).toBeTruthy()); it('should be able to `has` a symbol', () => expect(symbols.has('someSymbol')).toBeTruthy());
it('should be able to `get` a symbol value', it('should be able to `get` a symbol value',
() => expect(symbols.get(someSymbol)).toBe(someValue)); () => expect(symbols.resolve('someSymbol')).toBe(someValue));
it('should be able to `has` an alias symbol',
() => expect(symbols.has(aliasSymbol)).toBeTruthy());
it('should be able to `get` a symbol value', it('should be able to `get` a symbol value',
() => expect(symbols.get(aliasSymbol)).toBe(someValue)); () => expect(symbols.resolve('someSymbol')).toBe(someValue));
it('should be able to determine symbol is missing', it('should be able to determine symbol is missing',
() => expect(symbols.has(missingSymbol)).toBeFalsy()); () => expect(symbols.has('missingSymbol')).toBeFalsy());
it('should return undefined from `get` for a missing symbol', it('should return undefined from `get` for a missing symbol',
() => expect(symbols.get(missingSymbol)).toBeUndefined()); () => expect(symbols.resolve('missingSymbol')).toBeUndefined());
let host: ts.LanguageServiceHost;
let service: ts.LanguageService;
let program: ts.Program;
let expressions: ts.SourceFile;
let imports: ts.SourceFile;
beforeEach(() => {
host = new Host(FILES, ['consts.ts', 'expressions.ts', 'imports.ts']);
service = ts.createLanguageService(host);
program = service.getProgram();
expressions = program.getSourceFile('expressions.ts');
imports = program.getSourceFile('imports.ts');
});
it('should not have syntax errors in the test sources', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (const sourceFile of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
}
});
it('should be able to find the source files', () => {
expect(expressions).toBeDefined();
expect(imports).toBeDefined();
})
it('should be able to create symbols for a source file', () => {
let symbols = new Symbols(expressions);
expect(symbols).toBeDefined();
});
it('should be able to find symbols in expression', () => {
let symbols = new Symbols(expressions);
expect(symbols.has('someName')).toBeTruthy();
expect(symbols.resolve('someName'))
.toEqual({__symbolic: 'reference', module: './consts', name: 'someName'});
expect(symbols.has('someBool')).toBeTruthy();
expect(symbols.resolve('someBool'))
.toEqual({__symbolic: 'reference', module: './consts', name: 'someBool'});
});
it('should be able to detect a * import', () => {
let symbols = new Symbols(imports);
expect(symbols.resolve('b')).toEqual({__symbolic: 'reference', module: 'b'});
});
it('should be able to detect importing a default export', () => {
let symbols = new Symbols(imports);
expect(symbols.resolve('d')).toEqual({__symbolic: 'reference', module: 'd', default: true});
});
it('should be able to import a renamed symbol', () => {
let symbols = new Symbols(imports);
expect(symbols.resolve('g')).toEqual({__symbolic: 'reference', name: 'f', module: 'f'});
});
it('should be able to resolve any symbol in core global scope', () => {
let core = program.getSourceFiles().find(source => source.fileName.endsWith('lib.d.ts'));
expect(core).toBeDefined();
let visit = (node: ts.Node): boolean => {
switch (node.kind) {
case ts.SyntaxKind.VariableStatement:
case ts.SyntaxKind.VariableDeclarationList:
return ts.forEachChild(node, visit);
case ts.SyntaxKind.VariableDeclaration:
const variableDeclaration = <ts.VariableDeclaration>node;
const nameNode = <ts.Identifier>variableDeclaration.name;
const name = nameNode.text;
const result = symbols.resolve(name);
expect(isMetadataGlobalReferenceExpression(result) && result.name).toEqual(name);
// Ignore everything after Float64Array as it is IE specific.
return name === 'Float64Array';
}
return false;
};
ts.forEachChild(core, visit);
});
}); });
const FILES: Directory = {
'consts.ts': `
export var someName = 'some-name';
export var someBool = true;
export var one = 1;
export var two = 2;
`,
'expressions.ts': `
import {someName, someBool, one, two} from './consts';
`,
'imports.ts': `
import * as b from 'b';
import 'c';
import d from 'd';
import {f as g} from 'f';
`
};

View File

@ -121,7 +121,6 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
this.doFullBuild(); this.doFullBuild();
} else { } else {
let program = this.tsService.getProgram(); let program = this.tsService.getProgram();
let typeChecker = program.getTypeChecker();
tsEmitInternal = false; tsEmitInternal = false;
pathsToEmit.forEach((tsFilePath) => { pathsToEmit.forEach((tsFilePath) => {
let output = this.tsService.getEmitOutput(tsFilePath); let output = this.tsService.getEmitOutput(tsFilePath);
@ -139,7 +138,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
fs.writeFileSync(o.name, this.fixSourceMapSources(o.text), FS_OPTS); fs.writeFileSync(o.name, this.fixSourceMapSources(o.text), FS_OPTS);
if (endsWith(o.name, '.d.ts')) { if (endsWith(o.name, '.d.ts')) {
const sourceFile = program.getSourceFile(tsFilePath); const sourceFile = program.getSourceFile(tsFilePath);
this.emitMetadata(o.name, sourceFile, typeChecker); this.emitMetadata(o.name, sourceFile);
} }
}); });
} }
@ -210,7 +209,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
const originalFile = absoluteFilePath.replace(this.tsOpts.outDir, this.tsOpts.rootDir) const originalFile = absoluteFilePath.replace(this.tsOpts.outDir, this.tsOpts.rootDir)
.replace(/\.d\.ts$/, '.ts'); .replace(/\.d\.ts$/, '.ts');
const sourceFile = program.getSourceFile(originalFile); const sourceFile = program.getSourceFile(originalFile);
this.emitMetadata(absoluteFilePath, sourceFile, typeChecker); this.emitMetadata(absoluteFilePath, sourceFile);
} }
}); });
@ -256,10 +255,9 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
* Emit a .metadata.json file to correspond to the .d.ts file if the module contains classes that * Emit a .metadata.json file to correspond to the .d.ts file if the module contains classes that
* use decorators or exported constants. * use decorators or exported constants.
*/ */
private emitMetadata( private emitMetadata(dtsFileName: string, sourceFile: ts.SourceFile) {
dtsFileName: string, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) {
if (sourceFile) { if (sourceFile) {
const metadata = this.metadataCollector.getMetadata(sourceFile, typeChecker); const metadata = this.metadataCollector.getMetadata(sourceFile);
if (metadata && metadata.metadata) { if (metadata && metadata.metadata) {
const metadataText = JSON.stringify(metadata); const metadataText = JSON.stringify(metadata);
const metadataFileName = dtsFileName.replace(/\.d.ts$/, '.metadata.json'); const metadataFileName = dtsFileName.replace(/\.d.ts$/, '.metadata.json');