Skip to main content

cargo_spellcheck/
lib.rs

1#![deny(dead_code)]
2#![deny(missing_docs)]
3// #![deny(unused_crate_dependencies)]
4#![allow(clippy::non_ascii_literal)]
5// be explicit about certain offsets and how they are constructed
6#![allow(clippy::identity_op)]
7// in small cli projects, this is ok for now
8#![allow(clippy::wildcard_imports)]
9// personal strong preference for `from_iter`
10#![allow(clippy::from_iter_instead_of_collect)]
11#![allow(clippy::new_without_default)]
12#![allow(clippy::items_after_statements)]
13// Prevent the stray dbg! macros
14#![cfg_attr(not(test), deny(clippy::dbg_macro))]
15#![cfg_attr(test, allow(clippy::dbg_macro))]
16
17//! cargo-spellcheck
18//!
19//! A syntax tree based doc comment and common mark spell checker.
20
21pub use doc_chunks as documentation;
22#[cfg(test)]
23pub(crate) use doc_chunks::{chyrp_up, fluff_up};
24
25pub mod action;
26mod checker;
27mod config;
28pub mod errors;
29mod reflow;
30mod suggestion;
31mod tinhat;
32mod traverse;
33
34pub use self::action::*;
35pub use self::config::args::*;
36pub use self::config::{Config, HunspellConfig, LanguageToolConfig};
37pub use self::documentation::span::*;
38pub use self::documentation::util::*;
39pub use self::documentation::{
40    util, CheckableChunk, Clusters, CommentVariant, CommentVariantCategory, ContentOrigin,
41    Documentation, PlainOverlay, Range,
42};
43pub use self::suggestion::*;
44pub use self::tinhat::*;
45
46use self::errors::{bail, Result};
47
48use std::io::Write;
49
50#[cfg(target_os = "windows")]
51use signal_hook as _;
52
53use checker::Checker;
54
55/// A simple exit code representation.
56///
57/// `Custom` can be specified by the user, others map to their UNIX equivalents
58/// where available.
59#[derive(Debug, Clone, Copy, Eq, PartialEq)]
60pub enum ExitCode {
61    /// Regular termination and does not imply anything in regards to spelling
62    /// mistakes found or not.
63    Success,
64    /// Terminate requested by a *nix signal.
65    Signal,
66    /// A custom exit code, as specified with `--code=<code>`.
67    Custom(u8),
68    // Failure is already default for `Err(_)`
69}
70
71impl ExitCode {
72    /// Convert `ExitCode` to primitive.
73    pub fn as_u8(&self) -> u8 {
74        match *self {
75            Self::Success => 0u8,
76            Self::Signal => 130u8,
77            Self::Custom(code) => code,
78        }
79    }
80}
81
82/// The inner main.
83pub fn run(args: Args) -> Result<ExitCode> {
84    let _ = ::rayon::ThreadPoolBuilder::new()
85        .num_threads(args.job_count())
86        .build_global();
87
88    env_logger::Builder::from_env(env_logger::Env::new().filter_or("CARGO_SPELLCHECK", "warn"))
89        .filter_level(args.verbosity())
90        .filter_module("nlprule", log::LevelFilter::Error)
91        .filter_module("mio", log::LevelFilter::Error)
92        .init();
93
94    #[cfg(not(target_os = "windows"))]
95    signal_handler(move || {
96        if let Err(e) = action::interactive::ScopedRaw::restore_terminal() {
97            log::warn!("Failed to restore terminal: {e}");
98        }
99    });
100
101    let (unified, config) = match &args.command {
102        Some(Sub::Completions { shell }) => {
103            let sink = &mut std::io::stdout();
104            generate_completions(*shell, sink);
105            let _ = sink.flush();
106            return Ok(ExitCode::Success);
107        }
108        _ => args.unified()?,
109    };
110
111    match unified {
112        // must unify first, for the proper paths
113        UnifiedArgs::Config {
114            dest_config,
115            checker_filter_set,
116        } => {
117            log::trace!("Configuration chore");
118            let mut config = Config::full();
119            Args::checker_selection_override(
120                checker_filter_set.as_ref().map(AsRef::as_ref),
121                &mut config,
122            )?;
123
124            match dest_config {
125                ConfigWriteDestination::Stdout => {
126                    println!("{}", config.to_toml()?);
127                    return Ok(ExitCode::Success);
128                }
129                ConfigWriteDestination::File { overwrite, path } => {
130                    if path.exists() && !overwrite {
131                        bail!(
132                            "Attempting to overwrite {} requires `--force`.",
133                            path.display()
134                        );
135                    }
136
137                    log::info!("Writing configuration file to {}", path.display());
138                    config.write_values_to_path(path)?;
139                }
140            }
141            Ok(ExitCode::Success)
142        }
143        UnifiedArgs::Operate {
144            action,
145            paths,
146            recursive,
147            skip_readme,
148            config_path,
149            dev_comments,
150            exit_code_override,
151        } => {
152            log::debug!("Executing: {action:?} with {config:?} from {config_path:?}");
153
154            let documents =
155                traverse::extract(paths, recursive, skip_readme, dev_comments, &config)?;
156
157            let rt = tokio::runtime::Runtime::new()?;
158            let finish = rt.block_on(async move { action.run(documents, config).await })?;
159
160            match finish {
161                Finish::Success | Finish::MistakeCount(0) => Ok(ExitCode::Success),
162                Finish::MistakeCount(_n) => Ok(ExitCode::Custom(exit_code_override)),
163                Finish::Abort => Ok(ExitCode::Signal),
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests;