Skip to content

Commit 05454c1

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

5 files changed

Lines changed: 122 additions & 64 deletions

File tree

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: 28 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,35 @@ 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+
self.current_use_def_map_mut()
2036+
.record_expression_reachability(expression_id);
2037+
2038+
walk_expr(self, expr);
2039+
}
2040+
ast::Expr::StringLiteral(_) => {
2041+
self.current_use_def_map_mut()
2042+
.record_expression_reachability(expression_id);
2043+
20322044
walk_expr(self, expr);
20332045
}
20342046
_ => {

crates/red_knot_python_semantic/src/semantic_index/use_def.rs

Lines changed: 34 additions & 11 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,
@@ -298,7 +298,7 @@ pub(crate) struct UseDefMap<'db> {
298298
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
299299

300300
/// 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>,
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,23 @@ 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+
#[track_caller]
363+
pub(super) fn is_expression_reachable(
364+
&self,
365+
db: &dyn crate::Db,
366+
expression_id: ScopedExpressionId,
367+
) -> bool {
368+
!self
369+
.visibility_constraints
370+
.evaluate(
371+
db,
372+
&self.predicates,
373+
*self
374+
.expression_reachability
375+
.get(&expression_id)
376+
.expect("only called on expressions with recorded reachability"),
377+
)
378+
.is_always_false()
364379
}
365380

366381
pub(crate) fn public_bindings(
@@ -618,7 +633,7 @@ pub(super) struct UseDefMapBuilder<'db> {
618633
pub(super) reachability: ScopedVisibilityConstraintId,
619634

620635
/// 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>,
636+
expression_reachability: FxHashMap<ScopedExpressionId, ScopedVisibilityConstraintId>,
622637

623638
/// Live declarations for each so-far-recorded binding.
624639
declarations_by_binding: FxHashMap<Definition<'db>, SymbolDeclarations>,
@@ -644,7 +659,7 @@ impl Default for UseDefMapBuilder<'_> {
644659
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
645660
bindings_by_use: IndexVec::new(),
646661
reachability: ScopedVisibilityConstraintId::ALWAYS_TRUE,
647-
reachability_by_use: IndexVec::new(),
662+
expression_reachability: FxHashMap::default(),
648663
declarations_by_binding: FxHashMap::default(),
649664
bindings_by_declaration: FxHashMap::default(),
650665
symbol_states: IndexVec::new(),
@@ -799,16 +814,25 @@ impl<'db> UseDefMapBuilder<'db> {
799814
symbol_state.record_binding(def_id, self.scope_start_visibility);
800815
}
801816

802-
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
817+
pub(super) fn record_use(
818+
&mut self,
819+
symbol: ScopedSymbolId,
820+
use_id: ScopedUseId,
821+
expression_id: ScopedExpressionId,
822+
) {
803823
// We have a use of a symbol; clone the current bindings for that symbol, and record them
804824
// as the live bindings for this use.
805825
let new_use = self
806826
.bindings_by_use
807827
.push(self.symbol_states[symbol].bindings().clone());
808828
debug_assert_eq!(use_id, new_use);
809829

810-
let new_use = self.reachability_by_use.push(self.reachability);
811-
debug_assert_eq!(use_id, new_use);
830+
self.record_expression_reachability(expression_id);
831+
}
832+
833+
pub(super) fn record_expression_reachability(&mut self, expression_id: ScopedExpressionId) {
834+
self.expression_reachability
835+
.insert(expression_id, self.reachability);
812836
}
813837

814838
pub(super) fn snapshot_eager_bindings(
@@ -905,7 +929,6 @@ impl<'db> UseDefMapBuilder<'db> {
905929
self.all_definitions.shrink_to_fit();
906930
self.symbol_states.shrink_to_fit();
907931
self.bindings_by_use.shrink_to_fit();
908-
self.reachability_by_use.shrink_to_fit();
909932
self.declarations_by_binding.shrink_to_fit();
910933
self.bindings_by_declaration.shrink_to_fit();
911934
self.eager_bindings.shrink_to_fit();
@@ -916,7 +939,7 @@ impl<'db> UseDefMapBuilder<'db> {
916939
narrowing_constraints: self.narrowing_constraints.build(),
917940
visibility_constraints: self.visibility_constraints.build(),
918941
bindings_by_use: self.bindings_by_use,
919-
reachability_by_use: self.reachability_by_use,
942+
expression_reachability: self.expression_reachability,
920943
public_symbols: self.symbol_states,
921944
declarations_by_binding: self.declarations_by_binding,
922945
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_diagnostic = 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_diagnostic {
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)