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 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 pub fn parent(&self) -> Option<MakefileItem> {
41 self.syntax().parent().and_then(MakefileItem::cast)
42 }
43
44 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 pub fn set_path(&mut self, new_path: &str) {
83 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 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 self.syntax()
101 .splice_children(expr_idx..expr_idx + 1, vec![new_expr.into()]);
102 }
103 }
104
105 pub fn set_optional(&mut self, optional: bool) {
119 use crate::SyntaxKind::INCLUDE;
120
121 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 if current_text == "include" {
135 "-include"
136 } else if current_text == "sinclude" || current_text == "-include" {
137 return;
139 } else {
140 return;
142 }
143 } else {
144 if current_text == "-include" || current_text == "sinclude" {
146 "include"
147 } else if current_text == "include" {
148 return;
150 } else {
151 return;
153 }
154 };
155
156 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 builder.token(IDENTIFIER.into(), new_keyword);
167 }
168 rowan::NodeOrToken::Token(tok) => {
169 builder.token(tok.kind().into(), tok.text());
171 }
172 rowan::NodeOrToken::Node(node) => {
173 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 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 *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 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 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 let files: Vec<_> = makefile.included_files().collect();
245 assert_eq!(files, vec!["config.mk"]);
246
247 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 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 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 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 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 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 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 assert_eq!(makefile.includes().count(), 0);
446 assert!(!makefile.to_string().contains("# Comment"));
447 }
448}