chore: rename tools/metadata into tools/ts-metadata-collector

Needed to that we can use the locally compiled one during
our tests.
This commit is contained in:
Tobias Bosch
2016-05-04 10:01:35 -07:00
parent 29700aa188
commit 188bda813e
11 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,15 @@
# TypeScript Decorator metadata collector
The `.d.ts` format does not preserve information about the Decorators applied to symbols.
Some tools, such as Angular 2 template compiler, need access to statically analyzable
information about Decorators, so this library allows programs to produce a `foo.metadata.json`
to accompany a `foo.d.ts` file, and preserves the information that was lost in the declaration
emit.
## Releasing
```
$ gulp build.tools
$ cp tools/metadata/package.json dist/tools/metadata/
$ npm login [angularcore]
$ npm publish dist/tools/metadata
```

View File

@ -0,0 +1,2 @@
export * from './src/collector';
export * from './src/schema';

View File

@ -0,0 +1,16 @@
{
"name": "ts-metadata-collector",
"version": "0.1.1",
"description": "Collects static Decorator metadata from TypeScript sources",
"homepage": "https://github.com/angular/angular/tree/master/tools/metadata",
"bugs": "https://github.com/angular/angular/issues",
"contributors": [
"Chuck Jazdzewski <chuckj@google.com>"
],
"license": "MIT",
"repository": {"type":"git","url":"https://github.com/angular/angular.git"},
"devDependencies": {},
"peerDependencies": {
"typescript": "^1.8.9 || ^1.9.0-dev"
}
}

View File

@ -0,0 +1,199 @@
import * as ts from 'typescript';
import {Evaluator, ImportMetadata, ImportSpecifierMetadata} from './evaluator';
import {Symbols} from './symbols';
import {
ClassMetadata,
ConstructorMetadata,
ModuleMetadata,
MemberMetadata,
MetadataMap,
MetadataSymbolicExpression,
MetadataSymbolicReferenceExpression,
MetadataValue,
MethodMetadata
} from './schema';
/**
* 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) {
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) {
namedImport['propertyName'] = i.propertyName.text;
}
namedImports.push(namedImport);
});
newImport['namedImports'] = namedImports;
break;
case ts.SyntaxKind.NamespaceImport:
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));
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"};
function getDecorators(decorators: ts.Decorator[]): MetadataSymbolicExpression[] {
if (decorators && decorators.length)
return decorators.map(decorator => objFromDecorator(decorator));
return undefined;
}
// Add class decorators
if (classDeclaration.decorators) {
result.decorators = getDecorators(classDeclaration.decorators);
}
// member decorators
let members: MetadataMap = null;
function recordMember(name: string, metadata: MemberMetadata) {
if (!members) members = {};
let data = members.hasOwnProperty(name) ? members[name] : [];
data.push(metadata);
members[name] = data;
}
for (const member of classDeclaration.members) {
let isConstructor = false;
switch (member.kind) {
case ts.SyntaxKind.Constructor:
isConstructor = true;
// fallthrough
case ts.SyntaxKind.MethodDeclaration:
const method = <ts.MethodDeclaration | ts.ConstructorDeclaration>member;
const methodDecorators = getDecorators(method.decorators);
const parameters = method.parameters;
const parameterDecoratorData: MetadataSymbolicExpression[][] = [];
const parametersData: MetadataSymbolicReferenceExpression[] = [];
let hasDecoratorData: boolean = false;
let hasParameterData: boolean = false;
for (const parameter of parameters) {
const parameterData = getDecorators(parameter.decorators);
parameterDecoratorData.push(parameterData);
hasDecoratorData = hasDecoratorData || !!parameterData;
if (isConstructor) {
const parameterType = typeChecker.getTypeAtLocation(parameter);
parametersData.push(referenceFromType(parameterType) || null);
hasParameterData = true;
}
}
const data: MethodMetadata = {__symbolic: isConstructor ? "constructor" : "method"};
const name = isConstructor ? "__ctor__" : evaluator.nameOf(member.name);
if (methodDecorators) {
data.decorators = methodDecorators;
}
if (hasDecoratorData) {
data.parameterDecorators = parameterDecoratorData;
}
if (hasParameterData) {
(<ConstructorMetadata>data).parameters = parametersData;
}
recordMember(name, data);
break;
case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.SetAccessor:
const property = <ts.PropertyDeclaration>member;
const propertyDecorators = getDecorators(property.decorators);
if (propertyDecorators) {
recordMember(evaluator.nameOf(property.name),
{__symbolic: 'property', decorators: propertyDecorators});
}
break;
}
}
if (members) {
result.members = members;
}
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;
if (classDeclaration.decorators) {
if (!metadata) metadata = {};
metadata[classDeclaration.name.text] = 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);
}
}
break;
}
}
}
return metadata && {__symbolic: "module", metadata};
}
}

View File

@ -0,0 +1,469 @@
import * as ts from 'typescript';
import {Symbols} from './symbols';
import {
MetadataValue,
MetadataSymbolicCallExpression,
MetadataSymbolicReferenceExpression
} from './schema';
// 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) {
const propertyAccessExpression = <ts.PropertyAccessExpression>expression;
const name = propertyAccessExpression.name;
if (name.kind == ts.SyntaxKind.Identifier) {
return name.text === memberName;
}
}
return false;
}
function isCallOf(callExpression: ts.CallExpression, ident: string): boolean {
const expression = callExpression.expression;
if (expression.kind === ts.SyntaxKind.Identifier) {
const identifier = <ts.Identifier>expression;
return identifier.text === ident;
}
return false;
}
/**
* ts.forEachChild stops iterating children when the callback return a truthy value.
* This method inverts this to implement an `every` style iterator. It will return
* true if every call to `cb` returns `true`.
*/
function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
return !ts.forEachChild(node, node => !cb(node));
}
function isPrimitive(value: any): boolean {
return Object(value) !== value;
}
function isDefined(obj: any): boolean {
return obj !== undefined;
}
// import {propertyName as name} from 'place'
// import {name} from 'place'
export interface ImportSpecifierMetadata {
name: string;
propertyName?: string;
}
export interface ImportMetadata {
defaultName?: string; // import d from 'place'
namespace?: string; // import * as d from 'place'
namedImports?: ImportSpecifierMetadata[]; // import {a} from 'place'
from: string; // from 'place'
}
/**
* Produce a symbolic representation of an expression folding values into their final value when
* possible.
*/
export class Evaluator {
constructor(private typeChecker: ts.TypeChecker, private symbols: Symbols,
private imports: ImportMetadata[]) {}
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 {
if (node.kind == ts.SyntaxKind.Identifier) {
return (<ts.Identifier>node).text;
}
return <string>this.evaluateNode(node);
}
/**
* Returns true if the expression represented by `node` can be folded into a literal expression.
*
* For example, a literal is always foldable. This means that literal expressions such as `1.2`
* `"Some value"` `true` `false` are foldable.
*
* - An object literal is foldable if all the properties in the literal are foldable.
* - An array literal is foldable if all the elements are foldable.
* - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR.
* - A property access is foldable if the object is foldable.
* - A array index is foldable if index expression is foldable and the array is foldable.
* - Binary operator expressions are foldable if the left and right expressions are foldable and
* it is one of '+', '-', '*', '/', '%', '||', and '&&'.
* - An identifier is foldable if a value can be found for its symbol in the evaluator symbol
* table.
*/
public isFoldable(node: ts.Node): boolean {
return this.isFoldableWorker(node, new Map<ts.Node, boolean>());
}
private isFoldableWorker(node: ts.Node, folding: Map<ts.Node, boolean>): boolean {
if (node) {
switch (node.kind) {
case ts.SyntaxKind.ObjectLiteralExpression:
return everyNodeChild(node, child => {
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
const propertyAssignment = <ts.PropertyAssignment>child;
return this.isFoldableWorker(propertyAssignment.initializer, folding);
}
return false;
});
case ts.SyntaxKind.ArrayLiteralExpression:
return everyNodeChild(node, child => this.isFoldableWorker(child, folding));
case ts.SyntaxKind.CallExpression:
const callExpression = <ts.CallExpression>node;
// We can fold a <array>.concat(<v>).
if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) {
const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression;
if (this.isFoldableWorker(arrayNode, folding) &&
this.isFoldableWorker(callExpression.arguments[0], folding)) {
// It needs to be an array.
const arrayValue = this.evaluateNode(arrayNode);
if (arrayValue && Array.isArray(arrayValue)) {
return true;
}
}
}
// We can fold a call to CONST_EXPR
if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1)
return this.isFoldableWorker(callExpression.arguments[0], folding);
return false;
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.NullKeyword:
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
return true;
case ts.SyntaxKind.ParenthesizedExpression:
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
return this.isFoldableWorker(parenthesizedExpression.expression, folding);
case ts.SyntaxKind.BinaryExpression:
const binaryExpression = <ts.BinaryExpression>node;
switch (binaryExpression.operatorToken.kind) {
case ts.SyntaxKind.PlusToken:
case ts.SyntaxKind.MinusToken:
case ts.SyntaxKind.AsteriskToken:
case ts.SyntaxKind.SlashToken:
case ts.SyntaxKind.PercentToken:
case ts.SyntaxKind.AmpersandAmpersandToken:
case ts.SyntaxKind.BarBarToken:
return this.isFoldableWorker(binaryExpression.left, folding) &&
this.isFoldableWorker(binaryExpression.right, folding);
}
case ts.SyntaxKind.PropertyAccessExpression:
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
return this.isFoldableWorker(propertyAccessExpression.expression, folding);
case ts.SyntaxKind.ElementAccessExpression:
const elementAccessExpression = <ts.ElementAccessExpression>node;
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;
}
break;
}
}
return false;
}
/**
* Produce a JSON serialiable object representing `node`. The foldable values in the expression
* tree are folded. For example, a node representing `1 + 2` is folded into `3`.
*/
public evaluateNode(node: ts.Node): MetadataValue {
switch (node.kind) {
case ts.SyntaxKind.ObjectLiteralExpression:
let obj: MetadataValue = {};
let allPropertiesDefined = true;
ts.forEachChild(node, child => {
switch (child.kind) {
case ts.SyntaxKind.PropertyAssignment:
const assignment = <ts.PropertyAssignment>child;
const propertyName = this.nameOf(assignment.name);
const propertyValue = this.evaluateNode(assignment.initializer);
obj[propertyName] = propertyValue;
allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined;
}
});
if (allPropertiesDefined) return obj;
break;
case ts.SyntaxKind.ArrayLiteralExpression:
let arr = [];
let allElementsDefined = true;
ts.forEachChild(node, child => {
const value = this.evaluateNode(child);
arr.push(value);
allElementsDefined = isDefined(value) && allElementsDefined;
});
if (allElementsDefined) return arr;
break;
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) {
const arrowFunction = <ts.ArrowFunction>firstArgument;
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;
}
break;
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;
}
break;
case ts.SyntaxKind.PropertyAccessExpression: {
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
const expression = this.evaluateNode(propertyAccessExpression.expression);
const member = this.nameOf(propertyAccessExpression.name);
if (this.isFoldable(propertyAccessExpression.expression)) return expression[member];
if (this.findImportNamespace(propertyAccessExpression)) {
return this.nodeSymbolReference(propertyAccessExpression);
}
if (isDefined(expression)) {
return {__symbolic: "select", expression, member};
}
break;
}
case ts.SyntaxKind.ElementAccessExpression: {
const elementAccessExpression = <ts.ElementAccessExpression>node;
const expression = this.evaluateNode(elementAccessExpression.expression);
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
if (this.isFoldable(elementAccessExpression.expression) &&
this.isFoldable(elementAccessExpression.argumentExpression))
return expression[<string | number>index];
if (isDefined(expression) && isDefined(index)) {
return {__symbolic: "index", expression, index};
}
break;
}
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 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 this.nodeSymbolReference(node);
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.NullKeyword:
return null;
case ts.SyntaxKind.TrueKeyword:
return true;
case ts.SyntaxKind.FalseKeyword:
return false;
case ts.SyntaxKind.ParenthesizedExpression:
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
return this.evaluateNode(parenthesizedExpression.expression);
case ts.SyntaxKind.TypeAssertionExpression:
const typeAssertion = <ts.TypeAssertion>node;
return this.evaluateNode(typeAssertion.expression);
case ts.SyntaxKind.PrefixUnaryExpression:
const prefixUnaryExpression = <ts.PrefixUnaryExpression>node;
const operand = this.evaluateNode(prefixUnaryExpression.operand);
if (isDefined(operand) && isPrimitive(operand)) {
switch (prefixUnaryExpression.operator) {
case ts.SyntaxKind.PlusToken:
return +operand;
case ts.SyntaxKind.MinusToken:
return -operand;
case ts.SyntaxKind.TildeToken:
return ~operand;
case ts.SyntaxKind.ExclamationToken:
return !operand;
}
}
let operatorText: string;
switch (prefixUnaryExpression.operator) {
case ts.SyntaxKind.PlusToken:
operatorText = '+';
break;
case ts.SyntaxKind.MinusToken:
operatorText = '-';
break;
case ts.SyntaxKind.TildeToken:
operatorText = '~';
break;
case ts.SyntaxKind.ExclamationToken:
operatorText = '!';
break;
default:
return undefined;
}
return {__symbolic: "pre", operator: operatorText, operand: operand };
case ts.SyntaxKind.BinaryExpression:
const binaryExpression = <ts.BinaryExpression>node;
const left = this.evaluateNode(binaryExpression.left);
const right = this.evaluateNode(binaryExpression.right);
if (isDefined(left) && isDefined(right)) {
if (isPrimitive(left) && isPrimitive(right))
switch (binaryExpression.operatorToken.kind) {
case ts.SyntaxKind.BarBarToken:
return <any>left || <any>right;
case ts.SyntaxKind.AmpersandAmpersandToken:
return <any>left && <any>right;
case ts.SyntaxKind.AmpersandToken:
return <any>left & <any>right;
case ts.SyntaxKind.BarToken:
return <any>left | <any>right;
case ts.SyntaxKind.CaretToken:
return <any>left ^ <any>right;
case ts.SyntaxKind.EqualsEqualsToken:
return <any>left == <any>right;
case ts.SyntaxKind.ExclamationEqualsToken:
return <any>left != <any>right;
case ts.SyntaxKind.EqualsEqualsEqualsToken:
return <any>left === <any>right;
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
return <any>left !== <any>right;
case ts.SyntaxKind.LessThanToken:
return <any>left < <any>right;
case ts.SyntaxKind.GreaterThanToken:
return <any>left > <any>right;
case ts.SyntaxKind.LessThanEqualsToken:
return <any>left <= <any>right;
case ts.SyntaxKind.GreaterThanEqualsToken:
return <any>left >= <any>right;
case ts.SyntaxKind.LessThanLessThanToken:
return (<any>left) << (<any>right);
case ts.SyntaxKind.GreaterThanGreaterThanToken:
return <any>left >> <any>right;
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return <any>left >>> <any>right;
case ts.SyntaxKind.PlusToken:
return <any>left + <any>right;
case ts.SyntaxKind.MinusToken:
return <any>left - <any>right;
case ts.SyntaxKind.AsteriskToken:
return <any>left * <any>right;
case ts.SyntaxKind.SlashToken:
return <any>left / <any>right;
case ts.SyntaxKind.PercentToken:
return <any>left % <any>right;
}
return {
__symbolic: "binop",
operator: binaryExpression.operatorToken.getText(),
left: left,
right: right
};
}
break;
}
return undefined;
}
}

View File

@ -0,0 +1,138 @@
export interface ModuleMetadata {
__symbolic: "module";
metadata: {[name: string]: (ClassMetadata | MetadataValue)};
}
export function isModuleMetadata(value: any): value is ModuleMetadata {
return value && value.__symbolic === "module";
}
export interface ClassMetadata {
__symbolic: "class";
decorators?: MetadataSymbolicExpression[];
members?: MetadataMap;
}
export function isClassMetadata(value: any): value is ClassMetadata {
return value && value.__symbolic === "class";
}
export interface MetadataMap { [name: string]: MemberMetadata[]; }
export interface MemberMetadata {
__symbolic: "constructor" | "method" | "property";
decorators?: MetadataSymbolicExpression[];
}
export function isMemberMetadata(value: any): value is MemberMetadata {
if (value) {
switch (value.__symbolic) {
case "constructor":
case "method":
case "property":
return true;
}
}
return false;
}
export interface MethodMetadata extends MemberMetadata {
__symbolic: "constructor" | "method";
parameterDecorators?: MetadataSymbolicExpression[][];
}
export function isMethodMetadata(value: any): value is MemberMetadata {
return value && (value.__symbolic === "constructor" || value.__symbolic === "method");
}
export interface ConstructorMetadata extends MethodMetadata {
__symbolic: "constructor";
parameters?: MetadataSymbolicExpression[];
}
export function isConstructorMetadata(value: any): value is ConstructorMetadata {
return value && value.__symbolic === "constructor";
}
export type MetadataValue =
string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression;
export interface MetadataObject { [name: string]: MetadataValue; }
export interface MetadataArray { [name: number]: MetadataValue; }
export interface MetadataSymbolicExpression {
__symbolic: "binary" | "call" | "index" | "new" | "pre" | "reference" | "select"
}
export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression {
if (value) {
switch (value.__symbolic) {
case "binary":
case "call":
case "index":
case "new":
case "pre":
case "reference":
case "select":
return true;
}
}
return false;
}
export interface MetadataSymbolicBinaryExpression extends MetadataSymbolicExpression {
__symbolic: "binary";
operator: "&&" | "||" | "|" | "^" | "&" | "==" | "!=" | "===" | "!==" | "<" | ">" | "<=" | ">=" |
"instanceof" | "in" | "as" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "**";
left: MetadataValue;
right: MetadataValue;
}
export function isMetadataSymbolicBinaryExpression(
value: any): value is MetadataSymbolicBinaryExpression {
return value && value.__symbolic === "binary";
}
export interface MetadataSymbolicIndexExpression extends MetadataSymbolicExpression {
__symbolic: "index";
expression: MetadataValue;
index: MetadataValue;
}
export function isMetadataSymbolicIndexExpression(
value: any): value is MetadataSymbolicIndexExpression {
return value && value.__symbolic === "index";
}
export interface MetadataSymbolicCallExpression extends MetadataSymbolicExpression {
__symbolic: "call" | "new";
expression: MetadataValue;
arguments?: MetadataValue[];
}
export function isMetadataSymbolicCallExpression(
value: any): value is MetadataSymbolicCallExpression {
return value && (value.__symbolic === "call" || value.__symbolic === "new");
}
export interface MetadataSymbolicPrefixExpression extends MetadataSymbolicExpression {
__symbolic: "pre";
operator: "+" | "-" | "~" | "!";
operand: MetadataValue;
}
export function isMetadataSymbolicPrefixExpression(
value: any): value is MetadataSymbolicPrefixExpression {
return value && value.__symbolic === "pre";
}
export interface MetadataSymbolicReferenceExpression extends MetadataSymbolicExpression {
__symbolic: "reference";
name: string;
module: string;
}
export function isMetadataSymbolicReferenceExpression(
value: any): value is MetadataSymbolicReferenceExpression {
return value && value.__symbolic === "reference";
}
export interface MetadataSymbolicSelectExpression extends MetadataSymbolicExpression {
__symbolic: "select";
expression: MetadataValue;
name: string;
}
export function isMetadataSymbolicSelectExpression(
value: any): value is MetadataSymbolicSelectExpression {
return value && value.__symbolic === "select";
}

View File

@ -0,0 +1,34 @@
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;
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>();
public has(symbol: ts.Symbol): boolean { return this.map.has(symbol.getDeclarations()[0]); }
public set(symbol: ts.Symbol, value): void { this.map.set(symbol.getDeclarations()[0], value); }
public get(symbol: ts.Symbol): any { return this.map.get(symbol.getDeclarations()[0]); }
static empty: Symbols = new Symbols();
}

View File

@ -0,0 +1,469 @@
import * as ts from 'typescript';
import {MetadataCollector} from '../src/collector';
import {ClassMetadata} from '../src/schema';
import {Directory, expectValidSources, Host} from './typescript.mocks';
describe('Collector', () => {
let host: ts.LanguageServiceHost;
let service: ts.LanguageService;
let program: ts.Program;
let typeChecker: ts.TypeChecker;
let collector: MetadataCollector;
beforeEach(() => {
host = new Host(
FILES,
['/app/app.component.ts', '/app/cases-data.ts', '/app/cases-no-data.ts', '/promise.ts']);
service = ts.createLanguageService(host);
program = service.getProgram();
typeChecker = program.getTypeChecker();
collector = new MetadataCollector();
});
it('should not have errors in test data', () => { expectValidSources(service, program); });
it('should return undefined for modules that have no metadata', () => {
const sourceFile = program.getSourceFile('app/hero.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
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", () => {
const sourceFile = program.getSourceFile('app/hero-detail.component.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
metadata: {
HeroDetailComponent: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'},
arguments: [
{
selector: 'my-hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`
}
]
}
],
members: {
hero: [
{
__symbolic: 'property',
decorators: [
{
__symbolic: 'call',
expression:
{__symbolic: 'reference', name: 'Input', module: 'angular2/core'}
}
]
}
]
}
}
}
});
});
it("should be able to get a more complicated component's metadata", () => {
const sourceFile = program.getSourceFile('/app/app.component.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
metadata: {
AppComponent: {
__symbolic: 'class',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', name: 'Component', module: 'angular2/core'},
arguments: [
{
selector: 'my-app',
template: `
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id | lowercase}}</span> {{hero.name | uppercase}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
directives: [
{
__symbolic: 'reference',
name: 'HeroDetailComponent',
module: './hero-detail.component'
},
{__symbolic: 'reference', name: 'NgFor', module: 'angular2/common'}
],
providers: [{__symbolic: 'reference', name: undefined, module: './hero.service'}],
pipes: [
{__symbolic: 'reference', name: 'LowerCasePipe', module: 'angular2/common'},
{
__symbolic: 'reference',
name: 'UpperCasePipe',
module: 'angular2/common'
}
]
}
]
}
],
members: {
__ctor__: [
{
__symbolic: 'constructor',
parameters:
[{__symbolic: 'reference', name: undefined, module: './hero.service'}]
}
],
onSelect: [{__symbolic: 'method'}],
ngOnInit: [{__symbolic: 'method'}],
getHeroes: [{__symbolic: 'method'}]
}
}
}
});
});
it('should return the values of exported variables', () => {
const sourceFile = program.getSourceFile('/app/mock-heroes.ts');
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toEqual({
__symbolic: 'module',
metadata: {
HEROES: [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
]
}
});
});
it('should have no data produced for the no data cases', () => {
const sourceFile = program.getSourceFile('/app/cases-no-data.ts');
expect(sourceFile).toBeTruthy(sourceFile);
const metadata = collector.getMetadata(sourceFile, typeChecker);
expect(metadata).toBeFalsy();
});
let casesFile;
let casesMetadata;
beforeEach(() => {
casesFile = program.getSourceFile('/app/cases-data.ts');
casesMetadata = collector.getMetadata(casesFile, typeChecker);
});
it('should provide null for an any ctor pameter type', () => {
const casesAny = <ClassMetadata>casesMetadata.metadata['CaseAny'];
expect(casesAny).toBeTruthy();
const ctorData = casesAny.members['__ctor__'];
expect(ctorData).toEqual([{__symbolic: 'constructor', parameters: [null]}]);
});
it('should record annotations on set and get declarations', () => {
const propertyData = {
name: [
{
__symbolic: 'property',
decorators: [
{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Input'},
arguments: ['firstName']
}
]
}
]
};
const caseGetProp = <ClassMetadata>casesMetadata.metadata['GetProp'];
expect(caseGetProp.members).toEqual(propertyData);
const caseSetProp = <ClassMetadata>casesMetadata.metadata['SetProp'];
expect(caseSetProp.members).toEqual(propertyData);
const caseFullProp = <ClassMetadata>casesMetadata.metadata['FullProp'];
expect(caseFullProp.members).toEqual(propertyData);
});
});
// TODO: Do not use \` in a template literal as it confuses clang-format
const FILES: Directory = {
'app': {
'app.component.ts': `
import {Component as MyComponent, OnInit} from 'angular2/core';
import * as common from 'angular2/common';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import HeroService from './hero.service';
// thrown away
import 'angular2/core';
@MyComponent({
selector: 'my-app',
template:` + "`" + `
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id | lowercase}}</span> {{hero.name | uppercase}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
` +
"`" + `,
directives: [HeroDetailComponent, common.NgFor],
providers: [HeroService],
pipes: [common.LowerCasePipe, common.UpperCasePipe]
})
export class AppComponent implements OnInit {
public title = 'Tour of Heroes';
public heroes: Hero[];
public selectedHero: Hero;
constructor(private _heroService: HeroService) { }
onSelect(hero: Hero) { this.selectedHero = hero; }
ngOnInit() {
this.getHeroes()
}
getHeroes() {
this._heroService.getHeroesSlowly().then(heros => this.heroes = heros);
}
}`,
'hero.ts': `
export interface Hero {
id: number;
name: string;
}`,
'hero-detail.component.ts': `
import {Component, Input} from 'angular2/core';
import {Hero} from './hero';
@Component({
selector: 'my-hero-detail',
template: ` + "`" + `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
` + "`" + `,
})
export class HeroDetailComponent {
@Input() public hero: Hero;
}`,
'mock-heroes.ts': `
import {Hero as Hero} from './hero';
export const HEROES: Hero[] = [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
];`,
'default-exporter.ts': `
let a: string;
export default a;
`,
'hero.service.ts': `
import {Injectable} from 'angular2/core';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';
@Injectable()
class HeroService {
getHeros() {
return Promise.resolve(HEROES);
}
getHeroesSlowly() {
return new Promise<Hero[]>(resolve =>
setTimeout(()=>resolve(HEROES), 2000)); // 2 seconds
}
}
export default HeroService;`,
'cases-data.ts': `
import {Injectable, Input} from 'angular2/core';
@Injectable()
export class CaseAny {
constructor(param: any) {}
}
@Injectable()
export class GetProp {
private _name: string;
@Input('firstName') get name(): string {
return this._name;
}
}
@Injectable()
export class SetProp {
private _name: string;
@Input('firstName') set name(value: string) {
this._name = value;
}
}
@Injectable()
export class FullProp {
private _name: string;
@Input('firstName') get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
`,
'cases-no-data.ts': `
import HeroService from './hero.service';
export class CaseCtor {
constructor(private _heroService: HeroService) { }
}
`
},
'promise.ts': `
interface PromiseLike<T> {
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): PromiseLike<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): PromiseLike<TResult>;
}
interface Promise<T> {
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): Promise<TResult>;
catch(onrejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;
catch(onrejected?: (reason: any) => void): Promise<T>;
}
interface PromiseConstructor {
prototype: Promise<any>;
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
reject(reason: any): Promise<void>;
reject<T>(reason: any): Promise<T>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
}
declare var Promise: PromiseConstructor;
`,
'node_modules': {
'angular2': {
'core.d.ts': `
export interface Type extends Function { }
export interface TypeDecorator {
<T extends Type>(type: T): T;
(target: Object, propertyKey?: string | symbol, parameterIndex?: number): void;
annotations: any[];
}
export interface ComponentDecorator extends TypeDecorator { }
export interface ComponentFactory {
(obj: {
selector?: string;
inputs?: string[];
outputs?: string[];
properties?: string[];
events?: string[];
host?: {
[key: string]: string;
};
bindings?: any[];
providers?: any[];
exportAs?: string;
moduleId?: string;
queries?: {
[key: string]: any;
};
viewBindings?: any[];
viewProviders?: any[];
templateUrl?: string;
template?: string;
styleUrls?: string[];
styles?: string[];
directives?: Array<Type | any[]>;
pipes?: Array<Type | any[]>;
}): ComponentDecorator;
}
export declare var Component: ComponentFactory;
export interface InputFactory {
(bindingPropertyName?: string): any;
new (bindingPropertyName?: string): any;
}
export declare var Input: InputFactory;
export interface InjectableFactory {
(): any;
}
export declare var Injectable: InjectableFactory;
export interface OnInit {
ngOnInit(): any;
}
`,
'common.d.ts': `
export declare class NgFor {
ngForOf: any;
ngForTemplate: any;
ngDoCheck(): void;
}
export declare class LowerCasePipe {
transform(value: string, args?: any[]): string;
}
export declare class UpperCasePipe {
transform(value: string, args?: any[]): string;
}
`
}
}
};

View File

@ -0,0 +1,218 @@
import * as ts from 'typescript';
import * as fs from 'fs';
import {Directory, Host, expectNoDiagnostics, findVar} from './typescript.mocks';
import {Evaluator} from '../src/evaluator';
import {Symbols} from '../src/symbols';
describe('Evaluator', () => {
let host: ts.LanguageServiceHost;
let service: ts.LanguageService;
let program: ts.Program;
let typeChecker: ts.TypeChecker;
let symbols: Symbols;
let evaluator: Evaluator;
beforeEach(() => {
host = new Host(
FILES,
['expressions.ts', 'const_expr.ts', 'forwardRef.ts', 'classes.ts', 'newExpression.ts']);
service = ts.createLanguageService(host);
program = service.getProgram();
typeChecker = program.getTypeChecker();
symbols = new Symbols();
evaluator = new Evaluator(typeChecker, symbols, []);
});
it('should not have typescript errors in test data', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (const sourceFile of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(sourceFile.fileName));
}
});
it('should be able to fold literal expressions', () => {
var consts = program.getSourceFile('consts.ts');
expect(evaluator.isFoldable(findVar(consts, 'someName').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(consts, 'someBool').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(consts, 'one').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(consts, 'two').initializer)).toBeTruthy();
});
it('should be able to fold expressions with foldable references', () => {
var expressions = program.getSourceFile('expressions.ts');
expect(evaluator.isFoldable(findVar(expressions, 'three').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(expressions, 'four').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(expressions, 'obj').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(expressions, 'arr').initializer)).toBeTruthy();
});
it('should be able to evaluate literal expressions', () => {
var consts = program.getSourceFile('consts.ts');
expect(evaluator.evaluateNode(findVar(consts, 'someName').initializer)).toBe('some-name');
expect(evaluator.evaluateNode(findVar(consts, 'someBool').initializer)).toBe(true);
expect(evaluator.evaluateNode(findVar(consts, 'one').initializer)).toBe(1);
expect(evaluator.evaluateNode(findVar(consts, 'two').initializer)).toBe(2);
});
it('should be able to evaluate expressions', () => {
var expressions = program.getSourceFile('expressions.ts');
expect(evaluator.evaluateNode(findVar(expressions, 'three').initializer)).toBe(3);
expect(evaluator.evaluateNode(findVar(expressions, 'four').initializer)).toBe(4);
expect(evaluator.evaluateNode(findVar(expressions, 'obj').initializer))
.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, 'bTrue').initializer)).toEqual(true);
expect(evaluator.evaluateNode(findVar(expressions, 'bFalse').initializer)).toEqual(false);
expect(evaluator.evaluateNode(findVar(expressions, 'bAnd').initializer)).toEqual(true);
expect(evaluator.evaluateNode(findVar(expressions, 'bOr').initializer)).toEqual(true);
expect(evaluator.evaluateNode(findVar(expressions, 'nDiv').initializer)).toEqual(2);
expect(evaluator.evaluateNode(findVar(expressions, 'nMod').initializer)).toEqual(1);
expect(evaluator.evaluateNode(findVar(expressions, 'bLOr').initializer)).toEqual(false || true);
expect(evaluator.evaluateNode(findVar(expressions, 'bLAnd').initializer)).toEqual(true && true);
expect(evaluator.evaluateNode(findVar(expressions, 'bBOr').initializer)).toEqual(0x11 | 0x22);
expect(evaluator.evaluateNode(findVar(expressions, 'bBAnd').initializer)).toEqual(0x11 & 0x03);
expect(evaluator.evaluateNode(findVar(expressions, 'bXor').initializer)).toEqual(0x11 ^ 0x21);
expect(evaluator.evaluateNode(findVar(expressions, 'bEqual').initializer))
.toEqual(1 == <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bNotEqual').initializer))
.toEqual(1 != <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bIdentical').initializer))
.toEqual(1 === <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bNotIdentical').initializer))
.toEqual(1 !== <any>"1");
expect(evaluator.evaluateNode(findVar(expressions, 'bLessThan').initializer)).toEqual(1 < 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bGreaterThan').initializer)).toEqual(1 > 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bLessThanEqual').initializer))
.toEqual(1 <= 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bGreaterThanEqual').initializer))
.toEqual(1 >= 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bShiftLeft').initializer)).toEqual(1 << 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bShiftRight').initializer))
.toEqual(-1 >> 2);
expect(evaluator.evaluateNode(findVar(expressions, 'bShiftRightU').initializer))
.toEqual(-1 >>> 2);
});
it('should report recursive references as symbolic', () => {
var expressions = program.getSourceFile('expressions.ts');
expect(evaluator.evaluateNode(findVar(expressions, 'recursiveA').initializer))
.toEqual({__symbolic: "reference", name: "recursiveB", module: undefined});
expect(evaluator.evaluateNode(findVar(expressions, 'recursiveB').initializer))
.toEqual({__symbolic: "reference", name: "recursiveA", module: undefined});
});
it('should correctly handle special cases for CONST_EXPR', () => {
var const_expr = program.getSourceFile('const_expr.ts');
expect(evaluator.evaluateNode(findVar(const_expr, 'bTrue').initializer)).toEqual(true);
expect(evaluator.evaluateNode(findVar(const_expr, 'bFalse').initializer)).toEqual(false);
});
it('should resolve a forwardRef', () => {
var forwardRef = program.getSourceFile('forwardRef.ts');
expect(evaluator.evaluateNode(findVar(forwardRef, 'bTrue').initializer)).toEqual(true);
expect(evaluator.evaluateNode(findVar(forwardRef, 'bFalse').initializer)).toEqual(false);
});
it('should return new expressions', () => {
evaluator =
new Evaluator(typeChecker, symbols, [{from: './classes', namedImports: [{name: 'Value'}]}]);
var newExpression = program.getSourceFile('newExpression.ts');
expect(evaluator.evaluateNode(findVar(newExpression, 'someValue').initializer))
.toEqual({
__symbolic: "new",
expression: {__symbolic: "reference", name: "Value", module: "./classes"},
arguments: ["name", 12]
});
expect(evaluator.evaluateNode(findVar(newExpression, 'complex').initializer))
.toEqual({
__symbolic: "new",
expression: {__symbolic: "reference", name: "Value", module: "./classes"},
arguments: ["name", 12]
});
});
});
const FILES: Directory = {
'directives.ts': `
export function Pipe(options: { name?: string, pure?: boolean}) {
return function(fn: Function) { }
}
`,
'classes.ts': `
export class Value {
constructor(public name: string, public value: any) {}
}
`,
'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';
export var three = one + two;
export var four = two * two;
export var obj = { one: one, two: two, three: three, four: four };
export var arr = [one, two, three, four];
export var bTrue = someBool;
export var bFalse = !someBool;
export var bAnd = someBool && someBool;
export var bOr = someBool || someBool;
export var nDiv = four / two;
export var nMod = (four + one) % two;
export var bLOr = false || true; // true
export var bLAnd = true && true; // true
export var bBOr = 0x11 | 0x22; // 0x33
export var bBAnd = 0x11 & 0x03; // 0x01
export var bXor = 0x11 ^ 0x21; // 0x20
export var bEqual = 1 == <any>"1"; // true
export var bNotEqual = 1 != <any>"1"; // false
export var bIdentical = 1 === <any>"1"; // false
export var bNotIdentical = 1 !== <any>"1"; // true
export var bLessThan = 1 < 2; // true
export var bGreaterThan = 1 > 2; // false
export var bLessThanEqual = 1 <= 2; // true
export var bGreaterThanEqual = 1 >= 2; // false
export var bShiftLeft = 1 << 2; // 0x04
export var bShiftRight = -1 >> 2; // -1
export var bShiftRightU = -1 >>> 2; // 0x3fffffff
export var recursiveA = recursiveB;
export var recursiveB = recursiveA;
`,
'A.ts': `
import {Pipe} from './directives';
@Pipe({name: 'A', pure: false})
export class A {}`,
'B.ts': `
import {Pipe} from './directives';
import {someName, someBool} from './consts';
@Pipe({name: someName, pure: someBool})
export class B {}`,
'const_expr.ts': `
function CONST_EXPR(value: any) { return value; }
export var bTrue = CONST_EXPR(true);
export var bFalse = CONST_EXPR(false);
`,
'forwardRef.ts': `
function forwardRef(value: any) { return value; }
export var bTrue = forwardRef(() => true);
export var bFalse = forwardRef(() => false);
`,
'newExpression.ts': `
import {Value} from './classes';
function CONST_EXPR(value: any) { return value; }
function forwardRef(value: any) { return value; }
export const someValue = new Value("name", 12);
export const complex = CONST_EXPR(new Value("name", forwardRef(() => 12)));
`
};

View File

@ -0,0 +1,29 @@
import * as ts from 'typescript';
import {Symbols} from '../src/symbols';
import {MockSymbol, MockVariableDeclaration} from './typescript.mocks';
describe('Symbols', () => {
let symbols: Symbols;
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());
it('should be able to add a symbol', () => symbols.set(someSymbol, someValue));
beforeEach(() => symbols.set(someSymbol, someValue));
it('should be able to `has` a symbol', () => expect(symbols.has(someSymbol)).toBeTruthy());
it('should be able to `get` a symbol value',
() => expect(symbols.get(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',
() => expect(symbols.get(aliasSymbol)).toBe(someValue));
it('should be able to determine symbol is missing',
() => expect(symbols.has(missingSymbol)).toBeFalsy());
it('should return undefined from `get` for a missing symbol',
() => expect(symbols.get(missingSymbol)).toBeUndefined());
});

View File

@ -0,0 +1,153 @@
import * as path from 'path';
import * as fs from 'fs';
import * as ts from 'typescript';
export interface Directory { [name: string]: (Directory | string); }
export class Host implements ts.LanguageServiceHost {
constructor(private directory: Directory, private scripts: string[]) {}
getCompilationSettings(): ts.CompilerOptions {
return {
experimentalDecorators: true,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5
};
}
getScriptFileNames(): string[] { return this.scripts; }
getScriptVersion(fileName: string): string { return "1"; }
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
let content = this.getFileContent(fileName);
if (content) return ts.ScriptSnapshot.fromString(content);
}
getCurrentDirectory(): string { return '/'; }
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
private getFileContent(fileName: string): string {
const names = fileName.split(path.sep);
if (names[names.length - 1] === 'lib.d.ts') {
return fs.readFileSync(ts.getDefaultLibFilePath(this.getCompilationSettings()), 'utf8');
}
let current: Directory | string = this.directory;
if (names.length && names[0] === '') names.shift();
for (const name of names) {
if (!current || typeof current === 'string') return undefined;
current = current[name];
}
if (typeof current === 'string') return current;
}
}
export class MockNode implements ts.Node {
constructor(public kind: ts.SyntaxKind = ts.SyntaxKind.Identifier, public flags: ts.NodeFlags = 0,
public pos: number = 0, public end: number = 0) {}
getSourceFile(): ts.SourceFile { return null; }
getChildCount(sourceFile?: ts.SourceFile): number { return 0 }
getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node { return null; }
getChildren(sourceFile?: ts.SourceFile): ts.Node[] { return []; }
getStart(sourceFile?: ts.SourceFile): number { return 0; }
getFullStart(): number { return 0; }
getEnd(): number { return 0; }
getWidth(sourceFile?: ts.SourceFile): number { return 0; }
getFullWidth(): number { return 0; }
getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { return 0; }
getFullText(sourceFile?: ts.SourceFile): string { return ''; }
getText(sourceFile?: ts.SourceFile): string { return ''; }
getFirstToken(sourceFile?: ts.SourceFile): ts.Node { return null; }
getLastToken(sourceFile?: ts.SourceFile): ts.Node { return null; }
}
export class MockIdentifier extends MockNode implements ts.Identifier {
public text: string;
public _primaryExpressionBrand: any;
public _memberExpressionBrand: any;
public _leftHandSideExpressionBrand: any;
public _incrementExpressionBrand: any;
public _unaryExpressionBrand: any;
public _expressionBrand: any;
constructor(public name: string, kind: ts.SyntaxKind = ts.SyntaxKind.Identifier,
flags: ts.NodeFlags = 0, pos: number = 0, end: number = 0) {
super(kind, flags, pos, end);
this.text = name;
}
}
export class MockVariableDeclaration extends MockNode implements ts.VariableDeclaration {
public _declarationBrand: any;
constructor(public name: ts.Identifier, kind: ts.SyntaxKind = ts.SyntaxKind.VariableDeclaration,
flags: ts.NodeFlags = 0, pos: number = 0, end: number = 0) {
super(kind, flags, pos, end);
}
static of(name: string): MockVariableDeclaration {
return new MockVariableDeclaration(new MockIdentifier(name));
}
}
export class MockSymbol implements ts.Symbol {
constructor(public name: string, private node: ts.Declaration = MockVariableDeclaration.of(name),
public flags: ts.SymbolFlags = 0) {}
getFlags(): ts.SymbolFlags { return this.flags; }
getName(): string { return this.name; }
getDeclarations(): ts.Declaration[] { return [this.node]; }
getDocumentationComment(): ts.SymbolDisplayPart[] { return []; }
static of(name: string): MockSymbol { return new MockSymbol(name); }
}
export function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
for (const diagnostic of diagnostics) {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
}
expect(diagnostics.length).toBe(0);
}
export function expectValidSources(service: ts.LanguageService, program: ts.Program) {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (const sourceFile of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(sourceFile.fileName));
}
}
export function allChildren<T>(node: ts.Node, cb: (node: ts.Node) => T) {
return ts.forEachChild(node, child => {
const result = cb(node);
if (result) {
return result;
}
return allChildren(child, cb);
})
}
export function findClass(sourceFile: ts.SourceFile, name: string): ts.ClassDeclaration {
return ts.forEachChild(sourceFile,
node => isClass(node) && isNamed(node.name, name) ? node : undefined);
}
export function findVar(sourceFile: ts.SourceFile, name: string): ts.VariableDeclaration {
return allChildren(sourceFile,
node => isVar(node) && isNamed(node.name, name) ? node : undefined);
}
export function isClass(node: ts.Node): node is ts.ClassDeclaration {
return node.kind === ts.SyntaxKind.ClassDeclaration;
}
export function isNamed(node: ts.Node, name: string): node is ts.Identifier {
return node.kind === ts.SyntaxKind.Identifier && (<ts.Identifier>node).text === name;
}
export function isVar(node: ts.Node): node is ts.VariableDeclaration {
return node.kind === ts.SyntaxKind.VariableDeclaration;
}