diff --git a/doc/data/messages/f/function-return-not-assigned/bad.py b/doc/data/messages/f/function-return-not-assigned/bad.py new file mode 100644 index 0000000000..779ce785d5 --- /dev/null +++ b/doc/data/messages/f/function-return-not-assigned/bad.py @@ -0,0 +1,5 @@ +def return_int(): + return 1 + + +return_int() # [function-return-not-assigned] diff --git a/doc/data/messages/f/function-return-not-assigned/good.py b/doc/data/messages/f/function-return-not-assigned/good.py new file mode 100644 index 0000000000..f409a51e16 --- /dev/null +++ b/doc/data/messages/f/function-return-not-assigned/good.py @@ -0,0 +1,5 @@ +def return_int(): + return 1 + + +_ = return_int() diff --git a/doc/whatsnew/fragments/7935.new_check b/doc/whatsnew/fragments/7935.new_check new file mode 100644 index 0000000000..9d80757362 --- /dev/null +++ b/doc/whatsnew/fragments/7935.new_check @@ -0,0 +1,3 @@ +Add ``FunctionReturnNotAssignedChecker`` extension and new ``function-return-not-assigned`` message if return value not used. + +Refs #7935 diff --git a/pylint/extensions/function_return_not_assigned.py b/pylint/extensions/function_return_not_assigned.py new file mode 100644 index 0000000000..3fe3dd8d86 --- /dev/null +++ b/pylint/extensions/function_return_not_assigned.py @@ -0,0 +1,91 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Looks for unassigned function/method calls that have non-nullable return type.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import astroid +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.typecheck import TypeChecker +from pylint.checkers.utils import only_required_for_messages, safe_infer + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class FunctionReturnNotAssignedChecker(BaseChecker): + name = "function_return_not_assigned" + msgs = { + "W5486": ( + "Function returned value which is never used", + "function-return-not-assigned", + "Function returns non-nullable value which is never used. " + "Use explicit `_ = func_call()` if you are not interested in returned value", + ) + } + + @only_required_for_messages("function-return-not-assigned") + def visit_call(self, node: nodes.Call) -> None: + result_is_used = not isinstance(node.parent, nodes.Expr) + + if result_is_used: + return + + function_node = safe_infer(node.func) + funcs = (nodes.FunctionDef, astroid.UnboundMethod, astroid.BoundMethod) + + # FIXME: more elegant solution probably exists + # methods called on instances returned by functions in some libraries + # are having function_node None and needs to be handled here + # for example: + # attrs.evolve returned instances + # instances returned by any pyrsistent method (pmap.set, pvector.append, ...) + if function_node is None: + try: + for n in node.func.infer(): + if not isinstance(n, astroid.BoundMethod): + continue + function_node = n + break + except Exception: # pylint:disable=broad-exception-caught + pass + + if not isinstance(function_node, funcs): + return + + # Unwrap to get the actual function node object + if isinstance(function_node, astroid.BoundMethod) and isinstance( + function_node._proxied, astroid.UnboundMethod + ): + function_node = function_node._proxied._proxied + + # Make sure that it's a valid function that we can analyze. + # Ordered from less expensive to more expensive checks. + if ( + not function_node.is_function + or function_node.decorators + or TypeChecker._is_ignored_function(function_node) + ): + return + + return_nodes = list( + function_node.nodes_of_class(nodes.Return, skip_klass=nodes.FunctionDef) + ) + for ret_node in return_nodes: + if not ( + isinstance(ret_node.value, nodes.Const) + and ret_node.value.value is None + or ret_node.value is None + ): + self.add_message("function-return-not-assigned", node=node) + return + + +def register(linter: PyLinter) -> None: + linter.register_checker(FunctionReturnNotAssignedChecker(linter)) diff --git a/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.py b/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.py new file mode 100644 index 0000000000..ac9e69ba80 --- /dev/null +++ b/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.py @@ -0,0 +1,56 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring, expression-not-assigned, invalid-name +from dataclasses import dataclass, replace + + +def func_that_returns_something(): + return 1 + + +func_that_returns_something() # [function-return-not-assigned] + +_ = func_that_returns_something() + +if func_that_returns_something(): + pass + + +def func_that_returns_none(): + return None + + +def func_with_no_explicit_return(): + print("I am doing something") + + +func_that_returns_none() +func_with_no_explicit_return() + +some_var = "" +# next line should probably raise? +func_that_returns_something() if some_var else func_that_returns_none() +_ = func_that_returns_something() if some_var else func_that_returns_none() +func_with_no_explicit_return() if some_var else func_that_returns_none() + + +@dataclass +class TestClass: + value: int + + def return_self(self): + return self + + def return_none(self): + pass + + +inst = TestClass(1) +inst.return_self() # [function-return-not-assigned] +inst.return_none() + +replace(inst, value=3) # [function-return-not-assigned] + +inst = replace(inst, value=3) + +inst.return_self() # [function-return-not-assigned] +inst.return_none() +inst = inst.return_self() diff --git a/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.rc b/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.rc new file mode 100644 index 0000000000..3b66585607 --- /dev/null +++ b/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.rc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.function_return_not_assigned, diff --git a/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.txt b/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.txt new file mode 100644 index 0000000000..5da590fa7a --- /dev/null +++ b/tests/functional/ext/function_return_not_assigned/function_return_not_assigned.txt @@ -0,0 +1,4 @@ +function-return-not-assigned:9:0:9:29::Function returned value which is never used:UNDEFINED +function-return-not-assigned:47:0:47:18::Function returned value which is never used:UNDEFINED +function-return-not-assigned:50:0:50:22::Function returned value which is never used:UNDEFINED +function-return-not-assigned:54:0:54:18::Function returned value which is never used:UNDEFINED diff --git a/tests/functional/f/function_return_not_assigned.txt b/tests/functional/f/function_return_not_assigned.txt new file mode 100644 index 0000000000..e69de29bb2