rsass/input/
sourcepos.rs

1use crate::input::{SourceFile, SourceName};
2use crate::parser::{DebugBytes, Span};
3use crate::sass::{FormalArgs, Name};
4use std::cmp::Ordering;
5use std::fmt::{self, Write};
6use std::ops::Range;
7
8/// A specific part of input.
9///
10/// This is represented as start and end offsets into [`SourceFile`].
11/// The sourcefile is internally reference counted, so a `SourcePos` can
12/// be cheaply cloned, but does not implement copy.
13#[derive(Clone)]
14pub struct SourcePos {
15    start: usize,
16    end: usize,
17    source: SourceFile,
18}
19
20impl SourcePos {
21    pub(crate) fn new_range(source: SourceFile, range: Range<usize>) -> Self {
22        Self {
23            start: range.start,
24            end: range.end,
25            source,
26        }
27    }
28    pub(crate) fn borrow(&self) -> Span {
29        Span::new_range(&self.source, self.range())
30    }
31    fn range(&self) -> Range<usize> {
32        self.start..self.end
33    }
34    /// This span of input as actual bytes.
35    pub fn fragment(&self) -> &[u8] {
36        &self.source.data()[self.range()]
37    }
38    /// Return true if the pos is at the end of the source.
39    pub fn is_at_end(&self) -> bool {
40        self.start == self.source.data().len()
41    }
42
43    /// Return the line number for (the beginning of) this span.
44    pub fn line_no(&self) -> usize {
45        self.source.data()[0..self.start]
46            .iter()
47            .filter(|c| c == &&b'\n')
48            .count()
49            + 1
50    }
51    fn line_pos(&self) -> usize {
52        let data = self.source.data();
53        let start = (0..self.start)
54            .rev()
55            .find(|i| data.get(*i) == Some(&b'\n'))
56            .map_or(0, |n| n + 1);
57        self.start - start + 1
58    }
59
60    /// Show this source position.
61    ///
62    /// Dislays the line containg the position, highlighting
63    /// the position.
64    /// This is typically used when there is one source position
65    /// relevant for an error.
66    pub fn show(&self, out: &mut impl Write) -> fmt::Result {
67        self.show_impl(out, "", '^', "")?;
68        self.show_files(out)
69    }
70
71    /// Show two source positions together.
72    pub fn show_two(
73        out: &mut fmt::Formatter,
74        one: &Self,
75        one_name: &str,
76        other: &Self,
77        other_name: &str,
78    ) -> fmt::Result {
79        if one.file_url() == other.file_url() {
80            if one < other {
81                Self::show_in_file(out, one, one_name, other, other_name)
82            } else {
83                Self::show_in_file(out, other, other_name, one, one_name)
84            }?;
85        } else {
86            one.show_detail(out, '^', one_name)?;
87            writeln!(out)?;
88            other.show_detail(out, '=', other_name)?;
89        }
90        one.show_files(out)
91    }
92
93    fn show_in_file(
94        out: &mut fmt::Formatter,
95        first: &Self,
96        first_name: &str,
97        second: &Self,
98        second_name: &str,
99    ) -> fmt::Result {
100        let ellipsis = first.line_no() + 1 < second.line_no();
101        let lnw = second.line_no().to_string().len();
102        let lnw = if ellipsis { std::cmp::max(3, lnw) } else { lnw };
103        writeln!(out, "{0:lnw$} ,", "", lnw = lnw)?;
104        first.show_inner(out, lnw, '=', first_name)?;
105        if ellipsis {
106            writeln!(out, "... |")?;
107        }
108        second.show_inner(out, lnw, '^', second_name)?;
109        write!(out, "{0:lnw$} '", "", lnw = lnw)?;
110        Ok(())
111    }
112
113    fn show_detail(
114        &self,
115        out: &mut impl Write,
116        marker: char,
117        what: &str,
118    ) -> fmt::Result {
119        let filename = self.file_url();
120        let filename = if filename.is_empty() {
121            String::new()
122        } else {
123            format!("--> {filename}")
124        };
125        self.show_impl(out, &filename, marker, what)
126    }
127
128    fn show_impl(
129        &self,
130        out: &mut impl Write,
131        arrow: &str,
132        marker: char,
133        what: &str,
134    ) -> fmt::Result {
135        let lnw = self.line_no().to_string().len();
136        writeln!(out, "{0:lnw$} ,{arrow}", "", arrow = arrow, lnw = lnw)?;
137        self.show_inner(out, lnw, marker, what)?;
138        write!(out, "{0:lnw$} '", "", lnw = lnw)
139    }
140    fn show_inner(
141        &self,
142        out: &mut impl Write,
143        lnw: usize,
144        marker: char,
145        what: &str,
146    ) -> fmt::Result {
147        let data = self.source.data();
148        let start = (0..self.start)
149            .rev()
150            .find(|i| data.get(*i) == Some(&b'\n'))
151            .map_or(0, |n| n + 1);
152        let end = (self.start..)
153            .find(|i| data.get(*i).map_or(true, |b| b == &b'\n'))
154            .unwrap();
155        let line = &data[start..end];
156        let line_no = self.line_no();
157        write!(
158            out,
159            "{ln:<lnw$} | {line}\
160             \n{0:lnw$} | {0:>lpos$}{mark}",
161            "",
162            line = String::from_utf8_lossy(line).trim_end_matches('\r'),
163            ln = line_no,
164            lnw = lnw,
165            lpos = self.start - start, // TODO: Count visible char width
166            mark = marker.to_string().repeat((self.end - self.start).max(1)), // DITO
167        )?;
168        if what.is_empty() {
169            writeln!(out)
170        } else {
171            writeln!(out, " {what}")
172        }
173    }
174
175    /// Show the file name of this pos and where it was imported from.
176    fn show_files(&self, out: &mut impl Write) -> fmt::Result {
177        let mut nextpos = Some(self.clone());
178        let mut lines = Vec::new();
179        while let Some(pos) = nextpos {
180            lines.push((
181                format!(
182                    "{file} {row}:{col}",
183                    file = pos.file_url(),
184                    row = pos.line_no(),
185                    col = pos.line_pos(),
186                ),
187                pos.source.source().imported.to_string(),
188            ));
189            nextpos = pos.source.source().imported.next().cloned();
190        }
191        if let Some(whatw) = lines.iter().map(|(what, _why)| what.len()).max()
192        {
193            for (what, why) in lines {
194                write!(
195                    out,
196                    "\n{0:lnw$} {what:whatw$}  {why}",
197                    "",
198                    lnw = self.line_no().to_string().len(),
199                    what = what,
200                    whatw = whatw,
201                    why = why,
202                )?;
203            }
204        }
205        Ok(())
206    }
207
208    /// If self is preceded (on same line) by `s`, include `s` in self.
209    pub(crate) fn opt_back(mut self, s: &str) -> Self {
210        let len = s.len();
211        if self.source.data().get(self.start - len..self.start)
212            == Some(s.as_bytes())
213        {
214            self.start -= len;
215        }
216        self
217    }
218
219    /// If the position is `calc(some-arg)`, change to only `some-arg`.
220    ///
221    /// This is only used to make errors from rsass more similar to
222    /// dart-sass errors.
223    pub(crate) fn opt_in_calc(mut self) -> Self {
224        let s = b"calc(";
225        let part = &self.fragment();
226        if part.starts_with(s) && part.ends_with(b")") {
227            self.start += s.len();
228            self.end -= 1;
229        }
230        self
231    }
232
233    /// Only to make error messages match dart-sass in peculiar cases.
234    pub(crate) fn opt_trail_ws(mut self) -> Self {
235        while self.source.data().get(self.end) == Some(&b' ') {
236            self.end += 1;
237        }
238        self
239    }
240
241    pub(crate) fn in_call(&self, name: &str) -> Self {
242        Self {
243            start: self.start,
244            end: self.end,
245            source: SourceFile::scss_bytes(
246                self.source.data(),
247                SourceName::called(name, self.clone()),
248            ),
249        }
250    }
251
252    /// True if the source of this position is built-in.
253    pub fn is_builtin(&self) -> bool {
254        self.source.source().is_builtin()
255    }
256
257    pub(crate) fn mock_function(
258        name: &Name,
259        args: &FormalArgs,
260        module: &str,
261    ) -> Self {
262        Self::mock_impl("@function", name, args, module)
263    }
264    pub(crate) fn mock_mixin(
265        name: &Name,
266        args: &FormalArgs,
267        module: &str,
268    ) -> Self {
269        Self::mock_impl("@mixin", name, args, module)
270    }
271    fn mock_impl(
272        kind: &str,
273        name: &Name,
274        args: &FormalArgs,
275        module: &str,
276    ) -> Self {
277        let line = format!("{kind} {name}{args} {{");
278        Self {
279            start: kind.len() + 1,
280            end: line.len() - 2,
281            source: SourceFile::scss_bytes(line, SourceName::root(module)),
282        }
283    }
284
285    /// Get the resolved name / url of the file this source is loaded from.
286    pub fn file_url(&self) -> &str {
287        self.source.source().name()
288    }
289}
290
291impl From<SourceFile> for SourcePos {
292    fn from(source: SourceFile) -> Self {
293        Self {
294            start: 0,
295            end: source.data().len(),
296            source,
297        }
298    }
299}
300
301impl PartialOrd for SourcePos {
302    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
303        Some(self.cmp(other))
304    }
305}
306impl Ord for SourcePos {
307    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
308        self.source
309            .cmp(&other.source)
310            .then(self.start.cmp(&other.start))
311            .then(self.end.cmp(&other.end))
312    }
313}
314impl PartialEq for SourcePos {
315    fn eq(&self, other: &Self) -> bool {
316        self.partial_cmp(other) == Some(Ordering::Equal)
317    }
318}
319impl Eq for SourcePos {}
320
321impl std::fmt::Debug for SourcePos {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        f.debug_struct("OwnedSpan")
324            .field("range", &self.range())
325            .field("data", &DebugBytes(self.fragment()))
326            .finish()
327    }
328}