Skip to main content

skif_cli/
error.rs

1//! The ways a run can fail to happen at all (exit code 2), with the `kind` JSON mode reports.
2
3use std::fmt;
4use std::path::Path;
5
6use skif::{ConfigError, LintError, WorkspaceError, slash};
7
8/// Machine-readable category of a failure.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Kind {
11    Config,
12    Workspace,
13    Io,
14    Usage,
15    CiRefusal,
16    /// A bug in skif: a failed rule provider, a thread pool that cannot be built, a panic.
17    Internal,
18}
19
20impl Kind {
21    #[must_use]
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::Config => "config",
25            Self::Workspace => "workspace",
26            Self::Io => "io",
27            Self::Usage => "usage",
28            Self::CiRefusal => "ci-refusal",
29            Self::Internal => "internal",
30        }
31    }
32}
33
34/// A failure that stops skif from checking the repository.
35#[derive(Debug)]
36pub struct CliError {
37    pub kind: Kind,
38    pub message: String,
39    pub path: Option<String>,
40    pub line: Option<u32>,
41    pub column: Option<u32>,
42}
43
44impl CliError {
45    pub fn new(kind: Kind, message: impl Into<String>) -> Self {
46        Self {
47            kind,
48            message: slashes(message.into()),
49            path: None,
50            line: None,
51            column: None,
52        }
53    }
54
55    #[must_use]
56    pub fn io(context: &str, path: &Path, error: &std::io::Error) -> Self {
57        Self {
58            path: Some(slash(path)),
59            ..Self::new(Kind::Io, format!("{context} {}: {error}", path.display()))
60        }
61    }
62
63    #[must_use]
64    pub fn ci_refusal() -> Self {
65        Self::new(
66            Kind::CiRefusal,
67            "refusing to fix: the CI environment variable is set; run --fix locally or unset CI",
68        )
69    }
70}
71
72impl fmt::Display for CliError {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(&self.message)
75    }
76}
77
78/// Native separators rewritten to `/` on Windows, so messages and snapshots read the same on every platform.
79pub(crate) fn slashes(text: String) -> String {
80    if cfg!(windows) { text.replace('\\', "/") } else { text }
81}
82
83fn position(value: Option<usize>) -> Option<u32> {
84    value.and_then(|value| u32::try_from(value).ok())
85}
86
87impl From<ConfigError> for CliError {
88    fn from(error: ConfigError) -> Self {
89        let mut cli = Self::new(Kind::Config, error.to_string());
90        match &error {
91            ConfigError::Parse { path, line, column, .. } => {
92                cli.path = path.as_deref().map(slash);
93                cli.line = position(*line);
94                cli.column = position(*column);
95            }
96            ConfigError::Invalid { path, .. } => cli.path = path.as_deref().map(slash),
97            ConfigError::Io { path, .. } => {
98                cli.kind = Kind::Io;
99                cli.path = Some(slash(path));
100            }
101            ConfigError::Ambiguous { first, .. } => cli.path = Some(slash(first)),
102            _ => {}
103        }
104        cli
105    }
106}
107
108impl From<WorkspaceError> for CliError {
109    fn from(error: WorkspaceError) -> Self {
110        let mut cli = Self::new(Kind::Workspace, error.to_string());
111        match &error {
112            WorkspaceError::InvalidManifest { path, line, column, .. } => {
113                cli.path = Some(slash(path));
114                cli.line = Some(*line);
115                cli.column = Some(*column);
116            }
117            WorkspaceError::InvalidPnpmWorkspace { path, .. } => cli.path = Some(slash(path)),
118            WorkspaceError::Io { path, .. } => {
119                cli.kind = Kind::Io;
120                cli.path = Some(slash(path));
121            }
122            _ => {}
123        }
124        cli
125    }
126}
127
128impl From<skif::Error> for CliError {
129    fn from(error: skif::Error) -> Self {
130        let message = error.to_string();
131        match error {
132            skif::Error::Config(error) | skif::Error::Lint(LintError::Config(error)) => error.into(),
133            skif::Error::Workspace(error) => error.into(),
134            skif::Error::NoWorkspace(_) => Self::new(Kind::Workspace, message),
135            skif::Error::UnknownRule(_) => Self::new(Kind::Config, message),
136            skif::Error::RelativePath(_) => Self::new(Kind::Usage, message),
137            skif::Error::Registry(_)
138            | skif::Error::ThreadPool(_)
139            | skif::Error::Lint(LintError::Provider { .. } | LintError::Cancelled) => {
140                Self::new(Kind::Internal, message)
141            }
142            _ => Self::new(Kind::Io, message),
143        }
144    }
145}
146
147impl From<skif::FixError> for CliError {
148    fn from(error: skif::FixError) -> Self {
149        Self::new(Kind::Io, error.to_string())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use std::path::PathBuf;
156
157    use super::*;
158
159    #[test]
160    fn kinds_have_stable_names() {
161        assert_eq!(Kind::Internal.as_str(), "internal");
162        assert_eq!(Kind::Usage.as_str(), "usage");
163    }
164
165    #[test]
166    fn a_relative_path_is_a_usage_error() {
167        let error = CliError::from(skif::Error::RelativePath(PathBuf::from("relative")));
168        assert_eq!(error.kind, Kind::Usage);
169    }
170
171    #[test]
172    fn failures_of_the_engine_are_internal_errors() {
173        assert_eq!(
174            CliError::from(skif::Error::ThreadPool("no threads".to_owned())).kind,
175            Kind::Internal
176        );
177        assert_eq!(
178            CliError::from(skif::Error::Lint(LintError::Cancelled)).kind,
179            Kind::Internal
180        );
181        let duplicate = skif::RegistryError::DuplicateRule { id: "twice".to_owned() };
182        assert_eq!(CliError::from(skif::Error::Registry(duplicate)).kind, Kind::Internal);
183    }
184
185    #[test]
186    fn missing_workspaces_and_unknown_rules_keep_their_kinds() {
187        assert_eq!(
188            CliError::from(skif::Error::NoWorkspace(PathBuf::from("/repo"))).kind,
189            Kind::Workspace
190        );
191        assert_eq!(
192            CliError::from(skif::Error::UnknownRule("x".to_owned())).kind,
193            Kind::Config
194        );
195    }
196}