use std::fmt::Write;
use std::mem;
use oxc_allocator::{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 oxc_coverage_types::{BranchEntry, FileCoverage, FnEntry, Location, Position};
pub struct CoverageState {
pub pragmas: PragmaMap,
}
pub struct CoverageTransform<'src, 'arena> {
source: &'src str,
line_offsets: Vec<u32>,
source_is_ascii: bool,
pub fn_map: Vec<FnEntry>,
pub statement_map: Vec<Location>,
pub branch_map: Vec<BranchEntry>,
pub branch_arm_body_byte_spans: Vec<Vec<(u32, u32)>>,
pending_name: Option<String>,
pending_method_decl: Option<Span>,
pending_stmts: Vec<PendingInsertion>,
pending_fn_counters: Vec<Option<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: &'arena str,
cov_fn_bt_name: Option<&'arena str>,
cov_fn_oc_name: &'arena str,
report_logic: bool,
ignore_class_methods: Vec<String>,
pub logical_branch_ids: Vec<usize>,
pending_class_field_hoists: Vec<Vec<ClassFieldHoist>>,
eager_remapper: Option<oxc_coverage_source_maps::PositionRemapper>,
}
struct ClassFieldHoist {
target_start: u32,
counter_id: usize,
is_static: bool,
}
struct PendingInsertion {
target_start: u32,
counter_id: usize,
counter_type: CounterType,
}
#[derive(Clone, Copy)]
enum CounterType {
Statement,
BranchLeft,
}
pub struct TransformInit<'src, 'arena> {
pub allocator: &'arena Allocator,
pub source: &'src str,
pub cov_fn_name: &'src str,
pub report_logic: bool,
pub ignore_class_methods: Vec<String>,
pub eager_remapper: Option<oxc_coverage_source_maps::PositionRemapper>,
}
impl<'src, 'arena> CoverageTransform<'src, 'arena> {
pub fn new(init: TransformInit<'src, 'arena>) -> Self {
let TransformInit {
allocator,
source,
cov_fn_name,
report_logic,
ignore_class_methods,
eager_remapper,
} = init;
let cov_fn_name = allocator.alloc_str(cov_fn_name);
Self {
source,
line_offsets: compute_line_offsets(source),
source_is_ascii: source.is_ascii(),
fn_map: Vec::new(),
statement_map: Vec::new(),
branch_map: Vec::new(),
branch_arm_body_byte_spans: Vec::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,
cov_fn_bt_name: report_logic.then(|| allocator.alloc_str(&format!("{cov_fn_name}_bt"))),
cov_fn_oc_name: allocator.alloc_str(&format!("{cov_fn_name}_oc")),
report_logic,
ignore_class_methods,
logical_branch_ids: Vec::new(),
pending_class_field_hoists: Vec::new(),
eager_remapper,
}
}
fn location_maps(&self, loc: &Location) -> bool {
self.eager_remapper.as_ref().is_none_or(|r| {
r.maps(loc.start.line, loc.start.column) && r.maps(loc.end.line, loc.end.column)
})
}
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 = if self.source_is_ascii {
(end - line_start) as u32
} else {
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) -> Option<usize> {
let decl = self.span_to_location(decl_span);
let loc = self.span_to_location(body_span);
if !self.location_maps(&decl) || !self.location_maps(&loc) {
return None;
}
let id_num = self.fn_map.len();
let line = decl.start.line;
self.fn_map.push(FnEntry { name, line, decl, loc });
Some(id_num)
}
fn add_statement(&mut self, span: Span) -> Option<usize> {
let loc = self.span_to_location(span);
if !self.location_maps(&loc) {
return None;
}
let id_num = self.statement_map.len();
self.statement_map.push(loc);
Some(id_num)
}
fn add_branch(&mut self, branch_type: &str, span: Span) -> Option<usize> {
let loc = self.span_to_location(span);
if !self.location_maps(&loc) {
return None;
}
let id_num = self.branch_map.len();
let line = loc.start.line;
self.branch_map.push(BranchEntry {
loc,
line,
branch_type: branch_type.to_string(),
locations: Vec::new(),
});
self.branch_arm_body_byte_spans.push(Vec::new());
Some(id_num)
}
fn add_branch_path(&mut self, branch_id: usize, span: Span) -> Option<usize> {
let location = self.span_to_location(span);
if !self.location_maps(&location) {
return None;
}
Some(self.add_branch_path_location(branch_id, location, (span.start, span.end)))
}
fn add_branch_path_with_body(
&mut self,
branch_id: usize,
location_span: Span,
body_span: Span,
) -> Option<usize> {
let location = self.span_to_location(location_span);
if !self.location_maps(&location) {
return None;
}
Some(self.add_branch_path_location(branch_id, location, (body_span.start, body_span.end)))
}
fn record_ignored_if_arm(&mut self, stmt: &IfStatement<'arena>, pragma: Option<IgnoreType>) {
let mut ignored_arm_count = 0_usize;
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);
}
fn inject_else_branch_counter(
&mut self,
stmt: &mut IfStatement<'arena>,
branch_id: usize,
synthetic_anchor: u32,
ctx: &mut TraverseCtx<'arena, CoverageState>,
) {
let arm_span = match &stmt.alternate {
Some(alt) if !(alt.span().start == 0 && alt.span().end == 0) => alt.span(),
_ => Span::new(synthetic_anchor, synthetic_anchor),
};
let Some(path_idx) = self.add_branch_path(branch_id, arm_span) else {
return;
};
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 {
inject_branch_counter_into_statement(
alt,
CounterKind::branch(self.cov_fn_name, branch_id, path_idx),
ctx,
);
}
}
fn add_branch_path_location(
&mut self,
branch_id: usize,
location: Location,
body_byte_span: (u32, u32),
) -> usize {
let entry = self
.branch_map
.get_mut(branch_id)
.expect("branch path must reference an existing branch");
let path_idx = entry.locations.len();
entry.locations.push(location);
let body_spans = self
.branch_arm_body_byte_spans
.get_mut(branch_id)
.expect("branch arm body span vec must exist for every branch id");
body_spans.push(body_byte_span);
path_idx
}
fn drain_pending_insertions_for_target(
&mut self,
target_start: u32,
) -> impl Iterator<Item = PendingInsertion> + '_ {
self.pending_stmts.extract_if(.., move |p| p.target_start == target_start)
}
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(
&mut self,
body: &mut Statement<'arena>,
ctx: &mut TraverseCtx<'arena, CoverageState>,
) {
if matches!(body, Statement::BlockStatement(_)) {
return;
}
let span = body.span();
if span.start == 0 && span.end == 0 {
return;
}
let pending: Vec<_> = self.drain_pending_insertions_for_target(span.start).collect();
if pending.is_empty() {
return;
}
let cov_fn = self.cov_fn_name;
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_counter_stmt(CounterKind::from_pending(cov_fn, &insertion), ctx));
}
stmts.push(original);
*body = ctx.ast.statement_block_with_scope_id(SPAN, stmts, scope_id);
}
#[expect(
clippy::needless_pass_by_ref_mut,
reason = "ctx is conventionally &mut throughout traverse hooks; matching the contract"
)]
fn wrap_optional_chain_link(
&mut self,
object: &mut Expression<'arena>,
link_span: Span,
ctx: &mut TraverseCtx<'arena, CoverageState>,
) {
let Some(branch_id) = self.add_branch("optional-chain", link_span) else {
return;
};
let anchor = Span::new(link_span.start, link_span.start);
let anchor_loc = self.span_to_location(anchor);
self.add_branch_path_location(branch_id, anchor_loc, (anchor.start, anchor.end));
let link_loc = self.span_to_location(link_span);
self.add_branch_path_location(branch_id, link_loc, (link_span.start, link_span.end));
let callee = ctx.ast.expression_identifier(SPAN, self.cov_fn_oc_name);
let original = mem::replace(object, dummy_expr(ctx));
let mut args = ctx.ast.vec();
args.push(Argument::from(original));
args.push(Argument::from(numeric_literal(ctx, branch_id as f64)));
*object = ctx.ast.expression_call(
SPAN,
callee,
None::<TSTypeParameterInstantiation>,
args,
false,
);
}
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_map.len())
}
}
fn alloc_str<'a>(s: &str, ctx: &TraverseCtx<'a, CoverageState>) -> &'a str {
ctx.ast.allocator.alloc_str(s)
}
#[derive(Clone, Copy)]
enum CounterKind<'a> {
Statement { cov_fn_name: &'a str, type_: &'static str, id: usize },
Branch { cov_fn_name: &'a str, branch_id: usize, path_idx: usize },
}
impl<'a> CounterKind<'a> {
const fn stmt(cov_fn_name: &'a str, id: usize) -> Self {
Self::Statement { cov_fn_name, type_: "s", id }
}
const fn func(cov_fn_name: &'a str, id: usize) -> Self {
Self::Statement { cov_fn_name, type_: "f", id }
}
const fn branch(cov_fn_name: &'a str, branch_id: usize, path_idx: usize) -> Self {
Self::Branch { cov_fn_name, branch_id, path_idx }
}
fn from_pending(cov_fn_name: &'a str, pending: &PendingInsertion) -> Self {
match pending.counter_type {
CounterType::Statement => Self::stmt(cov_fn_name, pending.counter_id),
CounterType::BranchLeft => Self::branch(cov_fn_name, pending.counter_id, 0),
}
}
}
fn static_field<'a>(
base: Expression<'a>,
field: &'a str,
ctx: &TraverseCtx<'a, CoverageState>,
) -> MemberExpression<'a> {
ctx.ast.member_expression_static(SPAN, base, ctx.ast.identifier_name(SPAN, field), false)
}
fn computed_index<'a>(
base: MemberExpression<'a>,
idx: usize,
ctx: &TraverseCtx<'a, CoverageState>,
) -> MemberExpression<'a> {
ctx.ast.member_expression_computed(
SPAN,
Expression::from(base),
numeric_literal(ctx, idx as f64),
false,
)
}
fn build_counter_expr<'a>(
kind: CounterKind<'a>,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Expression<'a> {
let target = match kind {
CounterKind::Statement { cov_fn_name, type_, id } => {
let coverage = ctx.ast.expression_identifier(SPAN, cov_fn_name);
let field = static_field(coverage, alloc_str(type_, ctx), ctx);
computed_index(field, id, ctx)
}
CounterKind::Branch { cov_fn_name, branch_id, path_idx } => {
let coverage = ctx.ast.expression_identifier(SPAN, cov_fn_name);
let b = static_field(coverage, "b", ctx);
let outer = computed_index(b, branch_id, ctx);
computed_index(outer, path_idx, ctx)
}
};
ctx.ast.expression_update(
SPAN,
UpdateOperator::Increment,
true,
SimpleAssignmentTarget::from(target),
)
}
fn build_counter_stmt<'a>(
kind: CounterKind<'a>,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Statement<'a> {
let expr = build_counter_expr(kind, ctx);
ctx.ast.statement_expression(SPAN, expr)
}
fn prepend_counter<'a>(
target: &mut Expression<'a>,
kind: CounterKind<'a>,
ctx: &TraverseCtx<'a, CoverageState>,
) {
let counter = build_counter_expr(kind, ctx);
let orig = mem::replace(target, dummy_expr(ctx));
let mut items = ctx.ast.vec();
items.push(counter);
items.push(orig);
*target = ctx.ast.expression_sequence(SPAN, items);
}
fn numeric_literal<'a>(ctx: &TraverseCtx<'a, CoverageState>, value: f64) -> Expression<'a> {
ctx.ast.expression_numeric_literal(SPAN, value, None, oxc_syntax::number::NumberBase::Decimal)
}
pub struct PreambleInputs<'a> {
pub coverage: &'a FileCoverage,
pub coverage_json: &'a str,
pub coverage_hash: &'a str,
pub coverage_var: &'a str,
pub cov_fn_name: &'a str,
pub report_logic: bool,
}
pub fn generate_preamble_source(inputs: &PreambleInputs<'_>) -> String {
let PreambleInputs {
coverage,
coverage_json,
coverage_hash,
coverage_var,
cov_fn_name,
report_logic,
} = *inputs;
let mut buf = String::with_capacity(256 + coverage_json.len());
let _ = write!(buf, "var {cov_fn_name} = (function () {{ var path = ");
buf.push_str(
&serde_json::to_string(&coverage.path).expect("serializing a String to JSON is infallible"),
);
let _ = write!(buf, "; var hash = ");
buf.push_str(
&serde_json::to_string(coverage_hash).expect("serializing a &str to JSON is infallible"),
);
let _ = write!(buf, "; var gcv = '{coverage_var}'; var coverageData = ");
buf.push_str(coverage_json);
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 {
append_logic_helper(&mut buf, cov_fn_name);
}
if coverage.branch_map.values().any(|entry| entry.branch_type == "optional-chain") {
append_optional_chain_helper(&mut buf, cov_fn_name);
}
buf
}
fn append_logic_helper(buf: &mut String, cov_fn_name: &str) {
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; }}"
);
}
fn append_optional_chain_helper(buf: &mut String, cov_fn_name: &str) {
let _ = writeln!(
buf,
"function {cov_fn_name}_oc(val, id) {{ ++{cov_fn_name}.b[id][val == null ? 0 : 1]; return val; }}"
);
}
pub fn djb31_hex(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 generate_cov_fn_name(file_path: &str) -> String {
format!("cov_{}", djb31_hex(file_path))
}
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 compute_line_offsets(source: &str) -> Vec<u32> {
std::iter::once(0)
.chain(source.bytes().enumerate().filter(|(_, b)| *b == b'\n').map(|(i, _)| (i + 1) as u32))
.collect()
}
fn is_container_statement(stmt: &Statement<'_>) -> bool {
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(_)
)
}
fn enclosing_var_decl_hoist_target(ctx: &TraverseCtx<'_, CoverageState>) -> Option<u32> {
use oxc_traverse::Ancestor;
let mut iter = ctx.ancestors();
let var_decl_span = match iter.next()? {
Ancestor::VariableDeclarationDeclarations(a) => *a.span(),
_ => return None,
};
match iter.next()? {
Ancestor::ForStatementInit(_)
| Ancestor::ForInStatementLeft(_)
| Ancestor::ForOfStatementLeft(_) => None,
_ => Some(var_decl_span.start),
}
}
fn property_key_to_name(key: &PropertyKey<'_>, _source: &str) -> Option<String> {
match key {
PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
PropertyKey::PrivateIdentifier(id) => Some(format!("#{}", id.name)),
PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
PropertyKey::NumericLiteral(n) => {
Some(n.raw.map_or_else(|| n.value.to_string(), |raw| raw.to_string()))
}
PropertyKey::TemplateLiteral(t) if t.expressions.is_empty() => {
t.quasis.first().and_then(|quasi| quasi.value.cooked.as_ref()).map(ToString::to_string)
}
_ => None,
}
}
fn enclosing_destructure_property_pragma(ctx: &TraverseCtx<'_, CoverageState>) -> bool {
use oxc_traverse::Ancestor;
for a in ctx.ancestors() {
match a {
Ancestor::AssignmentPatternLeft(_) | Ancestor::AssignmentPatternRight(_) => {}
Ancestor::BindingPropertyValue(prop) => {
return ctx.state.pragmas.get(prop.span().start) == Some(IgnoreType::Next);
}
Ancestor::BindingPropertyKey(prop) => {
return ctx.state.pragmas.get(prop.span().start) == Some(IgnoreType::Next);
}
_ => return false,
}
}
false
}
fn is_synthetic_span(span: Span) -> bool {
span.start == 0 && span.end == 0
}
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))
}
fn jsx_attribute_ignored(attr: &JSXAttribute, pragmas: &PragmaMap, skip_next: bool) -> bool {
pragmas.get(attr.span.start) == Some(IgnoreType::Next)
|| pragmas.get(attr.name.span().start) == Some(IgnoreType::Next)
|| skip_next
}
fn jsx_spread_attribute_ignored(
attr: &JSXSpreadAttribute,
pragmas: &PragmaMap,
skip_next: bool,
) -> bool {
pragmas.get(attr.span.start) == Some(IgnoreType::Next)
|| pragmas.get(attr.argument.span().start) == Some(IgnoreType::Next)
|| skip_next
}
fn jsx_child_ignored(child: &JSXChild, pragmas: &PragmaMap, skip_next: bool) -> bool {
pragmas.get(child.span().start) == Some(IgnoreType::Next)
|| match child {
JSXChild::ExpressionContainer(container) => {
pragmas.get(container.expression.span().start) == Some(IgnoreType::Next)
}
JSXChild::Spread(spread) => {
pragmas.get(spread.expression.span().start) == Some(IgnoreType::Next)
}
_ => false,
}
|| skip_next
}
struct LogicalWrapState<'b> {
cov_fn_name: &'b str,
cov_fn_bt_name: Option<&'b str>,
branch_id: usize,
report_logic: bool,
path_idx: usize,
}
impl<'b> LogicalWrapState<'b> {
fn new(
cov_fn_name: &'b str,
cov_fn_bt_name: Option<&'b str>,
branch_id: usize,
report_logic: bool,
) -> Self {
Self { cov_fn_name, cov_fn_bt_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<'a>,
ctx: &TraverseCtx<'a, CoverageState>,
) {
prepend_counter(
operand,
CounterKind::branch(state.cov_fn_name, state.branch_id, state.current_path_idx()),
ctx,
);
}
fn build_bt_call<'a>(
inner: Expression<'a>,
state: &LogicalWrapState<'a>,
ctx: &TraverseCtx<'a, CoverageState>,
) -> Expression<'a> {
let bt_name = state.cov_fn_bt_name.expect("report_logic requires cov_fn_bt_name");
let callee = ctx.ast.expression_identifier(SPAN, bt_name);
let mut args = ctx.ast.vec();
args.push(Argument::from(inner));
args.push(Argument::from(numeric_literal(ctx, state.branch_id as f64)));
args.push(Argument::from(numeric_literal(ctx, state.current_path_idx() as f64)));
ctx.ast.expression_call(SPAN, callee, None::<TSTypeParameterInstantiation>, args, false)
}
fn wrap_logical_leaf<'a>(
operand: &mut Expression<'a>,
state: &mut LogicalWrapState<'a>,
ctx: &TraverseCtx<'a, CoverageState>,
) {
wrap_expression_with_branch_counter(operand, state, ctx);
if state.report_logic {
let branch_wrapped = mem::replace(operand, dummy_expr(ctx));
*operand = build_bt_call(branch_wrapped, state, ctx);
}
state.advance_path();
}
fn wrap_logical_leaves<'a>(
expr: &mut LogicalExpression<'a>,
state: &mut LogicalWrapState<'a>,
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<'a>,
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<'_, 'a> {
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(Some(fn_id)) = self.pending_fn_counters.pop() {
let cov_fn = self.cov_fn_name;
let counter = build_counter_stmt(CounterKind::func(cov_fn, 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_map.len()));
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 is_named_initializer = matches!(
init,
Expression::FunctionExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_)
);
if is_named_initializer
&& let Some(hoist_target_start) = enclosing_var_decl_hoist_target(ctx)
{
if let Some(stmt_id) = self.add_statement(init_span) {
self.pending_stmts.push(PendingInsertion {
target_start: hoist_target_start,
counter_id: stmt_id,
counter_type: CounterType::Statement,
});
}
return;
}
if let Some(stmt_id) = self.add_statement(init_span) {
prepend_counter(init, CounterKind::stmt(self.cov_fn_name, stmt_id), ctx);
}
}
fn exit_variable_declarator(
&mut self,
_decl: &mut VariableDeclarator<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.pending_name = None;
}
fn enter_export_default_declaration(
&mut self,
decl: &mut ExportDefaultDeclaration<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
let anonymous = match &decl.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(func) => func.id.is_none(),
ExportDefaultDeclarationKind::ArrowFunctionExpression(_) => true,
_ => false,
};
if anonymous {
self.pending_name = Some("default".to_string());
}
}
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 key_span = method.key.span();
if matches!(method.key, PropertyKey::PrivateIdentifier(_)) {
self.skip_fn_counter_only = true;
return;
}
let Some(name) = property_key_to_name(&method.key, self.source) else { return };
if self.ignore_class_methods.contains(&name) {
if let Some(ignored) = self.ignored_prop_stack.last_mut() {
*ignored = true;
}
return;
}
let label = match method.kind {
MethodDefinitionKind::Get => format!("get {name}"),
MethodDefinitionKind::Set => format!("set {name}"),
MethodDefinitionKind::Method | MethodDefinitionKind::Constructor => name,
};
self.pending_name = Some(label);
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.as_mut() else { return };
let span = value.span();
if span.start == 0 && span.end == 0 {
return;
}
let is_named_initializer = matches!(
value,
Expression::FunctionExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_)
);
if is_named_initializer && !self.pending_class_field_hoists.is_empty() {
if let Some(name) = property_key_to_name(&prop.key, self.source) {
self.pending_name = Some(name);
}
if let Some(stmt_id) = self.add_statement(span) {
let target_start = prop.span.start;
let is_static = prop.r#static;
if let Some(top) = self.pending_class_field_hoists.last_mut() {
top.push(ClassFieldHoist { target_start, counter_id: stmt_id, is_static });
}
}
return;
}
if let Some(stmt_id) = self.add_statement(span) {
prepend_counter(value, CounterKind::stmt(self.cov_fn_name, stmt_id), ctx);
}
}
fn enter_class_body(
&mut self,
_body: &mut ClassBody<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.pending_class_field_hoists.push(Vec::new());
}
fn exit_class_body(
&mut self,
body: &mut ClassBody<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let Some(hoists) = self.pending_class_field_hoists.pop() else { return };
if hoists.is_empty() {
return;
}
let cov_fn = self.cov_fn_name;
let mut by_target: std::collections::BTreeMap<u32, &ClassFieldHoist> =
std::collections::BTreeMap::new();
for hoist in &hoists {
by_target.insert(hoist.target_start, hoist);
}
let original = std::mem::replace(&mut body.body, ctx.ast.vec());
for element in original {
if let ClassElement::PropertyDefinition(prop) = &element
&& let Some(hoist) = by_target.get(&prop.span.start)
{
let counter = build_counter_expr(CounterKind::stmt(cov_fn, hoist.counter_id), ctx);
let key_name =
alloc_str(&format!("__cov_{}_init_{}", cov_fn, hoist.counter_id), ctx);
let key =
PropertyKey::StaticIdentifier(ctx.ast.alloc_identifier_name(SPAN, key_name));
let synthetic = ctx.ast.class_element_property_definition(
SPAN,
PropertyDefinitionType::PropertyDefinition,
ctx.ast.vec(),
key,
None::<TSTypeAnnotation>,
Some(counter),
false,
hoist.is_static,
false,
false,
false,
false,
false,
None,
);
body.body.push(synthetic);
}
body.body.push(element);
}
}
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;
}
if !has_ignore_next && !self.in_ignored_subtree() {
let inherits_name = is_method_like || is_function_valued;
if inherits_name && let Some(base) = property_key_to_name(&prop.key, self.source) {
let label = match prop.kind {
PropertyKind::Get => format!("get {base}"),
PropertyKind::Set => format!("set {base}"),
PropertyKind::Init => base,
};
self.pending_name = Some(label);
}
}
}
fn exit_object_property(
&mut self,
_prop: &mut ObjectProperty<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_stack.pop();
self.pending_name = None;
}
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 || is_container_statement(stmt) || parent_ignored {
return;
}
if has_ignore_next {
self.skip_next = true;
return;
}
if self.skip_next {
self.skip_next = false;
return;
}
if let Some(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;
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_counter_stmt(CounterKind::from_pending(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);
self.record_ignored_if_arm(stmt, pragma);
let consequent_span = stmt.span;
let consequent_body_span = stmt.consequent.span();
let Some(branch_id) = self.add_branch("if", stmt.span) else {
return;
};
let cov_fn = self.cov_fn_name;
if pragma != Some(IgnoreType::If)
&& let Some(path_idx) =
self.add_branch_path_with_body(branch_id, consequent_span, consequent_body_span)
{
inject_branch_counter_into_statement(
&mut stmt.consequent,
CounterKind::branch(cov_fn, branch_id, path_idx),
ctx,
);
}
if pragma != Some(IgnoreType::Else) {
self.inject_else_branch_counter(stmt, branch_id, consequent_body_span.end, 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)
|| is_synthetic_span(expr.span)
{
return;
}
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);
if ignore_consequent && ignore_alternate {
return;
}
let Some(branch_id) = self.add_branch("cond-expr", expr.span) else {
return;
};
if !ignore_consequent
&& let Some(path_idx) = self.add_branch_path(branch_id, expr.consequent.span())
{
prepend_counter(
&mut expr.consequent,
CounterKind::branch(self.cov_fn_name, branch_id, path_idx),
ctx,
);
}
if !ignore_alternate
&& let Some(path_idx) = self.add_branch_path(branch_id, expr.alternate.span())
{
prepend_counter(
&mut expr.alternate,
CounterKind::branch(self.cov_fn_name, branch_id, path_idx),
ctx,
);
}
}
fn enter_switch_statement(
&mut self,
stmt: &mut SwitchStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
let Some(branch_id) = self.add_branch("switch", stmt.span) else {
return;
};
let cov_fn = self.cov_fn_name;
for case in &mut stmt.cases {
if is_ignored_case(case, &ctx.state.pragmas) {
continue;
}
let Some(path_idx) = self.add_branch_path(branch_id, case.span) else {
continue;
};
let branch_stmt =
build_counter_stmt(CounterKind::branch(cov_fn, 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_jsx_attribute(
&mut self,
attr: &mut JSXAttribute<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let ignored = jsx_attribute_ignored(attr, &ctx.state.pragmas, self.skip_next);
self.ignored_prop_stack.push(ignored);
if ignored {
self.skip_next = false;
}
}
fn exit_jsx_attribute(
&mut self,
_attr: &mut JSXAttribute<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_stack.pop();
}
fn enter_jsx_spread_attribute(
&mut self,
attr: &mut JSXSpreadAttribute<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let ignored = jsx_spread_attribute_ignored(attr, &ctx.state.pragmas, self.skip_next);
self.ignored_prop_stack.push(ignored);
if ignored {
self.skip_next = false;
}
}
fn exit_jsx_spread_attribute(
&mut self,
_attr: &mut JSXSpreadAttribute<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_stack.pop();
}
fn enter_jsx_child(
&mut self,
child: &mut JSXChild<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let ignored = jsx_child_ignored(child, &ctx.state.pragmas, self.skip_next);
self.ignored_prop_stack.push(ignored);
if ignored {
self.skip_next = false;
}
}
fn exit_jsx_child(
&mut self,
_child: &mut JSXChild<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_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)
|| is_synthetic_span(expr.span)
{
return;
}
match expr.operator {
LogicalOperator::And | LogicalOperator::Or | LogicalOperator::Coalesce => {
if is_parent_logical(ctx) {
return;
}
let leaf_spans = collect_logical_leaf_spans(expr, &ctx.state.pragmas);
if leaf_spans.is_empty() {
return;
}
let Some(branch_id) = self.add_branch("binary-expr", expr.span) else {
return;
};
for span in leaf_spans {
let location = self.span_to_location(span);
self.add_branch_path_location(branch_id, location, (span.start, span.end));
}
if self.report_logic {
self.logical_branch_ids.push(branch_id);
}
let mut state = LogicalWrapState::new(
self.cov_fn_name,
self.cov_fn_bt_name,
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 ctx.state.pragmas.get(param.span.start) == Some(IgnoreType::Next) {
return;
}
if let Some(init) = &mut param.initializer {
if matches!(
**init,
Expression::FunctionExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_)
) && let Some(id) = param.pattern.get_binding_identifier()
{
self.pending_name = Some(id.name.to_string());
}
let init_span = init.span();
let Some(branch_id) = self.add_branch("default-arg", param.span) else {
return;
};
if self.add_branch_path(branch_id, init_span).is_some() {
let state = LogicalWrapState::new(self.cov_fn_name, None, branch_id, false);
wrap_expression_with_branch_counter(init, &state, ctx);
}
}
}
fn enter_static_member_expression(
&mut self,
member: &mut StaticMemberExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if member.optional && !self.in_ignored_subtree() {
self.wrap_optional_chain_link(&mut member.object, member.span, ctx);
}
}
fn enter_computed_member_expression(
&mut self,
member: &mut ComputedMemberExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if member.optional && !self.in_ignored_subtree() {
self.wrap_optional_chain_link(&mut member.object, member.span, ctx);
}
}
fn enter_call_expression(
&mut self,
call: &mut CallExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if call.optional && !self.in_ignored_subtree() {
self.wrap_optional_chain_link(&mut call.callee, call.span, ctx);
}
}
fn enter_assignment_pattern(
&mut self,
pattern: &mut AssignmentPattern<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
if ctx.state.pragmas.get(pattern.span.start) == Some(IgnoreType::Next)
|| enclosing_destructure_property_pragma(ctx)
{
return;
}
if matches!(
pattern.right,
Expression::FunctionExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_)
) && let Some(id) = pattern.left.get_binding_identifier()
{
self.pending_name = Some(id.name.to_string());
}
let right_span = pattern.right.span();
let Some(branch_id) = self.add_branch("default-arg", pattern.span) else {
return;
};
if self.add_branch_path(branch_id, right_span).is_some() {
let state = LogicalWrapState::new(self.cov_fn_name, None, 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::Assign)
&& matches!(
expr.right,
Expression::FunctionExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_)
)
{
self.pending_name = match &expr.left {
AssignmentTarget::StaticMemberExpression(member) => {
Some(member.property.name.to_string())
}
AssignmentTarget::ComputedMemberExpression(member) => match &member.expression {
Expression::StringLiteral(lit) => Some(lit.value.to_string()),
_ => None,
},
_ => None,
};
}
if matches!(
expr.operator,
AssignmentOperator::LogicalOr
| AssignmentOperator::LogicalAnd
| AssignmentOperator::LogicalNullish
) {
let left_span = expr.left.span();
let right_span = expr.right.span();
let Some(branch_id) = self.add_branch("binary-expr", expr.span) else {
return;
};
let left_loc = self.span_to_location(left_span);
self.add_branch_path_location(branch_id, left_loc, (left_span.start, left_span.end));
let right_loc = self.span_to_location(right_span);
self.add_branch_path_location(branch_id, right_loc, (right_span.start, right_span.end));
self.pending_stmts.push(PendingInsertion {
target_start: expr.span.start,
counter_id: branch_id,
counter_type: CounterType::BranchLeft,
});
prepend_counter(
&mut expr.right,
CounterKind::branch(self.cov_fn_name, branch_id, 1),
ctx,
);
}
}
}
fn inject_branch_counter_into_statement<'a>(
stmt: &mut Statement<'a>,
kind: CounterKind<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
let counter_stmt = build_counter_stmt(kind, 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);
}
}
}