diff --git a/src/client/common/platform/errors.ts b/src/client/common/platform/errors.ts new file mode 100644 index 000000000000..777d02b7430b --- /dev/null +++ b/src/client/common/platform/errors.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; + +/* +See: + + https://nodejs.org/api/errors.html + + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + + node_modules/@types/node/globals.d.ts + */ + +interface IError { + name: string; + message: string; + + toString(): string; +} + +interface INodeJSError extends IError { + code: string; + stack?: string; + stackTraceLimit: number; + + captureStackTrace(): void; +} + +//================================ +// "system" errors + +namespace vscErrors { + const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name; + const FILE_EXISTS = vscode.FileSystemError.FileExists().name; + const IS_DIR = vscode.FileSystemError.FileIsADirectory().name; + const NOT_DIR = vscode.FileSystemError.FileNotADirectory().name; + const NO_PERM = vscode.FileSystemError.NoPermissions().name; + // prettier-ignore + const known = [ + FILE_NOT_FOUND, + FILE_EXISTS, + IS_DIR, + NOT_DIR, + NO_PERM + ]; + function errorMatches(err: Error, expectedName: string): boolean | undefined { + if (!known.includes(err.name)) { + return undefined; + } + return err.name === expectedName; + } + + export function isFileNotFound(err: Error): boolean | undefined { + return errorMatches(err, FILE_NOT_FOUND); + } + export function isFileExists(err: Error): boolean | undefined { + return errorMatches(err, FILE_EXISTS); + } + export function isFileIsDir(err: Error): boolean | undefined { + return errorMatches(err, IS_DIR); + } + export function isNotDir(err: Error): boolean | undefined { + return errorMatches(err, NOT_DIR); + } + export function isNoPermissions(err: Error): boolean | undefined { + return errorMatches(err, NO_PERM); + } +} + +interface ISystemError extends INodeJSError { + errno: number; + syscall: string; + info?: string; + path?: string; + address?: string; + dest?: string; + port?: string; +} + +function isSystemError(err: Error, expectedCode: string): boolean | undefined { + const code = (err as ISystemError).code; + if (!code) { + return undefined; + } + return code === expectedCode; +} + +// Return true if the given error is ENOENT. +export function isFileNotFoundError(err: Error): boolean | undefined { + const matched = vscErrors.isFileNotFound(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'ENOENT'); +} + +// Return true if the given error is EEXIST. +export function isFileExistsError(err: Error): boolean | undefined { + const matched = vscErrors.isFileExists(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EEXIST'); +} + +// Return true if the given error is EISDIR. +export function isFileIsDirError(err: Error): boolean | undefined { + const matched = vscErrors.isFileIsDir(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EISDIR'); +} + +// Return true if the given error is ENOTDIR. +export function isNotDirError(err: Error): boolean | undefined { + const matched = vscErrors.isNotDir(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'ENOTDIR'); +} + +// Return true if the given error is EACCES. +export function isNoPermissionsError(err: Error): boolean | undefined { + const matched = vscErrors.isNoPermissions(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EACCES'); +} diff --git a/src/test/common/platform/errors.unit.test.ts b/src/test/common/platform/errors.unit.test.ts new file mode 100644 index 000000000000..dc026739a494 --- /dev/null +++ b/src/test/common/platform/errors.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as vscode from 'vscode'; +// prettier-ignore +import { + isFileExistsError, isFileIsDirError, isFileNotFoundError, + isNoPermissionsError, isNotDirError +} from '../../../client/common/platform/errors'; +import { SystemError } from './utils'; + +suite('FileSystem - errors', () => { + const filename = 'spam'; + + suite('isFileNotFoundError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileNotFound(filename), true], + [vscode.FileSystemError.FileExists(filename), false], + [new SystemError('ENOENT', 'stat', ''), true], + [new SystemError('EEXIST', '???', ''), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileNotFoundError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isFileExistsError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileExists(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EEXIST', '???', ''), true], + [new SystemError('ENOENT', 'stat', ''), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileExistsError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isFileIsDirError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileIsADirectory(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EISDIR', '???', ''), true], + [new SystemError('ENOENT', 'stat', ''), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileIsDirError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isNotDirError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileNotADirectory(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('ENOTDIR', '???', ''), true], + [new SystemError('ENOENT', 'stat', ''), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isNotDirError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isNoPermissionsError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.NoPermissions(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EACCES', '???', ''), true], + [new SystemError('ENOENT', 'stat', ''), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isNoPermissionsError(err); + + expect(matches).to.equal(expected); + }); + }); + }); +}); diff --git a/src/test/common/platform/utils.ts b/src/test/common/platform/utils.ts index 990fbe1e8598..ea614f9e2f53 100644 --- a/src/test/common/platform/utils.ts +++ b/src/test/common/platform/utils.ts @@ -52,6 +52,23 @@ export function fixPath(filename: string): string { return path.normalize(filename); } +export class SystemError extends Error { + public code: string; + public errno: number; + public syscall: string; + public info?: string; + public path?: string; + public address?: string; + public dest?: string; + public port?: string; + constructor(code: string, syscall: string, message: string) { + super(`${code}: ${message} ${syscall} '...'`); + this.code = code; + this.errno = 0; // Don't bother until we actually need it. + this.syscall = syscall; + } +} + export class CleanupFixture { private cleanups: (() => void | Promise)[]; constructor() { diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index ca059c27d42c..f3105a9bd1fd 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -83,6 +83,7 @@ mockedVSCode.DebugAdapterExecutable = vscodeMocks.vscMock.DebugAdapterExecutable mockedVSCode.DebugAdapterServer = vscodeMocks.vscMock.DebugAdapterServer; mockedVSCode.QuickInputButtons = vscodeMocks.vscMockExtHostedTypes.QuickInputButtons; mockedVSCode.FileType = vscodeMocks.vscMock.FileType; +mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError; // This API is used in src/client/telemetry/telemetry.ts const extensions = TypeMoq.Mock.ofType();