Skip to content

Commit 63dd68e

Browse files
authored
Refactor symbol lookup APIs to hide re-export implementation details (#16133)
## Summary This PR refactors the symbol lookup APIs to better facilitate the re-export implementation. Specifically, * Add `module_type_symbol` which returns the `Symbol` that's a member of `types.ModuleType` * Rename `symbol` -> `symbol_impl`; add `symbol` which delegates to `symbol_impl` with `RequireExplicitReExport::No` * Update `global_symbol` to do `symbol_impl` -> fall back to `module_type_symbol` and default to `RequireExplicitReExport::No` * Add `imported_symbol` to do `symbol_impl` with `RequireExplicitReExport` as `Yes` if the module is in a stub file else `No` * Update `known_module_symbol` to use `imported_symbol` with a fallback to `module_type_symbol` * Update `ModuleLiteralType::member` to use `imported_symbol` with a custom fallback We could potentially also update `symbol_from_declarations` and `symbol_from_bindings` to avoid passing in the `RequireExplicitReExport` as it would be always `No` if called directly. We could add `symbol_from_declarations_impl` and `symbol_from_bindings_impl`. Looking at the `_impl` functions, I think we should move all of these symbol related logic into `symbol.rs` where `Symbol` is defined and the `_impl` could be private while we expose the public APIs at the crate level. This would also make the `RequireExplicitReExport` an implementation detail and the caller doesn't need to worry about it.
1 parent 60b3ef2 commit 63dd68e

5 files changed

Lines changed: 160 additions & 119 deletions

File tree

crates/red_knot_python_semantic/src/semantic_index/definition.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ impl<'db> Definition<'db> {
5050
self.kind(db).category()
5151
}
5252

53-
pub(crate) fn in_stub(self, db: &'db dyn Db) -> bool {
54-
self.file(db).is_stub(db.upcast())
55-
}
56-
5753
pub(crate) fn is_declaration(self, db: &'db dyn Db) -> bool {
5854
self.kind(db).category().is_declaration()
5955
}

crates/red_knot_python_semantic/src/stdlib.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::module_resolver::{resolve_module, KnownModule};
22
use crate::semantic_index::global_scope;
33
use crate::semantic_index::symbol::ScopeId;
44
use crate::symbol::Symbol;
5-
use crate::types::{global_symbol, SymbolLookup};
5+
use crate::types::imported_symbol;
66
use crate::Db;
77

88
/// Lookup the type of `symbol` in a given known module
@@ -14,18 +14,10 @@ pub(crate) fn known_module_symbol<'db>(
1414
symbol: &str,
1515
) -> Symbol<'db> {
1616
resolve_module(db, &known_module.name())
17-
.map(|module| global_symbol(db, SymbolLookup::External, module.file(), symbol))
17+
.map(|module| imported_symbol(db, &module, symbol))
1818
.unwrap_or(Symbol::Unbound)
1919
}
2020

21-
/// Lookup the type of `symbol` in the builtins namespace.
22-
///
23-
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
24-
#[inline]
25-
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
26-
known_module_symbol(db, KnownModule::Builtins, symbol)
27-
}
28-
2921
/// Lookup the type of `symbol` in the `typing` module namespace.
3022
///
3123
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.

crates/red_knot_python_semantic/src/types.rs

Lines changed: 112 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::semantic_index::{
3232
use_def_map, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
3333
DeclarationsIterator,
3434
};
35-
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
35+
use crate::stdlib::{known_module_symbol, typing_extensions_symbol};
3636
use crate::suppression::check_suppressions;
3737
use crate::symbol::{Boundness, Symbol};
3838
use crate::types::call::{
@@ -107,40 +107,37 @@ fn widen_type_for_undeclared_public_symbol<'db>(
107107
}
108108

109109
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
110-
pub(crate) enum SymbolLookup {
111-
/// Look up the symbol as seen from within the same module.
112-
Internal,
113-
/// Look up the symbol as seen from outside the module.
114-
External,
110+
enum RequiresExplicitReExport {
111+
Yes,
112+
No,
115113
}
116114

117-
impl SymbolLookup {
118-
const fn is_external(self) -> bool {
119-
matches!(self, Self::External)
115+
impl RequiresExplicitReExport {
116+
const fn is_yes(self) -> bool {
117+
matches!(self, RequiresExplicitReExport::Yes)
120118
}
121119
}
122120

123-
/// Infer the public type of a symbol (its type as seen from outside its scope).
124-
fn symbol<'db>(
121+
fn symbol_impl<'db>(
125122
db: &'db dyn Db,
126-
lookup: SymbolLookup,
127123
scope: ScopeId<'db>,
128124
name: &str,
125+
requires_explicit_reexport: RequiresExplicitReExport,
129126
) -> Symbol<'db> {
130127
#[salsa::tracked]
131128
fn symbol_by_id<'db>(
132129
db: &'db dyn Db,
133-
lookup: SymbolLookup,
134130
scope: ScopeId<'db>,
135131
symbol_id: ScopedSymbolId,
132+
requires_explicit_reexport: RequiresExplicitReExport,
136133
) -> Symbol<'db> {
137134
let use_def = use_def_map(db, scope);
138135

139136
// If the symbol is declared, the public type is based on declarations; otherwise, it's based
140137
// on inference from bindings.
141138

142139
let declarations = use_def.public_declarations(symbol_id);
143-
let declared = symbol_from_declarations(db, lookup, declarations);
140+
let declared = symbol_from_declarations(db, declarations, requires_explicit_reexport);
144141
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
145142
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
146143

@@ -150,7 +147,7 @@ fn symbol<'db>(
150147
// Symbol is possibly declared
151148
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
152149
let bindings = use_def.public_bindings(symbol_id);
153-
let inferred = symbol_from_bindings(db, lookup, bindings);
150+
let inferred = symbol_from_bindings(db, bindings, requires_explicit_reexport);
154151

155152
match inferred {
156153
// Symbol is possibly undeclared and definitely unbound
@@ -170,7 +167,7 @@ fn symbol<'db>(
170167
// Symbol is undeclared, return the union of `Unknown` with the inferred type
171168
Ok(Symbol::Unbound) => {
172169
let bindings = use_def.public_bindings(symbol_id);
173-
let inferred = symbol_from_bindings(db, lookup, bindings);
170+
let inferred = symbol_from_bindings(db, bindings, requires_explicit_reexport);
174171

175172
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
176173
// modified externally, but those changes do not take effect. We therefore issue
@@ -232,7 +229,7 @@ fn symbol<'db>(
232229

233230
symbol_table(db, scope)
234231
.symbol_id_by_name(name)
235-
.map(|symbol| symbol_by_id(db, lookup, scope, symbol))
232+
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport))
236233
.unwrap_or(Symbol::Unbound)
237234
}
238235

@@ -271,27 +268,99 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N
271268
.collect()
272269
}
273270

274-
pub(crate) fn global_symbol<'db>(
275-
db: &'db dyn Db,
276-
lookup: SymbolLookup,
277-
file: File,
278-
name: &str,
279-
) -> Symbol<'db> {
280-
// Not defined explicitly in the global scope?
281-
// All modules are instances of `types.ModuleType`;
282-
// look it up there (with a few very special exceptions)
283-
symbol(db, lookup, global_scope(db, file), name).or_fall_back_to(db, || {
284-
if module_type_symbols(db)
285-
.iter()
286-
.any(|module_type_member| &**module_type_member == name)
287-
{
288-
KnownClass::ModuleType.to_instance(db).member(db, name)
289-
} else {
271+
/// Return the symbol for a member of `types.ModuleType`.
272+
pub(crate) fn module_type_symbol<'db>(db: &'db dyn Db, name: &str) -> Symbol<'db> {
273+
if module_type_symbols(db)
274+
.iter()
275+
.any(|module_type_member| &**module_type_member == name)
276+
{
277+
KnownClass::ModuleType.to_instance(db).member(db, name)
278+
} else {
279+
Symbol::Unbound
280+
}
281+
}
282+
283+
/// Infer the public type of a symbol (its type as seen from outside its scope) in the given
284+
/// `scope`.
285+
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
286+
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
287+
}
288+
289+
/// Infers the public type of a module-global symbol as seen from within the same file.
290+
///
291+
/// If it's not defined explicitly in the global scope, it will look it up in `types.ModuleType`
292+
/// with a few very special exceptions.
293+
///
294+
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
295+
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
296+
symbol_impl(
297+
db,
298+
global_scope(db, file),
299+
name,
300+
RequiresExplicitReExport::No,
301+
)
302+
.or_fall_back_to(db, || module_type_symbol(db, name))
303+
}
304+
305+
/// Infers the public type of an imported symbol.
306+
pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str) -> Symbol<'db> {
307+
// If it's not found in the global scope, check if it's present as an instance on
308+
// `types.ModuleType` or `builtins.object`.
309+
//
310+
// We do a more limited version of this in `global_symbol`, but there are two crucial
311+
// differences here:
312+
// - If a member is looked up as an attribute, `__init__` is also available on the module, but
313+
// it isn't available as a global from inside the module
314+
// - If a member is looked up as an attribute, members on `builtins.object` are also available
315+
// (because `types.ModuleType` inherits from `object`); these attributes are also not
316+
// available as globals from inside the module.
317+
//
318+
// The same way as in `global_symbol`, however, we need to be careful to ignore
319+
// `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
320+
// dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which
321+
// module we're dealing with.
322+
external_symbol_impl(db, module.file(), name).or_fall_back_to(db, || {
323+
if name == "__getattr__" {
290324
Symbol::Unbound
325+
} else {
326+
KnownClass::ModuleType.to_instance(db).member(db, name)
291327
}
292328
})
293329
}
294330

331+
/// Lookup the type of `symbol` in the builtins namespace.
332+
///
333+
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
334+
///
335+
/// Note that this function is only intended for use in the context of the builtins *namespace*
336+
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
337+
/// (e.g. `from builtins import int`).
338+
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
339+
resolve_module(db, &KnownModule::Builtins.name())
340+
.map(|module| {
341+
external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || {
342+
// We're looking up in the builtins namespace and not the module, so we should
343+
// do the normal lookup in `types.ModuleType` and not the special one as in
344+
// `imported_symbol`.
345+
module_type_symbol(db, symbol)
346+
})
347+
})
348+
.unwrap_or(Symbol::Unbound)
349+
}
350+
351+
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
352+
symbol_impl(
353+
db,
354+
global_scope(db, file),
355+
name,
356+
if file.is_stub(db.upcast()) {
357+
RequiresExplicitReExport::Yes
358+
} else {
359+
RequiresExplicitReExport::No
360+
},
361+
)
362+
}
363+
295364
/// Infer the type of a binding.
296365
pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
297366
let inference = infer_definition_types(db, definition);
@@ -340,14 +409,14 @@ fn definition_expression_type<'db>(
340409
/// The type will be a union if there are multiple bindings with different types.
341410
fn symbol_from_bindings<'db>(
342411
db: &'db dyn Db,
343-
lookup: SymbolLookup,
344412
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
413+
requires_explicit_reexport: RequiresExplicitReExport,
345414
) -> Symbol<'db> {
346415
let visibility_constraints = bindings_with_constraints.visibility_constraints;
347416
let mut bindings_with_constraints = bindings_with_constraints.peekable();
348417

349418
let is_non_exported = |binding: Definition<'db>| {
350-
lookup.is_external() && !binding.is_reexported(db) && binding.in_stub(db)
419+
requires_explicit_reexport.is_yes() && !binding.is_reexported(db)
351420
};
352421

353422
let unbound_visibility = match bindings_with_constraints.peek() {
@@ -471,14 +540,14 @@ type SymbolFromDeclarationsResult<'db> =
471540
/// [`TypeQualifiers`] that have been specified on the declaration(s).
472541
fn symbol_from_declarations<'db>(
473542
db: &'db dyn Db,
474-
lookup: SymbolLookup,
475543
declarations: DeclarationsIterator<'_, 'db>,
544+
requires_explicit_reexport: RequiresExplicitReExport,
476545
) -> SymbolFromDeclarationsResult<'db> {
477546
let visibility_constraints = declarations.visibility_constraints;
478547
let mut declarations = declarations.peekable();
479548

480549
let is_non_exported = |declaration: Definition<'db>| {
481-
lookup.is_external() && !declaration.is_reexported(db) && declaration.in_stub(db)
550+
requires_explicit_reexport.is_yes() && !declaration.is_reexported(db)
482551
};
483552

484553
let undeclared_visibility = match declarations.peek() {
@@ -3839,31 +3908,7 @@ impl<'db> ModuleLiteralType<'db> {
38393908
}
38403909
}
38413910

3842-
// If it's not found in the global scope, check if it's present as an instance
3843-
// on `types.ModuleType` or `builtins.object`.
3844-
//
3845-
// We do a more limited version of this in `global_symbol_ty`,
3846-
// but there are two crucial differences here:
3847-
// - If a member is looked up as an attribute, `__init__` is also available
3848-
// on the module, but it isn't available as a global from inside the module
3849-
// - If a member is looked up as an attribute, members on `builtins.object`
3850-
// are also available (because `types.ModuleType` inherits from `object`);
3851-
// these attributes are also not available as globals from inside the module.
3852-
//
3853-
// The same way as in `global_symbol_ty`, however, we need to be careful to
3854-
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType`
3855-
// to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types
3856-
// where we know exactly which module we're dealing with.
3857-
global_symbol(db, SymbolLookup::External, self.module(db).file(), name).or_fall_back_to(
3858-
db,
3859-
|| {
3860-
if name == "__getattr__" {
3861-
Symbol::Unbound
3862-
} else {
3863-
KnownClass::ModuleType.to_instance(db).member(db, name)
3864-
}
3865-
},
3866-
)
3911+
imported_symbol(db, &self.module(db), name)
38673912
}
38683913
}
38693914

@@ -4198,7 +4243,7 @@ impl<'db> Class<'db> {
41984243
/// traverse through the MRO until it finds the member.
41994244
pub(crate) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
42004245
let scope = self.body_scope(db);
4201-
symbol(db, SymbolLookup::Internal, scope, name)
4246+
symbol(db, scope, name)
42024247
}
42034248

42044249
/// Returns the `name` attribute of an instance of this class.
@@ -4340,7 +4385,7 @@ impl<'db> Class<'db> {
43404385

43414386
let declarations = use_def.public_declarations(symbol_id);
43424387

4343-
match symbol_from_declarations(db, SymbolLookup::Internal, declarations) {
4388+
match symbol_from_declarations(db, declarations, RequiresExplicitReExport::No) {
43444389
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
43454390
// The attribute is declared in the class body.
43464391

@@ -4362,7 +4407,7 @@ impl<'db> Class<'db> {
43624407
// in a method, and it could also be *bound* in the class body (and/or in a method).
43634408

43644409
let bindings = use_def.public_bindings(symbol_id);
4365-
let inferred = symbol_from_bindings(db, SymbolLookup::Internal, bindings);
4410+
let inferred = symbol_from_bindings(db, bindings, RequiresExplicitReExport::No);
43664411
let inferred_ty = inferred.ignore_possibly_unbound();
43674412

43684413
Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into()
@@ -4980,7 +5025,7 @@ pub(crate) mod tests {
49805025
)?;
49815026

49825027
let bar = system_path_to_file(&db, "src/bar.py")?;
4983-
let a = global_symbol(&db, SymbolLookup::Internal, bar, "a");
5028+
let a = global_symbol(&db, bar, "a");
49845029

49855030
assert_eq!(
49865031
a.expect_type(),
@@ -4999,7 +5044,7 @@ pub(crate) mod tests {
49995044
)?;
50005045
db.clear_salsa_events();
50015046

5002-
let a = global_symbol(&db, SymbolLookup::Internal, bar, "a");
5047+
let a = global_symbol(&db, bar, "a");
50035048

50045049
assert_eq!(
50055050
a.expect_type(),

0 commit comments

Comments
 (0)