[ty] Preserve final attribute restrictions on composite types (#29135)
ty can allow assignment to or deletion of a `Final` instance attribute
when the receiver is a union or has been narrowed through a protocol.
Another member of the composite type can make the attribute appear
writable.
In this contrived example, ty previously accepted the assignment because
`Mutable.value` is writable. With this change, ty reports
`invalid-assignment` because the receiver could be a `Fixed` instance:
```python
from typing import Final
class Fixed:
def __init__(self) -> None:
self.value: Final[int] = 1
class Mutable:
value: int = 1
def update(either: Fixed | Mutable) -> None:
# previously this was fine, but with this PR:
either.value = 2 # error: [invalid-assignment]
```
Check the members of unions and positive intersection components for
`Final` restrictions. Assignment checks also follow type aliases, so an
aliased union member retains the restriction.
A class can still assign to its own `Final` attribute through `self` in
`__init__`, even when `self` is narrowed through a protocol. If several
union members prohibit the same assignment, ty reports one error and
stops checking rather than producing duplicate errors.
Adapted from @tamird’s [original
change](https://github.com/tamird/ruff/commit/cf8a56c1a9ae9e6f71863546fb638f01ad4cb6b2).
---------
Co-authored-by: Tamir Duberstein <tamird@gmail.com>