use std::collections::BTreeMap;
use oxc_allocator::{Allocator, Vec as ArenaVec};
use oxc_ast::ast::*;
use oxc_coverage_types::{BranchEntry, FnEntry, Location};
use oxc_span::{GetSpan, Span};
use oxc_traverse::{Traverse, TraverseCtx};
use crate::{
pragma::{IgnoreType, PragmaMap},
source_text,
};
mod branches;
mod counters;
mod coverage_map;
mod functions;
mod ignore;
mod logical;
mod names;
mod preamble;
mod statements;
use branches::OptionalChainLinkInput;
use counters::{ClassFieldHoist, PendingInsertion};
use coverage_map::is_synthetic_span;
use ignore::{
is_ignored_case, jsx_attribute_ignored, jsx_child_ignored, jsx_spread_attribute_ignored,
};
use names::{AssignmentTargetName, assignment_target_name};
pub use preamble::{PreambleInputs, djb31_hex, generate_cov_fn_name, generate_preamble_source};
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_insertions: 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: Option<&'arena str>,
report_logic: bool,
track_optional_chain: bool,
ignore_class_methods: Vec<String>,
name_callback_arguments: bool,
pub logical_branch_ids: Vec<usize>,
pub used_optional_chain_helper: bool,
pending_class_field_hoists: Vec<Vec<ClassFieldHoist>>,
eager_remapper: Option<oxc_coverage_source_maps::PositionRemapper>,
eager_statement_ids: BTreeMap<coverage_map::EagerMergeKey, usize>,
eager_function_ids: BTreeMap<coverage_map::EagerMergeKey, usize>,
eager_branch_ids: BTreeMap<coverage_map::BranchKey, usize>,
pub eager_function_overlay_conflict: bool,
}
pub struct TransformInit<'src, 'arena> {
pub allocator: &'arena Allocator,
pub source: &'src str,
pub cov_fn_name: &'src str,
pub report_logic: bool,
pub track_optional_chain: bool,
pub ignore_class_methods: Vec<String>,
pub name_callback_arguments: bool,
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,
track_optional_chain,
ignore_class_methods,
name_callback_arguments,
eager_remapper,
} = init;
let cov_fn_name = allocator.alloc_str(cov_fn_name);
Self {
source,
line_offsets: source_text::line_starts(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_insertions: 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: track_optional_chain
.then(|| allocator.alloc_str(&format!("{cov_fn_name}_oc"))),
report_logic,
track_optional_chain,
ignore_class_methods,
name_callback_arguments,
logical_branch_ids: Vec::new(),
used_optional_chain_helper: false,
pending_class_field_hoists: Vec::new(),
eager_remapper,
eager_statement_ids: BTreeMap::new(),
eager_function_ids: BTreeMap::new(),
eager_branch_ids: BTreeMap::new(),
eager_function_overlay_conflict: false,
}
}
}
impl<'a> Traverse<'a, CoverageState> for CoverageTransform<'_, 'a> {
fn enter_function(
&mut self,
func: &mut Function<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.register_function_entry(func, ctx);
}
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>,
) {
self.insert_function_counter(body, ctx);
}
fn enter_arrow_function_expression(
&mut self,
arrow: &mut ArrowFunctionExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.register_arrow_entry(arrow, ctx);
}
fn exit_arrow_function_expression(
&mut self,
arrow: &mut ArrowFunctionExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.convert_arrow_expression_body(arrow, ctx);
}
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>,
) {
self.instrument_variable_declarator(decl, 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>,
) {
self.name_anonymous_default_export(decl);
}
fn enter_method_definition(
&mut self,
method: &mut MethodDefinition<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.register_method_definition(method, ctx);
}
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>,
) {
self.instrument_property_definition(prop, ctx);
}
fn exit_property_definition(
&mut self,
_prop: &mut PropertyDefinition<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.ignored_prop_stack.pop();
}
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>,
) {
self.insert_class_field_counters(body, ctx);
}
fn enter_object_property(
&mut self,
prop: &mut ObjectProperty<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.register_object_property(prop, ctx);
}
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>,
) {
self.register_statement_counter(stmt, ctx);
}
fn exit_statement(
&mut self,
_stmt: &mut Statement<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.finish_statement();
}
fn exit_statements(
&mut self,
stmts: &mut ArenaVec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.insert_pending_statement_counters(stmts, ctx);
}
fn enter_if_statement(
&mut self,
stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.instrument_if_branches(stmt, ctx);
}
fn exit_if_statement(
&mut self,
_stmt: &mut IfStatement<'a>,
_ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.pop_ignored_if_arms();
}
fn enter_conditional_expression(
&mut self,
expr: &mut ConditionalExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.instrument_conditional_branches(expr, ctx);
}
fn enter_switch_statement(
&mut self,
stmt: &mut SwitchStatement<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.instrument_switch_cases(stmt, ctx);
}
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.push_prop_ignore_frame(ignored);
}
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.push_prop_ignore_frame(ignored);
}
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.push_prop_ignore_frame(ignored);
}
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>,
) {
self.instrument_logical_expression(expr, 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 !is_synthetic_span(body_span) {
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>,
) {
self.instrument_parameter_default(param, ctx);
}
fn enter_static_member_expression(
&mut self,
member: &mut StaticMemberExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.track_optional_chain && member.optional && !self.in_ignored_subtree() {
self.wrap_optional_chain_link(
OptionalChainLinkInput { object: &mut member.object, link_span: member.span },
ctx,
);
}
}
fn enter_computed_member_expression(
&mut self,
member: &mut ComputedMemberExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.track_optional_chain && member.optional && !self.in_ignored_subtree() {
self.wrap_optional_chain_link(
OptionalChainLinkInput { object: &mut member.object, link_span: member.span },
ctx,
);
}
}
fn enter_call_expression(
&mut self,
call: &mut CallExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.track_optional_chain
&& call.optional
&& call.callee.get_member_expr().is_none()
&& !self.in_ignored_subtree()
{
self.wrap_optional_chain_link(
OptionalChainLinkInput { object: &mut call.callee, link_span: call.span },
ctx,
);
}
}
fn enter_assignment_pattern(
&mut self,
pattern: &mut AssignmentPattern<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
self.instrument_destructuring_default(pattern, ctx);
}
fn enter_assignment_expression(
&mut self,
expr: &mut AssignmentExpression<'a>,
ctx: &mut TraverseCtx<'a, CoverageState>,
) {
if self.in_ignored_subtree() {
return;
}
if let AssignmentTargetName::Update(name) = assignment_target_name(expr) {
self.pending_name = name;
}
self.try_instrument_logical_assignment(expr, ctx);
}
}