diff --git a/docs/configuration/config-files.md b/docs/configuration/config-files.md index bcb0a1176..c267f1d01 100644 --- a/docs/configuration/config-files.md +++ b/docs/configuration/config-files.md @@ -320,13 +320,15 @@ The following settings can be specified for each execution environment. Each sou - **root** [string, required]: Root path for the code that will execute within this execution environment. +- **typeCheckingMode** [string, optional]: Specifies the type checking mode to use for this execution environment. This overrides the global `typeCheckingMode` setting. Valid values are the same as [the global setting](#diagnostic-settings-defaults). If not specified, the global `typeCheckingMode` is used. + - **extraPaths** [array of strings, optional]: Additional search paths (in addition to the root path) that will be used when searching for modules imported by files within this execution environment. If specified, this overrides the default extraPaths setting when resolving imports for files within this execution environment. Note that each file’s execution environment mapping is independent, so if file A is in one execution environment and imports a second file B within a second execution environment, any imports from B will use the extraPaths in the second execution environment. - **pythonVersion** [string, optional]: The version of Python used for this execution environment. If not specified, the global `pythonVersion` setting is used instead. - **pythonPlatform** [string, optional]: Specifies the target platform that will be used for this execution environment. If not specified, the global `pythonPlatform` setting is used instead. -In addition, any of the [type check diagnostics settings](config-files.md#type-check-diagnostics-settings) listed above can be specified. These settings act as overrides for the files in this execution environment. +In addition, any of the [type check diagnostics settings](config-files.md#type-check-diagnostics-settings) listed above can be specified. These settings act as overrides for the files in this execution environment. Individual diagnostic rule overrides take precedence over the `typeCheckingMode` setting. ## Sample Config File The following is an example of a pyright config file: @@ -364,6 +366,7 @@ The following is an example of a pyright config file: "root": "src/web", "pythonVersion": "3.5", "pythonPlatform": "Windows", + "typeCheckingMode": "basic", "extraPaths": [ "src/service_libs" ], @@ -372,12 +375,14 @@ The following is an example of a pyright config file: { "root": "src/sdk", "pythonVersion": "3.0", + "typeCheckingMode": "strict", "extraPaths": [ "src/backend" ] }, { "root": "src/tests", + "typeCheckingMode": "standard", "reportPrivateUsage": false, "extraPaths": [ "src/tests/e2e", @@ -411,9 +416,9 @@ pythonVersion = "3.6" pythonPlatform = "Linux" executionEnvironments = [ - { root = "src/web", pythonVersion = "3.5", pythonPlatform = "Windows", extraPaths = [ "src/service_libs" ], reportMissingImports = "warning" }, - { root = "src/sdk", pythonVersion = "3.0", extraPaths = [ "src/backend" ] }, - { root = "src/tests", reportPrivateUsage = false, extraPaths = ["src/tests/e2e", "src/sdk" ]}, + { root = "src/web", pythonVersion = "3.5", pythonPlatform = "Windows", typeCheckingMode = "basic", extraPaths = [ "src/service_libs" ], reportMissingImports = "warning" }, + { root = "src/sdk", pythonVersion = "3.0", typeCheckingMode = "strict", extraPaths = [ "src/backend" ] }, + { root = "src/tests", typeCheckingMode = "standard", reportPrivateUsage = false, extraPaths = ["src/tests/e2e", "src/sdk" ]}, { root = "src" } ] ``` diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index fb861ea0d..6e3dc257c 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -61,6 +61,9 @@ export class ExecutionEnvironment { // Diagnostic rules with overrides. diagnosticRuleSet: DiagnosticRuleSet; + // The type checking mode specified for this execution environment, if any. + typeCheckingMode?: TypeCheckingMode; + // Skip import resolution attempts for native libraries. These can // be expensive and are not needed for some use cases (e.g. web-based // tools or playgrounds). @@ -1559,16 +1562,9 @@ export class ConfigOptions { * @returns any errors that occurred */ initializeTypeCheckingModeFromString(typeCheckingMode: string | undefined, console_: ConsoleInterface) { - if (typeCheckingMode !== undefined) { - if ((allTypeCheckingModes as readonly string[]).includes(typeCheckingMode)) { - this.initializeTypeCheckingMode(typeCheckingMode as TypeCheckingMode); - } else { - console_.error( - `invalid "typeCheckingMode" value: "${typeCheckingMode}". expected: ${userFacingOptionsList( - allTypeCheckingModes - )}` - ); - } + const validatedMode = ConfigOptions._validateTypeCheckingMode(typeCheckingMode, console_, ''); + if (validatedMode !== undefined) { + this.initializeTypeCheckingMode(validatedMode); } } @@ -1983,6 +1979,57 @@ export class ConfigOptions { } } + /** + * Validates a typeCheckingMode value and returns the corresponding TypeCheckingMode enum. + * Logs an error to the console if the value is invalid. + * + * @param typeCheckingMode - The typeCheckingMode value to validate (can be undefined) + * @param console_ - Console interface for logging errors + * @param errorContext - Context string for error messages (e.g., "Config executionEnvironments index 0"). Pass empty string for global config. + * @returns TypeCheckingMode if valid, undefined if not specified or invalid + */ + private static _validateTypeCheckingMode( + typeCheckingMode: unknown, + console_: ConsoleInterface, + errorContext: string + ): TypeCheckingMode | undefined { + if (typeCheckingMode === undefined) { + return undefined; + } + + const prefix = errorContext ? `${errorContext}: ` : ''; + if (typeof typeCheckingMode !== 'string') { + console_.error(`${prefix}typeCheckingMode must be a string.`); + return undefined; + } + + if ((allTypeCheckingModes as readonly string[]).includes(typeCheckingMode)) { + return typeCheckingMode as TypeCheckingMode; + } + console_.error( + `${prefix}invalid "typeCheckingMode" value: "${typeCheckingMode}". expected: ${userFacingOptionsList(allTypeCheckingModes)}` + ); + return undefined; + } + + /** + * Validates a typeCheckingMode string and returns the corresponding DiagnosticRuleSet. + * Logs an error to the console if the value is invalid. + * + * @param typeCheckingMode - The typeCheckingMode value to validate (can be undefined) + * @param console_ - Console interface for logging errors + * @param errorContext - Context string for error messages (e.g., "Config executionEnvironments index 0") + * @returns DiagnosticRuleSet if valid, undefined if not specified or invalid + */ + private static _getDiagnosticRuleSetFromString( + typeCheckingMode: unknown, + console_: ConsoleInterface, + errorContext: string + ): DiagnosticRuleSet | undefined { + const validatedMode = ConfigOptions._validateTypeCheckingMode(typeCheckingMode, console_, errorContext); + return validatedMode !== undefined ? ConfigOptions.getDiagnosticRuleSet(validatedMode) : undefined; + } + private _getEnvironmentName(): string { return this.pythonEnvironmentName || this.pythonPath?.toString() || 'python'; } @@ -2030,15 +2077,32 @@ export class ConfigOptions { configExtraPaths: Uri[] ): ExecutionEnvironment | undefined { try { + // If typeCheckingMode is specified for this execution environment, + // validate it once and use it to generate the base diagnostic rule set. + // Otherwise, use the config-level diagnostic rule set. + const validatedMode = ConfigOptions._validateTypeCheckingMode( + envObj.typeCheckingMode, + console, + `Config executionEnvironments index ${index}` + ); + const baseDiagnosticRuleSet = validatedMode !== undefined + ? ConfigOptions.getDiagnosticRuleSet(validatedMode) + : configDiagnosticRuleSet; + const newExecEnv = new ExecutionEnvironment( this._getEnvironmentName(), configDirUri, - configDiagnosticRuleSet, + baseDiagnosticRuleSet, configPythonVersion, configPythonPlatform, configExtraPaths ); + // Store the validated typeCheckingMode if it was specified and valid. + if (validatedMode !== undefined) { + newExecEnv.typeCheckingMode = validatedMode; + } + // Validate the root. if (envObj.root && typeof envObj.root === 'string') { newExecEnv.root = configDirUri.resolvePaths(envObj.root); diff --git a/packages/pyright-internal/src/tests/config.test.ts b/packages/pyright-internal/src/tests/config.test.ts index d7df114a8..fb690e6e4 100644 --- a/packages/pyright-internal/src/tests/config.test.ts +++ b/packages/pyright-internal/src/tests/config.test.ts @@ -12,7 +12,12 @@ import assert from 'assert'; import { AnalyzerService } from '../analyzer/service'; import { deserialize, serialize } from '../backgroundThreadBase'; import { CommandLineOptions, DiagnosticSeverityOverrides } from '../common/commandLineOptions'; -import { ConfigOptions, ExecutionEnvironment, getStandardDiagnosticRuleSet } from '../common/configOptions'; +import { + ConfigOptions, + ExecutionEnvironment, + allTypeCheckingModes, + getStandardDiagnosticRuleSet, +} from '../common/configOptions'; import { ConsoleInterface, NullConsole } from '../common/console'; import { TaskListPriority } from '../common/diagnostic'; import { combinePaths, normalizePath, normalizeSlashes } from '../common/pathUtils'; @@ -708,6 +713,134 @@ describe(`config test'}`, () => { assert.equal(configOptions.diagnosticRuleSet.reportAny, 'warning'); }); + describe('executionEnvironments typeCheckingMode', () => { + test('typeCheckingMode in executionEnvironment sets diagnostic rules correctly', () => { + const cwd = UriEx.file(normalizePath(process.cwd())); + const configOptions = new ConfigOptions(cwd); + + const json = { + typeCheckingMode: 'standard', + executionEnvironments: [ + { + root: 'src/strict_folder', + typeCheckingMode: 'strict', + }, + { + root: 'src/basic_folder', + typeCheckingMode: 'basic', + }, + ], + }; + + const fs = new TestFileSystem(/* ignoreCase */ false); + const console = new NullConsole(); + const sp = createServiceProvider(fs, console); + configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); + configOptions.setupExecutionEnvironments(json, cwd, console); + + assert.strictEqual(configOptions.executionEnvironments.length, 2); + + const [strictEnv, basicEnv] = configOptions.executionEnvironments; + + // Verify strict environment has strict settings + assert.strictEqual(strictEnv.diagnosticRuleSet.strictListInference, true); + assert.strictEqual(strictEnv.diagnosticRuleSet.reportMissingTypeStubs, 'error'); + assert.strictEqual(strictEnv.diagnosticRuleSet.reportUnusedVariable, 'error'); + + // Verify basic environment has basic settings + assert.strictEqual(basicEnv.diagnosticRuleSet.strictListInference, false); + assert.strictEqual(basicEnv.diagnosticRuleSet.reportMissingTypeStubs, 'none'); + assert.strictEqual(basicEnv.diagnosticRuleSet.reportUnusedVariable, 'hint'); + }); + + test('typeCheckingMode in executionEnvironment with individual rule overrides', () => { + const cwd = UriEx.file(normalizePath(process.cwd())); + const configOptions = new ConfigOptions(cwd); + + const json = { + typeCheckingMode: 'standard', + executionEnvironments: [ + { + root: 'src/custom', + typeCheckingMode: 'strict', + reportUnusedVariable: 'warning', + }, + ], + }; + + const fs = new TestFileSystem(/* ignoreCase */ false); + const console = new NullConsole(); + const sp = createServiceProvider(fs, console); + configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); + configOptions.setupExecutionEnvironments(json, cwd, console); + + const customEnv = configOptions.executionEnvironments[0]; + + // Verify strict mode is applied as base + assert.strictEqual(customEnv.diagnosticRuleSet.strictListInference, true); + + // Verify individual override takes precedence over typeCheckingMode + assert.strictEqual(customEnv.diagnosticRuleSet.reportUnusedVariable, 'warning'); + }); + + test('invalid typeCheckingMode in executionEnvironment logs error', () => { + const cwd = UriEx.file(normalizePath(process.cwd())); + const configOptions = new ConfigOptions(cwd); + + const json = { + executionEnvironments: [ + { + root: 'src/invalid', + typeCheckingMode: 'invalid_mode', + }, + ], + }; + + const fs = new TestFileSystem(/* ignoreCase */ false); + const console = new ErrorTrackingNullConsole(); + const sp = createServiceProvider(fs, console); + configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); + configOptions.setupExecutionEnvironments(json, cwd, console); + + assert.strictEqual(console.errors.length, 1); + assert( + console.errors[0].includes('invalid "typeCheckingMode"') && + console.errors[0].includes('invalid_mode') + ); + }); + + test('all typeCheckingMode values work in executionEnvironment', () => { + for (const mode of allTypeCheckingModes) { + const cwd = UriEx.file(normalizePath(process.cwd())); + const configOptions = new ConfigOptions(cwd); + + const json = { + executionEnvironments: [ + { + root: `src/${mode}`, + typeCheckingMode: mode, + }, + ], + }; + + const fs = new TestFileSystem(/* ignoreCase */ false); + const console = new ErrorTrackingNullConsole(); + const sp = createServiceProvider(fs, console); + configOptions.initializeFromJson(json, cwd, sp, new NoAccessHost()); + configOptions.setupExecutionEnvironments(json, cwd, console); + + // Should not log any errors for valid modes + assert.strictEqual( + console.errors.length, + 0, + `typeCheckingMode "${mode}" should be valid but got errors: ${console.errors.join(', ')}` + ); + assert.strictEqual(configOptions.executionEnvironments.length, 1); + } + }); + + }); + function createAnalyzer(console?: ConsoleInterface) { const cons = console ?? new ErrorTrackingNullConsole(); const fs = createFromRealFileSystem(tempFile, cons);