[red-knot] Add initial support for * imports (#16923)

## Summary

This PR adds initial support for `*` imports to red-knot. The approach
is to implement a standalone query, called from semantic indexing, that
visits the module referenced by the `*` import and collects all
global-scope public names that will be imported by the `*` import. The
`SemanticIndexBuilder` then adds separate definitions for each of these
names, all keyed to the same `ast::Alias` node that represents the `*`
import.

There are many pieces of `*`-import semantics that are still yet to be
done, even with this PR:
- This PR does not attempt to implement any of the semantics to do with
`__all__`. (If a module defines `__all__`, then only the symbols
included in `__all__` are imported, _not_ all public global-scope
symbols.
- With the logic implemented in this PR as it currently stands, we
sometimes incorrectly consider a symbol bound even though it is defined
in a branch that is statically known to be dead code, e.g. (assuming the
target Python version is set to 3.11):

  ```py
  # a.py

  import sys

  if sys.version_info < (3, 10):
      class Foo: ...

  ```

  ```py
  # b.py

  from a import *

  print(Foo)  # this is unbound at runtime on 3.11,
# but we currently consider it bound with the logic in this PR
  ```

Implementing these features is important, but is for now deferred to
followup PRs.

Many thanks to @ntBre, who contributed to this PR in a pairing session
on Friday!

## Test Plan

Assertions in existing mdtests are adjusted, and several new ones are
added.
This commit is contained in:
Alex Waygood 2025-03-24 13:15:58 -04:00 committed by GitHub
parent cba197e3c5
commit e87fee4b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 927 additions and 357 deletions

View file

@ -14,7 +14,7 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds;
use crate::semantic_index::attribute_assignment::AttributeAssignments;
use crate::semantic_index::builder::SemanticIndexBuilder;
use crate::semantic_index::definition::{Definition, DefinitionNodeKey};
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable,
@ -29,6 +29,7 @@ pub mod definition;
pub mod expression;
mod narrowing_constraints;
pub(crate) mod predicate;
mod re_exports;
pub mod symbol;
mod use_def;
mod visibility_constraints;
@ -136,7 +137,7 @@ pub(crate) struct SemanticIndex<'db> {
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
/// Map from a node creating a definition to its definition.
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
/// Map from a standalone expression to its [`Expression`] ingredient.
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
@ -250,13 +251,37 @@ impl<'db> SemanticIndex<'db> {
AncestorsIter::new(self, scope)
}
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
/// Returns the [`definition::Definition`] salsa ingredient(s) for `definition_key`.
///
/// There will only ever be >1 `Definition` associated with a `definition_key`
/// if the definition is created by a wildcard (`*`) import.
#[track_caller]
pub(crate) fn definition(
pub(crate) fn definitions(
&self,
definition_key: impl Into<DefinitionNodeKey>,
) -> &Definitions<'db> {
&self.definitions_by_node[&definition_key.into()]
}
/// Returns the [`definition::Definition`] salsa ingredient for `definition_key`.
///
/// ## Panics
///
/// If the number of definitions associated with the key is not exactly 1 and
/// the `debug_assertions` feature is enabled, this method will panic.
#[track_caller]
pub(crate) fn expect_single_definition(
&self,
definition_key: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy,
) -> Definition<'db> {
self.definitions_by_node[&definition_key.into()]
let definitions = self.definitions(definition_key);
debug_assert_eq!(
definitions.len(),
1,
"Expected exactly one definition to be associated with AST node {definition_key:?} but found {}",
definitions.len()
);
definitions[0]
}
/// Returns the [`Expression`] ingredient for an expression node.
@ -280,7 +305,8 @@ impl<'db> SemanticIndex<'db> {
.copied()
}
/// Returns the id of the scope that `node` creates. This is different from [`Definition::scope`] which
/// Returns the id of the scope that `node` creates.
/// This is different from [`definition::Definition::scope`] which
/// returns the scope in which that definition is defined in.
#[track_caller]
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {