Skip to main content

cmakefmt/spec/
registry.rs

1// SPDX-FileCopyrightText: Copyright 2026 Puneet Matharu
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Built-in and override-backed command registry.
6
7#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
8use std::fs;
9#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
10use std::path::Path;
11use std::path::PathBuf;
12use std::sync::OnceLock;
13
14use indexmap::{IndexMap, IndexSet};
15
16#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
17use crate::config::file::{detect_config_format, ConfigFileFormat};
18use crate::error::{Error, Result};
19
20use super::{
21    has_ascii_uppercase, CommandForm, CommandFormOverride, CommandSpec, CommandSpecOverride,
22    KwargSpec, KwargSpecOverride, LayoutOverrides, LayoutOverridesOverride, SpecFile, SpecMetadata,
23    SpecOverrideFile,
24};
25
26// The embedded command spec is split across two YAML files:
27//
28// * `builtins.yaml` covers commands documented by `cmake --help-command-list`
29//   — the CMake language itself (`if`, `add_executable`, `install`, etc.).
30// * `modules.yaml` covers commands defined in CMake's bundled modules
31//   (`FetchContent_Declare`, `ExternalProject_Add`, `find_dependency`, the
32//   `Check<X>` family, etc.) which become available after
33//   `include(<Module>)` or `find_package(<Module>)`.
34//
35// The two-file split mirrors the natural taxonomy users already use to
36// think about CMake commands and keeps `builtins.yaml` focused on the
37// language surface. The runtime loads both at startup and merges them
38// into a single command table; spec consumers see no difference.
39//
40// The pre-deserialised MessagePack blobs come from `build.rs`. Decoding
41// them with `rmp-serde` is roughly 20× faster than parsing YAML on every
42// process startup. The human-readable sources remain
43// `src/spec/builtins.yaml` and `src/spec/modules.yaml`.
44//
45// A future refactor could move modules to per-module YAML files under
46// `src/spec/modules/<Name>.yaml` if/when the formatter becomes aware
47// of which modules have actually been included earlier in the file
48// being formatted. Until then the single-file form is simpler and
49// equivalent.
50const BUILTINS_PATH: &str = "src/spec/builtins.yaml";
51const BUILTINS_MSGPACK: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/builtins.msgpack"));
52const MODULES_PATH: &str = "src/spec/modules.yaml";
53const MODULES_MSGPACK: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/modules.msgpack"));
54
55/// Registry of known CMake command specifications used to guide formatting.
56///
57/// The registry describes the argument structure of each command — positional
58/// slots, keyword sections, flags, and per-form layout hints — so the formatter
59/// can group and wrap arguments correctly.
60///
61/// # Two-tier model
62///
63/// The built-in registry covers the full CMake standard library.  User override
64/// files (TOML or YAML) can extend or modify any entry without replacing the
65/// whole registry.
66///
67/// # Getting a registry
68///
69/// | Situation | Recommended call |
70/// |-----------|-----------------|
71/// | No customisation needed | [`CommandRegistry::builtins`] — lazily initialised singleton, cheapest |
72/// | Fresh owned copy | [`CommandRegistry::load`] — allocates every call |
73/// | Merge with user override file | [`CommandRegistry::from_builtins_and_overrides`] |
74/// | Owned copy without overrides | [`CommandRegistry::from_builtins_and_overrides`] with `None::<&Path>` (equivalent to `load()`) |
75#[derive(Debug, Clone)]
76pub struct CommandRegistry {
77    metadata: SpecMetadata,
78    builtin_commands: IndexSet<String>,
79    commands: IndexMap<String, CommandSpec>,
80    fallback: CommandSpec,
81}
82
83impl CommandRegistry {
84    /// Load the embedded built-in registry from `builtins.yaml`.
85    ///
86    /// Returns a fresh owned [`CommandRegistry`] on every call.  Prefer
87    /// [`CommandRegistry::builtins`] when you only need a read-only reference —
88    /// it initialises once and amortises the parse cost across all callers.
89    pub fn load() -> Result<Self> {
90        Self::load_builtins_impl()
91    }
92
93    /// Return the lazily initialised built-in registry singleton.
94    ///
95    /// The registry is parsed exactly once on first call; subsequent calls
96    /// return a `&'static` reference at zero cost.  Use [`CommandRegistry::load`]
97    /// if you need an owned, mutable copy.
98    pub fn builtins() -> &'static Self {
99        static BUILTINS: OnceLock<CommandRegistry> = OnceLock::new();
100        BUILTINS.get_or_init(|| {
101            Self::load_builtins_impl()
102                .expect("embedded built-in command registry should deserialize")
103        })
104    }
105
106    #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
107    fn load_builtins_impl() -> Result<Self> {
108        Self::from_builtins_and_overrides(None::<&Path>)
109    }
110
111    #[cfg(any(target_arch = "wasm32", not(feature = "cli")))]
112    fn load_builtins_impl() -> Result<Self> {
113        Ok(Self::from_spec_file(parse_embedded_spec()?))
114    }
115
116    /// Load the embedded built-ins and optionally merge a user override file.
117    #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
118    pub fn from_builtins_and_overrides(path: Option<impl AsRef<Path>>) -> Result<Self> {
119        let mut registry = Self::from_spec_file(parse_embedded_spec()?);
120
121        if let Some(path) = path {
122            registry.merge_override_file(path.as_ref())?;
123        }
124
125        Ok(registry)
126    }
127
128    /// Build a registry directly from a deserialized [`SpecFile`].
129    pub(crate) fn from_spec_file(mut spec_file: SpecFile) -> Self {
130        normalize_spec_file(&mut spec_file);
131        let builtin_commands = spec_file.commands.keys().cloned().collect();
132        Self {
133            metadata: spec_file.metadata,
134            builtin_commands,
135            commands: spec_file.commands,
136            fallback: CommandSpec::Single(CommandForm::default()),
137        }
138    }
139
140    /// Merge TOML-formatted command spec overrides from a string.
141    ///
142    /// # Examples
143    ///
144    /// ```no_run
145    /// use cmakefmt::CommandRegistry;
146    ///
147    /// let mut registry = CommandRegistry::load().unwrap();
148    /// registry.merge_toml_overrides(r#"
149    ///     [commands.my_add_test]
150    ///     pargs = 0
151    ///     flags = ["VERBOSE"]
152    ///
153    ///     [commands.my_add_test.kwargs.NAME]
154    ///     nargs = 1
155    ///
156    ///     [commands.my_add_test.kwargs.SOURCES]
157    ///     nargs = "+"
158    /// "#).unwrap();
159    /// ```
160    ///
161    /// # Errors
162    ///
163    /// Returns [`Error::Formatter`] with an unstructured parse error
164    /// string. For structured line/column diagnostics, use
165    /// [`CommandRegistry::merge_override_str`] or
166    /// [`CommandRegistry::merge_override_file`] which return
167    /// [`Error::Spec`].
168    pub fn merge_toml_overrides(&mut self, toml_source: &str) -> Result<()> {
169        let mut overrides: SpecOverrideFile = toml::from_str(toml_source)
170            .map_err(|e| Error::Formatter(format!("spec TOML error: {e}")))?;
171        self.apply_overrides(&mut overrides);
172        Ok(())
173    }
174
175    /// Merge YAML-formatted command spec overrides from a string.
176    ///
177    /// # Examples
178    ///
179    /// ```no_run
180    /// use cmakefmt::CommandRegistry;
181    ///
182    /// let mut registry = CommandRegistry::load().unwrap();
183    /// registry.merge_yaml_overrides("
184    /// commands:
185    ///   my_add_test:
186    ///     pargs: 0
187    ///     flags: [VERBOSE]
188    ///     kwargs:
189    ///       NAME:
190    ///         nargs: 1
191    ///       SOURCES:
192    ///         nargs: \"+\"
193    /// ").unwrap();
194    /// ```
195    ///
196    /// # Errors
197    ///
198    /// Returns [`Error::Formatter`] with an unstructured parse error
199    /// string. For structured line/column diagnostics, use
200    /// [`CommandRegistry::merge_override_file`] which returns
201    /// [`Error::Spec`].
202    pub fn merge_yaml_overrides(&mut self, yaml_source: &str) -> Result<()> {
203        let mut overrides: SpecOverrideFile = serde_yaml::from_str(yaml_source)
204            .map_err(|e| Error::Formatter(format!("spec YAML error: {e}")))?;
205        self.apply_overrides(&mut overrides);
206        Ok(())
207    }
208
209    fn apply_overrides(&mut self, overrides: &mut SpecOverrideFile) {
210        normalize_override_file(overrides);
211        let commands = std::mem::take(&mut overrides.commands);
212        for (name, override_spec) in commands {
213            match self.commands.get_mut(&name) {
214                Some(existing) => merge_command_spec(existing, override_spec),
215                None => {
216                    self.commands.insert(name, override_spec.into_full_spec());
217                }
218            }
219        }
220    }
221
222    /// Merge a supported user override file from disk into the registry.
223    ///
224    /// # Errors
225    ///
226    /// Deserialisation failures are reported as [`Error::Spec`] with
227    /// structured [`crate::error::FileParseError`] metadata
228    /// including 1-based line and column numbers — suitable for
229    /// surfacing to editors and IDE integrations. I/O failures are
230    /// reported as [`Error::Io`].
231    #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
232    #[cfg_attr(docsrs, doc(cfg(feature = "cli")))]
233    pub fn merge_override_file(&mut self, path: &Path) -> Result<()> {
234        let source = fs::read_to_string(path)?;
235        self.merge_override_source(&source, path.to_path_buf(), detect_config_format(path)?)
236    }
237
238    /// Merge TOML override contents into the registry.
239    ///
240    /// # Errors
241    ///
242    /// Like [`merge_override_file`](Self::merge_override_file),
243    /// parse failures are reported as [`Error::Spec`] with
244    /// structured line/column metadata — unlike
245    /// [`merge_toml_overrides`](Self::merge_toml_overrides), which
246    /// returns an unstructured [`Error::Formatter`].
247    #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
248    #[cfg_attr(docsrs, doc(cfg(feature = "cli")))]
249    pub fn merge_override_str(&mut self, source: &str, path: impl Into<PathBuf>) -> Result<()> {
250        self.merge_override_source(source, path.into(), ConfigFileFormat::Toml)
251    }
252
253    #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
254    fn merge_override_source(
255        &mut self,
256        source: &str,
257        path: PathBuf,
258        format: ConfigFileFormat,
259    ) -> Result<()> {
260        let mut overrides: SpecOverrideFile = match format {
261            ConfigFileFormat::Toml => toml::from_str(source).map_err(|toml_err| {
262                let (line, column) = crate::config::file::toml_line_col(
263                    source,
264                    toml_err.span().map(|span| span.start),
265                );
266                Error::Spec(crate::error::SpecError::new(
267                    path.clone(),
268                    format.as_str(),
269                    toml_err.to_string(),
270                    line,
271                    column,
272                ))
273            })?,
274            ConfigFileFormat::Yaml => serde_yaml::from_str(source).map_err(|yaml_err| {
275                let location = yaml_err.location();
276                Error::Spec(crate::error::SpecError::new(
277                    path.clone(),
278                    format.as_str(),
279                    yaml_err.to_string(),
280                    location.as_ref().map(|loc| loc.line()),
281                    location.as_ref().map(|loc| loc.column()),
282                ))
283            })?,
284        };
285        normalize_override_file(&mut overrides);
286
287        for (name, override_spec) in overrides.commands {
288            match self.commands.get_mut(&name) {
289                Some(existing) => merge_command_spec(existing, override_spec),
290                None => {
291                    self.commands.insert(name, override_spec.into_full_spec());
292                }
293            }
294        }
295
296        Ok(())
297    }
298
299    /// Get the command spec for `command_name`, falling back to a
300    /// permissive default when the command is unknown.
301    ///
302    /// The fallback is a [`CommandSpec::Single`] with `pargs =
303    /// ZeroOrMore`, no kwargs, and no flags — i.e. "format as
304    /// generically as possible, treat every token as a positional
305    /// argument". This lets user-defined commands format sensibly
306    /// without requiring every project to author a spec override.
307    pub fn get(&self, command_name: &str) -> &CommandSpec {
308        if let Some(spec) = self.commands.get(command_name) {
309            return spec;
310        }
311
312        if !has_ascii_uppercase(command_name) {
313            return &self.fallback;
314        }
315
316        self.commands
317            .get(&command_name.to_ascii_lowercase())
318            .unwrap_or(&self.fallback)
319    }
320
321    /// Return `true` when the command has a known spec (built-in or
322    /// user-defined).
323    pub fn contains(&self, command_name: &str) -> bool {
324        self.commands.contains_key(command_name)
325            || (has_ascii_uppercase(command_name)
326                && self
327                    .commands
328                    .contains_key(&command_name.to_ascii_lowercase()))
329    }
330
331    /// Return `true` when the command is present in the built-in registry.
332    pub fn contains_builtin(&self, command_name: &str) -> bool {
333        self.builtin_commands.contains(command_name)
334            || (has_ascii_uppercase(command_name)
335                && self
336                    .builtin_commands
337                    .contains(&command_name.to_ascii_lowercase()))
338    }
339
340    /// Report the upstream CMake version the built-in spec was last
341    /// audited against. The return value is a SemVer-style string
342    /// (e.g. `"4.3.1"`) sourced from the `[metadata]` block in
343    /// `src/spec/builtins.yaml`. Useful for tooling that wants to
344    /// surface "cmakefmt knows about CMake X.Y" to end users.
345    pub fn audited_cmake_version(&self) -> &str {
346        &self.metadata.cmake_version
347    }
348
349    /// Iterate over the names of every built-in command in the
350    /// registry. Yields the lowercase canonical form; user-merged
351    /// override commands are excluded. Intended for tooling that
352    /// wants to introspect the spec surface (e.g.
353    /// `cmakefmt dump spec-coverage`).
354    pub fn builtin_command_names(&self) -> impl Iterator<Item = &str> {
355        self.builtin_commands.iter().map(String::as_str)
356    }
357}
358
359/// Decode the embedded `builtins.yaml` blob and merge in the embedded
360/// `modules.yaml` blob, returning a single combined [`SpecFile`].
361///
362/// Metadata (e.g. the audited CMake version) is taken from the builtins
363/// blob; `modules.yaml` is expected to contribute only command entries.
364/// On a key collision between the two blobs, the modules entry wins —
365/// in practice this should never happen, since CMake's
366/// `--help-command-list` and `--help-module-list` are disjoint.
367fn parse_embedded_spec() -> Result<SpecFile> {
368    let mut spec = parse_msgpack_spec(BUILTINS_MSGPACK, BUILTINS_PATH)?;
369    let modules = parse_msgpack_spec(MODULES_MSGPACK, MODULES_PATH)?;
370    spec.commands.extend(modules.commands);
371    Ok(spec)
372}
373
374fn parse_msgpack_spec(bytes: &[u8], path: &str) -> Result<SpecFile> {
375    let mut spec: SpecFile = rmp_serde::from_slice(bytes).map_err(|source| {
376        Error::Spec(crate::error::SpecError::new(
377            PathBuf::from(path),
378            "MessagePack",
379            source.to_string(),
380            None,
381            None,
382        ))
383    })?;
384    normalize_spec_file(&mut spec);
385    Ok(spec)
386}
387
388fn normalize_spec_file(spec: &mut SpecFile) {
389    spec.commands = std::mem::take(&mut spec.commands)
390        .into_iter()
391        .map(|(name, mut command)| {
392            normalize_command_spec(&mut command);
393            (name.to_ascii_lowercase(), command)
394        })
395        .collect();
396}
397
398fn normalize_override_file(spec: &mut SpecOverrideFile) {
399    spec.commands = std::mem::take(&mut spec.commands)
400        .into_iter()
401        .map(|(name, mut command)| {
402            normalize_command_override(&mut command);
403            (name.to_ascii_lowercase(), command)
404        })
405        .collect();
406}
407
408fn normalize_command_spec(spec: &mut CommandSpec) {
409    match spec {
410        CommandSpec::Single(form) => normalize_form(form),
411        CommandSpec::Discriminated { forms, fallback } => {
412            *forms = std::mem::take(forms)
413                .into_iter()
414                .map(|(name, mut form)| {
415                    normalize_form(&mut form);
416                    (name.to_ascii_uppercase(), form)
417                })
418                .collect();
419
420            if let Some(fallback) = fallback {
421                normalize_form(fallback);
422            }
423        }
424    }
425}
426
427fn normalize_command_override(spec: &mut CommandSpecOverride) {
428    match spec {
429        CommandSpecOverride::Single(form) => normalize_form_override(form),
430        CommandSpecOverride::Discriminated { forms, fallback } => {
431            *forms = std::mem::take(forms)
432                .into_iter()
433                .map(|(name, mut form)| {
434                    normalize_form_override(&mut form);
435                    (name.to_ascii_uppercase(), form)
436                })
437                .collect();
438
439            if let Some(fallback) = fallback {
440                normalize_form_override(fallback);
441            }
442        }
443    }
444}
445
446fn normalize_form(form: &mut CommandForm) {
447    form.kwargs = std::mem::take(&mut form.kwargs)
448        .into_iter()
449        .map(|(name, mut kwarg)| {
450            normalize_kwarg(&mut kwarg);
451            (name.to_ascii_uppercase(), kwarg)
452        })
453        .collect();
454
455    form.flags = std::mem::take(&mut form.flags)
456        .into_iter()
457        .map(|flag| flag.to_ascii_uppercase())
458        .collect();
459}
460
461fn normalize_form_override(form: &mut CommandFormOverride) {
462    form.kwargs = std::mem::take(&mut form.kwargs)
463        .into_iter()
464        .map(|(name, mut kwarg)| {
465            normalize_kwarg_override(&mut kwarg);
466            (name.to_ascii_uppercase(), kwarg)
467        })
468        .collect();
469
470    form.flags = std::mem::take(&mut form.flags)
471        .into_iter()
472        .map(|flag| flag.to_ascii_uppercase())
473        .collect();
474}
475
476fn normalize_kwarg(spec: &mut KwargSpec) {
477    spec.kwargs = std::mem::take(&mut spec.kwargs)
478        .into_iter()
479        .map(|(name, mut kwarg)| {
480            normalize_kwarg(&mut kwarg);
481            (name.to_ascii_uppercase(), kwarg)
482        })
483        .collect();
484
485    spec.flags = std::mem::take(&mut spec.flags)
486        .into_iter()
487        .map(|flag| flag.to_ascii_uppercase())
488        .collect();
489}
490
491fn normalize_kwarg_override(spec: &mut KwargSpecOverride) {
492    spec.kwargs = std::mem::take(&mut spec.kwargs)
493        .into_iter()
494        .map(|(name, mut kwarg)| {
495            normalize_kwarg_override(&mut kwarg);
496            (name.to_ascii_uppercase(), kwarg)
497        })
498        .collect();
499
500    spec.flags = std::mem::take(&mut spec.flags)
501        .into_iter()
502        .map(|flag| flag.to_ascii_uppercase())
503        .collect();
504}
505
506fn merge_command_spec(base: &mut CommandSpec, override_spec: CommandSpecOverride) {
507    match (base, override_spec) {
508        (CommandSpec::Single(base_form), CommandSpecOverride::Single(override_form)) => {
509            merge_form(base_form, override_form);
510        }
511        (
512            CommandSpec::Discriminated {
513                forms: base_forms,
514                fallback: base_fallback,
515            },
516            CommandSpecOverride::Discriminated {
517                forms: override_forms,
518                fallback: override_fallback,
519            },
520        ) => {
521            for (name, override_form) in override_forms {
522                match base_forms.get_mut(&name) {
523                    Some(base_form) => merge_form(base_form, override_form),
524                    None => {
525                        base_forms.insert(name, override_form.into_full_form());
526                    }
527                }
528            }
529
530            if let Some(override_fallback) = override_fallback {
531                match base_fallback {
532                    Some(base_fallback) => merge_form(base_fallback, override_fallback),
533                    None => {
534                        *base_fallback = Some(override_fallback.into_full_form());
535                    }
536                }
537            }
538        }
539        (base_spec, override_spec) => {
540            *base_spec = override_spec.into_full_spec();
541        }
542    }
543}
544
545fn merge_form(base: &mut CommandForm, override_form: CommandFormOverride) {
546    if let Some(pargs) = override_form.pargs {
547        base.pargs = pargs;
548    }
549
550    merge_flags(&mut base.flags, override_form.flags);
551
552    for (name, override_kwarg) in override_form.kwargs {
553        match base.kwargs.get_mut(&name) {
554            Some(base_kwarg) => merge_kwarg(base_kwarg, override_kwarg),
555            None => {
556                base.kwargs.insert(name, override_kwarg.into_full_spec());
557            }
558        }
559    }
560
561    if let Some(layout) = override_form.layout {
562        merge_layout(
563            base.layout.get_or_insert_with(LayoutOverrides::default),
564            layout,
565        );
566    }
567}
568
569fn merge_kwarg(base: &mut KwargSpec, override_kwarg: KwargSpecOverride) {
570    if let Some(nargs) = override_kwarg.nargs {
571        base.nargs = nargs;
572    }
573
574    merge_flags(&mut base.flags, override_kwarg.flags);
575
576    for (name, nested_override) in override_kwarg.kwargs {
577        match base.kwargs.get_mut(&name) {
578            Some(base_nested) => merge_kwarg(base_nested, nested_override),
579            None => {
580                base.kwargs.insert(name, nested_override.into_full_spec());
581            }
582        }
583    }
584}
585
586fn merge_layout(base: &mut LayoutOverrides, override_layout: LayoutOverridesOverride) {
587    // Destructure the override so adding a field to `LayoutOverridesOverride`
588    // forces a compile error here until it is handled — a previous version of
589    // this function silently dropped `wrap_after_first_arg`.
590    let LayoutOverridesOverride {
591        line_width,
592        tab_size,
593        dangle_parens,
594        always_wrap,
595        max_pargs_hwrap,
596        wrap_after_first_arg,
597        continuation_align,
598    } = override_layout;
599
600    if let Some(value) = line_width {
601        base.line_width = Some(value);
602    }
603    if let Some(value) = tab_size {
604        base.tab_size = Some(value);
605    }
606    if let Some(value) = dangle_parens {
607        base.dangle_parens = Some(value);
608    }
609    if let Some(value) = always_wrap {
610        base.always_wrap = Some(value);
611    }
612    if let Some(value) = max_pargs_hwrap {
613        base.max_pargs_hwrap = Some(value);
614    }
615    if let Some(value) = wrap_after_first_arg {
616        base.wrap_after_first_arg = Some(value);
617    }
618    if let Some(value) = continuation_align {
619        base.continuation_align = Some(value);
620    }
621}
622
623fn merge_flags(base: &mut IndexSet<String>, override_flags: IndexSet<String>) {
624    for flag in override_flags {
625        base.insert(flag);
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632    use crate::spec::NArgs;
633    use std::fs;
634
635    #[test]
636    fn registry_has_target_link_libraries_keywords() {
637        let registry = CommandRegistry::load().unwrap();
638        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
639            panic!()
640        };
641        assert!(form.kwargs.contains_key("PUBLIC"));
642        assert!(form.kwargs.contains_key("PRIVATE"));
643        assert!(form.kwargs.contains_key("INTERFACE"));
644    }
645
646    #[test]
647    fn registry_has_install_forms() {
648        let registry = CommandRegistry::load().unwrap();
649        assert!(matches!(
650            registry.get("install"),
651            CommandSpec::Discriminated { .. }
652        ));
653    }
654
655    #[test]
656    fn registry_unknown_command_uses_fallback() {
657        let registry = CommandRegistry::load().unwrap();
658        let spec = registry.get("my_unknown_command");
659        let CommandSpec::Single(form) = spec else {
660            panic!()
661        };
662        assert_eq!(form.pargs, NArgs::ZeroOrMore);
663        assert!(form.kwargs.is_empty());
664        assert!(form.flags.is_empty());
665    }
666
667    #[test]
668    fn registry_knows_builtin_surface() {
669        let registry = CommandRegistry::load().unwrap();
670        assert!(registry.contains_builtin("cmake_minimum_required"));
671        assert!(registry.contains_builtin("target_sources"));
672        assert!(registry.contains_builtin("while"));
673        assert!(registry.contains_builtin("external_project_add"));
674    }
675
676    #[test]
677    fn registry_reports_audited_cmake_version() {
678        let registry = CommandRegistry::load().unwrap();
679        assert_eq!(registry.audited_cmake_version(), "4.3.1");
680    }
681
682    #[test]
683    fn registry_knows_project_43_keywords() {
684        let registry = CommandRegistry::load().unwrap();
685        let CommandSpec::Single(form) = registry.get("project") else {
686            panic!()
687        };
688        assert!(form.flags.contains("COMPAT_VERSION"));
689        assert!(form.flags.contains("SPDX_LICENSE"));
690    }
691
692    #[test]
693    fn registry_knows_export_package_info_form() {
694        let registry = CommandRegistry::load().unwrap();
695        let CommandSpec::Discriminated { .. } = registry.get("export") else {
696            panic!()
697        };
698        let form = registry.get("export").form_for(Some("PACKAGE_INFO"));
699        assert_eq!(form.pargs, NArgs::Fixed(1));
700        assert!(form.kwargs.contains_key("EXPORT"));
701        assert!(form.kwargs.contains_key("CXX_MODULES_DIRECTORY"));
702    }
703
704    #[test]
705    fn registry_knows_install_package_info_form() {
706        let registry = CommandRegistry::load().unwrap();
707        let form = registry.get("install").form_for(Some("PACKAGE_INFO"));
708        assert_eq!(form.pargs, NArgs::Fixed(1));
709        assert!(form.kwargs.contains_key("DESTINATION"));
710        assert!(form.kwargs.contains_key("COMPAT_VERSION"));
711    }
712
713    #[test]
714    fn registry_knows_install_export_namespace_keyword() {
715        let registry = CommandRegistry::load().unwrap();
716        let form = registry.get("install").form_for(Some("EXPORT"));
717        assert!(form.kwargs.contains_key("DESTINATION"));
718        assert!(form.kwargs.contains_key("NAMESPACE"));
719        assert!(form.kwargs.contains_key("FILE"));
720        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
721    }
722
723    #[test]
724    fn registry_knows_install_targets_export_and_includes_sections() {
725        let registry = CommandRegistry::load().unwrap();
726        let form = registry.get("install").form_for(Some("TARGETS"));
727        assert!(form.kwargs.contains_key("EXPORT"));
728        assert!(form.kwargs.contains_key("INCLUDES"));
729        assert!(form
730            .kwargs
731            .get("INCLUDES")
732            .is_some_and(|spec| spec.kwargs.contains_key("DESTINATION")));
733        assert!(form.kwargs.contains_key("RUNTIME_DEPENDENCY_SET"));
734    }
735
736    #[test]
737    fn install_targets_artifact_kinds_are_kwargs_with_subgroups() {
738        let registry = CommandRegistry::load().unwrap();
739        let form = registry.get("install").form_for(Some("TARGETS"));
740
741        for kind in [
742            "ARCHIVE",
743            "LIBRARY",
744            "RUNTIME",
745            "OBJECTS",
746            "FRAMEWORK",
747            "BUNDLE",
748            "PRIVATE_HEADER",
749            "PUBLIC_HEADER",
750            "RESOURCE",
751            "FILE_SET",
752            "CXX_MODULES_BMI",
753        ] {
754            let spec = form
755                .kwargs
756                .get(kind)
757                .unwrap_or_else(|| panic!("install(TARGETS) missing artifact kind {kind}"));
758            for sub in [
759                "DESTINATION",
760                "PERMISSIONS",
761                "CONFIGURATIONS",
762                "COMPONENT",
763                "NAMELINK_COMPONENT",
764            ] {
765                assert!(
766                    spec.kwargs.contains_key(sub),
767                    "{kind} missing subkwarg {sub}"
768                );
769            }
770            for flag in [
771                "OPTIONAL",
772                "EXCLUDE_FROM_ALL",
773                "NAMELINK_ONLY",
774                "NAMELINK_SKIP",
775            ] {
776                assert!(spec.flags.contains(flag), "{kind} missing subflag {flag}");
777            }
778            assert!(
779                !form.flags.contains(kind),
780                "{kind} should not appear as an outer flag"
781            );
782        }
783    }
784
785    #[test]
786    fn install_targets_file_set_takes_positional_set_name() {
787        let registry = CommandRegistry::load().unwrap();
788        let form = registry.get("install").form_for(Some("TARGETS"));
789        let file_set = form.kwargs.get("FILE_SET").unwrap();
790        assert_eq!(file_set.nargs, crate::spec::NArgs::Fixed(1));
791    }
792
793    #[test]
794    fn install_targets_artifact_option_flags_are_not_outer_flags() {
795        let registry = CommandRegistry::load().unwrap();
796        let form = registry.get("install").form_for(Some("TARGETS"));
797        for flag in [
798            "OPTIONAL",
799            "EXCLUDE_FROM_ALL",
800            "NAMELINK_ONLY",
801            "NAMELINK_SKIP",
802        ] {
803            assert!(
804                !form.flags.contains(flag),
805                "{flag} should not appear at the outer TARGETS level"
806            );
807        }
808    }
809
810    #[test]
811    fn install_targets_runtime_dependencies_is_kwarg_group() {
812        let registry = CommandRegistry::load().unwrap();
813        let form = registry.get("install").form_for(Some("TARGETS"));
814        let rd = form.kwargs.get("RUNTIME_DEPENDENCIES").unwrap();
815        for sub in [
816            "DIRECTORIES",
817            "PRE_INCLUDE_REGEXES",
818            "PRE_EXCLUDE_REGEXES",
819            "POST_INCLUDE_REGEXES",
820            "POST_EXCLUDE_REGEXES",
821            "POST_INCLUDE_FILES",
822            "POST_EXCLUDE_FILES",
823        ] {
824            assert!(
825                rd.kwargs.contains_key(sub),
826                "RUNTIME_DEPENDENCIES missing subkwarg {sub}"
827            );
828        }
829    }
830
831    #[test]
832    fn install_imported_runtime_artifacts_artifact_kinds_are_kwargs() {
833        let registry = CommandRegistry::load().unwrap();
834        let form = registry
835            .get("install")
836            .form_for(Some("IMPORTED_RUNTIME_ARTIFACTS"));
837
838        for kind in ["LIBRARY", "RUNTIME", "FRAMEWORK", "BUNDLE"] {
839            let spec = form
840                .kwargs
841                .get(kind)
842                .unwrap_or_else(|| panic!("IMPORTED_RUNTIME_ARTIFACTS missing {kind}"));
843            for sub in ["DESTINATION", "PERMISSIONS", "CONFIGURATIONS", "COMPONENT"] {
844                assert!(
845                    spec.kwargs.contains_key(sub),
846                    "{kind} missing subkwarg {sub}"
847                );
848            }
849            for flag in ["OPTIONAL", "EXCLUDE_FROM_ALL"] {
850                assert!(spec.flags.contains(flag), "{kind} missing subflag {flag}");
851            }
852            assert!(!form.flags.contains(kind));
853        }
854    }
855
856    #[test]
857    fn install_files_has_type_rename_and_exclude_from_all() {
858        let registry = CommandRegistry::load().unwrap();
859        let form = registry.get("install").form_for(Some("FILES"));
860        assert!(form.kwargs.contains_key("TYPE"));
861        assert!(form.kwargs.contains_key("RENAME"));
862        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
863    }
864
865    #[test]
866    fn install_directory_has_full_option_coverage() {
867        let registry = CommandRegistry::load().unwrap();
868        let form = registry.get("install").form_for(Some("DIRECTORY"));
869        for kw in [
870            "TYPE",
871            "DESTINATION",
872            "FILE_PERMISSIONS",
873            "DIRECTORY_PERMISSIONS",
874            "CONFIGURATIONS",
875            "COMPONENT",
876            "PATTERN",
877            "REGEX",
878        ] {
879            assert!(form.kwargs.contains_key(kw), "DIRECTORY missing kwarg {kw}");
880        }
881        // PERMISSIONS is not a top-level kwarg of install(DIRECTORY) per
882        // CMake docs — it only appears nested under PATTERN/REGEX.
883        assert!(
884            !form.kwargs.contains_key("PERMISSIONS"),
885            "PERMISSIONS must not be a top-level DIRECTORY kwarg"
886        );
887        for flag in [
888            "OPTIONAL",
889            "USE_SOURCE_PERMISSIONS",
890            "MESSAGE_NEVER",
891            "EXCLUDE_FROM_ALL",
892            "FILES_MATCHING",
893        ] {
894            assert!(form.flags.contains(flag), "DIRECTORY missing flag {flag}");
895        }
896    }
897
898    #[test]
899    fn install_directory_pattern_and_regex_open_subgroup() {
900        let registry = CommandRegistry::load().unwrap();
901        let form = registry.get("install").form_for(Some("DIRECTORY"));
902        for name in ["PATTERN", "REGEX"] {
903            let spec = form.kwargs.get(name).unwrap();
904            assert_eq!(spec.nargs, crate::spec::NArgs::Fixed(1));
905            assert!(spec.flags.contains("EXCLUDE"), "{name} missing EXCLUDE");
906            assert!(
907                spec.kwargs.contains_key("PERMISSIONS"),
908                "{name} missing PERMISSIONS subkwarg"
909            );
910        }
911    }
912
913    #[test]
914    fn install_programs_mirrors_files_form() {
915        let registry = CommandRegistry::load().unwrap();
916        let form = registry.get("install").form_for(Some("PROGRAMS"));
917        for kw in [
918            "TYPE",
919            "DESTINATION",
920            "PERMISSIONS",
921            "CONFIGURATIONS",
922            "COMPONENT",
923            "RENAME",
924        ] {
925            assert!(form.kwargs.contains_key(kw), "PROGRAMS missing kwarg {kw}");
926        }
927        assert!(form.flags.contains("OPTIONAL"));
928        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
929    }
930
931    #[test]
932    fn install_script_and_code_accept_component_and_flags() {
933        let registry = CommandRegistry::load().unwrap();
934        for disc in ["SCRIPT", "CODE"] {
935            let form = registry.get("install").form_for(Some(disc));
936            assert!(
937                form.kwargs.contains_key("COMPONENT"),
938                "{disc} missing COMPONENT"
939            );
940            assert!(
941                form.flags.contains("ALL_COMPONENTS"),
942                "{disc} missing ALL_COMPONENTS"
943            );
944            assert!(
945                form.flags.contains("EXCLUDE_FROM_ALL"),
946                "{disc} missing EXCLUDE_FROM_ALL"
947            );
948        }
949    }
950
951    #[test]
952    fn install_runtime_dependency_set_has_filter_kwargs_and_artifact_kinds() {
953        let registry = CommandRegistry::load().unwrap();
954        let form = registry
955            .get("install")
956            .form_for(Some("RUNTIME_DEPENDENCY_SET"));
957
958        for sub in [
959            "DIRECTORIES",
960            "PRE_INCLUDE_REGEXES",
961            "PRE_EXCLUDE_REGEXES",
962            "POST_INCLUDE_REGEXES",
963            "POST_EXCLUDE_REGEXES",
964            "POST_INCLUDE_FILES",
965            "POST_EXCLUDE_FILES",
966        ] {
967            assert!(
968                form.kwargs.contains_key(sub),
969                "RUNTIME_DEPENDENCY_SET missing {sub}"
970            );
971        }
972
973        for kind in ["LIBRARY", "RUNTIME", "FRAMEWORK"] {
974            let spec = form
975                .kwargs
976                .get(kind)
977                .unwrap_or_else(|| panic!("RUNTIME_DEPENDENCY_SET missing {kind}"));
978            for k in [
979                "DESTINATION",
980                "PERMISSIONS",
981                "CONFIGURATIONS",
982                "COMPONENT",
983                "NAMELINK_COMPONENT",
984            ] {
985                assert!(spec.kwargs.contains_key(k), "{kind} missing subkwarg {k}");
986            }
987            for f in [
988                "OPTIONAL",
989                "EXCLUDE_FROM_ALL",
990                "NAMELINK_ONLY",
991                "NAMELINK_SKIP",
992            ] {
993                assert!(spec.flags.contains(f), "{kind} missing subflag {f}");
994            }
995        }
996    }
997
998    #[test]
999    fn registry_knows_cmake_language_trace_form() {
1000        let registry = CommandRegistry::load().unwrap();
1001        let form = registry.get("cmake_language").form_for(Some("TRACE"));
1002        assert!(form.flags.contains("ON"));
1003        assert!(form.flags.contains("OFF"));
1004        assert!(form.flags.contains("EXPAND"));
1005    }
1006
1007    #[test]
1008    fn registry_knows_cmake_pkg_config_import_keywords() {
1009        let registry = CommandRegistry::load().unwrap();
1010        let form = registry.get("cmake_pkg_config").form_for(Some("IMPORT"));
1011        assert!(form.kwargs.contains_key("NAME"));
1012        assert!(form.kwargs.contains_key("BIND_PC_REQUIRES"));
1013    }
1014
1015    #[test]
1016    fn registry_knows_file_archive_create_threads() {
1017        let registry = CommandRegistry::load().unwrap();
1018        let form = registry.get("file").form_for(Some("ARCHIVE_CREATE"));
1019        assert!(form.kwargs.contains_key("THREADS"));
1020        assert!(form.kwargs.contains_key("COMPRESSION_LEVEL"));
1021    }
1022
1023    #[test]
1024    fn registry_knows_file_strings_keywords() {
1025        let registry = CommandRegistry::load().unwrap();
1026        let form = registry.get("file").form_for(Some("STRINGS"));
1027        assert_eq!(form.pargs, NArgs::Fixed(2));
1028        assert!(form.kwargs.contains_key("REGEX"));
1029        assert!(form.kwargs.contains_key("LIMIT_COUNT"));
1030    }
1031
1032    #[test]
1033    fn registry_knows_cmake_package_config_helpers_commands() {
1034        let registry = CommandRegistry::load().unwrap();
1035        let configure = registry.get("configure_package_config_file").form_for(None);
1036        assert!(configure.kwargs.contains_key("INSTALL_DESTINATION"));
1037        assert!(configure.kwargs.contains_key("PATH_VARS"));
1038
1039        let version = registry
1040            .get("write_basic_package_version_file")
1041            .form_for(None);
1042        assert!(version.kwargs.contains_key("COMPATIBILITY"));
1043        assert!(version.kwargs.contains_key("VERSION"));
1044    }
1045
1046    #[test]
1047    fn registry_knows_utility_module_commands() {
1048        let registry = CommandRegistry::load().unwrap();
1049        assert_eq!(
1050            registry.get("cmake_dependent_option").form_for(None).pargs,
1051            NArgs::Fixed(5)
1052        );
1053        assert_eq!(
1054            registry.get("check_language").form_for(None).pargs,
1055            NArgs::Fixed(1)
1056        );
1057        assert_eq!(
1058            registry.get("check_include_file").form_for(None).pargs,
1059            NArgs::AtLeast(2)
1060        );
1061        assert_eq!(
1062            registry.get("check_compiler_flag").form_for(None).pargs,
1063            NArgs::Fixed(3)
1064        );
1065        assert_eq!(
1066            registry
1067                .get("check_objc_compiler_flag")
1068                .form_for(None)
1069                .pargs,
1070            NArgs::Fixed(2)
1071        );
1072        assert_eq!(
1073            registry.get("check_cxx_symbol_exists").form_for(None).pargs,
1074            NArgs::Fixed(3)
1075        );
1076        assert!(registry
1077            .get("cmake_push_check_state")
1078            .form_for(None)
1079            .flags
1080            .contains("RESET"));
1081        let print_props = registry.get("cmake_print_properties").form_for(None);
1082        assert!(print_props.kwargs.contains_key("TARGETS"));
1083        assert!(print_props.kwargs.contains_key("PROPERTIES"));
1084        let pie = registry.get("check_pie_supported").form_for(None);
1085        assert!(pie.kwargs.contains_key("OUTPUT_VARIABLE"));
1086        assert!(pie.kwargs.contains_key("LANGUAGES"));
1087        let source_compiles = registry.get("check_source_compiles").form_for(None);
1088        assert!(source_compiles.kwargs.contains_key("SRC_EXT"));
1089        assert!(source_compiles.kwargs.contains_key("FAIL_REGEX"));
1090        let find_dependency = registry.get("find_dependency").form_for(None);
1091        assert!(find_dependency.flags.contains("REQUIRED"));
1092        assert!(find_dependency.kwargs.contains_key("COMPONENTS"));
1093    }
1094
1095    #[test]
1096    fn registry_knows_supported_deprecated_module_commands() {
1097        let registry = CommandRegistry::load().unwrap();
1098        let version = registry
1099            .get("write_basic_config_version_file")
1100            .form_for(None);
1101        assert_eq!(version.pargs, NArgs::Fixed(1));
1102        assert!(version.kwargs.contains_key("COMPATIBILITY"));
1103        assert!(version.flags.contains("ARCH_INDEPENDENT"));
1104        assert_eq!(
1105            registry.get("check_cxx_accepts_flag").form_for(None).pargs,
1106            NArgs::Fixed(2)
1107        );
1108    }
1109
1110    #[test]
1111    fn registry_knows_fetchcontent_commands() {
1112        let registry = CommandRegistry::load().unwrap();
1113        let declare = registry.get("fetchcontent_declare").form_for(None);
1114        assert_eq!(declare.pargs, NArgs::Fixed(1));
1115        assert!(declare.flags.contains("EXCLUDE_FROM_ALL"));
1116        assert!(declare.kwargs.contains_key("FIND_PACKAGE_ARGS"));
1117
1118        let get_properties = registry.get("fetchcontent_getproperties").form_for(None);
1119        assert!(get_properties.kwargs.contains_key("SOURCE_DIR"));
1120        assert!(get_properties.kwargs.contains_key("BINARY_DIR"));
1121        assert!(get_properties.kwargs.contains_key("POPULATED"));
1122
1123        let populate = registry.get("fetchcontent_populate").form_for(None);
1124        assert!(populate.flags.contains("QUIET"));
1125        assert!(populate.kwargs.contains_key("SUBBUILD_DIR"));
1126    }
1127
1128    #[test]
1129    fn registry_knows_common_test_and_package_helper_modules() {
1130        let registry = CommandRegistry::load().unwrap();
1131
1132        let google_add = registry.get("gtest_add_tests").form_for(None);
1133        assert!(google_add.kwargs.contains_key("TARGET"));
1134        assert!(google_add.kwargs.contains_key("SOURCES"));
1135        assert!(google_add.flags.contains("SKIP_DEPENDENCY"));
1136
1137        let google_discover = registry.get("gtest_discover_tests").form_for(None);
1138        assert!(google_discover.kwargs.contains_key("DISCOVERY_MODE"));
1139        assert!(google_discover.kwargs.contains_key("XML_OUTPUT_DIR"));
1140        assert!(google_discover.flags.contains("NO_PRETTY_TYPES"));
1141
1142        assert_eq!(
1143            registry.get("processorcount").form_for(None).pargs,
1144            NArgs::Fixed(1)
1145        );
1146
1147        let fp_hsa = registry
1148            .get("find_package_handle_standard_args")
1149            .form_for(None);
1150        assert!(fp_hsa.flags.contains("DEFAULT_MSG"));
1151        assert!(fp_hsa.kwargs.contains_key("REQUIRED_VARS"));
1152        assert!(fp_hsa.kwargs.contains_key("VERSION_VAR"));
1153
1154        let fp_check = registry.get("find_package_check_version").form_for(None);
1155        assert_eq!(fp_check.pargs, NArgs::Fixed(2));
1156        assert!(fp_check.flags.contains("HANDLE_VERSION_RANGE"));
1157    }
1158
1159    #[test]
1160    fn registry_knows_externalproject_helper_commands() {
1161        let registry = CommandRegistry::load().unwrap();
1162        let step = registry.get("externalproject_add_step").form_for(None);
1163        assert_eq!(step.pargs, NArgs::Fixed(2));
1164        assert!(step.kwargs.contains_key("COMMAND"));
1165        assert!(step.kwargs.contains_key("DEPENDEES"));
1166        assert!(step.kwargs.contains_key("ENVIRONMENT_MODIFICATION"));
1167
1168        let targets = registry
1169            .get("externalproject_add_steptargets")
1170            .form_for(None);
1171        assert_eq!(targets.pargs, NArgs::AtLeast(2));
1172        assert!(targets.flags.contains("NO_DEPENDS"));
1173
1174        let deps = registry
1175            .get("externalproject_add_stepdependencies")
1176            .form_for(None);
1177        assert_eq!(deps.pargs, NArgs::AtLeast(3));
1178
1179        let props = registry.get("externalproject_get_property").form_for(None);
1180        assert_eq!(props.pargs, NArgs::AtLeast(2));
1181    }
1182
1183    #[test]
1184    fn registry_knows_packaging_and_find_helper_module_commands() {
1185        let registry = CommandRegistry::load().unwrap();
1186
1187        assert_eq!(
1188            registry.get("find_package_message").form_for(None).pargs,
1189            NArgs::Fixed(3)
1190        );
1191        assert_eq!(
1192            registry
1193                .get("select_library_configurations")
1194                .form_for(None)
1195                .pargs,
1196            NArgs::Fixed(1)
1197        );
1198
1199        let component = registry.get("cpack_add_component").form_for(None);
1200        assert!(component.flags.contains("HIDDEN"));
1201        assert!(component.kwargs.contains_key("DISPLAY_NAME"));
1202        assert!(component.kwargs.contains_key("DEPENDS"));
1203
1204        let group = registry.get("cpack_add_component_group").form_for(None);
1205        assert!(group.flags.contains("EXPANDED"));
1206        assert!(group.kwargs.contains_key("PARENT_GROUP"));
1207
1208        let downloads = registry.get("cpack_configure_downloads").form_for(None);
1209        assert_eq!(downloads.pargs, NArgs::Fixed(1));
1210        assert!(downloads.kwargs.contains_key("UPLOAD_DIRECTORY"));
1211    }
1212
1213    #[test]
1214    fn registry_knows_export_header_module_commands() {
1215        let registry = CommandRegistry::load().unwrap();
1216        let export_header = registry.get("generate_export_header").form_for(None);
1217        assert_eq!(export_header.pargs, NArgs::Fixed(1));
1218        assert!(export_header.flags.contains("DEFINE_NO_DEPRECATED"));
1219        assert!(export_header.kwargs.contains_key("EXPORT_FILE_NAME"));
1220        assert!(export_header.kwargs.contains_key("PREFIX_NAME"));
1221
1222        assert_eq!(
1223            registry
1224                .get("add_compiler_export_flags")
1225                .form_for(None)
1226                .pargs,
1227            NArgs::Optional
1228        );
1229    }
1230
1231    #[test]
1232    fn registry_knows_remaining_utility_module_commands() {
1233        let registry = CommandRegistry::load().unwrap();
1234
1235        for command in [
1236            "android_add_test_data",
1237            "add_file_dependencies",
1238            "cmake_add_fortran_subdirectory",
1239            "cmake_expand_imported_targets",
1240            "cmake_force_c_compiler",
1241            "cmake_force_cxx_compiler",
1242            "cmake_force_fortran_compiler",
1243            "ctest_coverage_collect_gcov",
1244            "copy_and_fixup_bundle",
1245            "fixup_bundle",
1246            "fixup_bundle_item",
1247            "verify_app",
1248            "verify_bundle_prerequisites",
1249            "verify_bundle_symlinks",
1250            "get_bundle_main_executable",
1251            "get_dotapp_dir",
1252            "get_bundle_and_executable",
1253            "get_bundle_all_executables",
1254            "get_bundle_keys",
1255            "get_item_key",
1256            "get_item_rpaths",
1257            "clear_bundle_keys",
1258            "set_bundle_key_values",
1259            "copy_resolved_framework_into_bundle",
1260            "copy_resolved_item_into_bundle",
1261            "cpack_ifw_add_package_resources",
1262            "cpack_ifw_add_repository",
1263            "cpack_ifw_configure_component",
1264            "cpack_ifw_configure_component_group",
1265            "cpack_ifw_update_repository",
1266            "cpack_ifw_configure_file",
1267            "csharp_set_windows_forms_properties",
1268            "csharp_set_designer_cs_properties",
1269            "csharp_set_xaml_cs_properties",
1270            "csharp_get_filename_keys",
1271            "csharp_get_filename_key_base",
1272            "csharp_get_dependentupon_name",
1273            "externaldata_expand_arguments",
1274            "externaldata_add_test",
1275            "externaldata_add_target",
1276            "fortrancinterface_header",
1277            "fortrancinterface_verify",
1278            "fetchcontent_setpopulated",
1279            "gnuinstalldirs_get_absolute_install_dir",
1280            "find_jar",
1281            "add_jar",
1282            "install_jar",
1283            "install_jar_exports",
1284            "export_jars",
1285            "create_javadoc",
1286            "create_javah",
1287            "install_jni_symlink",
1288            "swig_add_library",
1289            "swig_link_libraries",
1290            "print_enabled_features",
1291            "print_disabled_features",
1292            "set_feature_info",
1293            "set_package_info",
1294        ] {
1295            assert!(
1296                registry.contains_builtin(command),
1297                "missing built-in {command}"
1298            );
1299        }
1300
1301        assert_eq!(
1302            registry
1303                .get("ctest_coverage_collect_gcov")
1304                .form_for(None)
1305                .pargs,
1306            NArgs::ZeroOrMore
1307        );
1308        assert_eq!(
1309            registry
1310                .get("fortrancinterface_verify")
1311                .form_for(None)
1312                .pargs,
1313            NArgs::ZeroOrMore
1314        );
1315        assert_eq!(
1316            registry.get("add_jar").form_for(None).pargs,
1317            NArgs::AtLeast(2)
1318        );
1319        assert_eq!(
1320            registry
1321                .get("cpack_ifw_configure_file")
1322                .form_for(None)
1323                .pargs,
1324            NArgs::Fixed(2)
1325        );
1326        assert_eq!(
1327            registry
1328                .get("gnuinstalldirs_get_absolute_install_dir")
1329                .form_for(None)
1330                .pargs,
1331            NArgs::AtLeast(3)
1332        );
1333    }
1334
1335    #[test]
1336    fn registry_knows_string_json_43_modes() {
1337        let registry = CommandRegistry::load().unwrap();
1338        let form = registry.get("string").form_for(Some("JSON"));
1339        assert!(form.flags.contains("GET_RAW"));
1340        assert!(form.flags.contains("STRING_ENCODE"));
1341        assert!(form.kwargs.contains_key("ERROR_VARIABLE"));
1342    }
1343
1344    #[test]
1345    fn user_override_entries_merge_with_builtins() {
1346        let mut registry = CommandRegistry::load().unwrap();
1347        let overrides = r#"
1348[commands.target_link_libraries.layout]
1349always_wrap = true
1350
1351[commands.target_link_libraries.kwargs.LINKER_LANGUAGE]
1352nargs = 1
1353"#;
1354
1355        registry
1356            .merge_override_str(overrides, PathBuf::from("test-overrides.toml"))
1357            .unwrap();
1358
1359        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
1360            panic!()
1361        };
1362        assert_eq!(
1363            form.layout.as_ref().and_then(|layout| layout.always_wrap),
1364            Some(true)
1365        );
1366        assert!(form.kwargs.contains_key("PUBLIC"));
1367        assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
1368    }
1369
1370    #[test]
1371    fn merge_layout_override_applies_wrap_after_first_arg() {
1372        // `set` ships `layout.wrap_after_first_arg: true` in builtins.yaml, so
1373        // it already has a layout block. A user override of this field must
1374        // take effect when merged onto that existing layout (a regression
1375        // guard: merge_layout previously dropped this field).
1376        let mut registry = CommandRegistry::load().unwrap();
1377
1378        let CommandSpec::Single(form) = registry.get("set") else {
1379            panic!("set should be a single-form command")
1380        };
1381        assert_eq!(
1382            form.layout.as_ref().and_then(|l| l.wrap_after_first_arg),
1383            Some(true),
1384            "precondition: builtin set has wrap_after_first_arg = true"
1385        );
1386
1387        registry
1388            .merge_override_str(
1389                r#"
1390[commands.set.layout]
1391wrap_after_first_arg = false
1392"#,
1393                PathBuf::from("test-overrides.toml"),
1394            )
1395            .unwrap();
1396
1397        let CommandSpec::Single(form) = registry.get("set") else {
1398            panic!("set should still be a single-form command")
1399        };
1400        assert_eq!(
1401            form.layout.as_ref().and_then(|l| l.wrap_after_first_arg),
1402            Some(false),
1403            "user override of wrap_after_first_arg must win over the builtin"
1404        );
1405    }
1406
1407    #[test]
1408    fn uppercase_lookup_uses_builtin_normalization() {
1409        let registry = CommandRegistry::load().unwrap();
1410        assert!(registry.contains_builtin("TARGET_LINK_LIBRARIES"));
1411        let CommandSpec::Single(form) = registry.get("TARGET_LINK_LIBRARIES") else {
1412            panic!()
1413        };
1414        assert!(form.kwargs.contains_key("PUBLIC"));
1415        assert!(form.kwargs.contains_key("PRIVATE"));
1416    }
1417
1418    #[test]
1419    fn contains_builtin_excludes_user_added_commands_after_merge() {
1420        let mut registry = CommandRegistry::load().unwrap();
1421        registry
1422            .merge_toml_overrides(
1423                r#"
1424[commands.my_custom_command]
1425pargs = 1
1426"#,
1427            )
1428            .unwrap();
1429
1430        assert!(!registry.contains_builtin("my_custom_command"));
1431        assert!(!registry.contains_builtin("MY_CUSTOM_COMMAND"));
1432        assert!(matches!(
1433            registry.get("my_custom_command"),
1434            CommandSpec::Single(_)
1435        ));
1436    }
1437
1438    #[test]
1439    fn from_builtins_and_yaml_override_file_merges_entries() {
1440        let dir = tempfile::tempdir().unwrap();
1441        let overrides = dir.path().join("override.yaml");
1442        fs::write(
1443            &overrides,
1444            r#"
1445commands:
1446  target_link_libraries:
1447    kwargs:
1448      linker_language:
1449        nargs: 1
1450"#,
1451        )
1452        .unwrap();
1453
1454        let registry = CommandRegistry::from_builtins_and_overrides(Some(&overrides)).unwrap();
1455        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
1456            panic!()
1457        };
1458        assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
1459    }
1460
1461    #[test]
1462    fn merge_override_file_reports_structured_toml_parse_errors() {
1463        let mut registry = CommandRegistry::load().unwrap();
1464        let dir = tempfile::tempdir().unwrap();
1465        let path = dir.path().join("override.toml");
1466        fs::write(&path, "[commands.bad]\npargs = [\n").unwrap();
1467
1468        let err = registry.merge_override_file(&path).unwrap_err();
1469        match err {
1470            Error::Spec(spec_err) => {
1471                let details = &spec_err.details;
1472                assert_eq!(details.format, "TOML");
1473                assert!(details.line.is_some());
1474                assert!(details.column.is_some());
1475            }
1476            other => panic!("expected spec parse error, got {other:?}"),
1477        }
1478    }
1479
1480    #[test]
1481    fn merge_override_file_reports_structured_yaml_parse_errors() {
1482        let mut registry = CommandRegistry::load().unwrap();
1483        let dir = tempfile::tempdir().unwrap();
1484        let path = dir.path().join("override.yaml");
1485        fs::write(&path, "commands:\n  target_link_libraries: [\n").unwrap();
1486
1487        let err = registry.merge_override_file(&path).unwrap_err();
1488        match err {
1489            Error::Spec(spec_err) => {
1490                let details = &spec_err.details;
1491                assert_eq!(details.format, "YAML");
1492                assert!(details.line.is_some());
1493                assert!(details.column.is_some());
1494            }
1495            other => panic!("expected spec parse error, got {other:?}"),
1496        }
1497    }
1498
1499    #[test]
1500    fn override_with_mismatched_shape_replaces_base_command_spec() {
1501        let mut registry = CommandRegistry::load().unwrap();
1502        registry
1503            .merge_override_str(
1504                r#"
1505[commands.cmake_minimum_required.forms.VERSION]
1506pargs = 1
1507"#,
1508                PathBuf::from("override.toml"),
1509            )
1510            .unwrap();
1511
1512        let CommandSpec::Discriminated { .. } = registry.get("cmake_minimum_required") else {
1513            panic!("expected discriminated command after mismatched override")
1514        };
1515        assert_eq!(
1516            registry
1517                .get("cmake_minimum_required")
1518                .form_for(Some("VERSION"))
1519                .pargs,
1520            NArgs::Fixed(1)
1521        );
1522    }
1523}