From 64ba2a4b727964921de7e5de2e4c0d33e0f97032 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 16 Dec 2025 17:40:57 +0900 Subject: [PATCH 1/3] Delay errors in semanal for proper unreachability information --- mypy/build.py | 1 + mypy/checker.py | 18 +++++++++++++++++ mypy/checkexpr.py | 4 ++-- mypy/semanal.py | 28 +++++++++++++++++++++----- test-data/unit/check-generics.test | 6 +++--- test-data/unit/check-type-aliases.test | 12 +++++++++-- 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 1357047d78a0..0c377971bcbd 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2554,6 +2554,7 @@ def type_checker(self) -> TypeChecker: self.xpath, manager.plugin, self.per_line_checking_time_ns, + manager.semantic_analyzer.delayed_errors, ) return self._type_checker diff --git a/mypy/checker.py b/mypy/checker.py index f90fc4be41f4..f984d00d7b25 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -409,6 +409,7 @@ def __init__( path: str, plugin: Plugin, per_line_checking_time_ns: dict[int, int], + semanal_delayed_errors: dict[tuple[str, int, int], list[ErrorInfo]], ) -> None: """Construct a type checker. @@ -442,6 +443,7 @@ def __init__( self.inferred_attribute_types = None self.allow_constructor_cache = True self.local_type_map = LocalTypeMap(self) + self.semanal_delayed_errors = semanal_delayed_errors # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. @@ -637,6 +639,12 @@ def handle_cannot_determine_type(self, name: str, context: Context) -> None: def accept(self, stmt: Statement) -> None: """Type check a node in the given type context.""" + curr_module = self.scope.stack[0] + if isinstance(curr_module, MypyFile): + key = (curr_module.fullname, stmt.line, stmt.column) + if key in self.semanal_delayed_errors: + self.msg.add_errors(self.semanal_delayed_errors[key]) + try: stmt.accept(self) except Exception as err: @@ -1227,6 +1235,16 @@ def check_func_item( """ self.dynamic_funcs.append(defn.is_dynamic() and not type_override) + # top-level function definitions are one of the main + # things errors can be associated with, and are sometimes masked + # e.g. by a decorator. it's better to make sure to flush any errors + # just in case. + curr_module = self.scope.stack[0] + if isinstance(curr_module, MypyFile): + key = (curr_module.fullname, defn.line, defn.column) + if key in self.semanal_delayed_errors: + self.msg.add_errors(self.semanal_delayed_errors[key]) + enclosing_node_deferred = self.current_node_deferred with self.enter_partial_types(is_function=True): typ = self.function_type(defn) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9990caaeb7a1..9f4bd38d3677 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5495,8 +5495,8 @@ def visit_lambda_expr(self, e: LambdaExpr) -> Type: # Lambdas can have more than one element in body, # when we add "fictional" AssignmentStatement nodes, like in: # `lambda (a, b): a` - for stmt in e.body.body[:-1]: - stmt.accept(self.chk) + for stmt in e.body.body: + self.chk.accept(stmt) # Only type check the return expression, not the return statement. # There's no useful type context. ret_type = self.accept(e.expr(), allow_none_return=True) diff --git a/mypy/semanal.py b/mypy/semanal.py index adbd32ad51b1..e9ba51e5e385 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -59,7 +59,7 @@ from mypy import errorcodes as codes, message_registry from mypy.constant_fold import constant_fold_expr from mypy.errorcodes import PROPERTY_DECORATOR, ErrorCode -from mypy.errors import Errors, report_internal_error +from mypy.errors import ErrorInfo, Errors, report_internal_error from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.message_registry import ErrorMessage from mypy.messages import ( @@ -546,6 +546,8 @@ def __init__( # import foo.bar self.transitive_submodule_imports: dict[str, set[str]] = {} + self.delayed_errors: dict[tuple[str, int, int], list[ErrorInfo]] = {} + # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @property @@ -7093,6 +7095,7 @@ def _get_node_for_class_scoped_import( ) -> SymbolNode | None: if symbol_node is None: return None + # TODO: remove supposedly unnecessary `f` # I promise this type checks; I'm just making mypyc issues go away. # mypyc is absolutely convinced that `symbol_node` narrows to a Var in the following, # when it can also be a FuncBase. Once fixed, `f` in the following can be removed. @@ -7577,10 +7580,25 @@ def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool: return True def accept(self, node: Node) -> None: - try: - node.accept(self) - except Exception as err: - report_internal_error(err, self.errors.file, node.line, self.errors, self.options) + should_filter = isinstance(node, Statement) and not self.options.semantic_analysis_only + if should_filter: + filter_errors: bool | Callable[[str, ErrorInfo], bool] = lambda _, e: not e.blocker + else: + filter_errors = False + with self.msg.filter_errors(filter_errors=filter_errors, save_filtered_errors=True) as msg: + try: + node.accept(self) + except Exception as err: + report_internal_error(err, self.errors.file, node.line, self.errors, self.options) + + errors = msg.filtered_errors() + if errors: + # since nodes aren't hashable, carry things through values + assign_to = (self.cur_mod_id, node.line, node.column) + self.delayed_errors.setdefault(assign_to, []) + self.delayed_errors[assign_to].extend(errors) + + # print(node, [e.message for e in self.delayed_errors[node]]) def expr_to_analyzed_type( self, diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 32975350e20a..f18f5560228e 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -3100,11 +3100,11 @@ def dec4_bound(f: Callable[[I], List[T]]) -> Callable[[I], T]: reveal_type(dec1(lambda x: x)) # N: Revealed type is "def [T] (T`3) -> builtins.list[T`3]" reveal_type(dec2(lambda x: x)) # N: Revealed type is "def [S] (S`5) -> builtins.list[S`5]" reveal_type(dec3(lambda x: x[0])) # N: Revealed type is "def [S] (S`8) -> S`8" -reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`11) -> S`11" +reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`12) -> S`12" reveal_type(dec1(lambda x: 1)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]" reveal_type(dec5(lambda x: x)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]" -reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`19) -> builtins.list[S`19]" -reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`23]) -> T`23" +reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`20) -> builtins.list[S`20]" +reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`24]) -> T`24" dec4_bound(lambda x: x) # E: Value of type variable "I" of "dec4_bound" cannot be "list[T]" [builtins fixtures/list.pyi] diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 0d2e6b5f0c9d..f5c0ef3a5303 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -1216,14 +1216,22 @@ reveal_type(x) # N: Revealed type is "builtins.dict[builtins.str, Any]" [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] -[case testTypeAliasTypeNoUnpackInTypeParams311] +[case testTypeAliasTypeNoUnpackInTypeParams1_311] # flags: --python-version 3.11 from typing_extensions import TypeAliasType, TypeVar, TypeVarTuple, Unpack -T = TypeVar("T") Ts = TypeVarTuple("Ts") +# note that the following is a blocker, so the assignment after isn't checked Ta1 = TypeAliasType("Ta1", None, type_params=(*Ts,)) # E: can't use starred expression here +[builtins fixtures/tuple.pyi] + +[case testTypeAliasTypeNoUnpackInTypeParams2_311] +# flags: --python-version 3.11 +from typing_extensions import TypeAliasType, TypeVar, TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") + Ta2 = TypeAliasType("Ta2", None, type_params=(Unpack[Ts],)) # E: Free type variable expected in type_params argument to TypeAliasType \ # N: Don't Unpack type variables in type_params From edbd57b3c05d95996ae929c00c335743824b2c69 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 17 Dec 2025 16:52:57 +0900 Subject: [PATCH 2/3] Only opt into delayed erors for the most important semanal points --- mypy/semanal.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e9ba51e5e385..b07888d0b996 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -718,7 +718,7 @@ def refresh_top_level(self, file_node: MypyFile) -> None: self.recurse_into_functions = False self.add_implicit_module_attrs(file_node) for d in file_node.defs: - self.accept(d) + self.accept_delaying_errors(d) if file_node.fullname == "typing": self.add_builtin_aliases(file_node) if file_node.fullname == "typing_extensions": @@ -5385,7 +5385,7 @@ def visit_block(self, b: Block) -> None: return self.block_depth[-1] += 1 for s in b.body: - self.accept(s) + self.accept_delaying_errors(s) self.block_depth[-1] -= 1 def visit_block_maybe(self, b: Block | None) -> None: @@ -7579,26 +7579,27 @@ def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool: return False return True - def accept(self, node: Node) -> None: + def accept_delaying_errors(self, node: Node) -> None: should_filter = isinstance(node, Statement) and not self.options.semantic_analysis_only if should_filter: filter_errors: bool | Callable[[str, ErrorInfo], bool] = lambda _, e: not e.blocker else: filter_errors = False with self.msg.filter_errors(filter_errors=filter_errors, save_filtered_errors=True) as msg: - try: - node.accept(self) - except Exception as err: - report_internal_error(err, self.errors.file, node.line, self.errors, self.options) + self.accept(node) errors = msg.filtered_errors() if errors: - # since nodes aren't hashable, carry things through values + # since nodes don't implement hash(), carry things through values assign_to = (self.cur_mod_id, node.line, node.column) self.delayed_errors.setdefault(assign_to, []) self.delayed_errors[assign_to].extend(errors) - # print(node, [e.message for e in self.delayed_errors[node]]) + def accept(self, node: Node) -> None: + try: + node.accept(self) + except Exception as err: + report_internal_error(err, self.errors.file, node.line, self.errors, self.options) def expr_to_analyzed_type( self, From 806266c53875d8891cfeff412e75dafbd33faa30 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 17 Dec 2025 16:54:00 +0900 Subject: [PATCH 3/3] Opt out of unnecessary checks in checker.py Note that there are a few places where unreachable code can happen where this still doesn't help. I'm not sure what to do about them. --- mypy/checker.py | 21 +++++++-------------- mypy/checkexpr.py | 4 ++-- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f984d00d7b25..9be64fcb54a9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -529,7 +529,7 @@ def check_first_pass(self) -> None: self.msg.unreachable_statement(d) break else: - self.accept(d) + self.accept_with_delayed_errors(d) assert not self.current_node_deferred @@ -637,14 +637,17 @@ def handle_cannot_determine_type(self, name: str, context: Context) -> None: else: self.msg.cannot_determine_type(name, context) - def accept(self, stmt: Statement) -> None: - """Type check a node in the given type context.""" + def accept_with_delayed_errors(self, stmt: Statement) -> None: curr_module = self.scope.stack[0] if isinstance(curr_module, MypyFile): key = (curr_module.fullname, stmt.line, stmt.column) if key in self.semanal_delayed_errors: self.msg.add_errors(self.semanal_delayed_errors[key]) + self.accept(stmt) + + def accept(self, stmt: Statement) -> None: + """Type check a node in the given type context.""" try: stmt.accept(self) except Exception as err: @@ -1235,16 +1238,6 @@ def check_func_item( """ self.dynamic_funcs.append(defn.is_dynamic() and not type_override) - # top-level function definitions are one of the main - # things errors can be associated with, and are sometimes masked - # e.g. by a decorator. it's better to make sure to flush any errors - # just in case. - curr_module = self.scope.stack[0] - if isinstance(curr_module, MypyFile): - key = (curr_module.fullname, defn.line, defn.column) - if key in self.semanal_delayed_errors: - self.msg.add_errors(self.semanal_delayed_errors[key]) - enclosing_node_deferred = self.current_node_deferred with self.enter_partial_types(is_function=True): typ = self.function_type(defn) @@ -3174,7 +3167,7 @@ def visit_block(self, b: Block) -> None: self.msg.unreachable_statement(s) break else: - self.accept(s) + self.accept_with_delayed_errors(s) # Clear expression cache after each statement to avoid unlimited growth. self.expr_checker.expr_cache.clear() diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9f4bd38d3677..75a66e9e7a49 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5495,8 +5495,8 @@ def visit_lambda_expr(self, e: LambdaExpr) -> Type: # Lambdas can have more than one element in body, # when we add "fictional" AssignmentStatement nodes, like in: # `lambda (a, b): a` - for stmt in e.body.body: - self.chk.accept(stmt) + self.chk.accept(e.body) + # Only type check the return expression, not the return statement. # There's no useful type context. ret_type = self.accept(e.expr(), allow_none_return=True)