diff --git a/docs/configuration.md b/docs/configuration.md index 240acaa47..285de463a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -134,6 +134,8 @@ The following settings control pyright’s diagnostic output (warnings or errors **reportUnsupportedDunderAll** [boolean or string, optional]: Generate or suppress diagnostics for statements that define or manipulate `__all__` in a way that is not allowed by a static type checker, thus rendering the contents of `__all__` to be unknown or incorrect. The default value for this setting is 'warning'. +**reportUnusedCallResult** [boolean or string, optional]: Generate or suppress diagnostics for call statements whose return value is not used in any way and is not None. 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/packages/pyright-internal/src/analyzer/checker.ts b/packages/pyright-internal/src/analyzer/checker.ts index 505cdf3b9..c69710f16 100644 --- a/packages/pyright-internal/src/analyzer/checker.ts +++ b/packages/pyright-internal/src/analyzer/checker.ts @@ -421,6 +421,28 @@ export class Checker extends ParseTreeWalker { ); } + if (this._fileInfo.diagnosticRuleSet.reportUnusedCallResult !== 'none') { + if (node.parent?.nodeType === ParseNodeType.StatementList) { + const returnType = this._evaluator.getType(node); + if ( + returnType && + !isNone(returnType) && + !isNoReturnType(returnType) && + !isNever(returnType) && + !isAnyOrUnknown(returnType) + ) { + this._evaluator.addDiagnostic( + this._fileInfo.diagnosticRuleSet.reportUnusedCallResult, + DiagnosticRule.reportUnusedCallResult, + Localizer.Diagnostic.unusedCallResult().format({ + type: this._evaluator.printType(returnType, /* expandTypeAlias */ false), + }), + node + ); + } + } + } + return true; } diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index a03ed455e..82505acdb 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -229,6 +229,10 @@ export interface DiagnosticRuleSet { // Report operations on __all__ symbol that are not supported // by a static type checker. reportUnsupportedDunderAll: DiagnosticLevel; + + // Report cases where a call expression's return result is not + // None and is not used in any way. + reportUnusedCallResult: DiagnosticLevel; } export function cloneDiagnosticRuleSet(diagSettings: DiagnosticRuleSet): DiagnosticRuleSet { @@ -295,6 +299,7 @@ export function getDiagLevelDiagnosticRules() { DiagnosticRule.reportUnboundVariable, DiagnosticRule.reportInvalidStubStatement, DiagnosticRule.reportUnsupportedDunderAll, + DiagnosticRule.reportUnusedCallResult, ]; } @@ -358,6 +363,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet { reportUndefinedVariable: 'warning', reportInvalidStubStatement: 'none', reportUnsupportedDunderAll: 'none', + reportUnusedCallResult: 'none', }; return diagSettings; @@ -417,6 +423,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet { reportUndefinedVariable: 'error', reportInvalidStubStatement: 'none', reportUnsupportedDunderAll: 'warning', + reportUnusedCallResult: 'none', }; return diagSettings; @@ -476,6 +483,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { reportUndefinedVariable: 'error', reportInvalidStubStatement: 'error', reportUnsupportedDunderAll: 'error', + reportUnusedCallResult: 'none', }; return diagSettings; @@ -1111,6 +1119,13 @@ export class ConfigOptions { DiagnosticRule.reportUnsupportedDunderAll, defaultSettings.reportUnsupportedDunderAll ), + + // Read the "reportUnusedCallResult" entry. + reportUnusedCallResult: this._convertDiagnosticLevel( + configObj.reportUnusedCallResult, + DiagnosticRule.reportUnusedCallResult, + defaultSettings.reportUnusedCallResult + ), }; // Read the "venvPath". diff --git a/packages/pyright-internal/src/common/diagnosticRules.ts b/packages/pyright-internal/src/common/diagnosticRules.ts index 39a328d82..5f976d403 100644 --- a/packages/pyright-internal/src/common/diagnosticRules.ts +++ b/packages/pyright-internal/src/common/diagnosticRules.ts @@ -60,4 +60,5 @@ export enum DiagnosticRule { reportUnboundVariable = 'reportUnboundVariable', reportInvalidStubStatement = 'reportInvalidStubStatement', reportUnsupportedDunderAll = 'reportUnsupportedDunderAll', + reportUnusedCallResult = 'reportUnusedCallResult', } diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index 2f6d116f6..07e6b8e79 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -657,6 +657,10 @@ export namespace Localizer { export const unreachableCode = () => getRawString('Diagnostic.unreachableCode'); export const unsupportedDunderAllAssignment = () => getRawString('Diagnostic.unsupportedDunderAllAssignment'); export const unsupportedDunderAllOperation = () => getRawString('Diagnostic.unsupportedDunderAllOperation'); + export const unusedCallResult = () => + new ParameterizedString<{ type: string }>( + getRawString('Diagnostic.unusedCallResult') + ); export const varAnnotationIllegal = () => getRawString('Diagnostic.varAnnotationIllegal'); export const walrusIllegal = () => getRawString('Diagnostic.walrusIllegal'); export const walrusNotAllowed = () => getRawString('Diagnostic.walrusNotAllowed'); diff --git a/packages/pyright-internal/src/localization/package.nls.en-us.json b/packages/pyright-internal/src/localization/package.nls.en-us.json index 3235f2962..ae81458a1 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -328,6 +328,7 @@ "unreachableCode": "Code is unreachable", "unsupportedDunderAllAssignment": "Expression assigned to \"__all__\" is not supported, so exported symbol list may be incorrect; use list or tuple of string literal values in assignment", "unsupportedDunderAllOperation": "Operation on \"__all__\" is not supported, so exported symbol list may not be incorrect", + "unusedCallResult": "Result of call expression is of type \"{type}\" and is not used; assign to variable \"_\" if this is intentional", "varAnnotationIllegal": "Type annotations for variables requires Python 3.6 or newer; use type comment for compatibility with previous versions", "walrusIllegal": "Operator \":=\" requires Python 3.8 or newer", "walrusNotAllowed": "Operator \":=\" not allowed in this context", diff --git a/packages/pyright-internal/src/tests/samples/unusedCallResult1.py b/packages/pyright-internal/src/tests/samples/unusedCallResult1.py new file mode 100644 index 000000000..048c06361 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/unusedCallResult1.py @@ -0,0 +1,54 @@ +# This sample tests the reportUnusedCallResult diagnostic rule. + +from typing import Any, Iterable, List, Union + + +def func1(): + pass + + +def func2(): + raise RuntimeError() + + +def func3() -> Any: + pass + + +def func4(): + return 3 + + +def func5(a: int) -> Union[int, List[int]]: + if a < 0: + return 5 + return [3] + + +def func6() -> Iterable[int]: + return [] + + +func1() + +func2() + +func3() + +# This should generate a diagnostic if reportUnusedCallResult is enabled. +func4() + +# This should generate a diagnostic if reportUnusedCallResult is enabled. +func5(3) + +# This should generate a diagnostic if reportUnusedCallResult is enabled. +func6() + +_, _ = func5(3), func6() + +_ = func5(3) + +_ = func5(func4()) + +for _ in func6(): + pass diff --git a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts index 86988b3a4..c661eda6d 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts @@ -1211,3 +1211,16 @@ test('ClassGetItem1', () => { TestUtils.validateResults(analysisResults, 0, 1); }); + +test('UnusedCallResult1', () => { + const configOptions = new ConfigOptions('.'); + + // By default, this is disabled. + let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedCallResult1.py'], configOptions); + TestUtils.validateResults(analysisResults, 0); + + // Enable it as an error. + configOptions.diagnosticRuleSet.reportUnusedCallResult = 'error'; + analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedCallResult1.py'], configOptions); + TestUtils.validateResults(analysisResults, 3); +}); diff --git a/packages/vscode-pyright/package.json b/packages/vscode-pyright/package.json index 40b375d6f..d326aa0c3 100644 --- a/packages/vscode-pyright/package.json +++ b/packages/vscode-pyright/package.json @@ -587,6 +587,17 @@ "error" ] }, + "reportUnusedCallResult": { + "type": "string", + "description": "Diagnostics for call expressions whose results are not consumed and are not None.", + "default": "none", + "enum": [ + "none", + "information", + "warning", + "error" + ] + }, "reportUnsupportedDunderAll": { "type": "string", "description": "Diagnostics for unsupported operations performed on __all__.", diff --git a/packages/vscode-pyright/schemas/pyrightconfig.schema.json b/packages/vscode-pyright/schemas/pyrightconfig.schema.json index 8da300943..a9878c0f8 100644 --- a/packages/vscode-pyright/schemas/pyrightconfig.schema.json +++ b/packages/vscode-pyright/schemas/pyrightconfig.schema.json @@ -387,6 +387,12 @@ "title": "Controls reporting of unsupported operations performed on __all__", "default": "warning" }, + "reportUnusedCallResult": { + "$id": "#/properties/reportUnusedCallResult", + "$ref": "#/definitions/diagnostic", + "title": "Controls reporting of call expressions whose results are not consumed", + "default": "none" + }, "pythonVersion": { "$id": "#/properties/pythonVersion", "type": "string",