Repository: pylint-dev/pylint

Issue:
# False positive unreachable error when overload + NoReturn is used

### Bug description

Below code should be valid but pylint finds an issue. It seems to be caused by the NoReturn

```python
# x.py
from y import create_client as mart_create_client
client = mart_create_client(version=2)
x = 1

# y.py
from typing import Literal, NoReturn, overload

@overload
def create_client(version: int = ...) -> NoReturn: ...

@overload
def create_client(version: Literal[2] = ...) -> int: ...

def create_client(version: int = 2) -> int:
    return 1
```

### Command used

```shell
pylint x.py
```

### Pylint output

```python
x.py(6): [W0101(unreachable)] Unreachable code
```

### Expected behavior

no error

### Pylint version

```shell
pylint 4.0.4
astroid 4.0.2
Python 3.14.0 (main, Nov 10 2025, 09:33:32) [GCC 7.5.0]
```

### OS / Environment

SUSE Linux


Implementation patch:
diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py
index 9e8a3906e9..7a45b472bd 100644
--- a/pylint/checkers/utils.py
+++ b/pylint/checkers/utils.py
@@ -2200,41 +2200,40 @@ def is_terminating_func(node: nodes.Call) -> bool:
         node.parent, nodes.Lambda
     ):
         return False
-
     try:
-        for inferred in node.func.infer():
-            if (
-                hasattr(inferred, "qname")
-                and inferred.qname() in TERMINATING_FUNCS_QNAMES
-            ):
-                return True
-            match inferred:
-                case astroid.BoundMethod(_proxied=astroid.UnboundMethod(_proxied=p)):
-                    # Unwrap to get the actual function node object
-                    inferred = p
-            if (  # pylint: disable=too-many-boolean-expressions
-                isinstance(inferred, nodes.FunctionDef)
-                and (
-                    not isinstance(inferred, nodes.AsyncFunctionDef)
-                    or isinstance(node.parent, nodes.Await)
-                )
-                and isinstance(inferred.returns, nodes.Name)
-                and (inferred_func := safe_infer(inferred.returns))
-                and hasattr(inferred_func, "qname")
-                and inferred_func.qname()
-                in (
-                    *TYPING_NEVER,
-                    *TYPING_NORETURN,
-                    # In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm'
-                    # "typing._SpecialForm",
-                    # But 'typing.Any' also inherits _SpecialForm
-                    # See #9751
-                )
-            ):
-                return True
+        inferred_funcs = list(node.func.infer())
     except (StopIteration, astroid.InferenceError):
-        pass
+        return False
 
+    for inferred in inferred_funcs:
+        if hasattr(inferred, "qname") and inferred.qname() in TERMINATING_FUNCS_QNAMES:
+            return True
+        match inferred:
+            case astroid.BoundMethod(_proxied=astroid.UnboundMethod(_proxied=p)):
+                # Unwrap to get the actual function node object
+                inferred = p
+        if is_overload_stub(inferred):
+            continue
+        if (  # pylint: disable=too-many-boolean-expressions
+            isinstance(inferred, nodes.FunctionDef)
+            and (
+                not isinstance(inferred, nodes.AsyncFunctionDef)
+                or isinstance(node.parent, nodes.Await)
+            )
+            and isinstance(inferred.returns, nodes.Name)
+            and (inferred_func := safe_infer(inferred.returns))
+            and hasattr(inferred_func, "qname")
+            and inferred_func.qname()
+            in (
+                *TYPING_NEVER,
+                *TYPING_NORETURN,
+                # In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm'
+                # "typing._SpecialForm",
+                # But 'typing.Any' also inherits _SpecialForm
+                # See #9751
+            )
+        ):
+            return True
     return False
 
 


Test patch:
diff --git a/tests/checkers/unittest_utils.py b/tests/checkers/unittest_utils.py
index d0279d45a4..6d9bb1a5e9 100644
--- a/tests/checkers/unittest_utils.py
+++ b/tests/checkers/unittest_utils.py
@@ -641,3 +641,40 @@ def test_foo(self):
     )
     result = utils.is_terminating_func(node)
     assert result is True
+
+
+def test_is_terminating_func_ignored_overload_noreturn() -> None:
+    node = astroid.extract_node(
+        """
+    from typing import Literal, NoReturn, overload
+    @overload
+    def create_client(version: int = ...) -> NoReturn: ...
+    @overload
+    def create_client(version: Literal[2] = ...) -> int: ...
+    def create_client(version: int = 2) -> int:
+        return 1
+    create_client(version=2)  #@
+    """
+    )
+    result = utils.is_terminating_func(node)
+    assert result is False
+
+
+def test_is_terminating_func_overload_with_noreturn_implementation() -> None:
+    node = astroid.extract_node(
+        """
+    from typing import NoReturn, overload
+
+    @overload
+    def always_fails(code: int) -> NoReturn: ...
+    @overload
+    def always_fails(code: str) -> NoReturn: ...
+
+    def always_fails(code: int | str) -> NoReturn:
+        raise SystemExit(code)
+
+    always_fails(1)  #@
+"""
+    )
+    result = utils.is_terminating_func(node)
+    assert result is True