[ty] IDE: only provide declarations and bindings as completions (#18456)

## Summary

Previously, all symbols where provided as possible completions. In an
example like the following, both `foo` and `f` were suggested as
completions, because `f` itself is a symbol.
```py
foo = 1

f<CURSOR>
```
Similarly, in the following example, `hidden_symbol` was suggested, even
though it is not statically visible:
```py
if 1 + 2 != 3:
    hidden_symbol = 1

hidden_<CURSOR>
```

With the change suggested here, we only use statically visible
declarations and bindings as a source for completions.


## Test Plan

- Updated snapshot tests
- New test for statically hidden definitions
- Added test for star import
This commit is contained in:
David Peter 2025-06-04 16:11:05 +02:00 committed by GitHub
parent 11db567b0b
commit f1883d71a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 126 additions and 86 deletions

View file

@ -127,10 +127,7 @@ f<CURSOR>
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -143,10 +140,7 @@ g<CURSOR>
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
foo
g
");
} }
#[test] #[test]
@ -175,10 +169,7 @@ f<CURSOR>
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -208,7 +199,6 @@ def foo():
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
f
foo foo
foofoo foofoo
"); ");
@ -259,7 +249,6 @@ def foo():
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
f
foo foo
foofoo foofoo
"); ");
@ -276,7 +265,6 @@ def foo():
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
f
foo foo
foofoo foofoo
"); ");
@ -295,7 +283,6 @@ def frob(): ...
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
f
foo foo
foofoo foofoo
frob frob
@ -315,7 +302,6 @@ def frob(): ...
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
f
foo foo
frob frob
"); ");
@ -334,7 +320,6 @@ def frob(): ...
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
f
foo foo
foofoo foofoo
foofoofoo foofoofoo
@ -451,15 +436,10 @@ def frob(): ...
", ",
); );
// It's not totally clear why `for` shows up in the // TODO: it would be good if `bar` was included here, but
// symbol tables of the detected scopes here. My guess // the list comprehension is not yet valid and so we do not
// is that there's perhaps some sub-optimal behavior // detect this as a definition of `bar`.
// here because the list comprehension as written is not assert_snapshot!(test.completions(), @"<No completions found>");
// valid.
assert_snapshot!(test.completions(), @r"
bar
for
");
} }
#[test] #[test]
@ -470,10 +450,7 @@ def frob(): ...
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -484,10 +461,7 @@ def frob(): ...
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -498,10 +472,7 @@ def frob(): ...
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -512,10 +483,7 @@ def frob(): ...
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -526,10 +494,7 @@ def frob(): ...
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"foo");
f
foo
");
} }
#[test] #[test]
@ -602,7 +567,6 @@ class Foo:
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
Foo Foo
b
bar bar
frob frob
quux quux
@ -621,7 +585,6 @@ class Foo:
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
Foo Foo
b
bar bar
quux quux
"); ");
@ -750,7 +713,6 @@ bar(o<CURSOR>
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
bar bar
foo foo
o
"); ");
} }
@ -788,7 +750,6 @@ class C:
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
C C
bar bar
f
foo foo
self self
"); ");
@ -825,7 +786,6 @@ class C:
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @r"
C C
bar bar
f
foo foo
self self
"); ");
@ -854,11 +814,55 @@ print(f\"{some<CURSOR>
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"some_symbol");
print }
some
some_symbol #[test]
"); fn statically_invisible_symbols() {
let test = cursor_test(
"\
if 1 + 2 != 3:
hidden_symbol = 1
hidden_<CURSOR>
",
);
assert_snapshot!(test.completions(), @"<No completions found>");
}
#[test]
fn completions_inside_unreachable_sections() {
let test = cursor_test(
"\
import sys
if sys.platform == \"not-my-current-platform\":
only_available_in_this_branch = 1
on<CURSOR>
",
);
// TODO: ideally, `only_available_in_this_branch` should be available here, but we
// currently make no effort to provide a good IDE experience within sections that
// are unreachable
assert_snapshot!(test.completions(), @"sys");
}
#[test]
fn star_import() {
let test = cursor_test(
"\
from typing import *
Re<CURSOR>
",
);
test.assert_completions_include("Reversible");
// `ReadableBuffer` is a symbol in `typing`, but it is not re-exported
test.assert_completions_do_not_include("ReadableBuffer");
} }
// Ref: https://github.com/astral-sh/ty/issues/572 // Ref: https://github.com/astral-sh/ty/issues/572
@ -921,10 +925,7 @@ Fo<CURSOR> = float
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"Fo");
Fo
float
");
} }
// Ref: https://github.com/astral-sh/ty/issues/572 // Ref: https://github.com/astral-sh/ty/issues/572
@ -999,9 +1000,7 @@ except Type<CURSOR>:
", ",
); );
assert_snapshot!(test.completions(), @r" assert_snapshot!(test.completions(), @"<No completions found>");
Type
");
} }
// Ref: https://github.com/astral-sh/ty/issues/572 // Ref: https://github.com/astral-sh/ty/issues/572
@ -1029,5 +1028,29 @@ def _():
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .join("\n")
} }
#[track_caller]
fn assert_completions_include(&self, expected: &str) {
let completions = completion(&self.db, self.file, self.cursor_offset);
assert!(
completions
.iter()
.any(|completion| completion.label == expected),
"Expected completions to include `{expected}`"
);
}
#[track_caller]
fn assert_completions_do_not_include(&self, unexpected: &str) {
let completions = completion(&self.db, self.file, self.cursor_offset);
assert!(
completions
.iter()
.all(|completion| completion.label != unexpected),
"Expected completions to not include `{unexpected}`",
);
}
} }
} }

View file

@ -10,6 +10,7 @@ use crate::module_resolver::{Module, resolve_module};
use crate::semantic_index::ast_ids::HasScopedExpressionId; use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::semantic_index; use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::FileScopeId; use crate::semantic_index::symbol::FileScopeId;
use crate::types::ide_support::all_declarations_and_bindings;
use crate::types::{Type, binding_type, infer_scope_types}; use crate::types::{Type, binding_type, infer_scope_types};
pub struct SemanticModel<'db> { pub struct SemanticModel<'db> {
@ -66,9 +67,10 @@ impl<'db> SemanticModel<'db> {
}; };
let mut symbols = vec![]; let mut symbols = vec![];
for (file_scope, _) in index.ancestor_scopes(file_scope) { for (file_scope, _) in index.ancestor_scopes(file_scope) {
for symbol in index.symbol_table(file_scope).symbols() { symbols.extend(all_declarations_and_bindings(
symbols.push(symbol.name().clone()); self.db,
} file_scope.to_scope_id(self.db, self.file),
));
} }
symbols symbols
} }

View file

@ -65,7 +65,7 @@ mod diagnostic;
mod display; mod display;
mod function; mod function;
mod generics; mod generics;
mod ide_support; pub(crate) mod ide_support;
mod infer; mod infer;
mod instance; mod instance;
mod mro; mod mro;

View file

@ -8,6 +8,37 @@ use crate::types::{ClassBase, ClassLiteral, KnownClass, Type};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
pub(crate) fn all_declarations_and_bindings<'db>(
db: &'db dyn Db,
scope_id: ScopeId<'db>,
) -> impl Iterator<Item = Name> + 'db {
let use_def_map = use_def_map(db, scope_id);
let symbol_table = symbol_table(db, scope_id);
use_def_map
.all_public_declarations()
.filter_map(move |(symbol_id, declarations)| {
if symbol_from_declarations(db, declarations)
.is_ok_and(|result| !result.symbol.is_unbound())
{
Some(symbol_table.symbol(symbol_id).name().clone())
} else {
None
}
})
.chain(
use_def_map
.all_public_bindings()
.filter_map(move |(symbol_id, bindings)| {
if symbol_from_bindings(db, bindings).is_unbound() {
None
} else {
Some(symbol_table.symbol(symbol_id).name().clone())
}
}),
)
}
struct AllMembers { struct AllMembers {
members: FxHashSet<Name>, members: FxHashSet<Name>,
} }
@ -118,24 +149,8 @@ impl AllMembers {
} }
fn extend_with_declarations_and_bindings(&mut self, db: &dyn Db, scope_id: ScopeId) { fn extend_with_declarations_and_bindings(&mut self, db: &dyn Db, scope_id: ScopeId) {
let use_def_map = use_def_map(db, scope_id); self.members
let symbol_table = symbol_table(db, scope_id); .extend(all_declarations_and_bindings(db, scope_id));
for (symbol_id, declarations) in use_def_map.all_public_declarations() {
if symbol_from_declarations(db, declarations)
.is_ok_and(|result| !result.symbol.is_unbound())
{
self.members
.insert(symbol_table.symbol(symbol_id).name().clone());
}
}
for (symbol_id, bindings) in use_def_map.all_public_bindings() {
if !symbol_from_bindings(db, bindings).is_unbound() {
self.members
.insert(symbol_table.symbol(symbol_id).name().clone());
}
}
} }
fn extend_with_class_members<'db>( fn extend_with_class_members<'db>(