From 09f4d6f52d86638948d1584a9f691e2fc1b12035 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 22 Mar 2016 17:11:42 -0700 Subject: [PATCH] chore(refactor): Refactored metadata collector Renamed MetadataExtractor to MetadataCollector Reorganized to split src from tests Closes #7492 --- tools/broccoli/broccoli-typescript.ts | 8 +- tools/metadata/extractor.spec.ts | 138 ------ tools/metadata/extractor.ts | 92 ---- tools/metadata/index.ts | 2 + tools/metadata/src/collector.ts | 199 +++++++++ tools/metadata/{ => src}/evaluator.ts | 99 +++-- tools/metadata/src/schema.ts | 141 +++++++ tools/metadata/{ => src}/symbols.ts | 0 tools/metadata/test/collector.spec.ts | 394 ++++++++++++++++++ tools/metadata/{ => test}/evaluator.spec.ts | 63 ++- tools/metadata/{ => test}/symbols.spec.ts | 7 +- .../typescript.mocks.ts} | 62 ++- 12 files changed, 896 insertions(+), 309 deletions(-) delete mode 100644 tools/metadata/extractor.spec.ts delete mode 100644 tools/metadata/extractor.ts create mode 100644 tools/metadata/index.ts create mode 100644 tools/metadata/src/collector.ts rename tools/metadata/{ => src}/evaluator.ts (84%) create mode 100644 tools/metadata/src/schema.ts rename tools/metadata/{ => src}/symbols.ts (100%) create mode 100644 tools/metadata/test/collector.spec.ts rename tools/metadata/{ => test}/evaluator.spec.ts (63%) rename tools/metadata/{ => test}/symbols.spec.ts (89%) rename tools/metadata/{typescript.mock.ts => test/typescript.mocks.ts} (74%) diff --git a/tools/broccoli/broccoli-typescript.ts b/tools/broccoli/broccoli-typescript.ts index b09847555c..f2528231c0 100644 --- a/tools/broccoli/broccoli-typescript.ts +++ b/tools/broccoli/broccoli-typescript.ts @@ -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'); diff --git a/tools/metadata/extractor.spec.ts b/tools/metadata/extractor.spec.ts deleted file mode 100644 index 555d341d80..0000000000 --- a/tools/metadata/extractor.spec.ts +++ /dev/null @@ -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 {}` -} diff --git a/tools/metadata/extractor.ts b/tools/metadata/extractor.ts deleted file mode 100644 index 026f7722d4..0000000000 --- a/tools/metadata/extractor.ts +++ /dev/null @@ -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 = declaration; - if (classDeclaration.decorators) { - if (!metadata) metadata = {}; - metadata[classDeclaration.name.text] = classWithDecorators(classDeclaration) - } - break; - case ts.SyntaxKind.VariableDeclaration: - const 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}; - } -} diff --git a/tools/metadata/index.ts b/tools/metadata/index.ts new file mode 100644 index 0000000000..5b41d0dcd3 --- /dev/null +++ b/tools/metadata/index.ts @@ -0,0 +1,2 @@ +export * from './src/collector'; +export * from './src/schema'; diff --git a/tools/metadata/src/collector.ts b/tools/metadata/src/collector.ts new file mode 100644 index 0000000000..b948328a7a --- /dev/null +++ b/tools/metadata/src/collector.ts @@ -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 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 = 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) { + (data).parameters = parametersData; + } + recordMember(name, data); + } + break; + case ts.SyntaxKind.PropertyDeclaration: + const property = 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 = declaration; + if (classDeclaration.decorators) { + if (!metadata) metadata = {}; + metadata[classDeclaration.name.text] = classMetadataOf(classDeclaration) + } + break; + case ts.SyntaxKind.VariableDeclaration: + const 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}; + } +} diff --git a/tools/metadata/evaluator.ts b/tools/metadata/src/evaluator.ts similarity index 84% rename from tools/metadata/evaluator.ts rename to tools/metadata/src/evaluator.ts index 7b31090778..f80c1ce142 100644 --- a/tools/metadata/evaluator.ts +++ b/tools/metadata/src/evaluator.ts @@ -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 { 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 (node).text; } - return this.evaluateNode(node); + return 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 = this.evaluateNode( (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[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 = node; return this.evaluateNode(parenthesizedExpression.expression); + case ts.SyntaxKind.TypeAssertionExpression: + const typeAssertion = node; + return this.evaluateNode(typeAssertion.expression); case ts.SyntaxKind.PrefixUnaryExpression: const 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 left || right; + case ts.SyntaxKind.AmpersandAmpersandToken: + return left && right; + case ts.SyntaxKind.AmpersandToken: + return left & right; + case ts.SyntaxKind.BarToken: + return left | right; + case ts.SyntaxKind.CaretToken: + return left ^ right; + case ts.SyntaxKind.EqualsEqualsToken: + return left == right; + case ts.SyntaxKind.ExclamationEqualsToken: + return left != right; + case ts.SyntaxKind.EqualsEqualsEqualsToken: + return left === right; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return left !== right; + case ts.SyntaxKind.LessThanToken: + return left < right; + case ts.SyntaxKind.GreaterThanToken: + return left > right; + case ts.SyntaxKind.LessThanEqualsToken: + return left <= right; + case ts.SyntaxKind.GreaterThanEqualsToken: + return left >= right; + case ts.SyntaxKind.LessThanLessThanToken: + return (left) << (right); + case ts.SyntaxKind.GreaterThanGreaterThanToken: + return left >> right; + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return left >>> right; + 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; } return { __symbolic: "binop", diff --git a/tools/metadata/src/schema.ts b/tools/metadata/src/schema.ts new file mode 100644 index 0000000000..746c4d6d98 --- /dev/null +++ b/tools/metadata/src/schema.ts @@ -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"; +} diff --git a/tools/metadata/symbols.ts b/tools/metadata/src/symbols.ts similarity index 100% rename from tools/metadata/symbols.ts rename to tools/metadata/src/symbols.ts diff --git a/tools/metadata/test/collector.spec.ts b/tools/metadata/test/collector.spec.ts new file mode 100644 index 0000000000..63ccf60492 --- /dev/null +++ b/tools/metadata/test/collector.spec.ts @@ -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: ` +
+

{{hero.name}} details!

+
{{hero.id}}
+
+ + +
+
+ ` + } + ] + } + ], + 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: ` +

My Heroes

+
    +
  • + {{hero.id | lowercase}} {{hero.name | uppercase}} +
  • +
+ + `, + 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 = 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:` + "`" + ` +

My Heroes

+
    +
  • + {{hero.id | lowercase}} {{hero.name | uppercase}} +
  • +
+ + ` + + "`" + `, + 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: ` + "`" + ` +
+

{{hero.name}} details!

+
{{hero.id}}
+
+ + +
+
+ ` + "`" + `, + }) + 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(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 { + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => TResult | PromiseLike): PromiseLike; + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => void): PromiseLike; + } + + interface Promise { + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => TResult | PromiseLike): Promise; + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => void): Promise; + catch(onrejected?: (reason: any) => T | PromiseLike): Promise; + catch(onrejected?: (reason: any) => void): Promise; + } + + interface PromiseConstructor { + prototype: Promise; + new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; + reject(reason: any): Promise; + reject(reason: any): Promise; + resolve(value: T | PromiseLike): Promise; + resolve(): Promise; + } + + declare var Promise: PromiseConstructor; + `, + + 'node_modules': { + 'angular2': { + 'core.d.ts': ` + export interface Type extends Function { } + export interface TypeDecorator { + (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; + pipes?: Array; + }): 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; + } + ` + } + } +}; diff --git a/tools/metadata/evaluator.spec.ts b/tools/metadata/test/evaluator.spec.ts similarity index 63% rename from tools/metadata/evaluator.spec.ts rename to tools/metadata/test/evaluator.spec.ts index 63d493d398..accc44667e 100644 --- a/tools/metadata/evaluator.spec.ts +++ b/tools/metadata/test/evaluator.spec.ts @@ -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 == "1"); + expect(evaluator.evaluateNode(findVar(expressions, 'bNotEqual').initializer)) + .toEqual(1 != "1"); + expect(evaluator.evaluateNode(findVar(expressions, 'bIdentical').initializer)) + .toEqual(1 === "1"); + expect(evaluator.evaluateNode(findVar(expressions, 'bNotIdentical').initializer)) + .toEqual(1 !== "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 == "1"; // true + export var bNotEqual = 1 != "1"; // false + export var bIdentical = 1 === "1"; // false + export var bNotIdentical = 1 !== "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; `, diff --git a/tools/metadata/symbols.spec.ts b/tools/metadata/test/symbols.spec.ts similarity index 89% rename from tools/metadata/symbols.spec.ts rename to tools/metadata/test/symbols.spec.ts index 7b028678bf..54f69c4e33 100644 --- a/tools/metadata/symbols.spec.ts +++ b/tools/metadata/test/symbols.spec.ts @@ -1,9 +1,6 @@ -/// -/// - 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; diff --git a/tools/metadata/typescript.mock.ts b/tools/metadata/test/typescript.mocks.ts similarity index 74% rename from tools/metadata/typescript.mock.ts rename to tools/metadata/test/typescript.mocks.ts index 136305e685..c2c5a6b50f 100644 --- a/tools/metadata/typescript.mock.ts +++ b/tools/metadata/test/typescript.mocks.ts @@ -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(node: ts.Node, cb: (node: ts.Node) => T) { return ts.forEachChild(node, child => { const result = cb(node); @@ -112,18 +130,14 @@ export function allChildren(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 && (node).text === name; } + +export function isVar(node: ts.Node): node is ts.VariableDeclaration { + return node.kind === ts.SyntaxKind.VariableDeclaration; +}