Skip to content

Commit 06e8dc2

Browse files
committed
[red-knot] Improve handling of visibility constraints in external modules when resolving * imports
1 parent 81cf860 commit 06e8dc2

File tree

11 files changed

+324
-131
lines changed

11 files changed

+324
-131
lines changed

crates/red_knot_python_semantic/resources/mdtest/import/star.md

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,12 @@ match 42:
179179
...
180180
case object(foo=R):
181181
...
182+
183+
match 56:
182184
case object(S):
183185
...
186+
187+
match 12345:
184188
case T:
185189
...
186190

@@ -379,15 +383,21 @@ reveal_type(s) # revealed: Unknown
379383
# error: [unresolved-reference]
380384
reveal_type(t) # revealed: Unknown
381385

382-
# TODO: these should all reveal `Unknown | int`.
386+
# TODO: these should all reveal `Unknown | int` and should not emit errors.
383387
# (We don't generally model elsewhere in red-knot that bindings from walruses
384388
# "leak" from comprehension scopes into outer scopes, but we should.)
385389
# See https://github.com/astral-sh/ruff/issues/16954
390+
# error: [unresolved-reference]
386391
reveal_type(g) # revealed: Unknown
392+
# error: [unresolved-reference]
387393
reveal_type(i) # revealed: Unknown
394+
# error: [unresolved-reference]
388395
reveal_type(k) # revealed: Unknown
396+
# error: [unresolved-reference]
389397
reveal_type(m) # revealed: Unknown
398+
# error: [unresolved-reference]
390399
reveal_type(o) # revealed: Unknown
400+
# error: [unresolved-reference]
391401
reveal_type(q) # revealed: Unknown
392402
```
393403

@@ -549,26 +559,54 @@ from a import *
549559

550560
reveal_type(X) # revealed: bool
551561

552-
# TODO: should emit error: [unresolved-reference]
562+
# error: [unresolved-reference]
553563
reveal_type(Y) # revealed: Unknown
554564

555-
# TODO: The `*` import should not be considered a redefinition
565+
# The `*` import should not be considered a redefinition
556566
# of the global variable in this module, as the symbol in
557567
# the `a` module is in a branch that is statically known
558568
# to be dead code given the `python-version` configuration.
559-
# Thus this should reveal `Literal[True]`.
560-
reveal_type(Z) # revealed: Unknown
569+
# Thus this correctly reveals `Literal[True]` and does not have an error emitted
570+
reveal_type(Z) # revealed: Literal[True]
561571

562572
if sys.version_info >= (3, 12):
563573
from aa import *
564574

565-
# TODO: should reveal `Never`
575+
# TODO: should not error because this branch is unreachable;
576+
# should reveal `Never` (see https://github.com/astral-sh/ruff/issues/15967)
577+
# error: [unresolved-reference]
566578
reveal_type(AA) # revealed: Unknown
567579

568580
# error: [unresolved-reference]
569581
reveal_type(AA) # revealed: Unknown
570582
```
571583

584+
`c.py`:
585+
586+
```py
587+
def coinflip() -> bool:
588+
return True
589+
590+
if coinflip():
591+
Z: str = "Z"
592+
```
593+
594+
`d.py`:
595+
596+
```py
597+
Z = True
598+
599+
from a import *
600+
from a import *
601+
from a import *
602+
603+
reveal_type(Z) # revealed: Literal[True]
604+
605+
from c import *
606+
607+
reveal_type(Z) # revealed: Literal[True] | str
608+
```
609+
572610
### Relative `*` imports
573611

574612
Relative `*` imports are also supported by Python:
@@ -821,7 +859,7 @@ from a import *
821859
reveal_type(X) # revealed: bool
822860
reveal_type(Y) # revealed: bool
823861

824-
# TODO: should error with [unresolved-reference]
862+
# error: [unresolved-reference]
825863
reveal_type(Z) # revealed: Unknown
826864
```
827865

@@ -856,7 +894,7 @@ from a import *
856894
reveal_type(X) # revealed: bool
857895
reveal_type(Y) # revealed: bool
858896

859-
# TODO should have an [unresolved-reference] diagnostic
897+
# error: [unresolved-reference]
860898
reveal_type(Z) # revealed: Unknown
861899
```
862900

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//! Utilities for resolving import statements that are used from the `types` submodule and the `symbol` submodule.
2+
3+
use ruff_db::files::File;
4+
use ruff_python_ast as ast;
5+
6+
use crate::{
7+
db::Db,
8+
module_name::{ModuleName, ModuleNameResolutionError},
9+
module_resolver::resolve_module,
10+
semantic_index::{definition::StarImportDefinitionKind, symbol::SymbolTable},
11+
symbol::SymbolAndQualifiers,
12+
types::Type,
13+
};
14+
15+
pub(crate) fn resolve_star_import_definition<'db>(
16+
db: &'db dyn Db,
17+
file: File,
18+
star_import_definition: &StarImportDefinitionKind,
19+
symbol_table: &SymbolTable,
20+
) -> Result<SymbolAndQualifiers<'db>, UnresolvedImportFromError> {
21+
let import_from = star_import_definition.import();
22+
let alias = star_import_definition.alias();
23+
let symbol_id = star_import_definition.symbol_id();
24+
25+
let (_, module_type) = resolve_import_from_module(db, file, import_from, alias)?;
26+
let defined_name = symbol_table.symbol(symbol_id).name();
27+
let imported_symbol = module_type.member(db, defined_name);
28+
29+
Ok(imported_symbol)
30+
}
31+
32+
/// Resolve the [`ModuleName`], and the type of the module, being referred to by an
33+
/// [`ast::StmtImportFrom`] node. Emit a diagnostic if the module cannot be resolved.
34+
pub(crate) fn resolve_import_from_module<'db>(
35+
db: &'db dyn Db,
36+
file: File,
37+
import_from: &ast::StmtImportFrom,
38+
alias: &ast::Alias,
39+
) -> Result<(ModuleName, Type<'db>), UnresolvedImportFromError> {
40+
let ast::StmtImportFrom { module, level, .. } = import_from;
41+
let module = module.as_deref();
42+
43+
tracing::trace!(
44+
"Resolving imported object `{}` from module `{}` into file `{}`",
45+
alias.name,
46+
format_import_from_module(*level, module),
47+
file.path(db),
48+
);
49+
50+
let module_name =
51+
ModuleName::from_import_statement(db, file, import_from).map_err(|err| match err {
52+
ModuleNameResolutionError::InvalidSyntax => {
53+
tracing::debug!("Failed to resolve import due to invalid syntax");
54+
UnresolvedImportFromError::InvalidSyntax
55+
}
56+
ModuleNameResolutionError::TooManyDots => {
57+
tracing::debug!(
58+
"Relative module resolution `{}` failed: too many leading dots",
59+
format_import_from_module(*level, module),
60+
);
61+
UnresolvedImportFromError::UnresolvedModule
62+
}
63+
ModuleNameResolutionError::UnknownCurrentModule => {
64+
tracing::debug!(
65+
"Relative module resolution `{}` failed; could not resolve file `{}` to a module",
66+
format_import_from_module(*level, module),
67+
file.path(db)
68+
);
69+
UnresolvedImportFromError::UnresolvedModule
70+
}
71+
})?;
72+
73+
module_type_from_name(db, file, &module_name)
74+
.map(|module_type| (module_name, module_type))
75+
.ok_or(UnresolvedImportFromError::UnresolvedModule)
76+
}
77+
78+
fn format_import_from_module(level: u32, module: Option<&str>) -> String {
79+
format!(
80+
"{}{}",
81+
".".repeat(level as usize),
82+
module.unwrap_or_default()
83+
)
84+
}
85+
86+
pub(crate) fn module_type_from_name<'db>(
87+
db: &'db dyn Db,
88+
file: File,
89+
module_name: &ModuleName,
90+
) -> Option<Type<'db>> {
91+
resolve_module(db, module_name).map(|module| Type::module_literal(db, file, module))
92+
}
93+
94+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
95+
pub(crate) enum UnresolvedImportFromError {
96+
UnresolvedModule,
97+
InvalidSyntax,
98+
}
99+
100+
impl UnresolvedImportFromError {
101+
pub(crate) fn is_invalid_syntax(self) -> bool {
102+
matches!(self, UnresolvedImportFromError::InvalidSyntax)
103+
}
104+
}

crates/red_knot_python_semantic/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub use site_packages::SysPrefixPathOrigin;
1414

1515
pub mod ast_node_ref;
1616
mod db;
17+
mod import_resolution;
1718
pub mod lint;
1819
pub(crate) mod list;
1920
mod module_name;

crates/red_knot_python_semantic/src/semantic_index/builder.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ impl<'db> SemanticIndexBuilder<'db> {
398398
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
399399
let category = kind.category(self.file.is_stub(self.db.upcast()));
400400
let is_reexported = kind.is_reexported();
401+
let is_star_import = kind.is_star_import();
401402

402403
let definition = Definition::new(
403404
self.db,
@@ -425,7 +426,7 @@ impl<'db> SemanticIndexBuilder<'db> {
425426
let use_def = self.current_use_def_map_mut();
426427
match category {
427428
DefinitionCategory::DeclarationAndBinding => {
428-
use_def.record_declaration_and_binding(symbol, definition);
429+
use_def.record_declaration_and_binding(symbol, definition, is_star_import);
429430
}
430431
DefinitionCategory::Declaration => use_def.record_declaration(symbol, definition),
431432
DefinitionCategory::Binding => use_def.record_binding(symbol, definition),

crates/red_knot_python_semantic/src/semantic_index/definition.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,10 @@ impl DefinitionKind<'_> {
559559
}
560560
}
561561

562+
pub(crate) const fn is_star_import(&self) -> bool {
563+
matches!(self, DefinitionKind::StarImport(_))
564+
}
565+
562566
/// Returns the [`TextRange`] of the definition target.
563567
///
564568
/// A definition target would mainly be the node representing the symbol being defined i.e.,

crates/red_knot_python_semantic/src/semantic_index/use_def.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ pub(crate) struct UseDefMap<'db> {
321321
/// eager scope.
322322
eager_bindings: EagerBindings,
323323

324+
/// Snapshot of the [`SymbolState`] for a given symbol immediately prior
325+
/// to a star-import [`Definition`] that appears as though it may have provided
326+
/// a binding for that symbol.
327+
bindings_at_star_imports: FxHashMap<Definition<'db>, SymbolState>,
328+
324329
/// Whether or not the start of the scope is visible.
325330
/// This is used to check if the function can implicitly return `None`.
326331
/// For example:
@@ -352,6 +357,15 @@ impl<'db> UseDefMap<'db> {
352357
self.bindings_iterator(self.public_symbols[symbol].bindings())
353358
}
354359

360+
/// Iterate over the public bindings that were visible for a symbol immediately prior
361+
/// to a star-import [`Definition`] that may have created a binding for that symbol.
362+
pub(crate) fn public_bindings_prior_to_star_import(
363+
&self,
364+
star_import_definition: Definition<'db>,
365+
) -> BindingWithConstraintsIterator<'_, 'db> {
366+
self.bindings_iterator(self.bindings_at_star_imports[&star_import_definition].bindings())
367+
}
368+
355369
pub(crate) fn eager_bindings(
356370
&self,
357371
eager_bindings: ScopedEagerBindingsId,
@@ -570,6 +584,11 @@ pub(super) struct UseDefMapBuilder<'db> {
570584
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
571585
/// eager scope.
572586
eager_bindings: EagerBindings,
587+
588+
/// Snapshot of the [`SymbolState`] for a given symbol immediately prior
589+
/// to a star-import [`Definition`] that appears as though it may have provided
590+
/// a binding for that symbol.
591+
bindings_at_star_imports: FxHashMap<Definition<'db>, SymbolState>,
573592
}
574593

575594
impl Default for UseDefMapBuilder<'_> {
@@ -585,6 +604,7 @@ impl Default for UseDefMapBuilder<'_> {
585604
bindings_by_declaration: FxHashMap::default(),
586605
symbol_states: IndexVec::new(),
587606
eager_bindings: EagerBindings::default(),
607+
bindings_at_star_imports: FxHashMap::default(),
588608
}
589609
}
590610
}
@@ -687,11 +707,16 @@ impl<'db> UseDefMapBuilder<'db> {
687707
&mut self,
688708
symbol: ScopedSymbolId,
689709
definition: Definition<'db>,
710+
is_star_import: bool,
690711
) {
691712
// We don't need to store anything in self.bindings_by_declaration or
692713
// self.declarations_by_binding.
693714
let def_id = self.all_definitions.push(Some(definition));
694715
let symbol_state = &mut self.symbol_states[symbol];
716+
if is_star_import {
717+
self.bindings_at_star_imports
718+
.insert(definition, symbol_state.clone());
719+
}
695720
symbol_state.record_declaration(def_id);
696721
symbol_state.record_binding(def_id, self.scope_start_visibility);
697722
}
@@ -796,6 +821,7 @@ impl<'db> UseDefMapBuilder<'db> {
796821
self.declarations_by_binding.shrink_to_fit();
797822
self.bindings_by_declaration.shrink_to_fit();
798823
self.eager_bindings.shrink_to_fit();
824+
self.bindings_at_star_imports.shrink_to_fit();
799825

800826
UseDefMap {
801827
all_definitions: self.all_definitions,
@@ -807,6 +833,7 @@ impl<'db> UseDefMapBuilder<'db> {
807833
declarations_by_binding: self.declarations_by_binding,
808834
bindings_by_declaration: self.bindings_by_declaration,
809835
eager_bindings: self.eager_bindings,
836+
bindings_at_star_imports: self.bindings_at_star_imports,
810837
scope_start_visibility: self.scope_start_visibility,
811838
}
812839
}

crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ impl SymbolBindings {
313313
}
314314
}
315315

316-
#[derive(Clone, Debug, PartialEq, Eq)]
316+
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
317317
pub(super) struct SymbolState {
318318
declarations: SymbolDeclarations,
319319
bindings: SymbolBindings,

0 commit comments

Comments
 (0)