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
350/// Decode the embedded `builtins.yaml` blob and merge in the embedded
351/// `modules.yaml` blob, returning a single combined [`SpecFile`].
352///
353/// Metadata (e.g. the audited CMake version) is taken from the builtins
354/// blob; `modules.yaml` is expected to contribute only command entries.
355/// On a key collision between the two blobs, the modules entry wins —
356/// in practice this should never happen, since CMake's
357/// `--help-command-list` and `--help-module-list` are disjoint.
358fn parse_embedded_spec() -> Result<SpecFile> {
359    let mut spec = parse_msgpack_spec(BUILTINS_MSGPACK, BUILTINS_PATH)?;
360    let modules = parse_msgpack_spec(MODULES_MSGPACK, MODULES_PATH)?;
361    spec.commands.extend(modules.commands);
362    Ok(spec)
363}
364
365fn parse_msgpack_spec(bytes: &[u8], path: &str) -> Result<SpecFile> {
366    let mut spec: SpecFile = rmp_serde::from_slice(bytes).map_err(|source| {
367        Error::Spec(crate::error::SpecError::new(
368            PathBuf::from(path),
369            "MessagePack",
370            source.to_string(),
371            None,
372            None,
373        ))
374    })?;
375    normalize_spec_file(&mut spec);
376    Ok(spec)
377}
378
379fn normalize_spec_file(spec: &mut SpecFile) {
380    spec.commands = std::mem::take(&mut spec.commands)
381        .into_iter()
382        .map(|(name, mut command)| {
383            normalize_command_spec(&mut command);
384            (name.to_ascii_lowercase(), command)
385        })
386        .collect();
387}
388
389fn normalize_override_file(spec: &mut SpecOverrideFile) {
390    spec.commands = std::mem::take(&mut spec.commands)
391        .into_iter()
392        .map(|(name, mut command)| {
393            normalize_command_override(&mut command);
394            (name.to_ascii_lowercase(), command)
395        })
396        .collect();
397}
398
399fn normalize_command_spec(spec: &mut CommandSpec) {
400    match spec {
401        CommandSpec::Single(form) => normalize_form(form),
402        CommandSpec::Discriminated { forms, fallback } => {
403            *forms = std::mem::take(forms)
404                .into_iter()
405                .map(|(name, mut form)| {
406                    normalize_form(&mut form);
407                    (name.to_ascii_uppercase(), form)
408                })
409                .collect();
410
411            if let Some(fallback) = fallback {
412                normalize_form(fallback);
413            }
414        }
415    }
416}
417
418fn normalize_command_override(spec: &mut CommandSpecOverride) {
419    match spec {
420        CommandSpecOverride::Single(form) => normalize_form_override(form),
421        CommandSpecOverride::Discriminated { forms, fallback } => {
422            *forms = std::mem::take(forms)
423                .into_iter()
424                .map(|(name, mut form)| {
425                    normalize_form_override(&mut form);
426                    (name.to_ascii_uppercase(), form)
427                })
428                .collect();
429
430            if let Some(fallback) = fallback {
431                normalize_form_override(fallback);
432            }
433        }
434    }
435}
436
437fn normalize_form(form: &mut CommandForm) {
438    form.kwargs = std::mem::take(&mut form.kwargs)
439        .into_iter()
440        .map(|(name, mut kwarg)| {
441            normalize_kwarg(&mut kwarg);
442            (name.to_ascii_uppercase(), kwarg)
443        })
444        .collect();
445
446    form.flags = std::mem::take(&mut form.flags)
447        .into_iter()
448        .map(|flag| flag.to_ascii_uppercase())
449        .collect();
450}
451
452fn normalize_form_override(form: &mut CommandFormOverride) {
453    form.kwargs = std::mem::take(&mut form.kwargs)
454        .into_iter()
455        .map(|(name, mut kwarg)| {
456            normalize_kwarg_override(&mut kwarg);
457            (name.to_ascii_uppercase(), kwarg)
458        })
459        .collect();
460
461    form.flags = std::mem::take(&mut form.flags)
462        .into_iter()
463        .map(|flag| flag.to_ascii_uppercase())
464        .collect();
465}
466
467fn normalize_kwarg(spec: &mut KwargSpec) {
468    spec.kwargs = std::mem::take(&mut spec.kwargs)
469        .into_iter()
470        .map(|(name, mut kwarg)| {
471            normalize_kwarg(&mut kwarg);
472            (name.to_ascii_uppercase(), kwarg)
473        })
474        .collect();
475
476    spec.flags = std::mem::take(&mut spec.flags)
477        .into_iter()
478        .map(|flag| flag.to_ascii_uppercase())
479        .collect();
480}
481
482fn normalize_kwarg_override(spec: &mut KwargSpecOverride) {
483    spec.kwargs = std::mem::take(&mut spec.kwargs)
484        .into_iter()
485        .map(|(name, mut kwarg)| {
486            normalize_kwarg_override(&mut kwarg);
487            (name.to_ascii_uppercase(), kwarg)
488        })
489        .collect();
490
491    spec.flags = std::mem::take(&mut spec.flags)
492        .into_iter()
493        .map(|flag| flag.to_ascii_uppercase())
494        .collect();
495}
496
497fn merge_command_spec(base: &mut CommandSpec, override_spec: CommandSpecOverride) {
498    match (base, override_spec) {
499        (CommandSpec::Single(base_form), CommandSpecOverride::Single(override_form)) => {
500            merge_form(base_form, override_form);
501        }
502        (
503            CommandSpec::Discriminated {
504                forms: base_forms,
505                fallback: base_fallback,
506            },
507            CommandSpecOverride::Discriminated {
508                forms: override_forms,
509                fallback: override_fallback,
510            },
511        ) => {
512            for (name, override_form) in override_forms {
513                match base_forms.get_mut(&name) {
514                    Some(base_form) => merge_form(base_form, override_form),
515                    None => {
516                        base_forms.insert(name, override_form.into_full_form());
517                    }
518                }
519            }
520
521            if let Some(override_fallback) = override_fallback {
522                match base_fallback {
523                    Some(base_fallback) => merge_form(base_fallback, override_fallback),
524                    None => {
525                        *base_fallback = Some(override_fallback.into_full_form());
526                    }
527                }
528            }
529        }
530        (base_spec, override_spec) => {
531            *base_spec = override_spec.into_full_spec();
532        }
533    }
534}
535
536fn merge_form(base: &mut CommandForm, override_form: CommandFormOverride) {
537    if let Some(pargs) = override_form.pargs {
538        base.pargs = pargs;
539    }
540
541    merge_flags(&mut base.flags, override_form.flags);
542
543    for (name, override_kwarg) in override_form.kwargs {
544        match base.kwargs.get_mut(&name) {
545            Some(base_kwarg) => merge_kwarg(base_kwarg, override_kwarg),
546            None => {
547                base.kwargs.insert(name, override_kwarg.into_full_spec());
548            }
549        }
550    }
551
552    if let Some(layout) = override_form.layout {
553        merge_layout(
554            base.layout.get_or_insert_with(LayoutOverrides::default),
555            layout,
556        );
557    }
558}
559
560fn merge_kwarg(base: &mut KwargSpec, override_kwarg: KwargSpecOverride) {
561    if let Some(nargs) = override_kwarg.nargs {
562        base.nargs = nargs;
563    }
564
565    merge_flags(&mut base.flags, override_kwarg.flags);
566
567    for (name, nested_override) in override_kwarg.kwargs {
568        match base.kwargs.get_mut(&name) {
569            Some(base_nested) => merge_kwarg(base_nested, nested_override),
570            None => {
571                base.kwargs.insert(name, nested_override.into_full_spec());
572            }
573        }
574    }
575}
576
577fn merge_layout(base: &mut LayoutOverrides, override_layout: LayoutOverridesOverride) {
578    if let Some(value) = override_layout.line_width {
579        base.line_width = Some(value);
580    }
581    if let Some(value) = override_layout.tab_size {
582        base.tab_size = Some(value);
583    }
584    if let Some(value) = override_layout.dangle_parens {
585        base.dangle_parens = Some(value);
586    }
587    if let Some(value) = override_layout.always_wrap {
588        base.always_wrap = Some(value);
589    }
590    if let Some(value) = override_layout.max_pargs_hwrap {
591        base.max_pargs_hwrap = Some(value);
592    }
593    if let Some(value) = override_layout.continuation_align {
594        base.continuation_align = Some(value);
595    }
596}
597
598fn merge_flags(base: &mut IndexSet<String>, override_flags: IndexSet<String>) {
599    for flag in override_flags {
600        base.insert(flag);
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use crate::spec::NArgs;
608    use std::fs;
609
610    #[test]
611    fn registry_has_target_link_libraries_keywords() {
612        let registry = CommandRegistry::load().unwrap();
613        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
614            panic!()
615        };
616        assert!(form.kwargs.contains_key("PUBLIC"));
617        assert!(form.kwargs.contains_key("PRIVATE"));
618        assert!(form.kwargs.contains_key("INTERFACE"));
619    }
620
621    #[test]
622    fn registry_has_install_forms() {
623        let registry = CommandRegistry::load().unwrap();
624        assert!(matches!(
625            registry.get("install"),
626            CommandSpec::Discriminated { .. }
627        ));
628    }
629
630    #[test]
631    fn registry_unknown_command_uses_fallback() {
632        let registry = CommandRegistry::load().unwrap();
633        let spec = registry.get("my_unknown_command");
634        let CommandSpec::Single(form) = spec else {
635            panic!()
636        };
637        assert_eq!(form.pargs, NArgs::ZeroOrMore);
638        assert!(form.kwargs.is_empty());
639        assert!(form.flags.is_empty());
640    }
641
642    #[test]
643    fn registry_knows_builtin_surface() {
644        let registry = CommandRegistry::load().unwrap();
645        assert!(registry.contains_builtin("cmake_minimum_required"));
646        assert!(registry.contains_builtin("target_sources"));
647        assert!(registry.contains_builtin("while"));
648        assert!(registry.contains_builtin("external_project_add"));
649    }
650
651    #[test]
652    fn registry_reports_audited_cmake_version() {
653        let registry = CommandRegistry::load().unwrap();
654        assert_eq!(registry.audited_cmake_version(), "4.3.1");
655    }
656
657    #[test]
658    fn registry_knows_project_43_keywords() {
659        let registry = CommandRegistry::load().unwrap();
660        let CommandSpec::Single(form) = registry.get("project") else {
661            panic!()
662        };
663        assert!(form.flags.contains("COMPAT_VERSION"));
664        assert!(form.flags.contains("SPDX_LICENSE"));
665    }
666
667    #[test]
668    fn registry_knows_export_package_info_form() {
669        let registry = CommandRegistry::load().unwrap();
670        let CommandSpec::Discriminated { .. } = registry.get("export") else {
671            panic!()
672        };
673        let form = registry.get("export").form_for(Some("PACKAGE_INFO"));
674        assert_eq!(form.pargs, NArgs::Fixed(1));
675        assert!(form.kwargs.contains_key("EXPORT"));
676        assert!(form.kwargs.contains_key("CXX_MODULES_DIRECTORY"));
677    }
678
679    #[test]
680    fn registry_knows_install_package_info_form() {
681        let registry = CommandRegistry::load().unwrap();
682        let form = registry.get("install").form_for(Some("PACKAGE_INFO"));
683        assert_eq!(form.pargs, NArgs::Fixed(1));
684        assert!(form.kwargs.contains_key("DESTINATION"));
685        assert!(form.kwargs.contains_key("COMPAT_VERSION"));
686    }
687
688    #[test]
689    fn registry_knows_install_export_namespace_keyword() {
690        let registry = CommandRegistry::load().unwrap();
691        let form = registry.get("install").form_for(Some("EXPORT"));
692        assert!(form.kwargs.contains_key("DESTINATION"));
693        assert!(form.kwargs.contains_key("NAMESPACE"));
694        assert!(form.kwargs.contains_key("FILE"));
695        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
696    }
697
698    #[test]
699    fn registry_knows_install_targets_export_and_includes_sections() {
700        let registry = CommandRegistry::load().unwrap();
701        let form = registry.get("install").form_for(Some("TARGETS"));
702        assert!(form.kwargs.contains_key("EXPORT"));
703        assert!(form.kwargs.contains_key("INCLUDES"));
704        assert!(form
705            .kwargs
706            .get("INCLUDES")
707            .is_some_and(|spec| spec.kwargs.contains_key("DESTINATION")));
708        assert!(form.kwargs.contains_key("RUNTIME_DEPENDENCY_SET"));
709    }
710
711    #[test]
712    fn install_targets_artifact_kinds_are_kwargs_with_subgroups() {
713        let registry = CommandRegistry::load().unwrap();
714        let form = registry.get("install").form_for(Some("TARGETS"));
715
716        for kind in [
717            "ARCHIVE",
718            "LIBRARY",
719            "RUNTIME",
720            "OBJECTS",
721            "FRAMEWORK",
722            "BUNDLE",
723            "PRIVATE_HEADER",
724            "PUBLIC_HEADER",
725            "RESOURCE",
726            "FILE_SET",
727            "CXX_MODULES_BMI",
728        ] {
729            let spec = form
730                .kwargs
731                .get(kind)
732                .unwrap_or_else(|| panic!("install(TARGETS) missing artifact kind {kind}"));
733            for sub in [
734                "DESTINATION",
735                "PERMISSIONS",
736                "CONFIGURATIONS",
737                "COMPONENT",
738                "NAMELINK_COMPONENT",
739            ] {
740                assert!(
741                    spec.kwargs.contains_key(sub),
742                    "{kind} missing subkwarg {sub}"
743                );
744            }
745            for flag in [
746                "OPTIONAL",
747                "EXCLUDE_FROM_ALL",
748                "NAMELINK_ONLY",
749                "NAMELINK_SKIP",
750            ] {
751                assert!(spec.flags.contains(flag), "{kind} missing subflag {flag}");
752            }
753            assert!(
754                !form.flags.contains(kind),
755                "{kind} should not appear as an outer flag"
756            );
757        }
758    }
759
760    #[test]
761    fn install_targets_file_set_takes_positional_set_name() {
762        let registry = CommandRegistry::load().unwrap();
763        let form = registry.get("install").form_for(Some("TARGETS"));
764        let file_set = form.kwargs.get("FILE_SET").unwrap();
765        assert_eq!(file_set.nargs, crate::spec::NArgs::Fixed(1));
766    }
767
768    #[test]
769    fn install_targets_artifact_option_flags_are_not_outer_flags() {
770        let registry = CommandRegistry::load().unwrap();
771        let form = registry.get("install").form_for(Some("TARGETS"));
772        for flag in [
773            "OPTIONAL",
774            "EXCLUDE_FROM_ALL",
775            "NAMELINK_ONLY",
776            "NAMELINK_SKIP",
777        ] {
778            assert!(
779                !form.flags.contains(flag),
780                "{flag} should not appear at the outer TARGETS level"
781            );
782        }
783    }
784
785    #[test]
786    fn install_targets_runtime_dependencies_is_kwarg_group() {
787        let registry = CommandRegistry::load().unwrap();
788        let form = registry.get("install").form_for(Some("TARGETS"));
789        let rd = form.kwargs.get("RUNTIME_DEPENDENCIES").unwrap();
790        for sub in [
791            "DIRECTORIES",
792            "PRE_INCLUDE_REGEXES",
793            "PRE_EXCLUDE_REGEXES",
794            "POST_INCLUDE_REGEXES",
795            "POST_EXCLUDE_REGEXES",
796            "POST_INCLUDE_FILES",
797            "POST_EXCLUDE_FILES",
798        ] {
799            assert!(
800                rd.kwargs.contains_key(sub),
801                "RUNTIME_DEPENDENCIES missing subkwarg {sub}"
802            );
803        }
804    }
805
806    #[test]
807    fn install_imported_runtime_artifacts_artifact_kinds_are_kwargs() {
808        let registry = CommandRegistry::load().unwrap();
809        let form = registry
810            .get("install")
811            .form_for(Some("IMPORTED_RUNTIME_ARTIFACTS"));
812
813        for kind in ["LIBRARY", "RUNTIME", "FRAMEWORK", "BUNDLE"] {
814            let spec = form
815                .kwargs
816                .get(kind)
817                .unwrap_or_else(|| panic!("IMPORTED_RUNTIME_ARTIFACTS missing {kind}"));
818            for sub in ["DESTINATION", "PERMISSIONS", "CONFIGURATIONS", "COMPONENT"] {
819                assert!(
820                    spec.kwargs.contains_key(sub),
821                    "{kind} missing subkwarg {sub}"
822                );
823            }
824            for flag in ["OPTIONAL", "EXCLUDE_FROM_ALL"] {
825                assert!(spec.flags.contains(flag), "{kind} missing subflag {flag}");
826            }
827            assert!(!form.flags.contains(kind));
828        }
829    }
830
831    #[test]
832    fn install_files_has_type_rename_and_exclude_from_all() {
833        let registry = CommandRegistry::load().unwrap();
834        let form = registry.get("install").form_for(Some("FILES"));
835        assert!(form.kwargs.contains_key("TYPE"));
836        assert!(form.kwargs.contains_key("RENAME"));
837        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
838    }
839
840    #[test]
841    fn install_directory_has_full_option_coverage() {
842        let registry = CommandRegistry::load().unwrap();
843        let form = registry.get("install").form_for(Some("DIRECTORY"));
844        for kw in [
845            "TYPE",
846            "DESTINATION",
847            "FILE_PERMISSIONS",
848            "DIRECTORY_PERMISSIONS",
849            "CONFIGURATIONS",
850            "COMPONENT",
851            "PATTERN",
852            "REGEX",
853        ] {
854            assert!(form.kwargs.contains_key(kw), "DIRECTORY missing kwarg {kw}");
855        }
856        // PERMISSIONS is not a top-level kwarg of install(DIRECTORY) per
857        // CMake docs — it only appears nested under PATTERN/REGEX.
858        assert!(
859            !form.kwargs.contains_key("PERMISSIONS"),
860            "PERMISSIONS must not be a top-level DIRECTORY kwarg"
861        );
862        for flag in [
863            "OPTIONAL",
864            "USE_SOURCE_PERMISSIONS",
865            "MESSAGE_NEVER",
866            "EXCLUDE_FROM_ALL",
867            "FILES_MATCHING",
868        ] {
869            assert!(form.flags.contains(flag), "DIRECTORY missing flag {flag}");
870        }
871    }
872
873    #[test]
874    fn install_directory_pattern_and_regex_open_subgroup() {
875        let registry = CommandRegistry::load().unwrap();
876        let form = registry.get("install").form_for(Some("DIRECTORY"));
877        for name in ["PATTERN", "REGEX"] {
878            let spec = form.kwargs.get(name).unwrap();
879            assert_eq!(spec.nargs, crate::spec::NArgs::Fixed(1));
880            assert!(spec.flags.contains("EXCLUDE"), "{name} missing EXCLUDE");
881            assert!(
882                spec.kwargs.contains_key("PERMISSIONS"),
883                "{name} missing PERMISSIONS subkwarg"
884            );
885        }
886    }
887
888    #[test]
889    fn install_programs_mirrors_files_form() {
890        let registry = CommandRegistry::load().unwrap();
891        let form = registry.get("install").form_for(Some("PROGRAMS"));
892        for kw in [
893            "TYPE",
894            "DESTINATION",
895            "PERMISSIONS",
896            "CONFIGURATIONS",
897            "COMPONENT",
898            "RENAME",
899        ] {
900            assert!(form.kwargs.contains_key(kw), "PROGRAMS missing kwarg {kw}");
901        }
902        assert!(form.flags.contains("OPTIONAL"));
903        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
904    }
905
906    #[test]
907    fn install_script_and_code_accept_component_and_flags() {
908        let registry = CommandRegistry::load().unwrap();
909        for disc in ["SCRIPT", "CODE"] {
910            let form = registry.get("install").form_for(Some(disc));
911            assert!(
912                form.kwargs.contains_key("COMPONENT"),
913                "{disc} missing COMPONENT"
914            );
915            assert!(
916                form.flags.contains("ALL_COMPONENTS"),
917                "{disc} missing ALL_COMPONENTS"
918            );
919            assert!(
920                form.flags.contains("EXCLUDE_FROM_ALL"),
921                "{disc} missing EXCLUDE_FROM_ALL"
922            );
923        }
924    }
925
926    #[test]
927    fn install_runtime_dependency_set_has_filter_kwargs_and_artifact_kinds() {
928        let registry = CommandRegistry::load().unwrap();
929        let form = registry
930            .get("install")
931            .form_for(Some("RUNTIME_DEPENDENCY_SET"));
932
933        for sub in [
934            "DIRECTORIES",
935            "PRE_INCLUDE_REGEXES",
936            "PRE_EXCLUDE_REGEXES",
937            "POST_INCLUDE_REGEXES",
938            "POST_EXCLUDE_REGEXES",
939            "POST_INCLUDE_FILES",
940            "POST_EXCLUDE_FILES",
941        ] {
942            assert!(
943                form.kwargs.contains_key(sub),
944                "RUNTIME_DEPENDENCY_SET missing {sub}"
945            );
946        }
947
948        for kind in ["LIBRARY", "RUNTIME", "FRAMEWORK"] {
949            let spec = form
950                .kwargs
951                .get(kind)
952                .unwrap_or_else(|| panic!("RUNTIME_DEPENDENCY_SET missing {kind}"));
953            for k in [
954                "DESTINATION",
955                "PERMISSIONS",
956                "CONFIGURATIONS",
957                "COMPONENT",
958                "NAMELINK_COMPONENT",
959            ] {
960                assert!(spec.kwargs.contains_key(k), "{kind} missing subkwarg {k}");
961            }
962            for f in [
963                "OPTIONAL",
964                "EXCLUDE_FROM_ALL",
965                "NAMELINK_ONLY",
966                "NAMELINK_SKIP",
967            ] {
968                assert!(spec.flags.contains(f), "{kind} missing subflag {f}");
969            }
970        }
971    }
972
973    #[test]
974    fn registry_knows_cmake_language_trace_form() {
975        let registry = CommandRegistry::load().unwrap();
976        let form = registry.get("cmake_language").form_for(Some("TRACE"));
977        assert!(form.flags.contains("ON"));
978        assert!(form.flags.contains("OFF"));
979        assert!(form.flags.contains("EXPAND"));
980    }
981
982    #[test]
983    fn registry_knows_cmake_pkg_config_import_keywords() {
984        let registry = CommandRegistry::load().unwrap();
985        let form = registry.get("cmake_pkg_config").form_for(Some("IMPORT"));
986        assert!(form.kwargs.contains_key("NAME"));
987        assert!(form.kwargs.contains_key("BIND_PC_REQUIRES"));
988    }
989
990    #[test]
991    fn registry_knows_file_archive_create_threads() {
992        let registry = CommandRegistry::load().unwrap();
993        let form = registry.get("file").form_for(Some("ARCHIVE_CREATE"));
994        assert!(form.kwargs.contains_key("THREADS"));
995        assert!(form.kwargs.contains_key("COMPRESSION_LEVEL"));
996    }
997
998    #[test]
999    fn registry_knows_file_strings_keywords() {
1000        let registry = CommandRegistry::load().unwrap();
1001        let form = registry.get("file").form_for(Some("STRINGS"));
1002        assert_eq!(form.pargs, NArgs::Fixed(2));
1003        assert!(form.kwargs.contains_key("REGEX"));
1004        assert!(form.kwargs.contains_key("LIMIT_COUNT"));
1005    }
1006
1007    #[test]
1008    fn registry_knows_cmake_package_config_helpers_commands() {
1009        let registry = CommandRegistry::load().unwrap();
1010        let configure = registry.get("configure_package_config_file").form_for(None);
1011        assert!(configure.kwargs.contains_key("INSTALL_DESTINATION"));
1012        assert!(configure.kwargs.contains_key("PATH_VARS"));
1013
1014        let version = registry
1015            .get("write_basic_package_version_file")
1016            .form_for(None);
1017        assert!(version.kwargs.contains_key("COMPATIBILITY"));
1018        assert!(version.kwargs.contains_key("VERSION"));
1019    }
1020
1021    #[test]
1022    fn registry_knows_utility_module_commands() {
1023        let registry = CommandRegistry::load().unwrap();
1024        assert_eq!(
1025            registry.get("cmake_dependent_option").form_for(None).pargs,
1026            NArgs::Fixed(5)
1027        );
1028        assert_eq!(
1029            registry.get("check_language").form_for(None).pargs,
1030            NArgs::Fixed(1)
1031        );
1032        assert_eq!(
1033            registry.get("check_include_file").form_for(None).pargs,
1034            NArgs::AtLeast(2)
1035        );
1036        assert_eq!(
1037            registry.get("check_compiler_flag").form_for(None).pargs,
1038            NArgs::Fixed(3)
1039        );
1040        assert_eq!(
1041            registry
1042                .get("check_objc_compiler_flag")
1043                .form_for(None)
1044                .pargs,
1045            NArgs::Fixed(2)
1046        );
1047        assert_eq!(
1048            registry.get("check_cxx_symbol_exists").form_for(None).pargs,
1049            NArgs::Fixed(3)
1050        );
1051        assert!(registry
1052            .get("cmake_push_check_state")
1053            .form_for(None)
1054            .flags
1055            .contains("RESET"));
1056        let print_props = registry.get("cmake_print_properties").form_for(None);
1057        assert!(print_props.kwargs.contains_key("TARGETS"));
1058        assert!(print_props.kwargs.contains_key("PROPERTIES"));
1059        let pie = registry.get("check_pie_supported").form_for(None);
1060        assert!(pie.kwargs.contains_key("OUTPUT_VARIABLE"));
1061        assert!(pie.kwargs.contains_key("LANGUAGES"));
1062        let source_compiles = registry.get("check_source_compiles").form_for(None);
1063        assert!(source_compiles.kwargs.contains_key("SRC_EXT"));
1064        assert!(source_compiles.kwargs.contains_key("FAIL_REGEX"));
1065        let find_dependency = registry.get("find_dependency").form_for(None);
1066        assert!(find_dependency.flags.contains("REQUIRED"));
1067        assert!(find_dependency.kwargs.contains_key("COMPONENTS"));
1068    }
1069
1070    #[test]
1071    fn registry_knows_supported_deprecated_module_commands() {
1072        let registry = CommandRegistry::load().unwrap();
1073        let version = registry
1074            .get("write_basic_config_version_file")
1075            .form_for(None);
1076        assert_eq!(version.pargs, NArgs::Fixed(1));
1077        assert!(version.kwargs.contains_key("COMPATIBILITY"));
1078        assert!(version.flags.contains("ARCH_INDEPENDENT"));
1079        assert_eq!(
1080            registry.get("check_cxx_accepts_flag").form_for(None).pargs,
1081            NArgs::Fixed(2)
1082        );
1083    }
1084
1085    #[test]
1086    fn registry_knows_fetchcontent_commands() {
1087        let registry = CommandRegistry::load().unwrap();
1088        let declare = registry.get("fetchcontent_declare").form_for(None);
1089        assert_eq!(declare.pargs, NArgs::Fixed(1));
1090        assert!(declare.flags.contains("EXCLUDE_FROM_ALL"));
1091        assert!(declare.kwargs.contains_key("FIND_PACKAGE_ARGS"));
1092
1093        let get_properties = registry.get("fetchcontent_getproperties").form_for(None);
1094        assert!(get_properties.kwargs.contains_key("SOURCE_DIR"));
1095        assert!(get_properties.kwargs.contains_key("BINARY_DIR"));
1096        assert!(get_properties.kwargs.contains_key("POPULATED"));
1097
1098        let populate = registry.get("fetchcontent_populate").form_for(None);
1099        assert!(populate.flags.contains("QUIET"));
1100        assert!(populate.kwargs.contains_key("SUBBUILD_DIR"));
1101    }
1102
1103    #[test]
1104    fn registry_knows_common_test_and_package_helper_modules() {
1105        let registry = CommandRegistry::load().unwrap();
1106
1107        let google_add = registry.get("gtest_add_tests").form_for(None);
1108        assert!(google_add.kwargs.contains_key("TARGET"));
1109        assert!(google_add.kwargs.contains_key("SOURCES"));
1110        assert!(google_add.flags.contains("SKIP_DEPENDENCY"));
1111
1112        let google_discover = registry.get("gtest_discover_tests").form_for(None);
1113        assert!(google_discover.kwargs.contains_key("DISCOVERY_MODE"));
1114        assert!(google_discover.kwargs.contains_key("XML_OUTPUT_DIR"));
1115        assert!(google_discover.flags.contains("NO_PRETTY_TYPES"));
1116
1117        assert_eq!(
1118            registry.get("processorcount").form_for(None).pargs,
1119            NArgs::Fixed(1)
1120        );
1121
1122        let fp_hsa = registry
1123            .get("find_package_handle_standard_args")
1124            .form_for(None);
1125        assert!(fp_hsa.flags.contains("DEFAULT_MSG"));
1126        assert!(fp_hsa.kwargs.contains_key("REQUIRED_VARS"));
1127        assert!(fp_hsa.kwargs.contains_key("VERSION_VAR"));
1128
1129        let fp_check = registry.get("find_package_check_version").form_for(None);
1130        assert_eq!(fp_check.pargs, NArgs::Fixed(2));
1131        assert!(fp_check.flags.contains("HANDLE_VERSION_RANGE"));
1132    }
1133
1134    #[test]
1135    fn registry_knows_externalproject_helper_commands() {
1136        let registry = CommandRegistry::load().unwrap();
1137        let step = registry.get("externalproject_add_step").form_for(None);
1138        assert_eq!(step.pargs, NArgs::Fixed(2));
1139        assert!(step.kwargs.contains_key("COMMAND"));
1140        assert!(step.kwargs.contains_key("DEPENDEES"));
1141        assert!(step.kwargs.contains_key("ENVIRONMENT_MODIFICATION"));
1142
1143        let targets = registry
1144            .get("externalproject_add_steptargets")
1145            .form_for(None);
1146        assert_eq!(targets.pargs, NArgs::AtLeast(2));
1147        assert!(targets.flags.contains("NO_DEPENDS"));
1148
1149        let deps = registry
1150            .get("externalproject_add_stepdependencies")
1151            .form_for(None);
1152        assert_eq!(deps.pargs, NArgs::AtLeast(3));
1153
1154        let props = registry.get("externalproject_get_property").form_for(None);
1155        assert_eq!(props.pargs, NArgs::AtLeast(2));
1156    }
1157
1158    #[test]
1159    fn registry_knows_packaging_and_find_helper_module_commands() {
1160        let registry = CommandRegistry::load().unwrap();
1161
1162        assert_eq!(
1163            registry.get("find_package_message").form_for(None).pargs,
1164            NArgs::Fixed(3)
1165        );
1166        assert_eq!(
1167            registry
1168                .get("select_library_configurations")
1169                .form_for(None)
1170                .pargs,
1171            NArgs::Fixed(1)
1172        );
1173
1174        let component = registry.get("cpack_add_component").form_for(None);
1175        assert!(component.flags.contains("HIDDEN"));
1176        assert!(component.kwargs.contains_key("DISPLAY_NAME"));
1177        assert!(component.kwargs.contains_key("DEPENDS"));
1178
1179        let group = registry.get("cpack_add_component_group").form_for(None);
1180        assert!(group.flags.contains("EXPANDED"));
1181        assert!(group.kwargs.contains_key("PARENT_GROUP"));
1182
1183        let downloads = registry.get("cpack_configure_downloads").form_for(None);
1184        assert_eq!(downloads.pargs, NArgs::Fixed(1));
1185        assert!(downloads.kwargs.contains_key("UPLOAD_DIRECTORY"));
1186    }
1187
1188    #[test]
1189    fn registry_knows_export_header_module_commands() {
1190        let registry = CommandRegistry::load().unwrap();
1191        let export_header = registry.get("generate_export_header").form_for(None);
1192        assert_eq!(export_header.pargs, NArgs::Fixed(1));
1193        assert!(export_header.flags.contains("DEFINE_NO_DEPRECATED"));
1194        assert!(export_header.kwargs.contains_key("EXPORT_FILE_NAME"));
1195        assert!(export_header.kwargs.contains_key("PREFIX_NAME"));
1196
1197        assert_eq!(
1198            registry
1199                .get("add_compiler_export_flags")
1200                .form_for(None)
1201                .pargs,
1202            NArgs::Optional
1203        );
1204    }
1205
1206    #[test]
1207    fn registry_knows_remaining_utility_module_commands() {
1208        let registry = CommandRegistry::load().unwrap();
1209
1210        for command in [
1211            "android_add_test_data",
1212            "add_file_dependencies",
1213            "cmake_add_fortran_subdirectory",
1214            "cmake_expand_imported_targets",
1215            "cmake_force_c_compiler",
1216            "cmake_force_cxx_compiler",
1217            "cmake_force_fortran_compiler",
1218            "ctest_coverage_collect_gcov",
1219            "copy_and_fixup_bundle",
1220            "fixup_bundle",
1221            "fixup_bundle_item",
1222            "verify_app",
1223            "verify_bundle_prerequisites",
1224            "verify_bundle_symlinks",
1225            "get_bundle_main_executable",
1226            "get_dotapp_dir",
1227            "get_bundle_and_executable",
1228            "get_bundle_all_executables",
1229            "get_bundle_keys",
1230            "get_item_key",
1231            "get_item_rpaths",
1232            "clear_bundle_keys",
1233            "set_bundle_key_values",
1234            "copy_resolved_framework_into_bundle",
1235            "copy_resolved_item_into_bundle",
1236            "cpack_ifw_add_package_resources",
1237            "cpack_ifw_add_repository",
1238            "cpack_ifw_configure_component",
1239            "cpack_ifw_configure_component_group",
1240            "cpack_ifw_update_repository",
1241            "cpack_ifw_configure_file",
1242            "csharp_set_windows_forms_properties",
1243            "csharp_set_designer_cs_properties",
1244            "csharp_set_xaml_cs_properties",
1245            "csharp_get_filename_keys",
1246            "csharp_get_filename_key_base",
1247            "csharp_get_dependentupon_name",
1248            "externaldata_expand_arguments",
1249            "externaldata_add_test",
1250            "externaldata_add_target",
1251            "fortrancinterface_header",
1252            "fortrancinterface_verify",
1253            "fetchcontent_setpopulated",
1254            "gnuinstalldirs_get_absolute_install_dir",
1255            "find_jar",
1256            "add_jar",
1257            "install_jar",
1258            "install_jar_exports",
1259            "export_jars",
1260            "create_javadoc",
1261            "create_javah",
1262            "install_jni_symlink",
1263            "swig_add_library",
1264            "swig_link_libraries",
1265            "print_enabled_features",
1266            "print_disabled_features",
1267            "set_feature_info",
1268            "set_package_info",
1269        ] {
1270            assert!(
1271                registry.contains_builtin(command),
1272                "missing built-in {command}"
1273            );
1274        }
1275
1276        assert_eq!(
1277            registry
1278                .get("ctest_coverage_collect_gcov")
1279                .form_for(None)
1280                .pargs,
1281            NArgs::ZeroOrMore
1282        );
1283        assert_eq!(
1284            registry
1285                .get("fortrancinterface_verify")
1286                .form_for(None)
1287                .pargs,
1288            NArgs::ZeroOrMore
1289        );
1290        assert_eq!(
1291            registry.get("add_jar").form_for(None).pargs,
1292            NArgs::AtLeast(2)
1293        );
1294        assert_eq!(
1295            registry
1296                .get("cpack_ifw_configure_file")
1297                .form_for(None)
1298                .pargs,
1299            NArgs::Fixed(2)
1300        );
1301        assert_eq!(
1302            registry
1303                .get("gnuinstalldirs_get_absolute_install_dir")
1304                .form_for(None)
1305                .pargs,
1306            NArgs::AtLeast(3)
1307        );
1308    }
1309
1310    #[test]
1311    fn registry_knows_string_json_43_modes() {
1312        let registry = CommandRegistry::load().unwrap();
1313        let form = registry.get("string").form_for(Some("JSON"));
1314        assert!(form.flags.contains("GET_RAW"));
1315        assert!(form.flags.contains("STRING_ENCODE"));
1316        assert!(form.kwargs.contains_key("ERROR_VARIABLE"));
1317    }
1318
1319    #[test]
1320    fn user_override_entries_merge_with_builtins() {
1321        let mut registry = CommandRegistry::load().unwrap();
1322        let overrides = r#"
1323[commands.target_link_libraries.layout]
1324always_wrap = true
1325
1326[commands.target_link_libraries.kwargs.LINKER_LANGUAGE]
1327nargs = 1
1328"#;
1329
1330        registry
1331            .merge_override_str(overrides, PathBuf::from("test-overrides.toml"))
1332            .unwrap();
1333
1334        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
1335            panic!()
1336        };
1337        assert_eq!(
1338            form.layout.as_ref().and_then(|layout| layout.always_wrap),
1339            Some(true)
1340        );
1341        assert!(form.kwargs.contains_key("PUBLIC"));
1342        assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
1343    }
1344
1345    #[test]
1346    fn uppercase_lookup_uses_builtin_normalization() {
1347        let registry = CommandRegistry::load().unwrap();
1348        assert!(registry.contains_builtin("TARGET_LINK_LIBRARIES"));
1349        let CommandSpec::Single(form) = registry.get("TARGET_LINK_LIBRARIES") else {
1350            panic!()
1351        };
1352        assert!(form.kwargs.contains_key("PUBLIC"));
1353        assert!(form.kwargs.contains_key("PRIVATE"));
1354    }
1355
1356    #[test]
1357    fn contains_builtin_excludes_user_added_commands_after_merge() {
1358        let mut registry = CommandRegistry::load().unwrap();
1359        registry
1360            .merge_toml_overrides(
1361                r#"
1362[commands.my_custom_command]
1363pargs = 1
1364"#,
1365            )
1366            .unwrap();
1367
1368        assert!(!registry.contains_builtin("my_custom_command"));
1369        assert!(!registry.contains_builtin("MY_CUSTOM_COMMAND"));
1370        assert!(matches!(
1371            registry.get("my_custom_command"),
1372            CommandSpec::Single(_)
1373        ));
1374    }
1375
1376    #[test]
1377    fn from_builtins_and_yaml_override_file_merges_entries() {
1378        let dir = tempfile::tempdir().unwrap();
1379        let overrides = dir.path().join("override.yaml");
1380        fs::write(
1381            &overrides,
1382            r#"
1383commands:
1384  target_link_libraries:
1385    kwargs:
1386      linker_language:
1387        nargs: 1
1388"#,
1389        )
1390        .unwrap();
1391
1392        let registry = CommandRegistry::from_builtins_and_overrides(Some(&overrides)).unwrap();
1393        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
1394            panic!()
1395        };
1396        assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
1397    }
1398
1399    #[test]
1400    fn merge_override_file_reports_structured_toml_parse_errors() {
1401        let mut registry = CommandRegistry::load().unwrap();
1402        let dir = tempfile::tempdir().unwrap();
1403        let path = dir.path().join("override.toml");
1404        fs::write(&path, "[commands.bad]\npargs = [\n").unwrap();
1405
1406        let err = registry.merge_override_file(&path).unwrap_err();
1407        match err {
1408            Error::Spec(spec_err) => {
1409                let details = &spec_err.details;
1410                assert_eq!(details.format, "TOML");
1411                assert!(details.line.is_some());
1412                assert!(details.column.is_some());
1413            }
1414            other => panic!("expected spec parse error, got {other:?}"),
1415        }
1416    }
1417
1418    #[test]
1419    fn merge_override_file_reports_structured_yaml_parse_errors() {
1420        let mut registry = CommandRegistry::load().unwrap();
1421        let dir = tempfile::tempdir().unwrap();
1422        let path = dir.path().join("override.yaml");
1423        fs::write(&path, "commands:\n  target_link_libraries: [\n").unwrap();
1424
1425        let err = registry.merge_override_file(&path).unwrap_err();
1426        match err {
1427            Error::Spec(spec_err) => {
1428                let details = &spec_err.details;
1429                assert_eq!(details.format, "YAML");
1430                assert!(details.line.is_some());
1431                assert!(details.column.is_some());
1432            }
1433            other => panic!("expected spec parse error, got {other:?}"),
1434        }
1435    }
1436
1437    #[test]
1438    fn override_with_mismatched_shape_replaces_base_command_spec() {
1439        let mut registry = CommandRegistry::load().unwrap();
1440        registry
1441            .merge_override_str(
1442                r#"
1443[commands.cmake_minimum_required.forms.VERSION]
1444pargs = 1
1445"#,
1446                PathBuf::from("override.toml"),
1447            )
1448            .unwrap();
1449
1450        let CommandSpec::Discriminated { .. } = registry.get("cmake_minimum_required") else {
1451            panic!("expected discriminated command after mismatched override")
1452        };
1453        assert_eq!(
1454            registry
1455                .get("cmake_minimum_required")
1456                .form_for(Some("VERSION"))
1457                .pargs,
1458            NArgs::Fixed(1)
1459        );
1460    }
1461}