Repository: python/mypy

Issue:
Improve message when function is missing return type
If a function takes no arguments (or only `self`) and `--disallow-untyped-defs` is enabled, the error message "Function is missing a type annotation" is not very helpful. Here's an idea for a better message:

```
foo.py:2: error: Function must have a return type annotation ('disallow_untyped_defs' enabled)
foo.py:2: note: Use "-> None" if function does not return a value
```

The error message could be further specialized for `__init__`.

See #5943 for a related issue.


Discussion:
I'll work on this one!

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -66,6 +66,7 @@
 from mypy.scope import Scope
 from mypy.typeops import tuple_fallback
 from mypy import state
+from mypy.traverser import has_return_statement
 
 MYPY = False
 if MYPY:
@@ -987,7 +988,13 @@ def is_unannotated_any(t: Type) -> bool:
         check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation
         if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs):
             if fdef.type is None and self.options.disallow_untyped_defs:
-                self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
+                if (not fdef.arguments or (len(fdef.arguments) == 1 and
+                        (fdef.arg_names[0] == 'self' or fdef.arg_names[0] == 'cls'))):
+                    self.fail(message_registry.RETURN_TYPE_EXPECTED, fdef)
+                    if not has_return_statement(fdef):
+                        self.note('Use "-> None" if function does not return a value', fdef)
+                else:
+                    self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
             elif isinstance(fdef.type, CallableType):
                 ret_type = fdef.type.ret_type
                 if is_unannotated_any(ret_type):
diff --git a/mypy/stubgen.py b/mypy/stubgen.py
--- a/mypy/stubgen.py
+++ b/mypy/stubgen.py
@@ -86,6 +86,7 @@
 from mypy.find_sources import create_source_list, InvalidSourceList
 from mypy.build import build
 from mypy.errors import CompileError
+from mypy.traverser import has_return_statement
 
 MYPY = False
 if MYPY:
@@ -852,26 +853,6 @@ def find_self_initializers(fdef: FuncBase) -> List[Tuple[str, Expression]]:
     return traverser.results
 
 
-class ReturnSeeker(mypy.traverser.TraverserVisitor):
-    def __init__(self) -> None:
-        self.found = False
-
-    def visit_return_stmt(self, o: ReturnStmt) -> None:
-        if o.expr is None or isinstance(o.expr, NameExpr) and o.expr.name == 'None':
-            return
-        self.found = True
-
-
-def has_return_statement(fdef: FuncBase) -> bool:
-    """Find if a function has a non-trivial return statement.
-
-    Plain 'return' and 'return None' don't count.
-    """
-    seeker = ReturnSeeker()
-    fdef.accept(seeker)
-    return seeker.found
-
-
 def get_qualified_name(o: Expression) -> str:
     if isinstance(o, NameExpr):
         return o.name
diff --git a/mypy/traverser.py b/mypy/traverser.py
--- a/mypy/traverser.py
+++ b/mypy/traverser.py
@@ -2,15 +2,16 @@
 
 from mypy.visitor import NodeVisitor
 from mypy.nodes import (
-    Block, MypyFile, FuncItem, CallExpr, ClassDef, Decorator, FuncDef,
+    Block, MypyFile, FuncBase, FuncItem, CallExpr, ClassDef, Decorator, FuncDef,
     ExpressionStmt, AssignmentStmt, OperatorAssignmentStmt, WhileStmt,
     ForStmt, ReturnStmt, AssertStmt, DelStmt, IfStmt, RaiseStmt,
-    TryStmt, WithStmt, MemberExpr, OpExpr, SliceExpr, CastExpr, RevealExpr,
+    TryStmt, WithStmt, NameExpr, MemberExpr, OpExpr, SliceExpr, CastExpr, RevealExpr,
     UnaryExpr, ListExpr, TupleExpr, DictExpr, SetExpr, IndexExpr,
     GeneratorExpr, ListComprehension, SetComprehension, DictionaryComprehension,
     ConditionalExpr, TypeApplication, ExecStmt, Import, ImportFrom,
     LambdaExpr, ComparisonExpr, OverloadedFuncDef, YieldFromExpr,
-    YieldExpr, StarExpr, BackquoteExpr, AwaitExpr, PrintStmt, SuperExpr, REVEAL_TYPE
+    YieldExpr, StarExpr, BackquoteExpr, AwaitExpr, PrintStmt, SuperExpr, REVEAL_TYPE,
+    Statement
 )
 
 
@@ -285,3 +286,23 @@ def visit_exec_stmt(self, o: ExecStmt) -> None:
             o.globals.accept(self)
         if o.locals:
             o.locals.accept(self)
+
+
+class ReturnSeeker(TraverserVisitor):
+    def __init__(self) -> None:
+        self.found = False
+
+    def visit_return_stmt(self, o: ReturnStmt) -> None:
+        if (o.expr is None or isinstance(o.expr, NameExpr) and o.expr.name == 'None'):
+            return
+        self.found = True
+
+
+def has_return_statement(fdef: FuncBase) -> bool:
+    """Find if a function has a non-trivial return statement.
+
+    Plain 'return' and 'return None' don't count.
+    """
+    seeker = ReturnSeeker()
+    fdef.accept(seeker)
+    return seeker.found


Test patch:
diff --git a/test-data/unit/check-38.test b/test-data/unit/check-38.test
--- a/test-data/unit/check-38.test
+++ b/test-data/unit/check-38.test
@@ -10,7 +10,8 @@ class C: ...  # E: Name 'C' already defined on line 4
 def d(f): ...  # type: ignore
 @d
 
-def f(): ...  # E: Function is missing a type annotation
+def f(): ...  # E: Function is missing a return type annotation \
+              # N: Use "-> None" if function does not return a value
 
 [case testIgnoreDecoratedFunction1]
 # flags: --disallow-untyped-defs --warn-unused-ignores
diff --git a/test-data/unit/check-async-await.test b/test-data/unit/check-async-await.test
--- a/test-data/unit/check-async-await.test
+++ b/test-data/unit/check-async-await.test
@@ -694,7 +694,8 @@ async def f() -> AsyncGenerator[Any, Any]:
 async def h() -> Any:
     yield 0
 
-async def g():  # E: Function is missing a type annotation
+async def g():  # E: Function is missing a return type annotation \
+                # N: Use "-> None" if function does not return a value   
     yield 0
 [builtins fixtures/async_await.pyi]
 [typing fixtures/typing-full.pyi]
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -37,12 +37,27 @@ lambda x: x
 def f():
     1 + "str"
 [out]
-main:2: error: Function is missing a type annotation
+main:2: error: Function is missing a return type annotation
+main:2: note: Use "-> None" if function does not return a value
+
+[case testUnannotatedReturnWithOnlySelfArgument]
+# flags: --disallow-untyped-defs
+def f(self): pass
+[out]
+main:2: error: Function is missing a return type annotation
+main:2: note: Use "-> None" if function does not return a value
+
+[case testUnannotatedReturnWithNontrivialReturn]
+# flags: --disallow-untyped-defs
+def f(): return 1
+[out]
+main:2: error: Function is missing a return type annotation
 
 [case testUntypedAsyncDef]
 # flags: --disallow-untyped-defs
-async def f():  # E: Function is missing a type annotation
-    pass
+async def f():  # E: Function is missing a return type annotation \
+                # N: Use "-> None" if function does not return a value                
+    pass        
 [builtins fixtures/async_await.pyi]
 [typing fixtures/typing-full.pyi]
 
diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test
--- a/test-data/unit/cmdline.test
+++ b/test-data/unit/cmdline.test
@@ -1076,7 +1076,7 @@ disallow_any_generics = True
 def get_tasks(self):
     return 'whatever'
 [out]
-a.py:1: error: Function is missing a type annotation
+a.py:1: error: Function is missing a return type annotation
 
 [case testMissingFile]
 # cmd: mypy nope.py
diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test
--- a/test-data/unit/daemon.test
+++ b/test-data/unit/daemon.test
@@ -35,7 +35,8 @@ $ dmypy run -- foo.py --follow-imports=error
 Restarting: configuration changed
 Daemon stopped
 Daemon started
-foo.py:1: error: Function is missing a type annotation
+foo.py:1: error: Function is missing a return type annotation
+foo.py:1: note: Use "-> None" if function does not return a value
 == Return code: 1
 $ {python} -c "print('def f() -> None: pass')" >foo.py
 $ dmypy run -- foo.py --follow-imports=error