Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8e49664
Support redefinition of local variables with a different type
JukkaL Sep 20, 2018
3b409fc
Add docstring
JukkaL Sep 20, 2018
71cb8ea
Allow redefinitions of form "x = f(x)"
JukkaL Sep 20, 2018
35f56ae
Fix special cases
JukkaL Sep 20, 2018
2524777
Add test cases
JukkaL Sep 20, 2018
ab70d0d
Fix redefinition with 'break' and 'continue'
JukkaL Sep 20, 2018
525d5f4
Fix issues
JukkaL Sep 20, 2018
2c614a2
Fix some edge cases
JukkaL Sep 20, 2018
bf2f869
Support top-level redefinition through renaming (WIP)
JukkaL Sep 28, 2018
9e2c6b8
Remove obsolete code
JukkaL Sep 28, 2018
2b885da
Cleanup
JukkaL Sep 28, 2018
712669c
Merge transform code into a single class
JukkaL Sep 28, 2018
e0b7df0
Code cleanup
JukkaL Sep 28, 2018
f153809
Fix issues
JukkaL Sep 28, 2018
856d401
Fix issues
JukkaL Sep 28, 2018
865f1dd
Work around type check error
JukkaL Sep 28, 2018
2deff14
Merge branch 'master' into redefine-var
JukkaL Oct 4, 2018
27a1e57
Fix merge issue
JukkaL Oct 4, 2018
f04e37a
Fix lint
JukkaL Oct 4, 2018
1f9f62e
Fix more tests
JukkaL Oct 4, 2018
9ba82a9
Rename semanal_redef -> renaming
JukkaL Oct 4, 2018
fbf052e
Only perform redefinition if variable has been read
JukkaL Oct 9, 2018
1116b47
A few fixes
JukkaL Oct 9, 2018
70a3085
Add hacky workaround for 'self'
JukkaL Oct 9, 2018
cfad560
Merge branch 'master' into redefine-var
JukkaL Oct 9, 2018
b57960b
Remove unused things
JukkaL Oct 10, 2018
580a774
Revert some unnecessary changes
JukkaL Oct 10, 2018
051e695
Merge commit '3bea26f89a66834f42caaa90cf8db44f99d39dcc' into redefine…
JukkaL Dec 9, 2018
77495fb
Merge branch 'master' into redefine-var
JukkaL Dec 9, 2018
dc03143
Fix test case
JukkaL Dec 9, 2018
4ceed87
Merge branch 'master' into redefine-var
JukkaL Dec 19, 2018
ac00e6d
Merge branch 'master' into redefine-var
JukkaL Dec 20, 2018
20fd0f9
Use `if int():` consistently
JukkaL Dec 20, 2018
3843cea
Fix some test cases
JukkaL Dec 20, 2018
5cfaf9c
Add flag for allowing redefinitions
JukkaL Dec 20, 2018
83ba85f
Use the flag
JukkaL Dec 20, 2018
43a0362
Fix tests
JukkaL Dec 20, 2018
59b3413
Fix test cases
JukkaL Dec 20, 2018
7e3eff9
Add test case for turning flag off explicitly
JukkaL Dec 20, 2018
afa1096
Rename flag to --[dis]allow-redefinition (no plural)
JukkaL Dec 20, 2018
916288c
Fix semantic analyzer tests
JukkaL Dec 21, 2018
459187e
Update test case
JukkaL Dec 21, 2018
e165e46
Merge branch 'master' into redefine-var
JukkaL Jan 15, 2019
0a119b3
Merge branch 'master' into redefine-var
JukkaL Jan 15, 2019
fadb322
Merge branch 'master' into redefine-var
JukkaL Jan 17, 2019
76ad071
Fix brokenness from merge
JukkaL Jan 17, 2019
979a17c
Respond to review
JukkaL Jan 18, 2019
ae0043f
Clarify code
JukkaL Jan 18, 2019
512c1ed
Test additional special case
JukkaL Jan 18, 2019
1f7e36d
Add test case
JukkaL Jan 18, 2019
72043ae
Fix with statements
JukkaL Jan 18, 2019
8c30c90
Remove unnecessary check
JukkaL Jan 18, 2019
e390f38
Add comment
JukkaL Jan 18, 2019
bc0ee19
Ensure renaming cannot happen across scopes in class
JukkaL Jan 18, 2019
54b86d8
Remove unnecessary call
JukkaL Jan 18, 2019
496d2dd
The final assignments takes precedence in class when renaming
JukkaL Jan 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Support redefinition of local variables with a different type
  • Loading branch information
JukkaL committed Sep 28, 2018
commit 8e496645d8c64682afc3ab4dc6e0a4af1d5a2f4e
8 changes: 2 additions & 6 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
)
from mypy.nodes import (
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr,
ARG_POS, ARG_STAR, ARG_STAR2, Decorator, OverloadedFuncDef, TypeAlias, TempNode
ARG_POS, ARG_STAR, ARG_STAR2, Decorator, OverloadedFuncDef, TypeAlias, TempNode,
is_final_node
)
from mypy.messages import MessageBuilder
from mypy.maptype import map_instance_to_supertype
Expand Down Expand Up @@ -839,8 +840,3 @@ def erase_to_bound(t: Type) -> Type:
if isinstance(t.item, TypeVarType):
return TypeType.make_normalized(t.item.upper_bound)
return t


def is_final_node(node: Optional[SymbolNode]) -> bool:
"""Check whether `node` corresponds to a final attribute."""
return isinstance(node, (Var, FuncDef, OverloadedFuncDef, Decorator)) and node.is_final
14 changes: 11 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,15 +926,18 @@ def accept(self, visitor: StatementVisitor[T]) -> T:


class AssignmentStmt(Statement):
"""Assignment statement
"""Assignment statement.

The same node class is used for single assignment, multiple assignment
(e.g. x, y = z) and chained assignment (e.g. x = y = z), assignments
that define new names, and assignments with explicit types (# type).
that define new names, and assignments with explicit types ("# type: t"
or "x: t [= ...]").

An lvalue can be NameExpr, TupleExpr, ListExpr, MemberExpr, IndexExpr.
An lvalue can be NameExpr, TupleExpr, ListExpr, MemberExpr, or IndexExpr.
"""

lvalues = None # type: List[Lvalue]
# This is a TempNode if and only if no rvalue (x: t).
rvalue = None # type: Expression
# Declared type in a comment, may be None.
type = None # type: Optional[mypy.types.Type]
Expand Down Expand Up @@ -2932,3 +2935,8 @@ def is_class_var(expr: NameExpr) -> bool:
if isinstance(expr.node, Var):
return expr.node.is_classvar
return False


def is_final_node(node: Optional[SymbolNode]) -> bool:
"""Check whether `node` corresponds to a final attribute."""
return isinstance(node, (Var, FuncDef, OverloadedFuncDef, Decorator)) and node.is_final
55 changes: 40 additions & 15 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
YieldExpr, ExecStmt, BackquoteExpr, ImportBase, AwaitExpr,
IntExpr, FloatExpr, UnicodeExpr, TempNode, ImportedName,
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, nongen_builtins,
get_member_expr_fullname, REVEAL_TYPE, REVEAL_LOCALS
get_member_expr_fullname, REVEAL_TYPE, REVEAL_LOCALS, is_final_node
)
from mypy.literals import literal
from mypy.tvar_scope import TypeVarScope
Expand All @@ -82,7 +82,7 @@
from mypy import experiments
from mypy.plugin import Plugin, ClassDefContext, SemanticAnalyzerPluginInterface
from mypy.util import get_prefix, correct_relative_import
from mypy.semanal_shared import SemanticAnalyzerInterface, set_callable_name
from mypy.semanal_shared import SemanticAnalyzerInterface, set_callable_name, VarDefAnalyzer
from mypy.scope import Scope
from mypy.semanal_namedtuple import NamedTupleAnalyzer, NAMEDTUPLE_PROHIBITED_NAMES
from mypy.semanal_typeddict import TypedDictAnalyzer
Expand Down Expand Up @@ -255,6 +255,7 @@ def __init__(self,
# for processing module top levels in fine-grained incremental mode.
self.recurse_into_functions = True
self.scope = Scope()
self.var_def_analyzer = VarDefAnalyzer()

# mypyc doesn't properly handle implementing an abstractproperty
# with a regular attribute so we make it a property
Expand Down Expand Up @@ -283,6 +284,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
self.typed_dict_analyzer = TypedDictAnalyzer(options, self, self.msg)
self.enum_call_analyzer = EnumCallAnalyzer(options, self)
self.newtype_analyzer = NewTypeAnalyzer(options, self, self.msg)
self.var_def_analyzer.clear()

with experiments.strict_optional_set(options.strict_optional):
if 'builtins' in self.modules:
Expand All @@ -298,8 +300,10 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,

defs = file_node.defs
self.scope.enter_file(file_node.fullname())
self.var_def_analyzer.enter_block()
for d in defs:
self.accept(d)
self.var_def_analyzer.leave_block()
self.scope.leave()

if self.cur_mod_id == 'builtins':
Expand All @@ -321,11 +325,13 @@ def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
patches: List[Tuple[int, Callable[[], None]]]) -> None:
"""Refresh a stale target in fine-grained incremental mode."""
self.patches = patches
self.var_def_analyzer.enter_block()
if isinstance(node, MypyFile):
self.refresh_top_level(node)
else:
self.recurse_into_functions = True
self.accept(node)
self.var_def_analyzer.leave_block()
del self.patches

def refresh_top_level(self, file_node: MypyFile) -> None:
Expand Down Expand Up @@ -704,6 +710,7 @@ def analyze_function(self, defn: FuncItem) -> None:
self.function_stack.append(defn)
self.enter()
for arg in defn.arguments:
self.var_def_analyzer.process_assignment(arg.variable.name(), True)
self.add_local(arg.variable, defn)

# The first argument of a non-static, non-class method is like 'self'
Expand All @@ -715,7 +722,7 @@ def analyze_function(self, defn: FuncItem) -> None:
# First analyze body of the function but ignore nested functions.
self.postpone_nested_functions_stack.append(FUNCTION_FIRST_PHASE_POSTPONE_SECOND)
self.postponed_functions_stack.append([])
defn.body.accept(self)
self.visit_block(defn.body, new_var_scope=False)

# Analyze nested functions (if any) as a second phase.
self.postpone_nested_functions_stack[-1] = FUNCTION_SECOND_PHASE
Expand Down Expand Up @@ -1437,6 +1444,7 @@ def allow_patching(self, parent_mod: MypyFile, child: str) -> bool:

def add_module_symbol(self, id: str, as_id: str, module_public: bool,
context: Context, module_hidden: bool = False) -> None:
"""Add symbol that is a reference to a module object."""
if id in self.modules:
m = self.modules[id]
self.add_symbol(as_id, SymbolTableNode(MODULE_REF, m,
Expand Down Expand Up @@ -1633,13 +1641,17 @@ def add_unknown_symbol(self, name: str, context: Context, is_import: bool = Fals
# Statements
#

def visit_block(self, b: Block) -> None:
def visit_block(self, b: Block, new_var_scope: bool = True) -> None:
if b.is_unreachable:
return
self.block_depth[-1] += 1
if new_var_scope:
self.var_def_analyzer.enter_block()
for s in b.body:
self.accept(s)
self.block_depth[-1] -= 1
if new_var_scope:
self.var_def_analyzer.leave_block()

def visit_block_maybe(self, b: Optional[Block]) -> None:
if b:
Expand Down Expand Up @@ -1701,9 +1713,11 @@ def final_cb(keep_final: bool) -> None:
if not keep_final:
s.is_final_def = False

has_initializer = not isinstance(s.rvalue, TempNode)
for lval in s.lvalues:
self.analyze_lvalue(lval, explicit_type=s.type is not None,
final_cb=final_cb if s.is_final_def else None)
final_cb=final_cb if s.is_final_def else None,
has_initializer=has_initializer)
self.check_final_implicit_def(s)
self.check_classvar(s)
s.rvalue.accept(self)
Expand Down Expand Up @@ -1974,7 +1988,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
add_global: bool = False,
explicit_type: bool = False,
final_cb: Optional[Callable[[bool], None]] = None) -> None:
final_cb: Optional[Callable[[bool], None]] = None,
has_initializer: bool = True) -> None:
"""Analyze an lvalue or assignment target.

Args:
Expand All @@ -1984,9 +1999,10 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
explicit_type: Assignment has type annotation
final_cb: A callback to call in situation where a final declaration on `self`
overrides an existing name.
has_initializer: Is there a rvalue (i.e. not just "x: t")?
"""
if isinstance(lval, NameExpr):
self.analyze_name_lvalue(lval, add_global, explicit_type, final_cb)
self.analyze_name_lvalue(lval, add_global, explicit_type, final_cb, has_initializer)
elif isinstance(lval, MemberExpr):
if not add_global:
self.analyze_member_lvalue(lval, explicit_type, final_cb=final_cb)
Expand Down Expand Up @@ -2015,11 +2031,13 @@ def analyze_name_lvalue(self,
lval: NameExpr,
add_global: bool,
explicit_type: bool,
final_cb: Optional[Callable[[bool], None]]) -> None:
final_cb: Optional[Callable[[bool], None]],
has_initializer: bool) -> None:
"""Analyze an lvalue that targets a name expression.

Arguments are similar to "analyze_lvalue".
"""
is_new = self.var_def_analyzer.process_assignment(lval.name, has_initializer)
# Top-level definitions within some statements (at least while) are
# not handled in the first pass, so they have to be added now.
nested_global = (not self.is_func_scope() and
Expand All @@ -2035,12 +2053,14 @@ def analyze_name_lvalue(self,
# already in the first pass and added to the symbol table.
# An exception is typing module with incomplete test fixtures.
assert lval.node.name() in self.globals or self.cur_mod_id == 'typing'
elif (self.locals[-1] is not None and lval.name not in self.locals[-1] and
lval.name not in self.global_decls[-1] and
lval.name not in self.nonlocal_decls[-1]):
elif (self.locals[-1] is not None
and (lval.name not in self.locals[-1]
or (is_new and not self.is_local_final(lval.name)))
and lval.name not in self.global_decls[-1]
and lval.name not in self.nonlocal_decls[-1]):
# Define new local name.
v = self.make_name_lvalue_var(lval, LDEF)
self.add_local(v, lval)
self.add_local(v, lval, allow_redefine=is_new)
if lval.name == '_':
# Special case for assignment to local named '_': always infer 'Any'.
typ = AnyType(TypeOfAny.special_form)
Expand All @@ -2054,6 +2074,9 @@ def analyze_name_lvalue(self,
else:
self.make_name_lvalue_point_to_existing_def(lval, explicit_type, final_cb)

def is_local_final(self, name: str) -> bool:
return is_final_node(self.locals[-1][name].node)

def make_name_lvalue_var(self, lvalue: NameExpr, kind: int) -> Var:
"""Return a Var node for an lvalue that is a name expression."""
v = Var(lvalue.name)
Expand Down Expand Up @@ -3498,7 +3521,8 @@ def add_symbol(self, name: str, node: SymbolTableNode,
# TODO: Combine these methods in the first and second pass into a single one.
if self.is_func_scope():
assert self.locals[-1] is not None
if name in self.locals[-1]:
is_new = self.var_def_analyzer.process_assignment(name, can_be_redefined=False)
if name in self.locals[-1] and not is_new:
# Flag redefinition unless this is a reimport of a module.
if not (node.kind == MODULE_REF and
self.locals[-1][name].node == node.node):
Expand Down Expand Up @@ -3528,10 +3552,11 @@ def add_symbol(self, name: str, node: SymbolTableNode,
return
self.globals[name] = node

def add_local(self, node: Union[Var, FuncDef, OverloadedFuncDef], ctx: Context) -> None:
def add_local(self, node: Union[Var, FuncDef, OverloadedFuncDef], ctx: Context,
allow_redefine: bool = False) -> None:
assert self.locals[-1] is not None, "Should not add locals outside a function"
name = node.name()
if name in self.locals[-1]:
if name in self.locals[-1] and not allow_redefine:
self.name_already_defined(name, ctx, self.locals[-1][name])
return
node._fullname = name
Expand Down
4 changes: 4 additions & 0 deletions mypy/semanal_pass1.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) -
v._fullname = self.sem.qualified_name(name)
self.sem.globals[name] = SymbolTableNode(GDEF, v)

self.sem.var_def_analyzer.enter_block()
for d in defs:
d.accept(self)
self.sem.var_def_analyzer.leave_block()

# Add implicit definition of literals/keywords to builtins, as we
# cannot define a variable with them explicitly.
Expand Down Expand Up @@ -138,8 +140,10 @@ def visit_block(self, b: Block) -> None:
if b.is_unreachable:
return
self.sem.block_depth[-1] += 1
self.sem.var_def_analyzer.enter_block()
for node in b.body:
node.accept(self)
self.sem.var_def_analyzer.leave_block()
self.sem.block_depth[-1] -= 1

def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
Expand Down
65 changes: 65 additions & 0 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,68 @@ def set_callable_name(sig: Type, fdef: FuncDef) -> Type:
return sig.with_name(fdef.name())
else:
return sig


class VarDefAnalyzer:
"""Helper class that keeps track of variable redefinitions.

This decides whether an assignment to an existing variable should define a new variable.
This can happen if there are multiple assignments to a variable within the same block:

x = 0
x = str(x) # Defines a new 'x'

Since we have two distinct 'x' variables, they can have independent inferred types.
"""

def __init__(self) -> None:
self.block_id = 0
self.blocks = [] # type: List[int]
self.var_blocks = {} # type: Dict[str, int]

def clear(self) -> None:
self.blocks = []
self.var_blocks = {}

def enter_block(self) -> None:
self.block_id += 1
self.blocks.append(self.block_id)

def leave_block(self) -> None:
self.blocks.pop()

def current_block(self) -> int:
return self.blocks[-1]

def enter_scope(self) -> None:
pass # TODO

def leave_scope(self) -> None:
pass # TODO

def reject_redefinition_of_vars_in_scope(self) -> None:
"""Make it impossible to redefine defined variables in the current scope.

This is used if we encounter a function definition or break/continue that
can make it ambiguous which definition is live.
"""
for key in self.var_blocks:
self.var_blocks[key] = -1

def process_assignment(self, name: str, can_be_redefined: bool) -> bool:
"""Record assignment to given name and return True if it defines a new name."""
block = self.current_block()
if name not in self.var_blocks:
# New definition
if can_be_redefined:
self.var_blocks[name] = block
else:
# This doesn't support arbitrary redefinition.
# TODO: Make this less restricted.
self.var_blocks[name] = -1
return True
elif self.var_blocks[name] == block:
# Redefinition
return True
else:
return False
1 change: 1 addition & 0 deletions mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
'check-attr.test',
'check-dataclasses.test',
'check-final.test',
'check-redefine.test',
]


Expand Down
Loading