diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/star.md b/crates/red_knot_python_semantic/resources/mdtest/import/star.md index a7e8dd34f3..52e76d2205 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/star.md @@ -834,6 +834,35 @@ reveal_type(g) # revealed: Unknown reveal_type(h) # revealed: Unknown ``` +## Cyclic star imports + +Believe it or not, this code does _not_ raise an exception at runtime! + +`a.py`: + +```py +from b import * + +A: bool = True +``` + +`b.py`: + +```py +from a import * + +B: bool = True +``` + +`c.py`: + +```py +from a import * + +reveal_type(A) # revealed: bool +reveal_type(B) # revealed: bool +``` + ## Integration test: `collections.abc` The `collections.abc` standard-library module provides a good integration test, as all its symbols diff --git a/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs b/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs index 5902799d8b..a6b1c6529e 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs @@ -15,6 +15,10 @@ //! separate query, we would need to complete semantic indexing on `bar` in order to //! complete analysis of the global namespace of `foo`. Since semantic indexing is somewhat //! expensive, this would be undesirable. A separate query allows us to avoid this issue. +//! +//! An additional concern is that the recursive nature of this query means that it must be able +//! to handle cycles. We do this using fixpoint iteration; adding fixpoint iteration to the +//! whole [`super::semantic_index()`] query would probably be prohibitively expensive. use ruff_db::{files::File, parsed::parsed_module}; use ruff_python_ast::{ @@ -26,7 +30,20 @@ use rustc_hash::FxHashSet; use crate::{module_name::ModuleName, resolve_module, Db}; -#[salsa::tracked(return_ref)] +fn exports_cycle_recover( + _db: &dyn Db, + _value: &FxHashSet, + _count: u32, + _file: File, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn exports_cycle_initial(_db: &dyn Db, _file: File) -> FxHashSet { + FxHashSet::default() +} + +#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)] pub(super) fn exported_names(db: &dyn Db, file: File) -> FxHashSet { let module = parsed_module(db.upcast(), file); let mut finder = ExportFinder::new(db, file);