Skip to main content

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