-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bugfix - assignment-from-none
False Positive #7853
#7886
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Fix False Positive issue regarding a complex ``assignment-from-none``. | ||
|
||
Closes #7853. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -43,6 +43,7 @@ | |||||
node_ignores_exception, | ||||||
only_required_for_messages, | ||||||
safe_infer, | ||||||
safe_infer_multiple, | ||||||
supports_delitem, | ||||||
supports_getitem, | ||||||
supports_membership_test, | ||||||
|
@@ -1212,38 +1213,49 @@ def _check_assignment_from_function_call(self, node: nodes.Assign) -> None: | |||||
""" | ||||||
if not isinstance(node.value, nodes.Call): | ||||||
return | ||||||
|
||||||
function_node = safe_infer(node.value.func) | ||||||
funcs = (nodes.FunctionDef, astroid.UnboundMethod, astroid.BoundMethod) | ||||||
if not isinstance(function_node, funcs): | ||||||
ASTROID_FUNC_TYPES = ( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make this a frozen set at either the Module or Class level. |
||||||
nodes.FunctionDef, | ||||||
astroid.UnboundMethod, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally these get imported from |
||||||
astroid.BoundMethod, | ||||||
) | ||||||
return_nodes = [] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add annotation here? |
||||||
function_nodes: Any | None = safe_infer_multiple(node.value.func) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
if not function_nodes: | ||||||
return | ||||||
for function_node in function_nodes: | ||||||
|
||||||
# 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 | ||||||
if not isinstance(function_node, ASTROID_FUNC_TYPES): | ||||||
return | ||||||
|
||||||
# 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 self._is_ignored_function(function_node) | ||||||
): | ||||||
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 | ||||||
|
||||||
# Fix a false-negative for list.sort(), see issue #5722 | ||||||
if self._is_list_sort_method(node.value): | ||||||
self.add_message("assignment-from-none", node=node, confidence=INFERENCE) | ||||||
return | ||||||
# 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this really necessary? Edit: Ah I see that most of this is just indenting. Hmm, well we could also fix this in a follow up, but it feels unnecessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The purpose of refactoring |
||||||
or function_node.decorators | ||||||
or self._is_ignored_function(function_node) | ||||||
): | ||||||
return | ||||||
|
||||||
if not function_node.root().fully_defined(): | ||||||
return | ||||||
# Fix a false-negative for list.sort(), see issue #5722 | ||||||
if self._is_list_sort_method(node.value): | ||||||
self.add_message( | ||||||
"assignment-from-none", node=node, confidence=INFERENCE | ||||||
) | ||||||
return | ||||||
|
||||||
if not function_node.root().fully_defined(): | ||||||
return | ||||||
|
||||||
return_nodes.extend( | ||||||
function_node.nodes_of_class(nodes.Return, skip_klass=nodes.FunctionDef) | ||||||
) | ||||||
|
||||||
return_nodes = list( | ||||||
function_node.nodes_of_class(nodes.Return, skip_klass=nodes.FunctionDef) | ||||||
) | ||||||
if not return_nodes: | ||||||
self.add_message("assignment-from-no-return", node=node) | ||||||
else: | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,10 +14,10 @@ | |
import string | ||
import warnings | ||
from collections import deque | ||
from collections.abc import Iterable, Iterator | ||
from collections.abc import Generator, Iterable, Iterator | ||
from functools import lru_cache, partial | ||
from re import Match | ||
from typing import TYPE_CHECKING, Callable, TypeVar | ||
from typing import TYPE_CHECKING, Any, Callable, TypeVar | ||
|
||
import _string | ||
import astroid.objects | ||
|
@@ -1361,45 +1361,95 @@ def safe_infer( | |
If compare_constants is True and if multiple constants are inferred, | ||
unequal inferred values are also considered ambiguous and return None. | ||
""" | ||
inferred_types: set[str | None] = set() | ||
infer_gen = node.infer(context=context) | ||
first_infer = _safe_infer_validate_first(infer_gen) | ||
try: | ||
infer_gen = node.infer(context=context) | ||
value = next(infer_gen) | ||
for inferred in infer_gen: | ||
is_same_type = _safe_infer_compare_to_first( | ||
first_infer, inferred, compare_constants | ||
) | ||
if not is_same_type: | ||
return None | ||
except astroid.InferenceError: | ||
return None | ||
return None # There is some kind of ambiguity | ||
except StopIteration: | ||
return first_infer | ||
except Exception as e: # pragma: no cover | ||
raise AstroidError from e | ||
return first_infer | ||
|
||
if value is not astroid.Uninferable: | ||
inferred_types.add(_get_python_type_of_node(value)) | ||
|
||
@lru_cache(maxsize=1024) | ||
def safe_infer_multiple( | ||
node: nodes.NodeNG, | ||
context: InferenceContext | None = None, | ||
*, | ||
compare_constants: bool = False, | ||
) -> Any | None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I asked myself the same question. But this is what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's because |
||
infer_gen = node.infer(context=context) | ||
first_infer = _safe_infer_validate_first(infer_gen) | ||
try: | ||
for inferred in infer_gen: | ||
inferred_type = _get_python_type_of_node(inferred) | ||
if inferred_type not in inferred_types: | ||
return None # If there is ambiguity on the inferred node. | ||
if ( | ||
compare_constants | ||
and isinstance(inferred, nodes.Const) | ||
and isinstance(value, nodes.Const) | ||
and inferred.value != value.value | ||
): | ||
is_same_type = _safe_infer_compare_to_first( | ||
first_infer, inferred, compare_constants | ||
) | ||
if not is_same_type: | ||
return None | ||
if ( | ||
isinstance(inferred, nodes.FunctionDef) | ||
and inferred.args.args is not None | ||
and isinstance(value, nodes.FunctionDef) | ||
and value.args.args is not None | ||
and len(inferred.args.args) != len(value.args.args) | ||
): | ||
return None # Different number of arguments indicates ambiguity | ||
except astroid.InferenceError: | ||
return None # There is some kind of ambiguity | ||
except StopIteration: | ||
return value | ||
if first_infer is None: | ||
return None | ||
return node.infer(context=context) | ||
except Exception as e: # pragma: no cover | ||
raise AstroidError from e | ||
if first_infer is None: | ||
return None | ||
return node.infer(context=context) | ||
|
||
|
||
def _safe_infer_validate_first( | ||
infer_gen: Generator[InferenceResult, None, None] | ||
) -> InferenceResult | None: | ||
try: | ||
value = next(infer_gen) | ||
except astroid.InferenceError: | ||
return None | ||
except Exception as e: # pragma: no cover | ||
raise AstroidError from e | ||
return value | ||
|
||
|
||
def _safe_infer_compare_to_first( | ||
first_node: InferenceContext | None, | ||
inferred_node: InferenceContext | None, | ||
compare_constants: bool = False, | ||
) -> bool: | ||
try: | ||
inferred_type = _get_python_type_of_node(inferred_node) | ||
if ( | ||
first_node is not astroid.Uninferable | ||
and inferred_type != _get_python_type_of_node(first_node) | ||
): | ||
return False | ||
if ( | ||
compare_constants | ||
and isinstance(inferred_node, nodes.Const) | ||
and isinstance(first_node, nodes.Const) | ||
and inferred_node.value != first_node.value | ||
): | ||
return False | ||
if ( | ||
isinstance(inferred_node, nodes.FunctionDef) | ||
and inferred_node.args.args is not None | ||
and isinstance(first_node, nodes.FunctionDef) | ||
and first_node.args.args is not None | ||
and len(first_node.args.args) != len(inferred_node.args.args) | ||
): | ||
return False # Different number of arguments indicates ambiguity | ||
except Exception as e: # pragma: no cover | ||
raise AstroidError from e | ||
return value if len(inferred_types) <= 1 else None | ||
return True | ||
|
||
|
||
@lru_cache(maxsize=512) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
""" | ||
Check False positive issue #7853 | ||
""" | ||
# pylint: disable=missing-function-docstring | ||
|
||
|
||
def get_func(param): | ||
if param is None: | ||
def func(): | ||
return None | ||
else: | ||
def func(): | ||
return param | ||
return func | ||
|
||
|
||
def process_val(param): | ||
|
||
func_a = get_func(param) | ||
val = func_a() | ||
# Do stuff with 'val'; *None* is an useful case. | ||
return val |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might need some more specific details, but we can figure that out later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. yeah we just don't know what is the final solution yet.