Skip to content

Commit 6784d85

Browse files
authored
Don't produce spurious unused type ignore messages in incremental mode (#4841)
This is accomplished by generating diagnostics for suppressed dependencies before generating unused ignore notes. This requires that we store line information for suppressed dependencies in our cache files. Fixes #2960
1 parent 81d4317 commit 6784d85

File tree

2 files changed

+59
-15
lines changed

2 files changed

+59
-15
lines changed

mypy/build.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ def default_lib_path(data_dir: str,
396396
('suppressed', List[str]), # dependencies that weren't imported
397397
('child_modules', List[str]), # all submodules of the given module
398398
('options', Optional[Dict[str, object]]), # build options
399+
# dep_prios and dep_lines are in parallel with
400+
# dependencies + suppressed.
399401
('dep_prios', List[int]),
400402
('dep_lines', List[int]),
401403
('interface_hash', str), # hash representing the public interface
@@ -964,8 +966,8 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
964966
# Ignore cache if generated by an older mypy version.
965967
if ((m.version_id != manager.version_id and not manager.options.skip_version_check)
966968
or m.options is None
967-
or len(m.dependencies) != len(m.dep_prios)
968-
or len(m.dependencies) != len(m.dep_lines)):
969+
or len(m.dependencies) + len(m.suppressed) != len(m.dep_prios)
970+
or len(m.dependencies) + len(m.suppressed) != len(m.dep_lines)):
969971
manager.log('Metadata abandoned for {}: new attributes are missing'.format(id))
970972
return None
971973

@@ -1514,12 +1516,13 @@ def __init__(self,
15141516
# compare them to the originals later.
15151517
self.dependencies = list(self.meta.dependencies)
15161518
self.suppressed = list(self.meta.suppressed)
1517-
assert len(self.meta.dependencies) == len(self.meta.dep_prios)
1519+
all_deps = self.dependencies + self.suppressed
1520+
assert len(all_deps) == len(self.meta.dep_prios)
15181521
self.priorities = {id: pri
1519-
for id, pri in zip(self.meta.dependencies, self.meta.dep_prios)}
1520-
assert len(self.meta.dependencies) == len(self.meta.dep_lines)
1522+
for id, pri in zip(all_deps, self.meta.dep_prios)}
1523+
assert len(all_deps) == len(self.meta.dep_lines)
15211524
self.dep_line_map = {id: line
1522-
for id, line in zip(self.meta.dependencies, self.meta.dep_lines)}
1525+
for id, line in zip(all_deps, self.meta.dep_lines)}
15231526
self.child_modules = set(self.meta.child_modules)
15241527
else:
15251528
# When doing a fine-grained cache load, pretend we only
@@ -1909,14 +1912,21 @@ def write_cache(self) -> None:
19091912
self.mark_interface_stale()
19101913
self.interface_hash = new_interface_hash
19111914

1912-
def verify_dependencies(self) -> None:
1913-
"""Report errors for import targets in modules that don't exist."""
1914-
# Strip out indirect dependencies. See comment in build.load_graph().
1915+
def verify_dependencies(self, suppressed_only: bool = False) -> None:
1916+
"""Report errors for import targets in modules that don't exist.
1917+
1918+
If suppressed_only is set, only check suppressed dependencies.
1919+
"""
19151920
manager = self.manager
1916-
dependencies = [dep for dep in self.dependencies
1917-
if self.priorities.get(dep) != PRI_INDIRECT]
19181921
assert self.ancestors is not None
1919-
for dep in dependencies + self.suppressed + self.ancestors:
1922+
if suppressed_only:
1923+
all_deps = self.suppressed
1924+
else:
1925+
# Strip out indirect dependencies. See comment in build.load_graph().
1926+
dependencies = [dep for dep in self.dependencies
1927+
if self.priorities.get(dep) != PRI_INDIRECT]
1928+
all_deps = dependencies + self.suppressed + self.ancestors
1929+
for dep in all_deps:
19201930
options = manager.options.clone_for_module(dep)
19211931
if dep not in manager.modules and not options.ignore_missing_imports:
19221932
line = self.dep_line_map.get(dep, 1)
@@ -1939,13 +1949,18 @@ def verify_dependencies(self) -> None:
19391949
pass
19401950

19411951
def dependency_priorities(self) -> List[int]:
1942-
return [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
1952+
return [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies + self.suppressed]
19431953

19441954
def dependency_lines(self) -> List[int]:
1945-
return [self.dep_line_map.get(dep, 1) for dep in self.dependencies]
1955+
return [self.dep_line_map.get(dep, 1) for dep in self.dependencies + self.suppressed]
19461956

19471957
def generate_unused_ignore_notes(self) -> None:
19481958
if self.options.warn_unused_ignores:
1959+
# If this file was initially loaded from the cache, it may have suppressed
1960+
# dependencies due to imports with ignores on them. We need to generate
1961+
# those errors to avoid spuriously flagging them as unused ignores.
1962+
if self.meta:
1963+
self.verify_dependencies(suppressed_only=True)
19491964
self.manager.errors.generate_unused_ignore_notes(self.xpath)
19501965

19511966

test-data/unit/check-incremental.test

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
-- annotation, and any files expect to be stale (aka have a modified interface)
2929
-- should be annotated in the [stale] annotation. Note that a file that ends up
3030
-- producing an error has its caches deleted and is marked stale automatically.
31-
-- Such files don't need to be included in [stale ...] list.
31+
-- Such files do not need to be included in [stale ...] list.
3232
--
3333
-- The test suite will automatically assume that __main__ is stale and rechecked in
3434
-- all cases so we can avoid constantly having to annotate it. The list of
@@ -4206,3 +4206,32 @@ class Two:
42064206
pass
42074207
[out2]
42084208
tmp/m/two.py:2: error: Revealed type is 'builtins.str'
4209+
4210+
[case testImportUnusedIgnore1]
4211+
# flags: --warn-unused-ignores
4212+
import a
4213+
[file a.py]
4214+
import b
4215+
import foo # type: ignore
4216+
[file b.py]
4217+
x = 1
4218+
[file b.py.2]
4219+
x = '2'
4220+
4221+
-- TODO: Fails due to #4856
4222+
[case testImportUnusedIgnore2-skip]
4223+
# flags: --warn-unused-ignores
4224+
import a
4225+
[file a.py]
4226+
import b
4227+
import c # type: ignore
4228+
[file b.py]
4229+
x = 1
4230+
[file b.py.2]
4231+
x = 'hi'
4232+
[file c.py.3]
4233+
pass
4234+
[out]
4235+
[out2]
4236+
[out3]
4237+
tmp/a.py:1: note: unused 'type: ignore' comment

0 commit comments

Comments
 (0)