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
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::OnceLock;
10
11use indexmap::{IndexMap, IndexSet};
12
13use crate::config::file::{detect_config_format, ConfigFileFormat};
14use crate::error::{Error, Result};
15
16use super::{
17    CommandForm, CommandFormOverride, CommandSpec, CommandSpecOverride, KwargSpec,
18    KwargSpecOverride, LayoutOverrides, LayoutOverridesOverride, SpecFile, SpecMetadata,
19    SpecOverrideFile,
20};
21
22const BUILTINS_PATH: &str = "src/spec/builtins.toml";
23const BUILTINS_TOML: &str = include_str!("builtins.toml");
24
25/// Registry of known CMake command specifications used to guide formatting.
26///
27/// The registry describes the argument structure of each command — positional
28/// slots, keyword sections, flags, and per-form layout hints — so the formatter
29/// can group and wrap arguments correctly.
30///
31/// # Two-tier model
32///
33/// The built-in registry covers the full CMake standard library.  User override
34/// files (TOML or YAML) can extend or modify any entry without replacing the
35/// whole registry.
36///
37/// # Getting a registry
38///
39/// | Situation | Recommended call |
40/// |-----------|-----------------|
41/// | No customisation needed | [`CommandRegistry::builtins`] — lazily initialised singleton, cheapest |
42/// | Fresh owned copy | [`CommandRegistry::load`] — allocates every call |
43/// | Merge with user override file | [`CommandRegistry::from_builtins_and_overrides`] |
44#[derive(Debug, Clone)]
45pub struct CommandRegistry {
46    metadata: SpecMetadata,
47    commands: IndexMap<String, CommandSpec>,
48    fallback: CommandSpec,
49}
50
51impl CommandRegistry {
52    /// Load the embedded built-in registry from `builtins.toml`.
53    ///
54    /// Returns a fresh owned [`CommandRegistry`] on every call.  Prefer
55    /// [`CommandRegistry::builtins`] when you only need a read-only reference —
56    /// it initialises once and amortises the parse cost across all callers.
57    pub fn load() -> Result<Self> {
58        Self::from_builtins_and_overrides(None::<&Path>)
59    }
60
61    /// Return the lazily initialised built-in registry singleton.
62    ///
63    /// The registry is parsed exactly once on first call; subsequent calls
64    /// return a `&'static` reference at zero cost.  Use [`CommandRegistry::load`]
65    /// if you need an owned, mutable copy.
66    pub fn builtins() -> &'static Self {
67        static BUILTINS: OnceLock<CommandRegistry> = OnceLock::new();
68        BUILTINS.get_or_init(|| {
69            CommandRegistry::from_builtins_and_overrides(None::<&Path>)
70                .expect("embedded built-in command registry should deserialize")
71        })
72    }
73
74    /// Load the embedded built-ins and optionally merge a user override file.
75    pub fn from_builtins_and_overrides(path: Option<impl AsRef<Path>>) -> Result<Self> {
76        let mut registry = Self::from_spec_file(parse_builtins()?);
77
78        if let Some(path) = path {
79            registry.merge_override_file(path.as_ref())?;
80        }
81
82        Ok(registry)
83    }
84
85    /// Build a registry directly from a deserialized [`SpecFile`].
86    pub(crate) fn from_spec_file(mut spec_file: SpecFile) -> Self {
87        normalize_spec_file(&mut spec_file);
88        Self {
89            metadata: spec_file.metadata,
90            commands: spec_file.commands,
91            fallback: CommandSpec::Single(CommandForm::default()),
92        }
93    }
94
95    /// Merge a supported user override file from disk into the registry.
96    pub fn merge_override_file(&mut self, path: &Path) -> Result<()> {
97        let source = fs::read_to_string(path)?;
98        self.merge_override_source(&source, path.to_path_buf(), detect_config_format(path)?)
99    }
100
101    /// Merge TOML override contents into the registry.
102    pub fn merge_override_str(&mut self, source: &str, path: impl Into<PathBuf>) -> Result<()> {
103        self.merge_override_source(source, path.into(), ConfigFileFormat::Toml)
104    }
105
106    fn merge_override_source(
107        &mut self,
108        source: &str,
109        path: PathBuf,
110        format: ConfigFileFormat,
111    ) -> Result<()> {
112        let mut overrides: SpecOverrideFile = match format {
113            ConfigFileFormat::Toml => toml::from_str(source).map_err(|toml_err| {
114                let (line, column) = crate::config::file::toml_line_col(
115                    source,
116                    toml_err.span().map(|span| span.start),
117                );
118                Error::Spec {
119                    path: path.clone(),
120                    details: crate::error::FileParseError {
121                        format: format.as_str(),
122                        message: toml_err.to_string().into_boxed_str(),
123                        line,
124                        column,
125                    },
126                    source_message: toml_err.to_string().into_boxed_str(),
127                }
128            })?,
129            ConfigFileFormat::Yaml => serde_yaml::from_str(source).map_err(|yaml_err| {
130                let location = yaml_err.location();
131                Error::Spec {
132                    path: path.clone(),
133                    details: crate::error::FileParseError {
134                        format: format.as_str(),
135                        message: yaml_err.to_string().into_boxed_str(),
136                        line: location.as_ref().map(|loc| loc.line()),
137                        column: location.as_ref().map(|loc| loc.column()),
138                    },
139                    source_message: yaml_err.to_string().into_boxed_str(),
140                }
141            })?,
142        };
143        normalize_override_file(&mut overrides);
144
145        for (name, override_spec) in overrides.commands {
146            match self.commands.get_mut(&name) {
147                Some(existing) => merge_command_spec(existing, override_spec),
148                None => {
149                    self.commands.insert(name, override_spec.into_full_spec());
150                }
151            }
152        }
153
154        Ok(())
155    }
156
157    /// Get the command spec for `command_name`, falling back to a permissive
158    /// default when the command is unknown.
159    pub fn get(&self, command_name: &str) -> &CommandSpec {
160        if let Some(spec) = self.commands.get(command_name) {
161            return spec;
162        }
163
164        if !has_ascii_uppercase(command_name) {
165            return &self.fallback;
166        }
167
168        self.commands
169            .get(&command_name.to_ascii_lowercase())
170            .unwrap_or(&self.fallback)
171    }
172
173    /// Return `true` when the command is present in the built-in registry.
174    pub fn contains_builtin(&self, command_name: &str) -> bool {
175        self.commands.contains_key(command_name)
176            || (has_ascii_uppercase(command_name)
177                && self
178                    .commands
179                    .contains_key(&command_name.to_ascii_lowercase()))
180    }
181
182    /// Report the audited upstream CMake version for the built-in spec.
183    pub fn audited_cmake_version(&self) -> &str {
184        &self.metadata.cmake_version
185    }
186}
187
188fn has_ascii_uppercase(s: &str) -> bool {
189    s.bytes().any(|byte| byte.is_ascii_uppercase())
190}
191
192fn parse_builtins() -> Result<SpecFile> {
193    let mut spec: SpecFile = toml::from_str(BUILTINS_TOML).map_err(|source| Error::Spec {
194        path: PathBuf::from(BUILTINS_PATH),
195        details: crate::error::FileParseError {
196            format: "TOML",
197            message: source.to_string().into_boxed_str(),
198            line: None,
199            column: None,
200        },
201        source_message: source.to_string().into_boxed_str(),
202    })?;
203    normalize_spec_file(&mut spec);
204    Ok(spec)
205}
206
207fn normalize_spec_file(spec: &mut SpecFile) {
208    spec.commands = std::mem::take(&mut spec.commands)
209        .into_iter()
210        .map(|(name, mut command)| {
211            normalize_command_spec(&mut command);
212            (name.to_ascii_lowercase(), command)
213        })
214        .collect();
215}
216
217fn normalize_override_file(spec: &mut SpecOverrideFile) {
218    spec.commands = std::mem::take(&mut spec.commands)
219        .into_iter()
220        .map(|(name, mut command)| {
221            normalize_command_override(&mut command);
222            (name.to_ascii_lowercase(), command)
223        })
224        .collect();
225}
226
227fn normalize_command_spec(spec: &mut CommandSpec) {
228    match spec {
229        CommandSpec::Single(form) => normalize_form(form),
230        CommandSpec::Discriminated { forms, fallback } => {
231            *forms = std::mem::take(forms)
232                .into_iter()
233                .map(|(name, mut form)| {
234                    normalize_form(&mut form);
235                    (name.to_ascii_uppercase(), form)
236                })
237                .collect();
238
239            if let Some(fallback) = fallback {
240                normalize_form(fallback);
241            }
242        }
243    }
244}
245
246fn normalize_command_override(spec: &mut CommandSpecOverride) {
247    match spec {
248        CommandSpecOverride::Single(form) => normalize_form_override(form),
249        CommandSpecOverride::Discriminated { forms, fallback } => {
250            *forms = std::mem::take(forms)
251                .into_iter()
252                .map(|(name, mut form)| {
253                    normalize_form_override(&mut form);
254                    (name.to_ascii_uppercase(), form)
255                })
256                .collect();
257
258            if let Some(fallback) = fallback {
259                normalize_form_override(fallback);
260            }
261        }
262    }
263}
264
265fn normalize_form(form: &mut CommandForm) {
266    form.kwargs = std::mem::take(&mut form.kwargs)
267        .into_iter()
268        .map(|(name, mut kwarg)| {
269            normalize_kwarg(&mut kwarg);
270            (name.to_ascii_uppercase(), kwarg)
271        })
272        .collect();
273
274    form.flags = std::mem::take(&mut form.flags)
275        .into_iter()
276        .map(|flag| flag.to_ascii_uppercase())
277        .collect();
278}
279
280fn normalize_form_override(form: &mut CommandFormOverride) {
281    form.kwargs = std::mem::take(&mut form.kwargs)
282        .into_iter()
283        .map(|(name, mut kwarg)| {
284            normalize_kwarg_override(&mut kwarg);
285            (name.to_ascii_uppercase(), kwarg)
286        })
287        .collect();
288
289    form.flags = std::mem::take(&mut form.flags)
290        .into_iter()
291        .map(|flag| flag.to_ascii_uppercase())
292        .collect();
293}
294
295fn normalize_kwarg(spec: &mut KwargSpec) {
296    spec.kwargs = std::mem::take(&mut spec.kwargs)
297        .into_iter()
298        .map(|(name, mut kwarg)| {
299            normalize_kwarg(&mut kwarg);
300            (name.to_ascii_uppercase(), kwarg)
301        })
302        .collect();
303
304    spec.flags = std::mem::take(&mut spec.flags)
305        .into_iter()
306        .map(|flag| flag.to_ascii_uppercase())
307        .collect();
308}
309
310fn normalize_kwarg_override(spec: &mut KwargSpecOverride) {
311    spec.kwargs = std::mem::take(&mut spec.kwargs)
312        .into_iter()
313        .map(|(name, mut kwarg)| {
314            normalize_kwarg_override(&mut kwarg);
315            (name.to_ascii_uppercase(), kwarg)
316        })
317        .collect();
318
319    spec.flags = std::mem::take(&mut spec.flags)
320        .into_iter()
321        .map(|flag| flag.to_ascii_uppercase())
322        .collect();
323}
324
325fn merge_command_spec(base: &mut CommandSpec, override_spec: CommandSpecOverride) {
326    match (base, override_spec) {
327        (CommandSpec::Single(base_form), CommandSpecOverride::Single(override_form)) => {
328            merge_form(base_form, override_form);
329        }
330        (
331            CommandSpec::Discriminated {
332                forms: base_forms,
333                fallback: base_fallback,
334            },
335            CommandSpecOverride::Discriminated {
336                forms: override_forms,
337                fallback: override_fallback,
338            },
339        ) => {
340            for (name, override_form) in override_forms {
341                match base_forms.get_mut(&name) {
342                    Some(base_form) => merge_form(base_form, override_form),
343                    None => {
344                        base_forms.insert(name, override_form.into_full_form());
345                    }
346                }
347            }
348
349            if let Some(override_fallback) = override_fallback {
350                match base_fallback {
351                    Some(base_fallback) => merge_form(base_fallback, override_fallback),
352                    None => {
353                        *base_fallback = Some(override_fallback.into_full_form());
354                    }
355                }
356            }
357        }
358        (base_spec, override_spec) => {
359            *base_spec = override_spec.into_full_spec();
360        }
361    }
362}
363
364fn merge_form(base: &mut CommandForm, override_form: CommandFormOverride) {
365    if let Some(pargs) = override_form.pargs {
366        base.pargs = pargs;
367    }
368
369    merge_flags(&mut base.flags, override_form.flags);
370
371    for (name, override_kwarg) in override_form.kwargs {
372        match base.kwargs.get_mut(&name) {
373            Some(base_kwarg) => merge_kwarg(base_kwarg, override_kwarg),
374            None => {
375                base.kwargs.insert(name, override_kwarg.into_full_spec());
376            }
377        }
378    }
379
380    if let Some(layout) = override_form.layout {
381        merge_layout(
382            base.layout.get_or_insert_with(LayoutOverrides::default),
383            layout,
384        );
385    }
386}
387
388fn merge_kwarg(base: &mut KwargSpec, override_kwarg: KwargSpecOverride) {
389    if let Some(nargs) = override_kwarg.nargs {
390        base.nargs = nargs;
391    }
392
393    merge_flags(&mut base.flags, override_kwarg.flags);
394
395    for (name, nested_override) in override_kwarg.kwargs {
396        match base.kwargs.get_mut(&name) {
397            Some(base_nested) => merge_kwarg(base_nested, nested_override),
398            None => {
399                base.kwargs.insert(name, nested_override.into_full_spec());
400            }
401        }
402    }
403}
404
405fn merge_layout(base: &mut LayoutOverrides, override_layout: LayoutOverridesOverride) {
406    if let Some(value) = override_layout.line_width {
407        base.line_width = Some(value);
408    }
409    if let Some(value) = override_layout.tab_size {
410        base.tab_size = Some(value);
411    }
412    if let Some(value) = override_layout.dangle_parens {
413        base.dangle_parens = Some(value);
414    }
415    if let Some(value) = override_layout.always_wrap {
416        base.always_wrap = Some(value);
417    }
418    if let Some(value) = override_layout.max_pargs_hwrap {
419        base.max_pargs_hwrap = Some(value);
420    }
421}
422
423fn merge_flags(base: &mut IndexSet<String>, override_flags: IndexSet<String>) {
424    for flag in override_flags {
425        base.insert(flag);
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use crate::spec::NArgs;
433    use std::fs;
434
435    #[test]
436    fn registry_has_target_link_libraries_keywords() {
437        let registry = CommandRegistry::load().unwrap();
438        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
439            panic!()
440        };
441        assert!(form.kwargs.contains_key("PUBLIC"));
442        assert!(form.kwargs.contains_key("PRIVATE"));
443        assert!(form.kwargs.contains_key("INTERFACE"));
444    }
445
446    #[test]
447    fn registry_has_install_forms() {
448        let registry = CommandRegistry::load().unwrap();
449        assert!(matches!(
450            registry.get("install"),
451            CommandSpec::Discriminated { .. }
452        ));
453    }
454
455    #[test]
456    fn registry_unknown_command_uses_fallback() {
457        let registry = CommandRegistry::load().unwrap();
458        let spec = registry.get("my_unknown_command");
459        let CommandSpec::Single(form) = spec else {
460            panic!()
461        };
462        assert_eq!(form.pargs, NArgs::ZeroOrMore);
463        assert!(form.kwargs.is_empty());
464        assert!(form.flags.is_empty());
465    }
466
467    #[test]
468    fn registry_knows_builtin_surface() {
469        let registry = CommandRegistry::load().unwrap();
470        assert!(registry.contains_builtin("cmake_minimum_required"));
471        assert!(registry.contains_builtin("target_sources"));
472        assert!(registry.contains_builtin("while"));
473        assert!(registry.contains_builtin("external_project_add"));
474    }
475
476    #[test]
477    fn registry_reports_audited_cmake_version() {
478        let registry = CommandRegistry::load().unwrap();
479        assert_eq!(registry.audited_cmake_version(), "4.3.1");
480    }
481
482    #[test]
483    fn registry_knows_project_43_keywords() {
484        let registry = CommandRegistry::load().unwrap();
485        let CommandSpec::Single(form) = registry.get("project") else {
486            panic!()
487        };
488        assert!(form.flags.contains("COMPAT_VERSION"));
489        assert!(form.flags.contains("SPDX_LICENSE"));
490    }
491
492    #[test]
493    fn registry_knows_export_package_info_form() {
494        let registry = CommandRegistry::load().unwrap();
495        let CommandSpec::Discriminated { .. } = registry.get("export") else {
496            panic!()
497        };
498        let form = registry.get("export").form_for(Some("PACKAGE_INFO"));
499        assert_eq!(form.pargs, NArgs::Fixed(1));
500        assert!(form.kwargs.contains_key("EXPORT"));
501        assert!(form.kwargs.contains_key("CXX_MODULES_DIRECTORY"));
502    }
503
504    #[test]
505    fn registry_knows_install_package_info_form() {
506        let registry = CommandRegistry::load().unwrap();
507        let form = registry.get("install").form_for(Some("PACKAGE_INFO"));
508        assert_eq!(form.pargs, NArgs::Fixed(1));
509        assert!(form.kwargs.contains_key("DESTINATION"));
510        assert!(form.kwargs.contains_key("COMPAT_VERSION"));
511    }
512
513    #[test]
514    fn registry_knows_install_export_namespace_keyword() {
515        let registry = CommandRegistry::load().unwrap();
516        let form = registry.get("install").form_for(Some("EXPORT"));
517        assert!(form.kwargs.contains_key("DESTINATION"));
518        assert!(form.kwargs.contains_key("NAMESPACE"));
519        assert!(form.kwargs.contains_key("FILE"));
520        assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
521    }
522
523    #[test]
524    fn registry_knows_install_targets_export_and_includes_sections() {
525        let registry = CommandRegistry::load().unwrap();
526        let form = registry.get("install").form_for(Some("TARGETS"));
527        assert!(form.kwargs.contains_key("EXPORT"));
528        assert!(form.kwargs.contains_key("INCLUDES"));
529        assert!(form
530            .kwargs
531            .get("INCLUDES")
532            .is_some_and(|spec| spec.kwargs.contains_key("DESTINATION")));
533        assert!(form.kwargs.contains_key("RUNTIME_DEPENDENCY_SET"));
534        assert!(form.flags.contains("RUNTIME"));
535        assert!(form.flags.contains("LIBRARY"));
536        assert!(form.flags.contains("ARCHIVE"));
537    }
538
539    #[test]
540    fn registry_knows_cmake_language_trace_form() {
541        let registry = CommandRegistry::load().unwrap();
542        let form = registry.get("cmake_language").form_for(Some("TRACE"));
543        assert!(form.flags.contains("ON"));
544        assert!(form.flags.contains("OFF"));
545        assert!(form.flags.contains("EXPAND"));
546    }
547
548    #[test]
549    fn registry_knows_cmake_pkg_config_import_keywords() {
550        let registry = CommandRegistry::load().unwrap();
551        let form = registry.get("cmake_pkg_config").form_for(Some("IMPORT"));
552        assert!(form.kwargs.contains_key("NAME"));
553        assert!(form.kwargs.contains_key("BIND_PC_REQUIRES"));
554    }
555
556    #[test]
557    fn registry_knows_file_archive_create_threads() {
558        let registry = CommandRegistry::load().unwrap();
559        let form = registry.get("file").form_for(Some("ARCHIVE_CREATE"));
560        assert!(form.kwargs.contains_key("THREADS"));
561        assert!(form.kwargs.contains_key("COMPRESSION_LEVEL"));
562    }
563
564    #[test]
565    fn registry_knows_file_strings_keywords() {
566        let registry = CommandRegistry::load().unwrap();
567        let form = registry.get("file").form_for(Some("STRINGS"));
568        assert_eq!(form.pargs, NArgs::Fixed(2));
569        assert!(form.kwargs.contains_key("REGEX"));
570        assert!(form.kwargs.contains_key("LIMIT_COUNT"));
571    }
572
573    #[test]
574    fn registry_knows_cmake_package_config_helpers_commands() {
575        let registry = CommandRegistry::load().unwrap();
576        let configure = registry.get("configure_package_config_file").form_for(None);
577        assert!(configure.kwargs.contains_key("INSTALL_DESTINATION"));
578        assert!(configure.kwargs.contains_key("PATH_VARS"));
579
580        let version = registry
581            .get("write_basic_package_version_file")
582            .form_for(None);
583        assert!(version.kwargs.contains_key("COMPATIBILITY"));
584        assert!(version.kwargs.contains_key("VERSION"));
585    }
586
587    #[test]
588    fn registry_knows_utility_module_commands() {
589        let registry = CommandRegistry::load().unwrap();
590        assert_eq!(
591            registry.get("cmake_dependent_option").form_for(None).pargs,
592            NArgs::Fixed(5)
593        );
594        assert_eq!(
595            registry.get("check_language").form_for(None).pargs,
596            NArgs::Fixed(1)
597        );
598        assert_eq!(
599            registry.get("check_include_file").form_for(None).pargs,
600            NArgs::AtLeast(2)
601        );
602        assert_eq!(
603            registry.get("check_compiler_flag").form_for(None).pargs,
604            NArgs::Fixed(3)
605        );
606        assert_eq!(
607            registry
608                .get("check_objc_compiler_flag")
609                .form_for(None)
610                .pargs,
611            NArgs::Fixed(2)
612        );
613        assert_eq!(
614            registry.get("check_cxx_symbol_exists").form_for(None).pargs,
615            NArgs::Fixed(3)
616        );
617        assert!(registry
618            .get("cmake_push_check_state")
619            .form_for(None)
620            .flags
621            .contains("RESET"));
622        let print_props = registry.get("cmake_print_properties").form_for(None);
623        assert!(print_props.kwargs.contains_key("TARGETS"));
624        assert!(print_props.kwargs.contains_key("PROPERTIES"));
625        let pie = registry.get("check_pie_supported").form_for(None);
626        assert!(pie.kwargs.contains_key("OUTPUT_VARIABLE"));
627        assert!(pie.kwargs.contains_key("LANGUAGES"));
628        let source_compiles = registry.get("check_source_compiles").form_for(None);
629        assert!(source_compiles.kwargs.contains_key("SRC_EXT"));
630        assert!(source_compiles.kwargs.contains_key("FAIL_REGEX"));
631        let find_dependency = registry.get("find_dependency").form_for(None);
632        assert!(find_dependency.flags.contains("REQUIRED"));
633        assert!(find_dependency.kwargs.contains_key("COMPONENTS"));
634    }
635
636    #[test]
637    fn registry_knows_supported_deprecated_module_commands() {
638        let registry = CommandRegistry::load().unwrap();
639        let version = registry
640            .get("write_basic_config_version_file")
641            .form_for(None);
642        assert_eq!(version.pargs, NArgs::Fixed(1));
643        assert!(version.kwargs.contains_key("COMPATIBILITY"));
644        assert!(version.flags.contains("ARCH_INDEPENDENT"));
645        assert_eq!(
646            registry.get("check_cxx_accepts_flag").form_for(None).pargs,
647            NArgs::Fixed(2)
648        );
649    }
650
651    #[test]
652    fn registry_knows_fetchcontent_commands() {
653        let registry = CommandRegistry::load().unwrap();
654        let declare = registry.get("fetchcontent_declare").form_for(None);
655        assert_eq!(declare.pargs, NArgs::Fixed(1));
656        assert!(declare.flags.contains("EXCLUDE_FROM_ALL"));
657        assert!(declare.kwargs.contains_key("FIND_PACKAGE_ARGS"));
658
659        let get_properties = registry.get("fetchcontent_getproperties").form_for(None);
660        assert!(get_properties.kwargs.contains_key("SOURCE_DIR"));
661        assert!(get_properties.kwargs.contains_key("BINARY_DIR"));
662        assert!(get_properties.kwargs.contains_key("POPULATED"));
663
664        let populate = registry.get("fetchcontent_populate").form_for(None);
665        assert!(populate.flags.contains("QUIET"));
666        assert!(populate.kwargs.contains_key("SUBBUILD_DIR"));
667    }
668
669    #[test]
670    fn registry_knows_common_test_and_package_helper_modules() {
671        let registry = CommandRegistry::load().unwrap();
672
673        let google_add = registry.get("gtest_add_tests").form_for(None);
674        assert!(google_add.kwargs.contains_key("TARGET"));
675        assert!(google_add.kwargs.contains_key("SOURCES"));
676        assert!(google_add.flags.contains("SKIP_DEPENDENCY"));
677
678        let google_discover = registry.get("gtest_discover_tests").form_for(None);
679        assert!(google_discover.kwargs.contains_key("DISCOVERY_MODE"));
680        assert!(google_discover.kwargs.contains_key("XML_OUTPUT_DIR"));
681        assert!(google_discover.flags.contains("NO_PRETTY_TYPES"));
682
683        assert_eq!(
684            registry.get("processorcount").form_for(None).pargs,
685            NArgs::Fixed(1)
686        );
687
688        let fp_hsa = registry
689            .get("find_package_handle_standard_args")
690            .form_for(None);
691        assert!(fp_hsa.flags.contains("DEFAULT_MSG"));
692        assert!(fp_hsa.kwargs.contains_key("REQUIRED_VARS"));
693        assert!(fp_hsa.kwargs.contains_key("VERSION_VAR"));
694
695        let fp_check = registry.get("find_package_check_version").form_for(None);
696        assert_eq!(fp_check.pargs, NArgs::Fixed(2));
697        assert!(fp_check.flags.contains("HANDLE_VERSION_RANGE"));
698    }
699
700    #[test]
701    fn registry_knows_externalproject_helper_commands() {
702        let registry = CommandRegistry::load().unwrap();
703        let step = registry.get("externalproject_add_step").form_for(None);
704        assert_eq!(step.pargs, NArgs::Fixed(2));
705        assert!(step.kwargs.contains_key("COMMAND"));
706        assert!(step.kwargs.contains_key("DEPENDEES"));
707        assert!(step.kwargs.contains_key("ENVIRONMENT_MODIFICATION"));
708
709        let targets = registry
710            .get("externalproject_add_steptargets")
711            .form_for(None);
712        assert_eq!(targets.pargs, NArgs::AtLeast(2));
713        assert!(targets.flags.contains("NO_DEPENDS"));
714
715        let deps = registry
716            .get("externalproject_add_stepdependencies")
717            .form_for(None);
718        assert_eq!(deps.pargs, NArgs::AtLeast(3));
719
720        let props = registry.get("externalproject_get_property").form_for(None);
721        assert_eq!(props.pargs, NArgs::AtLeast(2));
722    }
723
724    #[test]
725    fn registry_knows_packaging_and_find_helper_module_commands() {
726        let registry = CommandRegistry::load().unwrap();
727
728        assert_eq!(
729            registry.get("find_package_message").form_for(None).pargs,
730            NArgs::Fixed(3)
731        );
732        assert_eq!(
733            registry
734                .get("select_library_configurations")
735                .form_for(None)
736                .pargs,
737            NArgs::Fixed(1)
738        );
739
740        let component = registry.get("cpack_add_component").form_for(None);
741        assert!(component.flags.contains("HIDDEN"));
742        assert!(component.kwargs.contains_key("DISPLAY_NAME"));
743        assert!(component.kwargs.contains_key("DEPENDS"));
744
745        let group = registry.get("cpack_add_component_group").form_for(None);
746        assert!(group.flags.contains("EXPANDED"));
747        assert!(group.kwargs.contains_key("PARENT_GROUP"));
748
749        let downloads = registry.get("cpack_configure_downloads").form_for(None);
750        assert_eq!(downloads.pargs, NArgs::Fixed(1));
751        assert!(downloads.kwargs.contains_key("UPLOAD_DIRECTORY"));
752    }
753
754    #[test]
755    fn registry_knows_export_header_module_commands() {
756        let registry = CommandRegistry::load().unwrap();
757        let export_header = registry.get("generate_export_header").form_for(None);
758        assert_eq!(export_header.pargs, NArgs::Fixed(1));
759        assert!(export_header.flags.contains("DEFINE_NO_DEPRECATED"));
760        assert!(export_header.kwargs.contains_key("EXPORT_FILE_NAME"));
761        assert!(export_header.kwargs.contains_key("PREFIX_NAME"));
762
763        assert_eq!(
764            registry
765                .get("add_compiler_export_flags")
766                .form_for(None)
767                .pargs,
768            NArgs::Optional
769        );
770    }
771
772    #[test]
773    fn registry_knows_remaining_utility_module_commands() {
774        let registry = CommandRegistry::load().unwrap();
775
776        for command in [
777            "android_add_test_data",
778            "add_file_dependencies",
779            "cmake_add_fortran_subdirectory",
780            "cmake_expand_imported_targets",
781            "cmake_force_c_compiler",
782            "cmake_force_cxx_compiler",
783            "cmake_force_fortran_compiler",
784            "ctest_coverage_collect_gcov",
785            "copy_and_fixup_bundle",
786            "fixup_bundle",
787            "fixup_bundle_item",
788            "verify_app",
789            "verify_bundle_prerequisites",
790            "verify_bundle_symlinks",
791            "get_bundle_main_executable",
792            "get_dotapp_dir",
793            "get_bundle_and_executable",
794            "get_bundle_all_executables",
795            "get_bundle_keys",
796            "get_item_key",
797            "get_item_rpaths",
798            "clear_bundle_keys",
799            "set_bundle_key_values",
800            "copy_resolved_framework_into_bundle",
801            "copy_resolved_item_into_bundle",
802            "cpack_ifw_add_package_resources",
803            "cpack_ifw_add_repository",
804            "cpack_ifw_configure_component",
805            "cpack_ifw_configure_component_group",
806            "cpack_ifw_update_repository",
807            "cpack_ifw_configure_file",
808            "csharp_set_windows_forms_properties",
809            "csharp_set_designer_cs_properties",
810            "csharp_set_xaml_cs_properties",
811            "csharp_get_filename_keys",
812            "csharp_get_filename_key_base",
813            "csharp_get_dependentupon_name",
814            "externaldata_expand_arguments",
815            "externaldata_add_test",
816            "externaldata_add_target",
817            "fortrancinterface_header",
818            "fortrancinterface_verify",
819            "fetchcontent_setpopulated",
820            "gnuinstalldirs_get_absolute_install_dir",
821            "find_jar",
822            "add_jar",
823            "install_jar",
824            "install_jar_exports",
825            "export_jars",
826            "create_javadoc",
827            "create_javah",
828            "install_jni_symlink",
829            "swig_add_library",
830            "swig_link_libraries",
831            "print_enabled_features",
832            "print_disabled_features",
833            "set_feature_info",
834            "set_package_info",
835        ] {
836            assert!(
837                registry.contains_builtin(command),
838                "missing built-in {command}"
839            );
840        }
841
842        assert_eq!(
843            registry
844                .get("ctest_coverage_collect_gcov")
845                .form_for(None)
846                .pargs,
847            NArgs::ZeroOrMore
848        );
849        assert_eq!(
850            registry
851                .get("fortrancinterface_verify")
852                .form_for(None)
853                .pargs,
854            NArgs::ZeroOrMore
855        );
856        assert_eq!(
857            registry.get("add_jar").form_for(None).pargs,
858            NArgs::AtLeast(2)
859        );
860        assert_eq!(
861            registry
862                .get("cpack_ifw_configure_file")
863                .form_for(None)
864                .pargs,
865            NArgs::Fixed(2)
866        );
867        assert_eq!(
868            registry
869                .get("gnuinstalldirs_get_absolute_install_dir")
870                .form_for(None)
871                .pargs,
872            NArgs::AtLeast(3)
873        );
874    }
875
876    #[test]
877    fn registry_knows_string_json_43_modes() {
878        let registry = CommandRegistry::load().unwrap();
879        let form = registry.get("string").form_for(Some("JSON"));
880        assert!(form.flags.contains("GET_RAW"));
881        assert!(form.flags.contains("STRING_ENCODE"));
882        assert!(form.kwargs.contains_key("ERROR_VARIABLE"));
883    }
884
885    #[test]
886    fn user_override_entries_merge_with_builtins() {
887        let mut registry = CommandRegistry::load().unwrap();
888        let overrides = r#"
889[commands.target_link_libraries.layout]
890always_wrap = true
891
892[commands.target_link_libraries.kwargs.LINKER_LANGUAGE]
893nargs = 1
894"#;
895
896        registry
897            .merge_override_str(overrides, PathBuf::from("test-overrides.toml"))
898            .unwrap();
899
900        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
901            panic!()
902        };
903        assert_eq!(
904            form.layout.as_ref().and_then(|layout| layout.always_wrap),
905            Some(true)
906        );
907        assert!(form.kwargs.contains_key("PUBLIC"));
908        assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
909    }
910
911    #[test]
912    fn uppercase_lookup_uses_builtin_normalization() {
913        let registry = CommandRegistry::load().unwrap();
914        assert!(registry.contains_builtin("TARGET_LINK_LIBRARIES"));
915        let CommandSpec::Single(form) = registry.get("TARGET_LINK_LIBRARIES") else {
916            panic!()
917        };
918        assert!(form.kwargs.contains_key("PUBLIC"));
919        assert!(form.kwargs.contains_key("PRIVATE"));
920    }
921
922    #[test]
923    fn from_builtins_and_yaml_override_file_merges_entries() {
924        let dir = tempfile::tempdir().unwrap();
925        let overrides = dir.path().join("override.yaml");
926        fs::write(
927            &overrides,
928            r#"
929commands:
930  target_link_libraries:
931    kwargs:
932      linker_language:
933        nargs: 1
934"#,
935        )
936        .unwrap();
937
938        let registry = CommandRegistry::from_builtins_and_overrides(Some(&overrides)).unwrap();
939        let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
940            panic!()
941        };
942        assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
943    }
944
945    #[test]
946    fn merge_override_file_reports_structured_toml_parse_errors() {
947        let mut registry = CommandRegistry::load().unwrap();
948        let dir = tempfile::tempdir().unwrap();
949        let path = dir.path().join("override.toml");
950        fs::write(&path, "[commands.bad]\npargs = [\n").unwrap();
951
952        let err = registry.merge_override_file(&path).unwrap_err();
953        match err {
954            Error::Spec { details, .. } => {
955                assert_eq!(details.format, "TOML");
956                assert!(details.line.is_some());
957                assert!(details.column.is_some());
958            }
959            other => panic!("expected spec parse error, got {other:?}"),
960        }
961    }
962
963    #[test]
964    fn merge_override_file_reports_structured_yaml_parse_errors() {
965        let mut registry = CommandRegistry::load().unwrap();
966        let dir = tempfile::tempdir().unwrap();
967        let path = dir.path().join("override.yaml");
968        fs::write(&path, "commands:\n  target_link_libraries: [\n").unwrap();
969
970        let err = registry.merge_override_file(&path).unwrap_err();
971        match err {
972            Error::Spec { details, .. } => {
973                assert_eq!(details.format, "YAML");
974                assert!(details.line.is_some());
975                assert!(details.column.is_some());
976            }
977            other => panic!("expected spec parse error, got {other:?}"),
978        }
979    }
980
981    #[test]
982    fn override_with_mismatched_shape_replaces_base_command_spec() {
983        let mut registry = CommandRegistry::load().unwrap();
984        registry
985            .merge_override_str(
986                r#"
987[commands.cmake_minimum_required.forms.VERSION]
988pargs = 1
989"#,
990                PathBuf::from("override.toml"),
991            )
992            .unwrap();
993
994        let CommandSpec::Discriminated { .. } = registry.get("cmake_minimum_required") else {
995            panic!("expected discriminated command after mismatched override")
996        };
997        assert_eq!(
998            registry
999                .get("cmake_minimum_required")
1000                .form_for(Some("VERSION"))
1001                .pargs,
1002            NArgs::Fixed(1)
1003        );
1004    }
1005}