mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
a8d3850605
<!-- 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 This implements structural promotion of tuple size in the inferred type of a collection literal. The promotion only applies to a very specific circumstance: when tuple literals in an inferred collection element type produce a union of homogeneous fixed-length tuples of differing lengths, and only literal tuple sources have contributed to that type, then we widen that union to a single variadic tuple (e.g., `tuple[str] | tuple[str, str]` is widened to `tuple[str, ...]`). The result is that this scenario described in https://github.com/astral-sh/ty/issues/2620 succeeds: ```python languages = { "python": (".py", ".pyi"), "javascript": (".js", ".jsx", ".ts", ".tsx"), } # This no longer errors after this change, because the type of languages is `dict[str, tuple[str, ...]` rather than `dict[str, tuple[str, str]] | tuple[str, str, str, str]]` languages["ruby"] = (".rb",) ``` Closes https://github.com/astral-sh/ty/issues/2620. ### Approach - I created a new submodule that encapsulates the tuple size promotion policy. It exposes a `TupleSizePromotionConstraints` struct that we use during inference to record the scenarios in which we should **not** attempt to promote a tuple. If no such disqualifying scenarios are encountered, then tuple size promotion is attempted. The set of disqualifying scenarios is documented in new mdtests. - I think the policy for when to promote unions that involve empty tuples deserves particular scrutiny. Since empty tuples do not have an element type, they present a special case. The rule I've chosen is that empty tuples do not contribute to evidence of different tuple lengths. That means that a union containing an empty tuple must also contain other tuples of differing lengths to trigger promotion (i.e., `[(), (1,)]` remains `list[tuple[()] | tuple[int]]`, but `[(), (1,), (1,2)]` is promoted to `list[tuple[int, ...]]`. This is conservative and, I hope, useful for modeling situations in which the size of a tuple is specifically meant to be 0 or N. ## Test Plan Please see new and updated mdtests. <!-- How was it tested? -->