Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/8714.false_negative
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Emit ``assignment-from-none`` for calls to builtin methods like ``dict.update()``.

Closes #8714
37 changes: 31 additions & 6 deletions pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions tests/functional/a/assignment/assignment_from_no_return_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/a/assignment/assignment_from_no_return_2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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