Skip to content

Commit 662dbb5

Browse files
committed
[red-knot] Rework Type::to_instance() to return Option<Type>
1 parent 2d1022a commit 662dbb5

File tree

7 files changed

+233
-73
lines changed

7 files changed

+233
-73
lines changed

crates/red_knot_python_semantic/src/db.rs

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

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

crates/red_knot_python_semantic/src/types.rs

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ impl<'db> Type<'db> {
733733
(Type::ClassLiteral(ClassLiteralType { class }), _) => class
734734
.metaclass(db)
735735
.to_instance(db)
736-
.is_subtype_of(db, target),
736+
.is_some_and(|instance_type| instance_type.is_subtype_of(db, target)),
737737

738738
// `type[str]` (== `SubclassOf("str")` in red-knot) describes all possible runtime subclasses
739739
// of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str`
@@ -745,11 +745,9 @@ impl<'db> Type<'db> {
745745
(Type::SubclassOf(subclass_of_ty), _) => subclass_of_ty
746746
.subclass_of()
747747
.into_class()
748-
.is_some_and(|class| {
749-
class
750-
.metaclass(db)
751-
.to_instance(db)
752-
.is_subtype_of(db, target)
748+
.and_then(|class| class.metaclass(db).to_instance(db))
749+
.is_some_and(|metaclass_instance_type| {
750+
metaclass_instance_type.is_subtype_of(db, target)
753751
}),
754752

755753
// For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::Instance(_SpecialForm)`,
@@ -1100,16 +1098,22 @@ impl<'db> Type<'db> {
11001098
ty.bool(db).is_always_true()
11011099
}
11021100

1101+
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
1102+
// so although the type is dynamic we can still determine disjointness in some situations
11031103
(Type::SubclassOf(subclass_of_ty), other)
1104-
| (other, Type::SubclassOf(subclass_of_ty)) => {
1105-
let metaclass_instance_ty = match subclass_of_ty.subclass_of() {
1106-
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
1107-
// so although the type is dynamic we can still determine disjointness in some situations
1108-
ClassBase::Dynamic(_) => KnownClass::Type.to_instance(db),
1109-
ClassBase::Class(class) => class.metaclass(db).to_instance(db),
1110-
};
1111-
other.is_disjoint_from(db, metaclass_instance_ty)
1112-
}
1104+
| (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() {
1105+
ClassBase::Dynamic(_) => {
1106+
KnownClass::Type.to_instance(db).is_disjoint_from(db, other)
1107+
}
1108+
ClassBase::Class(class) => {
1109+
class
1110+
.metaclass(db)
1111+
.to_instance(db)
1112+
.is_some_and(|metaclass_instance_type| {
1113+
metaclass_instance_type.is_disjoint_from(db, other)
1114+
})
1115+
}
1116+
},
11131117

11141118
(Type::KnownInstance(known_instance), Type::Instance(InstanceType { class }))
11151119
| (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => {
@@ -1180,7 +1184,9 @@ impl<'db> Type<'db> {
11801184
!class
11811185
.metaclass(db)
11821186
.to_instance(db)
1183-
.is_subtype_of(db, instance)
1187+
.is_some_and(|metaclass_instance_type| {
1188+
metaclass_instance_type.is_subtype_of(db, instance)
1189+
})
11841190
}
11851191

11861192
(Type::FunctionLiteral(..), Type::Instance(InstanceType { class }))
@@ -2085,18 +2091,16 @@ impl<'db> Type<'db> {
20852091
Type::Callable(_) => Truthiness::AlwaysTrue,
20862092
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
20872093
Type::ClassLiteral(ClassLiteralType { class }) => {
2088-
return class
2089-
.metaclass(db)
2090-
.to_instance(db)
2091-
.try_bool_impl(db, allow_short_circuit);
2094+
if let Some(metaclass_instance_type) = class.metaclass(db).to_instance(db) {
2095+
metaclass_instance_type.try_bool_impl(db, allow_short_circuit)?
2096+
} else {
2097+
Truthiness::Ambiguous
2098+
}
20922099
}
20932100
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
20942101
ClassBase::Dynamic(_) => Truthiness::Ambiguous,
20952102
ClassBase::Class(class) => {
2096-
return class
2097-
.metaclass(db)
2098-
.to_instance(db)
2099-
.try_bool_impl(db, allow_short_circuit);
2103+
Type::class_literal(class).try_bool_impl(db, allow_short_circuit)?
21002104
}
21012105
},
21022106
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
@@ -2919,17 +2923,19 @@ impl<'db> Type<'db> {
29192923
}
29202924

29212925
#[must_use]
2922-
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
2926+
pub fn to_instance(&self, db: &'db dyn Db) -> Option<Type<'db>> {
29232927
match self {
2924-
Type::Dynamic(_) => *self,
2925-
Type::Never => Type::Never,
2926-
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
2927-
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
2928-
ClassBase::Class(class) => Type::instance(class),
2929-
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
2930-
},
2931-
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
2932-
Type::Intersection(_) => todo_type!("Type::Intersection.to_instance()"),
2928+
Type::Dynamic(_) | Type::Never => Some(*self),
2929+
Type::ClassLiteral(ClassLiteralType { class }) => Some(Type::instance(*class)),
2930+
Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()),
2931+
Type::Union(union) => {
2932+
let mut builder = UnionBuilder::new(db);
2933+
for element in union.elements(db) {
2934+
builder = builder.add(element.to_instance(db)?);
2935+
}
2936+
Some(builder.build())
2937+
}
2938+
Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance()")),
29332939
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
29342940
// since they already indicate that the object is an instance of some kind:
29352941
Type::BooleanLiteral(_)
@@ -2945,7 +2951,7 @@ impl<'db> Type<'db> {
29452951
| Type::Tuple(_)
29462952
| Type::LiteralString
29472953
| Type::AlwaysTruthy
2948-
| Type::AlwaysFalsy => Type::unknown(),
2954+
| Type::AlwaysFalsy => None,
29492955
}
29502956
}
29512957

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 144 additions & 16 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,
@@ -938,14 +941,61 @@ impl<'db> KnownClass {
938941
}
939942

940943
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
941-
self.to_class_literal(db).to_instance(db)
944+
self.to_class_literal(db)
945+
.into_class_literal()
946+
.map(|ClassLiteralType { class }| Type::instance(class))
947+
.unwrap_or_else(Type::unknown)
948+
}
949+
950+
pub(crate) fn try_to_class_literal(
951+
self,
952+
db: &'db dyn Db,
953+
) -> Result<ClassLiteralType<'db>, KnownClassLookupError<'db>> {
954+
let symbol = known_module_symbol(db, self.canonical_module(db), self.as_str(db)).symbol;
955+
match symbol {
956+
Symbol::Type(Type::ClassLiteral(class_type), Boundness::Bound) => Ok(class_type),
957+
Symbol::Type(Type::ClassLiteral(class_type), Boundness::PossiblyUnbound) => {
958+
Err(KnownClassLookupError::ClassPossiblyUnbound { class_type })
959+
}
960+
Symbol::Type(found_type, _) => {
961+
Err(KnownClassLookupError::SymbolNotAClass { found_type })
962+
}
963+
Symbol::Unbound => Err(KnownClassLookupError::ClassNotFound),
964+
}
942965
}
943966

944967
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
945-
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
946-
.symbol
947-
.ignore_possibly_unbound()
948-
.unwrap_or(Type::unknown())
968+
// a cache of the `KnownClass`es that we have already failed to lookup in typeshed
969+
// (and therefore that we've already logged a warning for)
970+
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
971+
972+
self.try_to_class_literal(db)
973+
.map(Type::ClassLiteral)
974+
.unwrap_or_else(|lookup_error| {
975+
if cfg!(test) {
976+
panic!("{}", lookup_error.display(db, self));
977+
} else if MESSAGES.lock().unwrap().insert(self) {
978+
if matches!(
979+
lookup_error,
980+
KnownClassLookupError::ClassPossiblyUnbound { .. }
981+
) {
982+
tracing::warn!("{}", lookup_error.display(db, self));
983+
} else {
984+
tracing::warn!(
985+
"{}. Falling back to `Unknown` for the symbol instead.",
986+
lookup_error.display(db, self)
987+
);
988+
}
989+
}
990+
991+
match lookup_error {
992+
KnownClassLookupError::ClassPossiblyUnbound { class_type, .. } => {
993+
Type::class_literal(class_type.class)
994+
}
995+
KnownClassLookupError::ClassNotFound { .. }
996+
| KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(),
997+
}
998+
})
949999
}
9501000

9511001
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> {
@@ -958,11 +1008,8 @@ impl<'db> KnownClass {
9581008
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
9591009
/// *and* `class` is a subclass of `other`.
9601010
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: Class<'db>) -> bool {
961-
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
962-
.symbol
963-
.ignore_possibly_unbound()
964-
.and_then(Type::into_class_literal)
965-
.is_some_and(|ClassLiteralType { class }| class.is_subclass_of(db, other))
1011+
self.try_to_class_literal(db)
1012+
.is_ok_and(|ClassLiteralType { class }| class.is_subclass_of(db, other))
9661013
}
9671014

9681015
/// Return the module in which we should look up the definition for this class
@@ -995,11 +1042,10 @@ impl<'db> KnownClass {
9951042
| Self::MethodWrapperType
9961043
| Self::WrapperDescriptorType => KnownModule::Types,
9971044
Self::NoneType => KnownModule::Typeshed,
998-
Self::SpecialForm
999-
| Self::TypeVar
1000-
| Self::TypeAliasType
1001-
| Self::StdlibAlias
1002-
| Self::SupportsIndex => KnownModule::Typing,
1045+
Self::SpecialForm | Self::TypeVar | Self::StdlibAlias | Self::SupportsIndex => {
1046+
KnownModule::Typing
1047+
}
1048+
Self::TypeAliasType => KnownModule::TypingExtensions,
10031049
Self::NoDefaultType => {
10041050
let python_version = Program::get(db).python_version(db);
10051051

@@ -1228,6 +1274,57 @@ impl<'db> KnownClass {
12281274
}
12291275
}
12301276

1277+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1278+
pub(crate) enum KnownClassLookupError<'db> {
1279+
ClassNotFound,
1280+
SymbolNotAClass { found_type: Type<'db> },
1281+
ClassPossiblyUnbound { class_type: ClassLiteralType<'db> },
1282+
}
1283+
1284+
impl<'db> KnownClassLookupError<'db> {
1285+
fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db {
1286+
struct ErrorDisplay<'db> {
1287+
db: &'db dyn Db,
1288+
class: KnownClass,
1289+
error: KnownClassLookupError<'db>,
1290+
}
1291+
1292+
impl std::fmt::Display for ErrorDisplay<'_> {
1293+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1294+
let ErrorDisplay { db, class, error } = *self;
1295+
1296+
let module = class.canonical_module(db).as_str();
1297+
let class = class.as_str(db);
1298+
let python_version = Program::get(db).python_version(db);
1299+
1300+
match error {
1301+
KnownClassLookupError::ClassNotFound => write!(
1302+
f,
1303+
"Could not find class `{module}.{class}` in typeshed on Python {python_version}",
1304+
),
1305+
KnownClassLookupError::SymbolNotAClass { found_type } => write!(
1306+
f,
1307+
"Error looking up `{module}.{class}` in typeshed: expected to find a class definition \
1308+
on Python {python_version}, but found a symbol of type `{found_type}` instead",
1309+
found_type = found_type.display(db),
1310+
),
1311+
KnownClassLookupError::ClassPossiblyUnbound { .. } => write!(
1312+
f,
1313+
"Error looking up `{module}.{class}` in typeshed on Python {python_version}: \
1314+
expected to find a fully bound symbol, but found one that is possibly unbound",
1315+
)
1316+
}
1317+
}
1318+
}
1319+
1320+
ErrorDisplay {
1321+
db,
1322+
class,
1323+
error: *self,
1324+
}
1325+
}
1326+
}
1327+
12311328
/// Enumeration of specific runtime that are special enough to be considered their own type.
12321329
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
12331330
pub enum KnownInstanceType<'db> {
@@ -1601,7 +1698,7 @@ pub(super) enum MetaclassErrorKind<'db> {
16011698
#[cfg(test)]
16021699
mod tests {
16031700
use super::*;
1604-
use crate::db::tests::setup_db;
1701+
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
16051702
use crate::module_resolver::resolve_module;
16061703
use strum::IntoEnumIterator;
16071704

@@ -1619,4 +1716,35 @@ mod tests {
16191716
);
16201717
}
16211718
}
1719+
1720+
fn setup_db_with_broken_typeshed(builtins_file: &str) -> TestDb {
1721+
TestDbBuilder::new()
1722+
.with_custom_typeshed("/typeshed")
1723+
.with_file("/typeshed/stdlib/builtins.pyi", builtins_file)
1724+
.with_file("/typeshed/stdlib/types.pyi", "class ModuleType: ...")
1725+
.with_file("/typeshed/stdlib/VERSIONS", "builtins: 3.8-\ntypes: 3.8-")
1726+
.build()
1727+
.unwrap()
1728+
}
1729+
1730+
#[test]
1731+
#[should_panic(expected = "Could not find class `builtins.int` in typeshed")]
1732+
fn known_class_to_class_literal_panics_with_test_feature_enabled() {
1733+
let db = setup_db_with_broken_typeshed("class object: ...");
1734+
KnownClass::Int.to_class_literal(&db);
1735+
}
1736+
1737+
#[test]
1738+
#[should_panic(expected = "Could not find class `builtins.int` in typeshed")]
1739+
fn known_class_to_instance_panics_with_test_feature_enabled() {
1740+
let db = setup_db_with_broken_typeshed("class object: ...");
1741+
KnownClass::Int.to_instance(&db);
1742+
}
1743+
1744+
#[test]
1745+
#[should_panic(expected = "found a symbol of type `Unknown | Literal[42]` instead")]
1746+
fn known_class_to_subclass_of_panics_with_test_feature_enabled() {
1747+
let db = setup_db_with_broken_typeshed("int = 42");
1748+
KnownClass::Int.to_subclass_of(&db);
1749+
}
16221750
}

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,7 +1691,10 @@ impl<'db> TypeInferenceBuilder<'db> {
16911691
for element in tuple.elements(self.db()).iter().copied() {
16921692
builder = builder.add(
16931693
if element.is_assignable_to(self.db(), type_base_exception) {
1694-
element.to_instance(self.db())
1694+
element.to_instance(self.db()).expect(
1695+
"`Type::to_instance()` should always return `Some()` \
1696+
if called on a type assignable to `type[BaseException]`",
1697+
)
16951698
} else {
16961699
if let Some(node) = node {
16971700
report_invalid_exception_caught(&self.context, node, element);
@@ -1706,7 +1709,10 @@ impl<'db> TypeInferenceBuilder<'db> {
17061709
} else {
17071710
let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db());
17081711
if node_ty.is_assignable_to(self.db(), type_base_exception) {
1709-
node_ty.to_instance(self.db())
1712+
node_ty.to_instance(self.db()).expect(
1713+
"`Type::to_instance()` should always return `Some()` \
1714+
if called on a type assignable to `type[BaseException]`",
1715+
)
17101716
} else {
17111717
if let Some(node) = node {
17121718
report_invalid_exception_caught(&self.context, node, node_ty);
@@ -2531,7 +2537,7 @@ impl<'db> TypeInferenceBuilder<'db> {
25312537
} = raise;
25322538

25332539
let base_exception_type = KnownClass::BaseException.to_subclass_of(self.db());
2534-
let base_exception_instance = base_exception_type.to_instance(self.db());
2540+
let base_exception_instance = KnownClass::BaseException.to_instance(self.db());
25352541

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

0 commit comments

Comments
 (0)