Skip to main content

cmakefmt/parser/
mod.rs

1// SPDX-FileCopyrightText: Copyright 2026 Puneet Matharu
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Parser entry points for CMake source text.
6//!
7//! The grammar is defined in `parser/cmake.pest`, while
8//! [`crate::parser::ast`] contains the AST types returned by
9//! [`crate::parser::parse()`].
10
11use pest::Parser;
12
13pub mod ast;
14
15mod generated {
16    use pest_derive::Parser;
17
18    /// Internal pest parser generated from `cmake.pest`.
19    #[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
29/// Parse CMake source text into an AST [`File`].
30///
31/// The returned AST preserves command structure, blank lines, and comments so
32/// the formatter can round-trip files with stable semantics.
33pub 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                // Same line as previous content — try attaching as trailing comment.
122                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                    // Attached — record column for continuation detection.
130                    *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                // Aligned continuation merged into trailing comment.
137            } else {
138                // Standalone comment.
139                *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
181/// When a line comment on its own line is column-aligned with the `#` of a
182/// preceding trailing comment (no blank lines in between), merge the
183/// continuation body into the trailing comment text so the formatter can
184/// re-wrap them as a single unit.
185fn 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
332/// Validate that a bracket argument's opening and closing "=" counts match.
333fn 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); // mylib PUBLIC dep1 PRIVATE dep2
520    }
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); // Command, BlankLines, Comment
604    }
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        // Empty continuation (#) adds nothing; third line appends.
615        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        // Trailing # is at column 14; continuation # is at column 15 — should NOT merge.
624        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}