1#![allow(unused_qualifications)] use 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 pub ignore_hidden: Option<bool>,
153 pub ignore_files: Option<bool>,
155 pub ignore_dot: Option<bool>,
157 pub ignore_vcs: Option<bool>,
159 pub ignore_global: Option<bool>,
161 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(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(default)]
331#[serde(rename_all = "kebab-case")]
332#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
333pub struct EngineConfig {
334 pub binary: Option<bool>,
336 pub check_filename: Option<bool>,
338 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 = ®ex::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 pub unicode: Option<bool>,
420 pub ignore_hex: Option<bool>,
422 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 = ®ex::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 = ®ex::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::gen::SchemaGenerator) -> schemars::schema::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::gen::SchemaGenerator) -> schemars::schema::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::gen::SchemaGenerator,
650) -> schemars::schema::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}