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+ if matches ! (
917+ lookup_error,
918+ KnownClassLookupError :: ClassPossiblyUnbound { .. }
919+ ) {
920+ tracing:: warn!( "{}" , lookup_error. display( db, self ) ) ;
921+ } else {
922+ tracing:: warn!(
923+ "{}. Falling back to `Unknown` for the symbol instead." ,
924+ lookup_error. display( db, self )
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,57 @@ 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+ "Could not find class `{module}.{class}` in typeshed on Python {python_version}" ,
1242+ ) ,
1243+ KnownClassLookupError :: SymbolNotAClass { found_type } => write ! (
1244+ f,
1245+ "Error looking up `{module}.{class}` in typeshed: expected to find a class definition \
1246+ on Python {python_version}, but found a symbol of type `{found_type}` instead",
1247+ found_type = found_type. display( db) ,
1248+ ) ,
1249+ KnownClassLookupError :: ClassPossiblyUnbound { .. } => write ! (
1250+ f,
1251+ "Error looking up `{module}.{class}` in typeshed on Python {python_version}: \
1252+ expected to find a fully bound symbol, but found one that is possibly unbound",
1253+ )
1254+ }
1255+ }
1256+ }
1257+
1258+ ErrorDisplay {
1259+ db,
1260+ class,
1261+ error : * self ,
1262+ }
1263+ }
1264+ }
1265+
11671266/// Enumeration of specific runtime that are special enough to be considered their own type.
11681267#[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash , salsa:: Update ) ]
11691268pub enum KnownInstanceType < ' db > {
@@ -1539,7 +1638,7 @@ pub(super) enum MetaclassErrorKind<'db> {
15391638#[ cfg( test) ]
15401639mod tests {
15411640 use super :: * ;
1542- use crate :: db:: tests:: setup_db;
1641+ use crate :: db:: tests:: { setup_db, TestDb , TestDbBuilder } ;
15431642 use crate :: module_resolver:: resolve_module;
15441643 use strum:: IntoEnumIterator ;
15451644
@@ -1557,4 +1656,35 @@ mod tests {
15571656 ) ;
15581657 }
15591658 }
1659+
1660+ fn setup_db_with_broken_typeshed ( builtins_file : & str ) -> TestDb {
1661+ TestDbBuilder :: new ( )
1662+ . with_custom_typeshed ( "/typeshed" )
1663+ . with_file ( "/typeshed/stdlib/builtins.pyi" , builtins_file)
1664+ . with_file ( "/typeshed/stdlib/types.pyi" , "class ModuleType: ..." )
1665+ . with_file ( "/typeshed/stdlib/VERSIONS" , "builtins: 3.8-\n types: 3.8-" )
1666+ . build ( )
1667+ . unwrap ( )
1668+ }
1669+
1670+ #[ test]
1671+ #[ should_panic( expected = "Could not find class `builtins.int` in typeshed" ) ]
1672+ fn known_class_to_class_literal_panics_with_test_feature_enabled ( ) {
1673+ let db = setup_db_with_broken_typeshed ( "class object: ..." ) ;
1674+ KnownClass :: Int . to_class_literal ( & db) ;
1675+ }
1676+
1677+ #[ test]
1678+ #[ should_panic( expected = "Could not find class `builtins.int` in typeshed" ) ]
1679+ fn known_class_to_instance_panics_with_test_feature_enabled ( ) {
1680+ let db = setup_db_with_broken_typeshed ( "class object: ..." ) ;
1681+ KnownClass :: Int . to_instance ( & db) ;
1682+ }
1683+
1684+ #[ test]
1685+ #[ should_panic( expected = "found a symbol of type `Unknown | Literal[42]` instead" ) ]
1686+ fn known_class_to_subclass_of_panics_with_test_feature_enabled ( ) {
1687+ let db = setup_db_with_broken_typeshed ( "int = 42" ) ;
1688+ KnownClass :: Int . to_subclass_of ( & db) ;
1689+ }
15601690}
0 commit comments