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    WriteOutput {
51        path: String,
52        source: io::Error,
53    },
54    Rustfmt(String),
55    Generation(String),
56    /// Several independent build errors, collected so they can all be fixed in
57    /// one pass instead of one rebuild at a time.
58    Multiple(Vec<BuildError>),
59}
60
61impl BuildError {
62    /// Collapse a list of errors into one: the error itself when there is
63    /// exactly one, otherwise a [`BuildError::Multiple`].
64    pub(crate) fn collapse(mut errors: Vec<BuildError>) -> BuildError {
65        if errors.len() == 1 {
66            errors.pop().unwrap()
67        } else {
68            BuildError::Multiple(errors)
69        }
70    }
71}
72
73impl fmt::Display for BuildError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::FtlParse { path, errors } => {
77                write!(f, "Could not parse '{}':", path.display())?;
78                for e in errors {
79                    write!(f, "\n  {e}")?;
80                }
81                Ok(())
82            }
83            Self::FtlRead { path, .. } => {
84                write!(f, "Could not read '{}'", path.display())
85            }
86            Self::DuplicateKey {
87                key,
88                original,
89                original_line,
90                duplicate,
91                duplicate_line,
92            } => {
93                if original == duplicate {
94                    write!(
95                        f,
96                        "Duplicate message key '{key}' in '{}': lines {original_line} and \
97                         {duplicate_line}",
98                        duplicate.display(),
99                    )
100                } else {
101                    write!(
102                        f,
103                        "Duplicate message key '{key}' in '{}:{duplicate_line}', first defined \
104                         in '{}:{original_line}'",
105                        duplicate.display(),
106                        original.display(),
107                    )
108                }
109            }
110            Self::TermMessageCollision {
111                name,
112                term_file,
113                term_line,
114                message_file,
115                message_line,
116            } => {
117                write!(
118                    f,
119                    "Term '-{name}' and message '{name}' share the same name — \
120                     fluent-bundle treats them as the same key and will crash \
121                     at runtime. Rename one. Term defined in '{}:{term_line}', \
122                     message defined in '{}:{message_line}'.",
123                    term_file.display(),
124                    message_file.display(),
125                )
126            }
127            Self::LocalesFolder { folder, .. } => {
128                write!(f, "Could not read locales folder '{folder}'")
129            }
130            Self::NoLocaleFolders { folder } => {
131                write!(
132                    f,
133                    "No locale subfolders found in '{folder}'. Expected \
134                     '<lang-id>/<resource>.ftl' files, e.g. 'en/main.ftl'."
135                )
136            }
137            Self::DefaultLanguageNotFound { language, folder } => {
138                write!(
139                    f,
140                    "Default language '{language}' has no locale subfolder in '{folder}'. \
141                     Set it with `BuildOptions::with_default_language`."
142                )
143            }
144            Self::Lint { messages } => {
145                write!(f, "fluent-typed found {} lint error(s):", messages.len())?;
146                for m in messages {
147                    write!(f, "\n  {m}")?;
148                }
149                Ok(())
150            }
151            Self::WriteOutput { path, .. } => {
152                write!(f, "Could not write file '{path}'")
153            }
154            Self::Rustfmt(msg) => write!(f, "Rustfmt error: {msg}"),
155            Self::Generation(msg) => write!(f, "{msg}"),
156            Self::Multiple(errors) => {
157                write!(f, "{} build errors:", errors.len())?;
158                for e in errors {
159                    for (i, line) in e.to_string().lines().enumerate() {
160                        if i == 0 {
161                            write!(f, "\n  - {line}")?;
162                        } else {
163                            write!(f, "\n    {line}")?;
164                        }
165                    }
166                }
167                Ok(())
168            }
169        }
170    }
171}
172
173impl Error for BuildError {
174    fn source(&self) -> Option<&(dyn Error + 'static)> {
175        match self {
176            Self::FtlRead { source, .. } => Some(source),
177            Self::LocalesFolder { source, .. } => Some(source),
178            Self::WriteOutput { source, .. } => Some(source),
179            _ => None,
180        }
181    }
182}