diff --git a/client/schemas/pyrightconfig.schema.json b/client/schemas/pyrightconfig.schema.json index 2ea02c3af..3b90b2010 100644 --- a/client/schemas/pyrightconfig.schema.json +++ b/client/schemas/pyrightconfig.schema.json @@ -246,6 +246,12 @@ "title": "Controls reporting calls to 'isinstance' where the result is statically determined to be always true or false", "default": "none" }, + "reportUnnecessaryCast": { + "$id": "#/properties/reportUnnecessaryCast", + "$ref": "#/definitions/diagnostic", + "title": "Controls reporting calls to 'cast' that are unnecessary", + "default": "none" + }, "pythonVersion": { "$id": "#/properties/pythonVersion", "type": "string", diff --git a/docs/configuration.md b/docs/configuration.md index 6d5533a4c..7e1fe77ff 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -90,6 +90,8 @@ The following settings control pyright's diagnostic output (warnings or errors). **reportUnnecessaryIsInstance** [boolean or string, optional]: Generate or suppress diagnostics for 'isinstance' calls where the result is statically determined to be always true or always false. Such calls are often indicative of a programming error. The default value for this setting is 'none'. +**reportUnnecessaryCast** [boolean or string, optional]: Generate or suppress diagnostics for 'cast' calls that are statically determined to be unnecessary. Such calls are sometimes indicative of a programming error. The default value for this setting is 'none'. + ## Execution Environment Options Pyright allows multiple “execution environments” to be defined for different portions of your source tree. For example, a subtree may be designed to run with different import search paths or a different version of the python interpreter than the rest of the source base. diff --git a/server/src/analyzer/expressionEvaluator.ts b/server/src/analyzer/expressionEvaluator.ts index abcd0fc03..1b5679ac7 100644 --- a/server/src/analyzer/expressionEvaluator.ts +++ b/server/src/analyzer/expressionEvaluator.ts @@ -1576,6 +1576,20 @@ export class ExpressionEvaluator { const functionType = this._findOverloadedFunctionType(errorNode, argList, callType); if (functionType) { + if (functionType.getBuiltInName() === 'cast' && argList.length === 2) { + // Verify that the cast is necessary. + const castToType = argList[0].type; + const castFromType = argList[1].type; + if (castToType instanceof ClassType && castFromType instanceof ObjectType) { + if (castToType.isSame(castFromType.getClassType())) { + this._addDiagnostic( + this._fileInfo.diagnosticSettings.reportUnnecessaryCast, + `Unnecessary call to cast: type is already ${ castFromType.asString() }`, + errorNode); + } + } + } + type = this._validateCallArguments(errorNode, argList, callType, new TypeVarMap(), specializeReturnType); if (!type) { diff --git a/server/src/common/configOptions.ts b/server/src/common/configOptions.ts index f583a944d..f846e9b2f 100644 --- a/server/src/common/configOptions.ts +++ b/server/src/common/configOptions.ts @@ -135,6 +135,10 @@ export interface DiagnosticSettings { // Report calls to isinstance that are statically determined // to always be true or false. reportUnnecessaryIsInstance: DiagnosticLevel; + + // Report calls to cast that are statically determined + // to always unnecessary. + reportUnnecessaryCast: DiagnosticLevel; } export function cloneDiagnosticSettings( @@ -180,7 +184,8 @@ export function getDiagLevelSettings() { 'reportUnknownVariableType', 'reportUnknownMemberType', 'reportCallInDefaultInitializer', - 'reportUnnecessaryIsInstance' + 'reportUnnecessaryIsInstance', + 'reportUnnecessaryCast' ]; } @@ -215,7 +220,8 @@ export function getStrictDiagnosticSettings(): DiagnosticSettings { reportUnknownVariableType: 'error', reportUnknownMemberType: 'error', reportCallInDefaultInitializer: 'none', - reportUnnecessaryIsInstance: 'error' + reportUnnecessaryIsInstance: 'error', + reportUnnecessaryCast: 'error' }; return diagSettings; @@ -252,7 +258,8 @@ export function getDefaultDiagnosticSettings(): DiagnosticSettings { reportUnknownVariableType: 'none', reportUnknownMemberType: 'none', reportCallInDefaultInitializer: 'none', - reportUnnecessaryIsInstance: 'none' + reportUnnecessaryIsInstance: 'none', + reportUnnecessaryCast: 'none' }; return diagSettings; @@ -581,7 +588,12 @@ export class ConfigOptions { // Read the "reportUnnecessaryIsInstance" entry. reportUnnecessaryIsInstance: this._convertDiagnosticLevel( configObj.reportUnnecessaryIsInstance, 'reportUnnecessaryIsInstance', - defaultSettings.reportUnnecessaryIsInstance) + defaultSettings.reportUnnecessaryIsInstance), + + // Read the "reportUnnecessaryCast" entry. + reportUnnecessaryCast: this._convertDiagnosticLevel( + configObj.reportUnnecessaryCast, 'reportUnnecessaryCast', + defaultSettings.reportUnnecessaryCast) }; // Read the "venvPath". diff --git a/server/src/tests/samples/unnecessaryCast1.py b/server/src/tests/samples/unnecessaryCast1.py new file mode 100644 index 000000000..97dcc7e17 --- /dev/null +++ b/server/src/tests/samples/unnecessaryCast1.py @@ -0,0 +1,15 @@ +# This sample tests the type checker's reoprtUnnecessaryCast feature. + +from typing import cast, Union + +a = 3 +# This should generate an error if +# reportUnneessaryCast is enabled. +b = cast(int, a) + +c: Union[int, str] = 'hello' +d = cast(int, c) + + + + diff --git a/server/src/tests/typeAnalyzer.test.ts b/server/src/tests/typeAnalyzer.test.ts index c4506efb7..db3240857 100644 --- a/server/src/tests/typeAnalyzer.test.ts +++ b/server/src/tests/typeAnalyzer.test.ts @@ -547,3 +547,15 @@ test('Unbound1', () => { validateResults(analysisResults, 1); }); + +test('UnnecessaryCast', () => { + const configOptions = new ConfigOptions('.'); + + let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryCast1.py'], configOptions); + validateResults(analysisResults, 0); + + // Turn on errors. + configOptions.diagnosticSettings.reportUnnecessaryCast = 'error'; + analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryCast1.py'], configOptions); + validateResults(analysisResults, 1); +});