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