From 64ba2a4b727964921de7e5de2e4c0d33e0f97032 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 16 Dec 2025 17:40:57 +0900 Subject: [PATCH 1/9] 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/9] 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/9] 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) From 1dd8690c5ff059f87691f6f7486cc1dad67c1021 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 19 Dec 2025 11:26:31 +0900 Subject: [PATCH 4/9] Make semanal's side faster --- mypy/semanal.py | 103 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 17 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index b07888d0b996..d609bfafd058 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -547,6 +547,7 @@ def __init__( self.transitive_submodule_imports: dict[str, set[str]] = {} self.delayed_errors: dict[tuple[str, int, int], list[ErrorInfo]] = {} + self.associated_node: Statement | None = None # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @@ -7554,7 +7555,7 @@ def fail( if code is None: code = msg.code msg = msg.value - self.errors.report( + err_info = self.create_error_info( ctx.line, ctx.column, msg, @@ -7563,11 +7564,86 @@ def fail( end_line=ctx.end_line, end_column=ctx.end_column, ) + if self.errors._filter_error(self.errors.file, err_info): + return + + if self.associated_node is None or self.options.semantic_analysis_only: + self.errors.add_error_info(err_info) + else: + node = self.associated_node + assign_to = (self.cur_mod_id, node.line, node.column) + self.delayed_errors.setdefault(assign_to, []) + self.delayed_errors[assign_to].append(err_info) def note(self, msg: str, ctx: Context, code: ErrorCode | None = None) -> None: if not self.in_checked_function(): return - self.errors.report(ctx.line, ctx.column, msg, severity="note", code=code) + err_info = self.create_error_info(ctx.line, ctx.column, msg, severity="note", code=code) + if self.errors._filter_error(self.errors.file, err_info): + return + + if self.associated_node is None or self.options.semantic_analysis_only: + self.errors.add_error_info(err_info) + else: + node = self.associated_node + assign_to = (self.cur_mod_id, node.line, node.column) + self.delayed_errors.setdefault(assign_to, []) + self.delayed_errors[assign_to].append(err_info) + + def create_error_info( + self, + line: int, + column: int | None, + message: str, + code: ErrorCode | None = None, + *, + blocker: bool = False, + severity: str = "error", + end_line: int | None = None, + end_column: int | None = None, + ) -> ErrorInfo: + # TODO: move this into `errors.py`, probably + if self.errors.scope: + type = self.errors.scope.current_type_name() + if self.errors.scope.ignored > 0: + type = None # Omit type context if nested function + function = self.errors.scope.current_function_name() + else: + type = None + function = None + + if column is None: + column = -1 + if end_column is None: + if column == -1: + end_column = -1 + else: + end_column = column + 1 + + if end_line is None: + end_line = line + + code = code or (codes.MISC if not blocker else None) + + return ErrorInfo( + import_ctx=self.errors.import_context(), + file=self.errors.file, + module=self.errors.current_module(), + typ=type, + function_or_member=function, + line=line, + column=column, + end_line=end_line, + end_column=end_column, + severity=severity, + message=message, + code=code, + blocker=blocker, + only_once=False, + origin=(self.errors.file, [line]), + target=self.errors.current_target(), + parent_error=None, + ) def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool: if feature not in self.options.enable_incomplete_feature: @@ -7579,21 +7655,14 @@ def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool: return False return True - 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: - self.accept(node) - - errors = msg.filtered_errors() - if errors: - # 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) + def accept_delaying_errors(self, node: Statement) -> None: + previously_associated = self.associated_node + self.associated_node = node + try: + node.accept(self) + except Exception as err: + report_internal_error(err, self.errors.file, node.line, self.errors, self.options) + self.associated_node = previously_associated def accept(self, node: Node) -> None: try: From ec6b98d43e914d327f7b81bff24d6f1eca2a6cdc Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 19 Dec 2025 12:18:51 +0900 Subject: [PATCH 5/9] Try to fix spurious unreachability warnings --- mypy/checkexpr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 75a66e9e7a49..989347503366 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5495,7 +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` - self.chk.accept(e.body) + with self.chk.binder.frame_context(can_skip=True, fall_through=0): + self.chk.accept(e.body) # Only type check the return expression, not the return statement. # There's no useful type context. From 8267da1b078c122a20287a13a9ce586cf52355f3 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 19 Dec 2025 12:22:20 +0900 Subject: [PATCH 6/9] Fix test failures --- mypy/semanal.py | 5 ++++- test-data/unit/check-incomplete-fixture.test | 16 ++++++++-------- test-data/unit/check-statements.test | 2 +- test-data/unit/check-typevar-defaults.test | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d609bfafd058..7e960e2c9f0e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7567,7 +7567,7 @@ def fail( if self.errors._filter_error(self.errors.file, err_info): return - if self.associated_node is None or self.options.semantic_analysis_only: + if blocker or self.associated_node is None or self.options.semantic_analysis_only: self.errors.add_error_info(err_info) else: node = self.associated_node @@ -8155,6 +8155,8 @@ def isolated_error_analysis(self) -> Iterator[None]: original_deferral_debug_context_len = len(self.deferral_debug_context) self.errors = Errors(Options()) + previous_association = self.associated_node + self.associated_node = None try: yield finally: @@ -8163,6 +8165,7 @@ def isolated_error_analysis(self) -> Iterator[None]: self.num_incomplete_refs = original_num_incomplete_refs self.progress = original_progress self.deferred = original_deferred + self.associated_node = previous_association del self.deferral_debug_context[original_deferral_debug_context_len:] diff --git a/test-data/unit/check-incomplete-fixture.test b/test-data/unit/check-incomplete-fixture.test index 146494df1bd6..0bf79ee7ae60 100644 --- a/test-data/unit/check-incomplete-fixture.test +++ b/test-data/unit/check-incomplete-fixture.test @@ -16,38 +16,38 @@ m.x # E: "object" has no attribute "x" from typing import Set def f(x: Set[int]) -> None: pass [out] -main:1: error: Module "typing" has no attribute "Set" main:1: note: Maybe your test fixture does not define "builtins.set"? main:1: note: Consider adding [builtins fixtures/set.pyi] to your test description +main:1: error: Module "typing" has no attribute "Set" [case testBaseExceptionMissingFromStubs] e: BaseException [out] -main:1: error: Name "BaseException" is not defined main:1: note: Maybe your test fixture does not define "builtins.BaseException"? main:1: note: Consider adding [builtins fixtures/exception.pyi] to your test description +main:1: error: Name "BaseException" is not defined [case testExceptionMissingFromStubs] e: Exception [out] -main:1: error: Name "Exception" is not defined main:1: note: Maybe your test fixture does not define "builtins.Exception"? main:1: note: Consider adding [builtins fixtures/exception.pyi] to your test description +main:1: error: Name "Exception" is not defined [case testIsinstanceMissingFromStubs] if isinstance(1, int): pass [out] -main:1: error: Name "isinstance" is not defined main:1: note: Maybe your test fixture does not define "builtins.isinstance"? main:1: note: Consider adding [builtins fixtures/isinstancelist.pyi] to your test description +main:1: error: Name "isinstance" is not defined [case testTupleMissingFromStubs1] tuple() [out] -main:1: error: Name "tuple" is not defined main:1: note: Maybe your test fixture does not define "builtins.tuple"? main:1: note: Consider adding [builtins fixtures/tuple.pyi] to your test description +main:1: error: Name "tuple" is not defined main:1: note: Did you forget to import it from "typing"? (Suggestion: "from typing import Tuple") [case testTupleMissingFromStubs2] @@ -55,9 +55,9 @@ tuple() from typing import Tuple x: Tuple[int, str] [out] -main:1: error: Name "tuple" is not defined main:1: note: Maybe your test fixture does not define "builtins.tuple"? main:1: note: Consider adding [builtins fixtures/tuple.pyi] to your test description +main:1: error: Name "tuple" is not defined main:1: note: Did you forget to import it from "typing"? (Suggestion: "from typing import Tuple") main:3: error: Name "tuple" is not defined @@ -66,15 +66,15 @@ class A: @classmethod def f(cls): pass [out] -main:2: error: Name "classmethod" is not defined main:2: note: Maybe your test fixture does not define "builtins.classmethod"? main:2: note: Consider adding [builtins fixtures/classmethod.pyi] to your test description +main:2: error: Name "classmethod" is not defined [case testPropertyMissingFromStubs] class A: @property def f(self): pass [out] -main:2: error: Name "property" is not defined main:2: note: Maybe your test fixture does not define "builtins.property"? main:2: note: Consider adding [builtins fixtures/property.pyi] to your test description +main:2: error: Name "property" is not defined diff --git a/test-data/unit/check-statements.test b/test-data/unit/check-statements.test index 880df82671d4..c149cf67bd94 100644 --- a/test-data/unit/check-statements.test +++ b/test-data/unit/check-statements.test @@ -2359,9 +2359,9 @@ from typing import _FutureFeatureFixture # that day comes this suggestion will also be less helpful than it is today. import typing_extensions [out] -main:1: error: Module "typing" has no attribute "_FutureFeatureFixture" main:1: note: Use `from typing_extensions import _FutureFeatureFixture` instead main:1: note: See https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-new-additions-to-the-typing-module +main:1: error: Module "typing" has no attribute "_FutureFeatureFixture" [builtins fixtures/tuple.pyi] [case testNoCrashOnBreakOutsideLoopFunction] diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 2f9506b71bfb..401d202c00e2 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -930,8 +930,8 @@ reveal_type(C) # N: Revealed type is "def [T2 = Any] () -> __main__.C[T2`1 = An c: C reveal_type(c) # N: Revealed type is "__main__.C[Any]" -class D(Generic[T2, T1]): ... # E: Type variable T1 referenced in the default of T2 is unbound \ - # E: "T1" cannot appear after "T2" in type parameter list because it has no default type +class D(Generic[T2, T1]): ... # E: "T1" cannot appear after "T2" in type parameter list because it has no default type \ + # E: Type variable T1 referenced in the default of T2 is unbound reveal_type(D) # N: Revealed type is "def [T2 = Any, T1 = Any] () -> __main__.D[T2`1 = Any, T1`2 = Any]" d: D reveal_type(d) # N: Revealed type is "__main__.D[Any, Any]" From 347039d8498a4b77e974e8a09fdc14dec2b46d8a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 20 Dec 2025 07:11:25 +0900 Subject: [PATCH 7/9] Add test cases --- test-data/unit/check-unreachable-code.test | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 98c676dbf42b..66793bf043f2 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1650,3 +1650,16 @@ def x() -> None: main:4: error: Statement is unreachable if 5: ^~~~~ + +[case testUnreachableSemanalErrors] +# flags: --warn-unreachable +def f() -> None: + if True: + return + + x = xyz # E: Statement is unreachable + +if True: + assert False + +x = xyz # E: Statement is unreachable From 6eba664fc9ef72cde66a8394b051288cbbe7fe68 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 20 Dec 2025 07:16:39 +0900 Subject: [PATCH 8/9] Start removing special cased `assert False` --- mypy/checker.py | 7 ++++++- mypy/semanal_pass1.py | 3 +++ test-data/unit/check-unreachable-code.test | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9be64fcb54a9..d93429d6b7c2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -150,6 +150,7 @@ from mypy.patterns import AsPattern, StarredPattern from mypy.plugin import Plugin from mypy.plugins import dataclasses as dataclasses_plugin +from mypy.reachability import assert_will_always_fail from mypy.scope import Scope from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS @@ -5136,7 +5137,11 @@ def visit_assert_stmt(self, s: AssertStmt) -> None: self.expr_checker.analyze_cond_branch( else_map, s.msg, None, suppress_unreachable_errors=False ) - self.push_type_map(true_map) + if assert_will_always_fail(s, self.options): + # TODO: move this logic into `find_isinstance_check` + self.push_type_map(None) + else: + self.push_type_map(true_map) def visit_raise_stmt(self, s: RaiseStmt) -> None: """Type check a raise statement.""" diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index 266fd236a01f..23dd47501d69 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -66,6 +66,9 @@ def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) - for i, defn in enumerate(file.defs): defn.accept(self) if isinstance(defn, AssertStmt) and assert_will_always_fail(defn, options): + # TODO: remove this by passing through unreachability info to semanal + # for top-level definitions. (testUnreachableAfterToplevelAssertImport) + # We've encountered an assert that's always false, # e.g. assert sys.platform == 'lol'. Truncate the # list of statements. This mutates file.defs too. diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 66793bf043f2..b7e2b7660765 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -795,11 +795,12 @@ main:3: error: Cannot find implementation or library stub for module named "bad" main:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports [case testUnreachableAfterToplevelAssertNotInsideIf] +# flags: --warn-unreachable --platform unknown import sys if sys.version_info[0] >= 2: assert sys.platform == 'lol' - reveal_type('') # N: Revealed type is "Literal['']?" -reveal_type('') # N: Revealed type is "Literal['']?" + reveal_type('') # E: Statement is unreachable +reveal_type('') # E: Statement is unreachable [builtins fixtures/ops.pyi] [case testUnreachableFlagWithBadControlFlow1] From 556d37bc522dabcbe4cfcf8c925802e071490af9 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 20 Dec 2025 07:59:01 +0900 Subject: [PATCH 9/9] Revert --- mypy/checker.py | 7 +------ mypy/semanal_pass1.py | 3 --- test-data/unit/check-unreachable-code.test | 5 ++--- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d93429d6b7c2..9be64fcb54a9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -150,7 +150,6 @@ from mypy.patterns import AsPattern, StarredPattern from mypy.plugin import Plugin from mypy.plugins import dataclasses as dataclasses_plugin -from mypy.reachability import assert_will_always_fail from mypy.scope import Scope from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS @@ -5137,11 +5136,7 @@ def visit_assert_stmt(self, s: AssertStmt) -> None: self.expr_checker.analyze_cond_branch( else_map, s.msg, None, suppress_unreachable_errors=False ) - if assert_will_always_fail(s, self.options): - # TODO: move this logic into `find_isinstance_check` - self.push_type_map(None) - else: - self.push_type_map(true_map) + self.push_type_map(true_map) def visit_raise_stmt(self, s: RaiseStmt) -> None: """Type check a raise statement.""" diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index 23dd47501d69..266fd236a01f 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -66,9 +66,6 @@ def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) - for i, defn in enumerate(file.defs): defn.accept(self) if isinstance(defn, AssertStmt) and assert_will_always_fail(defn, options): - # TODO: remove this by passing through unreachability info to semanal - # for top-level definitions. (testUnreachableAfterToplevelAssertImport) - # We've encountered an assert that's always false, # e.g. assert sys.platform == 'lol'. Truncate the # list of statements. This mutates file.defs too. diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index b7e2b7660765..66793bf043f2 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -795,12 +795,11 @@ main:3: error: Cannot find implementation or library stub for module named "bad" main:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports [case testUnreachableAfterToplevelAssertNotInsideIf] -# flags: --warn-unreachable --platform unknown import sys if sys.version_info[0] >= 2: assert sys.platform == 'lol' - reveal_type('') # E: Statement is unreachable -reveal_type('') # E: Statement is unreachable + reveal_type('') # N: Revealed type is "Literal['']?" +reveal_type('') # N: Revealed type is "Literal['']?" [builtins fixtures/ops.pyi] [case testUnreachableFlagWithBadControlFlow1]