Skip to main content

gdscript_hir/
cst.rs

1//! Low-level helpers over the Phase-1 cstree CST, shared by the HIR lowering passes
2//! ([`crate::item_tree`], [`crate::body`], [`crate::infer`]).
3//!
4//! The typed AST ([`gdscript_syntax::ast`]) only models *declarations*; expressions and
5//! statements are walked here at the raw [`GdNode`]/[`SyntaxKind`] level. Everything in
6//! this module is a pure function of the tree — no interning, no engine API.
7
8use cstree::util::NodeOrToken;
9use gdscript_base::TextRange;
10use gdscript_syntax::{GdNode, GdToken, SyntaxKind};
11
12/// A reparse-stable pointer to a syntax node — its [`SyntaxKind`] plus byte [`TextRange`]
13/// (rust-analyzer's `SyntaxNodePtr`). Because it is plain `Copy` data keyed on text
14/// position, identical source re-parses to the identical pointer, which is what lets the
15/// [`crate::item_tree::ItemTree`] stay `Eq` while still being able to recover the CST node
16/// for deferred body lowering / initializer inference.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct AstPtr {
19    /// The pointed-to node's kind.
20    pub kind: SyntaxKind,
21    /// The pointed-to node's byte range.
22    pub range: TextRange,
23}
24
25impl AstPtr {
26    /// The pointer to `node`.
27    #[must_use]
28    pub fn of(node: &GdNode) -> Self {
29        Self {
30            kind: node.kind(),
31            range: text_range_of(node),
32        }
33    }
34
35    /// Recover the node this pointer refers to, searching from `root`. `None` if the tree
36    /// no longer contains a node of the matching kind + range (e.g. recovered from a stale
37    /// pointer against edited text).
38    ///
39    /// Prunes by range — only descends into the one child subtree that contains the target — so
40    /// recovery is ~O(tree depth), not O(nodes). This is on the hot path (called per function /
41    /// field / `TypeRef` during inference), so the pruning is what keeps single-file analysis
42    /// within the warm budget.
43    #[must_use]
44    pub fn to_node(self, root: &GdNode) -> Option<GdNode> {
45        find_node(root, self)
46    }
47}
48
49fn find_node(node: &GdNode, ptr: AstPtr) -> Option<GdNode> {
50    let range = text_range_of(node);
51    if node.kind() == ptr.kind && range == ptr.range {
52        return Some(node.clone());
53    }
54    // Only descend into the subtree that fully contains the target's range.
55    if range.start <= ptr.range.start && range.end >= ptr.range.end {
56        for child in node.children() {
57            if let Some(found) = find_node(child, ptr) {
58                return Some(found);
59            }
60        }
61    }
62    None
63}
64
65/// The [`gdscript_base::TextRange`] of `node` (converted from the `text_size` range the CST
66/// carries).
67#[must_use]
68pub fn text_range_of(node: &GdNode) -> TextRange {
69    let r = node.text_range();
70    TextRange::new(u32::from(r.start()), u32::from(r.end()))
71}
72
73/// Whether `node` has a direct child token of `kind`.
74#[must_use]
75pub fn has_token(node: &GdNode, kind: SyntaxKind) -> bool {
76    node.children_with_tokens()
77        .filter_map(NodeOrToken::into_token)
78        .any(|t| t.kind() == kind)
79}
80
81/// The text of the first direct child token of `kind`.
82#[must_use]
83pub fn child_token_text(node: &GdNode, kind: SyntaxKind) -> Option<String> {
84    node.children_with_tokens()
85        .filter_map(NodeOrToken::into_token)
86        .find(|t| t.kind() == kind)
87        .map(|t| t.text().to_owned())
88}
89
90/// Whether `kind` names an expression node (the unit the body lowerer turns into an
91/// `Expr`). Excludes `ArgList`/`DictEntry`/`ErrorNode` (structural, not values).
92#[must_use]
93pub fn is_expr_kind(kind: SyntaxKind) -> bool {
94    matches!(
95        kind,
96        SyntaxKind::BinExpr
97            | SyntaxKind::UnaryExpr
98            | SyntaxKind::TernaryExpr
99            | SyntaxKind::CastExpr
100            | SyntaxKind::IsExpr
101            | SyntaxKind::InExpr
102            | SyntaxKind::CallExpr
103            | SyntaxKind::IndexExpr
104            | SyntaxKind::FieldExpr
105            | SyntaxKind::AwaitExpr
106            | SyntaxKind::Literal
107            | SyntaxKind::NameRef
108            | SyntaxKind::ArrayLit
109            | SyntaxKind::DictLit
110            | SyntaxKind::LambdaExpr
111            | SyntaxKind::ParenExpr
112            | SyntaxKind::PreloadExpr
113            | SyntaxKind::GetNodeExpr
114            | SyntaxKind::UniqueNodeExpr
115    )
116}
117
118/// The first direct child node satisfying `pred`.
119pub fn first_child(node: &GdNode, pred: impl Fn(SyntaxKind) -> bool) -> Option<GdNode> {
120    node.children()
121        .find_map(|c| pred(c.kind()).then(|| c.clone()))
122}
123
124/// The first direct child node that is an expression.
125#[must_use]
126pub fn first_child_expr(node: &GdNode) -> Option<GdNode> {
127    first_child(node, is_expr_kind)
128}
129
130/// All direct child nodes that are expressions, in source order.
131#[must_use]
132pub fn child_exprs(node: &GdNode) -> Vec<GdNode> {
133    node.children()
134        .filter(|c| is_expr_kind(c.kind()))
135        .cloned()
136        .collect()
137}
138
139/// All direct child nodes of the given `kind`, in source order.
140#[must_use]
141pub fn children_of(node: &GdNode, kind: SyntaxKind) -> Vec<GdNode> {
142    node.children()
143        .filter(|c| c.kind() == kind)
144        .cloned()
145        .collect()
146}
147
148/// The head `Ident` token of `node`'s `extends Base[.Inner]` target — the bare identifier directly
149/// after the `extends` keyword (in an `ExtendsClause`, a `ClassNameDecl`, or an inner-class decl
150/// that inlines its `extends`). `None` when there is no `extends`, or the target is a string path
151/// (`extends "res://x.gd"`). The head names the base *class*; trailing `.Inner` segments are
152/// excluded (only the first `Ident` after `extends` is returned).
153#[must_use]
154pub fn extends_head_token(node: &GdNode) -> Option<GdToken> {
155    let mut after_extends = false;
156    for t in node
157        .children_with_tokens()
158        .filter_map(NodeOrToken::into_token)
159    {
160        if t.kind() == SyntaxKind::ExtendsKw {
161            after_extends = true;
162        } else if after_extends && t.kind() == SyntaxKind::Ident {
163            return Some(t.clone());
164        }
165    }
166    None
167}
168
169/// The first meaningful (non-trivia, non-layout) token of `node`.
170#[must_use]
171pub fn first_token(node: &GdNode) -> Option<GdToken> {
172    node.children_with_tokens()
173        .filter_map(NodeOrToken::into_token)
174        .find(|t| !t.kind().is_trivia() && !t.kind().is_synthetic_layout())
175        .cloned()
176}
177
178/// The [`TextRange`] of a token.
179#[must_use]
180pub fn token_range(token: &GdToken) -> TextRange {
181    let r = token.text_range();
182    TextRange::new(u32::from(r.start()), u32::from(r.end()))
183}