[red-knot] Fix relative imports in src.root (#15990)

## Summary

Fixes https://github.com/astral-sh/ruff/issues/15989

Red Knot failed to resolve relative imports if the importing module is
located at a search path root.

The issue was that the module resolver returned an `Err(TooManyDots)` as
soon as the parent of the current module is `None` (which is the case
for a module at the search path root).
However, this is incorrect if a `tail` (a module name) exists.
This commit is contained in:
Micha Reiser 2025-02-06 14:08:20 +00:00 committed by GitHub
parent 9d2105b863
commit 5588c75d65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 6 deletions

View file

@ -218,3 +218,33 @@ import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`" # error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown reveal_type(package.foo.X) # revealed: Unknown
``` ```
## In the src-root
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from .parser import X
reveal_type(X) # revealed: int
```
## Beyond the src-root
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from ..parser import X # error: [unresolved-import]
```

View file

@ -2513,18 +2513,32 @@ impl<'db> TypeInferenceBuilder<'db> {
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?; .ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
let mut level = level.get(); let mut level = level.get();
if module.kind().is_package() { if module.kind().is_package() {
level -= 1; level = level.saturating_sub(1);
} }
let mut module_name = module.name().clone(); let mut module_name = module.name().clone();
for _ in 0..level { let tail = tail
module_name = module_name .map(|tail| ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax))
.parent() .transpose()?;
.ok_or(ModuleNameResolutionError::TooManyDots)?;
for remaining_dots in (0..level).rev() {
if let Some(parent) = module_name.parent() {
module_name = parent;
} else if remaining_dots == 0 {
// If we reached a search path root and this was the last dot return the tail if any.
// If there's no tail, then we have a relative import that's too deep.
return tail.ok_or(ModuleNameResolutionError::TooManyDots);
} else {
// We're at a search path root. This is a too deep relative import if there's more than
// one dot remaining.
return Err(ModuleNameResolutionError::TooManyDots);
}
} }
if let Some(tail) = tail { if let Some(tail) = tail {
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
module_name.extend(&tail); module_name.extend(&tail);
} }
Ok(module_name) Ok(module_name)
} }