Skip to main content

source_lang/
map.rs

1//! The source map: many sources laid out across one global position space.
2
3use alloc::boxed::Box;
4use alloc::vec::Vec;
5
6use span_lang::{BytePos, LineCol, Span};
7
8use crate::{SourceFile, SourceId, SourceMapError};
9
10/// A collection of sources laid out end to end in a single global position space.
11///
12/// A `SourceMap` is the multi-file coordinate layer of a front-end. Each source
13/// added to it gets a stable [`SourceId`] and a non-overlapping range in one
14/// shared position space, so a single global [`BytePos`] names a point across the
15/// whole project. [`locate`](SourceMap::locate) maps such a position back to its
16/// `(SourceId, local offset)` — the inverse of the layout — which is how a
17/// diagnostic rendered against a global span knows *which file* to point at.
18///
19/// # Layout
20///
21/// Sources are placed in the order they are added: the first occupies
22/// `0..len₀`, the next `len₀..len₀ + len₁`, and so on. Because the bases only
23/// increase, the internal list is always sorted by start offset, so a lookup is a
24/// binary search over it — `O(log files)` — with no separate index to maintain.
25/// The whole space is 32 bits wide, the same envelope a single
26/// [`BytePos`] addresses, so the combined length of every source is capped at
27/// `u32::MAX`; overrunning it is the [`SpaceExhausted`] error, never a silent
28/// wrap into a neighbour's range.
29///
30/// [`SpaceExhausted`]: SourceMapError::SpaceExhausted
31///
32/// # Examples
33///
34/// ```
35/// use source_lang::{BytePos, SourceMap};
36///
37/// let mut map = SourceMap::new();
38/// let main = map.add("main.rs", "fn main() {}").expect("fits"); // global 0..12
39/// let util = map.add("util.rs", "fn helper() {}").expect("fits"); // global 12..26
40///
41/// // A global position resolves to the file it lands in and the local offset.
42/// let (id, local) = map.locate(BytePos::new(13)).expect("inside util.rs");
43/// assert_eq!(id, util);
44/// assert_eq!(local, BytePos::new(1)); // 13 - 12
45/// assert_eq!(map.source(id).unwrap().name(), "util.rs");
46///
47/// // Position 0 is the very start of the first file.
48/// assert_eq!(map.locate(BytePos::new(0)).unwrap().0, main);
49///
50/// // Anything past the last byte belongs to no file.
51/// assert_eq!(map.locate(BytePos::new(26)), None);
52/// ```
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct SourceMap {
55    /// Sources in insertion order; always sorted by `span().start()` because
56    /// each new base is the previous high-water mark.
57    files: Vec<SourceFile>,
58    /// The next free global offset — the exclusive end of the last source's
59    /// range, and the base the next source will be placed at.
60    next_base: u32,
61    /// The largest a single source may be, in bytes. Defaults to `u32::MAX`; a
62    /// smaller value bounds how much one untrusted input can load.
63    max_source_len: u32,
64}
65
66impl Default for SourceMap {
67    #[inline]
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl SourceMap {
74    /// Creates an empty map whose global position space starts at `0`.
75    ///
76    /// The per-source size ceiling starts at `u32::MAX`; lower it with
77    /// [`set_max_source_len`](SourceMap::set_max_source_len) to bound untrusted
78    /// input.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use source_lang::SourceMap;
84    ///
85    /// let map = SourceMap::new();
86    /// assert!(map.is_empty());
87    /// ```
88    #[inline]
89    #[must_use]
90    pub const fn new() -> Self {
91        Self {
92            files: Vec::new(),
93            next_base: 0,
94            max_source_len: u32::MAX,
95        }
96    }
97
98    /// Creates an empty map with room for `capacity` sources preallocated.
99    ///
100    /// A hint only: it sizes the internal list so that adding up to `capacity`
101    /// sources does not reallocate, which matters when the source count is known
102    /// up front. The global position space still starts empty.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use source_lang::SourceMap;
108    ///
109    /// let mut map = SourceMap::with_capacity(2);
110    /// map.add("a", "x").expect("fits");
111    /// map.add("b", "y").expect("fits");
112    /// assert_eq!(map.len(), 2);
113    /// ```
114    #[inline]
115    #[must_use]
116    pub fn with_capacity(capacity: usize) -> Self {
117        Self {
118            files: Vec::with_capacity(capacity),
119            next_base: 0,
120            max_source_len: u32::MAX,
121        }
122    }
123
124    /// Returns the current per-source size ceiling, in bytes.
125    ///
126    /// A source longer than this is rejected with
127    /// [`SourceMapError::Oversize`] before it consumes any global space. The
128    /// default is `u32::MAX`.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use source_lang::SourceMap;
134    ///
135    /// let map = SourceMap::new();
136    /// assert_eq!(map.max_source_len(), u32::MAX);
137    /// ```
138    #[inline]
139    #[must_use]
140    pub const fn max_source_len(&self) -> u32 {
141        self.max_source_len
142    }
143
144    /// Sets the largest a single source may be, in bytes.
145    ///
146    /// Use it to bound how much one untrusted input — a file named on a command
147    /// line, a buffer from the network — can pull into memory. The limit applies
148    /// to every later [`add`](SourceMap::add), [`add_bytes`](SourceMap::add_bytes),
149    /// and [`add_file`](SourceMap::add_file); for a file it is checked against the
150    /// path's metadata before any bytes are read. Sources already in the map are
151    /// unaffected.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use source_lang::{SourceMap, SourceMapError};
157    ///
158    /// let mut map = SourceMap::new();
159    /// map.set_max_source_len(8);
160    ///
161    /// assert!(map.add("ok", "12345678").is_ok()); // exactly 8 bytes
162    /// let err = map.add("big", "123456789").unwrap_err(); // 9 bytes
163    /// assert!(matches!(err, SourceMapError::Oversize { len: 9, .. }));
164    /// ```
165    #[inline]
166    pub fn set_max_source_len(&mut self, max: u32) {
167        self.max_source_len = max;
168    }
169
170    /// Adds a source under `name` with the given `text`, returning its
171    /// [`SourceId`].
172    ///
173    /// The source is appended after every existing one: it takes the range
174    /// `next..next + text.len()` where `next` is the current end of the global
175    /// space. Both `name` and `text` are taken by value (anything that converts
176    /// into a `Box<str>` — a `String` or a `&str`), so the map owns the text and
177    /// callers can borrow it back for the life of the map.
178    ///
179    /// Adding an empty `text` is allowed: it yields a valid id whose source has a
180    /// zero-width span and does not advance the global space, so it can never be
181    /// the target of a [`locate`](SourceMap::locate).
182    ///
183    /// # Errors
184    ///
185    /// Returns [`SourceMapError::SpaceExhausted`] if `text` does not fit in the
186    /// bytes left in the 32-bit global space, or if the map already holds the
187    /// maximum number of sources. The map is left unchanged, so the failure is
188    /// recoverable.
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// use source_lang::SourceMap;
194    ///
195    /// let mut map = SourceMap::new();
196    /// let id = map.add("config.toml", "name = \"demo\"").expect("fits");
197    /// assert_eq!(map.source(id).unwrap().text(), "name = \"demo\"");
198    ///
199    /// // A String works just as well as a &str.
200    /// let owned = String::from("generated");
201    /// let _ = map.add("out.txt", owned).expect("fits");
202    /// ```
203    pub fn add(
204        &mut self,
205        name: impl Into<Box<str>>,
206        text: impl Into<Box<str>>,
207    ) -> Result<SourceId, SourceMapError> {
208        self.push(name.into(), text.into())
209    }
210
211    /// Validates raw bytes as UTF-8 and adds them as a source under `name`.
212    ///
213    /// This is the in-memory counterpart to [`add_file`](SourceMap::add_file):
214    /// both turn untrusted bytes — from a buffer here, from disk there — into a
215    /// stored source through the same checks, so a network buffer and a file on
216    /// disk fail and succeed the same way.
217    ///
218    /// # Errors
219    ///
220    /// - [`SourceMapError::NotUtf8`] if `bytes` are not valid UTF-8.
221    /// - [`SourceMapError::Oversize`] if they exceed
222    ///   [`max_source_len`](SourceMap::max_source_len).
223    /// - [`SourceMapError::SpaceExhausted`] if they do not fit in the remaining
224    ///   global space.
225    ///
226    /// On any error the map is left unchanged.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// use source_lang::{SourceMap, SourceMapError};
232    ///
233    /// let mut map = SourceMap::new();
234    /// let id = map.add_bytes("greeting.txt", b"hello").expect("valid UTF-8");
235    /// assert_eq!(map.source(id).unwrap().text(), "hello");
236    ///
237    /// // A stray binary byte is rejected, not stored as corrupt text.
238    /// let err = map.add_bytes("blob", &[0xff]).unwrap_err();
239    /// assert!(matches!(err, SourceMapError::NotUtf8 { .. }));
240    /// ```
241    pub fn add_bytes(
242        &mut self,
243        name: impl Into<Box<str>>,
244        bytes: &[u8],
245    ) -> Result<SourceId, SourceMapError> {
246        let name = name.into();
247        match core::str::from_utf8(bytes) {
248            Ok(text) => self.push(name, Box::from(text)),
249            Err(_) => Err(SourceMapError::NotUtf8 { name }),
250        }
251    }
252
253    /// Reads a file from disk and adds its contents as a source named by `path`.
254    ///
255    /// The file's size is checked against [`max_source_len`](SourceMap::max_source_len)
256    /// from its metadata *before* a single byte is read, so an oversize file is
257    /// rejected without being loaded into memory. The bytes are then validated as
258    /// UTF-8 and stored. The source's name is the path as given.
259    ///
260    /// # Errors
261    ///
262    /// - [`SourceMapError::Oversize`] if the file's metadata length exceeds
263    ///   [`max_source_len`](SourceMap::max_source_len).
264    /// - [`SourceMapError::Io`] if the path cannot be opened or read (missing
265    ///   file, a directory, permission denied).
266    /// - [`SourceMapError::NotUtf8`] if the contents are not valid UTF-8.
267    /// - [`SourceMapError::SpaceExhausted`] if they do not fit in the remaining
268    ///   global space.
269    ///
270    /// On any error the map is left unchanged.
271    ///
272    /// # Examples
273    ///
274    /// ```no_run
275    /// use source_lang::SourceMap;
276    ///
277    /// let mut map = SourceMap::new();
278    /// let id = map.add_file("src/main.rs")?;
279    /// assert_eq!(map.source(id).unwrap().name(), "src/main.rs");
280    /// # Ok::<(), source_lang::SourceMapError>(())
281    /// ```
282    #[cfg(feature = "std")]
283    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
284    pub fn add_file(
285        &mut self,
286        path: impl AsRef<std::path::Path>,
287    ) -> Result<SourceId, SourceMapError> {
288        use std::io::Read;
289
290        let path = path.as_ref();
291        let name: Box<str> = Box::from(path.to_string_lossy().as_ref());
292
293        let io_err = |e: std::io::Error| SourceMapError::Io {
294            name: name.clone(),
295            kind: e.kind(),
296        };
297
298        // Reject from metadata before reading, so an oversize file never lands in
299        // memory. A file without a queryable length (a pipe, some virtual files)
300        // falls through to the streaming read, which the push guard still bounds.
301        let mut file = std::fs::File::open(path).map_err(io_err)?;
302        if let Ok(meta) = file.metadata() {
303            if meta.len() > u64::from(self.max_source_len) {
304                return Err(SourceMapError::Oversize {
305                    name,
306                    len: meta.len(),
307                });
308            }
309        }
310
311        let mut bytes = Vec::new();
312        let _ = file.read_to_end(&mut bytes).map_err(io_err)?;
313        match core::str::from_utf8(&bytes) {
314            Ok(text) => self.push(name, Box::from(text)),
315            Err(_) => Err(SourceMapError::NotUtf8 { name }),
316        }
317    }
318
319    /// The single insertion seam every loader funnels through: enforce the size
320    /// limits, assign a non-overlapping range, and append. Nothing is mutated
321    /// until both limits pass, so a rejected add leaves the map untouched.
322    fn push(&mut self, name: Box<str>, text: Box<str>) -> Result<SourceId, SourceMapError> {
323        let needed = text.len() as u64;
324        if needed > u64::from(self.max_source_len) {
325            return Err(SourceMapError::Oversize { name, len: needed });
326        }
327
328        let base = self.next_base;
329        // `base` never exceeds `u32::MAX`, so this is the bytes still free.
330        let available = u64::from(u32::MAX - base);
331
332        // The source must fit in the remaining bytes, and the map must have an
333        // unused id left to mint. Both are checked before anything is mutated.
334        let index = u32::try_from(self.files.len());
335        let (len, index) = match (needed <= available, index) {
336            // `needed <= available <= u32::MAX`, so the narrowing is lossless.
337            (true, Ok(index)) => (needed as u32, index),
338            _ => return Err(SourceMapError::SpaceExhausted { needed, available }),
339        };
340
341        // `base + len <= base + available == u32::MAX`, so this cannot overflow.
342        let end = base + len;
343        let span = Span::new(base, end);
344        let id = SourceId::from_index(index);
345        self.files.push(SourceFile::new(name, text, span));
346        self.next_base = end;
347        Ok(id)
348    }
349
350    /// Resolves a global position to the source it falls in and the local offset
351    /// within that source.
352    ///
353    /// The returned [`BytePos`] is `pos` minus the source's base, i.e. the offset
354    /// into [`SourceFile::text`]. Resolution is a binary search over the sources'
355    /// start offsets, so it is `O(log files)` and borrows the located source
356    /// rather than copying it.
357    ///
358    /// Returns `None` when `pos` belongs to no source: past the end of the last
359    /// one, or — since a zero-width source contains no position — at the exact
360    /// offset of an empty source. The membership is half-open: a source covering
361    /// `start..end` contains `start` but not `end`, so the boundary between two
362    /// adjacent sources resolves to the second, never to both.
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// use source_lang::{BytePos, SourceMap};
368    ///
369    /// let mut map = SourceMap::new();
370    /// let a = map.add("a", "abc").expect("fits");  // 0..3
371    /// let b = map.add("b", "de").expect("fits");   // 3..5
372    ///
373    /// assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
374    /// // The shared boundary at 3 is the start of `b`, not the end of `a`.
375    /// assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
376    /// assert_eq!(map.locate(BytePos::new(5)), None);
377    /// ```
378    #[must_use]
379    pub fn locate(&self, pos: BytePos) -> Option<(SourceId, BytePos)> {
380        let at = pos.to_u32();
381
382        // The list is sorted by start offset, so the last source whose range
383        // begins at or before `at` is the only one that can contain it.
384        let after = self
385            .files
386            .partition_point(|f| f.span().start().to_u32() <= at);
387        let index = after.checked_sub(1)?;
388        let file = &self.files[index];
389
390        if file.span().contains(pos) {
391            let local = at - file.span().start().to_u32();
392            // `index < files.len() <= u32::MAX`, so the cast is lossless.
393            Some((SourceId::from_index(index as u32), BytePos::new(local)))
394        } else {
395            None
396        }
397    }
398
399    /// Resolves a global position to its source and 1-based line/column.
400    ///
401    /// This is [`locate`](SourceMap::locate) composed with `span-lang`'s line
402    /// index: the position is mapped to its source and local offset, then that
403    /// offset is turned into a [`LineCol`] within the source's own text. The
404    /// column counts Unicode scalar values, so a multi-byte character advances
405    /// the column by one, not by its byte width.
406    ///
407    /// Returns `None` exactly when [`locate`](SourceMap::locate) does — for a
408    /// position past the end of the last source, or at a zero-width source.
409    ///
410    /// Each call builds a line index over the located source, an `O(source len)`
411    /// scan. To resolve many positions in the same source, take a reusable index
412    /// once with [`SourceFile::line_index`] instead.
413    ///
414    /// # Examples
415    ///
416    /// ```
417    /// use source_lang::{BytePos, LineCol, SourceMap};
418    ///
419    /// let mut map = SourceMap::new();
420    /// map.add("a.rs", "fn a() {}").expect("fits"); // 0..9
421    /// let b = map.add("b.rs", "let x = 1;\nlet y = 2;").expect("fits"); // 9..30
422    ///
423    /// // Global 20 is the second line of b.rs ("let y = 2;").
424    /// let (id, lc) = map.line_col(BytePos::new(20)).expect("in range");
425    /// assert_eq!(id, b);
426    /// assert_eq!(lc, LineCol::new(2, 1));
427    /// ```
428    #[must_use]
429    pub fn line_col(&self, pos: BytePos) -> Option<(SourceId, LineCol)> {
430        let (id, local) = self.locate(pos)?;
431        // `locate` returned this id, so the source is present.
432        let line_col = self.files[id.to_u32() as usize]
433            .line_index()
434            .line_col(local);
435        Some((id, line_col))
436    }
437
438    /// Borrows the source named by `id`, or `None` if the id is not from this map.
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use source_lang::SourceMap;
444    ///
445    /// let mut map = SourceMap::new();
446    /// let id = map.add("readme.md", "# title").expect("fits");
447    /// assert_eq!(map.source(id).unwrap().name(), "readme.md");
448    /// ```
449    #[inline]
450    #[must_use]
451    pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
452        self.files.get(id.to_u32() as usize)
453    }
454
455    /// Returns the number of sources in the map.
456    ///
457    /// # Examples
458    ///
459    /// ```
460    /// use source_lang::SourceMap;
461    ///
462    /// let mut map = SourceMap::new();
463    /// assert_eq!(map.len(), 0);
464    /// map.add("a", "x").expect("fits");
465    /// assert_eq!(map.len(), 1);
466    /// ```
467    #[inline]
468    #[must_use]
469    pub fn len(&self) -> usize {
470        self.files.len()
471    }
472
473    /// Returns `true` if the map holds no sources.
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use source_lang::SourceMap;
479    ///
480    /// let mut map = SourceMap::new();
481    /// assert!(map.is_empty());
482    /// map.add("a", "x").expect("fits");
483    /// assert!(!map.is_empty());
484    /// ```
485    #[inline]
486    #[must_use]
487    pub fn is_empty(&self) -> bool {
488        self.files.is_empty()
489    }
490
491    /// Iterates over the sources in insertion order, pairing each with its id.
492    ///
493    /// The order is also id order (`0`, `1`, …) and global-offset order, so the
494    /// iterator walks the global position space from start to end. Useful for
495    /// listing the loaded files or building a side table keyed by `SourceId`.
496    ///
497    /// # Examples
498    ///
499    /// ```
500    /// use source_lang::SourceMap;
501    ///
502    /// let mut map = SourceMap::new();
503    /// map.add("a.txt", "one").expect("fits");
504    /// map.add("b.txt", "two").expect("fits");
505    ///
506    /// let names: Vec<_> = map.iter().map(|(_, f)| f.name()).collect();
507    /// assert_eq!(names, ["a.txt", "b.txt"]);
508    /// ```
509    pub fn iter(&self) -> impl ExactSizeIterator<Item = (SourceId, &SourceFile)> + '_ {
510        self.files
511            .iter()
512            .enumerate()
513            // `i < files.len() <= u32::MAX`, so the cast is lossless.
514            .map(|(i, file)| (SourceId::from_index(i as u32), file))
515    }
516}
517
518#[cfg(feature = "serde")]
519mod serde_support {
520    //! `serde` for [`SourceMap`]. The wire form is the list of sources — name and
521    //! text — plus the size ceiling; everything else (spans, ids, the high-water
522    //! mark) is derived, so it is regenerated on load rather than trusted from the
523    //! bytes. Deserialisation replays the sources through the same insertion path
524    //! as [`SourceMap::add`], which keeps the non-overlap and unique-id invariants
525    //! intact even if the input was hand-edited or corrupted.
526
527    use alloc::string::String;
528    use alloc::vec::Vec;
529
530    use serde::de::Error as _;
531    use serde::ser::SerializeStruct;
532    use serde::{Deserialize, Deserializer, Serialize, Serializer};
533
534    use super::SourceMap;
535
536    /// Borrowed view of one source, so serialising copies no text.
537    #[derive(Serialize)]
538    struct SourceRef<'a> {
539        name: &'a str,
540        text: &'a str,
541    }
542
543    /// Owned form held only while deserialising, before the map is rebuilt.
544    #[derive(Deserialize)]
545    struct SourceOwned {
546        name: String,
547        text: String,
548    }
549
550    /// Default ceiling for input that predates the field, matching [`SourceMap::new`].
551    fn unbounded() -> u32 {
552        u32::MAX
553    }
554
555    #[derive(Deserialize)]
556    struct MapData {
557        sources: Vec<SourceOwned>,
558        #[serde(default = "unbounded")]
559        max_source_len: u32,
560    }
561
562    impl Serialize for SourceMap {
563        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
564            let sources: Vec<SourceRef<'_>> = self
565                .files
566                .iter()
567                .map(|f| SourceRef {
568                    name: f.name(),
569                    text: f.text(),
570                })
571                .collect();
572            let mut state = serializer.serialize_struct("SourceMap", 2)?;
573            state.serialize_field("sources", &sources)?;
574            state.serialize_field("max_source_len", &self.max_source_len)?;
575            state.end()
576        }
577    }
578
579    impl<'de> Deserialize<'de> for SourceMap {
580        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
581            let data = MapData::deserialize(deserializer)?;
582            // Rebuild through `push` so spans, ids, and the high-water mark are
583            // regenerated and validated. The ceiling is applied only afterwards, so
584            // sources accepted under a looser limit are not rejected on reload.
585            let mut map = SourceMap::with_capacity(data.sources.len());
586            for source in data.sources {
587                let _ = map
588                    .push(source.name.into(), source.text.into())
589                    .map_err(D::Error::custom)?;
590            }
591            map.max_source_len = data.max_source_len;
592            Ok(map)
593        }
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    extern crate alloc;
600    use alloc::format;
601    use alloc::vec::Vec;
602
603    use super::*;
604
605    #[test]
606    fn test_add_assigns_sequential_stable_ids() {
607        let mut map = SourceMap::new();
608        let a = map.add("a", "x").expect("fits");
609        let b = map.add("b", "yy").expect("fits");
610        let c = map.add("c", "zzz").expect("fits");
611        assert_eq!((a.to_u32(), b.to_u32(), c.to_u32()), (0, 1, 2));
612        // Earlier ids still resolve to their original source after later adds.
613        assert_eq!(map.source(a).unwrap().name(), "a");
614        assert_eq!(map.source(b).unwrap().name(), "b");
615    }
616
617    #[test]
618    fn test_layout_is_contiguous_and_non_overlapping() {
619        let mut map = SourceMap::new();
620        map.add("a", "abc").expect("fits"); // 0..3
621        map.add("b", "de").expect("fits"); // 3..5
622        map.add("c", "fghi").expect("fits"); // 5..9
623        let spans: Vec<_> = map.iter().map(|(_, f)| f.span()).collect();
624        assert_eq!(spans[0], Span::new(0, 3));
625        assert_eq!(spans[1], Span::new(3, 5));
626        assert_eq!(spans[2], Span::new(5, 9));
627    }
628
629    #[test]
630    fn test_locate_at_boundaries_zero_one_and_past_end() {
631        let mut map = SourceMap::new();
632        let a = map.add("a", "abc").expect("fits"); // 0..3
633        let b = map.add("b", "de").expect("fits"); // 3..5
634        assert_eq!(map.locate(BytePos::new(0)), Some((a, BytePos::new(0))));
635        assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
636        // The boundary belongs to the second file.
637        assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
638        assert_eq!(map.locate(BytePos::new(4)), Some((b, BytePos::new(1))));
639        // End of the space and beyond are unmapped.
640        assert_eq!(map.locate(BytePos::new(5)), None);
641        assert_eq!(map.locate(BytePos::new(6)), None);
642    }
643
644    #[test]
645    fn test_locate_on_empty_map_is_none() {
646        let map = SourceMap::new();
647        assert_eq!(map.locate(BytePos::new(0)), None);
648    }
649
650    #[test]
651    fn test_empty_source_does_not_advance_space_and_is_unlocatable() {
652        let mut map = SourceMap::new();
653        let a = map.add("a", "ab").expect("fits"); // 0..2
654        let empty = map.add("empty", "").expect("fits"); // 2..2
655        let b = map.add("b", "cd").expect("fits"); // 2..4
656
657        assert!(map.source(empty).unwrap().span().is_empty());
658        // Position 2 is the start of `b`, never the zero-width `empty`.
659        assert_eq!(map.locate(BytePos::new(2)), Some((b, BytePos::new(0))));
660        assert_eq!(map.locate(BytePos::new(1)), Some((a, BytePos::new(1))));
661    }
662
663    #[test]
664    fn test_source_rejects_foreign_id() {
665        let mut map = SourceMap::new();
666        let _ = map.add("a", "x").expect("fits");
667        let mut other = SourceMap::new();
668        let foreign = other.add("b", "y").expect("fits");
669        // `foreign` has index 0, which exists here too, so cross-map ids are not
670        // distinguishable by value — but an out-of-range index is rejected.
671        let beyond = other.add("c", "z").expect("fits");
672        assert!(map.source(beyond).is_none());
673        assert!(map.source(foreign).is_some());
674    }
675
676    #[test]
677    fn test_add_at_space_boundary_accepts_exact_fit() {
678        let mut map = SourceMap::new();
679        // Drive the high-water mark to four bytes below the ceiling without
680        // allocating gigabytes; the field is private to this module's tests.
681        map.next_base = u32::MAX - 4;
682        let id = map.add("edge", "abcd").expect("exactly fills the space");
683        assert_eq!(
684            map.source(id).unwrap().span(),
685            Span::new(u32::MAX - 4, u32::MAX)
686        );
687        assert_eq!(map.next_base, u32::MAX);
688    }
689
690    #[test]
691    fn test_add_past_space_boundary_is_rejected() {
692        let mut map = SourceMap::new();
693        map.next_base = u32::MAX - 4;
694        let err = map.add("edge", "abcde").expect_err("one byte too many");
695        assert_eq!(
696            err,
697            SourceMapError::SpaceExhausted {
698                needed: 5,
699                available: 4,
700            },
701        );
702        // The map is unchanged after a rejected add.
703        assert!(map.is_empty());
704        assert_eq!(map.next_base, u32::MAX - 4);
705    }
706
707    #[test]
708    fn test_add_empty_source_at_full_space_still_succeeds() {
709        let mut map = SourceMap::new();
710        map.next_base = u32::MAX;
711        // Zero bytes fit even when no space remains; one byte does not.
712        let empty = map.add("nothing", "").expect("zero bytes always fit");
713        assert!(map.source(empty).unwrap().span().is_empty());
714        assert!(map.add("one", "x").is_err());
715    }
716
717    #[test]
718    fn test_iter_reports_exact_len_and_pairs_ids_in_order() {
719        let mut map = SourceMap::new();
720        for i in 0..5 {
721            map.add(format!("f{i}"), "..").expect("fits");
722        }
723        let mut iter = map.iter();
724        assert_eq!(iter.len(), 5);
725        let collected: Vec<_> = iter.by_ref().map(|(id, _)| id.to_u32()).collect();
726        assert_eq!(collected, [0, 1, 2, 3, 4]);
727    }
728
729    #[test]
730    fn test_add_bytes_stores_valid_utf8() {
731        let mut map = SourceMap::new();
732        let id = map
733            .add_bytes("greeting", "héllo".as_bytes())
734            .expect("valid");
735        assert_eq!(map.source(id).unwrap().text(), "héllo");
736    }
737
738    #[test]
739    fn test_add_bytes_rejects_invalid_utf8_and_leaves_map_unchanged() {
740        let mut map = SourceMap::new();
741        // A lone continuation byte is not valid UTF-8.
742        let err = map.add_bytes("blob", &[0x68, 0xff, 0x69]).unwrap_err();
743        match err {
744            SourceMapError::NotUtf8 { name } => assert_eq!(&*name, "blob"),
745            other => panic!("expected NotUtf8, got {other:?}"),
746        }
747        assert!(map.is_empty());
748        assert_eq!(map.next_base, 0);
749    }
750
751    #[test]
752    fn test_add_bytes_empty_is_a_zero_width_source() {
753        let mut map = SourceMap::new();
754        let id = map
755            .add_bytes("empty", b"")
756            .expect("zero bytes are valid utf8");
757        assert!(map.source(id).unwrap().span().is_empty());
758    }
759
760    #[test]
761    fn test_max_source_len_rejects_at_the_byte_boundary() {
762        let mut map = SourceMap::new();
763        map.set_max_source_len(4);
764        assert_eq!(map.max_source_len(), 4);
765
766        // Exactly the limit fits; one byte over does not.
767        let ok = map.add("ok", "abcd").expect("exactly the limit");
768        assert_eq!(map.source(ok).unwrap().span().len(), 4);
769
770        let err = map.add("big", "abcde").unwrap_err();
771        match err {
772            SourceMapError::Oversize { name, len } => {
773                assert_eq!(&*name, "big");
774                assert_eq!(len, 5);
775            }
776            other => panic!("expected Oversize, got {other:?}"),
777        }
778        // The rejected add did not advance the space.
779        assert_eq!(map.next_base, 4);
780    }
781
782    #[test]
783    fn test_oversize_is_checked_before_space_exhaustion() {
784        let mut map = SourceMap::new();
785        map.set_max_source_len(2);
786        map.next_base = u32::MAX; // no global space left at all
787        // The per-source ceiling is reported, not space exhaustion.
788        let err = map.add("x", "abc").unwrap_err();
789        assert!(matches!(err, SourceMapError::Oversize { len: 3, .. }));
790    }
791
792    #[test]
793    fn test_line_col_resolves_across_files_and_lines() {
794        let mut map = SourceMap::new();
795        let a = map.add("a", "ab\ncd").expect("fits"); // 0..5, lines 1-2
796        let b = map.add("b", "wx\nyz").expect("fits"); // 5..10, lines 1-2
797
798        // First file, first line.
799        assert_eq!(map.line_col(BytePos::new(0)), Some((a, LineCol::new(1, 1))));
800        // First file, second line, second column ('d').
801        assert_eq!(map.line_col(BytePos::new(4)), Some((a, LineCol::new(2, 2))));
802        // Second file resets to its own line 1, column 1.
803        assert_eq!(map.line_col(BytePos::new(5)), Some((b, LineCol::new(1, 1))));
804        // Second file, second line ('y').
805        assert_eq!(map.line_col(BytePos::new(8)), Some((b, LineCol::new(2, 1))));
806    }
807
808    #[test]
809    fn test_line_col_counts_characters_not_bytes() {
810        let mut map = SourceMap::new();
811        // "αβ" is two characters but four bytes; the second char starts at byte 2.
812        let id = map.add("greek", "αβ").expect("fits");
813        assert_eq!(
814            map.line_col(BytePos::new(2)),
815            Some((id, LineCol::new(1, 2)))
816        );
817    }
818
819    #[test]
820    fn test_line_col_out_of_range_is_none() {
821        let mut map = SourceMap::new();
822        map.add("a", "abc").expect("fits"); // 0..3
823        assert_eq!(map.line_col(BytePos::new(3)), None);
824        assert_eq!(map.line_col(BytePos::new(99)), None);
825    }
826}