typos_cli/
config.rs

1#![allow(unused_qualifications)] // schemars
2
3use std::collections::HashMap;
4
5use kstring::KString;
6
7use crate::file_type_specifics;
8
9pub const SUPPORTED_FILE_NAMES: &[&str] = &[
10    "typos.toml",
11    "_typos.toml",
12    ".typos.toml",
13    CARGO_TOML,
14    PYPROJECT_TOML,
15];
16
17const CARGO_TOML: &str = "Cargo.toml";
18const PYPROJECT_TOML: &str = "pyproject.toml";
19
20#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21#[serde(deny_unknown_fields)]
22#[serde(default)]
23#[serde(rename_all = "kebab-case")]
24#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
25pub struct Config {
26    pub files: Walk,
27    pub default: EngineConfig,
28    #[serde(rename = "type")]
29    pub type_: TypeEngineConfig,
30    #[serde(skip)]
31    pub overrides: EngineConfig,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35#[serde(default)]
36#[serde(rename_all = "kebab-case")]
37pub struct CargoTomlConfig {
38    pub workspace: Option<CargoTomlPackage>,
39    pub package: Option<CargoTomlPackage>,
40}
41
42#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43#[serde(default)]
44#[serde(rename_all = "kebab-case")]
45pub struct CargoTomlPackage {
46    pub metadata: CargoTomlMetadata,
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50#[serde(default)]
51#[serde(rename_all = "kebab-case")]
52pub struct CargoTomlMetadata {
53    pub typos: Option<Config>,
54}
55
56#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
57#[serde(default)]
58#[serde(rename_all = "kebab-case")]
59pub struct PyprojectTomlConfig {
60    pub tool: PyprojectTomlTool,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
64#[serde(default)]
65#[serde(rename_all = "kebab-case")]
66pub struct PyprojectTomlTool {
67    pub typos: Option<Config>,
68}
69
70impl Config {
71    pub fn from_dir(cwd: &std::path::Path) -> Result<Option<Self>, anyhow::Error> {
72        for file in find_project_files(cwd, SUPPORTED_FILE_NAMES) {
73            log::debug!("Loading {}", file.display());
74            if let Some(config) = Self::from_file(&file)? {
75                return Ok(Some(config));
76            }
77        }
78
79        Ok(None)
80    }
81
82    pub fn from_file(path: &std::path::Path) -> Result<Option<Self>, anyhow::Error> {
83        let s = std::fs::read_to_string(path).map_err(|err| {
84            let kind = err.kind();
85            std::io::Error::new(
86                kind,
87                format!("could not read config at `{}`", path.display()),
88            )
89        })?;
90
91        if path.file_name().unwrap() == CARGO_TOML {
92            let config = toml::from_str::<CargoTomlConfig>(&s)?;
93            let typos = config
94                .workspace
95                .and_then(|w| w.metadata.typos)
96                .or(config.package.and_then(|p| p.metadata.typos));
97
98            if let Some(typos) = typos {
99                Ok(Some(typos))
100            } else {
101                log::debug!(
102                    "No `package.metadata.typos` section found in `{CARGO_TOML}`, skipping"
103                );
104
105                Ok(None)
106            }
107        } else if path.file_name().unwrap() == PYPROJECT_TOML {
108            let config = toml::from_str::<PyprojectTomlConfig>(&s)?;
109
110            if let Some(typos) = config.tool.typos {
111                Ok(Some(typos))
112            } else {
113                log::debug!("No `tool.typos` section found in `{PYPROJECT_TOML}`, skipping");
114
115                Ok(None)
116            }
117        } else {
118            Self::from_toml(&s).map(Some)
119        }
120    }
121
122    pub fn from_toml(data: &str) -> Result<Self, anyhow::Error> {
123        let content = toml::from_str(data)?;
124        Ok(content)
125    }
126
127    pub fn from_defaults() -> Self {
128        Self {
129            files: Walk::from_defaults(),
130            default: EngineConfig::from_defaults(),
131            type_: TypeEngineConfig::from_defaults(),
132            overrides: EngineConfig::default(),
133        }
134    }
135
136    pub fn update(&mut self, source: &Config) {
137        self.files.update(&source.files);
138        self.default.update(&source.default);
139        self.type_.update(&source.type_);
140        self.overrides.update(&source.overrides);
141    }
142}
143
144#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
145#[serde(deny_unknown_fields)]
146#[serde(default)]
147#[serde(rename_all = "kebab-case")]
148#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
149pub struct Walk {
150    pub extend_exclude: Vec<String>,
151    /// Skip hidden files and directories.
152    pub ignore_hidden: Option<bool>,
153    /// Respect ignore files.
154    pub ignore_files: Option<bool>,
155    /// Respect .ignore files.
156    pub ignore_dot: Option<bool>,
157    /// Respect ignore files in vcs directories.
158    pub ignore_vcs: Option<bool>,
159    /// Respect global ignore files.
160    pub ignore_global: Option<bool>,
161    /// Respect ignore files in parent directories.
162    pub ignore_parent: Option<bool>,
163}
164
165impl Walk {
166    pub fn from_defaults() -> Self {
167        let empty = Self::default();
168        Self {
169            extend_exclude: empty.extend_exclude.clone(),
170            ignore_hidden: Some(empty.ignore_hidden()),
171            ignore_files: Some(true),
172            ignore_dot: Some(empty.ignore_dot()),
173            ignore_vcs: Some(empty.ignore_vcs()),
174            ignore_global: Some(empty.ignore_global()),
175            ignore_parent: Some(empty.ignore_parent()),
176        }
177    }
178
179    pub fn update(&mut self, source: &Walk) {
180        self.extend_exclude
181            .extend(source.extend_exclude.iter().cloned());
182        if let Some(source) = source.ignore_hidden {
183            self.ignore_hidden = Some(source);
184        }
185        if let Some(source) = source.ignore_files {
186            self.ignore_files = Some(source);
187            self.ignore_dot = None;
188            self.ignore_vcs = None;
189            self.ignore_global = None;
190            self.ignore_parent = None;
191        }
192        if let Some(source) = source.ignore_dot {
193            self.ignore_dot = Some(source);
194        }
195        if let Some(source) = source.ignore_vcs {
196            self.ignore_vcs = Some(source);
197            self.ignore_global = None;
198        }
199        if let Some(source) = source.ignore_global {
200            self.ignore_global = Some(source);
201        }
202        if let Some(source) = source.ignore_parent {
203            self.ignore_parent = Some(source);
204        }
205    }
206
207    pub fn extend_exclude(&self) -> &[String] {
208        &self.extend_exclude
209    }
210
211    pub fn ignore_hidden(&self) -> bool {
212        self.ignore_hidden.unwrap_or(true)
213    }
214
215    pub fn ignore_dot(&self) -> bool {
216        self.ignore_dot.or(self.ignore_files).unwrap_or(true)
217    }
218
219    pub fn ignore_vcs(&self) -> bool {
220        self.ignore_vcs.or(self.ignore_files).unwrap_or(true)
221    }
222
223    pub fn ignore_global(&self) -> bool {
224        self.ignore_global
225            .or(self.ignore_vcs)
226            .or(self.ignore_files)
227            .unwrap_or(true)
228    }
229
230    pub fn ignore_parent(&self) -> bool {
231        self.ignore_parent.or(self.ignore_files).unwrap_or(true)
232    }
233}
234
235#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
236#[serde(deny_unknown_fields)]
237#[serde(default)]
238#[serde(transparent)]
239#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
240pub struct TypeEngineConfig {
241    #[cfg_attr(
242        feature = "unstable-schema",
243        schemars(schema_with = "hashmap_string_t::<GlobEngineConfig>")
244    )]
245    pub patterns: HashMap<KString, GlobEngineConfig>,
246}
247
248impl TypeEngineConfig {
249    pub fn from_defaults() -> Self {
250        let mut patterns = HashMap::new();
251
252        for no_check_type in file_type_specifics::NO_CHECK_TYPES {
253            patterns.insert(
254                KString::from(*no_check_type),
255                GlobEngineConfig {
256                    extend_glob: Vec::new(),
257                    engine: EngineConfig {
258                        check_file: Some(false),
259                        ..Default::default()
260                    },
261                },
262            );
263        }
264
265        for (typ, dict_config) in file_type_specifics::TYPE_SPECIFIC_DICTS {
266            patterns.insert(
267                KString::from(*typ),
268                GlobEngineConfig {
269                    extend_glob: Vec::new(),
270                    engine: EngineConfig {
271                        dict: DictConfig {
272                            extend_identifiers: dict_config
273                                .ignore_idents
274                                .iter()
275                                .map(|key| ((*key).into(), (*key).into()))
276                                .collect(),
277                            extend_words: dict_config
278                                .ignore_words
279                                .iter()
280                                .map(|key| ((*key).into(), (*key).into()))
281                                .collect(),
282                            ..Default::default()
283                        },
284                        ..Default::default()
285                    },
286                },
287            );
288        }
289
290        Self { patterns }
291    }
292
293    pub fn update(&mut self, source: &Self) {
294        for (type_name, engine) in source.patterns.iter() {
295            self.patterns
296                .entry(type_name.to_owned())
297                .or_default()
298                .update(engine);
299        }
300    }
301
302    pub fn patterns(&self) -> impl Iterator<Item = (KString, GlobEngineConfig)> {
303        let mut engine = Self::from_defaults();
304        engine.update(self);
305        engine.patterns.into_iter()
306    }
307}
308
309#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
310//#[serde(deny_unknown_fields)]  // Doesn't work with `flatten`
311#[serde(default)]
312#[serde(rename_all = "kebab-case")]
313#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
314pub struct GlobEngineConfig {
315    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
316    pub extend_glob: Vec<KString>,
317    #[serde(flatten)]
318    pub engine: EngineConfig,
319}
320
321impl GlobEngineConfig {
322    pub fn update(&mut self, source: &GlobEngineConfig) {
323        self.extend_glob.extend(source.extend_glob.iter().cloned());
324        self.engine.update(&source.engine);
325    }
326}
327
328#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
329//#[serde(deny_unknown_fields)]  // Doesn't work with `flatten`
330#[serde(default)]
331#[serde(rename_all = "kebab-case")]
332#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
333pub struct EngineConfig {
334    /// Check binary files.
335    pub binary: Option<bool>,
336    /// Verifying spelling in file names.
337    pub check_filename: Option<bool>,
338    /// Verifying spelling in files.
339    pub check_file: Option<bool>,
340    #[serde(flatten)]
341    pub tokenizer: TokenizerConfig,
342    #[serde(flatten)]
343    pub dict: DictConfig,
344    #[serde(with = "serde_regex")]
345    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
346    pub extend_ignore_re: Vec<regex::Regex>,
347}
348
349impl EngineConfig {
350    pub fn from_defaults() -> Self {
351        let empty = Self::default();
352        EngineConfig {
353            binary: Some(empty.binary()),
354            check_filename: Some(empty.check_filename()),
355            check_file: Some(empty.check_file()),
356            tokenizer: TokenizerConfig::from_defaults(),
357            dict: DictConfig::from_defaults(),
358            extend_ignore_re: Default::default(),
359        }
360    }
361
362    pub fn update(&mut self, source: &EngineConfig) {
363        if let Some(source) = source.binary {
364            self.binary = Some(source);
365        }
366        if let Some(source) = source.check_filename {
367            self.check_filename = Some(source);
368        }
369        if let Some(source) = source.check_file {
370            self.check_file = Some(source);
371        }
372        self.tokenizer.update(&source.tokenizer);
373        self.dict.update(&source.dict);
374        self.extend_ignore_re
375            .extend(source.extend_ignore_re.iter().cloned());
376    }
377
378    pub fn binary(&self) -> bool {
379        self.binary.unwrap_or(false)
380    }
381
382    pub fn check_filename(&self) -> bool {
383        self.check_filename.unwrap_or(true)
384    }
385
386    pub fn check_file(&self) -> bool {
387        self.check_file.unwrap_or(true)
388    }
389
390    pub fn extend_ignore_re(&self) -> Box<dyn Iterator<Item = &regex::Regex> + '_> {
391        Box::new(self.extend_ignore_re.iter())
392    }
393}
394
395impl PartialEq for EngineConfig {
396    fn eq(&self, rhs: &Self) -> bool {
397        self.binary == rhs.binary
398            && self.check_filename == rhs.check_filename
399            && self.check_file == rhs.check_file
400            && self.tokenizer == rhs.tokenizer
401            && self.dict == rhs.dict
402            && self
403                .extend_ignore_re
404                .iter()
405                .map(|r| r.as_str())
406                .eq(rhs.extend_ignore_re.iter().map(|r| r.as_str()))
407    }
408}
409
410impl Eq for EngineConfig {}
411
412#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
413#[serde(deny_unknown_fields)]
414#[serde(default)]
415#[serde(rename_all = "kebab-case")]
416#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
417pub struct TokenizerConfig {
418    /// Allow unicode characters in identifiers (and not just ASCII)
419    pub unicode: Option<bool>,
420    /// Do not check identifiers that appear to be hexadecimal values.
421    pub ignore_hex: Option<bool>,
422    /// Allow identifiers to start with digits, in addition to letters.
423    pub identifier_leading_digits: Option<bool>,
424}
425
426impl TokenizerConfig {
427    pub fn from_defaults() -> Self {
428        let empty = Self::default();
429        Self {
430            unicode: Some(empty.unicode()),
431            ignore_hex: Some(empty.ignore_hex()),
432            identifier_leading_digits: Some(empty.identifier_leading_digits()),
433        }
434    }
435
436    pub fn update(&mut self, source: &TokenizerConfig) {
437        if let Some(source) = source.unicode {
438            self.unicode = Some(source);
439        }
440        if let Some(source) = source.ignore_hex {
441            self.ignore_hex = Some(source);
442        }
443        if let Some(source) = source.identifier_leading_digits {
444            self.identifier_leading_digits = Some(source);
445        }
446    }
447
448    pub fn unicode(&self) -> bool {
449        self.unicode.unwrap_or(true)
450    }
451
452    pub fn ignore_hex(&self) -> bool {
453        self.ignore_hex.unwrap_or(true)
454    }
455
456    pub fn identifier_leading_digits(&self) -> bool {
457        self.identifier_leading_digits.unwrap_or(false)
458    }
459}
460
461#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
462#[serde(deny_unknown_fields)]
463#[serde(default)]
464#[serde(rename_all = "kebab-case")]
465#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
466pub struct DictConfig {
467    pub locale: Option<Locale>,
468    #[serde(with = "serde_regex")]
469    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
470    pub extend_ignore_identifiers_re: Vec<regex::Regex>,
471    #[cfg_attr(
472        feature = "unstable-schema",
473        schemars(schema_with = "hashmap_string_string")
474    )]
475    pub extend_identifiers: HashMap<KString, KString>,
476    #[serde(with = "serde_regex")]
477    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
478    pub extend_ignore_words_re: Vec<regex::Regex>,
479    #[cfg_attr(
480        feature = "unstable-schema",
481        schemars(schema_with = "hashmap_string_string")
482    )]
483    pub extend_words: HashMap<KString, KString>,
484}
485
486impl DictConfig {
487    pub fn from_defaults() -> Self {
488        let empty = Self::default();
489        Self {
490            locale: Some(empty.locale()),
491            extend_ignore_identifiers_re: Default::default(),
492            extend_identifiers: Default::default(),
493            extend_ignore_words_re: Default::default(),
494            extend_words: Default::default(),
495        }
496    }
497
498    pub fn update(&mut self, source: &DictConfig) {
499        if let Some(source) = source.locale {
500            self.locale = Some(source);
501        }
502        self.extend_ignore_identifiers_re
503            .extend(source.extend_ignore_identifiers_re.iter().cloned());
504        self.extend_identifiers.extend(
505            source
506                .extend_identifiers
507                .iter()
508                .map(|(key, value)| (key.clone(), value.clone())),
509        );
510        self.extend_ignore_words_re
511            .extend(source.extend_ignore_words_re.iter().cloned());
512        self.extend_words.extend(
513            source
514                .extend_words
515                .iter()
516                .map(|(key, value)| (key.clone(), value.clone())),
517        );
518    }
519
520    pub fn locale(&self) -> Locale {
521        self.locale.unwrap_or_default()
522    }
523
524    pub fn extend_ignore_identifiers_re(&self) -> Box<dyn Iterator<Item = &regex::Regex> + '_> {
525        Box::new(self.extend_ignore_identifiers_re.iter())
526    }
527
528    pub fn extend_identifiers(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
529        Box::new(
530            self.extend_identifiers
531                .iter()
532                .map(|(k, v)| (k.as_str(), v.as_str())),
533        )
534    }
535
536    pub fn extend_ignore_words_re(&self) -> Box<dyn Iterator<Item = &regex::Regex> + '_> {
537        Box::new(self.extend_ignore_words_re.iter())
538    }
539
540    pub fn extend_words(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
541        Box::new(
542            self.extend_words
543                .iter()
544                .map(|(k, v)| (k.as_str(), v.as_str())),
545        )
546    }
547}
548
549fn find_project_files<'a>(
550    dir: &'a std::path::Path,
551    names: &'a [&'a str],
552) -> impl Iterator<Item = std::path::PathBuf> + 'a {
553    names
554        .iter()
555        .map(|name| dir.join(name))
556        .filter(|path| path.exists())
557}
558
559impl PartialEq for DictConfig {
560    fn eq(&self, rhs: &Self) -> bool {
561        self.locale == rhs.locale
562            && self
563                .extend_ignore_identifiers_re
564                .iter()
565                .map(|r| r.as_str())
566                .eq(rhs.extend_ignore_identifiers_re.iter().map(|r| r.as_str()))
567            && self.extend_identifiers == rhs.extend_identifiers
568            && self
569                .extend_ignore_words_re
570                .iter()
571                .map(|r| r.as_str())
572                .eq(rhs.extend_ignore_words_re.iter().map(|r| r.as_str()))
573            && self.extend_words == rhs.extend_words
574    }
575}
576
577impl Eq for DictConfig {}
578
579#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
580#[serde(rename_all = "kebab-case")]
581#[derive(Default)]
582#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
583pub enum Locale {
584    #[default]
585    En,
586    EnUs,
587    EnGb,
588    EnCa,
589    EnAu,
590}
591
592impl Locale {
593    pub const fn category(self) -> Option<varcon_core::Category> {
594        match self {
595            Locale::En => None,
596            Locale::EnUs => Some(varcon_core::Category::American),
597            Locale::EnGb => Some(varcon_core::Category::BritishIse),
598            Locale::EnCa => Some(varcon_core::Category::Canadian),
599            Locale::EnAu => Some(varcon_core::Category::Australian),
600        }
601    }
602
603    pub const fn variants() -> [&'static str; 5] {
604        ["en", "en-us", "en-gb", "en-ca", "en-au"]
605    }
606}
607
608impl std::str::FromStr for Locale {
609    type Err = String;
610
611    fn from_str(s: &str) -> Result<Self, Self::Err> {
612        match s {
613            "en" => Ok(Locale::En),
614            "en-us" => Ok(Locale::EnUs),
615            "en-gb" => Ok(Locale::EnGb),
616            "en-ca" => Ok(Locale::EnCa),
617            "en-au" => Ok(Locale::EnAu),
618            _ => Err("valid values: en, en-us, en-gb, en-ca, en-au".to_owned()),
619        }
620    }
621}
622
623impl std::fmt::Display for Locale {
624    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625        match *self {
626            Locale::En => write!(f, "en"),
627            Locale::EnUs => write!(f, "en-us"),
628            Locale::EnGb => write!(f, "en-gb"),
629            Locale::EnCa => write!(f, "en-ca"),
630            Locale::EnAu => write!(f, "en-au"),
631        }
632    }
633}
634
635#[cfg(feature = "unstable-schema")]
636fn vec_string(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
637    type Type = Vec<String>;
638    <Type as schemars::JsonSchema>::json_schema(gen)
639}
640
641#[cfg(feature = "unstable-schema")]
642fn hashmap_string_string(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
643    type Type = HashMap<String, String>;
644    <Type as schemars::JsonSchema>::json_schema(gen)
645}
646
647#[cfg(feature = "unstable-schema")]
648fn hashmap_string_t<T: schemars::JsonSchema>(
649    gen: &mut schemars::SchemaGenerator,
650) -> schemars::Schema {
651    type Type<T> = HashMap<String, T>;
652    <Type<T> as schemars::JsonSchema>::json_schema(gen)
653}
654
655#[cfg(test)]
656mod test {
657    use super::*;
658
659    #[cfg(feature = "unstable-schema")]
660    #[test]
661    fn dump_schema() {
662        let schema = schemars::schema_for!(Config);
663        let dump = serde_json::to_string_pretty(&schema).unwrap();
664        snapbox::assert_data_eq!(dump, snapbox::file!("../../../config.schema.json").raw());
665    }
666
667    #[test]
668    fn test_from_defaults() {
669        let null = Config::default();
670        let defaulted = Config::from_defaults();
671        assert_ne!(defaulted, null);
672        assert_ne!(defaulted.files, null.files);
673        assert_ne!(defaulted.default, null.default);
674        assert_ne!(defaulted.default.tokenizer, null.default.tokenizer);
675        assert_ne!(defaulted.default.dict, null.default.dict);
676    }
677
678    #[test]
679    fn test_update_from_nothing() {
680        let null = Config::default();
681        let defaulted = Config::from_defaults();
682
683        let mut actual = defaulted.clone();
684        actual.update(&null);
685
686        assert_eq!(actual, defaulted);
687    }
688
689    #[test]
690    fn test_update_from_defaults() {
691        let null = Config::default();
692        let defaulted = Config::from_defaults();
693
694        let mut actual = null;
695        actual.update(&defaulted);
696
697        assert_eq!(actual, defaulted);
698    }
699
700    #[test]
701    fn test_extend_glob_updates() {
702        let null = GlobEngineConfig::default();
703        let extended = GlobEngineConfig {
704            extend_glob: vec!["*.foo".into()],
705            ..Default::default()
706        };
707
708        let mut actual = null;
709        actual.update(&extended);
710
711        assert_eq!(actual, extended);
712    }
713
714    #[test]
715    fn test_extend_glob_extends() {
716        let base = GlobEngineConfig {
717            extend_glob: vec!["*.foo".into()],
718            ..Default::default()
719        };
720        let extended = GlobEngineConfig {
721            extend_glob: vec!["*.bar".into()],
722            ..Default::default()
723        };
724
725        let mut actual = base;
726        actual.update(&extended);
727
728        let expected: Vec<KString> = vec!["*.foo".into(), "*.bar".into()];
729        assert_eq!(actual.extend_glob, expected);
730    }
731
732    #[test]
733    fn parse_extend_globs() {
734        let input = r#"[type.po]
735extend-glob = ["*.po"]
736check-file = true
737"#;
738        let mut expected = Config::default();
739        expected.type_.patterns.insert(
740            "po".into(),
741            GlobEngineConfig {
742                extend_glob: vec!["*.po".into()],
743                engine: EngineConfig {
744                    tokenizer: TokenizerConfig::default(),
745                    dict: DictConfig::default(),
746                    check_file: Some(true),
747                    ..Default::default()
748                },
749            },
750        );
751        let actual = Config::from_toml(input).unwrap();
752        assert_eq!(actual, expected);
753    }
754
755    #[test]
756    fn parse_extend_words() {
757        let input = r#"[type.shaders]
758extend-glob = [
759  '*.shader',
760  '*.cginc',
761]
762
763[type.shaders.extend-words]
764inout = "inout"
765"#;
766        let mut expected = Config::default();
767        expected.type_.patterns.insert(
768            "shaders".into(),
769            GlobEngineConfig {
770                extend_glob: vec!["*.shader".into(), "*.cginc".into()],
771                engine: EngineConfig {
772                    tokenizer: TokenizerConfig::default(),
773                    dict: DictConfig {
774                        extend_words: maplit::hashmap! {
775                            "inout".into() => "inout".into(),
776                        },
777                        ..Default::default()
778                    },
779                    ..Default::default()
780                },
781            },
782        );
783        let actual = Config::from_toml(input).unwrap();
784        assert_eq!(actual, expected);
785    }
786}