1 use std::{
2     error::Error,
3     fmt::{self, Display},
4     io,
5     rc::Rc,
6     string::FromUtf8Error,
7 };
8 
9 use codemap::{Span, SpanLoc};
10 
11 pub type SassResult<T> = Result<T, Box<SassError>>;
12 
13 /// `SassError`s can be either a structured error
14 /// specific to `grass` or an `io::Error`.
15 ///
16 /// In the former case, the best way to interact with
17 /// the error is to simply print it to the user. The
18 /// `Display` implementation of this kind of error
19 /// mirrors that of the errors `dart-sass` emits, e.g.
20 ///```scss
21 /// Error: $number: foo is not a number.
22 ///     |
23 /// 308 |     color: unit(foo);
24 ///     |                 ^^^
25 ///     |
26 /// ./input.scss:308:17
27 ///```
28 ///
29 /// The file name, line number, and column are structured in
30 /// such a way as to allow Visual Studio Code users to go
31 /// directly to the error by simply clicking the file name.
32 ///
33 /// Note that this is a deviation from the Sass specification.
34 #[derive(Debug, Clone)]
35 pub struct SassError {
36     kind: SassErrorKind,
37 }
38 
39 impl SassError {
raw(self) -> (String, Span)40     pub(crate) fn raw(self) -> (String, Span) {
41         match self.kind {
42             SassErrorKind::Raw(string, span) => (string, span),
43             e => todo!("unable to get raw of {:?}", e),
44         }
45     }
46 
from_loc(message: String, loc: SpanLoc, unicode: bool) -> Self47     pub(crate) const fn from_loc(message: String, loc: SpanLoc, unicode: bool) -> Self {
48         SassError {
49             kind: SassErrorKind::ParseError {
50                 message,
51                 loc,
52                 unicode,
53             },
54         }
55     }
56 }
57 
58 #[derive(Debug, Clone)]
59 enum SassErrorKind {
60     /// A raw error with no additional metadata
61     /// It contains only a `String` message and
62     /// a span
63     Raw(String, Span),
64     ParseError {
65         message: String,
66         loc: SpanLoc,
67         unicode: bool,
68     },
69     // we put IoErrors in an `Rc` to allow it to be
70     // cloneable
71     IoError(Rc<io::Error>),
72     FromUtf8Error(String),
73 }
74 
75 impl Display for SassError {
76     // TODO: trim whitespace from start of line shown in error
77     // TODO: color errors
78     // TODO: integrate with codemap-diagnostics
79     #[inline]
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result80     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81         let (message, loc, unicode) = match &self.kind {
82             SassErrorKind::ParseError {
83                 message,
84                 loc,
85                 unicode,
86             } => (message, loc, *unicode),
87             SassErrorKind::FromUtf8Error(s) => return writeln!(f, "Error: {}", s),
88             SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s),
89             SassErrorKind::Raw(..) => todo!(),
90         };
91 
92         let first_bar = if unicode { '╷' } else { '|' };
93         let second_bar = if unicode { '│' } else { '|' };
94         let third_bar = if unicode { '│' } else { '|' };
95         let fourth_bar = if unicode { '╵' } else { '|' };
96 
97         let line = loc.begin.line + 1;
98         let col = loc.begin.column + 1;
99         writeln!(f, "Error: {}", message)?;
100         let padding = vec![' '; format!("{}", line).len() + 1]
101             .iter()
102             .collect::<String>();
103         writeln!(f, "{}{}", padding, first_bar)?;
104         writeln!(
105             f,
106             "{} {} {}",
107             line,
108             second_bar,
109             loc.file.source_line(loc.begin.line)
110         )?;
111         writeln!(
112             f,
113             "{}{} {}{}",
114             padding,
115             third_bar,
116             vec![' '; loc.begin.column].iter().collect::<String>(),
117             vec!['^'; loc.end.column.max(loc.begin.column) - loc.begin.column.min(loc.end.column)]
118                 .iter()
119                 .collect::<String>()
120         )?;
121         writeln!(f, "{}{}", padding, fourth_bar)?;
122         writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
123         Ok(())
124     }
125 }
126 
127 impl From<io::Error> for Box<SassError> {
128     #[inline]
from(error: io::Error) -> Box<SassError>129     fn from(error: io::Error) -> Box<SassError> {
130         Box::new(SassError {
131             kind: SassErrorKind::IoError(Rc::new(error)),
132         })
133     }
134 }
135 
136 impl From<FromUtf8Error> for Box<SassError> {
137     #[inline]
from(error: FromUtf8Error) -> Box<SassError>138     fn from(error: FromUtf8Error) -> Box<SassError> {
139         Box::new(SassError {
140             kind: SassErrorKind::FromUtf8Error(format!(
141                 "Invalid UTF-8 character \"\\x{:X?}\"",
142                 error.as_bytes()[0]
143             )),
144         })
145     }
146 }
147 
148 impl From<(&str, Span)> for Box<SassError> {
149     #[inline]
from(error: (&str, Span)) -> Box<SassError>150     fn from(error: (&str, Span)) -> Box<SassError> {
151         Box::new(SassError {
152             kind: SassErrorKind::Raw(error.0.to_owned(), error.1),
153         })
154     }
155 }
156 
157 impl From<(String, Span)> for Box<SassError> {
158     #[inline]
from(error: (String, Span)) -> Box<SassError>159     fn from(error: (String, Span)) -> Box<SassError> {
160         Box::new(SassError {
161             kind: SassErrorKind::Raw(error.0, error.1),
162         })
163     }
164 }
165 
166 impl Error for SassError {
167     #[inline]
description(&self) -> &'static str168     fn description(&self) -> &'static str {
169         "Sass parsing error"
170     }
171 }
172