[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:
Charlie Marsh
2025-12-13 17:10:25 -05:00
committed by GitHub
parent a544c59186
commit be8eb92946
3 changed files with 172 additions and 3 deletions

View File

@@ -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,

View File

@@ -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, || {