[red-knot] Fix panic on cyclic * imports (#16958)

## Summary

Further work towards https://github.com/astral-sh/ruff/issues/14169.

We currently panic on encountering cyclic `*` imports. This is easily
fixed using fixpoint iteration.

## Test Plan

Added a test that panics on `main`, but passes with this PR
This commit is contained in:
Alex Waygood 2025-03-24 14:23:02 -04:00 committed by GitHub
parent dd5b02aaa2
commit 4975c2f027
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 1 deletions

View file

@ -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

View file

@ -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<Name>,
_count: u32,
_file: File,
) -> salsa::CycleRecoveryAction<FxHashSet<Name>> {
salsa::CycleRecoveryAction::Iterate
}
fn exports_cycle_initial(_db: &dyn Db, _file: File) -> FxHashSet<Name> {
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<Name> {
let module = parsed_module(db.upcast(), file);
let mut finder = ExportFinder::new(db, file);