TypeScript/tests/baselines/reference/parserharness.js
2015-05-01 10:49:54 -07:00

3861 lines
161 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//// [parserharness.ts]
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///<reference path='..\compiler\io.ts'/>
///<reference path='..\compiler\typescript.ts'/>
///<reference path='..\services\typescriptServices.ts' />
///<reference path='diff.ts'/>
declare var assert: Harness.Assert;
declare var it;
declare var describe;
declare var run;
declare var IO: IIO;
declare var __dirname; // Node-specific
function switchToForwardSlashes(path: string) {
return path.replace(/\\/g, "/");
}
function filePath(fullPath: string) {
fullPath = switchToForwardSlashes(fullPath);
var components = fullPath.split("/");
var path: string[] = components.slice(0, components.length - 1);
return path.join("/") + "/";
}
var typescriptServiceFileName = filePath(IO.getExecutingFilePath()) + "typescriptServices.js";
var typescriptServiceFile = IO.readFile(typescriptServiceFileName);
if (typeof ActiveXObject === "function") {
eval(typescriptServiceFile);
} else if (typeof require === "function") {
var vm = require('vm');
vm.runInThisContext(typescriptServiceFile, 'typescriptServices.js');
} else {
throw new Error('Unknown context');
}
declare module process {
export function nextTick(callback: () => any): void;
export function on(event: string, listener: Function);
}
module Harness {
// Settings
export var userSpecifiedroot = "";
var global = <any>Function("return this").call(null);
export var usePull = false;
export interface ITestMetadata {
id: string;
desc: string;
pass: boolean;
perfResults: {
mean: number;
min: number;
max: number;
stdDev: number;
trials: number[];
};
}
export interface IScenarioMetadata {
id: string;
desc: string;
pass: boolean;
bugs: string[];
}
// Assert functions
export module Assert {
export var bugIds: string[] = [];
export var throwAssertError = (error: Error) => {
throw error;
};
// Marks that the current scenario is impacted by a bug
export function bug(id: string) {
if (bugIds.indexOf(id) < 0) {
bugIds.push(id);
}
}
// If there are any bugs in the test code, mark the scenario as impacted appropriately
export function bugs(content: string) {
var bugs = content.match(/\bbug (\d+)/i);
if (bugs) {
bugs.forEach(bug => assert.bug(bug));
}
}
export function is(result: boolean, msg?: string) {
if (!result) {
throwAssertError(new Error(msg || "Expected true, got false."));
}
}
export function arrayLengthIs(arr: any[], length: number) {
if (arr.length != length) {
var actual = '';
arr.forEach(n => actual = actual + '\n ' + n.toString());
throwAssertError(new Error('Expected array to have ' + length + ' elements. Actual elements were:' + actual));
}
}
export function equal(actual, expected) {
if (actual !== expected) {
throwAssertError(new Error("Expected " + actual + " to equal " + expected));
}
}
export function notEqual(actual, expected) {
if (actual === expected) {
throwAssertError(new Error("Expected " + actual + " to *not* equal " + expected));
}
}
export function notNull(result) {
if (result === null) {
throwAssertError(new Error("Expected " + result + " to *not* be null"));
}
}
export function compilerWarning(result: Compiler.CompilerResult, line: number, column: number, desc: string) {
if (!result.isErrorAt(line, column, desc)) {
var actual = '';
result.errors.forEach(err => {
actual = actual + '\n ' + err.toString();
});
throwAssertError(new Error("Expected compiler warning at (" + line + ", " + column + "): " + desc + "\nActual errors follow: " + actual));
}
}
export function noDiff(text1, text2) {
text1 = text1.replace(/^\s+|\s+$/g, "").replace(/\r\n?/g, "\n");
text2 = text2.replace(/^\s+|\s+$/g, "").replace(/\r\n?/g, "\n");
if (text1 !== text2) {
var errorString = "";
var text1Lines = text1.split(/\n/);
var text2Lines = text2.split(/\n/);
for (var i = 0; i < text1Lines.length; i++) {
if (text1Lines[i] !== text2Lines[i]) {
errorString += "Difference at line " + (i + 1) + ":\n";
errorString += " Left File: " + text1Lines[i] + "\n";
errorString += " Right File: " + text2Lines[i] + "\n\n";
}
}
throwAssertError(new Error(errorString));
}
}
export function arrayContains(arr: any[], contains: any[]) {
var found;
for (var i = 0; i < contains.length; i++) {
found = false;
for (var j = 0; j < arr.length; j++) {
if (arr[j] === contains[i]) {
found = true;
break;
}
}
if (!found) {
throwAssertError(new Error("Expected array to contain \"" + contains[i] + "\""));
}
}
}
export function arrayContainsOnce(arr: any[], filter: (item: any) => boolean) {
var foundCount = 0;
for (var i = 0; i < arr.length; i++) {
if (filter(arr[i])) {
foundCount++;
}
}
if (foundCount !== 1) {
throwAssertError(new Error("Expected array to match element only once (instead of " + foundCount + " times)"));
}
}
}
/** Splits the given string on \r\n or on only \n if that fails */
export function splitContentByNewlines(content: string) {
// Split up the input file by line
// Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so
// we have to string-based splitting instead and try to figure out the delimiting chars
var lines = content.split('\r\n');
if (lines.length === 1) {
lines = content.split('\n');
}
return lines;
}
/** Reads a file under /tests */
export function readFile(path: string) {
if (path.indexOf('tests') < 0) {
path = "tests/" + path;
}
var content = IO.readFile(Harness.userSpecifiedroot + path);
if (content == null) {
throw new Error("failed to read file at: '" + Harness.userSpecifiedroot + path + "'");
}
return content;
}
// Logger
export interface ILogger {
start: (fileName?: string, priority?: number) => void;
end: (fileName?: string) => void;
scenarioStart: (scenario: IScenarioMetadata) => void;
scenarioEnd: (scenario: IScenarioMetadata, error?: Error) => void;
testStart: (test: ITestMetadata) => void;
pass: (test: ITestMetadata) => void;
bug: (test: ITestMetadata) => void;
fail: (test: ITestMetadata) => void;
error: (test: ITestMetadata, error: Error) => void;
comment: (comment: string) => void;
verify: (test: ITestMetadata, passed: boolean, actual: any, expected: any, message: string) => void;
}
export class Logger implements ILogger {
public start(fileName?: string, priority?: number) { }
public end(fileName?: string) { }
public scenarioStart(scenario: IScenarioMetadata) { }
public scenarioEnd(scenario: IScenarioMetadata, error?: Error) { }
public testStart(test: ITestMetadata) { }
public pass(test: ITestMetadata) { }
public bug(test: ITestMetadata) { }
public fail(test: ITestMetadata) { }
public error(test: ITestMetadata, error: Error) { }
public comment(comment: string) { }
public verify(test: ITestMetadata, passed: boolean, actual: any, expected: any, message: string) { }
}
// Logger-related functions
var loggers: ILogger[] = [];
export function registerLogger(logger: ILogger) {
loggers.push(logger);
}
export function emitLog(field: string, ...params: any[]) {
for (var i = 0; i < loggers.length; i++) {
if (typeof loggers[i][field] === 'function') {
loggers[i][field].apply(loggers[i], params);
}
}
}
// BDD Framework
export interface IDone {
(e?: Error): void;
}
export class Runnable {
constructor(public description: string, public block: any) { }
// The current stack of Runnable objects
static currentStack: Runnable[] = [];
// The error, if any, that occurred when running 'block'
public error: Error = null;
// Whether or not this object has any failures (including in its descendants)
public passed = null;
// A list of bugs impacting this object
public bugs: string[] = [];
// A list of all our child Runnables
public children: Runnable[] = [];
public addChild(child: Runnable): void {
this.children.push(child);
}
/** Call function fn, which may take a done function and may possibly execute
* asynchronously, calling done when finished. Returns true or false depending
* on whether the function was asynchronous or not.
*/
public call(fn: (done?: IDone) => void , done: IDone) {
var isAsync = true;
try {
if (fn.length === 0) {
// No async.
fn();
done();
return false;
} else {
// Possibly async
Runnable.pushGlobalErrorHandler(done);
fn(function () {
isAsync = false; // If we execute synchronously, this will get called before the return below.
Runnable.popGlobalErrorHandler();
done();
});
return isAsync;
}
} catch (e) {
done(e);
return false;
}
}
public run(done: IDone) { }
public runBlock(done: IDone) {
return this.call(this.block, done);
}
public runChild(index: number, done: IDone) {
return this.call(<any>((done) => this.children[index].run(done)), done);
}
static errorHandlerStack: { (e: Error): void; }[] = [];
static pushGlobalErrorHandler(done: IDone) {
errorHandlerStack.push(function (e) {
done(e);
});
}
static popGlobalErrorHandler() {
errorHandlerStack.pop();
}
static handleError(e: Error) {
if (errorHandlerStack.length === 0) {
IO.printLine('Global error: ' + e);
} else {
errorHandlerStack[errorHandlerStack.length - 1](e);
}
}
}
export class TestCase extends Runnable {
public description: string;
public block;
constructor(description: string, block: any) {
super(description, block);
this.description = description;
this.block = block;
}
public addChild(child: Runnable): void {
throw new Error("Testcases may not be nested inside other testcases");
}
/** Run the test case block and fail the test if it raised an error. If no error is raised, the test passes. */
public run(done: IDone) {
var that = this;
Runnable.currentStack.push(this);
emitLog('testStart', { desc: this.description });
if (this.block) {
var async = this.runBlock(<any>function (e) {
if (e) {
that.passed = false;
that.error = e;
emitLog('error', { desc: this.description, pass: false }, e);
} else {
that.passed = true;
emitLog('pass', { desc: this.description, pass: true });
}
Runnable.currentStack.pop();
done()
});
}
}
}
export class Scenario extends Runnable {
public description: string;
public block;
constructor(description: string, block: any) {
super(description, block);
this.description = description;
this.block = block;
}
/** Run the block, and if the block doesn't raise an error, run the children. */
public run(done: IDone) {
var that = this;
Runnable.currentStack.push(this);
emitLog('scenarioStart', { desc: this.description });
var async = this.runBlock(<any>function (e) {
Runnable.currentStack.pop();
if (e) {
that.passed = false;
that.error = e;
var metadata: IScenarioMetadata = { id: undefined, desc: this.description, pass: false, bugs: assert.bugIds };
// Report all bugs affecting this scenario
assert.bugIds.forEach(desc => emitLog('bug', metadata, desc));
emitLog('scenarioEnd', metadata, e);
done();
} else {
that.passed = true; // so far so good.
that.runChildren(done);
}
});
}
/** Run the children of the scenario (other scenarios and test cases). If any fail,
* set this scenario to failed. Synchronous tests will run synchronously without
* adding stack frames.
*/
public runChildren(done: IDone, index = 0) {
var that = this;
var async = false;
for (; index < this.children.length; index++) {
async = this.runChild(index, <any>function (e) {
that.passed = that.passed && that.children[index].passed;
if (async)
that.runChildren(done, index + 1);
});
if (async)
return;
}
var metadata: IScenarioMetadata = { id: undefined, desc: this.description, pass: this.passed, bugs: assert.bugIds };
// Report all bugs affecting this scenario
assert.bugIds.forEach(desc => emitLog('bug', metadata, desc));
emitLog('scenarioEnd', metadata);
done();
}
}
export class Run extends Runnable {
constructor() {
super('Test Run', null);
}
public run() {
emitLog('start');
this.runChildren();
}
public runChildren(index = 0) {
var async = false;
var that = this;
for (; index < this.children.length; index++) {
// Clear out bug descriptions
assert.bugIds = [];
async = this.runChild(index, <any>function (e) {
if (async) {
that.runChildren(index + 1);
}
});
if (async) {
return;
}
}
Perf.runBenchmarks();
emitLog('end');
}
}
// Performance test
export module Perf {
export module Clock {
export var now: () => number;
export var resolution: number;
declare module WScript {
export function InitializeProjection();
}
declare module TestUtilities {
export function QueryPerformanceCounter(): number;
export function QueryPerformanceFrequency(): number;
}
if (typeof WScript !== "undefined" && typeof global['WScript'].InitializeProjection !== "undefined") {
// Running in JSHost.
global['WScript'].InitializeProjection();
now = function () {
return TestUtilities.QueryPerformanceCounter();
}
resolution = TestUtilities.QueryPerformanceFrequency();
} else {
now = function () {
return Date.now();
}
resolution = 1000;
}
}
export class Timer {
public startTime;
public time = 0;
public start() {
this.time = 0;
this.startTime = Clock.now();
}
public end() {
// Set time to MS.
this.time = (Clock.now() - this.startTime) / Clock.resolution * 1000;
}
}
export class Dataset {
public data: number[] = [];
public add(value: number) {
this.data.push(value);
}
public mean() {
var sum = 0;
for (var i = 0; i < this.data.length; i++) {
sum += this.data[i];
}
return sum / this.data.length;
}
public min() {
var min = this.data[0];
for (var i = 1; i < this.data.length; i++) {
if (this.data[i] < min) {
min = this.data[i];
}
}
return min;
}
public max() {
var max = this.data[0];
for (var i = 1; i < this.data.length; i++) {
if (this.data[i] > max) {
max = this.data[i];
}
}
return max;
}
public stdDev() {
var sampleMean = this.mean();
var sumOfSquares = 0;
for (var i = 0; i < this.data.length; i++) {
sumOfSquares += Math.pow(this.data[i] - sampleMean, 2);
}
return Math.sqrt(sumOfSquares / this.data.length);
}
}
// Base benchmark class with some defaults.
export class Benchmark {
public iterations = 10;
public description = "";
public bench(subBench?: () => void ) { }
public before() { }
public beforeEach() { }
public after() { }
public afterEach() { }
public results: { [x: string]: Dataset; } = <{ [x: string]: Dataset; }>{};
public addTimingFor(name: string, timing: number) {
this.results[name] = this.results[name] || new Dataset();
this.results[name].add(timing);
}
}
export var benchmarks: { new (): Benchmark; }[] = [];
var timeFunction: (
benchmark: Benchmark,
description?: string,
name?: string,
f?: (bench?: { (): void; }) => void
) => void;
timeFunction = function (
benchmark: Benchmark,
description: string = benchmark.description,
name: string = '',
f = benchmark.bench
): void {
var t = new Timer();
t.start();
var subBenchmark = function (name, f): void {
timeFunction(benchmark, description, name, f);
}
f.call(benchmark, subBenchmark);
t.end();
benchmark.addTimingFor(name, t.time);
}
export function runBenchmarks() {
for (var i = 0; i < benchmarks.length; i++) {
var b = new benchmarks[i]();
var t = new Timer();
b.before();
for (var j = 0; j < b.iterations; j++) {
b.beforeEach();
timeFunction(b);
b.afterEach();
}
b.after();
for (var prop in b.results) {
var description = b.description + (prop ? ": " + prop : '');
emitLog('testStart', { desc: description });
emitLog('pass', {
desc: description, pass: true, perfResults: {
mean: b.results[prop].mean(),
min: b.results[prop].min(),
max: b.results[prop].max(),
stdDev: b.results[prop].stdDev(),
trials: b.results[prop].data
}
});
}
}
}
// Replace with better type when classes are assignment compatible with
// the below type.
// export function addBenchmark(BenchmarkClass: {new(): Benchmark;}) {
export function addBenchmark(BenchmarkClass: any) {
benchmarks.push(BenchmarkClass);
}
}
/** Functionality for compiling TypeScript code */
export module Compiler {
/** Aggregate various writes into a single array of lines. Useful for passing to the
* TypeScript compiler to fill with source code or errors.
*/
export class WriterAggregator implements ITextWriter {
public lines: string[] = [];
public currentLine = "";
public Write(str) {
this.currentLine += str;
}
public WriteLine(str) {
this.lines.push(this.currentLine + str);
this.currentLine = "";
}
public Close() {
if (this.currentLine.length > 0) { this.lines.push(this.currentLine); }
this.currentLine = "";
}
public reset() {
this.lines = [];
this.currentLine = "";
}
}
/** Mimics having multiple files, later concatenated to a single file. */
export class EmitterIOHost implements TypeScript.EmitterIOHost {
private fileCollection = {};
/** create file gets the whole path to create, so this works as expected with the --out parameter */
public createFile(s: string, useUTF8?: boolean): ITextWriter {
if (this.fileCollection[s]) {
return <ITextWriter>this.fileCollection[s];
}
var writer = new Harness.Compiler.WriterAggregator();
this.fileCollection[s] = writer;
return writer;
}
public directoryExists(s: string) { return false; }
public fileExists(s: string) { return typeof this.fileCollection[s] !== 'undefined'; }
public resolvePath(s: string) { return s; }
public reset() { this.fileCollection = {}; }
public toArray(): { filename: string; file: WriterAggregator; }[] {
var result: { filename: string; file: WriterAggregator; }[] = [];
for (var p in this.fileCollection) {
if (this.fileCollection.hasOwnProperty(p)) {
var current = <Harness.Compiler.WriterAggregator>this.fileCollection[p];
if (current.lines.length > 0) {
if (p !== '0.js') { current.lines.unshift('////[' + p + ']'); }
result.push({ filename: p, file: this.fileCollection[p] });
}
}
}
return result;
}
}
var libFolder: string = global['WScript'] ? TypeScript.filePath(global['WScript'].ScriptFullName) : (__dirname + '/');
export var libText = IO ? IO.readFile(libFolder + "lib.d.ts") : '';
var stdout = new EmitterIOHost();
var stderr = new WriterAggregator();
export function isDeclareFile(filename: string) {
return /\.d\.ts$/.test(filename);
}
export function makeDefaultCompilerForTest(c?: TypeScript.TypeScriptCompiler) {
var compiler = c || new TypeScript.TypeScriptCompiler(stderr);
compiler.parser.errorRecovery = true;
compiler.settings.codeGenTarget = TypeScript.CodeGenTarget.ES5;
compiler.settings.controlFlow = true;
compiler.settings.controlFlowUseDef = true;
if (Harness.usePull) {
compiler.settings.usePull = true;
compiler.settings.useFidelity = true;
}
compiler.parseEmitOption(stdout);
TypeScript.moduleGenTarget = TypeScript.ModuleGenTarget.Synchronous;
compiler.addUnit(Harness.Compiler.libText, "lib.d.ts", true);
return compiler;
}
var compiler: TypeScript.TypeScriptCompiler;
recreate();
// pullUpdateUnit is sufficient if an existing unit is updated, if a new unit is added we need to do a full typecheck
var needsFullTypeCheck = true;
export function compile(code?: string, filename?: string) {
if (usePull) {
if (needsFullTypeCheck) {
compiler.pullTypeCheck(true);
needsFullTypeCheck = false;
}
else {
// requires unit to already exist in the compiler
compiler.pullUpdateUnit(new TypeScript.StringSourceText(""), filename, true);
compiler.pullUpdateUnit(new TypeScript.StringSourceText(code), filename, true);
}
}
else {
compiler.reTypeCheck();
}
}
// Types
export class Type {
constructor(public type, public code, public identifier) { }
public normalizeToArray(arg: any) {
if ((Array.isArray && Array.isArray(arg)) || arg instanceof Array)
return arg;
return [arg];
}
public compilesOk(testCode): boolean {
var errors = null;
compileString(testCode, 'test.ts', function (compilerResult) {
errors = compilerResult.errors;
})
return errors.length === 0;
}
public isSubtypeOf(other: Type) {
var testCode = 'class __test1__ {\n';
testCode += ' public test() {\n';
testCode += ' ' + other.code + ';\n';
testCode += ' return ' + other.identifier + ';\n';
testCode += ' }\n';
testCode += '}\n';
testCode += 'class __test2__ extends __test1__ {\n';
testCode += ' public test() {\n';
testCode += ' ' + this.code + ';\n';
testCode += ' return ' + other.identifier + ';\n';
testCode += ' }\n';
testCode += '}\n';
return this.compilesOk(testCode);
}
// TODO: Find an implementation of isIdenticalTo that works.
//public isIdenticalTo(other: Type) {
// var testCode = 'module __test1__ {\n';
// testCode += ' ' + this.code + ';\n';
// testCode += ' export var __val__ = ' + this.identifier + ';\n';
// testCode += '}\n';
// testCode += 'var __test1__val__ = __test1__.__val__;\n';
// testCode += 'module __test2__ {\n';
// testCode += ' ' + other.code + ';\n';
// testCode += ' export var __val__ = ' + other.identifier + ';\n';
// testCode += '}\n';
// testCode += 'var __test2__val__ = __test2__.__val__;\n';
// testCode += 'function __test__function__() { if(true) { return __test1__val__ }; return __test2__val__; }';
// return this.compilesOk(testCode);
//}
public assertSubtypeOf(others: any) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
if (!this.isSubtypeOf(others[i])) {
throw new Error("Expected " + this.type + " to be a subtype of " + others[i].type);
}
}
}
public assertNotSubtypeOf(others: any) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
if (this.isSubtypeOf(others[i])) {
throw new Error("Expected " + this.type + " to be a subtype of " + others[i].type);
}
}
}
//public assertIdenticalTo(other: Type) {
// if (!this.isIdenticalTo(other)) {
// throw new Error("Expected " + this.type + " to be identical to " + other.type);
// }
//}
//public assertNotIdenticalTo(other: Type) {
// if (!this.isIdenticalTo(other)) {
// throw new Error("Expected " + this.type + " to not be identical to " + other.type);
// }
//}
public isAssignmentCompatibleWith(other: Type) {
var testCode = 'module __test1__ {\n';
testCode += ' ' + this.code + ';\n';
testCode += ' export var __val__ = ' + this.identifier + ';\n';
testCode += '}\n';
testCode += 'var __test1__val__ = __test1__.__val__;\n';
testCode += 'module __test2__ {\n';
testCode += ' export ' + other.code + ';\n';
testCode += ' export var __val__ = ' + other.identifier + ';\n';
testCode += '}\n';
testCode += 'var __test2__val__ = __test2__.__val__;\n';
testCode += '__test2__val__ = __test1__val__;';
return this.compilesOk(testCode);
}
public assertAssignmentCompatibleWith(others: any) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
var other = others[i];
if (!this.isAssignmentCompatibleWith(other)) {
throw new Error("Expected " + this.type + " to be assignment compatible with " + other.type);
}
}
}
public assertNotAssignmentCompatibleWith(others: any) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
var other = others[i];
if (this.isAssignmentCompatibleWith(other)) {
throw new Error("Expected " + this.type + " to not be assignment compatible with " + other.type);
}
}
}
public assertThisCanBeAssignedTo(desc: string, these: any[], notThese: any[]) {
it(desc + " is assignable to ", () => {
this.assertAssignmentCompatibleWith(these);
});
it(desc + " not assignable to ", () => {
this.assertNotAssignmentCompatibleWith(notThese);
});
}
}
export class TypeFactory {
public any: Type;
public number: Type;
public string: Type;
public boolean: Type;
constructor() {
this.any = this.get('var x : any', 'x');
this.number = this.get('var x : number', 'x');
this.string = this.get('var x : string', 'x');
this.boolean = this.get('var x : boolean', 'x');
}
public get (code: string, target: any) {
var targetIdentifier = '';
var targetPosition = -1;
if (typeof target === "string") {
targetIdentifier = target;
}
else if (typeof target === "number") {
targetPosition = target;
}
else {
throw new Error("Expected string or number not " + (typeof target));
}
var errors = null;
compileString(code, 'test.ts', function (compilerResult) {
errors = compilerResult.errors;
})
if (errors.length > 0)
throw new Error("Type definition contains errors: " + errors.join(","));
var matchingIdentifiers: Type[] = [];
if (!usePull) {
// This will find the requested identifier in the first script where it's present, a naive search of each member in each script,
// which means this won't play nicely if the same identifier is used in multiple units, but it will enable this to work on multi-file tests.
// m = 1 because the first script will always be lib.d.ts which we don't want to search.
for (var m = 1; m < compiler.scripts.members.length; m++) {
var script = compiler.scripts.members[m];
var enclosingScopeContext = TypeScript.findEnclosingScopeAt(new TypeScript.NullLogger(), <TypeScript.Script>script, new TypeScript.StringSourceText(code), 0, false);
var entries = new TypeScript.ScopeTraversal(compiler).getScopeEntries(enclosingScopeContext);
for (var i = 0; i < entries.length; i++) {
if (entries[i].name === targetIdentifier) {
matchingIdentifiers.push(new Type(entries[i].type, code, targetIdentifier));
}
}
}
}
else {
for (var m = 0; m < compiler.scripts.members.length; m++) {
var script2 = <TypeScript.Script>compiler.scripts.members[m];
if (script2.locationInfo.filename !== 'lib.d.ts') {
if (targetPosition > -1) {
var tyInfo = compiler.pullGetTypeInfoAtPosition(targetPosition, script2);
var name = this.getTypeInfoName(tyInfo.ast);
var foundValue = new Type(tyInfo.typeInfo, code, name);
if (!matchingIdentifiers.some(value => (value.identifier === foundValue.identifier) && (value.code === foundValue.code) && (value.type === foundValue.type))) {
matchingIdentifiers.push(foundValue);
}
}
else {
for (var pos = 0; pos < code.length; pos++) {
var tyInfo = compiler.pullGetTypeInfoAtPosition(pos, script2);
var name = this.getTypeInfoName(tyInfo.ast);
if (name === targetIdentifier) {
var foundValue = new Type(tyInfo.typeInfo, code, targetIdentifier);
if (!matchingIdentifiers.some(value => (value.identifier === foundValue.identifier) && (value.code === foundValue.code) && (value.type === foundValue.type))) {
matchingIdentifiers.push(foundValue);
}
}
}
}
}
}
}
if (matchingIdentifiers.length === 0) {
if (targetPosition > -1) {
throw new Error("Could not find an identifier at position " + targetPosition);
}
else {
throw new Error("Could not find an identifier " + targetIdentifier + " in any known scopes");
}
}
else if (matchingIdentifiers.length > 1) {
throw new Error("Found multiple matching identifiers for " + target);
}
else {
return matchingIdentifiers[0];
}
}
private getTypeInfoName(ast : TypeScript.AST) {
var name = '';
switch (ast.nodeType) {
case TypeScript.NodeType.Name: // Type Name?
case TypeScript.NodeType.Null:
case TypeScript.NodeType.List:
case TypeScript.NodeType.Empty:
case TypeScript.NodeType.EmptyExpr:
case TypeScript.NodeType.Asg:
case TypeScript.NodeType.True:
case TypeScript.NodeType.False:
case TypeScript.NodeType.ArrayLit:
case TypeScript.NodeType.TypeRef:
break;
case TypeScript.NodeType.Super:
name = (<any>ast).text;
break;
case TypeScript.NodeType.Regex:
name = (<TypeScript.RegexLiteral>ast).text;
break;
case TypeScript.NodeType.QString:
name = (<any>ast).text;
break;
case TypeScript.NodeType.NumberLit:
name = (<TypeScript.NumberLiteral>ast).text;
break;
case TypeScript.NodeType.Return:
//name = (<TypeScript.ReturnStatement>tyInfo.ast).returnExpression.actualText; // why is this complaining?
break;
case TypeScript.NodeType.InterfaceDeclaration:
name = (<TypeScript.InterfaceDeclaration>ast).name.actualText;
break;
case TypeScript.NodeType.ModuleDeclaration:
name = (<TypeScript.ModuleDeclaration>ast).name.actualText;
break;
case TypeScript.NodeType.ClassDeclaration:
name = (<TypeScript.ClassDeclaration>ast).name.actualText;
break;
case TypeScript.NodeType.FuncDecl:
name = !(<TypeScript.FuncDecl>ast).name ? "" : (<TypeScript.FuncDecl>ast).name.actualText; // name == null for lambdas
break;
default:
// TODO: is there a reason to mess with all the special cases above and not just do this (ie take whatever property is there and works?)
var a = <any>ast;
name = (a.id) ? (a.id.actualText) : (a.name) ? a.name.actualText : (a.text) ? a.text : '';
break;
}
return name;
}
public isOfType(expr: string, expectedType: string) {
var actualType = this.get('var _v_a_r_ = ' + expr, '_v_a_r_');
it('Expression "' + expr + '" is of type "' + expectedType + '"', function () {
assert.equal(actualType.type, expectedType);
});
}
}
/** Generates a .d.ts file for the given code
* @param verifyNoDeclFile pass true when the given code should generate no decl file, false otherwise
* @param unitName add the given code under thie name, else use '0.ts'
* @param compilationContext a set of functions to be run before and after compiling this code for doing things like adding dependencies first
* @param references the set of referenced files used by the given code
*/
export function generateDeclFile(code: string, verifyNoDeclFile: boolean, unitName?: string, compilationContext?: Harness.Compiler.CompilationContext, references?: TypeScript.IFileReference[]): string {
reset();
compiler.settings.generateDeclarationFiles = true;
var oldOutputOption = compiler.settings.outputOption;
var oldEmitterIOHost = compiler.emitSettings.ioHost;
try {
if (compilationContext && compilationContext.preCompile) {
compilationContext.preCompile();
}
addUnit(code, unitName, false, false, references);
compiler.reTypeCheck();
var outputs = {};
compiler.settings.outputOption = "";
compiler.parseEmitOption(
{
createFile: (fn: string) => {
outputs[fn] = new Harness.Compiler.WriterAggregator();
return outputs[fn];
},
directoryExists: (path: string) => true,
fileExists: (path: string) => true,
resolvePath: (path: string) => path
});
compiler.emitDeclarations();
var results: string = null;
for (var fn in outputs) {
if (fn.indexOf('.d.ts') >= 0) {
var writer = <Harness.Compiler.WriterAggregator>outputs[fn];
writer.Close();
results = writer.lines.join('\n');
if (verifyNoDeclFile && results != "") {
throw new Error('Compilation should not produce ' + fn);
}
}
}
if (results) {
return results;
}
if (!verifyNoDeclFile) {
throw new Error('Compilation did not produce .d.ts files');
}
} finally {
compiler.settings.generateDeclarationFiles = false;
compiler.settings.outputOption = oldOutputOption;
compiler.parseEmitOption(oldEmitterIOHost);
if (compilationContext && compilationContext.postCompile) {
compilationContext.postCompile();
}
var uName = unitName || '0.ts';
updateUnit('', uName);
}
return '';
}
/** Contains the code and errors of a compilation and some helper methods to check its status. */
export class CompilerResult {
public code: string;
public errors: CompilerError[];
/** @param fileResults an array of strings for the filename and an ITextWriter with its code */
constructor(public fileResults: { filename: string; file: WriterAggregator; }[], errorLines: string[], public scripts: TypeScript.Script[]) {
var lines = [];
fileResults.forEach(v => lines = lines.concat(v.file.lines));
this.code = lines.join("\n")
this.errors = [];
for (var i = 0; i < errorLines.length; i++) {
if (Harness.usePull) {
var err = <any>errorLines[i]; // TypeScript.PullError
this.errors.push(new CompilerError(err.filename, 0, 0, err.message));
} else {
var match = errorLines[i].match(/([^\(]*)\((\d+),(\d+)\):\s+((.*[\s\r\n]*.*)+)\s*$/);
if (match) {
this.errors.push(new CompilerError(match[1], parseFloat(match[2]), parseFloat(match[3]), match[4]));
}
else {
WScript.Echo("non-match on: " + errorLines[i]);
}
}
}
}
public isErrorAt(line: number, column: number, message: string) {
for (var i = 0; i < this.errors.length; i++) {
if (this.errors[i].line === line && this.errors[i].column === column && this.errors[i].message === message)
return true;
}
return false;
}
}
// Compiler Error.
export class CompilerError {
constructor(public file: string,
public line: number,
public column: number,
public message: string) { }
public toString() {
return this.file + "(" + this.line + "," + this.column + "): " + this.message;
}
}
/** Create a new instance of the compiler with default settings and lib.d.ts, then typecheck */
export function recreate() {
compiler = makeDefaultCompilerForTest();
if (usePull) {
compiler.pullTypeCheck(true);
}
else {
compiler.typeCheck();
}
}
export function reset() {
stdout.reset();
stderr.reset();
var files = compiler.units.map((value) => value.filename);
for (var i = 0; i < files.length; i++) {
var fname = files[i];
if(fname !== 'lib.d.ts') {
updateUnit('', fname);
}
}
compiler.errorReporter.hasErrors = false;
}
// Defines functions to invoke before compiling a piece of code and a post compile action intended to clean up the
// effects of preCompile, preferably with something lighter weight than a full recreate()
export interface CompilationContext {
filename: string;
preCompile: () => void;
postCompile: () => void;
}
export function addUnit(code: string, unitName?: string, isResident?: boolean, isDeclareFile?: boolean, references?: TypeScript.IFileReference[]) {
var script: TypeScript.Script = null;
var uName = unitName || '0' + (isDeclareFile ? '.d.ts' : '.ts');
for (var i = 0; i < compiler.units.length; i++) {
if (compiler.units[i].filename === uName) {
updateUnit(code, uName);
script = <TypeScript.Script>compiler.scripts.members[i];
}
}
if (!script) {
// TODO: make this toggleable, shouldn't be necessary once typecheck bugs are cleaned up
// but without it subsequent tests are treated as edits, making for somewhat useful stress testing
// of persistent typecheck state
//compiler.addUnit("", uName, isResident, references); // equivalent to compiler.deleteUnit(...)
script = compiler.addUnit(code, uName, isResident, references);
needsFullTypeCheck = true;
}
return script;
}
export function updateUnit(code: string, unitName: string, setRecovery?: boolean) {
if (Harness.usePull) {
compiler.pullUpdateUnit(new TypeScript.StringSourceText(code), unitName, setRecovery);
} else {
compiler.updateUnit(code, unitName, setRecovery);
}
}
export function compileFile(path: string, callback: (res: CompilerResult) => void , settingsCallback?: (settings?: TypeScript.CompilationSettings) => void , context?: CompilationContext, references?: TypeScript.IFileReference[]) {
path = switchToForwardSlashes(path);
var filename = path.match(/[^\/]*$/)[0];
var code = readFile(path);
compileUnit(code, filename, callback, settingsCallback, context, references);
}
export function compileUnit(code: string, filename: string, callback: (res: CompilerResult) => void , settingsCallback?: (settings?: TypeScript.CompilationSettings) => void , context?: CompilationContext, references?: TypeScript.IFileReference[]) {
// not recursive
function clone/* <T> */(source: any, target: any) {
for (var prop in source) {
target[prop] = source[prop];
}
}
var oldCompilerSettings = new TypeScript.CompilationSettings();
clone(compiler.settings, oldCompilerSettings);
var oldEmitSettings = new TypeScript.EmitOptions(compiler.settings);
clone(compiler.emitSettings, oldEmitSettings);
var oldModuleGenTarget = TypeScript.moduleGenTarget;
if (settingsCallback) {
settingsCallback(compiler.settings);
compiler.emitSettings = new TypeScript.EmitOptions(compiler.settings);
}
try {
compileString(code, filename, callback, context, references);
} finally {
// If settingsCallback exists, assume that it modified the global compiler instance's settings in some way.
// So that a test doesn't have side effects for tests run after it, restore the compiler settings to their previous state.
if (settingsCallback) {
compiler.settings = oldCompilerSettings;
compiler.emitSettings = oldEmitSettings;
TypeScript.moduleGenTarget = oldModuleGenTarget;
}
}
}
export function compileUnits(units: TestCaseParser.TestUnitData[], callback: (res: Compiler.CompilerResult) => void , settingsCallback?: () => void ) {
var lastUnit = units[units.length - 1];
var unitName = switchToForwardSlashes(lastUnit.name).match(/[^\/]*$/)[0];
var dependencies = units.slice(0, units.length - 1);
var compilationContext = Harness.Compiler.defineCompilationContextForTest(unitName, dependencies);
compileUnit(lastUnit.content, unitName, callback, settingsCallback, compilationContext, lastUnit.references);
}
export function emitToOutfile(outfile: WriterAggregator) {
compiler.emitToOutfile(outfile);
}
export function emit(ioHost: TypeScript.EmitterIOHost, usePullEmitter?: boolean) {
compiler.emit(ioHost, usePullEmitter);
}
export function compileString(code: string, unitName: string, callback: (res: Compiler.CompilerResult) => void , context?: CompilationContext, references?: TypeScript.IFileReference[]) {
var scripts: TypeScript.Script[] = [];
reset();
if (context) {
context.preCompile();
}
var isDeclareFile = Harness.Compiler.isDeclareFile(unitName);
// for single file tests just add them as using the old '0.ts' naming scheme
var uName = context ? unitName : ((isDeclareFile) ? '0.d.ts' : '0.ts');
scripts.push(addUnit(code, uName, false, isDeclareFile, references));
compile(code, uName);
var errors;
if (usePull) {
// TODO: no emit support with pull yet
errors = compiler.pullGetErrorsForFile(uName);
emit(stdout, true);
}
else {
errors = stderr.lines;
emit(stdout, false);
//output decl file
compiler.emitDeclarations();
}
if (context) {
context.postCompile();
}
callback(new CompilerResult(stdout.toArray(), errors, scripts));
}
/** Returns a set of functions which can be later executed to add and remove given dependencies to the compiler so that
* a file can be successfully compiled. These functions will add/remove named units and code to the compiler for each dependency.
*/
export function defineCompilationContextForTest(filename: string, dependencies: TestCaseParser.TestUnitData[]): CompilationContext {
// if the given file has no dependencies, there is no context to return, it can be compiled without additional work
if (dependencies.length == 0) {
return null;
} else {
var addedFiles = [];
var precompile = () => {
// REVIEW: if any dependency has a triple slash reference then does postCompile potentially have to do a recreate since we can't update references with updateUnit?
// easy enough to do if so, prefer to avoid the recreate cost until it proves to be an issue
dependencies.forEach(dep => {
addUnit(dep.content, dep.name, false, Harness.Compiler.isDeclareFile(dep.name));
addedFiles.push(dep.name);
});
};
var postcompile = () => {
addedFiles.forEach(file => {
updateUnit('', file);
});
};
var context = {
filename: filename,
preCompile: precompile,
postCompile: postcompile
};
return context;
}
}
}
/** Parses the test cases files
* extracts options and individual files in a multifile test
*/
export module TestCaseParser {
/** all the necesarry information to set the right compiler settings */
export interface CompilerSetting {
flag: string;
value: string;
}
/** All the necessary information to turn a multi file test into useful units for later compilation */
export interface TestUnitData {
content: string;
name: string;
originalFilePath: string;
references: TypeScript.IFileReference[];
}
// Regex for parsing options in the format "@Alpha: Value of any sort"
private optionRegex = /^[\/]{2}\s*@(\w+):\s*(\S*)/gm; // multiple matches on multiple lines
// List of allowed metadata names
var fileMetadataNames = ["filename", "comments", "declaration", "module", "nolib", "sourcemap", "target", "out"];
function extractCompilerSettings(content: string): CompilerSetting[] {
var opts = [];
var match;
while ((match = optionRegex.exec(content)) != null) {
opts.push({ flag: match[1], value: match[2] });
}
return opts;
}
/** Given a test file containing // @Filename directives, return an array of named units of code to be added to an existing compiler instance */
export function makeUnitsFromTest(code: string, filename: string): { settings: CompilerSetting[]; testUnitData: TestUnitData[]; } {
var settings = extractCompilerSettings(code);
// List of all the subfiles we've parsed out
var files: TestUnitData[] = [];
var lines = splitContentByNewlines(code);
// Stuff related to the subfile we're parsing
var currentFileContent: string = null;
var currentFileOptions = {};
var currentFileName = null;
var refs: TypeScript.IFileReference[] = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var isTripleSlashReference = /[\/]{3}\s*<reference path/.test(line);
var testMetaData = optionRegex.exec(line);
// Triple slash references need to be tracked as they are added to the compiler as an additional parameter to addUnit
if (isTripleSlashReference) {
var isRef = line.match(/reference\spath='(\w*_?\w*\.?d?\.ts)'/);
if (isRef) {
var ref = {
minChar: 0,
limChar: 0,
startLine:0,
startCol:0,
path: isRef[1],
isResident: false
};
refs.push(ref);
}
} else if (testMetaData) {
// Comment line, check for global/file @options and record them
optionRegex.lastIndex = 0;
var fileNameIndex = fileMetadataNames.indexOf(testMetaData[1].toLowerCase());
if (fileNameIndex == -1) {
throw new Error('Unrecognized metadata name "' + testMetaData[1] + '". Available file metadata names are: ' + fileMetadataNames.join(', '));
} else if (fileNameIndex == 0) {
currentFileOptions[testMetaData[1]] = testMetaData[2];
} else {
continue;
}
// New metadata statement after having collected some code to go with the previous metadata
if (currentFileName) {
// Store result file
var newTestFile =
{
content: currentFileContent,
name: currentFileName,
fileOptions: currentFileOptions,
originalFilePath: filename,
references: refs
};
files.push(newTestFile);
// Reset local data
currentFileContent = null;
currentFileOptions = {};
currentFileName = testMetaData[2];
refs = [];
} else {
// First metadata marker in the file
currentFileName = testMetaData[2];
}
} else {
// Subfile content line
// Append to the current subfile content, inserting a newline needed
if (currentFileContent === null) {
currentFileContent = '';
} else {
// End-of-line
currentFileContent = currentFileContent + '\n';
}
currentFileContent = currentFileContent + line;
}
}
// normalize the filename for the single file case
currentFileName = files.length > 0 ? currentFileName : '0.ts';
// EOF, push whatever remains
var newTestFile = {
content: currentFileContent || '',
name: currentFileName,
fileOptions: currentFileOptions,
originalFilePath: filename,
references: refs
};
files.push(newTestFile);
return { settings: settings, testUnitData: files };
}
}
export class ScriptInfo {
public version: number;
public editRanges: { length: number; editRange: TypeScript.ScriptEditRange; }[] = [];
constructor(public name: string, public content: string, public isResident: boolean, public maxScriptVersions: number) {
this.version = 1;
}
public updateContent(content: string, isResident: boolean) {
this.editRanges = [];
this.content = content;
this.isResident = isResident;
this.version++;
}
public editContent(minChar: number, limChar: number, newText: string) {
// Apply edits
var prefix = this.content.substring(0, minChar);
var middle = newText;
var suffix = this.content.substring(limChar);
this.content = prefix + middle + suffix;
// Store edit range + new length of script
this.editRanges.push({
length: this.content.length,
editRange: new TypeScript.ScriptEditRange(minChar, limChar, (limChar - minChar) + newText.length)
});
if (this.editRanges.length > this.maxScriptVersions) {
this.editRanges.splice(0, this.maxScriptVersions - this.editRanges.length);
}
// Update version #
this.version++;
}
public getEditRangeSinceVersion(version: number): TypeScript.ScriptEditRange {
if (this.version == version) {
// No edits!
return null;
}
var initialEditRangeIndex = this.editRanges.length - (this.version - version);
if (initialEditRangeIndex < 0 || initialEditRangeIndex >= this.editRanges.length) {
// Too far away from what we know
return TypeScript.ScriptEditRange.unknown();
}
var entries = this.editRanges.slice(initialEditRangeIndex);
var minDistFromStart = entries.map(x => x.editRange.minChar).reduce((prev, current) => Math.min(prev, current));
var minDistFromEnd = entries.map(x => x.length - x.editRange.limChar).reduce((prev, current) => Math.min(prev, current));
var aggDelta = entries.map(x => x.editRange.delta).reduce((prev, current) => prev + current);
return new TypeScript.ScriptEditRange(minDistFromStart, entries[0].length - minDistFromEnd, aggDelta);
}
}
export class TypeScriptLS implements Services.ILanguageServiceShimHost {
private ls: Services.ILanguageServiceShim = null;
public scripts: ScriptInfo[] = [];
public maxScriptVersions = 100;
public addDefaultLibrary() {
this.addScript("lib.d.ts", Harness.Compiler.libText, true);
}
public addFile(name: string, isResident = false) {
var code: string = readFile(name);
this.addScript(name, code, isResident);
}
public addScript(name: string, content: string, isResident = false) {
var script = new ScriptInfo(name, content, isResident, this.maxScriptVersions);
this.scripts.push(script);
}
public updateScript(name: string, content: string, isResident = false) {
for (var i = 0; i < this.scripts.length; i++) {
if (this.scripts[i].name == name) {
this.scripts[i].updateContent(content, isResident);
return;
}
}
this.addScript(name, content, isResident);
}
public editScript(name: string, minChar: number, limChar: number, newText: string) {
for (var i = 0; i < this.scripts.length; i++) {
if (this.scripts[i].name == name) {
this.scripts[i].editContent(minChar, limChar, newText);
return;
}
}
throw new Error("No script with name '" + name + "'");
}
public getScriptContent(scriptIndex: number): string {
return this.scripts[scriptIndex].content;
}
//////////////////////////////////////////////////////////////////////
// ILogger implementation
//
public information(): boolean { return false; }
public debug(): boolean { return true; }
public warning(): boolean { return true; }
public error(): boolean { return true; }
public fatal(): boolean { return true; }
public log(s: string): void {
// For debugging...
//IO.printLine("TypeScriptLS:" + s);
}
//////////////////////////////////////////////////////////////////////
// ILanguageServiceShimHost implementation
//
public getCompilationSettings(): string/*json for Tools.CompilationSettings*/ {
return ""; // i.e. default settings
}
public getScriptCount(): number {
return this.scripts.length;
}
public getScriptSourceText(scriptIndex: number, start: number, end: number): string {
return this.scripts[scriptIndex].content.substring(start, end);
}
public getScriptSourceLength(scriptIndex: number): number {
return this.scripts[scriptIndex].content.length;
}
public getScriptId(scriptIndex: number): string {
return this.scripts[scriptIndex].name;
}
public getScriptIsResident(scriptIndex: number): boolean {
return this.scripts[scriptIndex].isResident;
}
public getScriptVersion(scriptIndex: number): number {
return this.scripts[scriptIndex].version;
}
public getScriptEditRangeSinceVersion(scriptIndex: number, scriptVersion: number): string {
var range = this.scripts[scriptIndex].getEditRangeSinceVersion(scriptVersion);
var result = (range.minChar + "," + range.limChar + "," + range.delta);
return result;
}
/** Return a new instance of the language service shim, up-to-date wrt to typecheck.
* To access the non-shim (i.e. actual) language service, use the "ls.languageService" property.
*/
public getLanguageService(): Services.ILanguageServiceShim {
var ls = new Services.TypeScriptServicesFactory().createLanguageServiceShim(this);
ls.refresh(true);
this.ls = ls;
return ls;
}
/** Parse file given its source text */
public parseSourceText(fileName: string, sourceText: TypeScript.ISourceText): TypeScript.Script {
var parser = new TypeScript.Parser();
parser.setErrorRecovery(null);
parser.errorCallback = (a, b, c, d) => { };
var script = parser.parse(sourceText, fileName, 0);
return script;
}
/** Parse a file on disk given its filename */
public parseFile(fileName: string) {
var sourceText = new TypeScript.StringSourceText(IO.readFile(fileName))
return this.parseSourceText(fileName, sourceText);
}
/**
* @param line 1 based index
* @param col 1 based index
*/
public lineColToPosition(fileName: string, line: number, col: number): number {
var script = this.ls.languageService.getScriptAST(fileName);
assert.notNull(script);
assert.is(line >= 1);
assert.is(col >= 1);
assert.is(line <= script.locationInfo.lineMap.length);
return TypeScript.getPositionFromZeroBasedLineColumn(script, line - 1, col - 1);
}
/**
* @param line 0 based index
* @param col 0 based index
*/
public positionToZeroBasedLineCol(fileName: string, position: number): TypeScript.ILineCol {
var script = this.ls.languageService.getScriptAST(fileName);
assert.notNull(script);
var result = TypeScript.getZeroBasedLineColumnFromPosition(script, position);
assert.is(result.line >= 0);
assert.is(result.col >= 0);
return result;
}
/** Verify that applying edits to sourceFileName result in the content of the file baselineFileName */
public checkEdits(sourceFileName: string, baselineFileName: string, edits: Services.TextEdit[]) {
var script = readFile(sourceFileName);
var formattedScript = this.applyEdits(script, edits);
var baseline = readFile(baselineFileName);
assert.noDiff(formattedScript, baseline);
assert.equal(formattedScript, baseline);
}
/** Apply an array of text edits to a string, and return the resulting string. */
public applyEdits(content: string, edits: Services.TextEdit[]): string {
var result = content;
edits = this.normalizeEdits(edits);
for (var i = edits.length - 1; i >= 0; i--) {
var edit = edits[i];
var prefix = result.substring(0, edit.minChar);
var middle = edit.text;
var suffix = result.substring(edit.limChar);
result = prefix + middle + suffix;
}
return result;
}
/** Normalize an array of edits by removing overlapping entries and sorting entries on the minChar position. */
private normalizeEdits(edits: Services.TextEdit[]): Services.TextEdit[] {
var result: Services.TextEdit[] = [];
function mapEdits(edits: Services.TextEdit[]): { edit: Services.TextEdit; index: number; }[] {
var result = [];
for (var i = 0; i < edits.length; i++) {
result.push({ edit: edits[i], index: i });
}
return result;
}
var temp = mapEdits(edits).sort(function (a, b) {
var result = a.edit.minChar - b.edit.minChar;
if (result == 0)
result = a.index - b.index;
return result;
});
var current = 0;
var next = 1;
while (current < temp.length) {
var currentEdit = temp[current].edit;
// Last edit
if (next >= temp.length) {
result.push(currentEdit);
current++;
continue;
}
var nextEdit = temp[next].edit;
var gap = nextEdit.minChar - currentEdit.limChar;
// non-overlapping edits
if (gap >= 0) {
result.push(currentEdit);
current = next;
next++;
continue;
}
// overlapping edits: for now, we only support ignoring an next edit
// entirely contained in the current edit.
if (currentEdit.limChar >= nextEdit.limChar) {
next++;
continue;
}
else {
throw new Error("Trying to apply overlapping edits");
}
}
return result;
}
public getHostSettings(): string {
return JSON.stringify({ usePullLanguageService: usePull });
}
}
// Describe/it definitions
export function describe(description: string, block: () => any) {
var newScenario = new Scenario(description, block);
if (Runnable.currentStack.length === 0) {
Runnable.currentStack.push(currentRun);
}
Runnable.currentStack[Runnable.currentStack.length - 1].addChild(newScenario);
}
export function it(description: string, block: () => void ) {
var testCase = new TestCase(description, block);
Runnable.currentStack[Runnable.currentStack.length - 1].addChild(testCase);
}
export function run() {
if (typeof process !== "undefined") {
process.on('uncaughtException', Runnable.handleError);
}
Baseline.reset();
currentRun.run();
}
/** Runs TypeScript or Javascript code. */
export module Runner {
export function runCollateral(path: string, callback: (error: Error, result: any) => void ) {
path = switchToForwardSlashes(path);
runString(readFile(path), path.match(/[^\/]*$/)[0], callback);
}
export function runJSString(code: string, callback: (error: Error, result: any) => void ) {
// List of names that get overriden by various test code we eval
var dangerNames: any = ['Array'];
var globalBackup: any = {};
var n: string = null;
for (n in dangerNames) {
globalBackup[dangerNames[n]] = global[dangerNames[n]];
}
try {
var res = eval(code);
for (n in dangerNames) {
global[dangerNames[n]] = globalBackup[dangerNames[n]];
}
callback(null, res);
} catch (e) {
for (n in dangerNames) {
global[dangerNames[n]] = globalBackup[dangerNames[n]];
}
callback(e, null);
}
}
export function runString(code: string, unitName: string, callback: (error: Error, result: any) => void ) {
Compiler.compileString(code, unitName, function (res) {
runJSString(res.code, callback);
});
}
}
/** Support class for baseline files */
export module Baseline {
var reportFilename = 'baseline-report.html';
var firstRun = true;
var htmlTrailer = '</body></html>';
var htmlLeader = '<html><head><title>Baseline Report</title>';
htmlLeader += ("<style>");
htmlLeader += '\r\n' + (".code { font: 9pt 'Courier New'; }");
htmlLeader += '\r\n' + (".old { background-color: #EE1111; }");
htmlLeader += '\r\n' + (".new { background-color: #FFFF11; }");
htmlLeader += '\r\n' + (".from { background-color: #EE1111; color: #1111EE; }");
htmlLeader += '\r\n' + (".to { background-color: #EEEE11; color: #1111EE; }");
htmlLeader += '\r\n' + ("h2 { margin-bottom: 0px; }");
htmlLeader += '\r\n' + ("h2 { padding-bottom: 0px; }");
htmlLeader += '\r\n' + ("h4 { font-weight: normal; }");
htmlLeader += '\r\n' + ("</style>");
export interface BaselineOptions {
LineEndingSensitive?: boolean;
}
function localPath(filename: string) {
if (global.runners[0].testType === 'prototyping') {
return Harness.userSpecifiedroot + 'tests/baselines/prototyping/local/' + filename;
}
else {
return Harness.userSpecifiedroot + 'tests/baselines/local/' + filename;
}
}
function referencePath(filename: string) {
if (global.runners[0].testType === 'prototyping') {
return Harness.userSpecifiedroot + 'tests/baselines/prototyping/reference/' + filename;
}
else {
return Harness.userSpecifiedroot + 'tests/baselines/reference/' + filename;
}
}
export function reset() {
if (IO.fileExists(reportFilename)) {
IO.deleteFile(reportFilename);
}
}
function prepareBaselineReport(): string {
var reportContent = htmlLeader;
// Delete the baseline-report.html file if needed
if (IO.fileExists(reportFilename)) {
reportContent = IO.readFile(reportFilename);
reportContent = reportContent.replace(htmlTrailer, '');
} else {
reportContent = htmlLeader;
}
return reportContent;
}
function generateActual(actualFilename: string, generateContent: () => string): string {
// Create folders if needed
IO.createDirectory(IO.dirName(IO.dirName(actualFilename)));
IO.createDirectory(IO.dirName(actualFilename));
// Delete the actual file in case it fails
if (IO.fileExists(actualFilename)) {
IO.deleteFile(actualFilename);
}
var actual = generateContent();
if (actual === undefined) {
throw new Error('The generated content was "undefined". Return "null" if no baselining is required."');
}
// Store the content in the 'local' folder so we
// can accept it later (manually)
if (actual !== null) {
IO.writeFile(actualFilename, actual);
}
return actual;
}
function compareToBaseline(actual: string, relativeFilename: string, opts: BaselineOptions) {
// actual is now either undefined (the generator had an error), null (no file requested),
// or some real output of the function
if (actual === undefined) {
// Nothing to do
return;
}
var refFilename = referencePath(relativeFilename);
if (actual === null) {
actual = '<no content>';
}
var expected = '<no content>';
if (IO.fileExists(refFilename)) {
expected = IO.readFile(refFilename);
}
var lineEndingSensitive = opts && opts.LineEndingSensitive;
if (!lineEndingSensitive) {
expected = expected.replace(/\r\n?/g, '\n')
actual = actual.replace(/\r\n?/g, '\n')
}
return { expected: expected, actual: actual };
}
function writeComparison(expected: string, actual: string, relativeFilename: string, actualFilename: string, descriptionForDescribe: string) {
if (expected != actual) {
// Overwrite & issue error
var errMsg = 'The baseline file ' + relativeFilename + ' has changed. Please refer to baseline-report.html and ';
errMsg += 'either fix the regression (if unintended) or run nmake baseline-accept (if intended).'
var refFilename = referencePath(relativeFilename);
// Append diff to the report
var diff = new Diff.StringDiff(expected, actual);
var header = '<h2>' + descriptionForDescribe + '</h2>';
header += '<h4>Left file: ' + actualFilename + '; Right file: ' + refFilename + '</h4>';
var trailer = '<hr>';
var reportContentSoFar = prepareBaselineReport();
reportContentSoFar = reportContentSoFar + header + '<div class="code">' + diff.mergedHtml + '</div>' + trailer + htmlTrailer;
IO.writeFile(reportFilename, reportContentSoFar);
throw new Error(errMsg);
}
}
export function runBaseline(
descriptionForDescribe: string,
relativeFilename: string,
generateContent: () => string,
runImmediately? = false,
opts?: BaselineOptions) {
var actual = <string>undefined;
var actualFilename = localPath(relativeFilename);
if (runImmediately) {
var actual = generateActual(actualFilename, generateContent);
var comparison = compareToBaseline(actual, relativeFilename, opts);
writeComparison(comparison.expected, comparison.actual, relativeFilename, actualFilename, descriptionForDescribe);
} else {
describe(descriptionForDescribe, () => {
var actual: string;
it('Can generate the content without error', () => {
actual = generateActual(actualFilename, generateContent);
});
it('Matches the baseline file', () => {
var comparison = compareToBaseline(actual, relativeFilename, opts);
writeComparison(comparison.expected, comparison.actual, relativeFilename, actualFilename, descriptionForDescribe);
});
});
}
}
}
var currentRun = new Run();
global.describe = describe;
global.run = run;
global.it = it;
global.assert = Harness.Assert;
}
//// [parserharness.js]
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
};
///<reference path='..\compiler\io.ts'/>
///<reference path='..\compiler\typescript.ts'/>
///<reference path='..\services\typescriptServices.ts' />
///<reference path='diff.ts'/>
function switchToForwardSlashes(path) {
return path.replace(/\\/g, "/");
}
function filePath(fullPath) {
fullPath = switchToForwardSlashes(fullPath);
var components = fullPath.split("/");
var path = components.slice(0, components.length - 1);
return path.join("/") + "/";
}
var typescriptServiceFileName = filePath(IO.getExecutingFilePath()) + "typescriptServices.js";
var typescriptServiceFile = IO.readFile(typescriptServiceFileName);
if (typeof ActiveXObject === "function") {
eval(typescriptServiceFile);
}
else if (typeof require === "function") {
var vm = require('vm');
vm.runInThisContext(typescriptServiceFile, 'typescriptServices.js');
}
else {
throw new Error('Unknown context');
}
var Harness;
(function (Harness) {
// Settings
Harness.userSpecifiedroot = "";
var global = Function("return this").call(null);
Harness.usePull = false;
// Assert functions
var Assert;
(function (Assert) {
Assert.bugIds = [];
Assert.throwAssertError = function (error) {
throw error;
};
// Marks that the current scenario is impacted by a bug
function bug(id) {
if (Assert.bugIds.indexOf(id) < 0) {
Assert.bugIds.push(id);
}
}
Assert.bug = bug;
// If there are any bugs in the test code, mark the scenario as impacted appropriately
function bugs(content) {
var bugs = content.match(/\bbug (\d+)/i);
if (bugs) {
bugs.forEach(function (bug) { return assert.bug(bug); });
}
}
Assert.bugs = bugs;
function is(result, msg) {
if (!result) {
Assert.throwAssertError(new Error(msg || "Expected true, got false."));
}
}
Assert.is = is;
function arrayLengthIs(arr, length) {
if (arr.length != length) {
var actual = '';
arr.forEach(function (n) { return actual = actual + '\n ' + n.toString(); });
Assert.throwAssertError(new Error('Expected array to have ' + length + ' elements. Actual elements were:' + actual));
}
}
Assert.arrayLengthIs = arrayLengthIs;
function equal(actual, expected) {
if (actual !== expected) {
Assert.throwAssertError(new Error("Expected " + actual + " to equal " + expected));
}
}
Assert.equal = equal;
function notEqual(actual, expected) {
if (actual === expected) {
Assert.throwAssertError(new Error("Expected " + actual + " to *not* equal " + expected));
}
}
Assert.notEqual = notEqual;
function notNull(result) {
if (result === null) {
Assert.throwAssertError(new Error("Expected " + result + " to *not* be null"));
}
}
Assert.notNull = notNull;
function compilerWarning(result, line, column, desc) {
if (!result.isErrorAt(line, column, desc)) {
var actual = '';
result.errors.forEach(function (err) {
actual = actual + '\n ' + err.toString();
});
Assert.throwAssertError(new Error("Expected compiler warning at (" + line + ", " + column + "): " + desc + "\nActual errors follow: " + actual));
}
}
Assert.compilerWarning = compilerWarning;
function noDiff(text1, text2) {
text1 = text1.replace(/^\s+|\s+$/g, "").replace(/\r\n?/g, "\n");
text2 = text2.replace(/^\s+|\s+$/g, "").replace(/\r\n?/g, "\n");
if (text1 !== text2) {
var errorString = "";
var text1Lines = text1.split(/\n/);
var text2Lines = text2.split(/\n/);
for (var i = 0; i < text1Lines.length; i++) {
if (text1Lines[i] !== text2Lines[i]) {
errorString += "Difference at line " + (i + 1) + ":\n";
errorString += " Left File: " + text1Lines[i] + "\n";
errorString += " Right File: " + text2Lines[i] + "\n\n";
}
}
Assert.throwAssertError(new Error(errorString));
}
}
Assert.noDiff = noDiff;
function arrayContains(arr, contains) {
var found;
for (var i = 0; i < contains.length; i++) {
found = false;
for (var j = 0; j < arr.length; j++) {
if (arr[j] === contains[i]) {
found = true;
break;
}
}
if (!found) {
Assert.throwAssertError(new Error("Expected array to contain \"" + contains[i] + "\""));
}
}
}
Assert.arrayContains = arrayContains;
function arrayContainsOnce(arr, filter) {
var foundCount = 0;
for (var i = 0; i < arr.length; i++) {
if (filter(arr[i])) {
foundCount++;
}
}
if (foundCount !== 1) {
Assert.throwAssertError(new Error("Expected array to match element only once (instead of " + foundCount + " times)"));
}
}
Assert.arrayContainsOnce = arrayContainsOnce;
})(Assert = Harness.Assert || (Harness.Assert = {}));
/** Splits the given string on \r\n or on only \n if that fails */
function splitContentByNewlines(content) {
// Split up the input file by line
// Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so
// we have to string-based splitting instead and try to figure out the delimiting chars
var lines = content.split('\r\n');
if (lines.length === 1) {
lines = content.split('\n');
}
return lines;
}
Harness.splitContentByNewlines = splitContentByNewlines;
/** Reads a file under /tests */
function readFile(path) {
if (path.indexOf('tests') < 0) {
path = "tests/" + path;
}
var content = IO.readFile(Harness.userSpecifiedroot + path);
if (content == null) {
throw new Error("failed to read file at: '" + Harness.userSpecifiedroot + path + "'");
}
return content;
}
Harness.readFile = readFile;
var Logger = (function () {
function Logger() {
}
Logger.prototype.start = function (fileName, priority) { };
Logger.prototype.end = function (fileName) { };
Logger.prototype.scenarioStart = function (scenario) { };
Logger.prototype.scenarioEnd = function (scenario, error) { };
Logger.prototype.testStart = function (test) { };
Logger.prototype.pass = function (test) { };
Logger.prototype.bug = function (test) { };
Logger.prototype.fail = function (test) { };
Logger.prototype.error = function (test, error) { };
Logger.prototype.comment = function (comment) { };
Logger.prototype.verify = function (test, passed, actual, expected, message) { };
return Logger;
})();
Harness.Logger = Logger;
// Logger-related functions
var loggers = [];
function registerLogger(logger) {
loggers.push(logger);
}
Harness.registerLogger = registerLogger;
function emitLog(field) {
var params = [];
for (var _i = 1; _i < arguments.length; _i++) {
params[_i - 1] = arguments[_i];
}
for (var i = 0; i < loggers.length; i++) {
if (typeof loggers[i][field] === 'function') {
loggers[i][field].apply(loggers[i], params);
}
}
}
Harness.emitLog = emitLog;
var Runnable = (function () {
function Runnable(description, block) {
this.description = description;
this.block = block;
// The error, if any, that occurred when running 'block'
this.error = null;
// Whether or not this object has any failures (including in its descendants)
this.passed = null;
// A list of bugs impacting this object
this.bugs = [];
// A list of all our child Runnables
this.children = [];
}
Runnable.prototype.addChild = function (child) {
this.children.push(child);
};
/** Call function fn, which may take a done function and may possibly execute
* asynchronously, calling done when finished. Returns true or false depending
* on whether the function was asynchronous or not.
*/
Runnable.prototype.call = function (fn, done) {
var isAsync = true;
try {
if (fn.length === 0) {
// No async.
fn();
done();
return false;
}
else {
// Possibly async
Runnable.pushGlobalErrorHandler(done);
fn(function () {
isAsync = false; // If we execute synchronously, this will get called before the return below.
Runnable.popGlobalErrorHandler();
done();
});
return isAsync;
}
}
catch (e) {
done(e);
return false;
}
};
Runnable.prototype.run = function (done) { };
Runnable.prototype.runBlock = function (done) {
return this.call(this.block, done);
};
Runnable.prototype.runChild = function (index, done) {
var _this = this;
return this.call((function (done) { return _this.children[index].run(done); }), done);
};
Runnable.pushGlobalErrorHandler = function (done) {
errorHandlerStack.push(function (e) {
done(e);
});
};
Runnable.popGlobalErrorHandler = function () {
errorHandlerStack.pop();
};
Runnable.handleError = function (e) {
if (errorHandlerStack.length === 0) {
IO.printLine('Global error: ' + e);
}
else {
errorHandlerStack[errorHandlerStack.length - 1](e);
}
};
// The current stack of Runnable objects
Runnable.currentStack = [];
Runnable.errorHandlerStack = [];
return Runnable;
})();
Harness.Runnable = Runnable;
var TestCase = (function (_super) {
__extends(TestCase, _super);
function TestCase(description, block) {
_super.call(this, description, block);
this.description = description;
this.block = block;
}
TestCase.prototype.addChild = function (child) {
throw new Error("Testcases may not be nested inside other testcases");
};
/** Run the test case block and fail the test if it raised an error. If no error is raised, the test passes. */
TestCase.prototype.run = function (done) {
var that = this;
Runnable.currentStack.push(this);
emitLog('testStart', { desc: this.description });
if (this.block) {
var async = this.runBlock(function (e) {
if (e) {
that.passed = false;
that.error = e;
emitLog('error', { desc: this.description, pass: false }, e);
}
else {
that.passed = true;
emitLog('pass', { desc: this.description, pass: true });
}
Runnable.currentStack.pop();
done();
});
}
};
return TestCase;
})(Runnable);
Harness.TestCase = TestCase;
var Scenario = (function (_super) {
__extends(Scenario, _super);
function Scenario(description, block) {
_super.call(this, description, block);
this.description = description;
this.block = block;
}
/** Run the block, and if the block doesn't raise an error, run the children. */
Scenario.prototype.run = function (done) {
var that = this;
Runnable.currentStack.push(this);
emitLog('scenarioStart', { desc: this.description });
var async = this.runBlock(function (e) {
Runnable.currentStack.pop();
if (e) {
that.passed = false;
that.error = e;
var metadata = { id: undefined, desc: this.description, pass: false, bugs: assert.bugIds };
// Report all bugs affecting this scenario
assert.bugIds.forEach(function (desc) { return emitLog('bug', metadata, desc); });
emitLog('scenarioEnd', metadata, e);
done();
}
else {
that.passed = true; // so far so good.
that.runChildren(done);
}
});
};
/** Run the children of the scenario (other scenarios and test cases). If any fail,
* set this scenario to failed. Synchronous tests will run synchronously without
* adding stack frames.
*/
Scenario.prototype.runChildren = function (done, index) {
if (index === void 0) { index = 0; }
var that = this;
var async = false;
for (; index < this.children.length; index++) {
async = this.runChild(index, function (e) {
that.passed = that.passed && that.children[index].passed;
if (async)
that.runChildren(done, index + 1);
});
if (async)
return;
}
var metadata = { id: undefined, desc: this.description, pass: this.passed, bugs: assert.bugIds };
// Report all bugs affecting this scenario
assert.bugIds.forEach(function (desc) { return emitLog('bug', metadata, desc); });
emitLog('scenarioEnd', metadata);
done();
};
return Scenario;
})(Runnable);
Harness.Scenario = Scenario;
var Run = (function (_super) {
__extends(Run, _super);
function Run() {
_super.call(this, 'Test Run', null);
}
Run.prototype.run = function () {
emitLog('start');
this.runChildren();
};
Run.prototype.runChildren = function (index) {
if (index === void 0) { index = 0; }
var async = false;
var that = this;
for (; index < this.children.length; index++) {
// Clear out bug descriptions
assert.bugIds = [];
async = this.runChild(index, function (e) {
if (async) {
that.runChildren(index + 1);
}
});
if (async) {
return;
}
}
Perf.runBenchmarks();
emitLog('end');
};
return Run;
})(Runnable);
Harness.Run = Run;
// Performance test
var Perf;
(function (Perf) {
var Clock;
(function (Clock) {
Clock.now;
Clock.resolution;
if (typeof WScript !== "undefined" && typeof global['WScript'].InitializeProjection !== "undefined") {
// Running in JSHost.
global['WScript'].InitializeProjection();
Clock.now = function () {
return TestUtilities.QueryPerformanceCounter();
};
Clock.resolution = TestUtilities.QueryPerformanceFrequency();
}
else {
Clock.now = function () {
return Date.now();
};
Clock.resolution = 1000;
}
})(Clock = Perf.Clock || (Perf.Clock = {}));
var Timer = (function () {
function Timer() {
this.time = 0;
}
Timer.prototype.start = function () {
this.time = 0;
this.startTime = Clock.now();
};
Timer.prototype.end = function () {
// Set time to MS.
this.time = (Clock.now() - this.startTime) / Clock.resolution * 1000;
};
return Timer;
})();
Perf.Timer = Timer;
var Dataset = (function () {
function Dataset() {
this.data = [];
}
Dataset.prototype.add = function (value) {
this.data.push(value);
};
Dataset.prototype.mean = function () {
var sum = 0;
for (var i = 0; i < this.data.length; i++) {
sum += this.data[i];
}
return sum / this.data.length;
};
Dataset.prototype.min = function () {
var min = this.data[0];
for (var i = 1; i < this.data.length; i++) {
if (this.data[i] < min) {
min = this.data[i];
}
}
return min;
};
Dataset.prototype.max = function () {
var max = this.data[0];
for (var i = 1; i < this.data.length; i++) {
if (this.data[i] > max) {
max = this.data[i];
}
}
return max;
};
Dataset.prototype.stdDev = function () {
var sampleMean = this.mean();
var sumOfSquares = 0;
for (var i = 0; i < this.data.length; i++) {
sumOfSquares += Math.pow(this.data[i] - sampleMean, 2);
}
return Math.sqrt(sumOfSquares / this.data.length);
};
return Dataset;
})();
Perf.Dataset = Dataset;
// Base benchmark class with some defaults.
var Benchmark = (function () {
function Benchmark() {
this.iterations = 10;
this.description = "";
this.results = {};
}
Benchmark.prototype.bench = function (subBench) { };
Benchmark.prototype.before = function () { };
Benchmark.prototype.beforeEach = function () { };
Benchmark.prototype.after = function () { };
Benchmark.prototype.afterEach = function () { };
Benchmark.prototype.addTimingFor = function (name, timing) {
this.results[name] = this.results[name] || new Dataset();
this.results[name].add(timing);
};
return Benchmark;
})();
Perf.Benchmark = Benchmark;
Perf.benchmarks = [];
var timeFunction;
timeFunction = function (benchmark, description, name, f) {
if (description === void 0) { description = benchmark.description; }
if (name === void 0) { name = ''; }
if (f === void 0) { f = benchmark.bench; }
var t = new Timer();
t.start();
var subBenchmark = function (name, f) {
timeFunction(benchmark, description, name, f);
};
f.call(benchmark, subBenchmark);
t.end();
benchmark.addTimingFor(name, t.time);
};
function runBenchmarks() {
for (var i = 0; i < Perf.benchmarks.length; i++) {
var b = new Perf.benchmarks[i]();
var t = new Timer();
b.before();
for (var j = 0; j < b.iterations; j++) {
b.beforeEach();
timeFunction(b);
b.afterEach();
}
b.after();
for (var prop in b.results) {
var description = b.description + (prop ? ": " + prop : '');
emitLog('testStart', { desc: description });
emitLog('pass', {
desc: description, pass: true, perfResults: {
mean: b.results[prop].mean(),
min: b.results[prop].min(),
max: b.results[prop].max(),
stdDev: b.results[prop].stdDev(),
trials: b.results[prop].data
}
});
}
}
}
Perf.runBenchmarks = runBenchmarks;
// Replace with better type when classes are assignment compatible with
// the below type.
// export function addBenchmark(BenchmarkClass: {new(): Benchmark;}) {
function addBenchmark(BenchmarkClass) {
Perf.benchmarks.push(BenchmarkClass);
}
Perf.addBenchmark = addBenchmark;
})(Perf = Harness.Perf || (Harness.Perf = {}));
/** Functionality for compiling TypeScript code */
var Compiler;
(function (Compiler) {
/** Aggregate various writes into a single array of lines. Useful for passing to the
* TypeScript compiler to fill with source code or errors.
*/
var WriterAggregator = (function () {
function WriterAggregator() {
this.lines = [];
this.currentLine = "";
}
WriterAggregator.prototype.Write = function (str) {
this.currentLine += str;
};
WriterAggregator.prototype.WriteLine = function (str) {
this.lines.push(this.currentLine + str);
this.currentLine = "";
};
WriterAggregator.prototype.Close = function () {
if (this.currentLine.length > 0) {
this.lines.push(this.currentLine);
}
this.currentLine = "";
};
WriterAggregator.prototype.reset = function () {
this.lines = [];
this.currentLine = "";
};
return WriterAggregator;
})();
Compiler.WriterAggregator = WriterAggregator;
/** Mimics having multiple files, later concatenated to a single file. */
var EmitterIOHost = (function () {
function EmitterIOHost() {
this.fileCollection = {};
}
/** create file gets the whole path to create, so this works as expected with the --out parameter */
EmitterIOHost.prototype.createFile = function (s, useUTF8) {
if (this.fileCollection[s]) {
return this.fileCollection[s];
}
var writer = new Harness.Compiler.WriterAggregator();
this.fileCollection[s] = writer;
return writer;
};
EmitterIOHost.prototype.directoryExists = function (s) { return false; };
EmitterIOHost.prototype.fileExists = function (s) { return typeof this.fileCollection[s] !== 'undefined'; };
EmitterIOHost.prototype.resolvePath = function (s) { return s; };
EmitterIOHost.prototype.reset = function () { this.fileCollection = {}; };
EmitterIOHost.prototype.toArray = function () {
var result = [];
for (var p in this.fileCollection) {
if (this.fileCollection.hasOwnProperty(p)) {
var current = this.fileCollection[p];
if (current.lines.length > 0) {
if (p !== '0.js') {
current.lines.unshift('////[' + p + ']');
}
result.push({ filename: p, file: this.fileCollection[p] });
}
}
}
return result;
};
return EmitterIOHost;
})();
Compiler.EmitterIOHost = EmitterIOHost;
var libFolder = global['WScript'] ? TypeScript.filePath(global['WScript'].ScriptFullName) : (__dirname + '/');
Compiler.libText = IO ? IO.readFile(libFolder + "lib.d.ts") : '';
var stdout = new EmitterIOHost();
var stderr = new WriterAggregator();
function isDeclareFile(filename) {
return /\.d\.ts$/.test(filename);
}
Compiler.isDeclareFile = isDeclareFile;
function makeDefaultCompilerForTest(c) {
var compiler = c || new TypeScript.TypeScriptCompiler(stderr);
compiler.parser.errorRecovery = true;
compiler.settings.codeGenTarget = TypeScript.CodeGenTarget.ES5;
compiler.settings.controlFlow = true;
compiler.settings.controlFlowUseDef = true;
if (Harness.usePull) {
compiler.settings.usePull = true;
compiler.settings.useFidelity = true;
}
compiler.parseEmitOption(stdout);
TypeScript.moduleGenTarget = TypeScript.ModuleGenTarget.Synchronous;
compiler.addUnit(Harness.Compiler.libText, "lib.d.ts", true);
return compiler;
}
Compiler.makeDefaultCompilerForTest = makeDefaultCompilerForTest;
var compiler;
recreate();
// pullUpdateUnit is sufficient if an existing unit is updated, if a new unit is added we need to do a full typecheck
var needsFullTypeCheck = true;
function compile(code, filename) {
if (Harness.usePull) {
if (needsFullTypeCheck) {
compiler.pullTypeCheck(true);
needsFullTypeCheck = false;
}
else {
// requires unit to already exist in the compiler
compiler.pullUpdateUnit(new TypeScript.StringSourceText(""), filename, true);
compiler.pullUpdateUnit(new TypeScript.StringSourceText(code), filename, true);
}
}
else {
compiler.reTypeCheck();
}
}
Compiler.compile = compile;
// Types
var Type = (function () {
function Type(type, code, identifier) {
this.type = type;
this.code = code;
this.identifier = identifier;
}
Type.prototype.normalizeToArray = function (arg) {
if ((Array.isArray && Array.isArray(arg)) || arg instanceof Array)
return arg;
return [arg];
};
Type.prototype.compilesOk = function (testCode) {
var errors = null;
compileString(testCode, 'test.ts', function (compilerResult) {
errors = compilerResult.errors;
});
return errors.length === 0;
};
Type.prototype.isSubtypeOf = function (other) {
var testCode = 'class __test1__ {\n';
testCode += ' public test() {\n';
testCode += ' ' + other.code + ';\n';
testCode += ' return ' + other.identifier + ';\n';
testCode += ' }\n';
testCode += '}\n';
testCode += 'class __test2__ extends __test1__ {\n';
testCode += ' public test() {\n';
testCode += ' ' + this.code + ';\n';
testCode += ' return ' + other.identifier + ';\n';
testCode += ' }\n';
testCode += '}\n';
return this.compilesOk(testCode);
};
// TODO: Find an implementation of isIdenticalTo that works.
//public isIdenticalTo(other: Type) {
// var testCode = 'module __test1__ {\n';
// testCode += ' ' + this.code + ';\n';
// testCode += ' export var __val__ = ' + this.identifier + ';\n';
// testCode += '}\n';
// testCode += 'var __test1__val__ = __test1__.__val__;\n';
// testCode += 'module __test2__ {\n';
// testCode += ' ' + other.code + ';\n';
// testCode += ' export var __val__ = ' + other.identifier + ';\n';
// testCode += '}\n';
// testCode += 'var __test2__val__ = __test2__.__val__;\n';
// testCode += 'function __test__function__() { if(true) { return __test1__val__ }; return __test2__val__; }';
// return this.compilesOk(testCode);
//}
Type.prototype.assertSubtypeOf = function (others) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
if (!this.isSubtypeOf(others[i])) {
throw new Error("Expected " + this.type + " to be a subtype of " + others[i].type);
}
}
};
Type.prototype.assertNotSubtypeOf = function (others) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
if (this.isSubtypeOf(others[i])) {
throw new Error("Expected " + this.type + " to be a subtype of " + others[i].type);
}
}
};
//public assertIdenticalTo(other: Type) {
// if (!this.isIdenticalTo(other)) {
// throw new Error("Expected " + this.type + " to be identical to " + other.type);
// }
//}
//public assertNotIdenticalTo(other: Type) {
// if (!this.isIdenticalTo(other)) {
// throw new Error("Expected " + this.type + " to not be identical to " + other.type);
// }
//}
Type.prototype.isAssignmentCompatibleWith = function (other) {
var testCode = 'module __test1__ {\n';
testCode += ' ' + this.code + ';\n';
testCode += ' export var __val__ = ' + this.identifier + ';\n';
testCode += '}\n';
testCode += 'var __test1__val__ = __test1__.__val__;\n';
testCode += 'module __test2__ {\n';
testCode += ' export ' + other.code + ';\n';
testCode += ' export var __val__ = ' + other.identifier + ';\n';
testCode += '}\n';
testCode += 'var __test2__val__ = __test2__.__val__;\n';
testCode += '__test2__val__ = __test1__val__;';
return this.compilesOk(testCode);
};
Type.prototype.assertAssignmentCompatibleWith = function (others) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
var other = others[i];
if (!this.isAssignmentCompatibleWith(other)) {
throw new Error("Expected " + this.type + " to be assignment compatible with " + other.type);
}
}
};
Type.prototype.assertNotAssignmentCompatibleWith = function (others) {
others = this.normalizeToArray(others);
for (var i = 0; i < others.length; i++) {
var other = others[i];
if (this.isAssignmentCompatibleWith(other)) {
throw new Error("Expected " + this.type + " to not be assignment compatible with " + other.type);
}
}
};
Type.prototype.assertThisCanBeAssignedTo = function (desc, these, notThese) {
var _this = this;
it(desc + " is assignable to ", function () {
_this.assertAssignmentCompatibleWith(these);
});
it(desc + " not assignable to ", function () {
_this.assertNotAssignmentCompatibleWith(notThese);
});
};
return Type;
})();
Compiler.Type = Type;
var TypeFactory = (function () {
function TypeFactory() {
this.any = this.get('var x : any', 'x');
this.number = this.get('var x : number', 'x');
this.string = this.get('var x : string', 'x');
this.boolean = this.get('var x : boolean', 'x');
}
TypeFactory.prototype.get = function (code, target) {
var targetIdentifier = '';
var targetPosition = -1;
if (typeof target === "string") {
targetIdentifier = target;
}
else if (typeof target === "number") {
targetPosition = target;
}
else {
throw new Error("Expected string or number not " + (typeof target));
}
var errors = null;
compileString(code, 'test.ts', function (compilerResult) {
errors = compilerResult.errors;
});
if (errors.length > 0)
throw new Error("Type definition contains errors: " + errors.join(","));
var matchingIdentifiers = [];
if (!Harness.usePull) {
// This will find the requested identifier in the first script where it's present, a naive search of each member in each script,
// which means this won't play nicely if the same identifier is used in multiple units, but it will enable this to work on multi-file tests.
// m = 1 because the first script will always be lib.d.ts which we don't want to search.
for (var m = 1; m < compiler.scripts.members.length; m++) {
var script = compiler.scripts.members[m];
var enclosingScopeContext = TypeScript.findEnclosingScopeAt(new TypeScript.NullLogger(), script, new TypeScript.StringSourceText(code), 0, false);
var entries = new TypeScript.ScopeTraversal(compiler).getScopeEntries(enclosingScopeContext);
for (var i = 0; i < entries.length; i++) {
if (entries[i].name === targetIdentifier) {
matchingIdentifiers.push(new Type(entries[i].type, code, targetIdentifier));
}
}
}
}
else {
for (var m = 0; m < compiler.scripts.members.length; m++) {
var script2 = compiler.scripts.members[m];
if (script2.locationInfo.filename !== 'lib.d.ts') {
if (targetPosition > -1) {
var tyInfo = compiler.pullGetTypeInfoAtPosition(targetPosition, script2);
var name = this.getTypeInfoName(tyInfo.ast);
var foundValue = new Type(tyInfo.typeInfo, code, name);
if (!matchingIdentifiers.some(function (value) { return (value.identifier === foundValue.identifier) && (value.code === foundValue.code) && (value.type === foundValue.type); })) {
matchingIdentifiers.push(foundValue);
}
}
else {
for (var pos = 0; pos < code.length; pos++) {
var tyInfo = compiler.pullGetTypeInfoAtPosition(pos, script2);
var name = this.getTypeInfoName(tyInfo.ast);
if (name === targetIdentifier) {
var foundValue = new Type(tyInfo.typeInfo, code, targetIdentifier);
if (!matchingIdentifiers.some(function (value) { return (value.identifier === foundValue.identifier) && (value.code === foundValue.code) && (value.type === foundValue.type); })) {
matchingIdentifiers.push(foundValue);
}
}
}
}
}
}
}
if (matchingIdentifiers.length === 0) {
if (targetPosition > -1) {
throw new Error("Could not find an identifier at position " + targetPosition);
}
else {
throw new Error("Could not find an identifier " + targetIdentifier + " in any known scopes");
}
}
else if (matchingIdentifiers.length > 1) {
throw new Error("Found multiple matching identifiers for " + target);
}
else {
return matchingIdentifiers[0];
}
};
TypeFactory.prototype.getTypeInfoName = function (ast) {
var name = '';
switch (ast.nodeType) {
case TypeScript.NodeType.Name: // Type Name?
case TypeScript.NodeType.Null:
case TypeScript.NodeType.List:
case TypeScript.NodeType.Empty:
case TypeScript.NodeType.EmptyExpr:
case TypeScript.NodeType.Asg:
case TypeScript.NodeType.True:
case TypeScript.NodeType.False:
case TypeScript.NodeType.ArrayLit:
case TypeScript.NodeType.TypeRef:
break;
case TypeScript.NodeType.Super:
name = ast.text;
break;
case TypeScript.NodeType.Regex:
name = ast.text;
break;
case TypeScript.NodeType.QString:
name = ast.text;
break;
case TypeScript.NodeType.NumberLit:
name = ast.text;
break;
case TypeScript.NodeType.Return:
//name = (<TypeScript.ReturnStatement>tyInfo.ast).returnExpression.actualText; // why is this complaining?
break;
case TypeScript.NodeType.InterfaceDeclaration:
name = ast.name.actualText;
break;
case TypeScript.NodeType.ModuleDeclaration:
name = ast.name.actualText;
break;
case TypeScript.NodeType.ClassDeclaration:
name = ast.name.actualText;
break;
case TypeScript.NodeType.FuncDecl:
name = !ast.name ? "" : ast.name.actualText; // name == null for lambdas
break;
default:
// TODO: is there a reason to mess with all the special cases above and not just do this (ie take whatever property is there and works?)
var a = ast;
name = (a.id) ? (a.id.actualText) : (a.name) ? a.name.actualText : (a.text) ? a.text : '';
break;
}
return name;
};
TypeFactory.prototype.isOfType = function (expr, expectedType) {
var actualType = this.get('var _v_a_r_ = ' + expr, '_v_a_r_');
it('Expression "' + expr + '" is of type "' + expectedType + '"', function () {
assert.equal(actualType.type, expectedType);
});
};
return TypeFactory;
})();
Compiler.TypeFactory = TypeFactory;
/** Generates a .d.ts file for the given code
* @param verifyNoDeclFile pass true when the given code should generate no decl file, false otherwise
* @param unitName add the given code under thie name, else use '0.ts'
* @param compilationContext a set of functions to be run before and after compiling this code for doing things like adding dependencies first
* @param references the set of referenced files used by the given code
*/
function generateDeclFile(code, verifyNoDeclFile, unitName, compilationContext, references) {
reset();
compiler.settings.generateDeclarationFiles = true;
var oldOutputOption = compiler.settings.outputOption;
var oldEmitterIOHost = compiler.emitSettings.ioHost;
try {
if (compilationContext && compilationContext.preCompile) {
compilationContext.preCompile();
}
addUnit(code, unitName, false, false, references);
compiler.reTypeCheck();
var outputs = {};
compiler.settings.outputOption = "";
compiler.parseEmitOption({
createFile: function (fn) {
outputs[fn] = new Harness.Compiler.WriterAggregator();
return outputs[fn];
},
directoryExists: function (path) { return true; },
fileExists: function (path) { return true; },
resolvePath: function (path) { return path; }
});
compiler.emitDeclarations();
var results = null;
for (var fn in outputs) {
if (fn.indexOf('.d.ts') >= 0) {
var writer = outputs[fn];
writer.Close();
results = writer.lines.join('\n');
if (verifyNoDeclFile && results != "") {
throw new Error('Compilation should not produce ' + fn);
}
}
}
if (results) {
return results;
}
if (!verifyNoDeclFile) {
throw new Error('Compilation did not produce .d.ts files');
}
}
finally {
compiler.settings.generateDeclarationFiles = false;
compiler.settings.outputOption = oldOutputOption;
compiler.parseEmitOption(oldEmitterIOHost);
if (compilationContext && compilationContext.postCompile) {
compilationContext.postCompile();
}
var uName = unitName || '0.ts';
updateUnit('', uName);
}
return '';
}
Compiler.generateDeclFile = generateDeclFile;
/** Contains the code and errors of a compilation and some helper methods to check its status. */
var CompilerResult = (function () {
/** @param fileResults an array of strings for the filename and an ITextWriter with its code */
function CompilerResult(fileResults, errorLines, scripts) {
this.fileResults = fileResults;
this.scripts = scripts;
var lines = [];
fileResults.forEach(function (v) { return lines = lines.concat(v.file.lines); });
this.code = lines.join("\n");
this.errors = [];
for (var i = 0; i < errorLines.length; i++) {
if (Harness.usePull) {
var err = errorLines[i]; // TypeScript.PullError
this.errors.push(new CompilerError(err.filename, 0, 0, err.message));
}
else {
var match = errorLines[i].match(/([^\(]*)\((\d+),(\d+)\):\s+((.*[\s\r\n]*.*)+)\s*$/);
if (match) {
this.errors.push(new CompilerError(match[1], parseFloat(match[2]), parseFloat(match[3]), match[4]));
}
else {
WScript.Echo("non-match on: " + errorLines[i]);
}
}
}
}
CompilerResult.prototype.isErrorAt = function (line, column, message) {
for (var i = 0; i < this.errors.length; i++) {
if (this.errors[i].line === line && this.errors[i].column === column && this.errors[i].message === message)
return true;
}
return false;
};
return CompilerResult;
})();
Compiler.CompilerResult = CompilerResult;
// Compiler Error.
var CompilerError = (function () {
function CompilerError(file, line, column, message) {
this.file = file;
this.line = line;
this.column = column;
this.message = message;
}
CompilerError.prototype.toString = function () {
return this.file + "(" + this.line + "," + this.column + "): " + this.message;
};
return CompilerError;
})();
Compiler.CompilerError = CompilerError;
/** Create a new instance of the compiler with default settings and lib.d.ts, then typecheck */
function recreate() {
compiler = makeDefaultCompilerForTest();
if (Harness.usePull) {
compiler.pullTypeCheck(true);
}
else {
compiler.typeCheck();
}
}
Compiler.recreate = recreate;
function reset() {
stdout.reset();
stderr.reset();
var files = compiler.units.map(function (value) { return value.filename; });
for (var i = 0; i < files.length; i++) {
var fname = files[i];
if (fname !== 'lib.d.ts') {
updateUnit('', fname);
}
}
compiler.errorReporter.hasErrors = false;
}
Compiler.reset = reset;
function addUnit(code, unitName, isResident, isDeclareFile, references) {
var script = null;
var uName = unitName || '0' + (isDeclareFile ? '.d.ts' : '.ts');
for (var i = 0; i < compiler.units.length; i++) {
if (compiler.units[i].filename === uName) {
updateUnit(code, uName);
script = compiler.scripts.members[i];
}
}
if (!script) {
// TODO: make this toggleable, shouldn't be necessary once typecheck bugs are cleaned up
// but without it subsequent tests are treated as edits, making for somewhat useful stress testing
// of persistent typecheck state
//compiler.addUnit("", uName, isResident, references); // equivalent to compiler.deleteUnit(...)
script = compiler.addUnit(code, uName, isResident, references);
needsFullTypeCheck = true;
}
return script;
}
Compiler.addUnit = addUnit;
function updateUnit(code, unitName, setRecovery) {
if (Harness.usePull) {
compiler.pullUpdateUnit(new TypeScript.StringSourceText(code), unitName, setRecovery);
}
else {
compiler.updateUnit(code, unitName, setRecovery);
}
}
Compiler.updateUnit = updateUnit;
function compileFile(path, callback, settingsCallback, context, references) {
path = switchToForwardSlashes(path);
var filename = path.match(/[^\/]*$/)[0];
var code = readFile(path);
compileUnit(code, filename, callback, settingsCallback, context, references);
}
Compiler.compileFile = compileFile;
function compileUnit(code, filename, callback, settingsCallback, context, references) {
// not recursive
function clone /* <T> */(source, target) {
for (var prop in source) {
target[prop] = source[prop];
}
}
var oldCompilerSettings = new TypeScript.CompilationSettings();
clone(compiler.settings, oldCompilerSettings);
var oldEmitSettings = new TypeScript.EmitOptions(compiler.settings);
clone(compiler.emitSettings, oldEmitSettings);
var oldModuleGenTarget = TypeScript.moduleGenTarget;
if (settingsCallback) {
settingsCallback(compiler.settings);
compiler.emitSettings = new TypeScript.EmitOptions(compiler.settings);
}
try {
compileString(code, filename, callback, context, references);
}
finally {
// If settingsCallback exists, assume that it modified the global compiler instance's settings in some way.
// So that a test doesn't have side effects for tests run after it, restore the compiler settings to their previous state.
if (settingsCallback) {
compiler.settings = oldCompilerSettings;
compiler.emitSettings = oldEmitSettings;
TypeScript.moduleGenTarget = oldModuleGenTarget;
}
}
}
Compiler.compileUnit = compileUnit;
function compileUnits(units, callback, settingsCallback) {
var lastUnit = units[units.length - 1];
var unitName = switchToForwardSlashes(lastUnit.name).match(/[^\/]*$/)[0];
var dependencies = units.slice(0, units.length - 1);
var compilationContext = Harness.Compiler.defineCompilationContextForTest(unitName, dependencies);
compileUnit(lastUnit.content, unitName, callback, settingsCallback, compilationContext, lastUnit.references);
}
Compiler.compileUnits = compileUnits;
function emitToOutfile(outfile) {
compiler.emitToOutfile(outfile);
}
Compiler.emitToOutfile = emitToOutfile;
function emit(ioHost, usePullEmitter) {
compiler.emit(ioHost, usePullEmitter);
}
Compiler.emit = emit;
function compileString(code, unitName, callback, context, references) {
var scripts = [];
reset();
if (context) {
context.preCompile();
}
var isDeclareFile = Harness.Compiler.isDeclareFile(unitName);
// for single file tests just add them as using the old '0.ts' naming scheme
var uName = context ? unitName : ((isDeclareFile) ? '0.d.ts' : '0.ts');
scripts.push(addUnit(code, uName, false, isDeclareFile, references));
compile(code, uName);
var errors;
if (Harness.usePull) {
// TODO: no emit support with pull yet
errors = compiler.pullGetErrorsForFile(uName);
emit(stdout, true);
}
else {
errors = stderr.lines;
emit(stdout, false);
//output decl file
compiler.emitDeclarations();
}
if (context) {
context.postCompile();
}
callback(new CompilerResult(stdout.toArray(), errors, scripts));
}
Compiler.compileString = compileString;
/** Returns a set of functions which can be later executed to add and remove given dependencies to the compiler so that
* a file can be successfully compiled. These functions will add/remove named units and code to the compiler for each dependency.
*/
function defineCompilationContextForTest(filename, dependencies) {
// if the given file has no dependencies, there is no context to return, it can be compiled without additional work
if (dependencies.length == 0) {
return null;
}
else {
var addedFiles = [];
var precompile = function () {
// REVIEW: if any dependency has a triple slash reference then does postCompile potentially have to do a recreate since we can't update references with updateUnit?
// easy enough to do if so, prefer to avoid the recreate cost until it proves to be an issue
dependencies.forEach(function (dep) {
addUnit(dep.content, dep.name, false, Harness.Compiler.isDeclareFile(dep.name));
addedFiles.push(dep.name);
});
};
var postcompile = function () {
addedFiles.forEach(function (file) {
updateUnit('', file);
});
};
var context = {
filename: filename,
preCompile: precompile,
postCompile: postcompile
};
return context;
}
}
Compiler.defineCompilationContextForTest = defineCompilationContextForTest;
})(Compiler = Harness.Compiler || (Harness.Compiler = {}));
/** Parses the test cases files
* extracts options and individual files in a multifile test
*/
var TestCaseParser;
(function (TestCaseParser) {
optionRegex = /^[\/]{2}\s*@(\w+):\s*(\S*)/gm; // multiple matches on multiple lines
// List of allowed metadata names
var fileMetadataNames = ["filename", "comments", "declaration", "module", "nolib", "sourcemap", "target", "out"];
function extractCompilerSettings(content) {
var opts = [];
var match;
while ((match = optionRegex.exec(content)) != null) {
opts.push({ flag: match[1], value: match[2] });
}
return opts;
}
/** Given a test file containing // @Filename directives, return an array of named units of code to be added to an existing compiler instance */
function makeUnitsFromTest(code, filename) {
var settings = extractCompilerSettings(code);
// List of all the subfiles we've parsed out
var files = [];
var lines = splitContentByNewlines(code);
// Stuff related to the subfile we're parsing
var currentFileContent = null;
var currentFileOptions = {};
var currentFileName = null;
var refs = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var isTripleSlashReference = /[\/]{3}\s*<reference path/.test(line);
var testMetaData = optionRegex.exec(line);
// Triple slash references need to be tracked as they are added to the compiler as an additional parameter to addUnit
if (isTripleSlashReference) {
var isRef = line.match(/reference\spath='(\w*_?\w*\.?d?\.ts)'/);
if (isRef) {
var ref = {
minChar: 0,
limChar: 0,
startLine: 0,
startCol: 0,
path: isRef[1],
isResident: false
};
refs.push(ref);
}
}
else if (testMetaData) {
// Comment line, check for global/file @options and record them
optionRegex.lastIndex = 0;
var fileNameIndex = fileMetadataNames.indexOf(testMetaData[1].toLowerCase());
if (fileNameIndex == -1) {
throw new Error('Unrecognized metadata name "' + testMetaData[1] + '". Available file metadata names are: ' + fileMetadataNames.join(', '));
}
else if (fileNameIndex == 0) {
currentFileOptions[testMetaData[1]] = testMetaData[2];
}
else {
continue;
}
// New metadata statement after having collected some code to go with the previous metadata
if (currentFileName) {
// Store result file
var newTestFile = {
content: currentFileContent,
name: currentFileName,
fileOptions: currentFileOptions,
originalFilePath: filename,
references: refs
};
files.push(newTestFile);
// Reset local data
currentFileContent = null;
currentFileOptions = {};
currentFileName = testMetaData[2];
refs = [];
}
else {
// First metadata marker in the file
currentFileName = testMetaData[2];
}
}
else {
// Subfile content line
// Append to the current subfile content, inserting a newline needed
if (currentFileContent === null) {
currentFileContent = '';
}
else {
// End-of-line
currentFileContent = currentFileContent + '\n';
}
currentFileContent = currentFileContent + line;
}
}
// normalize the filename for the single file case
currentFileName = files.length > 0 ? currentFileName : '0.ts';
// EOF, push whatever remains
var newTestFile = {
content: currentFileContent || '',
name: currentFileName,
fileOptions: currentFileOptions,
originalFilePath: filename,
references: refs
};
files.push(newTestFile);
return { settings: settings, testUnitData: files };
}
TestCaseParser.makeUnitsFromTest = makeUnitsFromTest;
})(TestCaseParser = Harness.TestCaseParser || (Harness.TestCaseParser = {}));
var ScriptInfo = (function () {
function ScriptInfo(name, content, isResident, maxScriptVersions) {
this.name = name;
this.content = content;
this.isResident = isResident;
this.maxScriptVersions = maxScriptVersions;
this.editRanges = [];
this.version = 1;
}
ScriptInfo.prototype.updateContent = function (content, isResident) {
this.editRanges = [];
this.content = content;
this.isResident = isResident;
this.version++;
};
ScriptInfo.prototype.editContent = function (minChar, limChar, newText) {
// Apply edits
var prefix = this.content.substring(0, minChar);
var middle = newText;
var suffix = this.content.substring(limChar);
this.content = prefix + middle + suffix;
// Store edit range + new length of script
this.editRanges.push({
length: this.content.length,
editRange: new TypeScript.ScriptEditRange(minChar, limChar, (limChar - minChar) + newText.length)
});
if (this.editRanges.length > this.maxScriptVersions) {
this.editRanges.splice(0, this.maxScriptVersions - this.editRanges.length);
}
// Update version #
this.version++;
};
ScriptInfo.prototype.getEditRangeSinceVersion = function (version) {
if (this.version == version) {
// No edits!
return null;
}
var initialEditRangeIndex = this.editRanges.length - (this.version - version);
if (initialEditRangeIndex < 0 || initialEditRangeIndex >= this.editRanges.length) {
// Too far away from what we know
return TypeScript.ScriptEditRange.unknown();
}
var entries = this.editRanges.slice(initialEditRangeIndex);
var minDistFromStart = entries.map(function (x) { return x.editRange.minChar; }).reduce(function (prev, current) { return Math.min(prev, current); });
var minDistFromEnd = entries.map(function (x) { return x.length - x.editRange.limChar; }).reduce(function (prev, current) { return Math.min(prev, current); });
var aggDelta = entries.map(function (x) { return x.editRange.delta; }).reduce(function (prev, current) { return prev + current; });
return new TypeScript.ScriptEditRange(minDistFromStart, entries[0].length - minDistFromEnd, aggDelta);
};
return ScriptInfo;
})();
Harness.ScriptInfo = ScriptInfo;
var TypeScriptLS = (function () {
function TypeScriptLS() {
this.ls = null;
this.scripts = [];
this.maxScriptVersions = 100;
}
TypeScriptLS.prototype.addDefaultLibrary = function () {
this.addScript("lib.d.ts", Harness.Compiler.libText, true);
};
TypeScriptLS.prototype.addFile = function (name, isResident) {
if (isResident === void 0) { isResident = false; }
var code = readFile(name);
this.addScript(name, code, isResident);
};
TypeScriptLS.prototype.addScript = function (name, content, isResident) {
if (isResident === void 0) { isResident = false; }
var script = new ScriptInfo(name, content, isResident, this.maxScriptVersions);
this.scripts.push(script);
};
TypeScriptLS.prototype.updateScript = function (name, content, isResident) {
if (isResident === void 0) { isResident = false; }
for (var i = 0; i < this.scripts.length; i++) {
if (this.scripts[i].name == name) {
this.scripts[i].updateContent(content, isResident);
return;
}
}
this.addScript(name, content, isResident);
};
TypeScriptLS.prototype.editScript = function (name, minChar, limChar, newText) {
for (var i = 0; i < this.scripts.length; i++) {
if (this.scripts[i].name == name) {
this.scripts[i].editContent(minChar, limChar, newText);
return;
}
}
throw new Error("No script with name '" + name + "'");
};
TypeScriptLS.prototype.getScriptContent = function (scriptIndex) {
return this.scripts[scriptIndex].content;
};
//////////////////////////////////////////////////////////////////////
// ILogger implementation
//
TypeScriptLS.prototype.information = function () { return false; };
TypeScriptLS.prototype.debug = function () { return true; };
TypeScriptLS.prototype.warning = function () { return true; };
TypeScriptLS.prototype.error = function () { return true; };
TypeScriptLS.prototype.fatal = function () { return true; };
TypeScriptLS.prototype.log = function (s) {
// For debugging...
//IO.printLine("TypeScriptLS:" + s);
};
//////////////////////////////////////////////////////////////////////
// ILanguageServiceShimHost implementation
//
TypeScriptLS.prototype.getCompilationSettings = function () {
return ""; // i.e. default settings
};
TypeScriptLS.prototype.getScriptCount = function () {
return this.scripts.length;
};
TypeScriptLS.prototype.getScriptSourceText = function (scriptIndex, start, end) {
return this.scripts[scriptIndex].content.substring(start, end);
};
TypeScriptLS.prototype.getScriptSourceLength = function (scriptIndex) {
return this.scripts[scriptIndex].content.length;
};
TypeScriptLS.prototype.getScriptId = function (scriptIndex) {
return this.scripts[scriptIndex].name;
};
TypeScriptLS.prototype.getScriptIsResident = function (scriptIndex) {
return this.scripts[scriptIndex].isResident;
};
TypeScriptLS.prototype.getScriptVersion = function (scriptIndex) {
return this.scripts[scriptIndex].version;
};
TypeScriptLS.prototype.getScriptEditRangeSinceVersion = function (scriptIndex, scriptVersion) {
var range = this.scripts[scriptIndex].getEditRangeSinceVersion(scriptVersion);
var result = (range.minChar + "," + range.limChar + "," + range.delta);
return result;
};
/** Return a new instance of the language service shim, up-to-date wrt to typecheck.
* To access the non-shim (i.e. actual) language service, use the "ls.languageService" property.
*/
TypeScriptLS.prototype.getLanguageService = function () {
var ls = new Services.TypeScriptServicesFactory().createLanguageServiceShim(this);
ls.refresh(true);
this.ls = ls;
return ls;
};
/** Parse file given its source text */
TypeScriptLS.prototype.parseSourceText = function (fileName, sourceText) {
var parser = new TypeScript.Parser();
parser.setErrorRecovery(null);
parser.errorCallback = function (a, b, c, d) { };
var script = parser.parse(sourceText, fileName, 0);
return script;
};
/** Parse a file on disk given its filename */
TypeScriptLS.prototype.parseFile = function (fileName) {
var sourceText = new TypeScript.StringSourceText(IO.readFile(fileName));
return this.parseSourceText(fileName, sourceText);
};
/**
* @param line 1 based index
* @param col 1 based index
*/
TypeScriptLS.prototype.lineColToPosition = function (fileName, line, col) {
var script = this.ls.languageService.getScriptAST(fileName);
assert.notNull(script);
assert.is(line >= 1);
assert.is(col >= 1);
assert.is(line <= script.locationInfo.lineMap.length);
return TypeScript.getPositionFromZeroBasedLineColumn(script, line - 1, col - 1);
};
/**
* @param line 0 based index
* @param col 0 based index
*/
TypeScriptLS.prototype.positionToZeroBasedLineCol = function (fileName, position) {
var script = this.ls.languageService.getScriptAST(fileName);
assert.notNull(script);
var result = TypeScript.getZeroBasedLineColumnFromPosition(script, position);
assert.is(result.line >= 0);
assert.is(result.col >= 0);
return result;
};
/** Verify that applying edits to sourceFileName result in the content of the file baselineFileName */
TypeScriptLS.prototype.checkEdits = function (sourceFileName, baselineFileName, edits) {
var script = readFile(sourceFileName);
var formattedScript = this.applyEdits(script, edits);
var baseline = readFile(baselineFileName);
assert.noDiff(formattedScript, baseline);
assert.equal(formattedScript, baseline);
};
/** Apply an array of text edits to a string, and return the resulting string. */
TypeScriptLS.prototype.applyEdits = function (content, edits) {
var result = content;
edits = this.normalizeEdits(edits);
for (var i = edits.length - 1; i >= 0; i--) {
var edit = edits[i];
var prefix = result.substring(0, edit.minChar);
var middle = edit.text;
var suffix = result.substring(edit.limChar);
result = prefix + middle + suffix;
}
return result;
};
/** Normalize an array of edits by removing overlapping entries and sorting entries on the minChar position. */
TypeScriptLS.prototype.normalizeEdits = function (edits) {
var result = [];
function mapEdits(edits) {
var result = [];
for (var i = 0; i < edits.length; i++) {
result.push({ edit: edits[i], index: i });
}
return result;
}
var temp = mapEdits(edits).sort(function (a, b) {
var result = a.edit.minChar - b.edit.minChar;
if (result == 0)
result = a.index - b.index;
return result;
});
var current = 0;
var next = 1;
while (current < temp.length) {
var currentEdit = temp[current].edit;
// Last edit
if (next >= temp.length) {
result.push(currentEdit);
current++;
continue;
}
var nextEdit = temp[next].edit;
var gap = nextEdit.minChar - currentEdit.limChar;
// non-overlapping edits
if (gap >= 0) {
result.push(currentEdit);
current = next;
next++;
continue;
}
// overlapping edits: for now, we only support ignoring an next edit
// entirely contained in the current edit.
if (currentEdit.limChar >= nextEdit.limChar) {
next++;
continue;
}
else {
throw new Error("Trying to apply overlapping edits");
}
}
return result;
};
TypeScriptLS.prototype.getHostSettings = function () {
return JSON.stringify({ usePullLanguageService: Harness.usePull });
};
return TypeScriptLS;
})();
Harness.TypeScriptLS = TypeScriptLS;
// Describe/it definitions
function describe(description, block) {
var newScenario = new Scenario(description, block);
if (Runnable.currentStack.length === 0) {
Runnable.currentStack.push(currentRun);
}
Runnable.currentStack[Runnable.currentStack.length - 1].addChild(newScenario);
}
Harness.describe = describe;
function it(description, block) {
var testCase = new TestCase(description, block);
Runnable.currentStack[Runnable.currentStack.length - 1].addChild(testCase);
}
Harness.it = it;
function run() {
if (typeof process !== "undefined") {
process.on('uncaughtException', Runnable.handleError);
}
Baseline.reset();
currentRun.run();
}
Harness.run = run;
/** Runs TypeScript or Javascript code. */
var Runner;
(function (Runner) {
function runCollateral(path, callback) {
path = switchToForwardSlashes(path);
runString(readFile(path), path.match(/[^\/]*$/)[0], callback);
}
Runner.runCollateral = runCollateral;
function runJSString(code, callback) {
// List of names that get overriden by various test code we eval
var dangerNames = ['Array'];
var globalBackup = {};
var n = null;
for (n in dangerNames) {
globalBackup[dangerNames[n]] = global[dangerNames[n]];
}
try {
var res = eval(code);
for (n in dangerNames) {
global[dangerNames[n]] = globalBackup[dangerNames[n]];
}
callback(null, res);
}
catch (e) {
for (n in dangerNames) {
global[dangerNames[n]] = globalBackup[dangerNames[n]];
}
callback(e, null);
}
}
Runner.runJSString = runJSString;
function runString(code, unitName, callback) {
Compiler.compileString(code, unitName, function (res) {
runJSString(res.code, callback);
});
}
Runner.runString = runString;
})(Runner = Harness.Runner || (Harness.Runner = {}));
/** Support class for baseline files */
var Baseline;
(function (Baseline) {
var reportFilename = 'baseline-report.html';
var firstRun = true;
var htmlTrailer = '</body></html>';
var htmlLeader = '<html><head><title>Baseline Report</title>';
htmlLeader += ("<style>");
htmlLeader += '\r\n' + (".code { font: 9pt 'Courier New'; }");
htmlLeader += '\r\n' + (".old { background-color: #EE1111; }");
htmlLeader += '\r\n' + (".new { background-color: #FFFF11; }");
htmlLeader += '\r\n' + (".from { background-color: #EE1111; color: #1111EE; }");
htmlLeader += '\r\n' + (".to { background-color: #EEEE11; color: #1111EE; }");
htmlLeader += '\r\n' + ("h2 { margin-bottom: 0px; }");
htmlLeader += '\r\n' + ("h2 { padding-bottom: 0px; }");
htmlLeader += '\r\n' + ("h4 { font-weight: normal; }");
htmlLeader += '\r\n' + ("</style>");
function localPath(filename) {
if (global.runners[0].testType === 'prototyping') {
return Harness.userSpecifiedroot + 'tests/baselines/prototyping/local/' + filename;
}
else {
return Harness.userSpecifiedroot + 'tests/baselines/local/' + filename;
}
}
function referencePath(filename) {
if (global.runners[0].testType === 'prototyping') {
return Harness.userSpecifiedroot + 'tests/baselines/prototyping/reference/' + filename;
}
else {
return Harness.userSpecifiedroot + 'tests/baselines/reference/' + filename;
}
}
function reset() {
if (IO.fileExists(reportFilename)) {
IO.deleteFile(reportFilename);
}
}
Baseline.reset = reset;
function prepareBaselineReport() {
var reportContent = htmlLeader;
// Delete the baseline-report.html file if needed
if (IO.fileExists(reportFilename)) {
reportContent = IO.readFile(reportFilename);
reportContent = reportContent.replace(htmlTrailer, '');
}
else {
reportContent = htmlLeader;
}
return reportContent;
}
function generateActual(actualFilename, generateContent) {
// Create folders if needed
IO.createDirectory(IO.dirName(IO.dirName(actualFilename)));
IO.createDirectory(IO.dirName(actualFilename));
// Delete the actual file in case it fails
if (IO.fileExists(actualFilename)) {
IO.deleteFile(actualFilename);
}
var actual = generateContent();
if (actual === undefined) {
throw new Error('The generated content was "undefined". Return "null" if no baselining is required."');
}
// Store the content in the 'local' folder so we
// can accept it later (manually)
if (actual !== null) {
IO.writeFile(actualFilename, actual);
}
return actual;
}
function compareToBaseline(actual, relativeFilename, opts) {
// actual is now either undefined (the generator had an error), null (no file requested),
// or some real output of the function
if (actual === undefined) {
// Nothing to do
return;
}
var refFilename = referencePath(relativeFilename);
if (actual === null) {
actual = '<no content>';
}
var expected = '<no content>';
if (IO.fileExists(refFilename)) {
expected = IO.readFile(refFilename);
}
var lineEndingSensitive = opts && opts.LineEndingSensitive;
if (!lineEndingSensitive) {
expected = expected.replace(/\r\n?/g, '\n');
actual = actual.replace(/\r\n?/g, '\n');
}
return { expected: expected, actual: actual };
}
function writeComparison(expected, actual, relativeFilename, actualFilename, descriptionForDescribe) {
if (expected != actual) {
// Overwrite & issue error
var errMsg = 'The baseline file ' + relativeFilename + ' has changed. Please refer to baseline-report.html and ';
errMsg += 'either fix the regression (if unintended) or run nmake baseline-accept (if intended).';
var refFilename = referencePath(relativeFilename);
// Append diff to the report
var diff = new Diff.StringDiff(expected, actual);
var header = '<h2>' + descriptionForDescribe + '</h2>';
header += '<h4>Left file: ' + actualFilename + '; Right file: ' + refFilename + '</h4>';
var trailer = '<hr>';
var reportContentSoFar = prepareBaselineReport();
reportContentSoFar = reportContentSoFar + header + '<div class="code">' + diff.mergedHtml + '</div>' + trailer + htmlTrailer;
IO.writeFile(reportFilename, reportContentSoFar);
throw new Error(errMsg);
}
}
function runBaseline(descriptionForDescribe, relativeFilename, generateContent, runImmediately, opts) {
if (runImmediately === void 0) { runImmediately = false; }
var actual = undefined;
var actualFilename = localPath(relativeFilename);
if (runImmediately) {
var actual = generateActual(actualFilename, generateContent);
var comparison = compareToBaseline(actual, relativeFilename, opts);
writeComparison(comparison.expected, comparison.actual, relativeFilename, actualFilename, descriptionForDescribe);
}
else {
describe(descriptionForDescribe, function () {
var actual;
it('Can generate the content without error', function () {
actual = generateActual(actualFilename, generateContent);
});
it('Matches the baseline file', function () {
var comparison = compareToBaseline(actual, relativeFilename, opts);
writeComparison(comparison.expected, comparison.actual, relativeFilename, actualFilename, descriptionForDescribe);
});
});
}
}
Baseline.runBaseline = runBaseline;
})(Baseline = Harness.Baseline || (Harness.Baseline = {}));
var currentRun = new Run();
global.describe = describe;
global.run = run;
global.it = it;
global.assert = Harness.Assert;
})(Harness || (Harness = {}));