diff --git a/client/schemas/pyrightconfig.schema.json b/client/schemas/pyrightconfig.schema.json index d258b651d..0f1625dbb 100644 --- a/client/schemas/pyrightconfig.schema.json +++ b/client/schemas/pyrightconfig.schema.json @@ -134,6 +134,12 @@ "title": "Controls reporting of imports that cannot be resolved", "default": "error" }, + "reportMissingModuleSource": { + "$id": "#/properties/reportMissingModuleSource", + "$ref": "#/definitions/diagnostic", + "title": "Controls reporting of imports that cannot be resolved to source files", + "default": "none" + }, "reportMissingTypeStubs": { "$id": "#/properties/reportMissingTypeStubs", "$ref": "#/definitions/diagnostic", diff --git a/docs/configuration.md b/docs/configuration.md index 80f454671..f07809472 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -50,6 +50,8 @@ The following settings control pyright’s diagnostic output (warnings or errors **reportMissingImports** [boolean or string, optional]: Generate or suppress diagnostics for imports that have no corresponding imported python file or type stub file. The default value for this setting is 'none', although pyright can do a much better job of static type checking if type stub files are provided for all imports. +**reportMissingModuleSource** [boolean or string, optional]: Generate or suppress diagnostics for imports that have no corresponding source file. This happens when a type stub is found, but the module source file was not found, indicating that the code may fail at runtime when using this execution environment. Type checking will be done using the type stub. The default value for this setting is 'warning'. + **reportMissingTypeStubs** [boolean or string, optional]: Generate or suppress diagnostics for imports that have no corresponding type stub file (either a typeshed file or a custom type stub). The type checker requires type stubs to do its best job at analysis. The default value for this setting is 'none'. Note that there is a corresponding quick fix for this diagnostics that let you generate custom type stub to improve editing experiences. **reportImportCycles** [boolean or string, optional]: Generate or suppress diagnostics for cyclical import chains. These are not errors in Python, but they do slow down type analysis and often hint at architectural layering issues. Generally, they should be avoided. The default value for this setting is 'none'. Note that there are import cycles in the typeshed stdlib typestub files that are ignored by this setting. diff --git a/server/src/analyzer/binder.ts b/server/src/analyzer/binder.ts index 97d683d22..464852496 100644 --- a/server/src/analyzer/binder.ts +++ b/server/src/analyzer/binder.ts @@ -262,8 +262,9 @@ export class Binder extends ParseTreeWalker { `Import "${importResult.importName}" could not be resolved`, node ); - } else if (importResult.importType === ImportType.ThirdParty) { - if (!importResult.isStubFile) { + } else { + // Source found, but type stub is missing + if (!importResult.isStubFile && importResult.importType === ImportType.ThirdParty) { const diagnostic = this._addDiagnostic( this._fileInfo.diagnosticRuleSet.reportMissingTypeStubs, DiagnosticRule.reportMissingTypeStubs, @@ -279,6 +280,21 @@ export class Binder extends ParseTreeWalker { diagnostic.addAction(createTypeStubAction); } } + + // Type stub found, but source is missing + if ( + importResult.isStubFile && + importResult.importType !== ImportType.BuiltIn && + importResult.nonStubImportResult && + !importResult.nonStubImportResult.isImportFound + ) { + this._addDiagnostic( + this._fileInfo.diagnosticRuleSet.reportMissingModuleSource, + DiagnosticRule.reportMissingModuleSource, + `Import "${importResult.importName}" could not be resolved from source`, + node + ); + } } } diff --git a/server/src/analyzer/commentUtils.ts b/server/src/analyzer/commentUtils.ts index c19b20123..bdf7b3828 100644 --- a/server/src/analyzer/commentUtils.ts +++ b/server/src/analyzer/commentUtils.ts @@ -15,6 +15,7 @@ import { getBooleanDiagnosticRules, getDiagLevelDiagnosticRules, getStrictDiagnosticRuleSet, + getStrictModeNotOverridenRules, } from '../common/configOptions'; import { TextRangeCollection } from '../common/textRangeCollection'; import { Token } from '../parser/tokenizerTypes'; @@ -48,15 +49,24 @@ function _applyStrictRules(ruleSet: DiagnosticRuleSet) { const strictRuleSet = getStrictDiagnosticRuleSet(); const boolRuleNames = getBooleanDiagnosticRules(); const diagRuleNames = getDiagLevelDiagnosticRules(); + const skipRuleNames = getStrictModeNotOverridenRules(); // Enable the strict rules as appropriate. for (const ruleName of boolRuleNames) { + if (skipRuleNames.find((r) => r === ruleName)) { + continue; + } + if ((strictRuleSet as any)[ruleName]) { (ruleSet as any)[ruleName] = true; } } for (const ruleName of diagRuleNames) { + if (skipRuleNames.find((r) => r === ruleName)) { + continue; + } + const strictValue: DiagnosticLevel = (strictRuleSet as any)[ruleName]; const prevValue: DiagnosticLevel = (ruleSet as any)[ruleName]; diff --git a/server/src/analyzer/importResolver.ts b/server/src/analyzer/importResolver.ts index 2b46dbc3e..44f17395e 100644 --- a/server/src/analyzer/importResolver.ts +++ b/server/src/analyzer/importResolver.ts @@ -71,6 +71,19 @@ export class ImportResolver { const importName = this._formatImportName(moduleDescriptor); const importFailureInfo: string[] = []; + const notFoundResult: ImportResult = { + importName, + isRelative: false, + isImportFound: false, + importFailureInfo, + resolvedPaths: [], + importType: ImportType.Local, + isStubFile: false, + isPydFile: false, + implicitImports: [], + nonStubImportResult: undefined, + }; + // Is it a relative import? if (moduleDescriptor.leadingDots > 0) { const relativeImport = this._resolveRelativeImport( @@ -91,52 +104,89 @@ export class ImportResolver { return cachedResults; } - // First check for a typeshed file. - if (moduleDescriptor.nameParts.length > 0) { - const builtInImport = this._findTypeshedPath( - execEnv, - moduleDescriptor, - importName, - true, - importFailureInfo - ); - if (builtInImport) { - builtInImport.isTypeshedFile = true; - return this._addResultsToCache( - execEnv, - importName, - builtInImport, - moduleDescriptor.importedSymbols - ); + const bestImport = this._resolveBestAbsoluteImport(sourceFilePath, execEnv, moduleDescriptor, true); + if (bestImport) { + if (bestImport.isStubFile && bestImport.importType !== ImportType.BuiltIn) { + bestImport.nonStubImportResult = + this._resolveBestAbsoluteImport(sourceFilePath, execEnv, moduleDescriptor, false) || + notFoundResult; } + return this._addResultsToCache(execEnv, importName, bestImport, moduleDescriptor.importedSymbols); } + } - let bestResultSoFar: ImportResult | undefined; + return this._addResultsToCache(execEnv, importName, notFoundResult, undefined); + } - // Look for it in the root directory of the execution environment. - importFailureInfo.push(`Looking in root directory of execution environment ` + `'${execEnv.root}'`); - let localImport = this.resolveAbsoluteImport(execEnv.root, moduleDescriptor, importName, importFailureInfo); + private _resolveBestAbsoluteImport( + sourceFilePath: string, + execEnv: ExecutionEnvironment, + moduleDescriptor: ImportedModuleDescriptor, + allowPyi: boolean + ): ImportResult | undefined { + const importName = this._formatImportName(moduleDescriptor); + const importFailureInfo: string[] = []; + + // First check for a typeshed file. + if (allowPyi && moduleDescriptor.nameParts.length > 0) { + const builtInImport = this._findTypeshedPath( + execEnv, + moduleDescriptor, + importName, + true, + importFailureInfo + ); + if (builtInImport) { + builtInImport.isTypeshedFile = true; + return builtInImport; + } + } + + let bestResultSoFar: ImportResult | undefined; + + // Look for it in the root directory of the execution environment. + importFailureInfo.push(`Looking in root directory of execution environment ` + `'${execEnv.root}'`); + let localImport = this.resolveAbsoluteImport( + execEnv.root, + moduleDescriptor, + importName, + importFailureInfo, + undefined, + undefined, + undefined, + allowPyi + ); + if (localImport && localImport.isImportFound) { + return localImport; + } + bestResultSoFar = localImport; + + for (const extraPath of execEnv.extraPaths) { + importFailureInfo.push(`Looking in extraPath '${extraPath}'`); + localImport = this.resolveAbsoluteImport( + extraPath, + moduleDescriptor, + importName, + importFailureInfo, + undefined, + undefined, + undefined, + allowPyi + ); if (localImport && localImport.isImportFound) { - return this._addResultsToCache(execEnv, importName, localImport, moduleDescriptor.importedSymbols); - } - bestResultSoFar = localImport; - - for (const extraPath of execEnv.extraPaths) { - importFailureInfo.push(`Looking in extraPath '${extraPath}'`); - localImport = this.resolveAbsoluteImport(extraPath, moduleDescriptor, importName, importFailureInfo); - if (localImport && localImport.isImportFound) { - return this._addResultsToCache(execEnv, importName, localImport, moduleDescriptor.importedSymbols); - } - - if ( - localImport && - (bestResultSoFar === undefined || - localImport.resolvedPaths.length > bestResultSoFar.resolvedPaths.length) - ) { - bestResultSoFar = localImport; - } + return localImport; } + if ( + localImport && + (bestResultSoFar === undefined || + localImport.resolvedPaths.length > bestResultSoFar.resolvedPaths.length) + ) { + bestResultSoFar = localImport; + } + } + + if (allowPyi) { // Check for a typings file. if (this._configOptions.typingsPath) { importFailureInfo.push(`Looking in typingsPath '${this._configOptions.typingsPath}'`); @@ -150,12 +200,7 @@ export class ImportResolver { // We will treat typings files as "local" rather than "third party". typingsImport.importType = ImportType.Local; typingsImport.isLocalTypingsFile = true; - return this._addResultsToCache( - execEnv, - importName, - typingsImport, - moduleDescriptor.importedSymbols - ); + return typingsImport; } } @@ -170,83 +215,64 @@ export class ImportResolver { ); if (typeshedImport) { typeshedImport.isTypeshedFile = true; - return this._addResultsToCache(execEnv, importName, typeshedImport, moduleDescriptor.importedSymbols); - } - - // Look for the import in the list of third-party packages. - const pythonSearchPaths = this._getPythonSearchPaths(execEnv, importFailureInfo); - if (pythonSearchPaths.length > 0) { - for (const searchPath of pythonSearchPaths) { - // Allow partial resolution because some third-party packages - // use tricks to populate their package namespaces. - importFailureInfo.push(`Looking in python search path '${searchPath}'`); - const thirdPartyImport = this.resolveAbsoluteImport( - searchPath, - moduleDescriptor, - importName, - importFailureInfo, - true, - true, - true - ); - if (thirdPartyImport) { - thirdPartyImport.importType = ImportType.ThirdParty; - - if (thirdPartyImport.isImportFound && thirdPartyImport.isStubFile) { - return this._addResultsToCache( - execEnv, - importName, - thirdPartyImport, - moduleDescriptor.importedSymbols - ); - } - - // We did not find it, or we did and it's not from a - // stub, so give chance for resolveImportEx to find - // one from a stub. - if ( - bestResultSoFar === undefined || - thirdPartyImport.resolvedPaths.length > bestResultSoFar.resolvedPaths.length - ) { - bestResultSoFar = thirdPartyImport; - } - } - } - } else { - importFailureInfo.push('No python interpreter search path'); - } - - const extraResults = this.resolveImportEx( - sourceFilePath, - execEnv, - moduleDescriptor, - importName, - importFailureInfo - ); - if (extraResults !== undefined) { - return this._addResultsToCache(execEnv, importName, extraResults, moduleDescriptor.importedSymbols); - } - - // We weren't able to find an exact match, so return the best - // partial match. - if (bestResultSoFar) { - return this._addResultsToCache(execEnv, importName, bestResultSoFar, moduleDescriptor.importedSymbols); + return typeshedImport; } } - const notFoundResult: ImportResult = { - importName, - isRelative: false, - isImportFound: false, - importFailureInfo, - resolvedPaths: [], - importType: ImportType.Local, - isStubFile: false, - isPydFile: false, - implicitImports: [], - }; + // Look for the import in the list of third-party packages. + const pythonSearchPaths = this._getPythonSearchPaths(execEnv, importFailureInfo); + if (pythonSearchPaths.length > 0) { + for (const searchPath of pythonSearchPaths) { + // Allow partial resolution because some third-party packages + // use tricks to populate their package namespaces. + importFailureInfo.push(`Looking in python search path '${searchPath}'`); + const thirdPartyImport = this.resolveAbsoluteImport( + searchPath, + moduleDescriptor, + importName, + importFailureInfo, + true, + true, + true, + allowPyi + ); + if (thirdPartyImport) { + thirdPartyImport.importType = ImportType.ThirdParty; - return this._addResultsToCache(execEnv, importName, notFoundResult, undefined); + if (thirdPartyImport.isImportFound && thirdPartyImport.isStubFile) { + return thirdPartyImport; + } + + // We did not find it, or we did and it's not from a + // stub, so give chance for resolveImportEx to find + // one from a stub. + if ( + bestResultSoFar === undefined || + thirdPartyImport.resolvedPaths.length > bestResultSoFar.resolvedPaths.length + ) { + bestResultSoFar = thirdPartyImport; + } + } + } + } else { + importFailureInfo.push('No python interpreter search path'); + } + + const extraResults = this.resolveImportEx( + sourceFilePath, + execEnv, + moduleDescriptor, + importName, + importFailureInfo, + allowPyi + ); + if (extraResults !== undefined) { + return extraResults; + } + + // We weren't able to find an exact match, so return the best + // partial match. + return bestResultSoFar; } // Intended to be overridden by subclasses to provide additional stub @@ -257,7 +283,8 @@ export class ImportResolver { execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, importName: string, - importFailureInfo: string[] = [] + importFailureInfo: string[] = [], + allowPyi = true ): ImportResult | undefined { return undefined; } @@ -677,7 +704,8 @@ export class ImportResolver { importFailureInfo: string[], allowPartial = false, allowPydFile = false, - allowStubsFolder = false + allowStubsFolder = false, + allowPyi = true ): ImportResult | undefined { importFailureInfo.push(`Attempting to resolve using root path '${rootPath}'`); @@ -696,7 +724,7 @@ export class ImportResolver { const pyiFilePath = pyFilePath + 'i'; const pydFilePath = pyFilePath + 'd'; - if (this.fileSystem.existsSync(pyiFilePath) && isFile(this.fileSystem, pyiFilePath)) { + if (allowPyi && this.fileSystem.existsSync(pyiFilePath) && isFile(this.fileSystem, pyiFilePath)) { importFailureInfo.push(`Resolved import with file '${pyiFilePath}'`); resolvedPaths.push(pyiFilePath); isStubFile = true; @@ -724,7 +752,7 @@ export class ImportResolver { dirPath = combinePaths(dirPath, moduleDescriptor.nameParts[i]); let foundDirectory = false; - if (allowStubsFolder) { + if (allowPyi && allowStubsFolder) { // PEP 561 indicates that package authors can ship their stubs // separately from their package implementation by appending // the string '-stubs' to its top-level directory name. We'll @@ -754,7 +782,7 @@ export class ImportResolver { const pyiFilePath = pyFilePath + 'i'; let foundInit = false; - if (this.fileSystem.existsSync(pyiFilePath) && isFile(this.fileSystem, pyiFilePath)) { + if (allowPyi && this.fileSystem.existsSync(pyiFilePath) && isFile(this.fileSystem, pyiFilePath)) { importFailureInfo.push(`Resolved import with file '${pyiFilePath}'`); resolvedPaths.push(pyiFilePath); if (isLastPart) { @@ -780,7 +808,7 @@ export class ImportResolver { const pyiFilePath = pyFilePath + 'i'; const pydFilePath = pyFilePath + 'd'; - if (this.fileSystem.existsSync(pyiFilePath) && isFile(this.fileSystem, pyiFilePath)) { + if (allowPyi && this.fileSystem.existsSync(pyiFilePath) && isFile(this.fileSystem, pyiFilePath)) { importFailureInfo.push(`Resolved import with file '${pyiFilePath}'`); resolvedPaths.push(pyiFilePath); if (isLastPart) { diff --git a/server/src/analyzer/importResult.ts b/server/src/analyzer/importResult.ts index d3876245c..57ad64bbb 100644 --- a/server/src/analyzer/importResult.ts +++ b/server/src/analyzer/importResult.ts @@ -65,4 +65,8 @@ export interface ImportResult { // imported as part of the package - used for both traditional and // namespace packages. implicitImports: ImplicitImport[]; + + // If resolved from a type hint (.pyi), then store the import result + // from .py here. + nonStubImportResult?: ImportResult; } diff --git a/server/src/common/configOptions.ts b/server/src/common/configOptions.ts index 82cd968e9..1c897dfad 100644 --- a/server/src/common/configOptions.ts +++ b/server/src/common/configOptions.ts @@ -65,6 +65,9 @@ export interface DiagnosticRuleSet { // Report missing imports? reportMissingImports: DiagnosticLevel; + // Report missing imported module source files? + reportMissingModuleSource: DiagnosticLevel; + // Report missing type stub files? reportMissingTypeStubs: DiagnosticLevel; @@ -190,6 +193,7 @@ export function getDiagLevelDiagnosticRules() { DiagnosticRule.reportGeneralTypeIssues, DiagnosticRule.reportTypeshedErrors, DiagnosticRule.reportMissingImports, + DiagnosticRule.reportMissingModuleSource, DiagnosticRule.reportMissingTypeStubs, DiagnosticRule.reportImportCycles, DiagnosticRule.reportUnusedImport, @@ -225,6 +229,12 @@ export function getDiagLevelDiagnosticRules() { ]; } +export function getStrictModeNotOverridenRules() { + // In strict mode, the value in the user config file should be honored and + // not overwritten by the value from the strict ruleset. + return [DiagnosticRule.reportMissingModuleSource]; +} + export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { const diagSettings: DiagnosticRuleSet = { strictListInference: true, @@ -234,6 +244,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { reportGeneralTypeIssues: 'error', reportTypeshedErrors: 'error', reportMissingImports: 'error', + reportMissingModuleSource: 'warning', reportMissingTypeStubs: 'error', reportImportCycles: 'error', reportUnusedImport: 'error', @@ -280,6 +291,7 @@ export function getNoTypeCheckingDiagnosticRuleSet(): DiagnosticRuleSet { reportGeneralTypeIssues: 'none', reportTypeshedErrors: 'none', reportMissingImports: 'none', + reportMissingModuleSource: 'warning', reportMissingTypeStubs: 'none', reportImportCycles: 'none', reportUnusedImport: 'none', @@ -326,6 +338,7 @@ export function getDefaultDiagnosticRuleSet(): DiagnosticRuleSet { reportGeneralTypeIssues: 'error', reportTypeshedErrors: 'none', reportMissingImports: 'error', + reportMissingModuleSource: 'warning', reportMissingTypeStubs: 'none', reportImportCycles: 'none', reportUnusedImport: 'none', @@ -677,6 +690,13 @@ export class ConfigOptions { defaultSettings.reportDuplicateImport ), + // Read the "reportMissingModuleSource" entry. + reportMissingModuleSource: this._convertDiagnosticLevel( + configObj.reportMissingModuleSource, + DiagnosticRule.reportMissingModuleSource, + defaultSettings.reportMissingModuleSource + ), + // Read the "reportMissingTypeStubs" entry. reportMissingTypeStubs: this._convertDiagnosticLevel( configObj.reportMissingTypeStubs, diff --git a/server/src/common/diagnosticRules.ts b/server/src/common/diagnosticRules.ts index a16d75913..247e56bd0 100644 --- a/server/src/common/diagnosticRules.ts +++ b/server/src/common/diagnosticRules.ts @@ -17,6 +17,7 @@ export const enum DiagnosticRule { reportGeneralTypeIssues = 'reportGeneralTypeIssues', reportTypeshedErrors = 'reportTypeshedErrors', reportMissingImports = 'reportMissingImports', + reportMissingModuleSource = 'reportMissingModuleSource', reportMissingTypeStubs = 'reportMissingTypeStubs', reportImportCycles = 'reportImportCycles', reportUnusedImport = 'reportUnusedImport', diff --git a/server/src/common/extensibility.ts b/server/src/common/extensibility.ts index 03a63e146..6f26dcf2b 100644 --- a/server/src/common/extensibility.ts +++ b/server/src/common/extensibility.ts @@ -12,7 +12,7 @@ import { ModuleNode } from '../parser/parseNodes'; import { ConfigOptions } from './configOptions'; export interface LanguageServiceExtension { - completionListExtension: CompletionListExtension; + readonly completionListExtension: CompletionListExtension; } export interface CompletionListExtension { diff --git a/server/src/tests/checker.test.ts b/server/src/tests/checker.test.ts index c5bb8ce90..5c9467ee3 100644 --- a/server/src/tests/checker.test.ts +++ b/server/src/tests/checker.test.ts @@ -743,13 +743,13 @@ test('SetComprehension1', () => { test('Literals1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['literals1.py']); - validateResults(analysisResults, 6); + validateResults(analysisResults, 6, 1); }); test('Literals2', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['literals2.py']); - validateResults(analysisResults, 3); + validateResults(analysisResults, 3, 1); }); test('TypeAlias1', () => { @@ -1165,7 +1165,7 @@ test('TypedDict6', () => { test('TypedDict7', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDict7.py']); - validateResults(analysisResults, 0); + validateResults(analysisResults, 0, 1); }); test('TypedDict8', () => { diff --git a/server/src/tests/fourslash/missingModuleSource.disablingInStrictMode.fourslash.ts b/server/src/tests/fourslash/missingModuleSource.disablingInStrictMode.fourslash.ts new file mode 100644 index 000000000..dfc17a898 --- /dev/null +++ b/server/src/tests/fourslash/missingModuleSource.disablingInStrictMode.fourslash.ts @@ -0,0 +1,20 @@ +/// + +// @filename: typings/pkg1234/__init__.pyi +//// __version__: str + +// @filename: importnotresolved.py +//// #pyright: strict +//// +//// # verify that reportMissingModuleSource can be disabled via config +//// # even when in strict mode +//// +//// import pkg1234 +//// print(pkg1234.__version__) + +// @filename: pyrightconfig.json +//// { +//// "reportMissingModuleSource": false +//// } + +helper.verifyDiagnostics({}); diff --git a/server/src/tests/fourslash/missingModuleSource.fourslash.ts b/server/src/tests/fourslash/missingModuleSource.fourslash.ts new file mode 100644 index 000000000..0d22eeb6c --- /dev/null +++ b/server/src/tests/fourslash/missingModuleSource.fourslash.ts @@ -0,0 +1,14 @@ +/// + +// @filename: typings/pkg1234/__init__.pyi +//// __version__: str + +// @filename: importnotresolved.py +//// # will not resolve, stub found but source not found +//// +//// import [|/*marker1*/pkg1234|] +//// print(pkg1234.__version__) + +helper.verifyDiagnostics({ + marker1: { category: 'warning', message: 'Import "pkg1234" could not be resolved from source' }, +}); diff --git a/server/src/tests/harness/fourslash/fourSlashTypes.ts b/server/src/tests/harness/fourslash/fourSlashTypes.ts index a3701fe91..3c2d1848a 100644 --- a/server/src/tests/harness/fourslash/fourSlashTypes.ts +++ b/server/src/tests/harness/fourslash/fourSlashTypes.ts @@ -7,9 +7,6 @@ */ import * as debug from '../../../common/debug'; -/** setting file name */ -export const pythonSettingFilename = 'mspythonconfig.json'; - /** well known global option names */ export const enum GlobalMetadataOptionNames { projectRoot = 'projectroot', diff --git a/server/src/tests/harness/fourslash/testState.ts b/server/src/tests/harness/fourslash/testState.ts index 82cad7058..42adb1007 100644 --- a/server/src/tests/harness/fourslash/testState.ts +++ b/server/src/tests/harness/fourslash/testState.ts @@ -13,7 +13,7 @@ import { CancellationToken, Command, CompletionItem, Diagnostic, MarkupContent, import { ImportResolver, ImportResolverFactory } from '../../../analyzer/importResolver'; import { Program } from '../../../analyzer/program'; -import { AnalyzerService } from '../../../analyzer/service'; +import { AnalyzerService, configFileNames } from '../../../analyzer/service'; import { CommandController } from '../../../commands/commandController'; import { ConfigOptions } from '../../../common/configOptions'; import { ConsoleInterface, NullConsole } from '../../../common/console'; @@ -49,7 +49,6 @@ import { Marker, MetadataOptionNames, MultiMap, - pythonSettingFilename, Range, TestCancellationToken, } from './fourSlashTypes'; @@ -443,15 +442,6 @@ export class TestState { return; } - // expected number of files - if (resultPerFile.size !== rangePerFile.size) { - this._raiseError( - `actual and expected doesn't match - expected: ${stringify(resultPerFile)}, actual: ${stringify( - rangePerFile - )}` - ); - } - for (const [file, ranges] of rangePerFile.entries()) { const rangesPerCategory = this._createMultiMap(ranges, (r) => { if (map) { @@ -462,7 +452,17 @@ export class TestState { return (r.marker!.data! as any).category as string; }); + if (!rangesPerCategory.has('error')) { + rangesPerCategory.set('error', []); + } + + if (!rangesPerCategory.has('warning')) { + rangesPerCategory.set('warning', []); + } + const result = resultPerFile.get(file)!; + resultPerFile.delete(file); + for (const [category, expected] of rangesPerCategory.entries()) { const lines = result.parseResults!.tokenizerOutput.lines; const actual = @@ -474,11 +474,11 @@ export class TestState { if (expected.length !== actual.length) { this._raiseError( - `contains unexpected result - expected: ${stringify(expected)}, actual: ${actual}` + `contains unexpected result - expected: ${stringify(expected)}, actual: ${stringify(actual)}` ); } - for (const range of ranges) { + for (const range of expected) { const rangeSpan = TextRange.fromBounds(range.pos, range.end); const matches = actual.filter((d) => { const diagnosticSpan = TextRange.fromBounds( @@ -509,6 +509,10 @@ export class TestState { } } + if (hasDiagnostics(resultPerFile)) { + this._raiseError(`these diagnostics were unexpected: ${stringify(resultPerFile)}`); + } + function hasDiagnostics( resultPerFile: Map< string, @@ -831,7 +835,7 @@ export class TestState { private _isConfig(file: FourSlashFile, ignoreCase: boolean): boolean { const comparer = getStringComparer(ignoreCase); - return comparer(getBaseFileName(file.fileName), pythonSettingFilename) === Comparison.EqualTo; + return configFileNames.some((f) => comparer(getBaseFileName(file.fileName), f) === Comparison.EqualTo); } private _convertGlobalOptionsToConfigOptions(globalOptions: CompilerSettings): ConfigOptions { diff --git a/server/src/tests/testState.test.ts b/server/src/tests/testState.test.ts index fa6902c6d..9aab9309d 100644 --- a/server/src/tests/testState.test.ts +++ b/server/src/tests/testState.test.ts @@ -119,6 +119,7 @@ test('Configuration', () => { assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')))); assert.equal(state.configOptions.diagnosticRuleSet.reportMissingImports, 'error'); + assert.equal(state.configOptions.diagnosticRuleSet.reportMissingModuleSource, 'warning'); assert.equal(state.configOptions.typingsPath, normalizeSlashes('/src/typestubs')); });