1use std::path::PathBuf;
4
5#[derive(Debug)]
7#[non_exhaustive]
8pub enum Error {
9 EntryNotFound(PathBuf, std::io::Error),
11 EntryIsDirectory(PathBuf),
13 UnsupportedFileType(Option<String>),
15 EntryNotInGraph(PathBuf),
17 SnapshotRead(PathBuf, std::io::Error),
19 SnapshotParse(PathBuf, serde_json::Error),
21 SnapshotWrite(PathBuf, std::io::Error),
23 MutuallyExclusiveFlags(String),
25 TargetIsEntryPoint(String),
27 EntryRequired,
29 NotAGitRepo,
31 NotSnapshotOrRef(String),
33 DiffFileNotFound(String),
35 GitError(String),
37 InvalidTopValue(&'static str, i32),
39 Readline(String),
41}
42
43impl Error {
44 pub fn hint(&self) -> Option<&str> {
46 match self {
47 Self::UnsupportedFileType(_) => Some(
48 "chainsaw supports TypeScript/JavaScript (.ts, .tsx, .js, .jsx, .mjs, .cjs) and Python (.py) files",
49 ),
50 Self::EntryNotInGraph(_) => Some("is it reachable from the project root?"),
51 Self::TargetIsEntryPoint(flag) => Some(if flag == "--chain" {
52 "--chain finds import chains from the entry to a dependency"
53 } else {
54 "--cut finds where to sever import chains to a dependency"
55 }),
56 Self::EntryRequired => Some("use --entry to specify the entry point to trace"),
57 Self::EntryIsDirectory(_) => {
58 Some("provide a source file (e.g. src/index.ts or main.py)")
59 }
60 _ => None,
61 }
62 }
63}
64
65impl std::fmt::Display for Error {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::EntryNotFound(path, source) => {
71 write!(f, "cannot find entry file '{}': {source}", path.display())
72 }
73 Self::EntryIsDirectory(path) => {
74 write!(f, "'{}' is a directory, not a source file", path.display())
75 }
76 Self::UnsupportedFileType(Some(ext)) => {
77 write!(f, "unsupported file type '.{ext}'")
78 }
79 Self::UnsupportedFileType(None) => {
80 write!(f, "file has no extension")
81 }
82 Self::EntryNotInGraph(path) => {
83 write!(f, "entry file '{}' not found in graph", path.display())
84 }
85 Self::SnapshotRead(path, source) => {
86 write!(f, "cannot read snapshot '{}': {source}", path.display())
87 }
88 Self::SnapshotParse(path, source) => {
89 write!(f, "invalid snapshot '{}': {source}", path.display())
90 }
91 Self::SnapshotWrite(path, source) => {
92 write!(f, "cannot write snapshot '{}': {source}", path.display())
93 }
94 Self::MutuallyExclusiveFlags(flags) => {
95 write!(f, "{flags} cannot be used together")
96 }
97 Self::TargetIsEntryPoint(flag) => {
98 write!(f, "{flag} target is the entry point itself")
99 }
100 Self::EntryRequired => {
101 write!(
102 f,
103 "--entry is required when diffing against a git ref or the working tree"
104 )
105 }
106 Self::NotAGitRepo => write!(f, "not inside a git repository"),
107 Self::NotSnapshotOrRef(arg) => {
108 write!(f, "'{arg}' is not a snapshot file or a valid git ref")
109 }
110 Self::DiffFileNotFound(arg) => write!(f, "file not found: {arg}"),
111 Self::GitError(msg) => write!(f, "git: {msg}"),
112 Self::InvalidTopValue(flag, n) => {
113 write!(f, "invalid value {n} for {flag}: must be -1 (all) or 0+")
114 }
115 Self::Readline(msg) => write!(f, "readline: {msg}"),
116 }
117 }
118}
119
120impl std::error::Error for Error {
122 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
123 match self {
124 Self::EntryNotFound(_, e) | Self::SnapshotRead(_, e) | Self::SnapshotWrite(_, e) => {
125 Some(e)
126 }
127 Self::SnapshotParse(_, e) => Some(e),
128 _ => None,
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn unsupported_file_type_no_extension() {
139 let err = Error::UnsupportedFileType(None);
140 assert!(err.to_string().contains("no extension"));
141 assert!(err.hint().unwrap().contains(".ts"));
142 }
143
144 #[test]
145 fn unsupported_file_type_with_extension() {
146 let err = Error::UnsupportedFileType(Some("rs".to_string()));
147 assert!(err.to_string().contains(".rs"));
148 }
149
150 #[test]
151 fn entry_is_directory_has_hint() {
152 let err = Error::EntryIsDirectory(PathBuf::from("/tmp/src"));
153 assert!(err.hint().unwrap().contains("source file"));
154 }
155}