Skip to content

Commit 5e822a8

Browse files
Michael0x2agvanrossum
authored andcommitted
Detect and record implicit dependencies in incremental mode (#2041)
For full details see #2041
1 parent 7c76609 commit 5e822a8

12 files changed

+920
-58
lines changed

mypy/build.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
SymbolTableNode, MODULE_REF)
3030
from mypy.semanal import FirstPass, SemanticAnalyzer, ThirdPass
3131
from mypy.checker import TypeChecker
32+
from mypy.indirection import TypeIndirectionVisitor
3233
from mypy.errors import Errors, CompileError, DecodeError, report_internal_error
3334
from mypy import fixup
3435
from mypy.report import Reports
@@ -306,6 +307,7 @@ def default_lib_path(data_dir: str, pyversion: Tuple[int, int]) -> List[str]:
306307
PRI_HIGH = 5 # top-level "from X import blah"
307308
PRI_MED = 10 # top-level "import X"
308309
PRI_LOW = 20 # either form inside a function
310+
PRI_INDIRECT = 30 # an indirect dependency
309311
PRI_ALL = 99 # include all priorities
310312

311313

@@ -352,6 +354,7 @@ def __init__(self, data_dir: str,
352354
self.modules = self.semantic_analyzer.modules
353355
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
354356
self.type_checker = TypeChecker(self.errors, self.modules, options=options)
357+
self.indirection_detector = TypeIndirectionVisitor()
355358
self.missing_modules = set() # type: Set[str]
356359
self.stale_modules = set() # type: Set[str]
357360
self.rechecked_modules = set() # type: Set[str]
@@ -1422,11 +1425,40 @@ def type_check(self) -> None:
14221425
return
14231426
with self.wrap_context():
14241427
manager.type_checker.visit_file(self.tree, self.xpath)
1428+
1429+
if manager.options.incremental:
1430+
self._patch_indirect_dependencies(manager.type_checker.module_refs)
1431+
14251432
if manager.options.dump_inference_stats:
14261433
dump_type_stats(self.tree, self.xpath, inferred=True,
14271434
typemap=manager.type_checker.type_map)
14281435
manager.report_file(self.tree)
14291436

1437+
def _patch_indirect_dependencies(self, module_refs: Set[str]) -> None:
1438+
types = self.manager.type_checker.module_type_map.values()
1439+
valid = self.valid_references()
1440+
1441+
encountered = self.manager.indirection_detector.find_modules(types) | module_refs
1442+
extra = encountered - valid
1443+
1444+
for dep in sorted(extra):
1445+
if dep not in self.manager.modules:
1446+
continue
1447+
if dep not in self.suppressed and dep not in self.manager.missing_modules:
1448+
self.dependencies.append(dep)
1449+
self.priorities[dep] = PRI_INDIRECT
1450+
elif dep not in self.suppressed and dep in self.manager.missing_modules:
1451+
self.suppressed.append(dep)
1452+
1453+
def valid_references(self) -> Set[str]:
1454+
valid_refs = set(self.dependencies + self.suppressed + self.ancestors)
1455+
valid_refs .add(self.id)
1456+
1457+
if "os" in valid_refs:
1458+
valid_refs.add("os.path")
1459+
1460+
return valid_refs
1461+
14301462
def write_cache(self) -> None:
14311463
if self.path and self.manager.options.incremental and not self.manager.errors.is_errors():
14321464
dep_prios = [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
@@ -1605,25 +1637,6 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
16051637
else:
16061638
process_stale_scc(graph, scc)
16071639

1608-
# TODO: This is a workaround to get around the "chaining imports" problem
1609-
# with the interface checks.
1610-
#
1611-
# That is, if we have a file named `module_a.py` which does:
1612-
#
1613-
# import module_b
1614-
# module_b.module_c.foo(3)
1615-
#
1616-
# ...and if the type signature of `module_c.foo(...)` were to change,
1617-
# module_a_ would not be rechecked since the interface of `module_b`
1618-
# would not be considered changed.
1619-
#
1620-
# As a workaround, this check will force a module's interface to be
1621-
# considered stale if anything it imports has a stale interface,
1622-
# which ensures these changes are caught and propagated.
1623-
if len(stale_deps) > 0:
1624-
for id in scc:
1625-
graph[id].mark_interface_stale()
1626-
16271640

16281641
def order_ascc(graph: Graph, ascc: AbstractSet[str], pri_max: int = PRI_ALL) -> List[str]:
16291642
"""Come up with the ideal processing order within an SCC.

mypy/checker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ class TypeChecker(NodeVisitor[Type]):
8787
msg = None # type: MessageBuilder
8888
# Types of type checked nodes
8989
type_map = None # type: Dict[Node, Type]
90+
# Types of type checked nodes within this specific module
91+
module_type_map = None # type: Dict[Node, Type]
9092

9193
# Helper for managing conditional types
9294
binder = None # type: ConditionalTypeBinder
@@ -121,6 +123,10 @@ class TypeChecker(NodeVisitor[Type]):
121123
suppress_none_errors = False
122124
options = None # type: Options
123125

126+
# The set of all dependencies (suppressed or not) that this module accesses, either
127+
# directly or indirectly.
128+
module_refs = None # type: Set[str]
129+
124130
def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options) -> None:
125131
"""Construct a type checker.
126132
@@ -131,6 +137,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option
131137
self.options = options
132138
self.msg = MessageBuilder(errors, modules)
133139
self.type_map = {}
140+
self.module_type_map = {}
134141
self.binder = ConditionalTypeBinder()
135142
self.expr_checker = mypy.checkexpr.ExpressionChecker(self, self.msg)
136143
self.return_types = []
@@ -142,6 +149,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option
142149
self.deferred_nodes = []
143150
self.pass_num = 0
144151
self.current_node_deferred = False
152+
self.module_refs = set()
145153

146154
def visit_file(self, file_node: MypyFile, path: str) -> None:
147155
"""Type check a mypy file with the given path."""
@@ -152,6 +160,8 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
152160
self.weak_opts = file_node.weak_opts
153161
self.enter_partial_types()
154162
self.is_typeshed_stub = self.errors.is_typeshed_file(path)
163+
self.module_type_map = {}
164+
self.module_refs = set()
155165
if self.options.strict_optional_whitelist is None:
156166
self.suppress_none_errors = False
157167
else:
@@ -2211,6 +2221,8 @@ def check_type_equivalency(self, t1: Type, t2: Type, node: Context,
22112221
def store_type(self, node: Node, typ: Type) -> None:
22122222
"""Store the type of a node in the type map."""
22132223
self.type_map[node] = typ
2224+
if typ is not None:
2225+
self.module_type_map[node] = typ
22142226

22152227
def typing_mode_none(self) -> bool:
22162228
if self.is_dynamic_function() and not self.options.check_untyped_defs:

mypy/checkexpr.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Expression type checker. This file is conceptually part of TypeChecker."""
22

3-
from typing import cast, Dict, List, Tuple, Callable, Union, Optional
3+
from typing import cast, Dict, Set, List, Iterable, Tuple, Callable, Union, Optional
44

55
from mypy.types import (
66
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
@@ -16,7 +16,7 @@
1616
ListComprehension, GeneratorExpr, SetExpr, MypyFile, Decorator,
1717
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension,
1818
DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr,
19-
TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2
19+
TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2, MODULE_REF,
2020
)
2121
from mypy.nodes import function_type
2222
from mypy import nodes
@@ -36,6 +36,7 @@
3636
from mypy.constraints import get_actual_type
3737
from mypy.checkstrformat import StringFormatterChecker
3838
from mypy.expandtype import expand_type
39+
from mypy.util import split_module_names
3940

4041
from mypy import experiments
4142

@@ -45,6 +46,35 @@
4546
None]
4647

4748

49+
def extract_refexpr_names(expr: RefExpr) -> Set[str]:
50+
"""Recursively extracts all module references from a reference expression.
51+
52+
Note that currently, the only two subclasses of RefExpr are NameExpr and
53+
MemberExpr."""
54+
output = set() # type: Set[str]
55+
while expr.kind == MODULE_REF or expr.fullname is not None:
56+
if expr.kind == MODULE_REF:
57+
output.add(expr.fullname)
58+
59+
if isinstance(expr, NameExpr):
60+
is_suppressed_import = isinstance(expr.node, Var) and expr.node.is_suppressed_import
61+
if isinstance(expr.node, TypeInfo):
62+
# Reference to a class or a nested class
63+
output.update(split_module_names(expr.node.module_name))
64+
elif expr.fullname is not None and '.' in expr.fullname and not is_suppressed_import:
65+
# Everything else (that is not a silenced import within a class)
66+
output.add(expr.fullname.rsplit('.', 1)[0])
67+
break
68+
elif isinstance(expr, MemberExpr):
69+
if isinstance(expr.expr, RefExpr):
70+
expr = expr.expr
71+
else:
72+
break
73+
else:
74+
raise AssertionError("Unknown RefExpr subclass: {}".format(type(expr)))
75+
return output
76+
77+
4878
class Finished(Exception):
4979
"""Raised if we can terminate overload argument check early (no match)."""
5080

@@ -75,6 +105,7 @@ def visit_name_expr(self, e: NameExpr) -> Type:
75105
76106
It can be of any kind: local, member or global.
77107
"""
108+
self.chk.module_refs.update(extract_refexpr_names(e))
78109
result = self.analyze_ref_expr(e)
79110
return self.chk.narrow_type_from_binder(e, result)
80111

@@ -861,6 +892,7 @@ def apply_generic_arguments2(self, overload: Overloaded, types: List[Type],
861892

862893
def visit_member_expr(self, e: MemberExpr) -> Type:
863894
"""Visit member expression (of form e.id)."""
895+
self.chk.module_refs.update(extract_refexpr_names(e))
864896
result = self.analyze_ordinary_member_access(e, False)
865897
return self.chk.narrow_type_from_binder(e, result)
866898

mypy/indirection.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from typing import Dict, Iterable, List, Optional, Set
2+
from abc import abstractmethod
3+
4+
from mypy.visitor import NodeVisitor
5+
from mypy.types import TypeVisitor
6+
from mypy.nodes import MODULE_REF
7+
import mypy.nodes as nodes
8+
import mypy.types as types
9+
from mypy.util import split_module_names
10+
11+
12+
def extract_module_names(type_name: Optional[str]) -> List[str]:
13+
"""Returns the module names of a fully qualified type name."""
14+
if type_name is not None:
15+
# Discard the first one, which is just the qualified name of the type
16+
possible_module_names = split_module_names(type_name)
17+
return possible_module_names[1:]
18+
else:
19+
return []
20+
21+
22+
class TypeIndirectionVisitor(TypeVisitor[Set[str]]):
23+
"""Returns all module references within a particular type."""
24+
25+
def __init__(self) -> None:
26+
self.cache = {} # type: Dict[types.Type, Set[str]]
27+
28+
def find_modules(self, typs: Iterable[types.Type]) -> Set[str]:
29+
return self._visit(*typs)
30+
31+
def _visit(self, *typs: types.Type) -> Set[str]:
32+
output = set() # type: Set[str]
33+
for typ in typs:
34+
if typ in self.cache:
35+
modules = self.cache[typ]
36+
else:
37+
modules = typ.accept(self)
38+
self.cache[typ] = set(modules)
39+
output.update(modules)
40+
return output
41+
42+
def visit_unbound_type(self, t: types.UnboundType) -> Set[str]:
43+
return self._visit(*t.args)
44+
45+
def visit_type_list(self, t: types.TypeList) -> Set[str]:
46+
return self._visit(*t.items)
47+
48+
def visit_error_type(self, t: types.ErrorType) -> Set[str]:
49+
return set()
50+
51+
def visit_any(self, t: types.AnyType) -> Set[str]:
52+
return set()
53+
54+
def visit_void(self, t: types.Void) -> Set[str]:
55+
return set()
56+
57+
def visit_none_type(self, t: types.NoneTyp) -> Set[str]:
58+
return set()
59+
60+
def visit_uninhabited_type(self, t: types.UninhabitedType) -> Set[str]:
61+
return set()
62+
63+
def visit_erased_type(self, t: types.ErasedType) -> Set[str]:
64+
return set()
65+
66+
def visit_deleted_type(self, t: types.DeletedType) -> Set[str]:
67+
return set()
68+
69+
def visit_type_var(self, t: types.TypeVarType) -> Set[str]:
70+
return self._visit(*t.values) | self._visit(t.upper_bound)
71+
72+
def visit_instance(self, t: types.Instance) -> Set[str]:
73+
out = self._visit(*t.args)
74+
if t.type is not None:
75+
out.update(split_module_names(t.type.module_name))
76+
return out
77+
78+
def visit_callable_type(self, t: types.CallableType) -> Set[str]:
79+
out = self._visit(*t.arg_types) | self._visit(t.ret_type)
80+
if t.definition is not None:
81+
out.update(extract_module_names(t.definition.fullname()))
82+
return out
83+
84+
def visit_overloaded(self, t: types.Overloaded) -> Set[str]:
85+
return self._visit(*t.items()) | self._visit(t.fallback)
86+
87+
def visit_tuple_type(self, t: types.TupleType) -> Set[str]:
88+
return self._visit(*t.items) | self._visit(t.fallback)
89+
90+
def visit_star_type(self, t: types.StarType) -> Set[str]:
91+
return set()
92+
93+
def visit_union_type(self, t: types.UnionType) -> Set[str]:
94+
return self._visit(*t.items)
95+
96+
def visit_partial_type(self, t: types.PartialType) -> Set[str]:
97+
return set()
98+
99+
def visit_ellipsis_type(self, t: types.EllipsisType) -> Set[str]:
100+
return set()
101+
102+
def visit_type_type(self, t: types.TypeType) -> Set[str]:
103+
return self._visit(t.item)

0 commit comments

Comments
 (0)