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