cairo_lang_defs/
patcher.rs

1use cairo_lang_filesystem::ids::{CodeMapping, CodeOrigin};
2use cairo_lang_filesystem::span::{TextOffset, TextSpan, TextWidth};
3use cairo_lang_syntax::node::db::SyntaxGroup;
4use cairo_lang_syntax::node::{SyntaxNode, TypedSyntaxNode};
5use cairo_lang_utils::extract_matches;
6use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
7use itertools::Itertools;
8
9/// Interface for modifying syntax nodes.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum RewriteNode {
12    /// A rewrite node that represents a trimmed copy of a syntax node:
13    /// one with the leading and trailing trivia excluded.
14    Trimmed {
15        node: SyntaxNode,
16        trim_left: bool,
17        trim_right: bool,
18    },
19    Copied(SyntaxNode),
20    Modified(ModifiedNode),
21    Mapped {
22        origin: TextSpan,
23        node: Box<RewriteNode>,
24    },
25    Text(String),
26    TextAndMapping(String, Vec<CodeMapping>),
27}
28impl RewriteNode {
29    pub fn new_trimmed(syntax_node: SyntaxNode) -> Self {
30        Self::Trimmed { node: syntax_node, trim_left: true, trim_right: true }
31    }
32
33    pub fn new_modified(children: Vec<RewriteNode>) -> Self {
34        Self::Modified(ModifiedNode { children: Some(children) })
35    }
36
37    pub fn text(text: &str) -> Self {
38        Self::Text(text.to_string())
39    }
40
41    pub fn mapped_text(
42        text: impl Into<String>,
43        db: &dyn SyntaxGroup,
44        origin: &impl TypedSyntaxNode,
45    ) -> Self {
46        RewriteNode::Text(text.into()).mapped(db, origin)
47    }
48
49    pub fn empty() -> Self {
50        Self::text("")
51    }
52
53    /// Creates a rewrite node from an AST object.
54    pub fn from_ast(node: &impl TypedSyntaxNode) -> Self {
55        RewriteNode::Copied(node.as_syntax_node())
56    }
57
58    /// Creates a rewrite node from an AST object - with .
59    pub fn from_ast_trimmed(node: &impl TypedSyntaxNode) -> Self {
60        Self::new_trimmed(node.as_syntax_node())
61    }
62
63    /// Prepares a node for modification.
64    pub fn modify(&mut self, db: &dyn SyntaxGroup) -> &mut ModifiedNode {
65        match self {
66            RewriteNode::Copied(syntax_node) => {
67                *self = RewriteNode::new_modified(
68                    db.get_children(syntax_node.clone())
69                        .iter()
70                        .cloned()
71                        .map(RewriteNode::Copied)
72                        .collect(),
73                );
74                extract_matches!(self, RewriteNode::Modified)
75            }
76            RewriteNode::Trimmed { node, trim_left, trim_right } => {
77                let children = db.get_children(node.clone());
78                let num_children = children.len();
79                let mut new_children = Vec::new();
80
81                // Get the index of the leftmost nonempty child.
82                let Some(left_idx) =
83                    children.iter().position(|child| child.width(db) != TextWidth::default())
84                else {
85                    *self = RewriteNode::Modified(ModifiedNode { children: None });
86                    return extract_matches!(self, RewriteNode::Modified);
87                };
88                // Get the index of the rightmost nonempty child.
89                let right_idx = children
90                    .iter()
91                    .rposition(|child| child.width(db) != TextWidth::default())
92                    .unwrap();
93                new_children.extend(itertools::repeat_n(
94                    RewriteNode::Modified(ModifiedNode { children: None }),
95                    left_idx,
96                ));
97
98                // The number of children between the first and last nonempty nodes.
99                let num_middle = right_idx - left_idx + 1;
100                let children = db.get_children(node.clone());
101                let mut children_iter = children.iter().skip(left_idx);
102                match num_middle {
103                    1 => {
104                        new_children.push(RewriteNode::Trimmed {
105                            node: children_iter.next().unwrap().clone(),
106                            trim_left: *trim_left,
107                            trim_right: *trim_right,
108                        });
109                    }
110                    _ => {
111                        new_children.push(RewriteNode::Trimmed {
112                            node: children_iter.next().unwrap().clone(),
113                            trim_left: *trim_left,
114                            trim_right: false,
115                        });
116                        for _ in 0..(num_middle - 2) {
117                            let child = children_iter.next().unwrap().clone();
118                            new_children.push(RewriteNode::Copied(child));
119                        }
120                        new_children.push(RewriteNode::Trimmed {
121                            node: children_iter.next().unwrap().clone(),
122                            trim_left: false,
123                            trim_right: *trim_right,
124                        });
125                    }
126                };
127                new_children.extend(itertools::repeat_n(
128                    RewriteNode::Modified(ModifiedNode { children: None }),
129                    num_children - right_idx - 1,
130                ));
131
132                *self = RewriteNode::Modified(ModifiedNode { children: Some(new_children) });
133                extract_matches!(self, RewriteNode::Modified)
134            }
135            RewriteNode::Modified(modified) => modified,
136            RewriteNode::Text(_) | RewriteNode::TextAndMapping(_, _) => {
137                panic!("A text node can't be modified")
138            }
139            RewriteNode::Mapped { .. } => panic!("A mapped node can't be modified"),
140        }
141    }
142
143    /// Prepares a node for modification and returns a specific child.
144    pub fn modify_child(&mut self, db: &dyn SyntaxGroup, index: usize) -> &mut RewriteNode {
145        if matches!(self, RewriteNode::Modified(ModifiedNode { children: None })) {
146            // Modification of an empty node is idempotent.
147            return self;
148        }
149        &mut self.modify(db).children.as_mut().unwrap()[index]
150    }
151
152    /// Replaces this node with text.
153    pub fn set_str(&mut self, s: String) {
154        *self = RewriteNode::Text(s)
155    }
156    /// Creates a new Rewrite node by interpolating a string with patches.
157    /// Each substring of the form `$<name>$` is replaced with syntax nodes from `patches`.
158    /// A `$$` substring is replaced with `$`.
159    pub fn interpolate_patched(
160        code: &str,
161        patches: &UnorderedHashMap<String, RewriteNode>,
162    ) -> RewriteNode {
163        let mut chars = code.chars().peekable();
164        let mut pending_text = String::new();
165        let mut children = Vec::new();
166        while let Some(c) = chars.next() {
167            if c != '$' {
168                pending_text.push(c);
169                continue;
170            }
171
172            // An opening $ was detected.
173
174            // Read the name
175            let mut name = String::new();
176            for c in chars.by_ref() {
177                if c == '$' {
178                    break;
179                }
180                name.push(c);
181            }
182
183            // A closing $ was found.
184            // If the string between the `$`s is empty - push a single `$` to the output.
185            if name.is_empty() {
186                pending_text.push('$');
187                continue;
188            }
189            // If the string wasn't empty and there is some pending text, first flush it as a text
190            // child.
191            if !pending_text.is_empty() {
192                children.push(RewriteNode::text(&pending_text));
193                pending_text.clear();
194            }
195            // Replace the substring with the relevant rewrite node.
196            // TODO(yuval): this currently panics. Fix it.
197            children.push(
198                patches.get(&name).cloned().unwrap_or_else(|| panic!("No patch named {}.", name)),
199            );
200        }
201        // Flush the remaining text as a text child.
202        if !pending_text.is_empty() {
203            children.push(RewriteNode::text(&pending_text));
204        }
205
206        RewriteNode::new_modified(children)
207    }
208
209    /// Creates a new Rewrite node by inserting a `separator` between each two given children.
210    pub fn interspersed(
211        children: impl IntoIterator<Item = RewriteNode>,
212        separator: RewriteNode,
213    ) -> RewriteNode {
214        RewriteNode::new_modified(itertools::intersperse(children, separator).collect_vec())
215    }
216
217    /// Creates a new rewrite node wrapped in a mapping to the original code.
218    pub fn mapped(self, db: &dyn SyntaxGroup, origin: &impl TypedSyntaxNode) -> Self {
219        RewriteNode::Mapped {
220            origin: origin.as_syntax_node().span_without_trivia(db),
221            node: Box::new(self),
222        }
223    }
224}
225impl Default for RewriteNode {
226    fn default() -> Self {
227        Self::empty()
228    }
229}
230impl From<SyntaxNode> for RewriteNode {
231    fn from(node: SyntaxNode) -> Self {
232        RewriteNode::Copied(node)
233    }
234}
235
236/// A modified rewrite node.
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct ModifiedNode {
239    /// Children of the node.
240    /// Can be None, in which case this is an empty node (of width 0). It's not the same as
241    /// Some(vec![]) - A child can be (idempotently) modified for None, whereas modifying a child
242    /// for Some(vec![]) would panic.
243    pub children: Option<Vec<RewriteNode>>,
244}
245
246pub struct PatchBuilder<'a> {
247    pub db: &'a dyn SyntaxGroup,
248    code: String,
249    code_mappings: Vec<CodeMapping>,
250    origin: CodeOrigin,
251}
252impl<'a> PatchBuilder<'a> {
253    /// Creates a new patch builder, originating from `origin` typed node.
254    pub fn new(db: &'a dyn SyntaxGroup, origin: &impl TypedSyntaxNode) -> Self {
255        Self::new_ex(db, &origin.as_syntax_node())
256    }
257
258    /// Creates a new patch builder, originating from `origin` node.
259    pub fn new_ex(db: &'a dyn SyntaxGroup, origin: &SyntaxNode) -> Self {
260        Self {
261            db,
262            code: String::default(),
263            code_mappings: vec![],
264            origin: CodeOrigin::Span(origin.span_without_trivia(db)),
265        }
266    }
267
268    /// Builds the resulting code and code mappings.
269    pub fn build(mut self) -> (String, Vec<CodeMapping>) {
270        // Adds the mapping to the original node from all code not previously mapped.
271        self.code_mappings
272            .push(CodeMapping { span: TextSpan::from_str(&self.code), origin: self.origin });
273        (self.code, self.code_mappings)
274    }
275
276    /// Builds the patcher into a rewrite node enabling adding it to other patchers.
277    pub fn into_rewrite_node(self) -> RewriteNode {
278        let (code, mappings) = self.build();
279        RewriteNode::TextAndMapping(code, mappings)
280    }
281
282    pub fn add_char(&mut self, c: char) {
283        self.code.push(c);
284    }
285
286    pub fn add_str(&mut self, s: &str) {
287        self.code += s;
288    }
289
290    pub fn add_modified(&mut self, node: RewriteNode) {
291        match node {
292            RewriteNode::Copied(node) => self.add_node(node),
293            RewriteNode::Mapped { origin, node } => self.add_mapped(*node, origin),
294            RewriteNode::Trimmed { node, trim_left, trim_right } => {
295                self.add_trimmed_node(node, trim_left, trim_right)
296            }
297            RewriteNode::Modified(modified) => {
298                if let Some(children) = modified.children {
299                    for child in children {
300                        self.add_modified(child)
301                    }
302                }
303            }
304            RewriteNode::Text(s) => self.add_str(s.as_str()),
305            RewriteNode::TextAndMapping(s, mappings) => {
306                let mapping_fix = TextWidth::from_str(&self.code);
307                self.add_str(&s);
308                self.code_mappings.extend(mappings.into_iter().map(|mut mapping| {
309                    mapping.span.start = mapping.span.start.add_width(mapping_fix);
310                    mapping.span.end = mapping.span.end.add_width(mapping_fix);
311                    mapping
312                }));
313            }
314        }
315    }
316
317    pub fn add_node(&mut self, node: SyntaxNode) {
318        let start = TextOffset::from_str(&self.code);
319        let orig_span = node.span(self.db);
320        self.code_mappings.push(CodeMapping {
321            span: TextSpan { start, end: start.add_width(orig_span.width()) },
322            origin: CodeOrigin::Start(orig_span.start),
323        });
324        self.code += &node.get_text(self.db);
325    }
326
327    fn add_mapped(&mut self, node: RewriteNode, origin: TextSpan) {
328        let start = TextOffset::from_str(&self.code);
329        self.add_modified(node);
330        let end = TextOffset::from_str(&self.code);
331        self.code_mappings
332            .push(CodeMapping { span: TextSpan { start, end }, origin: CodeOrigin::Span(origin) });
333    }
334
335    fn add_trimmed_node(&mut self, node: SyntaxNode, trim_left: bool, trim_right: bool) {
336        let TextSpan { start: trimmed_start, end: trimmed_end } = node.span_without_trivia(self.db);
337        let orig_start = if trim_left { trimmed_start } else { node.span(self.db).start };
338        let orig_end = if trim_right { trimmed_end } else { node.span(self.db).end };
339        let origin_span = TextSpan { start: orig_start, end: orig_end };
340
341        let text = node.get_text_of_span(self.db, origin_span);
342        let start = TextOffset::from_str(&self.code);
343
344        self.code += &text;
345
346        self.code_mappings.push(CodeMapping {
347            span: TextSpan { start, end: start.add_width(TextWidth::from_str(&text)) },
348            origin: CodeOrigin::Start(orig_start),
349        });
350    }
351}