Skip to content

Emit using-constant-test when testing truth value of a variable holding a generator #6916

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

Merged
merged 5 commits into from
Jun 12, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 5 additions & 4 deletions doc/whatsnew/2/2.15/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,16 @@ False negatives fixed

Closes #6648

* Emit ``using-constant-test`` when testing the truth value of a variable or call result
holding a generator.

Closes #6909

* Emit ``used-before-assignment`` for self-referencing named expressions (``:=``) lacking
prior assignments.

Closes #5653

* Emit ``using-constant-test`` when testing the truth value of a call that only returns generators.

Closes #6909


Other bug fixes
===============
Expand Down
37 changes: 36 additions & 1 deletion pylint/checkers/base/basic_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,17 @@ def _check_using_constant_test(
)
inferred = None
emit = isinstance(test, (nodes.Const,) + structs + const_nodes)
maybe_generator_call = None
if not isinstance(test, except_nodes):
inferred = utils.safe_infer(test)
if inferred is astroid.Uninferable and isinstance(test, nodes.Name):
emit, maybe_generator_call = BasicChecker._name_holds_generator(test)

# Emit if calling a function that only returns GeneratorExp (always tests True)
elif isinstance(test, nodes.Call):
inferred_call = utils.safe_infer(test.func)
maybe_generator_call = test
if maybe_generator_call:
inferred_call = utils.safe_infer(maybe_generator_call.func)
if isinstance(inferred_call, nodes.FunctionDef):
# Can't use all(x) or not any(not x) for this condition, because it
# will return True for empty generators, which is not what we want.
Expand Down Expand Up @@ -361,6 +367,35 @@ def _check_using_constant_test(
pass
self.add_message("using-constant-test", node=test, confidence=INFERENCE)

@staticmethod
def _name_holds_generator(test: nodes.Name) -> tuple[bool, nodes.Call | None]:
"""Return whether `test` tests a name certain to hold a generator, or optionally
a call that should be then tested to see if *it* returns only generators.
"""
assert isinstance(test, nodes.Name)
emit = False
maybe_generator_call = None
lookup_result = test.frame(future=True).lookup(test.name)
if lookup_result:
maybe_generator_assigned = (
isinstance(assign_name.parent.value, nodes.GeneratorExp)
for assign_name in lookup_result[1]
if isinstance(assign_name.parent, nodes.Assign)
)
first_item = next(maybe_generator_assigned, None)
if first_item is not None:
# Emit if this variable is certain to hold a generator
if all(itertools.chain((first_item,), maybe_generator_assigned)):
emit = True
# If this variable holds the result of a call, save it for next test
elif (
len(lookup_result[1]) == 1
and isinstance(lookup_result[1][0].parent, nodes.Assign)
and isinstance(lookup_result[1][0].parent.value, nodes.Call)
):
maybe_generator_call = lookup_result[1][0].parent.value
return emit, maybe_generator_call

def visit_module(self, _: nodes.Module) -> None:
"""Check module name, docstring and required arguments."""
self.linter.stats.node_count["module"] += 1
Expand Down
13 changes: 13 additions & 0 deletions tests/functional/u/using_constant_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,16 @@ def maybe_get_generator(arg):

if maybe_get_generator(None):
pass

y = (a for a in range(10))
if y: # [using-constant-test]
pass

z = (a for a in range(10))
z = "red herring"
if z:
pass

gen = get_generator()
if gen: # [using-constant-test]
pass
2 changes: 2 additions & 0 deletions tests/functional/u/using_constant_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ using-constant-test:89:3:89:15::Using a conditional statement with a constant va
using-constant-test:93:3:93:18::Using a conditional statement with a constant value:INFERENCE
comparison-of-constants:117:3:117:8::"Comparison between constants: '2 < 3' has a constant value":HIGH
using-constant-test:156:0:157:8::Using a conditional statement with a constant value:INFERENCE
using-constant-test:168:3:168:4::Using a conditional statement with a constant value:UNDEFINED
Copy link
Member

Choose a reason for hiding this comment

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

This message has confidence "undefined", it's a little strange but we can merge anyway, I'm just pointing it out in case you did not notice.

using-constant-test:177:0:178:8::Using a conditional statement with a constant value:INFERENCE