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 ,
@@ -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 ) ]
11691269pub enum KnownInstanceType < ' db > {
@@ -1539,7 +1639,7 @@ pub(super) enum MetaclassErrorKind<'db> {
15391639#[ cfg( test) ]
15401640mod 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-\n types: 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}
0 commit comments