Skip to main content

gdscript_hir/
def.rs

1//! Canonical symbol identity + cursor classification (Playbook §3.M5) — the basis of cross-file
2//! navigation (find-references, rename, goto-definition).
3//!
4//! [`GodotDef`] is the analyzer's analogue of rust-analyzer's `Definition`: a **stable identity**
5//! for a renameable/findable symbol, keyed on declaration site (file + name / body location),
6//! **never on the name string alone**. [`classify`] is the inverse of inference — it does the same
7//! local → member → inherited → global → autoload → engine lookup [`crate::infer`] does, but
8//! returns the *declaration identity* instead of the type. Find-references resolves the cursor to
9//! a `GodotDef`, then keeps only other tokens that classify to the **same** `GodotDef` (resolve,
10//! don't string-match), so two unrelated `i`s, `A.update` vs `B.update`, or a local shadowing a
11//! member are distinct by construction.
12//!
13//! GDScript forbids two same-named members in one class, so a [`GodotDef::Member`] is identified by
14//! `(owner_file, name)` alone — no member *kind* in the identity (which keeps decl-site and
15//! reference-site classification consistent; the kind is recovered from the item tree for display).
16
17use gdscript_base::{FileId, FilePosition, TextRange};
18use gdscript_db::{Db, FileText, parse};
19use gdscript_syntax::{GdNode, GdToken, SyntaxKind, ast};
20use smol_str::SmolStr;
21
22use crate::cst;
23use crate::ty::Ty;
24
25/// The canonical identity of a findable / renameable symbol. Equality is on **identity**, not the
26/// name string (rust-analyzer's `Definition`).
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum GodotDef {
29    /// A `class_name` global. Identity = the one file that declares it.
30    Global {
31        /// The declaring file.
32        decl_file: FileId,
33        /// The class name.
34        name: SmolStr,
35    },
36    /// A script member (func / var / const / signal / enum / inner class). Identity = the script
37    /// file that *declares* it (for an inherited member, the base file where it is found) + name.
38    Member {
39        /// The file declaring the member.
40        owner_file: FileId,
41        /// The member name.
42        name: SmolStr,
43    },
44    /// A local binding (var / param / `for`-var). Identity = the owning function body + the
45    /// binding's declaration-site name range. Two `i`s in different functions, or a local
46    /// shadowing a member, are distinct by construction.
47    Local {
48        /// The file the body lives in.
49        body_file: FileId,
50        /// The enclosing function/initializer unit's range.
51        body_range: TextRange,
52        /// The binding's declaration name-token range.
53        decl_name_range: TextRange,
54    },
55    /// An autoload **singleton** (the `*`-flagged `[autoload]` name; project-unique).
56    Autoload {
57        /// The autoload name.
58        name: SmolStr,
59        /// The `.gd` it points to, if resolvable (`None` for a `.tscn`/non-`.gd` target).
60        target_file: Option<FileId>,
61    },
62    /// An engine / builtin symbol (`Node`, `Vector2`, a builtin func, …) — resolved, but **not**
63    /// ours to rename, and find-references over it is out of scope. Distinguishes "resolved, it's
64    /// engine" from "unresolved" (the latter is `None`).
65    Engine {
66        /// The engine symbol name.
67        name: SmolStr,
68    },
69}
70
71impl GodotDef {
72    /// The symbol's name — the cheap text pre-filter key for find-references.
73    #[must_use]
74    pub fn name(&self) -> &str {
75        match self {
76            Self::Global { name, .. }
77            | Self::Member { name, .. }
78            | Self::Autoload { name, .. }
79            | Self::Engine { name } => name,
80            Self::Local { .. } => "", // filled by the caller from the decl range
81        }
82    }
83
84    /// Whether this symbol can be renamed at all (engine/builtin symbols cannot).
85    #[must_use]
86    pub fn is_renameable(&self) -> bool {
87        !matches!(self, Self::Engine { .. })
88    }
89}
90
91/// Classify the symbol the cursor (`pos`) sits on — the single entry point find-references and
92/// goto-definition share. `None` for a non-identifier token, or a reference whose target cannot be
93/// resolved (the seam — we never guess an identity).
94#[must_use]
95pub fn classify(db: &dyn Db, pos: FilePosition) -> Option<GodotDef> {
96    let ft = db.file_text(pos.file)?;
97    let root = parse(db, ft).syntax_node();
98    let tok = ast::token_at(&root, pos.offset.into())?;
99    if tok.kind() != SyntaxKind::Ident {
100        return None; // keywords / punctuation are not symbols
101    }
102    let name = SmolStr::new(tok.text());
103    let tok_range = cst::token_range(&tok);
104    let parent = tok.parent();
105
106    // (A) Declaration sites: the cursor is on the `Name` token of a declaration.
107    if parent.kind() == SyntaxKind::Name
108        && let Some(def) = classify_decl(db, ft, pos.file, parent, &name, tok_range)
109    {
110        return Some(def);
111    }
112    // (A2) The head name of an `extends Base` clause. Its `Ident` is a *bare* child of the
113    //      `ExtendsClause` / `ClassNameDecl` / inner-class decl — not wrapped in a `Name` or
114    //      `TypeRef` node — so neither (A) nor (B) catches it. Resolve it as a type name so a
115    //      `class_name`'s find-references / rename includes every `extends ThatClass` reference
116    //      (else a rename silently leaves the `extends` stale — an incomplete, corrupting edit).
117    if let Some(head) = cst::extends_head_token(parent)
118        && cst::token_range(&head) == tok_range
119    {
120        return classify_type_name(db, &name);
121    }
122    // (A3) An anonymous-enum variant declaration (`enum { FIRE }`): a bare `Ident` under an
123    //      `EnumVariant` whose enum has no name. Such a variant is a class-level `int` constant, so
124    //      it shares the Member identity space — resolve to Member{file, name} so find-refs / goto
125    //      reach it. (A *named* enum's variants are accessed as `Enum.NAME`, not as bare class-level
126    //      names, so they are out of scope here.)
127    if parent.kind() == SyntaxKind::EnumVariant && in_anon_enum(parent) {
128        return Some(GodotDef::Member {
129            owner_file: pos.file,
130            name,
131        });
132    }
133    // (B) A type reference (`var x: Foo`, `is Foo`, `as Foo`): the token is inside a `TypeRef`.
134    //     Resolve the type name to a class_name global or an engine class.
135    if has_ancestor(&tok, SyntaxKind::TypeRef) {
136        return classify_type_name(db, &name);
137    }
138    // (C) A reference inside a function body / field initializer (a `NameRef`, or the member token
139    //     of a `FieldExpr`). Resolve through the inference units.
140    classify_body_ref(db, ft, pos.file, pos.offset, &name)
141}
142
143/// Classify a declaration-site name (`parent` is the `Name` node; its parent is the decl).
144fn classify_decl(
145    db: &dyn Db,
146    ft: FileText,
147    file: FileId,
148    name_node: &GdNode,
149    name: &SmolStr,
150    tok_range: TextRange,
151) -> Option<GodotDef> {
152    let decl = name_node.parent()?;
153    // A `var`/`const` nested inside a function, a property accessor (`get`/`set`), or a lambda body
154    // is a LOCAL — not a class member. (A `FuncDecl` ancestor alone misses an accessor-body or a
155    // class-level-lambda-body local, which would otherwise be mis-typed as a `Member`.)
156    let in_body = node_has_ancestor(decl, SyntaxKind::FuncDecl)
157        || node_has_ancestor(decl, SyntaxKind::Getter)
158        || node_has_ancestor(decl, SyntaxKind::Setter)
159        || node_has_ancestor(decl, SyntaxKind::LambdaExpr);
160    // A declaration nested inside a `class Inner:` body is an inner-class member. Its `(file, name)`
161    // identity would collide with a same-named TOP-LEVEL member, letting find-refs / rename cross
162    // between two unrelated classes (a silent corrupting edit). Inner-class member identity isn't
163    // modeled yet (item_tree stores inner members separately), so treat them as out of scope —
164    // navigation refuses rather than mis-resolves. (The inner class's own *name* is unaffected: its
165    // decl node IS the `InnerClassDecl`, whose ancestor walk starts *above* it.)
166    let in_inner_class = node_has_ancestor(decl, SyntaxKind::InnerClassDecl);
167    match decl.kind() {
168        SyntaxKind::ClassNameDecl => Some(GodotDef::Global {
169            decl_file: file,
170            name: name.clone(),
171        }),
172        // A parameter, `for`-loop variable, or `match`-pattern `var` capture is always a local.
173        SyntaxKind::Param | SyntaxKind::ForStmt | SyntaxKind::PatternBind => {
174            local_def(db, ft, file, tok_range)
175        }
176        // A `var`/`const` inside a body is a local.
177        SyntaxKind::VarDecl | SyntaxKind::ConstDecl if in_body => {
178            local_def(db, ft, file, tok_range)
179        }
180        // Otherwise a class-level member — but only of the top-level class (inner-class members are
181        // out of scope, see above).
182        SyntaxKind::FuncDecl
183        | SyntaxKind::SignalDecl
184        | SyntaxKind::EnumDecl
185        | SyntaxKind::InnerClassDecl
186        | SyntaxKind::VarDecl
187        | SyntaxKind::ConstDecl
188            if !in_inner_class =>
189        {
190            Some(GodotDef::Member {
191                owner_file: file,
192                name: name.clone(),
193            })
194        }
195        _ => None,
196    }
197}
198
199/// Build a [`GodotDef::Local`] for the binding whose decl-name is at `tok_range`. The identity uses
200/// the **binding's** `name_range` (via `binding_at`), so a declaration cursor and a reference (which
201/// resolves to the same binding) produce the *same* `Local` — even if the raw token range and the
202/// lowered binding range differ.
203fn local_def(db: &dyn Db, ft: FileText, file: FileId, tok_range: TextRange) -> Option<GodotDef> {
204    let fi = crate::queries::analyze_file(db, ft);
205    let unit = fi.unit_at(tok_range.start)?;
206    let binding = unit.result.binding_at(tok_range.start)?;
207    Some(GodotDef::Local {
208        body_file: file,
209        body_range: unit.range,
210        decl_name_range: trim_range(ft.text(db), binding.name_range),
211    })
212}
213
214/// A binding's `name_range` can include leading whitespace (a body-lowering quirk); trim it to the
215/// bare identifier so a `Local`'s identity and a rename's edit range are both exact.
216fn trim_range(text: &str, nr: TextRange) -> TextRange {
217    match text.get(nr.start as usize..nr.end as usize) {
218        Some(s) => {
219            let lead = u32::try_from(s.len() - s.trim_start().len()).unwrap_or(0);
220            let len = u32::try_from(s.trim().len()).unwrap_or(0);
221            TextRange::new(nr.start + lead, nr.start + lead + len)
222        }
223        None => nr,
224    }
225}
226
227/// Resolve a bare type name (in a `TypeRef`) to a `class_name` global or an engine class.
228fn classify_type_name(db: &dyn Db, name: &SmolStr) -> Option<GodotDef> {
229    let api = db.engine()?;
230    match crate::resolve::resolve_type_name(db, api, name) {
231        Ty::ScriptRef(sref) => Some(GodotDef::Global {
232            decl_file: FileId(sref.0),
233            name: name.clone(),
234        }),
235        Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
236        _ => None,
237    }
238}
239
240/// Classify a reference inside a function/initializer body (a `NameRef`, or a `FieldExpr` member).
241fn classify_body_ref(
242    db: &dyn Db,
243    ft: FileText,
244    file: FileId,
245    offset: u32,
246    name: &SmolStr,
247) -> Option<GodotDef> {
248    let fi = crate::queries::analyze_file(db, ft);
249    let unit = fi.unit_at(offset)?;
250    let eid = unit.body.source_map.expr_at_offset(offset)?;
251    match unit.body.expr(eid) {
252        crate::body::Expr::Name(n) if n == name => {
253            resolve_name_to_def(db, ft, file, offset, unit, name)
254        }
255        crate::body::Expr::Field {
256            receiver,
257            name: fname,
258            name_range,
259        } if fname == name && name_range.start <= offset && offset < name_range.end => {
260            // `self.member` consults this file's own/inherited members (self's static type is the
261            // *base*, so we must resolve it as an own member, like `infer_field` does).
262            if matches!(unit.body.expr(*receiver), crate::body::Expr::SelfExpr) {
263                return member_owner(db, crate::ty::ScriptRefId(file.0), name, 0).map(|owner| {
264                    GodotDef::Member {
265                        owner_file: owner,
266                        name: name.clone(),
267                    }
268                });
269            }
270            let recv_ty = unit.result.type_of(*receiver)?;
271            match recv_ty {
272                Ty::ScriptRef(sref) => {
273                    member_owner(db, *sref, name, 0).map(|owner| GodotDef::Member {
274                        owner_file: owner,
275                        name: name.clone(),
276                    })
277                }
278                Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
279                _ => None, // uninformative receiver — cannot prove identity
280            }
281        }
282        _ => None,
283    }
284}
285
286/// Replicate [`crate::infer`]'s bare-name lookup order, returning the *declaration identity*:
287/// local → own/inherited member → engine global → `class_name` global → autoload. `offset` is the
288/// reference site, used to pick the correct binding when a name is shadowed (lexical scoping).
289fn resolve_name_to_def(
290    db: &dyn Db,
291    ft: FileText,
292    file: FileId,
293    offset: u32,
294    unit: &crate::infer::Unit,
295    name: &SmolStr,
296) -> Option<GodotDef> {
297    // 1. A local binding in this unit (var / param / for-var / match-capture). A name can be
298    //    shadowed (a param and a same-named local, or a re-declared `var`), so pick the
299    //    nearest-PRECEDING declaration — the binding with the greatest start `<=` the reference
300    //    offset — mirroring GDScript's lexical shadowing. (First-by-iteration would pick the
301    //    outermost, conflating two distinct locals and corrupting a rename.) The binding
302    //    `name_range` may carry leading whitespace, so trim before comparing / recording.
303    let text = ft.text(db);
304    let mut best: Option<TextRange> = None;
305    for b in &unit.result.bindings {
306        if !matches!(
307            b.kind,
308            crate::infer::BindingKind::Var
309                | crate::infer::BindingKind::Param
310                | crate::infer::BindingKind::ForVar
311                | crate::infer::BindingKind::MatchBind
312        ) {
313            continue;
314        }
315        let nr = trim_range(text, b.name_range);
316        if text.get(nr.start as usize..nr.end as usize) != Some(name.as_str()) {
317            continue;
318        }
319        if nr.start <= offset && best.is_none_or(|cur| nr.start >= cur.start) {
320            best = Some(nr);
321        }
322    }
323    if let Some(nr) = best {
324        return Some(GodotDef::Local {
325            body_file: file,
326            body_range: unit.range,
327            decl_name_range: nr,
328        });
329    }
330    // 2/3. Own or inherited member (walk this script's extends chain).
331    if let Some(owner) = member_owner(db, crate::ty::ScriptRefId(file.0), name, 0) {
332        return Some(GodotDef::Member {
333            owner_file: owner,
334            name: name.clone(),
335        });
336    }
337    // 4. An engine global (builtin / native class / singleton / utility / enum) — before
338    //    `class_name`, matching `resolve_name`'s precedence.
339    if let Some(api) = db.engine()
340        && crate::resolve::resolve_global(api, name).is_some()
341    {
342        return Some(GodotDef::Engine { name: name.clone() });
343    }
344    // 5. A `class_name` global.
345    if let Some(root) = db.source_root()
346        && let Some(decl) = crate::queries::global_registry(db, root).resolve(name)
347    {
348        return Some(GodotDef::Global {
349            decl_file: decl.file_id(db),
350            name: name.clone(),
351        });
352    }
353    // 6. An autoload singleton.
354    if let Some(config) = db.project_config()
355        && let Some(path) = crate::queries::autoload_registry(db, config)
356            .resolve_path(name)
357            .cloned()
358    {
359        let target = db.source_root().and_then(|root| {
360            crate::queries::res_path_registry(db, root)
361                .get(path.as_str())
362                .copied()
363        });
364        return Some(GodotDef::Autoload {
365            name: name.clone(),
366            target_file: target,
367        });
368    }
369    None
370}
371
372/// The file that *declares* member `name` for the script in `sref`, walking the `extends` chain
373/// (own members first, then user bases). Depth-bounded like the inference member walk.
374fn member_owner(
375    db: &dyn Db,
376    sref: crate::ty::ScriptRefId,
377    name: &str,
378    depth: u32,
379) -> Option<FileId> {
380    if depth > 32 {
381        return None;
382    }
383    let file = db.file_text(FileId(sref.0))?;
384    let tree = crate::queries::item_tree(db, file);
385    // An own member, OR an anonymous-enum variant (a class-level `int` constant that the member
386    // table doesn't expose — its enum has no name and its variants aren't `Member`s).
387    if tree.member(name).is_some() || anon_enum_has_variant(&tree, name) {
388        return Some(file.file_id(db));
389    }
390    match crate::queries::script_class(db, file).base() {
391        Ty::ScriptRef(base) => member_owner(db, *base, name, depth + 1),
392        _ => None, // engine base member, or none — not a user-declared member
393    }
394}
395
396/// Whether `tree` declares `name` as a variant of an **anonymous** `enum { … }` (a flattened
397/// class-level `int` constant). Named-enum variants are excluded — they are accessed as `Enum.NAME`,
398/// not as bare class-level names.
399fn anon_enum_has_variant(tree: &crate::item_tree::ItemTree, name: &str) -> bool {
400    tree.members.iter().any(|m| {
401        matches!(m, crate::item_tree::Member::Enum(e)
402            if e.name.is_none() && e.variants.iter().any(|v| v == name))
403    })
404}
405
406/// Whether `enum_variant` (an `EnumVariant` node) belongs to an anonymous `enum { … }` (no name).
407fn in_anon_enum(enum_variant: &GdNode) -> bool {
408    enum_variant.parent().is_some_and(|enum_decl| {
409        enum_decl.kind() == SyntaxKind::EnumDecl
410            && !enum_decl.children().any(|c| c.kind() == SyntaxKind::Name)
411    })
412}
413
414/// Whether `tok` has an ancestor node of `kind`.
415fn has_ancestor(tok: &GdToken, kind: SyntaxKind) -> bool {
416    node_has_ancestor_or_self(tok.parent(), kind)
417}
418
419/// Whether `node` itself or any ancestor is of `kind`.
420fn node_has_ancestor(node: &GdNode, kind: SyntaxKind) -> bool {
421    node.parent()
422        .is_some_and(|p| node_has_ancestor_or_self(p, kind))
423}
424
425fn node_has_ancestor_or_self(node: &GdNode, kind: SyntaxKind) -> bool {
426    let mut cur = Some(node.clone());
427    while let Some(n) = cur {
428        if n.kind() == kind {
429            return true;
430        }
431        cur = n.parent().cloned();
432    }
433    false
434}
435
436// ---- M2: node-path navigation (go-to-definition into the `.tscn`) -------------------------
437
438/// A `$Path`/`%Unique`/`get_node("…")` resolved to its scene-node declaration — for go-to-definition
439/// **into the owning `.tscn`** (the `[node …]` line). The inverse of M1's node-path typing.
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct NodePathTarget {
442    /// The owning scene's file.
443    pub scene: FileId,
444    /// The resolved node's name.
445    pub node_name: SmolStr,
446    /// Byte span of the whole `[node …]` header line.
447    pub header_span: TextRange,
448    /// Byte span of the `name="…"` value (the finer focus).
449    pub name_span: TextRange,
450}
451
452/// If the cursor sits on a node-path expression (`$Path`/`%Unique`/`get_node("…")`) that resolves
453/// against the owning scene, the target node's declaration in the `.tscn`. `None` otherwise.
454#[must_use]
455pub fn node_path_target(db: &dyn Db, pos: FilePosition) -> Option<NodePathTarget> {
456    let ft = db.file_text(pos.file)?;
457    let fi = crate::queries::analyze_file(db, ft);
458    let unit = fi.unit_at(pos.offset)?;
459    let eid = unit.body.source_map.expr_at_offset(pos.offset)?;
460    let crate::body::Expr::GetNode {
461        path: Some(path),
462        unique,
463    } = unit.body.expr(eid)
464    else {
465        return None;
466    };
467    let ctx = crate::queries::scene_context(db, ft)?;
468    let idx = if *unique {
469        ctx.model.resolve_unique(path)
470    } else {
471        ctx.model.resolve_path_from(ctx.attach, path)
472    }?;
473    let node = ctx.model.node(idx)?;
474    Some(NodePathTarget {
475        scene: ctx.scene,
476        node_name: node.name.clone(),
477        header_span: node.header_span,
478        name_span: node.name_span,
479    })
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use gdscript_db::RootDatabase;
486    use salsa::Durability;
487
488    fn db_with(files: &[(u32, &str)]) -> RootDatabase {
489        let mut db = RootDatabase::default();
490        for (id, src) in files {
491            db.set_file_text(FileId(*id), src, Durability::LOW);
492        }
493        db.sync_source_root();
494        db
495    }
496
497    fn at(db: &RootDatabase, file: u32, needle: &str, src: &str) -> Option<GodotDef> {
498        let offset = u32::try_from(src.find(needle).expect("needle")).unwrap();
499        classify(
500            db,
501            FilePosition {
502                file: FileId(file),
503                offset,
504            },
505        )
506    }
507
508    /// classify at the byte offset of the `nth` (0-based) occurrence of `needle`.
509    fn at_nth(db: &RootDatabase, file: u32, needle: &str, n: usize, src: &str) -> Option<GodotDef> {
510        let off = src.match_indices(needle).nth(n).expect("nth needle").0;
511        classify(
512            db,
513            FilePosition {
514                file: FileId(file),
515                offset: u32::try_from(off).unwrap(),
516            },
517        )
518    }
519
520    #[test]
521    fn two_unrelated_locals_are_distinct() {
522        let src =
523            "func a():\n\tvar i := 1\n\tvar ra := i\nfunc b():\n\tvar i := 2\n\tvar rb := i\n";
524        let db = db_with(&[(0, src)]);
525        // The `i` reference in a() (`ra := i`) vs in b() (`rb := i`) — the two `:= i` sites.
526        let off_a = u32::try_from(src.match_indices(":= i").next().unwrap().0 + 3).unwrap();
527        let off_b = u32::try_from(src.match_indices(":= i").nth(1).unwrap().0 + 3).unwrap();
528        let da = classify(
529            &db,
530            FilePosition {
531                file: FileId(0),
532                offset: off_a,
533            },
534        )
535        .unwrap();
536        let dbf = classify(
537            &db,
538            FilePosition {
539                file: FileId(0),
540                offset: off_b,
541            },
542        )
543        .unwrap();
544        assert!(matches!(da, GodotDef::Local { .. }), "{da:?}");
545        assert!(matches!(dbf, GodotDef::Local { .. }), "{dbf:?}");
546        assert_ne!(da, dbf, "two unrelated `i`s must be distinct locals");
547    }
548
549    #[test]
550    fn local_shadowing_a_member_is_distinct() {
551        let src = "var pos := 1\nfunc f():\n\tvar pos := 2\n\tprint(pos)\n";
552        let db = db_with(&[(0, src)]);
553        // The member decl `var pos` (1st "pos") vs the local `var pos` (2nd "pos").
554        let member = at_nth(&db, 0, "pos", 0, src).unwrap();
555        let local = at_nth(&db, 0, "pos", 1, src).unwrap();
556        assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
557        assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
558        assert_ne!(member, local);
559        // The reference `pos` in `print(pos)` (3rd "pos") resolves to the LOCAL (scope wins).
560        let r = at_nth(&db, 0, "pos", 2, src).unwrap();
561        assert_eq!(r, local);
562    }
563
564    #[test]
565    fn same_named_members_of_different_classes_are_distinct() {
566        let a = "class_name A\nfunc update():\n\tpass\n";
567        let b = "class_name B\nfunc update():\n\tpass\n";
568        let db = db_with(&[(0, a), (1, b)]);
569        let ua = at(&db, 0, "update", a).unwrap();
570        let ub = at(&db, 1, "update", b).unwrap();
571        assert!(matches!(ua, GodotDef::Member { .. }));
572        assert!(matches!(ub, GodotDef::Member { .. }));
573        assert_ne!(ua, ub, "A.update and B.update must be distinct");
574    }
575
576    #[test]
577    fn class_name_decl_and_reference_classify_to_the_same_global() {
578        let widget = "class_name Widget\nfunc make() -> int:\n\treturn 1\n";
579        let user = "func f():\n\tvar w: Widget\n\tvar x := Widget.new()\n";
580        let db = db_with(&[(0, widget), (1, user)]);
581        let decl = at(&db, 0, "Widget", widget).unwrap();
582        let ann = at(&db, 1, "Widget\n", user).unwrap(); // the annotation `: Widget`
583        let ctor = at(&db, 1, "Widget.new", user).unwrap();
584        assert!(matches!(
585            decl,
586            GodotDef::Global {
587                decl_file: FileId(0),
588                ..
589            }
590        ));
591        assert_eq!(decl, ann, "annotation must resolve to the class_name def");
592        assert_eq!(
593            decl, ctor,
594            "`Widget.new()` must resolve to the class_name def"
595        );
596    }
597
598    #[test]
599    fn extends_user_class_classifies_to_the_global() {
600        // The `Base` in `extends Base` is a bare Ident (not a Name/TypeRef node); it must still
601        // classify to Base's `class_name` global — else find-refs/rename of Base would miss the
602        // `extends` and leave it stale.
603        let base = "class_name Base\nfunc m():\n\tpass\n";
604        let derived = "class_name Derived\nextends Base\n";
605        let db = db_with(&[(0, base), (1, derived)]);
606        let decl = at(&db, 0, "Base", base).unwrap();
607        let ext = at(&db, 1, "Base", derived).unwrap(); // the `Base` in `extends Base`
608        assert!(matches!(
609            decl,
610            GodotDef::Global {
611                decl_file: FileId(0),
612                ..
613            }
614        ));
615        assert_eq!(
616            decl, ext,
617            "`extends Base` must classify to Base's class_name def"
618        );
619    }
620
621    #[test]
622    fn inherited_member_resolves_to_the_declaring_base() {
623        let base = "class_name Base\nfunc base_m() -> int:\n\treturn 1\n";
624        let derived = "class_name Derived\nextends Base\nfunc use_it():\n\tself.base_m()\n";
625        let db = db_with(&[(0, base), (1, derived)]);
626        let decl = at(&db, 0, "base_m", base).unwrap();
627        let call = at(&db, 1, "base_m()", derived).unwrap();
628        assert!(matches!(
629            decl,
630            GodotDef::Member {
631                owner_file: FileId(0),
632                ..
633            }
634        ));
635        assert_eq!(
636            decl, call,
637            "inherited call must resolve to the base's member def"
638        );
639    }
640
641    #[test]
642    fn inner_class_member_is_out_of_scope() {
643        // A method inside `class Inner:` must NOT share identity with a same-named top-level method
644        // (that would let rename cross between two unrelated classes). It is out of scope → None.
645        let src =
646            "class_name A\nfunc update():\n\tpass\nclass Inner:\n\tfunc update():\n\t\tpass\n";
647        let db = db_with(&[(0, src)]);
648        let top = at_nth(&db, 0, "update", 0, src).unwrap();
649        let inner = at_nth(&db, 0, "update", 1, src);
650        assert!(matches!(top, GodotDef::Member { .. }), "{top:?}");
651        assert_eq!(
652            inner, None,
653            "an inner-class member must not classify (out of scope), got {inner:?}"
654        );
655    }
656
657    #[test]
658    fn match_capture_classifies_as_local_distinct_from_member() {
659        // A `match`-captured `var cap` is a local that shadows a same-named member; a reference to
660        // it must resolve to the Local, not the member (else rename of the member would corrupt it).
661        let src = "var cap := 0\nfunc f(v):\n\tmatch v:\n\t\tvar cap:\n\t\t\tprint(cap)\n";
662        let db = db_with(&[(0, src)]);
663        let member = at_nth(&db, 0, "cap", 0, src).unwrap();
664        let capture = at_nth(&db, 0, "cap", 1, src).unwrap();
665        let usage = at_nth(&db, 0, "cap", 2, src).unwrap();
666        assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
667        assert!(matches!(capture, GodotDef::Local { .. }), "{capture:?}");
668        assert_eq!(
669            usage, capture,
670            "`print(cap)` must resolve to the match capture"
671        );
672        assert_ne!(usage, member);
673    }
674
675    #[test]
676    fn accessor_body_local_is_not_a_member() {
677        // A `var` inside a property `get`/`set` accessor is a local, never a class member.
678        let src = "var hp: int:\n\tget:\n\t\tvar tmp = 2\n\t\treturn tmp\n";
679        let db = db_with(&[(0, src)]);
680        let tmp = at_nth(&db, 0, "tmp", 0, src);
681        assert!(
682            !matches!(tmp, Some(GodotDef::Member { .. })),
683            "a local in a get/set body must not be a Member, got {tmp:?}"
684        );
685    }
686
687    #[test]
688    fn anon_enum_variant_classifies_as_member() {
689        // An anonymous-enum variant is a class-level constant; its declaration and a bare reference
690        // must classify to the same identity (so find-refs / goto reach it).
691        let src = "enum { FIRE, ICE }\nfunc f():\n\tprint(FIRE)\n";
692        let db = db_with(&[(0, src)]);
693        let decl = at_nth(&db, 0, "FIRE", 0, src).unwrap(); // enum { FIRE }
694        let usage = at_nth(&db, 0, "FIRE", 1, src).unwrap(); // print(FIRE)
695        assert!(matches!(decl, GodotDef::Member { .. }), "{decl:?}");
696        assert_eq!(
697            decl, usage,
698            "an anon-enum variant decl and use share identity"
699        );
700    }
701
702    #[test]
703    fn shadowed_local_reference_resolves_to_the_nearest_declaration() {
704        // A param `x` and a local `var x`: the `print(x)` reference must resolve to the LOCAL
705        // (nearest-preceding decl), not the param — else find-refs/rename conflates two locals.
706        let src = "func f(x):\n\tvar x := 2\n\tprint(x)\n";
707        let db = db_with(&[(0, src)]);
708        let param = at_nth(&db, 0, "x", 0, src).unwrap();
709        let local = at_nth(&db, 0, "x", 1, src).unwrap();
710        let usage = at_nth(&db, 0, "x", 2, src).unwrap();
711        assert!(matches!(param, GodotDef::Local { .. }), "{param:?}");
712        assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
713        assert_ne!(param, local, "param x and local x are distinct");
714        assert_eq!(
715            usage, local,
716            "the reference resolves to the nearest (local) declaration"
717        );
718    }
719}