Repository: python/mypy

Issue:
Using reveal_type with no errors should return exit code 0
**Bug Report**

When using [pytest-mypy](https://github.com/dbader/pytest-mypy) to test some dynamically determined types (with generics) results in a false-positive error in CI, because mypy's exit code becomes 1 when there are no errors but only `reveal_type` outputs.

**To Reproduce**

Just write an arbitrary Python file that has *no* type errors and put `reveal_type()` to any one of the identifiers.
Run mypy against that file and check the exit code.

I'd like to keep `reveal_type()` in my code, to test a mixture of runtime type validation and static typing results using `typeguard`, generics, and `pytest-mypy` plugin.

**Expected Behavior**

When there are no errors but only some `reveal_type` outputs, `mypy` executable should set the exit code 0.

**Actual Behavior**

It sets the exit code 1, breaking my CI.
![image](https://user-images.githubusercontent.com/555156/106738320-20022280-665b-11eb-82e1-f28b52153863.png)

**Your Environment**

- Mypy version used: 0.800
- Mypy command-line flags and options:
```dosini
[mypy]
ignore_missing_imports = true
mypy_path = stubs,src
namespace_packages = true
explicit_package_bases = true
```
- Python version used: 3.9.1
- Operating system and version: Ubuntu 20.04


Discussion:
I do think since IIRC we moved `reveal_type` from an error to a note it would make sense to exit 0 if the only output is reveal_type. More generally exiting 0 if there are only notes makes sense to me.

Resolving patch:
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -110,10 +110,10 @@ def main(
         print_memory_profile()
 
     code = 0
-    if messages:
+    n_errors, n_notes, n_files = util.count_stats(messages)
+    if messages and n_notes < len(messages):
         code = 2 if blockers else 1
     if options.error_summary:
-        n_errors, n_notes, n_files = util.count_stats(messages)
         if n_errors:
             summary = formatter.format_error(
                 n_errors, n_files, len(sources), blockers=blockers, use_color=options.color_output


Test patch:
diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py
--- a/mypy/test/testpythoneval.py
+++ b/mypy/test/testpythoneval.py
@@ -81,7 +81,7 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None
             # Normalize paths so that the output is the same on Windows and Linux/macOS.
             line = line.replace(test_temp_dir + os.sep, test_temp_dir + "/")
             output.append(line.rstrip("\r\n"))
-    if returncode == 0:
+    if returncode == 0 and not output:
         # Execute the program.
         proc = subprocess.run(
             [interpreter, "-Wignore", program], cwd=test_temp_dir, capture_output=True
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
@@ -425,6 +425,7 @@ follow_imports = skip
 [out]
 main.py:2: note: Revealed type is "Any"
 main.py:4: note: Revealed type is "Any"
+== Return code: 0
 
 [case testConfigFollowImportsError]
 # cmd: mypy main.py
@@ -517,7 +518,7 @@ reveal_type(missing.x)  # Expect Any
 ignore_missing_imports = True
 [out]
 main.py:2: note: Revealed type is "Any"
-
+== Return code: 0
 
 [case testFailedImportOnWrongCWD]
 # cmd: mypy main.py
@@ -654,15 +655,26 @@ python_version = 3.6
 [file int_pow.py]
 a = 1
 b = a + 2
-reveal_type(a**0) # N: Revealed type is "Literal[1]"
-reveal_type(a**1) # N: Revealed type is "builtins.int"
-reveal_type(a**2) # N: Revealed type is "builtins.int"
-reveal_type(a**-0) # N: Revealed type is "Literal[1]"
-reveal_type(a**-1) # N: Revealed type is "builtins.float"
-reveal_type(a**(-2)) # N: Revealed type is "builtins.float"
-reveal_type(a**b) # N: Revealed type is "Any"
-reveal_type(a.__pow__(2)) # N: Revealed type is "builtins.int"
-reveal_type(a.__pow__(a)) # N: Revealed type is "Any"
+reveal_type(a**0)
+reveal_type(a**1)
+reveal_type(a**2)
+reveal_type(a**-0)
+reveal_type(a**-1)
+reveal_type(a**(-2))
+reveal_type(a**b)
+reveal_type(a.__pow__(2))
+reveal_type(a.__pow__(a))
+[out]
+int_pow.py:3: note: Revealed type is "Literal[1]"
+int_pow.py:4: note: Revealed type is "builtins.int"
+int_pow.py:5: note: Revealed type is "builtins.int"
+int_pow.py:6: note: Revealed type is "Literal[1]"
+int_pow.py:7: note: Revealed type is "builtins.float"
+int_pow.py:8: note: Revealed type is "builtins.float"
+int_pow.py:9: note: Revealed type is "Any"
+int_pow.py:10: note: Revealed type is "builtins.int"
+int_pow.py:11: note: Revealed type is "Any"
+== Return code: 0
 
 [case testDisallowAnyGenericsBuiltinCollections]
 # cmd: mypy m.py
@@ -1484,3 +1496,12 @@ pass
 [out]
 Warning: --enable-recursive-aliases is deprecated; recursive types are enabled by default
 == Return code: 0
+
+[case testNotesOnlyResultInExitSuccess]
+# cmd: mypy a.py
+[file a.py]
+def f():
+    x: int = "no"
+[out]
+a.py:2: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs
+== Return code: 0