use crate::{
intern::{Intern, Interned},
util::DebugAsDisplay,
};
use hashbrown::HashMap;
use std::{cell::RefCell, fmt, num::NonZeroUsize, panic, path::Path};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceLocation {
file: Interned<str>,
line: u32,
column: u32,
}
impl fmt::Debug for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("SourceLocation")
.field(&DebugAsDisplay(self))
.finish()
}
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { file, line, column } = *self;
write!(f, "{file}:{line}:{column}")
}
}
#[derive(Copy, Clone, Debug)]
struct FilePattern(&'static str);
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct FilePatternMatch {
prefix: String,
suffix: String,
}
impl From<&'_ FilePatternMatch> for FilePatternMatch {
fn from(value: &'_ FilePatternMatch) -> Self {
value.clone()
}
}
impl FilePatternMatch {
fn generate_file_name(&self, file_name_id: NonZeroUsize) -> String {
if file_name_id.get() == 1 {
self.prefix.clone() + &self.suffix
} else {
format!("{}-{file_name_id}{}", self.prefix, self.suffix)
}
}
}
impl FilePattern {
const TEMPLATE_CHAR: char = 'X';
fn matches(self, file_name: &str) -> Option<FilePatternMatch> {
if self.0.len() != file_name.len() {
return None;
}
let mut prefix = String::with_capacity(file_name.len());
for (pattern_ch, ch) in self.0.chars().zip(file_name.chars()) {
if pattern_ch == Self::TEMPLATE_CHAR {
if !ch.is_ascii_hexdigit() {
return None;
} else {
prefix.push(Self::TEMPLATE_CHAR);
}
} else if pattern_ch != ch {
return None;
} else {
prefix.push(ch);
}
}
let split_point = self.0.rfind('.').unwrap_or(self.0.len());
Some(FilePatternMatch {
suffix: prefix.split_off(split_point),
prefix,
})
}
}
struct NormalizedFileForTestState {
file_name_id: NonZeroUsize,
positions_map: HashMap<(u32, u32), u32>,
}
struct NormalizeFilesForTestsState {
test_position: &'static panic::Location<'static>,
file_pattern_matches: HashMap<FilePatternMatch, HashMap<String, NormalizedFileForTestState>>,
}
impl NormalizeFilesForTestsState {
#[track_caller]
fn new() -> Self {
Self {
test_position: panic::Location::caller(),
file_pattern_matches: HashMap::new(),
}
}
}
const FILE_PATTERNS: &[FilePattern] = &[
FilePattern("module-XXXXXXXXXX.rs"),
FilePattern("value-struct-XXXXXXXXXX.rs"),
FilePattern("value-enum-XXXXXXXXXX.rs"),
];
thread_local! {
static NORMALIZE_FILES_FOR_TESTS: RefCell<Option<NormalizeFilesForTestsState>> = const {
RefCell::new(None)
};
}
impl From<&'_ panic::Location<'_>> for SourceLocation {
fn from(value: &'_ panic::Location<'_>) -> Self {
let mut file = value.file();
let mut line = value.line();
let mut column = value.column();
let mut file_str = String::new();
NORMALIZE_FILES_FOR_TESTS.with_borrow_mut(|state| {
let Some(state) = state else {
return;
};
if file == state.test_position.file() {
file = "the_test_file.rs";
line = line
.saturating_add(10000)
.saturating_sub(state.test_position.line());
return;
}
file = Path::new(file)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("<no-file-name>");
for p in FILE_PATTERNS {
if let Some(m) = p.matches(file) {
let map = state.file_pattern_matches.entry_ref(&m).or_default();
let len = map.len();
let file_state =
map.entry_ref(file)
.or_insert_with(|| NormalizedFileForTestState {
file_name_id: NonZeroUsize::new(len + 1).unwrap(),
positions_map: HashMap::new(),
});
file_str = m.generate_file_name(file_state.file_name_id);
file = &file_str;
let positions_len = file_state.positions_map.len();
line = *file_state
.positions_map
.entry((line, column))
.or_insert((positions_len + 1).try_into().unwrap());
column = 1;
break;
}
}
});
Self {
file: file.intern(),
line,
column,
}
}
}
#[must_use = "resets value when dropped"]
pub struct NormalizeFilesForTestsScope {
old_state: Option<NormalizeFilesForTestsState>,
}
impl Drop for NormalizeFilesForTestsScope {
fn drop(&mut self) {
NORMALIZE_FILES_FOR_TESTS.set(self.old_state.take());
}
}
impl SourceLocation {
#[track_caller]
pub fn normalize_files_for_tests() -> NormalizeFilesForTestsScope {
NormalizeFilesForTestsScope {
old_state: NORMALIZE_FILES_FOR_TESTS.replace(Some(NormalizeFilesForTestsState::new())),
}
}
#[track_caller]
pub fn caller() -> Self {
panic::Location::caller().into()
}
pub(crate) fn builtin() -> Self {
Self::new("builtin".intern(), 1, 1)
}
pub const fn new(file: Interned<str>, line: u32, column: u32) -> Self {
Self { file, line, column }
}
pub const fn file(self) -> Interned<str> {
self.file
}
pub const fn line(self) -> u32 {
self.line
}
pub const fn column(self) -> u32 {
self.column
}
}