diff --git a/.vscode/settings.json b/.vscode/settings.json index 769c05126e..b939feaa1c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -102,5 +102,7 @@ }, // package.json file copied into the pypi package // this is a top-level folder but the ** is still needed due to https://github.com/microsoft/vscode/issues/92114 - "npm.exclude": "**/basedpyright/*" + "npm.exclude": "**/basedpyright/*", + "git.blame.editorDecoration.enabled": true, + "git.blame.statusBarItem.enabled": true } diff --git a/packages/pyright-internal/src/analyzer/analysis.ts b/packages/pyright-internal/src/analyzer/analysis.ts index c551586e56..ce99e64b21 100644 --- a/packages/pyright-internal/src/analyzer/analysis.ts +++ b/packages/pyright-internal/src/analyzer/analysis.ts @@ -26,7 +26,6 @@ export interface AnalysisResults { checkingOnlyOpenFiles: boolean; requiringAnalysisCount: RequiringAnalysisCount; fatalErrorOccurred: boolean; - configParseErrors: string[]; elapsedTime: number; error?: Error | undefined; reason: 'analysis' | 'tracking'; @@ -75,7 +74,6 @@ export function analyzeProgram( requiringAnalysisCount: requiringAnalysisCount, checkingOnlyOpenFiles: program.isCheckingOnlyOpenFiles(), fatalErrorOccurred: false, - configParseErrors: [], elapsedTime, reason: 'analysis', }); @@ -94,7 +92,6 @@ export function analyzeProgram( requiringAnalysisCount: { files: 0, cells: 0 }, checkingOnlyOpenFiles: true, fatalErrorOccurred: true, - configParseErrors: [], elapsedTime: 0, error: debug.getSerializableError(e), reason: 'analysis', diff --git a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts index fd812036bb..0660814561 100644 --- a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts +++ b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts @@ -273,7 +273,6 @@ export class BackgroundAnalysisProgram { requiringAnalysisCount: this._program.getFilesToAnalyzeCount(), checkingOnlyOpenFiles: this._program.isCheckingOnlyOpenFiles(), fatalErrorOccurred: false, - configParseErrors: [], elapsedTime: 0, reason: 'tracking', }); diff --git a/packages/pyright-internal/src/analyzer/program.ts b/packages/pyright-internal/src/analyzer/program.ts index b25ff3c225..5495996599 100644 --- a/packages/pyright-internal/src/analyzer/program.ts +++ b/packages/pyright-internal/src/analyzer/program.ts @@ -51,6 +51,7 @@ import { createTypeEvaluatorWithTracker } from './typeEvaluatorWithTracker'; import { PrintTypeFlags } from './typePrinter'; import { TypeStubWriter } from './typeStubWriter'; import { Type } from './types'; +import { BaselineHandler } from '../baseline'; const _maxImportDepth = 256; @@ -137,6 +138,8 @@ export class Program { private _editModeTracker = new EditModeTracker(); private _sourceFileFactory: ISourceFileFactory; + baselineHandler: BaselineHandler; + constructor( initialImportResolver: ImportResolver, initialConfigOptions: ConfigOptions, @@ -151,6 +154,7 @@ export class Program { this._configOptions = initialConfigOptions; this._sourceFileFactory = serviceProvider.sourceFileFactory(); + this.baselineHandler = new BaselineHandler(this.fileSystem, this._configOptions, this._console); this._cacheManager = serviceProvider.tryGet(ServiceKeys.cacheManager) ?? new CacheManager(); this._cacheManager.registerCacheOwner(this); @@ -257,6 +261,7 @@ export class Program { setConfigOptions(configOptions: ConfigOptions) { this._configOptions = configOptions; this._importResolver.setConfigOptions(configOptions); + this.baselineHandler.configOptions = configOptions; // Create a new evaluator with the updated config options. this._createNewEvaluator(); @@ -351,6 +356,7 @@ export class Program { importName, isThirdPartyImport, isInPyTypedPackage, + this.baselineHandler, this._editModeTracker, this._console, this._logTracker @@ -379,6 +385,7 @@ export class Program { moduleImportInfo.moduleName, /* isThirdPartyImport */ false, moduleImportInfo.isThirdPartyPyTypedPresent, + this.baselineHandler, this._editModeTracker, this._console, this._logTracker, @@ -623,6 +630,7 @@ export class Program { // to the smaller value to maintain responsiveness. analyze(maxTime?: MaxAnalysisTime, token: CancellationToken = CancellationToken.None): boolean { return this._runEvaluatorWithCancellationToken(token, () => { + this.baselineHandler.invalidateCache(); const elapsedTime = new Duration(); const openFiles = this._sourceFileList.filter( @@ -1477,6 +1485,7 @@ export class Program { moduleImportInfo.moduleName, importInfo.isThirdPartyImport, importInfo.isPyTypedPresent, + this.baselineHandler, this._editModeTracker, this._console, this._logTracker @@ -1619,6 +1628,7 @@ export class Program { moduleImportInfo.moduleName, /* isThirdPartyImport */ false, /* isInPyTypedPackage */ false, + this.baselineHandler, this._editModeTracker, this._console, this._logTracker diff --git a/packages/pyright-internal/src/analyzer/programTypes.ts b/packages/pyright-internal/src/analyzer/programTypes.ts index 7775bdc690..f6613f194e 100644 --- a/packages/pyright-internal/src/analyzer/programTypes.ts +++ b/packages/pyright-internal/src/analyzer/programTypes.ts @@ -5,6 +5,7 @@ * * Various interfaces/types used in */ +import { BaselineHandler } from '../baseline'; import { ConsoleInterface } from '../common/console'; import { LogTracker } from '../common/logTracker'; import { ServiceProvider } from '../common/serviceProvider'; @@ -18,6 +19,7 @@ export interface ISourceFileFactory { moduleName: string, isThirdPartyImport: boolean, isThirdPartyPyTypedPresent: boolean, + baselineHandler: BaselineHandler, editMode: SourceFileEditMode, console?: ConsoleInterface, logTracker?: LogTracker, diff --git a/packages/pyright-internal/src/analyzer/service.ts b/packages/pyright-internal/src/analyzer/service.ts index b27c9171a3..6a1a6a47ec 100644 --- a/packages/pyright-internal/src/analyzer/service.ts +++ b/packages/pyright-internal/src/analyzer/service.ts @@ -19,7 +19,7 @@ import { CommandLineLanguageServerOptions, CommandLineOptions, } from '../common/commandLineOptions'; -import { BasedConfigOptions, ConfigErrors, ConfigOptions, matchFileSpecs } from '../common/configOptions'; +import { BasedConfigOptions, ConfigOptions, matchFileSpecs } from '../common/configOptions'; import { ConsoleInterface, LogLevel, NoErrorConsole, StandardConsole, log } from '../common/console'; import { isString } from '../common/core'; import { Diagnostic } from '../common/diagnostic'; @@ -480,6 +480,7 @@ export class AnalyzerService { } baselineUpdated = () => { + this.backgroundAnalysisProgram.program.baselineHandler.invalidateCache(); this.invalidateAndForceReanalysis(InvalidatedReason.BaselineFileUpdated); this._scheduleReanalysis(false); }; @@ -536,7 +537,7 @@ export class AnalyzerService { this._scheduleReanalysis(/* requireTrackedFileUpdate */ false); } - private get _console(): NoErrorConsole { + private get _console(): ConsoleInterface { return this.options.console!; } @@ -660,31 +661,18 @@ export class AnalyzerService { const configOptions = new BasedConfigOptions(projectRoot); - const errors: string[] = []; - // If we found a config file, load it and apply its settings. - let configs; - try { - configs = this._getExtendedConfigurations(configFilePath ?? pyprojectFilePath); - } catch (e) { - if (e instanceof ConfigErrors) { - errors.push(...e.errors); - } else { - throw e; - } - } + const configs = this._getExtendedConfigurations(configFilePath ?? pyprojectFilePath); configOptions.initializeTypeCheckingMode('recommended'); if (configs && configs.length > 0) { // Then we apply the config file settings. This can update the // the typeCheckingMode. for (const config of configs) { - errors.push( - ...configOptions.initializeFromJson( - config.configFileJsonObj, - config.configFileDirUri, - this.serviceProvider, - host - ) + configOptions.initializeFromJson( + config.configFileJsonObj, + config.configFileDirUri, + this.serviceProvider, + host ); } @@ -693,24 +681,15 @@ export class AnalyzerService { // When not in language server mode, command line options override config file options. if (!commandLineOptions.fromLanguageServer) { - errors.push( - ...this._applyCommandLineOverrides( - configOptions, - commandLineOptions.configSettings, - projectRoot, - false - ) - ); + this._applyCommandLineOverrides(configOptions, commandLineOptions.configSettings, projectRoot, false); } } else { // If there are no config files, we can then directly apply the command line options. - errors.push( - ...this._applyCommandLineOverrides( - configOptions, - commandLineOptions.configSettings, - projectRoot, - commandLineOptions.fromLanguageServer - ) + this._applyCommandLineOverrides( + configOptions, + commandLineOptions.configSettings, + projectRoot, + commandLineOptions.fromLanguageServer ); } @@ -719,22 +698,21 @@ export class AnalyzerService { this._applyLanguageServerOptions(configOptions, projectRoot, commandLineOptions.languageServerSettings); // Ensure that if no command line or config options were applied, we have some defaults. - errors.push(...this._ensureDefaultOptions(host, configOptions, projectRoot, executionRoot, commandLineOptions)); + this._ensureDefaultOptions(host, configOptions, projectRoot, executionRoot, commandLineOptions); // Once we have defaults, we can then setup the execution environments. Execution environments // inherit from the defaults. if (configs) { for (const config of configs) { - errors.push( - ...configOptions.setupExecutionEnvironments(config.configFileJsonObj, config.configFileDirUri) + configOptions.setupExecutionEnvironments( + config.configFileJsonObj, + config.configFileDirUri, + // TODO: will this ever be a different console to this._console? + this.serviceProvider.console() ); } } - if (errors.length > 0) { - this._reportConfigParseError(errors); - } - return configOptions; } @@ -744,8 +722,7 @@ export class AnalyzerService { projectRoot: Uri, executionRoot: Uri, commandLineOptions: CommandLineOptions - ): string[] { - const errors = []; + ) { const defaultExcludes = ['**/node_modules', '**/__pycache__', '**/.*']; // If no include paths were provided, assume that all files within @@ -811,7 +788,7 @@ export class AnalyzerService { if (configOptions.stubPath) { // If there was a stub path specified, validate it. if (!this.fs.existsSync(configOptions.stubPath) || !isDirectory(this.fs, configOptions.stubPath)) { - errors.push(`stubPath ${configOptions.stubPath} is not a valid directory.`); + this._console.error(`stubPath ${configOptions.stubPath} is not a valid directory.`); } } else { // If no stub path was specified, use a default path. @@ -822,7 +799,9 @@ export class AnalyzerService { // or inconsistent information. if (configOptions.venvPath) { if (!this.fs.existsSync(configOptions.venvPath) || !isDirectory(this.fs, configOptions.venvPath)) { - errors.push(`venvPath ${configOptions.venvPath.toUserVisibleString()} is not a valid directory.`); + this._console.error( + `venvPath ${configOptions.venvPath.toUserVisibleString()} is not a valid directory.` + ); } // venvPath without venv means it won't do anything while resolveImport. @@ -833,7 +812,7 @@ export class AnalyzerService { const fullVenvPath = configOptions.venvPath.resolvePaths(configOptions.venv); if (!this.fs.existsSync(fullVenvPath) || !isDirectory(this.fs, fullVenvPath)) { - errors.push( + this._console.error( `venv ${ configOptions.venv } subdirectory not found in venv path ${configOptions.venvPath.toUserVisibleString()}.` @@ -841,14 +820,14 @@ export class AnalyzerService { } else { const importFailureInfo: string[] = []; if (findPythonSearchPaths(this.fs, configOptions, host, importFailureInfo) === undefined) { - errors.push( + this._console.error( `site-packages directory cannot be located for venvPath ` + `${configOptions.venvPath.toUserVisibleString()} and venv ${configOptions.venv}.` ); if (configOptions.verboseOutput) { importFailureInfo.forEach((diag) => { - errors.push(` ${diag}`); + this._console.error(` ${diag}`); }); } } @@ -865,7 +844,7 @@ export class AnalyzerService { if (configOptions.typeshedPath) { if (!this.fs.existsSync(configOptions.typeshedPath) || !isDirectory(this.fs, configOptions.typeshedPath)) { - errors.push( + this._console.error( `typeshedPath ${configOptions.typeshedPath.toUserVisibleString()} is not a valid directory.` ); } @@ -883,7 +862,6 @@ export class AnalyzerService { configOptions.ensureDefaultPythonVersion(host, this._console); } configOptions.ensureDefaultPythonPlatform(host, this._console); - return errors; } private _applyLanguageServerOptions( @@ -930,8 +908,8 @@ export class AnalyzerService { commandLineOptions: CommandLineConfigOptions, projectRoot: Uri, fromLanguageServer: boolean - ): string[] { - const errors = configOptions.initializeTypeCheckingModeFromString(commandLineOptions.typeCheckingMode); + ) { + configOptions.initializeTypeCheckingModeFromString(commandLineOptions.typeCheckingMode, this._console); if (commandLineOptions.extraPaths) { configOptions.ensureDefaultExtraPaths( @@ -1035,7 +1013,6 @@ export class AnalyzerService { reportDuplicateSetting('stubPath', configOptions.stubPath.toUserVisibleString()); } } - return errors; } // Loads the config JSON object from the specified config file along with any @@ -1051,7 +1028,6 @@ export class AnalyzerService { let curConfigFileUri = primaryConfigFileUri; const configJsonObjs: ConfigFileContents[] = []; - const errors = []; while (true) { this._extendedConfigFileUris.push(curConfigFileUri); @@ -1073,23 +1049,18 @@ export class AnalyzerService { // Push onto the start of the array so base configs are processed first. configJsonObjs.unshift({ configFileJsonObj, configFileDirUri: curConfigFileUri.getDirectory() }); - let baseConfigUri; - try { - baseConfigUri = ConfigOptions.resolveExtends(configFileJsonObj, curConfigFileUri.getDirectory()); - } catch (e) { - if (e instanceof ConfigErrors) { - errors.push(...e.errors); - } else { - throw e; - } - } + const baseConfigUri = ConfigOptions.resolveExtends( + configFileJsonObj, + curConfigFileUri.getDirectory(), + this._console + ); if (!baseConfigUri) { break; } // Check for circular references. if (this._extendedConfigFileUris.some((uri) => uri.equals(baseConfigUri))) { - errors.push( + this._console.error( `Circular reference in configuration file "extends" setting: ${curConfigFileUri.toUserVisibleString()} ` + `extends ${baseConfigUri.toUserVisibleString()}` ); @@ -1098,9 +1069,6 @@ export class AnalyzerService { curConfigFileUri = baseConfigUri; } - if (errors.length) { - throw new ConfigErrors(errors); - } return configJsonObjs; } @@ -1209,8 +1177,7 @@ export class AnalyzerService { try { fileContents = this.fs.readFileSync(fileUri, 'utf8'); } catch { - const error = `Config file "${fileUri.toUserVisibleString()}" could not be read.`; - this._reportConfigParseError([error]); + this._console.error(`Config file "${fileUri.toUserVisibleString()}" could not be read.`); return undefined; } @@ -1230,8 +1197,9 @@ export class AnalyzerService { // may have been partially written when we read it, resulting in parse // errors. We'll give it a little more time and try again. if (parseAttemptCount++ >= 5) { - const error = `Config file "${fileUri.toUserVisibleString()}" could not be parsed. Verify that format is correct.`; - this._reportConfigParseError([error]); + this._console.error( + `Config file "${fileUri.toUserVisibleString()}" could not be parsed. Verify that format is correct.` + ); return undefined; } } @@ -1950,19 +1918,4 @@ export class AnalyzerService { this.runAnalysis(this._backgroundAnalysisCancellationSource.token); }, timeUntilNextAnalysisInMs); } - - private _reportConfigParseError(errors: string[]) { - if (this._onCompletionCallback) { - this._onCompletionCallback({ - diagnostics: [], - filesInProgram: 0, - requiringAnalysisCount: { files: 0, cells: 0 }, - checkingOnlyOpenFiles: true, - fatalErrorOccurred: false, - configParseErrors: errors, - elapsedTime: 0, - reason: 'analysis', - }); - } - } } diff --git a/packages/pyright-internal/src/analyzer/sourceFile.ts b/packages/pyright-internal/src/analyzer/sourceFile.ts index 7e687130b5..8708981bd2 100644 --- a/packages/pyright-internal/src/analyzer/sourceFile.ts +++ b/packages/pyright-internal/src/analyzer/sourceFile.ts @@ -261,6 +261,7 @@ export class SourceFile { isThirdPartyImport: boolean, isThirdPartyPyTypedPresent: boolean, editMode: SourceFileEditMode, + private _baselineHandler: BaselineHandler, console?: ConsoleInterface, logTracker?: LogTracker, ipythonMode?: IPythonMode @@ -1247,10 +1248,7 @@ export class SourceFile { // Now add in the "unnecessary type ignore" diagnostics. diagList = diagList.concat(unnecessaryTypeIgnoreDiags); - diagList = new BaselineHandler(this.fileSystem, configOptions.projectRoot).sortDiagnosticsAndMatchBaseline( - this._uri, - diagList - ); + diagList = this._baselineHandler.sortDiagnosticsAndMatchBaseline(this._uri, diagList); // If we're not returning any diagnostics, filter out all of // the errors and warnings, leaving only the unreachable code diff --git a/packages/pyright-internal/src/backgroundAnalysisBase.ts b/packages/pyright-internal/src/backgroundAnalysisBase.ts index 83085c9560..d56211900e 100644 --- a/packages/pyright-internal/src/backgroundAnalysisBase.ts +++ b/packages/pyright-internal/src/backgroundAnalysisBase.ts @@ -560,7 +560,6 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase requiringAnalysisCount: requiringAnalysisCount, checkingOnlyOpenFiles: this.program.isCheckingOnlyOpenFiles(), fatalErrorOccurred: false, - configParseErrors: [], elapsedTime: 0, reason: 'analysis', }); @@ -763,7 +762,6 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase requiringAnalysisCount: requiringAnalysisCount, checkingOnlyOpenFiles: this.program.isCheckingOnlyOpenFiles(), fatalErrorOccurred: false, - configParseErrors: [], elapsedTime, reason: 'tracking', }); diff --git a/packages/pyright-internal/src/baseline.ts b/packages/pyright-internal/src/baseline.ts index 3fdb812185..689ed1e61e 100644 --- a/packages/pyright-internal/src/baseline.ts +++ b/packages/pyright-internal/src/baseline.ts @@ -9,6 +9,8 @@ import { diffArrays } from 'diff'; import { assert } from './common/debug'; import { Range } from './common/textRange'; import { add } from 'lodash'; +import { ConsoleInterface, StandardConsole } from './common/console'; +import { ConfigOptions } from './common/configOptions'; export interface BaselinedDiagnostic { code: DiagnosticRule | undefined; @@ -77,26 +79,32 @@ class BaselineDiff { } export class BaselineHandler { - readonly fileUri: Uri; - /** - * when {@link baselineData} is `undefined` that means there is currently no baseline file, in which case - * none of this functionality should be observed by the user until they explicitly opt in to the baseline - * feature + * project root can change and we need to invalidate the cache when that happens */ - constructor(private _fs: FileSystem, private _rootDir: Uri) { - this.fileUri = baselineFilePath(_rootDir); + private _cache?: { content: BaselineData | undefined; projectRoot: Uri }; + private _console: ConsoleInterface; + + constructor(private _fs: FileSystem, public configOptions: ConfigOptions, console: ConsoleInterface | undefined) { + this._console = console ?? new StandardConsole(); + } + + get fileUri() { + return baselineFilePath(this.configOptions.projectRoot); } getContents = (): BaselineData | undefined => { - let baselineFileContents: string | undefined; - try { - baselineFileContents = this._fs.readFileSync(this.fileUri, 'utf8'); - } catch (e) { - // assume the file didn't exist - return undefined; + if (!this._cache || this._cache.projectRoot !== this.configOptions.projectRoot) { + const result = this._getContents(); + this._setCache(result); + return result; + } else { + return this._cache.content; } - return JSON.parse(baselineFileContents); + }; + + invalidateCache = () => { + this._cache = undefined; }; /** @@ -113,13 +121,12 @@ export class BaselineHandler { force: T, removeDeletedFiles: boolean, filesWithDiagnostics: readonly FileDiagnostics[] - ): OptionalIfFalse> => { - type Result = OptionalIfFalse>; + ): BaselineDiff | undefined => { const baselineData = this.getContents(); if (!force) { if (!baselineData) { // there currently is no baseline file and the user did not explicitly ask for one, so we do nothing - return undefined as Result; + return undefined; } /** diagnostics that haven't yet been baselined */ const newDiagnostics = filesWithDiagnostics.map((file) => ({ @@ -131,7 +138,7 @@ export class BaselineHandler { if (newDiagnostics.map((fileWithDiagnostics) => fileWithDiagnostics.diagnostics.length).reduce(add, 0)) { // there are unbaselined diagnostics and the user did not explicitly ask to update the baseline, so we do // nothing - return undefined as Result; + return undefined; } } const newBaselineFiles = this._filteredDiagnosticsToBaselineFormat(filesWithDiagnostics).files; @@ -144,7 +151,7 @@ export class BaselineHandler { for (const filePath in previousBaselineFiles) { if ( !newBaselineFiles[filePath] && - (!removeDeletedFiles || fileExists(this._fs, this._rootDir.combinePaths(filePath))) + (!removeDeletedFiles || fileExists(this._fs, this.configOptions.projectRoot.combinePaths(filePath))) ) { newBaselineFiles[filePath] = previousBaselineFiles[filePath]; } @@ -159,8 +166,14 @@ export class BaselineHandler { } } this._fs.mkdirSync(this.fileUri.getDirectory(), { recursive: true }); - this._fs.writeFileSync(this.fileUri, JSON.stringify(result, undefined, 4), null); - return new BaselineDiff(this._rootDir, { files: previousBaselineFiles }, result, force); + try { + this._fs.writeFileSync(this.fileUri, JSON.stringify(result, undefined, 4), null); + } catch (e) { + this._console.error(`failed to write baseline file - ${e}`); + return undefined; + } + this._setCache(result); + return new BaselineDiff(this.configOptions.projectRoot, { files: previousBaselineFiles }, result, force); }; sortDiagnosticsAndMatchBaseline = (moduleUri: Uri, diagnostics: Diagnostic[]): Diagnostic[] => { @@ -225,8 +238,28 @@ export class BaselineHandler { diagnostics: file.diagnostics.filter((diagnostic) => !diagnostic.baselined), })); + private _getContents = (): BaselineData | undefined => { + let baselineFileContents: string | undefined; + try { + baselineFileContents = this._fs.readFileSync(this.fileUri, 'utf8'); + } catch (e) { + // assume the file didn't exist + return undefined; + } + try { + return JSON.parse(baselineFileContents); + } catch (e) { + this._console.error(`failed to parse baseline file - ${e}`); + return undefined; + } + }; + + private _setCache = (content: BaselineData | undefined) => { + this._cache = { projectRoot: this.configOptions.projectRoot, content }; + }; + private _getBaselinedErrorsForFile = (file: Uri): BaselinedDiagnostic[] => { - const relativePath = this._rootDir.getRelativePath(file); + const relativePath = this.configOptions.projectRoot.getRelativePath(file); let result; // if this is undefined it means the file isn't in the workspace if (relativePath) { @@ -240,7 +273,7 @@ export class BaselineHandler { files: {}, }; for (const fileWithDiagnostics of filesWithDiagnostics) { - const filePath = this._rootDir.getRelativePath(fileWithDiagnostics.fileUri)!.toString(); + const filePath = this.configOptions.projectRoot.getRelativePath(fileWithDiagnostics.fileUri)!.toString(); const errorDiagnostics = fileWithDiagnostics.diagnostics.filter( (diagnostic) => diagnostic.category !== DiagnosticCategory.Hint || diagnostic.baselined ); diff --git a/packages/pyright-internal/src/commands/writeBaseline.ts b/packages/pyright-internal/src/commands/writeBaseline.ts index 16cf40a95e..50a228bd69 100644 --- a/packages/pyright-internal/src/commands/writeBaseline.ts +++ b/packages/pyright-internal/src/commands/writeBaseline.ts @@ -31,7 +31,11 @@ export class WriteBaselineCommand implements ServerCommand { if (workspace) { const workspaceRoot = workspace.rootUri; if (workspaceRoot) { - const baselineHandler = new BaselineHandler(workspace.service.fs, workspaceRoot); + const baselineHandler = new BaselineHandler( + workspace.service.fs, + workspace.service.getConfigOptions(), + this._ls.console + ); const configOptions = workspace.service.getConfigOptions(); // filter out excluded files. ideally they shouldn't be present at all. see // https://github.com/DetachHead/basedpyright/issues/31 @@ -46,7 +50,9 @@ export class WriteBaselineCommand implements ServerCommand { .map(([_, diagnostics]) => diagnostics); const newBaseline = baselineHandler.write(true, true, filteredFiles); workspace.service.baselineUpdated(); - this._ls.window.showInformationMessage(newBaseline.getSummaryMessage()); + if (newBaseline) { + this._ls.window.showInformationMessage(newBaseline.getSummaryMessage()); + } return; } } diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index 9b755a537e..fb7795579a 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -16,7 +16,7 @@ import { DiagnosticSeverityOverridesMap, getDiagnosticSeverityOverrides, } from './commandLineOptions'; -import { NoErrorConsole, NullConsole } from './console'; +import { ConsoleInterface, NoErrorConsole, NullConsole } from './console'; import { isBoolean } from './core'; import { TaskListToken } from './diagnostic'; import { DiagnosticRule } from './diagnosticRules'; @@ -1278,12 +1278,6 @@ export function matchFileSpecs(configOptions: ConfigOptions, uri: Uri, isFile = return false; } -export class ConfigErrors extends Error { - constructor(public errors: string[]) { - super(errors.join('\n')); - } -} - /** * keep track of which options have been parsed so we can raise an error on unknown options later. * this is pretty cringe and we should just replace all this with some sort of schema validator thingy @@ -1526,26 +1520,24 @@ export class ConfigOptions { * * @returns any errors that occurred */ - initializeTypeCheckingModeFromString(typeCheckingMode: string | undefined): string[] { + initializeTypeCheckingModeFromString(typeCheckingMode: string | undefined, console_: ConsoleInterface) { if (typeCheckingMode !== undefined) { if ((allTypeCheckingModes as readonly string[]).includes(typeCheckingMode)) { this.initializeTypeCheckingMode(typeCheckingMode as TypeCheckingMode); } else { - return [ + console_.error( `invalid "typeCheckingMode" value: "${typeCheckingMode}". expected: ${userFacingOptionsList( allTypeCheckingModes - )}`, - ]; + )}` + ); } } - return []; } // Initialize the structure from a JSON object. - initializeFromJson(configObj: any, configDirUri: Uri, serviceProvider: ServiceProvider, host: Host): string[] { + initializeFromJson(configObj: any, configDirUri: Uri, serviceProvider: ServiceProvider, host: Host) { this.initializedFromJson = true; const console = serviceProvider.tryGet(ServiceKeys.console) ?? new NullConsole(); - const errors: string[] = []; // we initialize it with `extends` because this option gets read before this function gets called // 'executionEnvironments' also gets read elsewhere @@ -1560,12 +1552,12 @@ export class ConfigOptions { const configValue = configObj[key]; if (configValue !== undefined) { if (!Array.isArray(configValue)) { - errors.push(`Config "${key}" entry must contain an array.`); + console.error(`Config "${key}" entry must contain an array.`); } else { this[key] = []; (configValue as unknown[]).forEach((fileSpec, index) => { if (typeof fileSpec !== 'string') { - errors.push(`Index ${index} of "${key}" array should be a string.`); + console.error(`Index ${index} of "${key}" array should be a string.`); } else { // We'll allow absolute paths. While it // is not recommended to use absolute paths anywhere in @@ -1579,13 +1571,13 @@ export class ConfigOptions { } // If there is a "typeCheckingMode", it can override the provided setting. - errors.push(...this.initializeTypeCheckingModeFromString(configObj.typeCheckingMode)); + this.initializeTypeCheckingModeFromString(configObj.typeCheckingMode, console); if (configObj.useLibraryCodeForTypes !== undefined) { if (typeof configObj.useLibraryCodeForTypes === 'boolean') { this.useLibraryCodeForTypes = configObj.useLibraryCodeForTypes; } else { - errors.push(`Config "useLibraryCodeForTypes" entry must be true or false.`); + console.error(`Config "useLibraryCodeForTypes" entry must be true or false.`); } } @@ -1606,7 +1598,7 @@ export class ConfigOptions { configObj[ruleName], ruleName, configRuleSet[ruleName] as DiagnosticLevel, - errors + console ); }); this.diagnosticRuleSet = { ...configRuleSet }; @@ -1614,7 +1606,7 @@ export class ConfigOptions { // Read the "venvPath". if (configObj.venvPath !== undefined) { if (typeof configObj.venvPath !== 'string') { - errors.push(`Config "venvPath" field must contain a string.`); + console.error(`Config "venvPath" field must contain a string.`); } else { this.venvPath = configDirUri.resolvePaths(configObj.venvPath); } @@ -1623,7 +1615,7 @@ export class ConfigOptions { // Read the "venv" name. if (configObj.venv !== undefined) { if (typeof configObj.venv !== 'string') { - errors.push(`Config "venv" field must contain a string.`); + console.error(`Config "venv" field must contain a string.`); } else { this.venv = configObj.venv; } @@ -1633,12 +1625,12 @@ export class ConfigOptions { const configExtraPaths: Uri[] = []; if (configObj.extraPaths !== undefined) { if (!Array.isArray(configObj.extraPaths)) { - errors.push(`Config "extraPaths" field must contain an array.`); + console.error(`Config "extraPaths" field must contain an array.`); } else { const pathList = configObj.extraPaths as string[]; pathList.forEach((path, pathIndex) => { if (typeof path !== 'string') { - errors.push(`Config "extraPaths" field ${pathIndex} must be a string.`); + console.error(`Config "extraPaths" field ${pathIndex} must be a string.`); } else { configExtraPaths!.push(configDirUri.resolvePaths(path)); } @@ -1654,17 +1646,17 @@ export class ConfigOptions { if (version) { this.defaultPythonVersion = version; } else { - errors.push(`Config "pythonVersion" field contains unsupported version.`); + console.error(`Config "pythonVersion" field contains unsupported version.`); } } else { - errors.push(`Config "pythonVersion" field must contain a string.`); + console.error(`Config "pythonVersion" field must contain a string.`); } } // Read the default "pythonPlatform". if (configObj.pythonPlatform !== undefined) { if (typeof configObj.pythonPlatform !== 'string') { - errors.push(`Config "pythonPlatform" field must contain a string.`); + console.error(`Config "pythonPlatform" field must contain a string.`); } else if (!['Linux', 'Windows', 'Darwin', 'All'].includes(configObj.pythonPlatform)) { `'${configObj.pythonPlatform}' is not a supported Python platform; specify All, Darwin, Linux, or Windows.`; } else { @@ -1687,7 +1679,7 @@ export class ConfigOptions { // Read the "typeshedPath" setting. if (configObj.typeshedPath !== undefined) { if (typeof configObj.typeshedPath !== 'string') { - errors.push(`Config "typeshedPath" field must contain a string.`); + console.error(`Config "typeshedPath" field must contain a string.`); } else { this.typeshedPath = configObj.typeshedPath ? configDirUri.resolvePaths(configObj.typeshedPath) @@ -1700,16 +1692,16 @@ export class ConfigOptions { // Keep this for backward compatibility if (configObj.typingsPath !== undefined) { if (typeof configObj.typingsPath !== 'string') { - errors.push(`Config "typingsPath" field must contain a string.`); + console.error(`Config "typingsPath" field must contain a string.`); } else { - errors.push(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`); + console.error(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`); this.stubPath = configDirUri.resolvePaths(configObj.typingsPath); } } if (configObj.stubPath !== undefined) { if (typeof configObj.stubPath !== 'string') { - errors.push(`Config "stubPath" field must contain a string.`); + console.error(`Config "stubPath" field must contain a string.`); } else { this.stubPath = configDirUri.resolvePaths(configObj.stubPath); } @@ -1720,7 +1712,7 @@ export class ConfigOptions { // switch to apply if this setting isn't specified in the config file. if (configObj.verboseOutput !== undefined) { if (typeof configObj.verboseOutput !== 'boolean') { - errors.push(`Config "verboseOutput" field must be true or false.`); + console.error(`Config "verboseOutput" field must be true or false.`); } else { this.verboseOutput = configObj.verboseOutput; } @@ -1729,14 +1721,14 @@ export class ConfigOptions { // Read the "defineConstant" setting. if (configObj.defineConstant !== undefined) { if (typeof configObj.defineConstant !== 'object' || Array.isArray(configObj.defineConstant)) { - errors.push(`Config "defineConstant" field must contain a map indexed by constant names.`); + console.error(`Config "defineConstant" field must contain a map indexed by constant names.`); } else { const keys = Object.getOwnPropertyNames(configObj.defineConstant); keys.forEach((key) => { const value = configObj.defineConstant[key]; const valueType = typeof value; if (valueType !== 'boolean' && valueType !== 'string') { - errors.push(`Defined constant "${key}" must be associated with a boolean or string value.`); + console.error(`Defined constant "${key}" must be associated with a boolean or string value.`); } else { this.defineConstant.set(key, value); } @@ -1747,7 +1739,7 @@ export class ConfigOptions { // Read the "useLibraryCodeForTypes" setting. if (configObj.useLibraryCodeForTypes !== undefined) { if (typeof configObj.useLibraryCodeForTypes !== 'boolean') { - errors.push(`Config "useLibraryCodeForTypes" field must be true or false.`); + console.error(`Config "useLibraryCodeForTypes" field must be true or false.`); } else { this.useLibraryCodeForTypes = configObj.useLibraryCodeForTypes; } @@ -1767,7 +1759,7 @@ export class ConfigOptions { const value = configObj[key]; if (value !== undefined) { if (typeof value !== 'boolean') { - errors.push(`Config "${key}" field must be true or false.`); + console.error(`Config "${key}" field must be true or false.`); } else { this[key] = value; } @@ -1777,7 +1769,7 @@ export class ConfigOptions { // Read the "typeEvaluationTimeThreshold" setting. if (configObj.typeEvaluationTimeThreshold !== undefined) { if (typeof configObj.typeEvaluationTimeThreshold !== 'number') { - errors.push(`Config "typeEvaluationTimeThreshold" field must be a number.`); + console.error(`Config "typeEvaluationTimeThreshold" field must be a number.`); } else { this.typeEvaluationTimeThreshold = configObj.typeEvaluationTimeThreshold; } @@ -1786,7 +1778,7 @@ export class ConfigOptions { // Read the "functionSignatureDisplay" setting. if (configObj.functionSignatureDisplay !== undefined) { if (typeof configObj.functionSignatureDisplay !== 'string') { - errors.push(`Config "functionSignatureDisplay" field must be true or false.`); + console.error(`Config "functionSignatureDisplay" field must be true or false.`); } else { if ( configObj.functionSignatureDisplay === 'compact' || @@ -1796,14 +1788,15 @@ export class ConfigOptions { } } } - errors.push(...unusedConfigDetector.unreadOptions().map((key) => `unknown config option: ${key}`)); - return errors; + for (const key of unusedConfigDetector.unreadOptions()) { + console.error(`unknown config option: ${key}`); + } } - static resolveExtends(configObj: any, configDirUri: Uri): Uri | undefined { + static resolveExtends(configObj: any, configDirUri: Uri, console: ConsoleInterface): Uri | undefined { if (configObj.extends !== undefined) { if (typeof configObj.extends !== 'string') { - throw new ConfigErrors([`Config "extends" field must contain a string.`]); + console.error(`Config "extends" field must contain a string.`); } else { return configDirUri.resolvePaths(configObj.extends); } @@ -1888,13 +1881,12 @@ export class ConfigOptions { } } - setupExecutionEnvironments(configObj: any, configDirUri: Uri): string[] { + setupExecutionEnvironments(configObj: any, configDirUri: Uri, console: ConsoleInterface) { // Read the "executionEnvironments" array. This should be done at the end // after we've established default values. - const errors = []; if (configObj.executionEnvironments !== undefined) { if (!Array.isArray(configObj.executionEnvironments)) { - errors.push(`Config "executionEnvironments" field must contain an array.`); + console.error(`Config "executionEnvironments" field must contain an array.`); } else { this.executionEnvironments = []; @@ -1903,30 +1895,25 @@ export class ConfigOptions { execEnvironments.forEach((env, index) => { const unusedConfigDetector = new UnusedConfigDetector(env); env = unusedConfigDetector.proxy; - const result = this._initExecutionEnvironmentFromJson( + const execEnv = this._initExecutionEnvironmentFromJson( env, configDirUri, index, + console, this.diagnosticRuleSet, this.defaultPythonVersion, this.defaultPythonPlatform, this.defaultExtraPaths || [] ); - - if (result instanceof ExecutionEnvironment) { - this.executionEnvironments.push(result); - errors.push( - ...unusedConfigDetector - .unreadOptions() - .map((key) => `unknown config option in execution environment "${env.root}": ${key}`) - ); - } else { - errors.push(...result); + if (execEnv) { + this.executionEnvironments.push(execEnv); + } + for (const key of unusedConfigDetector.unreadOptions()) { + console.error(`unknown config option in execution environment "${env.root}": ${key}`); } }); } } - return errors; } private _getEnvironmentName(): string { @@ -1948,14 +1935,14 @@ export class ConfigOptions { value: any, fieldName: string, defaultValue: DiagnosticLevel, - errors: string[] + console: ConsoleInterface ): DiagnosticLevel { if (value === undefined) { return defaultValue; } const result = parseDiagLevel(value); if (result === undefined) { - errors.push( + console.error( `Config "${fieldName}" entry must be true, false, ${userFacingOptionsList( allDiagnosticCategories )}. (received: "${value}")` @@ -1969,12 +1956,12 @@ export class ConfigOptions { envObj: any, configDirUri: Uri, index: number, + console: ConsoleInterface, configDiagnosticRuleSet: DiagnosticRuleSet, configPythonVersion: PythonVersion | undefined, configPythonPlatform: string | undefined, configExtraPaths: Uri[] - ): ExecutionEnvironment | string[] { - const errors: string[] = []; + ): ExecutionEnvironment | undefined { try { const newExecEnv = new ExecutionEnvironment( this._getEnvironmentName(), @@ -1989,18 +1976,20 @@ export class ConfigOptions { if (envObj.root && typeof envObj.root === 'string') { newExecEnv.root = configDirUri.resolvePaths(envObj.root); } else { - errors.push(`Config executionEnvironments index ${index}: missing root value.`); + console.error(`Config executionEnvironments index ${index}: missing root value.`); } // Validate the extraPaths. if (envObj.extraPaths) { if (!Array.isArray(envObj.extraPaths)) { - errors.push(`Config executionEnvironments index ${index}: extraPaths field must contain an array.`); + console.error( + `Config executionEnvironments index ${index}: extraPaths field must contain an array.` + ); } else { const pathList = envObj.extraPaths as string[]; pathList.forEach((path, pathIndex) => { if (typeof path !== 'string') { - errors.push( + console.error( `Config executionEnvironments index ${index}:` + ` extraPaths field ${pathIndex} must be a string.` ); @@ -2018,10 +2007,12 @@ export class ConfigOptions { if (version) { newExecEnv.pythonVersion = version; } else { - errors.push(`Config executionEnvironments index ${index} contains unsupported pythonVersion.`); + console.error( + `Config executionEnvironments index ${index} contains unsupported pythonVersion.` + ); } } else { - errors.push(`Config executionEnvironments index ${index} pythonVersion must be a string.`); + console.error(`Config executionEnvironments index ${index} pythonVersion must be a string.`); } } @@ -2030,7 +2021,7 @@ export class ConfigOptions { if (typeof envObj.pythonPlatform === 'string') { newExecEnv.pythonPlatform = envObj.pythonPlatform; } else { - errors.push(`Config executionEnvironments index ${index} pythonPlatform must be a string.`); + console.error(`Config executionEnvironments index ${index} pythonPlatform must be a string.`); } } @@ -2039,7 +2030,7 @@ export class ConfigOptions { if (typeof envObj.name === 'string') { newExecEnv.name = envObj.name; } else { - errors.push(`Config executionEnvironments index ${index} name must be a string.`); + console.error(`Config executionEnvironments index ${index} name must be a string.`); } } @@ -2058,14 +2049,14 @@ export class ConfigOptions { envObj[ruleName], ruleName, newExecEnv.diagnosticRuleSet[ruleName] as DiagnosticLevel, - errors + console ); }); - return errors.length > 0 ? errors : newExecEnv; + return newExecEnv; } catch { - errors.push(`Config executionEnvironments index ${index} is not accessible.`); - return errors; + console.error(`Config executionEnvironments index ${index} is not accessible.`); + return undefined; } } } diff --git a/packages/pyright-internal/src/common/console.ts b/packages/pyright-internal/src/common/console.ts index 9f084dbb02..fc4da5b86e 100644 --- a/packages/pyright-internal/src/common/console.ts +++ b/packages/pyright-internal/src/common/console.ts @@ -83,6 +83,8 @@ export class NullConsole implements ConsoleInterface { } export class StandardConsole implements ConsoleInterface { + /** useful for determining whether to exit with a non-zero exit code */ + errorWasLogged = false; constructor(private _maxLevel: LogLevel = LogLevel.Log) {} get level(): LogLevel { @@ -108,6 +110,9 @@ export class StandardConsole implements ConsoleInterface { } error(message: string) { + if (!this.errorWasLogged) { + this.errorWasLogged = true; + } if (getLevelNumber(this._maxLevel) >= getLevelNumber(LogLevel.Error)) { console.error(message); } diff --git a/packages/pyright-internal/src/common/serviceProviderExtensions.ts b/packages/pyright-internal/src/common/serviceProviderExtensions.ts index 4b9d9d2ce2..2a9d8bcae5 100644 --- a/packages/pyright-internal/src/common/serviceProviderExtensions.ts +++ b/packages/pyright-internal/src/common/serviceProviderExtensions.ts @@ -18,6 +18,7 @@ import { ServiceProvider } from './serviceProvider'; import { Uri } from './uri/uri'; import { DocStringService, PyrightDocStringService } from './docStringService'; import { CommandService, WindowService } from './languageServerInterface'; +import { BaselineHandler } from '../baseline'; declare module './serviceProvider' { interface ServiceProvider { @@ -104,6 +105,7 @@ const DefaultSourceFileFactory: ISourceFileFactory = { moduleName: string, isThirdPartyImport: boolean, isThirdPartyPyTypedPresent: boolean, + baselineHandler: BaselineHandler, editMode: SourceFileEditMode, console?: ConsoleInterface, logTracker?: LogTracker, @@ -116,6 +118,7 @@ const DefaultSourceFileFactory: ISourceFileFactory = { isThirdPartyImport, isThirdPartyPyTypedPresent, editMode, + baselineHandler, console, logTracker, ipythonMode diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index bedc3b045c..44ec3a6510 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -53,14 +53,12 @@ import { InitializeResult, Location, MarkupKind, - MessageType, PrepareRenameParams, PublishDiagnosticsParams, ReferenceParams, RemoteWindow, RenameFilesParams, RenameParams, - ShowMessageNotification, SignatureHelp, SignatureHelpParams, SymbolInformation, @@ -1285,10 +1283,6 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis this.sendDiagnostics(fs, { ...fileDiag, reason: results.reason }); }); - results.configParseErrors.forEach((error) => - this.connection.sendNotification(ShowMessageNotification.type, { message: error, type: MessageType.Error }) - ); - // if any baselined diagnostics disappeared, update the baseline for the effected files if ( results.reason === 'analysis' && @@ -1316,7 +1310,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis if (!workspace.rootUri) { continue; } - const baseline = new BaselineHandler(this.fs, workspace.rootUri); + const baseline = new BaselineHandler(this.fs, workspace.service.getConfigOptions(), this.console); const baselineDiffSummary = baseline.write(false, false, files)?.getSummaryMessage(); if (baselineDiffSummary) { this.console.info( diff --git a/packages/pyright-internal/src/pyright.ts b/packages/pyright-internal/src/pyright.ts index faa17a0f87..4304a8bf1e 100644 --- a/packages/pyright-internal/src/pyright.ts +++ b/packages/pyright-internal/src/pyright.ts @@ -30,7 +30,7 @@ import { ChokidarFileWatcherProvider } from './common/chokidarFileWatcherProvide import { CommandLineOptions as PyrightCommandLineOptions } from './common/commandLineOptions'; import { ConsoleInterface, LogLevel, StandardConsole, StderrConsole } from './common/console'; import { fail } from './common/debug'; -import { createDeferred } from './common/deferred'; +import { createDeferred, Deferred } from './common/deferred'; import { Diagnostic, DiagnosticCategory } from './common/diagnostic'; import { FileDiagnostics } from './common/diagnosticSink'; import { FullAccessHost } from './common/fullAccessHost'; @@ -49,7 +49,6 @@ import * as core from '@actions/core'; import * as command from '@actions/core/lib/command'; import { convertDiagnostics } from 'pyright-to-gitlab-ci/src/converter'; import path from 'path'; -import { BaselineHandler } from './baseline'; import { pluralize } from './common/stringUtils'; import { allTypeCheckingModes, @@ -481,12 +480,7 @@ const outputResults = ( minSeverityLevel: SeverityLevel, output: ConsoleInterface ) => { - const rootDir = - typeof options.executionRoot === 'string' || options.executionRoot === undefined - ? Uri.file(options.executionRoot ?? '', service.serviceProvider) - : options.executionRoot; - - const baselineFile = new BaselineHandler(service.fs, rootDir); + const baselineFile = service.backgroundAnalysisProgram.program.baselineHandler; const baselineDiffMessage = baselineFile.write(args.writebaseline, true, results.diagnostics)?.getSummaryMessage(); if (baselineDiffMessage) { console.info(baselineDiffMessage); @@ -543,6 +537,17 @@ const outputResults = ( return errorCount; }; +/** + * checks for errors parsing config files and / or the baseline file and exits with a non-zero exit code + * if there were any + */ +const checkForErrors = (exitStatus: Deferred, console: ConsoleInterface) => { + if (console instanceof StandardConsole && console.errorWasLogged) { + console.errorWasLogged = false; + exitStatus.resolve(ExitStatus.ConfigFileParseError); + } +}; + async function runSingleThreaded( args: CommandLineOptions, options: PyrightCommandLineOptions, @@ -560,13 +565,7 @@ async function runSingleThreaded( return; } - if (results.configParseErrors.length) { - for (const error of results.configParseErrors) { - output.error(error); - } - exitStatus.resolve(ExitStatus.ConfigFileParseError); - return; - } + checkForErrors(exitStatus, output); const errorCount = args.createstub || args.verifytypes @@ -759,10 +758,7 @@ async function runMultiThreaded( return; } - if (results.configParseErrors.length) { - exitStatus.resolve(ExitStatus.ConfigFileParseError); - return; - } + checkForErrors(exitStatus, console); for (const fileDiag of results.diagnostics) { fileDiagnostics.push(FileDiagnostics.fromJsonObj(fileDiag)); diff --git a/packages/pyright-internal/src/realLanguageServer.ts b/packages/pyright-internal/src/realLanguageServer.ts index e9f27e94a9..631353a05e 100644 --- a/packages/pyright-internal/src/realLanguageServer.ts +++ b/packages/pyright-internal/src/realLanguageServer.ts @@ -44,6 +44,17 @@ import { FileWatcherHandler } from './common/fileWatcher'; const maxAnalysisTimeInForeground = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 }; +class ErrorNotificationConsole extends ConsoleWithLogLevel { + constructor(private _connection: Connection) { + super(_connection.console); + } + + override error(message: string): void { + this._connection.sendNotification(ShowMessageNotification.type, { message, type: MessageType.Error }); + super.error(message); + } +} + //TODO: better name. this class is used by both the node and web language server, but not the test one export abstract class RealLanguageServer extends LanguageServerBase { protected controller: CommandController; @@ -58,7 +69,7 @@ export abstract class RealLanguageServer extends LanguageServerBase { // eslint-disable-next-line @typescript-eslint/no-var-requires const version = require('../package.json').version || ''; - const console = new ConsoleWithLogLevel(connection.console); + const console = new ErrorNotificationConsole(connection); const pyrightFs = new PyrightFileSystem(realFileSystem); const cacheManager = new CacheManager(maxWorkers); diff --git a/packages/pyright-internal/src/tests/config.test.ts b/packages/pyright-internal/src/tests/config.test.ts index c1642c2852..c5f30719d8 100644 --- a/packages/pyright-internal/src/tests/config.test.ts +++ b/packages/pyright-internal/src/tests/config.test.ts @@ -27,9 +27,18 @@ import { AnalysisResults } from '../analyzer/analysis'; import { existsSync } from 'fs'; import { NoAccessHost } from '../common/host'; +class ErrorTrackingNullConsole extends NullConsole { + errors = new Array(); + + override error(message: string) { + this.errors.push(message); + super.error(message); + } +} + function createAnalyzer(console?: ConsoleInterface) { const tempFile = new RealTempFile(); - const cons = console ?? new NullConsole(); + const cons = console ?? new ErrorTrackingNullConsole(); const fs = createFromRealFileSystem(tempFile, cons); const serviceProvider = createServiceProvider(fs, cons, tempFile); return new AnalyzerService('', serviceProvider, { console: cons }); @@ -227,12 +236,12 @@ describe('invalid config', () => { const json = { asdf: 1 }; const fs = new TestFileSystem(/* ignoreCase */ false); - const nullConsole = new NullConsole(); + const console = new ErrorTrackingNullConsole(); - const sp = createServiceProvider(fs, nullConsole); - const errors = configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); + const sp = createServiceProvider(fs, console); + configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); - assert.deepStrictEqual(errors, ['unknown config option: asdf']); + assert.deepStrictEqual(console.errors, ['unknown config option: asdf']); }); test('unknown value for top-level option', () => { const cwd = UriEx.file(normalizePath(process.cwd())); @@ -242,22 +251,20 @@ describe('invalid config', () => { const json = { typeCheckingMode: 'asdf' }; const fs = new TestFileSystem(/* ignoreCase */ false); - const nullConsole = new NullConsole(); + const console = new ErrorTrackingNullConsole(); - const sp = createServiceProvider(fs, nullConsole); - const errors = configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); + const sp = createServiceProvider(fs, console); + configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); - assert.deepStrictEqual(errors, [ + assert.deepStrictEqual(console.errors, [ 'invalid "typeCheckingMode" value: "asdf". expected: "off", "basic", "standard", "strict", "recommended", or "all"', ]); }); test('unknown value in execution environments', () => { - const { analysisResult } = setupPyprojectToml( + const { consoleErrors } = setupPyprojectToml( 'src/tests/samples/project_with_invalid_option_in_execution_environments' ); - assert.deepStrictEqual(analysisResult?.configParseErrors, [ - `unknown config option in execution environment "foo": asdf`, - ]); + assert.deepStrictEqual(consoleErrors, [`unknown config option in execution environment "foo": asdf`]); }); }); @@ -348,10 +355,15 @@ test('AutoSearchPathsOnAndExtraPaths', () => { const setupPyprojectToml = ( projectPath: string -): { configOptions: ConfigOptions; analysisResult: AnalysisResults | undefined } => { +): { + configOptions: ConfigOptions; + analysisResult: AnalysisResults | undefined; + consoleErrors: string[]; +} => { const cwd = normalizePath(combinePaths(process.cwd(), projectPath)); assert(existsSync(cwd)); - const service = createAnalyzer(); + const console = new ErrorTrackingNullConsole(); + const service = createAnalyzer(console); let analysisResult = undefined as AnalysisResults | undefined; service.setCompletionCallback((result) => (analysisResult = result)); const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); @@ -359,7 +371,8 @@ const setupPyprojectToml = ( service.setOptions(commandLineOptions); return { - configOptions: service.test_getConfigOptions(commandLineOptions), + configOptions: service.getConfigOptions(), + consoleErrors: console.errors, analysisResult, }; }; @@ -385,25 +398,26 @@ test('both pyright and basedpyright in pyproject.toml', () => { 'src/tests/samples/project_with_both_config_sections_in_pyproject_toml' ); assert.strictEqual(configOptions.defaultPythonVersion!, undefined); - assert(analysisResult?.configParseErrors.length); - assert(!analysisResult.fatalErrorOccurred); + assert(!analysisResult?.fatalErrorOccurred); }); test('invalid option value in pyproject.toml', () => { - const analysisResult = setupPyprojectToml( + const { consoleErrors, analysisResult } = setupPyprojectToml( 'src/tests/samples/project_with_invalid_option_value_in_pyproject_toml' - ).analysisResult; - assert(analysisResult?.configParseErrors.length); - assert(!analysisResult.fatalErrorOccurred); + ); + assert.deepStrictEqual(consoleErrors, [ + 'invalid "typeCheckingMode" value: "asdf". expected: "off", "basic", "standard", "strict", "recommended", or "all"', + ]); + assert(!analysisResult?.fatalErrorOccurred); }); test('unknown option name in pyproject.toml', () => { - const { configOptions, analysisResult } = setupPyprojectToml( + const { configOptions, analysisResult, consoleErrors } = setupPyprojectToml( 'src/tests/samples/project_with_invalid_option_name_in_pyproject_toml' ); assert(!('asdf' in configOptions)); - assert(analysisResult?.configParseErrors.length); - assert(!analysisResult.fatalErrorOccurred); + assert.deepStrictEqual(consoleErrors, ['unknown config option: asdf']); + assert(!analysisResult?.fatalErrorOccurred); }); test('FindFilesInMemoryOnly', () => { diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts index 387d6af061..7d545998eb 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts @@ -183,7 +183,7 @@ export class TestState { const configDirUri = Uri.file(projectRoot, this.serviceProvider); configOptions.initializeTypeCheckingMode('standard'); configOptions.initializeFromJson(this.rawConfigJson, configDirUri, this.serviceProvider, testAccessHost); - configOptions.setupExecutionEnvironments(this.rawConfigJson, configDirUri); + configOptions.setupExecutionEnvironments(this.rawConfigJson, configDirUri, this.console); this._applyTestConfigOptions(configOptions); } diff --git a/packages/pyright-internal/src/tests/sourceFile.test.ts b/packages/pyright-internal/src/tests/sourceFile.test.ts index f3fcdc4304..09bfd77bdd 100644 --- a/packages/pyright-internal/src/tests/sourceFile.test.ts +++ b/packages/pyright-internal/src/tests/sourceFile.test.ts @@ -17,16 +17,25 @@ import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem import { createServiceProvider } from '../common/serviceProviderExtensions'; import { parseAndGetTestState } from './harness/fourslash/testState'; import { Uri } from '../common/uri/uri'; +import { BaselineHandler } from '../baseline'; test('Empty', () => { const filePath = combinePaths(process.cwd(), 'tests/samples/test_file1.py'); const tempFile = new RealTempFile(); const fs = createFromRealFileSystem(tempFile); const serviceProvider = createServiceProvider(tempFile, fs); - const sourceFile = new SourceFile(serviceProvider, Uri.file(filePath, serviceProvider), '', false, false, { - isEditMode: false, - }); const configOptions = new ConfigOptions(Uri.file(process.cwd(), serviceProvider)); + const sourceFile = new SourceFile( + serviceProvider, + Uri.file(filePath, serviceProvider), + '', + false, + false, + { + isEditMode: false, + }, + new BaselineHandler(fs, configOptions, undefined) + ); const sp = createServiceProvider(fs); const importResolver = new ImportResolver(sp, configOptions, new FullAccessHost(sp));