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::START,
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                    if let Some(trivia) = ast::Trivia::cast(db, child.clone()) {
232                        trivia.elements(db).iter().for_each(|element| {
233                            if !matches!(
234                                element,
235                                ast::Trivium::SingleLineComment(_)
236                                    | ast::Trivium::SingleLineDocComment(_)
237                                    | ast::Trivium::SingleLineInnerComment(_)
238                            ) {
239                                buffer.push_str(
240                                    &element
241                                        .as_syntax_node()
242                                        .get_text_without_all_comment_trivia(db),
243                                );
244                            }
245                        });
246                    } else {
247                        buffer
248                            .push_str(&SyntaxNode::get_text_without_all_comment_trivia(child, db));
249                    }
250                }
251            }
252        }
253        buffer
254    }
255
256    /// Returns all the text under the syntax node, without the outmost trivia (the leading trivia
257    /// of the first token and the trailing trivia of the last token).
258    ///
259    /// Note that this traverses the syntax tree, and generates a new string, so use responsibly.
260    pub fn get_text_without_trivia(self, db: &dyn SyntaxGroup) -> String {
261        let trimmed_span = self.span_without_trivia(db);
262
263        self.get_text_of_span(db, trimmed_span)
264    }
265
266    /// Returns the text under the syntax node, according to the given span.
267    ///
268    /// `span` is assumed to be contained within the span of self.
269    ///
270    /// Note that this traverses the syntax tree, and generates a new string, so use responsibly.
271    pub fn get_text_of_span(self, db: &dyn SyntaxGroup, span: TextSpan) -> String {
272        let orig_span = self.span(db);
273        assert!(orig_span.contains(span));
274        let full_text = self.get_text(db);
275
276        let span_in_span = TextSpan {
277            start: (span.start - orig_span.start).as_offset(),
278            end: (span.end - orig_span.start).as_offset(),
279        };
280        span_in_span.take(&full_text).to_string()
281    }
282
283    /// Traverse the subtree rooted at the current node (including the current node) in preorder.
284    ///
285    /// This is a shortcut for [`Self::preorder`] paired with filtering for [`WalkEvent::Enter`]
286    /// events only.
287    pub fn descendants<'db>(
288        &self,
289        db: &'db dyn SyntaxGroup,
290    ) -> impl Iterator<Item = SyntaxNode> + 'db {
291        self.preorder(db).filter_map(|event| match event {
292            WalkEvent::Enter(node) => Some(node),
293            WalkEvent::Leave(_) => None,
294        })
295    }
296
297    /// Traverse the subtree rooted at the current node (including the current node) in preorder,
298    /// excluding tokens.
299    pub fn preorder<'db>(&self, db: &'db dyn SyntaxGroup) -> Preorder<'db> {
300        Preorder::new(self.clone(), db)
301    }
302
303    /// Gets all the leaves of the SyntaxTree, where the self node is the root of a tree.
304    pub fn tokens<'a>(&'a self, db: &'a dyn SyntaxGroup) -> impl Iterator<Item = Self> + 'a {
305        self.preorder(db).filter_map(|event| match event {
306            WalkEvent::Enter(node) if node.green_node(db).kind.is_terminal() => Some(node),
307            _ => None,
308        })
309    }
310}
311
312/// Trait for the typed view of the syntax tree. All the internal node implementations are under
313/// the ast module.
314pub trait TypedSyntaxNode: Sized {
315    /// The relevant SyntaxKind. None for enums.
316    const OPTIONAL_KIND: Option<SyntaxKind>;
317    type StablePtr: TypedStablePtr;
318    type Green;
319    fn missing(db: &dyn SyntaxGroup) -> Self::Green;
320    fn from_syntax_node(db: &dyn SyntaxGroup, node: SyntaxNode) -> Self;
321    fn cast(db: &dyn SyntaxGroup, node: SyntaxNode) -> Option<Self>;
322    fn as_syntax_node(&self) -> SyntaxNode;
323    fn stable_ptr(&self) -> Self::StablePtr;
324}
325
326pub trait Token: TypedSyntaxNode {
327    fn new_green(db: &dyn SyntaxGroup, text: SmolStr) -> Self::Green;
328    fn text(&self, db: &dyn SyntaxGroup) -> SmolStr;
329}
330
331pub trait Terminal: TypedSyntaxNode {
332    const KIND: SyntaxKind;
333    type TokenType: Token;
334    fn new_green(
335        db: &dyn SyntaxGroup,
336        leading_trivia: TriviaGreen,
337        token: <<Self as Terminal>::TokenType as TypedSyntaxNode>::Green,
338        trailing_trivia: TriviaGreen,
339    ) -> <Self as TypedSyntaxNode>::Green;
340    /// Returns the text of the token of this terminal (excluding the trivia).
341    fn text(&self, db: &dyn SyntaxGroup) -> SmolStr;
342    /// Casts a syntax node to this terminal type's token and then walks up to return the terminal.
343    fn cast_token(db: &dyn SyntaxGroup, node: SyntaxNode) -> Option<Self> {
344        if node.kind(db) == Self::TokenType::OPTIONAL_KIND? {
345            Some(Self::from_syntax_node(db, node.parent()?))
346        } else {
347            None
348        }
349    }
350}
351
352/// Trait for stable pointers to syntax nodes.
353pub trait TypedStablePtr {
354    type SyntaxNode: TypedSyntaxNode;
355    /// Returns the syntax node pointed to by this stable pointer.
356    fn lookup(&self, db: &dyn SyntaxGroup) -> Self::SyntaxNode;
357    /// Returns the untyped stable pointer.
358    fn untyped(&self) -> SyntaxStablePtrId;
359}
360
361/// Wrapper for formatting the text of syntax nodes.
362pub struct NodeTextFormatter<'a> {
363    /// The node to format.
364    pub node: &'a SyntaxNode,
365    /// The syntax db.
366    pub db: &'a dyn SyntaxGroup,
367}
368impl Display for NodeTextFormatter<'_> {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        match &self.node.green_node(self.db).as_ref().details {
371            green::GreenNodeDetails::Token(text) => write!(f, "{text}")?,
372            green::GreenNodeDetails::Node { .. } => {
373                for child in self.db.get_children(self.node.clone()).iter() {
374                    write!(f, "{}", NodeTextFormatter { node: child, db: self.db })?;
375                }
376            }
377        }
378        Ok(())
379    }
380}