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, 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, 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, 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, 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, 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, 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        let config = 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                typos
100            } else {
101                log::debug!(
102                    "No `package.metadata.typos` section found in `{CARGO_TOML}`, skipping"
103                );
104
105                return 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                typos
112            } else {
113                log::debug!("No `tool.typos` section found in `{PYPROJECT_TOML}`, skipping");
114
115                return Ok(None);
116            }
117        } else {
118            Self::from_toml(&s)?
119        };
120        if let Some(key) = config.unused().next() {
121            anyhow::bail!("unknown key `{key}`");
122        }
123
124        Ok(Some(config))
125    }
126
127    pub fn from_toml(data: &str) -> Result<Self, anyhow::Error> {
128        let content = toml::from_str(data)?;
129        Ok(content)
130    }
131
132    pub fn from_defaults() -> Self {
133        Self {
134            files: Walk::from_defaults(),
135            default: EngineConfig::from_defaults(),
136            type_: TypeEngineConfig::from_defaults(),
137            overrides: EngineConfig::default(),
138        }
139    }
140
141    pub fn update(&mut self, source: &Config) {
142        self.files.update(&source.files);
143        self.default.update(&source.default);
144        self.type_.update(&source.type_);
145        self.overrides.update(&source.overrides);
146    }
147
148    fn unused(&self) -> impl Iterator<Item = String> + '_ {
149        self.default
150            ._unused
151            .keys()
152            .map(|k| format!("default.{k}"))
153            .chain(self.type_.patterns.iter().flat_map(|(name, glob)| {
154                glob.engine._unused.keys().map(|k| {
155                    let name = name.clone();
156                    format!("type.{name}.{k}")
157                })
158            }))
159    }
160}
161
162#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
163#[serde(deny_unknown_fields)]
164#[serde(default)]
165#[serde(rename_all = "kebab-case")]
166#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
167pub struct Walk {
168    pub extend_exclude: Vec<String>,
169    /// Skip hidden files and directories.
170    pub ignore_hidden: Option<bool>,
171    /// Respect ignore files.
172    pub ignore_files: Option<bool>,
173    /// Respect .ignore files.
174    pub ignore_dot: Option<bool>,
175    /// Respect ignore files in vcs directories.
176    pub ignore_vcs: Option<bool>,
177    /// Respect global ignore files.
178    pub ignore_global: Option<bool>,
179    /// Respect ignore files in parent directories.
180    pub ignore_parent: Option<bool>,
181}
182
183impl Walk {
184    pub fn from_defaults() -> Self {
185        let empty = Self::default();
186        Self {
187            extend_exclude: empty.extend_exclude.clone(),
188            ignore_hidden: Some(empty.ignore_hidden()),
189            ignore_files: Some(true),
190            ignore_dot: Some(empty.ignore_dot()),
191            ignore_vcs: Some(empty.ignore_vcs()),
192            ignore_global: Some(empty.ignore_global()),
193            ignore_parent: Some(empty.ignore_parent()),
194        }
195    }
196
197    pub fn update(&mut self, source: &Walk) {
198        self.extend_exclude
199            .extend(source.extend_exclude.iter().cloned());
200        if let Some(source) = source.ignore_hidden {
201            self.ignore_hidden = Some(source);
202        }
203        if let Some(source) = source.ignore_files {
204            self.ignore_files = Some(source);
205            self.ignore_dot = None;
206            self.ignore_vcs = None;
207            self.ignore_global = None;
208            self.ignore_parent = None;
209        }
210        if let Some(source) = source.ignore_dot {
211            self.ignore_dot = Some(source);
212        }
213        if let Some(source) = source.ignore_vcs {
214            self.ignore_vcs = Some(source);
215            self.ignore_global = None;
216        }
217        if let Some(source) = source.ignore_global {
218            self.ignore_global = Some(source);
219        }
220        if let Some(source) = source.ignore_parent {
221            self.ignore_parent = Some(source);
222        }
223    }
224
225    pub fn extend_exclude(&self) -> &[String] {
226        &self.extend_exclude
227    }
228
229    pub fn ignore_hidden(&self) -> bool {
230        self.ignore_hidden.unwrap_or(true)
231    }
232
233    pub fn ignore_dot(&self) -> bool {
234        self.ignore_dot.or(self.ignore_files).unwrap_or(true)
235    }
236
237    pub fn ignore_vcs(&self) -> bool {
238        self.ignore_vcs.or(self.ignore_files).unwrap_or(true)
239    }
240
241    pub fn ignore_global(&self) -> bool {
242        self.ignore_global
243            .or(self.ignore_vcs)
244            .or(self.ignore_files)
245            .unwrap_or(true)
246    }
247
248    pub fn ignore_parent(&self) -> bool {
249        self.ignore_parent.or(self.ignore_files).unwrap_or(true)
250    }
251}
252
253#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
254#[serde(deny_unknown_fields)]
255#[serde(default)]
256#[serde(transparent)]
257#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
258pub struct TypeEngineConfig {
259    #[cfg_attr(
260        feature = "unstable-schema",
261        schemars(schema_with = "hashmap_string_t::<GlobEngineConfig>")
262    )]
263    pub patterns: HashMap<KString, GlobEngineConfig>,
264}
265
266impl TypeEngineConfig {
267    pub fn from_defaults() -> Self {
268        let mut patterns = HashMap::new();
269
270        for no_check_type in file_type_specifics::NO_CHECK_TYPES {
271            patterns.insert(
272                KString::from(*no_check_type),
273                GlobEngineConfig {
274                    extend_glob: Vec::new(),
275                    engine: EngineConfig {
276                        check_file: Some(false),
277                        ..Default::default()
278                    },
279                },
280            );
281        }
282
283        for (typ, dict_config) in file_type_specifics::TYPE_SPECIFIC_DICTS {
284            patterns.insert(
285                KString::from(*typ),
286                GlobEngineConfig {
287                    extend_glob: Vec::new(),
288                    engine: EngineConfig {
289                        dict: DictConfig {
290                            extend_identifiers: dict_config
291                                .ignore_idents
292                                .iter()
293                                .map(|key| ((*key).into(), (*key).into()))
294                                .collect(),
295                            extend_words: dict_config
296                                .ignore_words
297                                .iter()
298                                .map(|key| ((*key).into(), (*key).into()))
299                                .collect(),
300                            ..Default::default()
301                        },
302                        ..Default::default()
303                    },
304                },
305            );
306        }
307
308        Self { patterns }
309    }
310
311    pub fn update(&mut self, source: &Self) {
312        for (type_name, engine) in source.patterns.iter() {
313            self.patterns
314                .entry(type_name.to_owned())
315                .or_default()
316                .update(engine);
317        }
318    }
319
320    pub fn patterns(&self) -> impl Iterator<Item = (KString, GlobEngineConfig)> + use<> {
321        let mut engine = Self::from_defaults();
322        engine.update(self);
323        engine.patterns.into_iter()
324    }
325}
326
327#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
328//#[serde(deny_unknown_fields)]  // Doesn't work with `flatten`
329#[serde(default)]
330#[serde(rename_all = "kebab-case")]
331#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
332pub struct GlobEngineConfig {
333    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
334    pub extend_glob: Vec<KString>,
335    #[serde(flatten)]
336    pub engine: EngineConfig,
337}
338
339impl GlobEngineConfig {
340    pub fn update(&mut self, source: &GlobEngineConfig) {
341        self.extend_glob.extend(source.extend_glob.iter().cloned());
342        self.engine.update(&source.engine);
343    }
344}
345
346#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
347//#[serde(deny_unknown_fields)]  // Doesn't work with `flatten`
348#[serde(default)]
349#[serde(rename_all = "kebab-case")]
350#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
351pub struct EngineConfig {
352    /// Check binary files.
353    pub binary: Option<bool>,
354    /// Verifying spelling in file names.
355    pub check_filename: Option<bool>,
356    /// Verifying spelling in files.
357    pub check_file: Option<bool>,
358    #[serde(flatten)]
359    pub tokenizer: TokenizerConfig,
360    #[serde(flatten)]
361    pub dict: DictConfig,
362    #[serde(with = "serde_regex")]
363    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
364    pub extend_ignore_re: Vec<regex::Regex>,
365    #[serde(flatten)]
366    #[cfg_attr(feature = "unstable-schema", schemars(skip))]
367    pub _unused: toml::Table,
368}
369
370impl EngineConfig {
371    pub fn from_defaults() -> Self {
372        let empty = Self::default();
373        EngineConfig {
374            binary: Some(empty.binary()),
375            check_filename: Some(empty.check_filename()),
376            check_file: Some(empty.check_file()),
377            tokenizer: TokenizerConfig::from_defaults(),
378            dict: DictConfig::from_defaults(),
379            extend_ignore_re: Default::default(),
380            _unused: Default::default(),
381        }
382    }
383
384    pub fn update(&mut self, source: &EngineConfig) {
385        if let Some(source) = source.binary {
386            self.binary = Some(source);
387        }
388        if let Some(source) = source.check_filename {
389            self.check_filename = Some(source);
390        }
391        if let Some(source) = source.check_file {
392            self.check_file = Some(source);
393        }
394        self.tokenizer.update(&source.tokenizer);
395        self.dict.update(&source.dict);
396        self.extend_ignore_re
397            .extend(source.extend_ignore_re.iter().cloned());
398        self._unused.extend(source._unused.clone());
399    }
400
401    pub fn binary(&self) -> bool {
402        self.binary.unwrap_or(false)
403    }
404
405    pub fn check_filename(&self) -> bool {
406        self.check_filename.unwrap_or(true)
407    }
408
409    pub fn check_file(&self) -> bool {
410        self.check_file.unwrap_or(true)
411    }
412
413    pub fn extend_ignore_re(&self) -> Box<dyn Iterator<Item = &regex::Regex> + '_> {
414        Box::new(self.extend_ignore_re.iter())
415    }
416}
417
418#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
419#[serde(deny_unknown_fields)]
420#[serde(default)]
421#[serde(rename_all = "kebab-case")]
422#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
423pub struct TokenizerConfig {
424    /// Allow unicode characters in identifiers (and not just ASCII)
425    pub unicode: Option<bool>,
426    /// Do not check identifiers that appear to be hexadecimal values.
427    pub ignore_hex: Option<bool>,
428    /// Allow identifiers to start with digits, in addition to letters.
429    pub identifier_leading_digits: Option<bool>,
430}
431
432impl TokenizerConfig {
433    pub fn from_defaults() -> Self {
434        let empty = Self::default();
435        Self {
436            unicode: Some(empty.unicode()),
437            ignore_hex: Some(empty.ignore_hex()),
438            identifier_leading_digits: Some(empty.identifier_leading_digits()),
439        }
440    }
441
442    pub fn update(&mut self, source: &TokenizerConfig) {
443        if let Some(source) = source.unicode {
444            self.unicode = Some(source);
445        }
446        if let Some(source) = source.ignore_hex {
447            self.ignore_hex = Some(source);
448        }
449        if let Some(source) = source.identifier_leading_digits {
450            self.identifier_leading_digits = Some(source);
451        }
452    }
453
454    pub fn unicode(&self) -> bool {
455        self.unicode.unwrap_or(true)
456    }
457
458    pub fn ignore_hex(&self) -> bool {
459        self.ignore_hex.unwrap_or(true)
460    }
461
462    pub fn identifier_leading_digits(&self) -> bool {
463        self.identifier_leading_digits.unwrap_or(false)
464    }
465}
466
467#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
468#[serde(deny_unknown_fields)]
469#[serde(default)]
470#[serde(rename_all = "kebab-case")]
471#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
472pub struct DictConfig {
473    pub locale: Option<Locale>,
474    #[serde(with = "serde_regex")]
475    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
476    pub extend_ignore_identifiers_re: Vec<regex::Regex>,
477    #[cfg_attr(
478        feature = "unstable-schema",
479        schemars(schema_with = "hashmap_string_string")
480    )]
481    pub extend_identifiers: HashMap<KString, KString>,
482    #[serde(with = "serde_regex")]
483    #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))]
484    pub extend_ignore_words_re: Vec<regex::Regex>,
485    #[cfg_attr(
486        feature = "unstable-schema",
487        schemars(schema_with = "hashmap_string_string")
488    )]
489    pub extend_words: HashMap<KString, KString>,
490}
491
492impl DictConfig {
493    pub fn from_defaults() -> Self {
494        let empty = Self::default();
495        Self {
496            locale: Some(empty.locale()),
497            extend_ignore_identifiers_re: Default::default(),
498            extend_identifiers: Default::default(),
499            extend_ignore_words_re: Default::default(),
500            extend_words: Default::default(),
501        }
502    }
503
504    pub fn update(&mut self, source: &DictConfig) {
505        if let Some(source) = source.locale {
506            self.locale = Some(source);
507        }
508        self.extend_ignore_identifiers_re
509            .extend(source.extend_ignore_identifiers_re.iter().cloned());
510        self.extend_identifiers.extend(
511            source
512                .extend_identifiers
513                .iter()
514                .map(|(key, value)| (key.clone(), value.clone())),
515        );
516        self.extend_ignore_words_re
517            .extend(source.extend_ignore_words_re.iter().cloned());
518        self.extend_words.extend(
519            source
520                .extend_words
521                .iter()
522                .map(|(key, value)| (key.clone(), value.clone())),
523        );
524    }
525
526    pub fn locale(&self) -> Locale {
527        self.locale.unwrap_or_default()
528    }
529
530    pub fn extend_ignore_identifiers_re(&self) -> Box<dyn Iterator<Item = &regex::Regex> + '_> {
531        Box::new(self.extend_ignore_identifiers_re.iter())
532    }
533
534    pub fn extend_identifiers(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
535        Box::new(
536            self.extend_identifiers
537                .iter()
538                .map(|(k, v)| (k.as_str(), v.as_str())),
539        )
540    }
541
542    pub fn extend_ignore_words_re(&self) -> Box<dyn Iterator<Item = &regex::Regex> + '_> {
543        Box::new(self.extend_ignore_words_re.iter())
544    }
545
546    pub fn extend_words(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
547        Box::new(
548            self.extend_words
549                .iter()
550                .map(|(k, v)| (k.as_str(), v.as_str())),
551        )
552    }
553}
554
555fn find_project_files<'a>(
556    dir: &'a std::path::Path,
557    names: &'a [&'a str],
558) -> impl Iterator<Item = std::path::PathBuf> + 'a {
559    names
560        .iter()
561        .map(|name| dir.join(name))
562        .filter(|path| path.exists())
563}
564
565#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
566#[serde(rename_all = "kebab-case")]
567#[derive(Default)]
568#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
569pub enum Locale {
570    #[default]
571    En,
572    EnUs,
573    EnGb,
574    EnCa,
575    EnAu,
576}
577
578impl Locale {
579    pub const fn category(self) -> Option<varcon_core::Category> {
580        match self {
581            Locale::En => None,
582            Locale::EnUs => Some(varcon_core::Category::American),
583            Locale::EnGb => Some(varcon_core::Category::BritishIse),
584            Locale::EnCa => Some(varcon_core::Category::Canadian),
585            Locale::EnAu => Some(varcon_core::Category::Australian),
586        }
587    }
588
589    pub const fn variants() -> [&'static str; 5] {
590        ["en", "en-us", "en-gb", "en-ca", "en-au"]
591    }
592}
593
594impl std::str::FromStr for Locale {
595    type Err = String;
596
597    fn from_str(s: &str) -> Result<Self, Self::Err> {
598        match s {
599            "en" => Ok(Locale::En),
600            "en-us" => Ok(Locale::EnUs),
601            "en-gb" => Ok(Locale::EnGb),
602            "en-ca" => Ok(Locale::EnCa),
603            "en-au" => Ok(Locale::EnAu),
604            _ => Err("valid values: en, en-us, en-gb, en-ca, en-au".to_owned()),
605        }
606    }
607}
608
609impl std::fmt::Display for Locale {
610    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611        match *self {
612            Locale::En => write!(f, "en"),
613            Locale::EnUs => write!(f, "en-us"),
614            Locale::EnGb => write!(f, "en-gb"),
615            Locale::EnCa => write!(f, "en-ca"),
616            Locale::EnAu => write!(f, "en-au"),
617        }
618    }
619}
620
621#[cfg(feature = "unstable-schema")]
622fn vec_string(r#gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
623    type Type = Vec<String>;
624    <Type as schemars::JsonSchema>::json_schema(r#gen)
625}
626
627#[cfg(feature = "unstable-schema")]
628fn hashmap_string_string(r#gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
629    type Type = HashMap<String, String>;
630    <Type as schemars::JsonSchema>::json_schema(r#gen)
631}
632
633#[cfg(feature = "unstable-schema")]
634fn hashmap_string_t<T: schemars::JsonSchema>(
635    r#gen: &mut schemars::SchemaGenerator,
636) -> schemars::Schema {
637    type Type<T> = HashMap<String, T>;
638    <Type<T> as schemars::JsonSchema>::json_schema(r#gen)
639}
640
641#[cfg(test)]
642mod test {
643    use super::*;
644    use snapbox::assert_data_eq;
645    use snapbox::prelude::*;
646
647    #[cfg(feature = "unstable-schema")]
648    #[test]
649    fn dump_schema() {
650        let schema = schemars::schema_for!(Config);
651        let dump = serde_json::to_string_pretty(&schema).unwrap();
652        snapbox::assert_data_eq!(dump, snapbox::file!("../../../config.schema.json").raw());
653    }
654
655    #[test]
656    fn test_from_defaults() {
657        let null = Config::default();
658        let defaulted = Config::from_defaults();
659        assert_ne!(defaulted.clone().into_json(), null.clone().into_json());
660        assert_ne!(
661            defaulted.files.clone().into_json(),
662            null.files.clone().into_json()
663        );
664        assert_ne!(
665            defaulted.default.clone().into_json(),
666            null.default.clone().into_json()
667        );
668        assert_ne!(
669            defaulted.default.tokenizer.clone().into_json(),
670            null.default.tokenizer.clone().into_json()
671        );
672        assert_ne!(
673            defaulted.default.dict.clone().into_json(),
674            null.default.dict.clone().into_json()
675        );
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_data_eq!(actual.into_json(), defaulted.into_json());
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_data_eq!(actual.into_json(), defaulted.into_json());
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_data_eq!(actual.into_json(), extended.into_json());
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_data_eq!(actual.extend_glob.into_json(), expected.into_json());
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_data_eq!(actual.into_json(), expected.into_json());
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_data_eq!(actual.into_json(), expected.into_json());
785    }
786}