Skip to main content

aft/
error.rs

1use std::fmt;
2
3/// Core error type for the aft binary.
4///
5/// Each variant maps to a structured error response with a `code` string
6/// and a human-readable `message`.
7#[derive(Debug)]
8pub enum AftError {
9    SymbolNotFound {
10        name: String,
11        file: String,
12    },
13    AmbiguousSymbol {
14        name: String,
15        candidates: Vec<String>,
16    },
17    ParseError {
18        message: String,
19    },
20    FileNotFound {
21        path: String,
22    },
23    InvalidRequest {
24        message: String,
25    },
26    CheckpointNotFound {
27        name: String,
28    },
29    NoUndoHistory {
30        path: String,
31    },
32    AmbiguousMatch {
33        pattern: String,
34        count: usize,
35    },
36    ScopeNotFound {
37        scope: String,
38        available: Vec<String>,
39        file: String,
40    },
41    MemberNotFound {
42        member: String,
43        scope: String,
44        file: String,
45    },
46    IoError {
47        path: String,
48        message: String,
49    },
50    ProjectTooLarge {
51        count: usize,
52        max: usize,
53    },
54}
55
56impl AftError {
57    /// Returns the error code string used in JSON error responses.
58    pub fn code(&self) -> &'static str {
59        match self {
60            AftError::SymbolNotFound { .. } => "symbol_not_found",
61            AftError::AmbiguousSymbol { .. } => "ambiguous_symbol",
62            AftError::ParseError { .. } => "parse_error",
63            AftError::FileNotFound { .. } => "file_not_found",
64            AftError::InvalidRequest { .. } => "invalid_request",
65            AftError::CheckpointNotFound { .. } => "checkpoint_not_found",
66            AftError::NoUndoHistory { .. } => "no_undo_history",
67            AftError::AmbiguousMatch { .. } => "ambiguous_match",
68            AftError::ScopeNotFound { .. } => "scope_not_found",
69            AftError::MemberNotFound { .. } => "member_not_found",
70            AftError::IoError { .. } => "io_error",
71            AftError::ProjectTooLarge { .. } => "project_too_large",
72        }
73    }
74
75    /// Produces a `serde_json::Value` suitable for the error portion of a response.
76    ///
77    /// Shape: `{ "code": "...", "message": "..." }`
78    pub fn to_error_json(&self) -> serde_json::Value {
79        serde_json::json!({
80            "code": self.code(),
81            "message": self.to_string(),
82        })
83    }
84}
85
86impl fmt::Display for AftError {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            AftError::SymbolNotFound { name, file } => {
90                write!(f, "symbol '{}' not found in {}", name, file)
91            }
92            AftError::AmbiguousSymbol { name, candidates } => {
93                write!(
94                    f,
95                    "symbol '{}' is ambiguous, candidates: [{}]",
96                    name,
97                    candidates.join(", ")
98                )
99            }
100            AftError::ParseError { message } => {
101                write!(f, "parse error: {}", message)
102            }
103            AftError::FileNotFound { path } => {
104                write!(f, "file not found: {}", path)
105            }
106            AftError::InvalidRequest { message } => {
107                write!(f, "invalid request: {}", message)
108            }
109            AftError::CheckpointNotFound { name } => {
110                write!(f, "checkpoint not found: {}", name)
111            }
112            AftError::NoUndoHistory { path } => {
113                write!(f, "no undo history for: {}", path)
114            }
115            AftError::AmbiguousMatch { pattern, count } => {
116                write!(
117                    f,
118                    "pattern '{}' matches {} occurrences, expected exactly 1",
119                    pattern, count
120                )
121            }
122            AftError::ScopeNotFound {
123                scope,
124                available,
125                file,
126            } => {
127                if available.is_empty() {
128                    write!(
129                        f,
130                        "scope '{}' not found in {} (no scopes available)",
131                        scope, file
132                    )
133                } else {
134                    write!(
135                        f,
136                        "scope '{}' not found in {}, available: [{}]",
137                        scope,
138                        file,
139                        available.join(", ")
140                    )
141                }
142            }
143            AftError::MemberNotFound {
144                member,
145                scope,
146                file,
147            } => {
148                write!(
149                    f,
150                    "member '{}' not found in scope '{}' in {}",
151                    member, scope, file
152                )
153            }
154            AftError::IoError { path, message } => {
155                write!(f, "I/O error on {}: {}", path, message)
156            }
157            AftError::ProjectTooLarge { count, max } => {
158                write!(
159                    f,
160                    "project has {count} source files, exceeding max_callgraph_files={max}. Call-graph operations (callers, trace_to, trace_data, impact) are disabled for this root. Open a specific subdirectory or raise max_callgraph_files in config."
161                )
162            }
163        }
164    }
165}
166
167impl std::error::Error for AftError {}