Repository: cython/cython

Issue:
[BUG] Warn when noexcept is ignored
### Describe the bug

We should warn when `noexcept` is ignored - just to stop people thinking they're optimizing something that they aren't

### Code to reproduce the behaviour:

```cython
cdef object xx() noexcept:
    pass
```

This also applies to memoryviews

### Expected behaviour

This should generate a warning since exceptions are always checked for functions returning an object

### OS

Linux

### Python version

_No response_

### Cython version

current master

### Additional context

We might have to be a bit careful about fused functions - we probably don't want to generate warnings when *some* of the return types are valid for noexcept


Discussion:
Should also obviously only apply to a manual noexcept, not an implicit noexcept
Just a thought. Why not directly generate an error? When an object is returned the `noexcept` is not correctly used.
I just want to add the context of my stackoverflow question that lead to this issue:

https://stackoverflow.com/questions/76972950/cython-returned-memoryview-is-always-considered-uninitialized

I think an error would be acceptable to me for my case as long as it is well described or links to documentation describing the situation. Especially for memoryviews where I'm not sure I completely agree with compiler directives not having an effect on memoryview slicing...but I don't know all the ins and outs of the design so an error as an alternative seems good.
I think if I were starting from scratch I'd do an error. I think now it has to be a warning to avoid breaking existing working code.
> it has to be a warning to avoid breaking existing working code.

Yes. Let's make it a visible warning so that users can adapt, and make it clear in the message that the code is wrong and will be considered an error eventually. We should also issue that warning in a future 0.29.x release. We can still turn it into an error in 3.1, when users had time to adapt.

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -619,15 +619,16 @@ def analyse(self, base_type, env, nonempty=0, visibility=None, in_pxd=False):
 
 
 class CFuncDeclaratorNode(CDeclaratorNode):
-    # base             CDeclaratorNode
-    # args             [CArgDeclNode]
-    # templates        [TemplatePlaceholderType]
-    # has_varargs      boolean
-    # exception_value  ConstNode or NameNode    NameNode when the name of a c++ exception conversion function
-    # exception_check  boolean or "+"    True if PyErr_Occurred check needed, "+" for a c++ check
-    # nogil            boolean    Can be called without gil
-    # with_gil         boolean    Acquire gil around function body
-    # is_const_method  boolean    Whether this is a const method
+    # base                      CDeclaratorNode
+    # args                      [CArgDeclNode]
+    # templates                 [TemplatePlaceholderType]
+    # has_varargs               boolean
+    # exception_value           ConstNode or NameNode    NameNode when the name of a c++ exception conversion function
+    # exception_check           boolean or "+"    True if PyErr_Occurred check needed, "+" for a c++ check
+    # has_explicit_exc_clause   boolean    True if exception clause is explicitly declared
+    # nogil                     boolean    Can be called without gil
+    # with_gil                  boolean    Acquire gil around function body
+    # is_const_method           boolean    Whether this is a const method
 
     child_attrs = ["base", "args", "exception_value"]
 
@@ -718,6 +719,8 @@ def analyse(self, return_type, env, nonempty=0, directive_locals=None, visibilit
                 and (self.exception_value or self.exception_check)
                 and self.exception_check != '+'):
             error(self.pos, "Exception clause not allowed for function returning Python object")
+        elif return_type.is_pyobject and not self.exception_check and visibility != 'extern' and self.has_explicit_exc_clause:
+            warning(self.pos, "noexcept clause is ignored for function returning Python object", 1)
         else:
             if self.exception_value is None and self.exception_check and self.exception_check != '+':
                 # Use an explicit exception return value to speed up exception checks.
@@ -2691,6 +2694,7 @@ def analyse_declarations(self, env):
             defining=self.body is not None, modifiers=self.modifiers,
             overridable=self.overridable, in_pxd=self.inline_in_pxd)
         self.return_type = typ.return_type
+
         if self.return_type.is_array and self.visibility != 'extern':
             error(self.pos, "Function cannot return an array")
         if self.return_type.is_cpp_class:
@@ -3073,8 +3077,8 @@ def __init__(self, pos, **kwds):
         self.num_required_kw_args = rk
         self.num_required_args = r
 
-    def as_cfunction(self, cfunc=None, scope=None, overridable=True, returns=None, except_val=None, modifiers=None,
-                     nogil=False, with_gil=False):
+    def as_cfunction(self, cfunc=None, scope=None, overridable=True, returns=None, except_val=None, has_explicit_exc_clause=False,
+                     modifiers=None, nogil=False, with_gil=False):
         if self.star_arg:
             error(self.star_arg.pos, "cdef function cannot have star argument")
         if self.starstar_arg:
@@ -3124,6 +3128,7 @@ def as_cfunction(self, cfunc=None, scope=None, overridable=True, returns=None, e
                                          has_varargs=False,
                                          exception_check=cfunc_type.exception_check,
                                          exception_value=exception_value,
+                                         has_explicit_exc_clause = has_explicit_exc_clause,
                                          with_gil=cfunc_type.with_gil,
                                          nogil=cfunc_type.nogil)
         return CFuncDefNode(self.pos,
diff --git a/Cython/Compiler/ParseTreeTransforms.py b/Cython/Compiler/ParseTreeTransforms.py
--- a/Cython/Compiler/ParseTreeTransforms.py
+++ b/Cython/Compiler/ParseTreeTransforms.py
@@ -2976,6 +2976,7 @@ def visit_DefNode(self, node):
         nogil = self.directives.get('nogil')
         with_gil = self.directives.get('with_gil')
         except_val = self.directives.get('exceptval')
+        has_explicit_exc_clause = False if except_val is None else True
         return_type_node = self.directives.get('returns')
         if return_type_node is None and self.directives['annotation_typing']:
             return_type_node = node.return_type_annotation
@@ -2992,7 +2993,7 @@ def visit_DefNode(self, node):
                 error(node.pos, "ccall functions cannot be declared 'with_gil'")
             node = node.as_cfunction(
                 overridable=True, modifiers=modifiers, nogil=nogil,
-                returns=return_type_node, except_val=except_val)
+                returns=return_type_node, except_val=except_val, has_explicit_exc_clause=has_explicit_exc_clause)
             return self.visit(node)
         if 'cfunc' in self.directives:
             if self.in_py_class:
@@ -3000,7 +3001,7 @@ def visit_DefNode(self, node):
             else:
                 node = node.as_cfunction(
                     overridable=False, modifiers=modifiers, nogil=nogil, with_gil=with_gil,
-                    returns=return_type_node, except_val=except_val)
+                    returns=return_type_node, except_val=except_val, has_explicit_exc_clause=has_explicit_exc_clause)
                 return self.visit(node)
         if 'inline' in modifiers:
             error(node.pos, "Python functions cannot be declared 'inline'")
diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py
--- a/Cython/Compiler/Parsing.py
+++ b/Cython/Compiler/Parsing.py
@@ -2972,7 +2972,7 @@ def p_c_func_declarator(s, pos, ctx, base, cmethod_flag):
     return Nodes.CFuncDeclaratorNode(pos,
         base = base, args = args, has_varargs = ellipsis,
         exception_value = exc_val, exception_check = exc_check,
-        nogil = nogil or ctx.nogil or with_gil, with_gil = with_gil)
+        nogil = nogil or ctx.nogil or with_gil, with_gil = with_gil, has_explicit_exc_clause=exc_clause)
 
 supported_overloaded_operators = cython.declare(frozenset, frozenset((
     '+', '-', '*', '/', '%',


Test patch:
diff --git a/tests/errors/w_noexcept.pyx b/tests/errors/w_noexcept.pyx
new file mode 100644
--- /dev/null
+++ b/tests/errors/w_noexcept.pyx
@@ -0,0 +1,78 @@
+# mode: error
+# tag: werror
+
+import cython
+
+ctypedef fused double_or_object:
+    double
+    object
+
+cdef object test_return_object_noexcept(x) noexcept: # Err
+    return x
+
+cdef str test_return_str_noexcept() noexcept: # Err
+    return 'a'
+
+cdef test_noexcept() noexcept:  # Err
+    pass
+
+cdef test_implicit_noexcept(): # Ok
+    pass
+
+cdef object test_return_object(x): # Ok
+    return x
+
+cdef str test_return_str(): # Ok
+    return 'a'
+
+cdef extern from *:
+    cdef object extern_return_object(): # Ok
+        pass
+
+    cdef object extern_noexcept() noexcept: # Ok
+        pass
+
+cdef double_or_object test_fused_noexcept(double_or_object x) noexcept: # Ok
+    pass
+
+@cython.exceptval(check=False)
+@cython.cfunc
+def test_pure_noexcept(): # Err
+    pass
+
+@cython.exceptval(check=False)
+@cython.cfunc
+def test_pure_return_object_noexcept() -> object: # Err
+    pass
+
+@cython.exceptval(check=False)
+@cython.cfunc
+def test_pure_return_str_noexcept() -> str: # Err
+    pass
+
+@cython.cfunc
+def test_pure_implicit(): # Ok
+    pass
+
+@cython.cfunc
+def test_pure_return_object() -> object: # Ok
+    pass
+
+@cython.cfunc
+def test_pure_return_str() -> str: # Ok
+    pass
+
+@cython.exceptval(check=False)
+@cython.cfunc
+def test_pure_return_fused_noexcept(x: double_or_object) -> double_or_object: # Ok
+    return x
+
+
+_ERRORS = """
+10:39: noexcept clause is ignored for function returning Python object
+13:33: noexcept clause is ignored for function returning Python object
+16:18: noexcept clause is ignored for function returning Python object
+38:0: noexcept clause is ignored for function returning Python object
+43:0: noexcept clause is ignored for function returning Python object
+48:0: noexcept clause is ignored for function returning Python object
+"""
diff --git a/tests/run/legacy_implicit_noexcept.pyx b/tests/run/legacy_implicit_noexcept.pyx
--- a/tests/run/legacy_implicit_noexcept.pyx
+++ b/tests/run/legacy_implicit_noexcept.pyx
@@ -30,6 +30,18 @@ ptr_func_implicit = func_implicit
 cdef int(*ptr_func_noexcept)(int, int) noexcept
 ptr_func_noexcept = func_noexcept
 
+# tests checking that following warning does not occur:
+# noexcept clause is ignored for function returning Python object
+
+cdef object test_noexcept_warning_object(x):
+    return x
+
+cdef str test_noexcept_warning_str():
+    return 'a'
+
+cdef test_noexcept_warning():
+    pass
+
 @cython.cfunc
 def func_pure_implicit() -> cython.int:
     raise RuntimeError
@@ -140,4 +152,7 @@ _WARNINGS = """
 15:5: Unraisable exception in function 'legacy_implicit_noexcept.func_noexcept'.
 24:43: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
 27:38: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
+36:43: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
+39:36: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
+42:28: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
 """