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 ,
@@ -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 ) ]
12331330pub enum KnownInstanceType < ' db > {
@@ -1601,7 +1698,7 @@ pub(super) enum MetaclassErrorKind<'db> {
16011698#[ cfg( test) ]
16021699mod 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-\n types: 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}
0 commit comments