From abd41b7273f19cd657db7768821b0e1e84c6d217 Mon Sep 17 00:00:00 2001 From: Heejae Chang Date: Wed, 11 Aug 2021 13:11:54 -0700 Subject: [PATCH] Added literal type support in more places, fixed using wrong console bug and some refactoring around Host environment (#2176) --- package.json | 2 +- .../src/analyzer/backgroundAnalysisProgram.ts | 16 +- .../src/analyzer/importResolver.ts | 23 ++- .../src/analyzer/packageTypeVerifier.ts | 7 +- .../src/analyzer/pythonPathUtils.ts | 103 +--------- .../pyright-internal/src/analyzer/service.ts | 60 +++--- .../src/analyzer/typePrinter.ts | 17 +- .../src/backgroundAnalysis.ts | 16 +- .../src/backgroundAnalysisBase.ts | 46 +++-- .../src/common/configOptions.ts | 70 ++----- packages/pyright-internal/src/common/core.ts | 2 +- .../src/common/fullAccessHost.ts | 183 ++++++++++++++++++ packages/pyright-internal/src/common/host.ts | 49 +++++ .../src/common/realFileSystem.ts | 4 +- .../src/languageServerBase.ts | 17 +- .../src/languageService/completionProvider.ts | 107 ++++++++-- packages/pyright-internal/src/pyright.ts | 4 +- packages/pyright-internal/src/server.ts | 23 ++- .../pyright-internal/src/tests/config.test.ts | 3 +- .../src/tests/filesystem.test.ts | 2 +- .../src/tests/fourSlashParser.test.ts | 2 +- .../src/tests/fourSlashRunner.test.ts | 2 +- ...ompletions.call.stringLiteral.fourslash.ts | 55 ++++++ ....dictionary.keys.literalTypes.fourslash.ts | 161 +++++++++++++++ ...ictionary.keys.stringLiterals.fourslash.ts | 2 +- ...pletions.stringLiteral.escape.fourslash.ts | 71 +++++++ ...hover.docFromScr.stringFormat.fourslash.ts | 20 +- .../src/tests/harness/fourslash/runner.ts | 2 +- .../src/tests/harness/fourslash/testState.ts | 14 +- .../tests/harness/{host.ts => testHost.ts} | 0 .../src/tests/harness/vfs/factory.ts | 2 +- .../src/tests/importResolver.test.ts | 5 +- .../src/tests/sourceFile.test.ts | 3 +- .../pyright-internal/src/tests/testUtils.ts | 5 +- 34 files changed, 819 insertions(+), 279 deletions(-) create mode 100644 packages/pyright-internal/src/common/fullAccessHost.ts create mode 100644 packages/pyright-internal/src/common/host.ts create mode 100644 packages/pyright-internal/src/tests/fourslash/completions.call.stringLiteral.fourslash.ts create mode 100644 packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.literalTypes.fourslash.ts create mode 100644 packages/pyright-internal/src/tests/fourslash/completions.stringLiteral.escape.fourslash.ts rename packages/pyright-internal/src/tests/harness/{host.ts => testHost.ts} (100%) diff --git a/package.json b/package.json index cff52c32b..4d2ade6bf 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "scripts": { "postinstall": "npm run bootstrap", - "bootstrap": "node ./build/skipBootstrap.js || lerna bootstrap --no-ci", + "bootstrap": "node ./build/skipBootstrap.js || lerna bootstrap", "clean": "lerna run --no-bail --stream clean", "install:all": "npm install && lerna exec --no-bail npm install", "update:all": "node ./build/updateDeps.js", diff --git a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts index 2e11f3bf2..3f7cc18bc 100644 --- a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts +++ b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts @@ -58,6 +58,10 @@ export class BackgroundAnalysisProgram { return this._program; } + get host() { + return this._importResolver.host; + } + get backgroundAnalysis() { return this._backgroundAnalysis; } @@ -70,14 +74,10 @@ export class BackgroundAnalysisProgram { setImportResolver(importResolver: ImportResolver) { this._importResolver = importResolver; + this._backgroundAnalysis?.setImportResolver(importResolver); + this._program.setImportResolver(importResolver); - this._configOptions.getExecutionEnvironments().forEach((e) => this._ensurePartialStubPackages(e)); - - // Do nothing for background analysis. - // Background analysis updates importer when configOptions is changed rather than - // having two APIs to reduce the chance of the program and importer pointing to - // two different configOptions. } setTrackedFiles(filePaths: string[]) { @@ -165,7 +165,7 @@ export class BackgroundAnalysisProgram { return; } - this._backgroundAnalysis?.startIndexing(this._configOptions, this._getIndices()); + this._backgroundAnalysis?.startIndexing(this._configOptions, this.host.kind, this._getIndices()); } refreshIndexing() { @@ -173,7 +173,7 @@ export class BackgroundAnalysisProgram { return; } - this._backgroundAnalysis?.refreshIndexing(this._configOptions, this._indices); + this._backgroundAnalysis?.refreshIndexing(this._configOptions, this.host.kind, this._indices); } cancelIndexing() { diff --git a/packages/pyright-internal/src/analyzer/importResolver.ts b/packages/pyright-internal/src/analyzer/importResolver.ts index 5b6de0dd7..a4e360f0d 100644 --- a/packages/pyright-internal/src/analyzer/importResolver.ts +++ b/packages/pyright-internal/src/analyzer/importResolver.ts @@ -13,6 +13,7 @@ import type { Dirent } from 'fs'; import { getOrAdd } from '../common/collectionUtils'; import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { FileSystem } from '../common/fileSystem'; +import { Host } from '../common/host'; import { stubsSuffix } from '../common/pathConsts'; import { changeAnyExtension, @@ -74,8 +75,6 @@ export const supportedFileExtensions = ['.py', '.pyi', ...supportedNativeLibExte const allowPartialResolutionForThirdPartyPackages = false; export class ImportResolver { - protected _configOptions: ConfigOptions; - private _cachedPythonSearchPaths = new Map(); private _cachedImportResults = new Map(); private _cachedModuleNameResults = new Map>(); @@ -87,12 +86,11 @@ export class ImportResolver { private _cachedTypeshedThirdPartyPackageRoots: string[] | undefined; private _cachedEntriesForPath = new Map(); - readonly fileSystem: FileSystem; - - constructor(fs: FileSystem, configOptions: ConfigOptions) { - this.fileSystem = fs; - this._configOptions = configOptions; - } + constructor( + public readonly fileSystem: FileSystem, + protected _configOptions: ConfigOptions, + public readonly host: Host + ) {} invalidateCache() { this._cachedPythonSearchPaths = new Map(); @@ -1233,7 +1231,12 @@ export class ImportResolver { // Find the site packages for the configured virtual environment. if (!this._cachedPythonSearchPaths.has(cacheKey)) { let paths = ( - PythonPathUtils.findPythonSearchPaths(this.fileSystem, this._configOptions, importFailureInfo) || [] + PythonPathUtils.findPythonSearchPaths( + this.fileSystem, + this._configOptions, + this.host, + importFailureInfo + ) || [] ).map((p) => this.fileSystem.realCasePath(p)); // Remove duplicates (yes, it happens). @@ -1892,4 +1895,4 @@ export class ImportResolver { } } -export type ImportResolverFactory = (fs: FileSystem, options: ConfigOptions) => ImportResolver; +export type ImportResolverFactory = (fs: FileSystem, options: ConfigOptions, host: Host) => ImportResolver; diff --git a/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts b/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts index abcc2a696..a516ab774 100644 --- a/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts +++ b/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts @@ -12,6 +12,7 @@ import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { assert } from '../common/debug'; import { Diagnostic, DiagnosticAddendum, DiagnosticCategory } from '../common/diagnostic'; import { FileSystem } from '../common/fileSystem'; +import { FullAccessHost } from '../common/fullAccessHost'; import { combinePaths, getDirectoryPath, getFileExtension, stripFileExtension, tryStat } from '../common/pathUtils'; import { getEmptyRange, Range } from '../common/textRange'; import { DeclarationType, FunctionDeclaration, VariableDeclaration } from './declaration'; @@ -55,7 +56,11 @@ export class PackageTypeVerifier { constructor(private _fileSystem: FileSystem) { this._configOptions = new ConfigOptions(''); this._execEnv = this._configOptions.findExecEnvironment('.'); - this._importResolver = new ImportResolver(this._fileSystem, this._configOptions); + this._importResolver = new ImportResolver( + this._fileSystem, + this._configOptions, + new FullAccessHost(this._fileSystem) + ); this._program = new Program(this._importResolver, this._configOptions); } diff --git a/packages/pyright-internal/src/analyzer/pythonPathUtils.ts b/packages/pyright-internal/src/analyzer/pythonPathUtils.ts index 5684e84b7..29b60162c 100644 --- a/packages/pyright-internal/src/analyzer/pythonPathUtils.ts +++ b/packages/pyright-internal/src/analyzer/pythonPathUtils.ts @@ -7,11 +7,10 @@ * Utility routines used to resolve various paths in python. */ -import * as child_process from 'child_process'; - import { ConfigOptions } from '../common/configOptions'; import { compareComparableValues } from '../common/core'; import { FileSystem } from '../common/fileSystem'; +import { Host } from '../common/host'; import * as pathConsts from '../common/pathConsts'; import { combinePaths, @@ -24,20 +23,11 @@ import { tryStat, } from '../common/pathUtils'; -interface PythonPathResult { +export interface PythonPathResult { paths: string[]; prefix: string; } -const extractSys = [ - 'import os, os.path, sys', - 'normalize = lambda p: os.path.normcase(os.path.normpath(p))', - 'cwd = normalize(os.getcwd())', - 'sys.path[:] = [p for p in sys.path if p != "" and normalize(p) != cwd]', - 'import json', - 'json.dump(dict(path=sys.path, prefix=sys.prefix), sys.stdout)', -].join('; '); - export const stdLibFolderName = 'stdlib'; export const thirdPartyFolderName = 'stubs'; @@ -71,6 +61,7 @@ export function getTypeshedSubdirectory(typeshedPath: string, isStdLib: boolean) export function findPythonSearchPaths( fs: FileSystem, configOptions: ConfigOptions, + host: Host, importFailureInfo: string[], includeWatchPathsOnly?: boolean | undefined, workspaceRoot?: string | undefined @@ -114,7 +105,7 @@ export function findPythonSearchPaths( } // Fall back on the python interpreter. - const pathResult = getPythonPathFromPythonInterpreter(fs, configOptions.pythonPath, importFailureInfo); + const pathResult = host.getPythonSearchPaths(configOptions.pythonPath, importFailureInfo); if (includeWatchPathsOnly && workspaceRoot) { const paths = pathResult.paths.filter( (p) => !containsPath(workspaceRoot, p, true) || containsPath(pathResult.prefix, p, true) @@ -126,44 +117,6 @@ export function findPythonSearchPaths( return pathResult.paths; } -export function getPythonPathFromPythonInterpreter( - fs: FileSystem, - interpreterPath: string | undefined, - importFailureInfo: string[] -): PythonPathResult { - let result: PythonPathResult | undefined; - - if (interpreterPath) { - result = getPathResultFromInterpreter(fs, interpreterPath, importFailureInfo); - } else { - // On non-Windows platforms, always default to python3 first. We want to - // avoid this on Windows because it might invoke a script that displays - // a dialog box indicating that python can be downloaded from the app store. - if (process.platform !== 'win32') { - result = getPathResultFromInterpreter(fs, 'python3', importFailureInfo); - } - - // On some platforms, 'python3' might not exist. Try 'python' instead. - if (!result) { - result = getPathResultFromInterpreter(fs, 'python', importFailureInfo); - } - } - - if (!result) { - result = { - paths: [], - prefix: '', - }; - } - - importFailureInfo.push(`Received ${result.paths.length} paths from interpreter`); - result.paths.forEach((path) => { - importFailureInfo.push(` ${path}`); - }); - - return result; -} - export function isPythonBinary(p: string): boolean { p = p.trim(); return p === 'python' || p === 'python3'; @@ -204,54 +157,6 @@ function findSitePackagesPath(fs: FileSystem, libPath: string, importFailureInfo return undefined; } -function getPathResultFromInterpreter( - fs: FileSystem, - interpreter: string, - importFailureInfo: string[] -): PythonPathResult | undefined { - const result: PythonPathResult = { - paths: [], - prefix: '', - }; - - try { - const commandLineArgs: string[] = ['-c', extractSys]; - - importFailureInfo.push(`Executing interpreter: '${interpreter}'`); - const execOutput = child_process.execFileSync(interpreter, commandLineArgs, { encoding: 'utf8' }); - - // Parse the execOutput. It should be a JSON-encoded array of paths. - try { - const execSplit = JSON.parse(execOutput); - for (let execSplitEntry of execSplit.path) { - execSplitEntry = execSplitEntry.trim(); - if (execSplitEntry) { - const normalizedPath = normalizePath(execSplitEntry); - // Skip non-existent paths and broken zips/eggs. - if (fs.existsSync(normalizedPath) && isDirectory(fs, normalizedPath)) { - result.paths.push(normalizedPath); - } else { - importFailureInfo.push(`Skipping '${normalizedPath}' because it is not a valid directory`); - } - } - } - - result.prefix = execSplit.prefix; - - if (result.paths.length === 0) { - importFailureInfo.push(`Found no valid directories`); - } - } catch (err) { - importFailureInfo.push(`Could not parse output: '${execOutput}'`); - throw err; - } - } catch { - return undefined; - } - - return result; -} - function getPathsFromPthFiles(fs: FileSystem, parentDir: string): string[] { const searchPaths: string[] = []; diff --git a/packages/pyright-internal/src/analyzer/service.ts b/packages/pyright-internal/src/analyzer/service.ts index 4a437b1fc..8b207987c 100644 --- a/packages/pyright-internal/src/analyzer/service.ts +++ b/packages/pyright-internal/src/analyzer/service.ts @@ -34,6 +34,7 @@ import { Diagnostic } from '../common/diagnostic'; import { FileEditAction, TextEditAction } from '../common/editAction'; import { LanguageServiceExtension } from '../common/extensibility'; import { FileSystem, FileWatcher, ignoredWatchEventFunction } from '../common/fileSystem'; +import { Host, HostFactory, NoAccessHost } from '../common/host'; import { combinePaths, FileSpec, @@ -75,6 +76,7 @@ const _gitDirectory = normalizeSlashes('/.git/'); const _includeFileRegex = /\.pyi?$/; export class AnalyzerService { + private _hostFactory: HostFactory; private _instanceName: string; private _importResolverFactory: ImportResolverFactory; private _executionRootPath: string; @@ -104,6 +106,7 @@ export class AnalyzerService { instanceName: string, fs: FileSystem, console?: ConsoleInterface, + hostFactory?: HostFactory, importResolverFactory?: ImportResolverFactory, configOptions?: ConfigOptions, extension?: LanguageServiceExtension, @@ -120,9 +123,10 @@ export class AnalyzerService { this._maxAnalysisTimeInForeground = maxAnalysisTime; this._backgroundAnalysisProgramFactory = backgroundAnalysisProgramFactory; this._cancellationProvider = cancellationProvider ?? new DefaultCancellationProvider(); + this._hostFactory = hostFactory ?? (() => new NoAccessHost()); configOptions = configOptions ?? new ConfigOptions(process.cwd()); - const importResolver = this._importResolverFactory(fs, configOptions); + const importResolver = this._importResolverFactory(fs, configOptions, this._hostFactory()); this._backgroundAnalysisProgram = backgroundAnalysisProgramFactory !== undefined @@ -149,6 +153,7 @@ export class AnalyzerService { instanceName, this._fs, this._console, + this._hostFactory, this._importResolverFactory, this._backgroundAnalysisProgram.configOptions, this._extension, @@ -173,8 +178,8 @@ export class AnalyzerService { return this._backgroundAnalysisProgram; } - static createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver { - return new ImportResolver(fs, options); + static createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver { + return new ImportResolver(fs, options, host); } setCompletionCallback(callback: AnalysisCompleteCallback | undefined): void { @@ -185,21 +190,22 @@ export class AnalyzerService { setOptions(commandLineOptions: CommandLineOptions, reanalyze = true): void { this._commandLineOptions = commandLineOptions; - const configOptions = this._getConfigOptions(commandLineOptions); + const host = this._hostFactory(); + const configOptions = this._getConfigOptions(host, commandLineOptions); if (configOptions.pythonPath) { // Make sure we have default python environment set. - configOptions.ensureDefaultPythonVersion(configOptions.pythonPath, this._console); + configOptions.ensureDefaultPythonVersion(host, this._console); } - configOptions.ensureDefaultPythonPlatform(this._console); + configOptions.ensureDefaultPythonPlatform(host, this._console); this._backgroundAnalysisProgram.setConfigOptions(configOptions); this._executionRootPath = normalizePath( combinePaths(commandLineOptions.executionRoot, configOptions.projectRoot) ); - this._applyConfigOptions(reanalyze); + this._applyConfigOptions(host, reanalyze); } setFileOpened(path: string, version: number | null, contents: string) { @@ -429,7 +435,7 @@ export class AnalyzerService { } test_getConfigOptions(commandLineOptions: CommandLineOptions): ConfigOptions { - return this._getConfigOptions(commandLineOptions); + return this._getConfigOptions(this._backgroundAnalysisProgram.host, commandLineOptions); } test_getFileNamesFromFileSpecs(): string[] { @@ -438,7 +444,7 @@ export class AnalyzerService { // Calculates the effective options based on the command-line options, // an optional config file, and default values. - private _getConfigOptions(commandLineOptions: CommandLineOptions): ConfigOptions { + private _getConfigOptions(host: Host, commandLineOptions: CommandLineOptions): ConfigOptions { let projectRoot = commandLineOptions.executionRoot; let configFilePath: string | undefined; let pyprojectFilePath: string | undefined; @@ -504,6 +510,13 @@ export class AnalyzerService { const configOptions = new ConfigOptions(projectRoot, this._typeCheckingMode); const defaultExcludes = ['**/node_modules', '**/__pycache__', '.git']; + if (commandLineOptions.pythonPath) { + this._console.info( + `Setting pythonPath for service "${this._instanceName}": ` + `"${commandLineOptions.pythonPath}"` + ); + configOptions.pythonPath = commandLineOptions.pythonPath; + } + // The pythonPlatform and pythonVersion from the command-line can be overridden // by the config file, so initialize them upfront. configOptions.defaultPythonPlatform = commandLineOptions.pythonPlatform; @@ -549,8 +562,8 @@ export class AnalyzerService { configJsonObj, this._typeCheckingMode, this._console, + host, commandLineOptions.diagnosticSeverityOverrides, - commandLineOptions.pythonPath, commandLineOptions.fileSpecs.length > 0 ); @@ -601,13 +614,6 @@ export class AnalyzerService { } } - if (commandLineOptions.pythonPath) { - this._console.info( - `Setting pythonPath for service "${this._instanceName}": ` + `"${commandLineOptions.pythonPath}"` - ); - configOptions.pythonPath = commandLineOptions.pythonPath; - } - if (commandLineOptions.typeshedPath) { if (!configOptions.typeshedPath) { configOptions.typeshedPath = commandLineOptions.typeshedPath; @@ -664,7 +670,7 @@ export class AnalyzerService { ); } else { const importFailureInfo: string[] = []; - if (findPythonSearchPaths(this._fs, configOptions, importFailureInfo) === undefined) { + if (findPythonSearchPaths(this._fs, configOptions, host, importFailureInfo) === undefined) { this._console.error( `site-packages directory cannot be located for venvPath ` + `${configOptions.venvPath} and venv ${configOptions.venv}.` @@ -738,7 +744,7 @@ export class AnalyzerService { // Forces the service to stop all analysis, discard all its caches, // and research for files. restart() { - this._applyConfigOptions(); + this._applyConfigOptions(this._hostFactory()); this._backgroundAnalysisProgram.restart(); } @@ -1218,6 +1224,7 @@ export class AnalyzerService { const watchList = findPythonSearchPaths( this._fs, this._backgroundAnalysisProgram.configOptions, + this._backgroundAnalysisProgram.host, importFailureInfo, true, this._executionRootPath @@ -1339,19 +1346,26 @@ export class AnalyzerService { if (this._configFilePath) { this._console.info(`Reloading configuration file at ${this._configFilePath}`); + const host = this._backgroundAnalysisProgram.host; + // We can't just reload config file when it is changed; we need to consider // command line options as well to construct new config Options. - const configOptions = this._getConfigOptions(this._commandLineOptions!); + const configOptions = this._getConfigOptions(host, this._commandLineOptions!); this._backgroundAnalysisProgram.setConfigOptions(configOptions); - this._applyConfigOptions(); + this._applyConfigOptions(host); } } - private _applyConfigOptions(reanalyze = true) { + private _applyConfigOptions(host: Host, reanalyze = true) { // Allocate a new import resolver because the old one has information // cached based on the previous config options. - const importResolver = this._importResolverFactory(this._fs, this._backgroundAnalysisProgram.configOptions); + const importResolver = this._importResolverFactory( + this._fs, + this._backgroundAnalysisProgram.configOptions, + host + ); + this._backgroundAnalysisProgram.setImportResolver(importResolver); if (this._commandLineOptions?.fromVsCodeExtension || this._configOptions.verboseOutput) { diff --git a/packages/pyright-internal/src/analyzer/typePrinter.ts b/packages/pyright-internal/src/analyzer/typePrinter.ts index a9e49f5ca..6bbe0179e 100644 --- a/packages/pyright-internal/src/analyzer/typePrinter.ts +++ b/packages/pyright-internal/src/analyzer/typePrinter.ts @@ -29,6 +29,7 @@ import { import { doForEachSubtype, isOptionalType, isTupleClass } from './typeUtils'; const singleTickRegEx = /'/g; +const escapedDoubleQuoteRegEx = /\\"/g; export const enum PrintTypeFlags { None = 0, @@ -371,7 +372,7 @@ export function printType( } } -export function printLiteralValue(type: ClassType): string { +export function printLiteralValue(type: ClassType, quotation = "'"): string { const literalValue = type.literalValue; if (literalValue === undefined) { return ''; @@ -380,8 +381,20 @@ export function printLiteralValue(type: ClassType): string { let literalStr: string; if (typeof literalValue === 'string') { const prefix = type.details.name === 'bytes' ? 'b' : ''; + + // JSON.stringify will perform proper escaping for " case. + // So, we only need to do our own escaping for ' case. literalStr = JSON.stringify(literalValue).toString(); - literalStr = `${prefix}'${literalStr.substring(1, literalStr.length - 1).replace(singleTickRegEx, "\\'")}'`; + if (quotation !== '"') { + literalStr = `'${literalStr + .substring(1, literalStr.length - 1) + .replace(escapedDoubleQuoteRegEx, '"') + .replace(singleTickRegEx, "\\'")}'`; + } + + if (prefix) { + literalStr = `${prefix}${literalStr}`; + } } else if (typeof literalValue === 'boolean') { literalStr = literalValue ? 'True' : 'False'; } else if (literalValue instanceof EnumLiteral) { diff --git a/packages/pyright-internal/src/backgroundAnalysis.ts b/packages/pyright-internal/src/backgroundAnalysis.ts index 558c7babc..d444fe835 100644 --- a/packages/pyright-internal/src/backgroundAnalysis.ts +++ b/packages/pyright-internal/src/backgroundAnalysis.ts @@ -8,9 +8,15 @@ import { Worker } from 'worker_threads'; -import { BackgroundAnalysisBase, BackgroundAnalysisRunnerBase, InitializationData } from './backgroundAnalysisBase'; +import { ImportResolver } from './analyzer/importResolver'; +import { BackgroundAnalysisBase, BackgroundAnalysisRunnerBase } from './backgroundAnalysisBase'; +import { InitializationData } from './backgroundThreadBase'; import { getCancellationFolderName } from './common/cancellationUtils'; +import { ConfigOptions } from './common/configOptions'; import { ConsoleInterface } from './common/console'; +import { FileSystem } from './common/fileSystem'; +import { FullAccessHost } from './common/fullAccessHost'; +import { Host } from './common/host'; export class BackgroundAnalysis extends BackgroundAnalysisBase { constructor(console: ConsoleInterface) { @@ -32,4 +38,12 @@ export class BackgroundAnalysisRunner extends BackgroundAnalysisRunnerBase { constructor() { super(); } + + protected override createHost(): Host { + return new FullAccessHost(this.fs); + } + + protected override createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver { + return new ImportResolver(fs, options, host); + } } diff --git a/packages/pyright-internal/src/backgroundAnalysisBase.ts b/packages/pyright-internal/src/backgroundAnalysisBase.ts index 8eced6d32..6e93d5c88 100644 --- a/packages/pyright-internal/src/backgroundAnalysisBase.ts +++ b/packages/pyright-internal/src/backgroundAnalysisBase.ts @@ -17,6 +17,7 @@ import { BackgroundThreadBase, createConfigOptionsFrom, getBackgroundWaiter, + InitializationData, LogData, run, } from './backgroundThreadBase'; @@ -33,6 +34,7 @@ import { getCancellationTokenId, } from './common/fileBasedCancellationUtils'; import { FileSystem } from './common/fileSystem'; +import { Host, HostKind } from './common/host'; import { LogTracker } from './common/logTracker'; import { Range } from './common/textRange'; import { IndexResults } from './languageService/documentSymbolProvider'; @@ -82,6 +84,10 @@ export class BackgroundAnalysisBase { this._onAnalysisCompletion = callback ?? nullCallback; } + setImportResolver(importResolver: ImportResolver) { + this.enqueueRequest({ requestType: 'setImportResolver', data: importResolver.host.kind }); + } + setConfigOptions(configOptions: ConfigOptions) { this.enqueueRequest({ requestType: 'setConfigOptions', data: configOptions }); } @@ -170,11 +176,11 @@ export class BackgroundAnalysisBase { this.enqueueRequest({ requestType, data: cancellationId, port: port2 }); } - startIndexing(configOptions: ConfigOptions, indices: Indices) { + startIndexing(configOptions: ConfigOptions, kind: HostKind, indices: Indices) { /* noop */ } - refreshIndexing(configOptions: ConfigOptions, indices?: Indices) { + refreshIndexing(configOptions: ConfigOptions, kind: HostKind, indices?: Indices) { /* noop */ } @@ -246,10 +252,12 @@ export class BackgroundAnalysisBase { } } -export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase { +export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase { private _configOptions: ConfigOptions; protected _importResolver: ImportResolver; private _program: Program; + + protected _host: Host; protected _logTracker: LogTracker; get program(): Program { @@ -264,7 +272,8 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase { this.log(LogLevel.Info, `Background analysis(${threadId}) root directory: ${data.rootDirectory}`); this._configOptions = new ConfigOptions(data.rootDirectory); - this._importResolver = this.createImportResolver(this.fs, this._configOptions); + this._host = this.createHost(); + this._importResolver = this.createImportResolver(this.fs, this._configOptions, this._host); const console = this.getConsole(); this._logTracker = new LogTracker(console, `BG(${threadId})`); @@ -354,9 +363,17 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase { break; } + case 'setImportResolver': { + this._importResolver = this.createImportResolver(this.fs, this._configOptions, this.createHost()); + + this.program.setImportResolver(this._importResolver); + break; + } + case 'setConfigOptions': { this._configOptions = createConfigOptionsFrom(msg.data); - this._importResolver = this.createImportResolver(this.fs, this._configOptions); + + this._importResolver = this.createImportResolver(this.fs, this._configOptions, this._host); this.program.setConfigOptions(this._configOptions); this.program.setImportResolver(this._importResolver); break; @@ -417,7 +434,7 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase { case 'restart': { // recycle import resolver - this._importResolver = this.createImportResolver(this.fs, this._configOptions); + this._importResolver = this.createImportResolver(this.fs, this._configOptions, this._host); this.program.setImportResolver(this._importResolver); break; } @@ -451,11 +468,11 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase { } } - protected createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver { - return new ImportResolver(fs, options); - } + protected abstract createHost(): Host; - protected processIndexing(port: MessagePort, token: CancellationToken) { + protected abstract createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver; + + protected processIndexing(port: MessagePort, token: CancellationToken): void { /* noop */ } @@ -526,12 +543,6 @@ function convertDiagnostics(diagnostics: Diagnostic[]) { }); } -export interface InitializationData { - rootDirectory: string; - cancellationFolderName: string | undefined; - runner: string | undefined; -} - export interface AnalysisRequest { requestType: | 'analyze' @@ -549,7 +560,8 @@ export interface AnalysisRequest { | 'getDiagnosticsForRange' | 'writeTypeStub' | 'getSemanticTokens' - | 'setExperimentOptions'; + | 'setExperimentOptions' + | 'setImportResolver'; data: any; port?: MessagePort | undefined; diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index ca26f7f08..dd295a044 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -7,7 +7,6 @@ * Class that holds the configuration options for the analyzer. */ -import * as child_process from 'child_process'; import { isAbsolute } from 'path'; import * as pathConsts from '../common/pathConsts'; @@ -15,6 +14,7 @@ import { DiagnosticSeverityOverridesMap } from './commandLineOptions'; import { ConsoleInterface } from './console'; import { DiagnosticRule } from './diagnosticRules'; import { FileSystem } from './fileSystem'; +import { Host } from './host'; import { combinePaths, ensureTrailingDirectorySeparator, @@ -23,13 +23,7 @@ import { normalizePath, resolvePaths, } from './pathUtils'; -import { - latestStablePythonVersion, - PythonVersion, - versionFromMajorMinor, - versionFromString, - versionToString, -} from './pythonVersion'; +import { latestStablePythonVersion, PythonVersion, versionFromString, versionToString } from './pythonVersion'; export enum PythonPlatform { Darwin = 'Darwin', @@ -726,8 +720,8 @@ export class ConfigOptions { configObj: any, typeCheckingMode: string | undefined, console: ConsoleInterface, + host: Host, diagnosticOverrides?: DiagnosticSeverityOverridesMap, - pythonPath?: string, skipIncludeSection = false ) { // Read the "include" entry. @@ -1298,7 +1292,7 @@ export class ConfigOptions { } } - this.ensureDefaultPythonVersion(pythonPath, console); + this.ensureDefaultPythonVersion(host, console); // Read the default "pythonPlatform". if (configObj.pythonPlatform !== undefined) { @@ -1309,7 +1303,7 @@ export class ConfigOptions { } } - this.ensureDefaultPythonPlatform(console); + this.ensureDefaultPythonPlatform(host, console); // Read the "typeshedPath" setting. this.typeshedPath = undefined; @@ -1418,37 +1412,35 @@ export class ConfigOptions { } } - ensureDefaultPythonPlatform(console: ConsoleInterface) { + ensureDefaultPythonPlatform(host: Host, console: ConsoleInterface) { // If no default python platform was specified, assume that the // user wants to use the current platform. if (this.defaultPythonPlatform !== undefined) { return; } - if (process.platform === 'darwin') { - this.defaultPythonPlatform = PythonPlatform.Darwin; - } else if (process.platform === 'linux') { - this.defaultPythonPlatform = PythonPlatform.Linux; - } else if (process.platform === 'win32') { - this.defaultPythonPlatform = PythonPlatform.Windows; - } - + this.defaultPythonPlatform = host.getPythonPlatform(); if (this.defaultPythonPlatform !== undefined) { console.info(`Assuming Python platform ${this.defaultPythonPlatform}`); } } - ensureDefaultPythonVersion(pythonPath: string | undefined, console: ConsoleInterface) { + ensureDefaultPythonVersion(host: Host, console: ConsoleInterface) { // If no default python version was specified, retrieve the version // from the currently-selected python interpreter. if (this.defaultPythonVersion !== undefined) { return; } - this.defaultPythonVersion = this._getPythonVersionFromPythonInterpreter(pythonPath, console); + const importFailureInfo: string[] = []; + this.defaultPythonVersion = host.getPythonVersion(this.pythonPath, importFailureInfo); if (this.defaultPythonVersion !== undefined) { console.info(`Assuming Python version ${versionToString(this.defaultPythonVersion)}`); } + + for (const log of importFailureInfo) { + console.info(log); + } } ensureDefaultExtraPaths(fs: FileSystem, autoSearchPaths: boolean, extraPaths: string[] | undefined) { @@ -1580,38 +1572,4 @@ export class ConfigOptions { return undefined; } - - private _getPythonVersionFromPythonInterpreter( - interpreterPath: string | undefined, - console: ConsoleInterface - ): PythonVersion | undefined { - try { - const commandLineArgs: string[] = [ - '-c', - 'import sys, json; json.dump(dict(major=sys.version_info[0], minor=sys.version_info[1]), sys.stdout)', - ]; - let execOutput: string; - - if (interpreterPath) { - execOutput = child_process.execFileSync(interpreterPath, commandLineArgs, { encoding: 'utf8' }); - } else { - execOutput = child_process.execFileSync('python', commandLineArgs, { encoding: 'utf8' }); - } - - const versionJson: { major: number; minor: number } = JSON.parse(execOutput); - - const version = versionFromMajorMinor(versionJson.major, versionJson.minor); - if (version === undefined) { - console.warn( - `Python version ${versionJson.major}.${versionJson.minor} from interpreter is unsupported` - ); - return undefined; - } - - return version; - } catch { - console.info('Unable to get Python version from interpreter'); - return undefined; - } - } } diff --git a/packages/pyright-internal/src/common/core.ts b/packages/pyright-internal/src/common/core.ts index eebf8f02c..a9d9b9cc7 100644 --- a/packages/pyright-internal/src/common/core.ts +++ b/packages/pyright-internal/src/common/core.ts @@ -89,7 +89,7 @@ export function isNumber(x: unknown): x is number { return typeof x === 'number'; } -export function isBoolean(x: unknown): x is number { +export function isBoolean(x: unknown): x is boolean { return typeof x === 'boolean'; } diff --git a/packages/pyright-internal/src/common/fullAccessHost.ts b/packages/pyright-internal/src/common/fullAccessHost.ts new file mode 100644 index 000000000..3457829ca --- /dev/null +++ b/packages/pyright-internal/src/common/fullAccessHost.ts @@ -0,0 +1,183 @@ +/* + * fullAccessHost.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Implementation of host where it is allowed to run external executables. + */ + +import * as child_process from 'child_process'; + +import { PythonPathResult } from '../analyzer/pythonPathUtils'; +import { PythonPlatform } from './configOptions'; +import { assertNever } from './debug'; +import { FileSystem } from './fileSystem'; +import { HostKind, NoAccessHost } from './host'; +import { isDirectory, normalizePath } from './pathUtils'; +import { PythonVersion, versionFromMajorMinor } from './pythonVersion'; + +const extractSys = [ + 'import os, os.path, sys', + 'normalize = lambda p: os.path.normcase(os.path.normpath(p))', + 'cwd = normalize(os.getcwd())', + 'sys.path[:] = [p for p in sys.path if p != "" and normalize(p) != cwd]', + 'import json', + 'json.dump(dict(path=sys.path, prefix=sys.prefix), sys.stdout)', +].join('; '); + +export class LimitedAccessHost extends NoAccessHost { + override get kind(): HostKind { + return HostKind.LimitedAccess; + } + + override getPythonPlatform(logInfo?: string[]): PythonPlatform | undefined { + if (process.platform === 'darwin') { + return PythonPlatform.Darwin; + } else if (process.platform === 'linux') { + return PythonPlatform.Linux; + } else if (process.platform === 'win32') { + return PythonPlatform.Windows; + } + + return undefined; + } +} + +export class FullAccessHost extends LimitedAccessHost { + static createHost(kind: HostKind, fs: FileSystem) { + switch (kind) { + case HostKind.NoAccess: + return new NoAccessHost(); + case HostKind.LimitedAccess: + return new LimitedAccessHost(); + case HostKind.FullAccess: + return new FullAccessHost(fs); + default: + assertNever(kind); + } + } + + constructor(protected _fs: FileSystem) { + super(); + } + + override get kind(): HostKind { + return HostKind.FullAccess; + } + + override getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult { + const importFailureInfo = logInfo ?? []; + let result: PythonPathResult | undefined; + + if (pythonPath) { + result = this._getSearchPathResultFromInterpreter(this._fs, pythonPath, importFailureInfo); + } else { + // On non-Windows platforms, always default to python3 first. We want to + // avoid this on Windows because it might invoke a script that displays + // a dialog box indicating that python can be downloaded from the app store. + if (process.platform !== 'win32') { + result = this._getSearchPathResultFromInterpreter(this._fs, 'python3', importFailureInfo); + } + + // On some platforms, 'python3' might not exist. Try 'python' instead. + if (!result) { + result = this._getSearchPathResultFromInterpreter(this._fs, 'python', importFailureInfo); + } + } + + if (!result) { + result = { + paths: [], + prefix: '', + }; + } + + importFailureInfo.push(`Received ${result.paths.length} paths from interpreter`); + result.paths.forEach((path) => { + importFailureInfo.push(` ${path}`); + }); + + return result; + } + + override getPythonVersion(pythonPath?: string, logInfo?: string[]): PythonVersion | undefined { + const importFailureInfo = logInfo ?? []; + + try { + const commandLineArgs: string[] = [ + '-c', + 'import sys, json; json.dump(dict(major=sys.version_info[0], minor=sys.version_info[1]), sys.stdout)', + ]; + let execOutput: string; + + if (pythonPath) { + execOutput = child_process.execFileSync(pythonPath, commandLineArgs, { encoding: 'utf8' }); + } else { + execOutput = child_process.execFileSync('python', commandLineArgs, { encoding: 'utf8' }); + } + + const versionJson: { major: number; minor: number } = JSON.parse(execOutput); + + const version = versionFromMajorMinor(versionJson.major, versionJson.minor); + if (version === undefined) { + importFailureInfo.push( + `Python version ${versionJson.major}.${versionJson.minor} from interpreter is unsupported` + ); + return undefined; + } + + return version; + } catch { + importFailureInfo.push('Unable to get Python version from interpreter'); + return undefined; + } + } + + private _getSearchPathResultFromInterpreter( + fs: FileSystem, + interpreter: string, + importFailureInfo: string[] + ): PythonPathResult | undefined { + const result: PythonPathResult = { + paths: [], + prefix: '', + }; + + try { + const commandLineArgs: string[] = ['-c', extractSys]; + + importFailureInfo.push(`Executing interpreter: '${interpreter}'`); + const execOutput = child_process.execFileSync(interpreter, commandLineArgs, { encoding: 'utf8' }); + + // Parse the execOutput. It should be a JSON-encoded array of paths. + try { + const execSplit = JSON.parse(execOutput); + for (let execSplitEntry of execSplit.path) { + execSplitEntry = execSplitEntry.trim(); + if (execSplitEntry) { + const normalizedPath = normalizePath(execSplitEntry); + // Skip non-existent paths and broken zips/eggs. + if (fs.existsSync(normalizedPath) && isDirectory(fs, normalizedPath)) { + result.paths.push(normalizedPath); + } else { + importFailureInfo.push(`Skipping '${normalizedPath}' because it is not a valid directory`); + } + } + } + + result.prefix = execSplit.prefix; + + if (result.paths.length === 0) { + importFailureInfo.push(`Found no valid directories`); + } + } catch (err) { + importFailureInfo.push(`Could not parse output: '${execOutput}'`); + throw err; + } + } catch { + return undefined; + } + + return result; + } +} diff --git a/packages/pyright-internal/src/common/host.ts b/packages/pyright-internal/src/common/host.ts new file mode 100644 index 000000000..4885da096 --- /dev/null +++ b/packages/pyright-internal/src/common/host.ts @@ -0,0 +1,49 @@ +/* + * host.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Provides accesses to the host the language service runs on + */ + +import { PythonPathResult } from '../analyzer/pythonPathUtils'; +import { PythonPlatform } from './configOptions'; +import { PythonVersion } from './pythonVersion'; + +export const enum HostKind { + FullAccess, + LimitedAccess, + NoAccess, +} + +export interface Host { + readonly kind: HostKind; + getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult; + getPythonVersion(pythonPath?: string, logInfo?: string[]): PythonVersion | undefined; + getPythonPlatform(logInfo?: string[]): PythonPlatform | undefined; +} + +export class NoAccessHost implements Host { + get kind(): HostKind { + return HostKind.NoAccess; + } + + getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult { + logInfo?.push('No access to python executable.'); + + return { + paths: [], + prefix: '', + }; + } + + getPythonVersion(pythonPath?: string, logInfo?: string[]): PythonVersion | undefined { + return undefined; + } + + getPythonPlatform(logInfo?: string[]): PythonPlatform | undefined { + return undefined; + } +} + +export type HostFactory = () => Host; diff --git a/packages/pyright-internal/src/common/realFileSystem.ts b/packages/pyright-internal/src/common/realFileSystem.ts index 9aa7d6174..bdb663df1 100644 --- a/packages/pyright-internal/src/common/realFileSystem.ts +++ b/packages/pyright-internal/src/common/realFileSystem.ts @@ -372,7 +372,7 @@ interface WorkspaceFileWatcher extends FileWatcher { export class WorkspaceFileWatcherProvider implements FileWatcherProvider { private _fileWatchers: WorkspaceFileWatcher[] = []; - constructor(private _workspaceMap: WorkspaceMap) {} + constructor(private _workspaceMap: WorkspaceMap, private _console: ConsoleInterface) {} createFileWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher { // Determine which paths are located within one or more workspaces. @@ -404,7 +404,7 @@ export class WorkspaceFileWatcherProvider implements FileWatcherProvider { listener(event as FileWatcherEventType, filename) ); } catch (e: any) { - console.warn(`Exception received when installing recursive file system watcher: ${e}`); + this._console.warn(`Exception received when installing file system watcher: ${e}`); return undefined; } }) diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index 8fe466238..5fb429616 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -68,6 +68,7 @@ import { Diagnostic as AnalyzerDiagnostic, DiagnosticCategory } from './common/d import { DiagnosticRule } from './common/diagnosticRules'; import { LanguageServiceExtension } from './common/extensibility'; import { FileSystem, FileWatcherEventType, FileWatcherProvider, isInZipOrEgg } from './common/fileSystem'; +import { Host } from './common/host'; import { convertPathToUri, convertUriToPath } from './common/pathUtils'; import { ProgressReporter, ProgressReportTracker } from './common/progressReporter'; import { DocumentRange, Position } from './common/textRange'; @@ -206,15 +207,15 @@ export abstract class LanguageServerBase implements LanguageServerInterface { // File system abstraction. fs: FileSystem; - readonly console: ConsoleInterface; - - constructor(protected _serverOptions: ServerOptions, protected _connection: Connection) { + constructor( + protected _serverOptions: ServerOptions, + protected _connection: Connection, + readonly console: ConsoleInterface + ) { // Stash the base directory into a global variable. // This must happen before fs.getModulePath(). (global as any).__rootDirectory = _serverOptions.rootDirectory; - this.console = new ConsoleWithLogLevel(this._connection.console); - this.console.info( `${_serverOptions.productName} language server ${ _serverOptions.version && _serverOptions.version + ' ' @@ -304,9 +305,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface { return undefined; } - protected createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver { - return new ImportResolver(fs, options); - } + protected abstract createHost(): Host; + protected abstract createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver; protected createBackgroundAnalysisProgram( console: ConsoleInterface, @@ -343,6 +343,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface { name, this.fs, this.console, + this.createHost.bind(this), this.createImportResolver.bind(this), undefined, this._serverOptions.extension, diff --git a/packages/pyright-internal/src/languageService/completionProvider.ts b/packages/pyright-internal/src/languageService/completionProvider.ts index 3db92a8b6..3a3822380 100644 --- a/packages/pyright-internal/src/languageService/completionProvider.ts +++ b/packages/pyright-internal/src/languageService/completionProvider.ts @@ -39,6 +39,7 @@ import { getVariableInStubFileDocStrings, } from '../analyzer/typeDocStringUtils'; import { CallSignatureInfo, TypeEvaluator } from '../analyzer/typeEvaluator'; +import { printLiteralValue } from '../analyzer/typePrinter'; import { ClassType, FunctionType, @@ -61,6 +62,7 @@ import { getDeclaringModulesForType, getMembersForClass, getMembersForModule, + isLiteralType, isProperty, } from '../analyzer/typeUtils'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; @@ -1171,7 +1173,14 @@ export class CompletionProvider { } // Add call argument completions. - this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, completionList); + this._addCallArgumentCompletions( + parseNode, + priorWord, + priorText, + postText, + /*atArgument*/ false, + completionList + ); // Add symbols that are in scope. this._addSymbols(parseNode, priorWord, completionList); @@ -1209,7 +1218,13 @@ export class CompletionProvider { ); if (declaredTypeOfTarget) { - this._addLiteralValuesForTargetType(declaredTypeOfTarget, priorText, postText, completionList); + this._addLiteralValuesForTargetType( + declaredTypeOfTarget, + priorText, + priorWord, + postText, + completionList + ); } } } @@ -1257,6 +1272,7 @@ export class CompletionProvider { priorWord: string, priorText: string, postText: string, + atArgument: boolean, completionList: CompletionList ) { // If we're within the argument list of a call, add parameter names. @@ -1285,10 +1301,12 @@ export class CompletionProvider { ); if (comparePositions(this._position, callNameEnd) > 0) { - this._addNamedParameters(signatureInfo, priorWord, completionList); + if (!atArgument) { + this._addNamedParameters(signatureInfo, priorWord, completionList); + } // Add literals that apply to this parameter. - this._addLiteralValuesForArgument(signatureInfo, priorText, postText, completionList); + this._addLiteralValuesForArgument(signatureInfo, priorText, priorWord, postText, completionList); } } } @@ -1296,6 +1314,7 @@ export class CompletionProvider { private _addLiteralValuesForArgument( signatureInfo: CallSignatureInfo, priorText: string, + priorWord: string, postText: string, completionList: CompletionList ) { @@ -1312,7 +1331,7 @@ export class CompletionProvider { } const paramType = type.details.parameters[paramIndex].type; - this._addLiteralValuesForTargetType(paramType, priorText, postText, completionList); + this._addLiteralValuesForTargetType(paramType, priorText, priorWord, postText, completionList); return undefined; }); } @@ -1320,23 +1339,43 @@ export class CompletionProvider { private _addLiteralValuesForTargetType( type: Type, priorText: string, + priorWord: string, postText: string, completionList: CompletionList ) { const quoteValue = this._getQuoteValueFromPriorText(priorText); - doForEachSubtype(type, (subtype) => { - if (isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'str') && subtype.literalValue !== undefined) { - this._addStringLiteralToCompletionList( - subtype.literalValue as string, - quoteValue.stringValue, - postText, - quoteValue.quoteCharacter, - completionList - ); + this._getSubTypesWithLiteralValues(type).forEach((v) => { + if (ClassType.isBuiltIn(v, 'str')) { + const value = printLiteralValue(v, quoteValue.quoteCharacter); + if (quoteValue.stringValue === undefined) { + this._addNameToCompletionList(value, CompletionItemKind.Constant, priorWord, completionList, { + sortText: this._makeSortText(SortCategory.LiteralValue, v.literalValue as string), + }); + } else { + this._addStringLiteralToCompletionList( + value.substr(1, value.length - 2), + quoteValue.stringValue, + postText, + quoteValue.quoteCharacter, + completionList + ); + } } }); } + private _getSubTypesWithLiteralValues(type: Type) { + const values: ClassType[] = []; + + doForEachSubtype(type, (subtype) => { + if (isClassInstance(subtype) && isLiteralType(subtype)) { + values.push(subtype); + } + }); + + return values; + } + private _getDictionaryKeys(indexNode: IndexNode, invocationNode: ParseNode) { if (indexNode.baseExpression.nodeType !== ParseNodeType.Name) { // This completion only supports simple name case @@ -1349,10 +1388,34 @@ export class CompletionProvider { } // Must be dict type - if (!ClassType.isBuiltIn(baseType, 'dict')) { + if (!ClassType.isBuiltIn(baseType, 'dict') && !ClassType.isBuiltIn(baseType, 'Mapping')) { return []; } + // See whether dictionary is typed using Literal types. If it is, return those literal keys. + // For now, we are not using __getitem__ since we don't have a way to get effective parameter type of __getitem__. + if (baseType.typeArguments?.length === 2) { + const keys: string[] = []; + + this._getSubTypesWithLiteralValues(baseType.typeArguments[0]).forEach((v) => { + if ( + !ClassType.isBuiltIn(v, 'str') && + !ClassType.isBuiltIn(v, 'int') && + !ClassType.isBuiltIn(v, 'bool') && + !ClassType.isBuiltIn(v, 'bytes') && + !ClassType.isEnumClass(v) + ) { + return; + } + + keys.push(printLiteralValue(v, this._parseResults.tokenizerOutput.predominantSingleQuoteCharacter)); + }); + + if (keys.length > 0) { + return keys; + } + } + // Must be local variable/parameter const declarations = this._evaluator.getDeclarationsForNameNode(indexNode.baseExpression) ?? []; const declaration = declarations.length > 0 ? declarations[0] : undefined; @@ -1513,16 +1576,20 @@ export class CompletionProvider { const declaredTypeOfTarget = this._evaluator.getDeclaredTypeForExpression(parentNode.leftExpression); if (declaredTypeOfTarget) { - this._addLiteralValuesForTargetType(declaredTypeOfTarget, priorText, postText, completionList); + this._addLiteralValuesForTargetType( + declaredTypeOfTarget, + priorText, + priorWord, + postText, + completionList + ); } } else { - // Make sure we are not inside of the string literal. debug.assert(parseNode.nodeType === ParseNodeType.String); const offset = convertPositionToOffset(this._position, this._parseResults.tokenizerOutput.lines)!; - if (offset <= parentNode.start || TextRange.getEnd(parseNode) <= offset) { - this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, completionList); - } + const atArgument = parentNode.start < offset && offset < TextRange.getEnd(parseNode); + this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, atArgument, completionList); } return { completionList }; diff --git a/packages/pyright-internal/src/pyright.ts b/packages/pyright-internal/src/pyright.ts index 041107cae..0e1445255 100644 --- a/packages/pyright-internal/src/pyright.ts +++ b/packages/pyright-internal/src/pyright.ts @@ -30,6 +30,7 @@ import { versionFromString } from './common/pythonVersion'; import { PyrightFileSystem } from './pyrightFileSystem'; import { PackageTypeReport, TypeKnownStatus } from './analyzer/packageTypeReport'; import { createDeferred } from './common/deferred'; +import { FullAccessHost } from './common/fullAccessHost'; const toolName = 'pyright'; @@ -269,8 +270,7 @@ async function processArgs(): Promise { options.watchForSourceChanges = watch; options.watchForConfigChanges = watch; - const service = new AnalyzerService('', fileSystem, output); - + const service = new AnalyzerService('', fileSystem, output, () => new FullAccessHost(fileSystem)); const exitStatus = createDeferred(); service.setCompletionCallback((results) => { diff --git a/packages/pyright-internal/src/server.ts b/packages/pyright-internal/src/server.ts index 7df3ecf28..8d17b1661 100644 --- a/packages/pyright-internal/src/server.ts +++ b/packages/pyright-internal/src/server.ts @@ -16,14 +16,19 @@ import { } from 'vscode-languageserver'; import { AnalysisResults } from './analyzer/analysis'; +import { ImportResolver } from './analyzer/importResolver'; import { isPythonBinary } from './analyzer/pythonPathUtils'; import { BackgroundAnalysis } from './backgroundAnalysis'; import { BackgroundAnalysisBase } from './backgroundAnalysisBase'; import { CommandController } from './commands/commandController'; import { getCancellationFolderName } from './common/cancellationUtils'; -import { LogLevel } from './common/console'; +import { ConfigOptions } from './common/configOptions'; +import { ConsoleWithLogLevel, LogLevel } from './common/console'; import { isDebugMode, isString } from './common/core'; import { FileBasedCancellationProvider } from './common/fileBasedCancellationUtils'; +import { FileSystem } from './common/fileSystem'; +import { FullAccessHost } from './common/fullAccessHost'; +import { Host } from './common/host'; import { convertUriToPath, resolvePaths } from './common/pathUtils'; import { ProgressReporter } from './common/progressReporter'; import { createFromRealFileSystem, WorkspaceFileWatcherProvider } from './common/realFileSystem'; @@ -45,9 +50,10 @@ export class PyrightServer extends LanguageServerBase { // be __dirname. const rootDirectory = (global as any).__rootDirectory || __dirname; + const console = new ConsoleWithLogLevel(connection.console); const workspaceMap = new WorkspaceMap(); - const fileWatcherProvider = new WorkspaceFileWatcherProvider(workspaceMap); - const fileSystem = createFromRealFileSystem(connection.console, fileWatcherProvider); + const fileWatcherProvider = new WorkspaceFileWatcherProvider(workspaceMap, console); + const fileSystem = createFromRealFileSystem(console, fileWatcherProvider); super( { @@ -61,7 +67,8 @@ export class PyrightServer extends LanguageServerBase { maxAnalysisTimeInForeground, supportedCodeActions: [CodeActionKind.QuickFix, CodeActionKind.SourceOrganizeImports], }, - connection + connection, + console ); this._controller = new CommandController(this); @@ -211,6 +218,14 @@ export class PyrightServer extends LanguageServerBase { return new BackgroundAnalysis(this.console); } + protected override createHost() { + return new FullAccessHost(this.fs); + } + + protected override createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver { + return new ImportResolver(fs, options, host); + } + protected executeCommand(params: ExecuteCommandParams, token: CancellationToken): Promise { return this._controller.execute(params, token); } diff --git a/packages/pyright-internal/src/tests/config.test.ts b/packages/pyright-internal/src/tests/config.test.ts index 883b08a35..e1598c325 100644 --- a/packages/pyright-internal/src/tests/config.test.ts +++ b/packages/pyright-internal/src/tests/config.test.ts @@ -13,6 +13,7 @@ import { AnalyzerService } from '../analyzer/service'; import { CommandLineOptions } from '../common/commandLineOptions'; import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { NullConsole } from '../common/console'; +import { NoAccessHost } from '../common/host'; import { combinePaths, getBaseFileName, normalizePath, normalizeSlashes } from '../common/pathUtils'; import { PythonVersion } from '../common/pythonVersion'; import { createFromRealFileSystem } from '../common/realFileSystem'; @@ -190,7 +191,7 @@ test('PythonPlatform', () => { "extraPaths" : [] }]}`); - configOptions.initializeFromJson(json, undefined, nullConsole); + configOptions.initializeFromJson(json, undefined, nullConsole, new NoAccessHost()); const env = configOptions.executionEnvironments[0]; assert.strictEqual(env.pythonPlatform, 'platform'); diff --git a/packages/pyright-internal/src/tests/filesystem.test.ts b/packages/pyright-internal/src/tests/filesystem.test.ts index a4c8e6b9b..f096f9143 100644 --- a/packages/pyright-internal/src/tests/filesystem.test.ts +++ b/packages/pyright-internal/src/tests/filesystem.test.ts @@ -9,7 +9,7 @@ import assert from 'assert'; import { combinePaths, normalizeSlashes } from '../common/pathUtils'; -import * as host from './harness/host'; +import * as host from './harness/testHost'; import * as factory from './harness/vfs/factory'; import * as vfs from './harness/vfs/filesystem'; diff --git a/packages/pyright-internal/src/tests/fourSlashParser.test.ts b/packages/pyright-internal/src/tests/fourSlashParser.test.ts index e4be8df02..de53f3112 100644 --- a/packages/pyright-internal/src/tests/fourSlashParser.test.ts +++ b/packages/pyright-internal/src/tests/fourSlashParser.test.ts @@ -13,7 +13,7 @@ import { combinePaths, getBaseFileName, normalizeSlashes } from '../common/pathU import { compareStringsCaseSensitive } from '../common/stringUtils'; import { parseTestData } from './harness/fourslash/fourSlashParser'; import { CompilerSettings } from './harness/fourslash/fourSlashTypes'; -import * as host from './harness/host'; +import * as host from './harness/testHost'; import * as factory from './harness/vfs/factory'; test('GlobalOptions', () => { diff --git a/packages/pyright-internal/src/tests/fourSlashRunner.test.ts b/packages/pyright-internal/src/tests/fourSlashRunner.test.ts index a18acb053..7b174f8ea 100644 --- a/packages/pyright-internal/src/tests/fourSlashRunner.test.ts +++ b/packages/pyright-internal/src/tests/fourSlashRunner.test.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import { normalizeSlashes } from '../common/pathUtils'; import { runFourSlashTest } from './harness/fourslash/runner'; -import * as host from './harness/host'; +import * as host from './harness/testHost'; import { MODULE_PATH } from './harness/vfs/filesystem'; describe('fourslash tests', () => { diff --git a/packages/pyright-internal/src/tests/fourslash/completions.call.stringLiteral.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/completions.call.stringLiteral.fourslash.ts new file mode 100644 index 000000000..d03e7e7da --- /dev/null +++ b/packages/pyright-internal/src/tests/fourslash/completions.call.stringLiteral.fourslash.ts @@ -0,0 +1,55 @@ +/// + +// @filename: test.py +//// from typing import Literal +//// +//// def thing(foo: Literal["hello", "world"]): +//// pass +//// +//// thing([|/*marker1*/|]) +//// thing(hel[|/*marker2*/|]) +//// thing([|"/*marker3*/"|]) + +{ + // @ts-ignore + await helper.verifyCompletion('included', 'markdown', { + marker1: { + completions: [ + { + label: '"hello"', + kind: Consts.CompletionItemKind.Constant, + }, + { + label: '"world"', + kind: Consts.CompletionItemKind.Constant, + }, + ], + }, + marker2: { + completions: [ + { + label: '"hello"', + kind: Consts.CompletionItemKind.Constant, + }, + ], + }, + }); + + // @ts-ignore + await helper.verifyCompletion('exact', 'markdown', { + marker3: { + completions: [ + { + label: '"hello"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker3'), newText: '"hello"' }, + }, + { + label: '"world"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker3'), newText: '"world"' }, + }, + ], + }, + }); +} diff --git a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.literalTypes.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.literalTypes.fourslash.ts new file mode 100644 index 000000000..91eb2d7f4 --- /dev/null +++ b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.literalTypes.fourslash.ts @@ -0,0 +1,161 @@ +/// + +// @filename: literal_types.py +//// from typing import Mapping, Literal +//// +//// d: Mapping[Literal["key", "key2"], int] = { "key" : 1 } +//// d[[|/*marker1*/|]] + +// @filename: parameter_mapping.py +//// from typing import Mapping, Literal +//// +//// def foo(d: Mapping[Literal["key", "key2"], int]): +//// d[[|/*marker2*/|]] + +// @filename: literal_types_mixed.py +//// from typing import Mapping, Literal +//// +//// d: Mapping[Literal["key", 1], int] = { "key" : 1 } +//// d[[|/*marker3*/|]] + +// @filename: parameter_dict.py +//// from typing import Dict, Literal +//// +//// def foo(d: Dict[Literal["key", "key2"], int]): +//// d[[|/*marker4*/|]] + +// @filename: literal_types_boolean.py +//// from typing import Dict, Literal +//// +//// d: Dict[Literal[True, False], int] = { True: 1, False: 2 } +//// d[[|/*marker5*/|]] + +// @filename: literal_types_enum.py +//// from typing import Dict, Literal +//// from enum import Enum +//// +//// class MyEnum(Enum): +//// red = 1 +//// blue = 2 +//// +//// def foo(d: Dict[Literal[MyEnum.red, MyEum.blue], int]): +//// d[[|/*marker6/|]] + +// @filename: literal_bytes.py +//// from typing import Mapping, Literal +//// +//// d: Mapping[Literal[b"key", b"key2"], int] = { b"key" : 1 } +//// d[[|/*marker7*/|]] + +{ + helper.openFiles(helper.getMarkers().map((m) => m.fileName)); + + // @ts-ignore + await helper.verifyCompletion('exact', 'markdown', { + marker1: { + completions: [ + { + label: '"key"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker1'), newText: '"key"' }, + detail: 'Dictionary key', + }, + { + label: '"key2"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker1'), newText: '"key2"' }, + detail: 'Dictionary key', + }, + ], + }, + marker2: { + completions: [ + { + label: '"key"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker2'), newText: '"key"' }, + detail: 'Dictionary key', + }, + { + label: '"key2"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker2'), newText: '"key2"' }, + detail: 'Dictionary key', + }, + ], + }, + marker3: { + completions: [ + { + label: '"key"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker3'), newText: '"key"' }, + detail: 'Dictionary key', + }, + { + label: '1', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + ], + }, + marker4: { + completions: [ + { + label: '"key"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker4'), newText: '"key"' }, + detail: 'Dictionary key', + }, + { + label: '"key2"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker4'), newText: '"key2"' }, + detail: 'Dictionary key', + }, + ], + }, + marker5: { + completions: [ + { + label: 'True', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + { + label: 'False', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + ], + }, + marker6: { + completions: [ + { + label: 'MyEnum.red', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + { + label: 'MyEnum.blue', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + ], + }, + marker7: { + completions: [ + { + label: 'b"key"', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + { + label: 'b"key2"', + kind: Consts.CompletionItemKind.Constant, + detail: 'Dictionary key', + }, + ], + }, + }); +} diff --git a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts index 02bd91857..671e3f9a3 100644 --- a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts +++ b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts @@ -25,7 +25,7 @@ // @filename: dict_key_name_conflicts.py //// keyString = "key" //// d = dict(keyString=1) -//// d[keyStr/*marker6*/] +//// d[keyStr[|/*marker6*/|]] // @filename: dict_key_mixed_literals.py //// d = { "key": 1, 1 + 2: 1 } diff --git a/packages/pyright-internal/src/tests/fourslash/completions.stringLiteral.escape.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/completions.stringLiteral.escape.fourslash.ts new file mode 100644 index 000000000..ddc1dfc27 --- /dev/null +++ b/packages/pyright-internal/src/tests/fourslash/completions.stringLiteral.escape.fourslash.ts @@ -0,0 +1,71 @@ +/// + +// @filename: test.py +//// from typing import Literal +//// +//// def method(foo: Literal["'\"", '"\'', "'mixed'"]): +//// pass +//// +//// method([|/*marker1*/|]) +//// method([|"/*marker2*/"|]) +//// method([|'/*marker3*/'|]) + +{ + // @ts-ignore + await helper.verifyCompletion('included', 'markdown', { + marker1: { + completions: [ + { + label: '"\'\\""', + kind: Consts.CompletionItemKind.Constant, + }, + { + label: '"\\"\'"', + kind: Consts.CompletionItemKind.Constant, + }, + { + label: '"\'mixed\'"', + kind: Consts.CompletionItemKind.Constant, + }, + ], + }, + marker2: { + completions: [ + { + label: '"\'\\""', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker2'), newText: '"\'\\""' }, + }, + { + label: '"\\"\'"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker2'), newText: '"\\"\'"' }, + }, + { + label: '"\'mixed\'"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker2'), newText: '"\'mixed\'"' }, + }, + ], + }, + marker3: { + completions: [ + { + label: "'\\'\"'", + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker3'), newText: "'\\'\"'" }, + }, + { + label: "'\"\\''", + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker3'), newText: "'\"\\''" }, + }, + { + label: "'\\'mixed\\''", + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker3'), newText: "'\\'mixed\\''" }, + }, + ], + }, + }); +} diff --git a/packages/pyright-internal/src/tests/fourslash/hover.docFromScr.stringFormat.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/hover.docFromScr.stringFormat.fourslash.ts index b1409d0d4..1b0631b15 100644 --- a/packages/pyright-internal/src/tests/fourslash/hover.docFromScr.stringFormat.fourslash.ts +++ b/packages/pyright-internal/src/tests/fourslash/hover.docFromScr.stringFormat.fourslash.ts @@ -16,9 +16,9 @@ //// # escaped quotes //// [|/*marker9*/singleQuotesWithEscapedQuote|]= '\'' -//// [|/*marker10*/doubleQuotesWithEscapedQuote|]= "\""" +//// [|/*marker10*/doubleQuotesWithEscapedQuote|]= "\"" //// [|/*marker11*/tripleQuotesWithEscapedQuote|]= '''\n\'\'\'''' -//// [|/*marker12*/tripleDoubleQuotesWithEscapedQuote|]= """\n\"\"\""""" +//// [|/*marker12*/tripleDoubleQuotesWithEscapedQuote|]= """\n\"\"\"""" //// # mixing quotes //// [|/*marker13*/singleQuotesWithDouble|]= '"' @@ -40,13 +40,13 @@ helper.verifyHover('markdown', { marker7: `\`\`\`python\n(variable) simpleTripleQuotes: Literal['foo\\nbar']\n\`\`\``, marker8: `\`\`\`python\n(variable) simpleTripleDoubleQuotes: Literal['foo\\nbar']\n\`\`\``, marker9: `\`\`\`python\n(variable) singleQuotesWithEscapedQuote: Literal['\\\'']\n\`\`\``, - marker10: `\`\`\`python\n(variable) doubleQuotesWithEscapedQuote: Literal['\\\"']\n\`\`\``, + marker10: `\`\`\`python\n(variable) doubleQuotesWithEscapedQuote: Literal['"']\n\`\`\``, marker11: `\`\`\`python\n(variable) tripleQuotesWithEscapedQuote: Literal['\\n\\'\\'\\'']\n\`\`\``, - marker12: `\`\`\`python\n(variable) tripleDoubleQuotesWithEscapedQuote: Literal['\\n\\\"\\\"\\\"']\n\`\`\``, - marker13: `\`\`\`python\n(variable) singleQuotesWithDouble: Literal['\\"']\n\`\`\``, - marker14: `\`\`\`python\n(variable) singleQuotesWithTripleDouble: Literal['\\"\\"\\"']\n\`\`\``, - marker15: `\`\`\`python\n(variable) singleTripleQuoteWithSingleAndDoubleQuote: Literal[' \\'\\"\\' ']\n\`\`\``, - marker16: `\`\`\`python\n(variable) html: Literal['\\nTitle']\n\`\`\``, - marker17: `\`\`\`python\n(variable) htmlWithSingleQuotes: Literal['\\nTitle\\'s']\n\`\`\``, - marker18: `\`\`\`python\n(variable) htmlWithTripleEscapedQuotes: Literal['\\nTitle\\'\\'\\'s']\n\`\`\``, + marker12: `\`\`\`python\n(variable) tripleDoubleQuotesWithEscapedQuote: Literal['\\n"""']\n\`\`\``, + marker13: `\`\`\`python\n(variable) singleQuotesWithDouble: Literal['"']\n\`\`\``, + marker14: `\`\`\`python\n(variable) singleQuotesWithTripleDouble: Literal['"""']\n\`\`\``, + marker15: `\`\`\`python\n(variable) singleTripleQuoteWithSingleAndDoubleQuote: Literal[' \\'"\\' ']\n\`\`\``, + marker16: `\`\`\`python\n(variable) html: Literal['\\nTitle']\n\`\`\``, + marker17: `\`\`\`python\n(variable) htmlWithSingleQuotes: Literal['\\nTitle\\'s']\n\`\`\``, + marker18: `\`\`\`python\n(variable) htmlWithTripleEscapedQuotes: Literal['\\nTitle\\'\\'\\'s']\n\`\`\``, }); diff --git a/packages/pyright-internal/src/tests/harness/fourslash/runner.ts b/packages/pyright-internal/src/tests/harness/fourslash/runner.ts index 63405e4bc..a99b81e42 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/runner.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/runner.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import { combinePaths } from '../../../common/pathUtils'; -import * as host from '../host'; +import * as host from '../testHost'; import { parseTestData } from './fourSlashParser'; import { FourSlashData } from './fourSlashTypes'; import { HostSpecificFeatures, TestState } from './testState'; diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts index 4ca27bdad..8a9457451 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts @@ -34,6 +34,7 @@ import * as debug from '../../../common/debug'; import { createDeferred } from '../../../common/deferred'; import { DiagnosticCategory } from '../../../common/diagnostic'; import { FileEditAction } from '../../../common/editAction'; +import { NoAccessHost } from '../../../common/host'; import { combinePaths, comparePaths, @@ -54,7 +55,7 @@ import { convertHoverResults } from '../../../languageService/hoverProvider'; import { ParseResults } from '../../../parser/parser'; import { Tokenizer } from '../../../parser/tokenizer'; import { PyrightFileSystem } from '../../../pyrightFileSystem'; -import * as host from '../host'; +import * as host from '../testHost'; import { stringify } from '../utils'; import { createFromFileSystem } from '../vfs/factory'; import * as vfs from '../vfs/filesystem'; @@ -136,7 +137,7 @@ export class TestState { throw new Error(`Failed to parse test ${file.fileName}: ${e.message}`); } - configOptions.initializeFromJson(this.rawConfigJson, 'basic', nullConsole); + configOptions.initializeFromJson(this.rawConfigJson, 'basic', nullConsole, new NoAccessHost()); this._applyTestConfigOptions(configOptions); } else { files[file.fileName] = new vfs.File(file.content, { meta: file.fileOptions, encoding: 'utf8' }); @@ -1554,7 +1555,14 @@ export class TestState { configOptions: ConfigOptions ) { // we do not initiate automatic analysis or file watcher in test. - const service = new AnalyzerService('test service', this.fs, nullConsole, importResolverFactory, configOptions); + const service = new AnalyzerService( + 'test service', + this.fs, + nullConsole, + () => new NoAccessHost(), + importResolverFactory, + configOptions + ); // directly set files to track rather than using fileSpec from config // to discover those files from file system diff --git a/packages/pyright-internal/src/tests/harness/host.ts b/packages/pyright-internal/src/tests/harness/testHost.ts similarity index 100% rename from packages/pyright-internal/src/tests/harness/host.ts rename to packages/pyright-internal/src/tests/harness/testHost.ts diff --git a/packages/pyright-internal/src/tests/harness/vfs/factory.ts b/packages/pyright-internal/src/tests/harness/vfs/factory.ts index ff4bcaaf1..4d3995ebb 100644 --- a/packages/pyright-internal/src/tests/harness/vfs/factory.ts +++ b/packages/pyright-internal/src/tests/harness/vfs/factory.ts @@ -9,7 +9,7 @@ import * as pathConsts from '../../../common/pathConsts'; import { combinePaths, getDirectoryPath, normalizeSlashes, resolvePaths } from '../../../common/pathUtils'; import { GlobalMetadataOptionNames } from '../fourslash/fourSlashTypes'; -import { TestHost } from '../host'; +import { TestHost } from '../testHost'; import { bufferFrom } from '../utils'; import { FileSet, diff --git a/packages/pyright-internal/src/tests/importResolver.test.ts b/packages/pyright-internal/src/tests/importResolver.test.ts index 7147cd0f5..c11a0024e 100644 --- a/packages/pyright-internal/src/tests/importResolver.test.ts +++ b/packages/pyright-internal/src/tests/importResolver.test.ts @@ -8,6 +8,7 @@ import assert from 'assert'; import { ImportResolver } from '../analyzer/importResolver'; import { ConfigOptions } from '../common/configOptions'; +import { FullAccessHost } from '../common/fullAccessHost'; import { lib, sitePackages, typeshedFallback } from '../common/pathConsts'; import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; import { PyrightFileSystem } from '../pyrightFileSystem'; @@ -100,7 +101,7 @@ test('side by side files', () => { const fs = createFileSystem(files); const configOptions = getConfigOption(fs); - const importResolver = new ImportResolver(fs, configOptions); + const importResolver = new ImportResolver(fs, configOptions, new FullAccessHost(fs)); // Real side by side stub file win over virtual one. const sideBySideResult = importResolver.resolveImport(myFile, configOptions.findExecEnvironment(myFile), { @@ -350,7 +351,7 @@ function getImportResult( const configOptions = getConfigOption(fs); setup(configOptions); - const importResolver = new ImportResolver(fs, configOptions); + const importResolver = new ImportResolver(fs, configOptions, new FullAccessHost(fs)); const importResult = importResolver.resolveImport(file, configOptions.findExecEnvironment(file), { leadingDots: 0, nameParts: nameParts, diff --git a/packages/pyright-internal/src/tests/sourceFile.test.ts b/packages/pyright-internal/src/tests/sourceFile.test.ts index 79694f403..ea756db71 100644 --- a/packages/pyright-internal/src/tests/sourceFile.test.ts +++ b/packages/pyright-internal/src/tests/sourceFile.test.ts @@ -10,6 +10,7 @@ import { ImportResolver } from '../analyzer/importResolver'; import { SourceFile } from '../analyzer/sourceFile'; import { ConfigOptions } from '../common/configOptions'; +import { FullAccessHost } from '../common/fullAccessHost'; import { combinePaths } from '../common/pathUtils'; import { createFromRealFileSystem } from '../common/realFileSystem'; @@ -18,7 +19,7 @@ test('Empty', () => { const fs = createFromRealFileSystem(); const sourceFile = new SourceFile(fs, filePath, '', false, false); const configOptions = new ConfigOptions(process.cwd()); - const importResolver = new ImportResolver(fs, configOptions); + const importResolver = new ImportResolver(fs, configOptions, new FullAccessHost(fs)); sourceFile.parse(configOptions, importResolver); }); diff --git a/packages/pyright-internal/src/tests/testUtils.ts b/packages/pyright-internal/src/tests/testUtils.ts index 0e772c255..0fdbf36c4 100644 --- a/packages/pyright-internal/src/tests/testUtils.ts +++ b/packages/pyright-internal/src/tests/testUtils.ts @@ -21,6 +21,7 @@ import { cloneDiagnosticRuleSet, ConfigOptions, ExecutionEnvironment } from '../ import { fail } from '../common/debug'; import { Diagnostic, DiagnosticCategory } from '../common/diagnostic'; import { DiagnosticSink, TextRangeDiagnosticSink } from '../common/diagnosticSink'; +import { FullAccessHost } from '../common/fullAccessHost'; import { createFromRealFileSystem } from '../common/realFileSystem'; import { ParseOptions, Parser, ParseResults } from '../parser/parser'; @@ -151,7 +152,9 @@ export function typeAnalyzeSampleFiles( ): FileAnalysisResult[] { // Always enable "test mode". configOptions.internalTestMode = true; - const importResolver = new ImportResolver(createFromRealFileSystem(), configOptions); + + const fs = createFromRealFileSystem(); + const importResolver = new ImportResolver(fs, configOptions, new FullAccessHost(fs)); const program = new Program(importResolver, configOptions); const filePaths = fileNames.map((name) => resolveSampleFilePath(name));