Skip to content

Commit f456718

Browse files
JukkaLmsullivan
authored andcommitted
Fine-grained: Fix crashes due to stale dependencies (#4725)
We could have a dependency pointing to a target that no longer was valid, and various assertions could be triggered, causing a crash. Now we just filter out bad targets to avoid crashes. Fixes #4723 and other related issues.
1 parent e3a3a48 commit f456718

File tree

4 files changed

+201
-27
lines changed

4 files changed

+201
-27
lines changed

mypy/nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ def set_line(self, target: Union[Context, int], column: Optional[int] = None) ->
458458

459459

460460
class FuncItem(FuncBase):
461-
arguments = [] # type: List[Argument]
461+
arguments = [] # type: List[Argument] # Note: Can be None if deserialized (type is a lie!)
462462
arg_names = [] # type: List[str]
463463
arg_kinds = [] # type: List[int]
464464
# Minimum number of arguments

mypy/server/update.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,7 @@ def propagate_changes_using_dependencies(
690690
if num_iter > MAX_ITER:
691691
raise RuntimeError('Max number of iterations (%d) reached (endless loop?)' % MAX_ITER)
692692

693-
todo = find_targets_recursive(manager, triggered, deps,
694-
manager.modules, up_to_date_modules)
693+
todo = find_targets_recursive(manager, triggered, deps, up_to_date_modules)
695694
# Also process targets that used to have errors, as otherwise some
696695
# errors might be lost.
697696
for target in targets_with_errors:
@@ -700,7 +699,7 @@ def propagate_changes_using_dependencies(
700699
if id not in todo:
701700
todo[id] = set()
702701
manager.log_fine_grained('process target with error: %s' % target)
703-
todo[id].update(lookup_target(manager.modules, target))
702+
todo[id].update(lookup_target(manager, target))
704703
triggered = set()
705704
# TODO: Preserve order (set is not optimal)
706705
for id, nodes in sorted(todo.items(), key=lambda x: x[0]):
@@ -727,7 +726,6 @@ def find_targets_recursive(
727726
manager: BuildManager,
728727
triggers: Set[str],
729728
deps: Dict[str, Set[str]],
730-
modules: Dict[str, MypyFile],
731729
up_to_date_modules: Set[str]) -> Dict[str, Set[DeferredNode]]:
732730
"""Find names of all targets that need to reprocessed, given some triggers.
733731
@@ -748,7 +746,7 @@ def find_targets_recursive(
748746
if target.startswith('<'):
749747
worklist |= deps.get(target, set()) - processed
750748
else:
751-
module_id = module_prefix(modules, target)
749+
module_id = module_prefix(manager.modules, target)
752750
if module_id is None:
753751
# Deleted module.
754752
continue
@@ -758,7 +756,7 @@ def find_targets_recursive(
758756
if module_id not in result:
759757
result[module_id] = set()
760758
manager.log_fine_grained('process: %s' % target)
761-
deferred = lookup_target(modules, target)
759+
deferred = lookup_target(manager, target)
762760
result[module_id].update(deferred)
763761

764762
return result
@@ -794,8 +792,11 @@ def key(node: DeferredNode) -> int:
794792
# TODO: ignore_all argument to set_file_ignored_lines
795793
manager.errors.set_file_ignored_lines(file_node.path, file_node.ignored_lines)
796794

797-
targets = {target_from_node(module_id, node.node)
798-
for node in nodes}
795+
targets = set()
796+
for node in nodes:
797+
target = target_from_node(module_id, node.node)
798+
if target is not None:
799+
targets.add(target)
799800
manager.errors.clear_errors_in_targets(file_node.path, targets)
800801

801802
# Strip semantic analysis information.
@@ -897,11 +898,18 @@ def update_deps(module_id: str,
897898
deps.setdefault(trigger, set()).update(targets)
898899

899900

900-
def lookup_target(modules: Dict[str, MypyFile], target: str) -> List[DeferredNode]:
901+
def lookup_target(manager: BuildManager,
902+
target: str) -> List[DeferredNode]:
901903
"""Look up a target by fully-qualified name."""
904+
905+
def not_found() -> None:
906+
manager.log_fine_grained(
907+
"Can't find matching target for %s (stale dependency?)" % target)
908+
909+
modules = manager.modules
902910
items = split_target(modules, target)
903911
if items is None:
904-
# Deleted target
912+
not_found() # Stale dependency
905913
return []
906914
module, rest = items
907915
if rest:
@@ -916,12 +924,11 @@ def lookup_target(modules: Dict[str, MypyFile], target: str) -> List[DeferredNod
916924
if isinstance(node, TypeInfo):
917925
active_class = node
918926
active_class_name = node.name()
919-
# TODO: Is it possible for the assertion to fail?
920927
if isinstance(node, MypyFile):
921928
file = node
922-
assert isinstance(node, (MypyFile, TypeInfo))
923-
if c not in node.names:
924-
# Deleted target
929+
if (not isinstance(node, (MypyFile, TypeInfo))
930+
or c not in node.names):
931+
not_found() # Stale dependency
925932
return []
926933
node = node.names[c].node
927934
if isinstance(node, TypeInfo):
@@ -930,18 +937,33 @@ def lookup_target(modules: Dict[str, MypyFile], target: str) -> List[DeferredNod
930937
# typically a module top-level, since we don't support processing class
931938
# bodies as separate entitites for simplicity.
932939
assert file is not None
940+
if node.fullname() != target:
941+
# This is a reference to a different TypeInfo, likely due to a stale dependency.
942+
# Processing them would spell trouble -- for example, we could be refreshing
943+
# a deserialized TypeInfo with missing attributes.
944+
not_found()
945+
return []
933946
result = [DeferredNode(file, None, None)]
934947
for name, symnode in node.names.items():
935948
node = symnode.node
936949
if isinstance(node, FuncDef):
937-
result.extend(lookup_target(modules, target + '.' + name))
950+
result.extend(lookup_target(manager, target + '.' + name))
938951
return result
939952
if isinstance(node, Decorator):
940953
# Decorator targets actually refer to the function definition only.
941954
node = node.func
942-
assert isinstance(node, (FuncDef,
955+
if not isinstance(node, (FuncDef,
943956
MypyFile,
944-
OverloadedFuncDef)), 'unexpected type: %s' % type(node)
957+
OverloadedFuncDef)):
958+
# The target can't be refreshed. It's possible that the target was
959+
# changed to another type and we have a stale dependency pointing to it.
960+
not_found()
961+
return []
962+
if node.fullname() != target:
963+
# Stale reference points to something unexpected. We shouldn't process since the
964+
# context will be wrong and it could be a partially initialized deserialized node.
965+
not_found()
966+
return []
945967
return [DeferredNode(node, active_class_name, active_class)]
946968

947969

@@ -950,14 +972,20 @@ def is_verbose(manager: BuildManager) -> bool:
950972

951973

952974
def target_from_node(module: str,
953-
node: Union[FuncDef, MypyFile, OverloadedFuncDef, LambdaExpr]) -> str:
975+
node: Union[FuncDef, MypyFile, OverloadedFuncDef, LambdaExpr]
976+
) -> Optional[str]:
954977
"""Return the target name corresponding to a deferred node.
955978
956979
Args:
957980
module: Must be module id of the module that defines 'node'
981+
982+
Returns the target name, or None if the node is not a valid target in the given
983+
module (for example, if it's actually defined in another module).
958984
"""
959985
if isinstance(node, MypyFile):
960-
assert module == node.fullname()
986+
if module != node.fullname():
987+
# Actually a reference to another module -- likely a stale dependency.
988+
return None
961989
return module
962990
elif isinstance(node, (OverloadedFuncDef, FuncDef)):
963991
if node.info is not None:

mypy/traverser.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ def visit_block(self, block: Block) -> None:
3434
s.accept(self)
3535

3636
def visit_func(self, o: FuncItem) -> None:
37-
for arg in o.arguments:
38-
init = arg.initializer
39-
if init is not None:
40-
init.accept(self)
41-
42-
for arg in o.arguments:
43-
self.visit_var(arg.variable)
37+
if o.arguments is not None:
38+
for arg in o.arguments:
39+
init = arg.initializer
40+
if init is not None:
41+
init.accept(self)
42+
43+
for arg in o.arguments:
44+
self.visit_var(arg.variable)
4445

4546
o.body.accept(self)
4647

test-data/unit/fine-grained.test

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2833,6 +2833,151 @@ class C(Enum):
28332833
a.py:4: error: Argument 1 to "f" has incompatible type "C"; expected "int"
28342834
a.py:5: error: Argument 1 to "f" has incompatible type "C"; expected "int"
28352835

2836+
[case testChangeFunctionToVariableAndRefreshUsingStaleDependency]
2837+
import a
2838+
import c
2839+
[file a.py]
2840+
import c
2841+
def f() -> c.A: pass
2842+
[file a.py.2]
2843+
f = 1
2844+
[file c.py]
2845+
class A: pass
2846+
[file c.py.3]
2847+
[out]
2848+
==
2849+
==
2850+
2851+
[case testChangeFunctionToTypeVarAndRefreshUsingStaleDependency]
2852+
import a
2853+
import c
2854+
[file a.py]
2855+
import c
2856+
def f() -> c.A: pass
2857+
[file a.py.2]
2858+
from typing import TypeVar
2859+
f = TypeVar('f')
2860+
[file c.py]
2861+
class A: pass
2862+
[file c.py.3]
2863+
[out]
2864+
==
2865+
==
2866+
2867+
[case testChangeFunctionToModuleAndRefreshUsingStaleDependency]
2868+
import a
2869+
import c
2870+
[file a.py]
2871+
import c
2872+
def f() -> c.A: pass
2873+
[file a.py.2]
2874+
import c as f
2875+
[file c.py]
2876+
class A: pass
2877+
[file c.py.3]
2878+
[out]
2879+
==
2880+
==
2881+
2882+
[case testChangeFunctionToTypeAliasAndRefreshUsingStaleDependency1]
2883+
import a
2884+
import c
2885+
[file a.py]
2886+
import c
2887+
def f() -> c.A: pass
2888+
[file a.py.2]
2889+
f = int
2890+
[file c.py]
2891+
class A: pass
2892+
[file c.py.3]
2893+
[out]
2894+
==
2895+
==
2896+
2897+
[case testChangeFunctionToTypeAliasAndRefreshUsingStaleDependency2]
2898+
import a
2899+
import c
2900+
[file a.py]
2901+
import c
2902+
def f() -> c.A: pass
2903+
[file a.py.2]
2904+
from typing import List
2905+
f = List[int]
2906+
[file c.py]
2907+
class A: pass
2908+
[file c.py.3]
2909+
[builtins fixtures/list.pyi]
2910+
[out]
2911+
==
2912+
==
2913+
2914+
[case testChangeFunctionToClassAndRefreshUsingStaleDependency]
2915+
import a
2916+
import c
2917+
[file a.py]
2918+
import c
2919+
def f() -> c.A: pass
2920+
[file a.py.2]
2921+
class f: pass
2922+
[file c.py]
2923+
class A: pass
2924+
[file c.py.3]
2925+
[out]
2926+
==
2927+
==
2928+
2929+
[case testClassToVariableAndRefreshUsingStaleDependency]
2930+
import a
2931+
import c
2932+
[file a.py]
2933+
import c
2934+
class A:
2935+
def f(self) -> c.A: pass
2936+
[file a.py.2]
2937+
A = 0
2938+
[file c.py]
2939+
class A: pass
2940+
[file c.py.3]
2941+
[out]
2942+
==
2943+
==
2944+
2945+
[case testFunctionToImportedFunctionAndRefreshUsingStaleDependency]
2946+
import a
2947+
import c
2948+
[file a.py]
2949+
import c
2950+
def f() -> c.A: pass
2951+
[file a.py.2]
2952+
from d import f
2953+
[file c.py]
2954+
class A: pass
2955+
[file c.py.3]
2956+
[file d.py]
2957+
def g() -> None: pass
2958+
def f() -> None:
2959+
g()
2960+
[out]
2961+
==
2962+
==
2963+
2964+
[case testMethodToVariableAndRefreshUsingStaleDependency]
2965+
import a
2966+
import c
2967+
[file a.py]
2968+
import c
2969+
class B:
2970+
def f(self) -> c.A: pass
2971+
[file a.py.2]
2972+
class B:
2973+
f = 0
2974+
[file c.py]
2975+
class A: pass
2976+
[file c.py.3]
2977+
[out]
2978+
==
2979+
==
2980+
28362981
[case testRefreshNestedClassWithSelfReference]
28372982
import a
28382983
[file a.py]

0 commit comments

Comments
 (0)