js_source_scopes/
scope_index.rs

1use std::ops::Range;
2
3use indexmap::IndexSet;
4
5/// An indexed structure of scopes that allows quick lookup by byte offset.
6///
7/// Construction of the index will validate that the scopes are well nested and
8/// parents fully contain their children. A list of scopes that are not well
9/// nested will result in an `Err` on construction.
10///
11/// # Examples
12///
13/// ```
14/// use js_source_scopes::{ScopeIndex, ScopeLookupResult};
15///
16/// let scopes = vec![
17///     (5..25, Some(String::from("parent"))),
18///     (10..15, Some(String::from("child"))),
19///     (20..25, Some(String::from("child2"))),
20///     (30..50, None),
21/// ];
22///
23/// let idx = ScopeIndex::new(scopes).unwrap();
24/// assert_eq!(idx.lookup(3), ScopeLookupResult::Unknown);
25/// assert_eq!(idx.lookup(12), ScopeLookupResult::NamedScope("child"));
26/// assert_eq!(idx.lookup(40), ScopeLookupResult::AnonymousScope);
27/// ```
28#[derive(Debug)]
29pub struct ScopeIndex {
30    names: IndexSet<String>,
31    /// Offset -> Index into `names` (or `u32::MAX` for `None`)
32    ranges: Vec<(u32, u32)>,
33}
34
35impl ScopeIndex {
36    /// Creates a new Scope index from the given list of Scopes.
37    #[tracing::instrument(level = "trace", name = "ScopeIndex::new", skip_all)]
38    pub fn new(mut scopes: Vec<(Range<u32>, Option<String>)>) -> Result<Self, ScopeIndexError> {
39        let mut names = IndexSet::new();
40        let mut ranges = vec![];
41
42        scopes.sort_by_key(|s| s.0.start);
43
44        let needs_zero = scopes.first().map(|s| s.0.start != 0).unwrap_or(false);
45        if needs_zero {
46            ranges.push((0, GLOBAL_SCOPE_SENTINEL));
47        }
48
49        let mut stack: Vec<(Range<u32>, u32)> = vec![];
50
51        for (range, name) in scopes {
52            unwind_scope_stack(&mut ranges, &mut stack, range.clone())?;
53
54            let name_idx = match name {
55                Some(name) => names
56                    .insert_full(name)
57                    .0
58                    .try_into()
59                    .map_err(|_| ScopeIndexError(()))?,
60                None => ANONYMOUS_SCOPE_SENTINEL,
61            };
62
63            ranges.push((range.start, name_idx));
64
65            if let Some(last) = stack.last() {
66                if last.0.end == range.end {
67                    stack.pop();
68                }
69            }
70            stack.push((range, name_idx));
71        }
72
73        // push end markers for the remaining stack
74        while let Some(last) = stack.pop() {
75            // push a new range of the parent
76            let name_idx = stack
77                .last()
78                .map(|prev| prev.1)
79                .unwrap_or(GLOBAL_SCOPE_SENTINEL);
80            ranges.push((last.0.end, name_idx));
81        }
82
83        Ok(Self { names, ranges })
84    }
85
86    /// Looks up the scope corresponding to the given `offset`.
87    pub fn lookup(&self, offset: u32) -> ScopeLookupResult {
88        let range_idx = match self.ranges.binary_search_by_key(&offset, |r| r.0) {
89            Ok(idx) => idx,
90            Err(0) => 0, // this is pretty much unreachable since the first offset is 0
91            Err(idx) => idx - 1,
92        };
93
94        let name_idx = match self.ranges.get(range_idx) {
95            Some(r) => r.1,
96            None => return ScopeLookupResult::Unknown,
97        };
98
99        self.resolve_name(name_idx)
100    }
101
102    fn resolve_name(&self, name_idx: u32) -> ScopeLookupResult {
103        if name_idx == GLOBAL_SCOPE_SENTINEL {
104            ScopeLookupResult::Unknown
105        } else if name_idx == ANONYMOUS_SCOPE_SENTINEL {
106            ScopeLookupResult::AnonymousScope
107        } else {
108            match self.names.get_index(name_idx as usize) {
109                Some(name) => ScopeLookupResult::NamedScope(name.as_str()),
110                None => ScopeLookupResult::Unknown,
111            }
112        }
113    }
114
115    /// Returns an iterator over the scopes in this index and their starting
116    /// offsets.
117    ///
118    /// Scopes are returned in order of their starting offsets.
119    pub fn iter(&self) -> impl Iterator<Item = (u32, ScopeLookupResult)> {
120        self.ranges.iter().map(|r| (r.0, self.resolve_name(r.1)))
121    }
122}
123
124/// The Result of a Scope lookup.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum ScopeLookupResult<'data> {
127    /// A named function scope.
128    NamedScope(&'data str),
129    /// An anonymous function scope for which no name was inferred.
130    AnonymousScope,
131    /// The lookup did not result in any scope match.
132    ///
133    /// This most likely means that the offset belongs to the "global" scope.
134    Unknown,
135}
136
137/// Given a `stack` of ranges, this pushes all entries on the stack
138/// to `ranges` that end before `offset`, and ensures well-nestedness.
139fn unwind_scope_stack(
140    ranges: &mut Vec<(u32, u32)>,
141    stack: &mut Vec<(Range<u32>, u32)>,
142    range: Range<u32>,
143) -> Result<(), ScopeIndexError> {
144    while let Some(last) = stack.pop() {
145        // push a new range of the parent
146        if last.0.end <= range.start {
147            let name_idx = stack
148                .last()
149                .map(|prev| prev.1)
150                .unwrap_or(GLOBAL_SCOPE_SENTINEL);
151            ranges.push((last.0.end, name_idx));
152        } else if last.0.end < range.end {
153            // we have an overlap and improper nesting
154            return Err(ScopeIndexError(()));
155        } else {
156            // re-push to the stack, as it is still our same parent
157            stack.push(last);
158            return Ok(());
159        }
160    }
161    Ok(())
162}
163
164pub(crate) const GLOBAL_SCOPE_SENTINEL: u32 = u32::MAX;
165pub(crate) const ANONYMOUS_SCOPE_SENTINEL: u32 = u32::MAX - 1;
166
167/// An Error that can happen when building a [`ScopeIndex`].
168#[derive(Debug)]
169pub struct ScopeIndexError(());
170
171impl std::error::Error for ScopeIndexError {}
172
173impl std::fmt::Display for ScopeIndexError {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        f.write_str("source could not be converted to source context")
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn invalid_nesting() {
185        let scopes = vec![(0..10, None), (5..15, None)];
186        assert!(ScopeIndex::new(scopes).is_err());
187    }
188
189    #[test]
190    fn scope_index() {
191        let scopes = vec![
192            (5..25, Some(String::from("parent"))),
193            (10..15, Some(String::from("child"))),
194            (20..25, Some(String::from("child2"))),
195            (30..50, None),
196        ];
197
198        let idx = ScopeIndex::new(scopes).unwrap();
199
200        assert_eq!(idx.lookup(3), ScopeLookupResult::Unknown);
201        assert_eq!(idx.lookup(7), ScopeLookupResult::NamedScope("parent"));
202        assert_eq!(idx.lookup(12), ScopeLookupResult::NamedScope("child"));
203        assert_eq!(idx.lookup(17), ScopeLookupResult::NamedScope("parent"));
204        assert_eq!(idx.lookup(22), ScopeLookupResult::NamedScope("child2"));
205        assert_eq!(idx.lookup(25), ScopeLookupResult::Unknown);
206        assert_eq!(idx.lookup(30), ScopeLookupResult::AnonymousScope);
207        assert_eq!(idx.lookup(50), ScopeLookupResult::Unknown);
208    }
209}