use std::path::{Path, PathBuf};
use oxc_coverage_instrument::{
FileCoverage, Location, Position, V8CoverageRange, V8FunctionCoverage,
};
use rustc_hash::FxHashSet;
use serde::Deserialize;
use srcmap_sourcemap::{Bias, SourceMap};
#[derive(Deserialize)]
pub(super) struct SourceMapCacheEntry {
#[serde(rename = "lineLengths")]
line_lengths: Vec<u32>,
data: serde_json::Value,
}
pub(super) struct GeneratedScript {
map: SourceMap,
line_starts: Vec<u32>,
generated_len: u32,
}
impl GeneratedScript {
pub(super) fn parse(entry: &SourceMapCacheEntry) -> Option<Self> {
let map = SourceMap::from_json(&entry.data.to_string()).ok()?;
let mut line_starts = Vec::with_capacity(entry.line_lengths.len());
let mut offset = 0u32;
for length in &entry.line_lengths {
line_starts.push(offset);
offset = offset.saturating_add(*length).saturating_add(1);
}
let generated_len = offset.saturating_sub(1);
Some(Self {
map,
line_starts,
generated_len,
})
}
pub(super) fn source_paths(&self, script_url: &str) -> Vec<(u32, PathBuf)> {
let script_dir = url::Url::parse(script_url)
.ok()
.and_then(|url| url.to_file_path().ok())
.and_then(|path| path.parent().map(Path::to_path_buf));
self.map
.sources
.iter()
.enumerate()
.filter_map(|(index, source)| {
let path = source_path(source, script_dir.as_deref())?;
Some((u32::try_from(index).ok()?, path))
})
.collect()
}
pub(super) fn matches(&self, functions: &[V8FunctionCoverage]) -> bool {
super::module_range_end(functions) == Some(self.generated_len)
}
pub(super) fn content_matches(&self, source_index: u32, text: &str) -> bool {
let strip = |value: &str| value.strip_prefix('\u{FEFF}').unwrap_or(value).to_owned();
match self
.map
.sources_content
.get(source_index as usize)
.and_then(Option::as_deref)
{
Some(content) => strip(content) == strip(text),
None => true,
}
}
pub(super) fn apply(
&self,
coverage: &mut FileCoverage,
source_index: u32,
functions: &[V8FunctionCoverage],
) -> FxHashSet<String> {
let ranges = ranges_by_width(functions);
let source = self.map.source(source_index).to_owned();
let count_at = |position: &Position| -> Option<u32> {
let offset = self.generated_offset(&source, position)?;
Some(smallest_range_count(&ranges, offset))
};
let mut unmapped = FxHashSet::default();
let mut statement_counts = Vec::with_capacity(coverage.statement_map.len());
for (id, location) in &coverage.statement_map {
match count_at(&location.start) {
Some(count) => statement_counts.push((id.clone(), count)),
None => {
unmapped.insert(id.clone());
}
}
}
for (id, count) in statement_counts {
coverage.s.insert(id, count);
}
let function_counts: Vec<(String, u32)> = coverage
.fn_map
.iter()
.map(|(id, entry)| {
let count = count_at(&entry.loc.start).or_else(|| {
coverage
.statement_map
.iter()
.filter(|(statement, location)| {
!unmapped.contains(*statement) && contains(&entry.loc, &location.start)
})
.min_by_key(|(_, location)| (location.start.line, location.start.column))
.and_then(|(statement, _)| coverage.s.get(statement).copied())
});
(id.clone(), count.unwrap_or(0))
})
.collect();
for (id, count) in function_counts {
coverage.f.insert(id, count);
}
unmapped
}
fn generated_offset(&self, source: &str, position: &Position) -> Option<u32> {
let line = position.line.checked_sub(1)?;
let generated = self
.map
.generated_position_for(source, line, position.column)
.or_else(|| {
self.map.generated_position_for_with_bias(
source,
line,
position.column,
Bias::LeastUpperBound,
)
})?;
let line_start = *self.line_starts.get(generated.line as usize)?;
Some(line_start.saturating_add(generated.column))
}
}
fn contains(location: &Location, position: &Position) -> bool {
let key = |p: &Position| (p.line, p.column);
key(&location.start) <= key(position) && key(position) < key(&location.end)
}
fn source_path(source: &str, script_dir: Option<&Path>) -> Option<PathBuf> {
if let Ok(url) = url::Url::parse(source) {
return match url.scheme() {
"file" => url.to_file_path().ok(),
"webpack" | "vite" => resolve_virtual_source(&url, script_dir?),
_ if fallow_types::path_util::looks_like_windows_absolute_path(source) => {
Some(PathBuf::from(source))
}
_ => None,
};
}
let path = PathBuf::from(source);
if fallow_types::path_util::is_absolute_path_any_platform(&path) {
return Some(path);
}
Some(script_dir?.join(path))
}
fn resolve_virtual_source(url: &url::Url, script_dir: &Path) -> Option<PathBuf> {
let path = url.path().trim_start_matches('/');
let mut candidates = Vec::new();
if let Some(host) = url.host_str().map(|host| host.trim_matches('/'))
&& !host.is_empty()
&& !matches!(host, "." | "_N_E")
{
candidates.push(PathBuf::from(host).join(path));
}
if !path.is_empty() {
candidates.push(PathBuf::from(path));
}
script_dir.ancestors().find_map(|base| {
candidates
.iter()
.map(|candidate| base.join(candidate))
.find(|resolved| resolved.is_file())
})
}
fn ranges_by_width(functions: &[V8FunctionCoverage]) -> Vec<V8CoverageRange> {
let mut ranges: Vec<V8CoverageRange> = functions
.iter()
.flat_map(|function| function.ranges.iter().copied())
.collect();
ranges.sort_by_key(|range| range.end_offset.saturating_sub(range.start_offset));
ranges
}
fn smallest_range_count(ranges_by_width: &[V8CoverageRange], offset: u32) -> u32 {
ranges_by_width
.iter()
.find(|range| range.start_offset <= offset && offset < range.end_offset)
.map_or(0, |range| range.count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_lengths_give_line_starts_and_length() {
let entry = SourceMapCacheEntry {
line_lengths: vec![10, 0, 5],
data: serde_json::json!({ "version": 3, "sources": [], "mappings": "" }),
};
let script = GeneratedScript::parse(&entry).unwrap();
assert_eq!(script.line_starts, vec![0, 11, 12]);
assert_eq!(script.generated_len, 17);
}
#[test]
fn virtual_webpack_sources_resolve_from_an_ancestor() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("dist")).unwrap();
std::fs::write(dir.path().join("src/a.ts"), "export const a = 1;\n").unwrap();
let entry = SourceMapCacheEntry {
line_lengths: vec![1],
data: serde_json::json!({
"version": 3,
"sources": ["webpack://app/./src/a.ts", "webpack://app/./src/missing.ts"],
"mappings": ""
}),
};
let script = GeneratedScript::parse(&entry).unwrap();
let bundle = url::Url::from_file_path(dir.path().join("dist/bundle.js")).unwrap();
let paths = script.source_paths(bundle.as_str());
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].0, 0);
assert!(paths[0].1.ends_with(Path::new("src/a.ts")));
}
#[test]
fn sources_resolve_against_the_script_directory() {
let dir = if cfg!(windows) {
"file:///C:/p/dist/"
} else {
"file:///p/dist/"
};
let entry = SourceMapCacheEntry {
line_lengths: vec![1],
data: serde_json::json!({
"version": 3,
"sources": ["../src/a.ts", "webpack://app/./b.ts"],
"mappings": ""
}),
};
let script = GeneratedScript::parse(&entry).unwrap();
let paths = script.source_paths(&format!("{dir}bundle.js"));
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].0, 0);
assert!(paths[0].1.ends_with(Path::new("dist/../src/a.ts")));
}
#[test]
fn smallest_range_wins() {
let ranges = ranges_by_width(&[V8FunctionCoverage {
function_name: String::new(),
is_block_coverage: true,
ranges: vec![
V8CoverageRange {
start_offset: 0,
end_offset: 100,
count: 1,
},
V8CoverageRange {
start_offset: 40,
end_offset: 60,
count: 0,
},
],
}]);
assert_eq!(smallest_range_count(&ranges, 10), 1);
assert_eq!(smallest_range_count(&ranges, 50), 0);
assert_eq!(smallest_range_count(&ranges, 100), 0);
}
}