mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Retain typing-only symbols in explicit completions (#27435)
## Summary
We now exclude private type variables, parameter specifications,
type-variable tuples, aliases, and `@type_check_only` definitions from
completions, even though those names can be explicitly imported in
typing-only contexts:
```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from package import _Alias, _T
```
This PR makes those definitions consistent with existing
`@type_check_only` completion behavior: retain them as explicit import
and attribute completions, mark them as typing-only, and rank them below
runtime values.
This commit is contained in:
1 parent
b56bb12ea6
commit
d82ee58c34
4 files changed
+161
-40
No files matched your search
@@ -453,6 +453,7 @@ impl<'db> CompletionBuilder<'db> {
|
||||
Completion::builder(semantic.name)
|
||||
.ty(semantic.ty)
|
||||
.builtin(semantic.builtin)
|
||||
.type_check_only(semantic.is_type_check_only)
|
||||
.docstring(documentation)
|
||||
}
|
||||
|
||||
@@ -492,7 +493,7 @@ impl<'db> CompletionBuilder<'db> {
|
||||
query: &UserQuery,
|
||||
) -> Completion<'db> {
|
||||
if let Some(ty) = self.ty {
|
||||
self.is_type_check_only = ty.is_type_check_only(db);
|
||||
self.is_type_check_only |= ty.is_type_check_only(db);
|
||||
// Tags completions with context-specific if they are
|
||||
// known to be usable in an exception context and we have
|
||||
// determined an `exception_ty`.
|
||||
@@ -611,6 +612,11 @@ impl<'db> CompletionBuilder<'db> {
|
||||
self
|
||||
}
|
||||
|
||||
fn type_check_only(mut self, yes: bool) -> CompletionBuilder<'db> {
|
||||
self.is_type_check_only = yes;
|
||||
self
|
||||
}
|
||||
|
||||
fn context_specific(mut self, yes: bool) -> CompletionBuilder<'db> {
|
||||
self.is_context_specific = yes;
|
||||
self
|
||||
@@ -3609,25 +3615,95 @@ if TYPE_CHECKING:
|
||||
test.contains("__mangled_name");
|
||||
test.contains("__dunder_name__");
|
||||
test.contains("public_type_var");
|
||||
test.not_contains("_private_type_var");
|
||||
test.not_contains("__mangled_type_var");
|
||||
test.contains("public_param_spec");
|
||||
test.not_contains("_private_param_spec");
|
||||
test.contains("public_type_var_tuple");
|
||||
test.not_contains("_private_type_var_tuple");
|
||||
test.contains("public_explicit_type_alias");
|
||||
test.not_contains("_private_explicit_type_alias");
|
||||
test.contains("public_implicit_union_alias");
|
||||
test.not_contains("_private_implicit_union_alias");
|
||||
test.contains("_private_runtime_union");
|
||||
test.contains("_private_runtime_typevar");
|
||||
test.contains("_private_precise_runtime_union");
|
||||
test.contains("PublicProtocol");
|
||||
test.contains("_PrivateProtocol");
|
||||
test.not_contains("PublicTypeOnlyProtocol");
|
||||
test.not_contains("_PrivateTypeOnlyProtocol");
|
||||
test.not_contains("PublicTypeCheckingProtocol");
|
||||
test.not_contains("_PrivateTypeCheckingProtocol");
|
||||
|
||||
for name in [
|
||||
"_private_type_var",
|
||||
"__mangled_type_var",
|
||||
"_private_param_spec",
|
||||
"_private_type_var_tuple",
|
||||
"_private_explicit_type_alias",
|
||||
"_private_implicit_union_alias",
|
||||
"PublicTypeOnlyProtocol",
|
||||
"_PrivateTypeOnlyProtocol",
|
||||
"PublicTypeCheckingProtocol",
|
||||
"_PrivateTypeCheckingProtocol",
|
||||
] {
|
||||
assert!(
|
||||
test.completions()
|
||||
.iter()
|
||||
.any(|completion| completion.name == name && completion.is_type_check_only),
|
||||
"Expected `{name}` to be marked as typing-only",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_stub_symbols_rank_below_runtime_values() {
|
||||
let builder = CursorTest::builder()
|
||||
.source(
|
||||
"package/__init__.pyi",
|
||||
"from typing import TypeVar\n_Alpha = TypeVar(\"_Alpha\")\n_Zeta = 1",
|
||||
)
|
||||
.source("main.py", "import package; package._<CURSOR>")
|
||||
.completion_test_builder()
|
||||
.filter(|completion| matches!(completion.name.as_str(), "_Alpha" | "_Zeta"));
|
||||
|
||||
let test = builder.build();
|
||||
let completions = test
|
||||
.completions()
|
||||
.iter()
|
||||
.map(|completion| (completion.name.as_str(), completion.is_type_check_only))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(completions, [("_Zeta", false), ("_Alpha", true)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_checking_import_includes_private_stub_symbols() {
|
||||
let builder = CursorTest::builder()
|
||||
.source(
|
||||
"package/__init__.pyi",
|
||||
"from typing import TypeAlias, TypeVar\n_Alias: TypeAlias = int\n_T = TypeVar(\"_T\")",
|
||||
)
|
||||
.source(
|
||||
"main.py",
|
||||
"from typing import TYPE_CHECKING\nif TYPE_CHECKING:\n from package import _<CURSOR>",
|
||||
)
|
||||
.completion_test_builder();
|
||||
|
||||
let test = builder.build();
|
||||
for name in ["_Alias", "_T"] {
|
||||
assert!(
|
||||
test.completions()
|
||||
.iter()
|
||||
.any(|completion| completion.name == name && completion.is_type_check_only),
|
||||
"Expected `{name}` to be available as a typing-only completion",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_only_project_builtins_not_suggested_implicitly() {
|
||||
let builder = CursorTest::builder()
|
||||
.source(
|
||||
"__builtins__.pyi",
|
||||
"from typing import TypeVar\n_typing_only = TypeVar(\"_typing_only\")\n_runtime: int",
|
||||
)
|
||||
.source("main.py", "_<CURSOR>")
|
||||
.completion_test_builder()
|
||||
.skip_auto_import();
|
||||
|
||||
let test = builder.build();
|
||||
test.contains("_runtime").not_contains("_typing_only");
|
||||
}
|
||||
|
||||
/// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names
|
||||
|
||||
@@ -675,6 +675,32 @@ static_assert(has_member(module, "evaluate"))
|
||||
static_assert(not has_member(module, "Optional"))
|
||||
```
|
||||
|
||||
### Private typing-only stub members
|
||||
|
||||
Typing-only helpers in stubs remain available as module members for autocomplete.
|
||||
|
||||
`module.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import TypeAlias, TypeVar
|
||||
|
||||
_Alias: TypeAlias = int
|
||||
_T = TypeVar("_T")
|
||||
_runtime: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import module
|
||||
from ty_extensions import static_assert
|
||||
from ty_extensions._internal import has_member
|
||||
|
||||
static_assert(has_member(module, "_runtime"))
|
||||
static_assert(has_member(module, "_Alias"))
|
||||
static_assert(has_member(module, "_T"))
|
||||
```
|
||||
|
||||
## Conditionally available members
|
||||
|
||||
Some members are only conditionally available. For example, `bytearray.take_bytes` was only
|
||||
|
||||
@@ -18,7 +18,7 @@ use ty_module_resolver::{
|
||||
use crate::Db;
|
||||
use crate::place::implicit_globals::all_implicit_module_globals;
|
||||
use crate::types::ide_support::{ImportAliasResolution, definition_for_name};
|
||||
use crate::types::list_members::{Member, all_members, all_reachable_members};
|
||||
use crate::types::list_members::{all_members, all_reachable_members};
|
||||
use crate::types::{
|
||||
CycleDetector, ProgramEnvironment, SpecialFormType, Type, TypeQualifiers, binding_type,
|
||||
infer_complete_scope_types, inferred_declaration,
|
||||
@@ -169,6 +169,7 @@ impl<'db> SemanticModel<'db> {
|
||||
name: CompactString::new(module.name(self.db).as_str()),
|
||||
ty: Some(ty),
|
||||
builtin,
|
||||
is_type_check_only: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -239,11 +240,12 @@ impl<'db> SemanticModel<'db> {
|
||||
clippy::iter_over_hash_type,
|
||||
reason = "completion order is determined later by relevance ranking"
|
||||
)]
|
||||
for Member { name, ty } in all_members(db, &self.program_environment(), ty) {
|
||||
for member in all_members(db, &self.program_environment(), ty) {
|
||||
completions.push(Completion {
|
||||
name: CompactString::new(name),
|
||||
ty: Some(ty),
|
||||
name: CompactString::new(member.name),
|
||||
ty: Some(member.ty),
|
||||
builtin,
|
||||
is_type_check_only: member.is_type_check_only,
|
||||
});
|
||||
}
|
||||
completions.extend(self.submodule_completions(&module));
|
||||
@@ -262,6 +264,7 @@ impl<'db> SemanticModel<'db> {
|
||||
name: CompactString::new(base),
|
||||
ty: Some(ty),
|
||||
builtin,
|
||||
is_type_check_only: false,
|
||||
});
|
||||
}
|
||||
completions
|
||||
@@ -280,6 +283,7 @@ impl<'db> SemanticModel<'db> {
|
||||
name: CompactString::new(member.name),
|
||||
ty: Some(member.ty),
|
||||
builtin: false,
|
||||
is_type_check_only: member.is_type_check_only,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -304,6 +308,7 @@ impl<'db> SemanticModel<'db> {
|
||||
name: CompactString::new(memberdef.member.name),
|
||||
ty: Some(memberdef.member.ty),
|
||||
builtin: false,
|
||||
is_type_check_only: memberdef.member.is_type_check_only,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -318,6 +323,7 @@ impl<'db> SemanticModel<'db> {
|
||||
name: CompactString::new(name),
|
||||
ty: Some(ty),
|
||||
builtin: true,
|
||||
is_type_check_only: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -326,17 +332,24 @@ impl<'db> SemanticModel<'db> {
|
||||
let importing_file =
|
||||
ImportingFile::File(self.file(), self.file.resolver_environment(self.db));
|
||||
if resolve_module(self.db, importing_file, &project_builtins).is_some() {
|
||||
completions.extend(self.module_completions(&project_builtins).into_iter().map(
|
||||
|mut completion| {
|
||||
completion.builtin = true;
|
||||
completion
|
||||
},
|
||||
));
|
||||
completions.extend(
|
||||
self.module_completions(&project_builtins)
|
||||
.into_iter()
|
||||
.filter(|completion| !completion.is_type_check_only)
|
||||
.map(|mut completion| {
|
||||
completion.builtin = true;
|
||||
completion
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Builtins are available in all scopes.
|
||||
let builtins = KnownModule::Builtins.name();
|
||||
completions.extend(self.module_completions(&builtins));
|
||||
completions.extend(
|
||||
self.module_completions(&builtins)
|
||||
.into_iter()
|
||||
.filter(|completion| !completion.is_type_check_only),
|
||||
);
|
||||
|
||||
// The above can sometimes result in duplicates. Get rid of them.
|
||||
completions.sort_by(|c1, c2| c1.name.cmp(&c2.name));
|
||||
@@ -725,6 +738,9 @@ pub struct Completion<'db> {
|
||||
/// use it mainly in tests so that we can write less
|
||||
/// noisy tests.
|
||||
pub builtin: bool,
|
||||
/// Whether this symbol is known to exist only for type checking and should
|
||||
/// be ranked below runtime values.
|
||||
pub is_type_check_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
place_from_declarations,
|
||||
},
|
||||
types::{
|
||||
ClassBase, ClassLiteral, KnownClass, KnownFunction, ProgramEnvironment, StaticClassLiteral,
|
||||
ClassBase, ClassLiteral, KnownClass, ProgramEnvironment, StaticClassLiteral,
|
||||
SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind,
|
||||
exists_at_runtime,
|
||||
},
|
||||
@@ -52,6 +52,7 @@ pub(crate) fn all_end_of_scope_members<'db>(
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
is_type_check_only: false,
|
||||
};
|
||||
Some(MemberWithDefinition {
|
||||
member,
|
||||
@@ -72,6 +73,7 @@ pub(crate) fn all_end_of_scope_members<'db>(
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
is_type_check_only: false,
|
||||
};
|
||||
Some(MemberWithDefinition {
|
||||
member,
|
||||
@@ -109,6 +111,7 @@ pub(crate) fn all_reachable_members<'db>(
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
is_type_check_only: false,
|
||||
};
|
||||
Some(MemberWithDefinition {
|
||||
member,
|
||||
@@ -125,6 +128,7 @@ pub(crate) fn all_reachable_members<'db>(
|
||||
let member = Member {
|
||||
name: symbol.name().clone(),
|
||||
ty,
|
||||
is_type_check_only: false,
|
||||
};
|
||||
Some(MemberWithDefinition {
|
||||
member,
|
||||
@@ -419,6 +423,7 @@ impl<'db> AllMembers<'db> {
|
||||
self.members.insert(Member {
|
||||
name: Name::new_static("__file__"),
|
||||
ty: dunder_file_type,
|
||||
is_type_check_only: false,
|
||||
});
|
||||
|
||||
self.extend_with_type(db, env, KnownClass::ModuleType.to_instance(db, env));
|
||||
@@ -440,25 +445,13 @@ impl<'db> AllMembers<'db> {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(definition) = defined.provenance.definition()
|
||||
&& !exists_at_runtime(db, definition)
|
||||
// Source-module completions retain `@type_check_only` symbols and rank them
|
||||
// lower.
|
||||
&& (file.is_stub(db) || !defined.ty.is_type_check_only(db))
|
||||
// The decorator itself is typing-only, but users must still be able to
|
||||
// import it when defining typing-only classes and functions.
|
||||
&& !matches!(
|
||||
defined.ty,
|
||||
Type::FunctionLiteral(function)
|
||||
if function.known(db) == Some(KnownFunction::TypeCheckOnly)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
self.members.insert(Member {
|
||||
name: symbol_name.clone(),
|
||||
ty: defined.ty,
|
||||
is_type_check_only: defined
|
||||
.provenance
|
||||
.definition()
|
||||
.is_some_and(|definition| !exists_at_runtime(db, definition)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -467,7 +460,11 @@ impl<'db> AllMembers<'db> {
|
||||
|submodule_name| {
|
||||
let ty = literal.resolve_submodule(db, &submodule_name)?;
|
||||
let name = submodule_name.clone();
|
||||
Some(Member { name, ty })
|
||||
Some(Member {
|
||||
name,
|
||||
ty,
|
||||
is_type_check_only: false,
|
||||
})
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -516,6 +513,7 @@ impl<'db> AllMembers<'db> {
|
||||
self.members.insert(Member {
|
||||
name: memberdef.member.name,
|
||||
ty,
|
||||
is_type_check_only: memberdef.member.is_type_check_only,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -565,6 +563,7 @@ impl<'db> AllMembers<'db> {
|
||||
self.members.insert(Member {
|
||||
name: Name::new(name),
|
||||
ty,
|
||||
is_type_check_only: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -582,6 +581,7 @@ impl<'db> AllMembers<'db> {
|
||||
self.members.insert(Member {
|
||||
name: memberdef.member.name,
|
||||
ty,
|
||||
is_type_check_only: memberdef.member.is_type_check_only,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -644,6 +644,7 @@ impl<'db> AllMembers<'db> {
|
||||
self.members.insert(Member {
|
||||
name: Name::from(*attr),
|
||||
ty: synthetic_member,
|
||||
is_type_check_only: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -677,6 +678,8 @@ pub struct MemberWithDefinition<'db> {
|
||||
pub struct Member<'db> {
|
||||
pub(crate) name: Name,
|
||||
pub(crate) ty: Type<'db>,
|
||||
/// Whether this member is known to exist only during type checking.
|
||||
pub(crate) is_type_check_only: bool,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Member<'_> {
|
||||
|
||||
Reference in new issue
Block a user