chore(build): Added tests for metadata extractor
Adds unit test to metadata extractor classes Fixes issues found while testing
This commit is contained in:
parent
ae876d1317
commit
3f57fa6e0e
|
@ -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 {}`
|
||||||
|
}
|
|
@ -1,6 +1,18 @@
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {Symbols} from './symbols';
|
import {Symbols} from './symbols';
|
||||||
|
|
||||||
|
// TOOD: Remove when tools directory is upgraded to support es6 target
|
||||||
|
interface Map<K, V> {
|
||||||
|
has(k: K): boolean;
|
||||||
|
set(k: K, v: V): void;
|
||||||
|
get(k: K): V;
|
||||||
|
delete (k: K): void;
|
||||||
|
}
|
||||||
|
interface MapConstructor {
|
||||||
|
new<K, V>(): Map<K, V>;
|
||||||
|
}
|
||||||
|
declare var Map: MapConstructor;
|
||||||
|
|
||||||
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
|
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
|
||||||
const expression = callExpression.expression;
|
const expression = callExpression.expression;
|
||||||
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
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
|
* - An identifier is foldable if a value can be found for its symbol is in the evaluator symbol
|
||||||
* table.
|
* table.
|
||||||
*/
|
*/
|
||||||
public isFoldable(node: ts.Node) {
|
public isFoldable(node: ts.Node): boolean {
|
||||||
|
return this.isFoldableWorker(node, new Map<ts.Node, boolean>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private isFoldableWorker(node: ts.Node, folding: Map<ts.Node, boolean>): boolean {
|
||||||
if (node) {
|
if (node) {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||||
return everyNodeChild(node, child => {
|
return everyNodeChild(node, child => {
|
||||||
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
|
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
|
||||||
const propertyAssignment = <ts.PropertyAssignment>child;
|
const propertyAssignment = <ts.PropertyAssignment>child;
|
||||||
return this.isFoldable(propertyAssignment.initializer)
|
return this.isFoldableWorker(propertyAssignment.initializer, folding)
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
case ts.SyntaxKind.ArrayLiteralExpression:
|
case ts.SyntaxKind.ArrayLiteralExpression:
|
||||||
return everyNodeChild(node, child => this.isFoldable(child));
|
return everyNodeChild(node, child => this.isFoldableWorker(child, folding));
|
||||||
case ts.SyntaxKind.CallExpression:
|
case ts.SyntaxKind.CallExpression:
|
||||||
const callExpression = <ts.CallExpression>node;
|
const callExpression = <ts.CallExpression>node;
|
||||||
// We can fold a <array>.concat(<v>).
|
// We can fold a <array>.concat(<v>).
|
||||||
if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) {
|
if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) {
|
||||||
const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression;
|
const arrayNode = (<ts.PropertyAccessExpression>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.
|
// It needs to be an array.
|
||||||
const arrayValue = this.evaluateNode(arrayNode);
|
const arrayValue = this.evaluateNode(arrayNode);
|
||||||
if (arrayValue && Array.isArray(arrayValue)) {
|
if (arrayValue && Array.isArray(arrayValue)) {
|
||||||
|
@ -133,7 +150,7 @@ export class Evaluator {
|
||||||
}
|
}
|
||||||
// We can fold a call to CONST_EXPR
|
// We can fold a call to CONST_EXPR
|
||||||
if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1)
|
if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1)
|
||||||
return this.isFoldable(callExpression.arguments[0]);
|
return this.isFoldableWorker(callExpression.arguments[0], folding);
|
||||||
return false;
|
return false;
|
||||||
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
||||||
case ts.SyntaxKind.StringLiteral:
|
case ts.SyntaxKind.StringLiteral:
|
||||||
|
@ -142,6 +159,9 @@ export class Evaluator {
|
||||||
case ts.SyntaxKind.TrueKeyword:
|
case ts.SyntaxKind.TrueKeyword:
|
||||||
case ts.SyntaxKind.FalseKeyword:
|
case ts.SyntaxKind.FalseKeyword:
|
||||||
return true;
|
return true;
|
||||||
|
case ts.SyntaxKind.ParenthesizedExpression:
|
||||||
|
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
|
||||||
|
return this.isFoldableWorker(parenthesizedExpression.expression, folding);
|
||||||
case ts.SyntaxKind.BinaryExpression:
|
case ts.SyntaxKind.BinaryExpression:
|
||||||
const binaryExpression = <ts.BinaryExpression>node;
|
const binaryExpression = <ts.BinaryExpression>node;
|
||||||
switch (binaryExpression.operatorToken.kind) {
|
switch (binaryExpression.operatorToken.kind) {
|
||||||
|
@ -152,19 +172,37 @@ export class Evaluator {
|
||||||
case ts.SyntaxKind.PercentToken:
|
case ts.SyntaxKind.PercentToken:
|
||||||
case ts.SyntaxKind.AmpersandAmpersandToken:
|
case ts.SyntaxKind.AmpersandAmpersandToken:
|
||||||
case ts.SyntaxKind.BarBarToken:
|
case ts.SyntaxKind.BarBarToken:
|
||||||
return this.isFoldable(binaryExpression.left) &&
|
return this.isFoldableWorker(binaryExpression.left, folding) &&
|
||||||
this.isFoldable(binaryExpression.right);
|
this.isFoldableWorker(binaryExpression.right, folding);
|
||||||
}
|
}
|
||||||
case ts.SyntaxKind.PropertyAccessExpression:
|
case ts.SyntaxKind.PropertyAccessExpression:
|
||||||
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
||||||
return this.isFoldable(propertyAccessExpression.expression);
|
return this.isFoldableWorker(propertyAccessExpression.expression, folding);
|
||||||
case ts.SyntaxKind.ElementAccessExpression:
|
case ts.SyntaxKind.ElementAccessExpression:
|
||||||
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
||||||
return this.isFoldable(elementAccessExpression.expression) &&
|
return this.isFoldableWorker(elementAccessExpression.expression, folding) &&
|
||||||
this.isFoldable(elementAccessExpression.argumentExpression);
|
this.isFoldableWorker(elementAccessExpression.argumentExpression, folding);
|
||||||
case ts.SyntaxKind.Identifier:
|
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.symbols.has(symbol)) return true;
|
||||||
|
|
||||||
|
// If this is a reference to a foldable variable then it is foldable too.
|
||||||
|
const variableDeclaration = <ts.VariableDeclaration>(
|
||||||
|
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
|
||||||
|
if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) {
|
||||||
|
const initializer = variableDeclaration.initializer;
|
||||||
|
if (folding.has(initializer)) {
|
||||||
|
// A recursive reference is not foldable.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
folding.set(initializer, true);
|
||||||
|
const result = this.isFoldableWorker(initializer, folding);
|
||||||
|
folding.delete(initializer);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,8 +290,17 @@ export class Evaluator {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ts.SyntaxKind.Identifier:
|
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.symbols.has(symbol)) return this.symbols.get(symbol);
|
||||||
|
if (this.isFoldable(node)) {
|
||||||
|
// isFoldable implies, in this context, symbol declaration is a VariableDeclaration
|
||||||
|
const variableDeclaration = <ts.VariableDeclaration>(
|
||||||
|
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
|
||||||
|
return this.evaluateNode(variableDeclaration.initializer);
|
||||||
|
}
|
||||||
return this.nodeSymbolReference(node);
|
return this.nodeSymbolReference(node);
|
||||||
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
||||||
return (<ts.LiteralExpression>node).text;
|
return (<ts.LiteralExpression>node).text;
|
||||||
|
@ -267,7 +314,42 @@ export class Evaluator {
|
||||||
return true;
|
return true;
|
||||||
case ts.SyntaxKind.FalseKeyword:
|
case ts.SyntaxKind.FalseKeyword:
|
||||||
return false;
|
return false;
|
||||||
|
case ts.SyntaxKind.ParenthesizedExpression:
|
||||||
|
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
|
||||||
|
return this.evaluateNode(parenthesizedExpression.expression);
|
||||||
|
case ts.SyntaxKind.PrefixUnaryExpression:
|
||||||
|
const prefixUnaryExpression = <ts.PrefixUnaryExpression>node;
|
||||||
|
const operand = this.evaluateNode(prefixUnaryExpression.operand);
|
||||||
|
if (isDefined(operand) && isPrimitive(operand)) {
|
||||||
|
switch (prefixUnaryExpression.operator) {
|
||||||
|
case ts.SyntaxKind.PlusToken:
|
||||||
|
return +operand;
|
||||||
|
case ts.SyntaxKind.MinusToken:
|
||||||
|
return -operand;
|
||||||
|
case ts.SyntaxKind.TildeToken:
|
||||||
|
return ~operand;
|
||||||
|
case ts.SyntaxKind.ExclamationToken:
|
||||||
|
return !operand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let operatorText: string;
|
||||||
|
switch (prefixUnaryExpression.operator) {
|
||||||
|
case ts.SyntaxKind.PlusToken:
|
||||||
|
operatorText = '+';
|
||||||
|
break;
|
||||||
|
case ts.SyntaxKind.MinusToken:
|
||||||
|
operatorText = '-';
|
||||||
|
break;
|
||||||
|
case ts.SyntaxKind.TildeToken:
|
||||||
|
operatorText = '~';
|
||||||
|
break;
|
||||||
|
case ts.SyntaxKind.ExclamationToken:
|
||||||
|
operatorText = '!';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {__symbolic: "pre", operator: operatorText, operand: operand };
|
||||||
case ts.SyntaxKind.BinaryExpression:
|
case ts.SyntaxKind.BinaryExpression:
|
||||||
const binaryExpression = <ts.BinaryExpression>node;
|
const binaryExpression = <ts.BinaryExpression>node;
|
||||||
const left = this.evaluateNode(binaryExpression.left);
|
const left = this.evaluateNode(binaryExpression.left);
|
||||||
|
|
|
@ -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 {}`
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/// <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';
|
||||||
|
|
||||||
|
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());
|
||||||
|
});
|
|
@ -24,11 +24,11 @@ var a: Array<number>;
|
||||||
export class Symbols {
|
export class Symbols {
|
||||||
private map = new Map<ts.Node, any>();
|
private map = new Map<ts.Node, any>();
|
||||||
|
|
||||||
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();
|
static empty: Symbols = new Symbols();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<T>(node: ts.Node, cb: (node: ts.Node) => T) {
|
||||||
|
return ts.forEachChild(node, child => {
|
||||||
|
const result = cb(node);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return allChildren(child, cb);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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 && (<ts.Identifier>node).text === name;
|
||||||
|
}
|
Loading…
Reference in New Issue