Skip to content

Commit 1ff062d

Browse files
authored
[ty] Improve completion rankings for raise-from/except contexts (#22775)
1 parent 7e408a5 commit 1ff062d

File tree

4 files changed

+115
-41
lines changed

4 files changed

+115
-41
lines changed

crates/ty_completion_eval/completion-evaluation-tasks.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ object-attr-instance-methods,main.py,0,1
2727
object-attr-instance-methods,main.py,1,1
2828
pass-keyword-completion,main.py,0,1
2929
raise-uses-base-exception,main.py,0,1
30+
raise-uses-base-exception,main.py,1,1
31+
raise-uses-base-exception,main.py,2,1
3032
scope-existing-over-new-import,main.py,0,1
3133
scope-prioritize-closer,main.py,0,2
3234
scope-simple-long-identifier,main.py,0,1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
# ref: https://github.com/astral-sh/ty/issues/1262
22
raise NotImplement<CURSOR: NotImplementedError>
3+
4+
raise AssertionError from NotImplement<CURSOR: NotImplementedError>
5+
6+
try:
7+
raise AssertionError("invalid")
8+
except NotImplement<CURSOR: NotImplementedError>

crates/ty_ide/src/completion.rs

Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
10501061
struct 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
///

crates/ty_python_semantic/src/types/instance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl<'db> Type<'db> {
8181
Type::tuple_instance(tuple)
8282
}
8383

84-
pub(crate) fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self {
84+
pub fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self {
8585
Type::tuple_instance(TupleType::homogeneous(db, element))
8686
}
8787

0 commit comments

Comments
 (0)