Compare commits

...

2 Commits

Author SHA1 Message Date
Aria Desires
7ef86c9637 fixup 2025-09-18 21:08:49 -04:00
Aria Desires
793d0d0dd4 Implement additional implicit re-exports idiom for __init__.pyi 2025-09-18 15:09:01 -04:00
5 changed files with 177 additions and 40 deletions

View File

@@ -459,6 +459,12 @@ impl File {
self.source_type(db).is_stub()
}
/// Returns `true` if the file is an `__init__.py(i)`
pub fn is_init(self, db: &dyn Db) -> bool {
let path = self.path(db).as_str();
path.ends_with("__init__.py") || path.ends_with("__init__.pyi")
}
pub fn source_type(self, db: &dyn Db) -> PySourceType {
match self.path(db) {
FilePath::System(path) => path

View File

@@ -74,25 +74,52 @@ from typing import Any as Any, Literal as Literal
Here, none of the symbols are being re-exported in the stub file.
In this case the symbols shouldn't be available as imports or attributes.
```py
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
from a import b
# error: [unresolved-attribute] "no attribute `Any`"
reveal_type(b.Any) # revealed: Unknown
# error: [unresolved-attribute] "no attribute `Literal`"
reveal_type(b.Literal) # revealed: Unknown
# error: [unresolved-attribute] "no attribute `foo`"
reveal_type(b.foo) # revealed: Unknown
# error: [unresolved-attribute] "no attribute `bar`"
reveal_type(b.bar) # revealed: Unknown
# error: [unresolved-import] "Module `a.b` has no member `foo`"
# error: [unresolved-import] "Module `a.b` has no member `bar`"
# error: [unresolved-import] "Module `a.b` has no member `Any`"
# error: [unresolved-import] "Module `a.b` has no member `Literal`"
from a.b import foo, bar, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
reveal_type(bar) # revealed: Unknown
```
`b.pyi`:
`a/__init__.pyi`:
```pyi
import foo
```
`a/b.pyi`:
```pyi
import a.foo
from . import bar
from typing import Any, Literal
```
`foo.pyi`:
`a/foo.pyi`:
```pyi
```
`a/bar.pyi`:
```pyi
@@ -261,39 +288,93 @@ reveal_type(Foo) # revealed: Unknown
## Re-exports in `__init__.pyi`
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
but the inference would be `Unknown`.
Within `__init__.pyi` relative imports (`from . import xyz` or `from .pub import xyz`) are also
treated as a re-exports.
We check the both the members of the module and the imports of the module as you _should_ be able to
do `from a import priv` but the attribute `a.priv` _should not_ exist.
The most subtle detail here is whether `from .semipriv import Pub` should make the `a.semipriv`
attribute exist or not. We do not currently do this, although perhaps we should.
```py
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
import a
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
reveal_type(a.Pub) # revealed: <class 'Pub'>
# error: [unresolved-attribute]
reveal_type(a.Priv) # revealed: Unknown
reveal_type(a.pub) # revealed: <module 'a.pub'>
# error: [unresolved-attribute]
reveal_type(a.priv) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(a.semipriv) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(a.sub) # revealed: Unknown
reveal_type(a.subpub) # revealed: <module 'a.sub.subpub'>
# error: [unresolved-attribute]
reveal_type(a.subpriv) # revealed: Unknown
# error: [unresolved-import] "Priv"
from a import Pub, Priv
# error: [unresolved-import] "subpriv"
from a import pub, priv, semipriv, sub, subpub, subpriv
reveal_type(Pub) # revealed: <class 'Pub'>
reveal_type(Priv) # revealed: Unknown
reveal_type(pub) # revealed: <module 'a.pub'>
reveal_type(priv) # revealed: <module 'a.priv'>
reveal_type(semipriv) # revealed: <module 'a.semipriv'>
reveal_type(sub) # revealed: <module 'a.sub'>
reveal_type(subpub) # revealed: <module 'a.sub.subpub'>
reveal_type(subpriv) # revealed: Unknown
```
`a/__init__.pyi`:
```pyi
from .b import c
from .foo import Foo
# re-exported because they're relative
from .sub import subpub
from .semipriv import Pub
from . import pub
# not re-exported because they're absolute
from a.sub import subpriv
from a.semipriv import Priv
from a import priv
```
`a/foo.pyi`:
`a/pub.pyi`:
```pyi
class Foo: ...
```
`a/b/__init__.pyi`:
`a/priv.pyi`:
```pyi
```
`a/semipriv.pyi`:
```pyi
class Pub: ...
class Priv: ...
```
`a/sub/__init__.pyi`:
```pyi
```
`a/b/c.pyi`:
`a/sub/subpub.pyi`:
```pyi
```
`a/sub/subpriv.pyi`:
```pyi

View File

@@ -331,7 +331,11 @@ pub(crate) fn imported_symbol<'db>(
) -> PlaceAndQualifiers<'db> {
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
if file.is_stub(db) {
RequiresExplicitReExport::Yes
if file.is_init(db) {
RequiresExplicitReExport::YesButInitIdiomAllowed
} else {
RequiresExplicitReExport::Yes
}
} else {
RequiresExplicitReExport::No
}
@@ -932,7 +936,8 @@ fn place_from_bindings_impl<'db>(
let mut bindings_with_constraints = bindings_with_constraints.peekable();
let is_non_exported = |binding: Definition<'db>| {
requires_explicit_reexport.is_yes() && !is_reexported(db, binding)
requires_explicit_reexport.is_yes()
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, binding))
};
let unbound_reachability_constraint = match bindings_with_constraints.peek() {
@@ -1209,7 +1214,8 @@ fn place_from_declarations_impl<'db>(
let mut exactly_one_declaration = false;
let is_non_exported = |declaration: Definition<'db>| {
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
requires_explicit_reexport.is_yes()
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, declaration))
};
let undeclared_reachability = match declarations.peek() {
@@ -1320,21 +1326,26 @@ fn place_from_declarations_impl<'db>(
// This will first check if the definition is using the "redundant alias" pattern like `import foo
// as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being
// exported via `__all__`.
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> ReExportKind {
// This information is computed by the semantic index builder.
if definition.is_reexported(db) {
return true;
let reexported = definition.is_reexported(db);
if reexported != ReExportKind::No {
return reexported;
}
// At this point, the definition should either be an `import` or `from ... import` statement.
// This is because the default value of `is_reexported` is `true` for any other kind of
// definition.
let Some(all_names) = dunder_all_names(db, definition.file(db)) else {
return false;
return ReExportKind::No;
};
let table = place_table(db, definition.scope(db));
let symbol_id = definition.place(db).expect_symbol();
let symbol_name = table.symbol(symbol_id).name();
all_names.contains(symbol_name)
if all_names.contains(symbol_name) {
ReExportKind::Yes
} else {
ReExportKind::No
}
}
mod implicit_globals {
@@ -1500,13 +1511,35 @@ mod implicit_globals {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum RequiresExplicitReExport {
Yes,
No,
/// This is an `__init__.pyi` and `from . import b` is considered a re-export
YesButInitIdiomAllowed,
Yes,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ReExportKind {
No,
/// `b` in `from . import b`
InitIdiom,
Yes,
}
impl get_size2::GetSize for ReExportKind {}
impl RequiresExplicitReExport {
/// Whether re-exports are necessary at all (this is really "is not No")
const fn is_yes(self) -> bool {
matches!(self, RequiresExplicitReExport::Yes)
!matches!(self, RequiresExplicitReExport::No)
}
/// Whether the style of re-export is sufficient for the context
fn is_satisfied(self, reexport: ReExportKind) -> bool {
match self {
RequiresExplicitReExport::No => true,
RequiresExplicitReExport::YesButInitIdiomAllowed => reexport != ReExportKind::No,
RequiresExplicitReExport::Yes => reexport == ReExportKind::Yes,
}
}
}

View File

@@ -20,6 +20,7 @@ use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::node_key::NodeKey;
use crate::place::ReExportKind;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::definition::{
@@ -1436,6 +1437,12 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
(Name::new(alias.name.id.split('.').next().unwrap()), false)
};
let is_reexported = if is_reexported {
ReExportKind::Yes
} else {
ReExportKind::No
};
let symbol = self.add_symbol(symbol_name);
self.add_definition(
symbol.into(),
@@ -1562,6 +1569,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
(&alias.name.id, false)
};
let is_reexported = if is_reexported {
ReExportKind::Yes
} else if node.level == 1 {
// `from . import a`
ReExportKind::InitIdiom
} else {
ReExportKind::No
};
// Look for imports `from __future__ import annotations`, ignore `as ...`
// We intentionally don't enforce the rules about location of `__future__`
// imports here, we assume the user's intent was to apply the `__future__`

View File

@@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Db;
use crate::ast_node_ref::AstNodeRef;
use crate::node_key::NodeKey;
use crate::place::ReExportKind;
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::scope::{FileScopeId, ScopeId};
use crate::semantic_index::symbol::ScopedSymbolId;
@@ -41,7 +42,7 @@ pub struct Definition<'db> {
pub kind: DefinitionKind<'db>,
/// This is a dedicated field to avoid accessing `kind` to compute this value.
pub(crate) is_reexported: bool,
pub(crate) is_reexported: ReExportKind,
}
// The Salsa heap is tracked separately.
@@ -337,7 +338,7 @@ impl<'ast> From<StarImportDefinitionNodeRef<'ast>> for DefinitionNodeRef<'ast, '
pub(crate) struct ImportDefinitionNodeRef<'ast> {
pub(crate) node: &'ast ast::StmtImport,
pub(crate) alias_index: usize,
pub(crate) is_reexported: bool,
pub(crate) is_reexported: ReExportKind,
}
#[derive(Copy, Clone, Debug)]
@@ -350,7 +351,7 @@ pub(crate) struct StarImportDefinitionNodeRef<'ast> {
pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
pub(crate) node: &'ast ast::StmtImportFrom,
pub(crate) alias_index: usize,
pub(crate) is_reexported: bool,
pub(crate) is_reexported: ReExportKind,
}
#[derive(Copy, Clone, Debug)]
@@ -678,11 +679,11 @@ pub enum DefinitionKind<'db> {
}
impl DefinitionKind<'_> {
pub(crate) fn is_reexported(&self) -> bool {
pub(crate) fn is_reexported(&self) -> ReExportKind {
match self {
DefinitionKind::Import(import) => import.is_reexported(),
DefinitionKind::ImportFrom(import) => import.is_reexported(),
_ => true,
_ => ReExportKind::Yes,
}
}
@@ -956,7 +957,7 @@ impl<'db> ComprehensionDefinitionKind<'db> {
pub struct ImportDefinitionKind {
node: AstNodeRef<ast::StmtImport>,
alias_index: usize,
is_reexported: bool,
is_reexported: ReExportKind,
}
impl ImportDefinitionKind {
@@ -968,7 +969,7 @@ impl ImportDefinitionKind {
&self.node.node(module).names[self.alias_index]
}
pub(crate) fn is_reexported(&self) -> bool {
pub(crate) fn is_reexported(&self) -> ReExportKind {
self.is_reexported
}
}
@@ -977,7 +978,7 @@ impl ImportDefinitionKind {
pub struct ImportFromDefinitionKind {
node: AstNodeRef<ast::StmtImportFrom>,
alias_index: usize,
is_reexported: bool,
is_reexported: ReExportKind,
}
impl ImportFromDefinitionKind {
@@ -989,7 +990,7 @@ impl ImportFromDefinitionKind {
&self.node.node(module).names[self.alias_index]
}
pub(crate) fn is_reexported(&self) -> bool {
pub(crate) fn is_reexported(&self) -> ReExportKind {
self.is_reexported
}
}