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, 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 pub ignore_hidden: Option<bool>,
171 pub ignore_files: Option<bool>,
173 pub ignore_dot: Option<bool>,
175 pub ignore_vcs: Option<bool>,
177 pub ignore_global: Option<bool>,
179 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(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(default)]
349#[serde(rename_all = "kebab-case")]
350#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
351pub struct EngineConfig {
352 pub binary: Option<bool>,
354 pub check_filename: Option<bool>,
356 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 = ®ex::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 pub unicode: Option<bool>,
426 pub ignore_hex: Option<bool>,
428 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 = ®ex::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 = ®ex::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}