doc_chunks/
span.rs

1//! `Span` annotation, independent yet compatible with `proc_macro2::Span`
2//!
3//! Re-uses `LineColumn`, where `.line` is 1-indexed, and `.column`s are
4//! 0-indexed, `.end` is inclusive.
5
6use super::TrimmedLiteral;
7use crate::util;
8use crate::Range;
9pub use proc_macro2::LineColumn;
10
11use std::hash::{Hash, Hasher};
12
13use crate::errors::*;
14
15use std::convert::TryFrom;
16
17use super::CheckableChunk;
18
19/// Relative span in relation to the beginning of a doc comment.
20///
21/// Line values are 1-indexed relative, lines are inclusive. Column values in
22/// UTF-8 characters in a line, 0-indexed and inclusive.
23#[derive(Clone, Debug, Copy, PartialEq, Eq)]
24pub struct Span {
25    /// Start of the span, inclusive, see
26    /// [`LineColumn`](proc_macro2::LineColumn).
27    pub start: LineColumn,
28    /// End of the span, inclusive, see [`LineColumn`](proc_macro2::LineColumn).
29    pub end: LineColumn,
30}
31
32impl Hash for Span {
33    fn hash<H: Hasher>(&self, state: &mut H) {
34        self.start.line.hash(state);
35        self.start.column.hash(state);
36        self.end.line.hash(state);
37        self.end.column.hash(state);
38    }
39}
40
41impl Span {
42    /// Converts a span to a range, where `self` is converted to a range
43    /// relative to the passed span `scope`. Only works for literals spanning a
44    /// single line and the scope full contains `self, otherwise an an `Err(..)`
45    /// is returned.
46    pub fn relative_to<X: Into<Span>>(&self, scope: X) -> Result<Range> {
47        let scope: Span = scope.into();
48        let scope: Range = scope.try_into()?;
49        let me: Range = self.try_into()?;
50        if scope.start > me.start {
51            return Err(Error::Span(format!(
52                "start of {me:?} is not inside of {scope:?}",
53            )));
54        }
55        if scope.end < me.end {
56            return Err(Error::Span(format!(
57                "end of {me:?} is not inside of {scope:?}",
58            )));
59        }
60        let offset = me.start - scope.start;
61        let length = me.end - me.start;
62        let range = Range {
63            start: offset,
64            end: offset + length,
65        };
66        Ok(range)
67    }
68
69    /// Check if `self` span covers provided `line` number, which is 1-indexed.
70    pub fn covers_line(&self, line: usize) -> bool {
71        self.end.line <= line && line >= self.start.line
72    }
73
74    /// If this one resembles a single line, returns the a `Some(len)` value.
75    /// For multilines this cannot account for the length.
76    pub fn one_line_len(&self) -> Option<usize> {
77        if self.start.line == self.end.line {
78            Some(self.end.column + 1 - self.start.column)
79        } else {
80            None
81        }
82    }
83
84    ///  Check if `self` covers multiple lines
85    pub fn is_multiline(&self) -> bool {
86        self.start.line != self.end.line
87    }
88
89    /// Convert a given span `self` into a `Range`
90    ///
91    /// The `Chunk` has a associated `Span` (or a set of `Range` -> `Span`
92    /// mappings) which are used to map.
93    pub fn to_content_range(&self, chunk: &CheckableChunk) -> Result<Range> {
94        if chunk.fragment_count() == 0 {
95            return Err(Error::Span("Chunk contains 0 fragments".to_string()));
96        }
97        for (fragment_range, fragment_span) in chunk
98            .iter()
99            // pre-filter to reduce too many calls to `extract_sub_range`
100            .filter(|(fragment_range, fragment_span)| {
101                log::trace!(
102                    "extracting sub from {self:?} ::: {fragment_range:?} -> {fragment_span:?}",
103                );
104                fragment_span.start.line <= self.start.line
105                    && self.end.line <= fragment_span.end.line
106            })
107        {
108            match extract_sub_range_from_span(
109                chunk.as_str(),
110                *fragment_span,
111                fragment_range.clone(),
112                *self,
113            ) {
114                Ok(fragment_sub_range) => return Ok(fragment_sub_range),
115                Err(_e) => continue,
116            }
117        }
118        Err(Error::Span(
119            "The chunk internal map from range to span did not contain an overlapping entry"
120                .to_string(),
121        ))
122    }
123}
124
125use std::convert::{From, TryInto};
126
127impl From<proc_macro2::Span> for Span {
128    fn from(original: proc_macro2::Span) -> Self {
129        Self {
130            start: original.start(),
131            end: original.end(),
132        }
133    }
134}
135
136impl TryInto<Range> for Span {
137    type Error = Error;
138    fn try_into(self) -> Result<Range> {
139        (&self).try_into()
140    }
141}
142
143impl TryInto<Range> for &Span {
144    type Error = Error;
145    fn try_into(self) -> Result<Range> {
146        if self.start.line == self.end.line {
147            Ok(Range {
148                start: self.start.column,
149                end: self.end.column + 1,
150            })
151        } else {
152            Err(Error::Span(format!(
153                "Start and end are not in the same line {} vs {}",
154                self.start.line, self.end.line
155            )))
156        }
157    }
158}
159
160impl TryFrom<(usize, Range)> for Span {
161    type Error = Error;
162    fn try_from(original: (usize, Range)) -> Result<Self> {
163        if original.1.start < original.1.end {
164            Ok(Self {
165                start: LineColumn {
166                    line: original.0,
167                    column: original.1.start,
168                },
169                end: LineColumn {
170                    line: original.0,
171                    column: original.1.end - 1,
172                },
173            })
174        } else {
175            Err(Error::Span(format!(
176                "range must be valid to be converted to a Span {}..{}",
177                original.1.start, original.1.end
178            )))
179        }
180    }
181}
182
183impl TryFrom<(usize, std::ops::RangeInclusive<usize>)> for Span {
184    type Error = Error;
185    fn try_from(original: (usize, std::ops::RangeInclusive<usize>)) -> Result<Self> {
186        if original.1.start() <= original.1.end() {
187            Ok(Self {
188                start: LineColumn {
189                    line: original.0,
190                    column: *original.1.start(),
191                },
192                end: LineColumn {
193                    line: original.0,
194                    column: *original.1.end(),
195                },
196            })
197        } else {
198            Err(Error::Span(format!(
199                "range must be valid to be converted to a Span {}..{}",
200                original.1.start(),
201                original.1.end()
202            )))
203        }
204    }
205}
206
207impl From<&TrimmedLiteral> for Span {
208    fn from(literal: &TrimmedLiteral) -> Self {
209        literal.span()
210    }
211}
212
213// impl From<(usize, Range)> for Span {
214//     fn from(original: (usize, Range)) -> Self {
215//         Self::try_from(original).unwrap()
216//     }
217// }
218
219/// extract a `Range` which maps to `self` as `span` maps to `range`, where
220/// `range` is relative to `full_content`
221fn extract_sub_range_from_span(
222    full_content: &str,
223    span: Span,
224    range: Range,
225    sub_span: Span,
226) -> Result<Range> {
227    if let Some(span_len) = span.one_line_len() {
228        debug_assert_eq!(range.len(), span_len);
229    }
230
231    // extract the fragment of interest to which both `range` and `span` correspond.
232    let s = util::sub_chars(full_content, range.clone());
233    let offset = range.start;
234    // relative to the range given / offset
235    let mut start = 0usize;
236    let mut end = 0usize;
237    for (_c, _byte_offset, idx, LineColumn { line, column }) in
238        util::iter_with_line_column_from(s.as_str(), span.start)
239    {
240        if line < sub_span.start.line {
241            continue;
242        }
243        if line > sub_span.end.line {
244            return Err(Error::Span(format!(
245                "range must be valid to be converted to a Span {}..{}",
246                range.start, range.end
247            )));
248        }
249
250        if line == sub_span.start.line && column < sub_span.start.column {
251            continue;
252        }
253
254        if line >= sub_span.end.line && column > sub_span.end.column {
255            return Err(Error::Span(
256                "Moved beyond anticipated column and last line".to_string(),
257            ));
258        }
259        if line == sub_span.start.line && column == sub_span.start.column {
260            start = idx;
261            // do not continue, the first line/column could be the last one too!
262        }
263        end = idx;
264        // if the iterations go to the end of the string, the condition will never be met inside the loop
265        if line == sub_span.end.line && column == sub_span.end.column {
266            break;
267        }
268
269        if line > sub_span.end.line {
270            return Err(Error::Span("Moved beyond anticipated line".to_string()));
271        }
272
273        if line >= sub_span.end.line && column > sub_span.end.column {
274            return Err(Error::Span(
275                "Moved beyond anticipated column and last line".to_string(),
276            ));
277        }
278    }
279
280    let sub_range = (offset + start)..(offset + end + 1);
281    assert!(sub_range.len() <= range.len());
282
283    if let Some(sub_span_len) = sub_span.one_line_len() {
284        debug_assert_eq!(sub_range.len(), sub_span_len);
285    }
286
287    Ok(sub_range)
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    use crate::literalset::testhelper::gen_literal_set;
295    use crate::util::load_span_from;
296    use crate::{chyrp_dbg, chyrp_up, fluff_up};
297    use crate::{LineColumn, Range, Span};
298
299    #[test]
300    fn span_to_range_singleline() {
301        let _ = env_logger::builder()
302            .is_test(true)
303            .filter(None, log::LevelFilter::Trace)
304            .try_init();
305
306        const CONTENT: &str = fluff_up!("Itsyou!!", " ", "Game-Over!!", " ");
307        let set = gen_literal_set(CONTENT);
308        let chunk = dbg!(CheckableChunk::from_literalset(set));
309
310        // assuming a `///<space>` comment
311        const TRIPPLE_SLASH_PLUS_SPACE: usize = 4;
312
313        // within a file
314        const INPUTS: &[Span] = &[
315            Span {
316                start: LineColumn {
317                    line: 1usize,
318                    column: 3usize + TRIPPLE_SLASH_PLUS_SPACE,
319                },
320                end: LineColumn {
321                    line: 1usize,
322                    column: 7usize + TRIPPLE_SLASH_PLUS_SPACE,
323                },
324            },
325            Span {
326                start: LineColumn {
327                    line: 3usize,
328                    column: 0usize + TRIPPLE_SLASH_PLUS_SPACE,
329                },
330                end: LineColumn {
331                    line: 3usize,
332                    column: 8usize + TRIPPLE_SLASH_PLUS_SPACE,
333                },
334            },
335        ];
336
337        // ranges to be used with `chunk.as_str()`
338        // remember that ///<space> counts towards the range!
339        // and that newlines are also one char
340        const EXPECTED_RANGE: &[Range] = &[4..9, 14..23];
341
342        // note that this may only be single lines, since `///` implies separate literals
343        // and as such multiple spans
344        const FRAGMENT_STR: &[&'static str] = &["you!!", "Game-Over"];
345
346        for (input, expected, fragment) in itertools::cons_tuples(
347            INPUTS
348                .iter()
349                .zip(EXPECTED_RANGE.iter())
350                .zip(FRAGMENT_STR.iter()),
351        ) {
352            log::trace!(
353                ">>>>>>>>>>>>>>>>\ninput: {input:?}\nexpected: {expected:?}\nfragment:>{fragment}<",
354            );
355            let range = input
356                .to_content_range(&chunk)
357                .expect("Inputs are sane, conversion must work.");
358            assert_eq!(range, *expected);
359            // make sure the span covers what we expect it to cover
360            assert_eq!(
361                load_span_from(CONTENT.as_bytes(), *input).unwrap(),
362                fragment.to_owned()
363            );
364            assert_eq!(&(&chunk.as_str()[range]), fragment);
365        }
366    }
367
368    #[test]
369    fn span_to_range_multiline() {
370        let _ = env_logger::builder()
371            .is_test(true)
372            .filter(None, log::LevelFilter::Trace)
373            .try_init();
374
375        chyrp_dbg!("Xy fff?? Not.., you again!", "", "AlphaOmega", "");
376        const CONTENT: &str = chyrp_up!("Xy fff?? Not.., you again!", "", "AlphaOmega", "");
377        let set = gen_literal_set(dbg!(CONTENT));
378        let chunk = dbg!(CheckableChunk::from_literalset(set));
379
380        // assuming a `#[doc=r#"` comment
381        const HASH_BRACKET_DOC_EQ_RAW_HASH_QUOTE: usize = 9;
382        // const QUOTE_HASH: usize = 2;
383
384        // within a file
385        const INPUTS: &[Span] = &[
386            // full
387            Span {
388                start: LineColumn {
389                    line: 1usize,
390                    column: 0usize + HASH_BRACKET_DOC_EQ_RAW_HASH_QUOTE,
391                },
392                end: LineColumn {
393                    line: 3usize,
394                    column: 10usize,
395                },
396            },
397            // sub
398            Span {
399                start: LineColumn {
400                    line: 1usize,
401                    column: 3usize + HASH_BRACKET_DOC_EQ_RAW_HASH_QUOTE,
402                },
403                end: LineColumn {
404                    line: 1usize,
405                    column: 7usize + HASH_BRACKET_DOC_EQ_RAW_HASH_QUOTE,
406                },
407            },
408            Span {
409                start: LineColumn {
410                    line: 3usize,
411                    column: 0usize,
412                },
413                end: LineColumn {
414                    line: 3usize,
415                    column: 4usize,
416                },
417            },
418        ];
419
420        const EXPECTED_RANGE: &[Range] = &[0..(26 + 1 + 0 + 1 + 10 + 1), 3..8, 28..33];
421
422        const FRAGMENT_STR: &[&'static str] = &[
423            r#"Xy fff?? Not.., you again!
424
425AlphaOmega
426"#,
427            "fff??",
428            "Alpha",
429        ];
430
431        for (input, expected, fragment) in itertools::cons_tuples(
432            INPUTS
433                .iter()
434                .zip(EXPECTED_RANGE.iter())
435                .zip(FRAGMENT_STR.iter()),
436        ) {
437            log::trace!(
438                ">>>>>>>>>>>>>>>>\ninput: {input:?}\nexpected: {expected:?}\nfragment:>{fragment}<",
439            );
440
441            let range = dbg!(input)
442                .to_content_range(&chunk)
443                .expect("Inputs are sane, conversion must work. qed");
444            assert_eq!(range, *expected);
445
446            assert_eq!(
447                load_span_from(CONTENT.as_bytes(), *input).unwrap(),
448                fragment.to_owned()
449            );
450            assert_eq!(&(&chunk.as_str()[range]), fragment);
451        }
452    }
453
454    #[test]
455    fn extraction_fluff() {
456        const CHUNK_S: &str = r#" one
457 two
458 three"#;
459        const FRAGMENT_SPAN: Span = Span {
460            start: LineColumn { line: 1, column: 3 },
461            end: LineColumn { line: 1, column: 6 },
462        };
463        const FRAGMENT_RANGE: Range = 0..4;
464
465        const FRAGMENT_SUB_SPAN: Span = Span {
466            start: LineColumn { line: 1, column: 5 },
467            end: LineColumn { line: 1, column: 6 },
468        };
469        let range = dbg!(extract_sub_range_from_span(
470            CHUNK_S,
471            FRAGMENT_SPAN,
472            FRAGMENT_RANGE,
473            FRAGMENT_SUB_SPAN,
474        )
475        .expect("Must be able to extract trivial sub span"));
476        assert_eq!(&CHUNK_S[dbg!(range.clone())], "ne");
477        assert_eq!(range, 2..4);
478    }
479
480    #[test]
481    fn extraction_chyrp() {
482        const CHUNK_S: &str = r#"one
483two
484three"#;
485        const FRAGMENT_SPAN: Span = Span {
486            start: LineColumn {
487                line: 1,
488                column: 9 + 2,
489            },
490            end: LineColumn { line: 3, column: 5 },
491        };
492        const FRAGMENT_RANGE: Range = 0..(3 + 1 + 3 + 5);
493
494        {
495            const FRAGMENT_SUB_SPAN: Span = Span {
496                start: LineColumn {
497                    line: 1,
498                    column: 9 + 2 + 1,
499                },
500                end: LineColumn {
501                    line: 1,
502                    column: 9 + 2 + 1 + 1,
503                },
504            };
505            let range = dbg!(extract_sub_range_from_span(
506                CHUNK_S,
507                FRAGMENT_SPAN,
508                FRAGMENT_RANGE,
509                FRAGMENT_SUB_SPAN,
510            )
511            .expect("Must be able to extract trivial sub span"));
512            assert_eq!(&CHUNK_S[dbg!(range.clone())], "ne");
513            assert_eq!(range, 1..3);
514        }
515        {
516            const FRAGMENT_SUB_SPAN: Span = Span {
517                start: LineColumn { line: 2, column: 1 },
518                end: LineColumn { line: 2, column: 2 },
519            };
520            let range = dbg!(extract_sub_range_from_span(
521                CHUNK_S,
522                FRAGMENT_SPAN,
523                FRAGMENT_RANGE,
524                FRAGMENT_SUB_SPAN,
525            )
526            .expect("Must be able to extract trivial sub span"));
527            assert_eq!(&CHUNK_S[dbg!(range.clone())], "wo");
528            assert_eq!(range, 5..7);
529        }
530    }
531}