1+ use std:: sync:: { LazyLock , Mutex } ;
2+
13use crate :: {
24 module_resolver:: file_to_module,
35 semantic_index:: {
@@ -18,6 +20,7 @@ use indexmap::IndexSet;
1820use itertools:: Itertools as _;
1921use ruff_db:: files:: File ;
2022use ruff_python_ast:: { self as ast, PythonVersion } ;
23+ use rustc_hash:: FxHashSet ;
2124
2225use super :: {
2326 class_base:: ClassBase , infer_expression_type, infer_unpack_types, IntersectionBuilder ,
@@ -185,6 +188,14 @@ impl<'db> Class<'db> {
185188 . unwrap_or_else ( |_| SubclassOfType :: subclass_of_unknown ( ) )
186189 }
187190
191+ /// Return a type representing "the set of all instances of the metaclass of this class".
192+ pub ( super ) fn metaclass_instance_type ( self , db : & ' db dyn Db ) -> Type < ' db > {
193+ self
194+ . metaclass ( db)
195+ . to_instance ( db)
196+ . expect ( "`Type::to_instance()` should always return `Some()` when called on the type of a metaclass" )
197+ }
198+
188199 /// Return the metaclass of this class, or an error if the metaclass cannot be inferred.
189200 #[ salsa:: tracked]
190201 pub ( super ) fn try_metaclass ( self , db : & ' db dyn Db ) -> Result < Type < ' db > , MetaclassError < ' db > > {
@@ -879,7 +890,7 @@ impl<'db> KnownClass {
879890 }
880891 }
881892
882- pub ( crate ) fn as_str ( self , db : & ' db dyn Db ) -> & ' static str {
893+ pub ( crate ) fn name ( self , db : & ' db dyn Db ) -> & ' static str {
883894 match self {
884895 Self :: Bool => "bool" ,
885896 Self :: Object => "object" ,
@@ -937,17 +948,101 @@ impl<'db> KnownClass {
937948 }
938949 }
939950
951+ fn display ( self , db : & ' db dyn Db ) -> impl std:: fmt:: Display + ' db {
952+ struct KnownClassDisplay < ' db > {
953+ db : & ' db dyn Db ,
954+ class : KnownClass ,
955+ }
956+
957+ impl std:: fmt:: Display for KnownClassDisplay < ' _ > {
958+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
959+ let KnownClassDisplay {
960+ class : known_class,
961+ db,
962+ } = * self ;
963+ write ! (
964+ f,
965+ "{module}.{class}" ,
966+ module = known_class. canonical_module( db) ,
967+ class = known_class. name( db)
968+ )
969+ }
970+ }
971+
972+ KnownClassDisplay { db, class : self }
973+ }
974+
975+ /// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
976+ /// representing all possible instances of the class.
977+ ///
978+ /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
940979 pub ( crate ) fn to_instance ( self , db : & ' db dyn Db ) -> Type < ' db > {
941- self . to_class_literal ( db) . to_instance ( db)
980+ self . to_class_literal ( db)
981+ . into_class_literal ( )
982+ . map ( |ClassLiteralType { class } | Type :: instance ( class) )
983+ . unwrap_or_else ( Type :: unknown)
942984 }
943985
986+ /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
987+ ///
988+ /// Return an error if the symbol cannot be found in the expected typeshed module,
989+ /// or if the symbol is not a class definition, or if the symbol is possibly unbound.
990+ pub ( crate ) fn try_to_class_literal (
991+ self ,
992+ db : & ' db dyn Db ,
993+ ) -> Result < ClassLiteralType < ' db > , KnownClassLookupError < ' db > > {
994+ let symbol = known_module_symbol ( db, self . canonical_module ( db) , self . name ( db) ) . symbol ;
995+ match symbol {
996+ Symbol :: Type ( Type :: ClassLiteral ( class_type) , Boundness :: Bound ) => Ok ( class_type) ,
997+ Symbol :: Type ( Type :: ClassLiteral ( class_type) , Boundness :: PossiblyUnbound ) => {
998+ Err ( KnownClassLookupError :: ClassPossiblyUnbound { class_type } )
999+ }
1000+ Symbol :: Type ( found_type, _) => {
1001+ Err ( KnownClassLookupError :: SymbolNotAClass { found_type } )
1002+ }
1003+ Symbol :: Unbound => Err ( KnownClassLookupError :: ClassNotFound ) ,
1004+ }
1005+ }
1006+
1007+ /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
1008+ ///
1009+ /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
9441010 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 ( ) )
1011+ // a cache of the `KnownClass`es that we have already failed to lookup in typeshed
1012+ // (and therefore that we've already logged a warning for)
1013+ static MESSAGES : LazyLock < Mutex < FxHashSet < KnownClass > > > = LazyLock :: new ( Mutex :: default) ;
1014+
1015+ self . try_to_class_literal ( db)
1016+ . map ( Type :: ClassLiteral )
1017+ . unwrap_or_else ( |lookup_error| {
1018+ if MESSAGES . lock ( ) . unwrap ( ) . insert ( self ) {
1019+ if matches ! (
1020+ lookup_error,
1021+ KnownClassLookupError :: ClassPossiblyUnbound { .. }
1022+ ) {
1023+ tracing:: info!( "{}" , lookup_error. display( db, self ) ) ;
1024+ } else {
1025+ tracing:: info!(
1026+ "{}. Falling back to `Unknown` for the symbol instead." ,
1027+ lookup_error. display( db, self )
1028+ ) ;
1029+ }
1030+ }
1031+
1032+ match lookup_error {
1033+ KnownClassLookupError :: ClassPossiblyUnbound { class_type, .. } => {
1034+ Type :: class_literal ( class_type. class )
1035+ }
1036+ KnownClassLookupError :: ClassNotFound { .. }
1037+ | KnownClassLookupError :: SymbolNotAClass { .. } => Type :: unknown ( ) ,
1038+ }
1039+ } )
9491040 }
9501041
1042+ /// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
1043+ /// representing that class and all possible subclasses of the class.
1044+ ///
1045+ /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
9511046 pub ( crate ) fn to_subclass_of ( self , db : & ' db dyn Db ) -> Type < ' db > {
9521047 self . to_class_literal ( db)
9531048 . into_class_literal ( )
@@ -958,11 +1053,8 @@ impl<'db> KnownClass {
9581053 /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
9591054 /// *and* `class` is a subclass of `other`.
9601055 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) )
1056+ self . try_to_class_literal ( db)
1057+ . is_ok_and ( |ClassLiteralType { class } | class. is_subclass_of ( db, other) )
9661058 }
9671059
9681060 /// Return the module in which we should look up the definition for this class
@@ -1227,6 +1319,62 @@ impl<'db> KnownClass {
12271319 }
12281320}
12291321
1322+ /// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail.
1323+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
1324+ pub ( crate ) enum KnownClassLookupError < ' db > {
1325+ /// There is no symbol by that name in the expected typeshed module.
1326+ ClassNotFound ,
1327+ /// There is a symbol by that name in the expected typeshed module,
1328+ /// but it's not a class.
1329+ SymbolNotAClass { found_type : Type < ' db > } ,
1330+ /// There is a symbol by that name in the expected typeshed module,
1331+ /// and it's a class definition, but it's possibly unbound.
1332+ ClassPossiblyUnbound { class_type : ClassLiteralType < ' db > } ,
1333+ }
1334+
1335+ impl < ' db > KnownClassLookupError < ' db > {
1336+ fn display ( & self , db : & ' db dyn Db , class : KnownClass ) -> impl std:: fmt:: Display + ' db {
1337+ struct ErrorDisplay < ' db > {
1338+ db : & ' db dyn Db ,
1339+ class : KnownClass ,
1340+ error : KnownClassLookupError < ' db > ,
1341+ }
1342+
1343+ impl std:: fmt:: Display for ErrorDisplay < ' _ > {
1344+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
1345+ let ErrorDisplay { db, class, error } = * self ;
1346+
1347+ let class = class. display ( db) ;
1348+ let python_version = Program :: get ( db) . python_version ( db) ;
1349+
1350+ match error {
1351+ KnownClassLookupError :: ClassNotFound => write ! (
1352+ f,
1353+ "Could not find class `{class}` in typeshed on Python {python_version}" ,
1354+ ) ,
1355+ KnownClassLookupError :: SymbolNotAClass { found_type } => write ! (
1356+ f,
1357+ "Error looking up `{class}` in typeshed: expected to find a class definition \
1358+ on Python {python_version}, but found a symbol of type `{found_type}` instead",
1359+ found_type = found_type. display( db) ,
1360+ ) ,
1361+ KnownClassLookupError :: ClassPossiblyUnbound { .. } => write ! (
1362+ f,
1363+ "Error looking up `{class}` in typeshed on Python {python_version}: \
1364+ expected to find a fully bound symbol, but found one that is possibly unbound",
1365+ )
1366+ }
1367+ }
1368+ }
1369+
1370+ ErrorDisplay {
1371+ db,
1372+ class,
1373+ error : * self ,
1374+ }
1375+ }
1376+ }
1377+
12301378/// Enumeration of specific runtime that are special enough to be considered their own type.
12311379#[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash , salsa:: Update ) ]
12321380pub enum KnownInstanceType < ' db > {
@@ -1609,7 +1757,7 @@ mod tests {
16091757 fn known_class_roundtrip_from_str ( ) {
16101758 let db = setup_db ( ) ;
16111759 for class in KnownClass :: iter ( ) {
1612- let class_name = class. as_str ( & db) ;
1760+ let class_name = class. name ( & db) ;
16131761 let class_module = resolve_module ( & db, & class. canonical_module ( & db) . name ( ) ) . unwrap ( ) ;
16141762
16151763 assert_eq ! (
0 commit comments