[ty] Add support for __qualname__ and other implicit class attributes (#21966)
## Summary Closes https://github.com/astral-sh/ty/issues/1873
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
|
||||
@@ -1633,6 +1634,35 @@ mod implicit_globals {
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up the type of an "implicit class body symbol". Returns [`Place::Undefined`] if
|
||||
/// `name` is not present as an implicit symbol in class bodies.
|
||||
///
|
||||
/// Implicit class body symbols are symbols such as `__qualname__`, `__module__`, `__doc__`,
|
||||
/// and `__firstlineno__` that Python implicitly makes available inside a class body during
|
||||
/// class creation.
|
||||
///
|
||||
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>
|
||||
pub(crate) fn class_body_implicit_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
match name {
|
||||
"__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||
"__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||
// __doc__ is `str` if there's a docstring, `None` if there isn't
|
||||
"__doc__" => Place::bound(UnionType::from_elements(
|
||||
db,
|
||||
[KnownClass::Str.to_instance(db), Type::none(db)],
|
||||
))
|
||||
.into(),
|
||||
// __firstlineno__ was added in Python 3.13
|
||||
"__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => {
|
||||
Place::bound(KnownClass::Int.to_instance(db)).into()
|
||||
}
|
||||
_ => Place::Undefined.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
|
||||
@@ -28,9 +28,9 @@ use crate::module_resolver::{
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::{
|
||||
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
|
||||
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
|
||||
module_type_implicit_global_declaration, module_type_implicit_global_symbol, place,
|
||||
place_from_bindings, place_from_declarations, typing_extensions_symbol,
|
||||
builtins_module_scope, builtins_symbol, class_body_implicit_symbol, explicit_global_symbol,
|
||||
global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_symbol,
|
||||
place, place_from_bindings, place_from_declarations, typing_extensions_symbol,
|
||||
};
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
||||
@@ -9210,6 +9210,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
PlaceAndQualifiers::from(Place::Undefined)
|
||||
// If we're in a class body, check for implicit class body symbols first.
|
||||
// These take precedence over globals.
|
||||
.or_fall_back_to(db, || {
|
||||
if scope.node(db).scope_kind().is_class()
|
||||
&& let Some(symbol) = place_expr.as_symbol()
|
||||
{
|
||||
let implicit = class_body_implicit_symbol(db, symbol.name());
|
||||
if implicit.place.is_definitely_bound() {
|
||||
return implicit.map_type(|ty| {
|
||||
self.narrow_place_with_applicable_constraints(
|
||||
place_expr,
|
||||
ty,
|
||||
&constraint_keys,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
Place::Undefined.into()
|
||||
})
|
||||
// No nonlocal binding? Check the module's explicit globals.
|
||||
// Avoid infinite recursion if `self.scope` already is the module's global scope.
|
||||
.or_fall_back_to(db, || {
|
||||
|
||||
Reference in New Issue
Block a user