Skip to main content

cmakefmt/spec/
registry.rs

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