chore(refactor): Refactored metadata collector
Renamed MetadataExtractor to MetadataCollector Reorganized to split src from tests Closes #7492
This commit is contained in:
parent
3f57fa6e0e
commit
09f4d6f52d
|
@ -5,7 +5,7 @@ import fse = require('fs-extra');
|
|||
import path = require('path');
|
||||
import * as ts from 'typescript';
|
||||
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
|
||||
import {MetadataExtractor} from '../metadata/extractor';
|
||||
import {MetadataCollector} from '../metadata';
|
||||
|
||||
type FileRegistry = ts.Map<{version: number}>;
|
||||
|
||||
|
@ -50,7 +50,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
|
|||
private rootFilePaths: string[];
|
||||
private tsServiceHost: ts.LanguageServiceHost;
|
||||
private tsService: ts.LanguageService;
|
||||
private metadataExtractor: MetadataExtractor;
|
||||
private metadataCollector: MetadataCollector;
|
||||
private firstRun: boolean = true;
|
||||
private previousRunFailed: boolean = false;
|
||||
// Whether to generate the @internal typing files (they are only generated when `stripInternal` is
|
||||
|
@ -93,7 +93,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
|
|||
this.tsServiceHost = new CustomLanguageServiceHost(this.tsOpts, this.rootFilePaths,
|
||||
this.fileRegistry, this.inputPath);
|
||||
this.tsService = ts.createLanguageService(this.tsServiceHost, ts.createDocumentRegistry());
|
||||
this.metadataExtractor = new MetadataExtractor(this.tsService);
|
||||
this.metadataCollector = new MetadataCollector(this.tsService);
|
||||
}
|
||||
|
||||
|
||||
|
@ -265,7 +265,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
|
|||
private emitMetadata(dtsFileName: string, sourceFile: ts.SourceFile,
|
||||
typeChecker: ts.TypeChecker) {
|
||||
if (sourceFile) {
|
||||
const metadata = this.metadataExtractor.getMetadata(sourceFile, typeChecker);
|
||||
const metadata = this.metadataCollector.getMetadata(sourceFile, typeChecker);
|
||||
if (metadata && metadata.metadata) {
|
||||
const metadataText = JSON.stringify(metadata);
|
||||
const metadataFileName = dtsFileName.replace(/\.d.ts$/, '.metadata.json');
|
||||
|
|
|
@ -1,138 +0,0 @@
|
|||
var mockfs = require('mock-fs');
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import {MockHost, expectNoDiagnostics, findClass} from './typescript.mock';
|
||||
import {MetadataExtractor} from './extractor';
|
||||
|
||||
describe('MetadataExtractor', () => {
|
||||
// Read the lib.d.ts before mocking fs.
|
||||
let libTs: string = fs.readFileSync(ts.getDefaultLibFilePath({}), 'utf8');
|
||||
|
||||
beforeEach(() => files['lib.d.ts'] = libTs);
|
||||
beforeEach(() => mockfs(files));
|
||||
afterEach(() => mockfs.restore());
|
||||
|
||||
let host: ts.LanguageServiceHost;
|
||||
let service: ts.LanguageService;
|
||||
let program: ts.Program;
|
||||
let typeChecker: ts.TypeChecker;
|
||||
let extractor: MetadataExtractor;
|
||||
|
||||
beforeEach(() => {
|
||||
host = new MockHost(['A.ts', 'B.ts', 'C.ts'], /*currentDirectory*/ undefined, 'lib.d.ts');
|
||||
service = ts.createLanguageService(host);
|
||||
program = service.getProgram();
|
||||
typeChecker = program.getTypeChecker();
|
||||
extractor = new MetadataExtractor(service);
|
||||
});
|
||||
|
||||
it('should not have typescript errors in test data', () => {
|
||||
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
|
||||
}
|
||||
});
|
||||
|
||||
it('should be able to extract metadata when defined by literals', () => {
|
||||
const sourceFile = program.getSourceFile('A.ts');
|
||||
const metadata = extractor.getMetadata(sourceFile, typeChecker);
|
||||
expect(metadata).toEqual({
|
||||
__symbolic: 'module',
|
||||
module: './A',
|
||||
metadata: {
|
||||
A: {
|
||||
__symbolic: 'class',
|
||||
decorators: [
|
||||
{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', name: 'Pipe', module: './directives'},
|
||||
arguments: [{name: 'A', pure: false}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to extract metadata from metadata defined using vars', () => {
|
||||
const sourceFile = program.getSourceFile('B.ts');
|
||||
const metadata = extractor.getMetadata(sourceFile, typeChecker);
|
||||
expect(metadata).toEqual({
|
||||
__symbolic: 'module',
|
||||
module: './B',
|
||||
metadata: {
|
||||
B: {
|
||||
__symbolic: 'class',
|
||||
decorators: [
|
||||
{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', name: 'Pipe', module: './directives'},
|
||||
arguments: [{name: 'some-name', pure: true}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('souce be able to extract metadata that uses external references', () => {
|
||||
const sourceFile = program.getSourceFile('C.ts');
|
||||
const metadata = extractor.getMetadata(sourceFile, typeChecker);
|
||||
expect(metadata).toEqual({
|
||||
__symbolic: 'module',
|
||||
module: './C',
|
||||
metadata: {
|
||||
B: {
|
||||
__symbolic: 'class',
|
||||
decorators: [
|
||||
{
|
||||
__symbolic: 'call',
|
||||
expression: {__symbolic: 'reference', name: 'Pipe', module: './directives'},
|
||||
arguments: [
|
||||
{
|
||||
name: {__symbolic: "reference", module: "./external", name: "externalName"},
|
||||
pure:
|
||||
{__symbolic: "reference", module: "./external", name: "externalBool"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const files = {
|
||||
'directives.ts': `
|
||||
export function Pipe(options: { name?: string, pure?: boolean}) {
|
||||
return function(fn: Function) { }
|
||||
}
|
||||
`,
|
||||
'consts.ts': `
|
||||
export var someName = 'some-name';
|
||||
export var someBool = true;
|
||||
`,
|
||||
'external.d.ts': `
|
||||
export const externalName: string;
|
||||
export const externalBool: boolean;
|
||||
`,
|
||||
'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 {}`,
|
||||
'C.ts': `
|
||||
import {Pipe} from './directives';
|
||||
import {externalName, externalBool} from './external';
|
||||
|
||||
@Pipe({name: externalName, pure: externalBool})
|
||||
export class B {}`
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import * as ts from 'typescript';
|
||||
import {Evaluator} from './evaluator';
|
||||
import {Symbols} from './symbols';
|
||||
import * as path from 'path';
|
||||
|
||||
const EXT_REGEX = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||
const NODE_MODULES = '/node_modules/';
|
||||
const NODE_MODULES_PREFIX = 'node_modules/';
|
||||
|
||||
function pathTo(from: string, to: string): string {
|
||||
var result = path.relative(path.dirname(from), to);
|
||||
if (path.dirname(result) === '.') {
|
||||
result = '.' + path.sep + result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function moduleNameFromBaseName(moduleFileName: string, baseFileName: string): string {
|
||||
// Remove the extension
|
||||
moduleFileName = moduleFileName.replace(EXT_REGEX, '');
|
||||
|
||||
// Check for node_modules
|
||||
const nodeModulesIndex = moduleFileName.lastIndexOf(NODE_MODULES);
|
||||
if (nodeModulesIndex >= 0) {
|
||||
return moduleFileName.substr(nodeModulesIndex + NODE_MODULES.length);
|
||||
}
|
||||
if (moduleFileName.lastIndexOf(NODE_MODULES_PREFIX, NODE_MODULES_PREFIX.length) !== -1) {
|
||||
return moduleFileName.substr(NODE_MODULES_PREFIX.length);
|
||||
}
|
||||
|
||||
// Construct a simplified path from the file to the module
|
||||
return pathTo(baseFileName, moduleFileName);
|
||||
}
|
||||
|
||||
// TODO: Support cross-module folding
|
||||
export class MetadataExtractor {
|
||||
constructor(private service: ts.LanguageService) {}
|
||||
|
||||
/**
|
||||
* 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): any {
|
||||
const locals = new Symbols();
|
||||
const moduleNameOf = (fileName: string) =>
|
||||
moduleNameFromBaseName(fileName, sourceFile.fileName);
|
||||
const evaluator = new Evaluator(this.service, typeChecker, locals, moduleNameOf);
|
||||
|
||||
function objFromDecorator(decoratorNode: ts.Decorator): any {
|
||||
return evaluator.evaluateNode(decoratorNode.expression);
|
||||
}
|
||||
|
||||
function classWithDecorators(classDeclaration: ts.ClassDeclaration): any {
|
||||
return {
|
||||
__symbolic: "class",
|
||||
decorators: classDeclaration.decorators.map(decorator => objFromDecorator(decorator))
|
||||
};
|
||||
}
|
||||
|
||||
let metadata: any;
|
||||
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] = classWithDecorators(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", module: moduleNameOf(sourceFile.fileName), metadata};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './src/collector';
|
||||
export * from './src/schema';
|
|
@ -0,0 +1,199 @@
|
|||
import * as ts from 'typescript';
|
||||
import {Evaluator} from './evaluator';
|
||||
import {Symbols} from './symbols';
|
||||
import {
|
||||
ClassMetadata,
|
||||
ConstructorMetadata,
|
||||
ModuleMetadata,
|
||||
MemberMetadata,
|
||||
MetadataMap,
|
||||
MetadataSymbolicExpression,
|
||||
MetadataSymbolicReferenceExpression,
|
||||
MetadataValue,
|
||||
MethodMetadata
|
||||
} from './schema';
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
const EXT_REGEX = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||
const NODE_MODULES = '/node_modules/';
|
||||
const NODE_MODULES_PREFIX = 'node_modules/';
|
||||
|
||||
function pathTo(from: string, to: string): string {
|
||||
var result = path.relative(path.dirname(from), to);
|
||||
if (path.dirname(result) === '.') {
|
||||
result = '.' + path.sep + result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function moduleNameFromBaseName(moduleFileName: string, baseFileName: string): string {
|
||||
// Remove the extension
|
||||
moduleFileName = moduleFileName.replace(EXT_REGEX, '');
|
||||
|
||||
// Check for node_modules
|
||||
const nodeModulesIndex = moduleFileName.lastIndexOf(NODE_MODULES);
|
||||
if (nodeModulesIndex >= 0) {
|
||||
return moduleFileName.substr(nodeModulesIndex + NODE_MODULES.length);
|
||||
}
|
||||
if (moduleFileName.lastIndexOf(NODE_MODULES_PREFIX, NODE_MODULES_PREFIX.length) !== -1) {
|
||||
return moduleFileName.substr(NODE_MODULES_PREFIX.length);
|
||||
}
|
||||
|
||||
// Construct a simplified path from the file to the module
|
||||
return pathTo(baseFileName, moduleFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect decorator metadata from a TypeScript module.
|
||||
*/
|
||||
export class MetadataCollector {
|
||||
constructor(private service: ts.LanguageService) {}
|
||||
|
||||
/**
|
||||
* 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 moduleNameOf = (fileName: string) =>
|
||||
moduleNameFromBaseName(fileName, sourceFile.fileName);
|
||||
const evaluator = new Evaluator(this.service, typeChecker, locals, moduleNameOf);
|
||||
|
||||
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) {
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
if (symbol.declarations.length) {
|
||||
const declaration = symbol.declarations[0];
|
||||
const sourceFile = declaration.getSourceFile();
|
||||
return {
|
||||
__symbolic: "reference",
|
||||
module: moduleNameOf(sourceFile.fileName),
|
||||
name: symbol.name
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (methodDecorators || hasDecoratorData || hasParameterData) {
|
||||
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:
|
||||
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", module: moduleNameOf(sourceFile.fileName), metadata};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
import * as ts from 'typescript';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
import {
|
||||
MetadataValue,
|
||||
MetadataObject,
|
||||
MetadataSymbolicCallExpression,
|
||||
MetadataSymbolicReferenceExpression
|
||||
} from './schema';
|
||||
|
||||
// TOOD: Remove when tools directory is upgraded to support es6 target
|
||||
interface Map<K, V> {
|
||||
has(k: K): boolean;
|
||||
|
@ -43,12 +50,6 @@ function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
|
|||
return !ts.forEachChild(node, node => !cb(node));
|
||||
}
|
||||
|
||||
export interface SymbolReference {
|
||||
__symbolic: string; // TODO: Change this to type "reference" when we move to TypeScript 1.8
|
||||
name: string;
|
||||
module: string;
|
||||
}
|
||||
|
||||
function isPrimitive(value: any): boolean {
|
||||
return Object(value) !== value;
|
||||
}
|
||||
|
@ -82,7 +83,7 @@ export class Evaluator {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
private symbolReference(symbol: ts.Symbol): SymbolReference {
|
||||
private symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression {
|
||||
if (symbol) {
|
||||
const name = symbol.name;
|
||||
const module = this.moduleNameOf(this.symbolFileName(symbol));
|
||||
|
@ -90,7 +91,7 @@ export class Evaluator {
|
|||
}
|
||||
}
|
||||
|
||||
private nodeSymbolReference(node: ts.Node): SymbolReference {
|
||||
private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression {
|
||||
return this.symbolReference(this.typeChecker.getSymbolAtLocation(node));
|
||||
}
|
||||
|
||||
|
@ -98,7 +99,7 @@ export class Evaluator {
|
|||
if (node.kind == ts.SyntaxKind.Identifier) {
|
||||
return (<ts.Identifier>node).text;
|
||||
}
|
||||
return this.evaluateNode(node);
|
||||
return <string>this.evaluateNode(node);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -213,10 +214,10 @@ export class Evaluator {
|
|||
* 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): any {
|
||||
public evaluateNode(node: ts.Node): MetadataValue {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||
let obj = {};
|
||||
let obj: MetadataValue = {};
|
||||
let allPropertiesDefined = true;
|
||||
ts.forEachChild(node, child => {
|
||||
switch (child.kind) {
|
||||
|
@ -245,7 +246,7 @@ export class Evaluator {
|
|||
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
|
||||
if (this.isFoldable(callExpression)) {
|
||||
if (isMethodCallOf(callExpression, "concat")) {
|
||||
const arrayValue = this.evaluateNode(
|
||||
const arrayValue = <MetadataValue[]>this.evaluateNode(
|
||||
(<ts.PropertyAccessExpression>callExpression.expression).expression);
|
||||
return arrayValue.concat(args[0]);
|
||||
}
|
||||
|
@ -256,11 +257,14 @@ export class Evaluator {
|
|||
}
|
||||
const expression = this.evaluateNode(callExpression.expression);
|
||||
if (isDefined(expression) && args.every(isDefined)) {
|
||||
return {
|
||||
const result: MetadataSymbolicCallExpression = {
|
||||
__symbolic: "call",
|
||||
expression: this.evaluateNode(callExpression.expression),
|
||||
arguments: args
|
||||
expression: this.evaluateNode(callExpression.expression)
|
||||
};
|
||||
if (args && args.length) {
|
||||
result.arguments = args;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.PropertyAccessExpression: {
|
||||
|
@ -279,13 +283,9 @@ export class Evaluator {
|
|||
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
|
||||
if (this.isFoldable(elementAccessExpression.expression) &&
|
||||
this.isFoldable(elementAccessExpression.argumentExpression))
|
||||
return expression[index];
|
||||
return expression[<string | number>index];
|
||||
if (isDefined(expression) && isDefined(index)) {
|
||||
return {
|
||||
__symbolic: "index",
|
||||
expression,
|
||||
index: this.evaluateNode(elementAccessExpression.argumentExpression)
|
||||
};
|
||||
return {__symbolic: "index", expression, index};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -317,6 +317,9 @@ export class Evaluator {
|
|||
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);
|
||||
|
@ -357,20 +360,48 @@ export class Evaluator {
|
|||
if (isDefined(left) && isDefined(right)) {
|
||||
if (isPrimitive(left) && isPrimitive(right))
|
||||
switch (binaryExpression.operatorToken.kind) {
|
||||
case ts.SyntaxKind.PlusToken:
|
||||
return left + right;
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
return left - right;
|
||||
case ts.SyntaxKind.AsteriskToken:
|
||||
return left * right;
|
||||
case ts.SyntaxKind.SlashToken:
|
||||
return left / right;
|
||||
case ts.SyntaxKind.PercentToken:
|
||||
return left % right;
|
||||
case ts.SyntaxKind.AmpersandAmpersandToken:
|
||||
return left && right;
|
||||
case ts.SyntaxKind.BarBarToken:
|
||||
return left || right;
|
||||
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",
|
|
@ -0,0 +1,141 @@
|
|||
// TODO: fix typings for __symbolic once angular moves to 1.8
|
||||
|
||||
export interface ModuleMetadata {
|
||||
__symbolic: string; // "module";
|
||||
module: string;
|
||||
metadata: {[name: string]: (ClassMetadata | MetadataValue)};
|
||||
}
|
||||
export function isModuleMetadata(value: any): value is ModuleMetadata {
|
||||
return value && value.__symbolic === "module";
|
||||
}
|
||||
|
||||
export interface ClassMetadata {
|
||||
__symbolic: string; // "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: string; // "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: string; // "binary" | "call" | "index" | "pre" | "reference" | "select"
|
||||
}
|
||||
export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression {
|
||||
if (value) {
|
||||
switch (value.__symbolic) {
|
||||
case "binary":
|
||||
case "call":
|
||||
case "index":
|
||||
case "pre":
|
||||
case "reference":
|
||||
case "select":
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicBinaryExpression extends MetadataSymbolicExpression {
|
||||
// __symbolic: "binary";
|
||||
operator: string; // "&&" | "||" | "|" | "^" | "&" | "==" | "!=" | "===" | "!==" | "<" | ">" |
|
||||
// "<=" | ">=" | "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";
|
||||
expression: MetadataValue;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataSymbolicCallExpression(
|
||||
value: any): value is MetadataSymbolicCallExpression {
|
||||
return value && value.__symbolic === "call";
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicPrefixExpression extends MetadataSymbolicExpression {
|
||||
// __symbolic: "pre";
|
||||
operator: string; // "+" | "-" | "~" | "!";
|
||||
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";
|
||||
}
|
|
@ -0,0 +1,394 @@
|
|||
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(service);
|
||||
});
|
||||
|
||||
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 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',
|
||||
module: './hero-detail.component',
|
||||
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',
|
||||
module: './app.component',
|
||||
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: 'HeroService', 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', module: './hero.service', name: 'HeroService'}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
module: './mock-heroes',
|
||||
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();
|
||||
});
|
||||
|
||||
it('should provide null for an any ctor pameter type', () => {
|
||||
const sourceFile = program.getSourceFile('/app/cases-data.ts');
|
||||
const metadata = collector.getMetadata(sourceFile, typeChecker);
|
||||
expect(metadata).toBeTruthy();
|
||||
const casesAny = <ClassMetadata>metadata.metadata['CaseAny'];
|
||||
expect(casesAny).toBeTruthy();
|
||||
const ctorData = casesAny.members['__ctor__'];
|
||||
expect(ctorData).toEqual([{__symbolic: 'constructor', parameters: [null]}]);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Do not use \` in a template literal as it confuses clang-format
|
||||
const FILES: Directory = {
|
||||
'app': {
|
||||
'app.component.ts': `
|
||||
import {Component, OnInit} from 'angular2/core';
|
||||
import {NgFor, LowerCasePipe, UpperCasePipe} from 'angular2/common';
|
||||
import {Hero} from './hero';
|
||||
import {HeroDetailComponent} from './hero-detail.component';
|
||||
import {HeroService} from './hero.service';
|
||||
|
||||
@Component({
|
||||
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, NgFor],
|
||||
providers: [HeroService],
|
||||
pipes: [LowerCasePipe, 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} 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"}
|
||||
];`,
|
||||
'hero.service.ts': `
|
||||
import {Injectable} from 'angular2/core';
|
||||
import {HEROES} from './mock-heroes';
|
||||
import {Hero} from './hero';
|
||||
|
||||
@Injectable()
|
||||
export class HeroService {
|
||||
getHeros() {
|
||||
return Promise.resolve(HEROES);
|
||||
}
|
||||
|
||||
getHeroesSlowly() {
|
||||
return new Promise<Hero[]>(resolve =>
|
||||
setTimeout(()=>resolve(HEROES), 2000)); // 2 seconds
|
||||
}
|
||||
}`,
|
||||
'cases-data.ts': `
|
||||
import {Injectable} from 'angular2/core';
|
||||
|
||||
@Injectable()
|
||||
export class CaseAny {
|
||||
constructor(param: any) {}
|
||||
}
|
||||
`,
|
||||
'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;
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,19 +1,10 @@
|
|||
var mockfs = require('mock-fs');
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import {MockHost, expectNoDiagnostics, findVar} from './typescript.mock';
|
||||
import {Evaluator} from './evaluator';
|
||||
import {Symbols} from './symbols';
|
||||
import {Directory, Host, expectNoDiagnostics, findVar} from './typescript.mocks';
|
||||
import {Evaluator} from '../src/evaluator';
|
||||
import {Symbols} from '../src/symbols';
|
||||
|
||||
describe('Evaluator', () => {
|
||||
// Read the lib.d.ts before mocking fs.
|
||||
let libTs: string = fs.readFileSync(ts.getDefaultLibFilePath({}), 'utf8');
|
||||
|
||||
beforeEach(() => files['lib.d.ts'] = libTs);
|
||||
beforeEach(() => mockfs(files));
|
||||
afterEach(() => mockfs.restore());
|
||||
|
||||
let host: ts.LanguageServiceHost;
|
||||
let service: ts.LanguageService;
|
||||
let program: ts.Program;
|
||||
|
@ -22,7 +13,7 @@ describe('Evaluator', () => {
|
|||
let evaluator: Evaluator;
|
||||
|
||||
beforeEach(() => {
|
||||
host = new MockHost(['expressions.ts'], /*currentDirectory*/ undefined, 'lib.d.ts');
|
||||
host = new Host(FILES, ['expressions.ts']);
|
||||
service = ts.createLanguageService(host);
|
||||
program = service.getProgram();
|
||||
typeChecker = program.getTypeChecker();
|
||||
|
@ -74,6 +65,33 @@ describe('Evaluator', () => {
|
|||
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', () => {
|
||||
|
@ -85,7 +103,7 @@ describe('Evaluator', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const files = {
|
||||
const FILES: Directory = {
|
||||
'directives.ts': `
|
||||
export function Pipe(options: { name?: string, pure?: boolean}) {
|
||||
return function(fn: Function) { }
|
||||
|
@ -111,6 +129,23 @@ const files = {
|
|||
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;
|
||||
`,
|
|
@ -1,9 +1,6 @@
|
|||
/// <reference path="../typings/node/node.d.ts" />
|
||||
/// <reference path="../typings/jasmine/jasmine.d.ts" />
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {Symbols} from './symbols';
|
||||
import {MockSymbol, MockVariableDeclaration} from './typescript.mock';
|
||||
import {Symbols} from '../src/symbols';
|
||||
import {MockSymbol, MockVariableDeclaration} from './typescript.mocks';
|
||||
|
||||
describe('Symbols', () => {
|
||||
let symbols: Symbols;
|
|
@ -1,35 +1,45 @@
|
|||
import * as ts from 'typescript';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* A mock language service host that assumes mock-fs is used for the file system.
|
||||
*/
|
||||
export class MockHost implements ts.LanguageServiceHost {
|
||||
constructor(private fileNames: string[], private currentDirectory: string = process.cwd(),
|
||||
private libName?: string) {}
|
||||
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,
|
||||
modules: ts.ModuleKind.CommonJS,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES5
|
||||
};
|
||||
}
|
||||
|
||||
getScriptFileNames(): string[] { return this.fileNames; }
|
||||
getScriptFileNames(): string[] { return this.scripts; }
|
||||
|
||||
getScriptVersion(fileName: string): string { return "1"; }
|
||||
|
||||
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
|
||||
if (fs.existsSync(fileName)) {
|
||||
return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName, 'utf8'))
|
||||
}
|
||||
let content = this.getFileContent(fileName);
|
||||
if (content) return ts.ScriptSnapshot.fromString(content);
|
||||
}
|
||||
|
||||
getCurrentDirectory(): string { return this.currentDirectory; }
|
||||
getCurrentDirectory(): string { return '/'; }
|
||||
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return this.libName || ts.getDefaultLibFilePath(options);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,6 +112,14 @@ export function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
|
|||
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);
|
||||
|
@ -112,18 +130,14 @@ export function allChildren<T>(node: ts.Node, cb: (node: ts.Node) => T) {
|
|||
})
|
||||
}
|
||||
|
||||
export function findVar(sourceFile: ts.SourceFile, name: string): ts.VariableDeclaration {
|
||||
return allChildren(sourceFile,
|
||||
node => isVar(node) && isNamed(node.name, name) ? node : undefined);
|
||||
}
|
||||
|
||||
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 isVar(node: ts.Node): node is ts.VariableDeclaration {
|
||||
return node.kind === ts.SyntaxKind.VariableDeclaration;
|
||||
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 {
|
||||
|
@ -133,3 +147,7 @@ export function isClass(node: ts.Node): node is ts.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;
|
||||
}
|
Loading…
Reference in New Issue