Skip to main content

pub_just/
error.rs

1use super::*;
2
3#[derive(Debug)]
4pub enum Error<'src> {
5  AmbiguousModuleFile {
6    module: Name<'src>,
7    found: Vec<PathBuf>,
8  },
9  ArgumentCountMismatch {
10    recipe: &'src str,
11    parameters: Vec<Parameter<'src>>,
12    found: usize,
13    min: usize,
14    max: usize,
15  },
16  Assert {
17    message: String,
18  },
19  Backtick {
20    token: Token<'src>,
21    output_error: OutputError,
22  },
23  RuntimeDirIo {
24    io_error: io::Error,
25    path: PathBuf,
26  },
27  ChooserInvoke {
28    shell_binary: String,
29    shell_arguments: String,
30    chooser: OsString,
31    io_error: io::Error,
32  },
33  ChooserRead {
34    chooser: OsString,
35    io_error: io::Error,
36  },
37  ChooserStatus {
38    chooser: OsString,
39    status: ExitStatus,
40  },
41  ChooserWrite {
42    chooser: OsString,
43    io_error: io::Error,
44  },
45  CircularImport {
46    current: PathBuf,
47    import: PathBuf,
48  },
49  Code {
50    recipe: &'src str,
51    line_number: Option<usize>,
52    code: i32,
53    print_message: bool,
54  },
55  CommandInvoke {
56    binary: OsString,
57    arguments: Vec<OsString>,
58    io_error: io::Error,
59  },
60  CommandStatus {
61    binary: OsString,
62    arguments: Vec<OsString>,
63    status: ExitStatus,
64  },
65  Compile {
66    compile_error: CompileError<'src>,
67  },
68  Config {
69    config_error: ConfigError,
70  },
71  Cygpath {
72    recipe: &'src str,
73    output_error: OutputError,
74  },
75  DefaultRecipeRequiresArguments {
76    recipe: &'src str,
77    min_arguments: usize,
78  },
79  Dotenv {
80    dotenv_error: dotenvy::Error,
81  },
82  DotenvRequired,
83  DumpJson {
84    serde_json_error: serde_json::Error,
85  },
86  EditorInvoke {
87    editor: OsString,
88    io_error: io::Error,
89  },
90  EditorStatus {
91    editor: OsString,
92    status: ExitStatus,
93  },
94  EvalUnknownVariable {
95    variable: String,
96    suggestion: Option<Suggestion<'src>>,
97  },
98  ExcessInvocations {
99    invocations: usize,
100  },
101  ExpectedSubmoduleButFoundRecipe {
102    path: String,
103  },
104  FormatCheckFoundDiff,
105  FunctionCall {
106    function: Name<'src>,
107    message: String,
108  },
109  GetConfirmation {
110    io_error: io::Error,
111  },
112  Homedir,
113  InitExists {
114    justfile: PathBuf,
115  },
116  Internal {
117    message: String,
118  },
119  Io {
120    recipe: &'src str,
121    io_error: io::Error,
122  },
123  Load {
124    path: PathBuf,
125    io_error: io::Error,
126  },
127  MissingImportFile {
128    path: Token<'src>,
129  },
130  MissingModuleFile {
131    module: Name<'src>,
132  },
133  NoChoosableRecipes,
134  NoDefaultRecipe,
135  NoRecipes,
136  NotConfirmed {
137    recipe: &'src str,
138  },
139  RegexCompile {
140    source: regex::Error,
141  },
142  Script {
143    command: String,
144    io_error: io::Error,
145    recipe: &'src str,
146  },
147  Search {
148    search_error: SearchError,
149  },
150  Shebang {
151    argument: Option<String>,
152    command: String,
153    io_error: io::Error,
154    recipe: &'src str,
155  },
156  Signal {
157    recipe: &'src str,
158    line_number: Option<usize>,
159    signal: i32,
160  },
161  StdoutIo {
162    io_error: io::Error,
163  },
164  TempdirIo {
165    recipe: &'src str,
166    io_error: io::Error,
167  },
168  TempfileIo {
169    io_error: io::Error,
170  },
171  Unknown {
172    recipe: &'src str,
173    line_number: Option<usize>,
174  },
175  UnknownSubmodule {
176    path: String,
177  },
178  UnknownOverrides {
179    overrides: Vec<String>,
180  },
181  UnknownRecipe {
182    recipe: String,
183    suggestion: Option<Suggestion<'src>>,
184  },
185  UnstableFeature {
186    unstable_feature: UnstableFeature,
187  },
188  WriteJustfile {
189    justfile: PathBuf,
190    io_error: io::Error,
191  },
192}
193
194impl<'src> Error<'src> {
195  pub fn code(&self) -> Option<i32> {
196    match self {
197      Self::Code { code, .. }
198      | Self::Backtick {
199        output_error: OutputError::Code(code),
200        ..
201      } => Some(*code),
202      Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(),
203      _ => None,
204    }
205  }
206
207  fn context(&self) -> Option<Token<'src>> {
208    match self {
209      Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {
210        Some(module.token)
211      }
212      Self::Backtick { token, .. } => Some(*token),
213      Self::Compile { compile_error } => Some(compile_error.context()),
214      Self::FunctionCall { function, .. } => Some(function.token),
215      Self::MissingImportFile { path } => Some(*path),
216      _ => None,
217    }
218  }
219
220  pub fn internal(message: impl Into<String>) -> Self {
221    Self::Internal {
222      message: message.into(),
223    }
224  }
225
226  pub fn print_message(&self) -> bool {
227    !matches!(
228      self,
229      Error::Code {
230        print_message: false,
231        ..
232      }
233    )
234  }
235}
236
237impl<'src> From<CompileError<'src>> for Error<'src> {
238  fn from(compile_error: CompileError<'src>) -> Self {
239    Self::Compile { compile_error }
240  }
241}
242
243impl<'src> From<ConfigError> for Error<'src> {
244  fn from(config_error: ConfigError) -> Self {
245    Self::Config { config_error }
246  }
247}
248
249impl<'src> From<dotenvy::Error> for Error<'src> {
250  fn from(dotenv_error: dotenvy::Error) -> Error<'src> {
251    Self::Dotenv { dotenv_error }
252  }
253}
254
255impl<'src> From<SearchError> for Error<'src> {
256  fn from(search_error: SearchError) -> Self {
257    Self::Search { search_error }
258  }
259}
260
261impl<'src> ColorDisplay for Error<'src> {
262  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
263    use Error::*;
264
265    let error = color.error().paint("error");
266    let message = color.message().prefix();
267    write!(f, "{error}: {message}")?;
268
269    match self {
270      AmbiguousModuleFile { module, found } =>
271        write!(f,
272          "Found multiple source files for module `{module}`: {}",
273          List::and_ticked(found.iter().map(|path| path.display())),
274        )?,
275      ArgumentCountMismatch { recipe, found, min, max, .. } => {
276        let count = Count("argument", *found);
277        if min == max {
278          let expected = min;
279          let only = if expected < found { "only " } else { "" };
280          write!(f, "Recipe `{recipe}` got {found} {count} but {only}takes {expected}")?;
281        } else if found < min {
282          write!(f, "Recipe `{recipe}` got {found} {count} but takes at least {min}")?;
283        } else if found > max {
284          write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?;
285        }
286      }
287      Assert { message }=> {
288        write!(f, "Assert failed: {message}")?;
289      }
290      Backtick { output_error, .. } => match output_error {
291        OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?,
292        OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?,
293        OutputError::Unknown => write!(f, "Backtick failed for an unknown reason")?,
294        OutputError::Io(io_error) => match io_error.kind() {
295            io::ErrorKind::NotFound => write!(f, "Backtick could not be run because just could not find the shell:\n{io_error}"),
296            io::ErrorKind::PermissionDenied => write!(f, "Backtick could not be run because just could not run the shell:\n{io_error}"),
297            _ => write!(f, "Backtick could not be run because of an IO error while launching the shell:\n{io_error}"),
298          }?,
299        OutputError::Utf8(utf8_error) => write!(f, "Backtick succeeded but stdout was not utf8: {utf8_error}")?,
300      }
301      ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => {
302        let chooser = chooser.to_string_lossy();
303        write!(f, "Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}")?;
304      }
305      ChooserRead { chooser, io_error } => {
306        let chooser = chooser.to_string_lossy();
307        write!(f, "Failed to read output from chooser `{chooser}`: {io_error}")?;
308      }
309      ChooserStatus { chooser, status } => {
310        let chooser = chooser.to_string_lossy();
311        write!(f, "Chooser `{chooser}` failed: {status}")?;
312      }
313      ChooserWrite { chooser, io_error } => {
314        let chooser = chooser.to_string_lossy();
315        write!(f, "Failed to write to chooser `{chooser}`: {io_error}")?;
316      }
317      CircularImport { current, import } => {
318        let import = import.display();
319        let current = current.display();
320        write!(f, "Import `{import}` in `{current}` is circular")?;
321      }
322      Code { recipe, line_number, code, .. } => {
323        if let Some(n) = line_number {
324          write!(f, "Recipe `{recipe}` failed on line {n} with exit code {code}")?;
325        } else {
326          write!(f, "Recipe `{recipe}` failed with exit code {code}")?;
327        }
328      }
329      CommandInvoke { binary, arguments, io_error } => {
330        let cmd = format_cmd(binary, arguments);
331        write!(f, "Failed to invoke {cmd}: {io_error}")?;
332      }
333      CommandStatus { binary, arguments, status} => {
334        let cmd = format_cmd(binary, arguments);
335        write!(f, "Command {cmd} failed: {status}")?;
336      }
337      Compile { compile_error } => Display::fmt(compile_error, f)?,
338      Config { config_error } => Display::fmt(config_error, f)?,
339      Cygpath { recipe, output_error} => match output_error {
340        OutputError::Code(code) => write!(f, "Cygpath failed with exit code {code} while translating recipe `{recipe}` shebang interpreter path")?,
341        OutputError::Signal(signal) => write!(f, "Cygpath terminated by signal {signal} while translating recipe `{recipe}` shebang interpreter path")?,
342        OutputError::Unknown => write!(f, "Cygpath experienced an unknown failure while translating recipe `{recipe}` shebang interpreter path")?,
343        OutputError::Io(io_error) => {
344          match io_error.kind() {
345            io::ErrorKind::NotFound => write!(f, "Could not find `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}"),
346            io::ErrorKind::PermissionDenied => write!(f, "Could not run `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}"),
347            _ => write!(f, "Could not run `cygpath` executable:\n{io_error}"),
348          }?;
349        }
350        OutputError::Utf8(utf8_error) => write!(f, "Cygpath successfully translated recipe `{recipe}` shebang interpreter path, but output was not utf8: {utf8_error}")?,
351      }
352      DefaultRecipeRequiresArguments { recipe, min_arguments} => {
353        let count = Count("argument", *min_arguments);
354        write!(f, "Recipe `{recipe}` cannot be used as default recipe since it requires at least {min_arguments} {count}.")?;
355      }
356      Dotenv { dotenv_error } => {
357        write!(f, "Failed to load environment file: {dotenv_error}")?;
358      }
359      DotenvRequired => {
360        write!(f, "Dotenv file not found")?;
361      }
362      DumpJson { serde_json_error } => {
363        write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?;
364      }
365      EditorInvoke { editor, io_error } => {
366        let editor = editor.to_string_lossy();
367        write!(f, "Editor `{editor}` invocation failed: {io_error}")?;
368      }
369      EditorStatus { editor, status } => {
370        let editor = editor.to_string_lossy();
371        write!(f, "Editor `{editor}` failed: {status}")?;
372      }
373      EvalUnknownVariable { variable, suggestion} => {
374        write!(f, "Justfile does not contain variable `{variable}`.")?;
375        if let Some(suggestion) = suggestion {
376          write!(f, "\n{suggestion}")?;
377        }
378      }
379      ExcessInvocations { invocations } => {
380        write!(f, "Expected 1 command-line recipe invocation but found {invocations}.")?;
381      },
382      ExpectedSubmoduleButFoundRecipe { path } => {
383        write!(f, "Expected submodule at `{path}` but found recipe.")?;
384      },
385      FormatCheckFoundDiff => {
386        write!(f, "Formatted justfile differs from original.")?;
387      }
388      FunctionCall { function, message } => {
389        let function = function.lexeme();
390        write!(f, "Call to function `{function}` failed: {message}")?;
391      }
392      GetConfirmation { io_error } => {
393        write!(f, "Failed to read confirmation from stdin: {io_error}")?;
394      }
395      Homedir => {
396        write!(f, "Failed to get homedir")?;
397      }
398      InitExists { justfile } => {
399        write!(f, "Justfile `{}` already exists", justfile.display())?;
400      }
401      Internal { message } => {
402        write!(f, "Internal runtime error, this may indicate a bug in just: {message} \
403                   consider filing an issue: https://github.com/casey/just/issues/new")?;
404      }
405      Io { recipe, io_error } => {
406        match io_error.kind() {
407          io::ErrorKind::NotFound => write!(f, "Recipe `{recipe}` could not be run because just could not find the shell: {io_error}"),
408          io::ErrorKind::PermissionDenied => write!(f, "Recipe `{recipe}` could not be run because just could not run the shell: {io_error}"),
409          _ => write!(f, "Recipe `{recipe}` could not be run because of an IO error while launching the shell: {io_error}"),
410        }?;
411      }
412      Load { io_error, path } => {
413        write!(f, "Failed to read justfile at `{}`: {io_error}", path.display())?;
414      }
415      MissingImportFile { .. } => write!(f, "Could not find source file for import.")?,
416      MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?,
417      NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
418      NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,
419      NoRecipes => write!(f, "Justfile contains no recipes.")?,
420      NotConfirmed { recipe } => {
421        write!(f, "Recipe `{recipe}` was not confirmed")?;
422      }
423      RegexCompile { source } => write!(f, "{source}")?,
424      RuntimeDirIo { io_error, path } => {
425        write!(f, "I/O error in runtime dir `{}`: {io_error}", path.display())?;
426      }
427      Script { command, io_error, recipe } => {
428        write!(f, "Recipe `{recipe}` with command `{command}` execution error: {io_error}")?;
429      }
430      Search { search_error } => Display::fmt(search_error, f)?,
431      Shebang { recipe, command, argument, io_error} => {
432        if let Some(argument) = argument {
433          write!(f, "Recipe `{recipe}` with shebang `#!{command} {argument}` execution error: {io_error}")?;
434        } else {
435          write!(f, "Recipe `{recipe}` with shebang `#!{command}` execution error: {io_error}")?;
436        }
437      }
438      Signal { recipe, line_number, signal } => {
439        if let Some(n) = line_number {
440          write!(f, "Recipe `{recipe}` was terminated on line {n} by signal {signal}")?;
441        } else {
442          write!(f, "Recipe `{recipe}` was terminated by signal {signal}")?;
443        }
444      }
445      StdoutIo { io_error } => {
446        write!(f, "I/O error writing to stdout: {io_error}?")?;
447      }
448      TempdirIo { recipe, io_error } => {
449        write!(f, "Recipe `{recipe}` could not be run because of an IO error while trying to create a temporary \
450                   directory or write a file to that directory: {io_error}")?;
451      }
452      TempfileIo { io_error } => {
453        write!(f, "Tempfile I/O error: {io_error}")?;
454      }
455      Unknown { recipe, line_number} => {
456        if let Some(n) = line_number {
457          write!(f, "Recipe `{recipe}` failed on line {n} for an unknown reason")?;
458        } else {
459          write!(f, "Recipe `{recipe}` failed for an unknown reason")?;
460        }
461      }
462      UnknownSubmodule { path } => {
463        write!(f, "Justfile does not contain submodule `{path}`")?;
464      }
465      UnknownOverrides { overrides } => {
466        let count = Count("Variable", overrides.len());
467        let overrides = List::and_ticked(overrides);
468        write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
469      }
470      UnknownRecipe { recipe, suggestion } => {
471        write!(f, "Justfile does not contain recipe `{recipe}`.")?;
472        if let Some(suggestion) = suggestion {
473          write!(f, "\n{suggestion}")?;
474        }
475      }
476      UnstableFeature { unstable_feature } => {
477        write!(f, "{unstable_feature} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
478      }
479      WriteJustfile { justfile, io_error } => {
480        let justfile = justfile.display();
481        write!(f, "Failed to write justfile to `{justfile}`: {io_error}")?;
482      }
483    }
484
485    write!(f, "{}", color.message().suffix())?;
486
487    if let ArgumentCountMismatch {
488      recipe, parameters, ..
489    } = self
490    {
491      writeln!(f)?;
492      write!(f, "{}:\n    just {recipe}", color.message().paint("usage"))?;
493      for param in parameters {
494        write!(f, " {}", param.color_display(color))?;
495      }
496    }
497
498    if let Some(token) = self.context() {
499      writeln!(f)?;
500      write!(f, "{}", token.color_display(color.error()))?;
501    }
502
503    Ok(())
504  }
505}
506
507fn format_cmd(binary: &OsString, arguments: &Vec<OsString>) -> String {
508  iter::once(binary)
509    .chain(arguments)
510    .map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
511    .collect::<Vec<String>>()
512    .join(" ")
513}