diff --git a/tools/metadata/evaluator.spec.ts b/tools/metadata/evaluator.spec.ts new file mode 100644 index 0000000000..63d493d398 --- /dev/null +++ b/tools/metadata/evaluator.spec.ts @@ -0,0 +1,128 @@ +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'; + +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; + let typeChecker: ts.TypeChecker; + let symbols: Symbols; + let evaluator: Evaluator; + + beforeEach(() => { + host = new MockHost(['expressions.ts'], /*currentDirectory*/ undefined, 'lib.d.ts'); + service = ts.createLanguageService(host); + program = service.getProgram(); + typeChecker = program.getTypeChecker(); + symbols = new Symbols(); + evaluator = new Evaluator(service, typeChecker, symbols, f => f); + }); + + 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 fold literal expressions', () => { + var consts = program.getSourceFile('consts.ts'); + expect(evaluator.isFoldable(findVar(consts, 'someName').initializer)).toBeTruthy(); + expect(evaluator.isFoldable(findVar(consts, 'someBool').initializer)).toBeTruthy(); + expect(evaluator.isFoldable(findVar(consts, 'one').initializer)).toBeTruthy(); + expect(evaluator.isFoldable(findVar(consts, 'two').initializer)).toBeTruthy(); + }); + + it('should be able to fold expressions with foldable references', () => { + var expressions = program.getSourceFile('expressions.ts'); + expect(evaluator.isFoldable(findVar(expressions, 'three').initializer)).toBeTruthy(); + expect(evaluator.isFoldable(findVar(expressions, 'four').initializer)).toBeTruthy(); + expect(evaluator.isFoldable(findVar(expressions, 'obj').initializer)).toBeTruthy(); + expect(evaluator.isFoldable(findVar(expressions, 'arr').initializer)).toBeTruthy(); + }); + + it('should be able to evaluate literal expressions', () => { + var consts = program.getSourceFile('consts.ts'); + expect(evaluator.evaluateNode(findVar(consts, 'someName').initializer)).toBe('some-name'); + expect(evaluator.evaluateNode(findVar(consts, 'someBool').initializer)).toBe(true); + expect(evaluator.evaluateNode(findVar(consts, 'one').initializer)).toBe(1); + expect(evaluator.evaluateNode(findVar(consts, 'two').initializer)).toBe(2); + }); + + it('should be able to evaluate expressions', () => { + var expressions = program.getSourceFile('expressions.ts'); + expect(evaluator.evaluateNode(findVar(expressions, 'three').initializer)).toBe(3); + expect(evaluator.evaluateNode(findVar(expressions, 'four').initializer)).toBe(4); + expect(evaluator.evaluateNode(findVar(expressions, 'obj').initializer)) + .toEqual({one: 1, two: 2, three: 3, four: 4}); + expect(evaluator.evaluateNode(findVar(expressions, 'arr').initializer)).toEqual([1, 2, 3, 4]); + expect(evaluator.evaluateNode(findVar(expressions, 'bTrue').initializer)).toEqual(true); + expect(evaluator.evaluateNode(findVar(expressions, 'bFalse').initializer)).toEqual(false); + expect(evaluator.evaluateNode(findVar(expressions, 'bAnd').initializer)).toEqual(true); + expect(evaluator.evaluateNode(findVar(expressions, 'bOr').initializer)).toEqual(true); + expect(evaluator.evaluateNode(findVar(expressions, 'nDiv').initializer)).toEqual(2); + expect(evaluator.evaluateNode(findVar(expressions, 'nMod').initializer)).toEqual(1); + }); + + it('should report recursive references as symbolic', () => { + var expressions = program.getSourceFile('expressions.ts'); + expect(evaluator.evaluateNode(findVar(expressions, 'recursiveA').initializer)) + .toEqual({__symbolic: "reference", name: "recursiveB", module: "expressions.ts"}); + expect(evaluator.evaluateNode(findVar(expressions, 'recursiveB').initializer)) + .toEqual({__symbolic: "reference", name: "recursiveA", module: "expressions.ts"}); + }); +}); + +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; + export var one = 1; + export var two = 2; + `, + 'expressions.ts': ` + import {someName, someBool, one, two} from './consts'; + + export var three = one + two; + export var four = two * two; + export var obj = { one: one, two: two, three: three, four: four }; + export var arr = [one, two, three, four]; + export var bTrue = someBool; + export var bFalse = !someBool; + export var bAnd = someBool && someBool; + export var bOr = someBool || someBool; + export var nDiv = four / two; + export var nMod = (four + one) % two; + + export var recursiveA = recursiveB; + export var recursiveB = recursiveA; + `, + 'A.ts': ` + import {Pipe} from './directives'; + + @Pipe({name: 'A', pure: false}) + export class A {}`, + 'B.ts': ` + import {Pipe} from './directives'; + import {someName, someBool} from './consts'; + + @Pipe({name: someName, pure: someBool}) + export class B {}` +} diff --git a/tools/metadata/evaluator.ts b/tools/metadata/evaluator.ts index 776b77b50a..7b31090778 100644 --- a/tools/metadata/evaluator.ts +++ b/tools/metadata/evaluator.ts @@ -1,6 +1,18 @@ import * as ts from 'typescript'; import {Symbols} from './symbols'; +// TOOD: Remove when tools directory is upgraded to support es6 target +interface Map { + has(k: K): boolean; + set(k: K, v: V): void; + get(k: K): V; + delete (k: K): void; +} +interface MapConstructor { + new(): Map; +} +declare var Map: MapConstructor; + function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { const expression = callExpression.expression; if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) { @@ -105,25 +117,30 @@ export class Evaluator { * - An identifier is foldable if a value can be found for its symbol is in the evaluator symbol * table. */ - public isFoldable(node: ts.Node) { + public isFoldable(node: ts.Node): boolean { + return this.isFoldableWorker(node, new Map()); + } + + private isFoldableWorker(node: ts.Node, folding: Map): boolean { if (node) { switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: return everyNodeChild(node, child => { if (child.kind === ts.SyntaxKind.PropertyAssignment) { const propertyAssignment = child; - return this.isFoldable(propertyAssignment.initializer) + return this.isFoldableWorker(propertyAssignment.initializer, folding) } return false; }); case ts.SyntaxKind.ArrayLiteralExpression: - return everyNodeChild(node, child => this.isFoldable(child)); + return everyNodeChild(node, child => this.isFoldableWorker(child, folding)); case ts.SyntaxKind.CallExpression: const callExpression = node; // We can fold a .concat(). if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) { const arrayNode = (callExpression.expression).expression; - if (this.isFoldable(arrayNode) && this.isFoldable(callExpression.arguments[0])) { + if (this.isFoldableWorker(arrayNode, folding) && + this.isFoldableWorker(callExpression.arguments[0], folding)) { // It needs to be an array. const arrayValue = this.evaluateNode(arrayNode); if (arrayValue && Array.isArray(arrayValue)) { @@ -133,7 +150,7 @@ export class Evaluator { } // We can fold a call to CONST_EXPR if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1) - return this.isFoldable(callExpression.arguments[0]); + return this.isFoldableWorker(callExpression.arguments[0], folding); return false; case ts.SyntaxKind.NoSubstitutionTemplateLiteral: case ts.SyntaxKind.StringLiteral: @@ -142,6 +159,9 @@ export class Evaluator { case ts.SyntaxKind.TrueKeyword: case ts.SyntaxKind.FalseKeyword: return true; + case ts.SyntaxKind.ParenthesizedExpression: + const parenthesizedExpression = node; + return this.isFoldableWorker(parenthesizedExpression.expression, folding); case ts.SyntaxKind.BinaryExpression: const binaryExpression = node; switch (binaryExpression.operatorToken.kind) { @@ -152,19 +172,37 @@ export class Evaluator { case ts.SyntaxKind.PercentToken: case ts.SyntaxKind.AmpersandAmpersandToken: case ts.SyntaxKind.BarBarToken: - return this.isFoldable(binaryExpression.left) && - this.isFoldable(binaryExpression.right); + return this.isFoldableWorker(binaryExpression.left, folding) && + this.isFoldableWorker(binaryExpression.right, folding); } case ts.SyntaxKind.PropertyAccessExpression: const propertyAccessExpression = node; - return this.isFoldable(propertyAccessExpression.expression); + return this.isFoldableWorker(propertyAccessExpression.expression, folding); case ts.SyntaxKind.ElementAccessExpression: const elementAccessExpression = node; - return this.isFoldable(elementAccessExpression.expression) && - this.isFoldable(elementAccessExpression.argumentExpression); + return this.isFoldableWorker(elementAccessExpression.expression, folding) && + this.isFoldableWorker(elementAccessExpression.argumentExpression, folding); case ts.SyntaxKind.Identifier: - const symbol = this.typeChecker.getSymbolAtLocation(node); + let symbol = this.typeChecker.getSymbolAtLocation(node); + if (symbol.flags & ts.SymbolFlags.Alias) { + symbol = this.typeChecker.getAliasedSymbol(symbol); + } if (this.symbols.has(symbol)) return true; + + // If this is a reference to a foldable variable then it is foldable too. + const variableDeclaration = ( + symbol.declarations && symbol.declarations.length && symbol.declarations[0]); + if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) { + const initializer = variableDeclaration.initializer; + if (folding.has(initializer)) { + // A recursive reference is not foldable. + return false; + } + folding.set(initializer, true); + const result = this.isFoldableWorker(initializer, folding); + folding.delete(initializer); + return result; + } break; } } @@ -252,8 +290,17 @@ export class Evaluator { break; } case ts.SyntaxKind.Identifier: - const symbol = this.typeChecker.getSymbolAtLocation(node); + let symbol = this.typeChecker.getSymbolAtLocation(node); + if (symbol.flags & ts.SymbolFlags.Alias) { + symbol = this.typeChecker.getAliasedSymbol(symbol); + } if (this.symbols.has(symbol)) return this.symbols.get(symbol); + if (this.isFoldable(node)) { + // isFoldable implies, in this context, symbol declaration is a VariableDeclaration + const variableDeclaration = ( + symbol.declarations && symbol.declarations.length && symbol.declarations[0]); + return this.evaluateNode(variableDeclaration.initializer); + } return this.nodeSymbolReference(node); case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return (node).text; @@ -267,7 +314,42 @@ export class Evaluator { return true; case ts.SyntaxKind.FalseKeyword: return false; - + case ts.SyntaxKind.ParenthesizedExpression: + const parenthesizedExpression = node; + return this.evaluateNode(parenthesizedExpression.expression); + case ts.SyntaxKind.PrefixUnaryExpression: + const prefixUnaryExpression = node; + const operand = this.evaluateNode(prefixUnaryExpression.operand); + if (isDefined(operand) && isPrimitive(operand)) { + switch (prefixUnaryExpression.operator) { + case ts.SyntaxKind.PlusToken: + return +operand; + case ts.SyntaxKind.MinusToken: + return -operand; + case ts.SyntaxKind.TildeToken: + return ~operand; + case ts.SyntaxKind.ExclamationToken: + return !operand; + } + } + let operatorText: string; + switch (prefixUnaryExpression.operator) { + case ts.SyntaxKind.PlusToken: + operatorText = '+'; + break; + case ts.SyntaxKind.MinusToken: + operatorText = '-'; + break; + case ts.SyntaxKind.TildeToken: + operatorText = '~'; + break; + case ts.SyntaxKind.ExclamationToken: + operatorText = '!'; + break; + default: + return undefined; + } + return {__symbolic: "pre", operator: operatorText, operand: operand }; case ts.SyntaxKind.BinaryExpression: const binaryExpression = node; const left = this.evaluateNode(binaryExpression.left); diff --git a/tools/metadata/extractor.spec.ts b/tools/metadata/extractor.spec.ts new file mode 100644 index 0000000000..555d341d80 --- /dev/null +++ b/tools/metadata/extractor.spec.ts @@ -0,0 +1,138 @@ +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/symbols.spec.ts b/tools/metadata/symbols.spec.ts new file mode 100644 index 0000000000..7b028678bf --- /dev/null +++ b/tools/metadata/symbols.spec.ts @@ -0,0 +1,32 @@ +/// +/// + +import * as ts from 'typescript'; +import {Symbols} from './symbols'; +import {MockSymbol, MockVariableDeclaration} from './typescript.mock'; + +describe('Symbols', () => { + let symbols: Symbols; + const someValue = 'some-value'; + const someSymbol = MockSymbol.of('some-symbol'); + const aliasSymbol = new MockSymbol('some-symbol', someSymbol.getDeclarations()[0]); + const missingSymbol = MockSymbol.of('some-other-symbol'); + + beforeEach(() => symbols = new Symbols()); + + it('should be able to add a symbol', () => symbols.set(someSymbol, someValue)); + + beforeEach(() => symbols.set(someSymbol, someValue)); + + it('should be able to `has` a symbol', () => expect(symbols.has(someSymbol)).toBeTruthy()); + it('should be able to `get` a symbol value', + () => expect(symbols.get(someSymbol)).toBe(someValue)); + it('should be able to `has` an alias symbol', + () => expect(symbols.has(aliasSymbol)).toBeTruthy()); + it('should be able to `get` a symbol value', + () => expect(symbols.get(aliasSymbol)).toBe(someValue)); + it('should be able to determine symbol is missing', + () => expect(symbols.has(missingSymbol)).toBeFalsy()); + it('should return undefined from `get` for a missing symbol', + () => expect(symbols.get(missingSymbol)).toBeUndefined()); +}); \ No newline at end of file diff --git a/tools/metadata/symbols.ts b/tools/metadata/symbols.ts index 9dfe2e2804..17cb4b9b1a 100644 --- a/tools/metadata/symbols.ts +++ b/tools/metadata/symbols.ts @@ -24,11 +24,11 @@ var a: Array; export class Symbols { private map = new Map(); - public has(symbol: ts.Symbol): boolean { return this.map.has(symbol.declarations[0]); } + public has(symbol: ts.Symbol): boolean { return this.map.has(symbol.getDeclarations()[0]); } - public set(symbol: ts.Symbol, value): void { this.map.set(symbol.declarations[0], value); } + public set(symbol: ts.Symbol, value): void { this.map.set(symbol.getDeclarations()[0], value); } - public get(symbol: ts.Symbol): any { return this.map.get(symbol.declarations[0]); } + public get(symbol: ts.Symbol): any { return this.map.get(symbol.getDeclarations()[0]); } static empty: Symbols = new Symbols(); } diff --git a/tools/metadata/typescript.mock.ts b/tools/metadata/typescript.mock.ts new file mode 100644 index 0000000000..136305e685 --- /dev/null +++ b/tools/metadata/typescript.mock.ts @@ -0,0 +1,135 @@ +import * as ts from 'typescript'; +import * as fs from 'fs'; + +/** + * 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) {} + + getCompilationSettings(): ts.CompilerOptions { + return { + experimentalDecorators: true, + modules: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5 + }; + } + + getScriptFileNames(): string[] { return this.fileNames; } + + getScriptVersion(fileName: string): string { return "1"; } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot { + if (fs.existsSync(fileName)) { + return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName, 'utf8')) + } + } + + getCurrentDirectory(): string { return this.currentDirectory; } + + getDefaultLibFileName(options: ts.CompilerOptions): string { + return this.libName || ts.getDefaultLibFilePath(options); + } +} + +export class MockNode implements ts.Node { + constructor(public kind: ts.SyntaxKind = ts.SyntaxKind.Identifier, public flags: ts.NodeFlags = 0, + public pos: number = 0, public end: number = 0) {} + getSourceFile(): ts.SourceFile { return null; } + getChildCount(sourceFile?: ts.SourceFile): number { return 0 } + getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node { return null; } + getChildren(sourceFile?: ts.SourceFile): ts.Node[] { return []; } + getStart(sourceFile?: ts.SourceFile): number { return 0; } + getFullStart(): number { return 0; } + getEnd(): number { return 0; } + getWidth(sourceFile?: ts.SourceFile): number { return 0; } + getFullWidth(): number { return 0; } + getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { return 0; } + getFullText(sourceFile?: ts.SourceFile): string { return ''; } + getText(sourceFile?: ts.SourceFile): string { return ''; } + getFirstToken(sourceFile?: ts.SourceFile): ts.Node { return null; } + getLastToken(sourceFile?: ts.SourceFile): ts.Node { return null; } +} + +export class MockIdentifier extends MockNode implements ts.Identifier { + public text: string; + public _primaryExpressionBrand: any; + public _memberExpressionBrand: any; + public _leftHandSideExpressionBrand: any; + public _incrementExpressionBrand: any; + public _unaryExpressionBrand: any; + public _expressionBrand: any; + + constructor(public name: string, kind: ts.SyntaxKind = ts.SyntaxKind.Identifier, + flags: ts.NodeFlags = 0, pos: number = 0, end: number = 0) { + super(kind, flags, pos, end); + this.text = name; + } +} + +export class MockVariableDeclaration extends MockNode implements ts.VariableDeclaration { + public _declarationBrand: any; + + constructor(public name: ts.Identifier, kind: ts.SyntaxKind = ts.SyntaxKind.VariableDeclaration, + flags: ts.NodeFlags = 0, pos: number = 0, end: number = 0) { + super(kind, flags, pos, end); + } + + static of(name: string): MockVariableDeclaration { + return new MockVariableDeclaration(new MockIdentifier(name)); + } +} + +export class MockSymbol implements ts.Symbol { + constructor(public name: string, private node: ts.Declaration = MockVariableDeclaration.of(name), + public flags: ts.SymbolFlags = 0) {} + + getFlags(): ts.SymbolFlags { return this.flags; } + getName(): string { return this.name; } + getDeclarations(): ts.Declaration[] { return [this.node]; } + getDocumentationComment(): ts.SymbolDisplayPart[] { return []; } + + static of(name: string): MockSymbol { return new MockSymbol(name); } +} + +export function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) { + for (const diagnostic of diagnostics) { + let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); + let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); + } + expect(diagnostics.length).toBe(0); +} + +export function allChildren(node: ts.Node, cb: (node: ts.Node) => T) { + return ts.forEachChild(node, child => { + const result = cb(node); + if (result) { + return result; + } + return allChildren(child, cb); + }) +} + +export function 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 isClass(node: ts.Node): node is ts.ClassDeclaration { + return node.kind === ts.SyntaxKind.ClassDeclaration; +} + +export function isNamed(node: ts.Node, name: string): node is ts.Identifier { + return node.kind === ts.SyntaxKind.Identifier && (node).text === name; +}