Reorganize and remove duplication

This commit is contained in:
Ron Buckton 2018-04-24 10:48:55 -07:00
parent bb26ab5556
commit c9c562afac
19 changed files with 181 additions and 589 deletions

View File

@ -1,5 +1,4 @@
/// <reference path="./functions.ts" />
namespace core {
namespace collections {
export interface SortOptions<T> {
comparer: (a: T, b: T) => number;
sort: "insertion" | "comparison";
@ -43,16 +42,16 @@ namespace core {
}
public has(key: K) {
return binarySearch(this._keys, key, identity, this._comparer) >= 0;
return ts.binarySearch(this._keys, key, ts.identity, this._comparer) >= 0;
}
public get(key: K) {
const index = binarySearch(this._keys, key, identity, this._comparer);
const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer);
return index >= 0 ? this._values[index] : undefined;
}
public set(key: K, value: V) {
const index = binarySearch(this._keys, key, identity, this._comparer);
const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer);
if (index >= 0) {
this._values[index] = value;
}
@ -67,12 +66,12 @@ namespace core {
}
public delete(key: K) {
const index = binarySearch(this._keys, key, identity, this._comparer);
const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer);
if (index >= 0) {
this.writePreamble();
removeAt(this._keys, index);
removeAt(this._values, index);
if (this._order) removeAt(this._order, index);
ts.orderedRemoveItemAt(this._keys, index);
ts.orderedRemoveItemAt(this._values, index);
if (this._order) ts.orderedRemoveItemAt(this._order, index);
this.writePostScript();
return true;
}
@ -211,226 +210,6 @@ namespace core {
}
}
export class SortedSet<T> {
private _comparer: (a: T, b: T) => number;
private _values: T[] = [];
private _order: number[] | undefined;
private _version = 0;
private _copyOnWrite = false;
constructor(comparer: ((a: T, b: T) => number) | SortOptions<T>, iterable?: Iterable<T>) {
this._comparer = typeof comparer === "object" ? comparer.comparer : comparer;
this._order = typeof comparer === "object" && comparer.sort === "insertion" ? [] : undefined;
if (iterable) {
const iterator = getIterator(iterable);
try {
for (let i = nextResult(iterator); i; i = nextResult(iterator)) {
const value = i.value;
this.add(value);
}
}
finally {
closeIterator(iterator);
}
}
}
public get size() {
return this._values.length;
}
public get comparer() {
return this._comparer;
}
public get [Symbol.toStringTag]() {
return "SortedSet";
}
public has(value: T) {
return binarySearch(this._values, value, identity, this._comparer) >= 0;
}
public add(value: T) {
const index = binarySearch(this._values, value, identity, this._comparer);
if (index < 0) {
this.writePreamble();
insertAt(this._values, ~index, value);
if (this._order) insertAt(this._order, ~index, this._version);
this.writePostScript();
}
return this;
}
public delete(value: T) {
const index = binarySearch(this._values, value, identity, this._comparer);
if (index >= 0) {
this.writePreamble();
removeAt(this._values, index);
if (this._order) removeAt(this._order, index);
this.writePostScript();
return true;
}
return false;
}
public clear() {
if (this.size > 0) {
this.writePreamble();
this._values.length = 0;
if (this._order) this._order.length = 0;
this.writePostScript();
}
}
public forEach(callback: (value: T, key: T, collection: this) => void, thisArg?: any) {
const values = this._values;
const indices = this.getIterationOrder();
const version = this._version;
this._copyOnWrite = true;
try {
if (indices) {
for (const i of indices) {
callback.call(thisArg, values[i], values[i], this);
}
}
else {
for (const value of values) {
callback.call(thisArg, value, value, this);
}
}
}
finally {
if (version === this._version) {
this._copyOnWrite = false;
}
}
}
public keys() {
return this.values();
}
public * values() {
const values = this._values;
const indices = this.getIterationOrder();
const version = this._version;
this._copyOnWrite = true;
try {
if (indices) {
for (const i of indices) {
yield values[i];
}
}
else {
for (const value of values) {
yield value;
}
}
}
finally {
if (version === this._version) {
this._copyOnWrite = false;
}
}
}
public * entries() {
const values = this._values;
const indices = this.getIterationOrder();
const version = this._version;
this._copyOnWrite = true;
try {
if (indices) {
for (const i of indices) {
yield [values[i], values[i]] as [T, T];
}
}
else {
for (const value of values) {
yield [value, value] as [T, T];
}
}
}
finally {
if (version === this._version) {
this._copyOnWrite = false;
}
}
}
public [Symbol.iterator]() {
return this.values();
}
private writePreamble() {
if (this._copyOnWrite) {
this._values = this._values.slice();
if (this._order) this._order = this._order.slice();
this._copyOnWrite = false;
}
}
private writePostScript() {
this._version++;
}
private getIterationOrder() {
if (this._order) {
const order = this._order;
return this._order
.map((_, i) => i)
.sort((x, y) => order[x] - order[y]);
}
return undefined;
}
}
export function binarySearch<T, U>(array: ReadonlyArray<T>, value: T, keySelector: (v: T) => U, keyComparer: (a: U, b: U) => number, offset?: number): number {
if (!array || array.length === 0) {
return -1;
}
let low = offset || 0;
let high = array.length - 1;
const key = keySelector(value);
while (low <= high) {
const middle = low + ((high - low) >> 1);
const midKey = keySelector(array[middle]);
const result = keyComparer(midKey, key);
if (result < 0) {
low = middle + 1;
}
else if (result > 0) {
high = middle - 1;
}
else {
return middle;
}
}
return ~low;
}
export function removeAt<T>(array: T[], index: number): void {
if (index < 0 || index >= array.length) {
return;
}
else if (index === 0) {
array.shift();
}
else if (index === array.length - 1) {
array.pop();
}
else {
for (let i = index; i < array.length - 1; i++) {
array[i] = array[i + 1];
}
array.length--;
}
}
export function insertAt<T>(array: T[], index: number, value: T): void {
if (index === 0) {
array.unshift(value);

View File

@ -1,14 +1,6 @@
/// <reference path="./harness.ts" />
/// <reference path="./documents.ts" />
/// <reference path="./core.ts" />
/// <reference path="./vpath.ts" />
/// <reference path="./vfs.ts" />
/// <reference path="./utils.ts" />
/// <reference path="./fakes.ts" />
// NOTE: The contents of this file are all exported from the namespace 'compiler'. This is to
// support the eventual conversion of harness into a modular system.
/**
* Test harness compiler functionality.
*/
namespace compiler {
export interface Project {
file: string;
@ -64,7 +56,7 @@ namespace compiler {
public readonly maps: ReadonlyMap<string, documents.TextDocument>;
private _inputs: documents.TextDocument[] = [];
private _inputsAndOutputs: core.SortedMap<string, CompilationOutput>;
private _inputsAndOutputs: collections.SortedMap<string, CompilationOutput>;
constructor(host: fakes.CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: ts.Diagnostic[]) {
this.host = host;
@ -74,9 +66,9 @@ namespace compiler {
this.options = program ? program.getCompilerOptions() : options;
// collect outputs
const js = this.js = new core.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const dts = this.dts = new core.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const maps = this.maps = new core.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const js = this.js = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const dts = this.dts = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const maps = this.maps = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
for (const document of this.host.outputs) {
if (vpath.isJavaScript(document.file)) {
js.set(document.file, document);
@ -90,7 +82,7 @@ namespace compiler {
}
// correlate inputs and outputs
this._inputsAndOutputs = new core.SortedMap<string, CompilationOutput>({ comparer: this.vfs.stringComparer, sort: "insertion" });
this._inputsAndOutputs = new collections.SortedMap<string, CompilationOutput>({ comparer: this.vfs.stringComparer, sort: "insertion" });
if (program) {
if (this.options.out || this.options.outFile) {
const outFile = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);

View File

@ -273,8 +273,8 @@ class CompilerTest {
}
private makeUnitName(name: string, root: string) {
const path = ts.toPath(name, root, core.identity);
const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", core.identity);
const path = ts.toPath(name, root, ts.identity);
const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.identity);
return pathStart ? path.replace(pathStart, "/") : path;
}

View File

@ -1,4 +0,0 @@
/// <reference path="./core/functions.ts" />
/// <reference path="./core/comparers.ts" />
/// <reference path="./core/collections.ts" />
/// <reference path="./core/strings.ts" />

View File

@ -1,43 +0,0 @@
namespace core {
export function compareNumbers(a: number, b: number): number {
if (a === b) return 0;
if (a === undefined) return -1;
if (b === undefined) return +1;
return a < b ? -1 : +1;
}
export function compareStrings(a: string, b: string, ignoreCase: boolean): number {
return ignoreCase
? compareStringsCaseInsensitive(a, b)
: compareStringsCaseSensitive(a, b);
}
// NOTE: This is a duplicate of `compareNumbers` above, but is intended to be used only with
// strings to reduce polymorphism.
export function compareStringsCaseSensitive(a: string, b: string): number {
if (a === b) return 0;
if (a === undefined) return -1;
if (b === undefined) return +1;
return a < b ? -1 : +1;
}
export function compareStringsCaseInsensitive(a: string, b: string): number {
if (a === b) return 0;
if (a === undefined) return -1;
if (b === undefined) return +1;
a = a.toUpperCase();
b = b.toUpperCase();
return a < b ? -1 : a > b ? +1 : 0;
}
export function equateStringsCaseSensitive(a: string, b: string): boolean {
return a === b;
}
export function equateStringsCaseInsensitive(a: string, b: string): boolean {
return a === b
|| a !== undefined
&& b !== undefined
&& a.toUpperCase() === b.toUpperCase();
}
}

View File

@ -1,3 +0,0 @@
namespace core {
export function identity<T>(v: T): T { return v; }
}

View File

@ -1,130 +0,0 @@
namespace core {
export function padLeft(text: string, size: number, ch = " "): string {
while (text.length < size) text = ch + text;
return text;
}
export function padRight(text: string, size: number, ch = " "): string {
while (text.length < size) text += ch;
return text;
}
export function getByteOrderMark(text: string): string {
const length = getByteOrderMarkLength(text);
return length > 0 ? text.slice(0, length) : "";
}
export function getByteOrderMarkLength(text: string): number {
if (text.length >= 1) {
const ch0 = text.charCodeAt(0);
if (ch0 === 0xfeff) return 1;
if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0;
if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0;
if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0;
}
return 0;
}
export function removeByteOrderMark(text: string): string {
const length = getByteOrderMarkLength(text);
return length ? text.slice(length) : text;
}
export function addUTF8ByteOrderMark(text: string) {
return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text;
}
function splitLinesWorker(text: string, lineStarts: number[] | undefined, lines: string[] | undefined, removeEmptyElements: boolean) {
let pos = 0;
let end = 0;
let lineStart = 0;
let nonWhiteSpace = false;
while (pos < text.length) {
const ch = text.charCodeAt(pos);
end = pos;
pos++;
switch (ch) {
// LineTerminator
case 0x000d: // <CR> carriage return
if (pos < text.length && text.charCodeAt(pos) === 0x000a) {
pos++;
}
// falls through
case 0x000a: // <LF> line feed
case 0x2028: // <LS> line separator
case 0x2029: // <PS> paragraph separator
if (lineStarts) {
lineStarts.push(lineStart);
}
if (lines && (!removeEmptyElements || nonWhiteSpace)) {
lines.push(text.slice(lineStart, end));
}
lineStart = pos;
nonWhiteSpace = false;
break;
// WhiteSpace
case 0x0009: // <TAB> tab
case 0x000b: // <VT> vertical tab
case 0x000c: // <FF> form feed
case 0x0020: // <SP> space
case 0x00a0: // <NBSP> no-break space
case 0xfeff: // <ZWNBSP> zero width no-break space
case 0x1680: // <USP> ogham space mark
case 0x2000: // <USP> en quad
case 0x2001: // <USP> em quad
case 0x2002: // <USP> en space
case 0x2003: // <USP> em space
case 0x2004: // <USP> three-per-em space
case 0x2005: // <USP> four-per-em space
case 0x2006: // <USP> six-per-em space
case 0x2007: // <USP> figure space
case 0x2008: // <USP> punctuation space
case 0x2009: // <USP> thin space
case 0x200a: // <USP> hair space
case 0x202f: // <USP> narrow no-break space
case 0x205f: // <USP> medium mathematical space
case 0x3000: // <USP> ideographic space
case 0x0085: // next-line (not strictly per spec, but used by the compiler)
break;
default:
nonWhiteSpace = true;
break;
}
}
if (lineStarts) {
lineStarts.push(lineStart);
}
if (lines && (!removeEmptyElements || nonWhiteSpace)) {
lines.push(text.slice(lineStart, text.length));
}
}
export type LineStarts = ReadonlyArray<number>;
export interface LinesAndLineStarts {
readonly lines: ReadonlyArray<string>;
readonly lineStarts: LineStarts;
}
export function getLinesAndLineStarts(text: string): LinesAndLineStarts {
const lines: string[] = [];
const lineStarts: number[] = [];
splitLinesWorker(text, lineStarts, lines, /*removeEmptyElements*/ false);
return { lines, lineStarts };
}
export function splitLines(text: string, removeEmptyElements = false): string[] {
const lines: string[] = [];
splitLinesWorker(text, /*lineStarts*/ undefined, lines, removeEmptyElements);
return lines;
}
export function computeLineStarts(text: string): LineStarts {
const lineStarts: number[] = [];
splitLinesWorker(text, lineStarts, /*lines*/ undefined, /*removeEmptyElements*/ false);
return lineStarts;
}
}

View File

@ -1,5 +1,3 @@
/// <reference path="./core.ts" />
// NOTE: The contents of this file are all exported from the namespace 'documents'. This is to
// support the eventual conversion of harness into a modular system.
@ -9,7 +7,7 @@ namespace documents {
public readonly file: string;
public readonly text: string;
private _lineStarts: core.LineStarts | undefined;
private _lineStarts: ReadonlyArray<number> | undefined;
private _testFile: Harness.Compiler.TestFile | undefined;
constructor(file: string, text: string, meta?: Map<string, string>) {
@ -18,8 +16,8 @@ namespace documents {
this.meta = meta || new Map<string, string>();
}
public get lineStarts(): core.LineStarts {
return this._lineStarts || (this._lineStarts = core.computeLineStarts(this.text));
public get lineStarts(): ReadonlyArray<number> {
return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text));
}
public static fromTestFile(file: Harness.Compiler.TestFile) {

View File

@ -1,11 +1,6 @@
/// <reference path="./core.ts" />
/// <reference path="./utils.ts" />
/// <reference path="./vfs.ts" />
// NOTE: The contents of this file are all exported from the namespace 'fakes'. This is to
// support the eventual conversion of harness into a modular system.
// harness fakes
/**
* Fake implementations of various compiler dependencies.
*/
namespace fakes {
const processExitSentinel = new Error("System exit");
@ -45,8 +40,8 @@ namespace fakes {
try {
const content = this.vfs.readFileSync(path, "utf8");
return content === undefined ? undefined :
vpath.extname(path) === ".json" ? utils.removeComments(core.removeByteOrderMark(content), utils.CommentRemoval.leadingAndTrailing) :
core.removeByteOrderMark(content);
vpath.extname(path) === ".json" ? utils.removeComments(utils.removeByteOrderMark(content), utils.CommentRemoval.leadingAndTrailing) :
utils.removeByteOrderMark(content);
}
catch {
return undefined;
@ -55,7 +50,7 @@ namespace fakes {
public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
this.vfs.mkdirpSync(vpath.dirname(path));
this.vfs.writeFileSync(path, writeByteOrderMark ? core.addUTF8ByteOrderMark(data) : data);
this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data);
}
public fileExists(path: string) {
@ -212,7 +207,7 @@ namespace fakes {
public readonly shouldAssertInvariants = !Harness.lightMode;
private _setParentNodes: boolean;
private _sourceFiles: core.SortedMap<string, ts.SourceFile>;
private _sourceFiles: collections.SortedMap<string, ts.SourceFile>;
private _parseConfigHost: ParseConfigHost;
private _newLine: string;
@ -221,7 +216,7 @@ namespace fakes {
this.sys = sys;
this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || "";
this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine);
this._sourceFiles = new core.SortedMap<string, ts.SourceFile>({ comparer: sys.vfs.stringComparer, sort: "insertion" });
this._sourceFiles = new collections.SortedMap<string, ts.SourceFile>({ comparer: sys.vfs.stringComparer, sort: "insertion" });
this._setParentNodes = setParentNodes;
}
@ -266,7 +261,7 @@ namespace fakes {
}
public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) {
if (writeByteOrderMark) content = core.addUTF8ByteOrderMark(content);
if (writeByteOrderMark) content = utils.addUTF8ByteOrderMark(content);
this.sys.writeFile(fileName, content);
const document = new documents.TextDocument(fileName, content);

View File

@ -678,9 +678,9 @@ namespace Harness {
function createBrowserIO(): IO {
const serverRoot = new URL("http://localhost:8888/");
class HttpHeaders extends core.SortedMap<string, string | string[]> {
class HttpHeaders extends collections.SortedMap<string, string | string[]> {
constructor(template?: Record<string, string | string[]>) {
super(core.compareStringsCaseInsensitive);
super(ts.compareStringsCaseInsensitive);
if (template) {
for (const key in template) {
if (ts.hasProperty(template, key)) {
@ -1237,7 +1237,7 @@ namespace Harness {
}
const docs = inputFiles.concat(otherFiles).map(documents.TextDocument.fromTestFile);
const fs = vfs.FileSystem.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory });
const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory });
const host = new fakes.CompilerHost(fs, options);
return compiler.compileFiles(host, programFileNames, options);
}
@ -1286,7 +1286,7 @@ namespace Harness {
else if (vpath.isTypeScript(file.unitName)) {
const declFile = findResultCodeFile(file.unitName);
if (declFile && !findUnit(declFile.file, declInputFiles) && !findUnit(declFile.file, declOtherFiles)) {
dtsFiles.push({ unitName: declFile.file, content: core.removeByteOrderMark(declFile.text) });
dtsFiles.push({ unitName: declFile.file, content: utils.removeByteOrderMark(declFile.text) });
}
}
}
@ -1733,7 +1733,7 @@ namespace Harness {
const dupeCase = ts.createMap<number>();
// Yield them
for (const outputFile of files) {
yield [checkDuplicatedFileName(outputFile.file, dupeCase), "/*====== " + outputFile.file + " ======*/\r\n" + core.removeByteOrderMark(outputFile.text)];
yield [checkDuplicatedFileName(outputFile.file, dupeCase), "/*====== " + outputFile.file + " ======*/\r\n" + utils.removeByteOrderMark(outputFile.text)];
}
function cleanName(fn: string) {

View File

@ -119,11 +119,11 @@ namespace Harness.LanguageService {
export abstract class LanguageServiceAdapterHost {
public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot }));
public typesRegistry: ts.Map<void> | undefined;
private scriptInfos: core.SortedMap<string, ScriptInfo>;
private scriptInfos: collections.SortedMap<string, ScriptInfo>;
constructor(protected cancellationToken = DefaultHostCancellationToken.instance,
protected settings = ts.getDefaultCompilerOptions()) {
this.scriptInfos = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
}
public get vfs() {

View File

@ -193,7 +193,7 @@ namespace project {
assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message);
}
const fs = vfs.FileSystem.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
fs.mountSync(vpath.resolve(Harness.IO.getWorkspaceRoot(), "tests"), vpath.combine(vfs.srcFolder, "tests"), vfs.createResolver(Harness.IO));
fs.mkdirpSync(vpath.combine(vfs.srcFolder, testCase.projectRoot));
fs.chdir(vpath.combine(vfs.srcFolder, testCase.projectRoot));
@ -393,7 +393,7 @@ namespace project {
}
});
const _vfs = vfs.FileSystem.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, {
const _vfs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, {
documents: allInputFiles,
cwd: vpath.combine(vfs.srcFolder, this.testCase.projectRoot)
});

View File

@ -317,7 +317,7 @@ namespace Harness.SourceMapRecorder {
const startPos = lineMap[line];
const endPos = lineMap[line + 1];
const text = code.substring(startPos, endPos);
return line === 0 ? core.removeByteOrderMark(text) : text;
return line === 0 ? utils.removeByteOrderMark(text) : text;
}
function writeJsFileLines(endJsLine: number) {

View File

@ -134,12 +134,7 @@
"../server/session.ts",
"../server/scriptVersionCache.ts",
"./core/functions.ts",
"./core/comparers.ts",
"./core/collections.ts",
"./core/strings.ts",
"core.ts",
"collections.ts",
"utils.ts",
"documents.ts",
"vpath.ts",

View File

@ -37,7 +37,7 @@ namespace ts {
);
const testCompilerHost = new fakes.CompilerHost(
vfs.FileSystem.createFromFileSystem(
vfs.createFromFileSystem(
Harness.IO,
/*ignoreCase*/ true,
{ documents: [emptyFile, referenceFile], cwd: "d:\\pretend\\" }),

View File

@ -14,7 +14,7 @@ describe("Public APIs", () => {
});
it("should compile", () => {
const fs = vfs.FileSystem.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
fs.linkSync(`${vfs.builtFolder}/${fileName}`, `${vfs.srcFolder}/${fileName}`);
const sys = new fakes.System(fs);
const host = new fakes.CompilerHost(sys);

View File

@ -1,8 +1,6 @@
/// <reference path="./core.ts" />
// NOTE: The contents of this file are all exported from the namespace 'utils'. This is to
// support the eventual conversion of harness into a modular system.
/**
* Common utilities
*/
namespace utils {
const leadingCommentRegExp = /^(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+/;
const trailingCommentRegExp = /(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+$/;
@ -34,28 +32,6 @@ namespace utils {
return text !== undefined ? text.replace(testPathPrefixRegExp, "") : undefined;
}
/**
* SameValueZero (from ECMAScript spec), which has stricter equality sematics than "==" or "===".
*/
export function is(x: any, y: any) {
return (x === y) ? (x !== 0 || 1 / x === 1 / y) : (x !== x && y !== y);
}
export interface Theory {
title: string;
args: any[];
}
export function theory(name: string, data: (Theory | any[])[], callback: (...args: any[]) => any) {
describe(name, () => {
for (const theory of data) {
const title = Array.isArray(theory) ? theory.toString() : theory.title;
const args = Array.isArray(theory) ? theory : theory.args;
it(title, () => callback(...args));
}
});
}
/**
* Removes leading indentation from a template literal string.
*/
@ -112,14 +88,27 @@ namespace utils {
return indentation;
}
export function checkFileNames(caption: string, actualFileNames: ReadonlyArray<string>, expectedFileNames: string[]) {
assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${expectedFileNames}, got ${actualFileNames}`);
for (const f of expectedFileNames) {
assert.isTrue(actualFileNames.indexOf(f) >= 0, `${caption}: expected to find ${f} in ${actualFileNames}`);
}
}
export function toUtf8(text: string): string {
return new Buffer(text).toString("utf8");
}
export function getByteOrderMarkLength(text: string): number {
if (text.length >= 1) {
const ch0 = text.charCodeAt(0);
if (ch0 === 0xfeff) return 1;
if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0;
if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0;
if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0;
}
return 0;
}
export function removeByteOrderMark(text: string): string {
const length = getByteOrderMarkLength(text);
return length ? text.slice(length) : text;
}
export function addUTF8ByteOrderMark(text: string) {
return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text;
}
}

View File

@ -40,9 +40,9 @@ namespace vfs {
// lazy-initialized state that should be mutable even if the FileSystem is frozen.
private _lazy: {
links?: core.SortedMap<string, Inode>;
links?: collections.SortedMap<string, Inode>;
shadows?: Map<number, Inode>;
meta?: core.Metadata;
meta?: collections.Metadata;
} = {};
private _cwd: string; // current working directory
@ -68,16 +68,16 @@ namespace vfs {
let cwd = options.cwd;
if ((!cwd || !vpath.isRoot(cwd)) && this._lazy.links) {
const iterator = core.getIterator(this._lazy.links.keys());
const iterator = collections.getIterator(this._lazy.links.keys());
try {
for (let i = core.nextResult(iterator); i; i = core.nextResult(iterator)) {
for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
const name = i.value;
cwd = cwd ? vpath.resolve(name, cwd) : name;
break;
}
}
finally {
core.closeIterator(iterator);
collections.closeIterator(iterator);
}
}
@ -92,9 +92,9 @@ namespace vfs {
/**
* Gets metadata for this `FileSystem`.
*/
public get meta(): core.Metadata {
public get meta(): collections.Metadata {
if (!this._lazy.meta) {
this._lazy.meta = new core.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined);
this._lazy.meta = new collections.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined);
}
return this._lazy.meta;
}
@ -121,40 +121,6 @@ namespace vfs {
return this._shadowRoot;
}
/**
* Create a virtual file system from a physical file system using the following path mappings:
*
* - `/.ts` is a directory mapped to `${workspaceRoot}/built/local`
* - `/.lib` is a directory mapped to `${workspaceRoot}/tests/lib`
* - `/.src` is a virtual directory to be used for tests.
*
* Unless overridden, `/.src` will be the current working directory for the virtual file system.
*/
public static createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, cwd }: FileSystemCreateOptions = {}) {
const fs = getBuiltLocal(host, ignoreCase).shadow();
if (cwd) {
fs.mkdirpSync(cwd);
fs.chdir(cwd);
}
if (documents) {
for (const document of documents) {
fs.mkdirpSync(vpath.dirname(document.file));
fs.writeFileSync(document.file, document.text, "utf8");
fs.filemeta(document.file).set("document", document);
// Add symlinks
const symlink = document.meta.get("symlink");
if (symlink) {
for (const link of symlink.split(",").map(link => link.trim())) {
fs.mkdirpSync(vpath.dirname(link));
fs.symlinkSync(document.file, link);
fs.filemeta(link).set("document", document);
}
}
}
}
return fs;
}
/**
* Gets a shadow copy of this file system. Changes to the shadow copy do not affect the
* original, allowing multiple copies of the same core file system without multiple copies
@ -190,16 +156,16 @@ namespace vfs {
* Gets the metadata object for a path.
* @param path
*/
public filemeta(path: string): core.Metadata {
public filemeta(path: string): collections.Metadata {
const { node } = this._walk(this._resolve(path));
if (!node) throw createIOError("ENOENT");
return this._filemeta(node);
}
private _filemeta(node: Inode): core.Metadata {
private _filemeta(node: Inode): collections.Metadata {
if (!node.meta) {
const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot);
node.meta = new core.Metadata(parentMeta);
node.meta = new collections.Metadata(parentMeta);
}
return node.meta;
}
@ -390,10 +356,10 @@ namespace vfs {
*/
public debugPrint(): void {
let result = "";
const printLinks = (dirname: string | undefined, links: core.SortedMap<string, Inode>) => {
const iterator = core.getIterator(links);
const printLinks = (dirname: string | undefined, links: collections.SortedMap<string, Inode>) => {
const iterator = collections.getIterator(links);
try {
for (let i = core.nextResult(iterator); i; i = core.nextResult(iterator)) {
for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
const [name, node] = i.value;
const path = dirname ? vpath.combine(dirname, name) : name;
const marker = vpath.compare(this._cwd, path, this.ignoreCase) === 0 ? "*" : " ";
@ -412,7 +378,7 @@ namespace vfs {
}
}
finally {
core.closeIterator(iterator);
collections.closeIterator(iterator);
}
};
printLinks(/*dirname*/ undefined, this._getRootLinks());
@ -686,7 +652,7 @@ namespace vfs {
};
}
private _addLink(parent: DirectoryInode | undefined, links: core.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) {
private _addLink(parent: DirectoryInode | undefined, links: collections.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) {
links.set(name, node);
node.nlink++;
node.ctimeMs = time;
@ -694,14 +660,14 @@ namespace vfs {
if (!parent && !this._cwd) this._cwd = name;
}
private _removeLink(parent: DirectoryInode | undefined, links: core.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) {
private _removeLink(parent: DirectoryInode | undefined, links: collections.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) {
links.delete(name);
node.nlink--;
node.ctimeMs = time;
if (parent) parent.mtimeMs = time;
}
private _replaceLink(oldParent: DirectoryInode, oldLinks: core.SortedMap<string, Inode>, oldName: string, newParent: DirectoryInode, newLinks: core.SortedMap<string, Inode>, newName: string, node: Inode, time: number) {
private _replaceLink(oldParent: DirectoryInode, oldLinks: collections.SortedMap<string, Inode>, oldName: string, newParent: DirectoryInode, newLinks: collections.SortedMap<string, Inode>, newName: string, node: Inode, time: number) {
if (oldParent !== newParent) {
this._removeLink(oldParent, oldLinks, oldName, node, time);
this._addLink(newParent, newLinks, newName, node, time);
@ -716,7 +682,7 @@ namespace vfs {
private _getRootLinks() {
if (!this._lazy.links) {
this._lazy.links = new core.SortedMap<string, Inode>(this.stringComparer);
this._lazy.links = new collections.SortedMap<string, Inode>(this.stringComparer);
if (this._shadowRoot) {
this._copyShadowLinks(this._shadowRoot._getRootLinks(), this._lazy.links);
}
@ -727,7 +693,7 @@ namespace vfs {
private _getLinks(node: DirectoryInode) {
if (!node.links) {
const links = new core.SortedMap<string, Inode>(this.stringComparer);
const links = new collections.SortedMap<string, Inode>(this.stringComparer);
const { source, resolver } = node;
if (source && resolver) {
node.source = undefined;
@ -786,16 +752,16 @@ namespace vfs {
return shadow;
}
private _copyShadowLinks(source: ReadonlyMap<string, Inode>, target: core.SortedMap<string, Inode>) {
const iterator = core.getIterator(source);
private _copyShadowLinks(source: ReadonlyMap<string, Inode>, target: collections.SortedMap<string, Inode>) {
const iterator = collections.getIterator(source);
try {
for (let i = core.nextResult(iterator); i; i = core.nextResult(iterator)) {
for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
const [name, root] = i.value;
target.set(name, this._getShadow(root));
}
}
finally {
core.closeIterator(iterator);
collections.closeIterator(iterator);
}
}
@ -1035,6 +1001,40 @@ namespace vfs {
};
}
/**
* Create a virtual file system from a physical file system using the following path mappings:
*
* - `/.ts` is a directory mapped to `${workspaceRoot}/built/local`
* - `/.lib` is a directory mapped to `${workspaceRoot}/tests/lib`
* - `/.src` is a virtual directory to be used for tests.
*
* Unless overridden, `/.src` will be the current working directory for the virtual file system.
*/
export function createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, cwd }: FileSystemCreateOptions = {}) {
const fs = getBuiltLocal(host, ignoreCase).shadow();
if (cwd) {
fs.mkdirpSync(cwd);
fs.chdir(cwd);
}
if (documents) {
for (const document of documents) {
fs.mkdirpSync(vpath.dirname(document.file));
fs.writeFileSync(document.file, document.text, "utf8");
fs.filemeta(document.file).set("document", document);
// Add symlinks
const symlink = document.meta.get("symlink");
if (symlink) {
for (const link of symlink.split(",").map(link => link.trim())) {
fs.mkdirpSync(vpath.dirname(link));
fs.symlinkSync(document.file, link);
fs.filemeta(link).set("document", document);
}
}
}
}
return fs;
}
export class Stats {
public dev: number;
public ino: number;
@ -1188,7 +1188,7 @@ namespace vfs {
source?: string;
resolver?: FileSystemResolver;
shadowRoot?: FileInode;
meta?: core.Metadata;
meta?: collections.Metadata;
}
interface DirectoryInode {
@ -1200,11 +1200,11 @@ namespace vfs {
ctimeMs: number; // status change time
birthtimeMs: number; // creation time
nlink: number; // number of hard links
links?: core.SortedMap<string, Inode>;
links?: collections.SortedMap<string, Inode>;
source?: string;
resolver?: FileSystemResolver;
shadowRoot?: DirectoryInode;
meta?: core.Metadata;
meta?: collections.Metadata;
}
interface SymlinkInode {
@ -1218,7 +1218,7 @@ namespace vfs {
nlink: number; // number of hard links
symlink?: string;
shadowRoot?: SymlinkInode;
meta?: core.Metadata;
meta?: collections.Metadata;
}
function isFile(node: Inode | undefined): node is FileInode {
@ -1237,7 +1237,7 @@ namespace vfs {
realpath: string;
basename: string;
parent: DirectoryInode | undefined;
links: core.SortedMap<string, Inode> | undefined;
links: collections.SortedMap<string, Inode> | undefined;
node: Inode | undefined;
}

View File

@ -1,17 +1,16 @@
/// <reference path="./core/comparers.ts" />
/// <reference path="./vfs.ts" />
namespace vpath {
/**
* Virtual path separator.
*/
export const sep = "/";
export import sep = ts.directorySeparator;
/**
* Normalize path separators.
*/
export function normalizeSeparators(path: string): string {
return path.replace(/\s*[\\/]\s*/g, sep).trim();
}
export import normalizeSeparators = ts.normalizeSlashes;
// export function normalizeSeparators(path: string): string {
// return ts.normalizeSlashes(path);
// }
const invalidRootComponentRegExp = /^(?!(\/|\/\/\w+\/|[a-zA-Z]:\/?|)$)/;
const invalidNavigableComponentRegExp = /[:*?"<>|]/;
@ -96,6 +95,7 @@ namespace vpath {
const absolutePathRegExp = /^[\\/]([\\/](.*?[\\/](.*?[\\/])?)?)?|^[a-zA-Z]:[\\/]?|^\w+:\/{2}[^\\/]*\/?/;
// NOTE: this differs from `ts.getRootLength` in that it doesn't support URIs.
function getRootLength(path: string) {
const match = absolutePathRegExp.exec(path);
return match ? match[0].length : 0;
@ -105,9 +105,10 @@ namespace vpath {
* Determines whether a path is an absolute path (e.g. starts with `/`, `\\`, or a dos path
* like `c:`).
*/
export function isAbsolute(path: string) {
return absolutePathRegExp.test(path);
}
export import isAbsolute = ts.isRootedDiskPath;
// export function isAbsolute(path: string) {
// return absolutePathRegExp.test(path);
// }
/**
* Determines whether a path consists only of a path root.
@ -129,16 +130,18 @@ namespace vpath {
/**
* Adds a trailing separator (`/`) to a path if it doesn't have one.
*/
export function addTrailingSeparator(path: string) {
return !trailingSeperatorRegExp.test(path) && path ? path + "/" : path;
}
export import addTrailingSeparator = ts.ensureTrailingDirectorySeparator;
// export function addTrailingSeparator(path: string) {
// return !trailingSeperatorRegExp.test(path) && path ? path + "/" : path;
// }
/**
* Removes a trailing separator (`/`) from a path if it has one.
*/
export function removeTrailingSeparator(path: string) {
return trailingSeperatorRegExp.test(path) && !isRoot(path) ? path.slice(0, -1) : path;
}
export import removeTrailingSeparator = ts.removeTrailingDirectorySeparator;
// export function removeTrailingSeparator(path: string) {
// return trailingSeperatorRegExp.test(path) && !isRoot(path) ? path.slice(0, -1) : path;
// }
function reduce(components: ReadonlyArray<string>) {
const normalized = [components[0]];
@ -172,11 +175,12 @@ namespace vpath {
*/
export function combine(path: string, ...paths: string[]) {
path = normalizeSeparators(path);
for (let name of paths) {
name = normalizeSeparators(name);
if (name.length === 0) continue;
path = path.length === 0 || isAbsolute(name) ? name :
addTrailingSeparator(path) + name;
for (const name of paths) {
path = ts.combinePaths(path, normalizeSeparators(name));
// name = normalizeSeparators(name);
// if (name.length === 0) continue;
// path = path.length === 0 || isAbsolute(name) ? name :
// addTrailingSeparator(path) + name;
}
return path;
}
@ -188,6 +192,8 @@ namespace vpath {
return normalize(combine(path, ...paths));
}
// NOTE: this differs from `ts.getRelativePathToDirectoryOrUrl` in that it requires both paths
// are already absolute and does not perform "canonicalization".
function relativeWorker(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean) {
if (!isAbsolute(from)) throw new Error("Path not absolute");
if (!isAbsolute(to)) throw new Error("Path not absolute");
@ -215,20 +221,23 @@ namespace vpath {
}
function relativeCaseSensitive(from: string, to: string) {
return relativeWorker(from, to, core.equateStringsCaseSensitive);
return relativeWorker(from, to, ts.equateStringsCaseSensitive);
}
function relativeCaseInsensitive(from: string, to: string) {
return relativeWorker(from, to, core.equateStringsCaseInsensitive);
return relativeWorker(from, to, ts.equateStringsCaseInsensitive);
}
/**
* Gets a relative path that can be used to traverse between `from` and `to`.
*/
// NOTE: this differs from `ts.getRelativePathToDirectoryOrUrl` in that it requires both paths
// are already absolute and does not perform "canonicalization".
export function relative(from: string, to: string, ignoreCase: boolean) {
return ignoreCase ? relativeCaseInsensitive(from, to) : relativeCaseSensitive(from, to);
}
// NOTE: this differs from `ts.comparePaths` due to the behavior of `parse`.
function compareWorker(a: string, b: string, stringComparer: (a: string, b: string) => number) {
if (a === b) return 0;
a = removeTrailingSeparator(a);
@ -241,32 +250,33 @@ namespace vpath {
const result = stringComparer(aComponents[i], bComponents[i]);
if (result !== 0) return result;
}
return core.compareNumbers(aComponents.length, bComponents.length);
return ts.compareValues(aComponents.length, bComponents.length);
}
/**
* Performs a case-sensitive comparison of two paths.
*/
export function compareCaseSensitive(a: string, b: string) {
return compareWorker(a, b, core.compareStringsCaseSensitive);
return compareWorker(a, b, ts.compareStringsCaseSensitive);
}
/**
* Performs a case-insensitive comparison of two paths.
*/
export function compareCaseInsensitive(a: string, b: string) {
return compareWorker(a, b, core.compareStringsCaseInsensitive);
return compareWorker(a, b, ts.compareStringsCaseInsensitive);
}
/**
* Compare two paths.
*/
// NOTE: this differs from `ts.comparePaths` due to the behavior of `parse`.
export function compare(a: string, b: string, ignoreCase: boolean) {
return ignoreCase ? compareCaseInsensitive(a, b) : compareCaseSensitive(a, b);
}
/**
* Determines whether two strings are equal.
* Determines whether two paths are equal.
*/
export function equals(a: string, b: string, ignoreCase: boolean) {
if (!isAbsolute(a)) throw new Error("Path not absolute");
@ -281,6 +291,7 @@ namespace vpath {
return ignoreCase && a.toUpperCase() === b.toUpperCase();
}
// NOTE: this differs from `ts.containsPath` due to the behavior of `parse`.
function beneathWorker(ancestor: string, descendant: string, stringEqualityComparer: (a: string, b: string) => boolean) {
if (!isAbsolute(ancestor)) throw new Error("Path not absolute");
if (!isAbsolute(descendant)) throw new Error("Path not absolute");
@ -296,16 +307,17 @@ namespace vpath {
}
function beneathCaseSensitive(ancestor: string, descendant: string) {
return beneathWorker(ancestor, descendant, core.equateStringsCaseSensitive);
return beneathWorker(ancestor, descendant, ts.equateStringsCaseSensitive);
}
function beneathCaseInsensitive(ancestor: string, descendant: string) {
return beneathWorker(ancestor, descendant, core.equateStringsCaseInsensitive);
return beneathWorker(ancestor, descendant, ts.equateStringsCaseInsensitive);
}
/**
* Determines whether the path `descendant` is beneath the path `ancestor`.
*/
// NOTE: this differs from `containsPath` in compiler/core.ts due to the behavior of `parse`.
export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) {
return ignoreCase ? beneathCaseInsensitive(ancestor, descendant) : beneathCaseSensitive(ancestor, descendant);
}
@ -315,6 +327,9 @@ namespace vpath {
* If the path is relative, the root component is `""`.
* If the path is absolute, the root component includes the first path separator (`/`).
*/
// NOTE: this differs from `ts.getNormalizedPathComponents` due to the fact that `parse` does
// not automatically normalize relative paths and does not perform path normalization. This is
// necessary to support proper path navigation in `vfs`.
export function parse(path: string) {
path = normalizeSeparators(path);
const rootLength = getRootLength(path);
@ -327,6 +342,8 @@ namespace vpath {
/**
* Formats a parsed path consisting of a root component and zero or more path segments.
*/
// NOTE: this differs from `ts.getNormalizedPathFromPathComponents` in that this function
// always returns a string.
export function format(components: ReadonlyArray<string>) {
return components.length ? components[0] + components.slice(1).join(sep) : "";
}
@ -334,6 +351,7 @@ namespace vpath {
/**
* Gets the parent directory name of a path.
*/
// NOTE: this differs from `ts.getDirectoryPath` due to the behavior of `getRootLength`.
export function dirname(path: string) {
path = normalizeSeparators(path);
path = removeTrailingSeparator(path);
@ -349,6 +367,8 @@ namespace vpath {
* If the base name has any one of the provided extensions, it is removed.
*/
export function basename(path: string, extensions: string | ReadonlyArray<string>, ignoreCase: boolean): string;
// NOTE: this differs from `ts.getBaseFileName` in that this function handles extensions in a
// fashion similar to the NodeJS `path.basename` function as well as handles case sensitivity.
export function basename(path: string, extensions?: string | ReadonlyArray<string>, ignoreCase?: boolean) {
path = normalizeSeparators(path);
path = removeTrailingSeparator(path);
@ -384,9 +404,11 @@ namespace vpath {
* Gets the file extension for a path, provided it is one of the provided extensions.
*/
export function extname(path: string, extensions: string | ReadonlyArray<string>, ignoreCase: boolean): string;
// NOTE: this differs from `ts.getAnyExtensionFromPath` in that this function allows you to
// restrict extensions and handle case sensitivity
export function extname(path: string, extensions?: string | ReadonlyArray<string>, ignoreCase?: boolean) {
if (extensions) {
return extnameWorker(path, extensions, ignoreCase ? core.equateStringsCaseInsensitive : core.equateStringsCaseSensitive);
return extnameWorker(path, extensions, ignoreCase ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive);
}
const match = extRegExp.exec(path);
@ -395,6 +417,8 @@ namespace vpath {
export function changeExtension(path: string, ext: string): string;
export function changeExtension(path: string, ext: string, extensions: string | ReadonlyArray<string>, ignoreCase: boolean): string;
// NOTE: this differs from `ts.changeExtension` in that this function allows you to
// specify extensions and handle case sensitivity
export function changeExtension(path: string, ext: string, extensions?: string | ReadonlyArray<string>, ignoreCase?: boolean) {
const pathext = extensions !== undefined && ignoreCase !== undefined ? extname(path, extensions, ignoreCase) : extname(path);
return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path;