Files
ruff/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md
David Peter 7a739d6b76 [ty] Custom concise diagnostic messages (#21498)
## Summary

This PR proposes that we add a new `set_concise_message` functionality
to our `Diagnostic` construction API. When used, the concise message
that is otherwise auto-generated from the main diagnostic message and
the primary annotation will be overwritten with the custom message.

To understand why this is desirable, let's look at the `invalid-key`
diagnostic. This is how I *want* the full diagnostic to look like:

<img width="620" height="282" alt="image"
src="https://github.com/user-attachments/assets/3bf70f52-9d9f-4817-bc16-fb0ebf7c2113"
/>

However, without the change in this PR, the concise message would have
the following form:

```
error[invalid-key]: Unknown key "Age" for TypedDict `Person`: Unknown key "Age" - did you mean "age"?
```

This duplication is why the full `invalid-key` diagnostic used a main
diagnostic message that is only "Invalid key for TypedDict `Person`", to
make that bearable:

```
error[invalid-key] Invalid key for TypedDict `Person`: Unknown key "Age" - did you mean "age"?
```

This is still less than ideal, *and* we had to make the "full"
diagnostic worse. With the new API here, we have to make no such
compromises. We need to do slightly more work (provide one additional
custom-designed message), but we get to keep the "full" diagnostic that
we actually want, and we can make the concise message more terse and
readable:

```
error[invalid-key] Unknown key "Age" for TypedDict `Person` - did you mean "age"?
```

Similar problems exist for other diagnostics as well (I really want this
for https://github.com/astral-sh/ruff/pull/21476). In this PR, I only
changed `invalid-key` and `type-assertion-failure`.

The PR here is somewhat related to the discussion in
https://github.com/astral-sh/ty/issues/1418, but note that we are
solving a problem that is unrelated to sub-diagnostics.

## Test Plan

Updated tests
2025-11-18 09:35:40 +01:00

3.2 KiB

assert_never

Basic functionality

assert_never makes sure that the type of the argument is Never.

Correct usage

from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown

def _(never: Never):
    assert_never(never)  # fine

Diagnostics

If it is not, a type-assertion-failure diagnostic is emitted.

from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown

def _():
    assert_never(0)  # error: [type-assertion-failure]

def _():
    assert_never("")  # error: [type-assertion-failure]

def _():
    assert_never(None)  # error: [type-assertion-failure]

def _():
    assert_never(())  # error: [type-assertion-failure]

def _(flag: bool, never: Never):
    assert_never(1 if flag else never)  # error: [type-assertion-failure]

def _(any_: Any):
    assert_never(any_)  # error: [type-assertion-failure]

def _(unknown: Unknown):
    assert_never(unknown)  # error: [type-assertion-failure]

Use case: Type narrowing and exhaustiveness checking

[environment]
python-version = "3.10"

assert_never can be used in combination with type narrowing as a way to make sure that all cases are handled in a series of isinstance checks or other narrowing patterns that are supported.

from typing_extensions import assert_never, Literal

class A: ...
class B: ...
class C: ...

def if_else_isinstance_success(obj: A | B):
    if isinstance(obj, A):
        pass
    elif isinstance(obj, B):
        pass
    elif isinstance(obj, C):
        pass
    else:
        assert_never(obj)

def if_else_isinstance_error(obj: A | B):
    if isinstance(obj, A):
        pass
    # B is missing
    elif isinstance(obj, C):
        pass
    else:
        # error: [type-assertion-failure] "Type `B & ~A & ~C` is not equivalent to `Never`"
        assert_never(obj)

def if_else_singletons_success(obj: Literal[1, "a"] | None):
    if obj == 1:
        pass
    elif obj == "a":
        pass
    elif obj is None:
        pass
    else:
        assert_never(obj)

def if_else_singletons_error(obj: Literal[1, "a"] | None):
    if obj == 1:
        pass
    elif obj is "A":  # "A" instead of "a"
        pass
    elif obj is None:
        pass
    else:
        # error: [type-assertion-failure] "Type `Literal["a"]` is not equivalent to `Never`"
        assert_never(obj)

def match_singletons_success(obj: Literal[1, "a"] | None):
    match obj:
        case 1:
            pass
        case "a":
            pass
        case None:
            pass
        case _ as obj:
            assert_never(obj)

def match_singletons_error(obj: Literal[1, "a"] | None):
    match obj:
        case 1:
            pass
        case "A":  # "A" instead of "a"
            pass
        case None:
            pass
        case _ as obj:
            # TODO: We should emit an error here, but the message should
            # show the type `Literal["a"]` instead of `@Todo(…)`. We only
            # assert on the first part of the message because the `@Todo`
            # message is not available in release mode builds.
            # error: [type-assertion-failure] "Type `@Todo"
            assert_never(obj)