Skip to content

Commit bb7020d

Browse files
committed
[red-knot] Rework Type::to_instance() to return Option<Type>
1 parent 09d0b22 commit bb7020d

File tree

7 files changed

+233
-71
lines changed

7 files changed

+233
-71
lines changed

crates/red_knot_python_semantic/src/db.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ pub(crate) mod tests {
153153
}
154154
}
155155

156+
pub(crate) fn with_custom_typeshed(mut self, path: &'a str) -> Self {
157+
self.custom_typeshed = Some(SystemPathBuf::from(path));
158+
self
159+
}
160+
156161
pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self {
157162
self.python_version = version;
158163
self

crates/red_knot_python_semantic/src/types.rs

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ impl<'db> Type<'db> {
680680
(Type::ClassLiteral(ClassLiteralType { class }), _) => class
681681
.metaclass(db)
682682
.to_instance(db)
683-
.is_subtype_of(db, target),
683+
.is_some_and(|instance_type| instance_type.is_subtype_of(db, target)),
684684

685685
// `type[str]` (== `SubclassOf("str")` in red-knot) describes all possible runtime subclasses
686686
// of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str`
@@ -692,11 +692,9 @@ impl<'db> Type<'db> {
692692
(Type::SubclassOf(subclass_of_ty), _) => subclass_of_ty
693693
.subclass_of()
694694
.into_class()
695-
.is_some_and(|class| {
696-
class
697-
.metaclass(db)
698-
.to_instance(db)
699-
.is_subtype_of(db, target)
695+
.and_then(|class| class.metaclass(db).to_instance(db))
696+
.is_some_and(|metaclass_instance_type| {
697+
metaclass_instance_type.is_subtype_of(db, target)
700698
}),
701699

702700
// For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::Instance(_SpecialForm)`,
@@ -1047,16 +1045,22 @@ impl<'db> Type<'db> {
10471045
ty.bool(db).is_always_true()
10481046
}
10491047

1048+
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
1049+
// so although the type is dynamic we can still determine disjointness in some situations
10501050
(Type::SubclassOf(subclass_of_ty), other)
1051-
| (other, Type::SubclassOf(subclass_of_ty)) => {
1052-
let metaclass_instance_ty = match subclass_of_ty.subclass_of() {
1053-
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
1054-
// so although the type is dynamic we can still determine disjointness in some situations
1055-
ClassBase::Dynamic(_) => KnownClass::Type.to_instance(db),
1056-
ClassBase::Class(class) => class.metaclass(db).to_instance(db),
1057-
};
1058-
other.is_disjoint_from(db, metaclass_instance_ty)
1059-
}
1051+
| (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() {
1052+
ClassBase::Dynamic(_) => {
1053+
KnownClass::Type.to_instance(db).is_disjoint_from(db, other)
1054+
}
1055+
ClassBase::Class(class) => {
1056+
class
1057+
.metaclass(db)
1058+
.to_instance(db)
1059+
.is_some_and(|metaclass_instance_type| {
1060+
metaclass_instance_type.is_disjoint_from(db, other)
1061+
})
1062+
}
1063+
},
10601064

10611065
(Type::KnownInstance(known_instance), Type::Instance(InstanceType { class }))
10621066
| (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => {
@@ -1127,7 +1131,9 @@ impl<'db> Type<'db> {
11271131
!class
11281132
.metaclass(db)
11291133
.to_instance(db)
1130-
.is_subtype_of(db, instance)
1134+
.is_some_and(|metaclass_instance_type| {
1135+
metaclass_instance_type.is_subtype_of(db, instance)
1136+
})
11311137
}
11321138

11331139
(Type::FunctionLiteral(..), Type::Instance(InstanceType { class }))
@@ -1677,18 +1683,16 @@ impl<'db> Type<'db> {
16771683
Type::Callable(_) => Truthiness::AlwaysTrue,
16781684
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
16791685
Type::ClassLiteral(ClassLiteralType { class }) => {
1680-
return class
1681-
.metaclass(db)
1682-
.to_instance(db)
1683-
.try_bool_impl(db, allow_short_circuit);
1686+
if let Some(metaclass_instance_type) = class.metaclass(db).to_instance(db) {
1687+
metaclass_instance_type.try_bool_impl(db, allow_short_circuit)?
1688+
} else {
1689+
Truthiness::Ambiguous
1690+
}
16841691
}
16851692
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
16861693
ClassBase::Dynamic(_) => Truthiness::Ambiguous,
16871694
ClassBase::Class(class) => {
1688-
return class
1689-
.metaclass(db)
1690-
.to_instance(db)
1691-
.try_bool_impl(db, allow_short_circuit);
1695+
Type::class_literal(class).try_bool_impl(db, allow_short_circuit)?
16921696
}
16931697
},
16941698
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
@@ -2427,17 +2431,19 @@ impl<'db> Type<'db> {
24272431
}
24282432

24292433
#[must_use]
2430-
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
2434+
pub fn to_instance(&self, db: &'db dyn Db) -> Option<Type<'db>> {
24312435
match self {
2432-
Type::Dynamic(_) => *self,
2433-
Type::Never => Type::Never,
2434-
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
2435-
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
2436-
ClassBase::Class(class) => Type::instance(class),
2437-
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
2438-
},
2439-
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
2440-
Type::Intersection(_) => todo_type!("Type::Intersection.to_instance()"),
2436+
Type::Dynamic(_) | Type::Never => Some(*self),
2437+
Type::ClassLiteral(ClassLiteralType { class }) => Some(Type::instance(*class)),
2438+
Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()),
2439+
Type::Union(union) => {
2440+
let mut builder = UnionBuilder::new(db);
2441+
for element in union.elements(db) {
2442+
builder = builder.add(element.to_instance(db)?);
2443+
}
2444+
Some(builder.build())
2445+
}
2446+
Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance()")),
24412447
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
24422448
// since they already indicate that the object is an instance of some kind:
24432449
Type::BooleanLiteral(_)
@@ -2453,7 +2459,7 @@ impl<'db> Type<'db> {
24532459
| Type::Tuple(_)
24542460
| Type::LiteralString
24552461
| Type::AlwaysTruthy
2456-
| Type::AlwaysFalsy => Type::unknown(),
2462+
| Type::AlwaysFalsy => None,
24572463
}
24582464
}
24592465

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 145 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::{LazyLock, Mutex};
2+
13
use crate::{
24
module_resolver::file_to_module,
35
semantic_index::{
@@ -18,6 +20,7 @@ use indexmap::IndexSet;
1820
use itertools::Itertools as _;
1921
use ruff_db::files::File;
2022
use ruff_python_ast::{self as ast, PythonVersion};
23+
use rustc_hash::FxHashSet;
2124

2225
use super::{
2326
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
@@ -876,13 +879,61 @@ impl<'db> KnownClass {
876879
}
877880

878881
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
879-
self.to_class_literal(db).to_instance(db)
882+
self.to_class_literal(db)
883+
.into_class_literal()
884+
.map(|ClassLiteralType { class }| Type::instance(class))
885+
.unwrap_or_else(Type::unknown)
886+
}
887+
888+
pub(crate) fn try_to_class_literal(
889+
self,
890+
db: &'db dyn Db,
891+
) -> Result<ClassLiteralType<'db>, KnownClassLookupError<'db>> {
892+
let symbol = known_module_symbol(db, self.canonical_module(db), self.as_str(db));
893+
match symbol {
894+
Symbol::Type(Type::ClassLiteral(class_type), Boundness::Bound) => Ok(class_type),
895+
Symbol::Type(Type::ClassLiteral(class_type), Boundness::PossiblyUnbound) => {
896+
Err(KnownClassLookupError::ClassPossiblyUnbound { class_type })
897+
}
898+
Symbol::Type(found_type, _) => {
899+
Err(KnownClassLookupError::SymbolNotAClass { found_type })
900+
}
901+
Symbol::Unbound => Err(KnownClassLookupError::ClassNotFound),
902+
}
880903
}
881904

882905
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
883-
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
884-
.ignore_possibly_unbound()
885-
.unwrap_or(Type::unknown())
906+
// a cache of the `KnownClass`es that we have already failed to lookup in typeshed
907+
// (and therefore that we've already logged a warning for)
908+
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
909+
910+
self.try_to_class_literal(db)
911+
.map(Type::ClassLiteral)
912+
.unwrap_or_else(|lookup_error| {
913+
if cfg!(test) {
914+
panic!("{}", lookup_error.display(db, self));
915+
} else if MESSAGES.lock().unwrap().insert(self) {
916+
tracing::warn!("{}", lookup_error.display(db, self));
917+
if !matches!(
918+
lookup_error,
919+
KnownClassLookupError::ClassPossiblyUnbound { .. }
920+
) {
921+
tracing::warn!(
922+
"Falling back to `Type::Unknown` for the symbol `{module}.{class}` instead",
923+
module = self.canonical_module(db).as_str(),
924+
class = self.as_str(db)
925+
);
926+
}
927+
}
928+
929+
match lookup_error {
930+
KnownClassLookupError::ClassPossiblyUnbound { class_type, .. } => {
931+
Type::class_literal(class_type.class)
932+
}
933+
KnownClassLookupError::ClassNotFound { .. }
934+
| KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(),
935+
}
936+
})
886937
}
887938

888939
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> {
@@ -895,10 +946,8 @@ impl<'db> KnownClass {
895946
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
896947
/// *and* `class` is a subclass of `other`.
897948
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: Class<'db>) -> bool {
898-
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
899-
.ignore_possibly_unbound()
900-
.and_then(Type::into_class_literal)
901-
.is_some_and(|ClassLiteralType { class }| class.is_subclass_of(db, other))
949+
self.try_to_class_literal(db)
950+
.is_ok_and(|ClassLiteralType { class }| class.is_subclass_of(db, other))
902951
}
903952

904953
/// Return the module in which we should look up the definition for this class
@@ -931,11 +980,10 @@ impl<'db> KnownClass {
931980
| Self::MethodWrapperType
932981
| Self::WrapperDescriptorType => KnownModule::Types,
933982
Self::NoneType => KnownModule::Typeshed,
934-
Self::SpecialForm
935-
| Self::TypeVar
936-
| Self::TypeAliasType
937-
| Self::StdlibAlias
938-
| Self::SupportsIndex => KnownModule::Typing,
983+
Self::SpecialForm | Self::TypeVar | Self::StdlibAlias | Self::SupportsIndex => {
984+
KnownModule::Typing
985+
}
986+
Self::TypeAliasType => KnownModule::TypingExtensions,
939987
Self::NoDefaultType => {
940988
let python_version = Program::get(db).python_version(db);
941989

@@ -1164,6 +1212,58 @@ impl<'db> KnownClass {
11641212
}
11651213
}
11661214

1215+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1216+
pub(crate) enum KnownClassLookupError<'db> {
1217+
ClassNotFound,
1218+
SymbolNotAClass { found_type: Type<'db> },
1219+
ClassPossiblyUnbound { class_type: ClassLiteralType<'db> },
1220+
}
1221+
1222+
impl<'db> KnownClassLookupError<'db> {
1223+
fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db {
1224+
struct ErrorDisplay<'db> {
1225+
db: &'db dyn Db,
1226+
class: KnownClass,
1227+
error: KnownClassLookupError<'db>,
1228+
}
1229+
1230+
impl std::fmt::Display for ErrorDisplay<'_> {
1231+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1232+
let ErrorDisplay { db, class, error } = *self;
1233+
1234+
let module = class.canonical_module(db).as_str();
1235+
let class = class.as_str(db);
1236+
let python_version = Program::get(db).python_version(db);
1237+
1238+
match error {
1239+
KnownClassLookupError::ClassNotFound => write!(
1240+
f,
1241+
"Error looking up `{class}` in typeshed: could not find a symbol by that name in module `{module}` \
1242+
on Python {python_version}",
1243+
),
1244+
KnownClassLookupError::SymbolNotAClass { found_type } => write!(
1245+
f,
1246+
"Error looking up `{class}` in typeshed: expected to find a class by that name in module `{module}` \
1247+
on Python {python_version}, but found a symbol of type `{found_type}` instead",
1248+
found_type = found_type.display(db),
1249+
),
1250+
KnownClassLookupError::ClassPossiblyUnbound { .. } => write!(
1251+
f,
1252+
"Error looking up `{class}` in typeshed: expected to find a fully bound symbol in module `{module}, \
1253+
but found one that is possibly unbound on Python {python_version}",
1254+
)
1255+
}
1256+
}
1257+
}
1258+
1259+
ErrorDisplay {
1260+
db,
1261+
class,
1262+
error: *self,
1263+
}
1264+
}
1265+
}
1266+
11671267
/// Enumeration of specific runtime that are special enough to be considered their own type.
11681268
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
11691269
pub enum KnownInstanceType<'db> {
@@ -1539,7 +1639,7 @@ pub(super) enum MetaclassErrorKind<'db> {
15391639
#[cfg(test)]
15401640
mod tests {
15411641
use super::*;
1542-
use crate::db::tests::setup_db;
1642+
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
15431643
use crate::module_resolver::resolve_module;
15441644
use strum::IntoEnumIterator;
15451645

@@ -1557,4 +1657,35 @@ mod tests {
15571657
);
15581658
}
15591659
}
1660+
1661+
fn setup_db_with_broken_typeshed(builtins_file: &str) -> TestDb {
1662+
TestDbBuilder::new()
1663+
.with_custom_typeshed("/typeshed")
1664+
.with_file("/typeshed/stdlib/builtins.pyi", builtins_file)
1665+
.with_file("/typeshed/stdlib/types.pyi", "class ModuleType: ...")
1666+
.with_file("/typeshed/stdlib/VERSIONS", "builtins: 3.8-\ntypes: 3.8-")
1667+
.build()
1668+
.unwrap()
1669+
}
1670+
1671+
#[test]
1672+
#[should_panic(expected = "could not find a symbol by that name in module `builtins`")]
1673+
fn known_class_to_class_literal_panics_with_test_feature_enabled() {
1674+
let db = setup_db_with_broken_typeshed("class object: ...");
1675+
KnownClass::Int.to_class_literal(&db);
1676+
}
1677+
1678+
#[test]
1679+
#[should_panic(expected = "could not find a symbol by that name in module `builtins`")]
1680+
fn known_class_to_instance_panics_with_test_feature_enabled() {
1681+
let db = setup_db_with_broken_typeshed("class object: ...");
1682+
KnownClass::Int.to_instance(&db);
1683+
}
1684+
1685+
#[test]
1686+
#[should_panic(expected = "found a symbol of type `Unknown | Literal[42]` instead")]
1687+
fn known_class_to_subclass_of_panics_with_test_feature_enabled() {
1688+
let db = setup_db_with_broken_typeshed("int = 42");
1689+
KnownClass::Int.to_subclass_of(&db);
1690+
}
15601691
}

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,7 +1751,10 @@ impl<'db> TypeInferenceBuilder<'db> {
17511751
for element in tuple.elements(self.db()).iter().copied() {
17521752
builder = builder.add(
17531753
if element.is_assignable_to(self.db(), type_base_exception) {
1754-
element.to_instance(self.db())
1754+
element.to_instance(self.db()).expect(
1755+
"`Type::to_instance()` should always return `Some()` \
1756+
if called on a type assignable to `type[BaseException]`",
1757+
)
17551758
} else {
17561759
if let Some(node) = node {
17571760
report_invalid_exception_caught(&self.context, node, element);
@@ -1766,7 +1769,10 @@ impl<'db> TypeInferenceBuilder<'db> {
17661769
} else {
17671770
let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db());
17681771
if node_ty.is_assignable_to(self.db(), type_base_exception) {
1769-
node_ty.to_instance(self.db())
1772+
node_ty.to_instance(self.db()).expect(
1773+
"`Type::to_instance()` should always return `Some()` \
1774+
if called on a type assignable to `type[BaseException]`",
1775+
)
17701776
} else {
17711777
if let Some(node) = node {
17721778
report_invalid_exception_caught(&self.context, node, node_ty);
@@ -2535,7 +2541,7 @@ impl<'db> TypeInferenceBuilder<'db> {
25352541
} = raise;
25362542

25372543
let base_exception_type = KnownClass::BaseException.to_subclass_of(self.db());
2538-
let base_exception_instance = base_exception_type.to_instance(self.db());
2544+
let base_exception_instance = KnownClass::BaseException.to_instance(self.db());
25392545

25402546
let can_be_raised =
25412547
UnionType::from_elements(self.db(), [base_exception_type, base_exception_instance]);

0 commit comments

Comments
 (0)