Skip to main content

recast_core/
error.rs

1//! Typed errors returned by the planner, walker, and commit phases.
2//!
3//! [`Error`] is the single source of truth for failure shapes;
4//! [`ErrorKind`] is the machine-readable discriminator that tags each
5//! variant for JSON output. The mapping lives here so adding an
6//! [`Error`] variant without extending [`ErrorKind`] is a compile
7//! error rather than a runtime mis-tag.
8
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13    #[error("invalid regex: {0}")]
14    InvalidRegex(#[from] regex::Error),
15
16    #[error("invalid glob: {0}")]
17    InvalidGlob(#[from] globset::Error),
18
19    #[error("walk failed: {0}")]
20    Walk(#[from] ignore::Error),
21
22    #[error("i/o error at {path}: {source}")]
23    Io { path: PathBuf, source: std::io::Error },
24
25    #[error("file {path} exceeds --max-bytes ({size} > {limit})")]
26    FileTooLarge { path: PathBuf, size: u64, limit: u64 },
27
28    #[error("scan touched {count} files; refusing (--max-files = {limit})")]
29    TooManyFiles { count: usize, limit: usize },
30
31    #[error(
32        "pattern is non-convergent: re-applying it to the rewrite of {path} would produce {extra} more match(es)"
33    )]
34    NonConvergent { path: PathBuf, extra: usize },
35
36    #[error("match-count guard violated: found {found}, required at least {required}")]
37    TooFewMatches { found: usize, required: usize },
38
39    #[error("match-count guard violated: found {found}, allowed at most {allowed}")]
40    TooManyMatches { found: usize, allowed: usize },
41
42    #[error("script parse error: {0}")]
43    ScriptParse(String),
44
45    #[error("script runtime error: {0}")]
46    ScriptRuntime(String),
47
48    #[error("structural: unknown language `{0}`")]
49    UnknownLanguage(String),
50
51    #[error("structural: query error: {0}")]
52    StructuralQuery(String),
53
54    #[error("structural: template error: {0}")]
55    StructuralTemplate(String),
56
57    #[error("structural: parse error")]
58    StructuralParse,
59
60    #[error(
61        "rewrite introduced {new_errors} new syntax error(s) in {path} ({lang}); pass allow_syntax_errors to override"
62    )]
63    SyntaxRegression { path: PathBuf, lang: &'static str, new_errors: usize },
64
65    #[error(
66        "another recast is already applying to this tree (lockfile {path} held); use --force to override"
67    )]
68    Locked { path: PathBuf },
69
70    #[error("invalid --threads value: must be at least 1")]
71    InvalidThreads,
72
73    #[error("failed to build worker thread pool: {0}")]
74    ThreadPool(String),
75}
76
77/// Machine-readable tag for an [`Error`] variant. Stable across releases;
78/// every variant in [`Error`] has exactly one [`ErrorKind`] counterpart.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize))]
81#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
82pub enum ErrorKind {
83    InvalidRegex,
84    InvalidGlob,
85    Walk,
86    Io,
87    FileTooLarge,
88    TooManyFiles,
89    NonConvergent,
90    TooFewMatches,
91    TooManyMatches,
92    ScriptParse,
93    ScriptRuntime,
94    UnknownLanguage,
95    StructuralQuery,
96    StructuralTemplate,
97    StructuralParse,
98    SyntaxRegression,
99    Locked,
100    InvalidThreads,
101    ThreadPool,
102}
103
104impl Error {
105    /// Tag for this variant. The match is exhaustive; adding a new
106    /// variant without extending [`ErrorKind`] is a compile error.
107    pub fn kind(&self) -> ErrorKind {
108        match self {
109            Error::InvalidRegex(_) => ErrorKind::InvalidRegex,
110            Error::InvalidGlob(_) => ErrorKind::InvalidGlob,
111            Error::Walk(_) => ErrorKind::Walk,
112            Error::Io { .. } => ErrorKind::Io,
113            Error::FileTooLarge { .. } => ErrorKind::FileTooLarge,
114            Error::TooManyFiles { .. } => ErrorKind::TooManyFiles,
115            Error::NonConvergent { .. } => ErrorKind::NonConvergent,
116            Error::TooFewMatches { .. } => ErrorKind::TooFewMatches,
117            Error::TooManyMatches { .. } => ErrorKind::TooManyMatches,
118            Error::ScriptParse(_) => ErrorKind::ScriptParse,
119            Error::ScriptRuntime(_) => ErrorKind::ScriptRuntime,
120            Error::UnknownLanguage(_) => ErrorKind::UnknownLanguage,
121            Error::StructuralQuery(_) => ErrorKind::StructuralQuery,
122            Error::StructuralTemplate(_) => ErrorKind::StructuralTemplate,
123            Error::StructuralParse => ErrorKind::StructuralParse,
124            Error::SyntaxRegression { .. } => ErrorKind::SyntaxRegression,
125            Error::Locked { .. } => ErrorKind::Locked,
126            Error::InvalidThreads => ErrorKind::InvalidThreads,
127            Error::ThreadPool(_) => ErrorKind::ThreadPool,
128        }
129    }
130}
131
132pub type Result<T> = std::result::Result<T, Error>;
133
134/// Extension that converts a `std::io::Result<T>` into a `crate::Result<T>`
135/// by attaching the offending path to the `Error::Io` variant. Cuts the
136/// repeated `|e| Error::Io { path: ..., source: e }` closure that would
137/// otherwise show up at every `fs::*` call site.
138pub(crate) trait IoCtx<T> {
139    fn io_ctx(self, path: &Path) -> Result<T>;
140}
141
142impl<T> IoCtx<T> for std::result::Result<T, std::io::Error> {
143    fn io_ctx(self, path: &Path) -> Result<T> {
144        self.map_err(|source| Error::Io { path: path.to_path_buf(), source })
145    }
146}