Skip to main content

cmakefmt/spec/
registry.rs

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