Skip to content

Commit 149860e

Browse files
committed
[ty] Don't suggest things that aren't subclasses of BaseException after raise
This only applies to items that have a type associated with them. That is, things that are already in scope. For items that don't have a type associated with them (i.e., suggestions from auto-import), we still suggest them since we can't know if they're appropriate or not. It's not quite clear on how best to improve here for the auto-import case. (Short of, say, asking for the type of each such symbol. But the performance implications of that aren't known yet.) Note that because of auto-import, we were still suggesting `NotImplemented` even though astral-sh/ty#1262 specifically cites it as the motivating example that we *shouldn't* suggest. This was occuring because auto-import was including symbols from the `builtins` module, even though those are actually already in scope. So this PR also gets rid of those suggestions from auto-import. Overall, this means that, at least, `raise NotImpl` won't suggest `NotImplemented`. Fixes astral-sh/ty#1262
1 parent a57e291 commit 149860e

File tree

7 files changed

+108
-16
lines changed

7 files changed

+108
-16
lines changed

crates/ty_completion_eval/completion-evaluation-tasks.csv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ numpy-array,main.py,1,1
1818
object-attr-instance-methods,main.py,0,1
1919
object-attr-instance-methods,main.py,1,1
2020
pass-keyword-completion,main.py,0,1
21-
raise-uses-base-exception,main.py,0,2
21+
raise-uses-base-exception,main.py,0,1
2222
scope-existing-over-new-import,main.py,0,1
2323
scope-prioritize-closer,main.py,0,2
2424
scope-simple-long-identifier,main.py,0,1
2525
tstring-completions,main.py,0,1
2626
ty-extensions-lower-stdlib,main.py,0,8
2727
type-var-typing-over-ast,main.py,0,3
28-
type-var-typing-over-ast,main.py,1,279
28+
type-var-typing-over-ast,main.py,1,278

crates/ty_ide/src/completion.rs

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ use ruff_python_ast::name::Name;
99
use ruff_python_codegen::Stylist;
1010
use ruff_python_parser::{Token, TokenAt, TokenKind, Tokens};
1111
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
12+
use ty_python_semantic::types::UnionType;
1213
use ty_python_semantic::{
13-
Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel,
14-
types::{CycleDetector, Type},
14+
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
15+
types::{CycleDetector, KnownClass, Type},
1516
};
1617

1718
use crate::docstring::Docstring;
@@ -82,6 +83,31 @@ impl<'db> Completions<'db> {
8283
fn force_add(&mut self, completion: Completion<'db>) {
8384
self.items.push(completion);
8485
}
86+
87+
/// Tags completions with whether they are known to be usable in
88+
/// a `raise` context.
89+
///
90+
/// It's possible that some completions are usable in a `raise`
91+
/// but aren't marked by this method. That is, false negatives are
92+
/// possible but false positives are not.
93+
fn tag_raisable(&mut self) {
94+
let raisable_type = UnionType::from_elements(
95+
self.db,
96+
[
97+
KnownClass::BaseException.to_subclass_of(self.db),
98+
KnownClass::BaseException.to_instance(self.db),
99+
],
100+
);
101+
for item in &mut self.items {
102+
let Some(ty) = item.ty else { continue };
103+
item.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_type);
104+
}
105+
}
106+
107+
/// Removes any completion that doesn't satisfy the given predicate.
108+
fn retain(&mut self, predicate: impl FnMut(&Completion<'_>) -> bool) {
109+
self.items.retain(predicate);
110+
}
85111
}
86112

87113
impl<'db> Extend<SemanticCompletion<'db>> for Completions<'db> {
@@ -153,6 +179,13 @@ pub struct Completion<'db> {
153179
/// Whether this item only exists for type checking purposes and
154180
/// will be missing at runtime
155181
pub is_type_check_only: bool,
182+
/// Whether this item can definitively be used in a `raise` context.
183+
///
184+
/// Note that this may not always be computed. (i.e., Only computed
185+
/// when we are in a `raise` context.) And also note that if this
186+
/// is `true`, then it's definitively usable in `raise`, but if
187+
/// it's `false`, it _may_ still be usable in `raise`.
188+
pub is_definitively_raisable: bool,
156189
/// The documentation associated with this item, if
157190
/// available.
158191
pub documentation: Option<Docstring>,
@@ -177,6 +210,7 @@ impl<'db> Completion<'db> {
177210
import: None,
178211
builtin: semantic.builtin,
179212
is_type_check_only,
213+
is_definitively_raisable: false,
180214
documentation,
181215
}
182216
}
@@ -257,6 +291,7 @@ impl<'db> Completion<'db> {
257291
import: None,
258292
builtin: false,
259293
is_type_check_only: false,
294+
is_definitively_raisable: false,
260295
documentation: None,
261296
}
262297
}
@@ -271,6 +306,7 @@ impl<'db> Completion<'db> {
271306
import: None,
272307
builtin: true,
273308
is_type_check_only: false,
309+
is_definitively_raisable: false,
274310
documentation: None,
275311
}
276312
}
@@ -364,6 +400,27 @@ pub fn completion<'db>(
364400
}
365401
}
366402

403+
if is_raising_exception(tokens) {
404+
completions.tag_raisable();
405+
406+
// As a special case, and because it's a common footgun, we
407+
// specifically disallow `NotImplemented` in this context.
408+
// `NotImplementedError` should be used instead. So if we can
409+
// definitively detect `NotImplemented`, then we can safely
410+
// omit it from suggestions.
411+
let not_implemented_type = UnionType::from_elements(
412+
db,
413+
[
414+
KnownClass::NotImplementedType.to_subclass_of(db),
415+
KnownClass::NotImplementedType.to_instance(db),
416+
],
417+
);
418+
completions.retain(|item| {
419+
let Some(ty) = item.ty else { return true };
420+
!ty.is_assignable_to(db, not_implemented_type)
421+
});
422+
}
423+
367424
completions.into_completions()
368425
}
369426

@@ -427,7 +484,8 @@ fn add_unimported_completions<'db>(
427484
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
428485

429486
for symbol in all_symbols(db, &completions.query) {
430-
if symbol.module.file(db) == Some(file) {
487+
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
488+
{
431489
continue;
432490
}
433491

@@ -450,6 +508,7 @@ fn add_unimported_completions<'db>(
450508
builtin: false,
451509
// TODO: `is_type_check_only` requires inferring the type of the symbol
452510
is_type_check_only: false,
511+
is_definitively_raisable: false,
453512
documentation: None,
454513
});
455514
}
@@ -1358,6 +1417,30 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
13581417
})
13591418
}
13601419

1420+
/// Returns true when the cursor is after a `raise` keyword.
1421+
fn is_raising_exception(tokens: &[Token]) -> bool {
1422+
/// The maximum number of tokens we're willing to
1423+
/// look-behind to find a `raise` keyword.
1424+
const LIMIT: usize = 10;
1425+
1426+
// This only looks for things like `raise foo.bar.baz.qu<CURSOR>`.
1427+
// Technically, any kind of expression is allowed after `raise`.
1428+
// But we may not always want to treat it specially. So we're
1429+
// rather conservative about what we consider "raising an
1430+
// exception" to be for the purposes of completions. The failure
1431+
// mode here is that we may wind up suggesting things that
1432+
// shouldn't be raised. The benefit is that when this heuristic
1433+
// does work, we won't suggest things that shouldn't be raised.
1434+
for token in tokens.iter().rev().take(LIMIT) {
1435+
match token.kind() {
1436+
TokenKind::Name | TokenKind::Dot => continue,
1437+
TokenKind::Raise => return true,
1438+
_ => return false,
1439+
}
1440+
}
1441+
false
1442+
}
1443+
13611444
/// Order completions according to the following rules:
13621445
///
13631446
/// 1) Names with no underscore prefix
@@ -1370,8 +1453,16 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
13701453
/// This has the effect of putting all dunder attributes after "normal"
13711454
/// attributes, and all single-underscore attributes after dunder attributes.
13721455
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
1373-
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, NameKind, bool, &'a Name) {
1456+
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, bool, NameKind, bool, &'a Name) {
13741457
(
1458+
// This is only true when we are both in a `raise` context
1459+
// *and* we know this suggestion is definitively usable
1460+
// in a `raise` context. So we should sort these before
1461+
// anything else.
1462+
!completion.is_definitively_raisable,
1463+
// When `None`, a completion is for something in the
1464+
// current module, which we should generally prefer over
1465+
// something from outside the module.
13751466
completion.module_name.is_some(),
13761467
// At time of writing (2025-11-11), keyword completions
13771468
// are classified as builtins, which makes them sort after

crates/ty_python_semantic/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ pub use db::Db;
1212
pub use diagnostic::add_inferred_python_version_hint_to_diagnostic;
1313
pub use module_name::{ModuleName, ModuleNameResolutionError};
1414
pub use module_resolver::{
15-
Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules,
16-
resolve_module, resolve_real_module, system_module_search_paths,
15+
KnownModule, Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules,
16+
list_modules, resolve_module, resolve_real_module, system_module_search_paths,
1717
};
1818
pub use program::{
1919
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,

crates/ty_python_semantic/src/module_resolver/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::iter::FusedIterator;
22

33
pub use list::{all_modules, list_modules};
4-
pub(crate) use module::KnownModule;
4+
pub use module::KnownModule;
55
pub use module::Module;
66
pub use path::{SearchPath, SearchPathValidationError};
77
pub use resolver::SearchPaths;

crates/ty_python_semantic/src/module_resolver/module.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl<'db> Module<'db> {
6767
}
6868

6969
/// Does this module represent the given known module?
70-
pub(crate) fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
70+
pub fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
7171
self.known(db) == Some(known_module)
7272
}
7373

crates/ty_python_semantic/src/types.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ use crate::types::variance::VarianceInferable;
7474
use crate::types::visitor::{any_over_type, exceeds_max_specialization_depth};
7575
use crate::unpack::EvaluationMode;
7676
use crate::{Db, FxOrderSet, Module, Program};
77-
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
77+
pub use class::KnownClass;
78+
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias};
7879
use instance::Protocol;
7980
pub use instance::{NominalInstanceType, ProtocolInstanceType};
8081
pub use special_form::SpecialFormType;
@@ -1670,8 +1671,8 @@ impl<'db> Type<'db> {
16701671

16711672
/// Return true if this type is assignable to type `target`.
16721673
///
1673-
/// See [`TypeRelation::Assignability`] for more details.
1674-
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
1674+
/// See `TypeRelation::Assignability` for more details.
1675+
pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
16751676
self.when_assignable_to(db, target, InferableTypeVars::None)
16761677
.is_always_satisfied(db)
16771678
}
@@ -12108,7 +12109,7 @@ impl get_size2::GetSize for UnionType<'_> {}
1210812109
impl<'db> UnionType<'db> {
1210912110
/// Create a union from a list of elements
1211012111
/// (which may be eagerly simplified into a different variant of [`Type`] altogether).
12111-
pub(crate) fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
12112+
pub fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
1211212113
where
1211312114
I: IntoIterator<Item = T>,
1211412115
T: Into<Type<'db>>,

crates/ty_python_semantic/src/types/class.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4743,7 +4743,7 @@ impl KnownClass {
47434743
///
47444744
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
47454745
#[track_caller]
4746-
pub(crate) fn to_instance(self, db: &dyn Db) -> Type<'_> {
4746+
pub fn to_instance(self, db: &dyn Db) -> Type<'_> {
47474747
debug_assert_ne!(
47484748
self,
47494749
KnownClass::Tuple,
@@ -4896,7 +4896,7 @@ impl KnownClass {
48964896
/// representing that class and all possible subclasses of the class.
48974897
///
48984898
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
4899-
pub(crate) fn to_subclass_of(self, db: &dyn Db) -> Type<'_> {
4899+
pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> {
49004900
self.to_class_literal(db)
49014901
.to_class_type(db)
49024902
.map(|class| SubclassOfType::from(db, class))

0 commit comments

Comments
 (0)