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

Conversation

orSolocate
Copy link
Contributor

Type of Changes

Type
βœ“ πŸ› Bug fix
✨ New feature
πŸ”¨ Refactoring
πŸ“œ Docs

Description

See comment about this PR in this comment.

Closes #7853

@orSolocate
Copy link
Contributor Author

That is interesting:

Expected in testdata:
  35: possibly-unused-variable

Unexpected in testdata:
  35: unused-variable
FAILED tests/test_functional.py::test_functional[useless_with_lock] - AssertionError: Wrong results for file "useless_with_lock":

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

We should definitely not touch safe_infer for something happening in a particular checker, this is going to affect a lot of code. What we can do is make astroid better for a specific piece of code it parse, or make the checker handle a specific piece of code it analyses.

@jacobtylerwalls jacobtylerwalls changed the title bugfix - assignment-from-none False Positive #7853 bugfix - assignment-from-none False Positive #7853 Dec 2, 2022
@orSolocate
Copy link
Contributor Author

We should definitely not touch safe_infer for something happening in a particular checker, this is going to affect a lot of code.

I uploaded a fix that refactors the safe_infer method and using its parts to build a safe_infer_multiple function. It took a lot of effort to make all regression tests pass locally.. since there are many logical flows :(
The safe_infer_multiple returns all return nodes and checks all of them, no matter the order whatsoever. The typecheck checker use this fucntion to check for the assignment-from-none and the assignment-no-return messages.

There were some unnecessary things in the original function, like the use of a set that can have a maximum of 1 element, and then verifying that in a condition in the return statement..

@github-actions
Copy link
Contributor

github-actions bot commented Dec 3, 2022

πŸ€– Effect of this PR on checked open source code: πŸ€–

Effect on sentry:
The following messages are no longer emitted:

  1. assignment-from-no-return:
    Assigning result of a function call, where the function has no return
    https://github.com/getsentry/sentry/blob/916fe38f45a23d27f2bde851e6521b66c1404299/src/sentry/models/userip.py#L47

This comment was generated for commit b992e12

@orSolocate
Copy link
Contributor Author

orSolocate commented Dec 3, 2022

EDIT:

Not sure where it fails, when I run test_runner_with_arguments[run_pylint] test locally it passes..

The /home/runner/work/pylint/pylint/tests/test_pylint_runners.py:38:4: E1111: Assigning result of a function call, where the function has no return (assignment-from-no-return) error does not pop out on my machine...

When I run the tox tests I get the error.. is it something that has to do with changing something about the tox environment? Could anybody help solve this mystery?

@DanielNoord DanielNoord self-requested a review December 3, 2022 14:32
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

I'm still not sure whether I like this approach. It feels like we're complicating the safe_infer util for a very specific use case.
We could also consider doing the double infer call in the checker itself. We would need to duplicate a little bit of safe_infer inside the checker, but at least the safe_infer function remains somewhat legible.

Perhaps other maintainers have an opinion here?

@@ -0,0 +1,3 @@
# Default ignored files
Copy link
Collaborator

Choose a reason for hiding this comment

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

This shouldn't be committed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah my bad, will remove it in my next commit

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

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.

astroid.BoundMethod,
)
return_nodes = []
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)

astroid.UnboundMethod,
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?

if not isinstance(function_node, funcs):
ASTROID_FUNC_TYPES = (
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.

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

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.

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

We might need to extend safe_infer, but I consider it to be an API. It's used too much in pylint as well as in lib depending on it, to be open to modification.

@orSolocate
Copy link
Contributor Author

I agree that this function is super fragile. I had a really hard time making all the functional tests pass..

The purpose of the change was just to refactor safe_infer to be able to use its parts in a different function to be used by the problematic checker. The only parts that changed is that the inferred_types set is removed, since it can contain a maximum of 1 element, and the ending return else case removed, since it can never be reached, for the same reason.

@orSolocate
Copy link
Contributor Author

We could also consider doing the double infer call in the checker itself.

@DanielNoord - I think I didn't understand the suggestion here, could you explain more on that please?

@DanielNoord
Copy link
Collaborator

We could also consider doing the double infer call in the checker itself.

@DanielNoord - I think I didn't understand the suggestion here, could you explain more on that please?

The issue is that for some nodes we want to do a double inference right? So not call safe_infer once, but actually twice? Can't we hide that logic inside the checker?

@orSolocate
Copy link
Contributor Author

orSolocate commented Dec 4, 2022

EDIT: first of all thank you for reviewing the code and for your insights, they've always been very significant. Even though you didn't like the spirit of this PR initially..

The issue is that for some nodes we want to do a double inference right? So not call safe_infer once, but actually twice? Can't we hide that logic inside the checker?

Not necessary twice, for our case which is a function infer, you want safe_infer to retrieve all function declarations, it can theoretically be any number, see this example:

"""

def get_func(param):
    if param is None:
        def func():
            return None
    elif isinstance(param, int):
        def func():
            return str(param)
    else:
        def func():
            return param
    return func

"""

In this case you want to check return nodes for all 3 functionDefs.
The safe_infer returns only the first functionDef, so you can't get around it the way I see it without modifying what it returns..

@DanielNoord
Copy link
Collaborator

Can't we get away with calling .infer() then? We'd need to catch StopIteration and InferenceError, but we would get access to all inferred nodes.

@jacobtylerwalls
Copy link
Member

safe_infer()'s docstring should really be updated to say return the first inferred value to avoid confusion.

@jacobtylerwalls
Copy link
Member

We already have a cached util for getting all inferred values, it's utils.infer_all().

@jacobtylerwalls
Copy link
Member

Thanks for helping think through these issues. I think changing the assignment-from-none checker to use infer_all() instead of safe_infer() is the right approach. Feel free to reopen or start with a fresh PR if you'd like to continue with the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

False Positive - assignment-from-none raised when function not necessarily returns None
4 participants