Skip to main content

chainsaw/
error.rs

1//! Error types for the chainsaw CLI.
2
3use std::path::PathBuf;
4
5/// Errors from entry validation, graph loading, and snapshot I/O.
6#[derive(Debug)]
7#[non_exhaustive]
8pub enum Error {
9    /// Entry point file not found on disk.
10    EntryNotFound(PathBuf, std::io::Error),
11    /// Entry point path refers to a directory, not a file.
12    EntryIsDirectory(PathBuf),
13    /// File has an unsupported extension.
14    UnsupportedFileType(String),
15    /// Entry point exists but was not found in the dependency graph.
16    EntryNotInGraph(PathBuf),
17    /// Cannot read a snapshot file from disk.
18    SnapshotRead(PathBuf, std::io::Error),
19    /// Snapshot file contains invalid JSON.
20    SnapshotParse(PathBuf, serde_json::Error),
21    /// Cannot write a snapshot file to disk.
22    SnapshotWrite(PathBuf, std::io::Error),
23    /// Mutually exclusive CLI flags were used together.
24    MutuallyExclusiveFlags(String),
25    /// --chain/--cut target is the entry point itself.
26    TargetIsEntryPoint(String),
27}
28
29impl Error {
30    /// User-facing hint to accompany the error message.
31    pub fn hint(&self) -> Option<&str> {
32        match self {
33            Self::UnsupportedFileType(_) => {
34                Some("chainsaw supports TypeScript/JavaScript and Python files")
35            }
36            Self::EntryNotInGraph(_) => Some("is it reachable from the project root?"),
37            Self::TargetIsEntryPoint(flag) => Some(if flag == "--chain" {
38                "--chain finds import chains from the entry to a dependency"
39            } else {
40                "--cut finds where to sever import chains to a dependency"
41            }),
42            _ => None,
43        }
44    }
45}
46
47// Display: lowercase, no trailing punctuation, so it composes into
48// larger error messages.
49impl std::fmt::Display for Error {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            Self::EntryNotFound(path, source) => {
53                write!(f, "cannot find entry file '{}': {source}", path.display())
54            }
55            Self::EntryIsDirectory(path) => {
56                write!(f, "'{}' is a directory, not a source file", path.display())
57            }
58            Self::UnsupportedFileType(ext) => {
59                write!(f, "unsupported file type '.{ext}'")
60            }
61            Self::EntryNotInGraph(path) => {
62                write!(f, "entry file '{}' not found in graph", path.display())
63            }
64            Self::SnapshotRead(path, source) => {
65                write!(f, "cannot read snapshot '{}': {source}", path.display())
66            }
67            Self::SnapshotParse(path, source) => {
68                write!(f, "invalid snapshot '{}': {source}", path.display())
69            }
70            Self::SnapshotWrite(path, source) => {
71                write!(f, "cannot write snapshot '{}': {source}", path.display())
72            }
73            Self::MutuallyExclusiveFlags(flags) => {
74                write!(f, "{flags} cannot be used together")
75            }
76            Self::TargetIsEntryPoint(flag) => {
77                write!(f, "{flag} target is the entry point itself")
78            }
79        }
80    }
81}
82
83// Implement source() for error chain introspection.
84impl std::error::Error for Error {
85    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
86        match self {
87            Self::EntryNotFound(_, e)
88            | Self::SnapshotRead(_, e)
89            | Self::SnapshotWrite(_, e) => Some(e),
90            Self::SnapshotParse(_, e) => Some(e),
91            _ => None,
92        }
93    }
94}