Skip to content

Commit 8f253c2

Browse files
authored
Support type aliases in fine-grained incremental mode (#4525)
Fixes #4394 This covers all common cases and majority of corner cases: *Simple aliases *Generic aliases *Forward references *Nested aliases *Chained aliases * Special forms Note that among tests added some also pass without this addition (probably because some dependencies are added by coincidence). Note that I mainly focus on false negatives, since in my experience while playing with fine-grained daemon, this is the most typical problem.
1 parent b2dd508 commit 8f253c2

File tree

12 files changed

+1438
-104
lines changed

12 files changed

+1438
-104
lines changed

mypy/nodes.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
import os
44
from abc import abstractmethod
5-
from collections import OrderedDict
5+
from collections import OrderedDict, defaultdict
66
from typing import (
7-
Any, TypeVar, List, Tuple, cast, Set, Dict, Union, Optional, Callable, Sequence,
7+
Any, TypeVar, List, Tuple, cast, Set, Dict, Union, Optional, Callable, Sequence
88
)
99

10+
MYPY = False
11+
if MYPY:
12+
from typing import DefaultDict
13+
1014
import mypy.strconv
1115
from mypy.util import short_type
1216
from mypy.visitor import NodeVisitor, StatementVisitor, ExpressionVisitor
@@ -194,6 +198,8 @@ class MypyFile(SymbolNode):
194198
path = ''
195199
# Top-level definitions and statements
196200
defs = None # type: List[Statement]
201+
# Type alias dependencies as mapping from target to set of alias full names
202+
alias_deps = None # type: DefaultDict[str, Set[str]]
197203
# Is there a UTF-8 BOM at the start?
198204
is_bom = False
199205
names = None # type: SymbolTable
@@ -215,6 +221,7 @@ def __init__(self,
215221
self.line = 1 # Dummy line number
216222
self.imports = imports
217223
self.is_bom = is_bom
224+
self.alias_deps = defaultdict(set)
218225
if ignored_lines:
219226
self.ignored_lines = ignored_lines
220227
else:
@@ -797,6 +804,8 @@ class AssignmentStmt(Statement):
797804
unanalyzed_type = None # type: Optional[mypy.types.Type]
798805
# This indicates usage of PEP 526 type annotation syntax in assignment.
799806
new_syntax = False # type: bool
807+
# Does this assignment define a type alias?
808+
is_alias_def = False
800809

801810
def __init__(self, lvalues: List[Lvalue], rvalue: Expression,
802811
type: 'Optional[mypy.types.Type]' = None, new_syntax: bool = False) -> None:
@@ -2343,6 +2352,10 @@ class SymbolTableNode:
23432352
normalized = False # type: bool
23442353
# Was this defined by assignment to self attribute?
23452354
implicit = False # type: bool
2355+
# Is this node refers to other node via node aliasing?
2356+
# (This is currently used for simple aliases like `A = int` instead of .type_override)
2357+
is_aliasing = False # type: bool
2358+
alias_name = None # type: Optional[str]
23462359

23472360
def __init__(self,
23482361
kind: int,

mypy/scope.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Track current scope to easily calculate the corresponding fine-grained target.
2+
3+
TODO: Use everywhere where we track targets, including in mypy.errors.
4+
"""
5+
6+
from typing import List, Optional
7+
8+
from mypy.nodes import TypeInfo, FuncItem
9+
10+
11+
class Scope:
12+
"""Track which target we are processing at any given time."""
13+
14+
def __init__(self) -> None:
15+
self.module = None # type: Optional[str]
16+
self.classes = [] # type: List[TypeInfo]
17+
self.function = None # type: Optional[FuncItem]
18+
# Number of nested scopes ignored (that don't get their own separate targets)
19+
self.ignored = 0
20+
21+
def current_module_id(self) -> str:
22+
assert self.module
23+
return self.module
24+
25+
def current_target(self) -> str:
26+
"""Return the current target (non-class; for a class return enclosing module)."""
27+
assert self.module
28+
target = self.module
29+
if self.function:
30+
if self.classes:
31+
target += '.' + '.'.join(c.name() for c in self.classes)
32+
target += '.' + self.function.name()
33+
return target
34+
35+
def current_full_target(self) -> str:
36+
"""Return the current target (may be a class)."""
37+
assert self.module
38+
target = self.module
39+
if self.classes:
40+
target += '.' + '.'.join(c.name() for c in self.classes)
41+
if self.function:
42+
target += '.' + self.function.name()
43+
return target
44+
45+
def enter_file(self, prefix: str) -> None:
46+
self.module = prefix
47+
self.classes = []
48+
self.function = None
49+
self.ignored = 0
50+
51+
def enter_function(self, fdef: FuncItem) -> None:
52+
if not self.function:
53+
self.function = fdef
54+
else:
55+
# Nested functions are part of the topmost function target.
56+
self.ignored += 1
57+
58+
def enter_class(self, info: TypeInfo) -> None:
59+
"""Enter a class target scope."""
60+
if not self.function:
61+
self.classes.append(info)
62+
else:
63+
# Classes within functions are part of the enclosing function target.
64+
self.ignored += 1
65+
66+
def leave(self) -> None:
67+
"""Leave the innermost scope (can be any kind of scope)."""
68+
if self.ignored:
69+
# Leave a scope that's included in the enclosing target.
70+
self.ignored -= 1
71+
elif self.function:
72+
# Function is always the innermost target.
73+
self.function = None
74+
elif self.classes:
75+
# Leave the innermost class.
76+
self.classes.pop()
77+
else:
78+
# Leave module.
79+
assert self.module
80+
self.module = None

mypy/semanal.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
from mypy import join
8686
from mypy.util import get_prefix, correct_relative_import
8787
from mypy.semanal_shared import PRIORITY_FALLBACKS
88+
from mypy.scope import Scope
8889

8990

9091
T = TypeVar('T')
@@ -255,6 +256,7 @@ def __init__(self,
255256
# If True, process function definitions. If False, don't. This is used
256257
# for processing module top levels in fine-grained incremental mode.
257258
self.recurse_into_functions = True
259+
self.scope = Scope()
258260

259261
def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
260262
patches: List[Tuple[int, Callable[[], None]]]) -> None:
@@ -287,8 +289,10 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
287289
v.is_ready = True
288290

289291
defs = file_node.defs
292+
self.scope.enter_file(file_node.fullname())
290293
for d in defs:
291294
self.accept(d)
295+
self.scope.leave()
292296

293297
if self.cur_mod_id == 'builtins':
294298
remove_imported_names_from_symtable(self.globals, 'builtins')
@@ -305,11 +309,13 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
305309

306310
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None:
307311
"""Refresh a stale target in fine-grained incremental mode."""
312+
self.scope.enter_file(self.cur_mod_id)
308313
if isinstance(node, MypyFile):
309314
self.refresh_top_level(node)
310315
else:
311316
self.recurse_into_functions = True
312317
self.accept(node)
318+
self.scope.leave()
313319

314320
def refresh_top_level(self, file_node: MypyFile) -> None:
315321
"""Reanalyze a stale module top-level in fine-grained incremental mode."""
@@ -591,15 +597,19 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
591597

592598
def analyze_function(self, defn: FuncItem) -> None:
593599
is_method = self.is_class_scope()
600+
self.scope.enter_function(defn)
594601
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
595602
if defn.type:
596603
self.check_classvar_in_signature(defn.type)
597604
assert isinstance(defn.type, CallableType)
598605
# Signature must be analyzed in the surrounding scope so that
599606
# class-level imported names and type variables are in scope.
600-
defn.type = self.type_analyzer().visit_callable_type(defn.type, nested=False)
607+
analyzer = self.type_analyzer()
608+
defn.type = analyzer.visit_callable_type(defn.type, nested=False)
609+
self.add_type_alias_deps(analyzer.aliases_used)
601610
self.check_function_signature(defn)
602611
if isinstance(defn, FuncDef):
612+
assert isinstance(defn.type, CallableType)
603613
defn.type = set_callable_name(defn.type, defn)
604614
for arg in defn.arguments:
605615
if arg.initializer:
@@ -633,6 +643,7 @@ def analyze_function(self, defn: FuncItem) -> None:
633643

634644
self.leave()
635645
self.function_stack.pop()
646+
self.scope.leave()
636647

637648
def check_classvar_in_signature(self, typ: Type) -> None:
638649
if isinstance(typ, Overloaded):
@@ -660,10 +671,12 @@ def check_function_signature(self, fdef: FuncItem) -> None:
660671
self.fail('Type signature has too many arguments', fdef, blocker=True)
661672

662673
def visit_class_def(self, defn: ClassDef) -> None:
674+
self.scope.enter_class(defn.info)
663675
with self.analyze_class_body(defn) as should_continue:
664676
if should_continue:
665677
# Analyze class body.
666678
defn.defs.accept(self)
679+
self.scope.leave()
667680

668681
@contextmanager
669682
def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]:
@@ -1679,7 +1692,24 @@ def anal_type(self, t: Type, *,
16791692
aliasing=aliasing,
16801693
allow_tuple_literal=allow_tuple_literal,
16811694
third_pass=third_pass)
1682-
return t.accept(a)
1695+
typ = t.accept(a)
1696+
self.add_type_alias_deps(a.aliases_used)
1697+
return typ
1698+
1699+
def add_type_alias_deps(self, aliases_used: Iterable[str],
1700+
target: Optional[str] = None) -> None:
1701+
"""Add full names of type aliases on which the current node depends.
1702+
1703+
This is used by fine-grained incremental mode to re-check the corresponding nodes.
1704+
If `target` is None, then the target node used will be the current scope.
1705+
"""
1706+
if not aliases_used:
1707+
# A basic optimization to avoid adding targets with no dependencies to
1708+
# the `alias_deps` dict.
1709+
return
1710+
if target is None:
1711+
target = self.scope.current_target()
1712+
self.cur_mod_node.alias_deps[target].update(aliases_used)
16831713

16841714
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
16851715
for lval in s.lvalues:
@@ -1755,10 +1785,17 @@ def alias_fallback(self, tp: Type) -> Instance:
17551785
return Instance(fb_info, [])
17561786

17571787
def analyze_alias(self, rvalue: Expression,
1758-
warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str]]:
1759-
"""Check if 'rvalue' represents a valid type allowed for aliasing
1760-
(e.g. not a type variable). If yes, return the corresponding type and a list of
1761-
qualified type variable names for generic aliases.
1788+
warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str],
1789+
Set[str], List[str]]:
1790+
"""Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).
1791+
1792+
If yes, return the corresponding type, a list of
1793+
qualified type variable names for generic aliases, a set of names the alias depends on,
1794+
and a list of type variables if the alias is generic.
1795+
An schematic example for the dependencies:
1796+
A = int
1797+
B = str
1798+
analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'}
17621799
"""
17631800
dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())
17641801
global_scope = not self.type and not self.function_stack
@@ -1775,15 +1812,21 @@ def analyze_alias(self, rvalue: Expression,
17751812
in_dynamic_func=dynamic,
17761813
global_scope=global_scope,
17771814
warn_bound_tvar=warn_bound_tvar)
1815+
typ = None # type: Optional[Type]
17781816
if res:
1779-
alias_tvars = [name for (name, _) in
1780-
res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))]
1817+
typ, depends_on = res
1818+
found_type_vars = typ.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))
1819+
alias_tvars = [name for (name, node) in found_type_vars]
1820+
qualified_tvars = [node.fullname() for (name, node) in found_type_vars]
17811821
else:
17821822
alias_tvars = []
1783-
return res, alias_tvars
1823+
depends_on = set()
1824+
qualified_tvars = []
1825+
return typ, alias_tvars, depends_on, qualified_tvars
17841826

17851827
def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
17861828
"""Check if assignment creates a type alias and set it up as needed.
1829+
17871830
For simple aliases like L = List we use a simpler mechanism, just copying TypeInfo.
17881831
For subscripted (including generic) aliases the resulting types are stored
17891832
in rvalue.analyzed.
@@ -1809,11 +1852,20 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
18091852
# annotations (see the second rule).
18101853
return
18111854
rvalue = s.rvalue
1812-
res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True)
1855+
res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias(rvalue,
1856+
warn_bound_tvar=True)
18131857
if not res:
18141858
return
1859+
s.is_alias_def = True
18151860
node = self.lookup(lvalue.name, lvalue)
18161861
assert node is not None
1862+
if lvalue.fullname is not None:
1863+
node.alias_name = lvalue.fullname
1864+
self.add_type_alias_deps(depends_on)
1865+
self.add_type_alias_deps(qualified_tvars)
1866+
# The above are only direct deps on other aliases.
1867+
# For subscripted aliases, type deps from expansion are added in deps.py
1868+
# (because the type is stored)
18171869
if not lvalue.is_inferred_def:
18181870
# Type aliases can't be re-defined.
18191871
if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)):
@@ -1830,7 +1882,14 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
18301882
# For simple (on-generic) aliases we use aliasing TypeInfo's
18311883
# to allow using them in runtime context where it makes sense.
18321884
node.node = res.type
1885+
node.is_aliasing = True
18331886
if isinstance(rvalue, RefExpr):
1887+
# For non-subscripted aliases we add type deps right here
1888+
# (because the node is stored, not type)
1889+
# TODO: currently subscripted and unsubscripted aliases are processed differently
1890+
# This leads to duplication of most of the logic with small variations.
1891+
# Fix this.
1892+
self.add_type_alias_deps({node.node.fullname()})
18341893
sym = self.lookup_type_node(rvalue)
18351894
if sym:
18361895
node.normalized = sym.normalized
@@ -3445,12 +3504,15 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
34453504
elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS:
34463505
# Special form -- subscripting a generic type alias.
34473506
# Perform the type substitution and create a new alias.
3448-
res, alias_tvars = self.analyze_alias(expr)
3507+
res, alias_tvars, depends_on, _ = self.analyze_alias(expr)
34493508
assert res is not None, "Failed analyzing already defined alias"
34503509
expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res),
34513510
in_runtime=True)
34523511
expr.analyzed.line = expr.line
34533512
expr.analyzed.column = expr.column
3513+
# We also store fine-grained dependencies to correctly re-process nodes
3514+
# with situations like `L = LongGeneric; x = L[int]()`.
3515+
self.add_type_alias_deps(depends_on)
34543516
elif refers_to_class_or_function(expr.base):
34553517
# Special form -- type application.
34563518
# Translate index to an unanalyzed type.

0 commit comments

Comments
 (0)