Skip to content

Commit 3f57fa6

Browse files
committed
chore(build): Added tests for metadata extractor
Adds unit test to metadata extractor classes Fixes issues found while testing
1 parent ae876d1 commit 3f57fa6

File tree

6 files changed

+531
-16
lines changed

6 files changed

+531
-16
lines changed

tools/metadata/evaluator.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
var mockfs = require('mock-fs');
2+
3+
import * as ts from 'typescript';
4+
import * as fs from 'fs';
5+
import {MockHost, expectNoDiagnostics, findVar} from './typescript.mock';
6+
import {Evaluator} from './evaluator';
7+
import {Symbols} from './symbols';
8+
9+
describe('Evaluator', () => {
10+
// Read the lib.d.ts before mocking fs.
11+
let libTs: string = fs.readFileSync(ts.getDefaultLibFilePath({}), 'utf8');
12+
13+
beforeEach(() => files['lib.d.ts'] = libTs);
14+
beforeEach(() => mockfs(files));
15+
afterEach(() => mockfs.restore());
16+
17+
let host: ts.LanguageServiceHost;
18+
let service: ts.LanguageService;
19+
let program: ts.Program;
20+
let typeChecker: ts.TypeChecker;
21+
let symbols: Symbols;
22+
let evaluator: Evaluator;
23+
24+
beforeEach(() => {
25+
host = new MockHost(['expressions.ts'], /*currentDirectory*/ undefined, 'lib.d.ts');
26+
service = ts.createLanguageService(host);
27+
program = service.getProgram();
28+
typeChecker = program.getTypeChecker();
29+
symbols = new Symbols();
30+
evaluator = new Evaluator(service, typeChecker, symbols, f => f);
31+
});
32+
33+
it('should not have typescript errors in test data', () => {
34+
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
35+
for (const sourceFile of program.getSourceFiles()) {
36+
expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName));
37+
}
38+
});
39+
40+
it('should be able to fold literal expressions', () => {
41+
var consts = program.getSourceFile('consts.ts');
42+
expect(evaluator.isFoldable(findVar(consts, 'someName').initializer)).toBeTruthy();
43+
expect(evaluator.isFoldable(findVar(consts, 'someBool').initializer)).toBeTruthy();
44+
expect(evaluator.isFoldable(findVar(consts, 'one').initializer)).toBeTruthy();
45+
expect(evaluator.isFoldable(findVar(consts, 'two').initializer)).toBeTruthy();
46+
});
47+
48+
it('should be able to fold expressions with foldable references', () => {
49+
var expressions = program.getSourceFile('expressions.ts');
50+
expect(evaluator.isFoldable(findVar(expressions, 'three').initializer)).toBeTruthy();
51+
expect(evaluator.isFoldable(findVar(expressions, 'four').initializer)).toBeTruthy();
52+
expect(evaluator.isFoldable(findVar(expressions, 'obj').initializer)).toBeTruthy();
53+
expect(evaluator.isFoldable(findVar(expressions, 'arr').initializer)).toBeTruthy();
54+
});
55+
56+
it('should be able to evaluate literal expressions', () => {
57+
var consts = program.getSourceFile('consts.ts');
58+
expect(evaluator.evaluateNode(findVar(consts, 'someName').initializer)).toBe('some-name');
59+
expect(evaluator.evaluateNode(findVar(consts, 'someBool').initializer)).toBe(true);
60+
expect(evaluator.evaluateNode(findVar(consts, 'one').initializer)).toBe(1);
61+
expect(evaluator.evaluateNode(findVar(consts, 'two').initializer)).toBe(2);
62+
});
63+
64+
it('should be able to evaluate expressions', () => {
65+
var expressions = program.getSourceFile('expressions.ts');
66+
expect(evaluator.evaluateNode(findVar(expressions, 'three').initializer)).toBe(3);
67+
expect(evaluator.evaluateNode(findVar(expressions, 'four').initializer)).toBe(4);
68+
expect(evaluator.evaluateNode(findVar(expressions, 'obj').initializer))
69+
.toEqual({one: 1, two: 2, three: 3, four: 4});
70+
expect(evaluator.evaluateNode(findVar(expressions, 'arr').initializer)).toEqual([1, 2, 3, 4]);
71+
expect(evaluator.evaluateNode(findVar(expressions, 'bTrue').initializer)).toEqual(true);
72+
expect(evaluator.evaluateNode(findVar(expressions, 'bFalse').initializer)).toEqual(false);
73+
expect(evaluator.evaluateNode(findVar(expressions, 'bAnd').initializer)).toEqual(true);
74+
expect(evaluator.evaluateNode(findVar(expressions, 'bOr').initializer)).toEqual(true);
75+
expect(evaluator.evaluateNode(findVar(expressions, 'nDiv').initializer)).toEqual(2);
76+
expect(evaluator.evaluateNode(findVar(expressions, 'nMod').initializer)).toEqual(1);
77+
});
78+
79+
it('should report recursive references as symbolic', () => {
80+
var expressions = program.getSourceFile('expressions.ts');
81+
expect(evaluator.evaluateNode(findVar(expressions, 'recursiveA').initializer))
82+
.toEqual({__symbolic: "reference", name: "recursiveB", module: "expressions.ts"});
83+
expect(evaluator.evaluateNode(findVar(expressions, 'recursiveB').initializer))
84+
.toEqual({__symbolic: "reference", name: "recursiveA", module: "expressions.ts"});
85+
});
86+
});
87+
88+
const files = {
89+
'directives.ts': `
90+
export function Pipe(options: { name?: string, pure?: boolean}) {
91+
return function(fn: Function) { }
92+
}
93+
`,
94+
'consts.ts': `
95+
export var someName = 'some-name';
96+
export var someBool = true;
97+
export var one = 1;
98+
export var two = 2;
99+
`,
100+
'expressions.ts': `
101+
import {someName, someBool, one, two} from './consts';
102+
103+
export var three = one + two;
104+
export var four = two * two;
105+
export var obj = { one: one, two: two, three: three, four: four };
106+
export var arr = [one, two, three, four];
107+
export var bTrue = someBool;
108+
export var bFalse = !someBool;
109+
export var bAnd = someBool && someBool;
110+
export var bOr = someBool || someBool;
111+
export var nDiv = four / two;
112+
export var nMod = (four + one) % two;
113+
114+
export var recursiveA = recursiveB;
115+
export var recursiveB = recursiveA;
116+
`,
117+
'A.ts': `
118+
import {Pipe} from './directives';
119+
120+
@Pipe({name: 'A', pure: false})
121+
export class A {}`,
122+
'B.ts': `
123+
import {Pipe} from './directives';
124+
import {someName, someBool} from './consts';
125+
126+
@Pipe({name: someName, pure: someBool})
127+
export class B {}`
128+
}

tools/metadata/evaluator.ts

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import * as ts from 'typescript';
22
import {Symbols} from './symbols';
33

4+
// TOOD: Remove when tools directory is upgraded to support es6 target
5+
interface Map<K, V> {
6+
has(k: K): boolean;
7+
set(k: K, v: V): void;
8+
get(k: K): V;
9+
delete (k: K): void;
10+
}
11+
interface MapConstructor {
12+
new<K, V>(): Map<K, V>;
13+
}
14+
declare var Map: MapConstructor;
15+
416
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
517
const expression = callExpression.expression;
618
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
@@ -105,25 +117,30 @@ export class Evaluator {
105117
* - An identifier is foldable if a value can be found for its symbol is in the evaluator symbol
106118
* table.
107119
*/
108-
public isFoldable(node: ts.Node) {
120+
public isFoldable(node: ts.Node): boolean {
121+
return this.isFoldableWorker(node, new Map<ts.Node, boolean>());
122+
}
123+
124+
private isFoldableWorker(node: ts.Node, folding: Map<ts.Node, boolean>): boolean {
109125
if (node) {
110126
switch (node.kind) {
111127
case ts.SyntaxKind.ObjectLiteralExpression:
112128
return everyNodeChild(node, child => {
113129
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
114130
const propertyAssignment = <ts.PropertyAssignment>child;
115-
return this.isFoldable(propertyAssignment.initializer)
131+
return this.isFoldableWorker(propertyAssignment.initializer, folding)
116132
}
117133
return false;
118134
});
119135
case ts.SyntaxKind.ArrayLiteralExpression:
120-
return everyNodeChild(node, child => this.isFoldable(child));
136+
return everyNodeChild(node, child => this.isFoldableWorker(child, folding));
121137
case ts.SyntaxKind.CallExpression:
122138
const callExpression = <ts.CallExpression>node;
123139
// We can fold a <array>.concat(<v>).
124140
if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) {
125141
const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression;
126-
if (this.isFoldable(arrayNode) && this.isFoldable(callExpression.arguments[0])) {
142+
if (this.isFoldableWorker(arrayNode, folding) &&
143+
this.isFoldableWorker(callExpression.arguments[0], folding)) {
127144
// It needs to be an array.
128145
const arrayValue = this.evaluateNode(arrayNode);
129146
if (arrayValue && Array.isArray(arrayValue)) {
@@ -133,7 +150,7 @@ export class Evaluator {
133150
}
134151
// We can fold a call to CONST_EXPR
135152
if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1)
136-
return this.isFoldable(callExpression.arguments[0]);
153+
return this.isFoldableWorker(callExpression.arguments[0], folding);
137154
return false;
138155
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
139156
case ts.SyntaxKind.StringLiteral:
@@ -142,6 +159,9 @@ export class Evaluator {
142159
case ts.SyntaxKind.TrueKeyword:
143160
case ts.SyntaxKind.FalseKeyword:
144161
return true;
162+
case ts.SyntaxKind.ParenthesizedExpression:
163+
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
164+
return this.isFoldableWorker(parenthesizedExpression.expression, folding);
145165
case ts.SyntaxKind.BinaryExpression:
146166
const binaryExpression = <ts.BinaryExpression>node;
147167
switch (binaryExpression.operatorToken.kind) {
@@ -152,19 +172,37 @@ export class Evaluator {
152172
case ts.SyntaxKind.PercentToken:
153173
case ts.SyntaxKind.AmpersandAmpersandToken:
154174
case ts.SyntaxKind.BarBarToken:
155-
return this.isFoldable(binaryExpression.left) &&
156-
this.isFoldable(binaryExpression.right);
175+
return this.isFoldableWorker(binaryExpression.left, folding) &&
176+
this.isFoldableWorker(binaryExpression.right, folding);
157177
}
158178
case ts.SyntaxKind.PropertyAccessExpression:
159179
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
160-
return this.isFoldable(propertyAccessExpression.expression);
180+
return this.isFoldableWorker(propertyAccessExpression.expression, folding);
161181
case ts.SyntaxKind.ElementAccessExpression:
162182
const elementAccessExpression = <ts.ElementAccessExpression>node;
163-
return this.isFoldable(elementAccessExpression.expression) &&
164-
this.isFoldable(elementAccessExpression.argumentExpression);
183+
return this.isFoldableWorker(elementAccessExpression.expression, folding) &&
184+
this.isFoldableWorker(elementAccessExpression.argumentExpression, folding);
165185
case ts.SyntaxKind.Identifier:
166-
const symbol = this.typeChecker.getSymbolAtLocation(node);
186+
let symbol = this.typeChecker.getSymbolAtLocation(node);
187+
if (symbol.flags & ts.SymbolFlags.Alias) {
188+
symbol = this.typeChecker.getAliasedSymbol(symbol);
189+
}
167190
if (this.symbols.has(symbol)) return true;
191+
192+
// If this is a reference to a foldable variable then it is foldable too.
193+
const variableDeclaration = <ts.VariableDeclaration>(
194+
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
195+
if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) {
196+
const initializer = variableDeclaration.initializer;
197+
if (folding.has(initializer)) {
198+
// A recursive reference is not foldable.
199+
return false;
200+
}
201+
folding.set(initializer, true);
202+
const result = this.isFoldableWorker(initializer, folding);
203+
folding.delete(initializer);
204+
return result;
205+
}
168206
break;
169207
}
170208
}
@@ -252,8 +290,17 @@ export class Evaluator {
252290
break;
253291
}
254292
case ts.SyntaxKind.Identifier:
255-
const symbol = this.typeChecker.getSymbolAtLocation(node);
293+
let symbol = this.typeChecker.getSymbolAtLocation(node);
294+
if (symbol.flags & ts.SymbolFlags.Alias) {
295+
symbol = this.typeChecker.getAliasedSymbol(symbol);
296+
}
256297
if (this.symbols.has(symbol)) return this.symbols.get(symbol);
298+
if (this.isFoldable(node)) {
299+
// isFoldable implies, in this context, symbol declaration is a VariableDeclaration
300+
const variableDeclaration = <ts.VariableDeclaration>(
301+
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
302+
return this.evaluateNode(variableDeclaration.initializer);
303+
}
257304
return this.nodeSymbolReference(node);
258305
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
259306
return (<ts.LiteralExpression>node).text;
@@ -267,7 +314,42 @@ export class Evaluator {
267314
return true;
268315
case ts.SyntaxKind.FalseKeyword:
269316
return false;
270-
317+
case ts.SyntaxKind.ParenthesizedExpression:
318+
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
319+
return this.evaluateNode(parenthesizedExpression.expression);
320+
case ts.SyntaxKind.PrefixUnaryExpression:
321+
const prefixUnaryExpression = <ts.PrefixUnaryExpression>node;
322+
const operand = this.evaluateNode(prefixUnaryExpression.operand);
323+
if (isDefined(operand) && isPrimitive(operand)) {
324+
switch (prefixUnaryExpression.operator) {
325+
case ts.SyntaxKind.PlusToken:
326+
return +operand;
327+
case ts.SyntaxKind.MinusToken:
328+
return -operand;
329+
case ts.SyntaxKind.TildeToken:
330+
return ~operand;
331+
case ts.SyntaxKind.ExclamationToken:
332+
return !operand;
333+
}
334+
}
335+
let operatorText: string;
336+
switch (prefixUnaryExpression.operator) {
337+
case ts.SyntaxKind.PlusToken:
338+
operatorText = '+';
339+
break;
340+
case ts.SyntaxKind.MinusToken:
341+
operatorText = '-';
342+
break;
343+
case ts.SyntaxKind.TildeToken:
344+
operatorText = '~';
345+
break;
346+
case ts.SyntaxKind.ExclamationToken:
347+
operatorText = '!';
348+
break;
349+
default:
350+
return undefined;
351+
}
352+
return {__symbolic: "pre", operator: operatorText, operand: operand };
271353
case ts.SyntaxKind.BinaryExpression:
272354
const binaryExpression = <ts.BinaryExpression>node;
273355
const left = this.evaluateNode(binaryExpression.left);

0 commit comments

Comments
 (0)