Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
50 changes: 47 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5019,21 +5019,65 @@ def visit_if_stmt(self, s: IfStmt) -> None:

if_map, else_map = self.find_isinstance_check(e)

# XXX Issue a warning if condition is always False?
self._visit_if_stmt_redundant_expr_helper(
stmt=s, expr=e, body=b, if_map=if_map, else_map=else_map
)

with self.binder.frame_context(can_skip=True, fall_through=2):
self.push_type_map(if_map, from_assignment=False)
self.accept(b)

# XXX Issue a warning if condition is always True?
self.push_type_map(else_map, from_assignment=False)

with self.binder.frame_context(can_skip=False, fall_through=2):
if s.else_body:
self.accept(s.else_body)

def _visit_if_stmt_redundant_expr_helper(
self, stmt: IfStmt, expr: Expression, body: Block, if_map: TypeMap, else_map: TypeMap
) -> None:
"""Emits `redundant-expr` errors for if statements that are always true or always false.

We try to avoid emitting such errors if the redundancy seems to be intended as part of
dynamic type or exhaustiveness checking (risking to miss some unintended redundant if
statements).
"""

if codes.REDUNDANT_EXPR not in self.options.enabled_error_codes:
return
if refers_to_fullname(expr, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING")):
return

def _filter(body: Block | None) -> bool:
if body is None:
return False
s = body.body[0]
if isinstance(s, AssertStmt) and is_false_literal(s.expr):
return True
if isinstance(s, RaiseStmt):
return True
elif isinstance(s, ExpressionStmt) and isinstance(s.expr, CallExpr):
with self.expr_checker.msg.filter_errors(filter_revealed_type=True):
typ = self.expr_checker.accept(
s.expr, allow_none_return=True, always_allow_any=True
)
if isinstance(get_proper_type(typ), UninhabitedType):
return True
return False

if if_map is None:
if stmt.while_stmt:
self.msg.redundant_condition_in_while(expr)
elif not _filter(body):
self.msg.redundant_condition_in_if(False, expr)

if else_map is None and not stmt.while_stmt:
if not (isinstance(body.body[0], ReturnStmt) or _filter(stmt.else_body)):
self.msg.redundant_condition_in_if(True, expr)

def visit_while_stmt(self, s: WhileStmt) -> None:
"""Type check a while statement."""
if_stmt = IfStmt([s.expr], [s.body], None)
if_stmt = IfStmt([s.expr], [s.body], None, while_stmt=True)
if_stmt.set_line(s)
self.accept_loop(if_stmt, s.else_body, exit_condition=s.expr)

Expand Down
3 changes: 3 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2091,6 +2091,9 @@ def redundant_condition_in_comprehension(self, truthiness: bool, context: Contex
def redundant_condition_in_if(self, truthiness: bool, context: Context) -> None:
self.redundant_expr("If condition", truthiness, context)

def redundant_condition_in_while(self, context: Context) -> None:
self.redundant_expr("While condition", False, context)

def redundant_expr(self, description: str, truthiness: bool, context: Context) -> None:
self.fail(
f"{description} is always {str(truthiness).lower()}",
Expand Down
15 changes: 12 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1792,19 +1792,28 @@ def accept(self, visitor: StatementVisitor[T]) -> T:


class IfStmt(Statement):
__slots__ = ("expr", "body", "else_body")
__slots__ = ("expr", "body", "else_body", "while_stmt")

__match_args__ = ("expr", "body", "else_body")
__match_args__ = ("expr", "body", "else_body", "while_stmt")

expr: list[Expression]
body: list[Block]
else_body: Block | None
while_stmt: bool # Is the if statement a converted while statement?

def __init__(self, expr: list[Expression], body: list[Block], else_body: Block | None) -> None:
def __init__(
self,
expr: list[Expression],
body: list[Block],
else_body: Block | None,
*,
while_stmt: bool = False,
) -> None:
super().__init__()
self.expr = expr
self.body = body
self.else_body = else_body
self.while_stmt = while_stmt

def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_if_stmt(self)
Expand Down
95 changes: 95 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -3033,3 +3033,98 @@ if isinstance(a, B):
c = a

[builtins fixtures/isinstance.pyi]

[case testRedundantExpressionWarningsForIfStatements]
# flags: --enable-error-code=redundant-expr
from typing import Literal

if True: # E: If condition is always true
...

if False: # E: If condition is always false
...

class W:
...
if W():
...

class X:
def __bool__(self) -> bool: ...
if X():
...

class Y:
def __bool__(self) -> Literal[True]: ...
if Y(): # E: If condition is always true
...

class Z:
def __bool__(self) -> Literal[False]: ...
if Z(): # E: If condition is always false
...

[builtins fixtures/bool.pyi]

[case testRedundantExpressionWarningsForWhileStatements]
# flags: --enable-error-code=redundant-expr

x = None
while True:
if x:
break
x = True

while False: # E: While condition is always false
...

[builtins fixtures/bool.pyi]

[case testNoRedundantExpressionWarningsForExhaustivenessChecks]
# flags: --enable-error-code=redundant-expr
from typing import Literal, Never, NoReturn

def assert_never(x: Never) -> NoReturn:
...
def f1(x: Literal[1, 2]) -> str:
if x == 1:
y = "a"
elif x == 2:
y = "b"
else:
assert_never(x)
return y

class ValueError: ...
def f2(x: Literal[1]) -> str:
if x == 1:
y = "a"
else:
raise ValueError
return y

def f3(x: Literal[1]) -> str:
if x == 1:
y = "a"
else:
assert False
return y

def f4(x: Literal[1, 2]) -> int:
if x == 1:
return 1
if x == 2:
return 2

def f5(x: Literal[1, 2]) -> str:
if x == 1:
y = "a"
elif x == 2: # E: If condition is always true
y = "b"
return y

def f6(x: Literal[1]) -> None:
if x != 1:
raise ValueError

[builtins fixtures/bool.pyi]
Loading