From 1d56d6765f3cb7f579081d5b3c363a3df7d83337 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 15 May 2020 13:01:55 -0700 Subject: [PATCH] Fix windows bug, add PEP 604 printing, fix unused variable action, add stubPath and diagnosticMode (#673) --- client/package.json | 25 +++++++++++---- client/schemas/pyrightconfig.schema.json | 4 +-- docs/configuration.md | 4 +-- docs/settings.md | 5 +-- docs/type-stubs.md | 2 +- .../src/analyzer/backgroundAnalysisProgram.ts | 6 ++-- server/src/analyzer/checker.ts | 11 +++++-- server/src/analyzer/importResolver.ts | 16 +++++----- server/src/analyzer/importStatementUtils.ts | 7 +++- server/src/analyzer/program.ts | 19 ++++++----- server/src/analyzer/service.ts | 32 ++++++++++++------- server/src/analyzer/typeEvaluator.ts | 24 ++++++++++++-- server/src/analyzer/typeStubWriter.ts | 6 ++-- server/src/backgroundAnalysisBase.ts | 10 +++--- server/src/commands/commands.ts | 1 + server/src/commands/createTypeStub.ts | 6 ---- server/src/common/commandLineOptions.ts | 3 ++ server/src/common/configOptions.ts | 26 ++++++++++++--- server/src/common/diagnosticSink.ts | 18 ++++++++--- server/src/common/pathUtils.ts | 4 +-- server/src/languageServerBase.ts | 5 +++ .../analyzerServiceExecutor.ts | 10 +++++- server/src/server.ts | 20 ++++++++++-- server/src/tests/fourslash/fourslash.ts | 9 ++++-- .../signature.complicated.fourslash.ts | 4 +-- .../src/tests/harness/fourslash/testState.ts | 32 +++++++++++++------ server/src/tests/testState.test.ts | 27 +++++++++++++++- 27 files changed, 241 insertions(+), 95 deletions(-) diff --git a/client/package.json b/client/package.json index 15cbf9f5c..42af30f5b 100644 --- a/client/package.json +++ b/client/package.json @@ -81,6 +81,25 @@ "description": "Automatically add common search paths like 'src'?", "scope": "resource" }, + "python.analysis.stubPath": { + "type": "string", + "default": "", + "description": "Path to directory containing custom type stub files.", + "scope": "resource" + }, + "python.analysis.diagnosticMode": { + "type": "string", + "default": "openFilesOnly", + "enum": [ + "openFilesOnly", + "workspace" + ], + "enumDescriptions": [ + "Analyzes and reports errors on only open files.", + "Analyzes and reports errors on all files in the workspace." + ], + "scope": "resource" + }, "python.pythonPath": { "type": "string", "default": "python", @@ -105,12 +124,6 @@ "description": "Disables the “Organize Imports” command.", "scope": "resource" }, - "pyright.openFilesOnly": { - "type": "boolean", - "default": true, - "description": "Report errors only for currently-open files.", - "scope": "resource" - }, "pyright.useLibraryCodeForTypes": { "type": "boolean", "default": false, diff --git a/client/schemas/pyrightconfig.schema.json b/client/schemas/pyrightconfig.schema.json index 4414f592a..1ebcc2f35 100644 --- a/client/schemas/pyrightconfig.schema.json +++ b/client/schemas/pyrightconfig.schema.json @@ -82,8 +82,8 @@ "default": "", "pattern": "^(.*)$" }, - "typingsPath": { - "$id": "#/properties/typingsPath", + "stubPath": { + "$id": "#/properties/stubPath", "type": "string", "title": "Path to directory containing custom type stub files", "default": "", diff --git a/docs/configuration.md b/docs/configuration.md index 22e53e067..ffcbef399 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,7 +16,7 @@ Relative paths specified within the config file are relative to the config file **typeshedPath** [path, optional]: Path to a directory that contains typeshed type stub files. Pyright ships with an internal copy of some typeshed type stubs (those that cover the Python stdlib packages). If you want to use a full copy of the typeshed type stubs (including those for third-party packages), you can clone the [typeshed github repo](https://github.com/python/typeshed) to a local directory and reference the location with this path. -**typingsPath** [path, optional]: Path to a directory that contains custom type stubs. Each package's type stub file(s) are expected to be in its own subdirectory. The default value of this setting is "./typings". +**stubPath** [path, optional]: Path to a directory that contains custom type stubs. Each package's type stub file(s) are expected to be in its own subdirectory. The default value of this setting is "./typings". (typingsPath is now deprecated) **venvPath** [path, optional]: Path to a directory containing one or more subdirectories, each of which contains a virtual environment. Each execution environment (see below for details) can refer to a different virtual environment. When used in conjunction with a **venv** setting (see below), pyright will search for imports in the virtual environment’s site-packages directory rather than the paths specified in PYTHONPATH. @@ -165,7 +165,7 @@ The following is an example of a pyright config file: "src/oldstuff" ], - "typingsPath": "src/typestubs", + "stubPath": "src/typestubs", "venvPath": "/home/foo/.venvs", "reportTypeshedErrors": false, diff --git a/docs/settings.md b/docs/settings.md index c33d88ec5..6f9240c9e 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -6,8 +6,6 @@ The Pyright VS Code extension honors the following settings. **pyright.disableOrganizeImports** [boolean]: Disables the “Organize Imports” command. This is useful if you are using another extension that provides similar functionality and you don’t want the two extensions to fight each other. -**pyright.openFilesOnly** [boolean]: Determines whether pyright analyzes (and reports errors for) all files in the workspace, as indicated by the config file. If this option is set to true, pyright analyzes only open files. - **pyright.typeCheckingMode** ["off", "basic", "strict"]: Determines the default type-checking level used by pyright. This can be overridden in the configuration file. **pyright.useLibraryCodeForTypes** [boolean]: Determines whether pyright reads, parses and analyzes library code to extract type information in the absence of type stub files. This can add significant overhead and may result in poor-quality type information. The default value for this option is false. @@ -22,4 +20,7 @@ The Pyright VS Code extension honors the following settings. **python.venvPath** [path]: Path to folder with subdirectories that contain virtual environments. +**python.analysis.stubPath** [path]: Path to directory containing custom type stub files. + +**python.analysis.diagnosticMode** ["openFilesOnly", "workspace"]: Determines whether pyright analyzes (and reports errors for) all files in the workspace, as indicated by the config file. If this option is set to "openFilesOnly", pyright analyzes only open files. diff --git a/docs/type-stubs.md b/docs/type-stubs.md index b3c5b733f..47db294ee 100644 --- a/docs/type-stubs.md +++ b/docs/type-stubs.md @@ -19,7 +19,7 @@ If you’re serious about static type checking for your Python source base, it ## Generating Type Stubs If you use only a few classes, methods or functions within a library, writing a type stub file by hand is feasible. For large libraries, this can become tedious and error-prone. Pyright can generate “draft” versions of type stub files for you. -To generate a type stub file from within VS Code, enable the reportMissingTypeStubs” setting in your pyrightconfig.json file or by adding a comment `# pyright: reportMissingTypeStubs=true` to individual source files. Make sure you have the target library installed in the python environment that pyright is configured to use for import resolution. Optionally specify a “typingsPath” in your pyrightconfig.json file. This is where pyright will generate your type stub files. By default, the typingsPath is set to "./typings". +To generate a type stub file from within VS Code, enable the reportMissingTypeStubs” setting in your pyrightconfig.json file or by adding a comment `# pyright: reportMissingTypeStubs=true` to individual source files. Make sure you have the target library installed in the python environment that pyright is configured to use for import resolution. Optionally specify a “stubPath” in your pyrightconfig.json file. This is where pyright will generate your type stub files. By default, the stubPath is set to "./typings". ### Generating Type Stubs in VS Code If “reportMissingTypeStubs” is enabled, pyright will highlight any imports that have no type stub. Hover over the error message, and you will see a “Quick Fix” link. Clicking on this link will reveal a popup menu item titled “Create Type Stub For XXX”. The example below shows a missing typestub for the `django` library. diff --git a/server/src/analyzer/backgroundAnalysisProgram.ts b/server/src/analyzer/backgroundAnalysisProgram.ts index 5c8e2d4fe..cf7a133bf 100644 --- a/server/src/analyzer/backgroundAnalysisProgram.ts +++ b/server/src/analyzer/backgroundAnalysisProgram.ts @@ -140,15 +140,15 @@ export class BackgroundAnalysisProgram { async writeTypeStub( targetImportPath: string, targetIsSingleFile: boolean, - typingsPath: string, + stubPath: string, token: CancellationToken ): Promise { if (this._backgroundAnalysis) { - return this._backgroundAnalysis.writeTypeStub(targetImportPath, targetIsSingleFile, typingsPath, token); + return this._backgroundAnalysis.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token); } analyzeProgram(this._program, undefined, this._configOptions, this._onAnalysisCompletion, this._console, token); - return this._program.writeTypeStub(targetImportPath, targetIsSingleFile, typingsPath, token); + return this._program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token); } invalidateAndForceReanalysis() { diff --git a/server/src/analyzer/checker.ts b/server/src/analyzer/checker.ts index 5c9f54eba..f4c2d5209 100644 --- a/server/src/analyzer/checker.ts +++ b/server/src/analyzer/checker.ts @@ -12,6 +12,7 @@ * cannot (or should not be) performed lazily. */ +import { Commands } from '../commands/commands'; import { DiagnosticLevel } from '../common/configOptions'; import { assert } from '../common/debug'; import { Diagnostic, DiagnosticAddendum } from '../common/diagnostic'; @@ -963,7 +964,8 @@ export class Checker extends ParseTreeWalker { TextRange.extend(textRange, nameParts[nameParts.length - 1]); this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange( `"${multipartName}" is not accessed`, - textRange + textRange, + { action: Commands.unusedImport } ); this._evaluator.addDiagnosticForTextRange( @@ -1033,7 +1035,12 @@ export class Checker extends ParseTreeWalker { } if (nameNode && rule !== undefined && message) { - this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange(`"${nameNode.value}" is not accessed`, nameNode); + const action = rule === DiagnosticRule.reportUnusedImport ? { action: Commands.unusedImport } : undefined; + this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange( + `"${nameNode.value}" is not accessed`, + nameNode, + action + ); this._evaluator.addDiagnostic(diagnosticLevel, rule, message, nameNode); } } diff --git a/server/src/analyzer/importResolver.ts b/server/src/analyzer/importResolver.ts index 7c3f62f19..e90a63a6b 100644 --- a/server/src/analyzer/importResolver.ts +++ b/server/src/analyzer/importResolver.ts @@ -193,11 +193,11 @@ export class ImportResolver { } if (allowPyi) { - // Check for a typings file. - if (this._configOptions.typingsPath) { - importFailureInfo.push(`Looking in typingsPath '${this._configOptions.typingsPath}'`); + // Check for a stub file. + if (this._configOptions.stubPath) { + importFailureInfo.push(`Looking in stubPath '${this._configOptions.stubPath}'`); const typingsImport = this.resolveAbsoluteImport( - this._configOptions.typingsPath, + this._configOptions.stubPath, moduleDescriptor, importName, importFailureInfo @@ -327,9 +327,9 @@ export class ImportResolver { } // Check for a typings file. - if (this._configOptions.typingsPath) { + if (this._configOptions.stubPath) { this._getCompletionSuggestionsAbsolute( - this._configOptions.typingsPath, + this._configOptions.stubPath, moduleDescriptor, suggestions, similarityLimit @@ -383,8 +383,8 @@ export class ImportResolver { } // Check for a typings file. - if (this._configOptions.typingsPath) { - const candidateModuleName = this._getModuleNameFromPath(this._configOptions.typingsPath, filePath); + if (this._configOptions.stubPath) { + const candidateModuleName = this._getModuleNameFromPath(this._configOptions.stubPath, filePath); // Does this candidate look better than the previous best module name? // We'll always try to use the shortest version. diff --git a/server/src/analyzer/importStatementUtils.ts b/server/src/analyzer/importStatementUtils.ts index b363f028a..5260d732e 100644 --- a/server/src/analyzer/importStatementUtils.ts +++ b/server/src/analyzer/importStatementUtils.ts @@ -8,6 +8,9 @@ * import statements in a python source file. */ +import { CancellationToken } from 'vscode-languageserver'; + +import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { TextEditAction } from '../common/editAction'; import { convertOffsetToPosition } from '../common/positionUtils'; import { Position } from '../common/textRange'; @@ -378,8 +381,10 @@ function _formatModuleName(node: ModuleNameNode): string { return moduleName; } -export function getContainingImportStatement(node: ParseNode | undefined) { +export function getContainingImportStatement(node: ParseNode | undefined, token: CancellationToken) { while (node) { + throwIfCancellationRequested(token); + if (node.nodeType === ParseNodeType.Import || node.nodeType === ParseNodeType.ImportFrom) { break; } diff --git a/server/src/analyzer/program.ts b/server/src/analyzer/program.ts index dfd889a4d..602a1a041 100644 --- a/server/src/analyzer/program.ts +++ b/server/src/analyzer/program.ts @@ -109,16 +109,16 @@ export class Program { private _extension?: LanguageServiceExtension ) { this._console = console || new StandardConsole(); - this._evaluator = createTypeEvaluator(this._lookUpImport, Program._getPrintTypeFlags(initialConfigOptions)); this._importResolver = initialImportResolver; this._configOptions = initialConfigOptions; + this._createNewEvaluator(); } setConfigOptions(configOptions: ConfigOptions) { this._configOptions = configOptions; // Create a new evaluator with the updated config options. - this._evaluator = createTypeEvaluator(this._lookUpImport, Program._getPrintTypeFlags(this._configOptions)); + this._createNewEvaluator(); } setImportResolver(importResolver: ImportResolver) { @@ -432,12 +432,7 @@ export class Program { } } - writeTypeStub( - targetImportPath: string, - targetIsSingleFile: boolean, - typingsPath: string, - token: CancellationToken - ) { + writeTypeStub(targetImportPath: string, targetIsSingleFile: boolean, stubPath: string, token: CancellationToken) { for (const sourceFileInfo of this._sourceFileList) { throwIfCancellationRequested(token); @@ -447,7 +442,7 @@ export class Program { // not any files that the target module happened to import. const relativePath = getRelativePath(filePath, targetImportPath); if (relativePath !== undefined) { - let typeStubPath = normalizePath(combinePaths(typingsPath, relativePath)); + let typeStubPath = normalizePath(combinePaths(stubPath, relativePath)); // If the target is a single file implementation, as opposed to // a package in a directory, transform the name of the type stub @@ -461,7 +456,7 @@ export class Program { const typeStubDir = getDirectoryPath(typeStubPath); try { - makeDirectories(this._fs, typeStubDir, typingsPath); + makeDirectories(this._fs, typeStubDir, stubPath); } catch (e) { const errMsg = `Could not create directory for '${typeStubDir}'`; throw new Error(errMsg); @@ -488,6 +483,10 @@ export class Program { flags |= PrintTypeFlags.OmitTypeArgumentsIfAny; } + if (configOptions.diagnosticRuleSet.pep604Printing) { + flags |= PrintTypeFlags.PEP604; + } + return flags; } diff --git a/server/src/analyzer/service.ts b/server/src/analyzer/service.ts index a48dc6974..8ad625c20 100644 --- a/server/src/analyzer/service.ts +++ b/server/src/analyzer/service.ts @@ -458,9 +458,17 @@ export class AnalyzerService { configOptions.checkOnlyOpenFiles = !!commandLineOptions.checkOnlyOpenFiles; configOptions.useLibraryCodeForTypes = !!commandLineOptions.useLibraryCodeForTypes; - // If there was no typings path specified, use a default path. - if (configOptions.typingsPath === undefined) { - configOptions.typingsPath = normalizePath(combinePaths(configOptions.projectRoot, 'typings')); + // If there was no stub path specified, use a default path. + if (commandLineOptions.stubPath) { + if (!configOptions.stubPath) { + configOptions.stubPath = commandLineOptions.stubPath; + } else { + reportDuplicateSetting('stubPath'); + } + } else { + if (!configOptions.stubPath) { + configOptions.stubPath = normalizePath(combinePaths(configOptions.projectRoot, 'typings')); + } } // Do some sanity checks on the specified settings and report missing @@ -544,9 +552,9 @@ export class AnalyzerService { } } - if (configOptions.typingsPath) { - if (!this._fs.existsSync(configOptions.typingsPath) || !isDirectory(this._fs, configOptions.typingsPath)) { - this._console.log(`typingsPath ${configOptions.typingsPath} is not a valid directory.`); + if (configOptions.stubPath) { + if (!this._fs.existsSync(configOptions.stubPath) || !isDirectory(this._fs, configOptions.stubPath)) { + this._console.log(`stubPath ${configOptions.stubPath} is not a valid directory.`); } } @@ -604,13 +612,13 @@ export class AnalyzerService { } private _getTypeStubFolder() { - const typingsPath = this._configOptions.typingsPath; + const stubPath = this._configOptions.stubPath; if (!this._typeStubTargetPath || !this._typeStubTargetImportName) { const errMsg = `Import '${this._typeStubTargetImportName}'` + ` could not be resolved`; this._console.error(errMsg); throw new Error(errMsg); } - if (!typingsPath) { + if (!stubPath) { // We should never get here because we always generate a // default typings path if none was specified. const errMsg = 'No typings path was specified'; @@ -627,16 +635,16 @@ export class AnalyzerService { } try { // Generate a new typings directory if necessary. - if (!this._fs.existsSync(typingsPath)) { - this._fs.mkdirSync(typingsPath); + if (!this._fs.existsSync(stubPath)) { + this._fs.mkdirSync(stubPath); } } catch (e) { - const errMsg = `Could not create typings directory '${typingsPath}'`; + const errMsg = `Could not create typings directory '${stubPath}'`; this._console.error(errMsg); throw new Error(errMsg); } // Generate a typings subdirectory. - const typingsSubdirPath = combinePaths(typingsPath, typeStubInputTargetParts[0]); + const typingsSubdirPath = combinePaths(stubPath, typeStubInputTargetParts[0]); try { // Generate a new typings subdirectory if necessary. if (!this._fs.existsSync(typingsSubdirPath)) { diff --git a/server/src/analyzer/typeEvaluator.ts b/server/src/analyzer/typeEvaluator.ts index 47fe640b8..68ba23d32 100644 --- a/server/src/analyzer/typeEvaluator.ts +++ b/server/src/analyzer/typeEvaluator.ts @@ -299,6 +299,9 @@ export const enum PrintTypeFlags { // Omit type arguments for generic classes if they are "Any". OmitTypeArgumentsIfAny = 1 << 1, + + // Print Union and Optional in PEP 604 format. + PEP604 = 1 << 2, } interface ParamAssignmentInfo { @@ -12229,8 +12232,16 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags: }); const returnType = getFunctionEffectiveReturnType(type); - const returnTypeString = - recursionCount < maxTypeRecursionCount ? printType(returnType, recursionCount + 1) : ''; + let returnTypeString = recursionCount < maxTypeRecursionCount ? printType(returnType, recursionCount + 1) : ''; + + if ( + printTypeFlags & PrintTypeFlags.PEP604 && + returnType.category === TypeCategory.Union && + recursionCount > 0 + ) { + returnTypeString = `(${returnTypeString})`; + } + return [paramTypeStrings, returnTypeString]; } @@ -12293,6 +12304,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags: if (subtypes.find((t) => t.category === TypeCategory.None) !== undefined) { const optionalType = printType(removeNoneFromUnion(unionType), recursionCount + 1); + + if (printTypeFlags & PrintTypeFlags.PEP604) { + return optionalType + ' | None'; + } + return 'Optional[' + optionalType + ']'; } @@ -12327,6 +12343,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags: return subtypeStrings[0]; } + if (printTypeFlags & PrintTypeFlags.PEP604) { + return subtypeStrings.join(' | '); + } + return `Union[${subtypeStrings.join(', ')}]`; } diff --git a/server/src/analyzer/typeStubWriter.ts b/server/src/analyzer/typeStubWriter.ts index 11d66cebe..ae3e8bbca 100644 --- a/server/src/analyzer/typeStubWriter.ts +++ b/server/src/analyzer/typeStubWriter.ts @@ -127,13 +127,13 @@ export class TypeStubWriter extends ParseTreeWalker { private _trackedImportFrom = new Map(); private _accessedImportedSymbols = new Map(); - constructor(private _typingsPath: string, private _sourceFile: SourceFile, private _evaluator: TypeEvaluator) { + constructor(private _stubPath: string, private _sourceFile: SourceFile, private _evaluator: TypeEvaluator) { super(); // As a heuristic, we'll include all of the import statements // in "__init__.pyi" files even if they're not locally referenced // because these are often used as ways to re-export symbols. - if (this._typingsPath.endsWith('__init__.pyi')) { + if (this._stubPath.endsWith('__init__.pyi')) { this._includeAllImports = true; } } @@ -662,6 +662,6 @@ export class TypeStubWriter extends ParseTreeWalker { finalText += this._printTrackedImports(); finalText += this._typeStubText; - this._sourceFile.fileSystem.writeFileSync(this._typingsPath, finalText, 'utf8'); + this._sourceFile.fileSystem.writeFileSync(this._stubPath, finalText, 'utf8'); } } diff --git a/server/src/backgroundAnalysisBase.ts b/server/src/backgroundAnalysisBase.ts index b2ee45737..b9b8afae3 100644 --- a/server/src/backgroundAnalysisBase.ts +++ b/server/src/backgroundAnalysisBase.ts @@ -152,7 +152,7 @@ export class BackgroundAnalysisBase { async writeTypeStub( targetImportPath: string, targetIsSingleFile: boolean, - typingsPath: string, + stubPath: string, token: CancellationToken ): Promise { throwIfCancellationRequested(token); @@ -163,7 +163,7 @@ export class BackgroundAnalysisBase { const cancellationId = getCancellationTokenId(token); this._enqueueRequest({ requestType: 'writeTypeStub', - data: { targetImportPath, targetIsSingleFile, typingsPath, cancellationId }, + data: { targetImportPath, targetIsSingleFile, stubPath, cancellationId }, port: port2, }); @@ -253,7 +253,7 @@ export class BackgroundAnalysisRunnerBase { case 'writeTypeStub': { run(() => { - const { targetImportPath, targetIsSingleFile, typingsPath, cancellationId } = msg.data; + const { targetImportPath, targetIsSingleFile, stubPath, cancellationId } = msg.data; const token = getCancellationTokenFromId(cancellationId); analyzeProgram( @@ -264,7 +264,7 @@ export class BackgroundAnalysisRunnerBase { this._getConsole(), token ); - this._program.writeTypeStub(targetImportPath, targetIsSingleFile, typingsPath, token); + this._program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token); }, msg.port!); break; } @@ -392,7 +392,7 @@ function createConfigOptionsFrom(jsonObject: any): ConfigOptions { configOptions.pythonPath = jsonObject.pythonPath; configOptions.typeshedPath = jsonObject.typeshedPath; - configOptions.typingsPath = jsonObject.typingsPath; + configOptions.stubPath = jsonObject.stubPath; configOptions.autoExcludeVenv = jsonObject.autoExcludeVenv; configOptions.verboseOutput = jsonObject.verboseOutput; configOptions.checkOnlyOpenFiles = jsonObject.checkOnlyOpenFiles; diff --git a/server/src/commands/commands.ts b/server/src/commands/commands.ts index c7505fa40..2dbc7b51a 100644 --- a/server/src/commands/commands.ts +++ b/server/src/commands/commands.ts @@ -12,4 +12,5 @@ export const enum Commands { restartServer = 'pyright.restartserver', orderImports = 'pyright.organizeimports', addMissingOptionalToParam = 'pyright.addoptionalforparam', + unusedImport = 'pyright.unusedImport', } diff --git a/server/src/commands/createTypeStub.ts b/server/src/commands/createTypeStub.ts index 8010564ca..33e57090e 100644 --- a/server/src/commands/createTypeStub.ts +++ b/server/src/commands/createTypeStub.ts @@ -68,12 +68,6 @@ export class CreateTypeStubCommand implements ServerCommand { // Creates a service instance that's used for creating type // stubs for a specified target library. private async _createTypeStubService(callingFile?: string): Promise { - return this._createAnalyzerService(callingFile); - } - - private async _createAnalyzerService(callingFile: string | undefined) { - this._ls.console.log('Starting type stub service instance'); - if (callingFile) { // this should let us to inherit all execution env of the calling file // if it is invoked from IDE through code action diff --git a/server/src/common/commandLineOptions.ts b/server/src/common/commandLineOptions.ts index 58d0411b9..901ed5bb1 100644 --- a/server/src/common/commandLineOptions.ts +++ b/server/src/common/commandLineOptions.ts @@ -40,6 +40,9 @@ export class CommandLineOptions { // Path of typeshed stubs. typeshedPath?: string; + // Path of typing folder + stubPath?: string; + // Absolute execution root (current working directory). executionRoot: string; diff --git a/server/src/common/configOptions.ts b/server/src/common/configOptions.ts index 0d8eebcd7..15d1f4abf 100644 --- a/server/src/common/configOptions.ts +++ b/server/src/common/configOptions.ts @@ -51,6 +51,9 @@ export interface DiagnosticRuleSet { // when printed if all arguments are Unknown or Any? omitTypeArgsIfAny: boolean; + // Should Union and Optional types be printed in PEP 604 format? + pep604Printing: boolean; + // Use strict inference rules for list expressions? strictListInference: boolean; @@ -254,6 +257,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { const diagSettings: DiagnosticRuleSet = { printUnknownAsAny: false, omitTypeArgsIfAny: false, + pep604Printing: true, strictListInference: true, strictDictionaryInference: true, strictParameterNoneValue: true, @@ -305,6 +309,7 @@ export function getNoTypeCheckingDiagnosticRuleSet(): DiagnosticRuleSet { const diagSettings: DiagnosticRuleSet = { printUnknownAsAny: true, omitTypeArgsIfAny: true, + pep604Printing: true, strictListInference: false, strictDictionaryInference: false, strictParameterNoneValue: false, @@ -356,6 +361,7 @@ export function getDefaultDiagnosticRuleSet(): DiagnosticRuleSet { const diagSettings: DiagnosticRuleSet = { printUnknownAsAny: false, omitTypeArgsIfAny: false, + pep604Printing: true, strictListInference: false, strictDictionaryInference: false, strictParameterNoneValue: false, @@ -422,7 +428,7 @@ export class ConfigOptions { typeshedPath?: string; // Path to custom typings (stub) modules. - typingsPath?: string; + stubPath?: string; // A list of file specs to include in the analysis. Can contain // directories, in which case all "*.py" files within those directories @@ -648,6 +654,7 @@ export class ConfigOptions { this.diagnosticRuleSet = { printUnknownAsAny: defaultSettings.printUnknownAsAny, omitTypeArgsIfAny: defaultSettings.omitTypeArgsIfAny, + pep604Printing: defaultSettings.pep604Printing, // Use strict inference rules for list expressions? strictListInference: this._convertBoolean( @@ -1002,13 +1009,24 @@ export class ConfigOptions { } } - // Read the "typingsPath" setting. - this.typingsPath = undefined; + // Read the "stubPath" setting. + this.stubPath = undefined; + + // Keep this for backward compatibility if (configObj.typingsPath !== undefined) { if (typeof configObj.typingsPath !== 'string') { console.log(`Config "typingsPath" field must contain a string.`); } else { - this.typingsPath = normalizePath(combinePaths(this.projectRoot, configObj.typingsPath)); + console.log(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`); + this.stubPath = normalizePath(combinePaths(this.projectRoot, configObj.typingsPath)); + } + } + + if (configObj.stubPath !== undefined) { + if (typeof configObj.stubPath !== 'string') { + console.log(`Config "stubPath" field must contain a string.`); + } else { + this.stubPath = normalizePath(combinePaths(this.projectRoot, configObj.stubPath)); } } diff --git a/server/src/common/diagnosticSink.ts b/server/src/common/diagnosticSink.ts index e65c2e799..5b800a853 100644 --- a/server/src/common/diagnosticSink.ts +++ b/server/src/common/diagnosticSink.ts @@ -7,7 +7,7 @@ * Class that represents errors and warnings. */ -import { Diagnostic, DiagnosticCategory } from './diagnostic'; +import { Diagnostic, DiagnosticAction, DiagnosticCategory } from './diagnostic'; import { convertOffsetsToRange } from './positionUtils'; import { Range, TextRange } from './textRange'; import { TextRangeCollection } from './textRangeCollection'; @@ -43,8 +43,12 @@ export class DiagnosticSink { return this.addDiagnostic(new Diagnostic(DiagnosticCategory.Warning, message, range)); } - addUnusedCode(message: string, range: Range) { - return this.addDiagnostic(new Diagnostic(DiagnosticCategory.UnusedCode, message, range)); + addUnusedCode(message: string, range: Range, action?: DiagnosticAction) { + const diag = new Diagnostic(DiagnosticCategory.UnusedCode, message, range); + if (action) { + diag.addAction(action); + } + return this.addDiagnostic(diag); } addDiagnostic(diag: Diagnostic) { @@ -91,7 +95,11 @@ export class TextRangeDiagnosticSink extends DiagnosticSink { return this.addWarning(message, convertOffsetsToRange(range.start, range.start + range.length, this._lines)); } - addUnusedCodeWithTextRange(message: string, range: TextRange) { - return this.addUnusedCode(message, convertOffsetsToRange(range.start, range.start + range.length, this._lines)); + addUnusedCodeWithTextRange(message: string, range: TextRange, action?: DiagnosticAction) { + return this.addUnusedCode( + message, + convertOffsetsToRange(range.start, range.start + range.length, this._lines), + action + ); } } diff --git a/server/src/common/pathUtils.ts b/server/src/common/pathUtils.ts index 42dc32b99..222e46bc7 100644 --- a/server/src/common/pathUtils.ts +++ b/server/src/common/pathUtils.ts @@ -511,8 +511,8 @@ export function getFileExtension(fileName: string, multiDotExtension = false) { return path.extname(fileName); } - const lastDividerIndex = fileName.lastIndexOf(path.sep); - const firstDotIndex = fileName.indexOf('.', lastDividerIndex + 1); + fileName = getFileName(fileName); + const firstDotIndex = fileName.indexOf('.'); return fileName.substr(firstDotIndex); } diff --git a/server/src/languageServerBase.ts b/server/src/languageServerBase.ts index ec9a0b8ce..cb1f4f761 100644 --- a/server/src/languageServerBase.ts +++ b/server/src/languageServerBase.ts @@ -75,6 +75,7 @@ export interface ServerSettings { venvPath?: string; pythonPath?: string; typeshedPath?: string; + stubPath?: string; openFilesOnly?: boolean; typeCheckingMode?: string; useLibraryCodeForTypes?: boolean; @@ -216,6 +217,10 @@ export abstract class LanguageServerBase implements LanguageServerInterface { return undefined; } + protected isOpenFilesOnly(diagnosticMode: string): boolean { + return diagnosticMode !== 'workspace'; + } + protected createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver { return new ImportResolver(fs, options); } diff --git a/server/src/languageService/analyzerServiceExecutor.ts b/server/src/languageService/analyzerServiceExecutor.ts index 04713ef39..ffe268f5e 100644 --- a/server/src/languageService/analyzerServiceExecutor.ts +++ b/server/src/languageService/analyzerServiceExecutor.ts @@ -73,7 +73,15 @@ function _getCommandLineOptions( // Pyright supports only one typeshed path currently, whereas the // official VS Code Python extension supports multiple typeshed paths. // We'll use the first one specified and ignore the rest. - commandLineOptions.typeshedPath = _expandPathVariables(languageServiceRootPath, serverSettings.typeshedPath); + commandLineOptions.typeshedPath = normalizePath( + _expandPathVariables(languageServiceRootPath, serverSettings.typeshedPath) + ); + } + + if (serverSettings.stubPath) { + commandLineOptions.stubPath = normalizePath( + _expandPathVariables(languageServiceRootPath, serverSettings.stubPath) + ); } if (typeStubTargetImportName) { diff --git a/server/src/server.ts b/server/src/server.ts index 8c9ec7482..0208b46cf 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -12,7 +12,7 @@ import { BackgroundAnalysis } from './backgroundAnalysis'; import { BackgroundAnalysisBase } from './backgroundAnalysisBase'; import { CommandController } from './commands/commandController'; import { getCancellationFolderName } from './common/cancellationUtils'; -import { isDebugMode } from './common/core'; +import { isDebugMode, isString } from './common/core'; import { convertUriToPath, getDirectoryPath, normalizeSlashes } from './common/pathUtils'; import { LanguageServerBase, ServerSettings, WorkspaceServiceInstance } from './languageServerBase'; import { CodeActionProvider } from './languageService/codeActionProvider'; @@ -45,6 +45,16 @@ class PyrightServer extends LanguageServerBase { if (typeshedPaths && isArray(typeshedPaths) && typeshedPaths.length > 0) { serverSettings.typeshedPath = normalizeSlashes(typeshedPaths[0]); } + + const stubPath = pythonAnalysisSection.stubPath; + if (stubPath && isString(stubPath)) { + serverSettings.stubPath = normalizeSlashes(stubPath); + } + + if (pythonAnalysisSection.diagnosticMode !== undefined) { + serverSettings.openFilesOnly = this.isOpenFilesOnly(pythonAnalysisSection.diagnosticMode); + } + serverSettings.autoSearchPaths = !!pythonAnalysisSection.autoSearchPaths; const extraPaths = pythonAnalysisSection.extraPaths; @@ -57,13 +67,17 @@ class PyrightServer extends LanguageServerBase { const pyrightSection = await this.getConfiguration(workspace, 'pyright'); if (pyrightSection) { - serverSettings.openFilesOnly = !!pyrightSection.openFilesOnly; + if (pyrightSection.openFilesOnly !== undefined) { + serverSettings.openFilesOnly = !!pyrightSection.openFilesOnly; + } + serverSettings.useLibraryCodeForTypes = !!pyrightSection.useLibraryCodeForTypes; serverSettings.disableLanguageServices = !!pyrightSection.disableLanguageServices; serverSettings.disableOrganizeImports = !!pyrightSection.disableOrganizeImports; serverSettings.typeCheckingMode = pyrightSection.typeCheckingMode; } else { - serverSettings.openFilesOnly = true; + // Unless openFilesOnly is set explicitly, set it to true by default. + serverSettings.openFilesOnly = serverSettings.openFilesOnly ?? true; serverSettings.useLibraryCodeForTypes = false; serverSettings.disableLanguageServices = false; serverSettings.disableOrganizeImports = false; diff --git a/server/src/tests/fourslash/fourslash.ts b/server/src/tests/fourslash/fourslash.ts index 284c3b450..73a5a963b 100644 --- a/server/src/tests/fourslash/fourslash.ts +++ b/server/src/tests/fourslash/fourslash.ts @@ -117,9 +117,12 @@ declare namespace _ { moveCaretRight(count: number): void; verifyDiagnostics(map?: { [marker: string]: { category: string; message: string } }): void; - verifyCodeActions(map: { - [marker: string]: { codeActions: { title: string; kind: string; command: Command }[] }; - }): void; + verifyCodeActions( + map: { + [marker: string]: { codeActions: { title: string; kind: string; command: Command }[] }; + }, + verifyCodeActionCount?: boolean + ): void; verifyCommand(command: Command, files: { [filePath: string]: string }): void; verifyInvokeCodeAction( map: { diff --git a/server/src/tests/fourslash/signature.complicated.fourslash.ts b/server/src/tests/fourslash/signature.complicated.fourslash.ts index e7e97e895..2641d46f9 100644 --- a/server/src/tests/fourslash/signature.complicated.fourslash.ts +++ b/server/src/tests/fourslash/signature.complicated.fourslash.ts @@ -31,8 +31,8 @@ const xInitSignatures = [ const xComplicatedSignatures = [ { - label: '(a: int, b: int, c: int = 1234, d: Optional[str] = None, **kwargs) -> Union[int, str]', - parameters: ['a: int', 'b: int', 'c: int = 1234', 'd: Optional[str] = None', '**kwargs'], + label: '(a: int, b: int, c: int = 1234, d: str | None = None, **kwargs) -> int | str', + parameters: ['a: int', 'b: int', 'c: int = 1234', 'd: str | None = None', '**kwargs'], }, ]; diff --git a/server/src/tests/harness/fourslash/testState.ts b/server/src/tests/harness/fourslash/testState.ts index 1e6e31a7e..2d4af98ea 100644 --- a/server/src/tests/harness/fourslash/testState.ts +++ b/server/src/tests/harness/fourslash/testState.ts @@ -575,23 +575,37 @@ export class TestState { } } - async verifyCodeActions(map: { - [marker: string]: { codeActions: { title: string; kind: string; command: Command }[] }; - }): Promise { + async verifyCodeActions( + map: { + [marker: string]: { codeActions: { title: string; kind: string; command: Command }[] }; + }, + verifyCodeActionCount?: boolean + ): Promise { this._analyze(); for (const range of this.getRanges()) { const name = this.getMarkerName(range.marker!); - for (const expected of map[name].codeActions) { - const actual = await this._getCodeActions(range); + if (!map[name]) { + continue; + } + const codeActions = await this._getCodeActions(range); + if (verifyCodeActionCount) { + if (codeActions.length !== map[name].codeActions.length) { + this._raiseError( + `doesn't contain expected result: ${stringify(map[name])}, actual: ${stringify(codeActions)}` + ); + } + } + + for (const expected of map[name].codeActions) { const expectedCommand = { title: expected.command.title, command: expected.command.command, arguments: convertToString(expected.command.arguments), }; - const matches = actual.filter((a) => { + const matches = codeActions.filter((a) => { const actualCommand = a.command ? { title: a.command.title, @@ -609,7 +623,7 @@ export class TestState { if (matches.length !== 1) { this._raiseError( - `doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(actual)}` + `doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(codeActions)}` ); } } @@ -1035,8 +1049,8 @@ export class TestState { configOptions.defaultVenv = vfs.MODULE_PATH; // make sure we set typing path - if (configOptions.typingsPath === undefined) { - configOptions.typingsPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings')); + if (configOptions.stubPath === undefined) { + configOptions.stubPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings')); } return configOptions; diff --git a/server/src/tests/testState.test.ts b/server/src/tests/testState.test.ts index 9aab9309d..a660709c4 100644 --- a/server/src/tests/testState.test.ts +++ b/server/src/tests/testState.test.ts @@ -120,7 +120,32 @@ test('Configuration', () => { assert.equal(state.configOptions.diagnosticRuleSet.reportMissingImports, 'error'); assert.equal(state.configOptions.diagnosticRuleSet.reportMissingModuleSource, 'warning'); - assert.equal(state.configOptions.typingsPath, normalizeSlashes('/src/typestubs')); + assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs')); +}); + +test('stubPath configuration', () => { + const code = ` +// @filename: mspythonconfig.json +//// { +//// "stubPath": "src/typestubs" +//// } + `; + + const state = parseAndGetTestState(code).state; + assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs')); +}); + +test('Duplicated stubPath configuration', () => { + const code = ` +// @filename: mspythonconfig.json +//// { +//// "typingsPath": "src/typestubs1", +//// "stubPath": "src/typestubs2" +//// } + `; + + const state = parseAndGetTestState(code).state; + assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs2')); }); test('ProjectRoot', () => {