Skip to main content

whitaker_common/rstest/
span.rs

1//! Pure helpers for recovering user-editable spans from macro-heavy frame chains.
2
3/// A single span-recovery frame and whether it still comes from macro expansion.
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct SpanRecoveryFrame<T> {
6    value: T,
7    from_expansion: bool,
8}
9
10impl<T> SpanRecoveryFrame<T> {
11    /// Builds a recovery frame from a value and expansion flag.
12    ///
13    /// # Examples
14    ///
15    /// ```
16    /// use whitaker_common::rstest::SpanRecoveryFrame;
17    /// use whitaker_common::span::{SourceLocation, SourceSpan};
18    ///
19    /// let span = SourceSpan::new(SourceLocation::new(3, 1), SourceLocation::new(3, 8))
20    ///     .expect("example span should be valid");
21    /// let frame = SpanRecoveryFrame::new(span, true);
22    ///
23    /// assert!(frame.from_expansion());
24    /// ```
25    #[must_use]
26    pub const fn new(value: T, from_expansion: bool) -> Self {
27        Self {
28            value,
29            from_expansion,
30        }
31    }
32
33    /// Returns the stored frame value.
34    #[must_use]
35    pub const fn value(&self) -> &T {
36        &self.value
37    }
38
39    /// Consumes the frame and returns the stored value.
40    #[must_use]
41    pub fn into_value(self) -> T {
42        self.value
43    }
44
45    /// Returns whether the frame still originates from macro expansion.
46    #[must_use]
47    pub const fn from_expansion(&self) -> bool {
48        self.from_expansion
49    }
50}
51
52/// The result of recovering a user-editable span from an ordered frame chain.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum UserEditableSpan<T> {
55    /// The original frame was already user-editable.
56    Direct(T),
57    /// A later frame recovered a user-editable span.
58    Recovered(T),
59    /// No frame in the chain pointed at user-editable code.
60    MacroOnly,
61}
62
63impl<T> UserEditableSpan<T> {
64    /// Converts the recovery result into an optional recovered value.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use whitaker_common::rstest::UserEditableSpan;
70    /// use whitaker_common::span::{SourceLocation, SourceSpan};
71    ///
72    /// let span = SourceSpan::new(SourceLocation::new(5, 1), SourceLocation::new(5, 7))
73    ///     .expect("example span should be valid");
74    ///
75    /// assert_eq!(UserEditableSpan::Recovered(span).into_option(), Some(span));
76    /// assert_eq!(UserEditableSpan::<SourceSpan>::MacroOnly.into_option(), None);
77    /// ```
78    #[must_use]
79    pub fn into_option(self) -> Option<T> {
80        match self {
81            Self::Direct(value) | Self::Recovered(value) => Some(value),
82            Self::MacroOnly => None,
83        }
84    }
85}
86
87/// Recovers the first user-editable frame from an ordered recovery chain.
88///
89/// The first frame represents the original location. Later frames are fallback
90/// candidates such as macro invocation sites or expansion call-sites.
91///
92/// # Examples
93///
94/// ```
95/// use whitaker_common::rstest::{SpanRecoveryFrame, UserEditableSpan, recover_user_editable_span};
96/// use whitaker_common::span::{SourceLocation, SourceSpan};
97///
98/// let macro_span = SourceSpan::new(SourceLocation::new(2, 1), SourceLocation::new(2, 5))
99///     .expect("example span should be valid");
100/// let user_span = SourceSpan::new(SourceLocation::new(10, 1), SourceLocation::new(10, 12))
101///     .expect("example span should be valid");
102///
103/// let recovered = recover_user_editable_span(&[
104///     SpanRecoveryFrame::new(macro_span, true),
105///     SpanRecoveryFrame::new(user_span, false),
106/// ]);
107///
108/// assert_eq!(recovered, UserEditableSpan::Recovered(user_span));
109/// ```
110#[must_use]
111pub fn recover_user_editable_span<T: Clone>(
112    frames: &[SpanRecoveryFrame<T>],
113) -> UserEditableSpan<T> {
114    frames
115        .iter()
116        .enumerate()
117        .find(|(_, frame)| !frame.from_expansion())
118        .map_or(UserEditableSpan::MacroOnly, |(index, frame)| {
119            if index == 0 {
120                UserEditableSpan::Direct(frame.value().clone())
121            } else {
122                UserEditableSpan::Recovered(frame.value().clone())
123            }
124        })
125}