Skip to content

Commit 1a76d8e

Browse files
committed
[red-knot] Silence unresolved-attribute diagnostics
1 parent 4d50ee6 commit 1a76d8e

File tree

5 files changed

+133
-66
lines changed

5 files changed

+133
-66
lines changed

crates/red_knot_python_semantic/resources/mdtest/unreachable.md

Lines changed: 0 additions & 4 deletions
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

crates/red_knot_python_semantic/src/semantic_index.rs

Lines changed: 6 additions & 6 deletions
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

Lines changed: 32 additions & 16 deletions
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

Lines changed: 41 additions & 13 deletions
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,26 @@ 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 [`SemanticIndex::is_expression_reachable`] for the global analysis.
365+
#[track_caller]
366+
pub(super) fn is_expression_reachable(
367+
&self,
368+
db: &dyn crate::Db,
369+
expression_id: ScopedExpressionId,
370+
) -> bool {
371+
!self
372+
.visibility_constraints
373+
.evaluate(
374+
db,
375+
&self.predicates,
376+
*self
377+
.expression_reachability
378+
.get(&expression_id)
379+
.expect("only called on expressions with recorded reachability"),
380+
)
381+
.is_always_false()
364382
}
365383

366384
pub(crate) fn public_bindings(
@@ -617,8 +635,8 @@ pub(super) struct UseDefMapBuilder<'db> {
617635
/// The use of `x` is recorded with a reachability constraint of `[test]`.
618636
pub(super) reachability: ScopedVisibilityConstraintId,
619637

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>,
638+
/// Tracks whether or not a given expression is reachable from the start of the scope.
639+
expression_reachability: FxHashMap<ScopedExpressionId, ScopedVisibilityConstraintId>,
622640

623641
/// Live declarations for each so-far-recorded binding.
624642
declarations_by_binding: FxHashMap<Definition<'db>, SymbolDeclarations>,
@@ -644,7 +662,7 @@ impl Default for UseDefMapBuilder<'_> {
644662
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
645663
bindings_by_use: IndexVec::new(),
646664
reachability: ScopedVisibilityConstraintId::ALWAYS_TRUE,
647-
reachability_by_use: IndexVec::new(),
665+
expression_reachability: FxHashMap::default(),
648666
declarations_by_binding: FxHashMap::default(),
649667
bindings_by_declaration: FxHashMap::default(),
650668
symbol_states: IndexVec::new(),
@@ -799,16 +817,27 @@ impl<'db> UseDefMapBuilder<'db> {
799817
symbol_state.record_binding(def_id, self.scope_start_visibility);
800818
}
801819

802-
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
820+
pub(super) fn record_use(
821+
&mut self,
822+
symbol: ScopedSymbolId,
823+
use_id: ScopedUseId,
824+
expression_id: ScopedExpressionId,
825+
) {
803826
// We have a use of a symbol; clone the current bindings for that symbol, and record them
804827
// as the live bindings for this use.
805828
let new_use = self
806829
.bindings_by_use
807830
.push(self.symbol_states[symbol].bindings().clone());
808831
debug_assert_eq!(use_id, new_use);
809832

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

814843
pub(super) fn snapshot_eager_bindings(
@@ -905,7 +934,6 @@ impl<'db> UseDefMapBuilder<'db> {
905934
self.all_definitions.shrink_to_fit();
906935
self.symbol_states.shrink_to_fit();
907936
self.bindings_by_use.shrink_to_fit();
908-
self.reachability_by_use.shrink_to_fit();
909937
self.declarations_by_binding.shrink_to_fit();
910938
self.bindings_by_declaration.shrink_to_fit();
911939
self.eager_bindings.shrink_to_fit();
@@ -916,7 +944,7 @@ impl<'db> UseDefMapBuilder<'db> {
916944
narrowing_constraints: self.narrowing_constraints.build(),
917945
visibility_constraints: self.visibility_constraints.build(),
918946
bindings_by_use: self.bindings_by_use,
919-
reachability_by_use: self.reachability_by_use,
947+
expression_reachability: self.expression_reachability,
920948
public_symbols: self.symbol_states,
921949
declarations_by_binding: self.declarations_by_binding,
922950
bindings_by_declaration: self.bindings_by_declaration,

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,17 @@ impl<'db> TypeInferenceBuilder<'db> {
590590
matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred()
591591
}
592592

593+
/// Return the ID of the given expression, or the ID of the outermost enclosing string literal,
594+
/// if the expression originates from a stringified annotation.
595+
fn enclosing_expression_id(&self, expr: &impl HasScopedExpressionId) -> ScopedExpressionId {
596+
match self.deferred_state {
597+
DeferredExpressionState::InStringAnnotation(enclosing_string_expression) => {
598+
enclosing_string_expression
599+
}
600+
_ => expr.scoped_expression_id(self.db(), self.scope()),
601+
}
602+
}
603+
593604
fn in_stub(&self) -> bool {
594605
self.context.in_stub()
595606
}
@@ -4302,9 +4313,11 @@ impl<'db> TypeInferenceBuilder<'db> {
43024313
} else {
43034314
let use_id = name_node.scoped_use_id(db, scope);
43044315
let symbol = symbol_from_bindings(db, use_def.bindings_at_use(use_id));
4305-
let report_unresolved_usage =
4306-
self.index
4307-
.is_symbol_use_reachable(db, file_scope_id, use_id);
4316+
let report_unresolved_usage = self.index.is_expression_reachable(
4317+
db,
4318+
file_scope_id,
4319+
name_node.scoped_expression_id(db, scope),
4320+
);
43084321
(symbol, report_unresolved_usage)
43094322
};
43104323

@@ -4511,26 +4524,36 @@ impl<'db> TypeInferenceBuilder<'db> {
45114524
_ => false,
45124525
};
45134526

4514-
if bound_on_instance {
4515-
self.context.report_lint(
4516-
&UNRESOLVED_ATTRIBUTE,
4517-
attribute,
4518-
format_args!(
4519-
"Attribute `{}` can only be accessed on instances, not on the class object `{}` itself.",
4520-
attr.id,
4521-
value_type.display(db)
4522-
),
4523-
);
4524-
} else {
4525-
self.context.report_lint(
4526-
&UNRESOLVED_ATTRIBUTE,
4527-
attribute,
4528-
format_args!(
4529-
"Type `{}` has no attribute `{}`",
4530-
value_type.display(db),
4531-
attr.id
4532-
),
4527+
let report_unresolved_attribute = self
4528+
.index
4529+
.is_expression_reachable(
4530+
db,
4531+
self.scope().file_scope_id(db),
4532+
self.enclosing_expression_id(attribute),
45334533
);
4534+
4535+
if report_unresolved_attribute {
4536+
if bound_on_instance {
4537+
self.context.report_lint(
4538+
&UNRESOLVED_ATTRIBUTE,
4539+
attribute,
4540+
format_args!(
4541+
"Attribute `{}` can only be accessed on instances, not on the class object `{}` itself.",
4542+
attr.id,
4543+
value_type.display(db)
4544+
),
4545+
);
4546+
} else {
4547+
self.context.report_lint(
4548+
&UNRESOLVED_ATTRIBUTE,
4549+
attribute,
4550+
format_args!(
4551+
"Type `{}` has no attribute `{}`",
4552+
value_type.display(db),
4553+
attr.id
4554+
),
4555+
);
4556+
}
45344557
}
45354558

45364559
Type::unknown().into()
@@ -6368,7 +6391,9 @@ impl<'db> TypeInferenceBuilder<'db> {
63686391
// String annotations are always evaluated in the deferred context.
63696392
self.infer_annotation_expression(
63706393
parsed.expr(),
6371-
DeferredExpressionState::InStringAnnotation,
6394+
DeferredExpressionState::InStringAnnotation(
6395+
self.enclosing_expression_id(string),
6396+
),
63726397
)
63736398
}
63746399
None => TypeAndQualifiers::unknown(),
@@ -6732,7 +6757,9 @@ impl<'db> TypeInferenceBuilder<'db> {
67326757
// String annotations are always evaluated in the deferred context.
67336758
self.infer_type_expression_with_state(
67346759
parsed.expr(),
6735-
DeferredExpressionState::InStringAnnotation,
6760+
DeferredExpressionState::InStringAnnotation(
6761+
self.enclosing_expression_id(string),
6762+
),
67366763
)
67376764
}
67386765
None => Type::unknown(),
@@ -7449,19 +7476,19 @@ enum DeferredExpressionState {
74497476
///
74507477
/// The annotation of `a` is completely inside a string while for `b`, it's only partially
74517478
/// stringified.
7452-
InStringAnnotation,
7479+
InStringAnnotation(ScopedExpressionId),
74537480
}
74547481

74557482
impl DeferredExpressionState {
74567483
const fn is_deferred(self) -> bool {
74577484
matches!(
74587485
self,
7459-
DeferredExpressionState::Deferred | DeferredExpressionState::InStringAnnotation
7486+
DeferredExpressionState::Deferred | DeferredExpressionState::InStringAnnotation(_)
74607487
)
74617488
}
74627489

74637490
const fn in_string_annotation(self) -> bool {
7464-
matches!(self, DeferredExpressionState::InStringAnnotation)
7491+
matches!(self, DeferredExpressionState::InStringAnnotation(_))
74657492
}
74667493
}
74677494

0 commit comments

Comments
 (0)