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    /// --entry is required when comparing git refs.
28    EntryRequired,
29    /// Not inside a git repository.
30    NotAGitRepo,
31    /// Argument is not a snapshot file or valid git ref.
32    NotSnapshotOrRef(String),
33    /// A path-like argument that doesn't exist on disk.
34    DiffFileNotFound(String),
35    /// Git command failed.
36    GitError(String),
37}
38
39impl Error {
40    /// User-facing hint to accompany the error message.
41    pub fn hint(&self) -> Option<&str> {
42        match self {
43            Self::UnsupportedFileType(_) => {
44                Some("chainsaw supports TypeScript/JavaScript and Python files")
45            }
46            Self::EntryNotInGraph(_) => Some("is it reachable from the project root?"),
47            Self::TargetIsEntryPoint(flag) => Some(if flag == "--chain" {
48                "--chain finds import chains from the entry to a dependency"
49            } else {
50                "--cut finds where to sever import chains to a dependency"
51            }),
52            Self::EntryRequired => {
53                Some("use --entry to specify the entry point to trace")
54            }
55            _ => None,
56        }
57    }
58}
59
60// Display: lowercase, no trailing punctuation, so it composes into
61// larger error messages.
62impl std::fmt::Display for Error {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Self::EntryNotFound(path, source) => {
66                write!(f, "cannot find entry file '{}': {source}", path.display())
67            }
68            Self::EntryIsDirectory(path) => {
69                write!(f, "'{}' is a directory, not a source file", path.display())
70            }
71            Self::UnsupportedFileType(ext) => {
72                write!(f, "unsupported file type '.{ext}'")
73            }
74            Self::EntryNotInGraph(path) => {
75                write!(f, "entry file '{}' not found in graph", path.display())
76            }
77            Self::SnapshotRead(path, source) => {
78                write!(f, "cannot read snapshot '{}': {source}", path.display())
79            }
80            Self::SnapshotParse(path, source) => {
81                write!(f, "invalid snapshot '{}': {source}", path.display())
82            }
83            Self::SnapshotWrite(path, source) => {
84                write!(f, "cannot write snapshot '{}': {source}", path.display())
85            }
86            Self::MutuallyExclusiveFlags(flags) => {
87                write!(f, "{flags} cannot be used together")
88            }
89            Self::TargetIsEntryPoint(flag) => {
90                write!(f, "{flag} target is the entry point itself")
91            }
92            Self::EntryRequired => {
93                write!(f, "--entry is required when diffing against a git ref or the working tree")
94            }
95            Self::NotAGitRepo => write!(f, "not inside a git repository"),
96            Self::NotSnapshotOrRef(arg) => {
97                write!(f, "'{arg}' is not a snapshot file or a valid git ref")
98            }
99            Self::DiffFileNotFound(arg) => write!(f, "file not found: {arg}"),
100            Self::GitError(msg) => write!(f, "git: {msg}"),
101        }
102    }
103}
104
105// Implement source() for error chain introspection.
106impl std::error::Error for Error {
107    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
108        match self {
109            Self::EntryNotFound(_, e)
110            | Self::SnapshotRead(_, e)
111            | Self::SnapshotWrite(_, e) => Some(e),
112            Self::SnapshotParse(_, e) => Some(e),
113            _ => None,
114        }
115    }
116}