Skip to main content

fugo_sass_compiler/
error.rs

1use std::{
2    error::Error,
3    fmt::{self, Display},
4    io,
5    string::FromUtf8Error,
6    sync::Arc,
7};
8
9use codemap::{Span, SpanLoc};
10
11pub type SassResult<T> = Result<T, Box<SassError>>;
12
13/// `SassError`s can be either a structured error specific to `fugo-sass` or an
14/// `io::Error`.
15///
16/// In the former case, the best way to interact with the error is to simply print
17/// it to the user. The `Display` implementation of this kind of error mirrors
18/// that of the errors `dart-sass` emits, e.g.
19///```scss
20/// Error: $number: foo is not a number.
21///     |
22/// 308 |     color: unit(foo);
23///     |                 ^^^
24///     |
25/// ./input.scss:308:17
26///```
27///
28#[derive(Debug, Clone)]
29pub struct SassError {
30    kind: SassErrorKind,
31}
32
33impl SassError {
34    #[must_use]
35    pub fn kind(self) -> PublicSassErrorKind {
36        match self.kind {
37            SassErrorKind::ParseError {
38                message,
39                loc,
40                unicode,
41            } => PublicSassErrorKind::ParseError {
42                message,
43                loc,
44                unicode,
45            },
46            SassErrorKind::FromUtf8Error(s) => PublicSassErrorKind::FromUtf8Error(s),
47            SassErrorKind::IoError(io) => PublicSassErrorKind::IoError(io),
48            SassErrorKind::Raw(..) => unreachable!("raw errors should not be accessible by users"),
49        }
50    }
51
52    /// This error as one about the argument `$name`: its message prefixed as dart-sass's
53    /// `SassScriptException(message, name)` does (`$name: message`).
54    pub(crate) fn with_argument_name(mut self: Box<Self>, name: &str) -> Box<Self> {
55        if let SassErrorKind::Raw(message, _) = &mut self.kind {
56            *message = format!("${name}: {message}");
57        }
58        self
59    }
60
61    pub(crate) fn raw(self) -> (String, Span) {
62        match self.kind {
63            SassErrorKind::Raw(string, span) => (string, span),
64            e => unreachable!("unable to get raw of {:?}", e),
65        }
66    }
67
68    pub(crate) const fn from_loc(message: String, loc: SpanLoc, unicode: bool) -> Self {
69        SassError {
70            kind: SassErrorKind::ParseError {
71                message,
72                loc,
73                unicode,
74            },
75        }
76    }
77}
78
79#[non_exhaustive]
80#[derive(Debug, Clone)]
81pub enum PublicSassErrorKind {
82    ParseError {
83        /// The message related to this parse error.
84        ///
85        /// Error messages should only be used to assist in debugging for the
86        /// end user. They may change significantly between bugfix versions and
87        /// should not be relied on to remain stable.
88        ///
89        /// Error messages do not contain the `Error: ` prefix or pretty-printed
90        /// span and context information as is shown in the `Display` implementation.
91        message: String,
92        loc: SpanLoc,
93
94        /// Whether or not the user allows unicode characters to be emitted in
95        /// error messages.
96        ///
97        /// This is configurable with [`crate::Options::unicode_error_messages`]
98        unicode: bool,
99    },
100
101    /// Sass was unable to find the entry-point file.
102    ///
103    /// Files that cannot be found using `@import`, `@use`, and `@forward` will
104    /// emit [`Self::ParseError`]s
105    IoError(Arc<io::Error>),
106
107    /// The entry-point file or an imported file was not valid UTF-8.
108    FromUtf8Error(String),
109}
110
111#[derive(Debug, Clone)]
112enum SassErrorKind {
113    /// A raw error with no additional metadata
114    /// It contains only a `String` message and
115    /// a span
116    Raw(String, Span),
117    ParseError {
118        message: String,
119        loc: SpanLoc,
120        unicode: bool,
121    },
122    // we put `IoError`s in an `Arc` to allow them to be cloneable
123    IoError(Arc<io::Error>),
124    FromUtf8Error(String),
125}
126
127impl Display for SassError {
128    // TODO: trim whitespace from start of line shown in error
129    // TODO: color errors
130    // TODO: integrate with codemap-diagnostics
131    #[inline]
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        let (message, loc, unicode) = match &self.kind {
134            SassErrorKind::ParseError {
135                message,
136                loc,
137                unicode,
138            } => (message, loc, *unicode),
139            SassErrorKind::FromUtf8Error(..) => return writeln!(f, "Error: Invalid UTF-8."),
140            SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s),
141            SassErrorKind::Raw(..) => unreachable!(),
142        };
143
144        let first_bar = if unicode { '╷' } else { ',' };
145        let second_bar = if unicode { '│' } else { '|' };
146        let third_bar = if unicode { '│' } else { '|' };
147        let fourth_bar = if unicode { '╵' } else { '\'' };
148
149        let line = loc.begin.line + 1;
150        let col = loc.begin.column + 1;
151        writeln!(f, "Error: {}", message)?;
152        let padding = vec![' '; format!("{}", line).len() + 1]
153            .iter()
154            .collect::<String>();
155        writeln!(f, "{}{}", padding, first_bar)?;
156        writeln!(
157            f,
158            "{} {} {}",
159            line,
160            second_bar,
161            loc.file.source_line(loc.begin.line)
162        )?;
163        writeln!(
164            f,
165            "{}{} {}{}",
166            padding,
167            third_bar,
168            vec![' '; loc.begin.column].iter().collect::<String>(),
169            vec!['^'; loc.end.column.max(loc.begin.column) - loc.begin.column.min(loc.end.column)]
170                .iter()
171                .collect::<String>()
172        )?;
173        writeln!(f, "{}{}", padding, fourth_bar)?;
174
175        if unicode {
176            writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
177        } else {
178            writeln!(f, "  {} {}:{}  root stylesheet", loc.file.name(), line, col)?;
179        }
180        Ok(())
181    }
182}
183
184impl From<io::Error> for Box<SassError> {
185    #[inline]
186    fn from(error: io::Error) -> Box<SassError> {
187        Box::new(SassError {
188            kind: SassErrorKind::IoError(Arc::new(error)),
189        })
190    }
191}
192
193impl From<FromUtf8Error> for Box<SassError> {
194    #[inline]
195    fn from(error: FromUtf8Error) -> Box<SassError> {
196        Box::new(SassError {
197            kind: SassErrorKind::FromUtf8Error(format!(
198                "Invalid UTF-8 character \"\\x{:X?}\"",
199                error.as_bytes()[0]
200            )),
201        })
202    }
203}
204
205impl From<(&str, Span)> for Box<SassError> {
206    #[inline]
207    fn from(error: (&str, Span)) -> Box<SassError> {
208        Box::new(SassError {
209            kind: SassErrorKind::Raw(error.0.to_owned(), error.1),
210        })
211    }
212}
213
214impl From<(String, Span)> for Box<SassError> {
215    #[inline]
216    fn from(error: (String, Span)) -> Box<SassError> {
217        Box::new(SassError {
218            kind: SassErrorKind::Raw(error.0, error.1),
219        })
220    }
221}
222
223impl Error for SassError {
224    #[inline]
225    fn description(&self) -> &'static str {
226        "Sass parsing error"
227    }
228}