Repository: cython/cython

Issue:
[ENH] Warn when user provides undeclared symbol in type annotation
### Is your feature request related to a problem? Please describe.

Cython currently ignores annotations with undeclared types - e.g. following code is compiled correctly:

```python
def main():
    a: Bar = 6
```

Compiling code without warning user can lead to errors hard to find - e.g. when developer introduces typo in annotation or forgets to use correct module -e.g.: `a: Py_ssize_t` instead of `a: typing.Py_ssize_t`.


### Describe the solution you'd like.

I would like Cython to raise a warning when annotation contains symbol not defined. When user disables annotation typing with `cython.annotation_typing(False)`, warnings should be silenced.

### Describe alternatives you've considered.

Another possibility is to stop compilation with error but it was rejected as warning is better solution.

### Additional context

See the discussion here: https://github.com/cython/cython/pull/5069#discussion_r989736451


Discussion:

Resolving patch:
diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py
--- a/Cython/Compiler/ExprNodes.py
+++ b/Cython/Compiler/ExprNodes.py
@@ -2143,7 +2143,7 @@ def analyse_as_type(self, env):
 
         # Try to give a helpful warning when users write plain C type names.
         if not env.in_c_type_context and PyrexTypes.parse_basic_type(self.name):
-            warning(self.pos, "Found C type '%s' in a Python annotation. Did you mean to use a Python type?" % self.name)
+            warning(self.pos, "Found C type '%s' in a Python annotation. Did you mean to use 'cython.%s'?" % (self.name, self.name))
 
         return None
 
@@ -14428,6 +14428,33 @@ def analyse_as_type(self, env):
         # for compatibility when used as a return_type_node, have this interface too
         return self.analyse_type_annotation(env)[1]
 
+    def _warn_on_unknown_annotation(self, env, annotation):
+        """Method checks for cases when user should be warned that annotation contains unknown types."""
+        if annotation.is_name:
+            # Validate annotation in form `var: type`
+            if not env.lookup(annotation.name):
+                warning(annotation.pos,
+                        "Unknown type declaration '%s' in annotation, ignoring" % self.string.value, level=1)
+        elif annotation.is_attribute and annotation.obj.is_name:
+            # Validate annotation in form `var: module.type`
+            if not env.lookup(annotation.obj.name):
+                # `module` is undeclared
+                warning(annotation.pos,
+                        "Unknown type declaration '%s' in annotation, ignoring" % self.string.value, level=1)
+            elif annotation.obj.is_cython_module:
+                # `module` is cython
+                module_scope = annotation.obj.analyse_as_module(env)
+                if module_scope and not module_scope.lookup_type(annotation.attribute):
+                    error(annotation.pos,
+                            "Unknown type declaration '%s' in annotation" % self.string.value)
+            else:
+                module_scope = annotation.obj.analyse_as_module(env)
+                if module_scope and module_scope.pxd_file_loaded:
+                    warning(annotation.pos,
+                            "Unknown type declaration '%s' in annotation, ignoring" % self.string.value, level=1)
+        else:
+            warning(annotation.pos, "Unknown type declaration in annotation, ignoring")
+
     def analyse_type_annotation(self, env, assigned_value=None):
         if self.untyped:
             # Already applied as a fused type, not re-evaluating it here.
@@ -14454,7 +14481,7 @@ def analyse_type_annotation(self, env, assigned_value=None):
             arg_type = annotation.analyse_as_type(env)
 
             if arg_type is None:
-                warning(annotation.pos, "Unknown type declaration in annotation, ignoring")
+                self._warn_on_unknown_annotation(env, annotation)
                 return [], arg_type
 
             if annotation.is_string_literal:
diff --git a/docs/examples/userguide/buffer/matrix.py b/docs/examples/userguide/buffer/matrix.py
--- a/docs/examples/userguide/buffer/matrix.py
+++ b/docs/examples/userguide/buffer/matrix.py
@@ -4,10 +4,10 @@
 
 @cython.cclass
 class Matrix:
-    ncols: cython.unsigned
+    ncols: cython.uint
     v: vector[cython.float]
 
-    def __cinit__(self, ncols: cython.unsigned):
+    def __cinit__(self, ncols: cython.uint):
         self.ncols = ncols
 
     def add_row(self):


Test patch:
diff --git a/tests/errors/dataclass_e5.pyx b/tests/errors/dataclass_e5.pyx
--- a/tests/errors/dataclass_e5.pyx
+++ b/tests/errors/dataclass_e5.pyx
@@ -14,8 +14,8 @@ cdef class C:
 
 _WARNINGS = """
 9:7: Found Python 2.x type 'long' in a Python annotation. Did you mean to use 'cython.long'?
-10:7: Found C type 'Py_ssize_t' in a Python annotation. Did you mean to use a Python type?
-10:7: Unknown type declaration in annotation, ignoring
-12:7: Found C type 'double' in a Python annotation. Did you mean to use a Python type?
-12:7: Unknown type declaration in annotation, ignoring
+10:7: Found C type 'Py_ssize_t' in a Python annotation. Did you mean to use 'cython.Py_ssize_t'?
+10:7: Unknown type declaration 'Py_ssize_t' in annotation, ignoring
+12:7: Found C type 'double' in a Python annotation. Did you mean to use 'cython.double'?
+12:7: Unknown type declaration 'double' in annotation, ignoring
 """
diff --git a/tests/errors/pure_warnings.py b/tests/errors/pure_warnings.py
new file mode 100644
--- /dev/null
+++ b/tests/errors/pure_warnings.py
@@ -0,0 +1,63 @@
+# mode: error
+# tag: warnings
+
+import cython
+import typing
+from cython.cimports.libc import stdint
+
+
+def main():
+    foo1: typing.Tuple = None
+    foo1: typing.Bar = None
+    foo2: Bar = 1  # warning
+    foo3: int = 1
+    foo4: cython.int = 1
+    foo5: stdint.bar = 5  # warning
+    foo6: object = 1
+    foo7: cython.bar = 1  # error
+    foo8: (1 + x).b
+    foo9: mod.a.b
+    foo10: func().b
+    with cython.annotation_typing(False):
+        foo8: Bar = 1
+        foo9: stdint.bar = 5
+        foo10: cython.bar = 1
+
+
+@cython.cfunc
+def bar() -> cython.bar:
+    pass
+
+
+@cython.cfunc
+def bar2() -> Bar:
+    pass
+
+@cython.cfunc
+def bar3() -> stdint.bar:
+    pass
+
+_WARNINGS = """
+12:10: Unknown type declaration 'Bar' in annotation, ignoring
+15:16: Unknown type declaration 'stdint.bar' in annotation, ignoring
+18:17: Unknown type declaration in annotation, ignoring
+19:15: Unknown type declaration in annotation, ignoring
+20:17: Unknown type declaration in annotation, ignoring
+33:14: Unknown type declaration 'Bar' in annotation, ignoring
+37:20: Unknown type declaration 'stdint.bar' in annotation, ignoring
+
+# Spurious warnings from utility code - not part of the core test
+25:10: 'cpdef_method' redeclared
+36:10: 'cpdef_cname_method' redeclared
+979:29: Ambiguous exception value, same as default return value: 0
+1020:46: Ambiguous exception value, same as default return value: 0
+1110:29: Ambiguous exception value, same as default return value: 0
+"""
+
+_ERRORS = """
+17:16: Unknown type declaration 'cython.bar' in annotation
+28:13: Not a type
+28:19: Unknown type declaration 'cython.bar' in annotation
+33:14: Not a type
+37:14: Not a type
+"""