use std::path::PathBuf;
use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_traverse::traverse_mut;
use crate::pragma::PragmaMap;
use crate::transform::{
CoverageState, CoverageTransform, generate_cov_fn_name, generate_preamble_source,
};
use crate::types::{FileCoverage, UnhandledPragma};
#[derive(Debug, Clone)]
pub struct InstrumentOptions {
pub coverage_variable: String,
pub source_map: bool,
pub input_source_map: Option<String>,
pub report_logic: bool,
pub ignore_class_methods: Vec<String>,
}
impl Default for InstrumentOptions {
fn default() -> Self {
Self {
coverage_variable: "__coverage__".to_string(),
source_map: false,
input_source_map: None,
report_logic: false,
ignore_class_methods: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct InstrumentResult {
pub code: String,
pub coverage_map: FileCoverage,
pub source_map: Option<String>,
pub unhandled_pragmas: Vec<UnhandledPragma>,
}
fn is_valid_js_identifier(s: &str) -> bool {
!s.is_empty()
&& s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_' || c == '$')
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
}
fn stable_hex_hash(input: &str) -> String {
let mut hash: u64 = 0;
for byte in input.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(u64::from(byte));
}
format!("{hash:x}")
}
pub fn instrument(
source: &str,
filename: &str,
options: &InstrumentOptions,
) -> Result<InstrumentResult, InstrumentError> {
if !is_valid_js_identifier(&options.coverage_variable) {
return Err(InstrumentError::InvalidCoverageVariable(options.coverage_variable.clone()));
}
let allocator = Allocator::default();
let source_type = SourceType::from_path(filename).unwrap_or_default();
let parser = Parser::new(&allocator, source, source_type);
let mut parsed = parser.parse();
if !parsed.errors.is_empty() {
return Err(InstrumentError::ParseError(
parsed.errors.iter().map(|e| format!("{e}")).collect::<Vec<_>>().join("; "),
));
}
let (pragmas, unhandled_pragmas) = PragmaMap::from_program(&parsed.program, source);
if pragmas.ignore_file {
let coverage_map = FileCoverage::from_maps(
filename.to_string(),
std::collections::BTreeMap::new(),
std::collections::BTreeMap::new(),
std::collections::BTreeMap::new(),
&[],
);
return Ok(InstrumentResult {
code: source.to_string(),
coverage_map,
source_map: None,
unhandled_pragmas,
});
}
let semantic_ret = SemanticBuilder::new().build(&parsed.program);
let scoping = semantic_ret.semantic.into_scoping();
let cov_fn_name = generate_cov_fn_name(filename);
let mut transform = CoverageTransform::new(
source,
cov_fn_name.clone(),
options.report_logic,
options.ignore_class_methods.clone(),
);
let state = CoverageState { pragmas };
let scoping = traverse_mut(&mut transform, &allocator, &mut parsed.program, scoping, state);
let mut coverage_map = FileCoverage::from_maps(
filename.to_string(),
transform.statement_map,
transform.fn_map,
transform.branch_map,
&transform.logical_branch_ids,
);
if let Some(ref input_sm) = options.input_source_map {
coverage_map.input_source_map = serde_json::from_str(input_sm).ok();
}
let coverage_hash = stable_hex_hash(
&serde_json::to_string(&coverage_map)
.map_err(|e| InstrumentError::SerializationError(e.to_string()))?,
);
let preamble = generate_preamble_source(
&coverage_map,
&coverage_hash,
&options.coverage_variable,
&cov_fn_name,
options.report_logic,
)
.map_err(|e| InstrumentError::SerializationError(e.to_string()))?;
let codegen_options = CodegenOptions {
source_map_path: if options.source_map { Some(PathBuf::from(filename)) } else { None },
..CodegenOptions::default()
};
let codegen_ret = Codegen::new()
.with_options(codegen_options)
.with_source_text(source)
.with_scoping(Some(scoping))
.build(&parsed.program);
let code = format!("{preamble}{}", codegen_ret.code);
let source_map_json = codegen_ret.map.map(|sm| {
let preamble_lines = preamble.chars().filter(|&c| c == '\n').count() as u32;
let offset_sm = if preamble_lines > 0 {
let builder =
oxc_sourcemap::ConcatSourceMapBuilder::from_sourcemaps(&[(&sm, preamble_lines)]);
builder.into_sourcemap()
} else {
sm
};
if let Some(ref input_sm_json) = options.input_source_map
&& let Ok(input_sm) = oxc_sourcemap::SourceMap::from_json_string(input_sm_json)
{
return compose_source_maps(&offset_sm, &input_sm).to_json_string();
}
offset_sm.to_json_string()
});
Ok(InstrumentResult { code, coverage_map, source_map: source_map_json, unhandled_pragmas })
}
fn compose_source_maps(
output_sm: &oxc_sourcemap::SourceMap,
input_sm: &oxc_sourcemap::SourceMap,
) -> oxc_sourcemap::SourceMap {
let input_lookup = input_sm.generate_lookup_table();
let mut builder = oxc_sourcemap::SourceMapBuilder::default();
for (source, content) in input_sm.get_sources().zip(input_sm.get_source_contents()) {
let content_str = content.map_or("", |c| c.as_ref());
builder.add_source_and_content(source, content_str);
}
for name in input_sm.get_names() {
builder.add_name(name);
}
for token in output_sm.get_tokens() {
let src_line = token.get_src_line();
let src_col = token.get_src_col();
if let Some(original) = input_sm.lookup_token(&input_lookup, src_line, src_col) {
builder.add_token(
token.get_dst_line(),
token.get_dst_col(),
original.get_src_line(),
original.get_src_col(),
original.get_source_id(),
original.get_name_id(),
);
} else {
builder.add_token(token.get_dst_line(), token.get_dst_col(), 0, 0, None, None);
}
}
builder.into_sourcemap()
}
#[derive(Debug, Clone)]
pub enum InstrumentError {
ParseError(String),
InvalidCoverageVariable(String),
SerializationError(String),
}
impl std::fmt::Display for InstrumentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseError(msg) => write!(f, "parse error: {msg}"),
Self::SerializationError(msg) => write!(f, "serialization error: {msg}"),
Self::InvalidCoverageVariable(name) => {
write!(
f,
"invalid coverage variable: {name:?} is not a valid JavaScript identifier"
)
}
}
}
}
impl std::error::Error for InstrumentError {}