[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:
Ivan Yakushev 2025-07-07 14:55:32 +04:00 committed by GitHub
parent b6edfbc70f
commit e0b7f496f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 16 deletions

View file

@ -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.
///
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it

View file

@ -509,6 +509,18 @@ impl<'db> UseDefMap<'db> {
.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(
&self,
db: &dyn crate::Db,

View file

@ -11,7 +11,7 @@ use super::{
};
use crate::semantic_index::definition::{Definition, DefinitionState};
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::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
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_table = place_table(db, class_body_scope);
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);
let is_valid_scope = |method_scope: ScopeId<'db>| {
if let Some(method_def) = method_scope.node(db).as_function(&module) {
let method_name = method_def.name.as_str();
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);
if method_decorator != Ok(target_method_decorator) {
continue;
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;
}
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);
// 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 declared_and_qualifiers = place_from_declarations(db, declarations);
match declared_and_qualifiers {
Ok(PlaceAndQualifiers {
place: mut declared @ Place::Type(declared_ty, declaredness),