From b9bc57306a1730393f7ff5fc29f22b0ecaf322ae Mon Sep 17 00:00:00 2001 From: Heejae Chang Date: Thu, 6 Feb 2020 12:33:09 -0800 Subject: [PATCH 1/4] fixed bunch of tslint issue and refactoring on how command handling works (#502) * fixes many es/tslint style warnings * Remove map inside map --- client/src/extension.ts | 6 +- server/src/analyzer/binder.ts | 7 +- server/src/analyzer/service.ts | 4 +- server/src/analyzer/typeEvaluator.ts | 6 +- server/src/commands/commandController.ts | 35 + .../src/{definitions => commands}/commands.ts | 2 +- server/src/commands/createTypeStub.ts | 76 +++ server/src/commands/quickActionCommand.ts | 37 ++ server/src/common/collectionUtils.ts | 42 +- server/src/common/debug.ts | 45 +- server/src/common/diagnostic.ts | 7 +- server/src/common/vfs.ts | 34 +- server/src/languageServerBase.ts | 306 +++------ server/src/languageService/quickActions.ts | 6 +- server/src/pyright.ts | 2 +- server/src/tests/collectionUtils.test.ts | 44 +- server/src/tests/debug.test.ts | 30 +- server/src/tests/filesystem.test.ts | 119 ++-- server/src/tests/fourSlashParser.test.ts | 128 ++-- server/src/tests/fourSlashRunner.test.ts | 24 +- .../harness/fourslash/fourSlashParser.ts | 134 ++-- .../tests/harness/fourslash/fourSlashTypes.ts | 20 +- server/src/tests/harness/fourslash/runner.ts | 27 +- .../src/tests/harness/fourslash/testState.ts | 231 ++++--- server/src/tests/harness/host.ts | 57 +- server/src/tests/harness/utils.ts | 168 +++-- server/src/tests/harness/vfs/factory.ts | 48 +- server/src/tests/harness/vfs/filesystem.ts | 596 +++++++++--------- .../src/tests/harness/vfs/pathValidation.ts | 47 +- server/src/tests/pathUtils.test.ts | 100 +-- server/src/tests/testState.test.ts | 187 +++--- server/src/workspaceMap.ts | 65 ++ server/tsconfig.json | 3 +- 33 files changed, 1351 insertions(+), 1292 deletions(-) create mode 100644 server/src/commands/commandController.ts rename server/src/{definitions => commands}/commands.ts (92%) create mode 100644 server/src/commands/createTypeStub.ts create mode 100644 server/src/commands/quickActionCommand.ts create mode 100644 server/src/workspaceMap.ts diff --git a/client/src/extension.ts b/client/src/extension.ts index 23b0f441ac8b..b52723b3c8a5 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -16,7 +16,7 @@ import { ExtensionContext, commands, TextEditor, Range, Position, TextEditorEdit import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, TextEdit } from 'vscode-languageclient'; import { ProgressReporting } from './progress'; -import { CommandId } from '../../server/src/definitions/commands'; +import { Commands } from '../../server/src/commands/commands'; export function activate(context: ExtensionContext) { const bundlePath = context.asAbsolutePath(path.join('server', 'server.bundle.js')); @@ -59,7 +59,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(progressReporting); // Register our custom commands. - const textEditorCommands = [CommandId.orderImports, CommandId.addMissingOptionalToParam]; + const textEditorCommands = [Commands.orderImports, Commands.addMissingOptionalToParam]; textEditorCommands.forEach(commandName => { context.subscriptions.push(commands.registerTextEditorCommand(commandName, (editor: TextEditor, edit: TextEditorEdit, ...args: any[]) => { @@ -86,7 +86,7 @@ export function activate(context: ExtensionContext) { })); }); - const genericCommands = [ CommandId.createTypeStub, ]; + const genericCommands = [ Commands.createTypeStub, ]; genericCommands.forEach(command => { context.subscriptions.push(commands.registerCommand(command, (...args: any[]) => { languageClient.sendRequest('workspace/executeCommand', { command, arguments: args }); diff --git a/server/src/analyzer/binder.ts b/server/src/analyzer/binder.ts index 44fb096711ef..fffc88d7c636 100644 --- a/server/src/analyzer/binder.ts +++ b/server/src/analyzer/binder.ts @@ -18,6 +18,7 @@ import * as assert from 'assert'; +import { Commands } from '../commands/commands'; import { DiagnosticLevel } from '../common/configOptions'; import { CreateTypeStubFileAction } from '../common/diagnostic'; import { getEmptyRange } from '../common/textRange'; @@ -49,7 +50,6 @@ import { Scope, ScopeType } from './scope'; import * as StaticExpressions from './staticExpressions'; import { indeterminateSymbolId, Symbol, SymbolFlags } from './symbol'; import { isConstantName, isPrivateOrProtectedName } from './symbolNameUtils'; -import { CommandId } from '../definitions/commands'; export const enum NameBindingType { // With "nonlocal" keyword @@ -210,7 +210,7 @@ export class Binder extends ParseTreeWalker { if (diagnostic) { // Add a diagnostic action for resolving this diagnostic. const createTypeStubAction: CreateTypeStubFileAction = { - action: CommandId.createTypeStub, + action: Commands.createTypeStub, moduleName: importResult.importName }; diagnostic.addAction(createTypeStubAction); @@ -2121,7 +2121,7 @@ export class Binder extends ParseTreeWalker { let finalTypeNode: ExpressionNode | undefined; if (typeAnnotation) { - if (typeAnnotation.nodeType == ParseNodeType.Name) { + if (typeAnnotation.nodeType === ParseNodeType.Name) { // We need to make an assumption in this code that the symbol "Final" // will resolve to typing.Final. This is because of the poor way // the "Final" support was specified. We need to evaluate it @@ -2404,4 +2404,3 @@ export class YieldFinder extends ParseTreeWalker { return false; } } - diff --git a/server/src/analyzer/service.ts b/server/src/analyzer/service.ts index 8e7290225d7d..49166682f8e5 100644 --- a/server/src/analyzer/service.ts +++ b/server/src/analyzer/service.ts @@ -21,14 +21,14 @@ import { getFileName, getFileSpec, getFileSystemEntries, isDirectory, normalizePath, stripFileExtension } from '../common/pathUtils'; +import { DocumentRange, Position, Range } from '../common/textRange'; import { Duration, timingStats } from '../common/timing'; +import { FileWatcher, VirtualFileSystem } from '../common/vfs'; import { HoverResults } from '../languageService/hoverProvider'; import { SignatureHelpResults } from '../languageService/signatureHelpProvider'; import { ImportedModuleDescriptor, ImportResolver } from './importResolver'; import { MaxAnalysisTime, Program } from './program'; import * as PythonPathUtils from './pythonPathUtils'; -import { Position, Range, DocumentRange } from '../common/textRange'; -import { VirtualFileSystem, FileWatcher } from '../common/vfs'; export { MaxAnalysisTime } from './program'; diff --git a/server/src/analyzer/typeEvaluator.ts b/server/src/analyzer/typeEvaluator.ts index b130dfabab89..c2d56b29dfaa 100644 --- a/server/src/analyzer/typeEvaluator.ts +++ b/server/src/analyzer/typeEvaluator.ts @@ -16,6 +16,7 @@ import * as assert from 'assert'; +import { Commands } from '../commands/commands'; import { DiagnosticLevel } from '../common/configOptions'; import { AddMissingOptionalToParamAction, Diagnostic, DiagnosticAddendum } from '../common/diagnostic'; import { getEmptyRange } from '../common/textRange'; @@ -30,6 +31,7 @@ import { ArgumentCategory, AssignmentNode, AugmentedAssignmentNode, BinaryOperat NameNode, ParameterCategory, ParameterNode, ParseNode, ParseNodeType, SetNode, SliceNode, StringListNode, TernaryNode, TupleNode, UnaryOperationNode, WithItemNode, YieldFromNode, YieldNode } from '../parser/parseNodes'; +import { ParseOptions, Parser } from '../parser/parser'; import { KeywordType, OperatorType, StringTokenFlags } from '../parser/tokenizerTypes'; import { AnalyzerFileInfo, ImportLookup, ImportLookupResult } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; @@ -62,8 +64,6 @@ import { addDefaultFunctionParameters, addTypeVarsToListIfUnique, specializeType, stripFirstParameter, stripLiteralTypeArgsValue, stripLiteralValue, transformTypeObjectToClass, TypedDictEntry } from './typeUtils'; import { TypeVarMap } from './typeVarMap'; -import { Parser, ParseOptions } from '../parser/parser'; -import { CommandId } from '../definitions/commands'; interface TypeResult { type: Type; @@ -6195,7 +6195,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator { if (isNoneWithoutOptional) { const addOptionalAction: AddMissingOptionalToParamAction = { - action: CommandId.addMissingOptionalToParam, + action: Commands.addMissingOptionalToParam, offsetOfTypeNode: param.typeAnnotation.start + 1 }; if (diag) { diff --git a/server/src/commands/commandController.ts b/server/src/commands/commandController.ts new file mode 100644 index 000000000000..1de147407c23 --- /dev/null +++ b/server/src/commands/commandController.ts @@ -0,0 +1,35 @@ +/* + * commandController.ts + * + * Implements language server commands execution functionality. + */ + +import { ExecuteCommandParams, ResponseError } from 'vscode-languageserver'; +import { LanguageServerBase } from '../languageServerBase'; +import { Commands } from './commands'; +import { CreateTypeStubCommand } from './createTypeStub'; +import { QuickActionCommand } from './quickActionCommand'; + +export interface ServerCommand { + execute(cmdParams: ExecuteCommandParams): Promise; +} + +export class CommandController implements ServerCommand { + private _createStub: CreateTypeStubCommand; + private _quickAction: QuickActionCommand; + + constructor(ls: LanguageServerBase) { + this._createStub = new CreateTypeStubCommand(ls); + this._quickAction = new QuickActionCommand(ls); + } + + async execute(cmdParams: ExecuteCommandParams): Promise { + if (cmdParams.command === Commands.orderImports || cmdParams.command === Commands.addMissingOptionalToParam) { + return this._quickAction.execute(cmdParams); + } + if (cmdParams.command === Commands.createTypeStub) { + return this._createStub.execute(cmdParams); + } + return new ResponseError(1, 'Unsupported command'); + } +} diff --git a/server/src/definitions/commands.ts b/server/src/commands/commands.ts similarity index 92% rename from server/src/definitions/commands.ts rename to server/src/commands/commands.ts index 87d37b729339..e5c5749273dc 100644 --- a/server/src/definitions/commands.ts +++ b/server/src/commands/commands.ts @@ -7,7 +7,7 @@ * Command identifier strings. */ -export enum CommandId { +export enum Commands { createTypeStub = 'pyright.createtypestub', orderImports = 'pyright.organizeimports', addMissingOptionalToParam = 'pyright.addoptionalforparam' diff --git a/server/src/commands/createTypeStub.ts b/server/src/commands/createTypeStub.ts new file mode 100644 index 000000000000..46ee33be52fe --- /dev/null +++ b/server/src/commands/createTypeStub.ts @@ -0,0 +1,76 @@ +/* + * createTypeStub.ts + * + * Implements 'create stub' command functionality. + */ + +import { ExecuteCommandParams } from 'vscode-languageserver'; +import { AnalyzerService } from '../analyzer/service'; +import { convertPathToUri } from '../common/pathUtils'; +import { LanguageServerBase, WorkspaceServiceInstance } from '../languageServerBase'; +import { ServerCommand } from './commandController'; + +export class CreateTypeStubCommand implements ServerCommand { + constructor(private _ls: LanguageServerBase) {} + + async execute(cmdParams: ExecuteCommandParams): Promise { + if (cmdParams.arguments && cmdParams.arguments.length >= 2) { + const workspaceRoot = cmdParams.arguments[0]; + const importName = cmdParams.arguments[1]; + const service = this._createTypeStubService(importName); + + // Allocate a temporary pseudo-workspace to perform this job. + const workspace: WorkspaceServiceInstance = { + workspaceName: `Create Type Stub ${importName}`, + rootPath: workspaceRoot, + rootUri: convertPathToUri(workspaceRoot), + serviceInstance: service, + disableLanguageServices: true + }; + + service.setCompletionCallback(results => { + if (results.filesRequiringAnalysis === 0) { + try { + service.writeTypeStub(); + service.dispose(); + const infoMessage = `Type stub was successfully created for '${importName}'.`; + this._ls.connection.window.showInformationMessage(infoMessage); + this._handlePostCreateTypeStub(); + } catch (err) { + let errMessage = ''; + if (err instanceof Error) { + errMessage = ': ' + err.message; + } + errMessage = `An error occurred when creating type stub for '${importName}'` + errMessage; + this._ls.connection.console.error(errMessage); + this._ls.connection.window.showErrorMessage(errMessage); + } + } + }); + + const serverSettings = await this._ls.getSettings(workspace); + this._ls.updateOptionsAndRestartService(workspace, serverSettings, importName); + return; + } + } + + // Creates a service instance that's used for creating type + // stubs for a specified target library. + private _createTypeStubService(importName: string): AnalyzerService { + this._ls.connection.console.log('Starting type stub service instance'); + const service = new AnalyzerService('Type stub', this._ls.fs, this._ls.connection.console); + + service.setMaxAnalysisDuration({ + openFilesTimeInMs: 500, + noOpenFilesTimeInMs: 500 + }); + + return service; + } + + private _handlePostCreateTypeStub() { + this._ls.workspaceMap.forEach(workspace => { + workspace.serviceInstance.handlePostCreateTypeStub(); + }); + } +} diff --git a/server/src/commands/quickActionCommand.ts b/server/src/commands/quickActionCommand.ts new file mode 100644 index 000000000000..cf87a9d07fc6 --- /dev/null +++ b/server/src/commands/quickActionCommand.ts @@ -0,0 +1,37 @@ +/* + * createTypeStub.ts + * + * Implements command that maps to a quick action. + */ + +import { ExecuteCommandParams, TextEdit } from 'vscode-languageserver'; +import { convertUriToPath } from '../common/pathUtils'; +import { LanguageServerBase } from '../languageServerBase'; +import { ServerCommand } from './commandController'; + +export class QuickActionCommand implements ServerCommand { + constructor(private _ls: LanguageServerBase) {} + + async execute(cmdParams: ExecuteCommandParams): Promise { + if (cmdParams.arguments && cmdParams.arguments.length >= 1) { + const docUri = cmdParams.arguments[0]; + const otherArgs = cmdParams.arguments.slice(1); + const filePath = convertUriToPath(docUri); + const workspace = this._ls.workspaceMap.getWorkspaceForFile(filePath); + const editActions = workspace.serviceInstance.performQuickAction(filePath, cmdParams.command, otherArgs); + if (!editActions) { + return []; + } + + const edits: TextEdit[] = []; + editActions.forEach(editAction => { + edits.push({ + range: editAction.range, + newText: editAction.replacementText + }); + }); + + return edits; + } + } +} diff --git a/server/src/common/collectionUtils.ts b/server/src/common/collectionUtils.ts index 9beb815f179f..2d36857517bd 100644 --- a/server/src/common/collectionUtils.ts +++ b/server/src/common/collectionUtils.ts @@ -6,7 +6,7 @@ * Various helper functions around collection/array */ -import { Comparison, equateValues, compareValues, isArray } from "./core"; +import { compareValues, Comparison, equateValues, isArray } from './core'; export const emptyArray: never[] = [] as never[]; export type EqualityComparer = (a: T, b: T) => boolean; @@ -35,14 +35,15 @@ export interface Push { * @param value The value to append to the array. If `value` is `undefined`, nothing is * appended. */ -export function append[number] | undefined>(to: TArray, value: TValue): [undefined, undefined] extends [TArray, TValue] ? TArray : NonNullable[number][]; +export function append[number] | undefined>( + to: TArray, value: TValue): [undefined, undefined] extends [TArray, TValue] ? TArray : NonNullable[number][]; export function append(to: T[], value: T | undefined): T[]; export function append(to: T[] | undefined, value: T): T[]; export function append(to: T[] | undefined, value: T | undefined): T[] | undefined; export function append(to: Push, value: T | undefined): void; export function append(to: T[], value: T | undefined): T[] | undefined { - if (value === undefined) return to; - if (to === undefined) return [value]; + if (value === undefined) { return to; } + if (to === undefined) { return [value]; } to.push(value); return to; } @@ -81,8 +82,8 @@ function toOffset(array: readonly any[], offset: number) { export function addRange(to: T[], from: readonly T[] | undefined, start?: number, end?: number): T[]; export function addRange(to: T[] | undefined, from: readonly T[] | undefined, start?: number, end?: number): T[] | undefined; export function addRange(to: T[] | undefined, from: readonly T[] | undefined, start?: number, end?: number): T[] | undefined { - if (from === undefined || from.length === 0) return to; - if (to === undefined) return from.slice(start, end); + if (from === undefined || from.length === 0) { return to; } + if (to === undefined) { return from.slice(start, end); } start = start === undefined ? 0 : toOffset(from, start); end = end === undefined ? from.length : toOffset(from, end); for (let i = start; i < end && i < from.length; i++) { @@ -96,11 +97,9 @@ export function addRange(to: T[] | undefined, from: readonly T[] | undefined, export function insertAt(array: T[], index: number, value: T) { if (index === 0) { array.unshift(value); - } - else if (index === array.length) { + } else if (index === array.length) { array.push(value); - } - else { + } else { for (let i = array.length; i > index; i--) { array[i] = array[i - 1]; } @@ -112,11 +111,11 @@ export function insertAt(array: T[], index: number, value: T) { export type Comparer = (a: T, b: T) => Comparison; export interface SortedReadonlyArray extends ReadonlyArray { - " __sortedArrayBrand": any; + ' __sortedArrayBrand': any; } export interface SortedArray extends Array { - " __sortedArrayBrand": any; + ' __sortedArrayBrand': any; } /** @@ -151,11 +150,10 @@ function stableSortIndices(array: readonly T[], indices: number[], comparer: export function map(array: readonly T[], f: (x: T, i: number) => U): U[]; export function map(array: readonly T[] | undefined, f: (x: T, i: number) => U): U[] | undefined; export function map(array: readonly T[] | undefined, f: (x: T, i: number) => U): U[] | undefined { - let result: U[] | undefined; if (array) { return array.map(f); } - return result; + return undefined; } export function some(array: readonly T[] | undefined): array is readonly T[]; @@ -164,8 +162,7 @@ export function some(array: readonly T[] | undefined, predicate?: (value: T) if (array) { if (predicate) { return array.some(predicate); - } - else { + } else { return array.length > 0; } } @@ -196,7 +193,9 @@ export function every(array: readonly T[], callback: (element: T, index: numb * @param keyComparer A callback used to compare two keys in a sorted array. * @param offset An offset into `array` at which to start the search. */ -export function binarySearch(array: readonly T[], value: T, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { +export function binarySearch(array: readonly T[], value: T, + keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { + return binarySearchKey(array, keySelector(value), keySelector, keyComparer, offset); } @@ -210,7 +209,9 @@ export function binarySearch(array: readonly T[], value: T, keySelector: ( * @param keyComparer A callback used to compare two keys in a sorted array. * @param offset An offset into `array` at which to start the search. */ -export function binarySearchKey(array: readonly T[], key: U, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { +export function binarySearchKey(array: readonly T[], key: U, + keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { + if (!some(array)) { return -1; } @@ -246,11 +247,10 @@ export function flatten(array: T[][] | readonly (T | readonly T[] | undefined if (v) { if (isArray(v)) { addRange(result, v); - } - else { + } else { result.push(v); } } } return result; -} \ No newline at end of file +} diff --git a/server/src/common/debug.ts b/server/src/common/debug.ts index 3d66b9495664..145981b08873 100644 --- a/server/src/common/debug.ts +++ b/server/src/common/debug.ts @@ -6,21 +6,23 @@ * Various debug helper methods to show user friendly debugging info */ -import { AnyFunction, compareValues, hasProperty } from "./core"; -import { stableSort } from "./collectionUtils"; +import { stableSort } from './collectionUtils'; +import { AnyFunction, compareValues, hasProperty } from './core'; + +export function assert(expression: boolean, message?: string, + verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): void { -export function assert(expression: boolean, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): void { if (!expression) { if (verboseDebugInfo) { - message += "\r\nVerbose Debug Information: " + (typeof verboseDebugInfo === "string" ? verboseDebugInfo : verboseDebugInfo()); + message += '\r\nVerbose Debug Information: ' + (typeof verboseDebugInfo === 'string' ? verboseDebugInfo : verboseDebugInfo()); } - fail(message ? "False expression: " + message : "False expression.", stackCrawlMark || assert); + fail(message ? 'False expression: ' + message : 'False expression.', stackCrawlMark || assert); } } export function fail(message?: string, stackCrawlMark?: AnyFunction): never { // debugger; - const e = new Error(message ? `Debug Failure. ${ message }` : "Debug Failure."); + const e = new Error(message ? `Debug Failure. ${ message }` : 'Debug Failure.'); if ((Error as any).captureStackTrace) { (Error as any).captureStackTrace(e, stackCrawlMark || fail); } @@ -28,7 +30,7 @@ export function fail(message?: string, stackCrawlMark?: AnyFunction): never { } export function assertDefined(value: T | null | undefined, message?: string): T { - if (value === undefined || value === null) return fail(message); + if (value === undefined || value === null) { return fail(message); } return value; } @@ -39,22 +41,20 @@ export function assertEachDefined(value: A, message?: return value; } -export function assertNever(member: never, message = "Illegal value:", stackCrawlMark?: AnyFunction): never { +export function assertNever(member: never, message = 'Illegal value:', stackCrawlMark?: AnyFunction): never { const detail = JSON.stringify(member); return fail(`${ message } ${ detail }`, stackCrawlMark || assertNever); } export function getFunctionName(func: AnyFunction) { - if (typeof func !== "function") { - return ""; - } - else if (hasProperty(func, "name")) { + if (typeof func !== 'function') { + return ''; + } else if (hasProperty(func, 'name')) { return (func as any).name; - } - else { + } else { const text = Function.prototype.toString.call(func); const match = /^function\s+([\w$]+)\s*\(/.exec(text); - return match ? match[1] : ""; + return match ? match[1] : ''; } } @@ -64,25 +64,24 @@ export function getFunctionName(func: AnyFunction) { export function formatEnum(value = 0, enumObject: any, isFlags?: boolean) { const members = getEnumMembers(enumObject); if (value === 0) { - return members.length > 0 && members[0][0] === 0 ? members[0][1] : "0"; + return members.length > 0 && members[0][0] === 0 ? members[0][1] : '0'; } if (isFlags) { - let result = ""; + let result = ''; let remainingFlags = value; for (const [enumValue, enumName] of members) { if (enumValue > value) { break; } if (enumValue !== 0 && enumValue & value) { - result = `${ result }${ result ? "|" : "" }${ enumName }`; + result = `${ result }${ result ? '|' : '' }${ enumName }`; remainingFlags &= ~enumValue; } } if (remainingFlags === 0) { return result; } - } - else { + } else { for (const [enumValue, enumName] of members) { if (enumValue === value) { return enumName; @@ -94,12 +93,12 @@ export function formatEnum(value = 0, enumObject: any, isFlags?: boolean) { function getEnumMembers(enumObject: any) { const result: [number, string][] = []; - for (const name in enumObject) { + for (const name of Object.keys(enumObject)) { const value = enumObject[name]; - if (typeof value === "number") { + if (typeof value === 'number') { result.push([value, name]); } } return stableSort<[number, string]>(result, (x, y) => compareValues(x[0], y[0])); -} \ No newline at end of file +} diff --git a/server/src/common/diagnostic.ts b/server/src/common/diagnostic.ts index 0a605a16e58b..bebf17c3ddd3 100644 --- a/server/src/common/diagnostic.ts +++ b/server/src/common/diagnostic.ts @@ -6,7 +6,8 @@ * * Class that represents errors and warnings. */ -import { CommandId } from '../definitions/commands'; + +import { Commands } from '../commands/commands'; import { Range } from './textRange'; export const enum DiagnosticCategory { @@ -20,12 +21,12 @@ export interface DiagnosticAction { } export interface CreateTypeStubFileAction extends DiagnosticAction { - action: CommandId.createTypeStub; + action: Commands.createTypeStub; moduleName: string; } export interface AddMissingOptionalToParamAction extends DiagnosticAction { - action: CommandId.addMissingOptionalToParam; + action: Commands.addMissingOptionalToParam; offsetOfTypeNode: number; } diff --git a/server/src/common/vfs.ts b/server/src/common/vfs.ts index 9c671c22239e..8ae78824bfca 100644 --- a/server/src/common/vfs.ts +++ b/server/src/common/vfs.ts @@ -10,8 +10,8 @@ /* eslint-disable no-dupe-class-members */ // * NOTE * except tests, this should be only file that import "fs" -import * as fs from 'fs'; import * as chokidar from 'chokidar'; +import * as fs from 'fs'; import { ConsoleInterface, NullConsole } from './console'; export type Listener = (eventName: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', path: string, stats?: Stats) => void; @@ -63,27 +63,27 @@ class FileSystem implements VirtualFileSystem { constructor(private _console: ConsoleInterface) { } - public existsSync(path: string) { return fs.existsSync(path) } - public mkdirSync(path: string) { fs.mkdirSync(path); } - public chdir(path: string) { process.chdir(path); } - public readdirSync(path: string) { return fs.readdirSync(path); } - public readFileSync(path: string, encoding?: null): Buffer; - public readFileSync(path: string, encoding: string): string; - public readFileSync(path: string, encoding?: string | null): Buffer | string; - public readFileSync(path: string, encoding: string | null = null) { return fs.readFileSync(path, { encoding: encoding }); } - public writeFileSync(path: string, data: string | Buffer, encoding: string | null) { fs.writeFileSync(path, data, { encoding: encoding }); } - public statSync(path: string) { return fs.statSync(path); } - public unlinkSync(path: string) { return fs.unlinkSync(path); } - public realpathSync(path: string) { return fs.realpathSync(path); } - - public getModulePath(): string { + existsSync(path: string) { return fs.existsSync(path); } + mkdirSync(path: string) { fs.mkdirSync(path); } + chdir(path: string) { process.chdir(path); } + readdirSync(path: string) { return fs.readdirSync(path); } + readFileSync(path: string, encoding?: null): Buffer; + readFileSync(path: string, encoding: string): string; + readFileSync(path: string, encoding?: string | null): Buffer | string; + readFileSync(path: string, encoding: string | null = null) { return fs.readFileSync(path, { encoding }); } + writeFileSync(path: string, data: string | Buffer, encoding: string | null) { fs.writeFileSync(path, data, { encoding }); } + statSync(path: string) { return fs.statSync(path); } + unlinkSync(path: string) { fs.unlinkSync(path); } + realpathSync(path: string) { return fs.realpathSync(path); } + + getModulePath(): string { // The entry point to the tool should have set the __rootDirectory // global variable to point to the directory that contains the // typeshed-fallback directory. return (global as any).__rootDirectory; } - public createFileSystemWatcher(paths: string[], event: 'all', listener: Listener): FileWatcher { + createFileSystemWatcher(paths: string[], event: 'all', listener: Listener): FileWatcher { return this._createBaseFileSystemWatcher(paths).on(event, listener); } @@ -128,4 +128,4 @@ class FileSystem implements VirtualFileSystem { return watcher; } -} \ No newline at end of file +} diff --git a/server/src/languageServerBase.ts b/server/src/languageServerBase.ts index 8789d1291e1a..38bd5589200b 100644 --- a/server/src/languageServerBase.ts +++ b/server/src/languageServerBase.ts @@ -9,10 +9,12 @@ import { createConnection, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, DocumentSymbol, ExecuteCommandParams, IConnection, InitializeResult, IPCMessageReader, IPCMessageWriter, Location, MarkupKind, ParameterInformation, - RemoteConsole, ResponseError, SignatureInformation, SymbolInformation, TextDocuments, TextEdit, WorkspaceEdit + RemoteConsole, SignatureInformation, SymbolInformation, TextDocuments, TextEdit, WorkspaceEdit } from 'vscode-languageserver'; import { AnalyzerService } from './analyzer/service'; +import { CommandController, ServerCommand } from './commands/commandController'; +import { Commands } from './commands/commands'; import { CommandLineOptions } from './common/commandLineOptions'; import { AddMissingOptionalToParamAction, CreateTypeStubFileAction, @@ -20,10 +22,10 @@ import { } from './common/diagnostic'; import './common/extensions'; import { combinePaths, convertPathToUri, convertUriToPath, getDirectoryPath, normalizePath } from './common/pathUtils'; -import { CommandId } from './definitions/commands'; -import { CompletionItemData } from './languageService/completionProvider'; -import { Range, Position } from './common/textRange'; +import { Position, Range } from './common/textRange'; import { createFromRealFileSystem, VirtualFileSystem } from './common/vfs'; +import { CompletionItemData } from './languageService/completionProvider'; +import { WorkspaceMap } from './workspaceMap'; export interface ServerSettings { venvPath?: string; @@ -44,39 +46,46 @@ export interface WorkspaceServiceInstance { export abstract class LanguageServerBase { // Create a connection for the server. The connection uses Node's IPC as a transport - private _connection: IConnection = createConnection(new IPCMessageReader(process), new IPCMessageWriter(process)); - private _fs: VirtualFileSystem; + connection: IConnection = createConnection(new IPCMessageReader(process), new IPCMessageWriter(process)); + workspaceMap: WorkspaceMap; + // File system abstraction. + fs: VirtualFileSystem; + + // Command controller. + private controller: ServerCommand; // Create a simple text document manager. The text document manager // supports full document sync only. private _documents: TextDocuments = new TextDocuments(); - private _workspaceMap = new Map(); // Global root path - the basis for all global settings. private _rootPath = ''; // Tracks whether we're currently displaying progress. private _isDisplayingProgress = false; - private _defaultWorkspacePath = ''; constructor(private _productName: string, rootDirectory?: string) { - this._connection.console.log(`${ _productName } language server starting`); + this.connection.console.log(`${ _productName } language server starting`); // virtual file system to be used. initialized to real file system by default. but can't be overritten - this._fs = createFromRealFileSystem(this._connection.console); + this.fs = createFromRealFileSystem(this.connection.console); // Stash the base directory into a global variable. (global as any).__rootDirectory = rootDirectory ? rootDirectory : getDirectoryPath(__dirname); + // Create workspace map. + this.workspaceMap = new WorkspaceMap(this); + // Create command controller. + this.controller = new CommandController(this); // Make the text document manager listen on the connection // for open, change and close text document events. - this._documents.listen(this._connection); + this._documents.listen(this.connection); // Setup callbacks this._setupConnection(); // Listen on the connection - this._connection.listen(); + this.connection.listen(); } abstract async getSettings(workspace: WorkspaceServiceInstance): Promise; // Provides access to logging to the client output window. protected get console(): RemoteConsole { - return this._connection.console; + return this.connection.console; } protected getConfiguration(workspace: WorkspaceServiceInstance, section: string) { @@ -85,14 +94,14 @@ export abstract class LanguageServerBase { scopeUri, section }; - return this._connection.workspace.getConfiguration(item); + return this.connection.workspace.getConfiguration(item); } // Creates a service instance that's used for analyzing a // program within a workspace. - private _createAnalyzerService(name: string): AnalyzerService { - this._connection.console.log(`Starting service instance "${ name }"`); - const service = new AnalyzerService(name, this._fs, this._connection.console); + createAnalyzerService(name: string): AnalyzerService { + this.connection.console.log(`Starting service instance "${ name }"`); + const service = new AnalyzerService(name, this.fs, this.connection.console); // Don't allow the analysis engine to go too long without // reporting results. This will keep it responsive. @@ -106,7 +115,7 @@ export abstract class LanguageServerBase { const diagnostics = this._convertDiagnostics(fileDiag.diagnostics); // Send the computed diagnostics to the client. - this._connection.sendDiagnostics({ + this.connection.sendDiagnostics({ uri: convertPathToUri(fileDiag.filePath), diagnostics }); @@ -116,17 +125,17 @@ export abstract class LanguageServerBase { // Display a progress spinner if we're checking the entire program. if (!this._isDisplayingProgress) { this._isDisplayingProgress = true; - this._connection.sendNotification('pyright/beginProgress'); + this.connection.sendNotification('pyright/beginProgress'); } const fileOrFiles = results.filesRequiringAnalysis !== 1 ? 'files' : 'file'; - this._connection.sendNotification('pyright/reportProgress', - `${ results.filesRequiringAnalysis } ${ fileOrFiles } to analyze`); + this.connection.sendNotification('pyright/reportProgress', + `${results.filesRequiringAnalysis} ${fileOrFiles} to analyze`); } } else { if (this._isDisplayingProgress) { this._isDisplayingProgress = false; - this._connection.sendNotification('pyright/endProgress'); + this.connection.sendNotification('pyright/endProgress'); } } }); @@ -135,101 +144,30 @@ export abstract class LanguageServerBase { return service; } - // Creates a service instance that's used for creating type - // stubs for a specified target library. - private _createTypeStubService(importName: string): AnalyzerService { - - this._connection.console.log('Starting type stub service instance'); - const service = new AnalyzerService('Type stub', this._fs, this._connection.console); - - service.setMaxAnalysisDuration({ - openFilesTimeInMs: 500, - noOpenFilesTimeInMs: 500 - }); - - return service; - } - - private _handlePostCreateTypeStub() { - this._workspaceMap.forEach(workspace => { - workspace.serviceInstance.handlePostCreateTypeStub(); - }); - } - - private _getWorkspaceForFile(filePath: string): WorkspaceServiceInstance { - let bestRootPath: string | undefined; - let bestInstance: WorkspaceServiceInstance | undefined; - - this._workspaceMap.forEach(workspace => { - if (workspace.rootPath) { - // Is the file is under this workspace folder? - if (filePath.startsWith(workspace.rootPath)) { - // Is this the fist candidate? If not, is this workspace folder - // contained within the previous candidate folder? We always want - // to select the innermost folder, since that overrides the - // outer folders. - if (bestRootPath === undefined || workspace.rootPath.startsWith(bestRootPath)) { - bestRootPath = workspace.rootPath; - bestInstance = workspace; - } - } - } - }); - - // If there were multiple workspaces or we couldn't find any, - // create a default one to use for this file. - if (bestInstance === undefined) { - let defaultWorkspace = this._workspaceMap.get(this._defaultWorkspacePath); - if (!defaultWorkspace) { - // If there is only one workspace, use that one. - const workspaceNames = [...this._workspaceMap.keys()]; - if (workspaceNames.length === 1) { - return this._workspaceMap.get(workspaceNames[0])!; - } - - // Create a default workspace for files that are outside - // of all workspaces. - defaultWorkspace = { - workspaceName: '', - rootPath: '', - rootUri: '', - serviceInstance: this._createAnalyzerService(this._defaultWorkspacePath), - disableLanguageServices: false - }; - this._workspaceMap.set(this._defaultWorkspacePath, defaultWorkspace); - this._updateSettingsForWorkspace(defaultWorkspace).ignoreErrors(); - } - - return defaultWorkspace; - } - - return bestInstance; - } - private _setupConnection(): void { // After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. - this._connection.onInitialize((params): InitializeResult => { + this.connection.onInitialize((params): InitializeResult => { this._rootPath = params.rootPath || ''; // Create a service instance for each of the workspace folders. if (params.workspaceFolders) { params.workspaceFolders.forEach(folder => { const path = convertUriToPath(folder.uri); - this._workspaceMap.set(path, { + this.workspaceMap.set(path, { workspaceName: folder.name, rootPath: path, rootUri: folder.uri, - serviceInstance: this._createAnalyzerService(folder.name), + serviceInstance: this.createAnalyzerService(folder.name), disableLanguageServices: false }); }); } else if (params.rootPath) { - this._workspaceMap.set(params.rootPath, { + this.workspaceMap.set(params.rootPath, { workspaceName: '', rootPath: params.rootPath, rootUri: '', - serviceInstance: this._createAnalyzerService(params.rootPath), + serviceInstance: this.createAnalyzerService(params.rootPath), disableLanguageServices: false }); } @@ -262,21 +200,21 @@ export abstract class LanguageServerBase { }; }); - this._connection.onDidChangeConfiguration(_ => { - this._connection.console.log(`Received updated settings`); - this._updateSettingsForAllWorkspaces(); + this.connection.onDidChangeConfiguration(_ => { + this.connection.console.log(`Received updated settings`); + this.updateSettingsForAllWorkspaces(); }); - this._connection.onCodeAction(params => { + this.connection.onCodeAction(params => { this._recordUserInteractionTime(); const sortImportsCodeAction = CodeAction.create( - 'Organize Imports', Command.create('Organize Imports', CommandId.orderImports), + 'Organize Imports', Command.create('Organize Imports', Commands.orderImports), CodeActionKind.SourceOrganizeImports); const codeActions: CodeAction[] = [sortImportsCodeAction]; const filePath = convertUriToPath(params.textDocument.uri); - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (!workspace.disableLanguageServices) { const range: Range = { start: { @@ -292,16 +230,16 @@ export abstract class LanguageServerBase { const diags = workspace.serviceInstance.getDiagnosticsForRange(filePath, range); const typeStubDiag = diags.find(d => { const actions = d.getActions(); - return actions && actions.find(a => a.action === CommandId.createTypeStub); + return actions && actions.find(a => a.action === Commands.createTypeStub); }); if (typeStubDiag) { const action = typeStubDiag.getActions()!.find( - a => a.action === CommandId.createTypeStub) as CreateTypeStubFileAction; + a => a.action === Commands.createTypeStub) as CreateTypeStubFileAction; if (action) { const createTypeStubAction = CodeAction.create( `Create Type Stub For ‘${action.moduleName}’`, - Command.create('Create Type Stub', CommandId.createTypeStub, + Command.create('Create Type Stub', Commands.createTypeStub, workspace.rootPath, action.moduleName), CodeActionKind.QuickFix); codeActions.push(createTypeStubAction); @@ -310,16 +248,16 @@ export abstract class LanguageServerBase { const addOptionalDiag = diags.find(d => { const actions = d.getActions(); - return actions && actions.find(a => a.action === CommandId.addMissingOptionalToParam); + return actions && actions.find(a => a.action === Commands.addMissingOptionalToParam); }); if (addOptionalDiag) { const action = addOptionalDiag.getActions()!.find( - a => a.action === CommandId.addMissingOptionalToParam) as AddMissingOptionalToParamAction; + a => a.action === Commands.addMissingOptionalToParam) as AddMissingOptionalToParamAction; if (action) { const addMissingOptionalAction = CodeAction.create( `Add 'Optional' to type annotation`, - Command.create(`Add 'Optional' to type annotation`, CommandId.addMissingOptionalToParam, + Command.create(`Add 'Optional' to type annotation`, Commands.addMissingOptionalToParam, action.offsetOfTypeNode), CodeActionKind.QuickFix); codeActions.push(addMissingOptionalAction); @@ -330,7 +268,7 @@ export abstract class LanguageServerBase { return codeActions; }); - this._connection.onDefinition(params => { + this.connection.onDefinition(params => { this._recordUserInteractionTime(); const filePath = convertUriToPath(params.textDocument.uri); @@ -340,7 +278,7 @@ export abstract class LanguageServerBase { character: params.position.character }; - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (workspace.disableLanguageServices) { return; } @@ -352,7 +290,7 @@ export abstract class LanguageServerBase { Location.create(convertPathToUri(loc.path), loc.range)); }); - this._connection.onReferences(params => { + this.connection.onReferences(params => { const filePath = convertUriToPath(params.textDocument.uri); const position: Position = { @@ -360,7 +298,7 @@ export abstract class LanguageServerBase { character: params.position.character }; - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (workspace.disableLanguageServices) { return; } @@ -373,12 +311,12 @@ export abstract class LanguageServerBase { Location.create(convertPathToUri(loc.path), loc.range)); }); - this._connection.onDocumentSymbol(params => { + this.connection.onDocumentSymbol(params => { this._recordUserInteractionTime(); const filePath = convertUriToPath(params.textDocument.uri); - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (workspace.disableLanguageServices) { return undefined; } @@ -388,10 +326,10 @@ export abstract class LanguageServerBase { return symbolList; }); - this._connection.onWorkspaceSymbol(params => { + this.connection.onWorkspaceSymbol(params => { const symbolList: SymbolInformation[] = []; - this._workspaceMap.forEach(workspace => { + this.workspaceMap.forEach(workspace => { if (!workspace.disableLanguageServices) { workspace.serviceInstance.addSymbolsForWorkspace( symbolList, params.query); @@ -401,7 +339,7 @@ export abstract class LanguageServerBase { return symbolList; }); - this._connection.onHover(params => { + this.connection.onHover(params => { const filePath = convertUriToPath(params.textDocument.uri); const position: Position = { @@ -409,7 +347,7 @@ export abstract class LanguageServerBase { character: params.position.character }; - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); const hoverResults = workspace.serviceInstance.getHoverForPosition(filePath, position); if (!hoverResults) { return undefined; @@ -431,7 +369,7 @@ export abstract class LanguageServerBase { }; }); - this._connection.onSignatureHelp(params => { + this.connection.onSignatureHelp(params => { const filePath = convertUriToPath(params.textDocument.uri); const position: Position = { @@ -439,7 +377,7 @@ export abstract class LanguageServerBase { character: params.position.character }; - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (workspace.disableLanguageServices) { return; } @@ -468,7 +406,7 @@ export abstract class LanguageServerBase { }; }); - this._connection.onCompletion(params => { + this.connection.onCompletion(params => { const filePath = convertUriToPath(params.textDocument.uri); const position: Position = { @@ -476,7 +414,7 @@ export abstract class LanguageServerBase { character: params.position.character }; - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (workspace.disableLanguageServices) { return; } @@ -495,10 +433,10 @@ export abstract class LanguageServerBase { return completions; }); - this._connection.onCompletionResolve(params => { + this.connection.onCompletionResolve(params => { const completionItemData = params.data as CompletionItemData; if (completionItemData) { - const workspace = this._workspaceMap.get(completionItemData.workspacePath); + const workspace = this.workspaceMap.get(completionItemData.workspacePath); if (workspace && completionItemData.filePath) { workspace.serviceInstance.resolveCompletionItem( completionItemData.filePath, params); @@ -507,7 +445,7 @@ export abstract class LanguageServerBase { return params; }); - this._connection.onRenameRequest(params => { + this.connection.onRenameRequest(params => { const filePath = convertUriToPath(params.textDocument.uri); const position: Position = { @@ -515,7 +453,7 @@ export abstract class LanguageServerBase { character: params.position.character }; - const workspace = this._getWorkspaceForFile(filePath); + const workspace = this.workspaceMap.getWorkspaceForFile(filePath); if (workspace.disableLanguageServices) { return; } @@ -545,37 +483,37 @@ export abstract class LanguageServerBase { return edits; }); - this._connection.onDidOpenTextDocument(params => { + this.connection.onDidOpenTextDocument(params => { const filePath = convertUriToPath(params.textDocument.uri); - const service = this._getWorkspaceForFile(filePath).serviceInstance; + const service = this.workspaceMap.getWorkspaceForFile(filePath).serviceInstance; service.setFileOpened( filePath, params.textDocument.version, params.textDocument.text); }); - this._connection.onDidChangeTextDocument(params => { + this.connection.onDidChangeTextDocument(params => { this._recordUserInteractionTime(); const filePath = convertUriToPath(params.textDocument.uri); - const service = this._getWorkspaceForFile(filePath).serviceInstance; + const service = this.workspaceMap.getWorkspaceForFile(filePath).serviceInstance; service.updateOpenFileContents( filePath, params.textDocument.version, params.contentChanges[0].text); }); - this._connection.onDidCloseTextDocument(params => { + this.connection.onDidCloseTextDocument(params => { const filePath = convertUriToPath(params.textDocument.uri); - const service = this._getWorkspaceForFile(filePath).serviceInstance; + const service = this.workspaceMap.getWorkspaceForFile(filePath).serviceInstance; service.setFileClosed(filePath); }); - this._connection.onInitialized(() => { - this._connection.workspace.onDidChangeWorkspaceFolders(event => { + this.connection.onInitialized(() => { + this.connection.workspace.onDidChangeWorkspaceFolders(event => { event.removed.forEach(workspace => { const rootPath = convertUriToPath(workspace.uri); - this._workspaceMap.delete(rootPath); + this.workspaceMap.delete(rootPath); }); event.added.forEach(async workspace => { @@ -584,103 +522,35 @@ export abstract class LanguageServerBase { workspaceName: workspace.name, rootPath, rootUri: workspace.uri, - serviceInstance: this._createAnalyzerService(workspace.name), + serviceInstance: this.createAnalyzerService(workspace.name), disableLanguageServices: false }; - this._workspaceMap.set(rootPath, newWorkspace); - await this._updateSettingsForWorkspace(newWorkspace); + this.workspaceMap.set(rootPath, newWorkspace); + await this.updateSettingsForWorkspace(newWorkspace); }); }); }); - this._connection.onExecuteCommand((cmdParams: ExecuteCommandParams) => this._executeCommand(cmdParams)); + this.connection.onExecuteCommand((cmdParams: ExecuteCommandParams) => this.executeCommand(cmdParams)); } - private async _executeCommand(cmdParams: ExecuteCommandParams): Promise { - if (cmdParams.command === CommandId.orderImports || - cmdParams.command === CommandId.addMissingOptionalToParam) { - - if (cmdParams.arguments && cmdParams.arguments.length >= 1) { - const docUri = cmdParams.arguments[0]; - const otherArgs = cmdParams.arguments.slice(1); - const filePath = convertUriToPath(docUri); - const workspace = this._getWorkspaceForFile(filePath); - const editActions = workspace.serviceInstance.performQuickAction( - filePath, cmdParams.command, otherArgs); - if (!editActions) { - return []; - } - - const edits: TextEdit[] = []; - editActions.forEach(editAction => { - edits.push({ - range: editAction.range, - newText: editAction.replacementText - }); - }); - - return edits; - } - } - - if (cmdParams.command === CommandId.createTypeStub) { - if (cmdParams.arguments && cmdParams.arguments.length >= 2) { - const workspaceRoot = cmdParams.arguments[0]; - const importName = cmdParams.arguments[1]; - const service = this._createTypeStubService(importName); - - // Allocate a temporary pseudo-workspace to perform this job. - const workspace: WorkspaceServiceInstance = { - workspaceName: `Create Type Stub ${ importName }`, - rootPath: workspaceRoot, - rootUri: convertPathToUri(workspaceRoot), - serviceInstance: service, - disableLanguageServices: true - }; - - service.setCompletionCallback(results => { - if (results.filesRequiringAnalysis === 0) { - try { - service.writeTypeStub(); - service.dispose(); - const infoMessage = `Type stub was successfully created for '${ importName }'.`; - this._connection.window.showInformationMessage(infoMessage); - this._handlePostCreateTypeStub(); - } catch (err) { - let errMessage = ''; - if (err instanceof Error) { - errMessage = ': ' + err.message; - } - errMessage = `An error occurred when creating type stub for '${ importName }'` + - errMessage; - this._connection.console.error(errMessage); - this._connection.window.showErrorMessage(errMessage); - } - } - }); - - const serverSettings = await this.getSettings(workspace); - this._updateOptionsAndRestartService(workspace, serverSettings, importName); - return; - } - } - - return new ResponseError(1, 'Unsupported command'); + protected executeCommand(cmdParams: ExecuteCommandParams): Promise { + return this.controller.execute(cmdParams); } - private _updateSettingsForAllWorkspaces(): void { - this._workspaceMap.forEach(workspace => { - this._updateSettingsForWorkspace(workspace).ignoreErrors(); + updateSettingsForAllWorkspaces(): void { + this.workspaceMap.forEach(workspace => { + this.updateSettingsForWorkspace(workspace).ignoreErrors(); }); } - private async _updateSettingsForWorkspace(workspace: WorkspaceServiceInstance): Promise { + async updateSettingsForWorkspace(workspace: WorkspaceServiceInstance): Promise { const serverSettings = await this.getSettings(workspace); - this._updateOptionsAndRestartService(workspace, serverSettings); + this.updateOptionsAndRestartService(workspace, serverSettings); workspace.disableLanguageServices = !!serverSettings.disableLanguageServices; } - private _updateOptionsAndRestartService(workspace: WorkspaceServiceInstance, + updateOptionsAndRestartService(workspace: WorkspaceServiceInstance, serverSettings: ServerSettings, typeStubTargetImportName?: string) { const commandLineOptions = new CommandLineOptions(workspace.rootPath, true); @@ -774,7 +644,7 @@ export abstract class LanguageServerBase { // Tell all of the services that the user is actively // interacting with one or more editors, so they should // back off from performing any work. - this._workspaceMap.forEach(workspace => { + this.workspaceMap.forEach(workspace => { workspace.serviceInstance.recordUserInteractionTime(); }); } diff --git a/server/src/languageService/quickActions.ts b/server/src/languageService/quickActions.ts index 6e9fb31bad40..01adb781fe00 100644 --- a/server/src/languageService/quickActions.ts +++ b/server/src/languageService/quickActions.ts @@ -10,21 +10,21 @@ import { ImportType } from '../analyzer/importResult'; import * as ImportStatementUtils from '../analyzer/importStatementUtils'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; +import { Commands } from '../commands/commands'; import { TextEditAction } from '../common/editAction'; import { convertOffsetToPosition } from '../common/positionUtils'; import { TextRange } from '../common/textRange'; import { ParseNode, ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; -import { CommandId } from '../definitions/commands'; import { ImportSorter } from './importSorter'; export function performQuickAction(command: string, args: any[], parseResults: ParseResults) { - if (command === CommandId.orderImports) { + if (command === Commands.orderImports) { const importSorter = new ImportSorter(parseResults); return importSorter.sort(); - } else if (command === CommandId.addMissingOptionalToParam) { + } else if (command === Commands.addMissingOptionalToParam) { if (args.length >= 1) { const nodeOffset = parseInt(args[0], 10); return _addMissingOptionalToParam(parseResults, nodeOffset); diff --git a/server/src/pyright.ts b/server/src/pyright.ts index dab666ad570c..29289609337b 100644 --- a/server/src/pyright.ts +++ b/server/src/pyright.ts @@ -20,9 +20,9 @@ import { AnalyzerService } from './analyzer/service'; import { CommandLineOptions as PyrightCommandLineOptions } from './common/commandLineOptions'; import { NullConsole } from './common/console'; import { DiagnosticCategory } from './common/diagnostic'; -import { Range } from './common/textRange'; import { FileDiagnostics } from './common/diagnosticSink'; import { combinePaths, normalizePath } from './common/pathUtils'; +import { Range } from './common/textRange'; import { createFromRealFileSystem } from './common/vfs'; const toolName = 'pyright'; diff --git a/server/src/tests/collectionUtils.test.ts b/server/src/tests/collectionUtils.test.ts index d4c27137140f..e232372f8aca 100644 --- a/server/src/tests/collectionUtils.test.ts +++ b/server/src/tests/collectionUtils.test.ts @@ -14,8 +14,8 @@ test('UtilsContainsDefault', () => { }); test('UtilsContainsComparer', () => { - const data = [new D(1, "A"), new D(2, "B"), new D(3, "C"), new D(4, "D")] - assert(utils.contains(data, new D(1, "D"), (a, b) => a.Value == b.Value)); + const data = [new D(1, 'A'), new D(2, 'B'), new D(3, 'C'), new D(4, 'D')]; + assert(utils.contains(data, new D(1, 'D'), (a, b) => a.value === b.value)); }); test('UtilsAppend', () => { @@ -45,47 +45,47 @@ test('UtilsFindNoMatch', () => { test('UtilsFindMatchSimple', () => { const data = [1]; - assert.equal(utils.find(data, e => e == 1), 1); + assert.equal(utils.find(data, e => e === 1), 1); }); test('UtilsFindMatch', () => { - const data = [new D(1, "Hello")]; - assert.equal(utils.find(data, e => e.Value == 1), data[0]); + const data = [new D(1, 'Hello')]; + assert.equal(utils.find(data, e => e.value === 1), data[0]); }); test('UtilsFindMatchCovariant', () => { - const item1 = new D(1, "Hello"); - const item2 = new D(2, "Hello2"); + const item1 = new D(1, 'Hello'); + const item2 = new D(2, 'Hello2'); const data: B[] = [new B(0), item1, item2, new B(3)]; - assert.equal(utils.find(data, (e: D) => e.Value == 2), item2); + assert.equal(utils.find(data, (e: D) => e.value === 2), item2); }); test('UtilsStableSort', () => { - const data = [new D(2, "Hello3"), new D(1, "Hello1"), new D(2, "Hello4"), new D(1, "Hello2")]; - const sorted = utils.stableSort(data, (a, b) => compareValues(a.Value, b.Value)); + const data = [new D(2, 'Hello3'), new D(1, 'Hello1'), new D(2, 'Hello4'), new D(1, 'Hello2')]; + const sorted = utils.stableSort(data, (a, b) => compareValues(a.value, b.value)); const result: string[] = []; - sorted.forEach(e => result.push(e.Name)); + sorted.forEach(e => result.push(e.name)); - assert.deepEqual(result, ["Hello1", "Hello2", "Hello3", "Hello4"]) + assert.deepEqual(result, ['Hello1', 'Hello2', 'Hello3', 'Hello4']); }); test('UtilsBinarySearch', () => { - const data = [new D(1, "Hello3"), new D(2, "Hello1"), new D(3, "Hello4"), new D(4, "Hello2")]; - const index = utils.binarySearch(data, new D(3, "Unused"), v => v.Value, compareValues, 0); + const data = [new D(1, 'Hello3'), new D(2, 'Hello1'), new D(3, 'Hello4'), new D(4, 'Hello2')]; + const index = utils.binarySearch(data, new D(3, 'Unused'), v => v.value, compareValues, 0); assert.equal(index, 2); }); test('UtilsBinarySearchMiss', () => { - const data = [new D(1, "Hello3"), new D(2, "Hello1"), new D(4, "Hello4"), new D(5, "Hello2")]; - const index = utils.binarySearch(data, new D(3, "Unused"), v => v.Value, compareValues, 0); + const data = [new D(1, 'Hello3'), new D(2, 'Hello1'), new D(4, 'Hello4'), new D(5, 'Hello2')]; + const index = utils.binarySearch(data, new D(3, 'Unused'), v => v.value, compareValues, 0); assert.equal(~index, 2); }); test('isArray1', () => { - const data = [new D(1, "Hello3")]; + const data = [new D(1, 'Hello3')]; assert(isArray(data)); }); @@ -130,18 +130,18 @@ test('flatten', () => { }); class B { - Value: number; + value: number; constructor(value: number) { - this.Value = value; + this.value = value; } } class D extends B { - Name: string; + name: string; constructor(value: number, name: string) { super(value); - this.Name = name; + this.name = name; } -} \ No newline at end of file +} diff --git a/server/src/tests/debug.test.ts b/server/src/tests/debug.test.ts index b448e80a3709..20ceb015be6d 100644 --- a/server/src/tests/debug.test.ts +++ b/server/src/tests/debug.test.ts @@ -8,32 +8,32 @@ import * as assert from 'assert'; import * as debug from '../common/debug'; test('DebugAssertTrue', () => { - assert.doesNotThrow(() => debug.assert(true, "doesn't throw")); + assert.doesNotThrow(() => { debug.assert(true, 'doesn\'t throw'); }); }); test('DebugAssertFalse', () => { assert.throws( - () => debug.assert(false, "should throw"), + () => { debug.assert(false, 'should throw'); }, (err: any) => err instanceof Error, - "unexpected"); + 'unexpected'); }); test('DebugAssertDetailInfo', () => { // let assert to show more detail info which will get collected when // assert raised - const detailInfo = "Detail Info"; + const detailInfo = 'Detail Info'; assert.throws( - () => debug.assert(false, "should throw", () => detailInfo), + () => { debug.assert(false, 'should throw', () => detailInfo); }, (err: any) => err instanceof Error && err.message.includes(detailInfo), - "unexpected"); + 'unexpected'); }); test('DebugAssertStackTrace', () => { // let assert to control what callstack to put in exception stack assert.throws( - () => debug.assert(false, "should throw", undefined, assert.throws), - (err: any) => err instanceof Error && !err.message.includes("assert.throws"), - "unexpected"); + () => { debug.assert(false, 'should throw', undefined, assert.throws); }, + (err: any) => err instanceof Error && !err.message.includes('assert.throws'), + 'unexpected'); }); test('DebugAssertUndefined', () => { @@ -41,7 +41,7 @@ test('DebugAssertUndefined', () => { assert.throws( () => debug.assertDefined(unused), (err: any) => err instanceof Error, - "unexpected"); + 'unexpected'); }); test('DebugAssertDefined', () => { @@ -55,7 +55,7 @@ test('DebugAssertEachUndefined', () => { assert.throws( () => debug.assertEachDefined(unused), (err: any) => err instanceof Error, - "unexpected"); + 'unexpected'); }); test('DebugAssertEachDefined', () => { @@ -81,17 +81,17 @@ test('DebugAssertNever', () => { } }, (err: any) => err instanceof Error, - "unexpected"); + 'unexpected'); }); test('DebugGetFunctionName', () => { // helper method to add better message in exception - assert(debug.getFunctionName(assert.throws) === "throws"); + assert(debug.getFunctionName(assert.throws) === 'throws'); }); test('DebugFormatEnum', () => { // helper method to add better message in exception around enum // const enum require --preserveConstEnums flag to work properly enum MyEnum { A, B, C } - assert(debug.formatEnum(MyEnum.A, MyEnum, false) === "A"); -}); \ No newline at end of file + assert(debug.formatEnum(MyEnum.A, MyEnum, false) === 'A'); +}); diff --git a/server/src/tests/filesystem.test.ts b/server/src/tests/filesystem.test.ts index 129f24a201a7..c4905c7f14d1 100644 --- a/server/src/tests/filesystem.test.ts +++ b/server/src/tests/filesystem.test.ts @@ -2,84 +2,84 @@ * filesystem.test.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * Test and Show how to use virtual file system */ import * as assert from 'assert'; +import { combinePaths, normalizeSlashes } from '../common/pathUtils'; import * as host from './harness/host'; -import * as vfs from "./harness/vfs/filesystem"; -import * as factory from "./harness/vfs/factory" -import { normalizeSlashes, combinePaths } from '../common/pathUtils'; +import * as factory from './harness/vfs/factory'; +import * as vfs from './harness/vfs/filesystem'; test('CreateVFS', () => { - const cwd = normalizeSlashes("/"); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: cwd }) - assert.equal(fs.cwd(), cwd) + const cwd = normalizeSlashes('/'); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd }); + assert.equal(fs.cwd(), cwd); }); test('Folders', () => { - const cwd = normalizeSlashes("/"); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: cwd }) + const cwd = normalizeSlashes('/'); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd }); // no such dir exist - assert.throws(() => fs.chdir("a")) + assert.throws(() => { fs.chdir('a'); }); - fs.mkdirSync("a"); - fs.chdir("a"); - assert.equal(fs.cwd(), normalizeSlashes("/a")); + fs.mkdirSync('a'); + fs.chdir('a'); + assert.equal(fs.cwd(), normalizeSlashes('/a')); - fs.chdir(".."); - fs.rmdirSync("a"); + fs.chdir('..'); + fs.rmdirSync('a'); // no such dir exist - assert.throws(() => fs.chdir("a")) + assert.throws(() => { fs.chdir('a'); }); }); test('Files', () => { - const cwd = normalizeSlashes("/"); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: cwd }) + const cwd = normalizeSlashes('/'); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd }); - fs.writeFileSync("1.txt", "hello", "utf8"); - const buffer1 = fs.readFileSync("1.txt"); - assert.equal(buffer1.toString(), "hello"); + fs.writeFileSync('1.txt', 'hello', 'utf8'); + const buffer1 = fs.readFileSync('1.txt'); + assert.equal(buffer1.toString(), 'hello'); - const p = normalizeSlashes("a/b/c"); + const p = normalizeSlashes('a/b/c'); fs.mkdirpSync(p); - const f = combinePaths(p, "2.txt"); - fs.writeFileSync(f, "hi"); + const f = combinePaths(p, '2.txt'); + fs.writeFileSync(f, 'hi'); - const str = fs.readFileSync(f, "utf8"); - assert.equal(str, "hi"); + const str = fs.readFileSync(f, 'utf8'); + assert.equal(str, 'hi'); }); test('CreateRich', () => { - const cwd = normalizeSlashes("/"); + const cwd = normalizeSlashes('/'); const files: vfs.FileSet = { - [normalizeSlashes("/a/b/c/1.txt")]: new vfs.File("hello1"), - [normalizeSlashes("/a/b/2.txt")]: new vfs.File("hello2"), - [normalizeSlashes("/a/3.txt")]: new vfs.File("hello3"), - [normalizeSlashes("/4.txt")]: new vfs.File("hello4", { encoding: "utf16le" }), - [normalizeSlashes("/a/b/../c/./5.txt")]: new vfs.File("hello5", { encoding: "ucs2" }) - } + [normalizeSlashes('/a/b/c/1.txt')]: new vfs.File('hello1'), + [normalizeSlashes('/a/b/2.txt')]: new vfs.File('hello2'), + [normalizeSlashes('/a/3.txt')]: new vfs.File('hello3'), + [normalizeSlashes('/4.txt')]: new vfs.File('hello4', { encoding: 'utf16le' }), + [normalizeSlashes('/a/b/../c/./5.txt')]: new vfs.File('hello5', { encoding: 'ucs2' }) + }; - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: cwd, files: files }) - const entries = fs.scanSync(cwd, "descendants-or-self", {}); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd, files }); + const entries = fs.scanSync(cwd, 'descendants-or-self', {}); // files + directory + root assert.equal(entries.length, 10); - assert.equal(fs.readFileSync(normalizeSlashes("/a/b/c/1.txt"), "ascii"), "hello1"); - assert.equal(fs.readFileSync(normalizeSlashes("/a/b/2.txt"), "utf8"), "hello2"); - assert.equal(fs.readFileSync(normalizeSlashes("/a/3.txt"), "utf-8"), "hello3"); - assert.equal(fs.readFileSync(normalizeSlashes("/4.txt"), "utf16le"), "hello4"); - assert.equal(fs.readFileSync(normalizeSlashes("/a/c/5.txt"), "ucs2"), "hello5"); + assert.equal(fs.readFileSync(normalizeSlashes('/a/b/c/1.txt'), 'ascii'), 'hello1'); + assert.equal(fs.readFileSync(normalizeSlashes('/a/b/2.txt'), 'utf8'), 'hello2'); + assert.equal(fs.readFileSync(normalizeSlashes('/a/3.txt'), 'utf-8'), 'hello3'); + assert.equal(fs.readFileSync(normalizeSlashes('/4.txt'), 'utf16le'), 'hello4'); + assert.equal(fs.readFileSync(normalizeSlashes('/a/c/5.txt'), 'ucs2'), 'hello5'); }); test('Shadow', () => { - const cwd = normalizeSlashes("/"); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: cwd }); + const cwd = normalizeSlashes('/'); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd }); // only readonly fs can be shadowed assert.throws(() => fs.shadow()); @@ -104,19 +104,19 @@ test('Shadow', () => { }); test('Diffing', () => { - const cwd = normalizeSlashes("/"); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: cwd }); + const cwd = normalizeSlashes('/'); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd }); // first snapshot fs.snapshot(); - fs.writeFileSync("test1.txt", "hello1"); + fs.writeFileSync('test1.txt', 'hello1'); // compared with original assert.equal(countFile(fs.diff()!), 1); // second snapshot fs.snapshot(); - fs.writeFileSync("test2.txt", "hello2"); + fs.writeFileSync('test2.txt', 'hello2'); // compared with first snapshot assert.equal(countFile(fs.diff()!), 1); @@ -128,11 +128,11 @@ test('Diffing', () => { const s = fs.shadowRoot!.shadow(); // "test2.txt" only exist in first snapshot - assert(!s.existsSync("test2.txt")); + assert(!s.existsSync('test2.txt')); // create parallel universe where it has another version of test2.txt with different content // compared to second snapshot which forked from same first snapshot - s.writeFileSync("test2.txt", "hello3"); + s.writeFileSync('test2.txt', 'hello3'); // diff between non direct snapshots // diff gives test2.txt even though it exist in both snapshot @@ -140,39 +140,40 @@ test('Diffing', () => { }); test('createFromFileSystem1', () => { - const filepath = normalizeSlashes(combinePaths(factory.srcFolder, "test.py")); - const content = "# test"; + const filepath = normalizeSlashes(combinePaths(factory.srcFolder, 'test.py')); + const content = '# test'; // file system will map physical file system to virtual one - const fs = factory.createFromFileSystem(host.Host, false, { documents: [new factory.TextDocument(filepath, content)], cwd: factory.srcFolder }); + const fs = factory.createFromFileSystem(host.HOST, false, + { documents: [new factory.TextDocument(filepath, content)], cwd: factory.srcFolder }); // check existing typeshed folder on virtual path inherited from base snapshot from physical file system const entries = fs.readdirSync(factory.typeshedFolder); assert(entries.length > 0); // confirm file - assert.equal(fs.readFileSync(filepath, "utf8"), content); + assert.equal(fs.readFileSync(filepath, 'utf8'), content); }); test('createFromFileSystem2', () => { - const fs = factory.createFromFileSystem(host.Host, /* ignoreCase */ true, { cwd: factory.srcFolder }); + const fs = factory.createFromFileSystem(host.HOST, /* ignoreCase */ true, { cwd: factory.srcFolder }); const entries = fs.readdirSync(factory.typeshedFolder.toUpperCase()); assert(entries.length > 0); }); test('createFromFileSystemWithCustomTypeshedPath', () => { - const invalidpath = normalizeSlashes(combinePaths(host.Host.getWorkspaceRoot(), "../docs")); - const fs = factory.createFromFileSystem(host.Host, /* ignoreCase */ false, { + const invalidpath = normalizeSlashes(combinePaths(host.HOST.getWorkspaceRoot(), '../docs')); + const fs = factory.createFromFileSystem(host.HOST, /* ignoreCase */ false, { cwd: factory.srcFolder, meta: { [factory.typeshedFolder]: invalidpath } }); const entries = fs.readdirSync(factory.typeshedFolder); - assert(entries.filter(e => e.endsWith(".md")).length > 0); + assert(entries.filter(e => e.endsWith('.md')).length > 0); }); test('createFromFileSystemWithMetadata', () => { - const fs = factory.createFromFileSystem(host.Host, /* ignoreCase */ false, { - cwd: factory.srcFolder, meta: { "unused": "unused" } + const fs = factory.createFromFileSystem(host.HOST, /* ignoreCase */ false, { + cwd: factory.srcFolder, meta: { 'unused': 'unused' } }); assert(fs.existsSync(factory.srcFolder)); @@ -202,4 +203,4 @@ function _flatten(files: vfs.FileSet, result: vfs.FileSet): void { _flatten(value.files, result); } } -} \ No newline at end of file +} diff --git a/server/src/tests/fourSlashParser.test.ts b/server/src/tests/fourSlashParser.test.ts index 126e746d0736..48576267fc8a 100644 --- a/server/src/tests/fourSlashParser.test.ts +++ b/server/src/tests/fourSlashParser.test.ts @@ -2,18 +2,18 @@ * fourSlashParser.test.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * Tests and show how to use fourslash markup languages * and how to use parseTestData API itself for other unit tests */ import * as assert from 'assert'; -import * as factory from "./harness/vfs/factory" -import * as host from './harness/host'; -import { parseTestData } from './harness/fourslash/fourSlashParser'; +import { getBaseFileName, normalizeSlashes } from '../common/pathUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; +import { parseTestData } from './harness/fourslash/fourSlashParser'; import { CompilerSettings } from './harness/fourslash/fourSlashTypes'; -import { normalizeSlashes, getBaseFileName } from '../common/pathUtils'; +import * as host from './harness/host'; +import * as factory from './harness/vfs/factory'; test('GlobalOptions', () => { const code = ` @@ -23,16 +23,16 @@ test('GlobalOptions', () => { ////class A: //// pass - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); - assertOptions(data.globalOptions, [["libpath", "../dist/lib"], ["pythonversion", "3.7"]]); + const data = parseTestData('.', code, 'test.py'); + assertOptions(data.globalOptions, [['libpath', '../dist/lib'], ['pythonversion', '3.7']]); assert.equal(data.files.length, 1); - assert.equal(data.files[0].fileName, "test.py"); + assert.equal(data.files[0].fileName, 'test.py'); assert.equal(data.files[0].content, content); }); @@ -41,16 +41,16 @@ test('Filename', () => { // @filename: file1.py ////class A: //// pass - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assertOptions(data.globalOptions, []); assert.equal(data.files.length, 1); - assert.equal(data.files[0].fileName, normalizeSlashes("./file1.py")); + assert.equal(data.files[0].fileName, normalizeSlashes('./file1.py')); assert.equal(data.files[0].content, content); }); @@ -61,44 +61,44 @@ test('Extra file options', () => { // @filename: file1.py ////class A: //// pass - ` + `; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assertOptions(data.globalOptions, []); - assertOptions(data.files[0].fileOptions, [["filename", "file1.py"], ["reserved", "not used"]]) + assertOptions(data.files[0].fileOptions, [['filename', 'file1.py'], ['reserved', 'not used']]); }); test('Range', () => { const code = ` ////class A: //// [|pass|] - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files[0].content, content); - assert.deepEqual(data.ranges, [{ fileName: "test.py", pos: 13, end: 17, marker: undefined }]); + assert.deepEqual(data.ranges, [{ fileName: 'test.py', pos: 13, end: 17, marker: undefined }]); }); test('Marker', () => { const code = ` ////class A: //// /*marker1*/pass - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files[0].content, content); - const marker = { fileName: "test.py", position: 13 }; + const marker = { fileName: 'test.py', position: 13 }; assert.deepEqual(data.markers, [marker]); - assert.deepEqual(data.markerPositions.get("marker1"), marker) + assert.deepEqual(data.markerPositions.get('marker1'), marker); }); test('MarkerWithData', () => { @@ -106,16 +106,16 @@ test('MarkerWithData', () => { const code = ` ////class A: //// {| "data1":"1", "data2":"2" |}pass - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files[0].content, content); - assert.deepEqual(data.markers, [{ fileName: "test.py", position: 13, data: { data1: "1", data2: "2" } }]); - assert.equal(data.markerPositions.size, 0) + assert.deepEqual(data.markers, [{ fileName: 'test.py', position: 13, data: { data1: '1', data2: '2' } }]); + assert.equal(data.markerPositions.size, 0); }); test('MarkerWithDataAndName', () => { @@ -123,17 +123,17 @@ test('MarkerWithDataAndName', () => { const code = ` ////class A: //// {| "name": "marker1", "data1":"1", "data2":"2" |}pass - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files[0].content, content); - const marker = { fileName: "test.py", position: 13, data: { name: "marker1", data1: "1", data2: "2" } }; + const marker = { fileName: 'test.py', position: 13, data: { name: 'marker1', data1: '1', data2: '2' } }; assert.deepEqual(data.markers, [marker]); - assert.deepEqual(data.markerPositions.get(marker.data.name), marker) + assert.deepEqual(data.markerPositions.get(marker.data.name), marker); }); test('RangeWithMarker', () => { @@ -141,19 +141,19 @@ test('RangeWithMarker', () => { const code = ` ////class A: //// [|/*marker1*/pass|] - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files[0].content, content); - const marker = { fileName: "test.py", position: 13 }; + const marker = { fileName: 'test.py', position: 13 }; assert.deepEqual(data.markers, [marker]); - assert.deepEqual(data.markerPositions.get("marker1"), marker) + assert.deepEqual(data.markerPositions.get('marker1'), marker); - assert.deepEqual(data.ranges, [{ fileName: "test.py", pos: 13, end: 17, marker: marker }]); + assert.deepEqual(data.ranges, [{ fileName: 'test.py', pos: 13, end: 17, marker }]); }); test('RangeWithMarkerAndJsonData', () => { @@ -161,19 +161,19 @@ test('RangeWithMarkerAndJsonData', () => { const code = ` ////class A: //// [|{| "name": "marker1", "data1":"1", "data2":"2" |}pass|] - ` + `; const content = `class A: pass`; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files[0].content, content); - const marker = { fileName: "test.py", position: 13, data: { name: "marker1", data1: "1", data2: "2" } }; + const marker = { fileName: 'test.py', position: 13, data: { name: 'marker1', data1: '1', data2: '2' } }; assert.deepEqual(data.markers, [marker]); - assert.deepEqual(data.markerPositions.get(marker.data.name), marker) + assert.deepEqual(data.markerPositions.get(marker.data.name), marker); - assert.deepEqual(data.ranges, [{ fileName: "test.py", pos: 13, end: 17, marker: marker }]); + assert.deepEqual(data.ranges, [{ fileName: 'test.py', pos: 13, end: 17, marker }]); }); test('Multiple Files', () => { @@ -190,14 +190,14 @@ test('Multiple Files', () => { // @filename: src/C.py ////class C: //// pass - ` + `; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files.length, 3); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/A.py"))[0].content, getContent("A")); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/B.py"))[0].content, getContent("B")); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/C.py"))[0].content, getContent("C")); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/A.py'))[0].content, getContent('A')); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/B.py'))[0].content, getContent('B')); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/C.py'))[0].content, getContent('C')); }); test('Multiple Files with default name', () => { @@ -213,14 +213,14 @@ test('Multiple Files with default name', () => { // @filename: src/C.py ////class C: //// pass - ` + `; - const data = parseTestData(".", code, "./src/test.py"); + const data = parseTestData('.', code, './src/test.py'); assert.equal(data.files.length, 3); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/test.py"))[0].content, getContent("A")); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/B.py"))[0].content, getContent("B")); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/C.py"))[0].content, getContent("C")); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/test.py'))[0].content, getContent('A')); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/B.py'))[0].content, getContent('B')); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/C.py'))[0].content, getContent('C')); }); test('Multiple Files with markers', () => { @@ -237,19 +237,19 @@ test('Multiple Files with markers', () => { // @filename: src/C.py ////class C: //// [|{|"name":"marker2", "data":"2"|}pass|] - ` + `; - const data = parseTestData(".", code, "test.py"); + const data = parseTestData('.', code, 'test.py'); assert.equal(data.files.length, 3); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/A.py"))[0].content, getContent("A")); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/B.py"))[0].content, getContent("B")); - assert.equal(data.files.filter(f => f.fileName === normalizeSlashes("./src/C.py"))[0].content, getContent("C")); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/A.py'))[0].content, getContent('A')); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/B.py'))[0].content, getContent('B')); + assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/C.py'))[0].content, getContent('C')); assert.equal(data.ranges.length, 3); - assert(data.markerPositions.get("marker1")); - assert(data.markerPositions.get("marker2")); + assert(data.markerPositions.get('marker1')); + assert(data.markerPositions.get('marker2')); assert.equal(data.ranges.filter(r => r.marker).length, 2); }); @@ -267,14 +267,16 @@ test('fourSlashWithFileSystem', () => { // @filename: src/C.py ////class C: //// pass - ` + `; + + const data = parseTestData('.', code, 'unused'); + const documents = data.files.map(f => new factory.TextDocument(f.fileName, f.content, + new Map(Object.entries(f.fileOptions)))); - const data = parseTestData(".", code, "unused"); - const documents = data.files.map(f => new factory.TextDocument(f.fileName, f.content, new Map(Object.entries(f.fileOptions)))); - const fs = factory.createFromFileSystem(host.Host, /* ignoreCase */ false, { documents: documents, cwd: normalizeSlashes("/") }); + const fs = factory.createFromFileSystem(host.HOST, /* ignoreCase */ false, { documents, cwd: normalizeSlashes('/') }); for (const file of data.files) { - assert.equal(fs.readFileSync(file.fileName, "utf8"), getContent(getBaseFileName(file.fileName, ".py", false))); + assert.equal(fs.readFileSync(file.fileName, 'utf8'), getContent(getBaseFileName(file.fileName, '.py', false))); } }); @@ -288,4 +290,4 @@ function assertOptions(actual: CompilerSettings, expected: [string, string][], m Object.entries(actual).sort((x, y) => compareStringsCaseSensitive(x[0], y[0])), expected, message); -} \ No newline at end of file +} diff --git a/server/src/tests/fourSlashRunner.test.ts b/server/src/tests/fourSlashRunner.test.ts index 68effa5c0e56..546347004a52 100644 --- a/server/src/tests/fourSlashRunner.test.ts +++ b/server/src/tests/fourSlashRunner.test.ts @@ -2,32 +2,32 @@ * fourslashrunner.test.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * Entry point that will read all *.fourslash.ts files and * register jest tests for them and run */ -import * as path from "path"; -import * as host from "./harness/host"; -import { normalizeSlashes } from "../common/pathUtils"; -import { runFourSlashTest } from "./harness/fourslash/runner"; -import { srcFolder } from "./harness/vfs/factory"; +import * as path from 'path'; +import { normalizeSlashes } from '../common/pathUtils'; +import { runFourSlashTest } from './harness/fourslash/runner'; +import * as host from './harness/host'; +import { srcFolder } from './harness/vfs/factory'; -describe("fourslash tests", () => { +describe('fourslash tests', () => { const testFiles: string[] = []; - const basePath = path.resolve(path.dirname(module.filename), "fourslash/"); - for (const file of host.Host.listFiles(basePath, /.*\.fourslash\.ts$/i, { recursive: true })) { + const basePath = path.resolve(path.dirname(module.filename), 'fourslash/'); + for (const file of host.HOST.listFiles(basePath, /.*\.fourslash\.ts$/i, { recursive: true })) { testFiles.push(file); } testFiles.forEach(file => { describe(file, () => { const fn = normalizeSlashes(file); - const justName = fn.replace(/^.*[\\/]/, ""); + const justName = fn.replace(/^.*[\\/]/, ''); - it("fourslash test " + justName + " runs correctly", () => { + it('fourslash test ' + justName + ' runs correctly', () => { runFourSlashTest(srcFolder, fn); }); }); }); -}); \ No newline at end of file +}); diff --git a/server/src/tests/harness/fourslash/fourSlashParser.ts b/server/src/tests/harness/fourslash/fourSlashParser.ts index 3d07f6b45add..5c07f133337d 100644 --- a/server/src/tests/harness/fourslash/fourSlashParser.ts +++ b/server/src/tests/harness/fourslash/fourSlashParser.ts @@ -2,13 +2,13 @@ * fourSlashParser.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * Parse fourslash markup code and return parsed content with marker/range data */ -import { contains } from "../../../common/collectionUtils"; -import { combinePaths, isRootedDiskPath, normalizeSlashes } from "../../../common/pathUtils"; -import { fileMetadataNames, FourSlashData, FourSlashFile, Marker, MetadataOptionNames, Range } from "./fourSlashTypes"; +import { contains } from '../../../common/collectionUtils'; +import { combinePaths, isRootedDiskPath, normalizeSlashes } from '../../../common/pathUtils'; +import { fileMetadataNames, FourSlashData, FourSlashFile, Marker, MetadataOptionNames, Range } from './fourSlashTypes'; /** * Parse given fourslash markup code and return content with markup/range data @@ -33,7 +33,7 @@ export function parseTestData(basePath: string, contents: string, fileName: stri // Split up the input file by line // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so // we have to string-based splitting instead and try to figure out the delimiting chars - const lines = contents.split("\n"); + const lines = contents.split('\n'); let i = 0; const markerPositions = new Map(); @@ -46,7 +46,7 @@ export function parseTestData(basePath: string, contents: string, fileName: stri let currentFileOptions: { [s: string]: string } = {}; function nextFile() { - if (currentFileContent === undefined) return; + if (currentFileContent === undefined) { return; } const file = parseFileContent(currentFileContent, currentFileName, markerPositions, markers, ranges); file.fileOptions = currentFileOptions; @@ -61,18 +61,16 @@ export function parseTestData(basePath: string, contents: string, fileName: stri for (let line of lines) { i++; - if (line.length > 0 && line.charAt(line.length - 1) === "\r") { + if (line.length > 0 && line.charAt(line.length - 1) === '\r') { line = line.substr(0, line.length - 1); } - if (line.substr(0, 4) === "////") { + if (line.substr(0, 4) === '////') { const text = line.substr(4); - currentFileContent = currentFileContent === undefined ? text : currentFileContent + "\n" + text; - } - else if (line.substr(0, 3) === "///" && currentFileContent !== undefined) { - throw new Error("Three-slash line in the middle of four-slash region at line " + i); - } - else if (line.substr(0, 2) === "//") { + currentFileContent = currentFileContent === undefined ? text : currentFileContent + '\n' + text; + } else if (line.substr(0, 3) === '///' && currentFileContent !== undefined) { + throw new Error(`Three-slash line in the middle of four-slash region at line ${ i }`); + } else if (line.substr(0, 2) === '//') { // Comment line, check for global/file @options and record them const match = optionRegex.exec(line.substr(2)); if (match) { @@ -84,15 +82,14 @@ export function parseTestData(basePath: string, contents: string, fileName: stri throw new Error(`Global option '${ key }' already exists`); } globalOptions[key] = value; - } - else { + } else { switch (key) { - case MetadataOptionNames.fileName: - { + case MetadataOptionNames.fileName: { // Found an @FileName directive, if this is not the first then create a new subfile nextFile(); const normalizedPath = normalizeSlashes(value); - currentFileName = isRootedDiskPath(normalizedPath) ? normalizedPath : combinePaths(normalizedBasePath, normalizedPath); + currentFileName = isRootedDiskPath(normalizedPath) ? normalizedPath : + combinePaths(normalizedBasePath, normalizedPath); currentFileOptions[key] = value; break; } @@ -102,10 +99,10 @@ export function parseTestData(basePath: string, contents: string, fileName: stri } } } - } - // Previously blank lines between fourslash content caused it to be considered as 2 files, - // Remove this behavior since it just causes errors now - else if (line !== "") { + } else if (line !== '') { + // Previously blank lines between fourslash content caused it to be considered as 2 files, + // Remove this behavior since it just causes errors now + // // Code line, terminate current subfile if there is one nextFile(); } @@ -138,22 +135,23 @@ const enum State { } function reportError(fileName: string, line: number, col: number, message: string) { - const errorMessage = fileName + "(" + line + "," + col + "): " + message; + const errorMessage = `${ fileName }(${ line },${ col }): ${ message }`; throw new Error(errorMessage); } -function recordObjectMarker(fileName: string, location: LocationInformation, text: string, markerMap: Map, markers: Marker[]): Marker | undefined { +function recordObjectMarker(fileName: string, location: LocationInformation, + text: string, markerMap: Map, markers: Marker[]): Marker | undefined { + let markerValue: any; try { // Attempt to parse the marker value as JSON - markerValue = JSON.parse("{ " + text + " }"); - } - catch (e) { - reportError(fileName, location.sourceLine, location.sourceColumn, "Unable to parse marker text " + e.message); + markerValue = JSON.parse('{ ' + text + ' }'); + } catch (e) { + reportError(fileName, location.sourceLine, location.sourceColumn, `Unable to parse marker text ${ e.message }`); } if (markerValue === undefined) { - reportError(fileName, location.sourceLine, location.sourceColumn, "Object markers can not be empty"); + reportError(fileName, location.sourceLine, location.sourceColumn, 'Object markers can not be empty'); return undefined; } @@ -173,7 +171,9 @@ function recordObjectMarker(fileName: string, location: LocationInformation, tex return marker; } -function recordMarker(fileName: string, location: LocationInformation, name: string, markerMap: Map, markers: Marker[]): Marker | undefined { +function recordMarker(fileName: string, location: LocationInformation, + name: string, markerMap: Map, markers: Marker[]): Marker | undefined { + const marker: Marker = { fileName, position: location.position @@ -181,25 +181,26 @@ function recordMarker(fileName: string, location: LocationInformation, name: str // Verify markers for uniqueness if (markerMap.has(name)) { - const message = "Marker '" + name + "' is duplicated in the source file contents."; + const message = 'Marker \'' + name + '\' is duplicated in the source file contents.'; reportError(marker.fileName, location.sourceLine, location.sourceColumn, message); return undefined; - } - else { + } else { markerMap.set(name, marker); markers.push(marker); return marker; } } -function parseFileContent(content: string, fileName: string, markerMap: Map, markers: Marker[], ranges: Range[]): FourSlashFile { +function parseFileContent(content: string, fileName: string, + markerMap: Map, markers: Marker[], ranges: Range[]): FourSlashFile { + content = chompLeadingSpace(content); // Any slash-star comment with a character not in this string is not a marker. - const validMarkerChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_"; + const validMarkerChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_'; /// The file content (minus metacharacters) so far - let output = ""; + let output = ''; /// The current marker (or maybe multi-line comment?) we're parsing, possibly let openMarker: LocationInformation | undefined; @@ -224,7 +225,8 @@ function parseFileContent(content: string, fileName: string, markerMap: Map { - output = output + content.substr(lastNormalCharPosition, lastSafeCharIndex === undefined ? undefined : lastSafeCharIndex - lastNormalCharPosition); + output = output + content.substr(lastNormalCharPosition, lastSafeCharIndex === undefined ? undefined + : lastSafeCharIndex - lastNormalCharPosition); }; if (content.length > 0) { @@ -233,31 +235,30 @@ function parseFileContent(content: string, fileName: string, markerMap: Map 0) { const openRange = openRanges[0]; - reportError(fileName, openRange.sourceLine, openRange.sourceColumn, "Unterminated range."); + reportError(fileName, openRange.sourceLine, openRange.sourceColumn, 'Unterminated range.'); } if (openMarker) { - reportError(fileName, openMarker.sourceLine, openMarker.sourceColumn, "Unterminated marker."); + reportError(fileName, openMarker.sourceLine, openMarker.sourceColumn, 'Unterminated marker.'); } // put ranges in the correct order localRanges = localRanges.sort((a, b) => a.pos < b.pos ? -1 : a.pos === b.pos && a.end > b.end ? -1 : 1); - localRanges.forEach((r) => { ranges.push(r); }); + localRanges.forEach(r => { ranges.push(r); }); return { content: output, fileOptions: {}, version: 0, - fileName, + fileName }; } function chompLeadingSpace(content: string) { - const lines = content.split("\n"); + const lines = content.split('\n'); for (const line of lines) { - if ((line.length !== 0) && (line.charAt(0) !== " ")) { + if ((line.length !== 0) && (line.charAt(0) !== ' ')) { return content; } } - return lines.map(s => s.substr(1)).join("\n"); -} \ No newline at end of file + return lines.map(s => s.substr(1)).join('\n'); +} diff --git a/server/src/tests/harness/fourslash/fourSlashTypes.ts b/server/src/tests/harness/fourslash/fourSlashTypes.ts index c247a119e16b..9b60dedb3e89 100644 --- a/server/src/tests/harness/fourslash/fourSlashTypes.ts +++ b/server/src/tests/harness/fourslash/fourSlashTypes.ts @@ -5,21 +5,21 @@ * * Various common types for fourslash test framework */ -import * as debug from "../../../common/debug"; +import * as debug from '../../../common/debug'; /** setting file name */ -export const pythonSettingFilename = "python.json"; +export const pythonSettingFilename = 'python.json'; /** well known global option names */ export const enum GlobalMetadataOptionNames { - projectRoot = "projectroot", - ignoreCase = "ignorecase" + projectRoot = 'projectroot', + ignoreCase = 'ignorecase' } /** Any option name not belong to this will become global option */ export const enum MetadataOptionNames { - fileName = "filename", - reserved = "reserved" + fileName = 'filename', + reserved = 'reserved' } /** List of allowed file metadata names */ @@ -102,7 +102,7 @@ export class TestCancellationToken implements HostCancellationToken { private static readonly notCanceled = -1; private numberOfCallsBeforeCancellation = TestCancellationToken.notCanceled; - public isCancellationRequested(): boolean { + isCancellationRequested(): boolean { if (this.numberOfCallsBeforeCancellation < 0) { return false; } @@ -115,12 +115,12 @@ export class TestCancellationToken implements HostCancellationToken { return true; } - public setCancelled(numberOfCalls = 0): void { + setCancelled(numberOfCalls = 0): void { debug.assert(numberOfCalls >= 0); this.numberOfCallsBeforeCancellation = numberOfCalls; } - public resetCancelled(): void { + resetCancelled(): void { this.numberOfCallsBeforeCancellation = TestCancellationToken.notCanceled; } -} \ No newline at end of file +} diff --git a/server/src/tests/harness/fourslash/runner.ts b/server/src/tests/harness/fourslash/runner.ts index 9781c4babfff..311c40c0c799 100644 --- a/server/src/tests/harness/fourslash/runner.ts +++ b/server/src/tests/harness/fourslash/runner.ts @@ -2,30 +2,30 @@ * runner.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * Provide APIs to run fourslash tests from provided fourslash markup contents */ -import * as ts from "typescript"; -import { combinePaths } from "../../../common/pathUtils"; -import * as host from "../host"; -import { parseTestData } from "./fourSlashParser"; -import { TestState } from "./testState"; +import * as ts from 'typescript'; +import { combinePaths } from '../../../common/pathUtils'; +import * as host from '../host'; +import { parseTestData } from './fourSlashParser'; +import { TestState } from './testState'; -/** +/** * run given fourslash test file - * + * * @param basePath this is used as a base path of the virtual file system the test will run upon * @param fileName this is the file path where fourslash test file will be read from */ export function runFourSlashTest(basePath: string, fileName: string) { - const content = (host.Host.readFile(fileName)!); + const content = (host.HOST.readFile(fileName)!); runFourSlashTestContent(basePath, fileName, content); } /** * run given fourslash markup content - * + * * @param basePath this is used as a base path of the virtual file system the test will run upon * @param fileName this will be used as a filename of the given `content` in the virtual file system * if fourslash markup `content` doesn't have explicit `@filename` option @@ -33,8 +33,8 @@ export function runFourSlashTest(basePath: string, fileName: string) { */ export function runFourSlashTestContent(basePath: string, fileName: string, content: string) { // give file paths an absolute path for the virtual file system - const absoluteBasePath = combinePaths("/", basePath); - const absoluteFileName = combinePaths("/", fileName); + const absoluteBasePath = combinePaths('/', basePath); + const absoluteFileName = combinePaths('/', fileName); // parse out the files and their metadata const testData = parseTestData(absoluteBasePath, content, absoluteFileName); @@ -54,6 +54,7 @@ function runCode(code: string, state: TestState): void { ${ code } })`; + // tslint:disable-next-line: no-eval const f = eval(wrappedCode); f(state); -} \ No newline at end of file +} diff --git a/server/src/tests/harness/fourslash/testState.ts b/server/src/tests/harness/fourslash/testState.ts index 5ed240419f0b..a992cb9e0d73 100644 --- a/server/src/tests/harness/fourslash/testState.ts +++ b/server/src/tests/harness/fourslash/testState.ts @@ -2,30 +2,31 @@ * testState.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * TestState wraps currently test states and provides a way to query and manipulate * the test states. */ -import * as path from "path"; -import * as assert from 'assert' -import Char from "typescript-char"; -import { ImportResolver } from "../../../analyzer/importResolver"; -import { Program } from "../../../analyzer/program"; -import { ConfigOptions } from "../../../common/configOptions"; -import { NullConsole } from "../../../common/console"; -import { Comparison, isNumber, isString } from "../../../common/core"; -import * as debug from "../../../common/debug"; -import { DiagnosticCategory } from "../../../common/diagnostic"; -import { combinePaths, comparePaths, getBaseFileName, normalizePath, normalizeSlashes } from "../../../common/pathUtils"; -import { convertOffsetToPosition, convertPositionToOffset } from "../../../common/positionUtils"; -import { getStringComparer } from "../../../common/stringUtils"; -import { Position, TextRange } from "../../../common/textRange"; -import * as host from "../host"; -import { createFromFileSystem } from "../vfs/factory"; -import * as vfs from "../vfs/filesystem"; -import { CompilerSettings, FourSlashData, FourSlashFile, GlobalMetadataOptionNames, Marker, MultiMap, pythonSettingFilename, Range, TestCancellationToken } from "./fourSlashTypes"; -import { stringify } from "../utils" +import * as assert from 'assert'; +import * as path from 'path'; +import Char from 'typescript-char'; +import { ImportResolver } from '../../../analyzer/importResolver'; +import { Program } from '../../../analyzer/program'; +import { ConfigOptions } from '../../../common/configOptions'; +import { NullConsole } from '../../../common/console'; +import { Comparison, isNumber, isString } from '../../../common/core'; +import * as debug from '../../../common/debug'; +import { DiagnosticCategory } from '../../../common/diagnostic'; +import { combinePaths, comparePaths, getBaseFileName, normalizePath, normalizeSlashes } from '../../../common/pathUtils'; +import { convertOffsetToPosition, convertPositionToOffset } from '../../../common/positionUtils'; +import { getStringComparer } from '../../../common/stringUtils'; +import { Position, TextRange } from '../../../common/textRange'; +import * as host from '../host'; +import { stringify } from '../utils'; +import { createFromFileSystem } from '../vfs/factory'; +import * as vfs from '../vfs/filesystem'; +import { CompilerSettings, FourSlashData, FourSlashFile, GlobalMetadataOptionNames, Marker, + MultiMap, pythonSettingFilename, Range, TestCancellationToken } from './fourSlashTypes'; export interface TextChange { span: TextRange; @@ -36,24 +37,24 @@ export class TestState { private readonly _cancellationToken: TestCancellationToken; private readonly _files: string[] = []; - public readonly fs: vfs.FileSystem; - public readonly importResolver: ImportResolver; - public readonly configOptions: ConfigOptions; - public readonly program: Program; + readonly fs: vfs.FileSystem; + readonly importResolver: ImportResolver; + readonly configOptions: ConfigOptions; + readonly program: Program; // The current caret position in the active file - public currentCaretPosition = 0; + currentCaretPosition = 0; // The position of the end of the current selection, or -1 if nothing is selected - public selectionEnd = -1; + selectionEnd = -1; - public lastKnownMarker = ""; + lastKnownMarker = ''; // The file that's currently 'opened' - public activeFile!: FourSlashFile; + activeFile!: FourSlashFile; constructor(private _basePath: string, public testData: FourSlashData) { const strIgnoreCase = GlobalMetadataOptionNames.ignoreCase; - const ignoreCase = testData.globalOptions[strIgnoreCase]?.toUpperCase() === "TRUE"; + const ignoreCase = testData.globalOptions[strIgnoreCase]?.toUpperCase() === 'TRUE'; this._cancellationToken = new TestCancellationToken(); const configOptions = this._convertGlobalOptionsToConfigOptions(this.testData.globalOptions); @@ -65,19 +66,17 @@ export class TestState { let configJson: any; try { configJson = JSON.parse(file.content); - } - catch (e) { + } catch (e) { throw new Error(`Failed to parse test ${ file.fileName }: ${ e.message }`); } configOptions.initializeFromJson(configJson, new NullConsole()); - } - else { - files[file.fileName] = new vfs.File(file.content, { meta: file.fileOptions, encoding: "utf8" }); + } else { + files[file.fileName] = new vfs.File(file.content, { meta: file.fileOptions, encoding: 'utf8' }); } } - const fs = createFromFileSystem(host.Host, ignoreCase, { cwd: _basePath, files: files, meta: testData.globalOptions }); + const fs = createFromFileSystem(host.HOST, ignoreCase, { cwd: _basePath, files, meta: testData.globalOptions }); // this should be change to AnalyzerService rather than Program const importResolver = new ImportResolver(fs, configOptions); @@ -93,15 +92,14 @@ export class TestState { this.program = program; this._files.push(...Object.keys(files)); - if (this._files.length > 0) - { + if (this._files.length > 0) { // Open the first file by default this.openFile(0); } } // Entry points from fourslash.ts - public goToMarker(nameOrMarker: string | Marker = "") { + goToMarker(nameOrMarker: string | Marker = '') { const marker = isString(nameOrMarker) ? this.getMarkerByName(nameOrMarker) : nameOrMarker; if (this.activeFile.fileName !== marker.fileName) { this.openFile(marker.fileName); @@ -117,7 +115,7 @@ export class TestState { this.goToPosition(marker.position); } - public goToEachMarker(markers: readonly Marker[], action: (marker: Marker, index: number) => void) { + goToEachMarker(markers: readonly Marker[], action: (marker: Marker, index: number) => void) { debug.assert(markers.length > 0); for (let i = 0; i < markers.length; i++) { this.goToMarker(markers[i]); @@ -125,8 +123,8 @@ export class TestState { } } - public getMarkerName(m: Marker): string { - let found: string | undefined = undefined; + getMarkerName(m: Marker): string { + let found: string | undefined; this.testData.markerPositions.forEach((marker, name) => { if (marker === m) { found = name; @@ -137,26 +135,25 @@ export class TestState { return found!; } - public getMarkerByName(markerName: string) { + getMarkerByName(markerName: string) { const markerPos = this.testData.markerPositions.get(markerName); if (markerPos === undefined) { - throw new Error(`Unknown marker "${ markerName }" Available markers: ${ this.getMarkerNames().map(m => "\"" + m + "\"").join(", ") }`); - } - else { + throw new Error(`Unknown marker "${ markerName }" Available markers: ${ this.getMarkerNames().map(m => '"' + m + '"').join(', ') }`); + } else { return markerPos; } } - public getMarkers(): Marker[] { + getMarkers(): Marker[] { // Return a copy of the list return this.testData.markers.slice(0); } - public getMarkerNames(): string[] { + getMarkerNames(): string[] { return [...this.testData.markerPositions.keys()]; } - public goToPosition(positionOrLineAndColumn: number | Position) { + goToPosition(positionOrLineAndColumn: number | Position) { const pos = isNumber(positionOrLineAndColumn) ? positionOrLineAndColumn : this._convertPositionToOffset(this.activeFile.fileName, positionOrLineAndColumn); @@ -164,8 +161,10 @@ export class TestState { this.selectionEnd = -1; } - public select(startMarker: string, endMarker: string) { - const start = this.getMarkerByName(startMarker), end = this.getMarkerByName(endMarker); + select(startMarker: string, endMarker: string) { + const start = this.getMarkerByName(startMarker); + const end = this.getMarkerByName(endMarker); + debug.assert(start.fileName === end.fileName); if (this.activeFile.fileName !== start.fileName) { this.openFile(start.fileName); @@ -174,24 +173,24 @@ export class TestState { this.selectionEnd = end.position; } - public selectAllInFile(fileName: string) { + selectAllInFile(fileName: string) { this.openFile(fileName); this.goToPosition(0); this.selectionEnd = this.activeFile.content.length; } - public selectRange(range: Range): void { + selectRange(range: Range): void { this.goToRangeStart(range); this.selectionEnd = range.end; } - public selectLine(index: number) { + selectLine(index: number) { const lineStart = this._convertPositionToOffset(this.activeFile.fileName, { line: index, character: 0 }); const lineEnd = lineStart + this._getLineContent(index).length; this.selectRange({ fileName: this.activeFile.fileName, pos: lineStart, end: lineEnd }); } - public goToEachRange(action: (range: Range) => void) { + goToEachRange(action: (range: Range) => void) { const ranges = this.getRanges(); debug.assert(ranges.length > 0); for (const range of ranges) { @@ -200,44 +199,44 @@ export class TestState { } } - public goToRangeStart({ fileName, pos }: Range) { + goToRangeStart({ fileName, pos }: Range) { this.openFile(fileName); this.goToPosition(pos); } - public getRanges(): Range[] { + getRanges(): Range[] { return this.testData.ranges; } - public getRangesInFile(fileName = this.activeFile.fileName) { + getRangesInFile(fileName = this.activeFile.fileName) { return this.getRanges().filter(r => r.fileName === fileName); } - public getRangesByText(): Map { - if (this.testData.rangesByText) return this.testData.rangesByText; + getRangesByText(): Map { + if (this.testData.rangesByText) { return this.testData.rangesByText; } const result = this._createMultiMap(this.getRanges(), r => this._rangeText(r)); this.testData.rangesByText = result; return result; } - public goToBOF() { + goToBOF() { this.goToPosition(0); } - public goToEOF() { + goToEOF() { const len = this._getFileContent(this.activeFile.fileName).length; this.goToPosition(len); } - public moveCaretRight(count = 1) { + moveCaretRight(count = 1) { this.currentCaretPosition += count; this.currentCaretPosition = Math.min(this.currentCaretPosition, this._getFileContent(this.activeFile.fileName).length); this.selectionEnd = -1; } // Opens a file given its 0-based index or fileName - public openFile(indexOrName: number | string, content?: string): void { + openFile(indexOrName: number | string, content?: string): void { const fileToOpen: FourSlashFile = this._findFile(indexOrName); fileToOpen.fileName = normalizeSlashes(fileToOpen.fileName); this.activeFile = fileToOpen; @@ -246,24 +245,25 @@ export class TestState { // this.languageServiceAdapterHost.openFile(fileToOpen.fileName, content); } - public printCurrentFileState(showWhitespace: boolean, makeCaretVisible: boolean) { + printCurrentFileState(showWhitespace: boolean, makeCaretVisible: boolean) { for (const file of this.testData.files) { const active = (this.activeFile === file); - host.Host.log(`=== Script (${ file.fileName }) ${ (active ? "(active, cursor at |)" : "") } ===`); + host.HOST.log(`=== Script (${ file.fileName }) ${ (active ? '(active, cursor at |)' : '') } ===`); let content = this._getFileContent(file.fileName); if (active) { - content = content.substr(0, this.currentCaretPosition) + (makeCaretVisible ? "|" : "") + content.substr(this.currentCaretPosition); + content = content.substr(0, this.currentCaretPosition) + + (makeCaretVisible ? '|' : '') + content.substr(this.currentCaretPosition); } if (showWhitespace) { content = this._makeWhitespaceVisible(content); } - host.Host.log(content); + host.HOST.log(content); } } - public deleteChar(count = 1) { + deleteChar(count = 1) { const offset = this.currentCaretPosition; - const ch = ""; + const ch = ''; const checkCadence = (count >> 2) + 1; @@ -278,20 +278,20 @@ export class TestState { this._checkPostEditInvariants(); } - public replace(start: number, length: number, text: string) { + replace(start: number, length: number, text: string) { this._editScriptAndUpdateMarkers(this.activeFile.fileName, start, start + length, text); this._checkPostEditInvariants(); } - public deleteLineRange(startIndex: number, endIndexInclusive: number) { + deleteLineRange(startIndex: number, endIndexInclusive: number) { const startPos = this._convertPositionToOffset(this.activeFile.fileName, { line: startIndex, character: 0 }); const endPos = this._convertPositionToOffset(this.activeFile.fileName, { line: endIndexInclusive + 1, character: 0 }); - this.replace(startPos, endPos - startPos, ""); + this.replace(startPos, endPos - startPos, ''); } - public deleteCharBehindMarker(count = 1) { + deleteCharBehindMarker(count = 1) { let offset = this.currentCaretPosition; - const ch = ""; + const ch = ''; const checkCadence = (count >> 2) + 1; for (let i = 0; i < count; i++) { @@ -310,10 +310,10 @@ export class TestState { } // Enters lines of text at the current caret position - public type(text: string) { + type(text: string) { let offset = this.currentCaretPosition; const selection = this._getSelection(); - this.replace(selection.start, selection.length, ""); + this.replace(selection.start, selection.length, ''); for (let i = 0; i < text.length; i++) { const ch = text.charAt(i); @@ -327,12 +327,12 @@ export class TestState { } // Enters text as if the user had pasted it - public paste(text: string) { + paste(text: string) { this._editScriptAndUpdateMarkers(this.activeFile.fileName, this.currentCaretPosition, this.currentCaretPosition, text); this._checkPostEditInvariants(); } - public verifyDiagnostics(map?: { [marker: string]: { category: string; message: string } }): void { + verifyDiagnostics(map?: { [marker: string]: { category: string; message: string } }): void { while (this.program.analyze()) { // Continue to call analyze until it completes. Since we're not // specifying a timeout, it should complete the first time. @@ -344,7 +344,7 @@ export class TestState { const diagnostics = sourceFile.getDiagnostics(this.configOptions) || []; const filePath = sourceFile.getFilePath(); const value = { - filePath: filePath, + filePath, parseResults: sourceFile.getParseResults(), errors: diagnostics.filter(diag => diag.category === DiagnosticCategory.Error), warnings: diagnostics.filter(diag => diag.category === DiagnosticCategory.Warning) @@ -360,7 +360,7 @@ export class TestState { const rangePerFile = this._createMultiMap(this.getRanges(), r => r.fileName); // expected number of files - if (resultPerFile.size != rangePerFile.size) { + if (resultPerFile.size !== rangePerFile.size) { this._raiseError(`actual and expected doesn't match - expected: ${ stringify(rangePerFile) }, actual: ${ stringify(rangePerFile) }`); } @@ -377,7 +377,7 @@ export class TestState { const result = resultPerFile.get(file)!; for (const [category, expected] of rangesPerCategory.entries()) { const lines = result.parseResults!.tokenizerOutput.lines; - const actual = category === "error" ? result.errors : category === "warning" ? result.warnings : this._raiseError(`unexpected category ${ category }`); + const actual = category === 'error' ? result.errors : category === 'warning' ? result.warnings : this._raiseError(`unexpected category ${ category }`); if (expected.length !== actual.length) { this._raiseError(`contains unexpected result - expected: ${ stringify(expected) }, actual: ${ actual }`); @@ -399,7 +399,7 @@ export class TestState { const name = this.getMarkerName(range.marker!); const message = map[name].message; - if (matches.filter(d => message == d.message).length !== 1) { + if (matches.filter(d => message === d.message).length !== 1) { this._raiseError(`message doesn't match: ${ message } of ${ name } - ${ stringify(range) }, actual: ${ stringify(matches) }`); } } @@ -408,7 +408,7 @@ export class TestState { } } - public verifyCaretAtMarker(markerName = "") { + verifyCaretAtMarker(markerName = '') { const pos = this.getMarkerByName(markerName); if (pos.fileName !== this.activeFile.fileName) { throw new Error(`verifyCaretAtMarker failed - expected to be in file "${ pos.fileName }", but was in file "${ this.activeFile.fileName }"`); @@ -418,44 +418,45 @@ export class TestState { } } - public verifyCurrentLineContent(text: string) { + verifyCurrentLineContent(text: string) { const actual = this._getCurrentLineContent(); if (actual !== text) { - throw new Error("verifyCurrentLineContent\n" + this._displayExpectedAndActualString(text, actual, /* quoted */ true)); + throw new Error('verifyCurrentLineContent\n' + this._displayExpectedAndActualString(text, actual, /* quoted */ true)); } } - public verifyCurrentFileContent(text: string) { + verifyCurrentFileContent(text: string) { this._verifyFileContent(this.activeFile.fileName, text); } - public verifyTextAtCaretIs(text: string) { - const actual = this._getFileContent(this.activeFile.fileName).substring(this.currentCaretPosition, this.currentCaretPosition + text.length); + verifyTextAtCaretIs(text: string) { + const actual = this._getFileContent(this.activeFile.fileName) + .substring(this.currentCaretPosition, this.currentCaretPosition + text.length); if (actual !== text) { - throw new Error("verifyTextAtCaretIs\n" + this._displayExpectedAndActualString(text, actual, /* quoted */ true)); + throw new Error('verifyTextAtCaretIs\n' + this._displayExpectedAndActualString(text, actual, /* quoted */ true)); } } - public verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean) { + verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean) { this._verifyTextMatches(this._rangeText(this._getOnlyRange()), !!includeWhiteSpace, expectedText); } - public setCancelled(numberOfCalls: number): void { + setCancelled(numberOfCalls: number): void { this._cancellationToken.setCancelled(numberOfCalls); } - public resetCancelled(): void { + resetCancelled(): void { this._cancellationToken.resetCancelled(); } private _isConfig(file: FourSlashFile, ignoreCase: boolean): boolean { const comparer = getStringComparer(ignoreCase); - return comparer(getBaseFileName(file.fileName), pythonSettingFilename) == Comparison.EqualTo; + return comparer(getBaseFileName(file.fileName), pythonSettingFilename) === Comparison.EqualTo; } private _convertGlobalOptionsToConfigOptions(globalOptions: CompilerSettings): ConfigOptions { const srtRoot: string = GlobalMetadataOptionNames.projectRoot; - const projectRoot = normalizeSlashes(globalOptions[srtRoot] ?? "."); + const projectRoot = normalizeSlashes(globalOptions[srtRoot] ?? '.'); const configOptions = new ConfigOptions(projectRoot); // add more global options as we need them @@ -492,7 +493,8 @@ export class TestState { } private _messageAtLastKnownMarker(message: string) { - const locationDescription = this.lastKnownMarker ? this.lastKnownMarker : this._getLineColStringAtPosition(this.currentCaretPosition); + const locationDescription = this.lastKnownMarker ? this.lastKnownMarker + : this._getLineColStringAtPosition(this.currentCaretPosition); return `At ${ locationDescription }: ${ message }`; } @@ -518,7 +520,7 @@ export class TestState { } private _removeWhitespace(text: string): string { - return text.replace(/\s/g, ""); + return text.replace(/\s/g, ''); } private _createMultiMap(values?: T[], getKey?: (t: T) => string): MultiMap { @@ -538,8 +540,7 @@ export class TestState { let values = this.get(key); if (values) { values.push(value); - } - else { + } else { this.set(key, values = [value]); } return values; @@ -548,7 +549,7 @@ export class TestState { function multiMapRemove(this: MultiMap, key: string, value: T) { const values = this.get(key); if (values) { - values.forEach((v, i, arr) => { if (v === value) arr.splice(i, 1) }); + values.forEach((v, i, arr) => { if (v === value) { arr.splice(i, 1); } }); if (!values.length) { this.delete(key); } @@ -563,7 +564,7 @@ export class TestState { private _getOnlyRange() { const ranges = this.getRanges(); if (ranges.length !== 1) { - this._raiseError("Exactly one range should be specified in the testfile."); + this._raiseError('Exactly one range should be specified in the testfile.'); } return ranges[0]; @@ -590,7 +591,8 @@ export class TestState { private _getLineContent(index: number) { const text = this._getFileContent(this.activeFile.fileName); const pos = this._convertPositionToOffset(this.activeFile.fileName, { line: index, character: 0 }); - let startPos = pos, endPos = pos; + let startPos = pos; + let endPos = pos; while (startPos > 0) { const ch = text.charCodeAt(startPos - 1); @@ -618,28 +620,25 @@ export class TestState { private _getCurrentLineContent() { return this._getLineContent(this._convertOffsetToPosition( this.activeFile.fileName, - this.currentCaretPosition, + this.currentCaretPosition ).line); } private _findFile(indexOrName: string | number): FourSlashFile { - if (typeof indexOrName === "number") { + if (typeof indexOrName === 'number') { const index = indexOrName; if (index >= this.testData.files.length) { throw new Error(`File index (${ index }) in openFile was out of range. There are only ${ this.testData.files.length } files in this test.`); - } - else { + } else { return this.testData.files[index]; } - } - else if (isString(indexOrName)) { + } else if (isString(indexOrName)) { const { file, availableNames } = this._tryFindFileWorker(indexOrName); if (!file) { - throw new Error(`No test file named "${ indexOrName }" exists. Available file names are: ${ availableNames.join(", ") }`); + throw new Error(`No test file named "${ indexOrName }" exists. Available file names are: ${ availableNames.join(', ') }`); } return file; - } - else { + } else { return debug.assertNever(indexOrName); } } @@ -650,7 +649,7 @@ export class TestState { // names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName name = name.indexOf(path.sep) === -1 ? combinePaths(this._basePath, name) : name; - let file: FourSlashFile | undefined = undefined; + let file: FourSlashFile | undefined; const availableNames: string[] = []; this.testData.files.forEach(f => { const fn = normalizePath(f.fileName); @@ -686,15 +685,15 @@ export class TestState { } private _displayExpectedAndActualString(expected: string, actual: string, quoted = false) { - const expectMsg = "\x1b[1mExpected\x1b[0m\x1b[31m"; - const actualMsg = "\x1b[1mActual\x1b[0m\x1b[31m"; - const expectedString = quoted ? "\"" + expected + "\"" : expected; - const actualString = quoted ? "\"" + actual + "\"" : actual; + const expectMsg = '\x1b[1mExpected\x1b[0m\x1b[31m'; + const actualMsg = '\x1b[1mActual\x1b[0m\x1b[31m'; + const expectedString = quoted ? '"' + expected + '"' : expected; + const actualString = quoted ? '"' + actual + '"' : actual; return `\n${ expectMsg }:\n${ expectedString }\n\n${ actualMsg }:\n${ actualString }`; } private _makeWhitespaceVisible(text: string) { - return text.replace(/ /g, "\u00B7").replace(/\r/g, "\u00B6").replace(/\n/g, "\u2193\n").replace(/\t/g, "\u2192 "); + return text.replace(/ /g, '\u00B7').replace(/\r/g, '\u00B6').replace(/\n/g, '\u2193\n').replace(/\t/g, '\u2192 '); } private _updatePosition(position: number, editStart: number, editEnd: number, { length }: string): number { diff --git a/server/src/tests/harness/host.ts b/server/src/tests/harness/host.ts index 324deeebc6ef..17ea25772ef6 100644 --- a/server/src/tests/harness/host.ts +++ b/server/src/tests/harness/host.ts @@ -4,15 +4,15 @@ * Licensed under the MIT license. */ -import * as pathModule from "path"; -import * as os from "os"; +import * as os from 'os'; +import * as pathModule from 'path'; -import { compareStringsCaseSensitive, compareStringsCaseInsensitive } from "../../common/stringUtils"; -import { directoryExists, FileSystemEntries, combinePaths, fileExists, getFileSize, resolvePaths } from "../../common/pathUtils"; -import { createFromRealFileSystem } from '../../common/vfs'; import { NullConsole } from '../../common/console'; +import { combinePaths, directoryExists, fileExists, FileSystemEntries, getFileSize, resolvePaths } from '../../common/pathUtils'; +import { compareStringsCaseInsensitive, compareStringsCaseSensitive } from '../../common/stringUtils'; +import { createFromRealFileSystem } from '../../common/vfs'; -export const Host: TestHost = createHost(); +export const HOST: TestHost = createHost(); // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface TestHost { @@ -35,7 +35,7 @@ function createHost(): TestHost { // NodeJS detects "\uFEFF" at the start of the string and *replaces* it with the actual // byte order mark from the specified encoding. Using any other byte order mark does // not actually work. - const byteOrderMarkIndicator = "\uFEFF"; + const byteOrderMarkIndicator = '\uFEFF'; const vfs = createFromRealFileSystem(new NullConsole()); const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); @@ -43,7 +43,7 @@ function createHost(): TestHost { function isFileSystemCaseSensitive(): boolean { // win32\win64 are case insensitive platforms const platform = os.platform(); - if (platform === "win32") { + if (platform === 'win32') { return false; } // If this file exists under a different case, we must be case-insensitve. @@ -51,7 +51,7 @@ function createHost(): TestHost { /** Convert all lowercase chars to uppercase, and vice-versa */ function swapCase(s: string): string { - return s.replace(/\w/g, (ch) => { + return s.replace(/\w/g, ch => { const up = ch.toUpperCase(); return ch === up ? ch.toLowerCase() : up; }); @@ -67,8 +67,7 @@ function createHost(): TestHost { const stat = vfs.statSync(pathToFile); if (options.recursive && stat.isDirectory()) { paths = paths.concat(filesInFolder(pathToFile)); - } - else if (stat.isFile() && (!spec || file.match(spec))) { + } else if (stat.isFile() && (!spec || file.match(spec))) { paths.push(pathToFile); } } @@ -81,27 +80,25 @@ function createHost(): TestHost { function getAccessibleFileSystemEntries(dirname: string): FileSystemEntries { try { - const entries: string[] = vfs.readdirSync(dirname || ".").sort(useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive); + const entries: string[] = vfs.readdirSync(dirname || '.').sort( + useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive); const files: string[] = []; const directories: string[] = []; for (const entry of entries) { - if (entry === "." || entry === "..") continue; + if (entry === '.' || entry === '..') { continue; } const name = combinePaths(dirname, entry); try { const stat = vfs.statSync(name); - if (!stat) continue; + if (!stat) { continue; } if (stat.isFile()) { files.push(entry); - } - else if (stat.isDirectory()) { + } else if (stat.isDirectory()) { directories.push(entry); } - } - catch { /*ignore*/ } + } catch { /*ignore*/ } } return { files, directories }; - } - catch (e) { + } catch (e) { return { files: [], directories: [] }; } } @@ -121,18 +118,18 @@ function createHost(): TestHost { buffer[i] = buffer[i + 1]; buffer[i + 1] = temp; } - return buffer.toString("utf16le", 2); + return buffer.toString('utf16le', 2); } if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { // Little endian UTF-16 byte order mark detected - return buffer.toString("utf16le", 2); + return buffer.toString('utf16le', 2); } if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { // UTF-8 byte order mark detected - return buffer.toString("utf8", 3); + return buffer.toString('utf8', 3); } // Default is UTF-8 with no byte order mark - return buffer.toString("utf8"); + return buffer.toString('utf8'); } function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { @@ -141,19 +138,19 @@ function createHost(): TestHost { data = byteOrderMarkIndicator + data; } - vfs.writeFileSync(fileName, data, "utf8"); + vfs.writeFileSync(fileName, data, 'utf8'); } return { useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getFileSize: (path: string) => getFileSize(vfs, path), readFile: path => readFile(path), - writeFile: (path, content) => writeFile(path, content), + writeFile: (path, content) => { writeFile(path, content); }, fileExists: path => fileExists(vfs, path), directoryExists: path => directoryExists(vfs, path), listFiles, - log: s => console.log(s), - getWorkspaceRoot: () => resolvePaths(__dirname, "../../.."), - getAccessibleFileSystemEntries, + log: s => { console.log(s); }, + getWorkspaceRoot: () => resolvePaths(__dirname, '../../..'), + getAccessibleFileSystemEntries }; -} \ No newline at end of file +} diff --git a/server/src/tests/harness/utils.ts b/server/src/tests/harness/utils.ts index 1b9f7ae526f7..2e1d65108614 100644 --- a/server/src/tests/harness/utils.ts +++ b/server/src/tests/harness/utils.ts @@ -4,12 +4,12 @@ * Licensed under the MIT license. */ -import { binarySearch, insertAt } from "../../common/collectionUtils"; -import { identity } from "../../common/core"; +import { binarySearch, insertAt } from '../../common/collectionUtils'; +import { identity } from '../../common/core'; export interface SortOptions { comparer: (a: T, b: T) => number; - sort: "insertion" | "comparison"; + sort: 'insertion' | 'comparison'; } export class SortedMap { @@ -21,8 +21,8 @@ export class SortedMap { private _copyOnWrite = false; constructor(comparer: ((a: K, b: K) => number) | SortOptions, iterable?: Iterable<[K, V]>) { - this._comparer = typeof comparer === "object" ? comparer.comparer : comparer; - this._order = typeof comparer === "object" && comparer.sort === "insertion" ? [] : undefined; + this._comparer = typeof comparer === 'object' ? comparer.comparer : comparer; + this._order = typeof comparer === 'object' && comparer.sort === 'insertion' ? [] : undefined; if (iterable) { const iterator = getIterator(iterable); try { @@ -30,76 +30,74 @@ export class SortedMap { const [key, value] = i.value; this.set(key, value); } - } - finally { + } finally { closeIterator(iterator); } } } - public get size() { + get size() { return this._keys.length; } - public get comparer() { + get comparer() { return this._comparer; } - public get [Symbol.toStringTag]() { - return "SortedMap"; + get [Symbol.toStringTag]() { + return 'SortedMap'; } - public has(key: K) { + has(key: K) { return binarySearch(this._keys, key, identity, this._comparer) >= 0; } - public get(key: K) { + get(key: K) { const index = binarySearch(this._keys, key, identity, this._comparer); return index >= 0 ? this._values[index] : undefined; } - public set(key: K, value: V) { + set(key: K, value: V) { const index = binarySearch(this._keys, key, identity, this._comparer); if (index >= 0) { this._values[index] = value; - } - else { - this.writePreamble(); + } else { + this._writePreamble(); insertAt(this._keys, ~index, key); insertAt(this._values, ~index, value); - if (this._order) insertAt(this._order, ~index, this._version); - this.writePostScript(); + if (this._order) { insertAt(this._order, ~index, this._version); } + this._writePostScript(); } return this; } - public delete(key: K) { + delete(key: K) { const index = binarySearch(this._keys, key, identity, this._comparer); if (index >= 0) { - this.writePreamble(); - this.orderedRemoveItemAt(this._keys, index); - this.orderedRemoveItemAt(this._values, index); - if (this._order) this.orderedRemoveItemAt(this._order, index); - this.writePostScript(); + this._writePreamble(); + this._orderedRemoveItemAt(this._keys, index); + this._orderedRemoveItemAt(this._values, index); + if (this._order) { this._orderedRemoveItemAt(this._order, index); } + this._writePostScript(); return true; } return false; } - public clear() { + clear() { if (this.size > 0) { - this.writePreamble(); + this._writePreamble(); this._keys.length = 0; this._values.length = 0; - if (this._order) this._order.length = 0; - this.writePostScript(); + if (this._order) { this._order.length = 0; } + this._writePostScript(); } } - public forEach(callback: (value: V, key: K, collection: this) => void, thisArg?: any) { + forEach(callback: (value: V, key: K, collection: this) => void, thisArg?: any) { const keys = this._keys; const values = this._values; - const indices = this.getIterationOrder(); + const indices = this._getIterationOrder(); const version = this._version; this._copyOnWrite = true; try { @@ -107,23 +105,21 @@ export class SortedMap { for (const i of indices) { callback.call(thisArg, values[i], keys[i], this); } - } - else { + } else { for (let i = 0; i < keys.length; i++) { callback.call(thisArg, values[i], keys[i], this); } } - } - finally { + } finally { if (version === this._version) { this._copyOnWrite = false; } } } - public * keys() { + * keys() { const keys = this._keys; - const indices = this.getIterationOrder(); + const indices = this._getIterationOrder(); const version = this._version; this._copyOnWrite = true; try { @@ -131,21 +127,19 @@ export class SortedMap { for (const i of indices) { yield keys[i]; } - } - else { + } else { yield* keys; } - } - finally { + } finally { if (version === this._version) { this._copyOnWrite = false; } } } - public * values() { + * values() { const values = this._values; - const indices = this.getIterationOrder(); + const indices = this._getIterationOrder(); const version = this._version; this._copyOnWrite = true; try { @@ -153,22 +147,20 @@ export class SortedMap { for (const i of indices) { yield values[i]; } - } - else { + } else { yield* values; } - } - finally { + } finally { if (version === this._version) { this._copyOnWrite = false; } } } - public * entries() { + * entries() { const keys = this._keys; const values = this._values; - const indices = this.getIterationOrder(); + const indices = this._getIterationOrder(); const version = this._version; this._copyOnWrite = true; try { @@ -176,38 +168,36 @@ export class SortedMap { for (const i of indices) { yield [keys[i], values[i]] as [K, V]; } - } - else { + } else { for (let i = 0; i < keys.length; i++) { yield [keys[i], values[i]] as [K, V]; } } - } - finally { + } finally { if (version === this._version) { this._copyOnWrite = false; } } } - public [Symbol.iterator]() { + [Symbol.iterator]() { return this.entries(); } - private writePreamble() { + private _writePreamble() { if (this._copyOnWrite) { this._keys = this._keys.slice(); this._values = this._values.slice(); - if (this._order) this._order = this._order.slice(); + if (this._order) { this._order = this._order.slice(); } this._copyOnWrite = false; } } - private writePostScript() { + private _writePostScript() { this._version++; } - private getIterationOrder() { + private _getIterationOrder() { if (this._order) { const order = this._order; return this._order @@ -218,7 +208,7 @@ export class SortedMap { } /** Remove an item by index from an array, moving everything to its right one space left. */ - private orderedRemoveItemAt(array: T[], index: number): void { + private _orderedRemoveItemAt(array: T[], index: number): void { // This seems to be faster than either `array.splice(i, 1)` or `array.copyWithin(i, i+ 1)`. for (let i = index; i < array.length - 1; i++) { array[i] = array[i + 1]; @@ -238,7 +228,7 @@ export function nextResult(iterator: Iterator): IteratorResult | undefi export function closeIterator(iterator: Iterator) { const fn = iterator.return; - if (typeof fn === "function") fn.call(iterator); + if (typeof fn === 'function') { fn.call(iterator); } } /** @@ -257,10 +247,10 @@ export class Metadata { this._map = Object.create(parent ? parent._map : null); } - public get size(): number { + get size(): number { if (this._size === -1 || (this._parent && this._parent._version !== this._parentVersion)) { let size = 0; - for (const _ in this._map) size++; + for (const _ of Object.keys(this._map)) { size++; } this._size = size; if (this._parent) { this._parentVersion = this._parent._version; @@ -269,27 +259,27 @@ export class Metadata { return this._size; } - public get parent() { + get parent() { return this._parent; } - public has(key: string): boolean { + has(key: string): boolean { return this._map[Metadata._escapeKey(key)] !== undefined; } - public get(key: string): any { + get(key: string): any { const value = this._map[Metadata._escapeKey(key)]; return value === Metadata._undefinedValue ? undefined : value; } - public set(key: string, value: any): this { + set(key: string, value: any): this { this._map[Metadata._escapeKey(key)] = value === undefined ? Metadata._undefinedValue : value; this._size = -1; this._version++; return this; } - public delete(key: string): boolean { + delete(key: string): boolean { const escapedKey = Metadata._escapeKey(key); if (this._map[escapedKey] !== undefined) { delete this._map[escapedKey]; @@ -300,24 +290,24 @@ export class Metadata { return false; } - public clear(): void { + clear(): void { this._map = Object.create(this._parent ? this._parent._map : null); this._size = -1; this._version++; } - public forEach(callback: (value: any, key: string, map: this) => void) { - for (const key in this._map) { + forEach(callback: (value: any, key: string, map: this) => void) { + for (const key of Object.keys(this._map)) { callback(this._map[key], Metadata._unescapeKey(key), this); } } private static _escapeKey(text: string) { - return (text.length >= 2 && text.charAt(0) === "_" && text.charAt(1) === "_" ? "_" + text : text); + return (text.length >= 2 && text.charAt(0) === '_' && text.charAt(1) === '_' ? '_' + text : text); } private static _unescapeKey(text: string) { - return (text.length >= 3 && text.charAt(0) === "_" && text.charAt(1) === "_" && text.charAt(2) === "_" ? text.slice(1) : text); + return (text.length >= 3 && text.charAt(0) === '_' && text.charAt(1) === '_' && text.charAt(2) === '_' ? text.slice(1) : text); } } @@ -327,28 +317,28 @@ export function bufferFrom(input: string, encoding?: BufferEncoding): Buffer { ? Buffer.from(input, encoding) : new Buffer(input, encoding); } -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 const IO_ERROR_MESSAGE = 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 }`); +export function createIOError(code: keyof typeof IO_ERROR_MESSAGE, details = '') { + const err: NodeJS.ErrnoException = new Error(`${ code }: ${ IO_ERROR_MESSAGE[code] } ${ details }`); err.code = code; - if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); + if (Error.captureStackTrace) { Error.captureStackTrace(err, createIOError); } return err; } export function stringify(data: any, replacer?: (key: string, value: any) => any): string { return JSON.stringify(data, replacer, 2); -} \ No newline at end of file +} diff --git a/server/src/tests/harness/vfs/factory.ts b/server/src/tests/harness/vfs/factory.ts index 4566337c3100..41dde0e1ed33 100644 --- a/server/src/tests/harness/vfs/factory.ts +++ b/server/src/tests/harness/vfs/factory.ts @@ -6,15 +6,15 @@ * Provides a factory to create virtual file system backed by a real file system with some path remapped */ -import { combinePaths, getDirectoryPath, normalizeSlashes, resolvePaths } from "../../../common/pathUtils"; -import { TestHost } from "../host"; -import { bufferFrom } from "../utils"; -import { FileSystem, FileSystemOptions, FileSystemResolver, ModulePath, Mount, S_IFDIR, S_IFREG } from "./filesystem"; +import { combinePaths, getDirectoryPath, normalizeSlashes, resolvePaths } from '../../../common/pathUtils'; +import { TestHost } from '../host'; +import { bufferFrom } from '../utils'; +import { FileSystem, FileSystemOptions, FileSystemResolver, MODULE_PATH, Mount, S_IFDIR, S_IFREG } from './filesystem'; export class TextDocument { - public readonly meta: Map; - public readonly file: string; - public readonly text: string; + readonly meta: Map; + readonly file: string; + readonly text: string; constructor(file: string, text: string, meta?: Map) { this.file = file; @@ -28,8 +28,8 @@ export interface FileSystemCreateOptions extends FileSystemOptions { documents?: readonly TextDocument[]; } -export const typeshedFolder = combinePaths(ModulePath, normalizeSlashes("typeshed-fallback")); -export const srcFolder = normalizeSlashes("/.src"); +export const typeshedFolder = combinePaths(MODULE_PATH, normalizeSlashes('typeshed-fallback')); +export const srcFolder = normalizeSlashes('/.src'); /** * Create a virtual file system from a physical file system using the following path mappings: @@ -44,10 +44,12 @@ export const srcFolder = normalizeSlashes("/.src"); * @param cwd initial current working directory in this virtual file system * @param time initial time in this virtual file system * @param meta initial metadata in this virtual file system - * + * * all `FileSystemCreateOptions` are optional */ -export function createFromFileSystem(host: TestHost, ignoreCase: boolean, { documents, files, cwd, time, meta }: FileSystemCreateOptions = {}) { +export function createFromFileSystem(host: TestHost, ignoreCase: boolean, + { documents, files, cwd, time, meta }: FileSystemCreateOptions = {}) { + const fs = getBuiltLocal(host, meta ? meta[typeshedFolder] : undefined, ignoreCase).shadow(); if (meta) { for (const key of Object.keys(meta)) { @@ -64,12 +66,12 @@ export function createFromFileSystem(host: TestHost, ignoreCase: boolean, { docu if (documents) { for (const document of documents) { fs.mkdirpSync(getDirectoryPath(document.file)); - fs.writeFileSync(document.file, document.text, "utf8"); - fs.filemeta(document.file).set("document", document); + fs.writeFileSync(document.file, document.text, 'utf8'); + fs.filemeta(document.file).set('document', document); // Add symlinks - const symlink = document.meta.get("symlink"); + const symlink = document.meta.get('symlink'); if (symlink) { - for (const link of symlink.split(",").map(link => link.trim())) { + for (const link of symlink.split(',').map(link => link.trim())) { fs.mkdirpSync(getDirectoryPath(link)); fs.symlinkSync(resolvePaths(fs.cwd(), document.file), link); } @@ -87,14 +89,14 @@ let localCIFSCache: FileSystem | undefined; let localCSFSCache: FileSystem | undefined; function getBuiltLocal(host: TestHost, typeshedFolderPath: string | undefined, ignoreCase: boolean): FileSystem { - if (cacheKey?.host !== host || cacheKey.typeshedFolderPath != typeshedFolderPath) { + if (cacheKey?.host !== host || cacheKey.typeshedFolderPath !== typeshedFolderPath) { localCIFSCache = undefined; localCSFSCache = undefined; cacheKey = { host, typeshedFolderPath }; } if (!localCIFSCache) { const resolver = createResolver(host); - typeshedFolderPath = typeshedFolderPath ?? resolvePaths(host.getWorkspaceRoot(), "../client/typeshed-fallback"); + typeshedFolderPath = typeshedFolderPath ?? resolvePaths(host.getWorkspaceRoot(), '../client/typeshed-fallback'); localCIFSCache = new FileSystem(/*ignoreCase*/ true, { files: { [typeshedFolder]: new Mount(typeshedFolderPath, resolver), @@ -106,7 +108,7 @@ function getBuiltLocal(host: TestHost, typeshedFolderPath: string | undefined, i localCIFSCache.makeReadonly(); } - if (ignoreCase) return localCIFSCache; + if (ignoreCase) { return localCIFSCache; } if (!localCSFSCache) { localCSFSCache = localCIFSCache.shadow(/*ignoreCase*/ false); @@ -125,16 +127,14 @@ function createResolver(host: TestHost): FileSystemResolver { statSync(path: string): { mode: number; size: number } { if (host.directoryExists(path)) { return { mode: S_IFDIR | 0o777, size: 0 }; - } - else if (host.fileExists(path)) { + } else if (host.fileExists(path)) { return { mode: S_IFREG | 0o666, size: host.getFileSize(path) }; - } - else { - throw new Error("ENOENT: path does not exist"); + } else { + throw new Error('ENOENT: path does not exist'); } }, readFileSync(path: string): Buffer { - return bufferFrom!(host.readFile(path)!, "utf8") as Buffer; + return bufferFrom(host.readFile(path)!, 'utf8'); } }; } diff --git a/server/src/tests/harness/vfs/filesystem.ts b/server/src/tests/harness/vfs/filesystem.ts index 8a0e49aafb38..ce9ac0113445 100644 --- a/server/src/tests/harness/vfs/filesystem.ts +++ b/server/src/tests/harness/vfs/filesystem.ts @@ -2,18 +2,18 @@ * filesystem.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * virtual file system implementation */ /* eslint-disable no-dupe-class-members */ -import * as pathUtil from "../../../common/pathUtils"; -import { FileWatcher, Listener, } from "../../../common/vfs"; -import { bufferFrom, createIOError } from "../utils"; -import { closeIterator, getIterator, Metadata, nextResult, SortedMap } from "./../utils"; -import { validate, ValidationFlags } from "./pathValidation"; +import * as pathUtil from '../../../common/pathUtils'; +import { FileWatcher, Listener } from '../../../common/vfs'; +import { bufferFrom, createIOError } from '../utils'; +import { closeIterator, getIterator, Metadata, nextResult, SortedMap } from './../utils'; +import { validate, ValidationFlags } from './pathValidation'; -export const ModulePath = pathUtil.normalizeSlashes("/"); +export const MODULE_PATH = pathUtil.normalizeSlashes('/'); let devCount = 0; // A monotonically increasing count of device ids let inoCount = 0; // A monotonically increasing count of inodes @@ -27,10 +27,10 @@ export interface DiffOptions { */ export class FileSystem { /** Indicates whether the file system is case-sensitive (`false`) or case-insensitive (`true`). */ - public readonly ignoreCase: boolean; + readonly ignoreCase: boolean; /** Gets the comparison function used to compare two paths. */ - public readonly stringComparer: (a: string, b: string) => number; + readonly stringComparer: (a: string, b: string) => number; // lazy-initialized state that should be mutable even if the FileSystem is frozen. private _lazy: { @@ -57,7 +57,7 @@ export class FileSystem { } if (files) { - this._applyFiles(files, /*dirname*/ ""); + this._applyFiles(files, /*dirname*/ ''); } let cwd = options.cwd; @@ -69,8 +69,7 @@ export class FileSystem { cwd = cwd ? pathUtil.resolvePaths(name, cwd) : name; break; } - } - finally { + } finally { closeIterator(iterator); } } @@ -80,13 +79,13 @@ export class FileSystem { this.mkdirpSync(cwd); } - this._cwd = cwd || ""; + this._cwd = cwd || ''; } /** * Gets metadata for this `FileSystem`. */ - public get meta(): Metadata { + get meta(): Metadata { if (!this._lazy.meta) { this._lazy.meta = new Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined); } @@ -96,14 +95,14 @@ export class FileSystem { /** * Gets a value indicating whether the file system is read-only. */ - public get isReadonly() { + get isReadonly() { return Object.isFrozen(this); } /** * Makes the file system read-only. */ - public makeReadonly() { + makeReadonly() { Object.freeze(this); return this; } @@ -111,7 +110,7 @@ export class FileSystem { /** * Gets the file system shadowed by this file system. */ - public get shadowRoot() { + get shadowRoot() { return this._shadowRoot; } @@ -120,8 +119,8 @@ export class FileSystem { * 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; + snapshot() { + if (this.isReadonly) { return; } const fs = new FileSystem(this.ignoreCase, { time: this._time }); fs._lazy = this._lazy; fs._cwd = this._cwd; @@ -138,9 +137,9 @@ export class FileSystem { * 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."); + 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; @@ -152,12 +151,12 @@ export class FileSystem { * * @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"); + 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 (typeof result === 'function') { result = result(); } + if (typeof result === 'object') { result = result.getTime(); } + if (result === -1) { result = Date.now(); } if (value !== undefined) { this._time = value; } @@ -168,9 +167,9 @@ export class FileSystem { * Gets the metadata object for a path. * @param path */ - public filemeta(path: string): Metadata { + filemeta(path: string): Metadata { const { node } = this._walk(this._resolve(path)); - if (!node) throw createIOError("ENOENT"); + if (!node) { throw createIOError('ENOENT'); } return this._filemeta(node); } @@ -187,11 +186,11 @@ export class FileSystem { * * @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."); + 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"); + if (!node) { throw createIOError('ENOENT'); } + if (!isDirectory(node)) { throw createIOError('ENOTDIR'); } return this._cwd; } @@ -200,23 +199,23 @@ export class FileSystem { * * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/chdir.html */ - public chdir(path: string) { - if (this.isReadonly) throw createIOError("EPERM"); + 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"); + 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); + pushd(path?: string) { + if (this.isReadonly) { throw createIOError('EPERM'); } + if (path) { path = this._resolve(path); } if (this._cwd) { - if (!this._dirStack) this._dirStack = []; + if (!this._dirStack) { this._dirStack = []; } this._dirStack.push(this._cwd); } if (path && path !== this._cwd) { @@ -227,8 +226,8 @@ export class FileSystem { /** * Pops the previous directory from the location stack and changes the current directory to that directory. */ - public popd() { - if (this.isReadonly) throw createIOError("EPERM"); + popd() { + if (this.isReadonly) { throw createIOError('EPERM'); } const path = this._dirStack && this._dirStack.pop(); if (path) { this.chdir(path); @@ -238,7 +237,7 @@ export class FileSystem { /** * Update the file system with a set of files. */ - public apply(files: FileSet) { + apply(files: FileSet) { this._applyFiles(files, this._cwd); } @@ -248,7 +247,7 @@ export class FileSystem { * @param axis The axis along which to traverse. * @param traversal The traversal scheme to use. */ - public scanSync(path: string, axis: Axis, traversal: Traversal) { + 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); @@ -261,49 +260,46 @@ export class FileSystem { * @param axis The axis along which to traverse. * @param traversal The traversal scheme to use. */ - public lscanSync(path: string, axis: Axis, traversal: Traversal) { + 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; } - public createFileSystemWatcher(paths: string[], event: 'all', listener: Listener): FileWatcher { - // eslint-disable-next-line @typescript-eslint/no-empty-function - return { close: () => { } }; + createFileSystemWatcher(paths: string[], event: 'all', listener: Listener): FileWatcher { + return { close: () => { /* left empty */ } }; } - public getModulePath(): string { - return ModulePath; + getModulePath(): string { + return MODULE_PATH; } 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 (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") { + if (axis === 'ancestors-or-self' || axis === 'ancestors') { const dirname = pathUtil.getDirectoryPath(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); + this._scan(dirname, stats, 'ancestors-or-self', traversal, noFollow, results); } - } - catch { /*ignored*/ } + } catch { /*ignored*/ } } } - if (axis === "descendants-or-self" || axis === "descendants") { + 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 = pathUtil.combinePaths(path, file); const stats = this._stat(this._walk(childpath, noFollow)); - this._scan(childpath, stats, "descendants-or-self", traversal, noFollow, results); - } - catch { /*ignored*/ } + this._scan(childpath, stats, 'descendants-or-self', traversal, noFollow, results); + } catch { /*ignored*/ } } } } @@ -316,13 +312,13 @@ export class FileSystem { * @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"); + mountSync(source: string, target: string, resolver: FileSystemResolver) { + if (this.isReadonly) { throw createIOError('EROFS'); } source = validate(source, ValidationFlags.Absolute); const { parent, links, node: existingNode, basename } = this._walk(this._resolve(target), /*noFollow*/ true); - if (existingNode) throw createIOError("EEXIST"); + if (existingNode) { throw createIOError('EEXIST'); } const time = this.time(); const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time); @@ -334,21 +330,19 @@ export class FileSystem { /** * Recursively remove all files and directories underneath the provided path. */ - public rimrafSync(path: string) { + rimrafSync(path: string) { try { const stats = this.lstatSync(path); if (stats.isFile() || stats.isSymbolicLink()) { this.unlinkSync(path); - } - else if (stats.isDirectory()) { + } else if (stats.isDirectory()) { for (const file of this.readdirSync(path)) { this.rimrafSync(pathUtil.combinePaths(path, file)); } this.rmdirSync(path); } - } - catch (e) { - if (e.code === "ENOENT") return; + } catch (e) { + if (e.code === 'ENOENT') { return; } throw e; } } @@ -356,43 +350,40 @@ export class FileSystem { /** * Make a directory and all of its parent paths (if they don't exist). */ - public mkdirpSync(path: string) { + mkdirpSync(path: string) { path = this._resolve(path); const result = this._walk(path, /*noFollow*/ true, (error, result) => { - if (error.code === "ENOENT") { + if (error.code === 'ENOENT') { this._mkdir(result); - return "retry"; + return 'retry'; } - return "throw"; + return 'throw'; }); - if (!result.node) this._mkdir(result); + if (!result.node) { this._mkdir(result); } } - public getFileListing(): string { - let result = ""; + getFileListing(): string { + let result = ''; const printLinks = (dirname: string | undefined, links: SortedMap) => { const iterator = getIterator(links); try { for (let i = nextResult(iterator); i; i = nextResult(iterator)) { const [name, node] = i.value; const path = dirname ? pathUtil.combinePaths(dirname, name) : name; - const marker = pathUtil.comparePaths(this._cwd, path, this.ignoreCase) === 0 ? "*" : " "; - if (result) result += "\n"; + const marker = pathUtil.comparePaths(this._cwd, path, this.ignoreCase) === 0 ? '*' : ' '; + if (result) { result += '\n'; } result += marker; if (isDirectory(node)) { result += pathUtil.ensureTrailingDirectorySeparator(path); printLinks(path, this._getLinks(node)); - } - else if (isFile(node)) { + } else if (isFile(node)) { result += path; - } - else if (isSymlink(node)) { - result += path + " -> " + node.symlink; + } else if (isSymlink(node)) { + result += `${ path } -> ${ node.symlink }`; } } - } - finally { + } finally { closeIterator(iterator); } }; @@ -403,7 +394,7 @@ export class FileSystem { /** * Print diagnostic information about the structure of the file system to the console. */ - public debugPrint(): void { + debugPrint(): void { console.log(this.getFileListing()); } @@ -412,8 +403,8 @@ export class FileSystem { /** * Determines whether a path exists. */ - public existsSync(path: string) { - const result = this._walk(this._resolve(path), /*noFollow*/ true, () => "stop"); + existsSync(path: string) { + const result = this._walk(this._resolve(path), /*noFollow*/ true, () => 'stop'); return result !== undefined && result.node !== undefined; } @@ -424,7 +415,7 @@ export class FileSystem { * * 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) { + statSync(path: string) { return this._stat(this._walk(this._resolve(path))); } @@ -433,13 +424,13 @@ export class FileSystem { * * 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"); + 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"); + throw createIOError('ENOENT'); } entry.node.atimeMs = +atime; entry.node.mtimeMs = +mtime; @@ -453,14 +444,13 @@ export class FileSystem { * * 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) { + 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); + if (!node) { throw createIOError(`ENOENT`, entry.realpath); } return new Stats( node.dev, node.ino, @@ -473,7 +463,7 @@ export class FileSystem { node.atimeMs, node.mtimeMs, node.ctimeMs, - node.birthtimeMs, + node.birthtimeMs ); } @@ -484,10 +474,10 @@ export class FileSystem { * * 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) { + readdirSync(path: string) { const { node } = this._walk(this._resolve(path)); - if (!node) throw createIOError("ENOENT"); - if (!isDirectory(node)) throw createIOError("ENOTDIR"); + if (!node) { throw createIOError('ENOENT'); } + if (!isDirectory(node)) { throw createIOError('ENOTDIR'); } return Array.from(this._getLinks(node).keys()); } @@ -498,14 +488,14 @@ export class FileSystem { * * 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"); + 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"); + 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); @@ -518,14 +508,14 @@ export class FileSystem { * * 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"); + 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"); + 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); } @@ -537,16 +527,16 @@ export class FileSystem { * * 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"); + 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"); + 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"); + if (!parent) { throw createIOError('EPERM'); } + if (existingNode) { throw createIOError('EEXIST'); } this._addLink(parent, links, basename, node); } @@ -558,13 +548,13 @@ export class FileSystem { * * 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"); + 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"); + if (!parent) { throw createIOError('EPERM'); } + if (!node) { throw createIOError('ENOENT'); } + if (isDirectory(node)) { throw createIOError('EISDIR'); } this._removeLink(parent, links, basename, node); } @@ -576,24 +566,27 @@ export class FileSystem { * * 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"); + 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); - 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"); + 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 { 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 (this._getLinks(existingNode).size > 0) throw createIOError("ENOTEMPTY"); - } - else { - if (isDirectory(existingNode)) throw createIOError("EISDIR"); + if (!isDirectory(existingNode)) { throw createIOError('ENOTDIR'); } + if (this._getLinks(existingNode).size > 0) { throw createIOError('ENOTEMPTY'); } + } else { + if (isDirectory(existingNode)) { throw createIOError('EISDIR'); } } this._removeLink(newParent, newParentLinks, newBasename, existingNode, time); } @@ -608,12 +601,12 @@ export class FileSystem { * * 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"); + 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"); + 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); @@ -628,7 +621,7 @@ export class FileSystem { * * 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) { + realpathSync(path: string) { const { realpath } = this._walk(this._resolve(path)); return realpath; } @@ -638,24 +631,24 @@ export class FileSystem { * * 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; + 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: string): string; + readFileSync(path: string, encoding: string): 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?: string | null): string | Buffer; - public readFileSync(path: string, encoding: string | null = null) { + readFileSync(path: string, encoding?: string | null): string | Buffer; + readFileSync(path: string, encoding: string | null = 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"); + 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; @@ -666,11 +659,11 @@ export class FileSystem { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - public writeFileSync(path: string, data: string | Buffer, encoding: string | null = null) { - if (this.isReadonly) throw createIOError("EROFS"); + 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"); + if (!parent) { throw createIOError('EPERM'); } const time = this.time(); let node = existingNode; @@ -679,9 +672,9 @@ export class FileSystem { 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() : bufferFrom!("" + data, (encoding as BufferEncoding) || "utf8") as Buffer; + if (isDirectory(node)) { throw createIOError('EISDIR'); } + if (!isFile(node)) { throw createIOError('EBADF'); } + node.buffer = Buffer.isBuffer(data) ? data.slice() : bufferFrom('' + data, (encoding as BufferEncoding) || 'utf8'); node.size = node.buffer.byteLength; node.mtimeMs = time; node.ctimeMs = time; @@ -691,27 +684,29 @@ export class FileSystem { * 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 = this.shadowRoot, options: DiffOptions = {}) { + diff(base = this.shadowRoot, options: DiffOptions = {}) { const differences: FileSet = {}; const hasDifferences = base ? - FileSystem.rootDiff(differences, this, base, options) : - FileSystem.trackCreatedInodes(differences, this, this._getRootLinks()); + 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 = {}) { + static diff(changed: FileSystem, base: FileSystem, options: DiffOptions = {}) { const differences: FileSet = {}; - return FileSystem.rootDiff(differences, changed, base, options) ? + return FileSystem._rootDiff(differences, changed, base, options) ? differences : undefined; } - private static diffWorker(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap | undefined, base: FileSystem, baseLinks: ReadonlyMap | undefined, options: DiffOptions) { - if (changedLinks && !baseLinks) return FileSystem.trackCreatedInodes(container, changed, changedLinks); - if (baseLinks && !changedLinks) return FileSystem.trackDeletedInodes(container, baseLinks); + private static _diffWorker(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap | undefined, + base: FileSystem, baseLinks: ReadonlyMap | 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 @@ -726,53 +721,57 @@ export class FileSystem { const baseNode = baseLinks.get(basename); if (baseNode) { if (isDirectory(changedNode) && isDirectory(baseNode)) { - return hasChanges = FileSystem.directoryDiff(container, basename, changed, changedNode, base, baseNode, options) || hasChanges; + 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; + 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._symlinkDiff(container, basename, changedNode, baseNode) || hasChanges; } } - return hasChanges = FileSystem.trackCreatedInode(container, basename, changed, changedNode) || 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; + 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; + 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; + if (!changed._lazy.links && !changed._shadowRoot && !base._lazy.links && !base._shadowRoot) { return false; } - return FileSystem.diffWorker(container, changed, changed._getRootLinks(), base, base._getRootLinks(), options); + 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; + 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; + if (changedNode === baseNode) { return false; } // no difference if both nodes are non shadowed and have no entries - if (isEmptyNonShadowedDirectory(changedNode) && isEmptyNonShadowedDirectory(baseNode)) return false; + 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; + 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)) { + if (!FileSystem._diffWorker(children, changed, changed._getLinks(changedNode), base, base._getLinks(baseNode), options)) { return false; } @@ -780,30 +779,32 @@ export class FileSystem { 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; + 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; + if (changedNode === baseNode) { return false; } // no difference if both nodes are non shadowed and have no entries - if (isEmptyNonShadowedFile(changedNode) && isEmptyNonShadowedFile(baseNode)) return false; + 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; + 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; + if (changedBuffer === baseBuffer) { return false; } // no difference if both buffers are identical if (Buffer.compare(changedBuffer, baseBuffer) === 0) { - if (!options.includeChangedFileWithSameContent) return false; + if (!options.includeChangedFileWithSameContent) { return false; } container[basename] = new SameFileContentFile(changedBuffer); return true; } @@ -812,39 +813,37 @@ export class FileSystem { return true; } - private static symlinkDiff(container: FileSet, basename: string, changedNode: SymlinkInode, baseNode: SymlinkInode) { + 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; + 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) { + private static _trackCreatedInode(container: FileSet, basename: string, changed: FileSystem, node: Inode) { if (isDirectory(node)) { const children: FileSet = {}; - FileSystem.trackCreatedInodes(children, changed, changed._getLinks(node)); + FileSystem._trackCreatedInodes(children, changed, changed._getLinks(node)); container[basename] = new Directory(children); - } - else if (isSymlink(node)) { + } else if (isSymlink(node)) { container[basename] = new Symlink(node.symlink); - } - else { - container[basename] = new File(node.buffer || ""); + } else { + container[basename] = new File(node.buffer || ''); } return true; } - private static trackCreatedInodes(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap) { + private static _trackCreatedInodes(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap) { // no difference if links are empty - if (!changedLinks.size) return false; + if (!changedLinks.size) { return false; } - changedLinks.forEach((node, basename) => { FileSystem.trackCreatedInode(container, basename, changed, node); }); + changedLinks.forEach((node, basename) => { FileSystem._trackCreatedInode(container, basename, changed, node); }); return true; } - private static trackDeletedInodes(container: FileSet, baseLinks: ReadonlyMap) { + private static _trackDeletedInodes(container: FileSet, baseLinks: ReadonlyMap) { // no difference if links are empty - if (!baseLinks.size) return false; + if (!baseLinks.size) { return false; } baseLinks.forEach((node, basename) => { container[basename] = isDirectory(node) ? new Rmdir() : new Unlink(); }); return true; } @@ -869,23 +868,26 @@ export class FileSystem { links.set(name, node); node.nlink++; node.ctimeMs = time; - if (parent) parent.mtimeMs = time; - if (!parent && !this._cwd) this._cwd = name; + if (parent) { parent.mtimeMs = time; } + if (!parent && !this._cwd) { this._cwd = name; } } - private _removeLink(parent: DirectoryInode | undefined, links: SortedMap, name: string, node: Inode, time = this.time()) { + private _removeLink(parent: DirectoryInode | undefined, links: SortedMap, + name: string, node: Inode, time = this.time()) { + links.delete(name); node.nlink--; node.ctimeMs = time; - if (parent) parent.mtimeMs = time; + if (parent) { parent.mtimeMs = time; } } - private _replaceLink(oldParent: DirectoryInode, oldLinks: SortedMap, oldName: string, newParent: DirectoryInode, newLinks: SortedMap, newName: string, node: Inode, time: number) { + private _replaceLink(oldParent: DirectoryInode, oldLinks: SortedMap, + oldName: string, newParent: DirectoryInode, newLinks: SortedMap, newName: string, node: Inode, time: number) { + if (oldParent !== newParent) { this._removeLink(oldParent, oldLinks, oldName, node, time); this._addLink(newParent, newLinks, newName, node, time); - } - else { + } else { oldLinks.delete(oldName); oldLinks.set(newName, node); oldParent.mtimeMs = time; @@ -915,16 +917,14 @@ export class FileSystem { const path = pathUtil.combinePaths(source, name); const stats = resolver.statSync(path); switch (stats.mode & S_IFMT) { - case S_IFDIR: - { + case S_IFDIR: { const dir = this._mknod(node.dev, S_IFDIR, 0o777); dir.source = pathUtil.combinePaths(source, name); dir.resolver = resolver; this._addLink(node, links, name, dir); break; } - case S_IFREG: - { + case S_IFREG: { const file = this._mknod(node.dev, S_IFREG, 0o666); file.source = pathUtil.combinePaths(source, name); file.resolver = resolver; @@ -934,8 +934,7 @@ export class FileSystem { } } } - } - else if (this._shadowRoot && node.shadowRoot) { + } else if (this._shadowRoot && node.shadowRoot) { this._copyShadowLinks(this._shadowRoot._getLinks(node.shadowRoot), links); } node.links = links; @@ -960,9 +959,9 @@ export class FileSystem { birthtimeMs: root.birthtimeMs, nlink: root.nlink, shadowRoot: root - } as Inode; + }; - if (isSymlink(root)) (shadow as SymlinkInode).symlink = root.symlink; + if (isSymlink(root)) { (shadow as SymlinkInode).symlink = root.symlink; } shadows.set(shadow.ino, shadow); } @@ -976,17 +975,16 @@ export class FileSystem { const [name, root] = i.value; target.set(name, this._getShadow(root)); } - } - finally { + } finally { 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); + 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; } @@ -998,11 +996,9 @@ export class FileSystem { node.resolver = undefined; node.size = undefined; node.buffer = resolver.readFileSync(source); - } - else if (this._shadowRoot && node.shadowRoot) { + } else if (this._shadowRoot && node.shadowRoot) { node.buffer = this._shadowRoot._getBuffer(node.shadowRoot); - } - else { + } else { node.buffer = Buffer.allocUnsafe(0); } } @@ -1018,9 +1014,10 @@ export class FileSystem { * * @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 { + 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 = pathUtil.getPathComponents(path); @@ -1028,7 +1025,7 @@ export class FileSystem { let depth = 0; let retry = false; while (true) { - if (depth >= 40) throw createIOError("ELOOP"); + if (depth >= 40) { throw createIOError('ELOOP'); } const lastStep = step === components.length - 1; const basename = components[step]; const node = links.get(basename); @@ -1036,7 +1033,7 @@ export class FileSystem { return { realpath: pathUtil.combinePathComponents(components), basename, parent, links, node }; } if (node === undefined) { - if (trapError(createIOError("ENOENT"), node)) continue; + if (trapError(createIOError('ENOENT'), node)) { continue; } return undefined; } if (isSymlink(node)) { @@ -1057,16 +1054,16 @@ export class FileSystem { retry = false; continue; } - if (trapError(createIOError("ENOTDIR"), node)) continue; + if (trapError(createIOError('ENOTDIR'), node)) { continue; } return undefined; } function trapError(error: NodeJS.ErrnoException, node?: Inode) { const realpath = pathUtil.combinePathComponents(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") { + const result = !retry && onError ? onError(error, { realpath, basename, parent, links, node }) : 'throw'; + if (result === 'stop') { return false; } + if (result === 'retry') { retry = true; return true; } @@ -1091,18 +1088,16 @@ export class FileSystem { this.pushd(pathUtil.getDirectoryPath(path)); if (entry instanceof Symlink) { if (this.stringComparer(pathUtil.getDirectoryPath(path), path) === 0) { - throw new TypeError("Roots cannot be symbolic links."); + throw new TypeError('Roots cannot be symbolic links.'); } this.symlinkSync(pathUtil.resolvePaths(dirname, entry.symlink), path); this._applyFileExtendedOptions(path, entry); - } - else if (entry instanceof Link) { + } else if (entry instanceof Link) { if (this.stringComparer(pathUtil.getDirectoryPath(path), path) === 0) { - throw new TypeError("Roots cannot be hard links."); + throw new TypeError('Roots cannot be hard links.'); } this.linkSync(entry.path, path); - } - else { + } else { this.mountSync(entry.source, path, entry.resolver); this._applyFileExtendedOptions(path, entry); } @@ -1128,24 +1123,21 @@ export class FileSystem { if (value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink) { if (this.stringComparer(pathUtil.getDirectoryPath(path), path) === 0) { - throw new TypeError("Roots cannot be deleted."); + throw new TypeError('Roots cannot be deleted.'); } this.rimrafSync(path); - } - else if (value instanceof File) { + } else if (value instanceof File) { if (this.stringComparer(pathUtil.getDirectoryPath(path), path) === 0) { - throw new TypeError("Roots cannot be files."); + throw new TypeError('Roots cannot be files.'); } this.mkdirpSync(pathUtil.getDirectoryPath(path)); this.writeFileSync(path, value.data, value.encoding); this._applyFileExtendedOptions(path, value); - } - else if (value instanceof Directory) { + } else if (value instanceof Directory) { this.mkdirpSync(path); this._applyFileExtendedOptions(path, value); this._applyFilesWorker(value.files, path, deferred); - } - else { + } else { deferred.push([value, path]); } } @@ -1167,7 +1159,7 @@ export interface FileSystemOptions { meta?: Record; } -export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants"; +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. */ @@ -1194,8 +1186,8 @@ 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 | undefined; + readonly files: FileSet; + readonly meta: Record | undefined; constructor(files: FileSet, { meta }: { meta?: Record } = {}) { this.files = files; this.meta = meta; @@ -1204,9 +1196,9 @@ export class Directory { /** 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 | undefined; + readonly data: Buffer | string; + readonly encoding: string | undefined; + readonly meta: Record | undefined; constructor(data: Buffer | string, { meta, encoding }: { encoding?: string; meta?: Record } = {}) { this.data = data; this.encoding = encoding; @@ -1222,7 +1214,7 @@ export class SameFileContentFile extends File { /** Extended options for a hard link in a `FileSet` */ export class Link { - public readonly path: string; + readonly path: string; constructor(path: string) { this.path = path; } @@ -1230,18 +1222,18 @@ export class Link { /** Removes a directory in a `FileSet` */ export class Rmdir { - public _rmdirBrand?: never; // brand necessary for proper type guards + _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 + _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 | undefined; + readonly symlink: string; + readonly meta: Record | undefined; constructor(symlink: string, { meta }: { meta?: Record } = {}) { this.symlink = symlink; this.meta = meta; @@ -1262,9 +1254,9 @@ export const S_IFIFO = 0o010000; // FIFO /** 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 | undefined; + readonly source: string; + readonly resolver: FileSystemResolver; + readonly meta: Record | undefined; constructor(source: string, resolver: FileSystemResolver, { meta }: { meta?: Record } = {}) { this.source = source; this.resolver = resolver; @@ -1362,43 +1354,36 @@ function normalizeFileSetEntry(value: FileSet[string]) { value instanceof Unlink) { return value; } - return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(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; + return patch ? formatPatchWorker('', patch) : null; } function formatPatchWorker(dirname: string, container: FileSet): string { - let text = ""; + let text = ''; for (const name of Object.keys(container)) { const entry = normalizeFileSetEntry(container[name]); const file = dirname ? pathUtil.combinePaths(dirname, name) : name; if (entry === null || entry === undefined || entry instanceof Unlink || entry instanceof Rmdir) { text += `//// [${ file }] unlink\r\n`; - } - else if (entry instanceof Rmdir) { + } else if (entry instanceof Rmdir) { text += `//// [${ pathUtil.ensureTrailingDirectorySeparator(file) }] rmdir\r\n`; - } - else if (entry instanceof Directory) { + } else if (entry instanceof Directory) { text += formatPatchWorker(file, entry.files); - } - else if (entry instanceof SameFileContentFile) { + } 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"); + } 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) { + } else if (entry instanceof Link) { text += `//// [${ file }] link(${ entry.path })\r\n`; - } - else if (entry instanceof Symlink) { + } else if (entry instanceof Symlink) { text += `//// [${ file }] symlink(${ entry.symlink })\r\n`; - } - else if (entry instanceof Mount) { + } else if (entry instanceof Mount) { text += `//// [${ file }] mount(${ entry.source })\r\n`; } } @@ -1406,28 +1391,31 @@ function formatPatchWorker(dirname: string, container: FileSet): string { } 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; + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + atime: Date; + mtime: Date; + ctime: Date; + 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) { + 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; @@ -1448,11 +1436,11 @@ class Stats { 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; } -} \ No newline at end of file + isFile() { return (this.mode & S_IFMT) === S_IFREG; } + isDirectory() { return (this.mode & S_IFMT) === S_IFDIR; } + isSymbolicLink() { return (this.mode & S_IFMT) === S_IFLNK; } + isBlockDevice() { return (this.mode & S_IFMT) === S_IFBLK; } + isCharacterDevice() { return (this.mode & S_IFMT) === S_IFCHR; } + isFIFO() { return (this.mode & S_IFMT) === S_IFIFO; } + isSocket() { return (this.mode & S_IFMT) === S_IFSOCK; } +} diff --git a/server/src/tests/harness/vfs/pathValidation.ts b/server/src/tests/harness/vfs/pathValidation.ts index 4d6117d68347..763d6205c84b 100644 --- a/server/src/tests/harness/vfs/pathValidation.ts +++ b/server/src/tests/harness/vfs/pathValidation.ts @@ -3,9 +3,9 @@ * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. */ -import { sep } from "path" -import * as pu from "../../../common/pathUtils" -import { createIOError } from "../utils"; +import { sep } from 'path'; +import * as pu from '../../../common/pathUtils'; +import { createIOError } from '../utils'; const invalidRootComponentRegExp = getInvalidRootComponentRegExp(); const invalidNavigableComponentRegExp = /[:*?"<>|]/; @@ -41,7 +41,7 @@ export const enum ValidationFlags { RelativeOrAbsolute = AllowRoot | AllowDirname | AllowBasename | AllowExtname | AllowTrailingSeparator | AllowNavigation, /** Path may only be a filename */ - Basename = RequireBasename | AllowExtname, + Basename = RequireBasename | AllowExtname } function validateComponents(components: string[], flags: ValidationFlags, hasTrailingSeparator: boolean) { @@ -54,30 +54,30 @@ function validateComponents(components: string[], flags: ValidationFlags, hasTra : flags & ValidationFlags.AllowWildcard ? invalidNonNavigableComponentWithWildcardsRegExp : invalidNonNavigableComponentRegExp; // Validate required components - if (flags & ValidationFlags.RequireRoot && !hasRoot) return false; - if (flags & ValidationFlags.RequireDirname && !hasDirname) return false; - if (flags & ValidationFlags.RequireBasename && !hasBasename) return false; - if (flags & ValidationFlags.RequireExtname && !hasExtname) return false; - if (flags & ValidationFlags.RequireTrailingSeparator && !hasTrailingSeparator) return false; + if (flags & ValidationFlags.RequireRoot && !hasRoot) { return false; } + if (flags & ValidationFlags.RequireDirname && !hasDirname) { return false; } + if (flags & ValidationFlags.RequireBasename && !hasBasename) { return false; } + if (flags & ValidationFlags.RequireExtname && !hasExtname) { return false; } + if (flags & ValidationFlags.RequireTrailingSeparator && !hasTrailingSeparator) { return false; } // Required components indicate allowed components - if (flags & ValidationFlags.RequireRoot) flags |= ValidationFlags.AllowRoot; - if (flags & ValidationFlags.RequireDirname) flags |= ValidationFlags.AllowDirname; - if (flags & ValidationFlags.RequireBasename) flags |= ValidationFlags.AllowBasename; - if (flags & ValidationFlags.RequireExtname) flags |= ValidationFlags.AllowExtname; - if (flags & ValidationFlags.RequireTrailingSeparator) flags |= ValidationFlags.AllowTrailingSeparator; + if (flags & ValidationFlags.RequireRoot) { flags |= ValidationFlags.AllowRoot; } + if (flags & ValidationFlags.RequireDirname) { flags |= ValidationFlags.AllowDirname; } + if (flags & ValidationFlags.RequireBasename) { flags |= ValidationFlags.AllowBasename; } + if (flags & ValidationFlags.RequireExtname) { flags |= ValidationFlags.AllowExtname; } + if (flags & ValidationFlags.RequireTrailingSeparator) { flags |= ValidationFlags.AllowTrailingSeparator; } // Validate disallowed components - if (~flags & ValidationFlags.AllowRoot && hasRoot) return false; - if (~flags & ValidationFlags.AllowDirname && hasDirname) return false; - if (~flags & ValidationFlags.AllowBasename && hasBasename) return false; - if (~flags & ValidationFlags.AllowExtname && hasExtname) return false; - if (~flags & ValidationFlags.AllowTrailingSeparator && hasTrailingSeparator) return false; + if (~flags & ValidationFlags.AllowRoot && hasRoot) { return false; } + if (~flags & ValidationFlags.AllowDirname && hasDirname) { return false; } + if (~flags & ValidationFlags.AllowBasename && hasBasename) { return false; } + if (~flags & ValidationFlags.AllowExtname && hasExtname) { return false; } + if (~flags & ValidationFlags.AllowTrailingSeparator && hasTrailingSeparator) { return false; } // Validate component strings - if (invalidRootComponentRegExp.test(components[0])) return false; + if (invalidRootComponentRegExp.test(components[0])) { return false; } for (let i = 1; i < components.length; i++) { - if (invalidComponentRegExp.test(components[i])) return false; + if (invalidComponentRegExp.test(components[i])) { return false; } } return true; @@ -86,8 +86,9 @@ function validateComponents(components: string[], flags: ValidationFlags, hasTra export function validate(path: string, flags: ValidationFlags = ValidationFlags.RelativeOrAbsolute) { const components = pu.getPathComponents(path); const trailing = pu.hasTrailingDirectorySeparator(path); - if (!validateComponents(components, flags, trailing)) throw createIOError("ENOENT"); - return components.length > 1 && trailing ? pu.combinePathComponents(pu.reducePathComponents(components)) + sep : pu.combinePathComponents(pu.reducePathComponents(components)); + if (!validateComponents(components, flags, trailing)) { throw createIOError('ENOENT'); } + return components.length > 1 && trailing ? pu.combinePathComponents(pu.reducePathComponents(components)) + sep + : pu.combinePathComponents(pu.reducePathComponents(components)); } function getInvalidRootComponentRegExp(): RegExp { diff --git a/server/src/tests/pathUtils.test.ts b/server/src/tests/pathUtils.test.ts index 463949b55333..bf57db36705c 100644 --- a/server/src/tests/pathUtils.test.ts +++ b/server/src/tests/pathUtils.test.ts @@ -10,27 +10,27 @@ import * as assert from 'assert'; import * as path from 'path'; +import { Comparison } from '../common/core'; import { - combinePaths, ensureTrailingDirectorySeparator, getFileExtension, - getFileName, getPathComponents, - getWildcardRegexPattern, getWildcardRoot, hasTrailingDirectorySeparator, stripFileExtension, - stripTrailingDirectorySeparator, - normalizeSlashes, + changeAnyExtension, combinePathComponents, combinePaths, + comparePaths, comparePathsCaseInsensitive, + comparePathsCaseSensitive, containsPath, ensureTrailingDirectorySeparator, getAnyExtensionFromPath, + getBaseFileName, + getFileExtension, + getFileName, + getPathComponents, getRegexEscapedSeparator, + getRelativePathFromDirectory, + getWildcardRegexPattern, + getWildcardRoot, + hasTrailingDirectorySeparator, + isRootedDiskPath, + normalizeSlashes, reducePathComponents, - combinePathComponents, resolvePaths, - comparePaths, - containsPath, - changeAnyExtension, - getAnyExtensionFromPath, - getBaseFileName, - getRelativePathFromDirectory, - comparePathsCaseSensitive, - comparePathsCaseInsensitive, - isRootedDiskPath + stripFileExtension, + stripTrailingDirectorySeparator } from '../common/pathUtils'; -import { Comparison } from '../common/core'; test('getPathComponents1', () => { const components = getPathComponents(''); @@ -144,137 +144,137 @@ test('reducePathComponentsEmpty', () => { }); test('reducePathComponents', () => { - assert.deepEqual(reducePathComponents(getPathComponents("/a/b/../c/.")), [path.sep, 'a', 'c']); + assert.deepEqual(reducePathComponents(getPathComponents('/a/b/../c/.')), [path.sep, 'a', 'c']); }); test('combinePathComponentsEmpty', () => { - assert.equal(combinePathComponents([]), ""); + assert.equal(combinePathComponents([]), ''); }); test('combinePathComponentsAbsolute', () => { - assert.equal(combinePathComponents(["/", "a", "b"]), normalizeSlashes("/a/b")); + assert.equal(combinePathComponents(['/', 'a', 'b']), normalizeSlashes('/a/b')); }); test('combinePathComponents', () => { - assert.equal(combinePathComponents(["a", "b"]), normalizeSlashes("a/b")); + assert.equal(combinePathComponents(['a', 'b']), normalizeSlashes('a/b')); }); test('resolvePath1', () => { - assert.equal(resolvePaths("/path", "to", "file.ext"), normalizeSlashes("/path/to/file.ext")); + assert.equal(resolvePaths('/path', 'to', 'file.ext'), normalizeSlashes('/path/to/file.ext')); }); test('resolvePath2', () => { - assert.equal(resolvePaths("/path", "to", "..", "from", "file.ext/"), normalizeSlashes("/path/from/file.ext/")); + assert.equal(resolvePaths('/path', 'to', '..', 'from', 'file.ext/'), normalizeSlashes('/path/from/file.ext/')); }); test('comparePaths1', () => { - assert.equal(comparePaths("/A/B/C", "\\a\\b\\c"), Comparison.LessThan); + assert.equal(comparePaths('/A/B/C', '\\a\\b\\c'), Comparison.LessThan); }); test('comparePaths2', () => { - assert.equal(comparePaths("/A/B/C", "\\a\\b\\c", true), Comparison.EqualTo); + assert.equal(comparePaths('/A/B/C', '\\a\\b\\c', true), Comparison.EqualTo); }); test('comparePaths3', () => { - assert.equal(comparePaths("/A/B/C", "/a/c/../b/./c", true), Comparison.EqualTo); + assert.equal(comparePaths('/A/B/C', '/a/c/../b/./c', true), Comparison.EqualTo); }); test('comparePaths4', () => { - assert.equal(comparePaths("/a/b/c", "/a/c/../b/./c", "current\\path\\", false), Comparison.EqualTo); + assert.equal(comparePaths('/a/b/c', '/a/c/../b/./c', 'current\\path\\', false), Comparison.EqualTo); }); test('comparePaths5', () => { - assert.equal(comparePaths("/a/b/c/", "/a/b/c"), Comparison.GreaterThan); + assert.equal(comparePaths('/a/b/c/', '/a/b/c'), Comparison.GreaterThan); }); test('containsPath1', () => { - assert.equal(containsPath("/a/b/c/", "/a/d/../b/c/./d"), true); + assert.equal(containsPath('/a/b/c/', '/a/d/../b/c/./d'), true); }); test('containsPath2', () => { - assert.equal(containsPath("/", "\\a"), true); + assert.equal(containsPath('/', '\\a'), true); }); test('containsPath3', () => { - assert.equal(containsPath("/a", "/A/B", true), true); + assert.equal(containsPath('/a', '/A/B', true), true); }); test('changeAnyExtension1', () => { - assert.equal(changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"], true), "/path/to/file.js"); + assert.equal(changeAnyExtension('/path/to/file.ext', '.js', ['.ext', '.ts'], true), '/path/to/file.js'); }); test('changeAnyExtension2', () => { - assert.equal(changeAnyExtension("/path/to/file.ext", ".js"), "/path/to/file.js"); + assert.equal(changeAnyExtension('/path/to/file.ext', '.js'), '/path/to/file.js'); }); test('changeAnyExtension3', () => { - assert.equal(changeAnyExtension("/path/to/file.ext", ".js", ".ts", false), "/path/to/file.ext"); + assert.equal(changeAnyExtension('/path/to/file.ext', '.js', '.ts', false), '/path/to/file.ext'); }); test('changeAnyExtension1', () => { - assert.equal(getAnyExtensionFromPath("/path/to/file.ext"), ".ext"); + assert.equal(getAnyExtensionFromPath('/path/to/file.ext'), '.ext'); }); test('changeAnyExtension2', () => { - assert.equal(getAnyExtensionFromPath("/path/to/file.ext", ".ts", true), ""); + assert.equal(getAnyExtensionFromPath('/path/to/file.ext', '.ts', true), ''); }); test('changeAnyExtension3', () => { - assert.equal(getAnyExtensionFromPath("/path/to/file.ext", [".ext", ".ts"], true), ".ext"); + assert.equal(getAnyExtensionFromPath('/path/to/file.ext', ['.ext', '.ts'], true), '.ext'); }); test('getBaseFileName1', () => { - assert.equal(getBaseFileName("/path/to/file.ext"), "file.ext"); + assert.equal(getBaseFileName('/path/to/file.ext'), 'file.ext'); }); test('getBaseFileName2', () => { - assert.equal(getBaseFileName("/path/to/"), "to"); + assert.equal(getBaseFileName('/path/to/'), 'to'); }); test('getBaseFileName3', () => { - assert.equal(getBaseFileName("c:/"), ""); + assert.equal(getBaseFileName('c:/'), ''); }); test('getBaseFileName4', () => { - assert.equal(getBaseFileName("/path/to/file.ext", [".ext"], true), "file"); + assert.equal(getBaseFileName('/path/to/file.ext', ['.ext'], true), 'file'); }); test('getRelativePathFromDirectory1', () => { - assert.equal(getRelativePathFromDirectory("/a", "/a/b/c/d", true), normalizeSlashes("b/c/d")); + assert.equal(getRelativePathFromDirectory('/a', '/a/b/c/d', true), normalizeSlashes('b/c/d')); }); test('getRelativePathFromDirectory2', () => { - assert.equal(getRelativePathFromDirectory("/a", "/b/c/d", true), normalizeSlashes("../b/c/d")); + assert.equal(getRelativePathFromDirectory('/a', '/b/c/d', true), normalizeSlashes('../b/c/d')); }); test('comparePathsCaseSensitive', () => { - assert.equal(comparePathsCaseSensitive("/a/b/C", "/a/b/c"), Comparison.LessThan); + assert.equal(comparePathsCaseSensitive('/a/b/C', '/a/b/c'), Comparison.LessThan); }); test('comparePathsCaseInsensitive', () => { - assert.equal(comparePathsCaseInsensitive("/a/b/C", "/a/b/c"), Comparison.EqualTo); + assert.equal(comparePathsCaseInsensitive('/a/b/C', '/a/b/c'), Comparison.EqualTo); }); test('isRootedDiskPath1', () => { - assert(isRootedDiskPath(normalizeSlashes("C:/a/b"))); + assert(isRootedDiskPath(normalizeSlashes('C:/a/b'))); }); test('isRootedDiskPath2', () => { - assert(isRootedDiskPath(normalizeSlashes("/"))); + assert(isRootedDiskPath(normalizeSlashes('/'))); }); test('isRootedDiskPath3', () => { - assert(!isRootedDiskPath(normalizeSlashes("a/b"))); + assert(!isRootedDiskPath(normalizeSlashes('a/b'))); }); test('isDiskPathRoot1', () => { - assert(isRootedDiskPath(normalizeSlashes("/"))); + assert(isRootedDiskPath(normalizeSlashes('/'))); }); test('isDiskPathRoot2', () => { - assert(isRootedDiskPath(normalizeSlashes("c:/"))); + assert(isRootedDiskPath(normalizeSlashes('c:/'))); }); test('isDiskPathRoot3', () => { - assert(!isRootedDiskPath(normalizeSlashes("c:"))); + assert(!isRootedDiskPath(normalizeSlashes('c:'))); }); diff --git a/server/src/tests/testState.test.ts b/server/src/tests/testState.test.ts index 4442332f19d0..c9b005082d04 100644 --- a/server/src/tests/testState.test.ts +++ b/server/src/tests/testState.test.ts @@ -2,25 +2,25 @@ * testState.test.ts * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. - * + * * Tests and show how to use TestState in unit test */ import * as assert from 'assert'; -import * as factory from "./harness/vfs/factory" -import { normalizeSlashes, combinePaths, comparePathsCaseSensitive } from '../common/pathUtils'; -import { parseTestData } from './harness/fourslash/fourSlashParser'; -import { TestState } from './harness/fourslash/testState'; +import { combinePaths, comparePathsCaseSensitive, normalizeSlashes } from '../common/pathUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; +import { parseTestData } from './harness/fourslash/fourSlashParser'; import { Range } from './harness/fourslash/fourSlashTypes'; import { runFourSlashTestContent } from './harness/fourslash/runner'; +import { TestState } from './harness/fourslash/testState'; +import * as factory from './harness/vfs/factory'; test('Create', () => { const code = ` // @filename: file1.py ////class A: //// pass - ` + `; const { data, state } = parseAndGetTestState(code); assert(state.activeFile === data.files[0]); @@ -39,12 +39,12 @@ test('Multiple files', () => { // @filename: file3.py ////class C: //// pass - ` + `; const state = parseAndGetTestState(code).state; - assert.equal(state.fs.cwd(), normalizeSlashes("/")); - assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, "file1.py")))); + assert.equal(state.fs.cwd(), normalizeSlashes('/')); + assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')))); }); test('Configuration', () => { @@ -54,7 +54,7 @@ test('Configuration', () => { //// "include": [ //// "src" //// ], -//// +//// //// "exclude": [ //// "**/node_modules", //// "**/__pycache__", @@ -62,21 +62,21 @@ test('Configuration', () => { //// "src/web/node_modules", //// "src/typestubs" //// ], -//// +//// //// "ignore": [ //// "src/oldstuff" //// ], -//// +//// //// "typingsPath": "src/typestubs", //// "venvPath": "/home/foo/.venvs", -//// +//// //// "reportTypeshedErrors": false, //// "reportMissingImports": true, //// "reportMissingTypeStubs": false, -//// +//// //// "pythonVersion": "3.6", //// "pythonPlatform": "Linux", -//// +//// //// "executionEnvironments": [ //// { //// "root": "src/web", @@ -110,15 +110,15 @@ test('Configuration', () => { // @filename: file1.py ////class A: //// pass - ` + `; const state = parseAndGetTestState(code).state; - assert.equal(state.fs.cwd(), normalizeSlashes("/")); - assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, "file1.py")))); + assert.equal(state.fs.cwd(), normalizeSlashes('/')); + assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')))); - assert.equal(state.configOptions.diagnosticSettings.reportMissingImports, "error"); - assert.equal(state.configOptions.typingsPath, normalizeSlashes("src/typestubs")); + assert.equal(state.configOptions.diagnosticSettings.reportMissingImports, 'error'); + assert.equal(state.configOptions.typingsPath, normalizeSlashes('src/typestubs')); }); test('ProjectRoot', () => { @@ -129,14 +129,14 @@ test('ProjectRoot', () => { // @filename: /root/file1.py ////class A: //// pass - ` + `; const state = parseAndGetTestState(code).state; - assert.equal(state.fs.cwd(), normalizeSlashes("/")); - assert(state.fs.existsSync(normalizeSlashes("/root/file1.py"))); + assert.equal(state.fs.cwd(), normalizeSlashes('/')); + assert(state.fs.existsSync(normalizeSlashes('/root/file1.py'))); - assert.equal(state.configOptions.projectRoot, normalizeSlashes("/root")); + assert.equal(state.configOptions.projectRoot, normalizeSlashes('/root')); }); test('IgnoreCase', () => { @@ -147,28 +147,28 @@ test('IgnoreCase', () => { // @filename: file1.py ////class A: //// pass - ` + `; const state = parseAndGetTestState(code).state; - assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, "FILE1.py")))); + assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'FILE1.py')))); }); test('GoToMarker', () => { const code = ` ////class A: //// /*marker1*/pass - ` + `; const { data, state } = parseAndGetTestState(code); - const marker = data.markerPositions.get("marker1"); + const marker = data.markerPositions.get('marker1'); - state.goToMarker("marker1"); - assert.equal(state.lastKnownMarker, "marker1"); + state.goToMarker('marker1'); + assert.equal(state.lastKnownMarker, 'marker1'); assert.equal(state.currentCaretPosition, marker!.position); state.goToMarker(marker); - assert.equal(state.lastKnownMarker, "marker1"); + assert.equal(state.lastKnownMarker, 'marker1'); assert.equal(state.currentCaretPosition, marker!.position); assert.equal(state.selectionEnd, -1); }); @@ -182,16 +182,16 @@ test('GoToEachMarker', () => { // @filename: file2.py ////class B: //// /*marker2*/pass - ` + `; const { data, state } = parseAndGetTestState(code); - const marker1 = data.markerPositions.get("marker1"); - const marker2 = data.markerPositions.get("marker2"); + const marker1 = data.markerPositions.get('marker1'); + const marker2 = data.markerPositions.get('marker2'); const results: number[] = []; state.goToEachMarker([marker1!, marker2!], m => { results.push(m.position); - }) + }); assert.deepEqual(results, [marker1!.position, marker2!.position]); @@ -209,13 +209,14 @@ test('Markers', () => { // @filename: file2.py ////class B: //// /*marker2*/pass - ` + `; const { data, state } = parseAndGetTestState(code); - const marker1 = data.markerPositions.get("marker1"); + const marker1 = data.markerPositions.get('marker1'); - assert.deepEqual(state.getMarkerName(marker1!), "marker1"); - assert.deepEqual(state.getMarkers().map(m => state.getMarkerName(m)).sort(compareStringsCaseSensitive), state.getMarkerNames().sort(comparePathsCaseSensitive)); + assert.deepEqual(state.getMarkerName(marker1!), 'marker1'); + assert.deepEqual(state.getMarkers().map(m => state.getMarkerName(m)).sort( + compareStringsCaseSensitive), state.getMarkerNames().sort(comparePathsCaseSensitive)); }); test('GoToPosition', () => { @@ -223,10 +224,10 @@ test('GoToPosition', () => { // @filename: file1.py ////class A: //// /*marker1*/pass - ` + `; const { data, state } = parseAndGetTestState(code); - const marker1 = data.markerPositions.get("marker1"); + const marker1 = data.markerPositions.get('marker1'); state.goToPosition(marker1!.position); assert.equal(state.currentCaretPosition, marker1!.position); @@ -240,17 +241,17 @@ test('select', () => { //// class B: //// def Test(self): //// pass -//// +//// //// def Test2(self): //// pass/*end*/ - ` + `; const { data, state } = parseAndGetTestState(code); - state.select("start", "end"); + state.select('start', 'end'); - assert.equal(state.currentCaretPosition, data.markerPositions.get("start")!.position); - assert.equal(state.selectionEnd, data.markerPositions.get("end")!.position); + assert.equal(state.currentCaretPosition, data.markerPositions.get('start')!.position); + assert.equal(state.selectionEnd, data.markerPositions.get('end')!.position); }); test('selectAllInFile', () => { @@ -260,16 +261,16 @@ test('selectAllInFile', () => { //// class B: //// def Test(self): //// pass -//// +//// //// def Test2(self): //// pass/*end*/ - ` + `; const { data, state } = parseAndGetTestState(code); state.selectAllInFile(data.files[0].fileName); - assert.equal(state.currentCaretPosition, data.markerPositions.get("start")!.position); - assert.equal(state.selectionEnd, data.markerPositions.get("end")!.position); + assert.equal(state.currentCaretPosition, data.markerPositions.get('start')!.position); + assert.equal(state.selectionEnd, data.markerPositions.get('end')!.position); }); test('selectRange', () => { @@ -279,10 +280,10 @@ test('selectRange', () => { //// class B: //// [|def Test(self): //// pass|] -//// +//// //// def Test2(self): //// pass - ` + `; const { data, state } = parseAndGetTestState(code); const range = data.ranges[0]; @@ -301,10 +302,10 @@ test('selectLine', () => { //// class B: ////[| def Test(self):|] //// pass -//// +//// //// def Test2(self): //// pass - ` + `; const { data, state } = parseAndGetTestState(code); const range = data.ranges[0]; @@ -322,10 +323,10 @@ test('goToEachRange', () => { //// class B: //// [|def Test(self):|] //// pass -//// +//// //// def Test2(self): //// [|pass|] - ` + `; const { state } = parseAndGetTestState(code); @@ -333,7 +334,7 @@ test('goToEachRange', () => { state.goToEachRange(r => { assert.equal(state.activeFile.fileName, r.fileName); results.push(r); - }) + }); assert.deepEqual(results, [state.getRanges()[0], state.getRanges()[1]]); }); @@ -349,7 +350,7 @@ test('getRangesInFile', () => { // @filename: file2.py //// def Test2(self): //// [|pass|] - ` + `; const { data, state } = parseAndGetTestState(code); @@ -367,13 +368,13 @@ test('rangesByText', () => { // @filename: file2.py //// def Test2(self): //// [|pass|] - ` + `; const { data, state } = parseAndGetTestState(code); const map = state.getRangesByText(); - assert.deepEqual(map.get("def Test(self):"), [data.ranges[0]]); - assert.deepEqual(map.get("pass"), [data.ranges[1]]); + assert.deepEqual(map.get('def Test(self):'), [data.ranges[0]]); + assert.deepEqual(map.get('pass'), [data.ranges[1]]); }); test('moveCaretRight', () => { @@ -383,13 +384,13 @@ test('moveCaretRight', () => { //// class B: //// /*position*/def Test(self): //// pass -//// +//// //// def Test2(self): //// pass - ` + `; const { data, state } = parseAndGetTestState(code); - const marker = data.markerPositions.get("position")!; + const marker = data.markerPositions.get('position')!; state.goToBOF(); assert.equal(state.currentCaretPosition, 0); @@ -398,9 +399,9 @@ test('moveCaretRight', () => { assert.equal(state.currentCaretPosition, data.files[0].content.length); state.goToPosition(marker.position); - state.moveCaretRight("def".length); + state.moveCaretRight('def'.length); - assert.equal(state.currentCaretPosition, marker.position + "def".length); + assert.equal(state.currentCaretPosition, marker.position + 'def'.length); assert.equal(state.selectionEnd, -1); }); @@ -413,14 +414,14 @@ test('runFourSlashTestContent', () => { //// class B: //// /*position*/def Test(self): //// pass -//// +//// //// def Test2(self): //// pass helper.getMarkerByName("position"); - ` + `; - runFourSlashTestContent(normalizeSlashes("/"), "unused.py", code); + runFourSlashTestContent(normalizeSlashes('/'), 'unused.py', code); }); test('VerifyDiagnosticsTest1', () => { @@ -429,12 +430,12 @@ test('VerifyDiagnosticsTest1', () => { // @filename: dataclass1.py //// # This sample validates the Python 3.7 data class feature. -//// +//// //// from typing import NamedTuple, Optional -//// +//// //// class Other: //// pass -//// +//// //// class DataTuple(NamedTuple): //// def _m(self): //// pass @@ -442,25 +443,25 @@ test('VerifyDiagnosticsTest1', () => { //// aid: Other //// valll: str = '' //// name: Optional[str] = None -//// +//// //// d1 = DataTuple(id=1, aid=Other()) //// d2 = DataTuple(id=1, aid=Other(), valll='v') //// d3 = DataTuple(id=1, aid=Other(), name='hello') //// d4 = DataTuple(id=1, aid=Other(), name=None) //// id = d1.id -//// +//// //// # This should generate an error because the name argument //// # is the incorrect type. //// d5 = DataTuple(id=1, aid=Other(), name=[|{|"category": "error"|}3|]) -//// +//// //// # This should generate an error because aid is a required //// # parameter and is missing an argument here. //// d6 = [|{|"category": "error"|}DataTuple(id=1, name=None|]) helper.verifyDiagnostics(); - ` + `; - runFourSlashTestContent(factory.srcFolder, "unused.py", code); + runFourSlashTestContent(factory.srcFolder, 'unused.py', code); }); test('VerifyDiagnosticsTest2', () => { @@ -468,49 +469,49 @@ test('VerifyDiagnosticsTest2', () => { /// //// # This sample tests the handling of the @dataclass decorator. -//// +//// //// from dataclasses import dataclass, InitVar -//// +//// //// @dataclass //// class Bar(): //// bbb: int //// ccc: str //// aaa = 'string' -//// +//// //// bar1 = Bar(bbb=5, ccc='hello') //// bar2 = Bar(5, 'hello') //// bar3 = Bar(5, 'hello', 'hello2') //// print(bar3.bbb) //// print(bar3.ccc) //// print(bar3.aaa) -//// +//// //// # This should generate an error because ddd //// # isn't a declared value. //// bar = Bar(bbb=5, [|/*marker1*/ddd|]=5, ccc='hello') -//// +//// //// # This should generate an error because the //// # parameter types don't match. //// bar = Bar([|/*marker2*/'hello'|], 'goodbye') -//// +//// //// # This should generate an error because a parameter //// # is missing. //// bar = [|/*marker3*/Bar(2)|] -//// +//// //// # This should generate an error because there are //// # too many parameters. //// bar = Bar(2, 'hello', 'hello', [|/*marker4*/4|]) -//// -//// +//// +//// //// @dataclass //// class Baz1(): //// bbb: int //// aaa = 'string' -//// +//// //// # This should generate an error because variables //// # with no default cannot come after those with //// # defaults. //// [|/*marker5*/ccc|]: str -//// +//// //// @dataclass //// class Baz2(): //// aaa: str @@ -523,14 +524,14 @@ helper.verifyDiagnostics({ "marker4": { category: "error", message: "Expected 3 positional arguments" }, "marker5": { category: "error", message: "Data fields without default value cannot appear after data fields with default values" }, }); - ` + `; - runFourSlashTestContent(factory.srcFolder, "unused.py", code); + runFourSlashTestContent(factory.srcFolder, 'unused.py', code); }); function parseAndGetTestState(code: string) { - const data = parseTestData(factory.srcFolder, code, "test.py"); - const state = new TestState(normalizeSlashes("/"), data); + const data = parseTestData(factory.srcFolder, code, 'test.py'); + const state = new TestState(normalizeSlashes('/'), data); return { data, state }; -} \ No newline at end of file +} diff --git a/server/src/workspaceMap.ts b/server/src/workspaceMap.ts new file mode 100644 index 000000000000..3f86b5dd6e27 --- /dev/null +++ b/server/src/workspaceMap.ts @@ -0,0 +1,65 @@ +/* + * workspaceMap.ts + * + * Workspace management related functionality. + */ + +import { LanguageServerBase, WorkspaceServiceInstance } from './languageServerBase'; + +export class WorkspaceMap extends Map { + private _defaultWorkspacePath = ''; + + constructor(private _ls: LanguageServerBase) { + super(); + } + + getWorkspaceForFile(filePath: string): WorkspaceServiceInstance { + let bestRootPath: string | undefined; + let bestInstance: WorkspaceServiceInstance | undefined; + + this.forEach(workspace => { + if (workspace.rootPath) { + // Is the file is under this workspace folder? + if (filePath.startsWith(workspace.rootPath)) { + // Is this the fist candidate? If not, is this workspace folder + // contained within the previous candidate folder? We always want + // to select the innermost folder, since that overrides the + // outer folders. + if (bestRootPath === undefined || workspace.rootPath.startsWith(bestRootPath)) { + bestRootPath = workspace.rootPath; + bestInstance = workspace; + } + } + } + }); + + // If there were multiple workspaces or we couldn't find any, + // create a default one to use for this file. + if (bestInstance === undefined) { + let defaultWorkspace = this.get(this._defaultWorkspacePath); + if (!defaultWorkspace) { + // If there is only one workspace, use that one. + const workspaceNames = [...this.keys()]; + if (workspaceNames.length === 1) { + return this.get(workspaceNames[0])!; + } + + // Create a default workspace for files that are outside + // of all workspaces. + defaultWorkspace = { + workspaceName: '', + rootPath: '', + rootUri: '', + serviceInstance: this._ls.createAnalyzerService(this._defaultWorkspacePath), + disableLanguageServices: false + }; + this.set(this._defaultWorkspacePath, defaultWorkspace); + this._ls.updateSettingsForWorkspace(defaultWorkspace).ignoreErrors(); + } + + return defaultWorkspace; + } + + return bestInstance; + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json index d5bf741c647a..6ab1e1cfa385 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -11,7 +11,8 @@ "sourceMap": true, "lib" : [ "es2016" ], "outDir": "../client/server", - "composite": true + "composite": true, + "declaration": true }, "exclude": [ "node_modules" From 146cd698e9cb2f35539079bff04cc070645fb9ab Mon Sep 17 00:00:00 2001 From: HeeJae Chang Date: Thu, 6 Feb 2020 16:52:48 -0800 Subject: [PATCH 2/4] fixed issues found on linux --- server/pyright/server/src/common/pathUtils.ts | 29 ++++++++++--------- .../server/src/tests/pathUtils.test.ts | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/server/pyright/server/src/common/pathUtils.ts b/server/pyright/server/src/common/pathUtils.ts index ba24a0a8abfb..e3cfe5b6ea20 100644 --- a/server/pyright/server/src/common/pathUtils.ts +++ b/server/pyright/server/src/common/pathUtils.ts @@ -9,12 +9,13 @@ import * as path from 'path'; import Char from 'typescript-char'; +import { URI } from 'vscode-uri'; import { some } from './collectionUtils'; import { compareValues, Comparison, GetCanonicalFileName, identity } from './core'; import * as debug from './debug'; -import { getStringComparer, equateStringsCaseInsensitive, equateStringsCaseSensitive, compareStringsCaseSensitive, compareStringsCaseInsensitive } from './stringUtils'; +import { compareStringsCaseInsensitive, compareStringsCaseSensitive, equateStringsCaseInsensitive, + equateStringsCaseSensitive, getStringComparer } from './stringUtils'; import { VirtualFileSystem } from './vfs'; -import { URI } from 'vscode-uri'; export interface FileSpec { // File specs can contain wildcard characters (**, *, ?). This @@ -503,8 +504,8 @@ export function getWildcardRegexPattern(rootPath: string, fileSpec: string): str const pathComponents = getPathComponents(absolutePath); const escapedSeparator = getRegexEscapedSeparator(); - const doubleAsteriskRegexFragment = `(${escapedSeparator}[^${escapedSeparator}.][^${escapedSeparator}]*)*?`; - const reservedCharacterPattern = new RegExp(`[^\\w\\s${escapedSeparator}]`, "g"); + const doubleAsteriskRegexFragment = `(${ escapedSeparator }[^${ escapedSeparator }.][^${ escapedSeparator }]*)*?`; + const reservedCharacterPattern = new RegExp(`[^\\w\\s${ escapedSeparator }]`, "g"); // Strip the directory separator from the root component. if (pathComponents.length > 0) { @@ -525,10 +526,11 @@ export function getWildcardRegexPattern(rootPath: string, fileSpec: string): str regExPattern += component.replace( reservedCharacterPattern, match => { if (match === '*') { - return `[^${escapedSeparator}]*`; + return `[^${ escapedSeparator }]*`; } else if (match === '?') { - return `[^${escapedSeparator}]`; + return `[^${ escapedSeparator }]`; } else { + // escaping anything that is not reserved characters - word/space/separator return '\\' + match; } }); @@ -580,7 +582,7 @@ export function getWildcardRoot(rootPath: string, fileSpec: string): string { export function getFileSpec(rootPath: string, fileSpec: string): FileSpec { let regExPattern = getWildcardRegexPattern(rootPath, fileSpec); const escapedSeparator = getRegexEscapedSeparator(); - regExPattern = `^(${regExPattern})($|${escapedSeparator})`; + regExPattern = `^(${ regExPattern })($|${ escapedSeparator })`; const regExp = new RegExp(regExPattern); const wildcardRoot = getWildcardRoot(rootPath, fileSpec); @@ -592,7 +594,8 @@ export function getFileSpec(rootPath: string, fileSpec: string): FileSpec { } export function getRegexEscapedSeparator() { - return path.sep === '/' ? '\\/' : '\\\\'; + // we don't need to escape "/" in typescript regular expression + return path.sep === '/' ? '/' : '\\\\'; } /** @@ -612,10 +615,6 @@ export function isDiskPathRoot(path: string) { } //// Path Comparisons - -// check path for these segments: '', '.'. '..' -const relativePathSegmentRegExp = /(^|\/)\.{0,2}($|\/)/; - function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) { if (a === b) return Comparison.EqualTo; if (a === undefined) return Comparison.LessThan; @@ -630,6 +629,10 @@ function comparePathsWorker(a: string, b: string, componentComparer: (a: string, return result; } + // check path for these segments: '', '.'. '..' + const escapedSeparator = getRegexEscapedSeparator(); + const relativePathSegmentRegExp = new RegExp(`(^|${escapedSeparator})\.{0,2}($|${escapedSeparator})`); + // NOTE: Performance optimization - shortcut if there are no relative path segments in // the non-root portion of the path const aRest = a.substring(aRoot.length); @@ -731,5 +734,5 @@ export function convertUriToPath(uriString: string): string { } export function convertPathToUri(path: string): string { - return URI.file(path).toString(); + return URI.file(path).toString(); } diff --git a/server/pyright/server/src/tests/pathUtils.test.ts b/server/pyright/server/src/tests/pathUtils.test.ts index bf57db36705c..2d4fc8613f82 100644 --- a/server/pyright/server/src/tests/pathUtils.test.ts +++ b/server/pyright/server/src/tests/pathUtils.test.ts @@ -184,7 +184,7 @@ test('comparePaths4', () => { }); test('comparePaths5', () => { - assert.equal(comparePaths('/a/b/c/', '/a/b/c'), Comparison.GreaterThan); + assert.equal(comparePaths('/a/b/c/', '/a/b/c'), Comparison.EqualTo); }); test('containsPath1', () => { From 45c5565857e31544362339921e96599753315379 Mon Sep 17 00:00:00 2001 From: HeeJae Chang Date: Thu, 6 Feb 2020 22:47:39 -0800 Subject: [PATCH 3/4] fixed eslint warnings --- server/pyright/server/src/common/pathUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pyright/server/src/common/pathUtils.ts b/server/pyright/server/src/common/pathUtils.ts index e3cfe5b6ea20..60249fafe696 100644 --- a/server/pyright/server/src/common/pathUtils.ts +++ b/server/pyright/server/src/common/pathUtils.ts @@ -631,7 +631,7 @@ function comparePathsWorker(a: string, b: string, componentComparer: (a: string, // check path for these segments: '', '.'. '..' const escapedSeparator = getRegexEscapedSeparator(); - const relativePathSegmentRegExp = new RegExp(`(^|${escapedSeparator})\.{0,2}($|${escapedSeparator})`); + const relativePathSegmentRegExp = new RegExp(`(^|${escapedSeparator}).{0,2}($|${escapedSeparator})`); // NOTE: Performance optimization - shortcut if there are no relative path segments in // the non-root portion of the path From 7899ec48aa0644ed859fff97ea36c2e958864d06 Mon Sep 17 00:00:00 2001 From: HeeJae Chang Date: Thu, 6 Feb 2020 23:06:00 -0800 Subject: [PATCH 4/4] make Jest to not hang even if there are some handles left open after test finished running. --- server/package.json | 2 +- server/pyright/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/package.json b/server/package.json index 7b3cd5802f3e..f3f30a598a3d 100644 --- a/server/package.json +++ b/server/package.json @@ -22,7 +22,7 @@ "eslint": "eslint ./**/*.ts", "tslint": "tslint --project tsconfig.json --fix", "watch": "tsc --watch", - "test": "jest", + "test": "jest --detectOpenHandles --forceExit", "test:all": "npm run test && cd pyright/server && npm run test" }, "dependencies": { diff --git a/server/pyright/server/package.json b/server/pyright/server/package.json index 84acfc77a7c0..2a6bd30d8d15 100644 --- a/server/pyright/server/package.json +++ b/server/pyright/server/package.json @@ -15,7 +15,7 @@ "build:cli": "node ./copyTypeshedFallback.js && npm run eslint && webpack --config webpack.config-cli.js", "eslint": "eslint src/**/*.ts", "watch": "tsc --watch", - "test": "jest" + "test": "jest --detectOpenHandles --forceExit" }, "dependencies": { "assert": "^2.0.0",