diff --git a/doc/whatsnew/fragments/8714.false_negative b/doc/whatsnew/fragments/8714.false_negative new file mode 100644 index 0000000000..1f274a717b --- /dev/null +++ b/doc/whatsnew/fragments/8714.false_negative @@ -0,0 +1,3 @@ +Emit ``assignment-from-none`` for calls to builtin methods like ``dict.update()``. + +Closes #8714 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 7038f2bc35..11b367fec4 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -74,6 +74,28 @@ nodes.Arguments, nodes.FunctionDef, ) +BUILTINS_RETURN_NONE = { + "builtins.dict": {"clear", "update"}, + "builtins.list": { + "append", + "clear", + "extend", + "insert", + "remove", + "reverse", + "sort", + }, + "builtins.set": { + "add", + "clear", + "difference_update", + "discard", + "intersection_update", + "remove", + "symmetric_difference_update", + "update", + }, +} class VERSION_COMPATIBLE_OVERLOAD: @@ -1254,8 +1276,8 @@ def _check_assignment_from_function_call(self, node: nodes.Assign) -> None: ): return - # Fix a false-negative for list.sort(), see issue #5722 - if self._is_list_sort_method(node.value): + # Handle builtins such as list.sort() or dict.update() + if self._is_builtin_no_return(node): self.add_message("assignment-from-none", node=node, confidence=INFERENCE) return @@ -1290,11 +1312,14 @@ def _is_ignored_function( ) @staticmethod - def _is_list_sort_method(node: nodes.Call) -> bool: + def _is_builtin_no_return(node: nodes.Assign) -> bool: return ( - isinstance(node.func, nodes.Attribute) - and node.func.attrname == "sort" - and isinstance(utils.safe_infer(node.func.expr), nodes.List) + isinstance(node.value, nodes.Call) + and isinstance(node.value.func, nodes.Attribute) + and bool(inferred := utils.safe_infer(node.value.func.expr)) + and isinstance(inferred, bases.Instance) + and node.value.func.attrname + in BUILTINS_RETURN_NONE.get(inferred.pytype(), ()) ) def _check_dundername_is_string(self, node: nodes.Assign) -> None: diff --git a/tests/functional/a/assignment/assignment_from_no_return_2.py b/tests/functional/a/assignment/assignment_from_no_return_2.py index f42b665453..5df0eddaa5 100644 --- a/tests/functional/a/assignment/assignment_from_no_return_2.py +++ b/tests/functional/a/assignment/assignment_from_no_return_2.py @@ -33,6 +33,10 @@ def func_implicit_return_none(): lst = [3, 2] A = lst.sort() # [assignment-from-none] +my_dict = {3: 2} +B = my_dict.update({2: 1}) # [assignment-from-none] +my_set = set() +C = my_set.symmetric_difference_update([6]) # [assignment-from-none] def func_return_none_and_smth(): """function returning none and something else""" diff --git a/tests/functional/a/assignment/assignment_from_no_return_2.txt b/tests/functional/a/assignment/assignment_from_no_return_2.txt index bc1fff92bf..f18190c82a 100644 --- a/tests/functional/a/assignment/assignment_from_no_return_2.txt +++ b/tests/functional/a/assignment/assignment_from_no_return_2.txt @@ -2,3 +2,5 @@ assignment-from-no-return:17:0:17:20::Assigning result of a function call, where assignment-from-none:25:0:25:22::Assigning result of a function call, where the function returns None:UNDEFINED assignment-from-none:32:0:32:31::Assigning result of a function call, where the function returns None:UNDEFINED assignment-from-none:35:0:35:14::Assigning result of a function call, where the function returns None:INFERENCE +assignment-from-none:37:0:37:26::Assigning result of a function call, where the function returns None:INFERENCE +assignment-from-none:39:0:39:43::Assigning result of a function call, where the function returns None:INFERENCE