Repository: pylint-dev/pylint

Issue:
possibly-used-before-assignment doesn't understand never-returning-functions
### Bug description

```python
# pylint: disable=missing-module-docstring,invalid-name
import sys

if input("Enter something or nothing: "):
    sys.exit()
else:
    a = 1
print(a)
```


### Configuration

_No response_

### Command used

```shell
pylint test.py
```


### Pylint output

```python
************* Module test
test.py:8:6: E0606: Possibly using variable 'a' before assignment (possibly-used-before-assignment)
```


### Expected behavior

Branches which end in a function which never returns shouldn't be considered as possible causes of never defined variables.

### Pylint version

```shell
pylint 3.2.0
astroid 3.2.0
Python 3.11.7 | packaged by conda-forge | (main, Dec 23 2023, 14:43:09) [GCC 12.3.0]
```


### OS / Environment

_No response_

### Additional dependencies

_No response_


Discussion:
I don't think is this a false positive. Variable `a` really is possibly undefined, in the sense that there are branches in which it is used but not defined. For example, if `sys.exit` is tampered with, a `NameError` can be raised:

```python
import sys

sys.exit = lambda: None
if input("Enter something or nothing: "):
    sys.exit()
else:
    a = 1
print(a)
```

There is an easy way to silence this warning, namely to define `a` unconditionally:

```python
import sys

if input("Enter something or nothing: "):
    sys.exit()

a = 1
print(a)
```
I think the proposal is to use the existing option `never-returning-functions`, which I think is a good idea and simple fix. The risk of tampering is almost nil, and it's under the user's control, if they want to test more rigorously they can supply an empty array to that setting.
Looks like `never-returning-functions` is documented as only dealing with one option, and there's already a TODO statement in the code to reuse it in pylint 4 for other purposes, so that's maybe better deferred.

In the meantime, we can solve the reporter's issue with using the existing util `is_terminating_func`.
Thanks a lot for the quick fix!



Patch:
diff --git a/doc/whatsnew/fragments/9627.false_positive b/doc/whatsnew/fragments/9627.false_positive
new file mode 100644
index 0000000000..2a9edb26d8
--- /dev/null
+++ b/doc/whatsnew/fragments/9627.false_positive
@@ -0,0 +1,4 @@
+Exclude if/else branches containing terminating functions (e.g. `sys.exit()`)
+from `possibly-used-before-assignment` checks.
+
+Closes #9627
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index 0a5a60c1b0..39c9051ed0 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -940,6 +940,12 @@ def _uncertain_nodes_in_except_blocks(
     def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool:
         if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return, nodes.Continue)):
             return True
+        if (
+            isinstance(node, nodes.Expr)
+            and isinstance(node.value, nodes.Call)
+            and utils.is_terminating_func(node.value)
+        ):
+            return True
         if (
             isinstance(node, nodes.AnnAssign)
             and node.value


Test patch:
diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py
index 83f85cc215..c706af043f 100644
--- a/tests/functional/u/used/used_before_assignment.py
+++ b/tests/functional/u/used/used_before_assignment.py
@@ -1,6 +1,7 @@
 """Miscellaneous used-before-assignment cases"""
 # pylint: disable=consider-using-f-string, missing-function-docstring
 import datetime
+import sys
 
 MSG = "hello %s" % MSG  # [used-before-assignment]
 
@@ -118,6 +119,12 @@ def redefine_time_import_with_global():
         VAR12 = False
 print(VAR12)  # [possibly-used-before-assignment]
 
+if input("This tests terminating functions: "):
+    sys.exit()
+else:
+    VAR13 = 1
+print(VAR13)
+
 def turn_on2(**kwargs):
     """https://github.com/pylint-dev/pylint/issues/7873"""
     if "brightness" in kwargs:
diff --git a/tests/functional/u/used/used_before_assignment.txt b/tests/functional/u/used/used_before_assignment.txt
index 127a5d7de4..6f97f4324c 100644
--- a/tests/functional/u/used/used_before_assignment.txt
+++ b/tests/functional/u/used/used_before_assignment.txt
@@ -1,15 +1,15 @@
-used-before-assignment:5:19:5:22::Using variable 'MSG' before assignment:HIGH
-used-before-assignment:7:20:7:24::Using variable 'MSG2' before assignment:HIGH
-used-before-assignment:10:4:10:9:outer:Using variable 'inner' before assignment:HIGH
-used-before-assignment:19:20:19:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH
-used-before-assignment:23:0:23:9::Using variable 'calculate' before assignment:HIGH
-used-before-assignment:31:10:31:14:redefine_time_import:Using variable 'time' before assignment:HIGH
-used-before-assignment:45:3:45:7::Using variable 'VAR2' before assignment:INFERENCE
-possibly-used-before-assignment:63:3:63:7::Possibly using variable 'VAR4' before assignment:INFERENCE
-possibly-used-before-assignment:73:3:73:7::Possibly using variable 'VAR5' before assignment:INFERENCE
-used-before-assignment:78:3:78:7::Using variable 'VAR6' before assignment:INFERENCE
-used-before-assignment:113:6:113:11::Using variable 'VAR10' before assignment:INFERENCE
-possibly-used-before-assignment:119:6:119:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW
-used-before-assignment:144:10:144:14::Using variable 'SALE' before assignment:INFERENCE
-used-before-assignment:176:10:176:18::Using variable 'ALL_DONE' before assignment:INFERENCE
-used-before-assignment:187:6:187:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE
+used-before-assignment:6:19:6:22::Using variable 'MSG' before assignment:HIGH
+used-before-assignment:8:20:8:24::Using variable 'MSG2' before assignment:HIGH
+used-before-assignment:11:4:11:9:outer:Using variable 'inner' before assignment:HIGH
+used-before-assignment:20:20:20:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH
+used-before-assignment:24:0:24:9::Using variable 'calculate' before assignment:HIGH
+used-before-assignment:32:10:32:14:redefine_time_import:Using variable 'time' before assignment:HIGH
+used-before-assignment:46:3:46:7::Using variable 'VAR2' before assignment:INFERENCE
+possibly-used-before-assignment:64:3:64:7::Possibly using variable 'VAR4' before assignment:INFERENCE
+possibly-used-before-assignment:74:3:74:7::Possibly using variable 'VAR5' before assignment:INFERENCE
+used-before-assignment:79:3:79:7::Using variable 'VAR6' before assignment:INFERENCE
+used-before-assignment:114:6:114:11::Using variable 'VAR10' before assignment:INFERENCE
+possibly-used-before-assignment:120:6:120:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW
+used-before-assignment:151:10:151:14::Using variable 'SALE' before assignment:INFERENCE
+used-before-assignment:183:10:183:18::Using variable 'ALL_DONE' before assignment:INFERENCE
+used-before-assignment:194:6:194:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE