1use std::fmt;
4use std::path::Path;
5
6use skif::{ConfigError, LintError, WorkspaceError, slash};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Kind {
11 Config,
12 Workspace,
13 Io,
14 Usage,
15 CiRefusal,
16 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#[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
78pub(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}