use alloc::vec;
use alloc::vec::Vec;
const PREFIXES: [&str; 7] = [
"_ctx.",
"$setup.",
"$props.",
"$data.",
"$options.",
"__props.",
"_unref(",
];
const SUFFIXES: [&str; 2] = [")", ".value"];
const MAX_CELLS: usize = 1 << 22;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RewrittenIdentifier<'a> {
pub emitted: usize,
pub authored: usize,
pub name: &'a str,
pub prefix_len: usize,
}
impl RewrittenIdentifier<'_> {
pub fn emitted_len(&self) -> usize {
self.prefix_len + self.name.len()
}
}
pub fn rewritten_identifier_spans<'a>(
authored: &'a str,
emitted: &str,
) -> Option<Vec<RewrittenIdentifier<'a>>> {
let (original, output) = (authored.as_bytes(), emitted.as_bytes());
let inserted = output.len().checked_sub(original.len())?;
let width = inserted + 1;
if (original.len() + 1).saturating_mul(width) > MAX_CELLS {
return None;
}
let tokens = || PREFIXES.iter().chain(SUFFIXES.iter()).map(|t| t.as_bytes());
let mut feasible = vec![false; (original.len() + 1) * width];
let cell = |feasible: &[bool], index: usize| feasible.get(index).copied().unwrap_or(false);
let same = |i: usize, k: usize| {
original
.get(i)
.is_some_and(|b| output.get(i + k) == Some(b))
};
for i in (0..=original.len()).rev() {
for k in (0..=inserted).rev() {
let at = output.get(i + k..).unwrap_or_default();
let keeps = same(i, k) && cell(&feasible, (i + 1) * width + k);
let ends = i == original.len() && k == inserted;
let inserts = || {
tokens().any(|t| {
k + t.len() <= inserted
&& at.starts_with(t)
&& cell(&feasible, i * width + k + t.len())
})
};
let value = keeps || ends || inserts();
if let Some(slot) = feasible.get_mut(i * width + k) {
*slot = value;
}
}
}
if !cell(&feasible, 0) {
return None;
}
let mut spans = Vec::new();
let (mut i, mut k) = (0usize, 0usize);
while i < original.len() || k < inserted {
if same(i, k) && cell(&feasible, (i + 1) * width + k) {
i += 1;
continue;
}
let at = output.get(i + k..).unwrap_or_default();
let token = tokens().find(|t| {
k + t.len() <= inserted && at.starts_with(t) && cell(&feasible, i * width + k + t.len())
})?;
let is_prefix = PREFIXES.iter().any(|p| p.as_bytes() == token);
let name = identifier_at(authored, i);
if is_prefix && !name.is_empty() {
spans.push(RewrittenIdentifier {
emitted: i + k,
authored: i,
name,
prefix_len: token.len(),
});
}
k += token.len();
}
Some(spans)
}
fn identifier_at(text: &str, offset: usize) -> &str {
let rest = text.get(offset..).unwrap_or_default();
let starts = rest
.chars()
.next()
.is_some_and(|ch| ch.is_alphabetic() || ch == '_' || ch == '$');
if !starts {
return "";
}
let end = rest
.find(|ch: char| !(ch.is_alphanumeric() || ch == '_' || ch == '$'))
.unwrap_or(rest.len());
rest.get(..end).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::{RewrittenIdentifier, rewritten_identifier_spans};
fn spans<'a>(authored: &'a str, emitted: &str) -> Option<Vec<(usize, usize, &'a str)>> {
rewritten_identifier_spans(authored, emitted).map(|spans| {
spans
.into_iter()
.map(
|RewrittenIdentifier {
emitted,
authored,
name,
..
}| (emitted, authored, name),
)
.collect()
})
}
#[test]
fn every_context_prefix_is_anchored_to_its_identifier() {
assert_eq!(
spans("count + offset", "_ctx.count + _ctx.offset"),
Some(vec![(0, 0, "count"), (13, 8, "offset")])
);
assert_eq!(
spans("a(b)", "$setup.a(_unref(b))"),
Some(vec![(0, 0, "a"), (9, 2, "b")])
);
assert_eq!(spans("n = n + 1", "n.value = n.value + 1"), Some(vec![]));
}
#[test]
fn authored_bytes_win_over_insertions() {
assert_eq!(spans("_ctx.a", "_ctx.a"), Some(vec![]));
assert_eq!(spans("_x", "_ctx._x"), Some(vec![(0, 0, "_x")]));
assert_eq!(spans("c", "_ctx.c"), Some(vec![(0, 0, "c")]));
}
#[test]
fn non_insertion_rewrites_yield_no_spans() {
assert_eq!(spans("a as B", "_ctx.a"), None);
assert_eq!(spans("foo", "__props.bar"), None);
assert_eq!(spans("ab", "a"), None);
}
}