mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-10 23:45:54 +03:00
Added literal type support in more places, fixed using wrong console bug and some refactoring around Host environment (#2176)
This commit is contained in:
parent
bf8b5511d3
commit
abd41b7273
@ -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",
|
||||
|
@ -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() {
|
||||
|
@ -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<string, string[]>();
|
||||
private _cachedImportResults = new Map<string, CachedImportResults>();
|
||||
private _cachedModuleNameResults = new Map<string, Map<string, ModuleNameAndType>>();
|
||||
@ -87,12 +86,11 @@ export class ImportResolver {
|
||||
private _cachedTypeshedThirdPartyPackageRoots: string[] | undefined;
|
||||
private _cachedEntriesForPath = new Map<string, Dirent[]>();
|
||||
|
||||
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<string, string[]>();
|
||||
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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[] = [];
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
183
packages/pyright-internal/src/common/fullAccessHost.ts
Normal file
183
packages/pyright-internal/src/common/fullAccessHost.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
49
packages/pyright-internal/src/common/host.ts
Normal file
49
packages/pyright-internal/src/common/host.ts
Normal file
@ -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;
|
@ -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;
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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 };
|
||||
|
@ -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<ExitStatus> {
|
||||
options.watchForSourceChanges = watch;
|
||||
options.watchForConfigChanges = watch;
|
||||
|
||||
const service = new AnalyzerService('<default>', fileSystem, output);
|
||||
|
||||
const service = new AnalyzerService('<default>', fileSystem, output, () => new FullAccessHost(fileSystem));
|
||||
const exitStatus = createDeferred<ExitStatus>();
|
||||
|
||||
service.setCompletionCallback((results) => {
|
||||
|
@ -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<any> {
|
||||
return this._controller.execute(params, token);
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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', () => {
|
||||
|
@ -0,0 +1,55 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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"' },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
@ -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 }
|
||||
|
@ -0,0 +1,71 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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\\''" },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
@ -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['<!DOCTYPE html><html lang=\\"en\\">\\n<head><title>Title</title></head></html>']\n\`\`\``,
|
||||
marker17: `\`\`\`python\n(variable) htmlWithSingleQuotes: Literal['<!DOCTYPE html><html lang=\\"en\\">\\n<head><title>Title\\'s</title></head></html>']\n\`\`\``,
|
||||
marker18: `\`\`\`python\n(variable) htmlWithTripleEscapedQuotes: Literal['<!DOCTYPE html><html lang=\\"en\\">\\n<head><title>Title\\'\\'\\'s</title></head></html>']\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['<!DOCTYPE html><html lang="en">\\n<head><title>Title</title></head></html>']\n\`\`\``,
|
||||
marker17: `\`\`\`python\n(variable) htmlWithSingleQuotes: Literal['<!DOCTYPE html><html lang="en">\\n<head><title>Title\\'s</title></head></html>']\n\`\`\``,
|
||||
marker18: `\`\`\`python\n(variable) htmlWithTripleEscapedQuotes: Literal['<!DOCTYPE html><html lang="en">\\n<head><title>Title\\'\\'\\'s</title></head></html>']\n\`\`\``,
|
||||
});
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user