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