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

Code before:
/// Returns a flat list of *only global* symbols in the file given.
///
/// While callers can convert this into a hierarchical collection of
/// symbols, it won't result in anything meaningful since the flat list
/// returned doesn't include children.
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: File) -> FlatSymbols {
    let parsed = parsed_module(db, file);
    let module = parsed.load(db);

    let mut visitor = SymbolVisitor {
        symbols: IndexVec::new(),
        symbol_stack: vec![],
        in_function: false,
        global_only: true,
    };
    visitor.visit_body(&module.syntax().body);

    if !db.project().files(db).contains(&file) {
        // Eagerly clear ASTs of third party files.
        parsed.clear();
    }

    FlatSymbols {
        symbols: visitor.symbols,
    }
}

#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
struct SymbolTree {
    parent: Option<SymbolId>,
    name: String,
    kind: SymbolKind,
    name_range: TextRange,
    full_range: TextRange,
}

/// A visitor over all symbols in a single file.
///
/// This guarantees that child symbols have a symbol ID greater
/// than all of its parents.
struct SymbolVisitor {
    symbols: IndexVec<SymbolId, SymbolTree>,
    symbol_stack: Vec<SymbolId>,
    /// Track if we're currently inside a function (to exclude local variables)
    in_function: bool,
    global_only: bool,
}

impl SymbolVisitor {
    fn visit_body(&mut self, body: &[Stmt]) {

Review comment (bug):
Does this mean that each time completions are requested, every file will need to be re-parsed?

I checked out this PR and tried it out. I used a single Python source file containing just `array`. I opened it in VS Code and requested completions at the end of `array`. On `main`, I get:

```
2025-12-02 09:48:31.234246045 DEBUG request{id=13 method="textDocument/completion"}: Completions request returned 326 suggestions in 149.088978ms
2025-12-02 09:48:34.461071428 DEBUG request{id=15 method="textDocument/completion"}: Completions request returned 326 suggestions in 17.225306ms
2025-12-02 09:48:36.677139939 DEBUG request{id=17 method="textDocument/completion"}: Completions request returned 326 suggestions in 19.559503ms
2025-12-02 09:48:40.656242428 DEBUG request{id=19 method="textDocument/completion"}: Completions request returned 326 suggestions in 18.719669ms
```

And with your branch I get:

```
2025-12-02 09:50:26.152804396 DEBUG request{id=14 method="textDocument/completion"}: Completions request returned 326 suggestions in 166.780705ms
2025-12-02 09:50:29.762248593 DEBUG request{id=16 method="textDocument/completion"}: Completions request returned 326 suggestions in 19.985813ms
2025-12-02 09:50:33.345613225 DEBUG request{id=18 method="textDocument/completion"}: Completions request returned 326 suggestions in 19.429199ms
2025-12-02 09:50:37.048968976 DEBUG request{id=20 method="textDocument/completion"}: Completions request returned 326 suggestions in 18.763721ms
```

So there still seems to be significant caching happening?

Code after:
/// Returns a flat list of *only global* symbols in the file given.
///
/// While callers can convert this into a hierarchical collection of
/// symbols, it won't result in anything meaningful since the flat list
/// returned doesn't include children.
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: File) -> FlatSymbols {
    let parsed = parsed_module(db, file);
    let module = parsed.load(db);

    let mut visitor = SymbolVisitor {
        symbols: IndexVec::new(),
        symbol_stack: vec![],
        in_function: false,
        global_only: true,
    };
    visitor.visit_body(&module.syntax().body);

    if file
        .path(db)
        .as_system_path()
        .is_none_or(|path| !db.project().is_file_included(db, path))
    {
        // Eagerly clear ASTs of third party files.
        parsed.clear();
    }

    FlatSymbols {
        symbols: visitor.symbols,
    }
}

#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
struct SymbolTree {
    parent: Option<SymbolId>,
    name: String,
    kind: SymbolKind,
    name_range: TextRange,
    full_range: TextRange,
}

/// A visitor over all symbols in a single file.
///
/// This guarantees that child symbols have a symbol ID greater
/// than all of its parents.
struct SymbolVisitor {
    symbols: IndexVec<SymbolId, SymbolTree>,
    symbol_stack: Vec<SymbolId>,
    /// Track if we're currently inside a function (to exclude local variables)
    in_function: bool,
    global_only: bool,