Skip to content

Commit 1c6717b

Browse files
authored
Filter private symbols from stubs if they are internal types (#19121)
This implements filtering of private symbols from stub files based on type information as discussed in #19102. It extends the previous implementation to apply to all stub files, instead of just the `builtins` module, and uses type information to retain private names that are may be relevant at runtime.
1 parent 1b813cd commit 1c6717b

File tree

4 files changed

+186
-47
lines changed

4 files changed

+186
-47
lines changed

crates/ty_ide/src/completion.rs

Lines changed: 113 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ruff_db::parsed::{ParsedModuleRef, parsed_module};
55
use ruff_python_ast as ast;
66
use ruff_python_parser::{Token, TokenAt, TokenKind};
77
use ruff_text_size::{Ranged, TextRange, TextSize};
8-
use ty_python_semantic::{Completion, SemanticModel};
8+
use ty_python_semantic::{Completion, NameKind, SemanticModel};
99

1010
use crate::Db;
1111
use crate::find_node::covering_node;
@@ -325,38 +325,7 @@ fn import_from_tokens(tokens: &[Token]) -> Option<&Token> {
325325
/// This has the effect of putting all dunder attributes after "normal"
326326
/// attributes, and all single-underscore attributes after dunder attributes.
327327
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
328-
/// A helper type for sorting completions based only on name.
329-
///
330-
/// This sorts "normal" names first, then dunder names and finally
331-
/// single-underscore names. This matches the order of the variants defined for
332-
/// this enum, which is in turn picked up by the derived trait implementation
333-
/// for `Ord`.
334-
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
335-
enum Kind {
336-
Normal,
337-
Dunder,
338-
Sunder,
339-
}
340-
341-
impl Kind {
342-
fn classify(c: &Completion) -> Kind {
343-
// Dunder needs a prefix and suffix double underscore.
344-
// When there's only a prefix double underscore, this
345-
// results in explicit name mangling. We let that be
346-
// classified as-if they were single underscore names.
347-
//
348-
// Ref: <https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers>
349-
if c.name.starts_with("__") && c.name.ends_with("__") {
350-
Kind::Dunder
351-
} else if c.name.starts_with('_') {
352-
Kind::Sunder
353-
} else {
354-
Kind::Normal
355-
}
356-
}
357-
}
358-
359-
let (kind1, kind2) = (Kind::classify(c1), Kind::classify(c2));
328+
let (kind1, kind2) = (NameKind::classify(&c1.name), NameKind::classify(&c2.name));
360329
kind1.cmp(&kind2).then_with(|| c1.name.cmp(&c2.name))
361330
}
362331

@@ -472,6 +441,11 @@ mod tests {
472441
",
473442
);
474443
test.assert_completions_include("filter");
444+
// Sunder items should be filtered out
445+
test.assert_completions_do_not_include("_T");
446+
// Dunder attributes should not be stripped
447+
test.assert_completions_include("__annotations__");
448+
// See `private_symbols_in_stub` for more comprehensive testing private of symbol filtering.
475449
}
476450

477451
#[test]
@@ -536,6 +510,112 @@ re.<CURSOR>
536510
test.assert_completions_include("findall");
537511
}
538512

513+
#[test]
514+
fn private_symbols_in_stub() {
515+
let test = CursorTest::builder()
516+
.source(
517+
"package/__init__.pyi",
518+
r#"\
519+
from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol
520+
521+
public_name = 1
522+
_private_name = 1
523+
__mangled_name = 1
524+
__dunder_name__ = 1
525+
526+
public_type_var = TypeVar("public_type_var")
527+
_private_type_var = TypeVar("_private_type_var")
528+
__mangled_type_var = TypeVar("__mangled_type_var")
529+
530+
public_param_spec = ParamSpec("public_param_spec")
531+
_private_param_spec = ParamSpec("_private_param_spec")
532+
533+
public_type_var_tuple = TypeVarTuple("public_type_var_tuple")
534+
_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple")
535+
536+
public_explicit_type_alias: TypeAlias = Literal[1]
537+
_private_explicit_type_alias: TypeAlias = Literal[1]
538+
539+
class PublicProtocol(Protocol):
540+
def method(self) -> None: ...
541+
542+
class _PrivateProtocol(Protocol):
543+
def method(self) -> None: ...
544+
"#,
545+
)
546+
.source("main.py", "import package; package.<CURSOR>")
547+
.build();
548+
test.assert_completions_include("public_name");
549+
test.assert_completions_include("_private_name");
550+
test.assert_completions_include("__mangled_name");
551+
test.assert_completions_include("__dunder_name__");
552+
test.assert_completions_include("public_type_var");
553+
test.assert_completions_do_not_include("_private_type_var");
554+
test.assert_completions_do_not_include("__mangled_type_var");
555+
test.assert_completions_include("public_param_spec");
556+
test.assert_completions_do_not_include("_private_param_spec");
557+
test.assert_completions_include("public_type_var_tuple");
558+
test.assert_completions_do_not_include("_private_type_var_tuple");
559+
test.assert_completions_include("public_explicit_type_alias");
560+
test.assert_completions_include("_private_explicit_type_alias");
561+
test.assert_completions_include("PublicProtocol");
562+
test.assert_completions_do_not_include("_PrivateProtocol");
563+
}
564+
565+
/// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names
566+
/// are visible.
567+
#[test]
568+
fn private_symbols_in_module() {
569+
let test = CursorTest::builder()
570+
.source(
571+
"package/__init__.py",
572+
r#"\
573+
from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol
574+
575+
public_name = 1
576+
_private_name = 1
577+
__mangled_name = 1
578+
__dunder_name__ = 1
579+
580+
public_type_var = TypeVar("public_type_var")
581+
_private_type_var = TypeVar("_private_type_var")
582+
__mangled_type_var = TypeVar("__mangled_type_var")
583+
584+
public_param_spec = ParamSpec("public_param_spec")
585+
_private_param_spec = ParamSpec("_private_param_spec")
586+
587+
public_type_var_tuple = TypeVarTuple("public_type_var_tuple")
588+
_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple")
589+
590+
public_explicit_type_alias: TypeAlias = Literal[1]
591+
_private_explicit_type_alias: TypeAlias = Literal[1]
592+
593+
class PublicProtocol(Protocol):
594+
def method(self) -> None: ...
595+
596+
class _PrivateProtocol(Protocol):
597+
def method(self) -> None: ...
598+
"#,
599+
)
600+
.source("main.py", "import package; package.<CURSOR>")
601+
.build();
602+
test.assert_completions_include("public_name");
603+
test.assert_completions_include("_private_name");
604+
test.assert_completions_include("__mangled_name");
605+
test.assert_completions_include("__dunder_name__");
606+
test.assert_completions_include("public_type_var");
607+
test.assert_completions_include("_private_type_var");
608+
test.assert_completions_include("__mangled_type_var");
609+
test.assert_completions_include("public_param_spec");
610+
test.assert_completions_include("_private_param_spec");
611+
test.assert_completions_include("public_type_var_tuple");
612+
test.assert_completions_include("_private_type_var_tuple");
613+
test.assert_completions_include("public_explicit_type_alias");
614+
test.assert_completions_include("_private_explicit_type_alias");
615+
test.assert_completions_include("PublicProtocol");
616+
test.assert_completions_include("_PrivateProtocol");
617+
}
618+
539619
#[test]
540620
fn one_function_prefix() {
541621
let test = cursor_test(

crates/ty_python_semantic/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub use program::{
1515
PythonVersionWithSource, SearchPathSettings,
1616
};
1717
pub use python_platform::PythonPlatform;
18-
pub use semantic_model::{Completion, HasType, SemanticModel};
18+
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
1919
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
2020
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
2121

crates/ty_python_semantic/src/semantic_model.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,10 @@ impl<'db> SemanticModel<'db> {
6868
return vec![];
6969
};
7070
let ty = Type::module_literal(self.db, self.file, &module);
71+
let builtin = module.is_known(KnownModule::Builtins);
7172
crate::types::all_members(self.db, ty)
7273
.into_iter()
73-
.map(|name| Completion {
74-
name,
75-
builtin: module.is_known(KnownModule::Builtins),
76-
})
74+
.map(|name| Completion { name, builtin })
7775
.collect()
7876
}
7977

@@ -130,6 +128,39 @@ impl<'db> SemanticModel<'db> {
130128
}
131129
}
132130

131+
/// A classification of symbol names.
132+
///
133+
/// The ordering here is used for sorting completions.
134+
///
135+
/// This sorts "normal" names first, then dunder names and finally
136+
/// single-underscore names. This matches the order of the variants defined for
137+
/// this enum, which is in turn picked up by the derived trait implementation
138+
/// for `Ord`.
139+
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
140+
pub enum NameKind {
141+
Normal,
142+
Dunder,
143+
Sunder,
144+
}
145+
146+
impl NameKind {
147+
pub fn classify(name: &Name) -> NameKind {
148+
// Dunder needs a prefix and suffix double underscore.
149+
// When there's only a prefix double underscore, this
150+
// results in explicit name mangling. We let that be
151+
// classified as-if they were single underscore names.
152+
//
153+
// Ref: <https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers>
154+
if name.starts_with("__") && name.ends_with("__") {
155+
NameKind::Dunder
156+
} else if name.starts_with('_') {
157+
NameKind::Sunder
158+
} else {
159+
NameKind::Normal
160+
}
161+
}
162+
}
163+
133164
/// A suggestion for code completion.
134165
#[derive(Clone, Debug)]
135166
pub struct Completion {

crates/ty_python_semantic/src/types/ide_support.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use crate::Db;
2-
use crate::place::{imported_symbol, place_from_bindings, place_from_declarations};
1+
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
32
use crate::semantic_index::place::ScopeId;
43
use crate::semantic_index::{
54
attribute_scopes, global_scope, imported_modules, place_table, semantic_index, use_def_map,
65
};
7-
use crate::types::{ClassBase, ClassLiteral, KnownClass, Type};
6+
use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type};
7+
use crate::{Db, NameKind};
88
use ruff_python_ast::name::Name;
99
use rustc_hash::FxHashSet;
1010

@@ -144,13 +144,41 @@ impl AllMembers {
144144
let Some(symbol_name) = place_table.place_expr(symbol_id).as_name() else {
145145
continue;
146146
};
147-
if !imported_symbol(db, file, symbol_name, None)
148-
.place
149-
.is_unbound()
150-
{
151-
self.members
152-
.insert(place_table.place_expr(symbol_id).expect_name().clone());
147+
let Place::Type(ty, _) = imported_symbol(db, file, symbol_name, None).place
148+
else {
149+
continue;
150+
};
151+
152+
// Filter private symbols from stubs if they appear to be internal types
153+
let is_stub_file = file.path(db).extension() == Some("pyi");
154+
let is_private_symbol = match NameKind::classify(symbol_name) {
155+
NameKind::Dunder | NameKind::Normal => false,
156+
NameKind::Sunder => true,
157+
};
158+
if is_private_symbol && is_stub_file {
159+
match ty {
160+
Type::NominalInstance(instance)
161+
if matches!(
162+
instance.class.known(db),
163+
Some(
164+
KnownClass::TypeVar
165+
| KnownClass::TypeVarTuple
166+
| KnownClass::ParamSpec
167+
)
168+
) =>
169+
{
170+
continue;
171+
}
172+
Type::ClassLiteral(class) if class.is_protocol(db) => continue,
173+
Type::KnownInstance(
174+
KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_),
175+
) => continue,
176+
_ => {}
177+
}
153178
}
179+
180+
self.members
181+
.insert(place_table.place_expr(symbol_id).expect_name().clone());
154182
}
155183

156184
let module_name = module.name();

0 commit comments

Comments
 (0)