makefile_lossless/ast/
include.rs1use 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 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 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 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 pub fn parent(&self) -> Option<MakefileItem> {
58 self.syntax().parent().and_then(MakefileItem::cast)
59 }
60
61 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 pub fn set_path(&mut self, new_path: &str) {
100 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 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 self.syntax()
118 .splice_children(expr_idx..expr_idx + 1, vec![new_expr.into()]);
119 }
120 }
121
122 pub fn set_optional(&mut self, optional: bool) {
136 use crate::SyntaxKind::INCLUDE;
137
138 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 if current_text == "include" {
152 "-include"
153 } else if current_text == "sinclude" || current_text == "-include" {
154 return;
156 } else {
157 return;
159 }
160 } else {
161 if current_text == "-include" || current_text == "sinclude" {
163 "include"
164 } else if current_text == "include" {
165 return;
167 } else {
168 return;
170 }
171 };
172
173 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 builder.token(IDENTIFIER.into(), new_keyword);
184 }
185 rowan::NodeOrToken::Token(tok) => {
186 builder.token(tok.kind().into(), tok.text());
188 }
189 rowan::NodeOrToken::Node(node) => {
190 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 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 *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 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 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 let files: Vec<_> = makefile.included_files().collect();
262 assert_eq!(files, vec!["config.mk"]);
263
264 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 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 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 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 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 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 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 assert_eq!(makefile.includes().count(), 0);
496 assert!(!makefile.to_string().contains("# Comment"));
497 }
498}