use std::fmt;
use std::str;
use std::sync::Arc;
use std::sync::Mutex;
use if_chain::if_chain;
use crate::detector::{locate_sourcemap_reference_slice, SourceMapRef};
use crate::errors::Result;
use crate::js_identifiers::{get_javascript_token, is_valid_javascript_identifier};
use crate::types::Token;
pub struct RevTokenIter<'view, 'map> {
sv: &'view SourceView,
token: Option<Token<'map>>,
source_line: Option<(&'view str, usize, usize, usize)>,
}
impl<'view, 'map> Iterator for RevTokenIter<'view, 'map> {
type Item = (Token<'map>, Option<&'view str>);
fn next(&mut self) -> Option<(Token<'map>, Option<&'view str>)> {
let token = self.token.take()?;
let idx = token.idx;
if idx > 0 {
self.token = token.sm.get_token(idx - 1);
}
let (source_line, last_char_offset, last_byte_offset) = if_chain! {
if let Some((source_line, dst_line, last_char_offset,
last_byte_offset)) = self.source_line;
if dst_line == token.get_dst_line() as usize;
then {
(source_line, last_char_offset, last_byte_offset)
} else {
if let Some(source_line) = self.sv.get_line(token.get_dst_line()) {
(source_line, !0, !0)
} else {
("", !0, !0)
}
}
};
let byte_offset = if last_byte_offset == !0 {
let mut off = 0;
let mut idx = 0;
for c in source_line.chars() {
if idx >= token.get_dst_col() as usize {
break;
}
off += c.len_utf8();
idx += c.len_utf16();
}
off
} else {
let chars_to_move = last_char_offset - token.get_dst_col() as usize;
let mut new_offset = last_byte_offset;
let mut idx = 0;
for c in source_line
.get(..last_byte_offset)
.unwrap_or("")
.chars()
.rev()
{
if idx >= chars_to_move {
break;
}
new_offset -= c.len_utf8();
idx += c.len_utf16();
}
new_offset
};
self.source_line = Some((
source_line,
token.get_dst_line() as usize,
token.get_dst_col() as usize,
byte_offset,
));
if byte_offset >= source_line.len() {
self.source_line = None;
Some((token, None))
} else {
Some((
token,
source_line
.get(byte_offset..)
.and_then(get_javascript_token),
))
}
}
}
pub struct Lines<'a> {
sv: &'a SourceView,
idx: u32,
}
impl<'a> Iterator for Lines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if let Some(line) = self.sv.get_line(self.idx) {
self.idx += 1;
Some(line)
} else {
None
}
}
}
pub struct SourceView {
source: Arc<str>,
line_end_offsets: Mutex<Vec<LineEndOffset>>,
}
impl Clone for SourceView {
fn clone(&self) -> SourceView {
SourceView {
source: self.source.clone(),
line_end_offsets: Mutex::new(vec![]),
}
}
}
impl fmt::Debug for SourceView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("SourceView")
.field("source", &self.source())
.finish()
}
}
impl PartialEq for SourceView {
fn eq(&self, other: &Self) -> bool {
self.source == other.source
}
}
impl SourceView {
pub fn new(source: Arc<str>) -> SourceView {
SourceView {
source,
line_end_offsets: Mutex::new(vec![]),
}
}
pub fn from_string(source: String) -> SourceView {
SourceView {
source: source.into(),
line_end_offsets: Mutex::new(vec![]),
}
}
pub fn get_line(&self, idx: u32) -> Option<&str> {
let idx = idx as usize;
let get_from_line_ends = |line_ends: &[LineEndOffset]| {
let end = line_ends.get(idx)?.to_end_index();
let start = if idx == 0 {
0
} else {
line_ends[idx - 1].to_start_index()
};
Some(&self.source[start..end])
};
let mut line_ends = self
.line_end_offsets
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(line) = get_from_line_ends(&line_ends) {
return Some(line);
}
if line_ends
.last()
.is_some_and(|i| i.to_end_index() == self.source.len())
{
return None;
}
let mut rest_offset = line_ends.last().map_or(0, |i| i.to_start_index());
let mut rest = &self.source[rest_offset..];
let mut done = false;
while !done {
let line_term = if let Some(idx) = rest.find(['\n', '\r']) {
rest_offset += idx;
rest = &rest[idx..];
if rest.starts_with("\r\n") {
LineTerminator::CrLf
} else {
LineTerminator::LfOrCr
}
} else {
rest_offset += rest.len();
rest = &rest[rest.len()..];
done = true;
LineTerminator::Eof
};
line_ends.push(LineEndOffset::new(rest_offset, line_term));
rest_offset += line_term as usize;
rest = &rest[line_term as usize..];
if let Some(line) = get_from_line_ends(&line_ends) {
return Some(line);
}
}
None
}
pub fn get_line_slice(&self, line: u32, col: u32, span: u32) -> Option<&str> {
self.get_line(line).and_then(|line| {
let mut off = 0;
let mut idx = 0;
let mut char_iter = line.chars().peekable();
while let Some(&c) = char_iter.peek() {
if idx >= col as usize {
break;
}
char_iter.next();
off += c.len_utf8();
idx += c.len_utf16();
}
let mut off_end = off;
for c in char_iter {
if idx >= (col + span) as usize {
break;
}
off_end += c.len_utf8();
idx += c.len_utf16();
}
if idx < ((col + span) as usize) {
None
} else {
line.get(off..off_end)
}
})
}
pub fn lines(&self) -> Lines<'_> {
Lines { sv: self, idx: 0 }
}
pub fn source(&self) -> &str {
&self.source
}
fn rev_token_iter<'this, 'map>(&'this self, token: Token<'map>) -> RevTokenIter<'this, 'map> {
RevTokenIter {
sv: self,
token: Some(token),
source_line: None,
}
}
pub fn get_original_function_name<'map>(
&self,
token: Token<'map>,
minified_name: &str,
) -> Option<&'map str> {
if !is_valid_javascript_identifier(minified_name) {
return None;
}
let mut iter = self.rev_token_iter(token).take(128).peekable();
while let Some((token, original_identifier)) = iter.next() {
if_chain! {
if original_identifier == Some(minified_name);
if let Some(item) = iter.peek();
if item.1 == Some("function");
then {
return token.get_name();
}
}
}
None
}
pub fn line_count(&self) -> usize {
self.get_line(!0);
self.line_end_offsets.lock().unwrap().len()
}
pub fn sourcemap_reference(&self) -> Result<Option<SourceMapRef>> {
locate_sourcemap_reference_slice(self.source.as_bytes())
}
}
#[derive(Clone, Copy)]
struct LineEndOffset(u64);
#[derive(Clone, Copy)]
enum LineTerminator {
Eof = 0,
LfOrCr = 1,
CrLf = 2,
}
impl LineEndOffset {
fn new(index: usize, line_end: LineTerminator) -> Self {
let shifted = (index as u64) << 2;
Self(shifted | line_end as u64)
}
fn to_end_index(self) -> usize {
(self.0 >> 2) as usize
}
fn to_start_index(self) -> usize {
self.to_end_index() + (self.0 & 0b11) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::cognitive_complexity)]
fn test_minified_source_view() {
let view = SourceView::new("a\nb\nc".into());
assert_eq!(view.get_line(0), Some("a"));
assert_eq!(view.get_line(0), Some("a"));
assert_eq!(view.get_line(2), Some("c"));
assert_eq!(view.get_line(1), Some("b"));
assert_eq!(view.get_line(3), None);
assert_eq!(view.line_count(), 3);
let view = SourceView::new("a\r\nb\r\nc".into());
assert_eq!(view.get_line(0), Some("a"));
assert_eq!(view.get_line(0), Some("a"));
assert_eq!(view.get_line(2), Some("c"));
assert_eq!(view.get_line(1), Some("b"));
assert_eq!(view.get_line(3), None);
assert_eq!(view.line_count(), 3);
let view = SourceView::new("abc👌def\nblah".into());
assert_eq!(view.get_line_slice(0, 0, 3), Some("abc"));
assert_eq!(view.get_line_slice(0, 3, 1), Some("👌"));
assert_eq!(view.get_line_slice(0, 3, 2), Some("👌"));
assert_eq!(view.get_line_slice(0, 3, 3), Some("👌d"));
assert_eq!(view.get_line_slice(0, 0, 4), Some("abc👌"));
assert_eq!(view.get_line_slice(0, 0, 5), Some("abc👌"));
assert_eq!(view.get_line_slice(0, 0, 6), Some("abc👌d"));
assert_eq!(view.get_line_slice(1, 0, 4), Some("blah"));
assert_eq!(view.get_line_slice(1, 0, 5), None);
assert_eq!(view.get_line_slice(1, 0, 12), None);
let view = SourceView::new("a\nb\nc\n".into());
assert_eq!(view.get_line(0), Some("a"));
assert_eq!(view.get_line(1), Some("b"));
assert_eq!(view.get_line(2), Some("c"));
assert_eq!(view.get_line(3), Some(""));
assert_eq!(view.get_line(4), None);
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
is_send::<SourceView>();
is_sync::<SourceView>();
}
}