ruff
54456cc6 - [ty] Emit more specific diagnostics for "possibly unbound" errors from context manager dunder methods invoked on a union. (#24662)

Commit
151 days ago
[ty] Emit more specific diagnostics for "possibly unbound" errors from context manager dunder methods invoked on a union. (#24662) <!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? - Does this PR follow our AI policy (https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)? --> ## Summary As part of https://github.com/astral-sh/ty/issues/940, this helps us emit more specific diagnostics for possibly unbound context manager dunders (e.g., `__enter__`, `__exit__`) invoked on a union type. Where previously the following snippet would produce just the top-level diagnostic commented below: ```py class Context: def __enter__(self): ... def __exit__(self, *args): ... class NotContext: pass def _(x: Context | NotContext): # error: [invalid-context-manager] "Object of type `Context | NotContext` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound" with x: pass ``` We will now produce two further "info" sub-diagnostics: ``` info: `NotContext` does not implement `__enter__` info: `NotContext` does not implement `__exit__` ``` ## Approach - This implements the approach suggested by `@carljm` [here](https://github.com/astral-sh/ruff/pull/20199#pullrequestreview-3178490835) from a previous attempt to address https://github.com/astral-sh/ty/issues/940; it extends `CallDunderError::PossiblyUnbound` with a new `unbound_on` field that stores a list of the union members on which a particular dunder is unbound. We create the new, richer error with a new `UnionType.try_call_dunder_with_policy` method that looks up the dunder on each member of the union, and then aggregates the results. This is supersedes the previous `UnionType.map_with_boundness_and_qualifiers` approach, and allows us to preserve the per-member binding information that we use to produce the more detailed diagnostic. - There are two alternatives to this approach that I considered but rejected: - Rebuild the specific union member diagnositic information at each callsite, and only when relevant. This was the approach originally taken by https://github.com/astral-sh/ruff/pull/20199, but I think it will lead to some unnecessary code duplication across callsites (of which there are at least three more). - Refactor such that `UnionType.map_with_boundness_and_qualifiers` such that it no longer loses member-specific binding information when producing its result. This would have required an extension to `PlaceAndQualifiers`, which would have a large blast radius and also introduce overhead in several cases where member-specific information for unions is not necessary. - There are more implicit dunder calls that can benefit from the new shape of `CallDunderError::PossiblyUnbound`, but I have intentionally deferred those to [a follow-up](https://github.com/astral-sh/ruff/pull/24676) in order to first collect feedback on a more targeted changeset. - The first three commits in this PR (926bcec8d80862bb63a4ffb991f0e92853686656, 65dc3fbd5ed7920b03df040c54b796007b90fdb6, 988e81d027e8e629bad1fce9ee6e05b8ebbc3af4) are "prefactors" that do not change any observable behaviour. The fourth (2422844d9a281240aad89148e9052e5e1208fbdf) actually implements the improvement, and deserves the most scrutiny. ## Test Plan Please see updated mdtests and associated snapshots.
Author
Parents
Loading