Use docstrings from source code, fix references showing in libraries, add information level, client override support (#685)

This commit is contained in:
Jake Bailey 2020-05-22 14:02:54 -07:00 committed by GitHub
parent ac5295933b
commit 0a0cd61f85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1416 additions and 224 deletions

View File

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

View File

@ -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 pyrights 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 pyrights 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'.

View File

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

View File

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

View File

@ -131,9 +131,11 @@ export class BackgroundAnalysisProgram {
async getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise<Diagnostic[]> {
if (this._backgroundAnalysis) {
// backgroundAnalysis returns Promise<Diagnostics[]>
return this._backgroundAnalysis.getDiagnosticsForRange(filePath, range, token);
}
// program returns Promise<Diagnostics[]>
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();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Diagnostic[]> {
getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise<Diagnostic[]> {
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();
}

View File

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

View File

@ -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<T>(element: T | undefined): element is T {
return element !== undefined;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,6 +90,7 @@ function _getCommandLineOptions(
commandLineOptions.autoSearchPaths = serverSettings.autoSearchPaths;
commandLineOptions.extraPaths = serverSettings.extraPaths;
commandLineOptions.diagnosticSeverityOverrides = serverSettings.diagnosticSeverityOverrides;
return commandLineOptions;
}

View File

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

View File

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

View File

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

View File

@ -31,7 +31,9 @@ class PyrightServer extends LanguageServerBase {
async getSettings(workspace: WorkspaceServiceInstance): Promise<ServerSettings> {
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) {

View File

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

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [] } });

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -1,11 +1,11 @@
/// <reference path="fourslash.ts" />
// @asynctest: true
// @filename: test.py
//// a = 42
//// a.n[|/*marker1*/|]
helper.verifyCompletion('excluded', {
// @ts-ignore
await helper.verifyCompletion('excluded', {
marker1: {
completions: [{ label: 'capitalize' }],
},

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: {

View File

@ -1,11 +1,11 @@
/// <reference path="fourslash.ts" />
// @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' }],
},

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -0,0 +1,31 @@
/// <reference path="fourslash.ts" />
// @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',
},
},
],
},
});

View File

@ -0,0 +1,26 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,32 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,33 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,28 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,29 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,29 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,28 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,28 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,22 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,22 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -0,0 +1,23 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

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

View File

@ -0,0 +1,37 @@
/// <reference path="fourslash.ts" />
// @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) };
}),
},
});
}

View File

@ -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<any>;
verifyCommand(command: Command, files: { [filePath: string]: string }): Promise<any>;
verifyInvokeCodeAction(
map: {
[marker: string]: { title: string; files?: { [filePath: string]: string }; edits?: TextEdit[] };
},
verifyCodeActionCount?: boolean
): void;
): Promise<any>;
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;

View File

@ -0,0 +1,110 @@
/// <reference path="fourslash.ts" />
// @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',
},
});

View File

@ -0,0 +1,29 @@
/// <reference path="fourslash.ts" />
// @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',
},
});

View File

@ -0,0 +1,25 @@
/// <reference path="fourslash.ts" />
// @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',
},
});

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: [
{

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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.
"""

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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: {

View File

@ -1,12 +1,12 @@
/// <reference path="fourslash.ts" />
// @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,

View File

@ -1,5 +1,4 @@
/// <reference path="fourslash.ts" />
// @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,

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"lib" : [ "es2016" ],
"lib" : [ "es2019" ],
"outDir": "../client/server",
"composite": true,
"declaration": true