use std::fmt::{self, Debug};
pub use itoa::Buffer;
use rspack_cacheable::cacheable;
#[cacheable]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourcePosition {
pub line: u32,
pub column: u32,
}
impl From<(u32, u32)> for SourcePosition {
fn from(range: (u32, u32)) -> Self {
Self {
line: range.0,
column: range.1,
}
}
}
#[cacheable]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RealDependencyLocation {
pub start: SourcePosition,
pub end: Option<SourcePosition>,
}
impl RealDependencyLocation {
pub fn new(start: SourcePosition, end: Option<SourcePosition>) -> Self {
Self { start, end }
}
pub fn from_byte_location(
source: &str,
line: u32,
column: u32,
length: Option<u32>,
) -> Option<Self> {
if line == 0 {
return None;
}
let bytes = source.as_bytes();
let target_line_idx = (line - 1) as usize;
let line_start_offset = if target_line_idx == 0 {
0
} else {
let mut iter = memchr::memchr_iter(b'\n', bytes);
match iter.nth(target_line_idx - 1) {
Some(idx) => idx + 1,
None => return None, }
};
let start_byte = line_start_offset + column as usize;
if start_byte > bytes.len() {
return None;
}
let current_line_end = memchr::memchr(b'\n', &bytes[line_start_offset..])
.map_or(bytes.len(), |rel| line_start_offset + rel);
if start_byte > current_line_end {
return None;
}
let start_line_slice = source.get(line_start_offset..start_byte)?;
let start_utf16_col = start_line_slice.encode_utf16().count() + 1;
let start = SourcePosition {
line,
column: start_utf16_col as u32,
};
let end = if let Some(len) = length {
let end_byte = start_byte + len as usize;
if end_byte > bytes.len() {
return Some(Self { start, end: None });
}
let Some(span_slice) = source.get(start_byte..end_byte) else {
return Some(Self { start, end: None });
};
let newlines_in_span = memchr::memchr_iter(b'\n', span_slice.as_bytes()).count();
let end_line = line.checked_add(newlines_in_span as u32)?;
let end_column = if newlines_in_span == 0 {
start_utf16_col + span_slice.encode_utf16().count()
} else {
#[allow(clippy::unwrap_used)]
let last_newline_pos = span_slice.rfind('\n').unwrap();
let text_after_last_newline = &span_slice[last_newline_pos + 1..];
text_after_last_newline.encode_utf16().count() + 1 };
Some(SourcePosition {
line: end_line,
column: end_column as u32,
})
} else {
None
};
Some(Self { start, end })
}
}
impl fmt::Display for RealDependencyLocation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(end) = self.end {
let mut start_line_buffer = itoa::Buffer::new();
let start_line = start_line_buffer.format(self.start.line);
let mut start_col_buffer = itoa::Buffer::new();
let start_col = start_col_buffer.format(self.start.column);
if self.start.line == end.line && self.start.column == end.column {
write!(f, "{start_line}:{start_col}")
} else if self.start.line == end.line {
let mut end_col_buffer = itoa::Buffer::new();
let end_col = end_col_buffer.format(end.column);
write!(f, "{start_line}:{start_col}-{end_col}")
} else {
let mut end_line_buffer = itoa::Buffer::new();
let end_line = end_line_buffer.format(end.line);
let mut end_col_buffer = itoa::Buffer::new();
let end_col = end_col_buffer.format(end.column);
write!(f, "{start_line}:{start_col}-{end_line}:{end_col}")
}
} else {
let mut start_line_buffer = itoa::Buffer::new();
let start_line = start_line_buffer.format(self.start.line);
let mut start_col_buffer = itoa::Buffer::new();
let start_col = start_col_buffer.format(self.start.column);
write!(f, "{start_line}:{start_col}")
}
}
}
#[cacheable]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SyntheticDependencyLocation {
pub name: String,
}
impl SyntheticDependencyLocation {
pub fn new(name: &str) -> Self {
SyntheticDependencyLocation {
name: name.to_string(),
}
}
}
impl fmt::Display for SyntheticDependencyLocation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[cacheable]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum DependencyLocation {
Real(RealDependencyLocation),
Synthetic(SyntheticDependencyLocation),
}
impl DependencyLocation {
pub fn from_byte_location(
source: &str,
line: u32,
column: u32,
length: Option<u32>,
) -> Option<Self> {
RealDependencyLocation::from_byte_location(source, line, column, length)
.map(DependencyLocation::Real)
}
}
impl fmt::Display for DependencyLocation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let loc = match self {
DependencyLocation::Real(real) => real.to_string(),
DependencyLocation::Synthetic(synthetic) => synthetic.to_string(),
};
write!(f, "{loc}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_byte_location_ascii() {
let source = "hello world\nfoo bar baz";
let loc = RealDependencyLocation::from_byte_location(source, 1, 6, Some(5));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 7); assert_eq!(loc.end.as_ref().unwrap().line, 1);
assert_eq!(loc.end.as_ref().unwrap().column, 12); }
#[test]
fn test_from_byte_location_second_line() {
let source = "hello world\nfoo bar baz";
let loc = RealDependencyLocation::from_byte_location(source, 2, 4, Some(3));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 2);
assert_eq!(loc.start.column, 5); assert_eq!(loc.end.as_ref().unwrap().line, 2);
assert_eq!(loc.end.as_ref().unwrap().column, 8); }
#[test]
fn test_from_byte_location_utf8_multibyte() {
let source = "你好世界abc";
let loc = RealDependencyLocation::from_byte_location(source, 1, 0, Some(6));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 1); assert_eq!(loc.end.as_ref().unwrap().line, 1);
assert_eq!(loc.end.as_ref().unwrap().column, 3); }
#[test]
fn test_from_byte_location_utf8_emoji() {
let source = "hello😀world";
let loc = RealDependencyLocation::from_byte_location(source, 1, 5, Some(4));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 6); assert_eq!(loc.end.as_ref().unwrap().line, 1);
assert_eq!(loc.end.as_ref().unwrap().column, 8); }
#[test]
fn test_from_byte_location_no_length() {
let source = "hello world";
let loc = RealDependencyLocation::from_byte_location(source, 1, 0, None);
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 1);
assert!(loc.end.is_none());
}
#[test]
fn test_from_byte_location_invalid_line() {
let source = "hello world";
let loc = RealDependencyLocation::from_byte_location(source, 0, 0, None);
assert!(loc.is_none());
}
#[test]
fn test_from_byte_location_line_out_of_bounds() {
let source = "hello world\nfoo bar";
let loc = RealDependencyLocation::from_byte_location(source, 10, 0, None);
assert!(loc.is_none());
}
#[test]
fn test_from_byte_location_column_out_of_bounds() {
let source = "hello";
let loc = RealDependencyLocation::from_byte_location(source, 1, 100, None);
assert!(loc.is_none());
}
#[test]
fn test_from_byte_location_empty_line() {
let source = "hello\n\nworld";
let loc = RealDependencyLocation::from_byte_location(source, 2, 0, None);
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 2);
assert_eq!(loc.start.column, 1);
}
#[test]
fn test_from_byte_location_length_exceeds_line() {
let source = "hello world";
let loc = RealDependencyLocation::from_byte_location(source, 1, 6, Some(100));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 7);
assert!(loc.end.is_none()); }
#[test]
fn test_from_byte_location_mixed_content() {
let source = "abc你好😀xyz\nline2\nline3";
let loc = RealDependencyLocation::from_byte_location(source, 1, 9, Some(4));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 6);
assert_eq!(loc.end.as_ref().unwrap().line, 1);
assert_eq!(loc.end.as_ref().unwrap().column, 8);
}
#[test]
fn test_from_byte_location_multiline() {
let source = "hello world\nfoo bar baz\nend";
let loc = RealDependencyLocation::from_byte_location(source, 1, 6, Some(18));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 7);
assert_eq!(loc.end.as_ref().unwrap().line, 3);
assert_eq!(loc.end.as_ref().unwrap().column, 1);
}
#[test]
fn test_from_byte_location_multiline_three_lines() {
let source = "abc\ndefg\nhij\nklm";
let loc = RealDependencyLocation::from_byte_location(source, 1, 2, Some(10));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 3); assert_eq!(loc.end.as_ref().unwrap().line, 3);
assert_eq!(loc.end.as_ref().unwrap().column, 4); }
#[test]
fn test_from_byte_location_multiline_utf8() {
let source = "你好\n世界abc\n测试";
let loc = RealDependencyLocation::from_byte_location(source, 1, 0, Some(13));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 1);
assert_eq!(loc.end.as_ref().unwrap().line, 2);
assert_eq!(loc.end.as_ref().unwrap().column, 3); }
#[test]
fn test_from_byte_location_multiline_exact_line_end() {
let source = "hello\nworld";
let loc = RealDependencyLocation::from_byte_location(source, 1, 0, Some(5));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 1);
assert_eq!(loc.end.as_ref().unwrap().line, 1);
assert_eq!(loc.end.as_ref().unwrap().column, 6); }
#[test]
fn test_from_byte_location_multiline_including_newline() {
let source = "hello\nworld";
let loc = RealDependencyLocation::from_byte_location(source, 1, 0, Some(6));
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.start.line, 1);
assert_eq!(loc.start.column, 1);
assert_eq!(loc.end.as_ref().unwrap().line, 2);
assert_eq!(loc.end.as_ref().unwrap().column, 1); }
}