grass_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 `grass` 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    pub(crate) fn raw(self) -> (String, Span) {
53        match self.kind {
54            SassErrorKind::Raw(string, span) => (string, span),
55            e => unreachable!("unable to get raw of {:?}", e),
56        }
57    }
58
59    pub(crate) const fn from_loc(message: String, loc: SpanLoc, unicode: bool) -> Self {
60        SassError {
61            kind: SassErrorKind::ParseError {
62                message,
63                loc,
64                unicode,
65            },
66        }
67    }
68}
69
70#[non_exhaustive]
71#[derive(Debug, Clone)]
72pub enum PublicSassErrorKind {
73    ParseError {
74        /// The message related to this parse error.
75        ///
76        /// Error messages should only be used to assist in debugging for the
77        /// end user. They may change significantly between bugfix versions and
78        /// should not be relied on to remain stable.
79        ///
80        /// Error messages do not contain the `Error: ` prefix or pretty-printed
81        /// span and context information as is shown in the `Display` implementation.
82        message: String,
83        loc: SpanLoc,
84
85        /// Whether or not the user allows unicode characters to be emitted in
86        /// error messages.
87        ///
88        /// This is configurable with [`crate::Options::unicode_error_messages`]
89        unicode: bool,
90    },
91
92    /// Sass was unable to find the entry-point file.
93    ///
94    /// Files that cannot be found using `@import`, `@use`, and `@forward` will
95    /// emit [`Self::ParseError`]s
96    IoError(Arc<io::Error>),
97
98    /// The entry-point file or an imported file was not valid UTF-8.
99    FromUtf8Error(String),
100}
101
102#[derive(Debug, Clone)]
103enum SassErrorKind {
104    /// A raw error with no additional metadata
105    /// It contains only a `String` message and
106    /// a span
107    Raw(String, Span),
108    ParseError {
109        message: String,
110        loc: SpanLoc,
111        unicode: bool,
112    },
113    // we put `IoError`s in an `Arc` to allow them to be cloneable
114    IoError(Arc<io::Error>),
115    FromUtf8Error(String),
116}
117
118impl Display for SassError {
119    // TODO: trim whitespace from start of line shown in error
120    // TODO: color errors
121    // TODO: integrate with codemap-diagnostics
122    #[inline]
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        let (message, loc, unicode) = match &self.kind {
125            SassErrorKind::ParseError {
126                message,
127                loc,
128                unicode,
129            } => (message, loc, *unicode),
130            SassErrorKind::FromUtf8Error(..) => return writeln!(f, "Error: Invalid UTF-8."),
131            SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s),
132            SassErrorKind::Raw(..) => unreachable!(),
133        };
134
135        let first_bar = if unicode { '╷' } else { ',' };
136        let second_bar = if unicode { '│' } else { '|' };
137        let third_bar = if unicode { '│' } else { '|' };
138        let fourth_bar = if unicode { '╵' } else { '\'' };
139
140        let line = loc.begin.line + 1;
141        let col = loc.begin.column + 1;
142        writeln!(f, "Error: {}", message)?;
143        let padding = vec![' '; format!("{}", line).len() + 1]
144            .iter()
145            .collect::<String>();
146        writeln!(f, "{}{}", padding, first_bar)?;
147        writeln!(
148            f,
149            "{} {} {}",
150            line,
151            second_bar,
152            loc.file.source_line(loc.begin.line)
153        )?;
154        writeln!(
155            f,
156            "{}{} {}{}",
157            padding,
158            third_bar,
159            vec![' '; loc.begin.column].iter().collect::<String>(),
160            vec!['^'; loc.end.column.max(loc.begin.column) - loc.begin.column.min(loc.end.column)]
161                .iter()
162                .collect::<String>()
163        )?;
164        writeln!(f, "{}{}", padding, fourth_bar)?;
165
166        if unicode {
167            writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
168        } else {
169            writeln!(f, "  {} {}:{}  root stylesheet", loc.file.name(), line, col)?;
170        }
171        Ok(())
172    }
173}
174
175impl From<io::Error> for Box<SassError> {
176    #[inline]
177    fn from(error: io::Error) -> Box<SassError> {
178        Box::new(SassError {
179            kind: SassErrorKind::IoError(Arc::new(error)),
180        })
181    }
182}
183
184impl From<FromUtf8Error> for Box<SassError> {
185    #[inline]
186    fn from(error: FromUtf8Error) -> Box<SassError> {
187        Box::new(SassError {
188            kind: SassErrorKind::FromUtf8Error(format!(
189                "Invalid UTF-8 character \"\\x{:X?}\"",
190                error.as_bytes()[0]
191            )),
192        })
193    }
194}
195
196impl From<(&str, Span)> for Box<SassError> {
197    #[inline]
198    fn from(error: (&str, Span)) -> Box<SassError> {
199        Box::new(SassError {
200            kind: SassErrorKind::Raw(error.0.to_owned(), error.1),
201        })
202    }
203}
204
205impl From<(String, Span)> for Box<SassError> {
206    #[inline]
207    fn from(error: (String, Span)) -> Box<SassError> {
208        Box::new(SassError {
209            kind: SassErrorKind::Raw(error.0, error.1),
210        })
211    }
212}
213
214impl Error for SassError {
215    #[inline]
216    fn description(&self) -> &'static str {
217        "Sass parsing error"
218    }
219}