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:
15
tools/ts-metadata-collector/README.md
Normal file
15
tools/ts-metadata-collector/README.md
Normal 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
|
||||
```
|
2
tools/ts-metadata-collector/index.ts
Normal file
2
tools/ts-metadata-collector/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './src/collector';
|
||||
export * from './src/schema';
|
16
tools/ts-metadata-collector/package.json
Normal file
16
tools/ts-metadata-collector/package.json
Normal 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"
|
||||
}
|
||||
}
|
199
tools/ts-metadata-collector/src/collector.ts
Normal file
199
tools/ts-metadata-collector/src/collector.ts
Normal 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};
|
||||
}
|
||||
}
|
469
tools/ts-metadata-collector/src/evaluator.ts
Normal file
469
tools/ts-metadata-collector/src/evaluator.ts
Normal 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;
|
||||
}
|
||||
}
|
138
tools/ts-metadata-collector/src/schema.ts
Normal file
138
tools/ts-metadata-collector/src/schema.ts
Normal 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";
|
||||
}
|
34
tools/ts-metadata-collector/src/symbols.ts
Normal file
34
tools/ts-metadata-collector/src/symbols.ts
Normal 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();
|
||||
}
|
469
tools/ts-metadata-collector/test/collector.spec.ts
Normal file
469
tools/ts-metadata-collector/test/collector.spec.ts
Normal 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;
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
};
|
218
tools/ts-metadata-collector/test/evaluator.spec.ts
Normal file
218
tools/ts-metadata-collector/test/evaluator.spec.ts
Normal 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)));
|
||||
`
|
||||
};
|
29
tools/ts-metadata-collector/test/symbols.spec.ts
Normal file
29
tools/ts-metadata-collector/test/symbols.spec.ts
Normal 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());
|
||||
});
|
153
tools/ts-metadata-collector/test/typescript.mocks.ts
Normal file
153
tools/ts-metadata-collector/test/typescript.mocks.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user