Skip to main content

fluent_typed/build/
error.rs

1use std::{error::Error, fmt, io, path::PathBuf};
2
3#[derive(Debug)]
4#[non_exhaustive]
5pub enum BuildError {
6    FtlParse {
7        path: PathBuf,
8        /// One entry per parse error, each prefixed with its line number.
9        errors: Vec<String>,
10    },
11    FtlRead {
12        path: PathBuf,
13        source: io::Error,
14    },
15    DuplicateKey {
16        key: String,
17        original: PathBuf,
18        original_line: usize,
19        duplicate: PathBuf,
20        duplicate_line: usize,
21    },
22    /// A term and a message share the same bare name (e.g. `-foo` and `foo`).
23    /// fluent-bundle stores both under the same key, so loading the resource
24    /// crashes at runtime with `FluentError::Overriding`. Detected at build
25    /// time so the cliff never happens.
26    TermMessageCollision {
27        name: String,
28        term_file: PathBuf,
29        term_line: usize,
30        message_file: PathBuf,
31        message_line: usize,
32    },
33    LocalesFolder {
34        folder: String,
35        source: io::Error,
36    },
37    NoLocaleFolders {
38        folder: String,
39    },
40    DefaultLanguageNotFound {
41        language: String,
42        folder: String,
43    },
44    /// One or more lint diagnostics, raised as a hard error under
45    /// [`crate::LintLevel::Deny`] or [`crate::LintLevel::Strict`]. Each entry
46    /// already carries `file:line`.
47    Lint {
48        messages: Vec<String>,
49    },
50    /// Invalid default-locale message contracts that would generate accessors
51    /// with silently wrong runtime output.
52    InvalidContract {
53        messages: Vec<String>,
54    },
55    WriteOutput {
56        path: String,
57        source: io::Error,
58    },
59    Rustfmt(String),
60    Generation(String),
61    /// Several independent build errors, collected so they can all be fixed in
62    /// one pass instead of one rebuild at a time.
63    Multiple(Vec<BuildError>),
64}
65
66impl BuildError {
67    /// Collapse a list of errors into one: the error itself when there is
68    /// exactly one, otherwise a [`BuildError::Multiple`].
69    pub(crate) fn collapse(mut errors: Vec<BuildError>) -> BuildError {
70        if errors.len() == 1 {
71            errors.pop().unwrap()
72        } else {
73            BuildError::Multiple(errors)
74        }
75    }
76}
77
78impl fmt::Display for BuildError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::FtlParse { path, errors } => {
82                write!(f, "Could not parse '{}':", path.display())?;
83                for e in errors {
84                    write!(f, "\n  {e}")?;
85                }
86                Ok(())
87            }
88            Self::FtlRead { path, .. } => {
89                write!(f, "Could not read '{}'", path.display())
90            }
91            Self::DuplicateKey {
92                key,
93                original,
94                original_line,
95                duplicate,
96                duplicate_line,
97            } => {
98                if original == duplicate {
99                    write!(
100                        f,
101                        "Duplicate message key '{key}' in '{}': lines {original_line} and \
102                         {duplicate_line}",
103                        duplicate.display(),
104                    )
105                } else {
106                    write!(
107                        f,
108                        "Duplicate message key '{key}' in '{}:{duplicate_line}', first defined \
109                         in '{}:{original_line}'",
110                        duplicate.display(),
111                        original.display(),
112                    )
113                }
114            }
115            Self::TermMessageCollision {
116                name,
117                term_file,
118                term_line,
119                message_file,
120                message_line,
121            } => {
122                write!(
123                    f,
124                    "Term '-{name}' and message '{name}' share the same name — \
125                     fluent-bundle treats them as the same key and will crash \
126                     at runtime. Rename one. Term defined in '{}:{term_line}', \
127                     message defined in '{}:{message_line}'.",
128                    term_file.display(),
129                    message_file.display(),
130                )
131            }
132            Self::LocalesFolder { folder, .. } => {
133                write!(f, "Could not read locales folder '{folder}'")
134            }
135            Self::NoLocaleFolders { folder } => {
136                write!(
137                    f,
138                    "No locale subfolders found in '{folder}'. Expected \
139                     '<lang-id>/<resource>.ftl' files, e.g. 'en/main.ftl'."
140                )
141            }
142            Self::DefaultLanguageNotFound { language, folder } => {
143                write!(
144                    f,
145                    "Default language '{language}' has no locale subfolder in '{folder}'. \
146                     Set it with `BuildOptions::with_default_language`."
147                )
148            }
149            Self::Lint { messages } => {
150                write!(f, "fluent-typed found {} lint error(s):", messages.len())?;
151                for m in messages {
152                    write!(f, "\n  {m}")?;
153                }
154                Ok(())
155            }
156            Self::InvalidContract { messages } => {
157                write!(
158                    f,
159                    "fluent-typed found {} invalid message contract(s):",
160                    messages.len()
161                )?;
162                for m in messages {
163                    write!(f, "\n  {m}")?;
164                }
165                Ok(())
166            }
167            Self::WriteOutput { path, .. } => {
168                write!(f, "Could not write file '{path}'")
169            }
170            Self::Rustfmt(msg) => write!(f, "Rustfmt error: {msg}"),
171            Self::Generation(msg) => write!(f, "{msg}"),
172            Self::Multiple(errors) => {
173                write!(f, "{} build errors:", errors.len())?;
174                for e in errors {
175                    for (i, line) in e.to_string().lines().enumerate() {
176                        if i == 0 {
177                            write!(f, "\n  - {line}")?;
178                        } else {
179                            write!(f, "\n    {line}")?;
180                        }
181                    }
182                }
183                Ok(())
184            }
185        }
186    }
187}
188
189impl Error for BuildError {
190    fn source(&self) -> Option<&(dyn Error + 'static)> {
191        match self {
192            Self::FtlRead { source, .. } => Some(source),
193            Self::LocalesFolder { source, .. } => Some(source),
194            Self::WriteOutput { source, .. } => Some(source),
195            _ => None,
196        }
197    }
198}