1use pest::Parser;
12
13pub mod ast;
14
15mod generated {
16 use pest_derive::Parser;
17
18 #[derive(Parser)]
20 #[grammar = "parser/cmake.pest"]
21 pub(super) struct CmakeParser;
22}
23
24use generated::{CmakeParser, Rule};
25
26use crate::error::{Error, Result};
27use ast::{Argument, BracketArgument, CommandInvocation, Comment, File, Statement};
28
29pub fn parse(source: &str) -> Result<File> {
34 let mut pairs = CmakeParser::parse(Rule::file, source).map_err(|e| {
35 Error::Parse(crate::error::ParseError {
36 display_name: "<source>".to_owned(),
37 source_text: source.to_owned().into_boxed_str(),
38 start_line: 1,
39 diagnostic: crate::error::ParseDiagnostic::from_pest(&e),
40 })
41 })?;
42 let file_pair = pairs
43 .next()
44 .ok_or_else(|| Error::Formatter("parser did not return a file pair".to_owned()))?;
45
46 build_file(file_pair)
47}
48
49fn build_file(pair: pest::iterators::Pair<'_, Rule>) -> Result<File> {
50 debug_assert_eq!(pair.as_rule(), Rule::file);
51
52 let items = pair.into_inner();
53 let mut statements = Vec::with_capacity(items.size_hint().0);
54 let mut pending_blank_lines = 0usize;
55 let mut line_has_content = false;
56 let mut trailing_comment_col: Option<usize> = None;
57
58 for item in items {
59 collect_file_item(
60 item,
61 &mut statements,
62 &mut pending_blank_lines,
63 &mut line_has_content,
64 &mut trailing_comment_col,
65 )?;
66 }
67
68 flush_blank_lines(&mut statements, &mut pending_blank_lines);
69 Ok(File { statements })
70}
71
72fn collect_file_item(
73 item: pest::iterators::Pair<'_, Rule>,
74 statements: &mut Vec<Statement>,
75 pending_blank_lines: &mut usize,
76 line_has_content: &mut bool,
77 trailing_comment_col: &mut Option<usize>,
78) -> Result<()> {
79 match item.as_rule() {
80 Rule::file_item => {
81 for inner in item.into_inner() {
82 collect_file_item(
83 inner,
84 statements,
85 pending_blank_lines,
86 line_has_content,
87 trailing_comment_col,
88 )?;
89 }
90 Ok(())
91 }
92 Rule::command_invocation => {
93 *trailing_comment_col = None;
94 flush_blank_lines(statements, pending_blank_lines);
95 statements.push(Statement::Command(build_command(item)?));
96 *line_has_content = true;
97 Ok(())
98 }
99 Rule::template_placeholder => {
100 *trailing_comment_col = None;
101 flush_blank_lines(statements, pending_blank_lines);
102 statements.push(Statement::TemplatePlaceholder(item.as_str().to_owned()));
103 *line_has_content = true;
104 Ok(())
105 }
106 Rule::bracket_comment => {
107 *trailing_comment_col = None;
108 let comment = Comment::Bracket(item.as_str().to_owned());
109 if let Some(comment) = attach_trailing_comment(statements, comment, *line_has_content) {
110 flush_blank_lines(statements, pending_blank_lines);
111 statements.push(Statement::Comment(comment));
112 }
113 *line_has_content = true;
114 Ok(())
115 }
116 Rule::line_comment => {
117 let col = item.as_span().start_pos().line_col().1;
118 let comment = Comment::Line(item.as_str().to_owned());
119
120 if *line_has_content {
121 if let Some(comment) =
123 attach_trailing_comment(statements, comment, *line_has_content)
124 {
125 flush_blank_lines(statements, pending_blank_lines);
126 statements.push(Statement::Comment(comment));
127 *trailing_comment_col = None;
128 } else {
129 *trailing_comment_col = Some(col);
131 }
132 } else if *pending_blank_lines == 0
133 && *trailing_comment_col == Some(col)
134 && merge_trailing_comment_continuation(statements, &comment)
135 {
136 } else {
138 *trailing_comment_col = None;
140 flush_blank_lines(statements, pending_blank_lines);
141 statements.push(Statement::Comment(comment));
142 }
143
144 *line_has_content = true;
145 Ok(())
146 }
147 Rule::newline => {
148 if *line_has_content {
149 *line_has_content = false;
150 } else {
151 *trailing_comment_col = None;
152 *pending_blank_lines += 1;
153 }
154 Ok(())
155 }
156 Rule::space | Rule::EOI => Ok(()),
157 other => Err(Error::Formatter(format!(
158 "unexpected top-level parser rule: {other:?}"
159 ))),
160 }
161}
162
163fn attach_trailing_comment(
164 statements: &mut [Statement],
165 comment: Comment,
166 line_has_content: bool,
167) -> Option<Comment> {
168 if !line_has_content {
169 return Some(comment);
170 }
171
172 match statements.last_mut() {
173 Some(Statement::Command(command)) if command.trailing_comment.is_none() => {
174 command.trailing_comment = Some(comment);
175 None
176 }
177 _ => Some(comment),
178 }
179}
180
181fn merge_trailing_comment_continuation(
186 statements: &mut [Statement],
187 continuation: &Comment,
188) -> bool {
189 let Some(Statement::Command(command)) = statements.last_mut() else {
190 return false;
191 };
192 let Some(Comment::Line(ref mut text)) = command.trailing_comment else {
193 return false;
194 };
195 let Comment::Line(cont_text) = continuation else {
196 return false;
197 };
198 let body = cont_text.trim_start_matches('#').trim_start();
199 if !body.is_empty() {
200 text.push(' ');
201 text.push_str(body);
202 }
203 true
204}
205
206fn flush_blank_lines(statements: &mut Vec<Statement>, pending_blank_lines: &mut usize) {
207 if *pending_blank_lines == 0 {
208 return;
209 }
210
211 match statements.last_mut() {
212 Some(Statement::BlankLines(count)) => *count += *pending_blank_lines,
213 _ => statements.push(Statement::BlankLines(*pending_blank_lines)),
214 }
215
216 *pending_blank_lines = 0;
217}
218
219fn build_command(pair: pest::iterators::Pair<'_, Rule>) -> Result<CommandInvocation> {
220 debug_assert_eq!(pair.as_rule(), Rule::command_invocation);
221
222 let span = pair.as_span();
223 let mut name = None;
224 let mut arguments = Vec::new();
225
226 for inner in pair.into_inner() {
227 match inner.as_rule() {
228 Rule::identifier => {
229 name = Some(inner.as_str().to_owned());
230 }
231 Rule::arguments => {
232 arguments = build_arguments(inner)?;
233 }
234 Rule::space => {}
235 other => {
236 return Err(Error::Formatter(format!(
237 "unexpected command parser rule: {other:?}"
238 )));
239 }
240 }
241 }
242
243 Ok(CommandInvocation {
244 name: name.ok_or_else(|| Error::Formatter("command missing identifier".to_owned()))?,
245 arguments,
246 trailing_comment: None,
247 span: (span.start(), span.end()),
248 })
249}
250
251fn build_arguments(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<Argument>> {
252 debug_assert_eq!(pair.as_rule(), Rule::arguments);
253
254 let inner = pair.into_inner();
255 let mut args = Vec::with_capacity(inner.size_hint().0);
256
257 for p in inner {
258 collect_argument_part(p, &mut args)?;
259 }
260
261 Ok(args)
262}
263
264fn collect_argument_part(
265 pair: pest::iterators::Pair<'_, Rule>,
266 out: &mut Vec<Argument>,
267) -> Result<()> {
268 match pair.as_rule() {
269 Rule::argument_part => {
270 for inner in pair.into_inner() {
271 collect_argument_part(inner, out)?;
272 }
273 Ok(())
274 }
275 Rule::arguments => {
276 for inner in pair.into_inner() {
277 collect_argument_part(inner, out)?;
278 }
279 Ok(())
280 }
281 Rule::argument => {
282 let mut inner = pair.into_inner();
283 let argument = inner
284 .next()
285 .ok_or_else(|| Error::Formatter("argument missing child node".to_owned()))?;
286 out.push(build_argument(argument)?);
287 Ok(())
288 }
289 Rule::bracket_comment => {
290 out.push(Argument::InlineComment(Comment::Bracket(
291 pair.as_str().to_owned(),
292 )));
293 Ok(())
294 }
295 Rule::line_ending => {
296 collect_line_ending_comments(pair, out);
297 Ok(())
298 }
299 Rule::space => Ok(()),
300 other => Err(Error::Formatter(format!(
301 "unexpected argument parser rule: {other:?}"
302 ))),
303 }
304}
305
306fn collect_line_ending_comments(pair: pest::iterators::Pair<'_, Rule>, out: &mut Vec<Argument>) {
307 for inner in pair.into_inner() {
308 if inner.as_rule() == Rule::line_comment {
309 out.push(Argument::InlineComment(Comment::Line(
310 inner.as_str().to_owned(),
311 )));
312 }
313 }
314}
315
316fn build_argument(pair: pest::iterators::Pair<'_, Rule>) -> Result<Argument> {
317 match pair.as_rule() {
318 Rule::bracket_argument => {
319 let raw = pair.as_str().to_owned();
320 Ok(Argument::Bracket(validate_bracket_argument(raw)?))
321 }
322 Rule::quoted_argument => Ok(Argument::Quoted(pair.as_str().to_owned())),
323 Rule::mixed_unquoted_argument | Rule::unquoted_argument => {
324 Ok(Argument::Unquoted(pair.as_str().to_owned()))
325 }
326 other => Err(Error::Formatter(format!(
327 "unexpected argument rule: {other:?}"
328 ))),
329 }
330}
331
332fn validate_bracket_argument(raw: String) -> Result<BracketArgument> {
334 let open_equals = raw
335 .strip_prefix('[')
336 .ok_or_else(|| Error::Formatter("bracket argument missing '[' prefix".to_owned()))?
337 .bytes()
338 .take_while(|&b| b == b'=')
339 .count();
340
341 let close_equals = raw
342 .strip_suffix(']')
343 .ok_or_else(|| Error::Formatter("bracket argument missing ']' suffix".to_owned()))?
344 .bytes()
345 .rev()
346 .take_while(|&b| b == b'=')
347 .count();
348
349 if open_equals != close_equals {
350 return Err(Error::Formatter(format!(
351 "invalid bracket argument delimiter: {raw}"
352 )));
353 }
354
355 Ok(BracketArgument {
356 level: open_equals,
357 raw,
358 })
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 fn parse_ok(src: &str) -> File {
366 parse(src).unwrap_or_else(|e| panic!("parse failed for {src:?}: {e}"))
367 }
368
369 #[test]
370 fn empty_file() {
371 let f = parse_ok("");
372 assert!(f.statements.is_empty());
373 }
374
375 #[test]
376 fn simple_command() {
377 let f = parse_ok("cmake_minimum_required(VERSION 3.20)\n");
378 assert_eq!(f.statements.len(), 1);
379 let Statement::Command(cmd) = &f.statements[0] else {
380 panic!()
381 };
382 assert_eq!(cmd.name, "cmake_minimum_required");
383 assert_eq!(cmd.arguments.len(), 2);
384 assert!(cmd.trailing_comment.is_none());
385 }
386
387 #[test]
388 fn command_no_args() {
389 let f = parse_ok("some_command()\n");
390 let Statement::Command(cmd) = &f.statements[0] else {
391 panic!()
392 };
393 assert!(cmd.arguments.is_empty());
394 }
395
396 #[test]
397 fn quoted_argument() {
398 let f = parse_ok("message(\"hello world\")\n");
399 let Statement::Command(cmd) = &f.statements[0] else {
400 panic!()
401 };
402 assert!(matches!(&cmd.arguments[0], Argument::Quoted(_)));
403 }
404
405 #[test]
406 fn bracket_argument_zero_equals() {
407 let f = parse_ok("set(VAR [[hello]])\n");
408 let Statement::Command(cmd) = &f.statements[0] else {
409 panic!()
410 };
411 let Argument::Bracket(b) = &cmd.arguments[1] else {
412 panic!()
413 };
414 assert_eq!(b.level, 0);
415 }
416
417 #[test]
418 fn bracket_argument_one_equals() {
419 let f = parse_ok("set(VAR [=[hello]=])\n");
420 let Statement::Command(cmd) = &f.statements[0] else {
421 panic!()
422 };
423 let Argument::Bracket(b) = &cmd.arguments[1] else {
424 panic!()
425 };
426 assert_eq!(b.level, 1);
427 }
428
429 #[test]
430 fn bracket_argument_two_equals() {
431 let f = parse_ok("set(VAR [==[contains ]= inside]==])\n");
432 let Statement::Command(cmd) = &f.statements[0] else {
433 panic!()
434 };
435 let Argument::Bracket(b) = &cmd.arguments[1] else {
436 panic!()
437 };
438 assert_eq!(b.level, 2);
439 }
440
441 #[test]
442 fn invalid_bracket_argument_returns_error() {
443 let err = parse("set(VAR [=[hello]==])\n").unwrap_err();
444 assert!(matches!(err, Error::Formatter(_)));
445 }
446
447 #[test]
448 fn invalid_syntax_returns_parse_error_with_crate_owned_diagnostic() {
449 let err = parse("message(\n").unwrap_err();
450 let Error::Parse(parse_err) = err else {
451 panic!("expected parse error");
452 };
453
454 assert_eq!(parse_err.display_name, "<source>");
455 assert_eq!(parse_err.source_text.as_ref(), "message(\n");
456 assert_eq!(parse_err.start_line, 1);
457 assert!(
458 parse_err.diagnostic.message.contains("expected"),
459 "unexpected parse diagnostic: {:?}",
460 parse_err.diagnostic
461 );
462 assert_eq!(parse_err.diagnostic.line, 2);
463 assert_eq!(parse_err.diagnostic.column, 1);
464 }
465
466 #[test]
467 fn line_comment_standalone() {
468 let f = parse_ok("# this is a comment\n");
469 assert!(matches!(
470 &f.statements[0],
471 Statement::Comment(Comment::Line(_))
472 ));
473 }
474
475 #[test]
476 fn bracket_comment() {
477 let f = parse_ok("#[[ multi\nline ]]\n");
478 assert!(matches!(
479 &f.statements[0],
480 Statement::Comment(Comment::Bracket(_))
481 ));
482 }
483
484 #[test]
485 fn variable_reference_in_unquoted() {
486 let f = parse_ok("message(${MY_VAR})\n");
487 let Statement::Command(cmd) = &f.statements[0] else {
488 panic!()
489 };
490 assert!(matches!(&cmd.arguments[0], Argument::Unquoted(_)));
491 }
492
493 #[test]
494 fn env_variable_reference() {
495 let f = parse_ok("message($ENV{PATH})\n");
496 let Statement::Command(cmd) = &f.statements[0] else {
497 panic!()
498 };
499 assert!(matches!(&cmd.arguments[0], Argument::Unquoted(_)));
500 }
501
502 #[test]
503 fn generator_expression() {
504 let f = parse_ok("target_link_libraries(foo $<TARGET_FILE:bar>)\n");
505 let Statement::Command(cmd) = &f.statements[0] else {
506 panic!()
507 };
508 assert_eq!(cmd.arguments.len(), 2);
509 }
510
511 #[test]
512 fn multiline_argument_list() {
513 let src = "target_link_libraries(mylib\n PUBLIC dep1\n PRIVATE dep2\n)\n";
514 let f = parse_ok(src);
515 let Statement::Command(cmd) = &f.statements[0] else {
516 panic!()
517 };
518 assert_eq!(cmd.name, "target_link_libraries");
519 assert_eq!(cmd.arguments.len(), 5); }
521
522 #[test]
523 fn inline_bracket_comment_in_arguments() {
524 let src = "message(\"First\" #[[inline comment]] \"Second\")\n";
525 let f = parse_ok(src);
526 let Statement::Command(cmd) = &f.statements[0] else {
527 panic!()
528 };
529 assert_eq!(cmd.arguments.len(), 3);
530 assert!(matches!(
531 &cmd.arguments[1],
532 Argument::InlineComment(Comment::Bracket(_))
533 ));
534 }
535
536 #[test]
537 fn line_comment_between_arguments() {
538 let src = "target_sources(foo\n PRIVATE a.cc # keep grouping\n b.cc\n)\n";
539 let f = parse_ok(src);
540 let Statement::Command(cmd) = &f.statements[0] else {
541 panic!()
542 };
543 assert!(cmd.arguments.iter().any(Argument::is_comment));
544 }
545
546 #[test]
547 fn trailing_comment_after_command() {
548 let src = "message(STATUS \"hello\") # trailing\n";
549 let f = parse_ok(src);
550 let Statement::Command(cmd) = &f.statements[0] else {
551 panic!()
552 };
553 assert!(matches!(cmd.trailing_comment, Some(Comment::Line(_))));
554 }
555
556 #[test]
557 fn aligned_continuation_merges_into_trailing_comment() {
558 let src = "set(FOO bar) # first line\n # second line\n";
559 let f = parse_ok(src);
560 assert_eq!(f.statements.len(), 1);
561 let Statement::Command(cmd) = &f.statements[0] else {
562 panic!()
563 };
564 assert_eq!(
565 cmd.trailing_comment,
566 Some(Comment::Line("# first line second line".to_owned()))
567 );
568 }
569
570 #[test]
571 fn multiple_aligned_continuations_merge() {
572 let src = "set(FOO bar) # line one\n # line two\n # line three\n";
573 let f = parse_ok(src);
574 assert_eq!(f.statements.len(), 1);
575 let Statement::Command(cmd) = &f.statements[0] else {
576 panic!()
577 };
578 assert_eq!(
579 cmd.trailing_comment,
580 Some(Comment::Line("# line one line two line three".to_owned()))
581 );
582 }
583
584 #[test]
585 fn non_aligned_comment_stays_standalone() {
586 let src = "set(FOO bar) # trailing\n# standalone\n";
587 let f = parse_ok(src);
588 assert_eq!(f.statements.len(), 2);
589 let Statement::Command(cmd) = &f.statements[0] else {
590 panic!()
591 };
592 assert_eq!(
593 cmd.trailing_comment,
594 Some(Comment::Line("# trailing".to_owned()))
595 );
596 assert!(matches!(f.statements[1], Statement::Comment(_)));
597 }
598
599 #[test]
600 fn blank_line_prevents_continuation_merge() {
601 let src = "set(FOO bar) # trailing\n\n # not a continuation\n";
602 let f = parse_ok(src);
603 assert_eq!(f.statements.len(), 3); }
605
606 #[test]
607 fn empty_continuation_line_merges_without_adding_text() {
608 let src = "set(FOO bar) # first\n #\n # third\n";
609 let f = parse_ok(src);
610 assert_eq!(f.statements.len(), 1);
611 let Statement::Command(cmd) = &f.statements[0] else {
612 panic!()
613 };
614 assert_eq!(
616 cmd.trailing_comment,
617 Some(Comment::Line("# first third".to_owned()))
618 );
619 }
620
621 #[test]
622 fn off_by_one_column_prevents_merge() {
623 let src = "set(FOO bar) # trailing\n # off by one\n";
625 let f = parse_ok(src);
626 assert_eq!(f.statements.len(), 2);
627 assert!(matches!(f.statements[1], Statement::Comment(_)));
628 }
629
630 #[test]
631 fn file_without_final_newline() {
632 let f = parse_ok("project(MyProject)");
633 assert_eq!(f.statements.len(), 1);
634 }
635
636 #[test]
637 fn blank_lines_are_preserved() {
638 let f = parse_ok("message(foo)\n\nproject(bar)\n");
639 assert_eq!(f.statements.len(), 3);
640 assert!(matches!(f.statements[1], Statement::BlankLines(1)));
641 }
642
643 #[test]
644 fn leading_blank_lines_are_preserved() {
645 let f = parse_ok("\nmessage(foo)\n");
646 assert!(matches!(f.statements[0], Statement::BlankLines(1)));
647 }
648
649 #[test]
650 fn escape_sequences_in_quoted() {
651 let f = parse_ok("message(\"tab\\there\\nnewline\")\n");
652 assert!(!f.statements.is_empty());
653 }
654
655 #[test]
656 fn escaped_quotes_in_quoted_argument_parse() {
657 let f = parse_ok("message(FATAL_ERROR \"foo \\\"Debug\\\"\")\n");
658 let Statement::Command(cmd) = &f.statements[0] else {
659 panic!()
660 };
661 let args: Vec<&str> = cmd.arguments.iter().map(Argument::as_str).collect();
662 assert_eq!(args, vec!["FATAL_ERROR", "\"foo \\\"Debug\\\"\""]);
663 }
664
665 #[test]
666 fn multiple_commands() {
667 let src = "cmake_minimum_required(VERSION 3.20)\nproject(MyProject)\n";
668 let f = parse_ok(src);
669 assert_eq!(f.statements.len(), 2);
670 }
671
672 #[test]
673 fn nested_variable_reference() {
674 let f = parse_ok("message(${${OUTER}})\n");
675 let Statement::Command(cmd) = &f.statements[0] else {
676 panic!()
677 };
678 assert_eq!(cmd.arguments.len(), 1);
679 }
680
681 #[test]
682 fn underscore_command_name_is_valid() {
683 let f = parse_ok("_my_command(ARG)\n");
684 let Statement::Command(cmd) = &f.statements[0] else {
685 panic!()
686 };
687 assert_eq!(cmd.name, "_my_command");
688 }
689
690 #[test]
691 fn nested_parentheses_in_arguments_are_preserved_as_unquoted_tokens() {
692 let f = parse_ok("if(FALSE AND (FALSE OR TRUE))\n");
693 let Statement::Command(cmd) = &f.statements[0] else {
694 panic!()
695 };
696 let args: Vec<&str> = cmd.arguments.iter().map(Argument::as_str).collect();
697 assert_eq!(args, vec!["FALSE", "AND", "(FALSE OR TRUE)"]);
698 }
699
700 #[test]
701 fn multiline_nested_parentheses_in_arguments_are_preserved_as_unquoted_tokens() {
702 let f = parse_ok(concat!(
703 "IF(NOT (have_C__fsanitize_memory__fsanitize_memory_track_origins__U_FORTIFY_SOURCE\n",
704 " AND have_CXX__fsanitize_memory__fsanitize_memory_track_origins__U_FORTIFY_SOURCE))\n",
705 ));
706 let Statement::Command(cmd) = &f.statements[0] else {
707 panic!()
708 };
709 let args: Vec<&str> = cmd.arguments.iter().map(Argument::as_str).collect();
710 assert_eq!(
711 args,
712 vec![
713 "NOT",
714 "(have_C__fsanitize_memory__fsanitize_memory_track_origins__U_FORTIFY_SOURCE\n AND have_CXX__fsanitize_memory__fsanitize_memory_track_origins__U_FORTIFY_SOURCE)"
715 ]
716 );
717 }
718
719 #[test]
720 fn source_file_with_utf8_bom_parses() {
721 let f = parse_ok("\u{FEFF}project(MyProject)\n");
722 assert_eq!(f.statements.len(), 1);
723 }
724
725 #[test]
726 fn top_level_template_placeholder_parses() {
727 let f = parse_ok("@PACKAGE_INIT@\n");
728 assert_eq!(
729 f.statements,
730 vec![Statement::TemplatePlaceholder("@PACKAGE_INIT@".to_owned())]
731 );
732 }
733
734 #[test]
735 fn legacy_unquoted_argument_with_embedded_quotes_parses() {
736 let f = parse_ok("set(x -Da=\"b c\")\n");
737 let Statement::Command(cmd) = &f.statements[0] else {
738 panic!()
739 };
740 assert_eq!(cmd.arguments[1].as_str(), "-Da=\"b c\"");
741 }
742
743 #[test]
744 fn legacy_unquoted_argument_with_make_style_reference_parses() {
745 let f = parse_ok("set(x -Da=$(v))\n");
746 let Statement::Command(cmd) = &f.statements[0] else {
747 panic!()
748 };
749 assert_eq!(cmd.arguments[1].as_str(), "-Da=$(v)");
750 }
751
752 #[test]
753 fn legacy_unquoted_argument_with_embedded_parens_parses() {
754 let f = parse_ok(r##"set(VERSION_REGEX "#define CLI11_VERSION[ ]+"(.+)"")"##);
755 let Statement::Command(cmd) = &f.statements[0] else {
756 panic!()
757 };
758 assert_eq!(
759 cmd.arguments[1].as_str(),
760 "\"#define CLI11_VERSION[ \t]+\"(.+)\"\""
761 );
762 }
763
764 #[test]
765 fn legacy_unquoted_argument_starting_with_quoted_segment_parses() {
766 let f = parse_ok(r##"list(APPEND force-libcxx "CMAKE_CXX_COMPILER_ID STREQUAL "Clang"")"##);
767 let Statement::Command(cmd) = &f.statements[0] else {
768 panic!()
769 };
770 assert_eq!(
771 cmd.arguments[2].as_str(),
772 "\"CMAKE_CXX_COMPILER_ID STREQUAL \"Clang\"\""
773 );
774 }
775}