use std::{
fmt::{self, Debug},
hash::{Hash, Hasher},
ops::{Index, IndexMut, Range},
};
use miette::{LabeledSpan, SourceOffset, SourceSpan};
#[cfg(feature = "serialize")]
use serde::{Serialize, Serializer as SerdeSerializer, ser::SerializeMap};
use oxc_allocator::{Allocator, CloneIn, Dummy};
use oxc_ast_macros::ast;
use oxc_estree::ESTree;
#[cfg(feature = "serialize")]
use oxc_estree::ESTreeSpan;
pub const SPAN: Span = Span::new(0, 0);
#[ast(visit)]
#[derive(Default, Clone, Copy, Eq, PartialOrd, Ord)]
#[generate_derive(ESTree)]
#[builder(skip)]
#[content_eq(skip)]
#[estree(
no_type,
flatten,
no_ts_def,
add_ts_def = "interface Span { start: number; end: number; range?: [number, number]; }"
)]
pub struct Span {
#[estree(via = "SpanStart")]
pub start: u32,
#[estree(via = "SpanEnd")]
pub end: u32,
#[estree(skip)]
_align: PointerAlign,
}
impl Span {
#[inline]
pub const fn new(start: u32, end: u32) -> Self {
Self { start, end, _align: PointerAlign::new() }
}
pub fn empty(at: u32) -> Self {
Self::new(at, at)
}
pub const fn sized(start: u32, size: u32) -> Self {
Self::new(start, start + size)
}
pub const fn size(self) -> u32 {
debug_assert!(self.start <= self.end);
self.end - self.start
}
pub const fn is_empty(self) -> bool {
debug_assert!(self.start <= self.end);
self.start == self.end
}
#[inline]
pub const fn is_unspanned(self) -> bool {
self.const_eq(SPAN)
}
#[inline]
pub const fn contains_inclusive(self, span: Span) -> bool {
self.start <= span.start && span.end <= self.end
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
Self::new(self.start.min(other.start), self.end.max(other.end))
}
#[must_use]
pub fn merge_within(self, other: Self, within: Self) -> Option<Self> {
let merged = self.merge(other);
if within.contains_inclusive(merged) { Some(merged) } else { None }
}
#[must_use]
pub fn expand(self, offset: u32) -> Self {
Self::new(self.start.saturating_sub(offset), self.end.saturating_add(offset))
}
#[must_use]
pub fn shrink(self, offset: u32) -> Self {
let start = self.start.saturating_add(offset);
let end = self.end.saturating_sub(offset);
debug_assert!(start <= end, "Cannot shrink span past zero length");
Self::new(start, end)
}
#[must_use]
pub const fn expand_left(self, offset: u32) -> Self {
Self::new(self.start.saturating_sub(offset), self.end)
}
#[must_use]
pub const fn shrink_left(self, offset: u32) -> Self {
let start = self.start.saturating_add(offset);
debug_assert!(start <= self.end);
Self::new(self.start.saturating_add(offset), self.end)
}
#[must_use]
pub const fn expand_right(self, offset: u32) -> Self {
Self::new(self.start, self.end.saturating_add(offset))
}
#[must_use]
pub const fn shrink_right(self, offset: u32) -> Self {
let end = self.end.saturating_sub(offset);
debug_assert!(self.start <= end);
Self::new(self.start, end)
}
#[must_use]
pub const fn move_left(self, offset: u32) -> Self {
let start = self.start.saturating_sub(offset);
#[cfg(debug_assertions)]
if start == 0 {
debug_assert!(self.start == offset, "Cannot move span past zero length");
}
Self::new(start, self.end.saturating_sub(offset))
}
#[must_use]
pub const fn move_right(self, offset: u32) -> Self {
let end = self.end.saturating_add(offset);
#[cfg(debug_assertions)]
if end == u32::MAX {
debug_assert!(
u32::MAX.saturating_sub(offset) == self.end,
"Cannot move span past `u32::MAX` length"
);
}
Self::new(self.start.saturating_add(offset), end)
}
pub fn source_text(self, source_text: &str) -> &str {
&source_text[self.start as usize..self.end as usize]
}
#[must_use]
pub fn label<S: Into<String>>(self, label: S) -> LabeledSpan {
LabeledSpan::new_with_span(Some(label.into()), self)
}
#[must_use]
pub fn primary_label<S: Into<String>>(self, label: S) -> LabeledSpan {
LabeledSpan::new_primary_with_span(Some(label.into()), self)
}
#[must_use]
pub fn primary(self) -> LabeledSpan {
LabeledSpan::new_primary_with_span(None, self)
}
#[expect(clippy::inline_always)] #[inline(always)]
const fn as_u64(self) -> u64 {
if cfg!(target_endian = "little") {
((self.end as u64) << 32) | (self.start as u64)
} else {
((self.start as u64) << 32) | (self.end as u64)
}
}
#[expect(clippy::inline_always)]
#[inline(always)]
const fn const_eq(self, other: Self) -> bool {
if cfg!(target_pointer_width = "64") {
self.as_u64() == other.as_u64()
} else {
self.start == other.start && self.end == other.end
}
}
}
impl Index<Span> for str {
type Output = str;
#[inline]
fn index(&self, index: Span) -> &Self::Output {
&self[index.start as usize..index.end as usize]
}
}
impl IndexMut<Span> for str {
#[inline]
fn index_mut(&mut self, index: Span) -> &mut Self::Output {
&mut self[index.start as usize..index.end as usize]
}
}
impl From<Range<u32>> for Span {
#[inline]
fn from(range: Range<u32>) -> Self {
Self::new(range.start, range.end)
}
}
impl From<Span> for SourceSpan {
fn from(val: Span) -> Self {
Self::new(SourceOffset::from(val.start as usize), val.size() as usize)
}
}
impl From<Span> for LabeledSpan {
fn from(val: Span) -> Self {
LabeledSpan::underline(val)
}
}
impl PartialEq for Span {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.const_eq(*other)
}
}
impl Hash for Span {
#[inline] fn hash<H: Hasher>(&self, hasher: &mut H) {
if cfg!(target_pointer_width = "64") {
self.as_u64().hash(hasher);
} else {
self.start.hash(hasher);
self.end.hash(hasher);
}
}
}
#[expect(clippy::missing_fields_in_debug)]
impl Debug for Span {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Span").field("start", &self.start).field("end", &self.end).finish()
}
}
pub trait GetSpan {
fn span(&self) -> Span;
}
pub trait GetSpanMut {
fn span_mut(&mut self) -> &mut Span;
}
impl GetSpan for Span {
#[inline]
fn span(&self) -> Span {
*self
}
}
impl GetSpanMut for Span {
#[inline]
fn span_mut(&mut self) -> &mut Span {
self
}
}
impl<'a> CloneIn<'a> for Span {
type Cloned = Self;
#[inline]
fn clone_in(&self, _: &'a Allocator) -> Self {
*self
}
}
impl<'a> Dummy<'a> for Span {
#[expect(clippy::inline_always)]
#[inline(always)]
fn dummy(_allocator: &'a Allocator) -> Self {
SPAN
}
}
#[cfg(feature = "serialize")]
impl Serialize for Span {
fn serialize<S: SerdeSerializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("start", &self.start)?;
map.serialize_entry("end", &self.end)?;
map.end()
}
}
#[cfg(feature = "serialize")]
impl ESTreeSpan for Span {
#[expect(clippy::inline_always)] #[inline(always)]
fn range(self) -> [u32; 2] {
[self.start, self.end]
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
struct PointerAlign([usize; 0]);
impl PointerAlign {
#[inline]
const fn new() -> Self {
Self([])
}
}
#[cfg_attr(not(debug_assertions), expect(dead_code))]
#[ast]
pub struct I32Dummy(i32);
#[cfg(test)]
mod test {
use super::Span;
#[test]
fn test_size() {
let s = Span::sized(0, 5);
assert_eq!(s.size(), 5);
assert!(!s.is_empty());
let s = Span::sized(5, 0);
assert_eq!(s.size(), 0);
assert!(s.is_empty());
}
#[test]
fn test_hash() {
use std::hash::{DefaultHasher, Hash, Hasher};
fn hash<T: Hash>(value: T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
let first_hash = hash(Span::new(1, 5));
let second_hash = hash(Span::new(1, 5));
assert_eq!(first_hash, second_hash);
#[cfg(target_pointer_width = "64")]
{
let u64_equivalent: u64 =
if cfg!(target_endian = "little") { 1 + (5 << 32) } else { (1 << 32) + 5 };
let u64_hash = hash(u64_equivalent);
assert_eq!(first_hash, u64_hash);
}
#[cfg(not(target_pointer_width = "64"))]
{
#[derive(Hash)]
#[repr(C)]
struct PlainSpan {
start: u32,
end: u32,
}
let plain_hash = hash(PlainSpan { start: 1, end: 5 });
assert_eq!(first_hash, plain_hash);
}
}
#[test]
fn test_eq() {
assert_eq!(Span::new(0, 0), Span::new(0, 0));
assert_eq!(Span::new(0, 1), Span::new(0, 1));
assert_eq!(Span::new(1, 5), Span::new(1, 5));
assert_ne!(Span::new(0, 0), Span::new(0, 1));
assert_ne!(Span::new(1, 5), Span::new(0, 5));
assert_ne!(Span::new(1, 5), Span::new(2, 5));
assert_ne!(Span::new(1, 5), Span::new(1, 4));
assert_ne!(Span::new(1, 5), Span::new(1, 6));
}
#[test]
fn test_ordering_less() {
assert!(Span::new(0, 0) < Span::new(0, 1));
assert!(Span::new(0, 3) < Span::new(2, 5));
}
#[test]
fn test_ordering_greater() {
assert!(Span::new(0, 1) > Span::new(0, 0));
assert!(Span::new(2, 5) > Span::new(0, 3));
}
#[test]
fn test_contains() {
let span = Span::new(5, 10);
assert!(span.contains_inclusive(span));
assert!(span.contains_inclusive(Span::new(5, 5)));
assert!(span.contains_inclusive(Span::new(10, 10)));
assert!(span.contains_inclusive(Span::new(6, 9)));
assert!(!span.contains_inclusive(Span::new(0, 0)));
assert!(!span.contains_inclusive(Span::new(4, 10)));
assert!(!span.contains_inclusive(Span::new(5, 11)));
assert!(!span.contains_inclusive(Span::new(4, 11)));
}
#[test]
fn test_expand() {
let span = Span::new(3, 5);
assert_eq!(span.expand(0), Span::new(3, 5));
assert_eq!(span.expand(1), Span::new(2, 6));
assert_eq!(span.expand(5), Span::new(0, 10));
}
#[test]
fn test_shrink() {
let span = Span::new(4, 8);
assert_eq!(span.shrink(0), Span::new(4, 8));
assert_eq!(span.shrink(1), Span::new(5, 7));
assert_eq!(span.shrink(2), Span::new(6, 6));
}
#[test]
#[should_panic(expected = "Cannot shrink span past zero length")]
fn test_shrink_past_start() {
let span = Span::new(5, 10);
let _ = span.shrink(5);
}
#[test]
fn test_move_left() {
let span = Span::new(5, 10);
assert_eq!(span.move_left(1), Span::new(4, 9));
assert_eq!(span.move_left(2), Span::new(3, 8));
assert_eq!(span.move_left(5), Span::new(0, 5));
}
#[test]
#[should_panic(expected = "Cannot move span past zero length")]
fn test_move_past_start() {
let span = Span::new(5, 10);
let _ = span.move_left(6);
}
#[test]
fn test_move_right() {
let span: Span = Span::new(5, 10);
assert_eq!(span.move_right(1), Span::new(6, 11));
assert_eq!(span.move_right(2), Span::new(7, 12));
assert_eq!(
span.move_right(u32::MAX.saturating_sub(10)),
Span::new(u32::MAX.saturating_sub(5), u32::MAX)
);
}
#[test]
#[should_panic(expected = "Cannot move span past `u32::MAX` length")]
fn test_move_past_end() {
let span = Span::new(u32::MAX.saturating_sub(2), u32::MAX.saturating_sub(1));
let _ = span.move_right(2);
}
}
#[cfg(test)]
mod doctests {
use super::Span;
#[test]
fn doctest() {
let text = "foo bar baz";
let span = Span::new(4, 7);
assert_eq!(&text[span], "bar");
let a = Span::new(5, 10); let b = Span::sized(5, 5); assert_eq!(a, b);
let s = Span::new(5, 10);
assert_eq!(s.shrink(2), Span::new(7, 8));
assert_eq!(s.shrink(2), s.shrink_left(2).shrink_right(2));
assert_eq!(s.expand(5), Span::new(0, 15));
assert_eq!(s.expand(5), s.expand_left(5).expand_right(5));
}
}
#[cfg(test)]
mod size_asserts {
use std::mem::{align_of, size_of};
use super::Span;
const _: () = assert!(size_of::<Span>() == 8);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(align_of::<Span>() == 8);
#[cfg(not(target_pointer_width = "64"))]
const _: () = assert!(align_of::<Span>() == 4);
}