mirror of https://github.com/astral-sh/ruff
Merge branch 'comprehension-assign-attr-unresolved' into david/type-of-self-in-methods-integration-2
This commit is contained in:
commit
249a807665
|
|
@ -377,6 +377,11 @@ reveal_type(c_instance.y) # revealed: Unknown | int
|
||||||
|
|
||||||
#### Attributes defined in comprehensions
|
#### Attributes defined in comprehensions
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class IntIterator:
|
class IntIterator:
|
||||||
def __next__(self) -> int:
|
def __next__(self) -> int:
|
||||||
|
|
@ -417,35 +422,73 @@ class D:
|
||||||
|
|
||||||
c_instance = C()
|
c_instance = C()
|
||||||
|
|
||||||
# TODO: no error, reveal Unknown | int
|
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.a) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: no error, reveal Unknown | int
|
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.b) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: no error, reveal Unknown | str
|
reveal_type(c_instance.c) # revealed: Unknown | str
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.c) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: no error, reveal Unknown | int
|
reveal_type(c_instance.d) # revealed: Unknown | int
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.d) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: no error, reveal Unknown | int
|
reveal_type(c_instance.e) # revealed: Unknown | int
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.e) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: no error, reveal Unknown | int
|
reveal_type(c_instance.f) # revealed: Unknown | int
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.f) # revealed: Unknown
|
|
||||||
|
|
||||||
# This one is correctly not resolved as an attribute:
|
# This one is correctly not resolved as an attribute:
|
||||||
# error: [unresolved-attribute]
|
# error: [unresolved-attribute]
|
||||||
reveal_type(c_instance.g) # revealed: Unknown
|
reveal_type(c_instance.g) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
It does not matter how much the comprehension is nested.
|
||||||
|
|
||||||
|
Similarly attributes defined by the comprehension in a generic method are recognized.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def f[T](self):
|
||||||
|
[... for self.a in [1]]
|
||||||
|
[[... for self.b in [1]] for _ in [1]]
|
||||||
|
|
||||||
|
c_instance = C()
|
||||||
|
|
||||||
|
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||||
|
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||||
|
```
|
||||||
|
|
||||||
|
If the comprehension is inside another scope like function then that attribute is not inferred.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
def f():
|
||||||
|
[... for self.a in IntIterable()]
|
||||||
|
|
||||||
|
def g():
|
||||||
|
[... for self.b in IntIterable()]
|
||||||
|
g()
|
||||||
|
|
||||||
|
c_instance = C()
|
||||||
|
|
||||||
|
# This attribute is in the function f and is not reachable
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(c_instance.a) # revealed: Unknown
|
||||||
|
|
||||||
|
# TODO: Even though g method is called and is reachable we do not record this attribute assignment
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(c_instance.b) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
If the comprehension is nested in any other eager scope it still can assign attributes.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
class D:
|
||||||
|
[[... for self.a in IntIterable()] for _ in IntIterable()]
|
||||||
|
|
||||||
|
reveal_type(C().a) # revealed: Unknown | int
|
||||||
|
```
|
||||||
|
|
||||||
#### Conditionally declared / bound attributes
|
#### Conditionally declared / bound attributes
|
||||||
|
|
||||||
We currently treat implicit instance attributes to be bound, even if they are only conditionally
|
We currently treat implicit instance attributes to be bound, even if they are only conditionally
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::iter::FusedIterator;
|
use std::iter::{FusedIterator, once};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
|
|
@ -154,29 +154,56 @@ pub(crate) fn attribute_declarations<'db, 's>(
|
||||||
///
|
///
|
||||||
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
||||||
/// introduces a direct dependency on that file's AST.
|
/// introduces a direct dependency on that file's AST.
|
||||||
pub(crate) fn attribute_scopes<'db, 's>(
|
pub(crate) fn attribute_scopes<'db>(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
class_body_scope: ScopeId<'db>,
|
class_body_scope: ScopeId<'db>,
|
||||||
) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> {
|
) -> impl Iterator<Item = FileScopeId> + 'db {
|
||||||
let file = class_body_scope.file(db);
|
let file = class_body_scope.file(db);
|
||||||
let index = semantic_index(db, file);
|
let index = semantic_index(db, file);
|
||||||
let class_scope_id = class_body_scope.file_scope_id(db);
|
let class_scope_id = class_body_scope.file_scope_id(db);
|
||||||
|
ChildrenIter::new(&index.scopes, class_scope_id)
|
||||||
|
.filter_map(move |(child_scope_id, scope)| {
|
||||||
|
let (function_scope_id, function_scope) =
|
||||||
|
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
||||||
|
// This could be a generic method with a type-params scope.
|
||||||
|
// Go one level deeper to find the function scope. The first
|
||||||
|
// descendant is the (potential) function scope.
|
||||||
|
let function_scope_id = scope.descendants().start;
|
||||||
|
(function_scope_id, index.scope(function_scope_id))
|
||||||
|
} else {
|
||||||
|
(child_scope_id, scope)
|
||||||
|
};
|
||||||
|
function_scope.node().as_function()?;
|
||||||
|
Some(function_scope_id)
|
||||||
|
})
|
||||||
|
.flat_map(move |func_id| {
|
||||||
|
// Add any descendent scope that is eager and have eager scopes between the scope
|
||||||
|
// and the method scope. Since attributes can be defined in this scope.
|
||||||
|
let nested = index.descendent_scopes(func_id).filter_map(move |(id, s)| {
|
||||||
|
let is_eager = s.kind().is_eager();
|
||||||
|
let parents_are_eager = {
|
||||||
|
let mut all_parents_eager = true;
|
||||||
|
let mut current = Some(id);
|
||||||
|
|
||||||
ChildrenIter::new(&index.scopes, class_scope_id).filter_map(move |(child_scope_id, scope)| {
|
while let Some(scope_id) = current {
|
||||||
let (function_scope_id, function_scope) =
|
if scope_id == func_id {
|
||||||
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
break;
|
||||||
// This could be a generic method with a type-params scope.
|
}
|
||||||
// Go one level deeper to find the function scope. The first
|
let scope = index.scope(scope_id);
|
||||||
// descendant is the (potential) function scope.
|
if !scope.is_eager() {
|
||||||
let function_scope_id = scope.descendants().start;
|
all_parents_eager = false;
|
||||||
(function_scope_id, index.scope(function_scope_id))
|
break;
|
||||||
} else {
|
}
|
||||||
(child_scope_id, scope)
|
current = scope.parent();
|
||||||
};
|
}
|
||||||
|
|
||||||
function_scope.node().as_function()?;
|
all_parents_eager
|
||||||
Some(function_scope_id)
|
};
|
||||||
})
|
|
||||||
|
(parents_are_eager && is_eager).then_some(id)
|
||||||
|
});
|
||||||
|
once(func_id).chain(nested)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the module global scope of `file`.
|
/// Returns the module global scope of `file`.
|
||||||
|
|
@ -751,7 +778,10 @@ mod tests {
|
||||||
use crate::semantic_index::scope::{FileScopeId, Scope, ScopeKind};
|
use crate::semantic_index::scope::{FileScopeId, Scope, ScopeKind};
|
||||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||||
use crate::semantic_index::use_def::UseDefMap;
|
use crate::semantic_index::use_def::UseDefMap;
|
||||||
use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map};
|
use crate::semantic_index::{
|
||||||
|
attribute_declarations, attribute_scopes, global_scope, place_table, semantic_index,
|
||||||
|
use_def_map,
|
||||||
|
};
|
||||||
|
|
||||||
impl UseDefMap<'_> {
|
impl UseDefMap<'_> {
|
||||||
fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
|
fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
|
||||||
|
|
@ -1128,6 +1158,124 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test case to validate that comprehensions inside a method are correctly marked as attribute
|
||||||
|
/// scopes
|
||||||
|
#[test]
|
||||||
|
fn comprehension_in_method_instance_attribute() {
|
||||||
|
let TestCase { db, file } = test_case(
|
||||||
|
"
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
[None for self.z in range(1)]
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
let index = semantic_index(&db, file);
|
||||||
|
|
||||||
|
let [(class_scope_id, class_scope)] = index
|
||||||
|
.child_scopes(FileScopeId::global())
|
||||||
|
.collect::<Vec<_>>()[..]
|
||||||
|
else {
|
||||||
|
panic!("expected one child scope (the class)")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(class_scope.kind(), ScopeKind::Class);
|
||||||
|
let class_scope = class_scope_id.to_scope_id(&db, file);
|
||||||
|
|
||||||
|
let method_scopes: Vec<_> = index.child_scopes(class_scope_id).collect();
|
||||||
|
assert_eq!(method_scopes.len(), 1, "expected __init__");
|
||||||
|
let (method_scope_id, method_scope) = method_scopes[0];
|
||||||
|
assert_eq!(method_scope.kind(), ScopeKind::Function);
|
||||||
|
|
||||||
|
let comp_scopes: Vec<_> = index.child_scopes(method_scope_id).collect();
|
||||||
|
assert_eq!(comp_scopes.len(), 1, "expected one comprehension scope");
|
||||||
|
let (comprehension_scope_id, comprehension_scope) = comp_scopes[0];
|
||||||
|
assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension);
|
||||||
|
|
||||||
|
let attr_scopes: Vec<_> = attribute_scopes(&db, class_scope).collect();
|
||||||
|
assert!(
|
||||||
|
attr_scopes.contains(&method_scope_id),
|
||||||
|
"attribute_scopes should include the method scope"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
attr_scopes.contains(&comprehension_scope_id),
|
||||||
|
"attribute_scopes should include the comprehension scope"
|
||||||
|
);
|
||||||
|
|
||||||
|
let comprehension_place_table = index.place_table(comprehension_scope_id);
|
||||||
|
let members: Vec<_> = comprehension_place_table.members().collect();
|
||||||
|
|
||||||
|
let has_z_instance_attr = members
|
||||||
|
.iter()
|
||||||
|
.any(|member| member.as_instance_attribute() == Some("z"));
|
||||||
|
assert!(
|
||||||
|
has_z_instance_attr,
|
||||||
|
"self.z should be marked as an instance attribute in the comprehension scope found members: {members:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let z_declarations: Vec<_> = attribute_declarations(&db, class_scope, "z")
|
||||||
|
.map(|(decls, scope_id)| {
|
||||||
|
let decl_vec: Vec<_> = decls.collect();
|
||||||
|
(decl_vec, scope_id)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
!z_declarations.is_empty(),
|
||||||
|
"attribute_declarations should find declarations for z"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that at least one declaration of z is in the comprehension scope
|
||||||
|
let has_comp_declaration = z_declarations
|
||||||
|
.iter()
|
||||||
|
.any(|(_, scope_id)| *scope_id == comprehension_scope_id);
|
||||||
|
assert!(
|
||||||
|
has_comp_declaration,
|
||||||
|
"attribute_declarations should include a declaration from the comprehension scope"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test case to validate that when first method argument is shadowed by a comprehension variable,
|
||||||
|
/// attributes accessed on the shadowed argument should NOT be tracked as instance attributes
|
||||||
|
/// of the containing class.
|
||||||
|
#[test]
|
||||||
|
fn comprehension_shadowing_self() {
|
||||||
|
let TestCase { db, file } = test_case(
|
||||||
|
"
|
||||||
|
class D:
|
||||||
|
g: int
|
||||||
|
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
[[None for self.g in range(1)] for self in [D()]]
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
let index = semantic_index(&db, file);
|
||||||
|
|
||||||
|
let all_scopes: Vec<_> = index.child_scopes(FileScopeId::global()).collect();
|
||||||
|
|
||||||
|
let (class_c_scope_id, class_c_scope) = all_scopes[1];
|
||||||
|
assert_eq!(class_c_scope.kind(), ScopeKind::Class);
|
||||||
|
let class_c_scope = class_c_scope_id.to_scope_id(&db, file);
|
||||||
|
|
||||||
|
let attr_scopes: Vec<_> = attribute_scopes(&db, class_c_scope).collect();
|
||||||
|
let mut has_g_attribute = false;
|
||||||
|
for scope_id in &attr_scopes {
|
||||||
|
let place_table = index.place_table(*scope_id);
|
||||||
|
if place_table
|
||||||
|
.member_id_by_instance_attribute_name("g")
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
has_g_attribute = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!has_g_attribute,
|
||||||
|
"self.g should NOT be tracked as an attribute of C because 'self' is shadowed by the outer comprehension variable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Test case to validate that the `x` variable used in the comprehension is referencing the
|
/// Test case to validate that the `x` variable used in the comprehension is referencing the
|
||||||
/// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one.
|
/// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -184,29 +184,34 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||||
self.current_scope_info().file_scope_id
|
self.current_scope_info().file_scope_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the scope ID of the surrounding class body scope if the current scope
|
/// Returns the scope ID of the method scope if the current scope
|
||||||
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
|
/// is a method inside a class body or current scope is in eagerly executed scope in a method.
|
||||||
/// scope is a function body outside of a class, or if the current scope is not a
|
/// Returns `None` otherwise, e.g. if the current scope is a function body outside of a class, or if the current scope is not a
|
||||||
/// function body.
|
/// function body.
|
||||||
fn is_method_of_class(&self) -> Option<FileScopeId> {
|
fn is_method_or_eagerly_executed_in_method(&self) -> Option<FileScopeId> {
|
||||||
let mut scopes_rev = self.scope_stack.iter().rev();
|
let mut scopes_rev = self
|
||||||
|
.scope_stack
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.skip_while(|scope| self.scopes[scope.file_scope_id].is_eager());
|
||||||
let current = scopes_rev.next()?;
|
let current = scopes_rev.next()?;
|
||||||
|
|
||||||
if self.scopes[current.file_scope_id].kind() != ScopeKind::Function {
|
if self.scopes[current.file_scope_id].kind() != ScopeKind::Function {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maybe_method = current.file_scope_id;
|
||||||
let parent = scopes_rev.next()?;
|
let parent = scopes_rev.next()?;
|
||||||
|
|
||||||
match self.scopes[parent.file_scope_id].kind() {
|
match self.scopes[parent.file_scope_id].kind() {
|
||||||
ScopeKind::Class => Some(parent.file_scope_id),
|
ScopeKind::Class => Some(maybe_method),
|
||||||
ScopeKind::TypeParams => {
|
ScopeKind::TypeParams => {
|
||||||
// If the function is generic, the parent scope is an annotation scope.
|
// If the function is generic, the parent scope is an annotation scope.
|
||||||
// In this case, we need to go up one level higher to find the class scope.
|
// In this case, we need to go up one level higher to find the class scope.
|
||||||
let grandparent = scopes_rev.next()?;
|
let grandparent = scopes_rev.next()?;
|
||||||
|
|
||||||
if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class {
|
if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class {
|
||||||
Some(grandparent.file_scope_id)
|
Some(maybe_method)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +220,32 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a symbol name is bound in any intermediate eager scopes
|
||||||
|
/// between the current scope and the specified method scope.
|
||||||
|
///
|
||||||
|
fn is_symbol_bound_in_intermediate_eager_scopes(
|
||||||
|
&self,
|
||||||
|
symbol_name: &str,
|
||||||
|
method_scope_id: FileScopeId,
|
||||||
|
) -> bool {
|
||||||
|
for scope_info in self.scope_stack.iter().rev() {
|
||||||
|
let scope_id = scope_info.file_scope_id;
|
||||||
|
|
||||||
|
if scope_id == method_scope_id {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(symbol_id) = self.place_tables[scope_id].symbol_id(symbol_name) {
|
||||||
|
let symbol = self.place_tables[scope_id].symbol(symbol_id);
|
||||||
|
if symbol.is_bound() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Push a new loop, returning the outer loop, if any.
|
/// Push a new loop, returning the outer loop, if any.
|
||||||
fn push_loop(&mut self) -> Option<Loop> {
|
fn push_loop(&mut self) -> Option<Loop> {
|
||||||
self.current_scope_info_mut()
|
self.current_scope_info_mut()
|
||||||
|
|
@ -1647,7 +1678,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||||
self.visit_expr(&node.annotation);
|
self.visit_expr(&node.annotation);
|
||||||
if let Some(value) = &node.value {
|
if let Some(value) = &node.value {
|
||||||
self.visit_expr(value);
|
self.visit_expr(value);
|
||||||
if self.is_method_of_class().is_some() {
|
if self.is_method_or_eagerly_executed_in_method().is_some() {
|
||||||
// Record the right-hand side of the assignment as a standalone expression
|
// Record the right-hand side of the assignment as a standalone expression
|
||||||
// if we're inside a method. This allows type inference to infer the type
|
// if we're inside a method. This allows type inference to infer the type
|
||||||
// of the value for annotated assignments like `self.CONSTANT: Final = 1`,
|
// of the value for annotated assignments like `self.CONSTANT: Final = 1`,
|
||||||
|
|
@ -2319,14 +2350,21 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||||
| ast::Expr::Attribute(ast::ExprAttribute { ctx, .. })
|
| ast::Expr::Attribute(ast::ExprAttribute { ctx, .. })
|
||||||
| ast::Expr::Subscript(ast::ExprSubscript { ctx, .. }) => {
|
| ast::Expr::Subscript(ast::ExprSubscript { ctx, .. }) => {
|
||||||
if let Some(mut place_expr) = PlaceExpr::try_from_expr(expr) {
|
if let Some(mut place_expr) = PlaceExpr::try_from_expr(expr) {
|
||||||
if self.is_method_of_class().is_some() {
|
if let Some(method_scope_id) = self.is_method_or_eagerly_executed_in_method() {
|
||||||
if let PlaceExpr::Member(member) = &mut place_expr {
|
if let PlaceExpr::Member(member) = &mut place_expr {
|
||||||
if member.is_instance_attribute_candidate() {
|
if member.is_instance_attribute_candidate() {
|
||||||
// We specifically mark attribute assignments to the first parameter of a method,
|
// We specifically mark attribute assignments to the first parameter of a method,
|
||||||
// i.e. typically `self` or `cls`.
|
// i.e. typically `self` or `cls`.
|
||||||
let accessed_object_refers_to_first_parameter = self
|
// However, we must check that the symbol hasn't been shadowed by an intermediate
|
||||||
.current_first_parameter_name
|
// scope (e.g., a comprehension variable: `for self in [...]`).
|
||||||
.is_some_and(|first| member.symbol_name() == first);
|
let accessed_object_refers_to_first_parameter =
|
||||||
|
self.current_first_parameter_name.is_some_and(|first| {
|
||||||
|
member.symbol_name() == first
|
||||||
|
&& !self.is_symbol_bound_in_intermediate_eager_scopes(
|
||||||
|
first,
|
||||||
|
method_scope_id,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
if accessed_object_refers_to_first_parameter {
|
if accessed_object_refers_to_first_parameter {
|
||||||
member.mark_instance_attribute();
|
member.mark_instance_attribute();
|
||||||
|
|
|
||||||
|
|
@ -3227,30 +3227,47 @@ impl<'db> ClassLiteral<'db> {
|
||||||
union_of_inferred_types = union_of_inferred_types.add(Type::unknown());
|
union_of_inferred_types = union_of_inferred_types.add(Type::unknown());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (attribute_assignments, method_scope_id) in
|
for (attribute_assignments, attribute_binding_scope_id) in
|
||||||
attribute_assignments(db, class_body_scope, &name)
|
attribute_assignments(db, class_body_scope, &name)
|
||||||
{
|
{
|
||||||
let method_scope = index.scope(method_scope_id);
|
let binding_scope = index.scope(attribute_binding_scope_id);
|
||||||
if !is_valid_scope(method_scope) {
|
if !is_valid_scope(binding_scope) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The attribute assignment inherits the reachability of the method which contains it
|
let scope_for_reachability_analysis = {
|
||||||
let is_method_reachable = if let Some(method_def) = method_scope.node().as_function() {
|
if binding_scope.node().as_function().is_some() {
|
||||||
let method = index.expect_single_definition(method_def);
|
binding_scope
|
||||||
let method_place = class_table
|
} else if binding_scope.is_eager() {
|
||||||
.symbol_id(&method_def.node(&module).name)
|
let mut eager_scope_parent = binding_scope;
|
||||||
.unwrap();
|
while eager_scope_parent.is_eager()
|
||||||
class_map
|
&& let Some(parent) = eager_scope_parent.parent()
|
||||||
.all_reachable_symbol_bindings(method_place)
|
{
|
||||||
.find_map(|bind| {
|
eager_scope_parent = index.scope(parent);
|
||||||
(bind.binding.is_defined_and(|def| def == method))
|
}
|
||||||
.then(|| class_map.binding_reachability(db, &bind))
|
eager_scope_parent
|
||||||
})
|
} else {
|
||||||
.unwrap_or(Truthiness::AlwaysFalse)
|
binding_scope
|
||||||
} else {
|
}
|
||||||
Truthiness::AlwaysFalse
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The attribute assignment inherits the reachability of the method which contains it
|
||||||
|
let is_method_reachable =
|
||||||
|
if let Some(method_def) = scope_for_reachability_analysis.node().as_function() {
|
||||||
|
let method = index.expect_single_definition(method_def);
|
||||||
|
let method_place = class_table
|
||||||
|
.symbol_id(&method_def.node(&module).name)
|
||||||
|
.unwrap();
|
||||||
|
class_map
|
||||||
|
.all_reachable_symbol_bindings(method_place)
|
||||||
|
.find_map(|bind| {
|
||||||
|
(bind.binding.is_defined_and(|def| def == method))
|
||||||
|
.then(|| class_map.binding_reachability(db, &bind))
|
||||||
|
})
|
||||||
|
.unwrap_or(Truthiness::AlwaysFalse)
|
||||||
|
} else {
|
||||||
|
Truthiness::AlwaysFalse
|
||||||
|
};
|
||||||
if is_method_reachable.is_always_false() {
|
if is_method_reachable.is_always_false() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue