use serde::{Deserialize, Serialize};
use crate::types::{FileCoverage, Location};
use crate::{InstrumentOptions, instrument};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V8FunctionCoverage {
#[serde(rename = "functionName")]
pub function_name: String,
pub ranges: Vec<V8CoverageRange>,
#[serde(rename = "isBlockCoverage")]
pub is_block_coverage: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct V8CoverageRange {
#[serde(rename = "startOffset")]
pub start_offset: u32,
#[serde(rename = "endOffset")]
pub end_offset: u32,
pub count: u32,
}
#[derive(Debug)]
pub enum V8ToIstanbulError {
Parse(String),
}
impl std::fmt::Display for V8ToIstanbulError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(msg) => write!(f, "parse error: {msg}"),
}
}
}
impl std::error::Error for V8ToIstanbulError {}
pub fn v8_to_istanbul(
source: &str,
filename: &str,
functions: &[V8FunctionCoverage],
wrapper_length: u32,
) -> Result<FileCoverage, V8ToIstanbulError> {
let instrumented = instrument(source, filename, &InstrumentOptions::default())
.map_err(|e| V8ToIstanbulError::Parse(e.to_string()))?;
let mut file_coverage = instrumented.coverage_map;
let line_offsets = compute_line_offsets(source);
let ranges: Vec<V8CoverageRange> =
functions.iter().flat_map(|f| f.ranges.iter().copied()).collect();
for (id, loc) in &file_coverage.statement_map {
let count = count_for_location(source, loc, &line_offsets, &ranges, wrapper_length);
if let Some(slot) = file_coverage.s.get_mut(id) {
*slot = count;
}
}
for (id, fn_entry) in &file_coverage.fn_map {
let count =
count_for_location(source, &fn_entry.loc, &line_offsets, &ranges, wrapper_length);
if let Some(slot) = file_coverage.f.get_mut(id) {
*slot = count;
}
}
Ok(file_coverage)
}
fn compute_line_offsets(source: &str) -> Vec<u32> {
let mut offsets = vec![0u32];
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
let next = u32::try_from(i + 1).unwrap_or(u32::MAX);
offsets.push(next);
}
}
let end = u32::try_from(source.len()).unwrap_or(u32::MAX);
offsets.push(end);
offsets
}
fn position_to_byte_offset(
source: &str,
line_1based: u32,
col_utf16: u32,
line_offsets: &[u32],
) -> u32 {
if line_1based == 0 {
return 0;
}
let line_idx = (line_1based - 1) as usize;
if line_idx >= line_offsets.len() - 1 {
return *line_offsets.last().unwrap_or(&0);
}
let line_start = line_offsets[line_idx] as usize;
let line_end = line_offsets[line_idx + 1] as usize;
let line_bytes = source.get(line_start..line_end).unwrap_or("");
let mut utf16_remaining = col_utf16;
let mut byte_in_line = 0usize;
for ch in line_bytes.chars() {
if utf16_remaining == 0 {
break;
}
let units = ch.len_utf16() as u32;
if units > utf16_remaining {
break;
}
utf16_remaining -= units;
byte_in_line += ch.len_utf8();
}
u32::try_from(line_start + byte_in_line).unwrap_or(u32::MAX)
}
fn count_for_location(
source: &str,
loc: &Location,
line_offsets: &[u32],
ranges: &[V8CoverageRange],
wrapper_length: u32,
) -> u32 {
let start = position_to_byte_offset(source, loc.start.line, loc.start.column, line_offsets)
+ wrapper_length;
let end = position_to_byte_offset(source, loc.end.line, loc.end.column, line_offsets)
+ wrapper_length;
smallest_containing_range_count(start, end, ranges)
}
fn smallest_containing_range_count(start: u32, end: u32, ranges: &[V8CoverageRange]) -> u32 {
let mut best: Option<V8CoverageRange> = None;
for r in ranges {
if r.start_offset <= start && r.end_offset >= end {
let width = r.end_offset.saturating_sub(r.start_offset);
match best {
None => best = Some(*r),
Some(prev) => {
let prev_width = prev.end_offset.saturating_sub(prev.start_offset);
if width < prev_width {
best = Some(*r);
}
}
}
}
}
best.map_or(0, |r| r.count)
}