Skip to content

Factor out path-related classes for better testing. #9352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jan 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/client/common/platform/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

// TO DO: Deprecate in favor of IPlatformService
// tslint:disable-next-line:no-suspicious-comment
// TODO (GH-8542): Drop all these in favor of IPlatformService

export const WINDOWS_PATH_VARIABLE_NAME = 'Path';
export const NON_WINDOWS_PATH_VARIABLE_NAME = 'PATH';
export const IS_WINDOWS = /^win/.test(process.platform);
42 changes: 24 additions & 18 deletions src/client/common/platform/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import * as fileSystem from 'fs';
import * as fs from 'fs-extra';
import * as glob from 'glob';
import { inject, injectable } from 'inversify';
import * as path from 'path';
import * as tmp from 'tmp';
import { promisify } from 'util';
import { createDeferred } from '../utils/async';
import { noop } from '../utils/misc';
import { FileStat, FileType, IFileSystem, IPlatformService, TemporaryFile } from './types';
import { FileSystemPaths, FileSystemPathUtils } from './fs-paths';
// prettier-ignore
import {
FileStat, FileType,
IFileSystem, IFileSystemPaths, IPlatformService,
TemporaryFile
} from './types';

const globAsync = promisify(glob);

Expand Down Expand Up @@ -78,31 +83,32 @@ export function convertStat(old: fs.Stats, filetype: FileType): FileStat {

@injectable()
export class FileSystem implements IFileSystem {
constructor(@inject(IPlatformService) private platformService: IPlatformService) {}
private readonly paths: IFileSystemPaths;
private readonly pathUtils: FileSystemPathUtils;
// prettier-ignore
constructor(
@inject(IPlatformService) platformService: IPlatformService
) {
// prettier-ignore
this.paths = FileSystemPaths.withDefaults(
platformService.isWindows
);
this.pathUtils = FileSystemPathUtils.withDefaults(this.paths);
}

//=================================
// path-related

public get directorySeparatorChar(): string {
return path.sep;
return this.paths.sep;
}

public arePathsSame(path1: string, path2: string): boolean {
path1 = path.normalize(path1);
path2 = path.normalize(path2);
if (this.platformService.isWindows) {
return path1.toUpperCase() === path2.toUpperCase();
} else {
return path1 === path2;
}
return this.pathUtils.arePathsSame(path1, path2);
}

public getRealPath(filePath: string): Promise<string> {
return new Promise<string>(resolve => {
fs.realpath(filePath, (err, realPath) => {
resolve(err ? filePath : realPath);
});
});
return this.pathUtils.getRealPath(filePath);
}

//=================================
Expand Down Expand Up @@ -154,7 +160,7 @@ export class FileSystem implements IFileSystem {
public async listdir(dirname: string): Promise<[string, FileType][]> {
const files = await fs.readdir(dirname);
const promises = files.map(async basename => {
const filename = path.join(dirname, basename);
const filename = this.paths.join(dirname, basename);
const fileType = await getFileType(filename);
return [filename, fileType] as [string, FileType];
});
Expand Down Expand Up @@ -312,7 +318,7 @@ export class FileSystem implements IFileSystem {
}

public async isDirReadonly(dirname: string): Promise<boolean> {
const filePath = `${dirname}${path.sep}___vscpTest___`;
const filePath = `${dirname}${this.paths.sep}___vscpTest___`;
return new Promise<boolean>(resolve => {
fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => {
if (!error) {
Expand Down
168 changes: 168 additions & 0 deletions src/client/common/platform/fs-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as fs from 'fs-extra';
import * as nodepath from 'path';
import { getOSType, OSType } from '../utils/platform';
// prettier-ignore
import {
IExecutables,
IFileSystemPaths
} from './types';
// tslint:disable-next-line:no-var-requires no-require-imports
const untildify = require('untildify');

// The parts of node's 'path' module used by FileSystemPaths.
interface INodePath {
sep: string;
join(...filenames: string[]): string;
dirname(filename: string): string;
basename(filename: string, ext?: string): string;
normalize(filename: string): string;
}

// The file path operations used by the extension.
export class FileSystemPaths {
// prettier-ignore
constructor(
private readonly isCaseInsensitive: boolean,
private readonly raw: INodePath
) { }
// Create a new object using common-case default values.
// We do not use an alternate constructor because defaults in the
// constructor runs counter to our typical approach.
// prettier-ignore
public static withDefaults(
isCaseInsensitive?: boolean
): FileSystemPaths {
if (isCaseInsensitive === undefined) {
isCaseInsensitive = getOSType() === OSType.Windows;
}
// prettier-ignore
return new FileSystemPaths(
isCaseInsensitive,
nodepath
);
}

public get sep(): string {
return this.raw.sep;
}

public join(...filenames: string[]): string {
return this.raw.join(...filenames);
}

public dirname(filename: string): string {
return this.raw.dirname(filename);
}

public basename(filename: string, suffix?: string): string {
return this.raw.basename(filename, suffix);
}

public normalize(filename: string): string {
return this.raw.normalize(filename);
}

public normCase(filename: string): string {
filename = this.raw.normalize(filename);
// prettier-ignore
return this.isCaseInsensitive
? filename.toUpperCase()
: filename;
}
}

// Where to fine executables.
//
// In particular this class provides all the tools needed to find
// executables, including through an environment variable.
export class Executables {
// prettier-ignore
constructor(
public readonly delimiter: string,
private readonly osType: OSType
) { }
// Create a new object using common-case default values.
// We do not use an alternate constructor because defaults in the
// constructor runs counter to our typical approach.
public static withDefaults(): Executables {
// prettier-ignore
return new Executables(
nodepath.delimiter,
getOSType()
);
}

public get envVar(): string {
// prettier-ignore
return this.osType === OSType.Windows
? 'Path'
: 'PATH';
}
}

// The dependencies FileSystemPathUtils has on node's path module.
interface IRawPaths {
relative(relpath: string, rootpath: string): string;
}

// A collection of high-level utilities related to filesystem paths.
export class FileSystemPathUtils {
// prettier-ignore
constructor(
public readonly home: string,
public readonly paths: IFileSystemPaths,
public readonly executables: IExecutables,
private readonly raw: IRawPaths
) { }
// Create a new object using common-case default values.
// We do not use an alternate constructor because defaults in the
// constructor runs counter to our typical approach.
// prettier-ignore
public static withDefaults(
paths?: IFileSystemPaths
): FileSystemPathUtils {
if (paths === undefined) {
paths = FileSystemPaths.withDefaults();
}
// prettier-ignore
return new FileSystemPathUtils(
untildify('~'),
paths,
Executables.withDefaults(),
nodepath
);
}

// Return true if the two paths are equivalent on the current
// filesystem and false otherwise. On Windows this is significant.
// On non-Windows the filenames must always be exactly the same.
public arePathsSame(path1: string, path2: string): boolean {
path1 = this.paths.normCase(path1);
path2 = this.paths.normCase(path2);
return path1 === path2;
}

// Return the canonicalized absolute filename.
public async getRealPath(filename: string): Promise<string> {
try {
return await fs.realpath(filename);
} catch {
// We ignore the error.
return filename;
}
}

// Return the clean (displayable) form of the given filename.
public getDisplayName(filename: string, cwd?: string): string {
if (cwd && filename.startsWith(cwd)) {
return `.${this.paths.sep}${this.raw.relative(cwd, filename)}`;
} else if (filename.startsWith(this.home)) {
return `~${this.paths.sep}${this.raw.relative(this.home, filename)}`;
} else {
return filename;
}
}
}
66 changes: 47 additions & 19 deletions src/client/common/platform/pathUtils.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,64 @@
// tslint:disable-next-line:no-suspicious-comment
// TODO(GH-8542): Drop this file.

import { inject, injectable } from 'inversify';
import * as path from 'path';
import { IPathUtils, IsWindows } from '../types';
import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants';
import { OSType } from '../utils/platform';
// prettier-ignore
import {
Executables,
FileSystemPaths,
FileSystemPathUtils
} from './fs-paths';
// tslint:disable-next-line:no-var-requires no-require-imports
const untildify = require('untildify');

@injectable()
export class PathUtils implements IPathUtils {
public readonly home = '';
constructor(@inject(IsWindows) private isWindows: boolean) {
this.home = untildify('~');
private readonly utils: FileSystemPathUtils;
// prettier-ignore
constructor(
@inject(IsWindows) isWindows: boolean
) {
// We cannot just use FileSystemPathUtils.withDefaults() because
// of the isWindows arg.
// prettier-ignore
this.utils = new FileSystemPathUtils(
untildify('~'),
FileSystemPaths.withDefaults(),
new Executables(
path.delimiter,
isWindows ? OSType.Windows : OSType.Unknown
),
path
);
}

public get home(): string {
return this.utils.home;
}

public get delimiter(): string {
return path.delimiter;
return this.utils.executables.delimiter;
}

public get separator(): string {
return path.sep;
}
// TO DO: Deprecate in favor of IPlatformService
public getPathVariableName() {
return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME;
return this.utils.paths.sep;
}
public basename(pathValue: string, ext?: string): string {
return path.basename(pathValue, ext);

// tslint:disable-next-line:no-suspicious-comment
// TODO: Deprecate in favor of IPlatformService?
public getPathVariableName(): 'Path' | 'PATH' {
// tslint:disable-next-line:no-any
return this.utils.executables.envVar as any;
}

public getDisplayName(pathValue: string, cwd?: string): string {
if (cwd && pathValue.startsWith(cwd)) {
return `.${path.sep}${path.relative(cwd, pathValue)}`;
} else if (pathValue.startsWith(this.home)) {
return `~${path.sep}${path.relative(this.home, pathValue)}`;
} else {
return pathValue;
}
return this.utils.getDisplayName(pathValue, cwd);
}

public basename(pathValue: string, ext?: string): string {
return this.utils.paths.basename(pathValue, ext);
}
}
Loading