Skip to content
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

Closed
Closed
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 .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/7853.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix False Positive issue regarding a complex ``assignment-from-none``.
Copy link
Collaborator

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.

Copy link
Contributor Author

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.


Closes #7853.
64 changes: 38 additions & 26 deletions pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
node_ignores_exception,
only_required_for_messages,
safe_infer,
safe_infer_multiple,
supports_delitem,
supports_getitem,
supports_membership_test,
Expand Down Expand Up @@ -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 = (
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally these get imported from bases.

astroid.BoundMethod,
)
return_nodes = []
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function_nodes: Any | None = safe_infer_multiple(node.value.func)
function_nodes = safe_infer_multiple(node.value.func)

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of refactoring safe_infer was only to separate it to functions. Any logic should not have been modified.

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:
Expand Down
104 changes: 77 additions & 27 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this Any?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked myself the same question. But this is what mypy accepted. :S

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's because mypy doesn't do any checking on astroid. It should be InferenceResult.

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)
Expand Down
22 changes: 22 additions & 0 deletions tests/functional/a/assignment/assignment_from_none_7853.py
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