@@ -229,11 +229,42 @@ def filtered_errors(self) -> list[ErrorInfo]:
229229 return self ._filtered
230230
231231
232+ class NonOverlapErrorInfo :
233+ line : int
234+ column : int
235+ end_line : int | None
236+ end_column : int | None
237+ kind : str
238+
239+ def __init__ (
240+ self , * , line : int , column : int , end_line : int | None , end_column : int | None , kind : str
241+ ) -> None :
242+ self .line = line
243+ self .column = column
244+ self .end_line = end_line
245+ self .end_column = end_column
246+ self .kind = kind
247+
248+ def __eq__ (self , other : object ) -> bool :
249+ if isinstance (other , NonOverlapErrorInfo ):
250+ return (
251+ self .line == other .line
252+ and self .column == other .column
253+ and self .end_line == other .end_line
254+ and self .end_column == other .end_column
255+ and self .kind == other .kind
256+ )
257+ return False
258+
259+ def __hash__ (self ) -> int :
260+ return hash ((self .line , self .column , self .end_line , self .end_column , self .kind ))
261+
262+
232263class IterationDependentErrors :
233264 """An `IterationDependentErrors` instance serves to collect the `unreachable`,
234- `redundant-expr`, and `redundant-casts` errors, as well as the revealed types,
235- handled by the individual `IterationErrorWatcher` instances sequentially applied to
236- the same code section."""
265+ `redundant-expr`, and `redundant-casts` errors, as well as the revealed types and
266+ non-overlapping types, handled by the individual `IterationErrorWatcher` instances
267+ sequentially applied to the same code section."""
237268
238269 # One set of `unreachable`, `redundant-expr`, and `redundant-casts` errors per
239270 # iteration step. Meaning of the tuple items: ErrorCode, message, line, column,
@@ -249,9 +280,13 @@ class IterationDependentErrors:
249280 # end_line, end_column:
250281 revealed_types : dict [tuple [int , int , int | None , int | None ], list [Type ]]
251282
283+ # One dictionary of non-overlapping types per iteration step:
284+ nonoverlapping_types : list [dict [NonOverlapErrorInfo , tuple [Type , Type ]]]
285+
252286 def __init__ (self ) -> None :
253287 self .uselessness_errors = []
254288 self .unreachable_lines = []
289+ self .nonoverlapping_types = []
255290 self .revealed_types = defaultdict (list )
256291
257292 def yield_uselessness_error_infos (self ) -> Iterator [tuple [str , Context , ErrorCode ]]:
@@ -271,6 +306,36 @@ def yield_uselessness_error_infos(self) -> Iterator[tuple[str, Context, ErrorCod
271306 context .end_column = error_info [5 ]
272307 yield error_info [1 ], context , error_info [0 ]
273308
309+ def yield_nonoverlapping_types (
310+ self ,
311+ ) -> Iterator [tuple [tuple [list [Type ], list [Type ]], str , Context ]]:
312+ """Report expressions where non-overlapping types were detected for all iterations
313+ were the expression was reachable."""
314+
315+ selected = set ()
316+ for candidate in set (chain .from_iterable (self .nonoverlapping_types )):
317+ if all (
318+ (candidate in nonoverlap ) or (candidate .line in lines )
319+ for nonoverlap , lines in zip (self .nonoverlapping_types , self .unreachable_lines )
320+ ):
321+ selected .add (candidate )
322+
323+ persistent_nonoverlaps : dict [NonOverlapErrorInfo , tuple [list [Type ], list [Type ]]] = (
324+ defaultdict (lambda : ([], []))
325+ )
326+ for nonoverlaps in self .nonoverlapping_types :
327+ for candidate , (left , right ) in nonoverlaps .items ():
328+ if candidate in selected :
329+ types = persistent_nonoverlaps [candidate ]
330+ types [0 ].append (left )
331+ types [1 ].append (right )
332+
333+ for error_info , types in persistent_nonoverlaps .items ():
334+ context = Context (line = error_info .line , column = error_info .column )
335+ context .end_line = error_info .end_line
336+ context .end_column = error_info .end_column
337+ yield (types [0 ], types [1 ]), error_info .kind , context
338+
274339 def yield_revealed_type_infos (self ) -> Iterator [tuple [list [Type ], Context ]]:
275340 """Yield all types revealed in at least one iteration step."""
276341
@@ -283,8 +348,9 @@ def yield_revealed_type_infos(self) -> Iterator[tuple[list[Type], Context]]:
283348
284349class IterationErrorWatcher (ErrorWatcher ):
285350 """Error watcher that filters and separately collects `unreachable` errors,
286- `redundant-expr` and `redundant-casts` errors, and revealed types when analysing
287- code sections iteratively to help avoid making too-hasty reports."""
351+ `redundant-expr` and `redundant-casts` errors, and revealed types and
352+ non-overlapping types when analysing code sections iteratively to help avoid
353+ making too-hasty reports."""
288354
289355 iteration_dependent_errors : IterationDependentErrors
290356
@@ -305,6 +371,7 @@ def __init__(
305371 )
306372 self .iteration_dependent_errors = iteration_dependent_errors
307373 iteration_dependent_errors .uselessness_errors .append (set ())
374+ iteration_dependent_errors .nonoverlapping_types .append ({})
308375 iteration_dependent_errors .unreachable_lines .append (set ())
309376
310377 def on_error (self , file : str , info : ErrorInfo ) -> bool :
0 commit comments