Repository: astral-sh/ruff
File: crates/ty_ide/src/symbols.rs (Rust)

Code before:
                    // If global_only, don't walk function bodies
                    return;
                }

                self.push_symbol(symbol);

                // Mark that we're entering a function scope
                let was_in_function = self.in_function;
                self.in_function = true;

                source_order::walk_stmt(self, stmt);

                // Restore the previous function scope state
                self.in_function = was_in_function;

                self.pop_symbol();
            }

            Stmt::ClassDef(class_def) => {
                let symbol = SymbolTree {
                    parent: None,
                    name: class_def.name.to_string(),
                    kind: SymbolKind::Class,
                    name_range: class_def.name.range(),
                    full_range: stmt.range(),
                    documentation: class_def.definition(&self.model).docstring(self.model.db()),
                };

                if self.global_only {
                    self.add_symbol(symbol);
                    // If global_only, don't walk class bodies
                    return;
                }

                self.push_symbol(symbol);
                source_order::walk_stmt(self, stmt);
                self.pop_symbol();
            }

            Stmt::Assign(assign) => {
                // Include assignments only when we're in global or class scope
                if self.in_function {
                    return;
                }
                for target in &assign.targets {
                    let Expr::Name(name) = target else { continue };
                    let kind = if Self::is_constant_name(name.id.as_str()) {
                        SymbolKind::Constant
                    } else if self
                        .iter_symbol_stack()
                        .any(|s| s.kind == SymbolKind::Class)

Review comment (bug):
I am pretty worried about doing this eagerly for every single symbol in an environment. I did some very ad hoc testing to compare perf here. On `main` with this `pyproject.toml`:

```toml
[project]
name = "ex003-reexport-simple"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "matplotlib>=3.10.7",
    "numpy>=2.3.4",
    "pandas>=2.3.3",
    "scikit-learn>=1.7.2",
    "scipy>=1.16.3",
    "whenever>=0.9.3",
]
```

And this Python source file:

```python
array
```

With my cursor right after `array`.

I get this in the trace logs after requesting completions at my cursor position a few times:

```
2025-12-02 09:59:31.888839440 DEBUG request{id=13 method="textDocument/completion"}: Completions request returned 1582 suggestions in 177.106611ms
2025-12-02 09:59:34.297544175 DEBUG request{id=15 method="textDocument/completion"}: Completions request returned 1582 suggestions in 56.358747ms
2025-12-02 09:59:36.489098862 DEBUG request{id=17 method="textDocument/completion"}: Completions request returned 1582 suggestions in 57.155421ms
2025-12-02 09:59:38.932666244 DEBUG request{id=20 method="textDocument/completion"}: Completions request returned 1582 suggestions in 57.014757ms
2025-12-02 09:59:42.342361845 DEBUG request{id=22 method="textDocument/completion"}: Completions request returned 1582 suggestions in 58.887699ms
2025-12-02 09:59:45.683431792 DEBUG request{id=24 method="textDocument/completion"}: Completions request returned 1582 suggestions in 57.7364ms
```

Now with this PR, I do the same test and I get:

```
2025-12-02 10:02:13.280166325 DEBUG request{id=13 method="textDocument/completion"}: Completions request returned 1582 suggestions in 139.561616ms
2025-12-02 10:02:16.136079062 DEBUG request{id=15 method="textDocument/completion"}: Completions request returned 1582 suggestions in 73.513749ms
2025-12-02 10:02:18.525222384 DEBUG request{id=17 method="textDocument/completion"}: Completions request returned 1582 suggestions in 71.187676ms
2025-12-02 10:02:20.888572941 DEBUG request{id=19 method="textDocument/completion"}: Completions request returned 1582 suggestions in 71.481457ms
2025-12-02 10:02:23.408312653 DEBUG request{id=21 method="textDocument/completion"}: Completions request returned 1582 suggestions in 70.057387ms
2025-12-02 10:02:26.422923788 DEBUG request{id=23 method="textDocument/completion"}: Completions request returned 1582 suggestions in 72.955201ms
```

Which seems like a noticeable dip to me. And this probably isn't that big of a Python project either.

So I think I'd prefer to hold off on doing this eagerly with auto-import specifically. I think it looks like that should be done via a resolve request.

An alternative that still involves eagerly getting the docstring is to:

1. Only do it for symbols that match the query given.
2. Perhaps only do it for the first N such symbols.

Code after:
impl<'db> Imports<'db> {
    /// Track the imports from the given `import ...` statement.
    fn add_import(&mut self, import: &'db ast::StmtImport) {
        for alias in &import.names {
            let asname = alias
                .asname
                .as_ref()
                .map(|ident| &ident.id)
                .unwrap_or(&alias.name.id);
            let module_name = ImportModuleName::Import(&alias.name.id);
            self.module_names
                .insert(asname, ImportModuleKind::Definitive(module_name));
        }
    }

    /// Track the imports from the given `from ... import ...` statement.
    fn add_import_from(&mut self, import_from: &'db ast::StmtImportFrom) {
        for alias in &import_from.names {
            if &alias.name == "*" {
                // FIXME: We'd ideally include the names
                // imported from the module, but we don't
                // want to do this eagerly. So supporting
                // this requires more infrastructure in
                // `Imports`.
                continue;
            }

            let asname = alias
                .asname
                .as_ref()
                .map(|ident| &ident.id)
                .unwrap_or(&alias.name.id);
            let module_name = ImportModuleName::ImportFrom {
                parent: import_from,
                child: &alias.name.id,
            };
            self.module_names
                .insert(asname, ImportModuleKind::Possible(module_name));
        }
    }

    /// Return the symbols exported by the module referred to by `name`.
    ///
    /// e.g., This can be used to resolve `__all__ += submodule.__all__`,
    /// where `name` is `submodule`.
    fn get_module_symbols(
        &self,
        db: &'db dyn Db,
        importing_file: File,
        name: &Name,
    ) -> Option<&'db FlatSymbols> {