ruff/crates/ty_python_semantic/resources/mdtest
David Peter 2a00eca66b
[ty] Exhaustiveness checking & reachability for match statements (#19508)
## Summary

Implements proper reachability analysis and — in effect — exhaustiveness
checking for `match` statements. This allows us to check the following
code without any errors (leads to *"can implicitly return `None`"* on
`main`):

```py
from enum import Enum, auto

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

def hex(color: Color) -> str:
    match color:
        case Color.RED:
            return "#ff0000"
        case Color.GREEN:
            return "#00ff00"
        case Color.BLUE:
            return "#0000ff"
```

Note that code like this already worked fine if there was a
`assert_never(color)` statement in a catch-all case, because we would
then consider that `assert_never` call terminal. But now this also works
without the wildcard case. Adding a member to the enum would still lead
to an error here, if that case would not be handled in `hex`.

What needed to happen to support this is a new way of evaluating match
pattern constraints. Previously, we would simply compare the type of the
subject expression against the patterns. For the last case here, the
subject type would still be `Color` and the value type would be
`Literal[Color.BLUE]`, so we would infer an ambiguous truthiness.

Now, before we compare the subject type against the pattern, we first
generate a union type that corresponds to the set of all values that
would have *definitely been matched* by previous patterns. Then, we
build a "narrowed" subject type by computing `subject_type &
~already_matched_type`, and compare *that* against the pattern type. For
the example here, `already_matched_type = Literal[Color.RED] |
Literal[Color.GREEN]`, and so we have a narrowed subject type of `Color
& ~(Literal[Color.RED] | Literal[Color.GREEN]) = Literal[Color.BLUE]`,
which allows us to infer a reachability of `AlwaysTrue`.

<details>

<summary>A note on negated reachability constraints</summary>

It might seem that we now perform duplicate work, because we also record
*negated* reachability constraints. But that is still important for
cases like the following (and possibly also for more realistic
scenarios):

```py
from typing import Literal

def _(x: int | str):
    match x:
        case None:
            pass # never reachable
        case _:
            y = 1

    y
```

</details>

closes https://github.com/astral-sh/ty/issues/99

## Test Plan

* I verified that this solves all examples from the linked ticket (the
first example needs a PEP 695 type alias, because we don't support
legacy type aliases yet)
* Verified that the ecosystem changes are all because of removed false
positives
* Updated tests
2025-07-23 22:45:45 +02:00
..
annotations [ty] Consistent use of American english (in rules) (#19488) 2025-07-22 16:10:38 +02:00
assignment [ty] Homogeneous and mixed tuples (#18600) 2025-06-20 18:23:54 -04:00
binary [ty] Homogeneous and mixed tuples (#18600) 2025-06-20 18:23:54 -04:00
boolean Revert "[ty] Better control flow for boolean expressions that are inside if (#18010)" (#18150) 2025-05-17 08:27:32 -04:00
boundness_declaredness
call [ty] Reachability analysis for isinstance(…) branches (#19503) 2025-07-23 13:06:30 +02:00
class
comparison [ty] Enum literal types (#19328) 2025-07-15 21:31:53 +02:00
comprehensions
conditional [ty] Exhaustiveness checking & reachability for match statements (#19508) 2025-07-23 22:45:45 +02:00
dataclasses [ty] Pass down specialization to generic dataclass bases (#19472) 2025-07-21 20:51:58 +02:00
declaration [ty] Format conflicting types as an enumeration (#18956) 2025-06-26 14:29:33 +02:00
diagnostics lint on the global keyword if there's no explicit definition in the global scope 2025-07-15 16:56:54 -07:00
directives [ty] Exhaustiveness checking & reachability for match statements (#19508) 2025-07-23 22:45:45 +02:00
doc
exception [ty] support del statement and deletion of except handler names (#18593) 2025-06-12 07:44:42 -07:00
expression [ty] Make tuple subclass constructors sound (#19469) 2025-07-21 21:25:11 +00:00
function [ty] Support empty function bodies in if TYPE_CHECKING blocks (#19372) 2025-07-16 14:48:04 -06:00
generics [ty] Improve protocol member type checking and relation handling (#18847) 2025-06-29 10:46:33 +00:00
ide_support [ty] Enum literal types (#19328) 2025-07-15 21:31:53 +02:00
import lint on the global keyword if there's no explicit definition in the global scope 2025-07-15 16:56:54 -07:00
literal
loops
narrow [ty] Splat variadic arguments into parameter list (#18996) 2025-07-22 14:33:08 -04:00
regression
scopes [ty] perform type narrowing for places marked global too (#19381) 2025-07-22 16:42:10 -07:00
shadowing
snapshots [ty] highlight the argument in static_assert error messages (#19426) 2025-07-23 08:24:12 -07:00
stubs [ty] Do not carry the generic context of Protocol or Generic in the ClassBase enum (#17989) 2025-05-22 21:37:03 -04:00
subscript [ty] Fix false positives when subscripting an object inferred as having an Intersection type (#18920) 2025-06-24 18:39:02 +00:00
suppressions [ty] Consistent use of American english (in rules) (#19488) 2025-07-22 16:10:38 +02:00
type_compendium [ty] Make tuple subclass constructors sound (#19469) 2025-07-21 21:25:11 +00:00
type_of [ty] Enum literal types (#19328) 2025-07-15 21:31:53 +02:00
type_properties [ty] Infer single-valuedness for enums based on int/str (#19510) 2025-07-23 15:55:42 +02:00
type_qualifiers [ty] Disallow illegal uses of ClassVar (#19483) 2025-07-22 14:21:29 +02:00
unary
with [ty] Add hint if async context manager is used in non-async with statement (#18299) 2025-05-26 21:34:47 +02:00
.mdformat.toml
attributes.md [ty] Expansion of enums into unions of literals (#19382) 2025-07-21 19:37:55 +02:00
cycle.md [ty] Add cycle handling for unpacking targets (#18078) 2025-05-13 21:27:48 +00:00
decorators.md
del.md [ty] make del x force local resolution of x in the current scope (#19389) 2025-07-18 14:58:32 -07:00
deprecated.md [ty] Consistent use of American english (in rules) (#19488) 2025-07-22 16:10:38 +02:00
descriptor_protocol.md [ty] Fix descriptor lookups for most types that overlap with None (#19120) 2025-07-05 19:34:23 +01:00
enums.md [ty] Exhaustiveness checking & reachability for match statements (#19508) 2025-07-23 22:45:45 +02:00
exhaustiveness_checking.md [ty] Exhaustiveness checking & reachability for match statements (#19508) 2025-07-23 22:45:45 +02:00
final.md
instance_layout_conflict.md [ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864) 2025-06-24 20:27:37 +00:00
intersection_types.md [ty] Expansion of enums into unions of literals (#19382) 2025-07-21 19:37:55 +02:00
invalid_syntax.md
known_constants.md
mdtest_config.md
mdtest_custom_typeshed.md
metaclass.md
mro.md [ty] Implement implicit inheritance from Generic[] for PEP-695 generic classes (#18283) 2025-05-26 20:40:16 +01:00
named_tuple.md [ty] Add generic inference for dataclasses (#18443) 2025-06-03 09:59:43 -07:00
overloads.md [ty] Add support for @staticmethods (#18809) 2025-06-20 10:38:17 +02:00
pep695_type_aliases.md [ty] Support typing.TypeAliasType (#18156) 2025-05-19 16:36:49 +02:00
properties.md
protocols.md [ty] Consistent use of American english (in rules) (#19488) 2025-07-22 16:10:38 +02:00
public_types.md [ty] Infer nonlocal types as unions of all reachable bindings (#18750) 2025-06-26 12:24:40 +02:00
statically_known_branches.md [ty] Infer nonlocal types as unions of all reachable bindings (#18750) 2025-06-26 12:24:40 +02:00
sys_platform.md
sys_version_info.md
terminal_statements.md [ty] Correctly handle calls to functions marked as returning Never / NoReturn (#18333) 2025-07-04 11:52:52 -07:00
type_api.md [ty] highlight the argument in static_assert error messages (#19426) 2025-07-23 08:24:12 -07:00
typed_dict.md [ty] Reduce false positives for TypedDict types (#19354) 2025-07-15 12:47:19 +01:00
union_types.md [ty] Expansion of enums into unions of literals (#19382) 2025-07-21 19:37:55 +02:00
unpacking.md [ty] Support variable-length tuples in unpacking assignments (#18948) 2025-06-27 15:29:04 -04:00
unreachable.md [ty] Infer nonlocal types as unions of all reachable bindings (#18750) 2025-06-26 12:24:40 +02:00