Skip to main content

cargo_spellcheck/config/
mod.rs

1//! Configure cargo-spellcheck
2//!
3//! Supports `Hunspell` and `LanguageTool` scopes.
4//!
5//! A default configuration will be generated in the default location by
6//! default. Default. Default default default.
7
8// TODO pendeng refactor, avoid spending time on documenting the status quo.
9#![allow(missing_docs)]
10
11pub mod args;
12
13mod regex;
14pub use self::regex::*;
15
16mod reflow;
17pub use self::reflow::*;
18
19mod hunspell;
20pub use self::hunspell::*;
21
22mod nlprules;
23pub use self::nlprules::*;
24
25mod search_dirs;
26pub use search_dirs::*;
27
28mod iso;
29pub use iso::*;
30
31use crate::errors::*;
32use crate::Detector;
33use fancy_regex::Regex;
34
35use fs_err as fs;
36use serde::{Deserialize, Serialize};
37use std::convert::AsRef;
38use std::fmt;
39use std::io::Read;
40use std::path::{Path, PathBuf};
41
42#[derive(Deserialize, Serialize, Debug, Clone)]
43#[serde(deny_unknown_fields)]
44pub struct Config {
45    // Options that modify the inputs being picked up.
46    #[serde(default)]
47    #[serde(alias = "dev-comments")]
48    #[serde(alias = "devcomments")]
49    pub dev_comments: bool,
50
51    #[serde(default)]
52    #[serde(alias = "skip-readme")]
53    #[serde(alias = "skipreadme")]
54    pub skip_readme: bool,
55
56    #[serde(alias = "Hunspell")]
57    #[serde(default = "default_hunspell")]
58    pub hunspell: Option<HunspellConfig>,
59
60    #[serde(alias = "ZSpell")]
61    #[serde(default = "default_zspell")]
62    pub zet: Option<ZetConfig>,
63
64    #[serde(alias = "Spellbook")]
65    #[serde(alias = "book")]
66    #[serde(default = "default_spellbook")]
67    pub spellbook: Option<SpellbookConfig>,
68
69    #[serde(alias = "Nlp")]
70    #[serde(alias = "NLP")]
71    #[serde(alias = "nlp")]
72    #[serde(alias = "NLP")]
73    #[serde(alias = "NlpRules")]
74    #[serde(default = "default_nlprules")]
75    pub nlprules: Option<NlpRulesConfig>,
76
77    #[serde(alias = "ReFlow")]
78    #[serde(alias = "Reflow")]
79    pub reflow: Option<ReflowConfig>,
80}
81
82impl Config {
83    const QUALIFIER: &'static str = "rs";
84    const ORGANIZATION: &'static str = "fff";
85    const APPLICATION: &'static str = "cargo_spellcheck";
86
87    /// Sanitize all relative paths to absolute paths in relation to `base`.
88    fn sanitize_paths(&mut self, base: &Path) -> Result<()> {
89        if let Some(ref mut hunspell) = self.hunspell {
90            hunspell.sanitize_paths(base)?;
91        }
92        if let Some(ref mut zspell) = self.zet {
93            zspell.sanitize_paths(base)?;
94        }
95        if let Some(ref mut spellbook) = self.spellbook {
96            spellbook.sanitize_paths(base)?;
97        }
98        Ok(())
99    }
100
101    pub fn parse<S: AsRef<str>>(s: S) -> Result<Self> {
102        Ok(toml::from_str(s.as_ref())?)
103    }
104
105    pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Option<Self>> {
106        let (contents, path) = match Self::load_content(path) {
107            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
108                return Ok(None);
109            }
110            Err(e) => bail!(e),
111            Ok(contents) => contents,
112        };
113        Self::parse(&contents)
114            .wrap_err_with(|| {
115                eyre!(
116                    "Syntax of a given config file({}) is broken",
117                    path.display()
118                )
119            })
120            .and_then(|mut cfg| {
121                if let Some(base) = path.parent() {
122                    cfg.sanitize_paths(base)?;
123                }
124                Ok(Some(cfg))
125            })
126    }
127
128    pub fn load_content<P: AsRef<Path>>(path: P) -> std::io::Result<(String, PathBuf)> {
129        let path = path.as_ref().canonicalize()?;
130        let mut file = fs::File::open(&path)?;
131
132        let mut contents = String::with_capacity(1024);
133        file.read_to_string(&mut contents)?;
134        Ok((contents, path))
135    }
136
137    pub fn load() -> Result<Option<Self>> {
138        if let Some(base) = directories::BaseDirs::new() {
139            Self::load_from(
140                base.config_dir()
141                    .join("cargo_spellcheck")
142                    .join("config.toml"),
143            )
144        } else {
145            bail!("No idea where your config directory is located. XDG compliance would be nice.")
146        }
147    }
148
149    pub fn to_toml(&self) -> Result<String> {
150        toml::to_string(self).wrap_err_with(|| eyre!("Failed to convert to toml"))
151    }
152
153    pub fn write_values_to<W: std::io::Write>(&self, mut writer: W) -> Result<Self> {
154        let s = self.to_toml()?;
155        writer.write_all(s.as_bytes())?;
156        Ok(self.clone())
157    }
158
159    pub fn write_values_to_path<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
160        let path = path.as_ref();
161
162        if let Some(path) = path.parent() {
163            fs::create_dir_all(path).wrap_err_with(|| {
164                eyre!("Failed to create config parent dirs {}", path.display())
165            })?;
166        }
167
168        let file = fs::OpenOptions::new()
169            .create(true)
170            .write(true)
171            .truncate(true)
172            .open(path)
173            .wrap_err_with(|| eyre!("Failed to write default values to {}", path.display()))?;
174
175        let writer = std::io::BufWriter::new(file);
176
177        self.write_values_to(writer)
178            .wrap_err_with(|| eyre!("Failed to write default config to {}", path.display()))
179    }
180
181    pub fn write_values_to_default_path(&self) -> Result<Self> {
182        let path = Self::default_path()?;
183        self.write_values_to_path(path)
184    }
185
186    pub fn write_default_values_to<P: AsRef<Path>>(path: P) -> Result<Self> {
187        Self::default().write_values_to_path(path)
188    }
189
190    pub fn default_path() -> Result<PathBuf> {
191        if let Some(base) =
192            directories::ProjectDirs::from(Self::QUALIFIER, Self::ORGANIZATION, Self::APPLICATION)
193        {
194            Ok(base.config_dir().join("config.toml"))
195        } else {
196            bail!("No idea where your config directory is located. `$HOME` must be set.")
197        }
198    }
199
200    /// Obtain a project specific config file.
201    pub fn project_config(manifest_dir: impl AsRef<Path>) -> Result<PathBuf> {
202        let path = manifest_dir
203            .as_ref()
204            .to_owned()
205            .join(".config")
206            .join("spellcheck.toml");
207
208        let path = path.canonicalize()?;
209
210        if path.is_file() {
211            Ok(path)
212        } else {
213            bail!(
214                "Local project dir config {} does not exist or is not a file.",
215                path.display()
216            )
217        }
218    }
219
220    pub fn write_default_values() -> Result<Self> {
221        let d = Self::default_path()?;
222        Self::write_default_values_to(d.join("config.toml"))
223    }
224
225    pub fn is_enabled(&self, detector: Detector) -> bool {
226        match detector {
227            Detector::Hunspell => self.hunspell.is_some(),
228            Detector::ZSpell => self.zet.is_some(),
229            Detector::Spellbook => self.spellbook.is_some(),
230            Detector::NlpRules => self.nlprules.is_some(),
231            Detector::Reflow => self.reflow.is_some(),
232            #[cfg(test)]
233            Detector::Dummy => true,
234        }
235    }
236
237    pub fn full() -> Self {
238        Default::default()
239    }
240}
241
242fn default_nlprules() -> Option<NlpRulesConfig> {
243    if cfg!(feature = "nlprules") {
244        Some(NlpRulesConfig::default())
245    } else {
246        log::warn!("Cannot enable nlprules, since it wasn't compiled with `nlprules` as checker");
247        None
248    }
249}
250
251fn default_hunspell() -> Option<HunspellConfig> {
252    Some(HunspellConfig::default())
253}
254fn default_zspell() -> Option<ZetConfig> {
255    Some(ZetConfig::default())
256}
257fn default_spellbook() -> Option<SpellbookConfig> {
258    Some(SpellbookConfig::default())
259}
260
261impl Default for Config {
262    fn default() -> Self {
263        Self {
264            dev_comments: false,
265            skip_readme: false,
266            hunspell: default_hunspell(),
267            zet: default_zspell(),
268            spellbook: default_spellbook(),
269            nlprules: default_nlprules(),
270            reflow: Some(ReflowConfig::default()),
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use assert_matches::assert_matches;
279
280    #[test]
281    fn can_serialize_to_toml() {
282        let config = dbg!(Config::full());
283        assert_matches!(config.to_toml(), Ok(_s));
284    }
285
286    #[test]
287    fn project_config_works() {
288        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
289            .join(".config")
290            .join("spellcheck.toml");
291        assert_matches!(Config::load_from(&path), Ok(_));
292    }
293
294    #[test]
295    fn all() {
296        let _ = Config::parse(
297            r#"
298dev_comments = true
299skip-readme = true
300
301[Hunspell]
302lang = "en_US"
303search_dirs = ["/usr/lib64/hunspell"]
304extra_dictionaries = ["/home/bernhard/test.dic"]
305			"#,
306        )
307        .unwrap();
308    }
309
310    #[test]
311    fn empty() {
312        assert!(Config::parse(
313            r#"
314			"#,
315        )
316        .is_ok());
317    }
318    #[test]
319    fn partial_1() {
320        let _cfg = Config::parse(
321            r#"
322[hunspell]
323lang = "en_GB"
324search_dirs = ["/usr/lib64/hunspell"]
325extra_dictionaries = ["/home/bernhard/test.dic"]
326			"#,
327        )
328        .unwrap();
329    }
330
331    #[test]
332    fn partial_3() {
333        let cfg = Config::parse(
334            r#"
335[Hunspell]
336lang = "de_AT"
337search_dirs = ["/usr/lib64/hunspell"]
338extra_dictionaries = ["/home/bernhard/test.dic"]
339			"#,
340        )
341        .unwrap();
342        let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
343    }
344
345    #[test]
346    fn partial_4() {
347        let cfg = Config::parse(
348            r#"
349[Hunspell]
350lang = "en_US"
351			"#,
352        )
353        .unwrap();
354        let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
355    }
356
357    #[test]
358    fn partial_5() {
359        assert!(Config::parse(
360            r#"
361[hUNspell]
362lang = "en_US"
363			"#,
364        )
365        .is_err());
366    }
367
368    #[test]
369    fn partial_6() {
370        let cfg = Config::parse(
371            r#"
372[hunspell]
373			"#,
374        )
375        .unwrap();
376        let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
377    }
378
379    #[test]
380    fn partial_7() {
381        let cfg = Config::parse(
382            r#"
383[Hunspell.quirks]
384allow_concatenation = true
385allow_dashes = true
386transform_regex = ["^'([^\\s])'$", "^[0-9]+x$"]
387			"#,
388        )
389        .unwrap();
390        let _hunspell = cfg.hunspell.expect("Must contain hunspell cfg");
391    }
392
393    #[test]
394    fn partial_8() {
395        let cfg = Config::parse(
396            r#"
397[Hunspell]
398search_dirs = ["/search/1", "/search/2"]
399skip_os_lookups = true
400			"#,
401        )
402        .unwrap();
403
404        let hunspell: HunspellConfig = cfg.hunspell.expect("Must contain hunspell cfg");
405        assert!(hunspell.skip_os_lookups);
406
407        let search_dirs = hunspell.search_dirs;
408        let search_dirs2: Vec<_> = search_dirs.as_ref().clone();
409        assert!(!search_dirs2.is_empty());
410
411        assert_eq!(search_dirs.iter(false).count(), 2);
412
413        #[cfg(target_os = "linux")]
414        assert_eq!(search_dirs.iter(true).count(), 5);
415
416        #[cfg(target_os = "windows")]
417        assert_eq!(search_dirs.iter(true).count(), 2);
418
419        #[cfg(target_os = "macos")]
420        assert!(search_dirs.iter(true).count() >= 3);
421    }
422
423    #[test]
424    fn partial_9() {
425        let cfg = Config::parse(
426            r#"
427[Reflow]
428max_line_length = 42
429"#,
430        )
431        .unwrap();
432        assert_eq!(
433            cfg.reflow.expect("Must contain reflow cfg").max_line_length,
434            42
435        );
436    }
437}