cairo_lang_syntax/node/
mod.rs

1use core::hash::Hash;
2use std::fmt::Display;
3use std::sync::Arc;
4
5use cairo_lang_filesystem::ids::FileId;
6use cairo_lang_filesystem::span::{TextOffset, TextPosition, TextSpan, TextWidth};
7use cairo_lang_utils::{Intern, LookupIntern, require};
8use smol_str::SmolStr;
9
10use self::ast::TriviaGreen;
11use self::db::SyntaxGroup;
12use self::green::GreenNode;
13use self::ids::{GreenId, SyntaxStablePtrId};
14use self::kind::SyntaxKind;
15use self::stable_ptr::SyntaxStablePtr;
16use crate::node::iter::{Preorder, WalkEvent};
17
18pub mod ast;
19pub mod db;
20pub mod element_list;
21pub mod green;
22pub mod helpers;
23pub mod ids;
24pub mod iter;
25pub mod key_fields;
26pub mod kind;
27pub mod stable_ptr;
28pub mod utils;
29pub mod with_db;
30
31#[cfg(test)]
32mod ast_test;
33#[cfg(test)]
34mod test_utils;
35
36/// SyntaxNode. Untyped view of the syntax tree. Adds parent() and offset() capabilities.
37#[derive(Clone, Debug, Hash, PartialEq, Eq)]
38pub struct SyntaxNode(Arc<SyntaxNodeInner>);
39#[derive(Clone, Debug, Hash, PartialEq, Eq)]
40struct SyntaxNodeInner {
41    green: GreenId,
42    /// Number of characters from the beginning of the file to the start of the span of this
43    /// syntax subtree.
44    offset: TextOffset,
45    parent: Option<SyntaxNode>,
46    stable_ptr: SyntaxStablePtrId,
47}
48impl SyntaxNode {
49    pub fn new_root(db: &dyn SyntaxGroup, file_id: FileId, green: GreenId) -> Self {
50        let inner = SyntaxNodeInner {
51            green,
52            offset: TextOffset::default(),
53            parent: None,
54            stable_ptr: SyntaxStablePtr::Root(file_id, green).intern(db),
55        };
56        Self(Arc::new(inner))
57    }
58
59    pub fn new_root_with_offset(
60        db: &dyn SyntaxGroup,
61        file_id: FileId,
62        green: GreenId,
63        initial_offset: Option<TextOffset>,
64    ) -> Self {
65        let inner = SyntaxNodeInner {
66            green,
67            offset: initial_offset.unwrap_or_default(),
68            parent: None,
69            stable_ptr: SyntaxStablePtr::Root(file_id, green).intern(db),
70        };
71        Self(Arc::new(inner))
72    }
73
74    pub fn offset(&self) -> TextOffset {
75        self.0.offset
76    }
77    pub fn width(&self, db: &dyn SyntaxGroup) -> TextWidth {
78        self.green_node(db).width()
79    }
80    pub fn kind(&self, db: &dyn SyntaxGroup) -> SyntaxKind {
81        self.green_node(db).kind
82    }
83    pub fn span(&self, db: &dyn SyntaxGroup) -> TextSpan {
84        let start = self.offset();
85        let end = start.add_width(self.width(db));
86        TextSpan { start, end }
87    }
88    /// Returns the text of the token if this node is a token.
89    pub fn text(&self, db: &dyn SyntaxGroup) -> Option<SmolStr> {
90        match &self.green_node(db).details {
91            green::GreenNodeDetails::Token(text) => Some(text.clone()),
92            green::GreenNodeDetails::Node { .. } => None,
93        }
94    }
95    pub fn green_node(&self, db: &dyn SyntaxGroup) -> Arc<GreenNode> {
96        self.0.green.lookup_intern(db)
97    }
98    pub fn span_without_trivia(&self, db: &dyn SyntaxGroup) -> TextSpan {
99        let start = self.span_start_without_trivia(db);
100        let end = self.span_end_without_trivia(db);
101        TextSpan { start, end }
102    }
103    pub fn parent(&self) -> Option<SyntaxNode> {
104        self.0.parent.as_ref().cloned()
105    }
106    /// Returns the position of a syntax node in its parent's children, or None if the node has no
107    /// parent.
108    pub fn position_in_parent(&self, db: &dyn SyntaxGroup) -> Option<usize> {
109        let parent_green = self.parent()?.green_node(db);
110        let parent_children = parent_green.children();
111        let self_green_id = self.0.green;
112        parent_children.iter().position(|child| child == &self_green_id)
113    }
114    pub fn stable_ptr(&self) -> SyntaxStablePtrId {
115        self.0.stable_ptr
116    }
117
118    /// Gets the inner token from a terminal SyntaxNode. If the given node is not a terminal,
119    /// returns None.
120    pub fn get_terminal_token(&self, db: &dyn SyntaxGroup) -> Option<SyntaxNode> {
121        let green_node = self.green_node(db);
122        require(green_node.kind.is_terminal())?;
123        // At this point we know we should have a second child which is the token.
124        let token_node = db.get_children(self.clone())[1].clone();
125        Some(token_node)
126    }
127
128    pub fn span_start_without_trivia(&self, db: &dyn SyntaxGroup) -> TextOffset {
129        let green_node = self.green_node(db);
130        match green_node.details {
131            green::GreenNodeDetails::Node { .. } => {
132                if let Some(token_node) = self.get_terminal_token(db) {
133                    return token_node.offset();
134                }
135                let children = db.get_children(self.clone());
136                if let Some(child) =
137                    children.iter().find(|child| child.width(db) != TextWidth::default())
138                {
139                    child.span_start_without_trivia(db)
140                } else {
141                    self.offset()
142                }
143            }
144            green::GreenNodeDetails::Token(_) => self.offset(),
145        }
146    }
147    pub fn span_end_without_trivia(&self, db: &dyn SyntaxGroup) -> TextOffset {
148        let green_node = self.green_node(db);
149        match green_node.details {
150            green::GreenNodeDetails::Node { .. } => {
151                if let Some(token_node) = self.get_terminal_token(db) {
152                    return token_node.span(db).end;
153                }
154                let children = &mut db.get_children(self.clone());
155                if let Some(child) =
156                    children.iter().filter(|child| child.width(db) != TextWidth::default()).last()
157                {
158                    child.span_end_without_trivia(db)
159                } else {
160                    self.span(db).end
161                }
162            }
163            green::GreenNodeDetails::Token(_) => self.span(db).end,
164        }
165    }
166
167    /// Lookups a syntax node using an offset.
168    pub fn lookup_offset(&self, db: &dyn SyntaxGroup, offset: TextOffset) -> SyntaxNode {
169        for child in db.get_children(self.clone()).iter() {
170            if child.offset().add_width(child.width(db)) > offset {
171                return child.lookup_offset(db, offset);
172            }
173        }
174        self.clone()
175    }
176
177    /// Lookups a syntax node using a position.
178    pub fn lookup_position(&self, db: &dyn SyntaxGroup, position: TextPosition) -> SyntaxNode {
179        match position.offset_in_file(db.upcast(), self.stable_ptr().file_id(db)) {
180            Some(offset) => self.lookup_offset(db, offset),
181            None => self.clone(),
182        }
183    }
184
185    /// Returns all the text under the syntax node.
186    /// Note that this traverses the syntax tree, and generates a new string, so use responsibly.
187    pub fn get_text(&self, db: &dyn SyntaxGroup) -> String {
188        format!("{}", NodeTextFormatter { node: self, db })
189    }
190
191    /// Returns all the text under the syntax node.
192    /// It traverses all the syntax tree of the node, but ignores functions and modules.
193    /// We ignore those, because if there's some inner functions or modules, we don't want to get
194    /// raw text of them. Comments inside them refer themselves directly, not this SyntaxNode.
195    pub fn get_text_without_inner_commentable_children(&self, db: &dyn SyntaxGroup) -> String {
196        let mut buffer = String::new();
197
198        match &self.green_node(db).as_ref().details {
199            green::GreenNodeDetails::Token(text) => buffer.push_str(text),
200            green::GreenNodeDetails::Node { .. } => {
201                for child in db.get_children(self.clone()).iter() {
202                    let kind = child.kind(db);
203
204                    // Checks all the items that the inner comment can be bubbled to (implementation
205                    // function is also a FunctionWithBody).
206                    if !matches!(
207                        kind,
208                        SyntaxKind::FunctionWithBody
209                            | SyntaxKind::ItemModule
210                            | SyntaxKind::TraitItemFunction
211                    ) {
212                        buffer.push_str(&SyntaxNode::get_text_without_inner_commentable_children(
213                            child, db,
214                        ));
215                    }
216                }
217            }
218        }
219        buffer
220    }
221
222    /// Returns all the text of the item without comments trivia.
223    /// It traverses all the syntax tree of the node.
224    pub fn get_text_without_all_comment_trivia(&self, db: &dyn SyntaxGroup) -> String {
225        let mut buffer = String::new();
226
227        match &self.green_node(db).as_ref().details {
228            green::GreenNodeDetails::Token(text) => buffer.push_str(text),
229            green::GreenNodeDetails::Node { .. } => {
230                for child in db.get_children(self.clone()).iter() {
231                    let kind = child.kind(db);
232
233                    if matches!(kind, SyntaxKind::Trivia) {
234                        ast::Trivia::from_syntax_node(db, child.clone())
235                            .elements(db)
236                            .iter()
237                            .for_each(|element| {
238                                if !matches!(
239                                    element,
240                                    ast::Trivium::SingleLineComment(_)
241                                        | ast::Trivium::SingleLineDocComment(_)
242                                        | ast::Trivium::SingleLineInnerComment(_)
243                                ) {
244                                    buffer.push_str(
245                                        &element
246                                            .as_syntax_node()
247                                            .get_text_without_all_comment_trivia(db),
248                                    );
249                                }
250                            });
251                    } else {
252                        buffer
253                            .push_str(&SyntaxNode::get_text_without_all_comment_trivia(child, db));
254                    }
255                }
256            }
257        }
258        buffer
259    }
260
261    /// Returns all the text under the syntax node, without the outmost trivia (the leading trivia
262    /// of the first token and the trailing trivia of the last token).
263    ///
264    /// Note that this traverses the syntax tree, and generates a new string, so use responsibly.
265    pub fn get_text_without_trivia(self, db: &dyn SyntaxGroup) -> String {
266        let trimmed_span = self.span_without_trivia(db);
267
268        self.get_text_of_span(db, trimmed_span)
269    }
270
271    /// Returns the text under the syntax node, according to the given span.
272    ///
273    /// `span` is assumed to be contained within the span of self.
274    ///
275    /// Note that this traverses the syntax tree, and generates a new string, so use responsibly.
276    pub fn get_text_of_span(self, db: &dyn SyntaxGroup, span: TextSpan) -> String {
277        let orig_span = self.span(db);
278        assert!(orig_span.contains(span));
279        let full_text = self.get_text(db);
280        let zero_offset = TextOffset::default();
281
282        let span_in_span = TextSpan {
283            start: zero_offset.add_width(span.start - orig_span.start),
284            end: zero_offset.add_width(span.end - orig_span.start),
285        };
286        span_in_span.take(&full_text).to_string()
287    }
288
289    /// Traverse the subtree rooted at the current node (including the current node) in preorder.
290    ///
291    /// This is a shortcut for [`Self::preorder`] paired with filtering for [`WalkEvent::Enter`]
292    /// events only.
293    pub fn descendants<'db>(
294        &self,
295        db: &'db dyn SyntaxGroup,
296    ) -> impl Iterator<Item = SyntaxNode> + 'db {
297        self.preorder(db).filter_map(|event| match event {
298            WalkEvent::Enter(node) => Some(node),
299            WalkEvent::Leave(_) => None,
300        })
301    }
302
303    /// Traverse the subtree rooted at the current node (including the current node) in preorder,
304    /// excluding tokens.
305    pub fn preorder<'db>(&self, db: &'db dyn SyntaxGroup) -> Preorder<'db> {
306        Preorder::new(self.clone(), db)
307    }
308
309    /// Gets all the leaves of the SyntaxTree, where the self node is the root of a tree.
310    pub fn tokens<'a>(&'a self, db: &'a dyn SyntaxGroup) -> impl Iterator<Item = Self> + 'a {
311        self.preorder(db).filter_map(|event| match event {
312            WalkEvent::Enter(node) if node.green_node(db).kind.is_terminal() => Some(node),
313            _ => None,
314        })
315    }
316}
317
318/// Trait for the typed view of the syntax tree. All the internal node implementations are under
319/// the ast module.
320pub trait TypedSyntaxNode {
321    /// The relevant SyntaxKind. None for enums.
322    const OPTIONAL_KIND: Option<SyntaxKind>;
323    type StablePtr: TypedStablePtr;
324    type Green;
325    fn missing(db: &dyn SyntaxGroup) -> Self::Green;
326    // TODO(spapini): Make this return an Option, if the kind is wrong.
327    fn from_syntax_node(db: &dyn SyntaxGroup, node: SyntaxNode) -> Self;
328    fn as_syntax_node(&self) -> SyntaxNode;
329    fn stable_ptr(&self) -> Self::StablePtr;
330}
331
332pub trait Token: TypedSyntaxNode {
333    fn new_green(db: &dyn SyntaxGroup, text: SmolStr) -> Self::Green;
334    fn text(&self, db: &dyn SyntaxGroup) -> SmolStr;
335}
336
337pub trait Terminal: TypedSyntaxNode {
338    const KIND: SyntaxKind;
339    type TokenType: Token;
340    fn new_green(
341        db: &dyn SyntaxGroup,
342        leading_trivia: TriviaGreen,
343        token: <<Self as Terminal>::TokenType as TypedSyntaxNode>::Green,
344        trailing_trivia: TriviaGreen,
345    ) -> <Self as TypedSyntaxNode>::Green;
346    /// Returns the text of the token of this terminal (excluding the trivia).
347    fn text(&self, db: &dyn SyntaxGroup) -> SmolStr;
348}
349
350/// Trait for stable pointers to syntax nodes.
351pub trait TypedStablePtr {
352    type SyntaxNode: TypedSyntaxNode;
353    /// Returns the syntax node pointed to by this stable pointer.
354    fn lookup(&self, db: &dyn SyntaxGroup) -> Self::SyntaxNode;
355    /// Returns the untyped stable pointer.
356    fn untyped(&self) -> SyntaxStablePtrId;
357}
358
359/// Wrapper for formatting the text of syntax nodes.
360pub struct NodeTextFormatter<'a> {
361    /// The node to format.
362    pub node: &'a SyntaxNode,
363    /// The syntax db.
364    pub db: &'a dyn SyntaxGroup,
365}
366impl Display for NodeTextFormatter<'_> {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        match &self.node.green_node(self.db).as_ref().details {
369            green::GreenNodeDetails::Token(text) => write!(f, "{text}")?,
370            green::GreenNodeDetails::Node { .. } => {
371                for child in self.db.get_children(self.node.clone()).iter() {
372                    write!(f, "{}", NodeTextFormatter { node: child, db: self.db })?;
373                }
374            }
375        }
376        Ok(())
377    }
378}