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