1
+ use std:: sync:: { LazyLock , Mutex } ;
2
+
1
3
use crate :: {
2
4
module_resolver:: file_to_module,
3
5
semantic_index:: {
@@ -18,6 +20,7 @@ use indexmap::IndexSet;
18
20
use itertools:: Itertools as _;
19
21
use ruff_db:: files:: File ;
20
22
use ruff_python_ast:: { self as ast, PythonVersion } ;
23
+ use rustc_hash:: FxHashSet ;
21
24
22
25
use super :: {
23
26
class_base:: ClassBase , infer_expression_type, infer_unpack_types, IntersectionBuilder ,
@@ -185,6 +188,14 @@ impl<'db> Class<'db> {
185
188
. unwrap_or_else ( |_| SubclassOfType :: subclass_of_unknown ( ) )
186
189
}
187
190
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
+
188
199
/// Return the metaclass of this class, or an error if the metaclass cannot be inferred.
189
200
#[ salsa:: tracked]
190
201
pub ( super ) fn try_metaclass ( self , db : & ' db dyn Db ) -> Result < Type < ' db > , MetaclassError < ' db > > {
@@ -879,7 +890,7 @@ impl<'db> KnownClass {
879
890
}
880
891
}
881
892
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 {
883
894
match self {
884
895
Self :: Bool => "bool" ,
885
896
Self :: Object => "object" ,
@@ -937,17 +948,101 @@ impl<'db> KnownClass {
937
948
}
938
949
}
939
950
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.
940
979
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)
942
984
}
943
985
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.
944
1010
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
+ } )
949
1040
}
950
1041
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.
951
1046
pub ( crate ) fn to_subclass_of ( self , db : & ' db dyn Db ) -> Type < ' db > {
952
1047
self . to_class_literal ( db)
953
1048
. into_class_literal ( )
@@ -958,11 +1053,8 @@ impl<'db> KnownClass {
958
1053
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
959
1054
/// *and* `class` is a subclass of `other`.
960
1055
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) )
966
1058
}
967
1059
968
1060
/// Return the module in which we should look up the definition for this class
@@ -1227,6 +1319,62 @@ impl<'db> KnownClass {
1227
1319
}
1228
1320
}
1229
1321
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
+
1230
1378
/// Enumeration of specific runtime that are special enough to be considered their own type.
1231
1379
#[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash , salsa:: Update ) ]
1232
1380
pub enum KnownInstanceType < ' db > {
@@ -1609,7 +1757,7 @@ mod tests {
1609
1757
fn known_class_roundtrip_from_str ( ) {
1610
1758
let db = setup_db ( ) ;
1611
1759
for class in KnownClass :: iter ( ) {
1612
- let class_name = class. as_str ( & db) ;
1760
+ let class_name = class. name ( & db) ;
1613
1761
let class_module = resolve_module ( & db, & class. canonical_module ( & db) . name ( ) ) . unwrap ( ) ;
1614
1762
1615
1763
assert_eq ! (
0 commit comments