1mod source_index;
35mod source_map_url;
36
37use std::collections::BTreeMap;
38
39use oxc_coverage_types::{FileCoverage, Location};
40use serde::{Deserialize, Serialize};
41
42use crate::source_index::SourceIndex;
43
44pub use crate::source_map_url::{extract_external_source_mapping_url, extract_inline_source_map};
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct V8FunctionCoverage {
53 #[serde(rename = "functionName")]
56 pub function_name: String,
57 pub ranges: Vec<V8CoverageRange>,
61 #[serde(rename = "isBlockCoverage")]
64 pub is_block_coverage: bool,
65}
66
67#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
69pub struct V8CoverageRange {
70 #[serde(rename = "startOffset")]
72 pub start_offset: u32,
73 #[serde(rename = "endOffset")]
75 pub end_offset: u32,
76 pub count: u32,
78}
79
80#[derive(Clone, Copy)]
81struct CoverageRangeWithMode {
82 range: V8CoverageRange,
83 is_block_coverage: bool,
84}
85
86struct CoverageContext<'a> {
87 source_index: &'a SourceIndex,
88 ranges: &'a [V8CoverageRange],
89 arm_ranges: &'a [V8CoverageRange],
90 inheritance_ranges: &'a [CoverageRangeWithMode],
91 wrapper_length_utf16: u32,
92}
93
94impl CoverageContext<'_> {
95 fn count_for_location(&self, loc: &Location) -> u32 {
96 let (start, end) = self.location_utf16_span(loc);
97 smallest_containing_range_count(start, end, self.ranges)
98 }
99
100 fn arm_count_for_arm(
107 &self,
108 arm_loc: &Location,
109 body_byte_span: Option<(u32, u32)>,
110 inherit_enclosing_count: bool,
111 ) -> u32 {
112 if body_byte_span.is_some_and(|(start, end)| start == end) {
113 return 0;
114 }
115 let (arm_start, arm_end) = self.arm_utf16_span(arm_loc, body_byte_span);
116 let (location_start, location_end) = self.location_utf16_span(arm_loc);
117 self.best_arm_range_count(arm_start, arm_end)
118 .or_else(|| self.best_arm_range_count(location_start, location_end))
119 .unwrap_or_else(|| {
120 let has_concrete_body = body_byte_span.is_some_and(|(start, end)| start < end);
121 if inherit_enclosing_count && has_concrete_body {
122 self.smallest_inheritable_range_count(arm_start, arm_end)
123 } else {
124 0
125 }
126 })
127 }
128
129 fn smallest_inheritable_range_count(&self, start: u32, end: u32) -> u32 {
130 self.inheritance_ranges
131 .iter()
132 .find(|entry| {
133 entry.range.start_offset <= start
134 && entry.range.end_offset >= end
135 && (entry.range.start_offset < start || entry.range.end_offset > end)
136 })
137 .filter(|entry| entry.is_block_coverage)
138 .map_or(0, |entry| entry.range.count)
139 }
140
141 fn arm_utf16_span(&self, arm_loc: &Location, body_byte_span: Option<(u32, u32)>) -> (u32, u32) {
142 match body_byte_span {
143 Some((start, end)) => (
144 self.source_index.byte_to_utf16(start).saturating_add(self.wrapper_length_utf16),
145 self.source_index.byte_to_utf16(end).saturating_add(self.wrapper_length_utf16),
146 ),
147 None => self.location_utf16_span(arm_loc),
148 }
149 }
150
151 fn location_utf16_span(&self, loc: &Location) -> (u32, u32) {
152 (
153 self.source_index
154 .position_to_utf16(loc.start.line, loc.start.column)
155 .saturating_add(self.wrapper_length_utf16),
156 self.source_index
157 .position_to_utf16(loc.end.line, loc.end.column)
158 .saturating_add(self.wrapper_length_utf16),
159 )
160 }
161
162 fn best_arm_range_count(&self, arm_start: u32, arm_end: u32) -> Option<u32> {
166 const TOLERANCE: u32 = 4;
167
168 let mut best: Option<(V8CoverageRange, u32)> = None;
169 let lower = arm_start.saturating_sub(TOLERANCE);
170 let upper = arm_start.saturating_add(TOLERANCE);
171 let start = self.arm_ranges.partition_point(|r| r.start_offset < lower);
172 for r in &self.arm_ranges[start..] {
173 if r.start_offset > upper {
174 break;
175 }
176 let dist_start = r.start_offset.abs_diff(arm_start);
177 let dist_end = r.end_offset.abs_diff(arm_end);
178 if dist_end > TOLERANCE {
179 continue;
180 }
181 let distance = dist_start + dist_end;
182 match best {
183 None => best = Some((*r, distance)),
184 Some((prev, prev_distance)) => {
185 let prev_width = prev.end_offset.saturating_sub(prev.start_offset);
186 let this_width = r.end_offset.saturating_sub(r.start_offset);
187 if distance < prev_distance
188 || (distance == prev_distance && this_width < prev_width)
189 {
190 best = Some((*r, distance));
191 }
192 }
193 }
194 }
195 best.map(|(r, _)| r.count)
196 }
197}
198
199pub fn apply_v8_coverage(
213 file_coverage: &mut FileCoverage,
214 source: &str,
215 functions: &[V8FunctionCoverage],
216 wrapper_length_utf16: u32,
217 arm_body_byte_spans: &BTreeMap<String, Vec<(u32, u32)>>,
218) {
219 let source_index = SourceIndex::new(source);
220 let mut ranges: Vec<V8CoverageRange> =
221 functions.iter().flat_map(|f| f.ranges.iter().copied()).collect();
222 ranges.sort_by_key(|r| r.end_offset.saturating_sub(r.start_offset));
223 let mut arm_ranges: Vec<V8CoverageRange> = functions
228 .iter()
229 .filter(|function| function.is_block_coverage)
230 .flat_map(|function| function.ranges.iter().skip(1).copied())
231 .collect();
232 arm_ranges.sort_by_key(|r| r.start_offset);
233 let mut inheritance_ranges: Vec<CoverageRangeWithMode> = functions
234 .iter()
235 .flat_map(|function| {
236 function.ranges.iter().copied().map(|range| CoverageRangeWithMode {
237 range,
238 is_block_coverage: function.is_block_coverage,
239 })
240 })
241 .collect();
242 inheritance_ranges
243 .sort_by_key(|entry| entry.range.end_offset.saturating_sub(entry.range.start_offset));
244 let context = CoverageContext {
245 source_index: &source_index,
246 ranges: &ranges,
247 arm_ranges: &arm_ranges,
248 inheritance_ranges: &inheritance_ranges,
249 wrapper_length_utf16,
250 };
251
252 apply_statement_counts(file_coverage, &context);
253 apply_function_counts(file_coverage, &context);
254 apply_branch_counts(file_coverage, arm_body_byte_spans, &context);
255}
256
257fn apply_statement_counts(file_coverage: &mut FileCoverage, context: &CoverageContext<'_>) {
258 for (id, loc) in &file_coverage.statement_map {
259 let count = context.count_for_location(loc);
260 if let Some(slot) = file_coverage.s.get_mut(id) {
261 *slot = count;
262 }
263 }
264}
265
266fn apply_function_counts(file_coverage: &mut FileCoverage, context: &CoverageContext<'_>) {
267 for (id, fn_entry) in &file_coverage.fn_map {
268 let count = context.count_for_location(&fn_entry.loc);
269 if let Some(slot) = file_coverage.f.get_mut(id) {
270 *slot = count;
271 }
272 }
273}
274
275fn apply_branch_counts(
276 file_coverage: &mut FileCoverage,
277 arm_body_byte_spans: &BTreeMap<String, Vec<(u32, u32)>>,
278 context: &CoverageContext<'_>,
279) {
280 for (id, branch_entry) in &file_coverage.branch_map {
281 let Some(slot) = file_coverage.b.get_mut(id) else {
282 continue;
283 };
284 let body_spans = arm_body_byte_spans.get(id);
285 let inherit_enclosing_count = branch_entry.branch_type == "if";
286 slot.clear();
287 slot.reserve(branch_entry.locations.len());
288 for (arm_idx, loc) in branch_entry.locations.iter().enumerate() {
289 slot.push(context.arm_count_for_arm(
290 loc,
291 body_spans.and_then(|spans| spans.get(arm_idx).copied()),
292 inherit_enclosing_count,
293 ));
294 }
295 }
296}
297
298fn smallest_containing_range_count(start: u32, end: u32, ranges: &[V8CoverageRange]) -> u32 {
307 for r in ranges {
308 if r.start_offset <= start && r.end_offset >= end {
309 return r.count;
310 }
311 }
312 0
313}