Skip to content

Commit a5a77f6

Browse files
authoredJun 21, 2024··
[possibly-used-before-assignment] Avoid FP for typing.NoReturn & Never (#9714)
1 parent fc9bdeb commit a5a77f6

File tree

6 files changed

+67
-17
lines changed

6 files changed

+67
-17
lines changed
 

‎doc/data/messages/p/possibly-used-before-assignment/details.rst

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ You can use ``assert_never`` to mark exhaustive choices:
1717
if suffix in "dmy":
1818
handle_date_suffix(suffix)
1919

20+
Or, instead of `assert_never()`, you can call a function with a return
21+
annotation of `Never` or `NoReturn`. Unlike in the general case, where
22+
by design pylint ignores type annotations and does its own static analysis,
23+
here, pylint treats these special annotations like a disable comment.
24+
2025
Pylint currently allows repeating the same test like this, even though this
2126
lets some error cases through, as pylint does not assess the intervening code:
2227

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Prevent emitting ``possibly-used-before-assignment`` when relying on names
2+
only potentially not defined in conditional blocks guarded by functions
3+
annotated with ``typing.Never`` or ``typing.NoReturn``.
4+
5+
Closes #9674

‎pylint/checkers/utils.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from astroid.nodes._base_nodes import ImportNode, Statement
2626
from astroid.typing import InferenceResult, SuccessfulInferenceResult
2727

28+
from pylint.constants import TYPING_NEVER, TYPING_NORETURN
29+
2830
if TYPE_CHECKING:
2931
from functools import _lru_cache_wrapper
3032

@@ -2150,7 +2152,9 @@ def is_singleton_const(node: nodes.NodeNG) -> bool:
21502152

21512153

21522154
def is_terminating_func(node: nodes.Call) -> bool:
2153-
"""Detect call to exit(), quit(), os._exit(), or sys.exit()."""
2155+
"""Detect call to exit(), quit(), os._exit(), sys.exit(), or
2156+
functions annotated with `typing.NoReturn` or `typing.Never`.
2157+
"""
21542158
if (
21552159
not isinstance(node.func, nodes.Attribute)
21562160
and not (isinstance(node.func, nodes.Name))
@@ -2165,6 +2169,25 @@ def is_terminating_func(node: nodes.Call) -> bool:
21652169
and inferred.qname() in TERMINATING_FUNCS_QNAMES
21662170
):
21672171
return True
2172+
# Unwrap to get the actual function node object
2173+
if isinstance(inferred, astroid.BoundMethod) and isinstance(
2174+
inferred._proxied, astroid.UnboundMethod
2175+
):
2176+
inferred = inferred._proxied._proxied
2177+
if (
2178+
isinstance(inferred, nodes.FunctionDef)
2179+
and isinstance(inferred.returns, nodes.Name)
2180+
and (inferred_func := safe_infer(inferred.returns))
2181+
and hasattr(inferred_func, "qname")
2182+
and inferred_func.qname()
2183+
in (
2184+
*TYPING_NEVER,
2185+
*TYPING_NORETURN,
2186+
# In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm'
2187+
"typing._SpecialForm",
2188+
)
2189+
):
2190+
return True
21682191
except (StopIteration, astroid.InferenceError):
21692192
pass
21702193

‎pylint/checkers/variables.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2630,7 +2630,7 @@ def _loopvar_name(self, node: astroid.Name) -> None:
26302630
else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue)
26312631
):
26322632
return
2633-
# TODO: 4.0: Consider using RefactoringChecker._is_function_def_never_returning
2633+
# TODO: 4.0: Consider using utils.is_terminating_func
26342634
if isinstance(else_stmt, nodes.Expr) and isinstance(
26352635
else_stmt.value, nodes.Call
26362636
):

‎tests/functional/u/used/used_before_assignment.py

+17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# pylint: disable=consider-using-f-string, missing-function-docstring
33
import datetime
44
import sys
5+
from typing import NoReturn
56

67
MSG = "hello %s" % MSG # [used-before-assignment]
78

@@ -205,3 +206,19 @@ def inner_if_continues_outer_if_has_no_other_statements():
205206
else:
206207
order = None
207208
print(order)
209+
210+
211+
class PlatformChecks:
212+
"""https://github.com/pylint-dev/pylint/issues/9674"""
213+
def skip(self, msg) -> NoReturn:
214+
raise Exception(msg) # pylint: disable=broad-exception-raised
215+
216+
def print_platform_specific_command(self):
217+
if sys.platform == "linux":
218+
cmd = "ls"
219+
elif sys.platform == "win32":
220+
cmd = "dir"
221+
else:
222+
self.skip("only runs on Linux/Windows")
223+
224+
print(cmd)
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
used-before-assignment:6:19:6:22::Using variable 'MSG' before assignment:HIGH
2-
used-before-assignment:8:20:8:24::Using variable 'MSG2' before assignment:HIGH
3-
used-before-assignment:11:4:11:9:outer:Using variable 'inner' before assignment:HIGH
4-
used-before-assignment:20:20:20:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH
5-
used-before-assignment:24:0:24:9::Using variable 'calculate' before assignment:HIGH
6-
used-before-assignment:32:10:32:14:redefine_time_import:Using variable 'time' before assignment:HIGH
7-
used-before-assignment:46:3:46:7::Using variable 'VAR2' before assignment:INFERENCE
8-
possibly-used-before-assignment:64:3:64:7::Possibly using variable 'VAR4' before assignment:INFERENCE
9-
possibly-used-before-assignment:74:3:74:7::Possibly using variable 'VAR5' before assignment:INFERENCE
10-
used-before-assignment:79:3:79:7::Using variable 'VAR6' before assignment:INFERENCE
11-
used-before-assignment:114:6:114:11::Using variable 'VAR10' before assignment:INFERENCE
12-
possibly-used-before-assignment:120:6:120:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW
13-
used-before-assignment:151:10:151:14::Using variable 'SALE' before assignment:INFERENCE
14-
used-before-assignment:183:10:183:18::Using variable 'ALL_DONE' before assignment:INFERENCE
15-
used-before-assignment:194:6:194:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE
1+
used-before-assignment:7:19:7:22::Using variable 'MSG' before assignment:HIGH
2+
used-before-assignment:9:20:9:24::Using variable 'MSG2' before assignment:HIGH
3+
used-before-assignment:12:4:12:9:outer:Using variable 'inner' before assignment:HIGH
4+
used-before-assignment:21:20:21:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH
5+
used-before-assignment:25:0:25:9::Using variable 'calculate' before assignment:HIGH
6+
used-before-assignment:33:10:33:14:redefine_time_import:Using variable 'time' before assignment:HIGH
7+
used-before-assignment:47:3:47:7::Using variable 'VAR2' before assignment:INFERENCE
8+
possibly-used-before-assignment:65:3:65:7::Possibly using variable 'VAR4' before assignment:INFERENCE
9+
possibly-used-before-assignment:75:3:75:7::Possibly using variable 'VAR5' before assignment:INFERENCE
10+
used-before-assignment:80:3:80:7::Using variable 'VAR6' before assignment:INFERENCE
11+
used-before-assignment:115:6:115:11::Using variable 'VAR10' before assignment:INFERENCE
12+
possibly-used-before-assignment:121:6:121:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW
13+
used-before-assignment:152:10:152:14::Using variable 'SALE' before assignment:INFERENCE
14+
used-before-assignment:184:10:184:18::Using variable 'ALL_DONE' before assignment:INFERENCE
15+
used-before-assignment:195:6:195:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE

0 commit comments

Comments
 (0)
Please sign in to comment.