Added literal type support in more places, fixed using wrong console bug and some refactoring around Host environment (#2176)

This commit is contained in:
Heejae Chang 2021-08-11 13:11:54 -07:00 committed by GitHub
parent bf8b5511d3
commit abd41b7273
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 819 additions and 279 deletions

View File

@ -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",

View File

@ -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() {

View File

@ -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;

View File

@ -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);
}

View File

@ -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[] = [];

View File

@ -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) {

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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';
}

View 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;
}
}

View 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;

View File

@ -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;
}
})

View File

@ -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,

View File

@ -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 };

View File

@ -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) => {

View File

@ -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);
}

View File

@ -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');

View File

@ -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';

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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"' },
},
],
},
});
}

View File

@ -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',
},
],
},
});
}

View File

@ -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 }

View File

@ -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\\''" },
},
],
},
});
}

View File

@ -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\`\`\``,
});

View File

@ -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';

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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);
});

View File

@ -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));