diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md b/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md new file mode 100644 index 0000000000..5e58b8a6f1 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md @@ -0,0 +1,371 @@ +# Import conventions + +This document describes the conventions for importing symbols. + +Reference: + +- + +## Builtins scope + +When looking up for a name, red knot will fallback to using the builtins scope if the name is not +found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the +builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not +re-exported. + +```py +# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being +# available in the builtins scope. + +# error: "Name `Literal` used when not defined" +reveal_type(Literal) # revealed: Unknown + +# error: "Name `sys` used when not defined" +reveal_type(sys) # revealed: Unknown +``` + +## Builtins import + +Similarly, trying to import the symbols from the builtins module which aren't re-exported should +also raise an error. + +```py +# error: "Module `builtins` has no member `Literal`" +# error: "Module `builtins` has no member `sys`" +from builtins import Literal, sys + +reveal_type(Literal) # revealed: Unknown +reveal_type(sys) # revealed: Unknown + +# error: "Module `math` has no member `Iterable`" +from math import Iterable + +reveal_type(Iterable) # revealed: Unknown +``` + +## Re-exported symbols in stub files + +When a symbol is re-exported, importing it should not raise an error. This tests both `import ...` +and `from ... import ...` forms. + +Note: Submodule imports in `import ...` form doesn't work because it's a syntax error. For example, +in `import os.path as os.path` the `os.path` is not a valid identifier. + +```py +from b import Any, Literal, foo + +reveal_type(Any) # revealed: typing.Any +reveal_type(Literal) # revealed: typing.Literal +reveal_type(foo) # revealed: +``` + +`b.pyi`: + +```pyi +import foo as foo +from typing import Any as Any, Literal as Literal +``` + +`foo.py`: + +```py +``` + +## Non-exported symbols in stub files + +Here, none of the symbols are being re-exported in the stub file. + +```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 + +reveal_type(Any) # revealed: Unknown +reveal_type(Literal) # revealed: Unknown +reveal_type(foo) # revealed: Unknown +``` + +`b.pyi`: + +```pyi +import foo +from typing import Any, Literal +``` + +`foo.pyi`: + +```pyi +``` + +## Nested non-exports + +Here, a chain of modules all don't re-export an import. + +```py +# error: "Module `a` has no member `Any`" +from a import Any + +reveal_type(Any) # revealed: Unknown +``` + +`a.pyi`: + +```pyi +# error: "Module `b` has no member `Any`" +from b import Any + +reveal_type(Any) # revealed: Unknown +``` + +`b.pyi`: + +```pyi +# error: "Module `c` has no member `Any`" +from c import Any + +reveal_type(Any) # revealed: Unknown +``` + +`c.pyi`: + +```pyi +from typing import Any + +reveal_type(Any) # revealed: typing.Any +``` + +## Nested mixed re-export and not + +But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not +raise an error at that step in the chain. + +```py +# error: "Module `a` has no member `Any`" +from a import Any + +reveal_type(Any) # revealed: Unknown +``` + +`a.pyi`: + +```pyi +from b import Any + +reveal_type(Any) # revealed: Unknown +``` + +`b.pyi`: + +```pyi +# error: "Module `c` has no member `Any`" +from c import Any as Any + +reveal_type(Any) # revealed: Unknown +``` + +`c.pyi`: + +```pyi +from typing import Any + +reveal_type(Any) # revealed: typing.Any +``` + +## Exported as different name + +The re-export convention only works when the aliased name is exactly the same as the original name. + +```py +# error: "Module `a` has no member `Foo`" +from a import Foo + +reveal_type(Foo) # revealed: Unknown +``` + +`a.pyi`: + +```pyi +from b import AnyFoo as Foo + +reveal_type(Foo) # revealed: Literal[AnyFoo] +``` + +`b.pyi`: + +```pyi +class AnyFoo: ... +``` + +## Exported using `__all__` + +Here, the symbol is re-exported using the `__all__` variable. + +```py +# TODO: This should *not* be an error but we don't understand `__all__` yet. +# error: "Module `a` has no member `Foo`" +from a import Foo +``` + +`a.pyi`: + +```pyi +from b import Foo + +__all__ = ['Foo'] +``` + +`b.pyi`: + +```pyi +class Foo: ... +``` + +## 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`. + +```py +# error: 15 "Module `a` has no member `Foo`" +# error: 20 "Module `a` has no member `c`" +from a import Foo, c, foo + +reveal_type(Foo) # revealed: Unknown +reveal_type(c) # revealed: Unknown +reveal_type(foo) # revealed: +``` + +`a/__init__.pyi`: + +```pyi +from .b import c +from .foo import Foo +``` + +`a/foo.pyi`: + +```pyi +class Foo: ... +``` + +`a/b/__init__.pyi`: + +```pyi +``` + +`a/b/c.pyi`: + +```pyi +``` + +## Conditional re-export in stub file + +The following scenarios are when a re-export happens conditionally in a stub file. + +### Global import + +```py +# error: "Member `Foo` of module `a` is possibly unbound" +from a import Foo + +reveal_type(Foo) # revealed: str +``` + +`a.pyi`: + +```pyi +from b import Foo + +def coinflip() -> bool: ... + +if coinflip(): + Foo: str = ... + +reveal_type(Foo) # revealed: Literal[Foo] | str +``` + +`b.pyi`: + +```pyi +class Foo: ... +``` + +### Both branch is an import + +Here, both the branches of the condition are import statements where one of them re-exports while +the other does not. + +```py +# error: "Member `Foo` of module `a` is possibly unbound" +from a import Foo + +reveal_type(Foo) # revealed: Literal[Foo] +``` + +`a.pyi`: + +```pyi +def coinflip() -> bool: ... + +if coinflip(): + from b import Foo +else: + from b import Foo as Foo + +reveal_type(Foo) # revealed: Literal[Foo] +``` + +`b.pyi`: + +```pyi +class Foo: ... +``` + +### Re-export in one branch + +```py +# error: "Member `Foo` of module `a` is possibly unbound" +from a import Foo + +reveal_type(Foo) # revealed: Literal[Foo] +``` + +`a.pyi`: + +```pyi +def coinflip() -> bool: ... + +if coinflip(): + from b import Foo as Foo +``` + +`b.pyi`: + +```pyi +class Foo: ... +``` + +### Non-export in one branch + +```py +# error: "Module `a` has no member `Foo`" +from a import Foo + +reveal_type(Foo) # revealed: Unknown +``` + +`a.pyi`: + +```pyi +def coinflip() -> bool: ... + +if coinflip(): + from b import Foo +``` + +`b.pyi`: + +```pyi +class Foo: ... +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index fff95086c1..9e97a1bf2e 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -33,8 +33,8 @@ use crate::Db; use super::constraint::{Constraint, ConstraintNode, PatternConstraint}; use super::definition::{ - DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef, - WithItemDefinitionNodeRef, + DefinitionCategory, ExceptHandlerDefinitionNodeRef, ImportDefinitionNodeRef, + MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef, }; mod except_handlers; @@ -886,22 +886,28 @@ where self.imported_modules.extend(module_name.ancestors()); } - let symbol_name = if let Some(asname) = &alias.asname { - asname.id.clone() + let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname { + (asname.id.clone(), asname.id == alias.name.id) } else { - Name::new(alias.name.id.split('.').next().unwrap()) + (Name::new(alias.name.id.split('.').next().unwrap()), false) }; let symbol = self.add_symbol(symbol_name); - self.add_definition(symbol, alias); + self.add_definition( + symbol, + ImportDefinitionNodeRef { + alias, + is_reexported, + }, + ); } } ast::Stmt::ImportFrom(node) => { for (alias_index, alias) in node.names.iter().enumerate() { - let symbol_name = if let Some(asname) = &alias.asname { - &asname.id + let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname { + (&asname.id, asname.id == alias.name.id) } else { - &alias.name.id + (&alias.name.id, false) }; // Look for imports `from __future__ import annotations`, ignore `as ...` @@ -914,7 +920,14 @@ where let symbol = self.add_symbol(symbol_name.clone()); - self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index }); + self.add_definition( + symbol, + ImportFromDefinitionNodeRef { + node, + alias_index, + is_reexported, + }, + ); } } ast::Stmt::Assign(node) => { diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index adc1367560..30f1927e80 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -50,6 +50,10 @@ impl<'db> Definition<'db> { self.kind(db).category() } + pub(crate) fn in_stub(self, db: &'db dyn Db) -> bool { + self.file(db).is_stub(db.upcast()) + } + pub(crate) fn is_declaration(self, db: &'db dyn Db) -> bool { self.kind(db).category().is_declaration() } @@ -57,11 +61,15 @@ impl<'db> Definition<'db> { pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool { self.kind(db).category().is_binding() } + + pub(crate) fn is_reexported(self, db: &'db dyn Db) -> bool { + self.kind(db).is_reexported() + } } #[derive(Copy, Clone, Debug)] pub(crate) enum DefinitionNodeRef<'a> { - Import(&'a ast::Alias), + Import(ImportDefinitionNodeRef<'a>), ImportFrom(ImportFromDefinitionNodeRef<'a>), For(ForStmtDefinitionNodeRef<'a>), Function(&'a ast::StmtFunctionDef), @@ -119,12 +127,6 @@ impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> { } } -impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> { - fn from(node_ref: &'a ast::Alias) -> Self { - Self::Import(node_ref) - } -} - impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> { fn from(value: &'a ast::TypeParamTypeVar) -> Self { Self::TypeVar(value) @@ -143,6 +145,12 @@ impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> { } } +impl<'a> From> for DefinitionNodeRef<'a> { + fn from(node_ref: ImportDefinitionNodeRef<'a>) -> Self { + Self::Import(node_ref) + } +} + impl<'a> From> for DefinitionNodeRef<'a> { fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self { Self::ImportFrom(node_ref) @@ -185,10 +193,17 @@ impl<'a> From> for DefinitionNodeRef<'a> { } } +#[derive(Copy, Clone, Debug)] +pub(crate) struct ImportDefinitionNodeRef<'a> { + pub(crate) alias: &'a ast::Alias, + pub(crate) is_reexported: bool, +} + #[derive(Copy, Clone, Debug)] pub(crate) struct ImportFromDefinitionNodeRef<'a> { pub(crate) node: &'a ast::StmtImportFrom, pub(crate) alias_index: usize, + pub(crate) is_reexported: bool, } #[derive(Copy, Clone, Debug)] @@ -244,15 +259,22 @@ impl<'db> DefinitionNodeRef<'db> { #[allow(unsafe_code)] pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> { match self { - DefinitionNodeRef::Import(alias) => { - DefinitionKind::Import(AstNodeRef::new(parsed, alias)) - } - DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef { node, alias_index }) => { - DefinitionKind::ImportFrom(ImportFromDefinitionKind { - node: AstNodeRef::new(parsed, node), - alias_index, - }) - } + DefinitionNodeRef::Import(ImportDefinitionNodeRef { + alias, + is_reexported, + }) => DefinitionKind::Import(ImportDefinitionKind { + alias: AstNodeRef::new(parsed, alias), + is_reexported, + }), + DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef { + node, + alias_index, + is_reexported, + }) => DefinitionKind::ImportFrom(ImportFromDefinitionKind { + node: AstNodeRef::new(parsed, node), + alias_index, + is_reexported, + }), DefinitionNodeRef::Function(function) => { DefinitionKind::Function(AstNodeRef::new(parsed, function)) } @@ -354,10 +376,15 @@ impl<'db> DefinitionNodeRef<'db> { pub(super) fn key(self) -> DefinitionNodeKey { match self { - Self::Import(node) => node.into(), - Self::ImportFrom(ImportFromDefinitionNodeRef { node, alias_index }) => { - (&node.names[alias_index]).into() - } + Self::Import(ImportDefinitionNodeRef { + alias, + is_reexported: _, + }) => alias.into(), + Self::ImportFrom(ImportFromDefinitionNodeRef { + node, + alias_index, + is_reexported: _, + }) => (&node.names[alias_index]).into(), Self::Function(node) => node.into(), Self::Class(node) => node.into(), Self::TypeAlias(node) => node.into(), @@ -441,7 +468,7 @@ impl DefinitionCategory { /// for an in-depth explanation of why this is necessary. #[derive(Clone, Debug)] pub enum DefinitionKind<'db> { - Import(AstNodeRef), + Import(ImportDefinitionKind), ImportFrom(ImportFromDefinitionKind), Function(AstNodeRef), Class(AstNodeRef), @@ -464,6 +491,14 @@ pub enum DefinitionKind<'db> { } impl DefinitionKind<'_> { + pub(crate) fn is_reexported(&self) -> bool { + match self { + DefinitionKind::Import(import) => import.is_reexported(), + DefinitionKind::ImportFrom(import) => import.is_reexported(), + _ => true, + } + } + /// Returns the [`TextRange`] of the definition target. /// /// A definition target would mainly be the node representing the symbol being defined i.e., @@ -472,7 +507,7 @@ impl DefinitionKind<'_> { /// This is mainly used for logging and debugging purposes. pub(crate) fn target_range(&self) -> TextRange { match self { - DefinitionKind::Import(alias) => alias.range(), + DefinitionKind::Import(import) => import.alias().range(), DefinitionKind::ImportFrom(import) => import.alias().range(), DefinitionKind::Function(function) => function.name.range(), DefinitionKind::Class(class) => class.name.range(), @@ -603,10 +638,27 @@ impl ComprehensionDefinitionKind { } } +#[derive(Clone, Debug)] +pub struct ImportDefinitionKind { + alias: AstNodeRef, + is_reexported: bool, +} + +impl ImportDefinitionKind { + pub(crate) fn alias(&self) -> &ast::Alias { + self.alias.node() + } + + pub(crate) fn is_reexported(&self) -> bool { + self.is_reexported + } +} + #[derive(Clone, Debug)] pub struct ImportFromDefinitionKind { node: AstNodeRef, alias_index: usize, + is_reexported: bool, } impl ImportFromDefinitionKind { @@ -617,6 +669,10 @@ impl ImportFromDefinitionKind { pub(crate) fn alias(&self) -> &ast::Alias { &self.node.node().names[self.alias_index] } + + pub(crate) fn is_reexported(&self) -> bool { + self.is_reexported + } } #[derive(Clone, Debug)] diff --git a/crates/red_knot_python_semantic/src/stdlib.rs b/crates/red_knot_python_semantic/src/stdlib.rs index d5e200aa08..13fcaefaa5 100644 --- a/crates/red_knot_python_semantic/src/stdlib.rs +++ b/crates/red_knot_python_semantic/src/stdlib.rs @@ -2,7 +2,7 @@ use crate::module_resolver::{resolve_module, KnownModule}; use crate::semantic_index::global_scope; use crate::semantic_index::symbol::ScopeId; use crate::symbol::Symbol; -use crate::types::global_symbol; +use crate::types::{global_symbol, SymbolLookup}; use crate::Db; /// Lookup the type of `symbol` in a given known module @@ -14,7 +14,7 @@ pub(crate) fn known_module_symbol<'db>( symbol: &str, ) -> Symbol<'db> { resolve_module(db, &known_module.name()) - .map(|module| global_symbol(db, module.file(), symbol)) + .map(|module| global_symbol(db, SymbolLookup::External, module.file(), symbol)) .unwrap_or(Symbol::Unbound) } diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e1fe29359c..9cc357e29f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -106,11 +106,31 @@ fn widen_type_for_undeclared_public_symbol<'db>( } } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum SymbolLookup { + /// Look up the symbol as seen from within the same module. + Internal, + /// Look up the symbol as seen from outside the module. + External, +} + +impl SymbolLookup { + const fn is_external(self) -> bool { + matches!(self, Self::External) + } +} + /// Infer the public type of a symbol (its type as seen from outside its scope). -fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> { +fn symbol<'db>( + db: &'db dyn Db, + lookup: SymbolLookup, + scope: ScopeId<'db>, + name: &str, +) -> Symbol<'db> { #[salsa::tracked] fn symbol_by_id<'db>( db: &'db dyn Db, + lookup: SymbolLookup, scope: ScopeId<'db>, symbol_id: ScopedSymbolId, ) -> Symbol<'db> { @@ -120,7 +140,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> // on inference from bindings. let declarations = use_def.public_declarations(symbol_id); - let declared = symbol_from_declarations(db, declarations); + let declared = symbol_from_declarations(db, lookup, declarations); let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final); let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol); @@ -130,7 +150,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> // Symbol is possibly declared Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => { let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); + let inferred = symbol_from_bindings(db, lookup, bindings); match inferred { // Symbol is possibly undeclared and definitely unbound @@ -150,7 +170,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> // Symbol is undeclared, return the union of `Unknown` with the inferred type Ok(Symbol::Unbound) => { let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); + let inferred = symbol_from_bindings(db, lookup, bindings); // `__slots__` is a symbol with special behavior in Python's runtime. It can be // modified externally, but those changes do not take effect. We therefore issue @@ -212,7 +232,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> symbol_table(db, scope) .symbol_id_by_name(name) - .map(|symbol_id| symbol_by_id(db, scope, symbol_id)) + .map(|symbol| symbol_by_id(db, lookup, scope, symbol)) .unwrap_or(Symbol::Unbound) } @@ -251,12 +271,16 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N .collect() } -/// Looks up a module-global symbol by name in a file. -pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> { +pub(crate) fn global_symbol<'db>( + db: &'db dyn Db, + lookup: SymbolLookup, + file: File, + name: &str, +) -> Symbol<'db> { // Not defined explicitly in the global scope? // All modules are instances of `types.ModuleType`; // look it up there (with a few very special exceptions) - symbol(db, global_scope(db, file), name).or_fall_back_to(db, || { + symbol(db, lookup, global_scope(db, file), name).or_fall_back_to(db, || { if module_type_symbols(db) .iter() .any(|module_type_member| &**module_type_member == name) @@ -316,20 +340,25 @@ fn definition_expression_type<'db>( /// The type will be a union if there are multiple bindings with different types. fn symbol_from_bindings<'db>( db: &'db dyn Db, + lookup: SymbolLookup, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, ) -> Symbol<'db> { let visibility_constraints = bindings_with_constraints.visibility_constraints; let mut bindings_with_constraints = bindings_with_constraints.peekable(); - let unbound_visibility = if let Some(BindingWithConstraints { - binding: None, - constraints: _, - visibility_constraint, - }) = bindings_with_constraints.peek() - { - visibility_constraints.evaluate(db, *visibility_constraint) - } else { - Truthiness::AlwaysFalse + let is_non_exported = |binding: Definition<'db>| { + lookup.is_external() && !binding.is_reexported(db) && binding.in_stub(db) + }; + + let unbound_visibility = match bindings_with_constraints.peek() { + Some(BindingWithConstraints { + binding, + visibility_constraint, + constraints: _, + }) if binding.map_or(true, is_non_exported) => { + visibility_constraints.evaluate(db, *visibility_constraint) + } + _ => Truthiness::AlwaysFalse, }; let mut types = bindings_with_constraints.filter_map( @@ -339,6 +368,11 @@ fn symbol_from_bindings<'db>( visibility_constraint, }| { let binding = binding?; + + if is_non_exported(binding) { + return None; + } + let static_visibility = visibility_constraints.evaluate(db, visibility_constraint); if static_visibility.is_always_false() { @@ -437,19 +471,24 @@ type SymbolFromDeclarationsResult<'db> = /// [`TypeQualifiers`] that have been specified on the declaration(s). fn symbol_from_declarations<'db>( db: &'db dyn Db, + lookup: SymbolLookup, declarations: DeclarationsIterator<'_, 'db>, ) -> SymbolFromDeclarationsResult<'db> { let visibility_constraints = declarations.visibility_constraints; let mut declarations = declarations.peekable(); - let undeclared_visibility = if let Some(DeclarationWithConstraint { - declaration: None, - visibility_constraint, - }) = declarations.peek() - { - visibility_constraints.evaluate(db, *visibility_constraint) - } else { - Truthiness::AlwaysFalse + let is_non_exported = |declaration: Definition<'db>| { + lookup.is_external() && !declaration.is_reexported(db) && declaration.in_stub(db) + }; + + let undeclared_visibility = match declarations.peek() { + Some(DeclarationWithConstraint { + declaration, + visibility_constraint, + }) if declaration.map_or(true, is_non_exported) => { + visibility_constraints.evaluate(db, *visibility_constraint) + } + _ => Truthiness::AlwaysFalse, }; let mut types = declarations.filter_map( @@ -458,6 +497,11 @@ fn symbol_from_declarations<'db>( visibility_constraint, }| { let declaration = declaration?; + + if is_non_exported(declaration) { + return None; + } + let static_visibility = visibility_constraints.evaluate(db, visibility_constraint); if static_visibility.is_always_false() { @@ -3810,13 +3854,16 @@ impl<'db> ModuleLiteralType<'db> { // ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` // to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types // where we know exactly which module we're dealing with. - symbol(db, global_scope(db, self.module(db).file()), name).or_fall_back_to(db, || { - if name == "__getattr__" { - Symbol::Unbound - } else { - KnownClass::ModuleType.to_instance(db).member(db, name) - } - }) + global_symbol(db, SymbolLookup::External, self.module(db).file(), name).or_fall_back_to( + db, + || { + if name == "__getattr__" { + Symbol::Unbound + } else { + KnownClass::ModuleType.to_instance(db).member(db, name) + } + }, + ) } } @@ -4151,7 +4198,7 @@ impl<'db> Class<'db> { /// traverse through the MRO until it finds the member. pub(crate) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { let scope = self.body_scope(db); - symbol(db, scope, name) + symbol(db, SymbolLookup::Internal, scope, name) } /// Returns the `name` attribute of an instance of this class. @@ -4293,7 +4340,7 @@ impl<'db> Class<'db> { let declarations = use_def.public_declarations(symbol_id); - match symbol_from_declarations(db, declarations) { + match symbol_from_declarations(db, SymbolLookup::Internal, declarations) { Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => { // The attribute is declared in the class body. @@ -4315,7 +4362,7 @@ impl<'db> Class<'db> { // in a method, and it could also be *bound* in the class body (and/or in a method). let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); + let inferred = symbol_from_bindings(db, SymbolLookup::Internal, bindings); let inferred_ty = inferred.ignore_possibly_unbound(); Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into() @@ -4933,7 +4980,7 @@ pub(crate) mod tests { )?; let bar = system_path_to_file(&db, "src/bar.py")?; - let a = global_symbol(&db, bar, "a"); + let a = global_symbol(&db, SymbolLookup::Internal, bar, "a"); assert_eq!( a.expect_type(), @@ -4952,7 +4999,7 @@ pub(crate) mod tests { )?; db.clear_salsa_events(); - let a = global_symbol(&db, bar, "a"); + let a = global_symbol(&db, SymbolLookup::Internal, bar, "a"); assert_eq!( a.expect_type(), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9a5379b639..1136968080 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -63,11 +63,11 @@ use crate::types::diagnostic::{ use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations, - todo_type, typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, - DynamicType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType, - IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, - MetaclassErrorKind, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, + builtins_symbol, symbol, symbol_from_bindings, symbol_from_declarations, todo_type, + typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType, + FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, + KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, + SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, SymbolLookup, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; @@ -86,7 +86,7 @@ use super::slots::check_class_slots; use super::string_annotation::{ parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, }; -use super::{ParameterExpectation, ParameterExpectations}; +use super::{global_symbol, ParameterExpectation, ParameterExpectations}; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the @@ -735,7 +735,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_alias_definition(type_alias.node(), definition); } DefinitionKind::Import(import) => { - self.infer_import_definition(import.node(), definition); + self.infer_import_definition(import.alias(), definition); } DefinitionKind::ImportFrom(import_from) => { self.infer_import_from_definition( @@ -871,7 +871,7 @@ impl<'db> TypeInferenceBuilder<'db> { let use_def = self.index.use_def_map(binding.file_scope(self.db())); let declarations = use_def.declarations_at_binding(binding); let mut bound_ty = ty; - let declared_ty = symbol_from_declarations(self.db(), declarations) + let declared_ty = symbol_from_declarations(self.db(), SymbolLookup::Internal, declarations) .map(|SymbolAndQualifiers(s, _)| s.ignore_possibly_unbound().unwrap_or(Type::unknown())) .unwrap_or_else(|(ty, conflicting)| { // TODO point out the conflicting declarations in the diagnostic? @@ -906,7 +906,7 @@ impl<'db> TypeInferenceBuilder<'db> { let use_def = self.index.use_def_map(declaration.file_scope(self.db())); let prior_bindings = use_def.bindings_at_declaration(declaration); // unbound_ty is Never because for this check we don't care about unbound - let inferred_ty = symbol_from_bindings(self.db(), prior_bindings) + let inferred_ty = symbol_from_bindings(self.db(), SymbolLookup::Internal, prior_bindings) .ignore_possibly_unbound() .unwrap_or(Type::Never); let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { @@ -3307,7 +3307,11 @@ impl<'db> TypeInferenceBuilder<'db> { // If we're inferring types of deferred expressions, always treat them as public symbols let local_scope_symbol = if self.is_deferred() { if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) { - symbol_from_bindings(db, use_def.public_bindings(symbol_id)) + symbol_from_bindings( + db, + SymbolLookup::Internal, + use_def.public_bindings(symbol_id), + ) } else { assert!( self.deferred_state.in_string_annotation(), @@ -3317,7 +3321,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } else { let use_id = name_node.scoped_use_id(db, scope); - symbol_from_bindings(db, use_def.bindings_at_use(use_id)) + symbol_from_bindings(db, SymbolLookup::Internal, use_def.bindings_at_use(use_id)) }; let symbol = local_scope_symbol.or_fall_back_to(db, || { @@ -3368,7 +3372,7 @@ impl<'db> TypeInferenceBuilder<'db> { // runtime, it is the scope that creates the cell for our closure.) If the name // isn't bound in that scope, we should get an unbound name, not continue // falling back to other scopes / globals / builtins. - return symbol(db, enclosing_scope_id, symbol_name); + return symbol(db, SymbolLookup::Internal, enclosing_scope_id, symbol_name); } } @@ -3379,7 +3383,7 @@ impl<'db> TypeInferenceBuilder<'db> { if file_scope_id.is_global() { Symbol::Unbound } else { - global_symbol(db, self.file(), symbol_name) + global_symbol(db, SymbolLookup::Internal, self.file(), symbol_name) } }) // Not found in globals? Fallback to builtins @@ -6051,7 +6055,7 @@ mod tests { assert_eq!(scope.name(db), *expected_scope_name); } - symbol(db, scope, symbol_name) + symbol(db, SymbolLookup::Internal, scope, symbol_name) } #[track_caller] @@ -6076,7 +6080,7 @@ mod tests { let mut db = setup_db(); let content = format!( r#" - from typing_extensions import assert_type + from typing_extensions import Literal, assert_type assert_type(not "{y}", bool) assert_type(not 10*"{y}", bool) @@ -6098,7 +6102,7 @@ mod tests { let mut db = setup_db(); let content = format!( r#" - from typing_extensions import assert_type + from typing_extensions import Literal, LiteralString, assert_type assert_type(2 * "hello", Literal["hellohello"]) assert_type("goodbye" * 3, Literal["goodbyegoodbyegoodbye"]) @@ -6123,7 +6127,7 @@ mod tests { let mut db = setup_db(); let content = format!( r#" - from typing_extensions import assert_type + from typing_extensions import Literal, LiteralString, assert_type assert_type("{y}", LiteralString) assert_type(10*"{y}", LiteralString) @@ -6145,7 +6149,7 @@ mod tests { let mut db = setup_db(); let content = format!( r#" - from typing_extensions import assert_type + from typing_extensions import LiteralString, assert_type assert_type("{y}", LiteralString) assert_type("a" + "{z}", LiteralString) @@ -6165,7 +6169,7 @@ mod tests { let mut db = setup_db(); let content = format!( r#" - from typing_extensions import assert_type + from typing_extensions import LiteralString, assert_type assert_type("{y}", LiteralString) assert_type("{y}" + "a", LiteralString) @@ -6267,7 +6271,7 @@ mod tests { ])?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").expect_type(); + let x_ty = global_symbol(&db, SymbolLookup::Internal, a, "x").expect_type(); assert_eq!(x_ty.display(&db).to_string(), "int"); @@ -6276,7 +6280,7 @@ mod tests { let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty_2 = global_symbol(&db, a, "x").expect_type(); + let x_ty_2 = global_symbol(&db, SymbolLookup::Internal, a, "x").expect_type(); assert_eq!(x_ty_2.display(&db).to_string(), "bool"); @@ -6293,7 +6297,7 @@ mod tests { ])?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").expect_type(); + let x_ty = global_symbol(&db, SymbolLookup::Internal, a, "x").expect_type(); assert_eq!(x_ty.display(&db).to_string(), "int"); @@ -6303,7 +6307,7 @@ mod tests { db.clear_salsa_events(); - let x_ty_2 = global_symbol(&db, a, "x").expect_type(); + let x_ty_2 = global_symbol(&db, SymbolLookup::Internal, a, "x").expect_type(); assert_eq!(x_ty_2.display(&db).to_string(), "int"); @@ -6329,7 +6333,7 @@ mod tests { ])?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").expect_type(); + let x_ty = global_symbol(&db, SymbolLookup::Internal, a, "x").expect_type(); assert_eq!(x_ty.display(&db).to_string(), "int"); @@ -6339,7 +6343,7 @@ mod tests { db.clear_salsa_events(); - let x_ty_2 = global_symbol(&db, a, "x").expect_type(); + let x_ty_2 = global_symbol(&db, SymbolLookup::Internal, a, "x").expect_type(); assert_eq!(x_ty_2.display(&db).to_string(), "int"); @@ -6386,7 +6390,7 @@ mod tests { )?; let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); - let attr_ty = global_symbol(&db, file_main, "x").expect_type(); + let attr_ty = global_symbol(&db, SymbolLookup::Internal, file_main, "x").expect_type(); assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None"); // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred @@ -6401,7 +6405,7 @@ mod tests { let events = { db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").expect_type(); + let attr_ty = global_symbol(&db, SymbolLookup::Internal, file_main, "x").expect_type(); assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); db.take_salsa_events() }; @@ -6420,7 +6424,7 @@ mod tests { let events = { db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").expect_type(); + let attr_ty = global_symbol(&db, SymbolLookup::Internal, file_main, "x").expect_type(); assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); db.take_salsa_events() }; diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 96d259a76b..511f94a241 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -322,13 +322,13 @@ pub(crate) enum ParameterKind<'db> { mod tests { use super::*; use crate::db::tests::{setup_db, TestDb}; - use crate::types::{global_symbol, FunctionType, KnownClass}; + use crate::types::{global_symbol, FunctionType, KnownClass, SymbolLookup}; use ruff_db::system::DbWithTestSystem; #[track_caller] fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { let module = ruff_db::files::system_path_to_file(db, file).unwrap(); - global_symbol(db, module, "f") + global_symbol(db, SymbolLookup::Internal, module, "f") .expect_type() .expect_function_literal() } @@ -357,6 +357,8 @@ mod tests { db.write_dedented( "/src/a.py", " + from typing import Literal + def f(a, b: int, c = 1, d: int = 2, /, e = 3, f: Literal[4] = 4, *args: object, g = 5, h: Literal[6] = 6, **kwargs: str) -> bytes: ...