use std::collections::BTreeMap;
use oxc_coverage_types::{FileCoverage, Location};
use serde::{Deserialize, Serialize};
#[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,
}
struct CoverageContext<'a> {
source: &'a str,
line_offsets: &'a [u32],
ranges: &'a [V8CoverageRange],
arm_ranges: &'a [V8CoverageRange],
wrapper_length: u32,
}
struct CoverageInput<'a> {
source: &'a str,
functions: &'a [V8FunctionCoverage],
wrapper_length: u32,
arm_body_byte_spans: &'a BTreeMap<String, Vec<(u32, u32)>>,
}
impl CoverageContext<'_> {
fn count_for_location(&self, loc: &Location) -> u32 {
let start =
self.position_to_byte_offset(loc.start.line, loc.start.column) + self.wrapper_length;
let end = self.position_to_byte_offset(loc.end.line, loc.end.column) + self.wrapper_length;
smallest_containing_range_count(start, end, self.ranges)
}
fn arm_count_for_arm(&self, arm_loc: &Location, body_byte_span: Option<(u32, u32)>) -> u32 {
let (arm_start, arm_end) = self.arm_byte_span(arm_loc, body_byte_span);
self.best_arm_range_count(arm_start, arm_end)
}
fn arm_byte_span(&self, arm_loc: &Location, body_byte_span: Option<(u32, u32)>) -> (u32, u32) {
match body_byte_span {
Some((start, end)) if !(start == 0 && end == 0) => {
(start + self.wrapper_length, end + self.wrapper_length)
}
_ => (
self.position_to_byte_offset(arm_loc.start.line, arm_loc.start.column)
+ self.wrapper_length,
self.position_to_byte_offset(arm_loc.end.line, arm_loc.end.column)
+ self.wrapper_length,
),
}
}
fn best_arm_range_count(&self, arm_start: u32, arm_end: u32) -> u32 {
const TOLERANCE: u32 = 4;
let mut best: Option<(V8CoverageRange, u32)> = None;
let lower = arm_start.saturating_sub(TOLERANCE);
let upper = arm_start.saturating_add(TOLERANCE);
let start = self.arm_ranges.partition_point(|r| r.start_offset < lower);
for r in &self.arm_ranges[start..] {
if r.start_offset > upper {
break;
}
let dist_start = r.start_offset.abs_diff(arm_start);
let dist_end = r.end_offset.abs_diff(arm_end);
if dist_end > TOLERANCE {
continue;
}
let distance = dist_start + dist_end;
match best {
None => best = Some((*r, distance)),
Some((prev, prev_distance)) => {
let prev_width = prev.end_offset.saturating_sub(prev.start_offset);
let this_width = r.end_offset.saturating_sub(r.start_offset);
if distance < prev_distance
|| (distance == prev_distance && this_width < prev_width)
{
best = Some((*r, distance));
}
}
}
}
best.map_or(0, |(r, _)| r.count)
}
fn position_to_byte_offset(&self, line_1based: u32, col_utf16: u32) -> u32 {
if line_1based == 0 {
return 0;
}
let line_idx = (line_1based - 1) as usize;
if line_idx >= self.line_offsets.len() - 1 {
return *self.line_offsets.last().unwrap_or(&0);
}
let line_start = self.line_offsets[line_idx] as usize;
let line_end = self.line_offsets[line_idx + 1] as usize;
let line_bytes = self.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)
}
}
pub fn apply_v8_coverage(
file_coverage: &mut FileCoverage,
source: &str,
functions: &[V8FunctionCoverage],
wrapper_length: u32,
arm_body_byte_spans: &BTreeMap<String, Vec<(u32, u32)>>,
) {
let input = CoverageInput { source, functions, wrapper_length, arm_body_byte_spans };
apply_v8_coverage_inner(file_coverage, &input);
}
fn apply_v8_coverage_inner(file_coverage: &mut FileCoverage, input: &CoverageInput<'_>) {
let line_offsets = compute_line_offsets(input.source);
let mut ranges: Vec<V8CoverageRange> =
input.functions.iter().flat_map(|f| f.ranges.iter().copied()).collect();
ranges.sort_by_key(|r| r.end_offset.saturating_sub(r.start_offset));
let mut arm_ranges = ranges.clone();
arm_ranges.sort_by_key(|r| r.start_offset);
let context = CoverageContext {
source: input.source,
line_offsets: &line_offsets,
ranges: &ranges,
arm_ranges: &arm_ranges,
wrapper_length: input.wrapper_length,
};
apply_statement_counts(file_coverage, &context);
apply_function_counts(file_coverage, &context);
apply_branch_counts(file_coverage, input, &context);
}
fn apply_statement_counts(file_coverage: &mut FileCoverage, context: &CoverageContext<'_>) {
for (id, loc) in &file_coverage.statement_map {
let count = context.count_for_location(loc);
if let Some(slot) = file_coverage.s.get_mut(id) {
*slot = count;
}
}
}
fn apply_function_counts(file_coverage: &mut FileCoverage, context: &CoverageContext<'_>) {
for (id, fn_entry) in &file_coverage.fn_map {
let count = context.count_for_location(&fn_entry.loc);
if let Some(slot) = file_coverage.f.get_mut(id) {
*slot = count;
}
}
}
fn apply_branch_counts(
file_coverage: &mut FileCoverage,
input: &CoverageInput<'_>,
context: &CoverageContext<'_>,
) {
for (id, branch_entry) in &file_coverage.branch_map {
let Some(slot) = file_coverage.b.get_mut(id) else {
continue;
};
let body_spans = input.arm_body_byte_spans.get(id);
slot.clear();
slot.reserve(branch_entry.locations.len());
for (arm_idx, loc) in branch_entry.locations.iter().enumerate() {
slot.push(
context.arm_count_for_arm(
loc,
body_spans.and_then(|spans| spans.get(arm_idx).copied()),
),
);
}
}
}
#[must_use]
pub fn extract_external_source_mapping_url(source: &str) -> Option<&str> {
const NEEDLE: &str = "//# sourceMappingURL=";
let idx = source.rfind(NEEDLE)?;
let after = &source[idx + NEEDLE.len()..];
let url = after.lines().next()?.trim();
if url.is_empty() || url.starts_with("data:") {
return None;
}
Some(url)
}
#[must_use]
pub fn extract_inline_source_map(source: &str) -> Option<serde_json::Value> {
const NEEDLE: &str = "//# sourceMappingURL=data:application/json";
let idx = source.rfind(NEEDLE)?;
let line = source[idx..].lines().next()?;
let comma = line.find(',')?;
let payload = &line[comma + 1..];
let is_base64 = line[..comma].contains(";base64");
let json = if is_base64 {
let bytes = decode_base64(payload).ok()?;
String::from_utf8(bytes).ok()?
} else {
urlencoding_decode(payload).ok()?
};
serde_json::from_str(&json).ok()
}
fn decode_base64(input: &str) -> Result<Vec<u8>, ()> {
fn value(c: u8) -> Result<u8, ()> {
match c {
b'A'..=b'Z' => Ok(c - b'A'),
b'a'..=b'z' => Ok(c - b'a' + 26),
b'0'..=b'9' => Ok(c - b'0' + 52),
b'+' | b'-' => Ok(62),
b'/' | b'_' => Ok(63),
_ => Err(()),
}
}
let mut out = Vec::with_capacity(input.len() * 3 / 4);
let mut chunk = [0u8; 4];
let mut len = 0usize;
for byte in input.bytes() {
if byte == b'=' || byte.is_ascii_whitespace() {
continue;
}
chunk[len] = byte;
len += 1;
if len == 4 {
let n0 = value(chunk[0])?;
let n1 = value(chunk[1])?;
let n2 = value(chunk[2])?;
let n3 = value(chunk[3])?;
out.push((n0 << 2) | (n1 >> 4));
out.push((n1 << 4) | (n2 >> 2));
out.push((n2 << 6) | n3);
len = 0;
}
}
if len == 1 {
return Err(());
}
if len > 1 {
let n0 = value(chunk[0])?;
let n1 = value(chunk[1])?;
out.push((n0 << 2) | (n1 >> 4));
if len > 2 {
let n2 = value(chunk[2])?;
out.push((n1 << 4) | (n2 >> 2));
}
}
Ok(out)
}
fn urlencoding_decode(input: &str) -> Result<String, ()> {
let mut out = Vec::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = (bytes[i + 1] as char).to_digit(16).ok_or(())? as u8;
let lo = (bytes[i + 2] as char).to_digit(16).ok_or(())? as u8;
out.push((hi << 4) | lo);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
String::from_utf8(out).map_err(|_| ())
}
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 smallest_containing_range_count(start: u32, end: u32, ranges: &[V8CoverageRange]) -> u32 {
for r in ranges {
if r.start_offset <= start && r.end_offset >= end {
return r.count;
}
}
0
}