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<'src> {
source: &'src str,
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_method_decl: Option<Span>,
pending_stmts: Vec<PendingInsertion>,
pending_fn_counters: Vec<usize>,
ignored_fn_stack: Vec<bool>,
ignored_stmt_stack: Vec<bool>,
ignored_prop_stack: Vec<bool>,
ignored_switch_case_stack: Vec<bool>,
ignored_if_arm_spans: Vec<Span>,
ignored_if_arm_push_counts: Vec<usize>,
skip_next: bool,
skip_fn_counter_only: 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<'src> CoverageTransform<'src> {
pub fn new(
source: &'src 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 {
source,
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_method_decl: None,
pending_stmts: Vec::new(),
pending_fn_counters: Vec::new(),
ignored_fn_stack: Vec::new(),
ignored_stmt_stack: Vec::new(),
ignored_prop_stack: Vec::new(),
ignored_switch_case_stack: Vec::new(),
ignored_if_arm_spans: Vec::new(),
ignored_if_arm_push_counts: Vec::new(),
skip_next: false,
skip_fn_counter_only: 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 in_ignored_subtree(&self) -> bool {
self.ignored_fn_stack.iter().any(|&ignored| ignored)
|| self.ignored_stmt_stack.iter().any(|&ignored| ignored)
|| self.ignored_prop_stack.iter().any(|&ignored| ignored)
|| self.ignored_switch_case_stack.iter().any(|&ignored| ignored)
}
fn is_in_ignored_if_arm(&self, span: Span) -> bool {
self.ignored_if_arm_spans
.iter()
.any(|ignored| ignored.start <= span.start && span.end <= ignored.end)
}
fn offset_to_position(&self, offset: u32) -> Position {
let line = self.line_offsets.partition_point(|&o| o <= offset).saturating_sub(1);
let line_start = self.line_offsets[line] as usize;
let end = (offset as usize).min(self.source.len());
let column =
self.source[line_start..end].chars().map(char::len_utf16).sum::<usize>() as u32;
Position { line: (line + 1) as u32, column }
}
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) -> 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: Vec::new() },
);
id_num
}
fn add_branch_path(&mut self, branch_id: usize, span: Span) -> usize {
let location = self.span_to_location(span);
self.add_branch_path_location(branch_id, location)
}
fn add_branch_path_unknown(&mut self, branch_id: usize) -> usize {
self.add_branch_path_location(
branch_id,
Location {
start: Position { line: 0, column: 0 },
end: Position { line: 0, column: 0 },
},
)
}
fn add_branch_path_location(&mut self, branch_id: usize, location: Location) -> usize {
let entry = self
.branch_map
.get_mut(&branch_id.to_string())
.expect("branch path must reference an existing branch");
let path_idx = entry.locations.len();
entry.locations.push(location);
path_idx
}
fn drain_pending_insertions_for_target(&mut self, target_start: u32) -> Vec<PendingInsertion> {
let mut drained = Vec::new();
let mut remaining = Vec::with_capacity(self.pending_stmts.len());
for pending in self.pending_stmts.drain(..) {
if pending.target_start == target_start {
drained.push(pending);
} else {
remaining.push(pending);
}
}
self.pending_stmts = remaining;
drained
}
fn retarget_pending_insertions(&mut self, from_start: u32, to_start: u32) {
for pending in &mut self.pending_stmts {
if pending.target_start == from_start {
pending.target_start = to_start;
}
}
}
fn inject_pending_counters_into_statement_child<'a>(
&mut self,
body: &mut Statement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if matches!(body, Statement::BlockStatement(_)) {
return;
}
let span = body.span();
if span.start == 0 && span.end == 0 {
return;
}
let pending = self.drain_pending_insertions_for_target(span.start);
if pending.is_empty() {
return;
}
let cov_fn = self.cov_fn_name.as_str();
let scope_id = ctx.create_child_scope_of_current(oxc_syntax::scope::ScopeFlags::empty());
let original = mem::replace(body, ctx.ast.statement_empty(SPAN));
let mut stmts = ctx.ast.vec();
for insertion in pending {
stmts.push(build_pending_counter_stmt(cov_fn, &insertion, ctx));
}
stmts.push(original);
*body = ctx.ast.statement_block_with_scope_id(SPAN, stmts, scope_id);
}
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)
}
fn build_pending_counter_stmt<'a>(
cov_fn_name: &str,
pending: &PendingInsertion,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Statement<'a> {
match pending.counter_type {
CounterType::Statement => build_counter_stmt(cov_fn_name, "s", pending.counter_id, ctx),
CounterType::BranchLeft => {
build_branch_counter_stmt(cov_fn_name, pending.counter_id, 0, ctx)
}
}
}
pub fn generate_preamble_source(
coverage: &FileCoverage,
coverage_hash: &str,
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 hash = ");
buf.push_str(&serde_json::to_string(coverage_hash)?);
let _ = write!(buf, "; var gcv = '{coverage_var}'; var coverageData = ");
buf.push_str(&serde_json::to_string(coverage)?);
let _ = writeln!(
buf,
"; coverageData.hash = hash; 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].hash !== hash) {{ 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, pragmas: &PragmaMap) -> Vec<Span> {
let mut spans = Vec::new();
collect_logical_leaves_inner(&expr.left, pragmas, &mut spans);
collect_logical_leaves_inner(&expr.right, pragmas, &mut spans);
spans
}
fn collect_logical_leaves_inner(expr: &Expression, pragmas: &PragmaMap, spans: &mut Vec<Span>) {
if let Expression::ParenthesizedExpression(paren) = expr {
collect_logical_leaves_inner(&paren.expression, pragmas, spans);
return;
}
if pragmas.get(expr.span().start) == Some(IgnoreType::Next) {
return;
}
if let Expression::LogicalExpression(logical) = expr {
collect_logical_leaves_inner(&logical.left, pragmas, spans);
collect_logical_leaves_inner(&logical.right, pragmas, spans);
} else {
spans.push(expr.span());
}
}
fn is_ignored_case(case: &SwitchCase, pragmas: &PragmaMap) -> bool {
pragmas.get(case.span.start) == Some(IgnoreType::Next)
|| case
.consequent
.first()
.is_some_and(|stmt| pragmas.get(stmt.span().start) == Some(IgnoreType::Next))
}
struct LogicalWrapState<'b> {
cov_fn_name: &'b str,
branch_id: usize,
report_logic: bool,
path_idx: usize,
}
impl<'b> LogicalWrapState<'b> {
fn new(cov_fn_name: &'b str, branch_id: usize, report_logic: bool) -> Self {
Self { cov_fn_name, branch_id, report_logic, path_idx: 0 }
}
fn current_path_idx(&self) -> usize {
self.path_idx
}
fn advance_path(&mut self) {
self.path_idx += 1;
}
}
fn wrap_expression_with_branch_counter<'a>(
operand: &mut Expression<'a>,
state: &LogicalWrapState<'_>,
ctx: &TraverseCtx<'a, CoverageState>,
) {
let counter = build_branch_counter_expr(
state.cov_fn_name,
state.branch_id,
state.current_path_idx(),
ctx,
);
let orig = mem::replace(operand, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig);
*operand = ctx.ast.expression_sequence(SPAN, items);
}
fn wrap_logical_leaf<'a>(
operand: &mut Expression<'a>,
state: &mut LogicalWrapState<'_>,
ctx: &TraverseCtx<'a, CoverageState>,
) {
wrap_expression_with_branch_counter(operand, state, ctx);
let branch_wrapped = mem::replace(operand, dummy_expr(ctx));
if state.report_logic {
let bt_name = alloc_str(&format!("{}_bt", state.cov_fn_name), 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,
state.branch_id as f64,
None,
oxc_syntax::number::NumberBase::Decimal,
)));
args.push(Argument::from(ctx.ast.expression_numeric_literal(
SPAN,
state.current_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;
}
state.advance_path();
}
fn wrap_logical_leaves<'a>(
expr: &mut LogicalExpression<'a>,
state: &mut LogicalWrapState<'_>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
wrap_logical_operand(&mut expr.left, state, ctx);
wrap_logical_operand(&mut expr.right, state, ctx);
}
fn wrap_logical_operand<'a>(
operand: &mut Expression<'a>,
state: &mut LogicalWrapState<'_>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if let Expression::ParenthesizedExpression(paren) = operand {
return wrap_logical_operand(&mut paren.expression, state, ctx);
}
if ctx.state.pragmas.get(operand.span().start) == Some(IgnoreType::Next) {
return;
}
if let Expression::LogicalExpression(inner) = operand {
wrap_logical_leaves(inner, state, ctx);
} else {
wrap_logical_leaf(operand, state, ctx);
}
}
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 ignored_named_function_expression = func.r#type == FunctionType::FunctionExpression
&& func
.id
.as_ref()
.is_some_and(|id| self.ignore_class_methods.contains(&id.name.to_string()));
let pragma_skip = has_pragma
|| self.skip_next
|| self.in_ignored_subtree()
|| ignored_named_function_expression;
let fn_counter_only_skip = self.skip_fn_counter_only;
self.skip_next = false;
self.skip_fn_counter_only = false;
self.ignored_fn_stack.push(pragma_skip);
if pragma_skip || fn_counter_only_skip {
self.pending_name = None;
return;
}
let name = self.resolve_function_name(func);
let decl_span = if let Some(id) = &func.id {
id.span
} else if let Some(span) = self.pending_method_decl.take() {
span
} else {
Span::new(func.span.start, func.span.start + 1)
};
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 exit_function(
&mut self,
_func: &mut Function<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_fn_stack.pop();
}
fn enter_function_body(
&mut self,
body: &mut FunctionBody<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
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>,
) {
let pragma_skip = ctx.state.pragmas.get(arrow.span.start) == Some(IgnoreType::Next)
|| self.skip_next
|| self.in_ignored_subtree();
self.ignored_fn_stack.push(pragma_skip);
if pragma_skip {
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;
}
self.ignored_fn_stack.pop();
}
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 };
if self.in_ignored_subtree() {
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 parent_ignored = self.in_ignored_subtree();
let key_span = method.key.span();
let is_private = matches!(method.key, PropertyKey::PrivateIdentifier(_));
let ignore_by_pragma = !is_private
&& (ctx.state.pragmas.get(method.span.start) == Some(IgnoreType::Next)
|| ctx.state.pragmas.get(key_span.start) == Some(IgnoreType::Next)
|| self.skip_next);
if ignore_by_pragma {
self.ignored_prop_stack.push(true);
self.skip_next = false;
return;
}
self.ignored_prop_stack.push(false);
if parent_ignored {
return;
}
let (name, key_span) = match &method.key {
PropertyKey::StaticIdentifier(id) => (id.name.to_string(), id.span),
PropertyKey::StringLiteral(s) => (s.value.to_string(), s.span),
PropertyKey::PrivateIdentifier(_) => {
self.skip_fn_counter_only = true;
return;
}
_ => return,
};
if self.ignore_class_methods.contains(&name) {
if let Some(ignored) = self.ignored_prop_stack.last_mut() {
*ignored = true;
}
return;
}
self.pending_name = Some(name);
self.pending_method_decl = Some(key_span);
}
fn exit_method_definition(
&mut self,
_method: &mut MethodDefinition<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.pending_name = None;
self.pending_method_decl = None;
self.ignored_prop_stack.pop();
}
fn enter_property_definition(
&mut self,
prop: &mut PropertyDefinition<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let parent_ignored = self.in_ignored_subtree();
let has_ignore_next =
ctx.state.pragmas.get(prop.span.start) == Some(IgnoreType::Next) || self.skip_next;
self.ignored_prop_stack.push(has_ignore_next);
if has_ignore_next {
self.skip_next = false;
return;
}
if parent_ignored {
return;
}
let Some(value) = &prop.value else { return };
let span = value.span();
if span.start == 0 && span.end == 0 {
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 exit_property_definition(
&mut self,
_prop: &mut PropertyDefinition<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_stack.pop();
}
fn enter_object_property(
&mut self,
prop: &mut ObjectProperty<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let is_method_like =
prop.method || matches!(prop.kind, PropertyKind::Get | PropertyKind::Set);
let key_has_ignore_next = ctx.state.pragmas.get(prop.span.start) == Some(IgnoreType::Next)
|| ctx.state.pragmas.get(prop.key.span().start) == Some(IgnoreType::Next)
|| self.skip_next;
let has_ignore_next = is_method_like && key_has_ignore_next;
let is_function_valued = matches!(
prop.value,
Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)
);
let value_has_ignore_next = !is_method_like && !is_function_valued && key_has_ignore_next;
let has_ignore_next = has_ignore_next || value_has_ignore_next;
self.ignored_prop_stack.push(has_ignore_next);
if has_ignore_next {
self.skip_next = false;
}
}
fn exit_object_property(
&mut self,
_prop: &mut ObjectProperty<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_stack.pop();
}
fn enter_statement(
&mut self,
stmt: &mut Statement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let span = stmt.span();
let parent_ignored = self.in_ignored_subtree();
let is_injected = span.start == 0 && span.end == 0;
let has_ignore_next = !is_injected
&& (ctx.state.pragmas.get(span.start) == Some(IgnoreType::Next)
|| self.is_in_ignored_if_arm(span));
self.ignored_stmt_stack.push(has_ignore_next);
if is_injected {
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 parent_ignored {
return;
}
if has_ignore_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_statement(
&mut self,
_stmt: &mut Statement<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.skip_next = false;
self.ignored_stmt_stack.pop();
}
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 = build_pending_counter_stmt(cov_fn, &p, ctx);
insertions.push((idx, counter));
} else {
i += 1;
}
}
}
if insertions.is_empty() {
return;
}
insertions.sort_by_key(|insertion| std::cmp::Reverse(insertion.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>,
) {
if self.in_ignored_subtree() {
self.ignored_if_arm_push_counts.push(0);
return;
}
let pragma = ctx.state.pragmas.get(stmt.span.start);
let mut ignored_arm_count = 0;
if pragma == Some(IgnoreType::If) {
self.ignored_if_arm_spans.push(stmt.consequent.span());
ignored_arm_count += 1;
} else if pragma == Some(IgnoreType::Else)
&& let Some(alt) = &stmt.alternate
{
self.ignored_if_arm_spans.push(alt.span());
ignored_arm_count += 1;
}
self.ignored_if_arm_push_counts.push(ignored_arm_count);
let consequent_span = stmt.span;
let branch_id = self.add_branch("if", stmt.span);
let cov_fn = self.cov_fn_name.clone();
if pragma != Some(IgnoreType::If) {
let path_idx = self.add_branch_path(branch_id, consequent_span);
inject_branch_counter_into_statement(
&mut stmt.consequent,
cov_fn.as_str(),
branch_id,
path_idx,
ctx,
);
}
if pragma != Some(IgnoreType::Else) {
if stmt.alternate.is_none() {
let scope_id =
ctx.create_child_scope_of_current(oxc_syntax::scope::ScopeFlags::empty());
stmt.alternate =
Some(ctx.ast.statement_block_with_scope_id(SPAN, ctx.ast.vec(), scope_id));
}
if let Some(alt) = &mut stmt.alternate {
let path_idx = if alt.span().start == 0 && alt.span().end == 0 {
self.add_branch_path_unknown(branch_id)
} else {
self.add_branch_path(branch_id, alt.span())
};
inject_branch_counter_into_statement(
alt,
cov_fn.as_str(),
branch_id,
path_idx,
ctx,
);
}
}
}
fn exit_if_statement(
&mut self,
_stmt: &mut IfStatement<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if let Some(count) = self.ignored_if_arm_push_counts.pop() {
for _ in 0..count {
self.ignored_if_arm_spans.pop();
}
}
}
fn enter_conditional_expression(
&mut self,
expr: &mut ConditionalExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree()
|| ctx.state.pragmas.get(expr.span.start) == Some(IgnoreType::Next)
{
return;
}
let branch_id = self.add_branch("cond-expr", expr.span);
let ignore_consequent =
ctx.state.pragmas.get(expr.consequent.span().start) == Some(IgnoreType::Next);
let ignore_alternate =
ctx.state.pragmas.get(expr.alternate.span().start) == Some(IgnoreType::Next);
let cov_fn = self.cov_fn_name.clone();
if !ignore_consequent {
let path_idx = self.add_branch_path(branch_id, expr.consequent.span());
let counter = build_branch_counter_expr(cov_fn.as_str(), branch_id, path_idx, ctx);
let orig_consequent = mem::replace(&mut expr.consequent, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig_consequent);
expr.consequent = ctx.ast.expression_sequence(SPAN, items);
}
if !ignore_alternate {
let path_idx = self.add_branch_path(branch_id, expr.alternate.span());
let counter = build_branch_counter_expr(cov_fn.as_str(), branch_id, path_idx, ctx);
let orig_alternate = mem::replace(&mut expr.alternate, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
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>,
) {
if self.in_ignored_subtree() {
return;
}
let branch_id = self.add_branch("switch", stmt.span);
let cov_fn = self.cov_fn_name.clone();
for case in &mut stmt.cases {
if is_ignored_case(case, &ctx.state.pragmas) {
continue;
}
let path_idx = self.add_branch_path(branch_id, case.span);
let branch_stmt = build_branch_counter_stmt(cov_fn.as_str(), branch_id, path_idx, ctx);
case.consequent.insert(0, branch_stmt);
}
}
fn enter_switch_case(
&mut self,
case: &mut SwitchCase<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_switch_case_stack.push(is_ignored_case(case, &ctx.state.pragmas));
}
fn exit_switch_case(
&mut self,
_case: &mut SwitchCase<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_switch_case_stack.pop();
}
fn enter_logical_expression(
&mut self,
expr: &mut LogicalExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree()
|| ctx.state.pragmas.get(expr.span.start) == Some(IgnoreType::Next)
{
return;
}
match expr.operator {
LogicalOperator::And | LogicalOperator::Or | LogicalOperator::Coalesce => {
if is_parent_logical(ctx) {
return;
}
let branch_id = self.add_branch("binary-expr", expr.span);
for span in collect_logical_leaf_spans(expr, &ctx.state.pragmas) {
self.add_branch_path(branch_id, span);
}
if self.report_logic {
self.logical_branch_ids.push(branch_id);
}
let cov_fn = self.cov_fn_name.as_str();
let mut state = LogicalWrapState::new(cov_fn, branch_id, self.report_logic);
wrap_logical_leaves(expr, &mut state, ctx);
}
}
}
fn exit_with_statement(
&mut self,
stmt: &mut WithStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.inject_pending_counters_into_statement_child(&mut stmt.body, ctx);
}
fn exit_labeled_statement(
&mut self,
stmt: &mut LabeledStatement<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let body_span = stmt.body.span();
if body_span.start != 0 || body_span.end != 0 {
self.retarget_pending_insertions(body_span.start, stmt.span.start);
}
}
fn exit_do_while_statement(
&mut self,
stmt: &mut DoWhileStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.inject_pending_counters_into_statement_child(&mut stmt.body, ctx);
}
fn exit_while_statement(
&mut self,
stmt: &mut WhileStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.inject_pending_counters_into_statement_child(&mut stmt.body, ctx);
}
fn exit_for_statement(
&mut self,
stmt: &mut ForStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.inject_pending_counters_into_statement_child(&mut stmt.body, ctx);
}
fn exit_for_in_statement(
&mut self,
stmt: &mut ForInStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.inject_pending_counters_into_statement_child(&mut stmt.body, ctx);
}
fn exit_for_of_statement(
&mut self,
stmt: &mut ForOfStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.inject_pending_counters_into_statement_child(&mut stmt.body, ctx);
}
fn enter_formal_parameter(
&mut self,
param: &mut FormalParameter<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
if let Some(init) = &mut param.initializer {
let init_span = init.span();
let branch_id = self.add_branch("default-arg", param.span);
self.add_branch_path(branch_id, init_span);
let cov_fn = self.cov_fn_name.as_str();
let state = LogicalWrapState::new(cov_fn, branch_id, false);
wrap_expression_with_branch_counter(init, &state, ctx);
}
}
fn enter_assignment_pattern(
&mut self,
pattern: &mut AssignmentPattern<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
let right_span = pattern.right.span();
let branch_id = self.add_branch("default-arg", pattern.span);
self.add_branch_path(branch_id, right_span);
let cov_fn = self.cov_fn_name.as_str();
let state = LogicalWrapState::new(cov_fn, branch_id, false);
wrap_expression_with_branch_counter(&mut pattern.right, &state, ctx);
}
fn enter_assignment_expression(
&mut self,
expr: &mut AssignmentExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
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);
self.add_branch_path(branch_id, left_span);
self.add_branch_path(branch_id, 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);
}
}
}