[ty] Retain recursively-defined state in binary expressions by charliermarsh · Pull Request #25277 · astral-sh/ruff (original) (raw)

@charliermarsh

@charliermarsh added the bug

Something isn't working

label

May 20, 2026

thejchap pushed a commit to thejchap/ruff that referenced this pull request

May 23, 2026

@charliermarsh @thejchap

…h#25277)

Summary

Prior to this change, a recursive implicit attribute like:

from __future__ import annotations
from typing import Final

class ScopeChain:
    def __init__(self, parent: ScopeChain | None = None) -> None:
        self.depth: Final = 1 if parent is None else parent.depth + 1

...could fail to converge during type inference. We now preserve the recursive-definition marker when literal binary operations produce a new literal result, so recursive literal unions continue through the existing widening path instead of accumulating results indefinitely.

Note: in the above snippet, the bare Final exposes the issue because we infer the RHS to get the type, as opposed to relying on the annotation, but in theory it needn't be specific to Final... Codex believes this is the only way to trigger that panic as of now, though because it's the only site where we create a recursive attribute lookup and retain the exact RHS literal type (e.g., the non-Final version, self.depth = 1 if parent is None else parent.depth + 1, promotes those literals).

Closes astral-sh/ty#3499.

anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request

May 28, 2026

@charliermarsh @anishgirianish

…h#25277)

Summary

Prior to this change, a recursive implicit attribute like:

from __future__ import annotations
from typing import Final

class ScopeChain:
    def __init__(self, parent: ScopeChain | None = None) -> None:
        self.depth: Final = 1 if parent is None else parent.depth + 1

...could fail to converge during type inference. We now preserve the recursive-definition marker when literal binary operations produce a new literal result, so recursive literal unions continue through the existing widening path instead of accumulating results indefinitely.

Note: in the above snippet, the bare Final exposes the issue because we infer the RHS to get the type, as opposed to relying on the annotation, but in theory it needn't be specific to Final... Codex believes this is the only way to trigger that panic as of now, though because it's the only site where we create a recursive attribute lookup and retain the exact RHS literal type (e.g., the non-Final version, self.depth = 1 if parent is None else parent.depth + 1, promotes those literals).

Closes astral-sh/ty#3499.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters

[ Show hidden characters]({{ revealButtonHref }})