Repository: python/mypy

Issue:
--incremental --follow-imports=silent run separately on different files does not produce errors
On mypy version 0.511

```
# a.py
import b

b.foo(1, 2)
```

```
# b.py

def foo(a, b):
    # type: (int, int) -> str
    return a + b
```

```
> mypy --incremental --py2 --follow-imports=silent a.py  # No error, as expected.
> mypy --incremental --py2 --follow-imports=silent b.py  # No error where it was expected!
> mypy --incremental --py2 --follow-imports=normal b.py  # Error
```


Discussion:
Yes, also confirmed on master (also without `--py2`). It should probably not write a cache entry for `b` if errors in it are being suppressed, or, even better, it should mark the cache as silenced so it can ignore it on the second run.

Resolving patch:
diff --git a/mypy/build.py b/mypy/build.py
--- a/mypy/build.py
+++ b/mypy/build.py
@@ -313,6 +313,7 @@ def default_lib_path(data_dir: str,
                         ('dep_prios', List[int]),
                         ('interface_hash', str),  # hash representing the public interface
                         ('version_id', str),  # mypy version for cache invalidation
+                        ('ignore_all', bool),  # if errors were ignored
                         ])
 # NOTE: dependencies + suppressed == all reachable imports;
 # suppressed contains those reachable imports that were prevented by
@@ -889,6 +890,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
         meta.get('dep_prios', []),
         meta.get('interface_hash', ''),
         meta.get('version_id', sentinel),
+        meta.get('ignore_all', True),
     )
     # Don't check for path match, that is dealt with in validate_meta().
     if (m.id != id or
@@ -946,7 +948,7 @@ def atomic_write(filename: str, *lines: str) -> bool:
 
 
 def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
-                  manager: BuildManager) -> Optional[CacheMeta]:
+                  ignore_all: bool, manager: BuildManager) -> Optional[CacheMeta]:
     '''Checks whether the cached AST of this module can be used.
 
     Return:
@@ -962,6 +964,11 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
     if meta is None:
         manager.log('Metadata not found for {}'.format(id))
         return None
+
+    if meta.ignore_all and not ignore_all:
+        manager.log('Metadata abandoned for {}: errors were previously ignored'.format(id))
+        return None
+
     assert path is not None, "Internal error: meta was provided without a path"
     # Check data_json; assume if its mtime matches it's good.
     # TODO: stat() errors
@@ -1004,6 +1011,7 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
                 'dep_prios': meta.dep_prios,
                 'interface_hash': meta.interface_hash,
                 'version_id': manager.version_id,
+                'ignore_all': meta.ignore_all,
             }
             if manager.options.debug_cache:
                 meta_str = json.dumps(meta_dict, indent=2, sort_keys=True)
@@ -1030,7 +1038,8 @@ def compute_hash(text: str) -> str:
 def write_cache(id: str, path: str, tree: MypyFile,
                 dependencies: List[str], suppressed: List[str],
                 child_modules: List[str], dep_prios: List[int],
-                old_interface_hash: str, source_hash: str, manager: BuildManager) -> str:
+                old_interface_hash: str, source_hash: str,
+                ignore_all: bool, manager: BuildManager) -> str:
     """Write cache files for a module.
 
     Note that this mypy's behavior is still correct when any given
@@ -1123,6 +1132,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
             'dep_prios': dep_prios,
             'interface_hash': interface_hash,
             'version_id': manager.version_id,
+            'ignore_all': ignore_all,
             }
 
     # Write meta cache file
@@ -1424,7 +1434,7 @@ def __init__(self,
             if self.meta is not None:
                 self.interface_hash = self.meta.interface_hash
         self.add_ancestors()
-        self.meta = validate_meta(self.meta, self.id, self.path, manager)
+        self.meta = validate_meta(self.meta, self.id, self.path, self.ignore_all, manager)
         if self.meta:
             # Make copies, since we may modify these and want to
             # compare them to the originals later.
@@ -1789,7 +1799,7 @@ def write_cache(self) -> None:
         new_interface_hash = write_cache(
             self.id, self.path, self.tree,
             list(self.dependencies), list(self.suppressed), list(self.child_modules),
-            dep_prios, self.interface_hash, self.source_hash,
+            dep_prios, self.interface_hash, self.source_hash, self.ignore_all,
             self.manager)
         if new_interface_hash == self.interface_hash:
             self.manager.log("Cached module {} has same interface".format(self.id))


Test patch:
diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py
--- a/mypy/test/testcheck.py
+++ b/mypy/test/testcheck.py
@@ -287,10 +287,11 @@ def find_module_files(self) -> Dict[str, str]:
 
     def find_missing_cache_files(self, modules: Dict[str, str],
                                  manager: build.BuildManager) -> Set[str]:
+        ignore_errors = True
         missing = {}
         for id, path in modules.items():
             meta = build.find_cache_meta(id, path, manager)
-            if not build.validate_meta(meta, id, path, manager):
+            if not build.validate_meta(meta, id, path, ignore_errors, manager):
                 missing[id] = path
         return set(missing.values())
 
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -2933,3 +2933,21 @@ foo.bar(b"test")
 [out]
 [out2]
 tmp/mod.py:7: error: Revealed type is 'builtins.bytes'
+
+[case testIncrementalWithSilentImports]
+# cmd: mypy -m a
+# cmd2: mypy -m b
+# flags: --follow-imports=silent
+# flags2: --follow-imports=silent
+[file a.py]
+import b
+
+b.foo(1, 2)
+
+[file b.py]
+def foo(a: int, b: int) -> str:
+    return a + b
+
+[out1]
+[out2]
+tmp/b.py:2: error: Incompatible return value type (got "int", expected "str")