TypeScript/src/harness/vfsUtil.ts
Ryan Cavanaugh e00b5ecd40
Enable max-statements-per-line lint rule (#45475)
* Enable the rule

* Fix all the violations
2021-08-16 13:53:51 -07:00

1621 lines
69 KiB
TypeScript

namespace vfs {
/**
* Posix-style path to the TypeScript compiler build outputs (including tsc.js, lib.d.ts, etc.)
*/
export const builtFolder = "/.ts";
/**
* Posix-style path to additional mountable folders (./tests/projects in this repo)
*/
export const projectsFolder = "/.projects";
/**
* Posix-style path to additional test libraries
*/
export const testLibFolder = "/.lib";
/**
* Posix-style path to sources under test
*/
export const srcFolder = "/.src";
// file type
const S_IFMT = 0o170000; // file type
const S_IFSOCK = 0o140000; // socket
const S_IFLNK = 0o120000; // symbolic link
const S_IFREG = 0o100000; // regular file
const S_IFBLK = 0o060000; // block device
const S_IFDIR = 0o040000; // directory
const S_IFCHR = 0o020000; // character device
const S_IFIFO = 0o010000; // FIFO
let devCount = 0; // A monotonically increasing count of device ids
let inoCount = 0; // A monotonically increasing count of inodes
export interface DiffOptions {
includeChangedFileWithSameContent?: boolean;
baseIsNotShadowRoot?: boolean;
}
/**
* Represents a virtual POSIX-like file system.
*/
export class FileSystem {
/** Indicates whether the file system is case-sensitive (`false`) or case-insensitive (`true`). */
public readonly ignoreCase: boolean;
/** Gets the comparison function used to compare two paths. */
public readonly stringComparer: (a: string, b: string) => number;
// lazy-initialized state that should be mutable even if the FileSystem is frozen.
private _lazy: {
links?: collections.SortedMap<string, Inode>;
shadows?: Map<number, Inode>;
meta?: collections.Metadata;
} = {};
private _cwd: string; // current working directory
private _time: number | Date | (() => number | Date);
private _shadowRoot: FileSystem | undefined;
private _dirStack: string[] | undefined;
constructor(ignoreCase: boolean, options: FileSystemOptions = {}) {
const { time = -1, files, meta } = options;
this.ignoreCase = ignoreCase;
this.stringComparer = this.ignoreCase ? vpath.compareCaseInsensitive : vpath.compareCaseSensitive;
this._time = time;
if (meta) {
for (const key of Object.keys(meta)) {
this.meta.set(key, meta[key]);
}
}
if (files) {
this._applyFiles(files, /*dirname*/ "");
}
let cwd = options.cwd;
if ((!cwd || !vpath.isRoot(cwd)) && this._lazy.links) {
const iterator = collections.getIterator(this._lazy.links.keys());
try {
for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
const name = i.value;
cwd = cwd ? vpath.resolve(name, cwd) : name;
break;
}
}
finally {
collections.closeIterator(iterator);
}
}
if (cwd) {
vpath.validate(cwd, vpath.ValidationFlags.Absolute);
this.mkdirpSync(cwd);
}
this._cwd = cwd || "";
}
/**
* Gets metadata for this `FileSystem`.
*/
public get meta(): collections.Metadata {
if (!this._lazy.meta) {
this._lazy.meta = new collections.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined);
}
return this._lazy.meta;
}
/**
* Gets a value indicating whether the file system is read-only.
*/
public get isReadonly() {
return Object.isFrozen(this);
}
/**
* Makes the file system read-only.
*/
public makeReadonly() {
Object.freeze(this);
return this;
}
/**
* Gets the file system shadowed by this file system.
*/
public get shadowRoot() {
return this._shadowRoot;
}
/**
* Snapshots the current file system, effectively shadowing itself. This is useful for
* generating file system patches using `.diff()` from one snapshot to the next. Performs
* no action if this file system is read-only.
*/
public snapshot() {
if (this.isReadonly) return;
const fs = new FileSystem(this.ignoreCase, { time: this._time });
fs._lazy = this._lazy;
fs._cwd = this._cwd;
fs._time = this._time;
fs._shadowRoot = this._shadowRoot;
fs._dirStack = this._dirStack;
fs.makeReadonly();
this._lazy = {};
this._shadowRoot = 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
* of the same data.
*/
public shadow(ignoreCase = this.ignoreCase) {
if (!this.isReadonly) throw new Error("Cannot shadow a mutable file system.");
if (ignoreCase && !this.ignoreCase) throw new Error("Cannot create a case-insensitive file system from a case-sensitive one.");
const fs = new FileSystem(ignoreCase, { time: this._time });
fs._shadowRoot = this;
fs._cwd = this._cwd;
return fs;
}
/**
* Gets or sets the timestamp (in milliseconds) used for file status, returning the previous timestamp.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/time.html
*/
public time(value?: number | Date | (() => number | Date)): number {
if (value !== undefined && this.isReadonly) throw createIOError("EPERM");
let result = this._time;
if (typeof result === "function") result = result();
if (typeof result === "object") result = result.getTime();
if (result === -1) result = Date.now();
if (value !== undefined) {
this._time = value;
}
return result;
}
/**
* Gets the metadata object for a path.
* @param path
*/
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): collections.Metadata {
if (!node.meta) {
const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot);
node.meta = new collections.Metadata(parentMeta);
}
return node.meta;
}
/**
* Get the pathname of the current working directory.
*
* @link - http://pubs.opengroup.org/onlinepubs/9699919799/functions/getcwd.html
*/
public cwd() {
if (!this._cwd) throw new Error("The current working directory has not been set.");
const { node } = this._walk(this._cwd);
if (!node) throw createIOError("ENOENT");
if (!isDirectory(node)) throw createIOError("ENOTDIR");
return this._cwd;
}
/**
* Changes the current working directory.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/chdir.html
*/
public chdir(path: string) {
if (this.isReadonly) throw createIOError("EPERM");
path = this._resolve(path);
const { node } = this._walk(path);
if (!node) throw createIOError("ENOENT");
if (!isDirectory(node)) throw createIOError("ENOTDIR");
this._cwd = path;
}
/**
* Pushes the current directory onto the directory stack and changes the current working directory to the supplied path.
*/
public pushd(path?: string) {
if (this.isReadonly) throw createIOError("EPERM");
if (path) path = this._resolve(path);
if (this._cwd) {
if (!this._dirStack) this._dirStack = [];
this._dirStack.push(this._cwd);
}
if (path && path !== this._cwd) {
this.chdir(path);
}
}
/**
* Pops the previous directory from the location stack and changes the current directory to that directory.
*/
public popd() {
if (this.isReadonly) throw createIOError("EPERM");
const path = this._dirStack && this._dirStack.pop();
if (path) {
this.chdir(path);
}
}
/**
* Update the file system with a set of files.
*/
public apply(files: FileSet) {
this._applyFiles(files, this._cwd);
}
/**
* Scan file system entries along a path. If `path` is a symbolic link, it is dereferenced.
* @param path The path at which to start the scan.
* @param axis The axis along which to traverse.
* @param traversal The traversal scheme to use.
*/
public scanSync(path: string, axis: Axis, traversal: Traversal) {
path = this._resolve(path);
const results: string[] = [];
this._scan(path, this._stat(this._walk(path)), axis, traversal, /*noFollow*/ false, results);
return results;
}
/**
* Scan file system entries along a path.
* @param path The path at which to start the scan.
* @param axis The axis along which to traverse.
* @param traversal The traversal scheme to use.
*/
public lscanSync(path: string, axis: Axis, traversal: Traversal) {
path = this._resolve(path);
const results: string[] = [];
this._scan(path, this._stat(this._walk(path, /*noFollow*/ true)), axis, traversal, /*noFollow*/ true, results);
return results;
}
private _scan(path: string, stats: Stats, axis: Axis, traversal: Traversal, noFollow: boolean, results: string[]) {
if (axis === "ancestors-or-self" || axis === "self" || axis === "descendants-or-self") {
if (!traversal.accept || traversal.accept(path, stats)) {
results.push(path);
}
}
if (axis === "ancestors-or-self" || axis === "ancestors") {
const dirname = vpath.dirname(path);
if (dirname !== path) {
try {
const stats = this._stat(this._walk(dirname, noFollow));
if (!traversal.traverse || traversal.traverse(dirname, stats)) {
this._scan(dirname, stats, "ancestors-or-self", traversal, noFollow, results);
}
}
catch { /*ignored*/ }
}
}
if (axis === "descendants-or-self" || axis === "descendants") {
if (stats.isDirectory() && (!traversal.traverse || traversal.traverse(path, stats))) {
for (const file of this.readdirSync(path)) {
try {
const childpath = vpath.combine(path, file);
const stats = this._stat(this._walk(childpath, noFollow));
this._scan(childpath, stats, "descendants-or-self", traversal, noFollow, results);
}
catch { /*ignored*/ }
}
}
}
}
/**
* Mounts a physical or virtual file system at a location in this virtual file system.
*
* @param source The path in the physical (or other virtual) file system.
* @param target The path in this virtual file system.
* @param resolver An object used to resolve files in `source`.
*/
public mountSync(source: string, target: string, resolver: FileSystemResolver) {
if (this.isReadonly) throw createIOError("EROFS");
source = vpath.validate(source, vpath.ValidationFlags.Absolute);
const { parent, links, node: existingNode, basename } = this._walk(this._resolve(target), /*noFollow*/ true);
if (existingNode) throw createIOError("EEXIST");
const time = this.time();
const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time);
node.source = source;
node.resolver = resolver;
this._addLink(parent, links, basename, node, time);
}
/**
* Recursively remove all files and directories underneath the provided path.
*/
public rimrafSync(path: string) {
try {
const stats = this.lstatSync(path);
if (stats.isFile() || stats.isSymbolicLink()) {
this.unlinkSync(path);
}
else if (stats.isDirectory()) {
for (const file of this.readdirSync(path)) {
this.rimrafSync(vpath.combine(path, file));
}
this.rmdirSync(path);
}
}
catch (e) {
if (e.code === "ENOENT") return;
throw e;
}
}
/**
* Make a directory and all of its parent paths (if they don't exist).
*/
public mkdirpSync(path: string) {
path = this._resolve(path);
const result = this._walk(path, /*noFollow*/ true, (error, result) => {
if (error.code === "ENOENT") {
this._mkdir(result);
return "retry";
}
return "throw";
});
if (!result.node) this._mkdir(result);
}
public getFileListing(): string {
let result = "";
const printLinks = (dirname: string | undefined, links: collections.SortedMap<string, Inode>) => {
const iterator = collections.getIterator(links);
try {
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 ? "*" : " ";
if (result) result += "\n";
result += marker;
if (isDirectory(node)) {
result += vpath.addTrailingSeparator(path);
printLinks(path, this._getLinks(node));
}
else if (isFile(node)) {
result += path;
}
else if (isSymlink(node)) {
result += path + " -> " + node.symlink;
}
}
}
finally {
collections.closeIterator(iterator);
}
};
printLinks(/*dirname*/ undefined, this._getRootLinks());
return result;
}
/**
* Print diagnostic information about the structure of the file system to the console.
*/
public debugPrint(): void {
console.log(this.getFileListing());
}
// POSIX API (aligns with NodeJS "fs" module API)
/**
* Determines whether a path exists.
*/
public existsSync(path: string) {
const result = this._walk(this._resolve(path), /*noFollow*/ true, () => "stop");
return result !== undefined && result.node !== undefined;
}
/**
* Get file status. If `path` is a symbolic link, it is dereferenced.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public statSync(path: string) {
return this._stat(this._walk(this._resolve(path)));
}
/**
* Change file access times
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public utimesSync(path: string, atime: Date, mtime: Date) {
if (this.isReadonly) throw createIOError("EROFS");
if (!isFinite(+atime) || !isFinite(+mtime)) throw createIOError("EINVAL");
const entry = this._walk(this._resolve(path));
if (!entry || !entry.node) {
throw createIOError("ENOENT");
}
entry.node.atimeMs = +atime;
entry.node.mtimeMs = +mtime;
entry.node.ctimeMs = this.time();
}
/**
* Get file status. If `path` is a symbolic link, it is dereferenced.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public lstatSync(path: string) {
return this._stat(this._walk(this._resolve(path), /*noFollow*/ true));
}
private _stat(entry: WalkResult) {
const node = entry.node;
if (!node) throw createIOError(`ENOENT`, entry.realpath);
return new Stats(
node.dev,
node.ino,
node.mode,
node.nlink,
/*rdev*/ 0,
/*size*/ isFile(node) ? this._getSize(node) : isSymlink(node) ? node.symlink.length : 0,
/*blksize*/ 4096,
/*blocks*/ 0,
node.atimeMs,
node.mtimeMs,
node.ctimeMs,
node.birthtimeMs,
);
}
/**
* Read a directory. If `path` is a symbolic link, it is dereferenced.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public readdirSync(path: string) {
const { node } = this._walk(this._resolve(path));
if (!node) throw createIOError("ENOENT");
if (!isDirectory(node)) throw createIOError("ENOTDIR");
return Array.from(this._getLinks(node).keys());
}
/**
* Make a directory.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdir.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public mkdirSync(path: string) {
if (this.isReadonly) throw createIOError("EROFS");
this._mkdir(this._walk(this._resolve(path), /*noFollow*/ true));
}
private _mkdir({ parent, links, node: existingNode, basename }: WalkResult) {
if (existingNode) throw createIOError("EEXIST");
const time = this.time();
const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time);
this._addLink(parent, links, basename, node, time);
}
/**
* Remove a directory.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/rmdir.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public rmdirSync(path: string) {
if (this.isReadonly) throw createIOError("EROFS");
path = this._resolve(path);
const { parent, links, node, basename } = this._walk(path, /*noFollow*/ true);
if (!parent) throw createIOError("EPERM");
if (!isDirectory(node)) throw createIOError("ENOTDIR");
if (this._getLinks(node).size !== 0) throw createIOError("ENOTEMPTY");
this._removeLink(parent, links, basename, node);
}
/**
* Link one file to another file (also known as a "hard link").
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public linkSync(oldpath: string, newpath: string) {
if (this.isReadonly) throw createIOError("EROFS");
const { node } = this._walk(this._resolve(oldpath));
if (!node) throw createIOError("ENOENT");
if (isDirectory(node)) throw createIOError("EPERM");
const { parent, links, basename, node: existingNode } = this._walk(this._resolve(newpath), /*noFollow*/ true);
if (!parent) throw createIOError("EPERM");
if (existingNode) throw createIOError("EEXIST");
this._addLink(parent, links, basename, node);
}
/**
* Remove a directory entry.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/unlink.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public unlinkSync(path: string) {
if (this.isReadonly) throw createIOError("EROFS");
const { parent, links, node, basename } = this._walk(this._resolve(path), /*noFollow*/ true);
if (!parent) throw createIOError("EPERM");
if (!node) throw createIOError("ENOENT");
if (isDirectory(node)) throw createIOError("EISDIR");
this._removeLink(parent, links, basename, node);
}
/**
* Rename a file.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public renameSync(oldpath: string, newpath: string) {
if (this.isReadonly) throw createIOError("EROFS");
const { parent: oldParent, links: oldParentLinks, node, basename: oldBasename } = this._walk(this._resolve(oldpath), /*noFollow*/ true);
if (!oldParent) throw createIOError("EPERM");
if (!node) throw createIOError("ENOENT");
const { parent: newParent, links: newParentLinks, node: existingNode, basename: newBasename } = this._walk(this._resolve(newpath), /*noFollow*/ true);
if (!newParent) throw createIOError("EPERM");
const time = this.time();
if (existingNode) {
if (isDirectory(node)) {
if (!isDirectory(existingNode)) throw createIOError("ENOTDIR");
// if both old and new arguments point to the same directory, just pass. So we could rename /src/a/1 to /src/A/1 in Win.
// if not and the directory pointed by the new path is not empty, throw an error.
if (this.stringComparer(oldpath, newpath) !== 0 && this._getLinks(existingNode).size > 0) throw createIOError("ENOTEMPTY");
}
else {
if (isDirectory(existingNode)) throw createIOError("EISDIR");
}
this._removeLink(newParent, newParentLinks, newBasename, existingNode, time);
}
this._replaceLink(oldParent, oldParentLinks, oldBasename, newParent, newParentLinks, newBasename, node, time);
}
/**
* Make a symbolic link.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public symlinkSync(target: string, linkpath: string) {
if (this.isReadonly) throw createIOError("EROFS");
const { parent, links, node: existingNode, basename } = this._walk(this._resolve(linkpath), /*noFollow*/ true);
if (!parent) throw createIOError("EPERM");
if (existingNode) throw createIOError("EEXIST");
const time = this.time();
const node = this._mknod(parent.dev, S_IFLNK, /*mode*/ 0o666, time);
node.symlink = vpath.validate(target, vpath.ValidationFlags.RelativeOrAbsolute);
this._addLink(parent, links, basename, node, time);
}
/**
* Resolve a pathname.
*
* @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public realpathSync(path: string) {
const { realpath } = this._walk(this._resolve(path));
return realpath;
}
/**
* Read from a file.
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public readFileSync(path: string, encoding?: null): Buffer;
/**
* Read from a file.
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public readFileSync(path: string, encoding: BufferEncoding): string;
/**
* Read from a file.
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
public readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer;
public readFileSync(path: string, encoding: BufferEncoding | null = null) { // eslint-disable-line no-null/no-null
const { node } = this._walk(this._resolve(path));
if (!node) throw createIOError("ENOENT");
if (isDirectory(node)) throw createIOError("EISDIR");
if (!isFile(node)) throw createIOError("EBADF");
const buffer = this._getBuffer(node).slice();
return encoding ? buffer.toString(encoding) : buffer;
}
/**
* Write to a file.
*
* NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
*/
// eslint-disable-next-line no-null/no-null
public writeFileSync(path: string, data: string | Buffer, encoding: string | null = null) {
if (this.isReadonly) throw createIOError("EROFS");
const { parent, links, node: existingNode, basename } = this._walk(this._resolve(path), /*noFollow*/ false);
if (!parent) throw createIOError("EPERM");
const time = this.time();
let node = existingNode;
if (!node) {
node = this._mknod(parent.dev, S_IFREG, 0o666, time);
this._addLink(parent, links, basename, node, time);
}
if (isDirectory(node)) throw createIOError("EISDIR");
if (!isFile(node)) throw createIOError("EBADF");
node.buffer = Buffer.isBuffer(data) ? data.slice() : ts.sys.bufferFrom!("" + data, encoding || "utf8") as Buffer;
node.size = node.buffer.byteLength;
node.mtimeMs = time;
node.ctimeMs = time;
}
/**
* Generates a `FileSet` patch containing all the entries in this `FileSystem` that are not in `base`.
* @param base The base file system. If not provided, this file system's `shadowRoot` is used (if present).
*/
public diff(base?: FileSystem | undefined, options: DiffOptions = {}) {
if (!base && !options.baseIsNotShadowRoot) base = this.shadowRoot;
const differences: FileSet = {};
const hasDifferences = base ?
FileSystem.rootDiff(differences, this, base, options) :
FileSystem.trackCreatedInodes(differences, this, this._getRootLinks());
return hasDifferences ? differences : undefined;
}
/**
* Generates a `FileSet` patch containing all the entries in `changed` that are not in `base`.
*/
public static diff(changed: FileSystem, base: FileSystem, options: DiffOptions = {}) {
const differences: FileSet = {};
return FileSystem.rootDiff(differences, changed, base, options) ?
differences :
undefined;
}
private static diffWorker(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap<string, Inode> | undefined, base: FileSystem, baseLinks: ReadonlyMap<string, Inode> | undefined, options: DiffOptions) {
if (changedLinks && !baseLinks) return FileSystem.trackCreatedInodes(container, changed, changedLinks);
if (baseLinks && !changedLinks) return FileSystem.trackDeletedInodes(container, baseLinks);
if (changedLinks && baseLinks) {
let hasChanges = false;
// track base items missing in changed
baseLinks.forEach((node, basename) => {
if (!changedLinks.has(basename)) {
container[basename] = isDirectory(node) ? new Rmdir() : new Unlink();
hasChanges = true;
}
});
// track changed items missing or differing in base
changedLinks.forEach((changedNode, basename) => {
const baseNode = baseLinks.get(basename);
if (baseNode) {
if (isDirectory(changedNode) && isDirectory(baseNode)) {
return hasChanges = FileSystem.directoryDiff(container, basename, changed, changedNode, base, baseNode, options) || hasChanges;
}
if (isFile(changedNode) && isFile(baseNode)) {
return hasChanges = FileSystem.fileDiff(container, basename, changed, changedNode, base, baseNode, options) || hasChanges;
}
if (isSymlink(changedNode) && isSymlink(baseNode)) {
return hasChanges = FileSystem.symlinkDiff(container, basename, changedNode, baseNode) || hasChanges;
}
}
return hasChanges = FileSystem.trackCreatedInode(container, basename, changed, changedNode) || hasChanges;
});
return hasChanges;
}
return false;
}
private static rootDiff(container: FileSet, changed: FileSystem, base: FileSystem, options: DiffOptions) {
while (!changed._lazy.links && changed._shadowRoot) changed = changed._shadowRoot;
while (!base._lazy.links && base._shadowRoot) base = base._shadowRoot;
// no difference if the file systems are the same reference
if (changed === base) return false;
// no difference if the root links are empty and unshadowed
if (!changed._lazy.links && !changed._shadowRoot && !base._lazy.links && !base._shadowRoot) return false;
return FileSystem.diffWorker(container, changed, changed._getRootLinks(), base, base._getRootLinks(), options);
}
private static directoryDiff(container: FileSet, basename: string, changed: FileSystem, changedNode: DirectoryInode, base: FileSystem, baseNode: DirectoryInode, options: DiffOptions) {
while (!changedNode.links && changedNode.shadowRoot) changedNode = changedNode.shadowRoot;
while (!baseNode.links && baseNode.shadowRoot) baseNode = baseNode.shadowRoot;
// no difference if the nodes are the same reference
if (changedNode === baseNode) return false;
// no difference if both nodes are non shadowed and have no entries
if (isEmptyNonShadowedDirectory(changedNode) && isEmptyNonShadowedDirectory(baseNode)) return false;
// no difference if both nodes are unpopulated and point to the same mounted file system
if (!changedNode.links && !baseNode.links &&
changedNode.resolver && changedNode.source !== undefined &&
baseNode.resolver === changedNode.resolver && baseNode.source === changedNode.source) return false;
// no difference if both nodes have identical children
const children: FileSet = {};
if (!FileSystem.diffWorker(children, changed, changed._getLinks(changedNode), base, base._getLinks(baseNode), options)) {
return false;
}
container[basename] = new Directory(children);
return true;
}
private static fileDiff(container: FileSet, basename: string, changed: FileSystem, changedNode: FileInode, base: FileSystem, baseNode: FileInode, options: DiffOptions) {
while (!changedNode.buffer && changedNode.shadowRoot) changedNode = changedNode.shadowRoot;
while (!baseNode.buffer && baseNode.shadowRoot) baseNode = baseNode.shadowRoot;
// no difference if the nodes are the same reference
if (changedNode === baseNode) return false;
// no difference if both nodes are non shadowed and have no entries
if (isEmptyNonShadowedFile(changedNode) && isEmptyNonShadowedFile(baseNode)) return false;
// no difference if both nodes are unpopulated and point to the same mounted file system
if (!changedNode.buffer && !baseNode.buffer &&
changedNode.resolver && changedNode.source !== undefined &&
baseNode.resolver === changedNode.resolver && baseNode.source === changedNode.source) return false;
const changedBuffer = changed._getBuffer(changedNode);
const baseBuffer = base._getBuffer(baseNode);
// no difference if both buffers are the same reference
if (changedBuffer === baseBuffer) return false;
// no difference if both buffers are identical
if (Buffer.compare(changedBuffer, baseBuffer) === 0) {
if (!options.includeChangedFileWithSameContent) return false;
container[basename] = new SameFileContentFile(changedBuffer);
return true;
}
container[basename] = new File(changedBuffer);
return true;
}
private static symlinkDiff(container: FileSet, basename: string, changedNode: SymlinkInode, baseNode: SymlinkInode) {
// no difference if the nodes are the same reference
if (changedNode.symlink === baseNode.symlink) return false;
container[basename] = new Symlink(changedNode.symlink);
return true;
}
private static trackCreatedInode(container: FileSet, basename: string, changed: FileSystem, node: Inode) {
if (isDirectory(node)) {
const children: FileSet = {};
FileSystem.trackCreatedInodes(children, changed, changed._getLinks(node));
container[basename] = new Directory(children);
}
else if (isSymlink(node)) {
container[basename] = new Symlink(node.symlink);
}
else {
container[basename] = new File(node.buffer || "");
}
return true;
}
private static trackCreatedInodes(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap<string, Inode>) {
// no difference if links are empty
if (!changedLinks.size) return false;
changedLinks.forEach((node, basename) => {
FileSystem.trackCreatedInode(container, basename, changed, node);
});
return true;
}
private static trackDeletedInodes(container: FileSet, baseLinks: ReadonlyMap<string, Inode>) {
// no difference if links are empty
if (!baseLinks.size) return false;
baseLinks.forEach((node, basename) => {
container[basename] = isDirectory(node) ? new Rmdir() : new Unlink();
});
return true;
}
private _mknod(dev: number, type: typeof S_IFREG, mode: number, time?: number): FileInode;
private _mknod(dev: number, type: typeof S_IFDIR, mode: number, time?: number): DirectoryInode;
private _mknod(dev: number, type: typeof S_IFLNK, mode: number, time?: number): SymlinkInode;
private _mknod(dev: number, type: number, mode: number, time = this.time()) {
return {
dev,
ino: ++inoCount,
mode: (mode & ~S_IFMT & ~0o022 & 0o7777) | (type & S_IFMT),
atimeMs: time,
mtimeMs: time,
ctimeMs: time,
birthtimeMs: time,
nlink: 0
} as Inode;
}
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;
if (parent) parent.mtimeMs = time;
if (!parent && !this._cwd) this._cwd = name;
}
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: 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);
}
else {
oldLinks.delete(oldName);
oldLinks.set(newName, node);
oldParent.mtimeMs = time;
newParent.mtimeMs = time;
}
}
private _getRootLinks() {
if (!this._lazy.links) {
this._lazy.links = new collections.SortedMap<string, Inode>(this.stringComparer);
if (this._shadowRoot) {
this._copyShadowLinks(this._shadowRoot._getRootLinks(), this._lazy.links);
}
this._lazy.links = this._lazy.links;
}
return this._lazy.links;
}
private _getLinks(node: DirectoryInode) {
if (!node.links) {
const links = new collections.SortedMap<string, Inode>(this.stringComparer);
const { source, resolver } = node;
if (source && resolver) {
node.source = undefined;
node.resolver = undefined;
for (const name of resolver.readdirSync(source)) {
const path = vpath.combine(source, name);
const stats = resolver.statSync(path);
switch (stats.mode & S_IFMT) {
case S_IFDIR:
const dir = this._mknod(node.dev, S_IFDIR, 0o777);
dir.source = vpath.combine(source, name);
dir.resolver = resolver;
this._addLink(node, links, name, dir);
break;
case S_IFREG:
const file = this._mknod(node.dev, S_IFREG, 0o666);
file.source = vpath.combine(source, name);
file.resolver = resolver;
file.size = stats.size;
this._addLink(node, links, name, file);
break;
}
}
}
else if (this._shadowRoot && node.shadowRoot) {
this._copyShadowLinks(this._shadowRoot._getLinks(node.shadowRoot), links);
}
node.links = links;
}
return node.links;
}
private _getShadow(root: DirectoryInode): DirectoryInode;
private _getShadow(root: Inode): Inode;
private _getShadow(root: Inode) {
const shadows = this._lazy.shadows || (this._lazy.shadows = new Map<number, Inode>());
let shadow = shadows.get(root.ino);
if (!shadow) {
shadow = {
dev: root.dev,
ino: root.ino,
mode: root.mode,
atimeMs: root.atimeMs,
mtimeMs: root.mtimeMs,
ctimeMs: root.ctimeMs,
birthtimeMs: root.birthtimeMs,
nlink: root.nlink,
shadowRoot: root
} as Inode;
if (isSymlink(root)) (shadow as SymlinkInode).symlink = root.symlink;
shadows.set(shadow.ino, shadow);
}
return shadow;
}
private _copyShadowLinks(source: ReadonlyMap<string, Inode>, target: collections.SortedMap<string, Inode>) {
const iterator = collections.getIterator(source);
try {
for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
const [name, root] = i.value;
target.set(name, this._getShadow(root));
}
}
finally {
collections.closeIterator(iterator);
}
}
private _getSize(node: FileInode): number {
if (node.buffer) return node.buffer.byteLength;
if (node.size !== undefined) return node.size;
if (node.source && node.resolver) return node.size = node.resolver.statSync(node.source).size;
if (this._shadowRoot && node.shadowRoot) return node.size = this._shadowRoot._getSize(node.shadowRoot);
return 0;
}
private _getBuffer(node: FileInode): Buffer {
if (!node.buffer) {
const { source, resolver } = node;
if (source && resolver) {
node.source = undefined;
node.resolver = undefined;
node.size = undefined;
node.buffer = resolver.readFileSync(source);
}
else if (this._shadowRoot && node.shadowRoot) {
node.buffer = this._shadowRoot._getBuffer(node.shadowRoot);
}
else {
node.buffer = Buffer.allocUnsafe(0);
}
}
return node.buffer;
}
/**
* Walk a path to its end.
*
* @param path The path to follow.
* @param noFollow A value indicating whether to *not* dereference a symbolic link at the
* end of a path.
*
* @link http://man7.org/linux/man-pages/man7/path_resolution.7.html
*/
private _walk(path: string, noFollow?: boolean, onError?: (error: NodeJS.ErrnoException, fragment: WalkResult) => "retry" | "throw"): WalkResult;
private _walk(path: string, noFollow?: boolean, onError?: (error: NodeJS.ErrnoException, fragment: WalkResult) => "stop" | "retry" | "throw"): WalkResult | undefined;
private _walk(path: string, noFollow?: boolean, onError?: (error: NodeJS.ErrnoException, fragment: WalkResult) => "stop" | "retry" | "throw"): WalkResult | undefined {
let links = this._getRootLinks();
let parent: DirectoryInode | undefined;
let components = vpath.parse(path);
let step = 0;
let depth = 0;
let retry = false;
while (true) {
if (depth >= 40) throw createIOError("ELOOP");
const lastStep = step === components.length - 1;
let basename = components[step];
const linkEntry = links.getEntry(basename);
if (linkEntry) {
components[step] = basename = linkEntry[0];
}
const node = linkEntry?.[1];
if (lastStep && (noFollow || !isSymlink(node))) {
return { realpath: vpath.format(components), basename, parent, links, node };
}
if (node === undefined) {
if (trapError(createIOError("ENOENT"), node)) continue;
return undefined;
}
if (isSymlink(node)) {
const dirname = vpath.format(components.slice(0, step));
const symlink = vpath.resolve(dirname, node.symlink);
links = this._getRootLinks();
parent = undefined;
components = vpath.parse(symlink).concat(components.slice(step + 1));
step = 0;
depth++;
retry = false;
continue;
}
if (isDirectory(node)) {
links = this._getLinks(node);
parent = node;
step++;
retry = false;
continue;
}
if (trapError(createIOError("ENOTDIR"), node)) continue;
return undefined;
}
function trapError(error: NodeJS.ErrnoException, node?: Inode) {
const realpath = vpath.format(components.slice(0, step + 1));
const basename = components[step];
const result = !retry && onError ? onError(error, { realpath, basename, parent, links, node }) : "throw";
if (result === "stop") return false;
if (result === "retry") {
retry = true;
return true;
}
throw error;
}
}
/**
* Resolve a path relative to the current working directory.
*/
private _resolve(path: string) {
return this._cwd
? vpath.resolve(this._cwd, vpath.validate(path, vpath.ValidationFlags.RelativeOrAbsolute | vpath.ValidationFlags.AllowWildcard))
: vpath.validate(path, vpath.ValidationFlags.Absolute | vpath.ValidationFlags.AllowWildcard);
}
private _applyFiles(files: FileSet, dirname: string) {
const deferred: [Symlink | Link | Mount, string][] = [];
this._applyFilesWorker(files, dirname, deferred);
for (const [entry, path] of deferred) {
this.mkdirpSync(vpath.dirname(path));
this.pushd(vpath.dirname(path));
if (entry instanceof Symlink) {
if (this.stringComparer(vpath.dirname(path), path) === 0) {
throw new TypeError("Roots cannot be symbolic links.");
}
this.symlinkSync(vpath.resolve(dirname, entry.symlink), path);
this._applyFileExtendedOptions(path, entry);
}
else if (entry instanceof Link) {
if (this.stringComparer(vpath.dirname(path), path) === 0) {
throw new TypeError("Roots cannot be hard links.");
}
this.linkSync(entry.path, path);
}
else {
this.mountSync(entry.source, path, entry.resolver);
this._applyFileExtendedOptions(path, entry);
}
this.popd();
}
}
private _applyFileExtendedOptions(path: string, entry: Directory | File | Symlink | Mount) {
const { meta } = entry;
if (meta !== undefined) {
const filemeta = this.filemeta(path);
for (const key of Object.keys(meta)) {
filemeta.set(key, meta[key]);
}
}
}
private _applyFilesWorker(files: FileSet, dirname: string, deferred: [Symlink | Link | Mount, string][]) {
for (const key of Object.keys(files)) {
const value = normalizeFileSetEntry(files[key]);
const path = dirname ? vpath.resolve(dirname, key) : key;
vpath.validate(path, vpath.ValidationFlags.Absolute);
// eslint-disable-next-line no-null/no-null
if (value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink) {
if (this.stringComparer(vpath.dirname(path), path) === 0) {
throw new TypeError("Roots cannot be deleted.");
}
this.rimrafSync(path);
}
else if (value instanceof File) {
if (this.stringComparer(vpath.dirname(path), path) === 0) {
throw new TypeError("Roots cannot be files.");
}
this.mkdirpSync(vpath.dirname(path));
this.writeFileSync(path, value.data, value.encoding);
this._applyFileExtendedOptions(path, value);
}
else if (value instanceof Directory) {
this.mkdirpSync(path);
this._applyFileExtendedOptions(path, value);
this._applyFilesWorker(value.files, path, deferred);
}
else {
deferred.push([value, path]);
}
}
}
}
export interface FileSystemOptions {
// Sets the initial timestamp for new files and directories, or the function used
// to calculate timestamps.
time?: number | Date | (() => number | Date);
// A set of file system entries to initially add to the file system.
files?: FileSet;
// Sets the initial working directory for the file system.
cwd?: string;
// Sets initial metadata attached to the file system.
meta?: Record<string, any>;
}
export interface FileSystemCreateOptions extends FileSystemOptions {
// Sets the documents to add to the file system.
documents?: readonly documents.TextDocument[];
}
export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants";
export interface Traversal {
/** A function called to choose whether to continue to traverse to either ancestors or descendants. */
traverse?(path: string, stats: Stats): boolean;
/** A function called to choose whether to accept a path as part of the result. */
accept?(path: string, stats: Stats): boolean;
}
export interface FileSystemResolver {
statSync(path: string): { mode: number; size: number; };
readdirSync(path: string): string[];
readFileSync(path: string): Buffer;
}
export interface FileSystemResolverHost {
useCaseSensitiveFileNames(): boolean;
getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries;
directoryExists(path: string): boolean;
fileExists(path: string): boolean;
getFileSize(path: string): number;
readFile(path: string): string | undefined;
getWorkspaceRoot(): string;
}
export function createResolver(host: FileSystemResolverHost): FileSystemResolver {
return {
readdirSync(path: string): string[] {
const { files, directories } = host.getAccessibleFileSystemEntries(path);
return directories.concat(files);
},
statSync(path: string): { mode: number; size: number; } {
if (host.directoryExists(path)) {
return { mode: S_IFDIR | 0o777, size: 0 };
}
else if (host.fileExists(path)) {
return { mode: S_IFREG | 0o666, size: host.getFileSize(path) };
}
else {
throw new Error("ENOENT: path does not exist");
}
},
readFileSync(path: string): Buffer {
return ts.sys.bufferFrom!(host.readFile(path)!, "utf8") as Buffer; // TODO: GH#18217
}
};
}
/**
* 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, files, cwd, time, meta }: FileSystemCreateOptions = {}) {
const fs = getBuiltLocal(host, ignoreCase).shadow();
if (meta) {
for (const key of Object.keys(meta)) {
fs.meta.set(key, meta[key]);
}
}
if (time) {
fs.time(time);
}
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(vpath.resolve(fs.cwd(), document.file), link);
}
}
}
}
if (files) {
fs.apply(files);
}
return fs;
}
export class Stats {
public dev: number;
public ino: number;
public mode: number;
public nlink: number;
public uid: number;
public gid: number;
public rdev: number;
public size: number;
public blksize: number;
public blocks: number;
public atimeMs: number;
public mtimeMs: number;
public ctimeMs: number;
public birthtimeMs: number;
public atime: Date;
public mtime: Date;
public ctime: Date;
public birthtime: Date;
constructor();
constructor(dev: number, ino: number, mode: number, nlink: number, rdev: number, size: number, blksize: number, blocks: number, atimeMs: number, mtimeMs: number, ctimeMs: number, birthtimeMs: number);
constructor(dev = 0, ino = 0, mode = 0, nlink = 0, rdev = 0, size = 0, blksize = 0, blocks = 0, atimeMs = 0, mtimeMs = 0, ctimeMs = 0, birthtimeMs = 0) {
this.dev = dev;
this.ino = ino;
this.mode = mode;
this.nlink = nlink;
this.uid = 0;
this.gid = 0;
this.rdev = rdev;
this.size = size;
this.blksize = blksize;
this.blocks = blocks;
this.atimeMs = atimeMs;
this.mtimeMs = mtimeMs;
this.ctimeMs = ctimeMs;
this.birthtimeMs = birthtimeMs;
this.atime = new Date(this.atimeMs);
this.mtime = new Date(this.mtimeMs);
this.ctime = new Date(this.ctimeMs);
this.birthtime = new Date(this.birthtimeMs);
}
public isFile() { return (this.mode & S_IFMT) === S_IFREG; }
public isDirectory() { return (this.mode & S_IFMT) === S_IFDIR; }
public isSymbolicLink() { return (this.mode & S_IFMT) === S_IFLNK; }
public isBlockDevice() { return (this.mode & S_IFMT) === S_IFBLK; }
public isCharacterDevice() { return (this.mode & S_IFMT) === S_IFCHR; }
public isFIFO() { return (this.mode & S_IFMT) === S_IFIFO; }
public isSocket() { return (this.mode & S_IFMT) === S_IFSOCK; }
}
export const IOErrorMessages = Object.freeze({
EACCES: "access denied",
EIO: "an I/O error occurred",
ENOENT: "no such file or directory",
EEXIST: "file already exists",
ELOOP: "too many symbolic links encountered",
ENOTDIR: "no such directory",
EISDIR: "path is a directory",
EBADF: "invalid file descriptor",
EINVAL: "invalid value",
ENOTEMPTY: "directory not empty",
EPERM: "operation not permitted",
EROFS: "file system is read-only"
});
export function createIOError(code: keyof typeof IOErrorMessages, details = "") {
const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`);
err.code = code;
if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError);
return err;
}
/**
* A template used to populate files, directories, links, etc. in a virtual file system.
*/
export interface FileSet {
[name: string]: DirectoryLike | FileLike | Link | Symlink | Mount | Rmdir | Unlink | null | undefined;
}
export type DirectoryLike = FileSet | Directory;
export type FileLike = File | Buffer | string;
/** Extended options for a directory in a `FileSet` */
export class Directory {
public readonly files: FileSet;
public readonly meta: Record<string, any> | undefined;
constructor(files: FileSet, { meta }: { meta?: Record<string, any> } = {}) {
this.files = files;
this.meta = meta;
}
}
/** Extended options for a file in a `FileSet` */
export class File {
public readonly data: Buffer | string;
public readonly encoding: string | undefined;
public readonly meta: Record<string, any> | undefined;
constructor(data: Buffer | string, { meta, encoding }: { encoding?: string, meta?: Record<string, any> } = {}) {
this.data = data;
this.encoding = encoding;
this.meta = meta;
}
}
export class SameFileContentFile extends File {
constructor(data: Buffer | string, metaAndEncoding?: { encoding?: string, meta?: Record<string, any> }) {
super(data, metaAndEncoding);
}
}
/** Extended options for a hard link in a `FileSet` */
export class Link {
public readonly path: string;
constructor(path: string) {
this.path = path;
}
}
/** Removes a directory in a `FileSet` */
export class Rmdir {
public _rmdirBrand?: never; // brand necessary for proper type guards
}
/** Unlinks a file in a `FileSet` */
export class Unlink {
public _unlinkBrand?: never; // brand necessary for proper type guards
}
/** Extended options for a symbolic link in a `FileSet` */
export class Symlink {
public readonly symlink: string;
public readonly meta: Record<string, any> | undefined;
constructor(symlink: string, { meta }: { meta?: Record<string, any> } = {}) {
this.symlink = symlink;
this.meta = meta;
}
}
/** Extended options for mounting a virtual copy of an external file system via a `FileSet` */
export class Mount {
public readonly source: string;
public readonly resolver: FileSystemResolver;
public readonly meta: Record<string, any> | undefined;
constructor(source: string, resolver: FileSystemResolver, { meta }: { meta?: Record<string, any> } = {}) {
this.source = source;
this.resolver = resolver;
this.meta = meta;
}
}
// a generic POSIX inode
type Inode = FileInode | DirectoryInode | SymlinkInode;
interface FileInode {
dev: number; // device id
ino: number; // inode id
mode: number; // file mode
atimeMs: number; // access time
mtimeMs: number; // modified time
ctimeMs: number; // status change time
birthtimeMs: number; // creation time
nlink: number; // number of hard links
size?: number;
buffer?: Buffer;
source?: string;
resolver?: FileSystemResolver;
shadowRoot?: FileInode;
meta?: collections.Metadata;
}
interface DirectoryInode {
dev: number; // device id
ino: number; // inode id
mode: number; // file mode
atimeMs: number; // access time
mtimeMs: number; // modified time
ctimeMs: number; // status change time
birthtimeMs: number; // creation time
nlink: number; // number of hard links
links?: collections.SortedMap<string, Inode>;
source?: string;
resolver?: FileSystemResolver;
shadowRoot?: DirectoryInode;
meta?: collections.Metadata;
}
interface SymlinkInode {
dev: number; // device id
ino: number; // inode id
mode: number; // file mode
atimeMs: number; // access time
mtimeMs: number; // modified time
ctimeMs: number; // status change time
birthtimeMs: number; // creation time
nlink: number; // number of hard links
symlink: string;
shadowRoot?: SymlinkInode;
meta?: collections.Metadata;
}
function isEmptyNonShadowedDirectory(node: DirectoryInode) {
return !node.links && !node.shadowRoot && !node.resolver && !node.source;
}
function isEmptyNonShadowedFile(node: FileInode) {
return !node.buffer && !node.shadowRoot && !node.resolver && !node.source;
}
function isFile(node: Inode | undefined): node is FileInode {
return node !== undefined && (node.mode & S_IFMT) === S_IFREG;
}
function isDirectory(node: Inode | undefined): node is DirectoryInode {
return node !== undefined && (node.mode & S_IFMT) === S_IFDIR;
}
function isSymlink(node: Inode | undefined): node is SymlinkInode {
return node !== undefined && (node.mode & S_IFMT) === S_IFLNK;
}
interface WalkResult {
realpath: string;
basename: string;
parent: DirectoryInode | undefined;
links: collections.SortedMap<string, Inode>;
node: Inode | undefined;
}
let builtLocalHost: FileSystemResolverHost | undefined;
let builtLocalCI: FileSystem | undefined;
let builtLocalCS: FileSystem | undefined;
function getBuiltLocal(host: FileSystemResolverHost, ignoreCase: boolean): FileSystem {
if (builtLocalHost !== host) {
builtLocalCI = undefined;
builtLocalCS = undefined;
builtLocalHost = host;
}
if (!builtLocalCI) {
const resolver = createResolver(host);
builtLocalCI = new FileSystem(/*ignoreCase*/ true, {
files: {
[builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver),
[testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver),
[projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver),
[srcFolder]: {}
},
cwd: srcFolder,
meta: { defaultLibLocation: builtFolder }
});
builtLocalCI.makeReadonly();
}
if (ignoreCase) return builtLocalCI;
if (!builtLocalCS) {
builtLocalCS = builtLocalCI.shadow(/*ignoreCase*/ false);
builtLocalCS.makeReadonly();
}
return builtLocalCS;
}
/* eslint-disable no-null/no-null */
function normalizeFileSetEntry(value: FileSet[string]) {
if (value === undefined ||
value === null ||
value instanceof Directory ||
value instanceof File ||
value instanceof Link ||
value instanceof Symlink ||
value instanceof Mount ||
value instanceof Rmdir ||
value instanceof Unlink) {
return value;
}
return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(value);
}
export function formatPatch(patch: FileSet): string;
export function formatPatch(patch: FileSet | undefined): string | null;
export function formatPatch(patch: FileSet | undefined) {
return patch ? formatPatchWorker("", patch) : null;
}
/* eslint-enable no-null/no-null */
function formatPatchWorker(dirname: string, container: FileSet): string {
let text = "";
for (const name of Object.keys(container)) {
const entry = normalizeFileSetEntry(container[name]);
const file = dirname ? vpath.combine(dirname, name) : name;
// eslint-disable-next-line no-null/no-null
if (entry === null || entry === undefined || entry instanceof Unlink || entry instanceof Rmdir) {
text += `//// [${file}] unlink\r\n`;
}
else if (entry instanceof Rmdir) {
text += `//// [${vpath.addTrailingSeparator(file)}] rmdir\r\n`;
}
else if (entry instanceof Directory) {
text += formatPatchWorker(file, entry.files);
}
else if (entry instanceof SameFileContentFile) {
text += `//// [${file}] file written with same contents\r\n`;
}
else if (entry instanceof File) {
const content = typeof entry.data === "string" ? entry.data : entry.data.toString("utf8");
text += `//// [${file}]\r\n${content}\r\n\r\n`;
}
else if (entry instanceof Link) {
text += `//// [${file}] link(${entry.path})\r\n`;
}
else if (entry instanceof Symlink) {
text += `//// [${file}] symlink(${entry.symlink})\r\n`;
}
else if (entry instanceof Mount) {
text += `//// [${file}] mount(${entry.source})\r\n`;
}
}
return text;
}
export function iteratePatch(patch: FileSet | undefined): IterableIterator<[string, string]> | null {
// eslint-disable-next-line no-null/no-null
return patch ? Harness.Compiler.iterateOutputs(iteratePatchWorker("", patch)) : null;
}
function* iteratePatchWorker(dirname: string, container: FileSet): IterableIterator<documents.TextDocument> {
for (const name of Object.keys(container)) {
const entry = normalizeFileSetEntry(container[name]);
const file = dirname ? vpath.combine(dirname, name) : name;
if (entry instanceof Directory) {
yield* ts.arrayFrom(iteratePatchWorker(file, entry.files));
}
else if (entry instanceof File) {
const content = typeof entry.data === "string" ? entry.data : entry.data.toString("utf8");
yield new documents.TextDocument(file, content);
}
}
}
}