Add diagnostic for missing imported source file. (#636)

Co-authored-by: Jake Bailey <5341706+jakebailey@users.noreply.github.com>
This commit is contained in:
Hugues Valois 2020-04-21 16:24:53 -07:00 committed by GitHub
parent dfa5dc39d2
commit a97d8f155d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 270 additions and 147 deletions

View File

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

View File

@ -50,6 +50,8 @@ The following settings control pyrights 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ export const enum DiagnosticRule {
reportGeneralTypeIssues = 'reportGeneralTypeIssues',
reportTypeshedErrors = 'reportTypeshedErrors',
reportMissingImports = 'reportMissingImports',
reportMissingModuleSource = 'reportMissingModuleSource',
reportMissingTypeStubs = 'reportMissingTypeStubs',
reportImportCycles = 'reportImportCycles',
reportUnusedImport = 'reportUnusedImport',

View File

@ -12,7 +12,7 @@ import { ModuleNode } from '../parser/parseNodes';
import { ConfigOptions } from './configOptions';
export interface LanguageServiceExtension {
completionListExtension: CompletionListExtension;
readonly completionListExtension: CompletionListExtension;
}
export interface CompletionListExtension {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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