mod source_index;
mod source_map_url;
use std::collections::BTreeMap;
use oxc_coverage_types::{FileCoverage, Location};
use serde::{Deserialize, Serialize};
use crate::source_index::SourceIndex;
pub use crate::source_map_url::{extract_external_source_mapping_url, extract_inline_source_map};
#[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(Clone, Copy)]
struct CoverageRangeWithMode {
range: V8CoverageRange,
is_block_coverage: bool,
}
struct CoverageContext<'a> {
source_index: &'a SourceIndex,
ranges: &'a [V8CoverageRange],
arm_ranges: &'a [V8CoverageRange],
inheritance_ranges: &'a [CoverageRangeWithMode],
wrapper_length_utf16: u32,
}
impl CoverageContext<'_> {
fn count_for_location(&self, loc: &Location) -> u32 {
let (start, end) = self.location_utf16_span(loc);
smallest_containing_range_count(start, end, self.ranges)
}
fn arm_count_for_arm(
&self,
arm_loc: &Location,
body_byte_span: Option<(u32, u32)>,
inherit_enclosing_count: bool,
) -> u32 {
if body_byte_span.is_some_and(|(start, end)| start == end) {
return 0;
}
let (arm_start, arm_end) = self.arm_utf16_span(arm_loc, body_byte_span);
let (location_start, location_end) = self.location_utf16_span(arm_loc);
self.best_arm_range_count(arm_start, arm_end)
.or_else(|| self.best_arm_range_count(location_start, location_end))
.unwrap_or_else(|| {
let has_concrete_body = body_byte_span.is_some_and(|(start, end)| start < end);
if inherit_enclosing_count && has_concrete_body {
self.smallest_inheritable_range_count(arm_start, arm_end)
} else {
0
}
})
}
fn smallest_inheritable_range_count(&self, start: u32, end: u32) -> u32 {
self.inheritance_ranges
.iter()
.find(|entry| {
entry.range.start_offset <= start
&& entry.range.end_offset >= end
&& (entry.range.start_offset < start || entry.range.end_offset > end)
})
.filter(|entry| entry.is_block_coverage)
.map_or(0, |entry| entry.range.count)
}
fn arm_utf16_span(&self, arm_loc: &Location, body_byte_span: Option<(u32, u32)>) -> (u32, u32) {
match body_byte_span {
Some((start, end)) => (
self.source_index.byte_to_utf16(start).saturating_add(self.wrapper_length_utf16),
self.source_index.byte_to_utf16(end).saturating_add(self.wrapper_length_utf16),
),
None => self.location_utf16_span(arm_loc),
}
}
fn location_utf16_span(&self, loc: &Location) -> (u32, u32) {
(
self.source_index
.position_to_utf16(loc.start.line, loc.start.column)
.saturating_add(self.wrapper_length_utf16),
self.source_index
.position_to_utf16(loc.end.line, loc.end.column)
.saturating_add(self.wrapper_length_utf16),
)
}
fn best_arm_range_count(&self, arm_start: u32, arm_end: u32) -> Option<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(|(r, _)| r.count)
}
}
pub fn apply_v8_coverage(
file_coverage: &mut FileCoverage,
source: &str,
functions: &[V8FunctionCoverage],
wrapper_length_utf16: u32,
arm_body_byte_spans: &BTreeMap<String, Vec<(u32, u32)>>,
) {
let source_index = SourceIndex::new(source);
let mut ranges: Vec<V8CoverageRange> =
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: Vec<V8CoverageRange> = functions
.iter()
.filter(|function| function.is_block_coverage)
.flat_map(|function| function.ranges.iter().skip(1).copied())
.collect();
arm_ranges.sort_by_key(|r| r.start_offset);
let mut inheritance_ranges: Vec<CoverageRangeWithMode> = functions
.iter()
.flat_map(|function| {
function.ranges.iter().copied().map(|range| CoverageRangeWithMode {
range,
is_block_coverage: function.is_block_coverage,
})
})
.collect();
inheritance_ranges
.sort_by_key(|entry| entry.range.end_offset.saturating_sub(entry.range.start_offset));
let context = CoverageContext {
source_index: &source_index,
ranges: &ranges,
arm_ranges: &arm_ranges,
inheritance_ranges: &inheritance_ranges,
wrapper_length_utf16,
};
apply_statement_counts(file_coverage, &context);
apply_function_counts(file_coverage, &context);
apply_branch_counts(file_coverage, arm_body_byte_spans, &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,
arm_body_byte_spans: &BTreeMap<String, Vec<(u32, u32)>>,
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 = arm_body_byte_spans.get(id);
let inherit_enclosing_count = branch_entry.branch_type == "if";
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()),
inherit_enclosing_count,
));
}
}
}
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
}