mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +00:00
[ty] Support declaration-only attributes (#19048)
## Summary Following ty issue [#698](https://github.com/astral-sh/ty/issues/698) this PR adds support for declarations. closes #698 ## Test Plan Tested against mdtest (specifically attributes). --------- Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
parent
b6edfbc70f
commit
e0b7f496f2
4 changed files with 90 additions and 16 deletions
|
@ -37,9 +37,7 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||||
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
|
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
|
||||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||||
|
|
||||||
# TODO: Should be `bytes` with no error, like mypy and pyright?
|
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.declared_only) # revealed: Unknown
|
|
||||||
|
|
||||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||||
|
|
||||||
|
@ -58,6 +56,9 @@ c_instance.declared_and_bound = "incompatible"
|
||||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `<class 'C'>` itself."
|
||||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||||
|
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(C.declared_and_bound) # revealed: Unknown
|
||||||
|
|
||||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `<class 'C'>`"
|
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `<class 'C'>`"
|
||||||
C.inferred_from_value = "overwritten on class"
|
C.inferred_from_value = "overwritten on class"
|
||||||
|
@ -143,16 +144,14 @@ class C:
|
||||||
c_instance = C(True)
|
c_instance = C(True)
|
||||||
|
|
||||||
reveal_type(c_instance.only_declared_in_body) # revealed: str | None
|
reveal_type(c_instance.only_declared_in_body) # revealed: str | None
|
||||||
# TODO: should be `str | None` without error
|
reveal_type(c_instance.only_declared_in_init) # revealed: str | None
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.only_declared_in_init) # revealed: Unknown
|
|
||||||
reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
|
reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
|
||||||
|
|
||||||
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
|
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
|
||||||
|
|
||||||
# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
|
# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
|
||||||
# which is planned in https://github.com/astral-sh/ruff/issues/14297
|
# which is planned in https://github.com/astral-sh/ruff/issues/14297
|
||||||
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | Literal["a"]
|
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
|
||||||
|
|
||||||
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
|
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
|
||||||
```
|
```
|
||||||
|
@ -183,9 +182,7 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||||
|
|
||||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||||
|
|
||||||
# TODO: should be `bytes` with no error, like mypy and pyright?
|
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||||
# error: [unresolved-attribute]
|
|
||||||
reveal_type(c_instance.declared_only) # revealed: Unknown
|
|
||||||
|
|
||||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,30 @@ pub(crate) fn attribute_assignments<'db, 's>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all attribute declarations (and their method scope IDs) with a symbol name matching
|
||||||
|
/// the one given for a specific class body scope.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
pub(crate) fn attribute_declarations<'db, 's>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
class_body_scope: ScopeId<'db>,
|
||||||
|
name: &'s str,
|
||||||
|
) -> impl Iterator<Item = (DeclarationsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
|
||||||
|
let file = class_body_scope.file(db);
|
||||||
|
let index = semantic_index(db, file);
|
||||||
|
|
||||||
|
attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| {
|
||||||
|
let place_table = index.place_table(function_scope_id);
|
||||||
|
let place = place_table.place_id_by_instance_attribute_name(name)?;
|
||||||
|
let use_def = &index.use_def_maps[function_scope_id];
|
||||||
|
Some((
|
||||||
|
use_def.inner.all_reachable_declarations(place),
|
||||||
|
function_scope_id,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all attribute assignments as scope IDs for a specific class body scope.
|
/// Returns all attribute assignments as scope IDs for a specific class body scope.
|
||||||
///
|
///
|
||||||
/// 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
|
||||||
|
|
|
@ -509,6 +509,18 @@ impl<'db> UseDefMap<'db> {
|
||||||
.is_always_false()
|
.is_always_false()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_declaration_reachable(
|
||||||
|
&self,
|
||||||
|
db: &dyn crate::Db,
|
||||||
|
declaration: &DeclarationWithConstraint<'db>,
|
||||||
|
) -> Truthiness {
|
||||||
|
self.reachability_constraints.evaluate(
|
||||||
|
db,
|
||||||
|
&self.predicates,
|
||||||
|
declaration.reachability_constraint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_binding_reachable(
|
pub(crate) fn is_binding_reachable(
|
||||||
&self,
|
&self,
|
||||||
db: &dyn crate::Db,
|
db: &dyn crate::Db,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::semantic_index::definition::{Definition, DefinitionState};
|
use crate::semantic_index::definition::{Definition, DefinitionState};
|
||||||
use crate::semantic_index::place::NodeWithScopeKind;
|
use crate::semantic_index::place::NodeWithScopeKind;
|
||||||
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex};
|
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex, attribute_declarations};
|
||||||
use crate::types::context::InferContext;
|
use crate::types::context::InferContext;
|
||||||
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
|
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
|
||||||
use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
||||||
|
@ -1763,10 +1763,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
let class_map = use_def_map(db, class_body_scope);
|
let class_map = use_def_map(db, class_body_scope);
|
||||||
let class_table = place_table(db, class_body_scope);
|
let class_table = place_table(db, class_body_scope);
|
||||||
|
|
||||||
for (attribute_assignments, method_scope_id) in
|
let is_valid_scope = |method_scope: ScopeId<'db>| {
|
||||||
attribute_assignments(db, class_body_scope, name)
|
|
||||||
{
|
|
||||||
let method_scope = method_scope_id.to_scope_id(db, file);
|
|
||||||
if let Some(method_def) = method_scope.node(db).as_function(&module) {
|
if let Some(method_def) = method_scope.node(db).as_function(&module) {
|
||||||
let method_name = method_def.name.as_str();
|
let method_name = method_def.name.as_str();
|
||||||
if let Place::Type(Type::FunctionLiteral(method_type), _) =
|
if let Place::Type(Type::FunctionLiteral(method_type), _) =
|
||||||
|
@ -1774,10 +1771,53 @@ impl<'db> ClassLiteral<'db> {
|
||||||
{
|
{
|
||||||
let method_decorator = MethodDecorator::try_from_fn_type(db, method_type);
|
let method_decorator = MethodDecorator::try_from_fn_type(db, method_type);
|
||||||
if method_decorator != Ok(target_method_decorator) {
|
if method_decorator != Ok(target_method_decorator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
// First check declarations
|
||||||
|
for (attribute_declarations, method_scope_id) in
|
||||||
|
attribute_declarations(db, class_body_scope, name)
|
||||||
|
{
|
||||||
|
let method_scope = method_scope_id.to_scope_id(db, file);
|
||||||
|
if !is_valid_scope(method_scope) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for attribute_declaration in attribute_declarations {
|
||||||
|
let DefinitionState::Defined(decl) = attribute_declaration.declaration else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let DefinitionKind::AnnotatedAssignment(annotated) = decl.kind(db) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if use_def_map(db, method_scope)
|
||||||
|
.is_declaration_reachable(db, &attribute_declaration)
|
||||||
|
.is_always_false()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let annotation_ty =
|
||||||
|
infer_expression_type(db, index.expression(annotated.annotation(&module)));
|
||||||
|
|
||||||
|
return Place::bound(annotation_ty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (attribute_assignments, method_scope_id) in
|
||||||
|
attribute_assignments(db, class_body_scope, name)
|
||||||
|
{
|
||||||
|
let method_scope = method_scope_id.to_scope_id(db, file);
|
||||||
|
if !is_valid_scope(method_scope) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let method_map = use_def_map(db, method_scope);
|
let method_map = use_def_map(db, method_scope);
|
||||||
|
|
||||||
// The attribute assignment inherits the reachability of the method which contains it
|
// The attribute assignment inherits the reachability of the method which contains it
|
||||||
|
@ -2015,6 +2055,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
|
|
||||||
let declarations = use_def.end_of_scope_declarations(place_id);
|
let declarations = use_def.end_of_scope_declarations(place_id);
|
||||||
let declared_and_qualifiers = place_from_declarations(db, declarations);
|
let declared_and_qualifiers = place_from_declarations(db, declarations);
|
||||||
|
|
||||||
match declared_and_qualifiers {
|
match declared_and_qualifiers {
|
||||||
Ok(PlaceAndQualifiers {
|
Ok(PlaceAndQualifiers {
|
||||||
place: mut declared @ Place::Type(declared_ty, declaredness),
|
place: mut declared @ Place::Type(declared_ty, declaredness),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue