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}