use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceLocation {
pub file_path: PathBuf,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub start_column: usize,
pub end_line: usize,
pub end_column: usize,
}
impl SourceLocation {
pub fn new(
file_path: impl Into<PathBuf>,
byte_start: usize,
byte_end: usize,
start_line: usize,
start_column: usize,
end_line: usize,
end_column: usize,
) -> Self {
Self {
file_path: file_path.into(),
byte_start,
byte_end,
start_line,
start_column,
end_line,
end_column,
}
}
pub fn from_bytes(
file_path: impl Into<PathBuf>,
source: &str,
byte_start: usize,
byte_end: usize,
) -> Self {
let (start_line, start_col) = byte_to_line_column(source, byte_start);
let (end_line, end_col) = byte_to_line_column(source, byte_end);
Self {
file_path: file_path.into(),
byte_start,
byte_end,
start_line,
start_column: start_col,
end_line,
end_column: end_col,
}
}
pub fn display(&self) -> String {
format!(
"{}:{}:{}-{}:{}",
self.file_path.display(),
self.start_line,
self.start_column,
self.end_line,
self.end_column
)
}
pub fn overlaps(&self, other: &SourceLocation) -> bool {
if self.file_path != other.file_path {
return false;
}
self.byte_start < other.byte_end && self.byte_end > other.byte_start
}
pub fn from_bytes_with_source(
file_path: impl Into<PathBuf>,
source: Option<&str>,
byte_start: usize,
byte_end: usize,
) -> Self {
let file_path = file_path.into();
if let Some(src) = source {
let (start_line, start_col) = byte_to_line_column(src, byte_start);
let (end_line, end_col) = byte_to_line_column(src, byte_end);
Self {
file_path,
byte_start,
byte_end,
start_line,
start_column: start_col,
end_line,
end_column: end_col,
}
} else {
Self {
file_path,
byte_start,
byte_end,
start_line: 0,
start_column: 0,
end_line: 0,
end_column: 0,
}
}
}
pub fn display_or_bytes(&self) -> String {
if self.start_line > 0 {
self.display()
} else {
format!(
"{}:bytes{}-{}",
self.file_path.display(),
self.byte_start,
self.byte_end
)
}
}
}
fn byte_to_line_column(source: &str, byte_offset: usize) -> (usize, usize) {
let mut line = 1;
let mut column = 1;
let mut current_byte = 0;
for ch in source.chars() {
if current_byte >= byte_offset {
break;
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
current_byte += ch.len_utf8();
}
(line, column)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_byte_to_line_column() {
let source = "line 1\nline 2\nline 3";
assert_eq!(byte_to_line_column(source, 0), (1, 1));
assert_eq!(byte_to_line_column(source, 6), (1, 7));
assert_eq!(byte_to_line_column(source, 7), (2, 1));
assert_eq!(byte_to_line_column(source, 13), (2, 7));
assert_eq!(byte_to_line_column(source, 14), (3, 1));
}
#[test]
fn test_source_location_from_bytes() {
let source = "hello\nworld";
let loc = SourceLocation::from_bytes("test.rs", source, 0, 5);
assert_eq!(loc.start_line, 1);
assert_eq!(loc.start_column, 1);
assert_eq!(loc.end_line, 1);
assert_eq!(loc.end_column, 6);
}
#[test]
fn test_source_location_display() {
let loc = SourceLocation {
file_path: PathBuf::from("src/test.rs"),
byte_start: 0,
byte_end: 10,
start_line: 5,
start_column: 3,
end_line: 5,
end_column: 13,
};
assert_eq!(loc.display(), "src/test.rs:5:3-5:13");
}
#[test]
fn test_overlaps() {
let loc1 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 0,
byte_end: 10,
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 11,
};
let loc2 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 5,
byte_end: 15,
start_line: 1,
start_column: 6,
end_line: 1,
end_column: 16,
};
assert!(loc1.overlaps(&loc2));
let loc3 = SourceLocation {
file_path: PathBuf::from("other.rs"),
byte_start: 0,
byte_end: 10,
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 11,
};
assert!(!loc1.overlaps(&loc3)); }
#[test]
fn test_source_location_new() {
let loc = SourceLocation::new("path/to/file.rs", 100, 200, 10, 5, 15, 20);
assert_eq!(loc.file_path, PathBuf::from("path/to/file.rs"));
assert_eq!(loc.byte_start, 100);
assert_eq!(loc.byte_end, 200);
assert_eq!(loc.start_line, 10);
assert_eq!(loc.start_column, 5);
assert_eq!(loc.end_line, 15);
assert_eq!(loc.end_column, 20);
}
#[test]
fn test_multibyte_character_handling() {
let source = "hello 世界\nworld";
let (line, col) = byte_to_line_column(source, 0);
assert_eq!(line, 1);
assert_eq!(col, 1);
let (line, col) = byte_to_line_column(source, 6);
assert_eq!(line, 1);
assert_eq!(col, 7);
let (line, col) = byte_to_line_column(source, 13);
assert_eq!(line, 2);
assert_eq!(col, 1); }
#[test]
fn test_overlaps_adjacent_no_overlap() {
let loc1 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 0,
byte_end: 10,
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 11,
};
let loc2 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 10,
byte_end: 20,
start_line: 1,
start_column: 11,
end_line: 1,
end_column: 21,
};
assert!(!loc1.overlaps(&loc2));
}
#[test]
fn test_overlaps_contained() {
let loc1 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 0,
byte_end: 100,
start_line: 1,
start_column: 1,
end_line: 5,
end_column: 1,
};
let loc2 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 20,
byte_end: 30,
start_line: 2,
start_column: 1,
end_line: 2,
end_column: 11,
};
assert!(loc1.overlaps(&loc2));
assert!(loc2.overlaps(&loc1));
}
#[test]
fn test_from_bytes_with_source_with_source() {
let source = "hello\nworld";
let loc = SourceLocation::from_bytes_with_source("test.rs", Some(source), 0, 5);
assert_eq!(loc.file_path, PathBuf::from("test.rs"));
assert_eq!(loc.byte_start, 0);
assert_eq!(loc.byte_end, 5);
assert_eq!(loc.start_line, 1);
assert_eq!(loc.start_column, 1);
assert_eq!(loc.end_line, 1);
assert_eq!(loc.end_column, 6);
}
#[test]
fn test_from_bytes_with_source_without_source() {
let loc = SourceLocation::from_bytes_with_source("test.rs", None, 10, 20);
assert_eq!(loc.file_path, PathBuf::from("test.rs"));
assert_eq!(loc.byte_start, 10);
assert_eq!(loc.byte_end, 20);
assert_eq!(loc.start_line, 0); assert_eq!(loc.start_column, 0);
assert_eq!(loc.end_line, 0);
assert_eq!(loc.end_column, 0);
}
#[test]
fn test_display_or_bytes_with_line_column() {
let loc = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 0,
byte_end: 10,
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 11,
};
assert_eq!(loc.display_or_bytes(), "test.rs:1:1-1:11");
}
#[test]
fn test_display_or_bytes_without_line_column() {
let loc = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 100,
byte_end: 200,
start_line: 0,
start_column: 0,
end_line: 0,
end_column: 0,
};
assert_eq!(loc.display_or_bytes(), "test.rs:bytes100-200");
}
}