From 0a0cd61f858d49f76eed01676df216fb1792c8ff Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 22 May 2020 14:02:54 -0700 Subject: [PATCH] Use docstrings from source code, fix references showing in libraries, add information level, client override support (#685) --- client/package.json | 6 + docs/configuration.md | 2 +- docs/settings.md | 2 + server/package-lock.json | 3 +- .../src/analyzer/backgroundAnalysisProgram.ts | 8 +- server/src/analyzer/binder.ts | 42 ++-- server/src/analyzer/commentUtils.ts | 27 ++- server/src/analyzer/importResolver.ts | 37 ++- server/src/analyzer/program.ts | 83 ++++++- server/src/analyzer/service.ts | 52 +++-- server/src/analyzer/sourceFile.ts | 33 ++- server/src/analyzer/sourceMapper.ts | 212 ++++++++++++++++++ server/src/analyzer/typeDocStringUtils.ts | 103 +++++++++ server/src/analyzer/typeEvaluator.ts | 47 ++-- server/src/common/commandLineOptions.ts | 21 ++ server/src/common/configOptions.ts | 27 ++- server/src/common/diagnostic.ts | 15 ++ server/src/common/diagnosticSink.ts | 23 +- server/src/common/textRange.ts | 22 +- server/src/languageServerBase.ts | 43 +++- .../analyzerServiceExecutor.ts | 1 + .../src/languageService/completionProvider.ts | 20 +- .../src/languageService/definitionProvider.ts | 12 + server/src/languageService/hoverProvider.ts | 66 ++++-- server/src/server.ts | 13 ++ server/src/tests/fourSlashRunner.test.ts | 2 +- ...ons.autoimport.Lib.Found.Type.fourslash.ts | 4 +- ...oimport.Lib.Found.duplication.fourslash.ts | 4 +- ...tions.autoimport.Lib.NotFound.fourslash.ts | 4 +- .../completions.autoimport.fourslash.ts | 4 +- .../completions.excluded.fourslash.ts | 4 +- .../tests/fourslash/completions.fourslash.ts | 4 +- .../completions.included.fourslash.ts | 4 +- ...> completions.libCodeAndStub.fourslash.ts} | 4 +- .../completions.libCodeNoStub.fourslash.ts | 4 +- .../completions.libStub.fourslash.ts | 4 +- .../completions.overloads.fourslash.ts | 31 +++ ...itions.sourceAndStub.function.fourslash.ts | 26 +++ ...ions.sourceAndStub.innerClass.fourslash.ts | 32 +++ ...ourceAndStub.innerClassMethod.fourslash.ts | 33 +++ ...ions.sourceAndStub.outerClass.fourslash.ts | 28 +++ ...ourceAndStub.outerClassMethod.fourslash.ts | 29 +++ ...dDefinitions.sourceOnly.class.fourslash.ts | 29 +++ ...initions.sourceOnly.function1.fourslash.ts | 28 +++ ...initions.sourceOnly.function2.fourslash.ts | 28 +++ ...ns.sourceOnly.relativeImport1.fourslash.ts | 22 ++ ...ns.sourceOnly.relativeImport2.fourslash.ts | 22 ++ .../findDefinitions.stubOnly.fourslash.ts | 23 ++ .../fourslash/findallreferences.fourslash.ts | 6 +- .../findallreferences.openFiles.fourslash.ts | 37 +++ server/src/tests/fourslash/fourslash.ts | 14 +- .../fourslash/hover.docFromSrc.fourslash.ts | 110 +++++++++ ...er.docFromSrc.relativeImport1.fourslash.ts | 29 +++ ...er.docFromSrc.relativeImport2.fourslash.ts | 25 +++ ...p.ts => hover.libCodeAndStub.fourslash.ts} | 0 .../missingTypeStub.codeAction.fourslash.ts | 4 +- .../missingTypeStub.command.fourslash.ts | 4 +- ...singTypeStub.invokeCodeAction.fourslash.ts | 4 +- .../orderImports1.command.fourslash.ts | 4 +- .../orderImports2.command.fourslash.ts | 4 +- .../tests/harness/fourslash/fourSlashTypes.ts | 1 - server/src/tests/harness/fourslash/runner.ts | 37 +-- .../src/tests/harness/fourslash/testState.ts | 66 +++--- server/tsconfig.json | 2 +- 64 files changed, 1416 insertions(+), 224 deletions(-) create mode 100644 server/src/analyzer/sourceMapper.ts create mode 100644 server/src/analyzer/typeDocStringUtils.ts rename server/src/tests/fourslash/{completions.libCodeAndStub.fourslash.skip.ts => completions.libCodeAndStub.fourslash.ts} (95%) create mode 100644 server/src/tests/fourslash/completions.overloads.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceAndStub.function.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClass.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClassMethod.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClass.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClassMethod.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceOnly.class.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceOnly.function1.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceOnly.function2.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport1.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport2.fourslash.ts create mode 100644 server/src/tests/fourslash/findDefinitions.stubOnly.fourslash.ts create mode 100644 server/src/tests/fourslash/findallreferences.openFiles.fourslash.ts create mode 100644 server/src/tests/fourslash/hover.docFromSrc.fourslash.ts create mode 100644 server/src/tests/fourslash/hover.docFromSrc.relativeImport1.fourslash.ts create mode 100644 server/src/tests/fourslash/hover.docFromSrc.relativeImport2.fourslash.ts rename server/src/tests/fourslash/{hover.libCodeAndStub.fourslash.skip.ts => hover.libCodeAndStub.fourslash.ts} (100%) diff --git a/client/package.json b/client/package.json index becb8d9bb..2e3263cd3 100644 --- a/client/package.json +++ b/client/package.json @@ -100,6 +100,12 @@ ], "scope": "resource" }, + "python.analysis.diagnosticSeverityOverrides": { + "type": "object", + "default": {}, + "description": "Allows a user to override the severity levels for individual diagnostics.", + "scope": "resource" + }, "python.pythonPath": { "type": "string", "default": "python", diff --git a/docs/configuration.md b/docs/configuration.md index ffcbef399..114134a0d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -34,7 +34,7 @@ Relative paths specified within the config file are relative to the config file ## Type Check Diagnostics Settings -The following settings control pyright’s diagnostic output (warnings or errors). Unless otherwise specified, each diagnostic setting can specify a boolean value (`false` indicating that no error is generated and `true` indicating that an error is generated). Alternatively, a string value of `"none"`, `"warning"`, or `"error"` can be used to specify the diagnostic level. +The following settings control pyright’s diagnostic output (warnings or errors). Unless otherwise specified, each diagnostic setting can specify a boolean value (`false` indicating that no error is generated and `true` indicating that an error is generated). Alternatively, a string value of `"none"`, `"warning"`, `"information"`, or `"error"` can be used to specify the diagnostic level. **strictListInference** [boolean]: When inferring the type of a list, use strict type assumptions. For example, the expression `[1, 'a', 3.4]` could be inferred to be of type `List[Any]` or `List[Union[int, str, float]]`. If this setting is true, it will use the latter (stricter) type. The default value for this setting is 'false'. diff --git a/docs/settings.md b/docs/settings.md index e10d6b4ba..0dd17c21d 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -22,6 +22,8 @@ The Pyright VS Code extension honors the following settings. **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. +**python.analysis.diagnosticSeverityOverrides** [map]: Allows a user to override the severity levels for individual diagnostic rules. "reportXXX" rules in the type check diagnostics settings in [configuration](https://github.com/microsoft/pyright/blob/master/docs/configuration.md#type-check-diagnostics-settings) are supported. Use the rule name as a key and one of "error," "warning," "information," "true," "false," or "none" as value. + **python.pythonPath** [path]: Path to Python interpreter. **python.venvPath** [path]: Path to folder with subdirectories that contain virtual environments. diff --git a/server/package-lock.json b/server/package-lock.json index a58e6f633..fab3eab01 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9977,7 +9977,8 @@ }, "yargs-parser": { "version": "13.1.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dev": true, "requires": { "camelcase": "^5.0.0", diff --git a/server/src/analyzer/backgroundAnalysisProgram.ts b/server/src/analyzer/backgroundAnalysisProgram.ts index cf7a133bf..c6a0b937d 100644 --- a/server/src/analyzer/backgroundAnalysisProgram.ts +++ b/server/src/analyzer/backgroundAnalysisProgram.ts @@ -131,9 +131,11 @@ export class BackgroundAnalysisProgram { async getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise { if (this._backgroundAnalysis) { + // backgroundAnalysis returns Promise return this._backgroundAnalysis.getDiagnosticsForRange(filePath, range, token); } + // program returns Promise return this._program.getDiagnosticsForRange(filePath, range); } @@ -169,12 +171,6 @@ export class BackgroundAnalysisProgram { this._importResolver.invalidateCache(); } - initializeFromJson(configJsonObj: any, typeCheckingMode: string | undefined) { - this._configOptions.initializeFromJson(configJsonObj, typeCheckingMode, this._console); - this._backgroundAnalysis?.setConfigOptions(this._configOptions); - this._program.setConfigOptions(this._configOptions); - } - restart() { this._backgroundAnalysis?.restart(); } diff --git a/server/src/analyzer/binder.ts b/server/src/analyzer/binder.ts index 8e67cfc87..0271c87ef 100644 --- a/server/src/analyzer/binder.ts +++ b/server/src/analyzer/binder.ts @@ -18,8 +18,8 @@ import { Commands } from '../commands/commands'; import { DiagnosticLevel } from '../common/configOptions'; -import { assert, fail } from '../common/debug'; -import { CreateTypeStubFileAction } from '../common/diagnostic'; +import { assert, assertNever, fail } from '../common/debug'; +import { CreateTypeStubFileAction, Diagnostic } from '../common/diagnostic'; import { DiagnosticRule } from '../common/diagnosticRules'; import { convertOffsetsToRange } from '../common/positionUtils'; import { PythonVersion } from '../common/pythonVersion'; @@ -2516,16 +2516,28 @@ export class Binder extends ParseTreeWalker { } private _addDiagnostic(diagLevel: DiagnosticLevel, rule: string, message: string, textRange: TextRange) { - if (diagLevel === 'error') { - const diagnostic = this._addError(message, textRange); - diagnostic.setRule(rule); - return diagnostic; - } else if (diagLevel === 'warning') { - const diagnostic = this._addWarning(message, textRange); - diagnostic.setRule(rule); - return diagnostic; + let diagnostic: Diagnostic | undefined; + switch (diagLevel) { + case 'error': + diagnostic = this._addError(message, textRange); + break; + case 'warning': + diagnostic = this._addWarning(message, textRange); + break; + case 'information': + diagnostic = this._addInformation(message, textRange); + break; + case 'none': + break; + default: + return assertNever(diagLevel, `${diagLevel} is not expected`); } - return undefined; + + if (diagnostic) { + diagnostic.setRule(rule); + } + + return diagnostic; } private _addUnusedCode(textRange: TextRange) { @@ -2533,11 +2545,15 @@ export class Binder extends ParseTreeWalker { } private _addError(message: string, textRange: TextRange) { - return this._fileInfo.diagnosticSink.addErrorWithTextRange(message, textRange); + return this._fileInfo.diagnosticSink.addDiagnosticWithTextRange('error', message, textRange); } private _addWarning(message: string, textRange: TextRange) { - return this._fileInfo.diagnosticSink.addWarningWithTextRange(message, textRange); + return this._fileInfo.diagnosticSink.addDiagnosticWithTextRange('warning', message, textRange); + } + + private _addInformation(message: string, textRange: TextRange) { + return this._fileInfo.diagnosticSink.addDiagnosticWithTextRange('information', message, textRange); } } diff --git a/server/src/analyzer/commentUtils.ts b/server/src/analyzer/commentUtils.ts index 88cf16f9c..f047c1d1c 100644 --- a/server/src/analyzer/commentUtils.ts +++ b/server/src/analyzer/commentUtils.ts @@ -70,7 +70,11 @@ function _applyStrictRules(ruleSet: DiagnosticRuleSet) { const strictValue: DiagnosticLevel = (strictRuleSet as any)[ruleName]; const prevValue: DiagnosticLevel = (ruleSet as any)[ruleName]; - if (strictValue === 'error' || (strictValue === 'warning' && prevValue !== 'error')) { + if ( + strictValue === 'error' || + (strictValue === 'warning' && prevValue !== 'error') || + (strictValue === 'information' && prevValue !== 'error' && prevValue !== 'warning') + ) { (ruleSet as any)[ruleName] = strictValue; } } @@ -124,15 +128,20 @@ function _parsePyrightOperand(operand: string, ruleSet: DiagnosticRuleSet) { } function _parseDiagLevel(value: string): DiagnosticLevel | undefined { - if (value === 'false' || value === 'none') { - return 'none'; - } else if (value === 'warning') { - return 'warning'; - } else if (value === 'true' || value === 'error') { - return 'error'; + switch (value) { + case 'false': + case 'none': + return 'none'; + case 'true': + case 'error': + return 'error'; + case 'warning': + return 'warning'; + case 'information': + return 'information'; + default: + return undefined; } - - return undefined; } function _parseBoolSetting(value: string): boolean | undefined { diff --git a/server/src/analyzer/importResolver.ts b/server/src/analyzer/importResolver.ts index e90a63a6b..b58fd66fb 100644 --- a/server/src/analyzer/importResolver.ts +++ b/server/src/analyzer/importResolver.ts @@ -11,6 +11,7 @@ import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { FileSystem } from '../common/fileSystem'; import { + changeAnyExtension, combinePaths, ensureTrailingDirectorySeparator, getDirectoryPath, @@ -112,7 +113,7 @@ export class ImportResolver { const bestImport = this._resolveBestAbsoluteImport(sourceFilePath, execEnv, moduleDescriptor, true); if (bestImport) { - if (bestImport.isStubFile && bestImport.importType !== ImportType.BuiltIn) { + if (bestImport.isStubFile) { bestImport.nonStubImportResult = this._resolveBestAbsoluteImport(sourceFilePath, execEnv, moduleDescriptor, false) || notFoundResult; @@ -349,6 +350,40 @@ export class ImportResolver { return suggestions; } + // Returns the implementation file(s) for the given stub file. + getSourceFilesFromStub(stubFilePath: string): string[] { + const sourceFilePaths: string[] = []; + + // When ImportResolver resolves an import to a stub file, a second resolve is done + // ignoring stub files, which gives us an approximation of where the implementation + // for that stub is located. + this._cachedImportResults.forEach((map, env) => { + map.forEach((result, importName) => { + if (result.isStubFile && result.isImportFound && result.nonStubImportResult) { + if (result.resolvedPaths.some((f) => f === stubFilePath)) { + if (result.nonStubImportResult.isImportFound) { + const nonEmptyPaths = result.nonStubImportResult.resolvedPaths.filter((p) => + p.endsWith('.py') + ); + sourceFilePaths.push(...nonEmptyPaths); + } + } + } + }); + }); + + // We haven't seen an import of that stub, attempt to find the source + // in some other ways. + if (sourceFilePaths.length === 0) { + // Simple case where the stub and source files are next to each other. + const sourceFilePath = changeAnyExtension(stubFilePath, '.py'); + if (this.fileSystem.existsSync(sourceFilePath)) { + sourceFilePaths.push(sourceFilePath); + } + } + return sourceFilePaths; + } + // Returns the module name (of the form X.Y.Z) that needs to be imported // from the current context to access the module with the specified file path. // In a sense, it's performing the inverse of resolveImport. diff --git a/server/src/analyzer/program.ts b/server/src/analyzer/program.ts index 602a1a041..c6b3dfea2 100644 --- a/server/src/analyzer/program.ts +++ b/server/src/analyzer/program.ts @@ -34,7 +34,7 @@ import { stripFileExtension, } from '../common/pathUtils'; import { convertPositionToOffset, convertRangeToTextRange } from '../common/positionUtils'; -import { DocumentRange, doRangesOverlap, Position, Range } from '../common/textRange'; +import { DocumentRange, doesRangeContain, doRangesOverlap, Position, Range } from '../common/textRange'; import { Duration, timingStats } from '../common/timing'; import { AutoImporter, @@ -43,6 +43,7 @@ import { ModuleSymbolMap, } from '../languageService/autoImporter'; import { HoverResults } from '../languageService/hoverProvider'; +import { ReferencesResult } from '../languageService/referencesProvider'; import { SignatureHelpResults } from '../languageService/signatureHelpProvider'; import { ImportLookupResult } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; @@ -53,6 +54,7 @@ import { findNodeByOffset } from './parseTreeUtils'; import { Scope } from './scope'; import { getScopeForNode } from './scopeUtils'; import { SourceFile } from './sourceFile'; +import { SourceMapper } from './sourceMapper'; import { createTypeEvaluator, PrintTypeFlags, TypeEvaluator } from './typeEvaluator'; import { TypeStubWriter } from './typeStubWriter'; @@ -884,7 +886,12 @@ export class Program { this._bindFile(sourceFileInfo); - return sourceFileInfo.sourceFile.getDefinitionsForPosition(position, this._evaluator, token); + return sourceFileInfo.sourceFile.getDefinitionsForPosition( + this._createSourceMapper(), + position, + this._evaluator, + token + ); }); } @@ -915,14 +922,55 @@ export class Program { // Do we need to do a global search as well? if (referencesResult.requiresGlobalSearch) { for (const curSourceFileInfo of this._sourceFileList) { - this._bindFile(curSourceFileInfo); + throwIfCancellationRequested(token); - curSourceFileInfo.sourceFile.addReferences( - referencesResult, - includeDeclaration, - this._evaluator, - token - ); + // "Find all references" will only include references from user code + // unless the file is explicitly opened in the editor. + if (curSourceFileInfo.isOpenByClient || this._isUserCode(curSourceFileInfo)) { + this._bindFile(curSourceFileInfo); + + curSourceFileInfo.sourceFile.addReferences( + referencesResult, + includeDeclaration, + this._evaluator, + token + ); + } + } + + // Make sure to include declarations regardless where they are defined + // if includeDeclaration is set. + if (includeDeclaration) { + for (const decl of referencesResult.declarations) { + throwIfCancellationRequested(token); + + if (referencesResult.locations.some((l) => l.path === decl.path)) { + // Already included. + continue; + } + + const declFileInfo = this._sourceFileMap.get(decl.path); + if (!declFileInfo) { + // The file the declaration belongs to doesn't belong to the program. + continue; + } + + const tempResult: ReferencesResult = { + requiresGlobalSearch: referencesResult.requiresGlobalSearch, + nodeAtOffset: referencesResult.nodeAtOffset, + symbolName: referencesResult.symbolName, + declarations: referencesResult.declarations, + locations: [], + }; + + declFileInfo.sourceFile.addReferences(tempResult, includeDeclaration, this._evaluator, token); + for (const loc of tempResult.locations) { + // Include declarations only. And throw away any references + if (loc.path === decl.path && doesRangeContain(decl.range, loc.range)) { + referencesResult.locations.push(loc); + } + } + } } } else { sourceFileInfo.sourceFile.addReferences(referencesResult, includeDeclaration, this._evaluator, token); @@ -968,7 +1016,12 @@ export class Program { this._bindFile(sourceFileInfo); - return sourceFileInfo.sourceFile.getHoverForPosition(position, this._evaluator, token); + return sourceFileInfo.sourceFile.getHoverForPosition( + this._createSourceMapper(), + position, + this._evaluator, + token + ); }); } @@ -1015,6 +1068,7 @@ export class Program { this._importResolver, this._lookUpImport, this._evaluator, + this._createSourceMapper(), () => this._buildModuleSymbolsMap(sourceFileInfo, token), token ); @@ -1057,6 +1111,7 @@ export class Program { this._importResolver, this._lookUpImport, this._evaluator, + this._createSourceMapper(), () => this._buildModuleSymbolsMap(sourceFileInfo, token), completionItem, token @@ -1284,6 +1339,14 @@ export class Program { return false; } + private _createSourceMapper() { + const sourceMapper = new SourceMapper(this._importResolver, this._evaluator, (sourceFilePath: string) => { + this.addTrackedFile(sourceFilePath); + return this.getBoundSourceFile(sourceFilePath); + }); + return sourceMapper; + } + private _isImportAllowed(importer: SourceFileInfo, importResult: ImportResult, isImportStubFile: boolean): boolean { let thirdPartyImportAllowed = this._configOptions.useLibraryCodeForTypes; diff --git a/server/src/analyzer/service.ts b/server/src/analyzer/service.ts index 09a212fe5..ec1912405 100644 --- a/server/src/analyzer/service.ts +++ b/server/src/analyzer/service.ts @@ -58,7 +58,6 @@ export class AnalyzerService { private _instanceName: string; private _importResolverFactory: ImportResolverFactory; private _executionRootPath: string; - private _typeStubTargetImportName: string | undefined; private _typeStubTargetPath: string | undefined; private _typeStubTargetIsSingleFile = false; private _console: ConsoleInterface; @@ -69,10 +68,7 @@ export class AnalyzerService { private _configFileWatcher: FileWatcher | undefined; private _libraryFileWatcher: FileWatcher | undefined; private _onCompletionCallback: AnalysisCompleteCallback | undefined; - private _watchForSourceChanges = false; - private _watchForLibraryChanges = false; - private _typeCheckingMode: string | undefined; - private _verboseOutput = false; + private _commandLineOptions: CommandLineOptions | undefined; private _analyzeTimer: any; private _requireTrackedFileUpdate = true; private _lastUserInteractionTime = Date.now(); @@ -95,7 +91,6 @@ export class AnalyzerService { this._instanceName = instanceName; this._console = console || new StandardConsole(); this._executionRootPath = ''; - this._typeStubTargetImportName = undefined; this._extension = extension; this._importResolverFactory = importResolverFactory || AnalyzerService.createImportResolver; this._maxAnalysisTimeInForeground = maxAnalysisTime; @@ -145,14 +140,10 @@ export class AnalyzerService { } setOptions(commandLineOptions: CommandLineOptions, reanalyze = true): void { - this._watchForSourceChanges = !!commandLineOptions.watchForSourceChanges; - this._watchForLibraryChanges = !!commandLineOptions.watchForLibraryChanges; - this._typeCheckingMode = commandLineOptions.typeCheckingMode; - this._verboseOutput = !!commandLineOptions.verboseOutput; + this._commandLineOptions = commandLineOptions; const configOptions = this._getConfigOptions(commandLineOptions); this._backgroundAnalysisProgram.setConfigOptions(configOptions); - this._typeStubTargetImportName = commandLineOptions.typeStubTargetImportName; this._executionRootPath = normalizePath( combinePaths(commandLineOptions.executionRoot, configOptions.projectRoot) @@ -263,7 +254,7 @@ export class AnalyzerService { this._program.printDependencies(this._executionRootPath, verbose); } - async getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise { + getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise { return this._backgroundAnalysisProgram.getDiagnosticsForRange(filePath, range, token); } @@ -371,7 +362,12 @@ export class AnalyzerService { this._console.log(`Loading configuration file at ${configFilePath}`); const configJsonObj = this._parseConfigFile(configFilePath); if (configJsonObj) { - configOptions.initializeFromJson(configJsonObj, this._typeCheckingMode, this._console); + configOptions.initializeFromJson( + configJsonObj, + this._typeCheckingMode, + this._console, + commandLineOptions.diagnosticSeverityOverrides + ); const configFileDir = getDirectoryPath(configFilePath); @@ -415,6 +411,7 @@ export class AnalyzerService { ); configOptions.autoExcludeVenv = true; + configOptions.applyDiagnosticOverrides(commandLineOptions.diagnosticSeverityOverrides); } const reportDuplicateSetting = (settingName: string) => { @@ -611,6 +608,26 @@ export class AnalyzerService { return this._backgroundAnalysisProgram.configOptions; } + private get _watchForSourceChanges() { + return !!this._commandLineOptions?.watchForSourceChanges; + } + + private get _watchForLibraryChanges() { + return !!this._commandLineOptions?.watchForLibraryChanges; + } + + private get _typeCheckingMode() { + return this._commandLineOptions?.typeCheckingMode; + } + + private get _verboseOutput() { + return !!this._commandLineOptions?.verboseOutput; + } + + private get _typeStubTargetImportName() { + return this._commandLineOptions?.typeStubTargetImportName; + } + private _getTypeStubFolder() { const stubPath = this._configOptions.stubPath; if (!this._typeStubTargetPath || !this._typeStubTargetImportName) { @@ -1045,10 +1062,11 @@ export class AnalyzerService { this._updateConfigFileWatcher(); this._console.log(`Reloading configuration file at ${this._configFilePath}`); - const configJsonObj = this._parseConfigFile(this._configFilePath); - if (configJsonObj) { - this._backgroundAnalysisProgram.initializeFromJson(configJsonObj, this._typeCheckingMode); - } + + // 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!); + this._backgroundAnalysisProgram.setConfigOptions(configOptions); this._applyConfigOptions(); } diff --git a/server/src/analyzer/sourceFile.ts b/server/src/analyzer/sourceFile.ts index c7ce4a333..abc248ee4 100644 --- a/server/src/analyzer/sourceFile.ts +++ b/server/src/analyzer/sourceFile.ts @@ -19,7 +19,7 @@ import { OperationCanceledException } from '../common/cancellationUtils'; import { ConfigOptions, ExecutionEnvironment, getDefaultDiagnosticRuleSet } from '../common/configOptions'; import { ConsoleInterface, StandardConsole } from '../common/console'; import { assert } from '../common/debug'; -import { Diagnostic, DiagnosticCategory } from '../common/diagnostic'; +import { convertLevelToCategory, Diagnostic, DiagnosticCategory } from '../common/diagnostic'; import { DiagnosticSink, TextRangeDiagnosticSink } from '../common/diagnosticSink'; import { TextEditAction } from '../common/editAction'; import { FileSystem } from '../common/fileSystem'; @@ -49,6 +49,7 @@ import { ImportResolver } from './importResolver'; import { ImportResult } from './importResult'; import { ParseTreeCleanerWalker } from './parseTreeCleaner'; import { Scope } from './scope'; +import { SourceMapper } from './sourceMapper'; import { SymbolTable } from './symbol'; import { TestWalker } from './testWalker'; import { TypeEvaluator } from './typeEvaluator'; @@ -228,10 +229,7 @@ export class SourceFile { } if (options.diagnosticRuleSet.reportImportCycles !== 'none' && this._circularDependencies.length > 0) { - const category = - options.diagnosticRuleSet.reportImportCycles === 'warning' - ? DiagnosticCategory.Warning - : DiagnosticCategory.Error; + const category = convertLevelToCategory(options.diagnosticRuleSet.reportImportCycles); this._circularDependencies.forEach((cirDep) => { diagList.push( @@ -261,11 +259,12 @@ export class SourceFile { if (this._isTypeshedStubFile) { if (options.diagnosticRuleSet.reportTypeshedErrors === 'none') { includeWarningsAndErrors = false; - } else if (options.diagnosticRuleSet.reportTypeshedErrors === 'warning') { - // Convert all the errors to warnings. + } else { + // convert category to effective ones + const effectiveCategory = convertLevelToCategory(options.diagnosticRuleSet.reportTypeshedErrors); diagList = diagList.map((diag) => { - if (diag.category === DiagnosticCategory.Error) { - return new Diagnostic(DiagnosticCategory.Warning, diag.message, diag.range); + if (diag.category !== effectiveCategory) { + return new Diagnostic(effectiveCategory, diag.message, diag.range); } return diag; }); @@ -567,6 +566,7 @@ export class SourceFile { } getDefinitionsForPosition( + sourceMapper: SourceMapper, position: Position, evaluator: TypeEvaluator, token: CancellationToken @@ -576,7 +576,13 @@ export class SourceFile { return undefined; } - return DefinitionProvider.getDefinitionsForPosition(this._parseResults, position, evaluator, token); + return DefinitionProvider.getDefinitionsForPosition( + sourceMapper, + this._parseResults, + position, + evaluator, + token + ); } getDeclarationForPosition( @@ -654,6 +660,7 @@ export class SourceFile { } getHoverForPosition( + sourceMapper: SourceMapper, position: Position, evaluator: TypeEvaluator, token: CancellationToken @@ -663,7 +670,7 @@ export class SourceFile { return undefined; } - return HoverProvider.getHoverForPosition(this._parseResults, position, evaluator, token); + return HoverProvider.getHoverForPosition(sourceMapper, this._parseResults, position, evaluator, token); } getSignatureHelpForPosition( @@ -687,6 +694,7 @@ export class SourceFile { importResolver: ImportResolver, importLookup: ImportLookup, evaluator: TypeEvaluator, + sourceMapper: SourceMapper, moduleSymbolsCallback: () => ModuleSymbolMap, token: CancellationToken ): CompletionList | undefined { @@ -711,6 +719,7 @@ export class SourceFile { configOptions, importLookup, evaluator, + sourceMapper, moduleSymbolsCallback, token ); @@ -723,6 +732,7 @@ export class SourceFile { importResolver: ImportResolver, importLookup: ImportLookup, evaluator: TypeEvaluator, + sourceMapper: SourceMapper, moduleSymbolsCallback: () => ModuleSymbolMap, completionItem: CompletionItem, token: CancellationToken @@ -742,6 +752,7 @@ export class SourceFile { configOptions, importLookup, evaluator, + sourceMapper, moduleSymbolsCallback, token ); diff --git a/server/src/analyzer/sourceMapper.ts b/server/src/analyzer/sourceMapper.ts new file mode 100644 index 000000000..2c5e829c5 --- /dev/null +++ b/server/src/analyzer/sourceMapper.ts @@ -0,0 +1,212 @@ +/* + * sourceMapper.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Logic that maps a (.pyi) stub to its (.py) implementation source file. + */ + +import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; +import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; +import { getAnyExtensionFromPath } from '../common/pathUtils'; +import { ClassNode, ModuleNode, ParseNode } from '../parser/parseNodes'; +import { ClassDeclaration, Declaration, DeclarationType, FunctionDeclaration } from './declaration'; +import { ImportResolver } from './importResolver'; +import { SourceFile } from './sourceFile'; +import { TypeEvaluator } from './typeEvaluator'; + +// Given a Python file path, creates a SourceFile and binds it. +// Abstracted as a callback to avoid a dependency on Program. +export type SourceMapperFileBinder = (sourceFilePath: string) => SourceFile | undefined; + +export class SourceMapper { + constructor( + private _importResolver: ImportResolver, + private _evaluator: TypeEvaluator, + private _fileBinder: SourceMapperFileBinder + ) {} + + public findModules(stubFilePath: string): ModuleNode[] { + const sourceFiles = this._getBoundSourceFiles(stubFilePath); + return sourceFiles.map((sf) => sf.getParseResults()?.parseTree).filter(_isDefined); + } + + public findDeclaration(stubDecl: Declaration): Declaration | undefined { + if (stubDecl.type === DeclarationType.Class) { + const decls = this.findClassDeclarations(stubDecl); + if (decls.length > 0) { + return decls[0]; + } else { + return undefined; + } + } else if (stubDecl.type === DeclarationType.Function) { + const decls = this.findFunctionDeclarations(stubDecl); + if (decls.length > 0) { + return decls[0]; + } else { + return undefined; + } + } + + return undefined; + } + + public findClassDeclarations(stubDecl: ClassDeclaration): ClassDeclaration[] { + const className = this._getFullClassName(stubDecl.node); + + const sourceFiles = this._getBoundSourceFiles(stubDecl.path); + return sourceFiles.flatMap((sourceFile) => this._findClassDeclarations(sourceFile, className)); + } + + public findFunctionDeclarations(stubDecl: FunctionDeclaration): FunctionDeclaration[] { + const functionName = stubDecl.node.name.value; + const sourceFiles = this._getBoundSourceFiles(stubDecl.path); + + if (stubDecl.isMethod) { + const classNode = ParseTreeUtils.getEnclosingClass(stubDecl.node); + if (classNode === undefined) { + return []; + } + + const className = this._getFullClassName(classNode); + + return sourceFiles.flatMap((sourceFile) => + this._findMethodDeclarations(sourceFile, className, functionName) + ); + } else { + return sourceFiles.flatMap((sourceFile) => this._findFunctionDeclarations(sourceFile, functionName)); + } + } + + private _findMethodDeclarations( + sourceFile: SourceFile, + className: string, + functionName: string + ): FunctionDeclaration[] { + const result: FunctionDeclaration[] = []; + + const classDecls = this._findClassDeclarations(sourceFile, className); + + for (const classDecl of classDecls) { + const methodDecls = this._lookUpSymbolDeclarations(classDecl.node, functionName); + for (const methodDecl of methodDecls) { + if (methodDecl.type === DeclarationType.Function && methodDecl.isMethod) { + result.push(methodDecl); + } + } + } + + return result; + } + + private _findFunctionDeclarations(sourceFile: SourceFile, functionName: string): FunctionDeclaration[] { + const result: FunctionDeclaration[] = []; + + const functionDecls = this._lookUpSymbolDeclarations(sourceFile.getParseResults()?.parseTree, functionName); + + for (const functionDecl of functionDecls) { + if (functionDecl.type === DeclarationType.Function) { + result.push(functionDecl); + } else if (functionDecl.type === DeclarationType.Alias) { + const resolvedDecl = this._evaluator.resolveAliasDeclaration( + functionDecl, + /* resolveLocalNames */ true + ); + if (resolvedDecl) { + if (resolvedDecl.type === DeclarationType.Function) { + if (isStubFile(resolvedDecl.path)) { + result.push(...this.findFunctionDeclarations(resolvedDecl)); + } else { + result.push(resolvedDecl); + } + } + } + } + } + + return result; + } + + private _findClassDeclarations(sourceFile: SourceFile, fullClassName: string): ClassDeclaration[] { + let result: ClassDeclaration[] = []; + + // fullClassName is period delimited, for example: 'OuterClass.InnerClass' + const parentNode = sourceFile.getParseResults()?.parseTree; + if (parentNode) { + let classNameParts = fullClassName.split('.'); + if (classNameParts.length > 0) { + result = this._findClassDeclarationsUnderNode(sourceFile, classNameParts[0], parentNode); + classNameParts = classNameParts.slice(1); + } + + for (const classNamePart of classNameParts) { + result = this._findClassDeclarationsUnderClass(sourceFile, classNamePart, result); + } + } + + return result; + } + + private _findClassDeclarationsUnderClass( + sourceFile: SourceFile, + className: string, + parentClassDecls: ClassDeclaration[] + ): ClassDeclaration[] { + return parentClassDecls.flatMap((parentDecl) => + this._findClassDeclarationsUnderNode(sourceFile, className, parentDecl.node) + ); + } + + private _findClassDeclarationsUnderNode( + sourceFile: SourceFile, + className: string, + parentNode: ParseNode + ): ClassDeclaration[] { + const result: ClassDeclaration[] = []; + + for (const decl of this._lookUpSymbolDeclarations(parentNode, className)) { + if (decl.type === DeclarationType.Class) { + result.push(decl); + } + } + + return result; + } + + private _lookUpSymbolDeclarations(node: ParseNode | undefined, symbolName: string): Declaration[] { + if (node === undefined) { + return []; + } + + const moduleScope = AnalyzerNodeInfo.getScope(node); + const symbol = moduleScope?.lookUpSymbol(symbolName); + const decls = symbol?.getDeclarations(); + + return decls ?? []; + } + + private _getFullClassName(node: ClassNode) { + const fullName: string[] = []; + + let current: ClassNode | undefined = node; + while (current !== undefined) { + fullName.push(current.name.value); + current = ParseTreeUtils.getEnclosingClass(current); + } + + return fullName.reverse().join('.'); + } + + private _getBoundSourceFiles(stubFilePath: string): SourceFile[] { + const paths = this._importResolver.getSourceFilesFromStub(stubFilePath); + return paths.map((fp) => this._fileBinder(fp)).filter(_isDefined); + } +} + +export function isStubFile(filePath: string): boolean { + return getAnyExtensionFromPath(filePath, ['.pyi'], /* ignoreCase */ false) === '.pyi'; +} + +function _isDefined(element: T | undefined): element is T { + return element !== undefined; +} diff --git a/server/src/analyzer/typeDocStringUtils.ts b/server/src/analyzer/typeDocStringUtils.ts new file mode 100644 index 000000000..27b2cfde0 --- /dev/null +++ b/server/src/analyzer/typeDocStringUtils.ts @@ -0,0 +1,103 @@ +/* + * typeDocStringUtils.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Logic that obtains the doc string for types by looking + * at the declaration in the type stub, and if needed, in + * the source file. + */ + +import { ClassDeclaration, DeclarationBase, DeclarationType, FunctionDeclaration } from '../analyzer/declaration'; +import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; +import { isStubFile, SourceMapper } from '../analyzer/sourceMapper'; +import { ClassType, FunctionType, ModuleType, OverloadedFunctionType } from '../analyzer/types'; +import { ModuleNode } from '../parser/parseNodes'; + +export function getOverloadedFunctionDocStrings( + type: OverloadedFunctionType, + resolvedDecl: DeclarationBase | undefined, + sourceMapper: SourceMapper +) { + const docStrings: string[] = []; + if (type.overloads.some((o) => o.details.docString)) { + type.overloads.forEach((overload) => { + if (overload.details.docString) { + docStrings.push(overload.details.docString); + } + }); + } else if (resolvedDecl && isStubFile(resolvedDecl.path) && resolvedDecl.type === DeclarationType.Function) { + const implDecls = sourceMapper.findFunctionDeclarations(resolvedDecl as FunctionDeclaration); + const docString = _getFunctionOrClassDeclDocString(implDecls); + if (docString) { + docStrings.push(docString); + } + } + return docStrings; +} + +export function getModuleDocString( + type: ModuleType, + resolvedDecl: DeclarationBase | undefined, + sourceMapper: SourceMapper +) { + let docString = type.docString; + if (!docString) { + if (resolvedDecl && isStubFile(resolvedDecl.path)) { + const modules = sourceMapper.findModules(resolvedDecl.path); + docString = _getModuleNodeDocString(modules); + } + } + return docString; +} + +export function getClassDocString( + type: ClassType, + resolvedDecl: DeclarationBase | undefined, + sourceMapper: SourceMapper +) { + let docString = type.details.docString; + if (!docString) { + if (resolvedDecl && isStubFile(resolvedDecl.path) && resolvedDecl.type === DeclarationType.Class) { + const implDecls = sourceMapper.findClassDeclarations(resolvedDecl as ClassDeclaration); + docString = _getFunctionOrClassDeclDocString(implDecls); + } + } + return docString; +} + +export function getFunctionDocString(type: FunctionType, sourceMapper: SourceMapper) { + let docString = type.details.docString; + if (!docString) { + const decl = type.details.declaration; + if (decl && isStubFile(decl.path)) { + const implDecls = sourceMapper.findFunctionDeclarations(decl); + docString = _getFunctionOrClassDeclDocString(implDecls); + } + } + return docString; +} + +function _getFunctionOrClassDeclDocString(decls: FunctionDeclaration[] | ClassDeclaration[]): string | undefined { + for (const decl of decls) { + const docString = ParseTreeUtils.getDocString(decl.node?.suite?.statements); + if (docString) { + return docString; + } + } + + return undefined; +} + +function _getModuleNodeDocString(modules: ModuleNode[]): string | undefined { + for (const module of modules) { + if (module.statements) { + const docString = ParseTreeUtils.getDocString(module.statements); + if (docString) { + return docString; + } + } + } + + return undefined; +} diff --git a/server/src/analyzer/typeEvaluator.ts b/server/src/analyzer/typeEvaluator.ts index 92481d641..c57856390 100644 --- a/server/src/analyzer/typeEvaluator.ts +++ b/server/src/analyzer/typeEvaluator.ts @@ -431,6 +431,8 @@ export interface TypeEvaluator { addError: (message: string, node: ParseNode) => Diagnostic | undefined; addWarning: (message: string, node: ParseNode) => Diagnostic | undefined; + addInformation: (message: string, node: ParseNode) => Diagnostic | undefined; + addDiagnostic: ( diagLevel: DiagnosticLevel, rule: string, @@ -1852,33 +1854,38 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags: return false; } - function addWarning(message: string, node: ParseNode, range?: TextRange) { - if (!isDiagnosticSuppressed && !isSpeculativeMode(node)) { - const fileInfo = getFileInfo(node); - return fileInfo.diagnosticSink.addWarningWithTextRange(message, range || node); - } + function addInformation(message: string, node: ParseNode, range?: TextRange) { + return addDiagnosticWithSuppressionCheck('information', message, node, range); + } - return undefined; + function addWarning(message: string, node: ParseNode, range?: TextRange) { + return addDiagnosticWithSuppressionCheck('warning', message, node, range); } function addError(message: string, node: ParseNode, range?: TextRange) { + return addDiagnosticWithSuppressionCheck('error', message, node, range); + } + + function addDiagnosticWithSuppressionCheck( + diagLevel: DiagnosticLevel, + message: string, + node: ParseNode, + range?: TextRange + ) { if (!isDiagnosticSuppressed && !isSpeculativeMode(node)) { const fileInfo = getFileInfo(node); - return fileInfo.diagnosticSink.addErrorWithTextRange(message, range || node); + return fileInfo.diagnosticSink.addDiagnosticWithTextRange(diagLevel, message, range || node); } return undefined; } function addDiagnostic(diagLevel: DiagnosticLevel, rule: string, message: string, node: ParseNode) { - let diagnostic: Diagnostic | undefined; - - if (diagLevel === 'error') { - diagnostic = addError(message, node); - } else if (diagLevel === 'warning') { - diagnostic = addWarning(message, node); + if (diagLevel === 'none') { + return undefined; } + const diagnostic = addDiagnosticWithSuppressionCheck(diagLevel, message, node); if (diagnostic) { diagnostic.setRule(rule); } @@ -1893,17 +1900,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags: message: string, range: TextRange ) { - let diagnostic: Diagnostic | undefined; - - if (diagLevel === 'error') { - diagnostic = fileInfo.diagnosticSink.addErrorWithTextRange(message, range); - } else if (diagLevel === 'warning') { - diagnostic = fileInfo.diagnosticSink.addWarningWithTextRange(message, range); + if (diagLevel === 'none') { + return undefined; } - if (diagnostic) { - diagnostic.setRule(rule); - } + const diagnostic = fileInfo.diagnosticSink.addDiagnosticWithTextRange(diagLevel, message, range); + diagnostic.setRule(rule); return diagnostic; } @@ -12651,6 +12653,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags: canOverrideMethod, addError, addWarning, + addInformation, addDiagnostic, addDiagnosticForTextRange, printType, diff --git a/server/src/common/commandLineOptions.ts b/server/src/common/commandLineOptions.ts index 901ed5bb1..6648c18ba 100644 --- a/server/src/common/commandLineOptions.ts +++ b/server/src/common/commandLineOptions.ts @@ -9,6 +9,24 @@ * of the analyzer). */ +export const enum DiagnosticSeverityOverrides { + Error = 'error', + Warning = 'warning', + Information = 'information', + None = 'none', +} + +export function getDiagnosticSeverityOverrides() { + return [ + DiagnosticSeverityOverrides.Error, + DiagnosticSeverityOverrides.Warning, + DiagnosticSeverityOverrides.Information, + DiagnosticSeverityOverrides.None, + ]; +} + +export type DiagnosticSeverityOverridesMap = { [ruleName: string]: DiagnosticSeverityOverrides }; + // Some options can be specified by command line. export class CommandLineOptions { constructor(executionRoot: string, fromVsCodeExtension: boolean) { @@ -76,4 +94,7 @@ export class CommandLineOptions { // from the command-line. Useful for providing clearer error // messages. fromVsCodeExtension: boolean; + + // Indicates diagnostic severity overrides + diagnosticSeverityOverrides?: DiagnosticSeverityOverridesMap; } diff --git a/server/src/common/configOptions.ts b/server/src/common/configOptions.ts index 15d1f4abf..2f0e5214d 100644 --- a/server/src/common/configOptions.ts +++ b/server/src/common/configOptions.ts @@ -10,6 +10,7 @@ import { isAbsolute } from 'path'; import * as pathConsts from '../common/pathConsts'; +import { DiagnosticSeverityOverridesMap } from './commandLineOptions'; import { ConsoleInterface } from './console'; import { DiagnosticRule } from './diagnosticRules'; import { FileSystem } from './fileSystem'; @@ -41,7 +42,7 @@ export class ExecutionEnvironment { venv?: string; } -export type DiagnosticLevel = 'none' | 'warning' | 'error'; +export type DiagnosticLevel = 'none' | 'information' | 'warning' | 'error'; export interface DiagnosticRuleSet { // Should "Unknown" types be reported as "Any"? @@ -558,7 +559,12 @@ export class ConfigOptions { } // Initialize the structure from a JSON object. - initializeFromJson(configObj: any, typeCheckingMode: string | undefined, console: ConsoleInterface) { + initializeFromJson( + configObj: any, + typeCheckingMode: string | undefined, + console: ConsoleInterface, + diagnosticOverrides?: DiagnosticSeverityOverridesMap + ) { // Read the "include" entry. this.include = []; if (configObj.include !== undefined) { @@ -651,6 +657,9 @@ export class ConfigOptions { const defaultSettings = ConfigOptions.getDiagnosticRuleSet(configTypeCheckingMode || typeCheckingMode); + // Apply host provided overrides first and then overrides from the config file + this.applyDiagnosticOverrides(diagnosticOverrides); + this.diagnosticRuleSet = { printUnknownAsAny: defaultSettings.printUnknownAsAny, omitTypeArgsIfAny: defaultSettings.omitTypeArgsIfAny, @@ -1068,6 +1077,16 @@ export class ConfigOptions { } } + applyDiagnosticOverrides(diagnosticSeverityOverrides: DiagnosticSeverityOverridesMap | undefined) { + if (!diagnosticSeverityOverrides) { + return; + } + + for (const [ruleName, severity] of Object.entries(diagnosticSeverityOverrides)) { + (this.diagnosticRuleSet as any)[ruleName] = severity; + } + } + private _convertBoolean(value: any, fieldName: string, defaultValue: boolean): boolean { if (value === undefined) { return defaultValue; @@ -1085,12 +1104,12 @@ export class ConfigOptions { } else if (typeof value === 'boolean') { return value ? 'error' : 'none'; } else if (typeof value === 'string') { - if (value === 'error' || value === 'warning' || value === 'none') { + if (value === 'error' || value === 'warning' || value === 'information' || value === 'none') { return value; } } - console.log(`Config "${fieldName}" entry must be true, false, "error", "warning" or "none".`); + console.log(`Config "${fieldName}" entry must be true, false, "error", "warning", "information" or "none".`); return defaultValue; } diff --git a/server/src/common/diagnostic.ts b/server/src/common/diagnostic.ts index 98c8c47e6..8f71791d5 100644 --- a/server/src/common/diagnostic.ts +++ b/server/src/common/diagnostic.ts @@ -8,14 +8,29 @@ */ import { Commands } from '../commands/commands'; +import { DiagnosticLevel } from './configOptions'; import { Range } from './textRange'; export const enum DiagnosticCategory { Error, Warning, + Information, UnusedCode, } +export function convertLevelToCategory(level: DiagnosticLevel) { + switch (level) { + case 'error': + return DiagnosticCategory.Error; + case 'warning': + return DiagnosticCategory.Warning; + case 'information': + return DiagnosticCategory.Information; + default: + throw new Error(`${level} is not expected`); + } +} + export interface DiagnosticAction { action: string; } diff --git a/server/src/common/diagnosticSink.ts b/server/src/common/diagnosticSink.ts index 5b800a853..4e9b56233 100644 --- a/server/src/common/diagnosticSink.ts +++ b/server/src/common/diagnosticSink.ts @@ -7,6 +7,7 @@ * Class that represents errors and warnings. */ +import { DiagnosticLevel } from './configOptions'; import { Diagnostic, DiagnosticAction, DiagnosticCategory } from './diagnostic'; import { convertOffsetsToRange } from './positionUtils'; import { Range, TextRange } from './textRange'; @@ -43,6 +44,10 @@ export class DiagnosticSink { return this.addDiagnostic(new Diagnostic(DiagnosticCategory.Warning, message, range)); } + addInformation(message: string, range: Range) { + return this.addDiagnostic(new Diagnostic(DiagnosticCategory.Information, message, range)); + } + addUnusedCode(message: string, range: Range, action?: DiagnosticAction) { const diag = new Diagnostic(DiagnosticCategory.UnusedCode, message, range); if (action) { @@ -87,12 +92,18 @@ export class TextRangeDiagnosticSink extends DiagnosticSink { this._lines = lines; } - addErrorWithTextRange(message: string, range: TextRange) { - return this.addError(message, convertOffsetsToRange(range.start, range.start + range.length, this._lines)); - } - - addWarningWithTextRange(message: string, range: TextRange) { - return this.addWarning(message, convertOffsetsToRange(range.start, range.start + range.length, this._lines)); + addDiagnosticWithTextRange(level: DiagnosticLevel, message: string, range: TextRange) { + const positionRange = convertOffsetsToRange(range.start, range.start + range.length, this._lines); + switch (level) { + case 'error': + return this.addError(message, positionRange); + case 'warning': + return this.addWarning(message, positionRange); + case 'information': + return this.addInformation(message, positionRange); + default: + throw new Error(`${level} is not expected value`); + } } addUnusedCodeWithTextRange(message: string, range: TextRange, action?: DiagnosticAction) { diff --git a/server/src/common/textRange.ts b/server/src/common/textRange.ts index 3a02f858f..c118f8d42 100644 --- a/server/src/common/textRange.ts +++ b/server/src/common/textRange.ts @@ -67,11 +67,25 @@ export interface Position { character: number; } +namespace Position { + export function is(value: any): value is Position { + const candidate = value as Position; + return candidate && candidate.line !== void 0 && candidate.character !== void 0; + } +} + export interface Range { start: Position; end: Position; } +namespace Range { + export function is(value: any): value is Range { + const candidate = value as Range; + return candidate && candidate.start !== void 0 && candidate.end !== void 0; + } +} + // Represents a range within a particular document. export interface DocumentRange { path: string; @@ -107,8 +121,12 @@ export function doRangesOverlap(a: Range, b: Range) { return true; } -export function doesRangeContain(range: Range, position: Position) { - return comparePositions(range.start, position) <= 0 && comparePositions(range.end, position) >= 0; +export function doesRangeContain(range: Range, positionOrRange: Position | Range): boolean { + if (Position.is(positionOrRange)) { + return comparePositions(range.start, positionOrRange) <= 0 && comparePositions(range.end, positionOrRange) >= 0; + } + + return doesRangeContain(range, positionOrRange.start) && doesRangeContain(range, positionOrRange.end); } export function rangesAreEqual(a: Range, b: Range) { diff --git a/server/src/languageServerBase.ts b/server/src/languageServerBase.ts index cb1f4f761..da0497e7a 100644 --- a/server/src/languageServerBase.ts +++ b/server/src/languageServerBase.ts @@ -51,10 +51,16 @@ import { AnalyzerService, configFileNames } from './analyzer/service'; import { BackgroundAnalysisBase } from './backgroundAnalysisBase'; import { CancelAfter, getCancellationStrategyFromArgv } from './common/cancellationUtils'; import { getNestedProperty } from './common/collectionUtils'; -import { ConfigOptions } from './common/configOptions'; +import { + DiagnosticSeverityOverrides, + DiagnosticSeverityOverridesMap, + getDiagnosticSeverityOverrides, +} from './common/commandLineOptions'; +import { ConfigOptions, getDiagLevelDiagnosticRules } from './common/configOptions'; import { ConsoleInterface } from './common/console'; import { createDeferred, Deferred } from './common/deferred'; import { Diagnostic as AnalyzerDiagnostic, DiagnosticCategory } from './common/diagnostic'; +import { DiagnosticRule } from './common/diagnosticRules'; import { LanguageServiceExtension } from './common/extensibility'; import { createFromRealFileSystem, @@ -85,6 +91,7 @@ export interface ServerSettings { extraPaths?: string[]; watchForSourceChanges?: boolean; watchForLibraryChanges?: boolean; + diagnosticSeverityOverrides?: DiagnosticSeverityOverridesMap; } export interface WorkspaceServiceInstance { @@ -221,6 +228,24 @@ export abstract class LanguageServerBase implements LanguageServerInterface { return diagnosticMode !== 'workspace'; } + protected getSeverityOverrides(value: string): DiagnosticSeverityOverrides | undefined { + const enumValue = value as DiagnosticSeverityOverrides; + if (getDiagnosticSeverityOverrides().includes(enumValue)) { + return enumValue; + } + + return undefined; + } + + protected getDiagnosticRuleName(value: string): DiagnosticRule | undefined { + const enumValue = value as DiagnosticRule; + if (getDiagLevelDiagnosticRules().includes(enumValue)) { + return enumValue; + } + + return undefined; + } + protected createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver { return new ImportResolver(fs, options); } @@ -825,8 +850,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface { private _convertDiagnostics(diags: AnalyzerDiagnostic[]): Diagnostic[] { return diags.map((diag) => { - const severity = - diag.category === DiagnosticCategory.Error ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning; + const severity = convertCategoryToSeverity(diag.category); let source = this._productName; const rule = diag.getRule(); @@ -853,6 +877,19 @@ export abstract class LanguageServerBase implements LanguageServerInterface { return vsDiag; }); + + function convertCategoryToSeverity(category: DiagnosticCategory) { + switch (category) { + case DiagnosticCategory.Error: + return DiagnosticSeverity.Error; + case DiagnosticCategory.Warning: + return DiagnosticSeverity.Warning; + case DiagnosticCategory.Information: + return DiagnosticSeverity.Information; + case DiagnosticCategory.UnusedCode: + return DiagnosticSeverity.Hint; + } + } } protected recordUserInteractionTime() { diff --git a/server/src/languageService/analyzerServiceExecutor.ts b/server/src/languageService/analyzerServiceExecutor.ts index ffe268f5e..015155173 100644 --- a/server/src/languageService/analyzerServiceExecutor.ts +++ b/server/src/languageService/analyzerServiceExecutor.ts @@ -90,6 +90,7 @@ function _getCommandLineOptions( commandLineOptions.autoSearchPaths = serverSettings.autoSearchPaths; commandLineOptions.extraPaths = serverSettings.extraPaths; + commandLineOptions.diagnosticSeverityOverrides = serverSettings.diagnosticSeverityOverrides; return commandLineOptions; } diff --git a/server/src/languageService/completionProvider.ts b/server/src/languageService/completionProvider.ts index 9e0681f95..3a342b84c 100644 --- a/server/src/languageService/completionProvider.ts +++ b/server/src/languageService/completionProvider.ts @@ -24,9 +24,16 @@ import { Declaration, DeclarationType } from '../analyzer/declaration'; import { convertDocStringToMarkdown } from '../analyzer/docStringToMarkdown'; import { ImportedModuleDescriptor, ImportResolver } from '../analyzer/importResolver'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; +import { SourceMapper } from '../analyzer/sourceMapper'; import { Symbol, SymbolTable } from '../analyzer/symbol'; import * as SymbolNameUtils from '../analyzer/symbolNameUtils'; import { getLastTypedDeclaredForSymbol } from '../analyzer/symbolUtils'; +import { + getClassDocString, + getFunctionDocString, + getModuleDocString, + getOverloadedFunctionDocStrings, +} from '../analyzer/typeDocStringUtils'; import { CallSignatureInfo, TypeEvaluator } from '../analyzer/typeEvaluator'; import { ClassType, FunctionType, Type, TypeCategory } from '../analyzer/types'; import { doForSubtypes, getMembersForClass, getMembersForModule } from '../analyzer/typeUtils'; @@ -176,6 +183,7 @@ export class CompletionProvider { private _configOptions: ConfigOptions, private _importLookup: ImportLookup, private _evaluator: TypeEvaluator, + private _sourceMapper: SourceMapper, private _moduleSymbolsCallback: () => ModuleSymbolMap, private _cancellationToken: CancellationToken ) {} @@ -1074,11 +1082,17 @@ export class CompletionProvider { } if (type.category === TypeCategory.Module) { - documentation = type.docString; + documentation = getModuleDocString(type, primaryDecl, this._sourceMapper); } else if (type.category === TypeCategory.Class) { - documentation = type.details.docString; + documentation = getClassDocString(type, primaryDecl, this._sourceMapper); } else if (type.category === TypeCategory.Function) { - documentation = type.details.docString; + documentation = getFunctionDocString(type, this._sourceMapper); + } else if (type.category === TypeCategory.OverloadedFunction) { + documentation = getOverloadedFunctionDocStrings( + type, + primaryDecl, + this._sourceMapper + ).find((doc) => doc); } let markdownString = '```python\n' + typeDetail + '\n```\n'; diff --git a/server/src/languageService/definitionProvider.ts b/server/src/languageService/definitionProvider.ts index b949aab53..27dfa28f3 100644 --- a/server/src/languageService/definitionProvider.ts +++ b/server/src/languageService/definitionProvider.ts @@ -13,6 +13,7 @@ import { CancellationToken } from 'vscode-languageserver'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; +import { isStubFile, SourceMapper } from '../analyzer/sourceMapper'; import { TypeEvaluator } from '../analyzer/typeEvaluator'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { convertPositionToOffset } from '../common/positionUtils'; @@ -22,6 +23,7 @@ import { ParseResults } from '../parser/parser'; export class DefinitionProvider { static getDefinitionsForPosition( + sourceMapper: SourceMapper, parseResults: ParseResults, position: Position, evaluator: TypeEvaluator, @@ -51,6 +53,16 @@ export class DefinitionProvider { path: resolvedDecl.path, range: resolvedDecl.range, }); + + if (isStubFile(resolvedDecl.path)) { + const implDecl = sourceMapper.findDeclaration(resolvedDecl); + if (implDecl && implDecl.path) { + this._addIfUnique(definitions, { + path: implDecl.path, + range: implDecl.range, + }); + } + } } }); } diff --git a/server/src/languageService/hoverProvider.ts b/server/src/languageService/hoverProvider.ts index 33736bcd3..5480bdbc5 100644 --- a/server/src/languageService/hoverProvider.ts +++ b/server/src/languageService/hoverProvider.ts @@ -11,9 +11,16 @@ import { CancellationToken, Hover, MarkupKind } from 'vscode-languageserver'; -import { Declaration, DeclarationType } from '../analyzer/declaration'; +import { Declaration, DeclarationBase, DeclarationType } from '../analyzer/declaration'; import { convertDocStringToMarkdown } from '../analyzer/docStringToMarkdown'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; +import { SourceMapper } from '../analyzer/sourceMapper'; +import { + getClassDocString, + getFunctionDocString, + getModuleDocString, + getOverloadedFunctionDocStrings, +} from '../analyzer/typeDocStringUtils'; import { TypeEvaluator } from '../analyzer/typeEvaluator'; import { Type, TypeCategory, UnknownType } from '../analyzer/types'; import { ClassMemberLookupFlags, isProperty, lookUpClassMember } from '../analyzer/typeUtils'; @@ -36,6 +43,7 @@ export interface HoverResults { export class HoverProvider { static getHoverForPosition( + sourceMapper: SourceMapper, parseResults: ParseResults, position: Position, evaluator: TypeEvaluator, @@ -64,7 +72,7 @@ export class HoverProvider { if (node.nodeType === ParseNodeType.Name) { const declarations = evaluator.getDeclarationsForNameNode(node); if (declarations && declarations.length > 0) { - this._addResultsForDeclaration(results.parts, declarations[0], node, evaluator); + this._addResultsForDeclaration(sourceMapper, results.parts, declarations[0], node, evaluator); } else if (!node.parent || node.parent.nodeType !== ParseNodeType.ModuleName) { // If we had no declaration, see if we can provide a minimal tooltip. We'll skip // this if it's part of a module name, since a module name part with no declaration @@ -84,7 +92,7 @@ export class HoverProvider { } this._addResultsPart(results.parts, typeText, true); - this._addDocumentationPart(results.parts, node, evaluator); + this._addDocumentationPart(sourceMapper, results.parts, node, evaluator, undefined); } } } @@ -93,6 +101,7 @@ export class HoverProvider { } private static _addResultsForDeclaration( + sourceMapper: SourceMapper, parts: HoverTextPart[], declaration: Declaration, node: NameNode, @@ -107,7 +116,7 @@ export class HoverProvider { switch (resolvedDecl.type) { case DeclarationType.Intrinsic: { this._addResultsPart(parts, node.value + this._getTypeText(node, evaluator), true); - this._addDocumentationPart(parts, node, evaluator); + this._addDocumentationPart(sourceMapper, parts, node, evaluator, resolvedDecl); break; } @@ -130,13 +139,13 @@ export class HoverProvider { } this._addResultsPart(parts, `(${label}) ` + node.value + this._getTypeText(typeNode, evaluator), true); - this._addDocumentationPart(parts, node, evaluator); + this._addDocumentationPart(sourceMapper, parts, node, evaluator, resolvedDecl); break; } case DeclarationType.Parameter: { this._addResultsPart(parts, '(parameter) ' + node.value + this._getTypeText(node, evaluator), true); - this._addDocumentationPart(parts, node, evaluator); + this._addDocumentationPart(sourceMapper, parts, node, evaluator, resolvedDecl); break; } @@ -184,7 +193,7 @@ export class HoverProvider { } this._addResultsPart(parts, '(class) ' + classText, true); - this._addDocumentationPart(parts, node, evaluator); + this._addDocumentationPart(sourceMapper, parts, node, evaluator, resolvedDecl); break; } @@ -196,13 +205,13 @@ export class HoverProvider { } this._addResultsPart(parts, `(${label}) ` + node.value + this._getTypeText(node, evaluator), true); - this._addDocumentationPart(parts, node, evaluator); + this._addDocumentationPart(sourceMapper, parts, node, evaluator, resolvedDecl); break; } case DeclarationType.Alias: { this._addResultsPart(parts, '(module) ' + node.value, true); - this._addDocumentationPart(parts, node, evaluator); + this._addDocumentationPart(sourceMapper, parts, node, evaluator, resolvedDecl); break; } } @@ -213,24 +222,45 @@ export class HoverProvider { return ': ' + evaluator.printType(type); } - private static _addDocumentationPart(parts: HoverTextPart[], node: NameNode, evaluator: TypeEvaluator) { + private static _addDocumentationPart( + sourceMapper: SourceMapper, + parts: HoverTextPart[], + node: NameNode, + evaluator: TypeEvaluator, + resolvedDecl: DeclarationBase | undefined + ) { const type = evaluator.getType(node); if (type) { - this._addDocumentationPartForType(parts, type); + this._addDocumentationPartForType(sourceMapper, parts, type, resolvedDecl); } } - private static _addDocumentationPartForType(parts: HoverTextPart[], type: Type) { + private static _addDocumentationPartForType( + sourceMapper: SourceMapper, + parts: HoverTextPart[], + type: Type, + resolvedDecl: DeclarationBase | undefined + ) { if (type.category === TypeCategory.Module) { - this._addDocumentationResultsPart(parts, type.docString); + const docString = getModuleDocString(type, resolvedDecl, sourceMapper); + if (docString) { + this._addDocumentationResultsPart(parts, docString); + } } else if (type.category === TypeCategory.Class) { - this._addDocumentationResultsPart(parts, type.details.docString); + const docString = getClassDocString(type, resolvedDecl, sourceMapper); + if (docString) { + this._addDocumentationResultsPart(parts, docString); + } } else if (type.category === TypeCategory.Function) { - this._addDocumentationResultsPart(parts, type.details.docString); + const docString = getFunctionDocString(type, sourceMapper); + if (docString) { + this._addDocumentationResultsPart(parts, docString); + } } else if (type.category === TypeCategory.OverloadedFunction) { - type.overloads.forEach((overload) => { - this._addDocumentationResultsPart(parts, overload.details.docString); - }); + const docStrings = getOverloadedFunctionDocStrings(type, resolvedDecl, sourceMapper); + for (const docString of docStrings) { + this._addDocumentationResultsPart(parts, docString); + } } } diff --git a/server/src/server.ts b/server/src/server.ts index 4c3ba69e8..e3b35759f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -31,7 +31,9 @@ class PyrightServer extends LanguageServerBase { async getSettings(workspace: WorkspaceServiceInstance): Promise { const serverSettings: ServerSettings = { watchForSourceChanges: true, + diagnosticSeverityOverrides: {}, }; + try { const pythonSection = await this.getConfiguration(workspace, 'python'); if (pythonSection) { @@ -51,6 +53,17 @@ class PyrightServer extends LanguageServerBase { serverSettings.stubPath = normalizeSlashes(stubPath); } + const diagnosticSeverityOverrides = pythonAnalysisSection.diagnosticSeverityOverrides; + if (diagnosticSeverityOverrides) { + for (const [name, value] of Object.entries(diagnosticSeverityOverrides)) { + const ruleName = this.getDiagnosticRuleName(name); + const severity = this.getSeverityOverrides(value as string); + if (ruleName && severity) { + serverSettings.diagnosticSeverityOverrides![ruleName] = severity!; + } + } + } + if (pythonAnalysisSection.diagnosticMode !== undefined) { serverSettings.openFilesOnly = this.isOpenFilesOnly(pythonAnalysisSection.diagnosticMode); } else if (pythonAnalysisSection.openFilesOnly !== undefined) { diff --git a/server/src/tests/fourSlashRunner.test.ts b/server/src/tests/fourSlashRunner.test.ts index d6e9c7267..edb93b9fa 100644 --- a/server/src/tests/fourSlashRunner.test.ts +++ b/server/src/tests/fourSlashRunner.test.ts @@ -28,7 +28,7 @@ describe('fourslash tests', () => { const justName = fn.replace(/^.*[\\/]/, ''); // TODO: make these to use promise/async rather than callback token - it('fourslash test ' + justName + ' runs correctly', (cb) => { + it('fourslash test ' + justName + ' run', (cb) => { runFourSlashTest(MODULE_PATH, fn, cb); }); }); diff --git a/server/src/tests/fourslash/completions.autoimport.Lib.Found.Type.fourslash.ts b/server/src/tests/fourslash/completions.autoimport.Lib.Found.Type.fourslash.ts index 9735bd6ec..90d12d46a 100644 --- a/server/src/tests/fourslash/completions.autoimport.Lib.Found.Type.fourslash.ts +++ b/server/src/tests/fourslash/completions.autoimport.Lib.Found.Type.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: test1.py //// Test[|/*marker*/|] @@ -12,7 +11,8 @@ //// class Test: //// pass -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker: { completions: [ { diff --git a/server/src/tests/fourslash/completions.autoimport.Lib.Found.duplication.fourslash.ts b/server/src/tests/fourslash/completions.autoimport.Lib.Found.duplication.fourslash.ts index 1a86adf60..cb2fca31f 100644 --- a/server/src/tests/fourslash/completions.autoimport.Lib.Found.duplication.fourslash.ts +++ b/server/src/tests/fourslash/completions.autoimport.Lib.Found.duplication.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: test1.py //// test[|/*marker*/|] @@ -26,7 +25,8 @@ //// class Test2: //// pass -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker: { completions: [ { diff --git a/server/src/tests/fourslash/completions.autoimport.Lib.NotFound.fourslash.ts b/server/src/tests/fourslash/completions.autoimport.Lib.NotFound.fourslash.ts index 5ee1b38b7..d080f6b52 100644 --- a/server/src/tests/fourslash/completions.autoimport.Lib.NotFound.fourslash.ts +++ b/server/src/tests/fourslash/completions.autoimport.Lib.NotFound.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: test1.py //// Test[|/*marker*/|] @@ -9,4 +8,5 @@ //// class Test: //// pass -helper.verifyCompletion('included', { marker: { completions: [] } }); +// @ts-ignore +await helper.verifyCompletion('included', { marker: { completions: [] } }); diff --git a/server/src/tests/fourslash/completions.autoimport.fourslash.ts b/server/src/tests/fourslash/completions.autoimport.fourslash.ts index b34884c7f..8a0138d34 100644 --- a/server/src/tests/fourslash/completions.autoimport.fourslash.ts +++ b/server/src/tests/fourslash/completions.autoimport.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: test1.py //// Test[|/*marker*/|] @@ -8,7 +7,8 @@ //// class Test: //// pass -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker: { completions: [ { diff --git a/server/src/tests/fourslash/completions.excluded.fourslash.ts b/server/src/tests/fourslash/completions.excluded.fourslash.ts index 431f69475..344f9cd8b 100644 --- a/server/src/tests/fourslash/completions.excluded.fourslash.ts +++ b/server/src/tests/fourslash/completions.excluded.fourslash.ts @@ -1,11 +1,11 @@ /// -// @asynctest: true // @filename: test.py //// a = 42 //// a.n[|/*marker1*/|] -helper.verifyCompletion('excluded', { +// @ts-ignore +await helper.verifyCompletion('excluded', { marker1: { completions: [{ label: 'capitalize' }], }, diff --git a/server/src/tests/fourslash/completions.fourslash.ts b/server/src/tests/fourslash/completions.fourslash.ts index 9cdf4d65c..1238ef586 100644 --- a/server/src/tests/fourslash/completions.fourslash.ts +++ b/server/src/tests/fourslash/completions.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: test.py //// import time @@ -14,7 +13,8 @@ //// pass //// some_fun[|/*marker3*/|] -helper.verifyCompletion('exact', { +// @ts-ignore +await helper.verifyCompletion('exact', { marker1: { completions: [{ label: 'localtime' }] }, marker2: { completions: [{ label: 'aaaaaa' }] }, marker3: { diff --git a/server/src/tests/fourslash/completions.included.fourslash.ts b/server/src/tests/fourslash/completions.included.fourslash.ts index 6dc3dfb31..a8dc4d090 100644 --- a/server/src/tests/fourslash/completions.included.fourslash.ts +++ b/server/src/tests/fourslash/completions.included.fourslash.ts @@ -1,11 +1,11 @@ /// -// @asynctest: true // @filename: test.py //// a = 42 //// a.n[|/*marker1*/|] -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker1: { completions: [{ label: 'denominator' }, { label: 'imag' }, { label: 'numerator' }, { label: 'real' }], }, diff --git a/server/src/tests/fourslash/completions.libCodeAndStub.fourslash.skip.ts b/server/src/tests/fourslash/completions.libCodeAndStub.fourslash.ts similarity index 95% rename from server/src/tests/fourslash/completions.libCodeAndStub.fourslash.skip.ts rename to server/src/tests/fourslash/completions.libCodeAndStub.fourslash.ts index d2eb74038..7ad981a90 100644 --- a/server/src/tests/fourslash/completions.libCodeAndStub.fourslash.skip.ts +++ b/server/src/tests/fourslash/completions.libCodeAndStub.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: mspythonconfig.json //// { @@ -24,7 +23,8 @@ //// obj = testLib.[|/*marker1*/Validator|]() //// obj.is[|/*marker2*/|] -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker1: { completions: [ { diff --git a/server/src/tests/fourslash/completions.libCodeNoStub.fourslash.ts b/server/src/tests/fourslash/completions.libCodeNoStub.fourslash.ts index f65c29a14..fd1918c4b 100644 --- a/server/src/tests/fourslash/completions.libCodeNoStub.fourslash.ts +++ b/server/src/tests/fourslash/completions.libCodeNoStub.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: mspythonconfig.json //// { @@ -19,7 +18,8 @@ //// obj = testLib.[|/*marker1*/Validator|]() //// obj.is[|/*marker2*/|] -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker1: { completions: [ { diff --git a/server/src/tests/fourslash/completions.libStub.fourslash.ts b/server/src/tests/fourslash/completions.libStub.fourslash.ts index 54643cd26..528e3e2fb 100644 --- a/server/src/tests/fourslash/completions.libStub.fourslash.ts +++ b/server/src/tests/fourslash/completions.libStub.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: mspythonconfig.json //// { @@ -19,7 +18,8 @@ //// obj = testLib.[|/*marker1*/Validator|]() //// obj.is[|/*marker2*/|] -helper.verifyCompletion('included', { +// @ts-ignore +await helper.verifyCompletion('included', { marker1: { completions: [ { diff --git a/server/src/tests/fourslash/completions.overloads.fourslash.ts b/server/src/tests/fourslash/completions.overloads.fourslash.ts new file mode 100644 index 000000000..5c5a5cf24 --- /dev/null +++ b/server/src/tests/fourslash/completions.overloads.fourslash.ts @@ -0,0 +1,31 @@ +/// + +// @filename: overloads_client.py +//// import overloads +//// overloads.f[|/*marker1*/|] + +// @filename: typings/overloads.pyi +//// from typing import overload +//// +//// @overload +//// def func(x: str) -> str: ... +//// +//// @overload +//// def func(x: int) -> int: +//// '''func docs''' +//// pass + +// @ts-ignore +await helper.verifyCompletion('included', { + marker1: { + completions: [ + { + label: 'func', + documentation: { + kind: 'markdown', + value: '```python\nfunc(x: str) -> str\nfunc(x: int) -> int\n```\n---\nfunc docs', + }, + }, + ], + }, +}); diff --git a/server/src/tests/fourslash/findDefinitions.sourceAndStub.function.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceAndStub.function.fourslash.ts new file mode 100644 index 000000000..0b4a10b6d --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceAndStub.function.fourslash.ts @@ -0,0 +1,26 @@ +/// + +// @filename: testLib1/__init__.py +// @library: true +//// def [|func1|](a): +//// pass + +// @filename: typings/testLib1/__init__.pyi +//// def [|func1|](a: str): ... + +// @filename: test.py +//// from testLib1 import func1 +//// +//// [|/*marker*/func1|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClass.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClass.fourslash.ts new file mode 100644 index 000000000..c1a985997 --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClass.fourslash.ts @@ -0,0 +1,32 @@ +/// + +// @filename: testLib1/__init__.py +// @library: true +//// class Outer: +//// class [|Middle|]: +//// class Inner: +//// def M(self, a): +//// pass + +// @filename: typings/testLib1/__init__.pyi +//// class Outer: +//// class [|Middle|]: +//// class Inner: +//// def M(self, a: str): ... + +// @filename: test.py +//// import testLib1 +//// +//// testLib1.Outer.[|/*marker*/Middle|].Inner() + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClassMethod.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClassMethod.fourslash.ts new file mode 100644 index 000000000..f8e850ae9 --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceAndStub.innerClassMethod.fourslash.ts @@ -0,0 +1,33 @@ +/// + +// @filename: testLib1/__init__.py +// @library: true +//// class Outer: +//// class Middle: +//// class Inner: +//// def [|M|](self, a): +//// pass + +// @filename: typings/testLib1/__init__.pyi +//// class Outer: +//// class Middle: +//// class Inner: +//// def [|M|](self, a: str): ... + +// @filename: test.py +//// import testLib1 +//// +//// a = testLib1.Outer.Middle.Inner() +//// a.[|/*marker*/M|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClass.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClass.fourslash.ts new file mode 100644 index 000000000..5bb1a4e3f --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClass.fourslash.ts @@ -0,0 +1,28 @@ +/// + +// @filename: testLib1/__init__.py +// @library: true +//// class [|Test1|]: +//// def M(self, a): +//// pass + +// @filename: typings/testLib1/__init__.pyi +//// class [|Test1|]: +//// def M(self, a: str): ... + +// @filename: test.py +//// import testLib1 +//// +//// a = testLib1.[|/*marker*/Test1|]() + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClassMethod.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClassMethod.fourslash.ts new file mode 100644 index 000000000..4e35524a4 --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceAndStub.outerClassMethod.fourslash.ts @@ -0,0 +1,29 @@ +/// + +// @filename: testLib1/__init__.py +// @library: true +//// class Test1: +//// def [|M|](self, a): +//// pass + +// @filename: typings/testLib1/__init__.pyi +//// class Test1: +//// def [|M|](self, a: str): ... + +// @filename: test.py +//// import testLib1 +//// +//// a = testLib1.Test1() +//// a.[|/*marker*/M|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceOnly.class.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceOnly.class.fourslash.ts new file mode 100644 index 000000000..3f16240ac --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceOnly.class.fourslash.ts @@ -0,0 +1,29 @@ +/// + +// @filename: mspythonconfig.json +//// { +//// "useLibraryCodeForTypes": true +//// } + +// @filename: testLib1/__init__.py +// @library: true +//// class [|Test1|]: +//// def M(self, a: str): +//// pass + +// @filename: test.py +//// import testLib1 +//// +//// a = testLib1.[|/*marker*/Test1|]() + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceOnly.function1.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceOnly.function1.fourslash.ts new file mode 100644 index 000000000..f096243dd --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceOnly.function1.fourslash.ts @@ -0,0 +1,28 @@ +/// + +// @filename: mspythonconfig.json +//// { +//// "useLibraryCodeForTypes": true +//// } + +// @filename: testLib1/__init__.py +// @library: true +//// def [|func1|](a): +//// pass + +// @filename: test.py +//// from testLib1 import func1 +//// +//// [|/*marker*/func1|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceOnly.function2.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceOnly.function2.fourslash.ts new file mode 100644 index 000000000..7acd3b0c2 --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceOnly.function2.fourslash.ts @@ -0,0 +1,28 @@ +/// + +// @filename: mspythonconfig.json +//// { +//// "useLibraryCodeForTypes": true +//// } + +// @filename: testLib1/__init__.py +// @library: true +//// def [|func1|](a): +//// pass + +// @filename: test.py +//// from testLib1 import func1 as test_func1 +//// +//// [|/*marker*/test_func1|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport1.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport1.fourslash.ts new file mode 100644 index 000000000..cb3190af6 --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport1.fourslash.ts @@ -0,0 +1,22 @@ +/// + +// @filename: testLib1/__init__.py +//// def [|func1|](a): +//// pass + +// @filename: test.py +//// from .testLib1 import func1 +//// +//// [|/*marker*/func1|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport2.fourslash.ts b/server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport2.fourslash.ts new file mode 100644 index 000000000..e5aea142d --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.sourceOnly.relativeImport2.fourslash.ts @@ -0,0 +1,22 @@ +/// + +// @filename: testLib1/__init__.py +//// def [|func1|](a): +//// pass + +// @filename: test.py +//// from . import testLib1 +//// +//// testLib1.[|/*marker*/func1|]('') + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findDefinitions.stubOnly.fourslash.ts b/server/src/tests/fourslash/findDefinitions.stubOnly.fourslash.ts new file mode 100644 index 000000000..a227ff730 --- /dev/null +++ b/server/src/tests/fourslash/findDefinitions.stubOnly.fourslash.ts @@ -0,0 +1,23 @@ +/// + +// @filename: typings/testLib1/__init__.pyi +//// class [|Test1|]: +//// def M(self, a: str): +//// pass + +// @filename: test.py +//// import testLib1 +//// +//// a = testLib1.[|/*marker*/Test1|]() + +{ + const ranges = helper.getRanges().filter((r) => !r.marker); + + helper.verifyFindDefinitions({ + marker: { + definitions: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/findallreferences.fourslash.ts b/server/src/tests/fourslash/findallreferences.fourslash.ts index 4a301dc8f..c266afef1 100644 --- a/server/src/tests/fourslash/findallreferences.fourslash.ts +++ b/server/src/tests/fourslash/findallreferences.fourslash.ts @@ -8,13 +8,13 @@ // @filename: testLib1/__init__.py // @library: true //// class [|Test1|]: -//// def M(self, a: [|Test1|]): +//// def M(self, a: Test1): //// pass // @filename: test.py //// from testLib1 import [|Test1|] //// -//// a = [|[|/*marker*/Test1|]|]() +//// a = [|/*marker*/Test1|]() // @filename: test2.py //// from testLib1 import [|Test1|] @@ -22,7 +22,7 @@ //// b = [|Test1|]() { - const ranges = helper.getRanges().filter((r) => !r.marker); + const ranges = helper.getRanges(); helper.verifyFindAllReferences({ marker: { diff --git a/server/src/tests/fourslash/findallreferences.openFiles.fourslash.ts b/server/src/tests/fourslash/findallreferences.openFiles.fourslash.ts new file mode 100644 index 000000000..29e5a2052 --- /dev/null +++ b/server/src/tests/fourslash/findallreferences.openFiles.fourslash.ts @@ -0,0 +1,37 @@ +/// + +// @filename: mspythonconfig.json +//// { +//// "useLibraryCodeForTypes": true +//// } + +// @filename: testLib1/__init__.py +// @library: true +//// class [|/*lib*/Test1|]: +//// def M(self, a: [|Test1|]): +//// pass + +// @filename: test.py +//// from testLib1 import [|Test1|] +//// +//// a = [|/*marker*/Test1|]() + +// @filename: test2.py +//// from testLib1 import [|Test1|] +//// +//// b = [|Test1|]() + +{ + const ranges = helper.getRanges(); + + const libMarker = helper.getMarkerByName('lib'); + helper.openFile(libMarker.fileName); + + helper.verifyFindAllReferences({ + marker: { + references: ranges.map((r) => { + return { path: r.fileName, range: helper.convertPositionRange(r) }; + }), + }, + }); +} diff --git a/server/src/tests/fourslash/fourslash.ts b/server/src/tests/fourslash/fourslash.ts index 73a5a963b..7324a29bc 100644 --- a/server/src/tests/fourslash/fourslash.ts +++ b/server/src/tests/fourslash/fourslash.ts @@ -116,20 +116,22 @@ declare namespace _ { moveCaretRight(count: number): void; + openFile(indexOrName: number | string, content?: string): void; + verifyDiagnostics(map?: { [marker: string]: { category: string; message: string } }): void; verifyCodeActions( map: { [marker: string]: { codeActions: { title: string; kind: string; command: Command }[] }; }, verifyCodeActionCount?: boolean - ): void; - verifyCommand(command: Command, files: { [filePath: string]: string }): void; + ): Promise; + verifyCommand(command: Command, files: { [filePath: string]: string }): Promise; verifyInvokeCodeAction( map: { [marker: string]: { title: string; files?: { [filePath: string]: string }; edits?: TextEdit[] }; }, verifyCodeActionCount?: boolean - ): void; + ): Promise; verifyHover(map: { [marker: string]: { value: string; kind: string } }): void; verifyCompletion( verifyMode: 'exact' | 'included' | 'excluded', @@ -153,6 +155,11 @@ declare namespace _ { references: DocumentRange[]; }; }): void; + verifyFindDefinitions(map: { + [marker: string]: { + definitions: DocumentRange[]; + }; + }): void; verifyRename(map: { [marker: string]: { newName: string; @@ -161,7 +168,6 @@ declare namespace _ { }): void; /* not tested yet - openFile(indexOrName: number | string, content?: string): void; paste(text: string): void; type(text: string): void; diff --git a/server/src/tests/fourslash/hover.docFromSrc.fourslash.ts b/server/src/tests/fourslash/hover.docFromSrc.fourslash.ts new file mode 100644 index 000000000..855d9468c --- /dev/null +++ b/server/src/tests/fourslash/hover.docFromSrc.fourslash.ts @@ -0,0 +1,110 @@ +/// + +// @filename: module1.py +//// '''module1 docs''' +//// +//// def func1(): +//// '''func1 docs''' +//// return True +//// +//// class A: +//// '''A docs''' +//// def method1(self): +//// '''A.method1 docs''' +//// return True +//// class Inner: +//// '''A.Inner docs''' +//// def method1(self): +//// '''A.Inner.method1 docs''' +//// return True +//// +//// class B: +//// '''B docs''' +//// def __init__(self): +//// '''B init docs''' +//// pass + +// @filename: module1.pyi +//// def func1() -> bool: ... +//// +//// class A: +//// def method1(self) -> bool: ... +//// class Inner: +//// def method1(self) -> bool: ... +//// +//// class B: +//// def __init__(self): ... + +// @filename: module2/__init__.py +// @library: true +//// '''module2 docs''' +//// +//// from ._internal import func2 + +// @filename: module2/_internal.py +// @library: true +//// from ._more_internal import func2 + +// @filename: module2/_more_internal.py +// @library: true +//// def func2(): +//// '''func2 docs''' +//// return True + +// @filename: typings/module2.pyi +//// def func2() -> bool: ... + +// @filename: test.py +//// import module1 +//// import module2 +//// +//// print([|/*module1_docs*/module1|].[|/*func1_docs*/func1|]()) +//// +//// a = module1.[|/*a_docs*/A|]() +//// print(a.[|/*method1_docs*/method1|]()) +//// +//// b = module1.[|/*b_docs*/B|]() +//// +//// print([|/*module2_docs*/module2|].[|/*func2_docs*/func2|]()) +//// +//// inner = module1.A.[|/*a_inner_docs*/Inner|]() +//// print(inner.[|/*inner_method1_docs*/method1|]()) + +helper.verifyHover({ + a_docs: { + value: '```python\n(class) A\n```\nA docs', + kind: 'markdown', + }, + b_docs: { + value: '```python\n(class) B(self: B)\n```\nB docs', + kind: 'markdown', + }, + a_inner_docs: { + value: '```python\n(class) Inner\n```\nA.Inner docs', + kind: 'markdown', + }, + func1_docs: { + value: '```python\n(function) func1: () -> bool\n```\nfunc1 docs', + kind: 'markdown', + }, + func2_docs: { + value: '```python\n(function) func2: () -> bool\n```\nfunc2 docs', + kind: 'markdown', + }, + inner_method1_docs: { + value: '```python\n(method) method1: () -> bool\n```\nA.Inner.method1 docs', + kind: 'markdown', + }, + method1_docs: { + value: '```python\n(method) method1: () -> bool\n```\nA.method1 docs', + kind: 'markdown', + }, + module1_docs: { + value: '```python\n(module) module1\n```\nmodule1 docs', + kind: 'markdown', + }, + module2_docs: { + value: '```python\n(module) module2\n```\nmodule2 docs', + kind: 'markdown', + }, +}); diff --git a/server/src/tests/fourslash/hover.docFromSrc.relativeImport1.fourslash.ts b/server/src/tests/fourslash/hover.docFromSrc.relativeImport1.fourslash.ts new file mode 100644 index 000000000..b74f476d3 --- /dev/null +++ b/server/src/tests/fourslash/hover.docFromSrc.relativeImport1.fourslash.ts @@ -0,0 +1,29 @@ +/// + +// @filename: module1.py +//// '''module1 docs''' +//// +//// def func1(): +//// '''func1 docs''' +//// return True +//// + +// @filename: module1.pyi +//// def func1() -> bool: ... +//// + +// @filename: test.py +//// from . import module1 +//// +//// print([|/*module1_docs*/module1|].[|/*func1_docs*/func1|]()) + +helper.verifyHover({ + func1_docs: { + value: '```python\n(function) func1: () -> bool\n```\nfunc1 docs', + kind: 'markdown', + }, + module1_docs: { + value: '```python\n(module) module1\n```\nmodule1 docs', + kind: 'markdown', + }, +}); diff --git a/server/src/tests/fourslash/hover.docFromSrc.relativeImport2.fourslash.ts b/server/src/tests/fourslash/hover.docFromSrc.relativeImport2.fourslash.ts new file mode 100644 index 000000000..91f7f95c8 --- /dev/null +++ b/server/src/tests/fourslash/hover.docFromSrc.relativeImport2.fourslash.ts @@ -0,0 +1,25 @@ +/// + +// @filename: module1.py +//// '''module1 docs''' +//// +//// def func1(): +//// '''func1 docs''' +//// return True +//// + +// @filename: module1.pyi +//// def func1() -> bool: ... +//// + +// @filename: test.py +//// from .module1 import func1 +//// +//// print([|/*func1_docs*/func1|]()) + +helper.verifyHover({ + func1_docs: { + value: '```python\n(function) func1: () -> bool\n```\nfunc1 docs', + kind: 'markdown', + }, +}); diff --git a/server/src/tests/fourslash/hover.libCodeAndStub.fourslash.skip.ts b/server/src/tests/fourslash/hover.libCodeAndStub.fourslash.ts similarity index 100% rename from server/src/tests/fourslash/hover.libCodeAndStub.fourslash.skip.ts rename to server/src/tests/fourslash/hover.libCodeAndStub.fourslash.ts diff --git a/server/src/tests/fourslash/missingTypeStub.codeAction.fourslash.ts b/server/src/tests/fourslash/missingTypeStub.codeAction.fourslash.ts index 7cbacaf49..9b2ae7d14 100644 --- a/server/src/tests/fourslash/missingTypeStub.codeAction.fourslash.ts +++ b/server/src/tests/fourslash/missingTypeStub.codeAction.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: mspythonconfig.json //// { @@ -16,7 +15,8 @@ // @filename: .src/test.py //// import [|/*marker*/testLi|]b -helper.verifyCodeActions({ +// @ts-ignore +await helper.verifyCodeActions({ marker: { codeActions: [ { diff --git a/server/src/tests/fourslash/missingTypeStub.command.fourslash.ts b/server/src/tests/fourslash/missingTypeStub.command.fourslash.ts index 4afb7fa52..6015d9f54 100644 --- a/server/src/tests/fourslash/missingTypeStub.command.fourslash.ts +++ b/server/src/tests/fourslash/missingTypeStub.command.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: mspythonconfig.json //// { @@ -23,7 +22,8 @@ const command = { arguments: ['/', 'testLib', filename], }; -helper.verifyCommand(command, { +// @ts-ignore +await helper.verifyCommand(command, { ['/typings/testLib/__init__.pyi']: `""" This type stub file was generated by pyright. """ diff --git a/server/src/tests/fourslash/missingTypeStub.invokeCodeAction.fourslash.ts b/server/src/tests/fourslash/missingTypeStub.invokeCodeAction.fourslash.ts index 23fa95115..74ad61f21 100644 --- a/server/src/tests/fourslash/missingTypeStub.invokeCodeAction.fourslash.ts +++ b/server/src/tests/fourslash/missingTypeStub.invokeCodeAction.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: mspythonconfig.json //// { @@ -16,7 +15,8 @@ // @filename: test.py //// import [|/*marker*/testLi|]b -helper.verifyInvokeCodeAction({ +// @ts-ignore +await helper.verifyInvokeCodeAction({ marker: { title: `Create Type Stub For "testLib"`, files: { diff --git a/server/src/tests/fourslash/orderImports1.command.fourslash.ts b/server/src/tests/fourslash/orderImports1.command.fourslash.ts index 4c3c06a74..445f1bc69 100644 --- a/server/src/tests/fourslash/orderImports1.command.fourslash.ts +++ b/server/src/tests/fourslash/orderImports1.command.fourslash.ts @@ -1,12 +1,12 @@ /// -// @asynctest: true // @filename: quickActionOrganizeImportTest1.py //// import time //// import os //// import sys -helper.verifyCommand( +// @ts-ignore +await helper.verifyCommand( { title: 'Quick action order imports 1', command: Consts.Commands.orderImports, diff --git a/server/src/tests/fourslash/orderImports2.command.fourslash.ts b/server/src/tests/fourslash/orderImports2.command.fourslash.ts index 42e290827..f452f0004 100644 --- a/server/src/tests/fourslash/orderImports2.command.fourslash.ts +++ b/server/src/tests/fourslash/orderImports2.command.fourslash.ts @@ -1,5 +1,4 @@ /// -// @asynctest: true // @filename: quickActionOrganizeImportTest2.py //// import time @@ -9,7 +8,8 @@ //// import math //// import os -helper.verifyCommand( +// @ts-ignore +await helper.verifyCommand( { title: 'Quick action order imports', command: Consts.Commands.orderImports, diff --git a/server/src/tests/harness/fourslash/fourSlashTypes.ts b/server/src/tests/harness/fourslash/fourSlashTypes.ts index 3c2d1848a..97ea8989e 100644 --- a/server/src/tests/harness/fourslash/fourSlashTypes.ts +++ b/server/src/tests/harness/fourslash/fourSlashTypes.ts @@ -12,7 +12,6 @@ export const enum GlobalMetadataOptionNames { projectRoot = 'projectroot', ignoreCase = 'ignorecase', typeshed = 'typeshed', - asynctest = 'asynctest', } /** Any option name not belong to this will become global option */ diff --git a/server/src/tests/harness/fourslash/runner.ts b/server/src/tests/harness/fourslash/runner.ts index 84a9a4b09..22db82c7d 100644 --- a/server/src/tests/harness/fourslash/runner.ts +++ b/server/src/tests/harness/fourslash/runner.ts @@ -53,7 +53,7 @@ export function runFourSlashTestContent( // parse out the files and their metadata const testData = parseTestData(absoluteBasePath, content, absoluteFileName); - const state = new TestState(absoluteBasePath, testData, cb, mountPaths, hostSpecificFeatures); + const state = new TestState(absoluteBasePath, testData, mountPaths, hostSpecificFeatures); const output = ts.transpileModule(content, { reportDiagnostics: true, compilerOptions: { target: ts.ScriptTarget.ES2015 }, @@ -62,40 +62,25 @@ export function runFourSlashTestContent( throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics![0].messageText}`); } - runCode(output.outputText, state); + runCode(output.outputText, state, cb); } -function runCode(code: string, state: TestState): void { +async function runCode(code: string, state: TestState, cb?: jest.DoneCallback) { // Compile and execute the test - try { - if (state.asyncTest) { - runAsyncCode(); - } else { - runPlainCode(); - } - } catch (error) { - markDone(error); - } - - function runAsyncCode() { const wrappedCode = `(async function(helper, Consts) { - ${code} - })`; +${code} +})`; const f = eval(wrappedCode); - f(state, Consts); - } - - function runPlainCode() { - const wrappedCode = `(function(helper, Consts) { - ${code} - })`; - const f = eval(wrappedCode); - f(state, Consts); + await f(state, Consts); markDone(); + } catch (ex) { + markDone(ex); } function markDone(...args: any[]) { - state.markTestDone(...args); + if (cb) { + cb(...args); + } } } diff --git a/server/src/tests/harness/fourslash/testState.ts b/server/src/tests/harness/fourslash/testState.ts index 2d4af98ea..9bebf044b 100644 --- a/server/src/tests/harness/fourslash/testState.ts +++ b/server/src/tests/harness/fourslash/testState.ts @@ -87,14 +87,9 @@ export class TestState { private readonly _files: string[] = []; private readonly _hostSpecificFeatures: HostSpecificFeatures; - // indicate whether test is done or not - private readonly _testDoneCallback?: jest.DoneCallback; - private _markedDone = false; - readonly fs: vfs.TestFileSystem; readonly workspace: WorkspaceServiceInstance; readonly console: ConsoleInterface; - readonly asyncTest: boolean; // The current caret position in the active file currentCaretPosition = 0; @@ -109,7 +104,6 @@ export class TestState { constructor( basePath: string, public testData: FourSlashData, - cb?: jest.DoneCallback, mountPaths?: Map, hostSpecificFeatures?: HostSpecificFeatures ) { @@ -173,9 +167,6 @@ export class TestState { // Open the first file by default this.openFile(this._files[0]); } - - this.asyncTest = toBoolean(testData.globalOptions[GlobalMetadataOptionNames.asynctest]); - this._testDoneCallback = cb; } get importResolver(): ImportResolver { @@ -190,20 +181,6 @@ export class TestState { return this.workspace.serviceInstance.test_program; } - markTestDone(...args: any[]) { - if (this._markedDone) { - // test is already marked done - return; - } - - // call callback to mark the test is done - if (this._testDoneCallback) { - this._testDoneCallback(...args); - } - - this._markedDone = true; - } - // Entry points from fourslash.ts goToMarker(nameOrMarker: string | Marker = '') { const marker = isString(nameOrMarker) ? this.getMarkerByName(nameOrMarker) : nameOrMarker; @@ -501,6 +478,10 @@ export class TestState { rangesPerCategory.set('warning', []); } + if (!rangesPerCategory.has('information')) { + rangesPerCategory.set('information', []); + } + const result = resultPerFile.get(file)!; resultPerFile.delete(file); @@ -511,6 +492,8 @@ export class TestState { ? result.errors : category === 'warning' ? result.warnings + : category === 'information' + ? result.information : this._raiseError(`unexpected category ${category}`); if (expected.length !== actual.length) { @@ -629,8 +612,6 @@ export class TestState { } } - this.markTestDone(); - function convertToString(args: any[] | undefined): string[] | undefined { return args?.map((a) => { if (isString(a)) { @@ -668,8 +649,6 @@ export class TestState { ); } } - - this.markTestDone(); } async verifyInvokeCodeAction( @@ -738,8 +717,6 @@ export class TestState { await this._verifyFiles(map[name].files!); } } - - this.markTestDone(); } verifyHover(map: { [marker: string]: { value: string; kind: string } }): void { @@ -894,8 +871,6 @@ export class TestState { assert.fail('Failed to get completions'); } } - - this.markTestDone(); } verifySignature(map: { @@ -979,6 +954,34 @@ export class TestState { } } + verifyFindDefinitions(map: { + [marker: string]: { + definitions: DocumentRange[]; + }; + }) { + this._analyze(); + + for (const marker of this.getMarkers()) { + const fileName = marker.fileName; + const name = this.getMarkerName(marker); + + if (!(name in map)) { + continue; + } + + const expected = map[name].definitions; + + const position = this._convertOffsetToPosition(fileName, marker.position); + const actual = this.program.getDefinitionsForPosition(fileName, position, CancellationToken.None); + + assert.equal(expected.length, actual?.length ?? 0); + + for (const r of expected) { + assert.equal(actual?.filter((d) => this._deepEqual(d, r)).length, 1); + } + } + } + verifyRename(map: { [marker: string]: { newName: string; @@ -1346,6 +1349,7 @@ export class TestState { parseResults: sourceFile.getParseResults(), errors: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Error), warnings: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Warning), + information: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Information), }; return [filePath, value] as [string, typeof value]; } else { diff --git a/server/tsconfig.json b/server/tsconfig.json index 6ab1e1cfa..bb8dbcd50 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,7 +9,7 @@ "module": "commonjs", "moduleResolution": "node", "sourceMap": true, - "lib" : [ "es2016" ], + "lib" : [ "es2019" ], "outDir": "../client/server", "composite": true, "declaration": true