From 00db9d68d99cbc89797da9bbb849b51e0702ed84 Mon Sep 17 00:00:00 2001 From: HeeJae Chang Date: Wed, 12 Feb 2020 16:58:36 -0800 Subject: [PATCH] includes 3 changes. 1. added a hook to ImportResolver for custom importing logics. 2. added an ability to add library code directly in fourslash test file 3. added an ability to mount different folder as typeshed folder in virtual file system in fourslash test 4. added an ability to mount multiple folders in virtual file system --- server/.vscode/launch.json | 4 +- server/src/analyzer/importResolver.ts | 38 +++++-- server/src/analyzer/program.ts | 3 +- server/src/analyzer/pythonPathUtils.ts | 17 ++-- server/src/analyzer/service.ts | 14 ++- server/src/common/consts.ts | 11 +++ server/src/common/core.ts | 13 +++ server/src/common/pathUtils.ts | 99 ++++++++++--------- server/src/languageServerBase.ts | 8 +- server/src/server.ts | 3 +- server/src/tests/fourSlashParser.test.ts | 31 ++++-- .../fourslash/importnotresolved.fourslash.ts | 14 +++ .../fourslash/missingTypeStub.fourslash.ts | 20 ++++ .../harness/fourslash/fourSlashParser.ts | 24 +++-- .../tests/harness/fourslash/fourSlashTypes.ts | 9 +- server/src/tests/harness/fourslash/runner.ts | 13 ++- .../src/tests/harness/fourslash/testState.ts | 48 ++++++--- server/src/tests/harness/vfs/factory.ts | 56 ++++++++--- server/src/tests/pathUtils.test.ts | 27 ++--- server/src/tests/testState.test.ts | 20 +++- 20 files changed, 333 insertions(+), 139 deletions(-) create mode 100644 server/src/common/consts.ts create mode 100644 server/src/tests/fourslash/importnotresolved.fourslash.ts create mode 100644 server/src/tests/fourslash/missingTypeStub.fourslash.ts diff --git a/server/.vscode/launch.json b/server/.vscode/launch.json index 11bcdbd78cf6..ad031c0b29d0 100644 --- a/server/.vscode/launch.json +++ b/server/.vscode/launch.json @@ -34,13 +34,13 @@ "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest", } - } + }, { "type": "node", "name": "fourslash current file", "request": "launch", "args": [ - "fourslashrunner.test.ts", + "fourSlashRunner.test.ts", "-t ${fileBasenameNoExtension}", "--config", "jest.config.js" diff --git a/server/src/analyzer/importResolver.ts b/server/src/analyzer/importResolver.ts index 1028d170c955..22bc1e1d2135 100644 --- a/server/src/analyzer/importResolver.ts +++ b/server/src/analyzer/importResolver.ts @@ -16,10 +16,10 @@ import { } from '../common/pathUtils'; import { versionToString } from '../common/pythonVersion'; import * as StringUtils from '../common/stringUtils'; +import { VirtualFileSystem } from '../common/vfs'; import { ImplicitImport, ImportResult, ImportType } from './importResult'; import * as PythonPathUtils from './pythonPathUtils'; import { isDunderName } from './symbolNameUtils'; -import { VirtualFileSystem } from '../common/vfs'; export interface ImportedModuleDescriptor { leadingDots: number; @@ -95,7 +95,7 @@ export class ImportResolver { // Look for it in the root directory of the execution environment. importFailureInfo.push(`Looking in root directory of execution environment ` + `'${ execEnv.root }'`); - let localImport = this._resolveAbsoluteImport( + let localImport = this.resolveAbsoluteImport( execEnv.root, moduleDescriptor, importName, importFailureInfo); if (localImport && localImport.isImportFound) { return this._addResultsToCache(execEnv, importName, localImport, @@ -105,7 +105,7 @@ export class ImportResolver { for (const extraPath of execEnv.extraPaths) { importFailureInfo.push(`Looking in extraPath '${ extraPath }'`); - localImport = this._resolveAbsoluteImport(extraPath, moduleDescriptor, + localImport = this.resolveAbsoluteImport(extraPath, moduleDescriptor, importName, importFailureInfo); if (localImport && localImport.isImportFound) { return this._addResultsToCache(execEnv, importName, localImport, @@ -121,7 +121,7 @@ export class ImportResolver { // Check for a typings file. if (this._configOptions.typingsPath) { importFailureInfo.push(`Looking in typingsPath '${ this._configOptions.typingsPath }'`); - const typingsImport = this._resolveAbsoluteImport( + const typingsImport = this.resolveAbsoluteImport( this._configOptions.typingsPath, moduleDescriptor, importName, importFailureInfo); if (typingsImport && typingsImport.isImportFound) { // We will treat typings files as "local" rather than "third party". @@ -149,17 +149,20 @@ export class ImportResolver { // Allow partial resolution because some third-party packages // use tricks to populate their package namespaces. importFailureInfo.push(`Looking in python search path '${ searchPath }'`); - const thirdPartyImport = this._resolveAbsoluteImport( + const thirdPartyImport = this.resolveAbsoluteImport( searchPath, moduleDescriptor, importName, importFailureInfo, true, true, true); if (thirdPartyImport) { thirdPartyImport.importType = ImportType.ThirdParty; - if (thirdPartyImport.isImportFound) { + if (thirdPartyImport.isImportFound && thirdPartyImport.isStubFile) { return this._addResultsToCache(execEnv, importName, thirdPartyImport, moduleDescriptor.importedSymbols); } + // We did not find it, or we did and it's not from a + // stub, so give chance for resolveImportEx to find + // one from a stub. if (bestResultSoFar === undefined || thirdPartyImport.resolvedPaths.length > bestResultSoFar.resolvedPaths.length) { bestResultSoFar = thirdPartyImport; @@ -170,6 +173,12 @@ export class ImportResolver { importFailureInfo.push('No python interpreter search path'); } + const extraResults = this.resolveImportEx(sourceFilePath, execEnv, moduleDescriptor, importName, importFailureInfo); + if (extraResults !== undefined) { + return this._addResultsToCache(execEnv, importName, extraResults, + moduleDescriptor.importedSymbols); + } + // We weren't able to find an exact match, so return the best // partial match. if (bestResultSoFar) { @@ -193,6 +202,15 @@ export class ImportResolver { return this._addResultsToCache(execEnv, importName, notFoundResult, undefined); } + // Intended to be overridden by subclasses to provide additional stub + // resolving capabilities. Return undefined if no stubs were found for + // this import. + protected resolveImportEx(sourceFilePath: string, execEnv: ExecutionEnvironment, + moduleDescriptor: ImportedModuleDescriptor, importName: string, + importFailureInfo: string[] = []): ImportResult | undefined { + return undefined; + } + getCompletionSuggestions(sourceFilePath: string, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, similarityLimit: number): string[] { @@ -414,7 +432,7 @@ export class ImportResolver { minorVersion === 0 ? '3' : '2and3'; const testPath = combinePaths(typeshedPath, pythonVersionString); if (this.fileSystem.existsSync(testPath)) { - const importInfo = this._resolveAbsoluteImport(testPath, moduleDescriptor, + const importInfo = this.resolveAbsoluteImport(testPath, moduleDescriptor, importName, importFailureInfo); if (importInfo && importInfo.isImportFound) { importInfo.importType = isStdLib ? ImportType.BuiltIn : ImportType.ThirdParty; @@ -536,7 +554,7 @@ export class ImportResolver { } // Now try to match the module parts from the current directory location. - const absImport = this._resolveAbsoluteImport(curDir, moduleDescriptor, + const absImport = this.resolveAbsoluteImport(curDir, moduleDescriptor, importName, importFailureInfo); if (!absImport) { return undefined; @@ -565,7 +583,7 @@ export class ImportResolver { // Follows import resolution algorithm defined in PEP-420: // https://www.python.org/dev/peps/pep-0420/ - private _resolveAbsoluteImport(rootPath: string, moduleDescriptor: ImportedModuleDescriptor, + protected resolveAbsoluteImport(rootPath: string, moduleDescriptor: ImportedModuleDescriptor, importName: string, importFailureInfo: string[], allowPartial = false, allowPydFile = false, allowStubsFolder = false): ImportResult | undefined { @@ -893,3 +911,5 @@ export class ImportResolver { return name + moduleDescriptor.nameParts.map(part => part).join('.'); } } + +export type ImportResolverFactory = (fs: VirtualFileSystem, options: ConfigOptions) => ImportResolver; diff --git a/server/src/analyzer/program.ts b/server/src/analyzer/program.ts index 78eec32e4ca3..c8893115f0d5 100644 --- a/server/src/analyzer/program.ts +++ b/server/src/analyzer/program.ts @@ -7,7 +7,6 @@ * An object that tracks all of the source files being analyzed * and all of their recursive imports. */ - import * as assert from 'assert'; import { CompletionItem, CompletionList, DocumentSymbol, SymbolInformation } from 'vscode-languageserver'; @@ -20,6 +19,7 @@ import { combinePaths, getDirectoryPath, getRelativePath, makeDirectories, normalizePath, stripFileExtension } from '../common/pathUtils'; +import { DocumentRange, doRangesOverlap, Position, Range } from '../common/textRange'; import { Duration, timingStats } from '../common/timing'; import { ModuleSymbolMap } from '../languageService/completionProvider'; import { HoverResults } from '../languageService/hoverProvider'; @@ -34,7 +34,6 @@ import { SourceFile } from './sourceFile'; import { SymbolTable } from './symbol'; import { createTypeEvaluator, TypeEvaluator } from './typeEvaluator'; import { TypeStubWriter } from './typeStubWriter'; -import { Position, Range, DocumentRange, doRangesOverlap } from '../common/textRange'; const _maxImportDepth = 256; diff --git a/server/src/analyzer/pythonPathUtils.ts b/server/src/analyzer/pythonPathUtils.ts index b1033d3bb052..d72492545dc4 100644 --- a/server/src/analyzer/pythonPathUtils.ts +++ b/server/src/analyzer/pythonPathUtils.ts @@ -9,6 +9,7 @@ import * as child_process from 'child_process'; import { ConfigOptions } from '../common/configOptions'; +import * as consts from '../common/consts'; import { combinePaths, ensureTrailingDirectorySeparator, getDirectoryPath, getFileSystemEntries, isDirectory, normalizePath @@ -22,7 +23,7 @@ export function getTypeShedFallbackPath(moduleDirectory?: string) { moduleDirectory = normalizePath(moduleDirectory); return combinePaths(getDirectoryPath( ensureTrailingDirectorySeparator(moduleDirectory)), - 'typeshed-fallback'); + consts.TYPESHED_FALLBACK); } return undefined; @@ -49,14 +50,14 @@ export function findPythonSearchPaths(fs: VirtualFileSystem, configOptions: Conf } if (venvPath) { - let libPath = combinePaths(venvPath, 'lib'); + let libPath = combinePaths(venvPath, consts.LIB); if (fs.existsSync(libPath)) { - importFailureInfo.push(`Found path '${ libPath }'; looking for site-packages`); + importFailureInfo.push(`Found path '${ libPath }'; looking for ${ consts.SITE_PACKAGES }`); } else { importFailureInfo.push(`Did not find '${ libPath }'; trying 'Lib' instead`); libPath = combinePaths(venvPath, 'Lib'); if (fs.existsSync(libPath)) { - importFailureInfo.push(`Found path '${ libPath }'; looking for site-packages`); + importFailureInfo.push(`Found path '${ libPath }'; looking for ${ consts.SITE_PACKAGES }`); } else { importFailureInfo.push(`Did not find '${ libPath }'`); libPath = ''; @@ -64,7 +65,7 @@ export function findPythonSearchPaths(fs: VirtualFileSystem, configOptions: Conf } if (libPath) { - const sitePackagesPath = combinePaths(libPath, 'site-packages'); + const sitePackagesPath = combinePaths(libPath, consts.SITE_PACKAGES); if (fs.existsSync(sitePackagesPath)) { importFailureInfo.push(`Found path '${ sitePackagesPath }'`); return [sitePackagesPath]; @@ -74,11 +75,11 @@ export function findPythonSearchPaths(fs: VirtualFileSystem, configOptions: Conf // We didn't find a site-packages directory directly in the lib // directory. Scan for a "python*" directory instead. - const entries = getFileSystemEntries(this._fs, libPath); + const entries = getFileSystemEntries(fs, libPath); for (let i = 0; i < entries.directories.length; i++) { const dirName = entries.directories[i]; if (dirName.startsWith('python')) { - const dirPath = combinePaths(libPath, dirName, 'site-packages'); + const dirPath = combinePaths(libPath, dirName, consts.SITE_PACKAGES); if (fs.existsSync(dirPath)) { importFailureInfo.push(`Found path '${ dirPath }'`); return [dirPath]; @@ -89,7 +90,7 @@ export function findPythonSearchPaths(fs: VirtualFileSystem, configOptions: Conf } } - importFailureInfo.push(`Did not find site-packages. Falling back on python interpreter.`); + importFailureInfo.push(`Did not find '${ consts.SITE_PACKAGES }'. Falling back on python interpreter.`); } // Fall back on the python interpreter. diff --git a/server/src/analyzer/service.ts b/server/src/analyzer/service.ts index 49166682f8e5..49a6d47cdcfa 100644 --- a/server/src/analyzer/service.ts +++ b/server/src/analyzer/service.ts @@ -26,7 +26,7 @@ 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 { ImportedModuleDescriptor, ImportResolver, ImportResolverFactory } from './importResolver'; import { MaxAnalysisTime, Program } from './program'; import * as PythonPathUtils from './pythonPathUtils'; @@ -50,6 +50,7 @@ export class AnalyzerService { private _instanceName: string; private _program: Program; private _configOptions: ConfigOptions; + private _importResolverFactory: ImportResolverFactory; private _importResolver: ImportResolver; private _executionRootPath: string; private _typeStubTargetImportName: string | undefined; @@ -68,11 +69,12 @@ export class AnalyzerService { private _requireTrackedFileUpdate = true; private _lastUserInteractionTime = Date.now(); - constructor(instanceName: string, fs: VirtualFileSystem, console?: ConsoleInterface, configOptions?: ConfigOptions) { + constructor(instanceName: string, fs: VirtualFileSystem, console?: ConsoleInterface, importResolverFactory?: ImportResolverFactory, configOptions?: ConfigOptions) { this._instanceName = instanceName; this._console = console || new StandardConsole(); this._configOptions = configOptions ?? new ConfigOptions(process.cwd()); - this._importResolver = new ImportResolver(fs, this._configOptions); + this._importResolverFactory = importResolverFactory || AnalyzerService.createImportResolver; + this._importResolver = this._importResolverFactory(fs, this._configOptions); this._program = new Program(this._importResolver, this._configOptions, this._console); this._executionRootPath = ''; this._typeStubTargetImportName = undefined; @@ -85,6 +87,10 @@ export class AnalyzerService { this._clearReanalysisTimer(); } + static createImportResolver(fs: VirtualFileSystem, options: ConfigOptions): ImportResolver { + return new ImportResolver(fs, options); + } + setCompletionCallback(callback: AnalysisCompleteCallback | undefined): void { this._onCompletionCallback = callback; } @@ -815,7 +821,7 @@ export class AnalyzerService { private _applyConfigOptions() { // Allocate a new import resolver because the old one has information // cached based on the previous config options. - this._importResolver = new ImportResolver(this._fs, this._configOptions); + this._importResolver = this._importResolverFactory(this._fs, this._configOptions); this._program.setImportResolver(this._importResolver); this._updateSourceFileWatchers(); diff --git a/server/src/common/consts.ts b/server/src/common/consts.ts new file mode 100644 index 000000000000..e7f95092e38d --- /dev/null +++ b/server/src/common/consts.ts @@ -0,0 +1,11 @@ +/* + * consts.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Defines well known consts and names + */ + +export const TYPESHED_FALLBACK = 'typeshed-fallback'; +export const LIB = 'lib'; +export const SITE_PACKAGES = 'site-packages'; diff --git a/server/src/common/core.ts b/server/src/common/core.ts index 00f171711884..ec8573c99e42 100644 --- a/server/src/common/core.ts +++ b/server/src/common/core.ts @@ -95,3 +95,16 @@ export interface MapLike { export function hasProperty(map: MapLike, key: string): boolean { return hasOwnProperty.call(map, key); } + +/** + * Convert the given value to boolean + * @param trueOrFalse string value 'true' or 'false' + */ +export function toBoolean(trueOrFalse: string): boolean { + const normalized = trueOrFalse?.trim().toUpperCase(); + if (normalized === 'TRUE') { + return true; + } + + return false; +} diff --git a/server/src/common/pathUtils.ts b/server/src/common/pathUtils.ts index 60249fafe696..8616ad96114b 100644 --- a/server/src/common/pathUtils.ts +++ b/server/src/common/pathUtils.ts @@ -13,8 +13,10 @@ import { URI } from 'vscode-uri'; import { some } from './collectionUtils'; import { compareValues, Comparison, GetCanonicalFileName, identity } from './core'; import * as debug from './debug'; -import { compareStringsCaseInsensitive, compareStringsCaseSensitive, equateStringsCaseInsensitive, - equateStringsCaseSensitive, getStringComparer } from './stringUtils'; +import { + compareStringsCaseInsensitive, compareStringsCaseSensitive, equateStringsCaseInsensitive, + equateStringsCaseSensitive, getStringComparer +} from './stringUtils'; import { VirtualFileSystem } from './vfs'; export interface FileSpec { @@ -81,7 +83,7 @@ export function getPathComponents(pathString: string) { } export function reducePathComponents(components: readonly string[]) { - if (!some(components)) return []; + if (!some(components)) { return []; } // Reduce the path components by eliminating // any '.' or '..'. @@ -109,7 +111,7 @@ export function reducePathComponents(components: readonly string[]) { } export function combinePathComponents(components: string[]): string { - if (components.length === 0) return ""; + if (components.length === 0) { return ''; } const root = components[0] && ensureTrailingDirectorySeparator(components[0]); return normalizeSlashes(root + components.slice(1).join(path.sep)); @@ -155,8 +157,7 @@ export function getFileSize(fs: VirtualFileSystem, path: string) { if (stat.isFile()) { return stat.size; } - } - catch { /*ignore*/ } + } catch { /*ignore*/ } return 0; } @@ -218,11 +219,10 @@ export function comparePaths(a: string, b: string, currentDirectory?: string | b a = normalizePath(a); b = normalizePath(b); - if (typeof currentDirectory === "string") { + if (typeof currentDirectory === 'string') { a = combinePaths(currentDirectory, a); b = combinePaths(currentDirectory, b); - } - else if (typeof currentDirectory === "boolean") { + } else if (typeof currentDirectory === 'boolean') { ignoreCase = currentDirectory; } return comparePathsWorker(a, b, getStringComparer(ignoreCase)); @@ -234,16 +234,15 @@ export function comparePaths(a: string, b: string, currentDirectory?: string | b export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean; export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean; export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { - if (typeof currentDirectory === "string") { + if (typeof currentDirectory === 'string') { parent = combinePaths(currentDirectory, parent); child = combinePaths(currentDirectory, child); - } - else if (typeof currentDirectory === "boolean") { + } else if (typeof currentDirectory === 'boolean') { ignoreCase = currentDirectory; } - if (parent === undefined || child === undefined) return false; - if (parent === child) return true; + if (parent === undefined || child === undefined) { return false; } + if (parent === child) { return true; } const parentComponents = getPathComponents(parent); const childComponents = getPathComponents(child); @@ -283,8 +282,10 @@ export function changeAnyExtension(path: string, ext: string): string; */ export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string; export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean): string { - const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); - return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path; + const pathext = extensions !== undefined && ignoreCase !== undefined ? + getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); + + return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith('.') ? ext : '.' + ext) : path; } /** @@ -312,14 +313,15 @@ export function getAnyExtensionFromPath(path: string, extensions?: string | read // Retrieves any string from the final "." onwards from a base file name. // Unlike extensionFromPath, which throws an exception on unrecognized extensions. if (extensions) { - return getAnyExtensionFromPathWorker(stripTrailingDirectorySeparator(path), extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); + return getAnyExtensionFromPathWorker(stripTrailingDirectorySeparator(path), extensions, + ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); } const baseFileName = getBaseFileName(path); - const extensionIndex = baseFileName.lastIndexOf("."); + const extensionIndex = baseFileName.lastIndexOf('.'); if (extensionIndex >= 0) { return baseFileName.substring(extensionIndex); } - return ""; + return ''; } /** @@ -357,13 +359,15 @@ export function getBaseFileName(pathString: string, extensions?: string | readon // if the path provided is itself the root, then it has not file name. const rootLength = getRootLength(pathString); - if (rootLength === pathString.length) return ""; + if (rootLength === pathString.length) { return ''; } // return the trailing portion of the path starting after the last (non-terminal) directory // separator but not including any trailing directory separator. pathString = stripTrailingDirectorySeparator(pathString); const name = pathString.slice(Math.max(getRootLength(pathString), pathString.lastIndexOf(path.sep) + 1)); - const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined; + const extension = extensions !== undefined && ignoreCase !== undefined ? + getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined; + return extension ? name.slice(0, name.length - extension.length) : name; } @@ -375,11 +379,15 @@ export function getRelativePathFromDirectory(from: string, to: string, ignoreCas * Gets a relative path that can be used to traverse between `from` and `to`. */ export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; -export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { - debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); - const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity; - const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; - const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); +export function getRelativePathFromDirectory(fromDirectory: string, to: string, + getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { + + debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), 'Paths must either both be absolute or both be relative'); + const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === 'function' ? getCanonicalFileNameOrIgnoreCase : identity; + const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === 'boolean' ? getCanonicalFileNameOrIgnoreCase : false; + const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? + equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); + return combinePathComponents(pathComponents); } @@ -505,7 +513,7 @@ export function getWildcardRegexPattern(rootPath: string, fileSpec: string): str const escapedSeparator = getRegexEscapedSeparator(); const doubleAsteriskRegexFragment = `(${ escapedSeparator }[^${ escapedSeparator }.][^${ escapedSeparator }]*)*?`; - const reservedCharacterPattern = new RegExp(`[^\\w\\s${ escapedSeparator }]`, "g"); + const reservedCharacterPattern = new RegExp(`[^\\w\\s${ escapedSeparator }]`, 'g'); // Strip the directory separator from the root component. if (pathComponents.length > 0) { @@ -616,9 +624,9 @@ export function isDiskPathRoot(path: string) { //// Path Comparisons function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) { - if (a === b) return Comparison.EqualTo; - if (a === undefined) return Comparison.LessThan; - if (b === undefined) return Comparison.GreaterThan; + if (a === b) { return Comparison.EqualTo; } + if (a === undefined) { return Comparison.LessThan; } + if (b === undefined) { return Comparison.GreaterThan; } // NOTE: Performance optimization - shortcut if the root segments differ as there would be no // need to perform path reduction. @@ -631,7 +639,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 @@ -656,19 +664,21 @@ function comparePathsWorker(a: string, b: string, componentComparer: (a: string, return compareValues(aComponents.length, bComponents.length); } -function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) { - if (typeof extensions === "string") { - return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ""; +function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], + stringEqualityComparer: (a: string, b: string) => boolean) { + + if (typeof extensions === 'string') { + return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ''; } for (const extension of extensions) { const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer); - if (result) return result; + if (result) { return result; } } - return ""; + return ''; } function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) { - if (!extension.startsWith(".")) extension = "." + extension; + if (!extension.startsWith('.')) { extension = '.' + extension; } if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === Char.Period) { const pathExtension = path.slice(path.length - extension.length); if (stringEqualityComparer(pathExtension, extension)) { @@ -679,7 +689,9 @@ function tryGetExtensionFromPath(path: string, extension: string, stringEquality return undefined; } -function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { +function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, + getCanonicalFileName: GetCanonicalFileName) { + const fromComponents = getPathComponents(from); const toComponents = getPathComponents(to); @@ -688,7 +700,7 @@ function getPathComponentsRelativeTo(from: string, to: string, stringEqualityCom const fromComponent = getCanonicalFileName(fromComponents[start]); const toComponent = getCanonicalFileName(toComponents[start]); const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer; - if (!comparer(fromComponent, toComponent)) break; + if (!comparer(fromComponent, toComponent)) { break; } } if (start === 0) { @@ -698,14 +710,14 @@ function getPathComponentsRelativeTo(from: string, to: string, stringEqualityCom const components = toComponents.slice(start); const relative: string[] = []; for (; start < fromComponents.length; start++) { - relative.push(".."); + relative.push('..'); } - return ["", ...relative, ...components]; + return ['', ...relative, ...components]; } const enum FileSystemEntryKind { File, - Directory, + Directory } function fileSystemEntryExists(fs: VirtualFileSystem, path: string, entryKind: FileSystemEntryKind): boolean { @@ -716,8 +728,7 @@ function fileSystemEntryExists(fs: VirtualFileSystem, path: string, entryKind: F case FileSystemEntryKind.Directory: return stat.isDirectory(); default: return false; } - } - catch (e) { + } catch (e) { return false; } } diff --git a/server/src/languageServerBase.ts b/server/src/languageServerBase.ts index 1f6ae7a30c07..4cd8eb17171a 100644 --- a/server/src/languageServerBase.ts +++ b/server/src/languageServerBase.ts @@ -12,8 +12,10 @@ import { ParameterInformation, RemoteConsole, SignatureInformation, SymbolInformation, TextDocuments, TextEdit, WorkspaceEdit } from 'vscode-languageserver'; +import { ImportResolver } from './analyzer/importResolver'; import { AnalyzerService } from './analyzer/service'; import { CommandLineOptions } from './common/commandLineOptions'; +import { ConfigOptions } from './common/configOptions'; import { Diagnostic as AnalyzerDiagnostic, DiagnosticCategory } from './common/diagnostic'; import './common/extensions'; import { combinePaths, convertPathToUri, convertUriToPath, normalizePath } from './common/pathUtils'; @@ -92,11 +94,15 @@ export abstract class LanguageServerBase { return this.connection.workspace.getConfiguration(item); } + protected createImportResolver(fs: VirtualFileSystem, options: ConfigOptions): ImportResolver { + return new ImportResolver(fs, options); + } + // Creates a service instance that's used for analyzing a // program within a workspace. createAnalyzerService(name: string): AnalyzerService { this.connection.console.log(`Starting service instance "${ name }"`); - const service = new AnalyzerService(name, this.fs, this.connection.console); + const service = new AnalyzerService(name, this.fs, this.connection.console, this.createImportResolver); // Don't allow the analysis engine to go too long without // reporting results. This will keep it responsive. diff --git a/server/src/server.ts b/server/src/server.ts index 5e95c17505f6..77eed84144f9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import { isArray } from 'util'; import { CodeAction, CodeActionParams, Command, ExecuteCommandParams } from 'vscode-languageserver'; import { CommandController } from './commands/commandController'; +import * as consts from './common/consts'; import * as debug from './common/debug'; import { convertUriToPath, getDirectoryPath, normalizeSlashes } from './common/pathUtils'; import { LanguageServerBase, ServerSettings, WorkspaceServiceInstance } from './languageServerBase'; @@ -27,7 +28,7 @@ class Server extends LanguageServerBase { // 1. to find "typeshed-fallback" folder. // 2. to set "cwd" to run python to find search path. const rootDirectory = getDirectoryPath(__dirname); - debug.assert(fs.existsSync(path.join(rootDirectory, 'typeshed-fallback')), `Unable to locate typeshed fallback folder at '${ rootDirectory }'`); + debug.assert(fs.existsSync(path.join(rootDirectory, consts.TYPESHED_FALLBACK)), `Unable to locate typeshed fallback folder at '${ rootDirectory }'`); super('Pyright', rootDirectory); this._controller = new CommandController(this); diff --git a/server/src/tests/fourSlashParser.test.ts b/server/src/tests/fourSlashParser.test.ts index 48576267fc8a..7abfbab2e0ac 100644 --- a/server/src/tests/fourSlashParser.test.ts +++ b/server/src/tests/fourSlashParser.test.ts @@ -8,7 +8,7 @@ */ import * as assert from 'assert'; -import { getBaseFileName, normalizeSlashes } from '../common/pathUtils'; +import { combinePaths, getBaseFileName, normalizeSlashes } from '../common/pathUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; import { parseTestData } from './harness/fourslash/fourSlashParser'; import { CompilerSettings } from './harness/fourslash/fourSlashTypes'; @@ -55,18 +55,34 @@ test('Filename', () => { }); test('Extra file options', () => { - // filename must be last file options + // filename must be the first file options const code = ` -// @reserved: not used // @filename: file1.py +// @library: false ////class A: //// pass `; const data = parseTestData('.', code, 'test.py'); + + assert.equal(data.files[0].fileName, normalizeSlashes('./file1.py')); + assertOptions(data.globalOptions, []); + assertOptions(data.files[0].fileOptions, [['filename', 'file1.py'], ['library', 'false']]); +}); - assertOptions(data.files[0].fileOptions, [['filename', 'file1.py'], ['reserved', 'not used']]); +test('Library options', () => { + // filename must be the first file options + const code = ` +// @filename: file1.py +// @library: true +////class A: +//// pass + `; + + const data = parseTestData('.', code, 'test.py'); + + assert.equal(data.files[0].fileName, normalizeSlashes(combinePaths(factory.libFolder, 'file1.py'))); }); test('Range', () => { @@ -180,10 +196,12 @@ test('Multiple Files', () => { // range can have 1 marker in it const code = ` // @filename: src/A.py +// @library: false ////class A: //// pass // @filename: src/B.py +// @library: true ////class B: //// pass @@ -196,7 +214,8 @@ test('Multiple Files', () => { 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(combinePaths(factory.libFolder, 'src/B.py')))[0].content, getContent('B')); assert.equal(data.files.filter(f => f.fileName === normalizeSlashes('./src/C.py'))[0].content, getContent('C')); }); @@ -281,7 +300,7 @@ test('fourSlashWithFileSystem', () => { }); function getContent(className: string) { - return `class ${className}: + return `class ${ className }: pass`; } diff --git a/server/src/tests/fourslash/importnotresolved.fourslash.ts b/server/src/tests/fourslash/importnotresolved.fourslash.ts new file mode 100644 index 000000000000..3dd1e0c28cfe --- /dev/null +++ b/server/src/tests/fourslash/importnotresolved.fourslash.ts @@ -0,0 +1,14 @@ +/// + +// @filename: importnotresolved.py +//// # these will not be resolve, no typestubs for django in typeshed +//// +//// import [|/*marker1*/notexistant|] +//// import [|/*marker2*/django|] +//// + + +helper.verifyDiagnostics({ + "marker1": { category: "error", message: "Import 'notexistant' could not be resolved" }, + "marker2": { category: "error", message: "Import 'django' could not be resolved" }, +}); \ No newline at end of file diff --git a/server/src/tests/fourslash/missingTypeStub.fourslash.ts b/server/src/tests/fourslash/missingTypeStub.fourslash.ts new file mode 100644 index 000000000000..9a75038d86d9 --- /dev/null +++ b/server/src/tests/fourslash/missingTypeStub.fourslash.ts @@ -0,0 +1,20 @@ +/// + +// @filename: mspythonconfig.json +//// { +//// "reportMissingTypeStubs": "warning" +//// } + +// @filename: testLib/__init__.py +// @library: true +//// # This is a library file +//// class MyLibrary: +//// def DoEveryThing(self, code: str): +//// pass + +// @filename: test.py +//// import [|/*marker*/testLi|]b + +helper.verifyDiagnostics({ + 'marker': { category: 'warning', message: 'Stub file not found for \'testLib\'' } +}); diff --git a/server/src/tests/harness/fourslash/fourSlashParser.ts b/server/src/tests/harness/fourslash/fourSlashParser.ts index 5c07f133337d..c705584679bf 100644 --- a/server/src/tests/harness/fourslash/fourSlashParser.ts +++ b/server/src/tests/harness/fourslash/fourSlashParser.ts @@ -7,7 +7,9 @@ */ import { contains } from '../../../common/collectionUtils'; -import { combinePaths, isRootedDiskPath, normalizeSlashes } from '../../../common/pathUtils'; +import { toBoolean } from '../../../common/core'; +import { combinePaths, getRelativePath, isRootedDiskPath, normalizePath, normalizeSlashes } from '../../../common/pathUtils'; +import { libFolder } from '../vfs/factory'; import { fileMetadataNames, FourSlashData, FourSlashFile, Marker, MetadataOptionNames, Range } from './fourSlashTypes'; /** @@ -48,6 +50,10 @@ export function parseTestData(basePath: string, contents: string, fileName: stri function nextFile() { if (currentFileContent === undefined) { return; } + if (toBoolean(currentFileOptions[MetadataOptionNames.library])) { + currentFileName = normalizePath(combinePaths(libFolder, getRelativePath(currentFileName, normalizedBasePath))); + } + const file = parseFileContent(currentFileContent, currentFileName, markerPositions, markers, ranges); file.fileOptions = currentFileOptions; @@ -85,14 +91,14 @@ export function parseTestData(basePath: string, contents: string, fileName: stri } else { switch (key) { 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); - currentFileOptions[key] = value; - break; - } + // 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); + currentFileOptions[key] = value; + break; + } default: // Add other fileMetadata flag currentFileOptions[key] = value; diff --git a/server/src/tests/harness/fourslash/fourSlashTypes.ts b/server/src/tests/harness/fourslash/fourSlashTypes.ts index 9b60dedb3e89..dc0ab73e12e6 100644 --- a/server/src/tests/harness/fourslash/fourSlashTypes.ts +++ b/server/src/tests/harness/fourslash/fourSlashTypes.ts @@ -8,22 +8,23 @@ import * as debug from '../../../common/debug'; /** setting file name */ -export const pythonSettingFilename = 'python.json'; +export const pythonSettingFilename = 'mspythonconfig.json'; /** well known global option names */ export const enum GlobalMetadataOptionNames { projectRoot = 'projectroot', - ignoreCase = 'ignorecase' + ignoreCase = 'ignorecase', + typeshed = 'typeshed' } /** Any option name not belong to this will become global option */ export const enum MetadataOptionNames { fileName = 'filename', - reserved = 'reserved' + library = 'library' } /** List of allowed file metadata names */ -export const fileMetadataNames = [MetadataOptionNames.fileName, MetadataOptionNames.reserved]; +export const fileMetadataNames = [MetadataOptionNames.fileName, MetadataOptionNames.library]; /** all the necessary information to set the right compiler settings */ export interface CompilerSettings { diff --git a/server/src/tests/harness/fourslash/runner.ts b/server/src/tests/harness/fourslash/runner.ts index 311c40c0c799..1c5865fe2fcd 100644 --- a/server/src/tests/harness/fourslash/runner.ts +++ b/server/src/tests/harness/fourslash/runner.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; +import { ImportResolverFactory } from '../../../analyzer/importResolver'; import { combinePaths } from '../../../common/pathUtils'; import * as host from '../host'; import { parseTestData } from './fourSlashParser'; @@ -18,9 +19,11 @@ import { TestState } from './testState'; * @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) { +export function runFourSlashTest(basePath: string, fileName: string, + mountPaths?: Map, importResolverFactory?: ImportResolverFactory) { + const content = (host.HOST.readFile(fileName)!); - runFourSlashTestContent(basePath, fileName, content); + runFourSlashTestContent(basePath, fileName, content, mountPaths, importResolverFactory); } /** @@ -31,14 +34,16 @@ export function runFourSlashTest(basePath: string, fileName: string) { * if fourslash markup `content` doesn't have explicit `@filename` option * @param content this is fourslash markup string */ -export function runFourSlashTestContent(basePath: string, fileName: string, content: string) { +export function runFourSlashTestContent(basePath: string, fileName: string, content: string, + mountPaths?: Map, importResolverFactory?: ImportResolverFactory) { + // give file paths an absolute path for the virtual file system const absoluteBasePath = combinePaths('/', basePath); const absoluteFileName = combinePaths('/', fileName); // parse out the files and their metadata const testData = parseTestData(absoluteBasePath, content, absoluteFileName); - const state = new TestState(absoluteBasePath, testData); + const state = new TestState(absoluteBasePath, testData, mountPaths, importResolverFactory); const output = ts.transpileModule(content, { reportDiagnostics: true, compilerOptions: { target: ts.ScriptTarget.ES2015 } }); if (output.diagnostics!.length > 0) { throw new Error(`Syntax error in ${ absoluteBasePath }: ${ output.diagnostics![0].messageText }`); diff --git a/server/src/tests/harness/fourslash/testState.ts b/server/src/tests/harness/fourslash/testState.ts index a992cb9e0d73..a16d93bf9931 100644 --- a/server/src/tests/harness/fourslash/testState.ts +++ b/server/src/tests/harness/fourslash/testState.ts @@ -10,11 +10,12 @@ import * as assert from 'assert'; import * as path from 'path'; import Char from 'typescript-char'; -import { ImportResolver } from '../../../analyzer/importResolver'; +import { ImportResolver, ImportResolverFactory } from '../../../analyzer/importResolver'; import { Program } from '../../../analyzer/program'; +import { AnalyzerService } from '../../../analyzer/service'; import { ConfigOptions } from '../../../common/configOptions'; import { NullConsole } from '../../../common/console'; -import { Comparison, isNumber, isString } from '../../../common/core'; +import { Comparison, isNumber, isString, toBoolean } from '../../../common/core'; import * as debug from '../../../common/debug'; import { DiagnosticCategory } from '../../../common/diagnostic'; import { combinePaths, comparePaths, getBaseFileName, normalizePath, normalizeSlashes } from '../../../common/pathUtils'; @@ -25,8 +26,10 @@ 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'; +import { + CompilerSettings, FourSlashData, FourSlashFile, GlobalMetadataOptionNames, Marker, + MetadataOptionNames, MultiMap, pythonSettingFilename, Range, TestCancellationToken +} from './fourSlashTypes'; export interface TextChange { span: TextRange; @@ -52,13 +55,16 @@ export class TestState { // The file that's currently 'opened' activeFile!: FourSlashFile; - constructor(private _basePath: string, public testData: FourSlashData) { + constructor(private _basePath: string, public testData: FourSlashData, mountPaths?: Map, + importResolverFactory?: ImportResolverFactory) { + const strIgnoreCase = GlobalMetadataOptionNames.ignoreCase; const ignoreCase = testData.globalOptions[strIgnoreCase]?.toUpperCase() === 'TRUE'; this._cancellationToken = new TestCancellationToken(); const configOptions = this._convertGlobalOptionsToConfigOptions(this.testData.globalOptions); + const sourceFiles = []; const files: vfs.FileSet = {}; for (const file of testData.files) { // if one of file is configuration file, set config options from the given json @@ -71,17 +77,24 @@ export class TestState { } configOptions.initializeFromJson(configJson, new NullConsole()); + this._applyTestConfigOptions(configOptions); } else { files[file.fileName] = new vfs.File(file.content, { meta: file.fileOptions, encoding: 'utf8' }); + + if (!toBoolean(file.fileOptions[MetadataOptionNames.library])) { + sourceFiles.push(file.fileName); + } } } - const fs = createFromFileSystem(host.HOST, ignoreCase, { cwd: _basePath, files, meta: testData.globalOptions }); + const fs = createFromFileSystem(host.HOST, ignoreCase, { cwd: _basePath, files, meta: testData.globalOptions }, mountPaths); + + importResolverFactory = importResolverFactory || AnalyzerService.createImportResolver; // this should be change to AnalyzerService rather than Program - const importResolver = new ImportResolver(fs, configOptions); + const importResolver = importResolverFactory(fs, configOptions); const program = new Program(importResolver, configOptions); - program.setTrackedFiles(Object.keys(files)); + program.setTrackedFiles(sourceFiles); // make sure these states are consistent between these objects. // later make sure we just hold onto AnalyzerService and get all these @@ -90,7 +103,7 @@ export class TestState { this.configOptions = configOptions; this.importResolver = importResolver; this.program = program; - this._files.push(...Object.keys(files)); + this._files = sourceFiles; if (this._files.length > 0) { // Open the first file by default @@ -361,7 +374,7 @@ export class TestState { // expected number of files if (resultPerFile.size !== rangePerFile.size) { - this._raiseError(`actual and expected doesn't match - expected: ${ stringify(rangePerFile) }, actual: ${ stringify(rangePerFile) }`); + this._raiseError(`actual and expected doesn't match - expected: ${ stringify(resultPerFile) }, actual: ${ stringify(rangePerFile) }`); } for (const [file, ranges] of rangePerFile.entries()) { @@ -384,11 +397,12 @@ export class TestState { } for (const range of ranges) { - const rangeSpan = TextRange.fromBounds(range.pos, range.end); + const rangeSpan = TextRange.fromBounds(range.pos, range.end); const matches = actual.filter(d => { const diagnosticSpan = TextRange.fromBounds(convertPositionToOffset(d.range.start, lines)!, convertPositionToOffset(d.range.end, lines)!); - return this._deepEqual(diagnosticSpan, rangeSpan); }); + return this._deepEqual(diagnosticSpan, rangeSpan); + }); if (matches.length === 0) { this._raiseError(`doesn't contain expected range: ${ stringify(range) }`); @@ -461,8 +475,18 @@ export class TestState { // add more global options as we need them + return this._applyTestConfigOptions(configOptions); + } + + private _applyTestConfigOptions(configOptions: ConfigOptions) { // Always enable "test mode". configOptions.internalTestMode = true; + + // run test in venv mode under root so that + // under test we can point to local lib folder + configOptions.venvPath = vfs.MODULE_PATH; + configOptions.defaultVenv = vfs.MODULE_PATH; + return configOptions; } diff --git a/server/src/tests/harness/vfs/factory.ts b/server/src/tests/harness/vfs/factory.ts index 41dde0e1ed33..047ccec4aa8c 100644 --- a/server/src/tests/harness/vfs/factory.ts +++ b/server/src/tests/harness/vfs/factory.ts @@ -6,10 +6,12 @@ * Provides a factory to create virtual file system backed by a real file system with some path remapped */ +import * as consts from '../../../common/consts'; import { combinePaths, getDirectoryPath, normalizeSlashes, resolvePaths } from '../../../common/pathUtils'; +import { GlobalMetadataOptionNames } from '../fourslash/fourSlashTypes'; import { TestHost } from '../host'; import { bufferFrom } from '../utils'; -import { FileSystem, FileSystemOptions, FileSystemResolver, MODULE_PATH, Mount, S_IFDIR, S_IFREG } from './filesystem'; +import { FileSet, FileSystem, FileSystemOptions, FileSystemResolver, MODULE_PATH, Mount, S_IFDIR, S_IFREG } from './filesystem'; export class TextDocument { readonly meta: Map; @@ -28,13 +30,14 @@ export interface FileSystemCreateOptions extends FileSystemOptions { documents?: readonly TextDocument[]; } -export const typeshedFolder = combinePaths(MODULE_PATH, normalizeSlashes('typeshed-fallback')); +export const libFolder = combinePaths(MODULE_PATH, normalizeSlashes(combinePaths(consts.LIB, consts.SITE_PACKAGES))); +export const typeshedFolder = combinePaths(MODULE_PATH, normalizeSlashes(consts.TYPESHED_FALLBACK)); export const srcFolder = normalizeSlashes('/.src'); /** * Create a virtual file system from a physical file system using the following path mappings: * - * - `/typeshed-fallback` is a directory mapped to `${workspaceRoot}/../dist/typeshed-fallback` + * - `/typeshed-fallback` is a directory mapped to `${workspaceRoot}/../client/typeshed-fallback` * - `/.src` is a virtual directory to be used for tests. * * @param host it provides an access to host (real) file system @@ -48,9 +51,15 @@ export const srcFolder = normalizeSlashes('/.src'); * all `FileSystemCreateOptions` are optional */ export function createFromFileSystem(host: TestHost, ignoreCase: boolean, - { documents, files, cwd, time, meta }: FileSystemCreateOptions = {}) { + { documents, files, cwd, time, meta }: FileSystemCreateOptions = {}, + mountPaths: Map = new Map()) { - const fs = getBuiltLocal(host, meta ? meta[typeshedFolder] : undefined, ignoreCase).shadow(); + const typeshedPath = meta ? meta[GlobalMetadataOptionNames.typeshed] : undefined; + if (typeshedPath) { + mountPaths.set(typeshedFolder, typeshedPath); + } + + const fs = getBuiltLocal(host, ignoreCase, mountPaths).shadow(); if (meta) { for (const key of Object.keys(meta)) { fs.meta.set(key, meta[key]); @@ -84,24 +93,29 @@ export function createFromFileSystem(host: TestHost, ignoreCase: boolean, return fs; } -let cacheKey: { host: TestHost; typeshedFolderPath: string | undefined } | undefined; +let cacheKey: { host: TestHost; mountPaths: Map } | undefined; 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) { +function getBuiltLocal(host: TestHost, ignoreCase: boolean, mountPaths: Map): FileSystem { + // Ensure typeshed folder + if (!mountPaths.has(typeshedFolder)) { + mountPaths.set(typeshedFolder, resolvePaths(host.getWorkspaceRoot(), '../client/' + consts.TYPESHED_FALLBACK)); + } + + if (!canReuseCache(host, mountPaths)) { localCIFSCache = undefined; localCSFSCache = undefined; - cacheKey = { host, typeshedFolderPath }; + cacheKey = { host, mountPaths }; } + if (!localCIFSCache) { const resolver = createResolver(host); - typeshedFolderPath = typeshedFolderPath ?? resolvePaths(host.getWorkspaceRoot(), '../client/typeshed-fallback'); + const files: FileSet = { [srcFolder]: {} }; + mountPaths.forEach((v, k) => files[k] = new Mount(v, resolver)); + localCIFSCache = new FileSystem(/*ignoreCase*/ true, { - files: { - [typeshedFolder]: new Mount(typeshedFolderPath, resolver), - [srcFolder]: {} - }, + files, cwd: srcFolder, meta: {} }); @@ -118,6 +132,20 @@ function getBuiltLocal(host: TestHost, typeshedFolderPath: string | undefined, i return localCSFSCache; } +function canReuseCache(host: TestHost, mountPaths: Map): boolean { + if (cacheKey === undefined) { return false; } + if (cacheKey.host !== host) { return false; } + if (cacheKey.mountPaths.size !== mountPaths.size) { return false; } + + for (const key of cacheKey.mountPaths.keys()) { + if (cacheKey.mountPaths.get(key) !== mountPaths.get(key)) { + return false; + } + } + + return true; +} + function createResolver(host: TestHost): FileSystemResolver { return { readdirSync(path: string): string[] { diff --git a/server/src/tests/pathUtils.test.ts b/server/src/tests/pathUtils.test.ts index 2d4fc8613f82..b3992c974c54 100644 --- a/server/src/tests/pathUtils.test.ts +++ b/server/src/tests/pathUtils.test.ts @@ -15,21 +15,10 @@ import { changeAnyExtension, combinePathComponents, combinePaths, comparePaths, comparePathsCaseInsensitive, comparePathsCaseSensitive, containsPath, ensureTrailingDirectorySeparator, getAnyExtensionFromPath, - getBaseFileName, - getFileExtension, - getFileName, - getPathComponents, - getRegexEscapedSeparator, - getRelativePathFromDirectory, - getWildcardRegexPattern, - getWildcardRoot, - hasTrailingDirectorySeparator, - isRootedDiskPath, - normalizeSlashes, - reducePathComponents, - resolvePaths, - stripFileExtension, - stripTrailingDirectorySeparator + getBaseFileName, getFileExtension, getFileName, getPathComponents, getRegexEscapedSeparator, + getRelativePath, getRelativePathFromDirectory, getWildcardRegexPattern, getWildcardRoot, + hasTrailingDirectorySeparator, isRootedDiskPath, normalizeSlashes, reducePathComponents, + resolvePaths, stripFileExtension, stripTrailingDirectorySeparator } from '../common/pathUtils'; test('getPathComponents1', () => { @@ -117,14 +106,14 @@ test('stripFileExtension', () => { test('getWildcardRegexPattern1', () => { const pattern = getWildcardRegexPattern('/users/me', './blah/'); const sep = getRegexEscapedSeparator(); - assert.equal(pattern, `${sep}users${sep}me${sep}blah`); + assert.equal(pattern, `${ sep }users${ sep }me${ sep }blah`); }); test('getWildcardRegexPattern2', () => { const pattern = getWildcardRegexPattern('/users/me', './**/*.py?/'); const sep = getRegexEscapedSeparator(); - assert.equal(pattern, `${sep}users${sep}me(${sep}[^${sep}.][^${sep}]*)*?${sep}[^${sep}]*\\.py[^${sep}]`); + assert.equal(pattern, `${ sep }users${ sep }me(${ sep }[^${ sep }.][^${ sep }]*)*?${ sep }[^${ sep }]*\\.py[^${ sep }]`); }); test('getWildcardRoot1', () => { @@ -278,3 +267,7 @@ test('isDiskPathRoot2', () => { test('isDiskPathRoot3', () => { assert(!isRootedDiskPath(normalizeSlashes('c:'))); }); + +test('getRelativePath', () => { + assert.equal(getRelativePath(normalizeSlashes('/a/b/c/d/e/f'), normalizeSlashes('/a/b/c')), normalizeSlashes('./d/e/f')); +}); diff --git a/server/src/tests/testState.test.ts b/server/src/tests/testState.test.ts index c9b005082d04..cdd0123d400f 100644 --- a/server/src/tests/testState.test.ts +++ b/server/src/tests/testState.test.ts @@ -7,7 +7,7 @@ */ import * as assert from 'assert'; -import { combinePaths, comparePathsCaseSensitive, normalizeSlashes } from '../common/pathUtils'; +import { combinePaths, comparePathsCaseSensitive, getFileName, normalizeSlashes } from '../common/pathUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; import { parseTestData } from './harness/fourslash/fourSlashParser'; import { Range } from './harness/fourslash/fourSlashTypes'; @@ -49,7 +49,7 @@ test('Multiple files', () => { test('Configuration', () => { const code = ` -// @filename: python.json +// @filename: mspythonconfig.json //// { //// "include": [ //// "src" @@ -139,6 +139,22 @@ test('ProjectRoot', () => { assert.equal(state.configOptions.projectRoot, normalizeSlashes('/root')); }); +test('CustomTypeshedFolder', () => { + // use differnt physical folder as typeshed folder. this is different than + // typeshed folder settings in config json file since that points to a path + // in virtual file system. not physical one. this decides which physical folder + // those virtual folder will mount to. + const code = ` +// global options +// @typeshed: ${ __dirname } + `; + + // mount the folder this file is in as typeshed folder and check whether + // in typeshed folder in virtual file system, this file exists. + const state = parseAndGetTestState(code).state; + assert(state.fs.existsSync(combinePaths(factory.typeshedFolder, getFileName(__filename)))); +}); + test('IgnoreCase', () => { const code = ` // global options