Skip to main content

gdscript_hir/
item_tree.rs

1//! The item tree (Playbook §3.1): a signature-level view of one `.gd` file — its
2//! `class_name`, `extends` target, and class members (funcs/vars/consts/signals/enums/inner
3//! classes) — lowered from the CST **without reading any function body**.
4//!
5//! This "no bodies" rule is the Phase-3 cache invariant: editing a function body must not
6//! change the item tree, so signature-derived data (and everything keyed on it) can be
7//! reused across body edits once salsa lands. To keep that promise the tree holds only plain
8//! owned data plus reparse-stable [`AstPtr`]s — never live CST nodes — so it is `Eq` and a
9//! body edit that doesn't move a declaration produces an identical tree.
10
11use std::sync::Arc;
12
13use gdscript_base::TextRange;
14use gdscript_syntax::ast::{self, AstNode};
15use gdscript_syntax::{GdNode, SyntaxKind};
16use smol_str::SmolStr;
17
18use crate::cst::{self, AstPtr};
19
20/// The signature-level model of one file (or one inner class).
21#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct ItemTree {
23    /// The registered global class name (`class_name X`), if any. Always `None` for an
24    /// inner class.
25    pub class_name: Option<SmolStr>,
26    /// The `extends` target, if written.
27    pub extends: Option<ExtendsRef>,
28    /// The class members, in source order.
29    pub members: Vec<Member>,
30}
31
32impl ItemTree {
33    /// The first member named `name` (linear scan — member lists are small).
34    #[must_use]
35    pub fn member(&self, name: &str) -> Option<&Member> {
36        self.members.iter().find(|m| m.name() == Some(name))
37    }
38}
39
40/// An `extends` target. Phase 2 only resolves a bare engine-class [`ExtendsRef::Name`]; the
41/// dotted and script-path forms funnel through the Phase-3 seam to `Ty::Unknown`.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ExtendsRef {
44    /// `extends Node` — a bare identifier, resolved against the engine table (else `Unknown`).
45    Name(SmolStr),
46    /// `extends A.B` — a dotted path (namespaced / inner class); `Unknown` in Phase 2.
47    Path(SmolStr),
48    /// `extends "res://x.gd"` — a script path literal; `Unknown` in Phase 2.
49    ScriptPath(SmolStr),
50}
51
52/// One class member.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Member {
55    /// `func f(...)`.
56    Func(FuncItem),
57    /// `var x`.
58    Var(VarItem),
59    /// `const X`.
60    Const(ConstItem),
61    /// `signal s`.
62    Signal(SignalItem),
63    /// `enum E { ... }` (or an anonymous `enum { ... }`).
64    Enum(EnumItem),
65    /// `class Inner: ...`.
66    Class(InnerClassItem),
67}
68
69impl Member {
70    /// The member's declared name, or `None` for an anonymous enum.
71    #[must_use]
72    pub fn name(&self) -> Option<&str> {
73        match self {
74            Self::Func(f) => Some(&f.name),
75            Self::Var(v) => Some(&v.name),
76            Self::Const(c) => Some(&c.name),
77            Self::Signal(s) => Some(&s.name),
78            Self::Enum(e) => e.name.as_deref(),
79            Self::Class(c) => Some(&c.name),
80        }
81    }
82}
83
84/// A parameter of a function or signal.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ParamItem {
87    /// The parameter name.
88    pub name: SmolStr,
89    /// The written type annotation (unresolved text, e.g. `"int"`, `"Array[int]"`), if any.
90    pub type_ref: Option<SmolStr>,
91    /// Whether the parameter has a default value (`p := expr` / `p: T = expr`).
92    pub has_default: bool,
93}
94
95/// A `func` member (signature only — the body is lowered lazily by [`crate::body`]).
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct FuncItem {
98    /// The function name.
99    pub name: SmolStr,
100    /// The parameters, in order.
101    pub params: Vec<ParamItem>,
102    /// The written return-type annotation (unresolved text), if any.
103    pub return_type: Option<SmolStr>,
104    /// Whether this is a `static func`.
105    pub is_static: bool,
106    /// Pointer to the `FuncDecl` node, for body lowering.
107    pub ptr: AstPtr,
108    /// The whole declaration's range.
109    pub range: TextRange,
110    /// The name token's range (the navigation focus).
111    pub name_range: TextRange,
112}
113
114/// A `var` member.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct VarItem {
117    /// The variable name.
118    pub name: SmolStr,
119    /// The written type annotation (unresolved text), if any.
120    pub type_ref: Option<SmolStr>,
121    /// Whether this is a `static var`.
122    pub is_static: bool,
123    /// Whether it has an initializer expression.
124    pub has_init: bool,
125    /// Whether the type was inferred with `:=`.
126    pub is_inferred: bool,
127    /// Pointer to the `VarDecl` node, for initializer inference.
128    pub ptr: AstPtr,
129    /// The whole declaration's range.
130    pub range: TextRange,
131    /// The name token's range.
132    pub name_range: TextRange,
133}
134
135/// A `const` member.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ConstItem {
138    /// The constant name.
139    pub name: SmolStr,
140    /// The written type annotation (unresolved text), if any.
141    pub type_ref: Option<SmolStr>,
142    /// Pointer to the `ConstDecl` node, for value inference.
143    pub ptr: AstPtr,
144    /// The whole declaration's range.
145    pub range: TextRange,
146    /// The name token's range.
147    pub name_range: TextRange,
148}
149
150/// A `signal` member.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct SignalItem {
153    /// The signal name.
154    pub name: SmolStr,
155    /// The typed parameters, in order.
156    pub params: Vec<ParamItem>,
157    /// The whole declaration's range.
158    pub range: TextRange,
159    /// The name token's range.
160    pub name_range: TextRange,
161}
162
163/// An `enum` member.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct EnumItem {
166    /// The enum name, or `None` for an anonymous `enum { ... }` (whose variants become
167    /// class-level `int` constants).
168    pub name: Option<SmolStr>,
169    /// The variant names, in order.
170    pub variants: Vec<SmolStr>,
171    /// The whole declaration's range.
172    pub range: TextRange,
173    /// The name token's range (the whole `enum` keyword range for an anonymous enum).
174    pub name_range: TextRange,
175}
176
177/// An inner `class` member: its name plus its own (recursively lowered) item tree.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct InnerClassItem {
180    /// The inner class name.
181    pub name: SmolStr,
182    /// The inner class's members + `extends`.
183    pub tree: ItemTree,
184    /// The whole declaration's range.
185    pub range: TextRange,
186    /// The name token's range.
187    pub name_range: TextRange,
188}
189
190/// Lower a parsed file to its [`ItemTree`] (Playbook §3.1). Pure; reads no bodies.
191#[must_use]
192pub fn item_tree(root: &GdNode) -> Arc<ItemTree> {
193    let Some(file) = ast::SourceFile::cast(root.clone()) else {
194        return Arc::new(ItemTree::default());
195    };
196    Arc::new(lower_class(root, file.decls()))
197}
198
199/// Lower a sequence of declarations (a file body or an inner-class body) plus the `extends`
200/// clause found among `container`'s structure into an [`ItemTree`].
201fn lower_class(container: &GdNode, decls: impl Iterator<Item = ast::Decl>) -> ItemTree {
202    let mut tree = ItemTree {
203        extends: find_extends(container),
204        ..ItemTree::default()
205    };
206    for decl in decls {
207        match decl {
208            ast::Decl::ClassName(d) => {
209                if let Some(name) = decl_name(d.name()) {
210                    tree.class_name = Some(name);
211                }
212            }
213            ast::Decl::Func(d) => tree.members.push(Member::Func(lower_func(&d))),
214            ast::Decl::Var(d) => tree.members.push(Member::Var(lower_var(&d))),
215            ast::Decl::Const(d) => tree.members.push(Member::Const(lower_const(&d))),
216            ast::Decl::Signal(d) => tree.members.push(Member::Signal(lower_signal(&d))),
217            ast::Decl::Enum(d) => tree.members.push(Member::Enum(lower_enum(&d))),
218            ast::Decl::Class(d) => {
219                if let Some(item) = lower_inner_class(&d) {
220                    tree.members.push(Member::Class(item));
221                }
222            }
223        }
224    }
225    tree
226}
227
228fn lower_func(d: &ast::FuncDecl) -> FuncItem {
229    let node = d.syntax();
230    FuncItem {
231        name: decl_name(d.name()).unwrap_or_default(),
232        params: d
233            .param_list()
234            .map(|pl| lower_params(&pl))
235            .unwrap_or_default(),
236        return_type: d.return_type().and_then(|t| t.text()).map(SmolStr::new),
237        is_static: d.is_static(),
238        ptr: AstPtr::of(node),
239        range: cst::text_range_of(node),
240        name_range: name_range(d.name(), node),
241    }
242}
243
244fn lower_var(d: &ast::VarDecl) -> VarItem {
245    let node = d.syntax();
246    VarItem {
247        name: decl_name(d.name()).unwrap_or_default(),
248        type_ref: d.type_ref().and_then(|t| t.text()).map(SmolStr::new),
249        is_static: d.is_static(),
250        has_init: cst::first_child_expr(node).is_some(),
251        is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
252        ptr: AstPtr::of(node),
253        range: cst::text_range_of(node),
254        name_range: name_range(d.name(), node),
255    }
256}
257
258fn lower_const(d: &ast::ConstDecl) -> ConstItem {
259    let node = d.syntax();
260    // The annotation, if any, is the `TypeRef` child (the AST exposes no accessor on
261    // `ConstDecl`, so read it directly).
262    let type_ref = cst::first_child(node, |k| k == SyntaxKind::TypeRef)
263        .and_then(ast::TypeRef::cast)
264        .and_then(|t| t.text())
265        .map(SmolStr::new);
266    ConstItem {
267        name: decl_name(d.name()).unwrap_or_default(),
268        type_ref,
269        ptr: AstPtr::of(node),
270        range: cst::text_range_of(node),
271        name_range: name_range(d.name(), node),
272    }
273}
274
275fn lower_signal(d: &ast::SignalDecl) -> SignalItem {
276    let node = d.syntax();
277    SignalItem {
278        name: decl_name(d.name()).unwrap_or_default(),
279        params: d
280            .param_list()
281            .map(|pl| lower_params(&pl))
282            .unwrap_or_default(),
283        range: cst::text_range_of(node),
284        name_range: name_range(d.name(), node),
285    }
286}
287
288fn lower_enum(d: &ast::EnumDecl) -> EnumItem {
289    let node = d.syntax();
290    EnumItem {
291        name: decl_name(d.name()),
292        variants: d
293            .variants()
294            .filter_map(|v| v.text())
295            .map(SmolStr::new)
296            .collect(),
297        range: cst::text_range_of(node),
298        name_range: name_range(d.name(), node),
299    }
300}
301
302fn lower_inner_class(d: &ast::InnerClassDecl) -> Option<InnerClassItem> {
303    let node = d.syntax();
304    let name = decl_name(d.name())?;
305    let mut tree = d
306        .body()
307        .map(|b| lower_class(b.syntax(), b.decls()))
308        .unwrap_or_default();
309    // An inner class inlines its `extends` directly on the decl (no `ExtendsClause` wrapper),
310    // so resolve it from the decl node rather than the (empty) body result.
311    tree.extends = find_extends(node);
312    Some(InnerClassItem {
313        name,
314        tree,
315        range: cst::text_range_of(node),
316        name_range: name_range(d.name(), node),
317    })
318}
319
320fn lower_params(pl: &ast::ParamList) -> Vec<ParamItem> {
321    pl.params()
322        .map(|p| ParamItem {
323            name: decl_name(p.name()).unwrap_or_default(),
324            type_ref: p.type_ref().and_then(|t| t.text()).map(SmolStr::new),
325            has_default: cst::has_token(p.syntax(), SyntaxKind::ColonEq)
326                || cst::has_token(p.syntax(), SyntaxKind::Eq)
327                || cst::first_child_expr(p.syntax()).is_some(),
328        })
329        .collect()
330}
331
332/// Find the `extends` target of `container`, in either of the two CST shapes the parser
333/// produces: the top-level form wraps it in an `ExtendsClause` child node, while an inner
334/// class inlines the `extends` keyword + target tokens directly on the `InnerClassDecl`. In
335/// both shapes the target tokens (a `String`, or `Ident` (`.` `Ident`)*) are *direct* tokens
336/// of the node we parse — the class name is wrapped in a `Name` node, never a bare token.
337fn find_extends(container: &GdNode) -> Option<ExtendsRef> {
338    if let Some(clause) = cst::first_child(container, |k| k == SyntaxKind::ExtendsClause) {
339        return parse_extends_tokens(&clause);
340    }
341    if cst::has_token(container, SyntaxKind::ExtendsKw) {
342        return parse_extends_tokens(container);
343    }
344    None
345}
346
347/// Parse the `extends` target from a node's direct tokens.
348fn parse_extends_tokens(node: &GdNode) -> Option<ExtendsRef> {
349    // A string literal path: `extends "res://x.gd"`.
350    if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
351        return Some(ExtendsRef::ScriptPath(SmolStr::new(
352            s.trim_matches(['"', '\'']),
353        )));
354    }
355    // Otherwise one or more dotted identifiers: `extends Node` / `extends A.B`.
356    let idents: Vec<String> = node
357        .children_with_tokens()
358        .filter_map(cstree::util::NodeOrToken::into_token)
359        .filter(|t| t.kind() == SyntaxKind::Ident)
360        .map(|t| t.text().to_owned())
361        .collect();
362    match idents.len() {
363        0 => None,
364        1 => Some(ExtendsRef::Name(SmolStr::new(&idents[0]))),
365        _ => Some(ExtendsRef::Path(SmolStr::new(idents.join(".")))),
366    }
367}
368
369fn decl_name(name: Option<ast::Name>) -> Option<SmolStr> {
370    name.and_then(|n| n.text()).map(SmolStr::new)
371}
372
373/// The focus range: the name token's range, or the whole declaration's range as a fallback
374/// (anonymous enums, recovered declarations).
375///
376/// The lossless tree flushes the inter-token whitespace *before* the identifier into the `Name`
377/// node (the `Name` marker opens before the `Ident`'s advance), so `Name`'s own range carries a
378/// leading-space. Trim it to the bare identifier — navigation uses this as a symbol's focus range
379/// and to tag its own declaration in find-references, both of which must be the exact identifier.
380fn name_range(name: Option<ast::Name>, decl: &GdNode) -> TextRange {
381    name.map_or_else(
382        || cst::text_range_of(decl),
383        |n| trimmed_name_range(n.syntax()),
384    )
385}
386
387/// `Name`'s range with the leading whitespace trivia stripped (see [`name_range`]). A `Name` is
388/// `[leading-trivia][Ident]` — no trailing trivia — so trimming the front yields the identifier.
389fn trimmed_name_range(name_node: &GdNode) -> TextRange {
390    let r = cst::text_range_of(name_node);
391    let text = name_node.text().to_string();
392    let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
393    let len = u32::try_from(text.trim().len()).unwrap_or(0);
394    TextRange::new(r.start + lead, r.start + lead + len)
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use gdscript_syntax::parse;
401
402    fn tree_of(src: &str) -> Arc<ItemTree> {
403        item_tree(&parse(src).syntax_node())
404    }
405
406    #[test]
407    fn class_header_and_members() {
408        let tree = tree_of(
409            "class_name Foo\nextends Node2D\nconst K = 1\nvar x: int\nstatic var s := 2\nsignal hit(dmg: int)\nenum E { A, B }\nfunc f(a: int, b := 1) -> void:\n\tpass\n",
410        );
411        assert_eq!(tree.class_name.as_deref(), Some("Foo"));
412        assert_eq!(tree.extends, Some(ExtendsRef::Name(SmolStr::new("Node2D"))));
413        let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
414        assert_eq!(names, vec!["K", "x", "s", "hit", "E", "f"]);
415    }
416
417    #[test]
418    fn func_signature() {
419        let tree = tree_of("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
420        let Member::Func(f) = &tree.members[0] else {
421            panic!("expected func")
422        };
423        assert_eq!(f.name, "add");
424        assert_eq!(f.return_type.as_deref(), Some("int"));
425        assert_eq!(f.params.len(), 2);
426        assert_eq!(f.params[0].type_ref.as_deref(), Some("int"));
427        assert!(!f.params[0].has_default);
428        assert!(f.params[1].has_default);
429    }
430
431    #[test]
432    fn var_init_and_inference_flags() {
433        let tree = tree_of("var a: int = 1\nvar b := 2\nvar c\nvar d = 3\n");
434        let vars: Vec<&VarItem> = tree
435            .members
436            .iter()
437            .filter_map(|m| match m {
438                Member::Var(v) => Some(v),
439                _ => None,
440            })
441            .collect();
442        // a: explicit type, has init, not inferred
443        assert_eq!(vars[0].type_ref.as_deref(), Some("int"));
444        assert!(vars[0].has_init && !vars[0].is_inferred);
445        // b: `:=` inferred, has init, no annotation
446        assert!(vars[1].type_ref.is_none() && vars[1].has_init && vars[1].is_inferred);
447        // c: no init, no annotation
448        assert!(!vars[2].has_init && vars[2].type_ref.is_none());
449        // d: untyped with init
450        assert!(vars[3].has_init && !vars[3].is_inferred && vars[3].type_ref.is_none());
451    }
452
453    #[test]
454    fn extends_script_path() {
455        let tree = tree_of("extends \"res://player.gd\"\n");
456        assert_eq!(
457            tree.extends,
458            Some(ExtendsRef::ScriptPath(SmolStr::new("res://player.gd")))
459        );
460    }
461
462    #[test]
463    fn anonymous_enum_has_no_name_but_variants() {
464        let tree = tree_of("enum { RED, GREEN, BLUE }\n");
465        let Member::Enum(e) = &tree.members[0] else {
466            panic!("expected enum")
467        };
468        assert!(e.name.is_none());
469        assert_eq!(
470            e.variants,
471            vec![
472                SmolStr::new("RED"),
473                SmolStr::new("GREEN"),
474                SmolStr::new("BLUE")
475            ]
476        );
477    }
478
479    #[test]
480    fn inner_class_members_and_extends() {
481        let tree = tree_of("class Inner extends RefCounted:\n\tvar y = 2\n\tfunc m():\n\t\tpass\n");
482        let Member::Class(inner) = &tree.members[0] else {
483            panic!("expected inner class")
484        };
485        assert_eq!(inner.name, "Inner");
486        let names: Vec<_> = inner.tree.members.iter().filter_map(Member::name).collect();
487        assert_eq!(names, vec!["y", "m"]);
488        assert_eq!(
489            inner.tree.extends,
490            Some(ExtendsRef::Name(SmolStr::new("RefCounted")))
491        );
492    }
493
494    #[test]
495    fn ptr_round_trips_to_node() {
496        let parse = parse("func f():\n\tpass\n");
497        let root = parse.syntax_node();
498        let tree = item_tree(&root);
499        let Member::Func(f) = &tree.members[0] else {
500            panic!()
501        };
502        let node = f.ptr.to_node(&root).expect("func node recovered");
503        assert_eq!(node.kind(), SyntaxKind::FuncDecl);
504    }
505}