Skip to content

Commit 5b6e949

Browse files
authored
[red-knot] Silence unresolved-attribute in unreachable code (#17305)
## Summary Basically just repeat the same thing that we did for `unresolved-reference`, but now for attribute expressions. We now also handle the case where the unresolved attribute (or the unresolved reference) diagnostic originates from a stringified type annotation. And I made the evaluation of reachability constraints lazy (will only be evaluated right before we are about to emit a diagnostic). ## Test Plan New Markdown tests for stringified annotations.
1 parent ec74f2d commit 5b6e949

File tree

5 files changed

+189
-90
lines changed

5 files changed

+189
-90
lines changed

crates/red_knot_python_semantic/resources/mdtest/unreachable.md

+27-4
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ python-platform = "linux"
166166
import sys
167167

168168
if sys.platform == "win32":
169-
# TODO: we should not emit an error here
170-
# error: [unresolved-attribute]
171169
sys.getwindowsversion()
172170
```
173171

@@ -381,8 +379,6 @@ import sys
381379
import builtins
382380

383381
if sys.version_info >= (3, 11):
384-
# TODO
385-
# error: [unresolved-attribute]
386382
builtins.ExceptionGroup
387383
```
388384

@@ -430,6 +426,33 @@ if False:
430426
print(x)
431427
```
432428

429+
### Type annotations
430+
431+
Silencing of diagnostics also works for type annotations, even if they are stringified:
432+
433+
```py
434+
import sys
435+
import typing
436+
437+
if sys.version_info >= (3, 11):
438+
# TODO (silence diagnostics for imports, see above)
439+
# error: [unresolved-import]
440+
from typing import Self
441+
442+
class C:
443+
def name_expr(self) -> Self:
444+
return self
445+
446+
def name_expr_stringified(self) -> "Self":
447+
return self
448+
449+
def attribute_expr(self) -> typing.Self:
450+
return self
451+
452+
def attribute_expr_stringified(self) -> "typing.Self":
453+
return self
454+
```
455+
433456
### Use of unreachable symbols in type annotations, or as class bases
434457

435458
We should not show any diagnostics in type annotations inside unreachable sections.

crates/red_knot_python_semantic/src/semantic_index.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use salsa::Update;
1111

1212
use crate::module_name::ModuleName;
1313
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
14-
use crate::semantic_index::ast_ids::{AstIds, ScopedUseId};
14+
use crate::semantic_index::ast_ids::{AstIds, ScopedExpressionId};
1515
use crate::semantic_index::attribute_assignment::AttributeAssignments;
1616
use crate::semantic_index::builder::SemanticIndexBuilder;
1717
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
@@ -254,8 +254,8 @@ impl<'db> SemanticIndex<'db> {
254254
})
255255
}
256256

257-
/// Returns true if a given 'use' of a symbol is reachable from the start of the scope.
258-
/// For example, in the following code, use `2` is reachable, but `1` and `3` are not:
257+
/// Returns true if a given expression is reachable from the start of the scope. For example,
258+
/// in the following code, expression `2` is reachable, but expressions `1` and `3` are not:
259259
/// ```py
260260
/// def f():
261261
/// x = 1
@@ -265,16 +265,16 @@ impl<'db> SemanticIndex<'db> {
265265
/// return
266266
/// x # 3
267267
/// ```
268-
pub(crate) fn is_symbol_use_reachable(
268+
pub(crate) fn is_expression_reachable(
269269
&self,
270270
db: &'db dyn crate::Db,
271271
scope_id: FileScopeId,
272-
use_id: ScopedUseId,
272+
expression_id: ScopedExpressionId,
273273
) -> bool {
274274
self.is_scope_reachable(db, scope_id)
275275
&& self
276276
.use_def_map(scope_id)
277-
.is_symbol_use_reachable(db, use_id)
277+
.is_expression_reachable(db, expression_id)
278278
}
279279

280280
/// Returns an iterator over the descendent scopes of `scope`.

crates/red_knot_python_semantic/src/semantic_index/builder.rs

+32-16
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use ruff_db::parsed::ParsedModule;
88
use ruff_index::IndexVec;
99
use ruff_python_ast::name::Name;
1010
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
11-
use ruff_python_ast::{self as ast, ExprContext};
11+
use ruff_python_ast::{self as ast};
1212

1313
use crate::ast_node_ref::AstNodeRef;
1414
use crate::module_name::ModuleName;
@@ -1770,7 +1770,8 @@ where
17701770
if is_use {
17711771
self.mark_symbol_used(symbol);
17721772
let use_id = self.current_ast_ids().record_use(expr);
1773-
self.current_use_def_map_mut().record_use(symbol, use_id);
1773+
self.current_use_def_map_mut()
1774+
.record_use(symbol, use_id, expression_id);
17741775
}
17751776

17761777
if is_definition {
@@ -2011,24 +2012,39 @@ where
20112012
ast::Expr::Attribute(ast::ExprAttribute {
20122013
value: object,
20132014
attr,
2014-
ctx: ExprContext::Store,
2015+
ctx,
20152016
range: _,
20162017
}) => {
2017-
if let Some(unpack) = self
2018-
.current_assignment()
2019-
.as_ref()
2020-
.and_then(CurrentAssignment::unpack)
2021-
{
2022-
self.register_attribute_assignment(
2023-
object,
2024-
attr,
2025-
AttributeAssignment::Unpack {
2026-
attribute_expression_id: expression_id,
2027-
unpack,
2028-
},
2029-
);
2018+
if ctx.is_store() {
2019+
if let Some(unpack) = self
2020+
.current_assignment()
2021+
.as_ref()
2022+
.and_then(CurrentAssignment::unpack)
2023+
{
2024+
self.register_attribute_assignment(
2025+
object,
2026+
attr,
2027+
AttributeAssignment::Unpack {
2028+
attribute_expression_id: expression_id,
2029+
unpack,
2030+
},
2031+
);
2032+
}
20302033
}
20312034

2035+
// Track reachability of attribute expressions to silence `unresolved-attribute`
2036+
// diagnostics in unreachable code.
2037+
self.current_use_def_map_mut()
2038+
.record_expression_reachability(expression_id);
2039+
2040+
walk_expr(self, expr);
2041+
}
2042+
ast::Expr::StringLiteral(_) => {
2043+
// Track reachability of string literals, as they could be a stringified annotation
2044+
// with child expressions whose reachability we are interested in.
2045+
self.current_use_def_map_mut()
2046+
.record_expression_reachability(expression_id);
2047+
20322048
walk_expr(self, expr);
20332049
}
20342050
_ => {

crates/red_knot_python_semantic/src/semantic_index/use_def.rs

+43-13
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ use self::symbol_state::{
263263
LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, ScopedDefinitionId,
264264
SymbolBindings, SymbolDeclarations, SymbolState,
265265
};
266-
use crate::semantic_index::ast_ids::ScopedUseId;
266+
use crate::semantic_index::ast_ids::{ScopedExpressionId, ScopedUseId};
267267
use crate::semantic_index::definition::Definition;
268268
use crate::semantic_index::narrowing_constraints::{
269269
NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator,
@@ -297,8 +297,8 @@ pub(crate) struct UseDefMap<'db> {
297297
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
298298
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
299299

300-
/// Tracks whether or not a given use of a symbol is reachable from the start of the scope.
301-
reachability_by_use: IndexVec<ScopedUseId, ScopedVisibilityConstraintId>,
300+
/// Tracks whether or not a given expression is reachable from the start of the scope.
301+
expression_reachability: FxHashMap<ScopedExpressionId, ScopedVisibilityConstraintId>,
302302

303303
/// If the definition is a binding (only) -- `x = 1` for example -- then we need
304304
/// [`SymbolDeclarations`] to know whether this binding is permitted by the live declarations.
@@ -359,8 +359,27 @@ impl<'db> UseDefMap<'db> {
359359
.is_always_false()
360360
}
361361

362-
pub(super) fn is_symbol_use_reachable(&self, db: &dyn crate::Db, use_id: ScopedUseId) -> bool {
363-
self.is_reachable(db, self.reachability_by_use[use_id])
362+
/// Check whether or not a given expression is reachable from the start of the scope. This
363+
/// is a local analysis which does not capture the possibility that the entire scope might
364+
/// be unreachable. Use [`super::SemanticIndex::is_expression_reachable`] for the global
365+
/// analysis.
366+
#[track_caller]
367+
pub(super) fn is_expression_reachable(
368+
&self,
369+
db: &dyn crate::Db,
370+
expression_id: ScopedExpressionId,
371+
) -> bool {
372+
!self
373+
.visibility_constraints
374+
.evaluate(
375+
db,
376+
&self.predicates,
377+
*self
378+
.expression_reachability
379+
.get(&expression_id)
380+
.expect("`is_expression_reachable` should only be called on expressions with recorded reachability"),
381+
)
382+
.is_always_false()
364383
}
365384

366385
pub(crate) fn public_bindings(
@@ -617,8 +636,8 @@ pub(super) struct UseDefMapBuilder<'db> {
617636
/// The use of `x` is recorded with a reachability constraint of `[test]`.
618637
pub(super) reachability: ScopedVisibilityConstraintId,
619638

620-
/// Tracks whether or not a given use of a symbol is reachable from the start of the scope.
621-
reachability_by_use: IndexVec<ScopedUseId, ScopedVisibilityConstraintId>,
639+
/// Tracks whether or not a given expression is reachable from the start of the scope.
640+
expression_reachability: FxHashMap<ScopedExpressionId, ScopedVisibilityConstraintId>,
622641

623642
/// Live declarations for each so-far-recorded binding.
624643
declarations_by_binding: FxHashMap<Definition<'db>, SymbolDeclarations>,
@@ -644,7 +663,7 @@ impl Default for UseDefMapBuilder<'_> {
644663
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
645664
bindings_by_use: IndexVec::new(),
646665
reachability: ScopedVisibilityConstraintId::ALWAYS_TRUE,
647-
reachability_by_use: IndexVec::new(),
666+
expression_reachability: FxHashMap::default(),
648667
declarations_by_binding: FxHashMap::default(),
649668
bindings_by_declaration: FxHashMap::default(),
650669
symbol_states: IndexVec::new(),
@@ -799,16 +818,27 @@ impl<'db> UseDefMapBuilder<'db> {
799818
symbol_state.record_binding(def_id, self.scope_start_visibility);
800819
}
801820

802-
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
821+
pub(super) fn record_use(
822+
&mut self,
823+
symbol: ScopedSymbolId,
824+
use_id: ScopedUseId,
825+
expression_id: ScopedExpressionId,
826+
) {
803827
// We have a use of a symbol; clone the current bindings for that symbol, and record them
804828
// as the live bindings for this use.
805829
let new_use = self
806830
.bindings_by_use
807831
.push(self.symbol_states[symbol].bindings().clone());
808832
debug_assert_eq!(use_id, new_use);
809833

810-
let new_use = self.reachability_by_use.push(self.reachability);
811-
debug_assert_eq!(use_id, new_use);
834+
// Track reachability of all uses of symbols to silence `unresolved-reference`
835+
// diagnostics in unreachable code.
836+
self.record_expression_reachability(expression_id);
837+
}
838+
839+
pub(super) fn record_expression_reachability(&mut self, expression_id: ScopedExpressionId) {
840+
self.expression_reachability
841+
.insert(expression_id, self.reachability);
812842
}
813843

814844
pub(super) fn snapshot_eager_bindings(
@@ -905,7 +935,7 @@ impl<'db> UseDefMapBuilder<'db> {
905935
self.all_definitions.shrink_to_fit();
906936
self.symbol_states.shrink_to_fit();
907937
self.bindings_by_use.shrink_to_fit();
908-
self.reachability_by_use.shrink_to_fit();
938+
self.expression_reachability.shrink_to_fit();
909939
self.declarations_by_binding.shrink_to_fit();
910940
self.bindings_by_declaration.shrink_to_fit();
911941
self.eager_bindings.shrink_to_fit();
@@ -916,7 +946,7 @@ impl<'db> UseDefMapBuilder<'db> {
916946
narrowing_constraints: self.narrowing_constraints.build(),
917947
visibility_constraints: self.visibility_constraints.build(),
918948
bindings_by_use: self.bindings_by_use,
919-
reachability_by_use: self.reachability_by_use,
949+
expression_reachability: self.expression_reachability,
920950
public_symbols: self.symbol_states,
921951
declarations_by_binding: self.declarations_by_binding,
922952
bindings_by_declaration: self.bindings_by_declaration,

0 commit comments

Comments
 (0)