use std::collections::BTreeMap;
use std::fmt::Write;
use std::mem;
use oxc_allocator::Vec as ArenaVec;
use oxc_ast::ast::*;
use oxc_span::{GetSpan, SPAN, Span};
use oxc_syntax::operator::{LogicalOperator, UpdateOperator};
use oxc_traverse::{Traverse, TraverseCtx};
use crate::pragma::{IgnoreType, PragmaMap};
use crate::types::{BranchEntry, FileCoverage, FnEntry, Location, Position};
pub struct CoverageState {
pub pragmas: PragmaMap,
}
pub struct CoverageTransform {
line_offsets: Vec<u32>,
fn_counter: usize,
stmt_counter: usize,
branch_counter: usize,
pub fn_map: BTreeMap<String, FnEntry>,
pub statement_map: BTreeMap<String, Location>,
pub branch_map: BTreeMap<String, BranchEntry>,
pending_name: Option<String>,
pending_stmts: Vec<PendingInsertion>,
pending_fn_counters: Vec<usize>,
skip_next: bool,
skip_current_var_decl: bool,
cov_fn_name: String,
report_logic: bool,
ignore_class_methods: Vec<String>,
pub logical_branch_ids: Vec<usize>,
}
struct PendingInsertion {
target_start: u32,
counter_id: usize,
counter_type: CounterType,
}
#[derive(Clone, Copy)]
enum CounterType {
Statement,
BranchLeft,
}
impl CoverageTransform {
pub fn new(
source: &str,
cov_fn_name: String,
report_logic: bool,
ignore_class_methods: Vec<String>,
) -> Self {
let line_offsets: Vec<u32> = std::iter::once(0)
.chain(
source
.bytes()
.enumerate()
.filter(|(_, b)| *b == b'\n')
.map(|(i, _)| (i + 1) as u32),
)
.collect();
Self {
line_offsets,
fn_counter: 0,
stmt_counter: 0,
branch_counter: 0,
fn_map: BTreeMap::new(),
statement_map: BTreeMap::new(),
branch_map: BTreeMap::new(),
pending_name: None,
pending_stmts: Vec::new(),
pending_fn_counters: Vec::new(),
skip_next: false,
skip_current_var_decl: false,
cov_fn_name,
report_logic,
ignore_class_methods,
logical_branch_ids: Vec::new(),
}
}
fn span_to_location(&self, span: Span) -> Location {
Location {
start: self.offset_to_position(span.start),
end: self.offset_to_position(span.end),
}
}
fn offset_to_position(&self, offset: u32) -> Position {
let line = self.line_offsets.partition_point(|&o| o <= offset).saturating_sub(1);
let col = offset - self.line_offsets[line];
Position { line: (line + 1) as u32, column: col }
}
fn add_function(&mut self, name: String, decl_span: Span, body_span: Span) -> usize {
let id_num = self.fn_counter;
let id = id_num.to_string();
self.fn_counter += 1;
let line = self.offset_to_position(decl_span.start).line;
self.fn_map.insert(
id,
FnEntry {
name,
line,
decl: self.span_to_location(decl_span),
loc: self.span_to_location(body_span),
},
);
id_num
}
fn add_statement(&mut self, span: Span) -> usize {
let id_num = self.stmt_counter;
let id = id_num.to_string();
self.stmt_counter += 1;
self.statement_map.insert(id, self.span_to_location(span));
id_num
}
fn add_branch(&mut self, branch_type: &str, span: Span, locations: &[Span]) -> usize {
let id_num = self.branch_counter;
let id = id_num.to_string();
self.branch_counter += 1;
let loc = self.span_to_location(span);
let line = loc.start.line;
self.branch_map.insert(
id,
BranchEntry {
loc,
line,
branch_type: branch_type.to_string(),
locations: locations.iter().map(|s| self.span_to_location(*s)).collect(),
},
);
id_num
}
fn resolve_function_name(&mut self, func: &Function) -> String {
if let Some(name) = self.pending_name.take() {
return name;
}
if let Some(id) = &func.id {
return id.name.to_string();
}
format!("(anonymous_{})", self.fn_counter)
}
}
fn alloc_str<'a>(s: &str, ctx: &TraverseCtx<'a, CoverageState>) -> &'a str {
ctx.ast.allocator.alloc_str(s)
}
fn build_counter_expr<'a>(
cov_fn_name: &str,
counter_type: &str,
counter_id: usize,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Expression<'a> {
let name = alloc_str(cov_fn_name, ctx);
let callee = ctx.ast.expression_identifier(SPAN, name);
let call = ctx.ast.expression_call(
SPAN,
callee,
None::<TSTypeParameterInstantiation>,
ctx.ast.vec(),
false,
);
let ct = alloc_str(counter_type, ctx);
let member =
ctx.ast.member_expression_static(SPAN, call, ctx.ast.identifier_name(SPAN, ct), false);
let member_expr = Expression::from(member);
let computed = ctx.ast.member_expression_computed(
SPAN,
member_expr,
ctx.ast.expression_numeric_literal(
SPAN,
counter_id as f64,
None,
oxc_syntax::number::NumberBase::Decimal,
),
false,
);
let target = SimpleAssignmentTarget::from(computed);
ctx.ast.expression_update(SPAN, UpdateOperator::Increment, true, target)
}
fn build_branch_counter_expr<'a>(
cov_fn_name: &str,
branch_id: usize,
path_idx: usize,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Expression<'a> {
let name = alloc_str(cov_fn_name, ctx);
let callee = ctx.ast.expression_identifier(SPAN, name);
let call = ctx.ast.expression_call(
SPAN,
callee,
None::<TSTypeParameterInstantiation>,
ctx.ast.vec(),
false,
);
let member =
ctx.ast.member_expression_static(SPAN, call, ctx.ast.identifier_name(SPAN, "b"), false);
let member_expr = Expression::from(member);
let computed1 = ctx.ast.member_expression_computed(
SPAN,
member_expr,
ctx.ast.expression_numeric_literal(
SPAN,
branch_id as f64,
None,
oxc_syntax::number::NumberBase::Decimal,
),
false,
);
let computed1_expr = Expression::from(computed1);
let computed2 = ctx.ast.member_expression_computed(
SPAN,
computed1_expr,
ctx.ast.expression_numeric_literal(
SPAN,
path_idx as f64,
None,
oxc_syntax::number::NumberBase::Decimal,
),
false,
);
let target = SimpleAssignmentTarget::from(computed2);
ctx.ast.expression_update(SPAN, UpdateOperator::Increment, true, target)
}
fn build_counter_stmt<'a>(
cov_fn_name: &str,
counter_type: &str,
counter_id: usize,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Statement<'a> {
let expr = build_counter_expr(cov_fn_name, counter_type, counter_id, ctx);
ctx.ast.statement_expression(SPAN, expr)
}
fn build_branch_counter_stmt<'a>(
cov_fn_name: &str,
branch_id: usize,
path_idx: usize,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Statement<'a> {
let expr = build_branch_counter_expr(cov_fn_name, branch_id, path_idx, ctx);
ctx.ast.statement_expression(SPAN, expr)
}
pub fn generate_preamble_source(
coverage: &FileCoverage,
coverage_var: &str,
cov_fn_name: &str,
report_logic: bool,
) -> Result<String, serde_json::Error> {
let estimated_size = 256
+ coverage.statement_map.len() * 80
+ coverage.fn_map.len() * 120
+ coverage.branch_map.len() * 120;
let mut buf = String::with_capacity(estimated_size);
let _ = write!(buf, "var {cov_fn_name} = (function () {{ var path = ");
buf.push_str(&serde_json::to_string(&coverage.path)?);
let _ = write!(buf, "; var gcv = '{coverage_var}'; var coverageData = ");
buf.push_str(&serde_json::to_string(coverage)?);
let _ = writeln!(
buf,
"; var coverage = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : this; if (!coverage[gcv]) {{ coverage[gcv] = {{}}; }} if (!coverage[gcv][path]) {{ coverage[gcv][path] = coverageData; }} var actualCoverage = coverage[gcv][path]; return actualCoverage; }});"
);
if report_logic {
let _ = writeln!(buf, "var {cov_fn_name}_temp;");
let _ = writeln!(
buf,
"function {cov_fn_name}_bt(val, id, idx) {{ {cov_fn_name}_temp = val; if ({cov_fn_name}_temp && (!Array.isArray({cov_fn_name}_temp) || {cov_fn_name}_temp.length) && (Object.getPrototypeOf({cov_fn_name}_temp) !== Object.prototype || Object.values({cov_fn_name}_temp).length)) {{ ++{cov_fn_name}().bT[id][idx]; }} return {cov_fn_name}_temp; }}"
);
}
Ok(buf)
}
pub fn generate_cov_fn_name(file_path: &str) -> String {
let mut hash: u64 = 0;
for byte in file_path.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(u64::from(byte));
}
format!("cov_{hash:x}")
}
fn dummy_expr<'a>(ctx: &TraverseCtx<'a, CoverageState>) -> Expression<'a> {
ctx.ast.expression_numeric_literal(SPAN, 0.0, None, oxc_syntax::number::NumberBase::Decimal)
}
fn is_parent_logical(ctx: &TraverseCtx<'_, CoverageState>) -> bool {
use oxc_traverse::Ancestor;
for a in ctx.ancestors() {
match a {
Ancestor::ParenthesizedExpressionExpression(_) => {}
Ancestor::LogicalExpressionLeft(_) | Ancestor::LogicalExpressionRight(_) => {
return true;
}
_ => return false,
}
}
false
}
fn collect_logical_leaf_spans(expr: &LogicalExpression) -> Vec<Span> {
let mut spans = Vec::new();
collect_logical_leaves_inner(&expr.left, &mut spans);
collect_logical_leaves_inner(&expr.right, &mut spans);
spans
}
fn collect_logical_leaves_inner(expr: &Expression, spans: &mut Vec<Span>) {
if let Expression::ParenthesizedExpression(paren) = expr {
collect_logical_leaves_inner(&paren.expression, spans);
return;
}
if let Expression::LogicalExpression(logical) = expr {
collect_logical_leaves_inner(&logical.left, spans);
collect_logical_leaves_inner(&logical.right, spans);
} else {
spans.push(expr.span());
}
}
fn wrap_logical_leaf<'a>(
operand: &mut Expression<'a>,
cov_fn_name: &str,
branch_id: usize,
path_idx: usize,
report_logic: bool,
ctx: &TraverseCtx<'a, CoverageState>,
) {
let counter = build_branch_counter_expr(cov_fn_name, branch_id, path_idx, ctx);
let orig = mem::replace(operand, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig);
let branch_wrapped = ctx.ast.expression_sequence(SPAN, items);
if report_logic {
let bt_name = alloc_str(&format!("{cov_fn_name}_bt"), ctx);
let callee = ctx.ast.expression_identifier(SPAN, bt_name);
let mut args = ctx.ast.vec();
args.push(Argument::from(branch_wrapped));
args.push(Argument::from(ctx.ast.expression_numeric_literal(
SPAN,
branch_id as f64,
None,
oxc_syntax::number::NumberBase::Decimal,
)));
args.push(Argument::from(ctx.ast.expression_numeric_literal(
SPAN,
path_idx as f64,
None,
oxc_syntax::number::NumberBase::Decimal,
)));
*operand = ctx.ast.expression_call(
SPAN,
callee,
None::<TSTypeParameterInstantiation>,
args,
false,
);
} else {
*operand = branch_wrapped;
}
}
fn wrap_logical_leaves<'a>(
expr: &mut LogicalExpression<'a>,
cov_fn_name: &str,
branch_id: usize,
path_idx: &mut usize,
report_logic: bool,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
wrap_logical_operand(&mut expr.left, cov_fn_name, branch_id, path_idx, report_logic, ctx);
wrap_logical_operand(&mut expr.right, cov_fn_name, branch_id, path_idx, report_logic, ctx);
}
fn wrap_logical_operand<'a>(
operand: &mut Expression<'a>,
cov_fn_name: &str,
branch_id: usize,
path_idx: &mut usize,
report_logic: bool,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if let Expression::ParenthesizedExpression(paren) = operand {
return wrap_logical_operand(
&mut paren.expression,
cov_fn_name,
branch_id,
path_idx,
report_logic,
ctx,
);
}
if let Expression::LogicalExpression(inner) = operand {
wrap_logical_leaves(inner, cov_fn_name, branch_id, path_idx, report_logic, ctx);
} else {
wrap_logical_leaf(operand, cov_fn_name, branch_id, *path_idx, report_logic, ctx);
*path_idx += 1;
}
}
impl<'a> Traverse<'a, CoverageState> for CoverageTransform {
fn enter_function(
&mut self,
func: &mut Function<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let has_pragma = ctx.state.pragmas.get(func.span.start) == Some(IgnoreType::Next);
let skip = has_pragma || self.skip_next;
self.skip_next = false;
if skip {
self.pending_name = None;
return;
}
let name = self.resolve_function_name(func);
let decl_span = if let Some(id) = &func.id {
Span::new(func.span.start, id.span.end)
} else {
let end = func.body.as_ref().map_or(func.span.start, |b| b.span.start);
Span::new(func.span.start, end)
};
if let Some(body) = &func.body {
let fn_id = self.add_function(name, decl_span, body.span);
self.pending_fn_counters.push(fn_id);
}
}
fn enter_function_body(
&mut self,
body: &mut FunctionBody<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if let Some(fn_id) = self.pending_fn_counters.pop() {
let cov_fn = self.cov_fn_name.as_str();
let counter = build_counter_stmt(cov_fn, "f", fn_id, ctx);
body.statements.insert(0, counter);
}
}
fn enter_arrow_function_expression(
&mut self,
arrow: &mut ArrowFunctionExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if ctx.state.pragmas.get(arrow.span.start) == Some(IgnoreType::Next) || self.skip_next {
self.skip_next = false;
self.pending_name = None;
return;
}
let name =
self.pending_name.take().unwrap_or_else(|| format!("(anonymous_{})", self.fn_counter));
let fn_id = self.add_function(
name,
Span::new(arrow.span.start, arrow.span.start + 1),
arrow.body.span,
);
self.pending_fn_counters.push(fn_id);
}
fn exit_arrow_function_expression(
&mut self,
arrow: &mut ArrowFunctionExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if arrow.expression && !arrow.body.statements.is_empty() {
if let Some(Statement::ExpressionStatement(expr_stmt)) =
arrow.body.statements.last_mut()
{
let dummy = dummy_expr(ctx);
let expr = mem::replace(&mut expr_stmt.expression, dummy);
let last_idx = arrow.body.statements.len() - 1;
arrow.body.statements[last_idx] = ctx.ast.statement_return(SPAN, Some(expr));
}
arrow.expression = false;
}
}
fn enter_variable_declaration(
&mut self,
decl: &mut VariableDeclaration<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if ctx.state.pragmas.get(decl.span.start) == Some(IgnoreType::Next) {
self.skip_current_var_decl = true;
}
}
fn exit_variable_declaration(
&mut self,
_decl: &mut VariableDeclaration<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.skip_current_var_decl = false;
}
fn enter_variable_declarator(
&mut self,
decl: &mut VariableDeclarator<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.skip_current_var_decl {
if matches!(
decl.init,
Some(Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_))
) {
self.skip_next = true;
}
return;
}
if let Some(id) = decl.id.get_binding_identifier()
&& decl.init.as_ref().is_some_and(|init| {
matches!(
init,
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
)
})
{
self.pending_name = Some(id.name.to_string());
}
let Some(init) = decl.init.as_mut() else { return };
let init_span = init.span();
if init_span.start == 0 && init_span.end == 0 {
return;
}
let stmt_id = self.add_statement(init_span);
let cov_fn = self.cov_fn_name.as_str();
let counter = build_counter_expr(cov_fn, "s", stmt_id, ctx);
let orig = mem::replace(init, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig);
*init = ctx.ast.expression_sequence(SPAN, items);
}
fn exit_variable_declarator(
&mut self,
_decl: &mut VariableDeclarator<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.pending_name = None;
}
fn enter_method_definition(
&mut self,
method: &mut MethodDefinition<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let name = match &method.key {
PropertyKey::StaticIdentifier(id) => id.name.to_string(),
PropertyKey::StringLiteral(s) => s.value.to_string(),
_ => return,
};
if self.ignore_class_methods.contains(&name) {
self.skip_next = true;
return;
}
self.pending_name = Some(name);
}
fn exit_method_definition(
&mut self,
_method: &mut MethodDefinition<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.pending_name = None;
}
fn enter_property_definition(
&mut self,
prop: &mut PropertyDefinition<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let Some(value) = &prop.value else { return };
let span = value.span();
if span.start == 0 && span.end == 0 {
return;
}
if ctx.state.pragmas.get(prop.span.start) == Some(IgnoreType::Next) || self.skip_next {
self.skip_next = false;
return;
}
let stmt_id = self.add_statement(span);
let cov_fn = self.cov_fn_name.as_str();
let counter = build_counter_expr(cov_fn, "s", stmt_id, ctx);
let orig = mem::replace(prop.value.as_mut().unwrap(), dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig);
*prop.value.as_mut().unwrap() = ctx.ast.expression_sequence(SPAN, items);
}
fn enter_statement(
&mut self,
stmt: &mut Statement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let span = stmt.span();
if span.start == 0 && span.end == 0 {
return;
}
if matches!(
stmt,
Statement::BlockStatement(_)
| Statement::EmptyStatement(_)
| Statement::FunctionDeclaration(_)
| Statement::ClassDeclaration(_)
| Statement::VariableDeclaration(_)
| Statement::ImportDeclaration(_)
| Statement::ExportNamedDeclaration(_)
| Statement::ExportDefaultDeclaration(_)
| Statement::ExportAllDeclaration(_)
| Statement::TSTypeAliasDeclaration(_)
| Statement::TSInterfaceDeclaration(_)
| Statement::TSEnumDeclaration(_)
| Statement::TSModuleDeclaration(_)
| Statement::TSImportEqualsDeclaration(_)
| Statement::TSExportAssignment(_)
| Statement::TSNamespaceExportDeclaration(_)
) {
return;
}
if ctx.state.pragmas.get(span.start) == Some(IgnoreType::Next) {
self.skip_next = true;
return;
}
if self.skip_next {
self.skip_next = false;
return;
}
let stmt_id = self.add_statement(span);
self.pending_stmts.push(PendingInsertion {
target_start: span.start,
counter_id: stmt_id,
counter_type: CounterType::Statement,
});
}
fn exit_statements(
&mut self,
stmts: &mut ArenaVec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.pending_stmts.is_empty() {
return;
}
let cov_fn = self.cov_fn_name.as_str();
let mut insertions: Vec<(usize, Statement<'a>)> = Vec::new();
let pending = &mut self.pending_stmts;
for (idx, stmt) in stmts.iter().enumerate() {
if pending.is_empty() {
break;
}
let span = stmt.span();
if span.start == 0 && span.end == 0 {
continue;
}
let start = span.start;
let mut i = 0;
while i < pending.len() {
if pending[i].target_start == start {
let p = pending.swap_remove(i);
let counter = match p.counter_type {
CounterType::Statement => {
build_counter_stmt(cov_fn, "s", p.counter_id, ctx)
}
CounterType::BranchLeft => {
build_branch_counter_stmt(cov_fn, p.counter_id, 0, ctx)
}
};
insertions.push((idx, counter));
} else {
i += 1;
}
}
}
if insertions.is_empty() {
return;
}
insertions.sort_by(|a, b| b.0.cmp(&a.0));
for (idx, counter) in insertions {
stmts.insert(idx, counter);
}
}
fn enter_if_statement(
&mut self,
stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let pragma = ctx.state.pragmas.get(stmt.span.start);
let consequent_span = stmt.consequent.span();
let alternate_span = stmt
.alternate
.as_ref()
.map_or_else(|| Span::new(stmt.span.end, stmt.span.end), |alt| alt.span());
let branch_id = self.add_branch("if", stmt.span, &[consequent_span, alternate_span]);
let cov_fn = self.cov_fn_name.as_str();
if pragma != Some(IgnoreType::If) {
inject_branch_counter_into_statement(&mut stmt.consequent, cov_fn, branch_id, 0, ctx);
}
if let Some(alt) = &mut stmt.alternate
&& pragma != Some(IgnoreType::Else)
{
inject_branch_counter_into_statement(alt, cov_fn, branch_id, 1, ctx);
}
}
fn enter_conditional_expression(
&mut self,
expr: &mut ConditionalExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let branch_id = self.add_branch(
"cond-expr",
expr.span,
&[expr.consequent.span(), expr.alternate.span()],
);
let cov_fn = self.cov_fn_name.as_str();
let counter0 = build_branch_counter_expr(cov_fn, branch_id, 0, ctx);
let orig_consequent = mem::replace(&mut expr.consequent, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter0);
items.push(orig_consequent);
expr.consequent = ctx.ast.expression_sequence(SPAN, items);
let counter1 = build_branch_counter_expr(cov_fn, branch_id, 1, ctx);
let orig_alternate = mem::replace(&mut expr.alternate, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter1);
items.push(orig_alternate);
expr.alternate = ctx.ast.expression_sequence(SPAN, items);
}
fn enter_switch_statement(
&mut self,
stmt: &mut SwitchStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let case_spans: Vec<Span> = stmt.cases.iter().map(|c| c.span).collect();
let branch_id = self.add_branch("switch", stmt.span, &case_spans);
let cov_fn = self.cov_fn_name.as_str();
for (path_idx, case) in stmt.cases.iter_mut().enumerate() {
let branch_stmt = build_branch_counter_stmt(cov_fn, branch_id, path_idx, ctx);
case.consequent.insert(0, branch_stmt);
}
}
fn enter_logical_expression(
&mut self,
expr: &mut LogicalExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
match expr.operator {
LogicalOperator::And | LogicalOperator::Or | LogicalOperator::Coalesce => {
if is_parent_logical(ctx) {
return;
}
let leaf_spans = collect_logical_leaf_spans(expr);
let branch_id = self.add_branch("binary-expr", expr.span, &leaf_spans);
if self.report_logic {
self.logical_branch_ids.push(branch_id);
}
let cov_fn = self.cov_fn_name.as_str();
wrap_logical_leaves(expr, cov_fn, branch_id, &mut 0, self.report_logic, ctx);
}
}
}
fn enter_formal_parameter(
&mut self,
param: &mut FormalParameter<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if let Some(init) = ¶m.initializer {
let init_span = init.span();
self.add_branch("default-arg", param.span, &[init_span]);
}
}
fn enter_assignment_pattern(
&mut self,
pattern: &mut AssignmentPattern<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let right_span = pattern.right.span();
self.add_branch("default-arg", pattern.span, &[right_span]);
}
fn enter_assignment_expression(
&mut self,
expr: &mut AssignmentExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
use oxc_syntax::operator::AssignmentOperator;
if matches!(
expr.operator,
AssignmentOperator::LogicalOr
| AssignmentOperator::LogicalAnd
| AssignmentOperator::LogicalNullish
) {
let left_span = expr.left.span();
let right_span = expr.right.span();
let branch_id = self.add_branch("binary-expr", expr.span, &[left_span, right_span]);
let cov_fn = self.cov_fn_name.as_str();
self.pending_stmts.push(PendingInsertion {
target_start: expr.span.start,
counter_id: branch_id,
counter_type: CounterType::BranchLeft,
});
let counter = build_branch_counter_expr(cov_fn, branch_id, 1, ctx);
let orig_right = mem::replace(&mut expr.right, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig_right);
expr.right = ctx.ast.expression_sequence(SPAN, items);
}
}
}
fn inject_branch_counter_into_statement<'a>(
stmt: &mut Statement<'a>,
cov_fn_name: &str,
branch_id: usize,
path_idx: usize,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let counter_stmt = build_branch_counter_stmt(cov_fn_name, branch_id, path_idx, ctx);
match stmt {
Statement::BlockStatement(block) => {
block.body.insert(0, counter_stmt);
}
_ => {
let scope_id =
ctx.create_child_scope_of_current(oxc_syntax::scope::ScopeFlags::empty());
let original = mem::replace(stmt, ctx.ast.statement_empty(SPAN));
let mut stmts = ctx.ast.vec();
stmts.push(counter_stmt);
stmts.push(original);
*stmt = ctx.ast.statement_block_with_scope_id(SPAN, stmts, scope_id);
}
}
}