Repository: pylint-dev/pylint

Issue:
False positive `unreachable` for `NoReturn` coroutine functions
### Bug description

```python
# pylint: disable=missing-module-docstring,missing-function-docstring

import asyncio
from typing import NoReturn


async def coro() -> NoReturn:
    while True:
        await asyncio.sleep(1)


async def main() -> None:
    asyncio.create_task(coro())
    print("hello")


if __name__ == "__main__":
    asyncio.run(main())
```


### Configuration

_No response_

### Command used

```shell
pylint a.py
```


### Pylint output

```python
************* Module a
a.py:14:4: W0101: Unreachable code (unreachable)

-----------------------------------
Your code has been rated at 9.00/10
```


### Expected behavior

No error

### Pylint version

```shell
pylint 3.2.6
astroid 3.2.4
Python 3.11.9 (main, Apr  6 2024, 17:59:24) [GCC 9.4.0]
```


### OS / Environment

Ubuntu 20.04.6 LTS

### Additional dependencies

_No response_


Discussion:
I believe this bug was introduced in Pylint 3.2.4, https://github.com/pylint-dev/pylint/pull/9714



Patch:
diff --git a/doc/whatsnew/fragments/9840.false_positive b/doc/whatsnew/fragments/9840.false_positive
new file mode 100644
index 0000000000..ed3cfb2e5c
--- /dev/null
+++ b/doc/whatsnew/fragments/9840.false_positive
@@ -0,0 +1,3 @@
+Fixed a false positive `unreachable` for `NoReturn` coroutine functions.
+
+Closes #9840.
diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py
index ae045facde..bfc4bc61da 100644
--- a/pylint/checkers/utils.py
+++ b/pylint/checkers/utils.py
@@ -2197,8 +2197,12 @@ def is_terminating_func(node: nodes.Call) -> bool:
                 inferred._proxied, astroid.UnboundMethod
             ):
                 inferred = inferred._proxied._proxied
-            if (
+            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")


Test patch:
diff --git a/tests/functional/u/unreachable.py b/tests/functional/u/unreachable.py
index e59ad218fe..2c9d5ce97e 100644
--- a/tests/functional/u/unreachable.py
+++ b/tests/functional/u/unreachable.py
@@ -4,6 +4,7 @@
 import os
 import signal
 import sys
+from typing import NoReturn
 
 def func1():
     return 1
@@ -79,3 +80,28 @@ def func12():
     incognito_function()
     var = 2 + 2  # [unreachable]
     print(var)
+
+def func13():
+    def inner() -> NoReturn:
+        while True:
+            pass
+
+    inner()
+    print("unreachable")  # [unreachable]
+
+async def func14():
+    async def inner() -> NoReturn:
+        while True:
+            pass
+
+    await inner()
+    print("unreachable")  # [unreachable]
+
+async def func15():
+    async def inner() -> NoReturn:
+        while True:
+            pass
+
+    coro = inner()
+    print("reachable")
+    await coro
diff --git a/tests/functional/u/unreachable.txt b/tests/functional/u/unreachable.txt
index 82f9797aa1..4c2b586456 100644
--- a/tests/functional/u/unreachable.txt
+++ b/tests/functional/u/unreachable.txt
@@ -1,10 +1,12 @@
-unreachable:10:4:10:24:func1:Unreachable code:HIGH
-unreachable:15:8:15:28:func2:Unreachable code:HIGH
-unreachable:21:8:21:28:func3:Unreachable code:HIGH
-unreachable:25:4:25:16:func4:Unreachable code:HIGH
-unreachable:38:4:38:24:func6:Unreachable code:HIGH
-unreachable:42:4:42:15:func7:Unreachable code:INFERENCE
-unreachable:64:4:64:15:func9:Unreachable code:INFERENCE
-unreachable:69:4:69:15:func10:Unreachable code:INFERENCE
-unreachable:74:4:74:15:func11:Unreachable code:INFERENCE
-unreachable:80:4:80:15:func12:Unreachable code:INFERENCE
+unreachable:11:4:11:24:func1:Unreachable code:HIGH
+unreachable:16:8:16:28:func2:Unreachable code:HIGH
+unreachable:22:8:22:28:func3:Unreachable code:HIGH
+unreachable:26:4:26:16:func4:Unreachable code:HIGH
+unreachable:39:4:39:24:func6:Unreachable code:HIGH
+unreachable:43:4:43:15:func7:Unreachable code:INFERENCE
+unreachable:65:4:65:15:func9:Unreachable code:INFERENCE
+unreachable:70:4:70:15:func10:Unreachable code:INFERENCE
+unreachable:75:4:75:15:func11:Unreachable code:INFERENCE
+unreachable:81:4:81:15:func12:Unreachable code:INFERENCE
+unreachable:90:4:90:24:func13:Unreachable code:INFERENCE
+unreachable:98:4:98:24:func14:Unreachable code:INFERENCE