[ty] Enforce typing.Final (#19178)

## Summary

Emit a diagnostic when a `Final`-qualified symbol is modified. This
first iteration only works for name targets. Tests with TODO comments
were added for attribute assignments as well.

related ticket: https://github.com/astral-sh/ty/issues/158

## Ecosystem impact

Correctly identified [modification of a `Final`
symbol](7b4164a5f2/sphinx/__init__.py (L44))
(behind a `# type: ignore`):
```diff
- warning[unused-ignore-comment] sphinx/__init__.py:44:56: Unused blanket `type: ignore` directive
```
And the same
[here](5471a37e82/src/trio/_core/_run.py (L128)):
```diff
- warning[unused-ignore-comment] src/trio/_core/_run.py:128:45: Unused blanket `type: ignore` directive
```

## Test Plan

New Markdown tests
This commit is contained in:
David Peter 2025-07-08 16:26:09 +02:00 committed by GitHub
parent 6a42d28867
commit 149350bf39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 252 additions and 40 deletions

View file

@ -618,6 +618,15 @@ impl DefinitionKind<'_> {
}
}
pub(crate) fn is_import(&self) -> bool {
matches!(
self,
DefinitionKind::Import(_)
| DefinitionKind::ImportFrom(_)
| DefinitionKind::StarImport(_)
)
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the place being defined i.e.,

View file

@ -306,7 +306,10 @@ pub(crate) struct UseDefMap<'db> {
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
/// valid assignment to our own annotation.
bindings_by_declaration: FxHashMap<Definition<'db>, Bindings>,
///
/// If we see a binding to a `Final`-qualified symbol, we also need this map to find previous
/// bindings to that symbol. If there are any, the assignment is invalid.
bindings_by_definition: FxHashMap<Definition<'db>, Bindings>,
/// [`PlaceState`] visible at end of scope for each place.
end_of_scope_places: IndexVec<ScopedPlaceId, PlaceState>,
@ -448,12 +451,12 @@ impl<'db> UseDefMap<'db> {
}
}
pub(crate) fn bindings_at_declaration(
pub(crate) fn bindings_at_definition(
&self,
declaration: Definition<'db>,
definition: Definition<'db>,
) -> BindingWithConstraintsIterator<'_, 'db> {
self.bindings_iterator(
&self.bindings_by_declaration[&declaration],
&self.bindings_by_definition[&definition],
BoundnessAnalysis::BasedOnUnboundVisibility,
)
}
@ -744,8 +747,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Live declarations for each so-far-recorded binding.
declarations_by_binding: FxHashMap<Definition<'db>, Declarations>,
/// Live bindings for each so-far-recorded declaration.
bindings_by_declaration: FxHashMap<Definition<'db>, Bindings>,
/// Live bindings for each so-far-recorded definition.
bindings_by_definition: FxHashMap<Definition<'db>, Bindings>,
/// Currently live bindings and declarations for each place.
place_states: IndexVec<ScopedPlaceId, PlaceState>,
@ -772,7 +775,7 @@ impl<'db> UseDefMapBuilder<'db> {
reachability: ScopedReachabilityConstraintId::ALWAYS_TRUE,
node_reachability: FxHashMap::default(),
declarations_by_binding: FxHashMap::default(),
bindings_by_declaration: FxHashMap::default(),
bindings_by_definition: FxHashMap::default(),
place_states: IndexVec::new(),
reachable_definitions: IndexVec::new(),
eager_snapshots: EagerSnapshots::default(),
@ -808,6 +811,9 @@ impl<'db> UseDefMapBuilder<'db> {
binding: Definition<'db>,
is_place_name: bool,
) {
self.bindings_by_definition
.insert(binding, self.place_states[place].bindings().clone());
let def_id = self.all_definitions.push(DefinitionState::Defined(binding));
let place_state = &mut self.place_states[place];
self.declarations_by_binding
@ -942,7 +948,7 @@ impl<'db> UseDefMapBuilder<'db> {
.all_definitions
.push(DefinitionState::Defined(declaration));
let place_state = &mut self.place_states[place];
self.bindings_by_declaration
self.bindings_by_definition
.insert(declaration, place_state.bindings().clone());
place_state.record_declaration(def_id, self.reachability);
@ -1119,7 +1125,7 @@ impl<'db> UseDefMapBuilder<'db> {
self.bindings_by_use.shrink_to_fit();
self.node_reachability.shrink_to_fit();
self.declarations_by_binding.shrink_to_fit();
self.bindings_by_declaration.shrink_to_fit();
self.bindings_by_definition.shrink_to_fit();
self.eager_snapshots.shrink_to_fit();
UseDefMap {
@ -1132,7 +1138,7 @@ impl<'db> UseDefMapBuilder<'db> {
end_of_scope_places: self.place_states,
reachable_definitions: self.reachable_definitions,
declarations_by_binding: self.declarations_by_binding,
bindings_by_declaration: self.bindings_by_declaration,
bindings_by_definition: self.bindings_by_definition,
eager_snapshots: self.eager_snapshots,
end_of_scope_reachability: self.reachability,
}