@@ -393,14 +393,14 @@ impl<'db> CompletionBuilder<'db> {
393393 if let Some ( ty) = self . ty {
394394 self . is_type_check_only = ty. is_type_check_only ( db) ;
395395 // Tags completions with context-specific if they are
396- // known to be usable in a `raise` context and we have
397- // determined a raisable type `raisable_ty `.
396+ // known to be usable in an exception context and we have
397+ // determined an `exception_ty `.
398398 //
399- // It's possible that some completions are usable in a `raise`
399+ // It's possible that some completions are usable in an exception
400400 // but aren't marked here. That is, false negatives are
401401 // possible but false positives are not.
402- if let Some ( raisable_ty ) = ctx. raisable_ty {
403- self . is_context_specific |= ty. is_assignable_to ( db, raisable_ty ) ;
402+ if let Some ( exception_ty ) = ctx. exception_ty {
403+ self . is_context_specific |= ty. is_assignable_to ( db, exception_ty ) ;
404404 }
405405 if ctx. is_in_class_def {
406406 self . is_context_specific |= ty. is_class_literal ( )
@@ -606,18 +606,10 @@ impl<'m> Context<'m> {
606606 match self . kind {
607607 ContextKind :: Import ( _) => CollectionContext :: none ( ) ,
608608 ContextKind :: NonImport ( _) => {
609- let is_raising_exception = self . cursor . is_raising_exception ( ) ;
609+ let exception_ty = self . cursor . exception_ty ( db ) ;
610610 CollectionContext {
611- raisable_ty : is_raising_exception. then ( || {
612- UnionType :: from_elements (
613- db,
614- [
615- KnownClass :: BaseException . to_subclass_of ( db) ,
616- KnownClass :: BaseException . to_instance ( db) ,
617- ] ,
618- )
619- } ) ,
620- is_raising_exception,
611+ exception_ty,
612+ is_raising_exception : exception_ty. is_some ( ) ,
621613 is_in_class_def : self . cursor . is_in_class_def ( ) ,
622614 valid_keywords : self . cursor . valid_keywords ( ) ,
623615 }
@@ -822,28 +814,47 @@ impl<'m> ContextCursor<'m> {
822814 } )
823815 }
824816
825- /// Returns true when the cursor is after a `raise` keyword.
826- fn is_raising_exception ( & self ) -> bool {
827- /// The maximum number of tokens we're willing to
828- /// look-behind to find a `raise` keyword.
829- const LIMIT : usize = 10 ;
830-
831- // This only looks for things like `raise foo.bar.baz.qu<CURSOR>`.
832- // Technically, any kind of expression is allowed after `raise`.
833- // But we may not always want to treat it specially. So we're
834- // rather conservative about what we consider "raising an
835- // exception" to be for the purposes of completions. The failure
836- // mode here is that we may wind up suggesting things that
837- // shouldn't be raised. The benefit is that when this heuristic
838- // does work, we won't suggest things that shouldn't be raised.
839- for token in self . tokens_before . iter ( ) . rev ( ) . take ( LIMIT ) {
840- match token. kind ( ) {
841- TokenKind :: Name | TokenKind :: Dot => continue ,
842- TokenKind :: Raise => return true ,
843- _ => return false ,
817+ /// Returns the expected type when the cursor sits inside
818+ /// a `raise` or `except` expression.
819+ ///
820+ /// The return value is always `None` if the cursor is not
821+ /// inside a `raise` or `except` context.
822+ fn exception_ty < ' db > ( & self , db : & ' db dyn Db ) -> Option < Type < ' db > > {
823+ let base_exception_ty = KnownClass :: BaseException . to_subclass_of ( db) ;
824+ let base_exception_instance = KnownClass :: BaseException . to_instance ( db) ;
825+ let raise_ty = UnionType :: from_elements ( db, [ base_exception_ty, base_exception_instance] ) ;
826+ let cause_ty = UnionType :: from_elements ( db, [ raise_ty, Type :: none ( db) ] ) ;
827+ let except_ty = UnionType :: from_elements (
828+ db,
829+ [
830+ base_exception_ty,
831+ Type :: homogeneous_tuple ( db, base_exception_ty) ,
832+ ] ,
833+ ) ;
834+
835+ let contains = |expr : & ast:: Expr | expr. range ( ) . contains_range ( self . range ) ;
836+
837+ for node in self . covering_node . ancestors ( ) {
838+ match node {
839+ ast:: AnyNodeRef :: StmtRaise ( stmt) => {
840+ if stmt. exc . as_deref ( ) . is_some_and ( contains) {
841+ return Some ( raise_ty) ;
842+ } else if stmt. cause . as_deref ( ) . is_some_and ( contains) {
843+ return Some ( cause_ty) ;
844+ }
845+ }
846+ ast:: AnyNodeRef :: ExceptHandlerExceptHandler ( handler) => {
847+ if handler. type_ . as_deref ( ) . is_some_and ( contains) {
848+ return Some ( except_ty) ;
849+ }
850+ }
851+ _ => { }
852+ }
853+ if node. is_statement ( ) {
854+ return None ;
844855 }
845856 }
846- false
857+ None
847858 }
848859
849860 /// Returns true when the curser is within the
@@ -1049,12 +1060,11 @@ impl UserQuery {
10491060#[ derive( Clone , Debug , Default ) ]
10501061struct CollectionContext < ' db > {
10511062 /// A pre-computed type corresponding to what is
1052- /// expected for the type of valuables that are
1053- /// raisable.
1063+ /// expected in an exception expression.
10541064 ///
10551065 /// This is only `Some` when `is_raising_exception` is `true`.
1056- raisable_ty : Option < Type < ' db > > ,
1057- /// Whether we're in a `raise <EXPR>` context or not.
1066+ exception_ty : Option < Type < ' db > > ,
1067+ /// Whether we're in an exception context ( `raise` or `except`) or not.
10581068 is_raising_exception : bool ,
10591069 /// Whether we're in a class definition context or not.
10601070 is_in_class_def : bool ,
@@ -8183,6 +8193,62 @@ re.match('', '', fla<CURSOR>
81838193 ) ;
81848194 }
81858195
8196+ // Ideally, we should favour completions that are definitely raisable
8197+ // here. However, doing so would require `exception_ty` to fall back to
8198+ // token matching when AST-matching fails, making the function signficantly
8199+ // more complex. At the time of writing, this trade-off was not
8200+ // considered worthwhile.
8201+ //
8202+ // Note that this only applies to bare raise/raise-from statements with no
8203+ // characters typed after it. It does not apply to `raise A<CURSOR>`.
8204+ #[ test]
8205+ fn empty_raise_statement ( ) {
8206+ let builder = completion_test_builder (
8207+ r#"
8208+ raise <CURSOR>
8209+ "# ,
8210+ ) ;
8211+ assert_snapshot ! (
8212+ builder. skip_auto_import( ) . skip_builtins( ) . build( ) . snapshot( ) ,
8213+ @r"
8214+ and
8215+ as
8216+ assert
8217+ async
8218+ await
8219+ break
8220+ case
8221+ class
8222+ continue
8223+ def
8224+ del
8225+ elif
8226+ else
8227+ except
8228+ finally
8229+ for
8230+ from
8231+ global
8232+ if
8233+ import
8234+ in
8235+ is
8236+ lambda
8237+ match
8238+ nonlocal
8239+ not
8240+ or
8241+ pass
8242+ raise
8243+ return
8244+ try
8245+ while
8246+ with
8247+ yield
8248+ " ,
8249+ ) ;
8250+ }
8251+
81868252 /// A way to create a simple single-file (named `main.py`) completion test
81878253 /// builder.
81888254 ///
0 commit comments