makefile_lossless/ast/
include.rs

1use super::makefile::MakefileItem;
2use crate::lossless::{remove_with_preceding_comments, Error, ErrorInfo, Include, ParseError};
3use crate::SyntaxKind::{EXPR, IDENTIFIER};
4use rowan::ast::AstNode;
5use rowan::{GreenNodeBuilder, SyntaxNode};
6
7impl Include {
8    /// Get the raw path of the include directive
9    pub fn path(&self) -> Option<String> {
10        self.syntax()
11            .children()
12            .find(|it| it.kind() == EXPR)
13            .map(|it| it.text().to_string().trim().to_string())
14    }
15
16    /// Check if this is an optional include (-include or sinclude)
17    pub fn is_optional(&self) -> bool {
18        let text = self.syntax().text();
19        text.to_string().starts_with("-include") || text.to_string().starts_with("sinclude")
20    }
21
22    /// Get the parent item of this include directive, if any
23    ///
24    /// Returns `Some(MakefileItem)` if this include has a parent that is a MakefileItem
25    /// (e.g., a Conditional), or `None` if the parent is the root Makefile node.
26    ///
27    /// # Example
28    /// ```
29    /// use makefile_lossless::Makefile;
30    ///
31    /// let makefile: Makefile = r#"ifdef DEBUG
32    /// include debug.mk
33    /// endif
34    /// "#.parse().unwrap();
35    /// let cond = makefile.conditionals().next().unwrap();
36    /// let inc = cond.if_items().next().unwrap();
37    /// // Include's parent is the conditional
38    /// assert!(matches!(inc, makefile_lossless::MakefileItem::Include(_)));
39    /// ```
40    pub fn parent(&self) -> Option<MakefileItem> {
41        self.syntax().parent().and_then(MakefileItem::cast)
42    }
43
44    /// Remove this include directive from the makefile
45    ///
46    /// This will also remove any preceding comments.
47    ///
48    /// # Example
49    /// ```
50    /// use makefile_lossless::Makefile;
51    /// let mut makefile: Makefile = "include config.mk\nVAR = value\n".parse().unwrap();
52    /// let mut inc = makefile.includes().next().unwrap();
53    /// inc.remove().unwrap();
54    /// assert_eq!(makefile.includes().count(), 0);
55    /// ```
56    pub fn remove(&mut self) -> Result<(), Error> {
57        let Some(parent) = self.syntax().parent() else {
58            return Err(Error::Parse(ParseError {
59                errors: vec![ErrorInfo {
60                    message: "Cannot remove include: no parent node".to_string(),
61                    line: 1,
62                    context: "include_remove".to_string(),
63                }],
64            }));
65        };
66
67        remove_with_preceding_comments(self.syntax(), &parent);
68        Ok(())
69    }
70
71    /// Set the path of this include directive
72    ///
73    /// # Example
74    /// ```
75    /// use makefile_lossless::Makefile;
76    /// let mut makefile: Makefile = "include old.mk\n".parse().unwrap();
77    /// let mut inc = makefile.includes().next().unwrap();
78    /// inc.set_path("new.mk");
79    /// assert_eq!(inc.path(), Some("new.mk".to_string()));
80    /// assert_eq!(makefile.to_string(), "include new.mk\n");
81    /// ```
82    pub fn set_path(&mut self, new_path: &str) {
83        // Find the EXPR node containing the path
84        let expr_index = self
85            .syntax()
86            .children()
87            .find(|it| it.kind() == EXPR)
88            .map(|it| it.index());
89
90        if let Some(expr_idx) = expr_index {
91            // Build a new EXPR node with the new path
92            let mut builder = GreenNodeBuilder::new();
93            builder.start_node(EXPR.into());
94            builder.token(IDENTIFIER.into(), new_path);
95            builder.finish_node();
96
97            let new_expr = SyntaxNode::new_root_mut(builder.finish());
98
99            // Replace the old EXPR with the new one
100            self.syntax()
101                .splice_children(expr_idx..expr_idx + 1, vec![new_expr.into()]);
102        }
103    }
104
105    /// Make this include optional (change "include" to "-include")
106    ///
107    /// If the include is already optional, this has no effect.
108    ///
109    /// # Example
110    /// ```
111    /// use makefile_lossless::Makefile;
112    /// let mut makefile: Makefile = "include config.mk\n".parse().unwrap();
113    /// let mut inc = makefile.includes().next().unwrap();
114    /// inc.set_optional(true);
115    /// assert!(inc.is_optional());
116    /// assert_eq!(makefile.to_string(), "-include config.mk\n");
117    /// ```
118    pub fn set_optional(&mut self, optional: bool) {
119        use crate::SyntaxKind::INCLUDE;
120
121        // Find the first IDENTIFIER token (which is the include keyword)
122        let keyword_token = self.syntax().children_with_tokens().find(|it| {
123            it.as_token()
124                .map(|t| t.kind() == IDENTIFIER)
125                .unwrap_or(false)
126        });
127
128        if let Some(token_element) = keyword_token {
129            let token = token_element.as_token().unwrap();
130            let current_text = token.text();
131
132            let new_keyword = if optional {
133                // Make it optional
134                if current_text == "include" {
135                    "-include"
136                } else if current_text == "sinclude" || current_text == "-include" {
137                    // Already optional, no change needed
138                    return;
139                } else {
140                    // Shouldn't happen, but handle gracefully
141                    return;
142                }
143            } else {
144                // Make it non-optional
145                if current_text == "-include" || current_text == "sinclude" {
146                    "include"
147                } else if current_text == "include" {
148                    // Already non-optional, no change needed
149                    return;
150                } else {
151                    // Shouldn't happen, but handle gracefully
152                    return;
153                }
154            };
155
156            // Rebuild the entire INCLUDE node, replacing just the keyword token
157            let mut builder = GreenNodeBuilder::new();
158            builder.start_node(INCLUDE.into());
159
160            for child in self.syntax().children_with_tokens() {
161                match child {
162                    rowan::NodeOrToken::Token(tok)
163                        if tok.kind() == IDENTIFIER && tok.text() == current_text =>
164                    {
165                        // Replace the include keyword
166                        builder.token(IDENTIFIER.into(), new_keyword);
167                    }
168                    rowan::NodeOrToken::Token(tok) => {
169                        // Copy other tokens as-is
170                        builder.token(tok.kind().into(), tok.text());
171                    }
172                    rowan::NodeOrToken::Node(node) => {
173                        // For nodes (like EXPR), rebuild them
174                        builder.start_node(node.kind().into());
175                        for node_child in node.children_with_tokens() {
176                            if let rowan::NodeOrToken::Token(tok) = node_child {
177                                builder.token(tok.kind().into(), tok.text());
178                            }
179                        }
180                        builder.finish_node();
181                    }
182                }
183            }
184
185            builder.finish_node();
186            let new_include = SyntaxNode::new_root_mut(builder.finish());
187
188            // Replace the old INCLUDE node with the new one
189            let index = self.syntax().index();
190            if let Some(parent) = self.syntax().parent() {
191                parent.splice_children(index..index + 1, vec![new_include.clone().into()]);
192
193                // Update self to point to the new node
194                *self = Include::cast(
195                    parent
196                        .children_with_tokens()
197                        .nth(index)
198                        .and_then(|it| it.into_node())
199                        .unwrap(),
200                )
201                .unwrap();
202            }
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209
210    use crate::lossless::Makefile;
211
212    #[test]
213    fn test_include_parent() {
214        let makefile: Makefile = "include common.mk\n".parse().unwrap();
215
216        let inc = makefile.includes().next().unwrap();
217        let parent = inc.parent();
218        // Parent is ROOT node which doesn't cast to MakefileItem
219        assert!(parent.is_none());
220    }
221
222    #[test]
223    fn test_add_include() {
224        let mut makefile = Makefile::new();
225        makefile.add_include("config.mk");
226
227        let includes: Vec<_> = makefile.includes().collect();
228        assert_eq!(includes.len(), 1);
229        assert_eq!(includes[0].path(), Some("config.mk".to_string()));
230
231        let files: Vec<_> = makefile.included_files().collect();
232        assert_eq!(files, vec!["config.mk"]);
233
234        // Check the generated text
235        assert_eq!(makefile.to_string(), "include config.mk\n");
236    }
237
238    #[test]
239    fn test_add_include_to_existing() {
240        let mut makefile: Makefile = "VAR = value\nrule:\n\tcommand\n".parse().unwrap();
241        makefile.add_include("config.mk");
242
243        // Include should be added at the beginning
244        let files: Vec<_> = makefile.included_files().collect();
245        assert_eq!(files, vec!["config.mk"]);
246
247        // Check that the include comes first
248        let text = makefile.to_string();
249        assert!(text.starts_with("include config.mk\n"));
250        assert!(text.contains("VAR = value"));
251    }
252
253    #[test]
254    fn test_insert_include() {
255        let mut makefile: Makefile = "VAR = value\nrule:\n\tcommand\n".parse().unwrap();
256        makefile.insert_include(1, "config.mk").unwrap();
257
258        let items: Vec<_> = makefile.items().collect();
259        assert_eq!(items.len(), 3);
260
261        // Check the middle item is the include
262        let files: Vec<_> = makefile.included_files().collect();
263        assert_eq!(files, vec!["config.mk"]);
264    }
265
266    #[test]
267    fn test_insert_include_at_beginning() {
268        let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
269        makefile.insert_include(0, "config.mk").unwrap();
270
271        let text = makefile.to_string();
272        assert!(text.starts_with("include config.mk\n"));
273    }
274
275    #[test]
276    fn test_insert_include_at_end() {
277        let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
278        let item_count = makefile.items().count();
279        makefile.insert_include(item_count, "config.mk").unwrap();
280
281        let text = makefile.to_string();
282        assert!(text.ends_with("include config.mk\n"));
283    }
284
285    #[test]
286    fn test_insert_include_out_of_bounds() {
287        let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
288        let result = makefile.insert_include(100, "config.mk");
289        assert!(result.is_err());
290    }
291
292    #[test]
293    fn test_insert_include_after() {
294        let mut makefile: Makefile = "VAR1 = value1\nVAR2 = value2\n".parse().unwrap();
295        let first_var = makefile.items().next().unwrap();
296        makefile
297            .insert_include_after(&first_var, "config.mk")
298            .unwrap();
299
300        let files: Vec<_> = makefile.included_files().collect();
301        assert_eq!(files, vec!["config.mk"]);
302
303        // Check that the include is after VAR1
304        let text = makefile.to_string();
305        let var1_pos = text.find("VAR1").unwrap();
306        let include_pos = text.find("include config.mk").unwrap();
307        assert!(include_pos > var1_pos);
308    }
309
310    #[test]
311    fn test_insert_include_after_with_rule() {
312        let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
313        let first_rule_item = makefile.items().next().unwrap();
314        makefile
315            .insert_include_after(&first_rule_item, "config.mk")
316            .unwrap();
317
318        let text = makefile.to_string();
319        let rule1_pos = text.find("rule1:").unwrap();
320        let include_pos = text.find("include config.mk").unwrap();
321        let rule2_pos = text.find("rule2:").unwrap();
322
323        // Include should be between rule1 and rule2
324        assert!(include_pos > rule1_pos);
325        assert!(include_pos < rule2_pos);
326    }
327
328    #[test]
329    fn test_include_remove() {
330        let makefile: Makefile = "include config.mk\nVAR = value\n".parse().unwrap();
331        let mut inc = makefile.includes().next().unwrap();
332        inc.remove().unwrap();
333
334        assert_eq!(makefile.includes().count(), 0);
335        assert_eq!(makefile.to_string(), "VAR = value\n");
336    }
337
338    #[test]
339    fn test_include_remove_multiple() {
340        let makefile: Makefile = "include first.mk\ninclude second.mk\nVAR = value\n"
341            .parse()
342            .unwrap();
343        let mut inc = makefile.includes().next().unwrap();
344        inc.remove().unwrap();
345
346        assert_eq!(makefile.includes().count(), 1);
347        let remaining = makefile.includes().next().unwrap();
348        assert_eq!(remaining.path(), Some("second.mk".to_string()));
349    }
350
351    #[test]
352    fn test_include_set_path() {
353        let makefile: Makefile = "include old.mk\n".parse().unwrap();
354        let mut inc = makefile.includes().next().unwrap();
355        inc.set_path("new.mk");
356
357        assert_eq!(inc.path(), Some("new.mk".to_string()));
358        assert_eq!(makefile.to_string(), "include new.mk\n");
359    }
360
361    #[test]
362    fn test_include_set_path_preserves_optional() {
363        let makefile: Makefile = "-include old.mk\n".parse().unwrap();
364        let mut inc = makefile.includes().next().unwrap();
365        inc.set_path("new.mk");
366
367        assert_eq!(inc.path(), Some("new.mk".to_string()));
368        assert!(inc.is_optional());
369        assert_eq!(makefile.to_string(), "-include new.mk\n");
370    }
371
372    #[test]
373    fn test_include_set_optional_true() {
374        let makefile: Makefile = "include config.mk\n".parse().unwrap();
375        let mut inc = makefile.includes().next().unwrap();
376        inc.set_optional(true);
377
378        assert!(inc.is_optional());
379        assert_eq!(makefile.to_string(), "-include config.mk\n");
380    }
381
382    #[test]
383    fn test_include_set_optional_false() {
384        let makefile: Makefile = "-include config.mk\n".parse().unwrap();
385        let mut inc = makefile.includes().next().unwrap();
386        inc.set_optional(false);
387
388        assert!(!inc.is_optional());
389        assert_eq!(makefile.to_string(), "include config.mk\n");
390    }
391
392    #[test]
393    fn test_include_set_optional_from_sinclude() {
394        let makefile: Makefile = "sinclude config.mk\n".parse().unwrap();
395        let mut inc = makefile.includes().next().unwrap();
396        inc.set_optional(false);
397
398        assert!(!inc.is_optional());
399        assert_eq!(makefile.to_string(), "include config.mk\n");
400    }
401
402    #[test]
403    fn test_include_set_optional_already_optional() {
404        let makefile: Makefile = "-include config.mk\n".parse().unwrap();
405        let mut inc = makefile.includes().next().unwrap();
406        inc.set_optional(true);
407
408        // Should remain unchanged
409        assert!(inc.is_optional());
410        assert_eq!(makefile.to_string(), "-include config.mk\n");
411    }
412
413    #[test]
414    fn test_include_set_optional_already_non_optional() {
415        let makefile: Makefile = "include config.mk\n".parse().unwrap();
416        let mut inc = makefile.includes().next().unwrap();
417        inc.set_optional(false);
418
419        // Should remain unchanged
420        assert!(!inc.is_optional());
421        assert_eq!(makefile.to_string(), "include config.mk\n");
422    }
423
424    #[test]
425    fn test_include_combined_operations() {
426        let makefile: Makefile = "include old.mk\nVAR = value\n".parse().unwrap();
427        let mut inc = makefile.includes().next().unwrap();
428
429        // Change path and make optional
430        inc.set_path("new.mk");
431        inc.set_optional(true);
432
433        assert_eq!(inc.path(), Some("new.mk".to_string()));
434        assert!(inc.is_optional());
435        assert_eq!(makefile.to_string(), "-include new.mk\nVAR = value\n");
436    }
437
438    #[test]
439    fn test_include_with_comment() {
440        let makefile: Makefile = "# Comment\ninclude config.mk\n".parse().unwrap();
441        let mut inc = makefile.includes().next().unwrap();
442        inc.remove().unwrap();
443
444        // Comment should also be removed
445        assert_eq!(makefile.includes().count(), 0);
446        assert!(!makefile.to_string().contains("# Comment"));
447    }
448}