Skip to main content

supercov_engine/
js_instrumenter.rs

1//! First oxc-backed vertical slice of the Rust JavaScript instrumenter.
2//!
3//! This candidate reports and instruments the complete frozen JavaScript
4//! denominator, including semantic-safety boundaries and exact wide-decision
5//! fallback. It remains private until its generated runtime import, evidence
6//! transport, and attribution behavior are defined by Supercov's frozen contracts.
7
8use std::{
9    collections::{HashMap, HashSet},
10    fmt::Write,
11    path::Path,
12    sync::Arc,
13};
14
15use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
16use oxc_allocator::{Allocator, CloneIn, TakeIn};
17use oxc_ast::{
18    AstBuilder, NONE,
19    ast::{
20        Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentExpression,
21        AssignmentPattern, AssignmentTarget, BindingPattern, CallExpression, CatchClause,
22        ChainElement, ChainExpression, Class, Comment, ComputedMemberExpression,
23        ConditionalExpression, Declaration, DoWhileStatement, ExportDefaultDeclarationKind,
24        Expression, ForInStatement, ForOfStatement, ForStatement, ForStatementLeft,
25        FormalParameter, FormalParameterKind, FormalParameters, Function, FunctionBody,
26        IfStatement, ImportDeclarationSpecifier, ImportOrExportKind, LogicalExpression,
27        NewExpression, ObjectPropertyKind, PrivateFieldExpression, Program, PropertyKey,
28        PropertyKind, Statement, StaticMemberExpression, SwitchStatement, TSGlobalDeclaration,
29        TSModuleDeclaration, TryStatement, VariableDeclaration, VariableDeclarationKind,
30        VariableDeclarator, WhileStatement, WithStatement,
31    },
32};
33use oxc_ast_visit::{Visit, VisitMut, walk, walk_mut};
34use oxc_codegen::{Codegen, CodegenOptions};
35use oxc_parser::Parser;
36use oxc_semantic::SemanticBuilder;
37use oxc_span::{GetSpan, SourceType, Span};
38use oxc_syntax::{
39    number::NumberBase,
40    operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator},
41    scope::ScopeFlags,
42    symbol::SymbolId,
43};
44use oxc_traverse::{Ancestor, Traverse, TraverseCtx, traverse_mut};
45use serde::{Deserialize, Serialize};
46use sha2::{Digest, Sha256};
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct CandidateDecision {
51    pub id: String,
52    pub file: String,
53    pub line: usize,
54    pub column: usize,
55    pub source: String,
56    pub conditions: Vec<String>,
57    pub kind: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct CandidatePoint {
63    pub id: String,
64    pub kind: String,
65    pub file: String,
66    pub line: usize,
67    pub column: usize,
68    pub source: String,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub label: Option<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct CandidateBranchAlternative {
76    pub id: String,
77    pub label: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct CandidateBranch {
83    pub id: String,
84    pub kind: String,
85    pub file: String,
86    pub line: usize,
87    pub column: usize,
88    pub source: String,
89    pub alternatives: Vec<CandidateBranchAlternative>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct CandidateOutput {
95    pub engine: String,
96    pub complete: bool,
97    pub supported_surface: String,
98    pub code: String,
99    pub map: Option<serde_json::Value>,
100    pub decisions: Vec<CandidateDecision>,
101    pub points: Vec<CandidatePoint>,
102    pub branches: Vec<CandidateBranch>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub runtime: Option<CandidateRuntime>,
105    pub coverage_limitations: Vec<CandidateLimitation>,
106    pub limitations: Vec<String>,
107}
108
109fn restore_comment_text(
110    program: &Program<'_>,
111    generated: &str,
112    map: oxc_sourcemap::SourceMap,
113) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
114    if program.comments.is_empty() {
115        return Ok((generated.to_string(), map));
116    }
117    let allocator = Allocator::default();
118    let reparsed = Parser::new(&allocator, generated, program.source_type).parse();
119    if !reparsed.errors.is_empty() {
120        return Err(CandidateError::Parse(
121            reparsed
122                .errors
123                .into_iter()
124                .map(|error| error.to_string())
125                .collect(),
126        ));
127    }
128    let mut matched = vec![false; program.comments.len()];
129    let mut original_index = 0;
130    let mut edits = Vec::<(usize, usize, String)>::new();
131    for emitted in &reparsed.program.comments {
132        let emitted_text = emitted.span.source_text(generated);
133        let (index, original) = loop {
134            let Some(original) = program.comments.get(original_index) else {
135                return Err(CandidateError::CommentPreservation {
136                    expected: program.comments.len(),
137                    actual: reparsed.program.comments.len(),
138                });
139            };
140            let index = original_index;
141            original_index += 1;
142            if original.kind == emitted.kind
143                && equal_ignoring_whitespace(
144                    original.span.source_text(program.source_text),
145                    emitted_text,
146                )
147            {
148                break (index, original);
149            }
150        };
151        matched[index] = true;
152        edits.push((
153            emitted.span.start as usize,
154            emitted.span.end as usize,
155            original.span.source_text(program.source_text).to_string(),
156        ));
157    }
158
159    let source_lines = Utf16LineIndex::new(program.source_text);
160    let generated_lines = Utf16LineIndex::new(generated);
161    let mut mappings = map
162        .get_tokens()
163        .filter_map(|token| {
164            token.get_source_id()?;
165            Some((
166                source_lines
167                    .byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
168                generated_lines
169                    .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
170            ))
171        })
172        .collect::<Vec<_>>();
173    mappings.sort_unstable_by_key(|(source, destination)| (*source, *destination));
174    let statements = statement_spans(&reparsed.program);
175    for (index, original) in program.comments.iter().enumerate() {
176        if matched[index] {
177            continue;
178        }
179        let anchor = if original.attached_to > 0 {
180            original.attached_to as usize
181        } else {
182            original.span.end as usize
183        };
184        let mapping_index = mappings.partition_point(|(source, _)| *source < anchor);
185        let mapped = mappings
186            .get(mapping_index)
187            .map_or(generated.len(), |(_, destination)| *destination);
188        let (destination, text) = place_restored_comment(
189            generated,
190            &statements,
191            mapped,
192            original,
193            original.span.source_text(program.source_text),
194        );
195        edits.push((destination, destination, text));
196    }
197    edits.sort_by_key(|(start, end, _)| (*start, *end));
198    let restored_len = edits
199        .iter()
200        .fold(generated.len(), |length, (start, end, text)| {
201            length + text.len() - (end - start)
202        });
203    let mut restored = String::with_capacity(restored_len);
204    let mut cursor = 0;
205    for (start, end, replacement) in &edits {
206        if *start < cursor {
207            return Err(CandidateError::CommentPreservation {
208                expected: program.comments.len(),
209                actual: reparsed.program.comments.len(),
210            });
211        }
212        restored.push_str(&generated[cursor..*start]);
213        restored.push_str(replacement);
214        cursor = *end;
215    }
216    restored.push_str(&generated[cursor..]);
217    let map = shift_source_map(map, generated, &restored, &edits);
218    Ok((restored, map))
219}
220
221/// Every statement span in a program, so a restored comment can be anchored
222/// to a statement boundary.
223fn statement_spans(program: &Program<'_>) -> Vec<Span> {
224    struct Spans(Vec<Span>);
225    impl<'a> Visit<'a> for Spans {
226        fn visit_statement(&mut self, statement: &Statement<'a>) {
227            self.0.push(statement.span());
228            walk::walk_statement(self, statement);
229        }
230    }
231    let mut spans = Spans(Vec::new());
232    spans.visit_program(program);
233    spans.0
234}
235
236/// Where a comment the generator dropped goes back in, and with what
237/// whitespace around it.
238///
239/// The mapped position is only a hint: it is where the node the comment was
240/// attached to landed, and that can be mid-line. A comment that carries a
241/// line break must not be dropped there. After `return`, `throw`, `break`,
242/// `continue` or `yield` the break lets automatic semicolon insertion end the
243/// statement early -- `return` + newline + JSX parsed as `return;` with the
244/// JSX unreachable, and the bundler then removed the whole tree, so a React
245/// component rendered nothing under measurement while passing plainly. A `//`
246/// comment inserted before more code on the same line comments that code out.
247/// Such a comment moves to the start of the innermost statement containing
248/// the position, where a line break is always inert. A single-line block
249/// comment is inert wherever it sits, so mid-line it stays inline.
250fn place_restored_comment(
251    generated: &str,
252    statements: &[Span],
253    mapped: usize,
254    comment: &Comment,
255    text: &str,
256) -> (usize, String) {
257    let line_prefix = |offset: usize| generated[..offset].rsplit('\n').next().unwrap_or("");
258    let at_line_start = line_prefix(mapped)
259        .chars()
260        .all(|character| character == ' ' || character == '\t');
261    if at_line_start {
262        let mut placed = String::new();
263        placed.push(if comment.preceded_by_newline() {
264            '\n'
265        } else {
266            ' '
267        });
268        placed.push_str(text);
269        placed.push(if comment.is_line() || comment.followed_by_newline() {
270            '\n'
271        } else {
272            ' '
273        });
274        return (mapped, placed);
275    }
276    if !comment.is_line() && !text.contains('\n') {
277        let leading = if generated[..mapped].ends_with([' ', '\t']) {
278            ""
279        } else {
280            " "
281        };
282        return (mapped, format!("{leading}{text} "));
283    }
284    let Some(statement) = statements
285        .iter()
286        .filter(|span| span.start as usize <= mapped && mapped < span.end as usize)
287        .max_by_key(|span| span.start)
288    else {
289        return (mapped, format!("\n{text}\n"));
290    };
291    let start = statement.start as usize;
292    let indentation: String = line_prefix(start)
293        .chars()
294        .take_while(|character| *character == ' ' || *character == '\t')
295        .collect();
296    (start, format!("{text}\n{indentation}"))
297}
298
299fn equal_ignoring_whitespace(left: &str, right: &str) -> bool {
300    left.chars()
301        .filter(|character| !character.is_whitespace())
302        .eq(right.chars().filter(|character| !character.is_whitespace()))
303}
304
305struct Utf16LineIndex<'s> {
306    source: &'s str,
307    starts: Vec<usize>,
308}
309
310impl<'s> Utf16LineIndex<'s> {
311    fn new(source: &'s str) -> Self {
312        let mut starts = Vec::with_capacity(source.lines().count() + 1);
313        starts.push(0);
314        starts.extend(
315            source
316                .char_indices()
317                .filter_map(|(offset, character)| (character == '\n').then_some(offset + 1)),
318        );
319        Self { source, starts }
320    }
321
322    fn byte_offset(&self, target_line: usize, target_utf16_col: usize) -> usize {
323        let Some(&start) = self.starts.get(target_line) else {
324            return self.source.len();
325        };
326        let end = self
327            .starts
328            .get(target_line + 1)
329            .copied()
330            .unwrap_or(self.source.len());
331        start + utf16_col_to_byte(&self.source[start..end], target_utf16_col)
332    }
333
334    fn line_col(&self, byte_offset: usize) -> (u32, u32) {
335        let byte_offset = byte_offset.min(self.source.len());
336        let line = self.starts.partition_point(|start| *start <= byte_offset) - 1;
337        let column = self.source[self.starts[line]..byte_offset]
338            .chars()
339            .map(char::len_utf16)
340            .sum::<usize>();
341        (line as u32, column as u32)
342    }
343}
344
345fn utf16_col_to_byte(line: &str, target_utf16_col: usize) -> usize {
346    let mut column = 0;
347    for (offset, character) in line.char_indices() {
348        if column >= target_utf16_col {
349            return offset;
350        }
351        column += character.len_utf16();
352    }
353    line.len()
354}
355
356fn shift_source_map(
357    map: oxc_sourcemap::SourceMap,
358    generated: &str,
359    restored: &str,
360    edits: &[(usize, usize, String)],
361) -> oxc_sourcemap::SourceMap {
362    let generated_lines = Utf16LineIndex::new(generated);
363    let restored_lines = Utf16LineIndex::new(restored);
364    let mut edit_index = 0;
365    let mut shift = 0isize;
366    let tokens = map
367        .get_tokens()
368        .map(|token| {
369            let original_offset = generated_lines
370                .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
371            while let Some((start, end, replacement)) = edits.get(edit_index) {
372                if *end > original_offset {
373                    break;
374                }
375                shift += replacement.len() as isize - (*end - *start) as isize;
376                edit_index += 1;
377            }
378            let shifted_offset = edits
379                .get(edit_index)
380                .filter(|(start, end, _)| *start <= original_offset && original_offset < *end)
381                .map_or_else(
382                    || original_offset.saturating_add_signed(shift),
383                    |(start, _, _)| start.saturating_add_signed(shift),
384                );
385            let (dst_line, dst_col) = restored_lines.line_col(shifted_offset);
386            oxc_sourcemap::Token::new(
387                dst_line,
388                dst_col,
389                token.get_src_line(),
390                token.get_src_col(),
391                token.get_source_id(),
392                token.get_name_id(),
393            )
394        })
395        .collect::<Vec<_>>();
396    let mut shifted = oxc_sourcemap::SourceMap::new(
397        map.get_file().cloned(),
398        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
399        map.get_source_root().map(str::to_string),
400        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
401        map.get_source_contents()
402            .map(|content| content.cloned())
403            .collect::<Vec<Option<Arc<str>>>>(),
404        tokens.into_boxed_slice(),
405        None,
406    );
407    if let Some(ignore_list) = map.get_x_google_ignore_list() {
408        shifted.set_x_google_ignore_list(ignore_list.to_vec());
409    }
410    if let Some(debug_id) = map.get_debug_id() {
411        shifted.set_debug_id(debug_id);
412    }
413    shifted
414}
415
416fn generate_candidate(
417    program: &Program<'_>,
418    file: &str,
419) -> Result<(String, Option<serde_json::Value>), CandidateError> {
420    let options = CodegenOptions {
421        source_map_path: Some(Path::new(file).to_path_buf()),
422        ..CodegenOptions::default()
423    };
424    let generated = Codegen::new().with_options(options).build(program);
425    let (code, map) = restore_comment_text(
426        program,
427        &generated.code,
428        generated.map.expect("source maps are enabled"),
429    )?;
430    let map = Some({
431        serde_json::from_str(&map.to_json_string())
432            .expect("oxc must serialize its own generated source map")
433    });
434    Ok((code, map))
435}
436
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct CandidateRuntime {
440    pub coverage_hit: String,
441    pub mcdc_begin: String,
442    pub mcdc_condition: String,
443    pub mcdc_end: String,
444    pub register_probe_v2: String,
445    pub mcdc_end_v2: String,
446    pub coverage_hit_v2: String,
447    pub probe_file_v2: String,
448    pub selection_begin: String,
449    pub selection_right: String,
450    pub selection_end: String,
451    pub parenthesized_assignment_value: String,
452    pub with_request_phase: String,
453    pub optional_select: String,
454    pub optional_call_begin: String,
455    pub optional_call_reached: String,
456    pub optional_call_continued: String,
457    pub optional_call_end: String,
458    pub default_selected: String,
459    pub default_entered: String,
460    pub try_begin: String,
461    pub try_catch: String,
462    pub try_end: String,
463    pub loop_begin: String,
464    pub loop_entered: String,
465    pub loop_end: String,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(rename_all = "camelCase")]
470pub struct CandidateLimitation {
471    pub id: String,
472    pub kind: String,
473    pub file: String,
474    pub line: usize,
475    pub column: usize,
476    pub source: String,
477    pub reason: String,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub enum CandidateError {
482    UnknownSourceType(String),
483    Parse(Vec<String>),
484    CommentPreservation { expected: usize, actual: usize },
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488enum RuntimeBinding {
489    ModuleImport,
490    DirectGlobal,
491}
492
493const NODE_ASSERT_MODULES: &[&str] = &[
494    "assert",
495    "assert/strict",
496    "node:assert",
497    "node:assert/strict",
498];
499const NODE_ASSERT_METHODS: &[&str] = &[
500    "deepEqual",
501    "deepStrictEqual",
502    "doesNotMatch",
503    "doesNotReject",
504    "doesNotThrow",
505    "equal",
506    "fail",
507    "ifError",
508    "match",
509    "notDeepEqual",
510    "notDeepStrictEqual",
511    "notEqual",
512    "notStrictEqual",
513    "ok",
514    "partialDeepStrictEqual",
515    "rejects",
516    "strictEqual",
517    "throws",
518];
519
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct NodeAssertionInstrumentation {
522    pub code: String,
523    pub assertions: usize,
524    pub capability_imports: usize,
525    /// Test-file statements that received a `testStatement(...)` marker (0 when the
526    /// module has no assertions).
527    pub statements: usize,
528}
529
530const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
531    &["@jest/globals", "@playwright/test", "playwright", "vitest"];
532
533fn capability_source_candidate(source: &str) -> bool {
534    // A capability wrapper is useful only when an argument can establish a
535    // host/guest workspace mapping. Generic words such as `snapshot`,
536    // `machine`, `acquire` and `spawn` are common in tests and application
537    // code; treating any of them as sufficient wrapped unrelated framework
538    // registration functions and changed their captured callsite.
539    let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
540        || (source.contains("hostRoot") && source.contains("guestRoot"));
541    // The guest path is commonly computed (for example
542    // `resolve(tmpdir(), "workspace")`), so requiring the literal
543    // `/workspace` loses genuine opaque launchers. The nested mount shape is
544    // already specific, and the AST pass below still restricts wrapping to
545    // the imported root actually called with that argument.
546    let mount_mapping =
547        source.contains("mounts") && source.contains("source") && source.contains("target");
548    direct_mapping || mount_mapping
549}
550
551fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
552    source == wrapper
553        || source.starts_with("node:")
554        || source.starts_with("virtual:supercov-")
555        || source.contains(".supercov/")
556        || CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
557}
558
559fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
560    match expression {
561        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
562        Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
563        Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
564        Expression::CallExpression(call) => capability_callee_root(&call.callee),
565        Expression::ParenthesizedExpression(parenthesized) => {
566            capability_callee_root(&parenthesized.expression)
567        }
568        Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
569        Expression::TSSatisfiesExpression(expression) => {
570            capability_callee_root(&expression.expression)
571        }
572        Expression::TSNonNullExpression(expression) => {
573            capability_callee_root(&expression.expression)
574        }
575        Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
576        _ => None,
577    }
578}
579
580struct CapabilityCallCollector<'s> {
581    source: &'s str,
582    mapping_variables: HashSet<String>,
583    roots: HashSet<String>,
584}
585
586impl CapabilityCallCollector<'_> {
587    fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
588        if let Argument::Identifier(identifier) = argument
589            && self.mapping_variables.contains(identifier.name.as_str())
590        {
591            return true;
592        }
593        capability_source_candidate(source_slice(self.source, argument.span()))
594    }
595}
596
597impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
598    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
599        if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
600            (&declarator.id, &declarator.init)
601            && capability_source_candidate(source_slice(self.source, initializer.span()))
602        {
603            self.mapping_variables.insert(identifier.name.to_string());
604        }
605        walk::walk_variable_declarator(self, declarator);
606    }
607
608    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
609        if call
610            .arguments
611            .iter()
612            .any(|argument| self.argument_has_mapping(argument))
613            && let Some(root) = capability_callee_root(&call.callee)
614        {
615            self.roots.insert(root);
616        }
617        walk::walk_call_expression(self, call);
618    }
619}
620
621fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
622    let mut collector = CapabilityCallCollector {
623        source,
624        mapping_variables: HashSet::new(),
625        roots: HashSet::new(),
626    };
627    // Mapping variables must be known before calls are inspected because a
628    // declaration may appear after a function that closes over it.
629    collector.visit_program(program);
630    let mapping_variables = collector.mapping_variables.clone();
631    collector.roots.clear();
632    collector.mapping_variables = mapping_variables;
633    collector.visit_program(program);
634    collector.roots
635}
636
637/// Wrap value imports that can hide a remote execution capability. This is an
638/// ahead-of-run Rust transform; the runtime shim only proxies the imported
639/// value and never parses or rewrites source.
640fn transform_capability_imports<'a>(
641    allocator: &'a Allocator,
642    program: &mut Program<'a>,
643    source: &str,
644    wrapper: &str,
645) -> usize {
646    if !capability_source_candidate(source) || !program.source_type.is_module() {
647        return 0;
648    }
649    let capability_roots = capability_import_roots(program, source);
650    if capability_roots.is_empty() {
651        return 0;
652    }
653    let ast = AstBuilder::new(allocator);
654    let mut names = CandidateNames::new(source);
655    let wrapper_local = names.allocate("__supercovImportedCapability");
656    let mut wrapped = 0;
657    let mut output = ast.vec();
658    for statement in program.body.take_in(allocator) {
659        let Statement::ImportDeclaration(mut declaration) = statement else {
660            output.push(statement);
661            continue;
662        };
663        let module_name = declaration.source.value.as_str();
664        if declaration.import_kind == ImportOrExportKind::Type
665            || excluded_capability_import(module_name, wrapper)
666        {
667            output.push(Statement::ImportDeclaration(declaration));
668            continue;
669        }
670        let mut declarations = ast.vec();
671        for specifier in declaration.specifiers.iter_mut().flatten() {
672            let local = match specifier {
673                ImportDeclarationSpecifier::ImportSpecifier(specifier)
674                    if specifier.import_kind == ImportOrExportKind::Type =>
675                {
676                    continue;
677                }
678                ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
679                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
680                    &mut specifier.local
681                }
682                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
683                    &mut specifier.local
684                }
685            };
686            let original = local.name.to_string();
687            if !capability_roots.contains(&original) {
688                continue;
689            }
690            let raw = names.allocate(&format!("__supercovRaw{original}"));
691            local.name = ast.ident(&raw);
692            let wrapped_value = ast.expression_call(
693                Span::default(),
694                ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
695                NONE,
696                ast.vec1(Argument::from(
697                    ast.expression_identifier(Span::default(), ast.ident(&raw)),
698                )),
699                false,
700            );
701            declarations.push(ast.variable_declarator(
702                Span::default(),
703                VariableDeclarationKind::Const,
704                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
705                NONE,
706                Some(wrapped_value),
707                false,
708            ));
709            wrapped += 1;
710        }
711        output.push(Statement::ImportDeclaration(declaration));
712        if !declarations.is_empty() {
713            output.push(Statement::VariableDeclaration(
714                ast.alloc_variable_declaration(
715                    Span::default(),
716                    VariableDeclarationKind::Const,
717                    declarations,
718                    false,
719                ),
720            ));
721        }
722    }
723    if wrapped > 0 {
724        output.insert(
725            0,
726            Statement::ImportDeclaration(ast.alloc_import_declaration(
727                Span::default(),
728                Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
729                    Span::default(),
730                    ast.module_export_name_identifier_name(
731                        Span::default(),
732                        ast.ident("wrapImportedCapability"),
733                    ),
734                    ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
735                    ImportOrExportKind::Value,
736                ))),
737                ast.string_literal(Span::default(), ast.str(wrapper), None),
738                None,
739                NONE,
740                ImportOrExportKind::Value,
741            )),
742        );
743    }
744    program.body = output;
745    wrapped
746}
747
748fn canonical_assert_module(value: &str) -> Option<String> {
749    NODE_ASSERT_MODULES.contains(&value).then(|| {
750        if value.starts_with("node:") {
751            value.into()
752        } else {
753            format!("node:{value}")
754        }
755    })
756}
757
758fn required_assert_module(
759    expression: &Expression<'_>,
760    scoping: &oxc_semantic::Scoping,
761) -> Option<String> {
762    if let Expression::CallExpression(call) = expression
763        && matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
764        && matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
765        && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
766    {
767        return canonical_assert_module(module.value.as_str());
768    }
769    if let Expression::StaticMemberExpression(member) = expression
770        && member.property.name == "strict"
771        && let Some(module) = required_assert_module(&member.object, scoping)
772    {
773        return Some(if module == "node:assert" {
774            "node:assert/strict".into()
775        } else {
776            module
777        });
778    }
779    None
780}
781
782#[derive(Default)]
783struct NodeAssertionBindings {
784    objects: HashMap<SymbolId, String>,
785    direct: HashMap<SymbolId, String>,
786    expects: HashSet<SymbolId>,
787    /// Jest (and Jasmine) inject `expect` as a global next to `test`, `it`
788    /// and `describe`: a file that reaches for all of those unresolved is a
789    /// test file whose bare `expect` is the assertion.
790    global_expect: bool,
791}
792
793fn bind_object(
794    bindings: &mut NodeAssertionBindings,
795    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
796    module: String,
797) {
798    if let Some(symbol) = identifier.symbol_id.get() {
799        bindings.objects.insert(symbol, module);
800    }
801}
802
803fn bind_direct(
804    bindings: &mut NodeAssertionBindings,
805    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
806    operation: String,
807) {
808    if let Some(symbol) = identifier.symbol_id.get() {
809        bindings.direct.insert(symbol, operation);
810    }
811}
812
813fn bind_expect(
814    bindings: &mut NodeAssertionBindings,
815    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
816) {
817    if let Some(symbol) = identifier.symbol_id.get() {
818        bindings.expects.insert(symbol);
819    }
820}
821
822struct NodeAssertionBindingCollector<'s> {
823    bindings: NodeAssertionBindings,
824    scoping: &'s oxc_semantic::Scoping,
825    allow_contextual_expect: bool,
826    expect_modules: HashSet<String>,
827}
828
829impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
830    fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
831        let module_name = declaration.source.value.as_str();
832        let assert_module = canonical_assert_module(module_name);
833        let expect_module =
834            self.expect_modules.contains(module_name) || self.allow_contextual_expect;
835        for specifier in declaration.specifiers.iter().flatten() {
836            match specifier {
837                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
838                    if let Some(module) = &assert_module {
839                        bind_object(&mut self.bindings, &specifier.local, module.clone());
840                    } else if expect_module && specifier.local.name == "expect" {
841                        bind_expect(&mut self.bindings, &specifier.local);
842                    }
843                }
844                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
845                    if let Some(module) = &assert_module {
846                        bind_object(&mut self.bindings, &specifier.local, module.clone());
847                    }
848                }
849                ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
850                    let imported = specifier.imported.name().to_string();
851                    if let Some(module) = &assert_module {
852                        if imported == "strict" {
853                            bind_object(
854                                &mut self.bindings,
855                                &specifier.local,
856                                "node:assert/strict".into(),
857                            );
858                        } else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
859                            bind_direct(
860                                &mut self.bindings,
861                                &specifier.local,
862                                format!("{module}.{imported}"),
863                            );
864                        }
865                    } else if expect_module && imported == "expect" {
866                        bind_expect(&mut self.bindings, &specifier.local);
867                    }
868                }
869            }
870        }
871        walk::walk_import_declaration(self, declaration);
872    }
873
874    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
875        if let Some(module) = declarator
876            .init
877            .as_ref()
878            .and_then(|expression| required_assert_module(expression, self.scoping))
879        {
880            match &declarator.id {
881                BindingPattern::BindingIdentifier(identifier) => {
882                    bind_object(&mut self.bindings, identifier, module);
883                }
884                BindingPattern::ObjectPattern(pattern) => {
885                    for property in &pattern.properties {
886                        let Some(imported) = property.key.static_name() else {
887                            continue;
888                        };
889                        let BindingPattern::BindingIdentifier(local) = &property.value else {
890                            continue;
891                        };
892                        if imported == "strict" {
893                            bind_object(&mut self.bindings, local, "node:assert/strict".into());
894                        } else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
895                            bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
896                        }
897                    }
898                }
899                _ => {}
900            }
901        }
902        walk::walk_variable_declarator(self, declarator);
903    }
904}
905
906fn node_assertion_bindings(
907    program: &Program<'_>,
908    scoping: &oxc_semantic::Scoping,
909    extra_expect_modules: &[String],
910) -> NodeAssertionBindings {
911    let allow_contextual_expect = program.source_text.contains("node:test")
912        && program
913            .source_text
914            .split(|character: char| !character.is_alphanumeric() && character != '_')
915            .any(|word| word == "expect");
916    let mut collector = NodeAssertionBindingCollector {
917        bindings: NodeAssertionBindings::default(),
918        scoping,
919        allow_contextual_expect,
920        expect_modules: ["vitest", "@jest/globals", "expect"]
921            .into_iter()
922            .map(str::to_owned)
923            .chain(extra_expect_modules.iter().cloned())
924            .collect(),
925    };
926    collector.visit_program(program);
927    let unresolved = scoping.root_unresolved_references();
928    let uses = |name: &str| unresolved.contains_key(name);
929    collector.bindings.global_expect =
930        uses("expect") && (uses("test") || uses("it") || uses("describe"));
931    collector.bindings
932}
933
934struct NodeAssertionSiteCollector<'s> {
935    source: &'s str,
936    file: &'s str,
937    bindings: &'s NodeAssertionBindings,
938    scoping: &'s oxc_semantic::Scoping,
939    sites: HashMap<SpanKey, NodeAssertionSite>,
940}
941
942struct NodeAssertionSite {
943    operation: String,
944    source: String,
945    bind_callee: bool,
946}
947
948fn referenced_symbol(
949    identifier: &oxc_ast::ast::IdentifierReference<'_>,
950    scoping: &oxc_semantic::Scoping,
951) -> Option<SymbolId> {
952    identifier
953        .reference_id
954        .get()
955        .and_then(|reference| scoping.get_reference(reference).symbol_id())
956}
957
958fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
959    match expression {
960        Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
961        Expression::ComputedMemberExpression(member) => match &member.expression {
962            Expression::StringLiteral(literal) => Some(literal.value.as_str()),
963            _ => None,
964        },
965        _ => None,
966    }
967}
968
969fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
970    match expression {
971        Expression::StaticMemberExpression(member) => Some(&member.object),
972        Expression::ComputedMemberExpression(member) => Some(&member.object),
973        _ => None,
974    }
975}
976
977fn expect_operation(
978    callee: &Expression<'_>,
979    bindings: &NodeAssertionBindings,
980    scoping: &oxc_semantic::Scoping,
981) -> Option<String> {
982    let mut current = callee;
983    let mut matchers = Vec::new();
984    while let Some(name) = assertion_member_name(current) {
985        matchers.push(name.to_owned());
986        current = assertion_member_object(current)?;
987    }
988    matchers.reverse();
989    let matcher = matchers.last()?;
990    if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
991        return None;
992    }
993    let Expression::CallExpression(expect_call) = current else {
994        return None;
995    };
996    let Expression::Identifier(identifier) = &expect_call.callee else {
997        return None;
998    };
999    let recognized = match referenced_symbol(identifier, scoping) {
1000        Some(symbol) => bindings.expects.contains(&symbol),
1001        None => bindings.global_expect && identifier.name == "expect",
1002    };
1003    recognized.then(|| format!("expect.{}", matchers.join(".")))
1004}
1005
1006#[derive(Default)]
1007struct AwaitYieldScanner {
1008    found: bool,
1009    has_yield: bool,
1010}
1011
1012impl<'a> Visit<'a> for AwaitYieldScanner {
1013    fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
1014        self.found = true;
1015    }
1016
1017    fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
1018        self.found = true;
1019        self.has_yield = true;
1020    }
1021
1022    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
1023
1024    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
1025}
1026
1027impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
1028    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1029        let operation = match &call.callee {
1030            Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
1031                .and_then(|symbol| {
1032                    self.bindings.direct.get(&symbol).cloned().or_else(|| {
1033                        self.bindings
1034                            .objects
1035                            .get(&symbol)
1036                            .map(|module| format!("{module}.ok"))
1037                    })
1038                }),
1039            callee => {
1040                let member_operation = assertion_member_object(callee).and_then(|object| {
1041                    let Expression::Identifier(identifier) = object else {
1042                        return None;
1043                    };
1044                    let symbol = referenced_symbol(identifier, self.scoping)?;
1045                    let method = assertion_member_name(callee)?;
1046                    self.bindings
1047                        .objects
1048                        .get(&symbol)
1049                        .filter(|_| NODE_ASSERT_METHODS.contains(&method))
1050                        .map(|module| format!("{module}.{method}"))
1051                });
1052                member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
1053            }
1054        };
1055        if let Some(operation) = operation {
1056            let mut unsafe_argument = AwaitYieldScanner::default();
1057            unsafe_argument.visit_expression(&call.callee);
1058            for argument in &call.arguments {
1059                unsafe_argument.visit_argument(argument);
1060            }
1061            // Binding the callee leaves awaited arguments in their original async
1062            // function. Start the phase at invocation, not before suspension.
1063            // Optional calls, generators, matchers and promise-returning assertion
1064            // methods retain the adapter fallback until separately supported.
1065            let bind_callee = unsafe_argument.found
1066                && !unsafe_argument.has_yield
1067                && operation.starts_with("node:assert")
1068                && !operation.ends_with(".rejects")
1069                && !operation.ends_with(".doesNotReject")
1070                && !call.optional
1071                && match &call.callee {
1072                    Expression::Identifier(_) => true,
1073                    Expression::StaticMemberExpression(member) => !member.optional,
1074                    Expression::ComputedMemberExpression(member) => !member.optional,
1075                    _ => false,
1076                };
1077            if unsafe_argument.found && !bind_callee {
1078                walk::walk_call_expression(self, call);
1079                return;
1080            }
1081            let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
1082            self.sites.insert(
1083                span_key(call.span),
1084                NodeAssertionSite {
1085                    operation,
1086                    source: format!("{}:{line}:{column}", self.file),
1087                    bind_callee,
1088                },
1089            );
1090        }
1091        walk::walk_call_expression(self, call);
1092    }
1093}
1094
1095struct NodeAssertionTransformer<'a> {
1096    ast: AstBuilder<'a>,
1097    sites: HashMap<SpanKey, NodeAssertionSite>,
1098}
1099
1100impl<'a> NodeAssertionTransformer<'a> {
1101    fn runtime_helper(&self, name: &str) -> Expression<'a> {
1102        let runtime = Expression::StaticMemberExpression(
1103            self.ast.alloc_static_member_expression(
1104                Span::default(),
1105                self.ast
1106                    .expression_identifier(Span::default(), self.ast.ident("globalThis")),
1107                self.ast.identifier_name(
1108                    Span::default(),
1109                    self.ast.ident("__SUPERCOV_DIRECT_RUNTIME__"),
1110                ),
1111                false,
1112            ),
1113        );
1114        Expression::StaticMemberExpression(
1115            self.ast.alloc_static_member_expression(
1116                Span::default(),
1117                runtime,
1118                self.ast
1119                    .identifier_name(Span::default(), self.ast.ident(name)),
1120                false,
1121            ),
1122        )
1123    }
1124
1125    fn bind_callee(&self, callee: Expression<'a>, operation: &str, source: &str) -> Expression<'a> {
1126        let mut arguments = self.ast.vec_from_array([
1127            Argument::from(self.ast.expression_string_literal(
1128                Span::default(),
1129                self.ast.str(operation),
1130                None,
1131            )),
1132            Argument::from(self.ast.expression_string_literal(
1133                Span::default(),
1134                self.ast.str(source),
1135                None,
1136            )),
1137        ]);
1138        match callee {
1139            Expression::StaticMemberExpression(member) => {
1140                let member = member.unbox();
1141                arguments.push(Argument::from(member.object));
1142                arguments.push(Argument::from(self.ast.expression_string_literal(
1143                    Span::default(),
1144                    self.ast.str(member.property.name.as_str()),
1145                    None,
1146                )));
1147            }
1148            Expression::ComputedMemberExpression(member) => {
1149                let member = member.unbox();
1150                arguments.push(Argument::from(member.object));
1151                arguments.push(Argument::from(member.expression));
1152            }
1153            callee => arguments.push(Argument::from(callee)),
1154        }
1155        self.ast.expression_call(
1156            Span::default(),
1157            self.runtime_helper("bindNodeAssertion"),
1158            NONE,
1159            arguments,
1160            false,
1161        )
1162    }
1163
1164    fn wrap(&self, original: Expression<'a>, operation: &str, source: &str) -> Expression<'a> {
1165        let parameters = self.ast.alloc_formal_parameters(
1166            Span::default(),
1167            FormalParameterKind::ArrowFormalParameters,
1168            self.ast.vec(),
1169            NONE,
1170        );
1171        let body = self.ast.alloc_function_body(
1172            Span::default(),
1173            self.ast.vec(),
1174            self.ast
1175                .vec1(self.ast.statement_return(Span::default(), Some(original))),
1176        );
1177        let callback = self.ast.expression_arrow_function(
1178            Span::default(),
1179            false,
1180            false,
1181            NONE,
1182            parameters,
1183            NONE,
1184            body,
1185        );
1186        self.ast.expression_call(
1187            Span::default(),
1188            self.runtime_helper("withNodeAssertionPhase"),
1189            NONE,
1190            self.ast.vec_from_array([
1191                Argument::from(self.ast.expression_string_literal(
1192                    Span::default(),
1193                    self.ast.str(operation),
1194                    None,
1195                )),
1196                Argument::from(self.ast.expression_string_literal(
1197                    Span::default(),
1198                    self.ast.str(source),
1199                    None,
1200                )),
1201                Argument::from(callback),
1202            ]),
1203            false,
1204        )
1205    }
1206}
1207
1208impl<'a> VisitMut<'a> for NodeAssertionTransformer<'a> {
1209    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
1210        let key = span_key(expression.span());
1211        walk_mut::walk_expression(self, expression);
1212        let Some(site) = self.sites.remove(&key) else {
1213            return;
1214        };
1215        if site.bind_callee {
1216            if let Expression::CallExpression(call) = expression {
1217                let callee = call.callee.take_in(self.ast.allocator);
1218                call.callee = self.bind_callee(callee, &site.operation, &site.source);
1219            }
1220            return;
1221        }
1222        let original = expression.take_in(self.ast.allocator);
1223        *expression = self.wrap(original, &site.operation, &site.source);
1224    }
1225}
1226
1227/// Attribute native node:assert and node:test expect calls by opening the
1228/// assertion phase before synchronous argument evaluation, or at invocation for
1229/// supported native calls with awaited arguments. Lexical symbol identity avoids
1230/// wrapping a shadowed or merely assert-shaped user binding.
1231pub fn instrument_node_assertion_phases(
1232    source: &str,
1233    file: &str,
1234) -> Result<NodeAssertionInstrumentation, CandidateError> {
1235    instrument_node_assertion_phases_with_expect_modules(source, file, &[])
1236}
1237
1238pub fn instrument_node_assertion_phases_with_expect_modules(
1239    source: &str,
1240    file: &str,
1241    extra_expect_modules: &[String],
1242) -> Result<NodeAssertionInstrumentation, CandidateError> {
1243    instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
1244}
1245
1246pub fn instrument_node_assertion_phases_with_runtime_hooks(
1247    source: &str,
1248    file: &str,
1249    extra_expect_modules: &[String],
1250    capability_wrapper: Option<&str>,
1251) -> Result<NodeAssertionInstrumentation, CandidateError> {
1252    instrument_node_assertion_phases_with_runtime_imports(
1253        source,
1254        file,
1255        extra_expect_modules,
1256        capability_wrapper,
1257        None,
1258    )
1259}
1260
1261/// Attribute assertions and, for an ESM test module, make the runtime
1262/// dependency explicit in the transformed module itself. This is required for
1263/// opaque runners which copy an already-instrumented workspace into another
1264/// process, container, or VM while constructing a fresh environment. Static
1265/// imports preserve ESM evaluation ordering and do not depend on runner-specific
1266/// environment forwarding.
1267pub fn instrument_node_assertion_phases_with_runtime_imports(
1268    source: &str,
1269    file: &str,
1270    extra_expect_modules: &[String],
1271    capability_wrapper: Option<&str>,
1272    assertion_runtime: Option<&str>,
1273) -> Result<NodeAssertionInstrumentation, CandidateError> {
1274    let assertion_candidate = source.contains("assert") || source.contains("expect");
1275    let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
1276    if !assertion_candidate && !capability_candidate {
1277        return Ok(NodeAssertionInstrumentation {
1278            code: source.into(),
1279            assertions: 0,
1280            capability_imports: 0,
1281            statements: 0,
1282        });
1283    }
1284    let source_type = SourceType::from_path(Path::new(file))
1285        .map_err(|error| CandidateError::UnknownSourceType(error.to_string()))?;
1286    let allocator = Allocator::default();
1287    let mut parsed = Parser::new(&allocator, source, source_type).parse();
1288    if !parsed.errors.is_empty() {
1289        return Err(CandidateError::Parse(
1290            parsed
1291                .errors
1292                .into_iter()
1293                .map(|error| error.to_string())
1294                .collect(),
1295        ));
1296    }
1297    let mut assertions = 0;
1298    let mut statements = 0;
1299    if assertion_candidate {
1300        let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
1301        let bindings =
1302            node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
1303        let mut collector = NodeAssertionSiteCollector {
1304            source,
1305            file,
1306            bindings: &bindings,
1307            scoping: semantic.scoping(),
1308            sites: HashMap::new(),
1309        };
1310        collector.visit_program(&parsed.program);
1311        assertions = collector.sites.len();
1312        if assertions > 0 {
1313            NodeAssertionTransformer {
1314                ast: AstBuilder::new(&allocator),
1315                sites: collector.sites,
1316            }
1317            .visit_program(&mut parsed.program);
1318            // A module with assertions is a test module: mark every statement inside its
1319            // functions so production hits recorded while it runs carry the statement's position
1320            // (the operand of an assertion is usually computed by an earlier statement).
1321            let mut markers = TestStatementTransformer {
1322                ast: AstBuilder::new(&allocator),
1323                source,
1324                file,
1325                depth: 0,
1326                statements: 0,
1327            };
1328            markers.visit_program(&mut parsed.program);
1329            statements = markers.statements;
1330        }
1331    }
1332    let capability_imports = capability_wrapper.map_or(0, |wrapper| {
1333        transform_capability_imports(&allocator, &mut parsed.program, source, wrapper)
1334    });
1335    if assertions == 0 && capability_imports == 0 {
1336        return Ok(NodeAssertionInstrumentation {
1337            code: source.into(),
1338            assertions: 0,
1339            capability_imports: 0,
1340            statements: 0,
1341        });
1342    }
1343    let module_assertion_runtime = (assertions > 0 && parsed.program.source_type.is_module())
1344        .then_some(assertion_runtime)
1345        .flatten();
1346    let (mut code, map) = generate_candidate(&parsed.program, file)?;
1347    // Append instead of prepend so every generated source-map location for
1348    // user code remains unchanged. Import declarations are instantiated before
1349    // module evaluation regardless of their textual position.
1350    if let Some(runtime) = module_assertion_runtime {
1351        code.push_str("\nimport ");
1352        code.push_str(
1353            &serde_json::to_string(runtime)
1354                .expect("a JavaScript module specifier always serializes as a string"),
1355        );
1356        code.push_str(";\n");
1357    }
1358    let mut map = map.expect("assertion source maps are enabled");
1359    // An inline map is resolved relative to the transformed file itself. The
1360    // code generator receives a project-relative path for manifest identity,
1361    // but retaining that full path here would resolve `tests/a.js` from
1362    // `tests/a.js` as `tests/tests/a.js`. Assertion-only transforms replace
1363    // the file in place, so its basename is the exact source-map reference.
1364    map["sources"] = serde_json::json!([Path::new(file)
1365        .file_name()
1366        .and_then(|name| name.to_str())
1367        .unwrap_or(file)]);
1368    let map = serde_json::to_vec(&map).expect("source-map values always serialize");
1369    code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
1370    BASE64_STANDARD.encode_string(map, &mut code);
1371    code.push('\n');
1372    Ok(NodeAssertionInstrumentation {
1373        code,
1374        assertions,
1375        capability_imports,
1376        statements,
1377    })
1378}
1379
1380/// Inserts `globalThis.__SUPERCOV_DIRECT_RUNTIME__.testStatement("<file>:<line>:<column>")`
1381/// before every statement that lives inside a function of a test module. The runtime keeps
1382/// the last position and stamps it on every hit and decision event recorded meanwhile, so
1383/// evidence can be attributed to the test statement that produced an assertion's operand.
1384/// Module-level statements run at import time and are left alone.
1385struct TestStatementTransformer<'a, 's> {
1386    ast: AstBuilder<'a>,
1387    source: &'s str,
1388    file: &'s str,
1389    depth: usize,
1390    statements: usize,
1391}
1392
1393impl<'a> TestStatementTransformer<'a, '_> {
1394    fn marker(&self, statement: &Statement<'a>) -> Statement<'a> {
1395        let (line, column) = line_and_utf16_column(self.source, statement.span().start as usize);
1396        let runtime = Expression::StaticMemberExpression(
1397            self.ast.alloc_static_member_expression(
1398                Span::default(),
1399                self.ast
1400                    .expression_identifier(Span::default(), self.ast.ident("globalThis")),
1401                self.ast.identifier_name(
1402                    Span::default(),
1403                    self.ast.ident("__SUPERCOV_DIRECT_RUNTIME__"),
1404                ),
1405                false,
1406            ),
1407        );
1408        let helper = Expression::StaticMemberExpression(
1409            self.ast.alloc_static_member_expression(
1410                Span::default(),
1411                runtime,
1412                self.ast
1413                    .identifier_name(Span::default(), self.ast.ident("testStatement")),
1414                false,
1415            ),
1416        );
1417        let position = format!("{}:{line}:{column}", self.file);
1418        self.ast.statement_expression(
1419            Span::default(),
1420            self.ast.expression_call(
1421                Span::default(),
1422                helper,
1423                NONE,
1424                self.ast
1425                    .vec1(Argument::from(self.ast.expression_string_literal(
1426                        Span::default(),
1427                        self.ast.str(&position),
1428                        None,
1429                    ))),
1430                false,
1431            ),
1432        )
1433    }
1434}
1435
1436impl<'a> VisitMut<'a> for TestStatementTransformer<'a, '_> {
1437    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
1438        if self.depth == 0 {
1439            for statement in statements.iter_mut() {
1440                self.visit_statement(statement);
1441            }
1442            return;
1443        }
1444        let original = statements.take_in(self.ast.allocator);
1445        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
1446        for mut statement in original {
1447            let marker = self.marker(&statement);
1448            self.visit_statement(&mut statement);
1449            instrumented.push(marker);
1450            instrumented.push(statement);
1451            self.statements += 1;
1452        }
1453        *statements = instrumented;
1454    }
1455
1456    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
1457        self.depth += 1;
1458        walk_mut::walk_function(self, function, flags);
1459        self.depth -= 1;
1460    }
1461
1462    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
1463        if function.expression {
1464            // `() => ({ ... })`: the body is one expression statement standing for the return
1465            // value; a marker before it would turn the arrow into a block body returning
1466            // undefined (every `vi.mock` factory is written this way). Nested block-bodied
1467            // functions inside the expression are still marked.
1468            let saved = self.depth;
1469            self.depth = 0;
1470            walk_mut::walk_arrow_function_expression(self, function);
1471            self.depth = saved;
1472            return;
1473        }
1474        self.depth += 1;
1475        walk_mut::walk_arrow_function_expression(self, function);
1476        self.depth -= 1;
1477    }
1478}
1479
1480type SpanKey = (u32, u32);
1481
1482#[derive(Default)]
1483struct SafetyAnalysis {
1484    source_sensitive_functions: HashSet<SpanKey>,
1485    with_statements: HashSet<SpanKey>,
1486    semantic_limitations: Vec<CandidateLimitation>,
1487    dynamic_limitations: Vec<CandidateLimitation>,
1488}
1489
1490struct SafetyScanner<'s> {
1491    source: &'s str,
1492    file: &'s str,
1493    source_sensitive_functions: HashSet<SpanKey>,
1494    with_statements: HashSet<SpanKey>,
1495    function_limitations: Vec<CandidateLimitation>,
1496    with_limitations: Vec<CandidateLimitation>,
1497    dynamic_limitations: Vec<CandidateLimitation>,
1498    unsafe_function_depth: usize,
1499    with_depth: usize,
1500}
1501
1502#[derive(Clone, Copy, PartialEq, Eq)]
1503enum PointPass {
1504    Statements,
1505    Functions,
1506}
1507
1508struct PointCollector<'s> {
1509    source: &'s str,
1510    file: &'s str,
1511    pass: PointPass,
1512    points: Vec<CandidatePoint>,
1513    statement_targets: HashMap<SpanKey, Vec<String>>,
1514    function_targets: HashMap<SpanKey, String>,
1515    source_sensitive_functions: &'s HashSet<SpanKey>,
1516    unsafe_function_depth: usize,
1517    with_depth: usize,
1518    ambient_depth: usize,
1519}
1520
1521#[derive(Default)]
1522struct PointAnalysis {
1523    points: Vec<CandidatePoint>,
1524    statement_targets: HashMap<SpanKey, Vec<String>>,
1525    function_targets: HashMap<SpanKey, String>,
1526}
1527
1528#[derive(Clone)]
1529struct PointTarget {
1530    index: usize,
1531}
1532
1533impl PointCollector<'_> {
1534    fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
1535        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
1536        CandidatePoint {
1537            id: stable_id(self.source, self.file, kind, span, ""),
1538            kind: kind.to_string(),
1539            file: self.file.to_string(),
1540            line,
1541            column,
1542            source: source_slice(self.source, span).to_string(),
1543            label,
1544        }
1545    }
1546
1547    fn unsafe_context(&self) -> bool {
1548        self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
1549    }
1550
1551    fn exit_function(&mut self, span: Span) {
1552        if self.source_sensitive_functions.contains(&span_key(span)) {
1553            self.unsafe_function_depth -= 1;
1554        }
1555    }
1556}
1557
1558fn function_label<State>(
1559    own_name: Option<&str>,
1560    context: &TraverseCtx<'_, State>,
1561) -> Option<String> {
1562    if let Some(name) = own_name {
1563        return Some(name.to_string());
1564    }
1565    match context.ancestors().next()? {
1566        Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
1567        Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
1568        Ancestor::VariableDeclaratorInit(parent) => parent
1569            .id()
1570            .get_binding_identifier()
1571            .map(|identifier| identifier.name.to_string()),
1572        _ => None,
1573    }
1574}
1575
1576fn property_label(key: &PropertyKey<'_>) -> Option<String> {
1577    key.static_name()
1578        .map(|name| name.into_owned())
1579        .or_else(|| match key {
1580            PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
1581            _ => None,
1582        })
1583}
1584
1585fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
1586    match context.ancestors().next() {
1587        Some(Ancestor::ObjectPropertyValue(parent))
1588            if *parent.method() || *parent.kind() != PropertyKind::Init =>
1589        {
1590            *parent.span()
1591        }
1592        Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
1593        _ => span,
1594    }
1595}
1596
1597fn type_only_import(statement: &Statement<'_>) -> bool {
1598    let Statement::ImportDeclaration(declaration) = statement else {
1599        return false;
1600    };
1601    if declaration.import_kind == ImportOrExportKind::Type {
1602        return true;
1603    }
1604    let Some(specifiers) = declaration.specifiers.as_ref() else {
1605        // A bare import (`import './side-effect.js'`) executes at runtime.
1606        return false;
1607    };
1608    !specifiers.is_empty()
1609        && specifiers.iter().all(|specifier| {
1610            matches!(
1611                specifier,
1612                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1613                    if specifier.import_kind == ImportOrExportKind::Type
1614            )
1615        })
1616}
1617
1618fn executable_statement(statement: &Statement<'_>) -> bool {
1619    !matches!(
1620        statement,
1621        Statement::BlockStatement(_)
1622            | Statement::EmptyStatement(_)
1623            | Statement::FunctionDeclaration(_)
1624            | Statement::ExportNamedDeclaration(_)
1625            | Statement::ExportDefaultDeclaration(_)
1626    ) && !statement.is_typescript_syntax()
1627        && !type_only_import(statement)
1628}
1629
1630impl<'a> Traverse<'a, ()> for PointCollector<'_> {
1631    fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
1632        let mut ancestors = context.ancestors();
1633        let parent = ancestors.next();
1634        let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
1635            && matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
1636        if self.pass != PointPass::Statements
1637            || self.unsafe_context()
1638            || !executable_statement(node)
1639            || matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
1640            || expression_arrow_body
1641        {
1642            return;
1643        }
1644        let point = self.point(node.span(), "statement", None);
1645        self.statement_targets
1646            .entry(span_key(node.span()))
1647            .or_default()
1648            .push(point.id.clone());
1649        self.points.push(point);
1650    }
1651
1652    fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
1653        if self.pass != PointPass::Statements
1654            || self.unsafe_context()
1655            || node.is_typescript_syntax()
1656            || matches!(node, Declaration::FunctionDeclaration(_))
1657            || !matches!(
1658                context.ancestors().next(),
1659                Some(Ancestor::ExportNamedDeclarationDeclaration(_))
1660                    | Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
1661            )
1662        {
1663            return;
1664        }
1665        let point = self.point(node.span(), "statement", None);
1666        self.statement_targets
1667            .entry(span_key(node.span()))
1668            .or_default()
1669            .push(point.id.clone());
1670        self.points.push(point);
1671    }
1672
1673    fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
1674        // oxc represents `export default class` directly inside
1675        // ExportDefaultDeclarationKind rather than through Declaration, so it
1676        // does not reach enter_declaration. Babel treats the inner class as the
1677        // executable statement obligation (excluding the `export default`
1678        // prefix), which is also the location where the probe belongs.
1679        if self.pass != PointPass::Statements
1680            || self.unsafe_context()
1681            || node.declare
1682            || !matches!(
1683                context.ancestors().next(),
1684                Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
1685            )
1686        {
1687            return;
1688        }
1689        let point = self.point(node.span, "statement", None);
1690        self.statement_targets
1691            .entry(span_key(node.span))
1692            .or_default()
1693            .push(point.id.clone());
1694        self.points.push(point);
1695    }
1696
1697    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
1698        let point_span = function_point_span(node.span, context);
1699        let label = if point_span == node.span {
1700            function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
1701        } else {
1702            None
1703        };
1704        if self
1705            .source_sensitive_functions
1706            .contains(&span_key(node.span))
1707        {
1708            self.unsafe_function_depth += 1;
1709            return;
1710        }
1711        if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
1712            let point = self.point(point_span, "function", label);
1713            self.function_targets
1714                .insert(span_key(node.span), point.id.clone());
1715            self.points.push(point);
1716        }
1717    }
1718
1719    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
1720        self.exit_function(node.span);
1721    }
1722
1723    fn enter_arrow_function_expression(
1724        &mut self,
1725        node: &mut ArrowFunctionExpression<'a>,
1726        context: &mut TraverseCtx<'a, ()>,
1727    ) {
1728        let point_span = function_point_span(node.span, context);
1729        let label = if point_span == node.span {
1730            function_label(None, context)
1731        } else {
1732            None
1733        };
1734        if self
1735            .source_sensitive_functions
1736            .contains(&span_key(node.span))
1737        {
1738            self.unsafe_function_depth += 1;
1739            return;
1740        }
1741        if self.pass == PointPass::Functions && !self.unsafe_context() {
1742            let point = self.point(point_span, "function", label);
1743            self.function_targets
1744                .insert(span_key(node.span), point.id.clone());
1745            self.points.push(point);
1746        }
1747    }
1748
1749    fn exit_arrow_function_expression(
1750        &mut self,
1751        node: &mut ArrowFunctionExpression<'a>,
1752        _context: &mut TraverseCtx<'a, ()>,
1753    ) {
1754        self.exit_function(node.span);
1755    }
1756
1757    fn enter_with_statement(
1758        &mut self,
1759        _node: &mut WithStatement<'a>,
1760        _context: &mut TraverseCtx<'a, ()>,
1761    ) {
1762        self.with_depth += 1;
1763    }
1764
1765    fn exit_with_statement(
1766        &mut self,
1767        _node: &mut WithStatement<'a>,
1768        _context: &mut TraverseCtx<'a, ()>,
1769    ) {
1770        self.with_depth -= 1;
1771    }
1772
1773    fn enter_ts_global_declaration(
1774        &mut self,
1775        _node: &mut TSGlobalDeclaration<'a>,
1776        _context: &mut TraverseCtx<'a, ()>,
1777    ) {
1778        self.ambient_depth += 1;
1779    }
1780
1781    fn exit_ts_global_declaration(
1782        &mut self,
1783        _node: &mut TSGlobalDeclaration<'a>,
1784        _context: &mut TraverseCtx<'a, ()>,
1785    ) {
1786        self.ambient_depth -= 1;
1787    }
1788
1789    fn enter_ts_module_declaration(
1790        &mut self,
1791        node: &mut TSModuleDeclaration<'a>,
1792        _context: &mut TraverseCtx<'a, ()>,
1793    ) {
1794        if node.declare {
1795            self.ambient_depth += 1;
1796        }
1797    }
1798
1799    fn exit_ts_module_declaration(
1800        &mut self,
1801        node: &mut TSModuleDeclaration<'a>,
1802        _context: &mut TraverseCtx<'a, ()>,
1803    ) {
1804        if node.declare {
1805            self.ambient_depth -= 1;
1806        }
1807    }
1808}
1809
1810fn collect_points<'a>(
1811    allocator: &'a Allocator,
1812    program: &mut Program<'a>,
1813    source: &str,
1814    file: &str,
1815    source_sensitive_functions: &HashSet<SpanKey>,
1816) -> PointAnalysis {
1817    let mut analysis = PointAnalysis::default();
1818    for pass in [PointPass::Statements, PointPass::Functions] {
1819        let mut collector = PointCollector {
1820            source,
1821            file,
1822            pass,
1823            points: Vec::new(),
1824            statement_targets: HashMap::new(),
1825            function_targets: HashMap::new(),
1826            source_sensitive_functions,
1827            unsafe_function_depth: 0,
1828            with_depth: 0,
1829            ambient_depth: 0,
1830        };
1831        traverse_mut(&mut collector, allocator, program, Default::default(), ());
1832        analysis.points.extend(collector.points);
1833        analysis
1834            .statement_targets
1835            .extend(collector.statement_targets);
1836        analysis.function_targets.extend(collector.function_targets);
1837    }
1838    analysis
1839}
1840
1841impl<'s> SafetyScanner<'s> {
1842    fn new(source: &'s str, file: &'s str) -> Self {
1843        Self {
1844            source,
1845            file,
1846            source_sensitive_functions: HashSet::new(),
1847            with_statements: HashSet::new(),
1848            function_limitations: Vec::new(),
1849            with_limitations: Vec::new(),
1850            dynamic_limitations: Vec::new(),
1851            unsafe_function_depth: 0,
1852            with_depth: 0,
1853        }
1854    }
1855
1856    fn limitation(
1857        &self,
1858        span: Span,
1859        kind: &str,
1860        suffix: &str,
1861        reason: &str,
1862    ) -> CandidateLimitation {
1863        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
1864        CandidateLimitation {
1865            id: stable_id(self.source, self.file, kind, span, suffix),
1866            kind: kind.to_string(),
1867            file: self.file.to_string(),
1868            line,
1869            column,
1870            source: source_slice(self.source, span).to_string(),
1871            reason: reason.to_string(),
1872        }
1873    }
1874
1875    fn enter_source_sensitive_function<State>(
1876        &mut self,
1877        span: Span,
1878        context: &TraverseCtx<'_, State>,
1879    ) {
1880        // A function handed to a compile-time style macro never runs: the
1881        // bundler's plugin evaluates it while building and replaces the whole
1882        // call. Probing its body has nothing to measure and breaks the build
1883        // (StyleX rejects a block-bodied dynamic style). Leave it as source,
1884        // with no limitation: there is no runtime behavior to lose.
1885        if compile_time_macro_argument(context) {
1886            self.source_sensitive_functions.insert(span_key(span));
1887            self.unsafe_function_depth += 1;
1888            return;
1889        }
1890        let sensitive = observes_function_source(span, context);
1891        if sensitive {
1892            self.source_sensitive_functions.insert(span_key(span));
1893            let limitation = self.limitation(
1894                span,
1895                "semantic-safety",
1896                "function-source",
1897                "function body is left uninstrumented because this expression observes or coerces Function source text",
1898            );
1899            self.function_limitations.push(limitation);
1900            self.unsafe_function_depth += 1;
1901        }
1902    }
1903
1904    fn exit_source_sensitive_function(&mut self, span: Span) {
1905        if self.source_sensitive_functions.contains(&span_key(span)) {
1906            self.unsafe_function_depth -= 1;
1907        }
1908    }
1909
1910    fn is_unsafe_context(&self) -> bool {
1911        self.unsafe_function_depth > 0 || self.with_depth > 0
1912    }
1913}
1914
1915impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
1916    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
1917        self.enter_source_sensitive_function(node.span, context);
1918    }
1919
1920    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
1921        self.exit_source_sensitive_function(node.span);
1922    }
1923
1924    fn enter_arrow_function_expression(
1925        &mut self,
1926        node: &mut ArrowFunctionExpression<'a>,
1927        context: &mut TraverseCtx<'a, ()>,
1928    ) {
1929        self.enter_source_sensitive_function(node.span, context);
1930    }
1931
1932    fn exit_arrow_function_expression(
1933        &mut self,
1934        node: &mut ArrowFunctionExpression<'a>,
1935        _context: &mut TraverseCtx<'a, ()>,
1936    ) {
1937        self.exit_source_sensitive_function(node.span);
1938    }
1939
1940    fn enter_with_statement(
1941        &mut self,
1942        node: &mut WithStatement<'a>,
1943        _context: &mut TraverseCtx<'a, ()>,
1944    ) {
1945        self.with_statements.insert(span_key(node.span));
1946        let limitation = self.limitation(
1947            node.span,
1948            "semantic-safety",
1949            "with-environment",
1950            "with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
1951        );
1952        self.with_limitations.push(limitation);
1953        self.with_depth += 1;
1954    }
1955
1956    fn exit_with_statement(
1957        &mut self,
1958        _node: &mut WithStatement<'a>,
1959        _context: &mut TraverseCtx<'a, ()>,
1960    ) {
1961        self.with_depth -= 1;
1962    }
1963
1964    fn enter_call_expression(
1965        &mut self,
1966        node: &mut CallExpression<'a>,
1967        _context: &mut TraverseCtx<'a, ()>,
1968    ) {
1969        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
1970            return;
1971        }
1972        let limitation = self.limitation(
1973            node.span,
1974            "dynamic-code",
1975            "eval",
1976            "eval-generated source has no stable pre-run coverage denominator",
1977        );
1978        self.dynamic_limitations.push(limitation);
1979    }
1980
1981    fn enter_new_expression(
1982        &mut self,
1983        node: &mut NewExpression<'a>,
1984        _context: &mut TraverseCtx<'a, ()>,
1985    ) {
1986        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
1987            return;
1988        }
1989        let limitation = self.limitation(
1990            node.span,
1991            "dynamic-code",
1992            "Function",
1993            "Function-generated source has no stable pre-run coverage denominator",
1994        );
1995        self.dynamic_limitations.push(limitation);
1996    }
1997}
1998
1999fn analyze_safety<'a>(
2000    allocator: &'a Allocator,
2001    program: &mut Program<'a>,
2002    source: &str,
2003    file: &str,
2004) -> SafetyAnalysis {
2005    // oxc_traverse uses resolved lexical scope IDs while walking ancestry.
2006    // Building semantics here initializes those IDs without changing the AST.
2007    SemanticBuilder::new().build(program);
2008    let mut scanner = SafetyScanner::new(source, file);
2009    traverse_mut(&mut scanner, allocator, program, Default::default(), ());
2010    let mut semantic_limitations = scanner.function_limitations;
2011    semantic_limitations.extend(scanner.with_limitations);
2012    SafetyAnalysis {
2013        source_sensitive_functions: scanner.source_sensitive_functions,
2014        with_statements: scanner.with_statements,
2015        semantic_limitations,
2016        dynamic_limitations: scanner.dynamic_limitations,
2017    }
2018}
2019
2020fn span_key(span: Span) -> SpanKey {
2021    (span.start, span.end)
2022}
2023
2024fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
2025    matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
2026}
2027
2028fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
2029    match pattern {
2030        BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
2031        _ => None,
2032    }
2033}
2034
2035fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
2036    match expression {
2037        Expression::ArrowFunctionExpression(_) => true,
2038        Expression::FunctionExpression(function) => function.id.is_none(),
2039        Expression::ClassExpression(class) => class.id.is_none(),
2040        Expression::ParenthesizedExpression(expression) => {
2041            expression_is_anonymous_definition(&expression.expression)
2042        }
2043        Expression::TSAsExpression(expression) => {
2044            expression_is_anonymous_definition(&expression.expression)
2045        }
2046        Expression::TSSatisfiesExpression(expression) => {
2047            expression_is_anonymous_definition(&expression.expression)
2048        }
2049        Expression::TSTypeAssertion(expression) => {
2050            expression_is_anonymous_definition(&expression.expression)
2051        }
2052        Expression::TSNonNullExpression(expression) => {
2053            expression_is_anonymous_definition(&expression.expression)
2054        }
2055        _ => false,
2056    }
2057}
2058
2059struct AssignmentNameSafetyTransformer<'a> {
2060    ast: AstBuilder<'a>,
2061    parenthesized_assignment_value: String,
2062}
2063
2064impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
2065    fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
2066        walk_mut::walk_assignment_expression(self, assignment);
2067        let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
2068            return;
2069        };
2070        if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
2071            || assignment.span.start == identifier.span.start
2072            || !expression_is_anonymous_definition(&assignment.right)
2073        {
2074            return;
2075        }
2076        let right = assignment.right.take_in(self.ast.allocator);
2077        assignment.right = self.ast.expression_call(
2078            Span::default(),
2079            self.ast.expression_identifier(
2080                Span::default(),
2081                self.ast.ident(&self.parenthesized_assignment_value),
2082            ),
2083            NONE,
2084            self.ast.vec_from_array([
2085                Argument::from(right),
2086                Argument::from(self.ast.expression_string_literal(
2087                    Span::default(),
2088                    identifier.name,
2089                    None,
2090                )),
2091            ]),
2092            false,
2093        );
2094    }
2095}
2096
2097/// Compile-time style macros whose arguments the bundler consumes at build
2098/// time. Only the namespaced form (`stylex.create(...)`) is recognized: it is
2099/// how the StyleX documentation and its Vite/Babel plugins expect the API to
2100/// be used, and it needs no binding resolution here.
2101const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
2102    "create",
2103    "createTheme",
2104    "defineConsts",
2105    "defineVars",
2106    "firstThatWorks",
2107    "keyframes",
2108    "positionTry",
2109    "viewTransitionClass",
2110];
2111
2112fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
2113    context.ancestors().any(|ancestor| {
2114        let Ancestor::CallExpressionArguments(parent) = ancestor else {
2115            return false;
2116        };
2117        let Expression::StaticMemberExpression(member) = parent.callee() else {
2118            return false;
2119        };
2120        let Expression::Identifier(object) = &member.object else {
2121            return false;
2122        };
2123        object.name == "stylex"
2124            && COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
2125    })
2126}
2127
2128fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
2129    let mut child_end = span.end;
2130    for ancestor in context.ancestors() {
2131        match ancestor {
2132            Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
2133            Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
2134            Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
2135            Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
2136            Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
2137            Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
2138            Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
2139            Ancestor::LogicalExpressionLeft(parent) => {
2140                child_end = parent.span().end;
2141            }
2142            Ancestor::LogicalExpressionRight(parent) => {
2143                child_end = parent.span().end;
2144            }
2145            Ancestor::SequenceExpressionExpressions(parent) => {
2146                if child_end != parent.span().end {
2147                    return false;
2148                }
2149                child_end = parent.span().end;
2150            }
2151            Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
2152            Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
2153            Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
2154            Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
2155            Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
2156            Ancestor::ComputedMemberExpressionExpression(_) => return true,
2157            Ancestor::StaticMemberExpressionObject(parent) => {
2158                return parent.property().name == "toString";
2159            }
2160            Ancestor::CallExpressionArguments(parent) => {
2161                return expression_is_identifier(parent.callee(), "String");
2162            }
2163            Ancestor::BinaryExpressionLeft(parent) => {
2164                return matches!(
2165                    parent.operator(),
2166                    BinaryOperator::Addition
2167                        | BinaryOperator::LessThan
2168                        | BinaryOperator::LessEqualThan
2169                        | BinaryOperator::GreaterThan
2170                        | BinaryOperator::GreaterEqualThan
2171                );
2172            }
2173            Ancestor::BinaryExpressionRight(parent) => {
2174                return matches!(
2175                    parent.operator(),
2176                    BinaryOperator::Addition
2177                        | BinaryOperator::LessThan
2178                        | BinaryOperator::LessEqualThan
2179                        | BinaryOperator::GreaterThan
2180                        | BinaryOperator::GreaterEqualThan
2181                );
2182            }
2183            _ => return false,
2184        }
2185    }
2186    false
2187}
2188
2189pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
2190    let source_type = SourceType::from_path(Path::new(file))
2191        .map_err(|error| CandidateError::UnknownSourceType(error.to_string()))?;
2192    let allocator = Allocator::default();
2193    let mut parsed = Parser::new(&allocator, source, source_type).parse();
2194    if !parsed.errors.is_empty() {
2195        return Err(CandidateError::Parse(
2196            parsed
2197                .errors
2198                .into_iter()
2199                .map(|error| format!("{error:?}"))
2200                .collect(),
2201        ));
2202    }
2203
2204    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
2205    let point_analysis = collect_points(
2206        &allocator,
2207        &mut parsed.program,
2208        source,
2209        file,
2210        &safety.source_sensitive_functions,
2211    );
2212    let mut collector = DecisionCollector {
2213        source,
2214        file,
2215        decisions: Vec::new(),
2216        decision_vector_counts: Vec::new(),
2217        decision_logical_nodes: HashSet::new(),
2218        source_sensitive_functions: &safety.source_sensitive_functions,
2219        with_statements: &safety.with_statements,
2220    };
2221    collector.visit_program(&parsed.program);
2222    let optional_analysis = collect_optional_member_branches(
2223        &allocator,
2224        &mut parsed.program,
2225        source,
2226        file,
2227        &safety.source_sensitive_functions,
2228    );
2229    let call_analysis = collect_optional_call_branches(
2230        &allocator,
2231        &mut parsed.program,
2232        source,
2233        file,
2234        &safety.source_sensitive_functions,
2235    );
2236    let assignment_analysis = collect_logical_assignment_branches(
2237        &allocator,
2238        &mut parsed.program,
2239        source,
2240        file,
2241        &safety.source_sensitive_functions,
2242    );
2243    let default_analysis = collect_default_branches(
2244        &allocator,
2245        &mut parsed.program,
2246        source,
2247        file,
2248        &safety.source_sensitive_functions,
2249    );
2250    let extended_analysis = collect_extended_branches(
2251        &allocator,
2252        &mut parsed.program,
2253        source,
2254        file,
2255        &safety.source_sensitive_functions,
2256    );
2257    let logical_analysis = collect_logical_value_branches(
2258        &allocator,
2259        &mut parsed.program,
2260        source,
2261        file,
2262        &collector.decision_logical_nodes,
2263        &safety.source_sensitive_functions,
2264    );
2265    let switch_analysis = collect_switch_branches(
2266        &allocator,
2267        &mut parsed.program,
2268        source,
2269        file,
2270        &safety.source_sensitive_functions,
2271    );
2272    let mut branches = optional_analysis.branches;
2273    branches.extend(call_analysis.branches);
2274    branches.extend(assignment_analysis.branches);
2275    branches.extend(default_analysis.branches);
2276    branches.extend(extended_analysis.branches);
2277    branches.extend(logical_analysis.branches);
2278    branches.extend(switch_analysis.branches);
2279    let (generated, map) = generate_candidate(&parsed.program, file)?;
2280    Ok(CandidateOutput {
2281        engine: "rust-oxc".to_string(),
2282        complete: false,
2283        supported_surface: "control-decision-manifest-v1".to_string(),
2284        code: generated,
2285        map,
2286        decisions: collector.decisions,
2287        points: point_analysis.points,
2288        branches,
2289        runtime: None,
2290        coverage_limitations: {
2291            let mut limitations = safety.semantic_limitations;
2292            limitations.extend(call_analysis.limitations);
2293            limitations.extend(default_analysis.limitations);
2294            limitations.extend(safety.dynamic_limitations);
2295            limitations
2296        },
2297        limitations: vec![
2298            "candidate emits metadata only; use the private differential transform for probes"
2299                .to_string(),
2300            "coverage points, value branches, and extended branch obligations are not included"
2301                .to_string(),
2302        ],
2303    })
2304}
2305
2306fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
2307    match value {
2308        serde_json::Value::Null => ast.expression_null_literal(Span::default()),
2309        serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
2310        serde_json::Value::Number(value) => ast.expression_numeric_literal(
2311            Span::default(),
2312            value
2313                .as_f64()
2314                .expect("coverage registration numbers must fit JavaScript"),
2315            None,
2316            NumberBase::Decimal,
2317        ),
2318        serde_json::Value::String(value) => {
2319            ast.expression_string_literal(Span::default(), ast.str(value), None)
2320        }
2321        serde_json::Value::Array(values) => ast.expression_array(
2322            Span::default(),
2323            ast.vec_from_iter(
2324                values
2325                    .iter()
2326                    .map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
2327            ),
2328        ),
2329        serde_json::Value::Object(properties) => ast.expression_object(
2330            Span::default(),
2331            ast.vec_from_iter(properties.iter().map(|(key, value)| {
2332                ast.object_property_kind_object_property(
2333                    Span::default(),
2334                    PropertyKind::Init,
2335                    ast.property_key_static_identifier(Span::default(), ast.ident(key)),
2336                    json_expression(ast, value),
2337                    false,
2338                    false,
2339                    false,
2340                )
2341            })),
2342        ),
2343    }
2344}
2345
2346/// Instrument JavaScript with the complete frozen v1 denominator and probe-v2
2347/// runtime ABI. The source-transform contract is complete and independently
2348/// conformance-tested; selection of the Rust engine remains private until the
2349/// Phase 4 CLI/orchestration cutover is complete.
2350pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
2351    instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None)
2352}
2353
2354pub fn instrument_candidate_with_runtime_hooks(
2355    source: &str,
2356    file: &str,
2357    capability_wrapper: &str,
2358) -> Result<CandidateOutput, CandidateError> {
2359    instrument_candidate_with_binding(
2360        source,
2361        file,
2362        RuntimeBinding::ModuleImport,
2363        Some(capability_wrapper),
2364    )
2365}
2366
2367/// Emit code for an isolated source-executing workspace. Unlike the module-
2368/// import form, this has no virtual module dependency: the generated
2369/// Node preload installs the frozen runtime on this global before user modules
2370/// evaluate.
2371pub fn instrument_direct_candidate(
2372    source: &str,
2373    file: &str,
2374) -> Result<CandidateOutput, CandidateError> {
2375    instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None)
2376}
2377
2378pub fn instrument_direct_candidate_with_runtime_hooks(
2379    source: &str,
2380    file: &str,
2381    capability_wrapper: &str,
2382) -> Result<CandidateOutput, CandidateError> {
2383    instrument_candidate_with_binding(
2384        source,
2385        file,
2386        RuntimeBinding::DirectGlobal,
2387        Some(capability_wrapper),
2388    )
2389}
2390
2391fn instrument_candidate_with_binding(
2392    source: &str,
2393    file: &str,
2394    runtime_binding: RuntimeBinding,
2395    capability_wrapper: Option<&str>,
2396) -> Result<CandidateOutput, CandidateError> {
2397    let source_type = SourceType::from_path(Path::new(file))
2398        .map_err(|error| CandidateError::UnknownSourceType(error.to_string()))?;
2399    let allocator = Allocator::default();
2400    let mut parsed = Parser::new(&allocator, source, source_type).parse();
2401    if !parsed.errors.is_empty() {
2402        return Err(CandidateError::Parse(
2403            parsed
2404                .errors
2405                .into_iter()
2406                .map(|error| format!("{error:?}"))
2407                .collect(),
2408        ));
2409    }
2410
2411    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
2412    let point_analysis = collect_points(
2413        &allocator,
2414        &mut parsed.program,
2415        source,
2416        file,
2417        &safety.source_sensitive_functions,
2418    );
2419    let mut collector = DecisionCollector {
2420        source,
2421        file,
2422        decisions: Vec::new(),
2423        decision_vector_counts: Vec::new(),
2424        decision_logical_nodes: HashSet::new(),
2425        source_sensitive_functions: &safety.source_sensitive_functions,
2426        with_statements: &safety.with_statements,
2427    };
2428    collector.visit_program(&parsed.program);
2429    let optional_analysis = collect_optional_member_branches(
2430        &allocator,
2431        &mut parsed.program,
2432        source,
2433        file,
2434        &safety.source_sensitive_functions,
2435    );
2436    let call_analysis = collect_optional_call_branches(
2437        &allocator,
2438        &mut parsed.program,
2439        source,
2440        file,
2441        &safety.source_sensitive_functions,
2442    );
2443    let assignment_analysis = collect_logical_assignment_branches(
2444        &allocator,
2445        &mut parsed.program,
2446        source,
2447        file,
2448        &safety.source_sensitive_functions,
2449    );
2450    let default_analysis = collect_default_branches(
2451        &allocator,
2452        &mut parsed.program,
2453        source,
2454        file,
2455        &safety.source_sensitive_functions,
2456    );
2457    let extended_analysis = collect_extended_branches(
2458        &allocator,
2459        &mut parsed.program,
2460        source,
2461        file,
2462        &safety.source_sensitive_functions,
2463    );
2464    let logical_analysis = collect_logical_value_branches(
2465        &allocator,
2466        &mut parsed.program,
2467        source,
2468        file,
2469        &collector.decision_logical_nodes,
2470        &safety.source_sensitive_functions,
2471    );
2472    let switch_analysis = collect_switch_branches(
2473        &allocator,
2474        &mut parsed.program,
2475        source,
2476        file,
2477        &safety.source_sensitive_functions,
2478    );
2479    let mut branches = optional_analysis.branches;
2480    branches.extend(call_analysis.branches.clone());
2481    branches.extend(assignment_analysis.branches);
2482    branches.extend(default_analysis.branches.clone());
2483    branches.extend(extended_analysis.branches.clone());
2484    branches.extend(logical_analysis.branches);
2485    branches.extend(switch_analysis.branches.clone());
2486
2487    let mut names = CandidateNames::new(source);
2488    let mcdc_begin = names.allocate("__supercovMcdcBegin");
2489    let mcdc_condition = names.allocate("__supercovMcdcCondition");
2490    let mcdc_end = names.allocate("__supercovMcdcEnd");
2491    let coverage_hit = names.allocate("__supercovCoverageHit");
2492    let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
2493    let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
2494    let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
2495    let probe_file_v2 = names.allocate("__supercovProbeFileV2");
2496    let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
2497    let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
2498    let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
2499    let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
2500    let selection_begin = names.allocate("__supercovSelectionBegin");
2501    let selection_right = names.allocate("__supercovSelectionRight");
2502    let selection_end = names.allocate("__supercovSelectionEnd");
2503    let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
2504    let with_request_phase = names.allocate("__supercovWithRequestPhase");
2505    let optional_select = names.allocate("__supercovOptionalSelect");
2506    let optional_call_begin = names.allocate("__supercovOptionalCallBegin");
2507    let optional_call_reached = names.allocate("__supercovOptionalCallReached");
2508    let optional_call_continued = names.allocate("__supercovOptionalCallContinued");
2509    let optional_call_end = names.allocate("__supercovOptionalCallEnd");
2510    let default_selected = names.allocate("__supercovDefaultSelected");
2511    let default_entered = names.allocate("__supercovDefaultEntered");
2512    let try_begin = names.allocate("__supercovTryBegin");
2513    let try_catch = names.allocate("__supercovTryCatch");
2514    let try_end = names.allocate("__supercovTryEnd");
2515    let loop_begin = names.allocate("__supercovLoopBegin");
2516    let loop_entered = names.allocate("__supercovLoopEntered");
2517    let loop_end = names.allocate("__supercovLoopEnd");
2518    let ast = AstBuilder::new(&allocator);
2519    let mut assignment_name_safety = AssignmentNameSafetyTransformer {
2520        ast,
2521        parenthesized_assignment_value: parenthesized_assignment_value.clone(),
2522    };
2523    assignment_name_safety.visit_program(&mut parsed.program);
2524    let point_indices = point_analysis
2525        .points
2526        .iter()
2527        .enumerate()
2528        .map(|(index, point)| (point.id.clone(), index))
2529        .collect::<HashMap<_, _>>();
2530    let statement_targets = point_analysis
2531        .statement_targets
2532        .into_iter()
2533        .map(|(span, ids)| {
2534            (
2535                span,
2536                ids.into_iter()
2537                    .map(|id| PointTarget {
2538                        index: *point_indices
2539                            .get(&id)
2540                            .expect("statement point must have a global index"),
2541                    })
2542                    .collect(),
2543            )
2544        })
2545        .collect();
2546    let function_targets = point_analysis
2547        .function_targets
2548        .into_iter()
2549        .map(|(span, id)| {
2550            (
2551                span,
2552                PointTarget {
2553                    index: *point_indices
2554                        .get(&id)
2555                        .expect("function point must have a global index"),
2556                },
2557            )
2558        })
2559        .collect();
2560    let mut statement_transformer = StatementProbeTransformer {
2561        ast,
2562        coverage_hit_v2: coverage_hit_v2.clone(),
2563        probe_file_v2: probe_file_v2.clone(),
2564        targets: statement_targets,
2565        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2566        with_statements: safety.with_statements.clone(),
2567    };
2568    statement_transformer.visit_program(&mut parsed.program);
2569    let mut function_transformer = FunctionProbeTransformer {
2570        ast,
2571        coverage_hit_v2: coverage_hit_v2.clone(),
2572        probe_file_v2: probe_file_v2.clone(),
2573        targets: function_targets,
2574        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2575    };
2576    function_transformer.visit_program(&mut parsed.program);
2577    let mut optional_transformer = OptionalMemberTransformer {
2578        ast,
2579        optional_select: optional_select.clone(),
2580        targets: optional_analysis.targets,
2581        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2582        with_statements: safety.with_statements.clone(),
2583    };
2584    optional_transformer.visit_program(&mut parsed.program);
2585    let mut call_transformer = OptionalCallTransformer::new(
2586        ast,
2587        source,
2588        optional_call_begin.clone(),
2589        optional_call_reached.clone(),
2590        optional_call_continued.clone(),
2591        optional_call_end.clone(),
2592        call_analysis.sites,
2593        call_analysis.roots,
2594        safety.source_sensitive_functions.clone(),
2595        safety.with_statements.clone(),
2596    );
2597    call_transformer.visit_program(&mut parsed.program);
2598    let mut default_transformer = DefaultTransformer {
2599        ast,
2600        default_selected: default_selected.clone(),
2601        default_entered: default_entered.clone(),
2602        parameter_targets: default_analysis.parameter_targets,
2603        binding_targets: default_analysis.binding_targets,
2604        function_entries: Vec::new(),
2605        declaration_entries: HashMap::new(),
2606        active_declaration: Vec::new(),
2607        parameter_pattern_depth: 0,
2608        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2609        with_statements: safety.with_statements.clone(),
2610    };
2611    default_transformer.visit_program(&mut parsed.program);
2612    let mut extended_transformer = ExtendedTransformer {
2613        ast,
2614        try_begin: try_begin.clone(),
2615        try_catch: try_catch.clone(),
2616        try_end: try_end.clone(),
2617        loop_begin: loop_begin.clone(),
2618        loop_entered: loop_entered.clone(),
2619        loop_end: loop_end.clone(),
2620        try_targets: extended_analysis.try_targets,
2621        loop_targets: extended_analysis.loop_targets,
2622        names: CandidateNames::new(source),
2623        scope_declarations: Vec::new(),
2624        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2625        with_statements: safety.with_statements.clone(),
2626    };
2627    extended_transformer.visit_program(&mut parsed.program);
2628    let mut transformer = ControlProbeV2Transformer {
2629        ast,
2630        decisions: &collector.decisions,
2631        mcdc_begin: mcdc_begin.clone(),
2632        mcdc_condition: mcdc_condition.clone(),
2633        mcdc_end: mcdc_end.clone(),
2634        mcdc_end_v2: mcdc_end_v2.clone(),
2635        probe_file_v2: probe_file_v2.clone(),
2636        names,
2637        scope_declarations: Vec::new(),
2638        decision_index: 0,
2639        parameter_depth: 0,
2640        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2641        with_statements: safety.with_statements.clone(),
2642    };
2643    transformer.visit_program(&mut parsed.program);
2644    let mut logical_transformer = LogicalValueTransformer {
2645        ast,
2646        selection_begin: selection_begin.clone(),
2647        selection_right: selection_right.clone(),
2648        selection_end: selection_end.clone(),
2649        names: CandidateNames::new(source),
2650        scope_declarations: Vec::new(),
2651        logical_targets: logical_analysis.logical_targets,
2652        assignment_targets: assignment_analysis.targets,
2653        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2654        with_statements: safety.with_statements.clone(),
2655    };
2656    logical_transformer.visit_program(&mut parsed.program);
2657    let mut switch_transformer = SwitchTransformer {
2658        ast,
2659        coverage_hit: coverage_hit.clone(),
2660        targets: switch_analysis.targets,
2661        names: CandidateNames::new(source),
2662        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2663        with_statements: safety.with_statements.clone(),
2664    };
2665    switch_transformer.visit_program(&mut parsed.program);
2666    let mut route_transformer = RouteRequestPhaseTransformer {
2667        ast,
2668        file,
2669        with_request_phase: with_request_phase.clone(),
2670        used: false,
2671        names: CandidateNames::new(source),
2672    };
2673    route_transformer.transform_program(&mut parsed.program);
2674    let mut request_transformer = RequestPhaseTransformer {
2675        ast,
2676        with_request_phase: with_request_phase.clone(),
2677        used: route_transformer.used,
2678        source_sensitive_functions: safety.source_sensitive_functions.clone(),
2679        with_statements: safety.with_statements.clone(),
2680    };
2681    request_transformer.visit_program(&mut parsed.program);
2682    let uses_request_phase = request_transformer.used;
2683
2684    let registration = serde_json::json!({
2685        "decisions": &collector.decisions,
2686        "pointIds": point_analysis.points.iter().map(|point| &point.id).collect::<Vec<_>>(),
2687        "decisionVectorCounts": &collector.decision_vector_counts,
2688    });
2689    let registration_call = ast.expression_call(
2690        Span::default(),
2691        ast.expression_identifier(Span::default(), ast.ident(&register_probe_v2)),
2692        NONE,
2693        ast.vec1(Argument::from(json_expression(ast, &registration))),
2694        false,
2695    );
2696    parsed.program.body.insert(
2697        0,
2698        Statement::VariableDeclaration(ast.alloc_variable_declaration(
2699            Span::default(),
2700            VariableDeclarationKind::Const,
2701            ast.vec1(ast.variable_declarator(
2702                Span::default(),
2703                VariableDeclarationKind::Const,
2704                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
2705                NONE,
2706                Some(registration_call),
2707                false,
2708            )),
2709            false,
2710        )),
2711    );
2712    let mut runtime_imports = vec![
2713        ("mcdcBegin", &mcdc_begin),
2714        ("mcdcCondition", &mcdc_condition),
2715        ("mcdcEnd", &mcdc_end),
2716        ("coverageHit", &coverage_hit),
2717        ("registerProbeV2", &register_probe_v2),
2718        ("mcdcEndV2", &mcdc_end_v2),
2719        ("coverageHitV2", &coverage_hit_v2),
2720        ("selectionBegin", &selection_begin),
2721        ("selectionRight", &selection_right),
2722        ("selectionEnd", &selection_end),
2723        (
2724            "parenthesizedAssignmentValue",
2725            &parenthesized_assignment_value,
2726        ),
2727        ("optionalSelect", &optional_select),
2728        ("optionalCallBegin", &optional_call_begin),
2729        ("optionalCallReached", &optional_call_reached),
2730        ("optionalCallContinued", &optional_call_continued),
2731        ("optionalCallEnd", &optional_call_end),
2732        ("defaultSelected", &default_selected),
2733        ("defaultEntered", &default_entered),
2734        ("tryBegin", &try_begin),
2735        ("tryCatch", &try_catch),
2736        ("tryEnd", &try_end),
2737        ("loopBegin", &loop_begin),
2738        ("loopEntered", &loop_entered),
2739        ("loopEnd", &loop_end),
2740    ];
2741    if uses_request_phase {
2742        runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
2743    }
2744    if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
2745        let declarators =
2746            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
2747                let global_runtime =
2748                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
2749                        Span::default(),
2750                        ast.expression_identifier(Span::default(), ast.ident("globalThis")),
2751                        ast.identifier_name(
2752                            Span::default(),
2753                            ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
2754                                "__SUPERCOV_DIRECT_RUNTIME__"
2755                            } else {
2756                                "__supercovRuntime"
2757                            }),
2758                        ),
2759                        false,
2760                    ));
2761                let runtime_helper =
2762                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
2763                        Span::default(),
2764                        global_runtime,
2765                        ast.identifier_name(Span::default(), ast.ident(imported)),
2766                        false,
2767                    ));
2768                ast.variable_declarator(
2769                    Span::default(),
2770                    VariableDeclarationKind::Const,
2771                    ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
2772                    NONE,
2773                    Some(runtime_helper),
2774                    false,
2775                )
2776            }));
2777        parsed.program.body.insert(
2778            0,
2779            Statement::VariableDeclaration(ast.alloc_variable_declaration(
2780                Span::default(),
2781                VariableDeclarationKind::Const,
2782                declarators,
2783                false,
2784            )),
2785        );
2786    } else {
2787        let import_specifiers =
2788            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
2789                ast.import_declaration_specifier_import_specifier(
2790                    Span::default(),
2791                    ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
2792                    ast.binding_identifier(Span::default(), ast.ident(local)),
2793                    oxc_ast::ast::ImportOrExportKind::Value,
2794                )
2795            }));
2796        parsed.program.body.insert(
2797            0,
2798            Statement::ImportDeclaration(ast.alloc_import_declaration(
2799                Span::default(),
2800                Some(import_specifiers),
2801                ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
2802                None,
2803                NONE,
2804                oxc_ast::ast::ImportOrExportKind::Value,
2805            )),
2806        );
2807    }
2808
2809    if let Some(wrapper) = capability_wrapper {
2810        transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
2811    }
2812    let limitations = Vec::new();
2813    let (code, map) = generate_candidate(&parsed.program, file)?;
2814    Ok(CandidateOutput {
2815        engine: "rust-oxc".to_string(),
2816        complete: true,
2817        supported_surface: "complete-js-instrumenter-v1".to_string(),
2818        code,
2819        map,
2820        decisions: collector.decisions,
2821        points: point_analysis.points,
2822        branches,
2823        runtime: Some(CandidateRuntime {
2824            coverage_hit,
2825            mcdc_begin,
2826            mcdc_condition,
2827            mcdc_end,
2828            register_probe_v2,
2829            mcdc_end_v2,
2830            coverage_hit_v2,
2831            probe_file_v2,
2832            selection_begin,
2833            selection_right,
2834            selection_end,
2835            parenthesized_assignment_value,
2836            with_request_phase,
2837            optional_select,
2838            optional_call_begin,
2839            optional_call_reached,
2840            optional_call_continued,
2841            optional_call_end,
2842            default_selected,
2843            default_entered,
2844            try_begin,
2845            try_catch,
2846            try_end,
2847            loop_begin,
2848            loop_entered,
2849            loop_end,
2850        }),
2851        coverage_limitations: {
2852            let mut limitations = safety.semantic_limitations;
2853            limitations.extend(call_analysis.limitations);
2854            limitations.extend(default_analysis.limitations);
2855            limitations.extend(safety.dynamic_limitations);
2856            limitations
2857        },
2858        limitations,
2859    })
2860}
2861
2862struct StatementProbeTransformer<'a> {
2863    ast: AstBuilder<'a>,
2864    coverage_hit_v2: String,
2865    probe_file_v2: String,
2866    targets: HashMap<SpanKey, Vec<PointTarget>>,
2867    source_sensitive_functions: HashSet<SpanKey>,
2868    with_statements: HashSet<SpanKey>,
2869}
2870
2871impl<'a> StatementProbeTransformer<'a> {
2872    fn probe(&self, target: &PointTarget) -> Statement<'a> {
2873        self.ast.statement_expression(
2874            Span::default(),
2875            self.ast.expression_call(
2876                Span::default(),
2877                self.ast
2878                    .expression_identifier(Span::default(), self.ast.ident(&self.coverage_hit_v2)),
2879                NONE,
2880                self.ast.vec_from_array([
2881                    Argument::from(self.ast.expression_identifier(
2882                        Span::default(),
2883                        self.ast.ident(&self.probe_file_v2),
2884                    )),
2885                    Argument::from(self.ast.expression_numeric_literal(
2886                        Span::default(),
2887                        target.index as f64,
2888                        None,
2889                        NumberBase::Decimal,
2890                    )),
2891                ]),
2892                false,
2893            ),
2894        )
2895    }
2896
2897    fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
2898        let mut ids = self
2899            .targets
2900            .remove(&span_key(statement.span()))
2901            .unwrap_or_default();
2902        if let Statement::ExportNamedDeclaration(export) = statement
2903            && let Some(declaration) = &export.declaration
2904            && let Some(nested) = self.targets.remove(&span_key(declaration.span()))
2905        {
2906            ids.extend(nested);
2907        }
2908        if let Statement::ExportDefaultDeclaration(export) = statement
2909            && let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
2910            && let Some(nested) = self.targets.remove(&span_key(class.span))
2911        {
2912            ids.extend(nested);
2913        }
2914        ids
2915    }
2916
2917    fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
2918        let ids = self.take_statement_ids(statement);
2919        if ids.is_empty() {
2920            return;
2921        }
2922        let original = statement.take_in(self.ast.allocator);
2923        let mut body = self.ast.vec_with_capacity(ids.len() + 1);
2924        body.extend(ids.iter().map(|target| self.probe(target)));
2925        body.push(original);
2926        *statement = self.ast.statement_block(Span::default(), body);
2927    }
2928}
2929
2930impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
2931    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
2932        let original = statements.take_in(self.ast.allocator);
2933        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
2934        for mut statement in original {
2935            let ids = self.take_statement_ids(&statement);
2936            self.visit_statement(&mut statement);
2937            instrumented.extend(ids.iter().map(|target| self.probe(target)));
2938            instrumented.push(statement);
2939        }
2940        *statements = instrumented;
2941    }
2942
2943    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
2944        if self
2945            .source_sensitive_functions
2946            .contains(&span_key(function.span))
2947        {
2948            return;
2949        }
2950        walk_mut::walk_function(self, function, flags);
2951    }
2952
2953    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
2954        if self
2955            .source_sensitive_functions
2956            .contains(&span_key(function.span))
2957        {
2958            return;
2959        }
2960        walk_mut::walk_arrow_function_expression(self, function);
2961    }
2962
2963    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
2964        if self.with_statements.contains(&span_key(statement.span)) {
2965            return;
2966        }
2967        walk_mut::walk_with_statement(self, statement);
2968    }
2969
2970    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
2971        self.wrap_bare(&mut statement.consequent);
2972        if let Some(alternate) = &mut statement.alternate {
2973            self.wrap_bare(alternate);
2974        }
2975        walk_mut::walk_if_statement(self, statement);
2976    }
2977
2978    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
2979        self.wrap_bare(&mut statement.body);
2980        walk_mut::walk_while_statement(self, statement);
2981    }
2982
2983    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
2984        self.wrap_bare(&mut statement.body);
2985        walk_mut::walk_do_while_statement(self, statement);
2986    }
2987
2988    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
2989        self.wrap_bare(&mut statement.body);
2990        walk_mut::walk_for_statement(self, statement);
2991    }
2992
2993    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
2994        self.wrap_bare(&mut statement.body);
2995        walk_mut::walk_for_in_statement(self, statement);
2996    }
2997
2998    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
2999        self.wrap_bare(&mut statement.body);
3000        walk_mut::walk_for_of_statement(self, statement);
3001    }
3002}
3003
3004struct FunctionProbeTransformer<'a> {
3005    ast: AstBuilder<'a>,
3006    coverage_hit_v2: String,
3007    probe_file_v2: String,
3008    targets: HashMap<SpanKey, PointTarget>,
3009    source_sensitive_functions: HashSet<SpanKey>,
3010}
3011
3012impl<'a> FunctionProbeTransformer<'a> {
3013    fn probe(&self, target: &PointTarget) -> Statement<'a> {
3014        self.ast.statement_expression(
3015            Span::default(),
3016            self.ast.expression_call(
3017                Span::default(),
3018                self.ast
3019                    .expression_identifier(Span::default(), self.ast.ident(&self.coverage_hit_v2)),
3020                NONE,
3021                self.ast.vec_from_array([
3022                    Argument::from(self.ast.expression_identifier(
3023                        Span::default(),
3024                        self.ast.ident(&self.probe_file_v2),
3025                    )),
3026                    Argument::from(self.ast.expression_numeric_literal(
3027                        Span::default(),
3028                        target.index as f64,
3029                        None,
3030                        NumberBase::Decimal,
3031                    )),
3032                ]),
3033                false,
3034            ),
3035        )
3036    }
3037}
3038
3039impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
3040    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3041        if self
3042            .source_sensitive_functions
3043            .contains(&span_key(function.span))
3044        {
3045            return;
3046        }
3047        if let Some(target) = self.targets.remove(&span_key(function.span))
3048            && let Some(body) = &mut function.body
3049        {
3050            body.statements.insert(0, self.probe(&target));
3051        }
3052        walk_mut::walk_function(self, function, flags);
3053    }
3054
3055    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3056        if self
3057            .source_sensitive_functions
3058            .contains(&span_key(function.span))
3059        {
3060            return;
3061        }
3062        if let Some(target) = self.targets.remove(&span_key(function.span)) {
3063            let probe = self.probe(&target);
3064            if function.expression {
3065                let original = function
3066                    .body
3067                    .statements
3068                    .pop()
3069                    .expect("expression arrow must contain its expression statement");
3070                let Statement::ExpressionStatement(expression) = original else {
3071                    panic!("expression arrow body must be represented as an expression statement");
3072                };
3073                function.expression = false;
3074                function.body.statements.push(probe);
3075                function.body.statements.push(
3076                    self.ast
3077                        .statement_return(Span::default(), Some(expression.unbox().expression)),
3078                );
3079            } else {
3080                function.body.statements.insert(0, probe);
3081            }
3082        }
3083        walk_mut::walk_arrow_function_expression(self, function);
3084    }
3085}
3086
3087struct OptionalMemberTransformer<'a> {
3088    ast: AstBuilder<'a>,
3089    optional_select: String,
3090    targets: HashMap<SpanKey, (String, String)>,
3091    source_sensitive_functions: HashSet<SpanKey>,
3092    with_statements: HashSet<SpanKey>,
3093}
3094
3095impl<'a> OptionalMemberTransformer<'a> {
3096    fn instrument_operand(
3097        &self,
3098        operand: Expression<'a>,
3099        short_id: &str,
3100        continued_id: &str,
3101    ) -> Expression<'a> {
3102        self.ast.expression_call(
3103            Span::default(),
3104            self.ast
3105                .expression_identifier(Span::default(), self.ast.ident(&self.optional_select)),
3106            NONE,
3107            self.ast.vec_from_array([
3108                Argument::from(self.ast.expression_string_literal(
3109                    Span::default(),
3110                    self.ast.str(short_id),
3111                    None,
3112                )),
3113                Argument::from(self.ast.expression_string_literal(
3114                    Span::default(),
3115                    self.ast.str(continued_id),
3116                    None,
3117                )),
3118                Argument::from(operand),
3119            ]),
3120            false,
3121        )
3122    }
3123
3124    fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
3125        let Some((short_id, continued_id)) = self.targets.remove(&span_key(span)) else {
3126            return;
3127        };
3128        let operand = object.take_in(self.ast.allocator);
3129        *object = self.instrument_operand(operand, &short_id, &continued_id);
3130    }
3131}
3132
3133impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
3134    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3135        if self
3136            .source_sensitive_functions
3137            .contains(&span_key(function.span))
3138        {
3139            return;
3140        }
3141        walk_mut::walk_function(self, function, flags);
3142    }
3143
3144    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3145        if self
3146            .source_sensitive_functions
3147            .contains(&span_key(function.span))
3148        {
3149            return;
3150        }
3151        walk_mut::walk_arrow_function_expression(self, function);
3152    }
3153
3154    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
3155        if self.with_statements.contains(&span_key(statement.span)) {
3156            return;
3157        }
3158        walk_mut::walk_with_statement(self, statement);
3159    }
3160
3161    fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
3162        self.instrument_target(member.span, &mut member.object);
3163        walk_mut::walk_computed_member_expression(self, member);
3164    }
3165
3166    fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
3167        self.instrument_target(member.span, &mut member.object);
3168        walk_mut::walk_static_member_expression(self, member);
3169    }
3170
3171    fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
3172        self.instrument_target(member.span, &mut member.object);
3173        walk_mut::walk_private_field_expression(self, member);
3174    }
3175}
3176
3177#[derive(Clone)]
3178struct OptionalCallSiteRuntime {
3179    frame: String,
3180    short_id: String,
3181    continued_id: String,
3182}
3183
3184struct OptionalCallTransformer<'a, 's> {
3185    ast: AstBuilder<'a>,
3186    optional_call_begin: String,
3187    optional_call_reached: String,
3188    optional_call_continued: String,
3189    optional_call_end: String,
3190    scope_declarations: Vec<Vec<String>>,
3191    sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
3192    roots: HashMap<SpanKey, Vec<SpanKey>>,
3193    source_sensitive_functions: HashSet<SpanKey>,
3194    with_statements: HashSet<SpanKey>,
3195    _source: std::marker::PhantomData<&'s str>,
3196}
3197
3198impl<'a, 's> OptionalCallTransformer<'a, 's> {
3199    #[allow(clippy::too_many_arguments)]
3200    fn new(
3201        ast: AstBuilder<'a>,
3202        source: &'s str,
3203        optional_call_begin: String,
3204        optional_call_reached: String,
3205        optional_call_continued: String,
3206        optional_call_end: String,
3207        sites: HashMap<SpanKey, (String, String)>,
3208        roots: HashMap<SpanKey, Vec<SpanKey>>,
3209        source_sensitive_functions: HashSet<SpanKey>,
3210        with_statements: HashSet<SpanKey>,
3211    ) -> Self {
3212        let mut names = CandidateNames::new(source);
3213        let sites = sites
3214            .into_iter()
3215            .map(|(key, (short_id, continued_id))| {
3216                (
3217                    key,
3218                    OptionalCallSiteRuntime {
3219                        frame: names.allocate("_optionalCall"),
3220                        short_id,
3221                        continued_id,
3222                    },
3223                )
3224            })
3225            .collect();
3226        Self {
3227            ast,
3228            optional_call_begin,
3229            optional_call_reached,
3230            optional_call_continued,
3231            optional_call_end,
3232            scope_declarations: Vec::new(),
3233            sites,
3234            roots,
3235            source_sensitive_functions,
3236            with_statements,
3237            _source: std::marker::PhantomData,
3238        }
3239    }
3240
3241    fn identifier(&self, name: &str) -> Expression<'a> {
3242        self.ast
3243            .expression_identifier(Span::default(), self.ast.ident(name))
3244    }
3245
3246    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
3247        AssignmentTarget::from(
3248            self.ast
3249                .simple_assignment_target_assignment_target_identifier(
3250                    Span::default(),
3251                    self.ast.ident(name),
3252                ),
3253        )
3254    }
3255
3256    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
3257        self.ast.expression_call(
3258            Span::default(),
3259            self.identifier(name),
3260            NONE,
3261            arguments,
3262            false,
3263        )
3264    }
3265
3266    fn string_argument(&self, value: &str) -> Argument<'a> {
3267        Argument::from(self.ast.expression_string_literal(
3268            Span::default(),
3269            self.ast.str(value),
3270            None,
3271        ))
3272    }
3273
3274    fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
3275        self.call(
3276            &self.optional_call_reached,
3277            self.ast.vec_from_array([
3278                Argument::from(self.identifier(frame)),
3279                Argument::from(value),
3280            ]),
3281        )
3282    }
3283
3284    fn enter_scope(&mut self) {
3285        self.scope_declarations.push(Vec::new());
3286    }
3287
3288    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
3289        let names = self
3290            .scope_declarations
3291            .pop()
3292            .expect("optional-call scope stack must remain balanced");
3293        if names.is_empty() {
3294            return;
3295        }
3296        let declarations = self.ast.vec_from_iter(names.into_iter().map(|name| {
3297            self.ast.variable_declarator(
3298                Span::default(),
3299                VariableDeclarationKind::Let,
3300                self.ast
3301                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
3302                NONE,
3303                None,
3304                false,
3305            )
3306        }));
3307        statements.insert(
3308            0,
3309            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
3310                Span::default(),
3311                VariableDeclarationKind::Let,
3312                declarations,
3313                false,
3314            )),
3315        );
3316    }
3317
3318    fn instrument_callee(
3319        &self,
3320        callee: Expression<'a>,
3321        site: &OptionalCallSiteRuntime,
3322    ) -> Expression<'a> {
3323        match callee {
3324            Expression::ComputedMemberExpression(mut member) => {
3325                let property = member.expression.take_in(self.ast.allocator);
3326                member.expression = self.reached(&site.frame, property);
3327                Expression::ComputedMemberExpression(member)
3328            }
3329            Expression::StaticMemberExpression(member) => {
3330                let member = member.unbox();
3331                let property = self.ast.expression_string_literal(
3332                    Span::default(),
3333                    self.ast.str(member.property.name.as_str()),
3334                    None,
3335                );
3336                Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
3337                    member.span,
3338                    member.object,
3339                    self.reached(&site.frame, property),
3340                    member.optional,
3341                ))
3342            }
3343            Expression::PrivateFieldExpression(mut member) => {
3344                let object = member.object.take_in(self.ast.allocator);
3345                member.object = self.reached(&site.frame, object);
3346                Expression::PrivateFieldExpression(member)
3347            }
3348            Expression::ChainExpression(mut chain) => {
3349                match &mut chain.expression {
3350                    ChainElement::ComputedMemberExpression(member) => {
3351                        let property = member.expression.take_in(self.ast.allocator);
3352                        member.expression = self.reached(&site.frame, property);
3353                    }
3354                    ChainElement::StaticMemberExpression(member) => {
3355                        let member = member.take_in(self.ast.allocator);
3356                        let property = self.ast.expression_string_literal(
3357                            Span::default(),
3358                            self.ast.str(member.property.name.as_str()),
3359                            None,
3360                        );
3361                        chain.expression = ChainElement::ComputedMemberExpression(
3362                            self.ast.alloc_computed_member_expression(
3363                                member.span,
3364                                member.object,
3365                                self.reached(&site.frame, property),
3366                                member.optional,
3367                            ),
3368                        );
3369                    }
3370                    ChainElement::PrivateFieldExpression(member) => {
3371                        let object = member.object.take_in(self.ast.allocator);
3372                        member.object = self.reached(&site.frame, object);
3373                    }
3374                    ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
3375                        return self.reached(&site.frame, Expression::ChainExpression(chain));
3376                    }
3377                }
3378                Expression::ChainExpression(chain)
3379            }
3380            Expression::ParenthesizedExpression(mut parenthesized) => {
3381                let inner = parenthesized.expression.take_in(self.ast.allocator);
3382                parenthesized.expression = self.instrument_callee(inner, site);
3383                Expression::ParenthesizedExpression(parenthesized)
3384            }
3385            Expression::TSAsExpression(mut wrapped) => {
3386                let inner = wrapped.expression.take_in(self.ast.allocator);
3387                wrapped.expression = self.instrument_callee(inner, site);
3388                Expression::TSAsExpression(wrapped)
3389            }
3390            Expression::TSSatisfiesExpression(mut wrapped) => {
3391                let inner = wrapped.expression.take_in(self.ast.allocator);
3392                wrapped.expression = self.instrument_callee(inner, site);
3393                Expression::TSSatisfiesExpression(wrapped)
3394            }
3395            Expression::TSTypeAssertion(mut wrapped) => {
3396                let inner = wrapped.expression.take_in(self.ast.allocator);
3397                wrapped.expression = self.instrument_callee(inner, site);
3398                Expression::TSTypeAssertion(wrapped)
3399            }
3400            Expression::TSNonNullExpression(mut wrapped) => {
3401                let inner = wrapped.expression.take_in(self.ast.allocator);
3402                wrapped.expression = self.instrument_callee(inner, site);
3403                Expression::TSNonNullExpression(wrapped)
3404            }
3405            other => self.reached(&site.frame, other),
3406        }
3407    }
3408
3409    fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
3410        let callee = call.callee.take_in(self.ast.allocator);
3411        call.callee = self.instrument_callee(callee, site);
3412        let continued = self.call(
3413            &self.optional_call_continued,
3414            self.ast.vec1(Argument::from(self.identifier(&site.frame))),
3415        );
3416        call.arguments.insert(
3417            0,
3418            Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), continued)),
3419        );
3420    }
3421
3422    fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
3423        let sites = site_keys
3424            .iter()
3425            .map(|key| {
3426                self.sites
3427                    .get(key)
3428                    .expect("optional-call root must reference a known site")
3429                    .clone()
3430            })
3431            .collect::<Vec<_>>();
3432        self.scope_declarations
3433            .last_mut()
3434            .expect("optional-call root must be inside a program or function")
3435            .extend(sites.iter().map(|site| site.frame.clone()));
3436
3437        let original = expression.take_in(self.ast.allocator);
3438        let mut measured = original;
3439        for site in sites.iter().rev() {
3440            measured = self.call(
3441                &self.optional_call_end,
3442                self.ast.vec_from_array([
3443                    Argument::from(self.identifier(&site.frame)),
3444                    Argument::from(measured),
3445                ]),
3446            );
3447        }
3448        let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
3449        for site in &sites {
3450            let begin = self.call(
3451                &self.optional_call_begin,
3452                self.ast.vec_from_array([
3453                    self.string_argument(&site.short_id),
3454                    self.string_argument(&site.continued_id),
3455                ]),
3456            );
3457            sequence.push(self.ast.expression_assignment(
3458                Span::default(),
3459                AssignmentOperator::Assign,
3460                self.assignment_target(&site.frame),
3461                begin,
3462            ));
3463        }
3464        sequence.push(measured);
3465        *expression = self.ast.expression_sequence(Span::default(), sequence);
3466    }
3467}
3468
3469impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
3470    fn visit_program(&mut self, program: &mut Program<'a>) {
3471        self.enter_scope();
3472        walk_mut::walk_program(self, program);
3473        self.leave_scope(&mut program.body);
3474    }
3475
3476    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
3477        self.enter_scope();
3478        walk_mut::walk_function_body(self, body);
3479        self.leave_scope(&mut body.statements);
3480    }
3481
3482    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3483        if self
3484            .source_sensitive_functions
3485            .contains(&span_key(function.span))
3486        {
3487            return;
3488        }
3489        walk_mut::walk_function(self, function, flags);
3490    }
3491
3492    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3493        if self
3494            .source_sensitive_functions
3495            .contains(&span_key(function.span))
3496        {
3497            return;
3498        }
3499        walk_mut::walk_arrow_function_expression(self, function);
3500    }
3501
3502    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
3503        if self.with_statements.contains(&span_key(statement.span)) {
3504            return;
3505        }
3506        walk_mut::walk_with_statement(self, statement);
3507    }
3508
3509    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
3510        walk_mut::walk_call_expression(self, call);
3511        if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
3512            self.instrument_call(call, &site);
3513        }
3514    }
3515
3516    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
3517        let key = span_key(expression.span());
3518        walk_mut::walk_expression(self, expression);
3519        if let Some(site_keys) = self.roots.remove(&key) {
3520            self.wrap_root(expression, &site_keys);
3521        }
3522    }
3523}
3524
3525struct DefaultTransformer<'a> {
3526    ast: AstBuilder<'a>,
3527    default_selected: String,
3528    default_entered: String,
3529    parameter_targets: HashMap<SpanKey, DefaultTarget>,
3530    binding_targets: HashMap<SpanKey, DefaultTarget>,
3531    function_entries: Vec<Vec<Statement<'a>>>,
3532    declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
3533    active_declaration: Vec<SpanKey>,
3534    parameter_pattern_depth: usize,
3535    source_sensitive_functions: HashSet<SpanKey>,
3536    with_statements: HashSet<SpanKey>,
3537}
3538
3539impl<'a> DefaultTransformer<'a> {
3540    fn identifier(&self, name: &str) -> Expression<'a> {
3541        self.ast
3542            .expression_identifier(Span::default(), self.ast.ident(name))
3543    }
3544
3545    fn string_argument(&self, value: &str) -> Argument<'a> {
3546        Argument::from(self.ast.expression_string_literal(
3547            Span::default(),
3548            self.ast.str(value),
3549            None,
3550        ))
3551    }
3552
3553    fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
3554        let mut arguments = self.ast.vec_from_array([
3555            self.string_argument(&target.default_id),
3556            Argument::from(value),
3557        ]);
3558        if let Some(name) = &target.inferred_name {
3559            arguments.push(self.string_argument(name));
3560        }
3561        self.ast.expression_call(
3562            Span::default(),
3563            self.identifier(&self.default_selected),
3564            NONE,
3565            arguments,
3566            false,
3567        )
3568    }
3569
3570    fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
3571        self.ast.statement_expression(
3572            Span::default(),
3573            self.ast.expression_call(
3574                Span::default(),
3575                self.identifier(&self.default_entered),
3576                NONE,
3577                self.ast.vec_from_array([
3578                    self.string_argument(&target.default_id),
3579                    self.string_argument(&target.provided_id),
3580                ]),
3581                false,
3582            ),
3583        )
3584    }
3585
3586    fn push_entry(&mut self, target: &DefaultTarget) {
3587        let entry = self.entered(target);
3588        if self.parameter_pattern_depth > 0 {
3589            self.function_entries
3590                .last_mut()
3591                .expect("parameter default must belong to a function")
3592                .push(entry);
3593        } else {
3594            let declaration = *self
3595                .active_declaration
3596                .last()
3597                .expect("binding default must belong to a declaration");
3598            self.declaration_entries
3599                .entry(declaration)
3600                .or_default()
3601                .push(entry);
3602        }
3603    }
3604
3605    fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
3606        if entries.is_empty() {
3607            return;
3608        }
3609        if let Statement::BlockStatement(block) = body {
3610            for (index, entry) in entries.into_iter().enumerate() {
3611                block.body.insert(index, entry);
3612            }
3613            return;
3614        }
3615        let original = body.take_in(self.ast.allocator);
3616        let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
3617        statements.extend(entries);
3618        statements.push(original);
3619        *body = self.ast.statement_block(Span::default(), statements);
3620    }
3621
3622    fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
3623        match statement {
3624            Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
3625            Statement::ExportNamedDeclaration(export) => match &export.declaration {
3626                Some(Declaration::VariableDeclaration(declaration)) => {
3627                    Some(span_key(declaration.span))
3628                }
3629                _ => None,
3630            },
3631            _ => None,
3632        }
3633    }
3634
3635    fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
3636        match left {
3637            ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
3638            _ => None,
3639        }
3640    }
3641}
3642
3643impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
3644    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
3645        let original = statements.take_in(self.ast.allocator);
3646        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
3647        for mut statement in original {
3648            let declaration = Self::variable_span(&statement);
3649            self.visit_statement(&mut statement);
3650            instrumented.push(statement);
3651            if let Some(declaration) = declaration
3652                && let Some(entries) = self.declaration_entries.remove(&declaration)
3653            {
3654                instrumented.extend(entries);
3655            }
3656        }
3657        *statements = instrumented;
3658    }
3659
3660    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3661        if self
3662            .source_sensitive_functions
3663            .contains(&span_key(function.span))
3664        {
3665            return;
3666        }
3667        self.function_entries.push(Vec::new());
3668        walk_mut::walk_function(self, function, flags);
3669        let entries = self
3670            .function_entries
3671            .pop()
3672            .expect("default function-entry stack must remain balanced");
3673        if let Some(body) = &mut function.body {
3674            for (index, entry) in entries.into_iter().enumerate() {
3675                body.statements.insert(index, entry);
3676            }
3677        }
3678    }
3679
3680    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3681        if self
3682            .source_sensitive_functions
3683            .contains(&span_key(function.span))
3684        {
3685            return;
3686        }
3687        self.function_entries.push(Vec::new());
3688        walk_mut::walk_arrow_function_expression(self, function);
3689        let entries = self
3690            .function_entries
3691            .pop()
3692            .expect("default arrow-entry stack must remain balanced");
3693        for (index, entry) in entries.into_iter().enumerate() {
3694            function.body.statements.insert(index, entry);
3695        }
3696    }
3697
3698    fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
3699        let outer = self.parameter_targets.remove(&span_key(parameter.span));
3700        if let Some(target) = &outer {
3701            let entry = self.entered(target);
3702            self.function_entries
3703                .last_mut()
3704                .expect("formal parameter must belong to a function")
3705                .push(entry);
3706        }
3707        self.visit_decorators(&mut parameter.decorators);
3708        self.parameter_pattern_depth += 1;
3709        self.visit_binding_pattern(&mut parameter.pattern);
3710        self.parameter_pattern_depth -= 1;
3711        if let Some(annotation) = &mut parameter.type_annotation {
3712            self.visit_ts_type_annotation(annotation);
3713        }
3714        if let Some(initializer) = &mut parameter.initializer {
3715            self.visit_expression(initializer);
3716            if let Some(target) = outer {
3717                let value = initializer.take_in(self.ast.allocator);
3718                **initializer = self.selected(value, &target);
3719            }
3720        }
3721    }
3722
3723    fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
3724        let target = if self.parameter_pattern_depth > 0 {
3725            self.parameter_targets.remove(&span_key(assignment.span))
3726        } else {
3727            self.binding_targets.remove(&span_key(assignment.span))
3728        };
3729        if let Some(target) = &target {
3730            self.push_entry(target);
3731        }
3732        walk_mut::walk_assignment_pattern(self, assignment);
3733        if let Some(target) = target {
3734            let value = assignment.right.take_in(self.ast.allocator);
3735            assignment.right = self.selected(value, &target);
3736        }
3737    }
3738
3739    fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
3740        self.active_declaration.push(span_key(declaration.span));
3741        walk_mut::walk_variable_declaration(self, declaration);
3742        self.active_declaration
3743            .pop()
3744            .expect("default declaration stack must remain balanced");
3745    }
3746
3747    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
3748        let declaration = Self::loop_declaration_span(&statement.left);
3749        walk_mut::walk_for_in_statement(self, statement);
3750        if let Some(declaration) = declaration
3751            && let Some(entries) = self.declaration_entries.remove(&declaration)
3752        {
3753            self.prepend_entries(&mut statement.body, entries);
3754        }
3755    }
3756
3757    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
3758        let declaration = Self::loop_declaration_span(&statement.left);
3759        walk_mut::walk_for_of_statement(self, statement);
3760        if let Some(declaration) = declaration
3761            && let Some(entries) = self.declaration_entries.remove(&declaration)
3762        {
3763            self.prepend_entries(&mut statement.body, entries);
3764        }
3765    }
3766
3767    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
3768        if self.with_statements.contains(&span_key(statement.span)) {
3769            return;
3770        }
3771        walk_mut::walk_with_statement(self, statement);
3772    }
3773}
3774
3775enum ExtendedKind {
3776    Try,
3777    Loop,
3778}
3779
3780struct ExtendedTransformer<'a, 's> {
3781    ast: AstBuilder<'a>,
3782    try_begin: String,
3783    try_catch: String,
3784    try_end: String,
3785    loop_begin: String,
3786    loop_entered: String,
3787    loop_end: String,
3788    try_targets: HashMap<SpanKey, ExtendedTarget>,
3789    loop_targets: HashMap<SpanKey, ExtendedTarget>,
3790    names: CandidateNames<'s>,
3791    scope_declarations: Vec<Vec<String>>,
3792    source_sensitive_functions: HashSet<SpanKey>,
3793    with_statements: HashSet<SpanKey>,
3794}
3795
3796impl<'a> ExtendedTransformer<'a, '_> {
3797    fn identifier(&self, name: &str) -> Expression<'a> {
3798        self.ast
3799            .expression_identifier(Span::default(), self.ast.ident(name))
3800    }
3801
3802    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
3803        AssignmentTarget::from(
3804            self.ast
3805                .simple_assignment_target_assignment_target_identifier(
3806                    Span::default(),
3807                    self.ast.ident(name),
3808                ),
3809        )
3810    }
3811
3812    fn string_argument(&self, value: &str) -> Argument<'a> {
3813        Argument::from(self.ast.expression_string_literal(
3814            Span::default(),
3815            self.ast.str(value),
3816            None,
3817        ))
3818    }
3819
3820    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
3821        self.ast.expression_call(
3822            Span::default(),
3823            self.identifier(name),
3824            NONE,
3825            arguments,
3826            false,
3827        )
3828    }
3829
3830    fn call_statement(
3831        &self,
3832        name: &str,
3833        arguments: oxc_allocator::Vec<'a, Argument<'a>>,
3834    ) -> Statement<'a> {
3835        self.ast
3836            .statement_expression(Span::default(), self.call(name, arguments))
3837    }
3838
3839    fn enter_scope(&mut self) {
3840        self.scope_declarations.push(Vec::new());
3841    }
3842
3843    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
3844        let names = self
3845            .scope_declarations
3846            .pop()
3847            .expect("extended scope stack must remain balanced");
3848        if names.is_empty() {
3849            return;
3850        }
3851        let declarations = self.ast.vec_from_iter(names.into_iter().map(|name| {
3852            self.ast.variable_declarator(
3853                Span::default(),
3854                VariableDeclarationKind::Let,
3855                self.ast
3856                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
3857                NONE,
3858                None,
3859                false,
3860            )
3861        }));
3862        statements.insert(
3863            0,
3864            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
3865                Span::default(),
3866                VariableDeclarationKind::Let,
3867                declarations,
3868                false,
3869            )),
3870        );
3871    }
3872
3873    fn scratch(&mut self, base: &str) -> String {
3874        let name = self.names.allocate(base);
3875        self.scope_declarations
3876            .last_mut()
3877            .expect("extended branch must be inside a program or function")
3878            .push(name.clone());
3879        name
3880    }
3881
3882    fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
3883        match statement {
3884            Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
3885            Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
3886            Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
3887            Statement::LabeledStatement(node) => Self::target(&node.body),
3888            _ => None,
3889        }
3890    }
3891
3892    fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
3893        match statement {
3894            Statement::TryStatement(node) => Some(node),
3895            Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
3896            _ => None,
3897        }
3898    }
3899
3900    fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
3901        match statement {
3902            Statement::ForInStatement(node) => Some(&mut node.body),
3903            Statement::ForOfStatement(node) => Some(&mut node.body),
3904            Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
3905            _ => None,
3906        }
3907    }
3908
3909    fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
3910        if let Statement::BlockStatement(block) = body {
3911            block.body.insert(0, entry);
3912            return;
3913        }
3914        let original = body.take_in(ast.allocator);
3915        *body = ast.statement_block(Span::default(), ast.vec_from_array([entry, original]));
3916    }
3917
3918    fn begin_assignment(&self, frame: &str, begin: &str, target: &ExtendedTarget) -> Statement<'a> {
3919        let call = self.call(
3920            begin,
3921            self.ast.vec_from_array([
3922                self.string_argument(&target.first_id),
3923                self.string_argument(&target.second_id),
3924            ]),
3925        );
3926        self.ast.statement_expression(
3927            Span::default(),
3928            self.ast.expression_assignment(
3929                Span::default(),
3930                AssignmentOperator::Assign,
3931                self.assignment_target(frame),
3932                call,
3933            ),
3934        )
3935    }
3936
3937    fn instrument_try(&mut self, statement: &mut Statement<'a>, target: ExtendedTarget) {
3938        let frame = self.scratch("_supercovTryFrame");
3939        let assignment = self.begin_assignment(&frame, &self.try_begin, &target);
3940        let node = Self::inner_try(statement).expect("try target must remain a try statement");
3941        node.handler
3942            .as_mut()
3943            .expect("try coverage requires a catch handler")
3944            .body
3945            .body
3946            .insert(
3947                0,
3948                self.call_statement(
3949                    &self.try_catch,
3950                    self.ast.vec_from_array([
3951                        Argument::from(self.identifier(&frame)),
3952                        Argument::from(self.identifier("undefined")),
3953                    ]),
3954                ),
3955            );
3956        let end = self.call_statement(
3957            &self.try_end,
3958            self.ast.vec1(Argument::from(self.identifier(&frame))),
3959        );
3960        if let Some(finalizer) = &mut node.finalizer {
3961            finalizer.body.insert(0, end);
3962        } else {
3963            node.finalizer = Some(
3964                self.ast
3965                    .alloc_block_statement(Span::default(), self.ast.vec1(end)),
3966            );
3967        }
3968        let original = statement.take_in(self.ast.allocator);
3969        *statement = self.ast.statement_block(
3970            Span::default(),
3971            self.ast.vec_from_array([assignment, original]),
3972        );
3973    }
3974
3975    fn instrument_loop(&mut self, statement: &mut Statement<'a>, target: ExtendedTarget) {
3976        let frame = self.scratch("_supercovLoopFrame");
3977        let assignment = self.begin_assignment(&frame, &self.loop_begin, &target);
3978        let entered = self.call_statement(
3979            &self.loop_entered,
3980            self.ast.vec1(Argument::from(self.identifier(&frame))),
3981        );
3982        Self::prepend(
3983            Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
3984            entered,
3985            self.ast,
3986        );
3987        let original = statement.take_in(self.ast.allocator);
3988        let end = self.call_statement(
3989            &self.loop_end,
3990            self.ast.vec1(Argument::from(self.identifier(&frame))),
3991        );
3992        let wrapped = self.ast.statement_try(
3993            Span::default(),
3994            self.ast
3995                .block_statement(Span::default(), self.ast.vec1(original)),
3996            None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
3997            Some(
3998                self.ast
3999                    .block_statement(Span::default(), self.ast.vec1(end)),
4000            ),
4001        );
4002        *statement = self.ast.statement_block(
4003            Span::default(),
4004            self.ast.vec_from_array([assignment, wrapped]),
4005        );
4006    }
4007}
4008
4009impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
4010    fn visit_program(&mut self, program: &mut Program<'a>) {
4011        self.enter_scope();
4012        walk_mut::walk_program(self, program);
4013        self.leave_scope(&mut program.body);
4014    }
4015
4016    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
4017        self.enter_scope();
4018        walk_mut::walk_function_body(self, body);
4019        self.leave_scope(&mut body.statements);
4020    }
4021
4022    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4023        if self
4024            .source_sensitive_functions
4025            .contains(&span_key(function.span))
4026        {
4027            return;
4028        }
4029        walk_mut::walk_function(self, function, flags);
4030    }
4031
4032    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4033        if self
4034            .source_sensitive_functions
4035            .contains(&span_key(function.span))
4036        {
4037            return;
4038        }
4039        walk_mut::walk_arrow_function_expression(self, function);
4040    }
4041
4042    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4043        if self.with_statements.contains(&span_key(statement.span)) {
4044            return;
4045        }
4046        walk_mut::walk_with_statement(self, statement);
4047    }
4048
4049    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
4050        let Some((kind, key)) = Self::target(statement) else {
4051            walk_mut::walk_statement(self, statement);
4052            return;
4053        };
4054        match kind {
4055            ExtendedKind::Try => {
4056                if let Some(target) = self.try_targets.remove(&key) {
4057                    walk_mut::walk_statement(self, statement);
4058                    self.instrument_try(statement, target);
4059                } else {
4060                    walk_mut::walk_statement(self, statement);
4061                }
4062            }
4063            ExtendedKind::Loop => {
4064                if let Some(target) = self.loop_targets.remove(&key) {
4065                    walk_mut::walk_statement(self, statement);
4066                    self.instrument_loop(statement, target);
4067                } else {
4068                    walk_mut::walk_statement(self, statement);
4069                }
4070            }
4071        }
4072    }
4073}
4074
4075struct LogicalValueTransformer<'a, 's> {
4076    ast: AstBuilder<'a>,
4077    selection_begin: String,
4078    selection_right: String,
4079    selection_end: String,
4080    names: CandidateNames<'s>,
4081    scope_declarations: Vec<Vec<String>>,
4082    logical_targets: HashMap<SpanKey, (String, String)>,
4083    assignment_targets: HashMap<SpanKey, (String, String)>,
4084    source_sensitive_functions: HashSet<SpanKey>,
4085    with_statements: HashSet<SpanKey>,
4086}
4087
4088impl<'a> LogicalValueTransformer<'a, '_> {
4089    fn identifier(&self, name: &str) -> Expression<'a> {
4090        self.ast
4091            .expression_identifier(Span::default(), self.ast.ident(name))
4092    }
4093
4094    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
4095        AssignmentTarget::from(
4096            self.ast
4097                .simple_assignment_target_assignment_target_identifier(
4098                    Span::default(),
4099                    self.ast.ident(name),
4100                ),
4101        )
4102    }
4103
4104    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
4105        self.ast.expression_call(
4106            Span::default(),
4107            self.identifier(name),
4108            NONE,
4109            arguments,
4110            false,
4111        )
4112    }
4113
4114    fn string_argument(&self, value: &str) -> Argument<'a> {
4115        Argument::from(self.ast.expression_string_literal(
4116            Span::default(),
4117            self.ast.str(value),
4118            None,
4119        ))
4120    }
4121
4122    fn enter_scope(&mut self) {
4123        self.scope_declarations.push(Vec::new());
4124    }
4125
4126    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4127        let names = self
4128            .scope_declarations
4129            .pop()
4130            .expect("logical-value scope stack must remain balanced");
4131        if names.is_empty() {
4132            return;
4133        }
4134        let declarations = self.ast.vec_from_iter(names.into_iter().map(|name| {
4135            self.ast.variable_declarator(
4136                Span::default(),
4137                VariableDeclarationKind::Let,
4138                self.ast
4139                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
4140                NONE,
4141                None,
4142                false,
4143            )
4144        }));
4145        statements.insert(
4146            0,
4147            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
4148                Span::default(),
4149                VariableDeclarationKind::Let,
4150                declarations,
4151                false,
4152            )),
4153        );
4154    }
4155
4156    fn scratch(&mut self) -> String {
4157        let name = self.names.allocate("_supercovSelectionFrame");
4158        self.scope_declarations
4159            .last_mut()
4160            .expect("logical expression must be inside a program or function")
4161            .push(name.clone());
4162        name
4163    }
4164
4165    fn instrument(
4166        &mut self,
4167        logical: oxc_allocator::Box<'a, LogicalExpression<'a>>,
4168        short_id: &str,
4169        right_id: &str,
4170    ) -> Expression<'a> {
4171        let logical = logical.unbox();
4172        let frame = self.scratch();
4173        let begin = self.call(
4174            &self.selection_begin,
4175            self.ast.vec_from_array([
4176                self.string_argument(short_id),
4177                self.string_argument(right_id),
4178            ]),
4179        );
4180        let assign = self.ast.expression_assignment(
4181            Span::default(),
4182            AssignmentOperator::Assign,
4183            self.assignment_target(&frame),
4184            begin,
4185        );
4186        let right = self.call(
4187            &self.selection_right,
4188            self.ast.vec_from_array([
4189                Argument::from(self.identifier(&frame)),
4190                Argument::from(logical.right),
4191            ]),
4192        );
4193        let selection =
4194            self.ast
4195                .expression_logical(Span::default(), logical.left, logical.operator, right);
4196        let end = self.call(
4197            &self.selection_end,
4198            self.ast.vec_from_array([
4199                Argument::from(self.identifier(&frame)),
4200                Argument::from(selection),
4201            ]),
4202        );
4203        self.ast
4204            .expression_sequence(Span::default(), self.ast.vec_from_array([assign, end]))
4205    }
4206
4207    fn instrument_assignment(
4208        &mut self,
4209        assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
4210        short_id: &str,
4211        right_id: &str,
4212    ) -> Expression<'a> {
4213        let assignment = assignment.unbox();
4214        let inferred_name = match &assignment.left {
4215            AssignmentTarget::AssignmentTargetIdentifier(identifier)
4216                if assignment.span.start == identifier.span.start
4217                    && expression_is_anonymous_definition(&assignment.right) =>
4218            {
4219                Some(identifier.name.to_string())
4220            }
4221            _ => None,
4222        };
4223        let frame = self.scratch();
4224        let begin = self.call(
4225            &self.selection_begin,
4226            self.ast.vec_from_array([
4227                self.string_argument(short_id),
4228                self.string_argument(right_id),
4229            ]),
4230        );
4231        let assign_frame = self.ast.expression_assignment(
4232            Span::default(),
4233            AssignmentOperator::Assign,
4234            self.assignment_target(&frame),
4235            begin,
4236        );
4237        let mut right_arguments = self.ast.vec_from_array([
4238            Argument::from(self.identifier(&frame)),
4239            Argument::from(assignment.right),
4240        ]);
4241        if let Some(name) = inferred_name {
4242            right_arguments.push(self.string_argument(&name));
4243        }
4244        let right = self.call(&self.selection_right, right_arguments);
4245        let measured_assignment = self.ast.expression_assignment(
4246            Span::default(),
4247            assignment.operator,
4248            assignment.left,
4249            right,
4250        );
4251        let end = self.call(
4252            &self.selection_end,
4253            self.ast.vec_from_array([
4254                Argument::from(self.identifier(&frame)),
4255                Argument::from(measured_assignment),
4256            ]),
4257        );
4258        self.ast.expression_sequence(
4259            Span::default(),
4260            self.ast.vec_from_array([assign_frame, end]),
4261        )
4262    }
4263}
4264
4265impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
4266    fn visit_program(&mut self, program: &mut Program<'a>) {
4267        self.enter_scope();
4268        walk_mut::walk_program(self, program);
4269        self.leave_scope(&mut program.body);
4270    }
4271
4272    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
4273        self.enter_scope();
4274        walk_mut::walk_function_body(self, body);
4275        self.leave_scope(&mut body.statements);
4276    }
4277
4278    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4279        if self
4280            .source_sensitive_functions
4281            .contains(&span_key(function.span))
4282        {
4283            return;
4284        }
4285        walk_mut::walk_function(self, function, flags);
4286    }
4287
4288    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4289        if self
4290            .source_sensitive_functions
4291            .contains(&span_key(function.span))
4292        {
4293            return;
4294        }
4295        walk_mut::walk_arrow_function_expression(self, function);
4296    }
4297
4298    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4299        if self.with_statements.contains(&span_key(statement.span)) {
4300            return;
4301        }
4302        walk_mut::walk_with_statement(self, statement);
4303    }
4304
4305    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
4306        let key = span_key(expression.span());
4307        walk_mut::walk_expression(self, expression);
4308        if let Some((short_id, right_id)) = self.assignment_targets.remove(&key) {
4309            let original = expression.take_in(self.ast.allocator);
4310            let Expression::AssignmentExpression(assignment) = original else {
4311                panic!("logical-assignment target must remain an assignment expression");
4312            };
4313            *expression = self.instrument_assignment(assignment, &short_id, &right_id);
4314            return;
4315        }
4316        let Some((short_id, right_id)) = self.logical_targets.remove(&key) else {
4317            return;
4318        };
4319        let original = expression.take_in(self.ast.allocator);
4320        let Expression::LogicalExpression(logical) = original else {
4321            panic!("logical-value target must remain a logical expression");
4322        };
4323        *expression = self.instrument(logical, &short_id, &right_id);
4324    }
4325}
4326
4327struct SwitchTransformer<'a, 's> {
4328    ast: AstBuilder<'a>,
4329    coverage_hit: String,
4330    targets: HashMap<SpanKey, SwitchTarget>,
4331    names: CandidateNames<'s>,
4332    source_sensitive_functions: HashSet<SpanKey>,
4333    with_statements: HashSet<SpanKey>,
4334}
4335
4336impl<'a> SwitchTransformer<'a, '_> {
4337    fn identifier(&self, name: &str) -> Expression<'a> {
4338        self.ast
4339            .expression_identifier(Span::default(), self.ast.ident(name))
4340    }
4341
4342    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
4343        AssignmentTarget::from(
4344            self.ast
4345                .simple_assignment_target_assignment_target_identifier(
4346                    Span::default(),
4347                    self.ast.ident(name),
4348                ),
4349        )
4350    }
4351
4352    fn probe(&self, id: &str) -> Statement<'a> {
4353        self.ast.statement_expression(
4354            Span::default(),
4355            self.ast.expression_call(
4356                Span::default(),
4357                self.identifier(&self.coverage_hit),
4358                NONE,
4359                self.ast
4360                    .vec1(Argument::from(self.ast.expression_string_literal(
4361                        Span::default(),
4362                        self.ast.str(id),
4363                        None,
4364                    ))),
4365                false,
4366            ),
4367        )
4368    }
4369
4370    fn target(statement: &Statement<'a>) -> Option<SpanKey> {
4371        match statement {
4372            Statement::SwitchStatement(node) => Some(span_key(node.span)),
4373            Statement::LabeledStatement(node) => Self::target(&node.body),
4374            _ => None,
4375        }
4376    }
4377
4378    fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
4379        match statement {
4380            Statement::SwitchStatement(node) => Some(node),
4381            Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
4382            _ => None,
4383        }
4384    }
4385
4386    fn entered_assignment(&self, entered: &str) -> Statement<'a> {
4387        self.ast.statement_expression(
4388            Span::default(),
4389            self.ast.expression_assignment(
4390                Span::default(),
4391                AssignmentOperator::Assign,
4392                self.assignment_target(entered),
4393                self.ast.expression_boolean_literal(Span::default(), true),
4394            ),
4395        )
4396    }
4397
4398    fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
4399        let entered = target
4400            .no_match_id
4401            .as_ref()
4402            .map(|_| self.names.allocate("_supercovSwitchEntered"));
4403        let node = Self::inner_switch(statement).expect("switch target must remain a switch");
4404        for (index, case) in node.cases.iter_mut().enumerate() {
4405            let probe = self.probe(
4406                target
4407                    .case_ids
4408                    .get(index)
4409                    .expect("switch case target count must remain stable"),
4410            );
4411            case.consequent.insert(0, probe);
4412            if let Some(entered) = &entered {
4413                case.consequent.insert(0, self.entered_assignment(entered));
4414            }
4415        }
4416        let (Some(entered), Some(no_match_id)) = (entered, &target.no_match_id) else {
4417            return;
4418        };
4419        let declaration =
4420            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
4421                Span::default(),
4422                VariableDeclarationKind::Let,
4423                self.ast.vec1(self.ast.variable_declarator(
4424                    Span::default(),
4425                    VariableDeclarationKind::Let,
4426                    self.ast.binding_pattern_binding_identifier(
4427                        Span::default(),
4428                        self.ast.ident(&entered),
4429                    ),
4430                    NONE,
4431                    Some(self.ast.expression_boolean_literal(Span::default(), false)),
4432                    false,
4433                )),
4434                false,
4435            ));
4436        let original = statement.take_in(self.ast.allocator);
4437        let no_match = self.ast.statement_if(
4438            Span::default(),
4439            self.ast.expression_unary(
4440                Span::default(),
4441                UnaryOperator::LogicalNot,
4442                self.identifier(&entered),
4443            ),
4444            self.probe(no_match_id),
4445            None,
4446        );
4447        *statement = self.ast.statement_block(
4448            Span::default(),
4449            self.ast.vec_from_array([declaration, original, no_match]),
4450        );
4451    }
4452}
4453
4454impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
4455    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4456        if self
4457            .source_sensitive_functions
4458            .contains(&span_key(function.span))
4459        {
4460            return;
4461        }
4462        walk_mut::walk_function(self, function, flags);
4463    }
4464
4465    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4466        if self
4467            .source_sensitive_functions
4468            .contains(&span_key(function.span))
4469        {
4470            return;
4471        }
4472        walk_mut::walk_arrow_function_expression(self, function);
4473    }
4474
4475    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4476        if self.with_statements.contains(&span_key(statement.span)) {
4477            return;
4478        }
4479        walk_mut::walk_with_statement(self, statement);
4480    }
4481
4482    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
4483        let Some(key) = Self::target(statement) else {
4484            walk_mut::walk_statement(self, statement);
4485            return;
4486        };
4487        let Some(target) = self.targets.remove(&key) else {
4488            walk_mut::walk_statement(self, statement);
4489            return;
4490        };
4491        let has_no_match = target.no_match_id.is_some();
4492        self.instrument(statement, &target);
4493        if !has_no_match {
4494            walk_mut::walk_statement(self, statement);
4495        }
4496    }
4497}
4498
4499struct RouteRequestPhaseTransformer<'a, 's> {
4500    ast: AstBuilder<'a>,
4501    file: &'s str,
4502    with_request_phase: String,
4503    used: bool,
4504    names: CandidateNames<'s>,
4505}
4506
4507impl<'a> RouteRequestPhaseTransformer<'a, '_> {
4508    fn is_remix_route(&self) -> bool {
4509        self.file.starts_with("app/routes/")
4510    }
4511
4512    fn is_next_route(&self) -> bool {
4513        let path = Path::new(self.file);
4514        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
4515            return false;
4516        };
4517        let is_route_module = [
4518            "route.js",
4519            "route.jsx",
4520            "route.ts",
4521            "route.tsx",
4522            "route.mjs",
4523            "route.mts",
4524            "route.cjs",
4525            "route.cts",
4526        ]
4527        .contains(&name);
4528        if !is_route_module {
4529            return false;
4530        }
4531        let components = path
4532            .components()
4533            .filter_map(|component| component.as_os_str().to_str())
4534            .collect::<Vec<_>>();
4535        components
4536            .windows(2)
4537            .any(|window| window[0] == "app" && window[1] != name)
4538    }
4539
4540    fn is_handler_name(&self, name: &str) -> bool {
4541        (self.is_remix_route() && matches!(name, "loader" | "action"))
4542            || (self.is_next_route()
4543                && matches!(
4544                    name,
4545                    "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
4546                ))
4547    }
4548
4549    fn identifier(&self, name: &str) -> Expression<'a> {
4550        self.ast
4551            .expression_identifier(Span::default(), self.ast.ident(name))
4552    }
4553
4554    fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
4555        self.ast.expression_call(
4556            Span::default(),
4557            self.identifier(&self.with_request_phase),
4558            NONE,
4559            self.ast.vec1(Argument::from(expression)),
4560            false,
4561        )
4562    }
4563
4564    fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
4565        Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
4566            Span::default(),
4567            Some(self.ast.declaration_variable(
4568                Span::default(),
4569                VariableDeclarationKind::Const,
4570                self.ast.vec1(self.ast.variable_declarator(
4571                    Span::default(),
4572                    VariableDeclarationKind::Const,
4573                    self.ast.binding_pattern_binding_identifier(
4574                        Span::default(),
4575                        self.ast.ident(exported_name),
4576                    ),
4577                    NONE,
4578                    Some(self.wrap_expression(self.identifier(original_name))),
4579                    false,
4580                )),
4581                false,
4582            )),
4583            self.ast.vec(),
4584            None,
4585            ImportOrExportKind::Value,
4586            NONE,
4587        ))
4588    }
4589
4590    fn transform_named_export(
4591        &mut self,
4592        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
4593        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
4594    ) {
4595        if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
4596            for declarator in &mut declaration.declarations {
4597                let Some(name) = binding_identifier_name(&declarator.id) else {
4598                    continue;
4599                };
4600                if !self.is_handler_name(&name) {
4601                    continue;
4602                }
4603                if let Some(initializer) = &mut declarator.init {
4604                    let original = initializer.take_in(self.ast.allocator);
4605                    *initializer = self.wrap_expression(original);
4606                    self.used = true;
4607                }
4608            }
4609            output.push(Statement::ExportNamedDeclaration(export));
4610            return;
4611        }
4612
4613        if matches!(
4614            export.declaration,
4615            Some(Declaration::FunctionDeclaration(_))
4616        ) {
4617            let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
4618            else {
4619                unreachable!();
4620            };
4621            let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
4622                output.push(Statement::FunctionDeclaration(function));
4623                return;
4624            };
4625            if !self.is_handler_name(&exported_name) {
4626                export.declaration = Some(Declaration::FunctionDeclaration(function));
4627                output.push(Statement::ExportNamedDeclaration(export));
4628                return;
4629            }
4630            let original_name = self
4631                .names
4632                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
4633            function.id = Some(
4634                self.ast
4635                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
4636            );
4637            output.push(Statement::FunctionDeclaration(function));
4638            output.push(self.wrapped_named_export(&exported_name, &original_name));
4639            self.used = true;
4640            return;
4641        }
4642
4643        if export.source.is_none() {
4644            output.push(Statement::ExportNamedDeclaration(export));
4645            return;
4646        }
4647        let source = export
4648            .source
4649            .as_ref()
4650            .expect("checked above")
4651            .clone_in(self.ast.allocator);
4652        let specifiers = export.specifiers.take_in(self.ast.allocator);
4653        let mut untouched = self.ast.vec();
4654        let mut handlers = Vec::new();
4655        for specifier in specifiers {
4656            let exported_name = specifier
4657                .exported
4658                .identifier_name()
4659                .map(|name| name.to_string());
4660            if exported_name
4661                .as_deref()
4662                .is_some_and(|name| self.is_handler_name(name))
4663            {
4664                handlers.push((exported_name.expect("checked above"), specifier));
4665            } else {
4666                untouched.push(specifier);
4667            }
4668        }
4669        if handlers.is_empty() {
4670            export.specifiers = untouched;
4671            output.push(Statement::ExportNamedDeclaration(export));
4672            return;
4673        }
4674        if !untouched.is_empty() {
4675            output.push(Statement::ExportNamedDeclaration(
4676                self.ast.alloc_export_named_declaration(
4677                    export.span,
4678                    None,
4679                    untouched,
4680                    Some(source.clone_in(self.ast.allocator)),
4681                    export.export_kind,
4682                    export.with_clause.take(),
4683                ),
4684            ));
4685        }
4686        for (exported_name, specifier) in handlers {
4687            let original_name = self
4688                .names
4689                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
4690            output.push(Statement::ImportDeclaration(
4691                self.ast.alloc_import_declaration(
4692                    Span::default(),
4693                    Some(self.ast.vec1(
4694                        self.ast.import_declaration_specifier_import_specifier(
4695                            Span::default(),
4696                            specifier.local,
4697                            self.ast.binding_identifier(
4698                                Span::default(),
4699                                self.ast.ident(&original_name),
4700                            ),
4701                            ImportOrExportKind::Value,
4702                        ),
4703                    )),
4704                    source.clone_in(self.ast.allocator),
4705                    None,
4706                    NONE,
4707                    ImportOrExportKind::Value,
4708                ),
4709            ));
4710            output.push(self.wrapped_named_export(&exported_name, &original_name));
4711        }
4712        self.used = true;
4713    }
4714
4715    fn transform_default_export(
4716        &mut self,
4717        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
4718        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
4719    ) {
4720        if let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
4721            export.declaration.take_in(self.ast.allocator)
4722        {
4723            let original_name = self
4724                .names
4725                .allocate("__supercovHandleRequestCoverageOriginal");
4726            function.id = Some(
4727                self.ast
4728                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
4729            );
4730            output.push(Statement::FunctionDeclaration(function));
4731            output.push(Statement::ExportDefaultDeclaration(
4732                self.ast.alloc_export_default_declaration(
4733                    Span::default(),
4734                    ExportDefaultDeclarationKind::from(
4735                        self.wrap_expression(self.identifier(&original_name)),
4736                    ),
4737                ),
4738            ));
4739            self.used = true;
4740            return;
4741        }
4742        if export.declaration.is_expression() {
4743            let original = export
4744                .declaration
4745                .take_in(self.ast.allocator)
4746                .into_expression();
4747            export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
4748            self.used = true;
4749        }
4750        output.push(Statement::ExportDefaultDeclaration(export));
4751    }
4752
4753    fn transform_program(&mut self, program: &mut Program<'a>) {
4754        let route_module = self.is_remix_route() || self.is_next_route();
4755        let server_entry = self.file.starts_with("app/entry.server.")
4756            && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
4757                &Path::new(self.file)
4758                    .extension()
4759                    .and_then(|value| value.to_str())
4760                    .unwrap_or(""),
4761            );
4762        if !route_module && !server_entry {
4763            return;
4764        }
4765        let original = program.body.take_in(self.ast.allocator);
4766        let mut output = self.ast.vec_with_capacity(original.len() + 4);
4767        for statement in original {
4768            match statement {
4769                Statement::ExportNamedDeclaration(export) if route_module => {
4770                    self.transform_named_export(export, &mut output);
4771                }
4772                Statement::ExportDefaultDeclaration(export) if server_entry => {
4773                    self.transform_default_export(export, &mut output);
4774                }
4775                statement => output.push(statement),
4776            }
4777        }
4778        program.body = output;
4779    }
4780}
4781
4782struct RequestPhaseTransformer<'a> {
4783    ast: AstBuilder<'a>,
4784    with_request_phase: String,
4785    used: bool,
4786    source_sensitive_functions: HashSet<SpanKey>,
4787    with_statements: HashSet<SpanKey>,
4788}
4789
4790impl<'a> RequestPhaseTransformer<'a> {
4791    fn identifier(&self, name: &str) -> Expression<'a> {
4792        self.ast
4793            .expression_identifier(Span::default(), self.ast.ident(name))
4794    }
4795
4796    fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
4797        match callee {
4798            Expression::Identifier(identifier) => identifier.name == name,
4799            Expression::StaticMemberExpression(member) => member.property.name == name,
4800            _ => false,
4801        }
4802    }
4803
4804    fn callback_candidate(argument: &Argument<'a>) -> bool {
4805        matches!(
4806            argument,
4807            Argument::FunctionExpression(_)
4808                | Argument::ArrowFunctionExpression(_)
4809                | Argument::Identifier(_)
4810                | Argument::ComputedMemberExpression(_)
4811                | Argument::StaticMemberExpression(_)
4812                | Argument::PrivateFieldExpression(_)
4813        )
4814    }
4815
4816    fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
4817        matches!(
4818            argument,
4819            Argument::CallExpression(call)
4820                if expression_is_identifier(&call.callee, &self.with_request_phase)
4821        )
4822    }
4823
4824    fn wrap_argument(&mut self, argument: &mut Argument<'a>) {
4825        if self.already_wrapped(argument) || !argument.is_expression() {
4826            return;
4827        }
4828        let expression = argument.to_expression_mut();
4829        let original = expression.take_in(self.ast.allocator);
4830        *expression = self.ast.expression_call(
4831            Span::default(),
4832            self.identifier(&self.with_request_phase),
4833            NONE,
4834            self.ast.vec1(Argument::from(original)),
4835            false,
4836        );
4837        self.used = true;
4838    }
4839}
4840
4841impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
4842    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4843        if self
4844            .source_sensitive_functions
4845            .contains(&span_key(function.span))
4846        {
4847            return;
4848        }
4849        walk_mut::walk_function(self, function, flags);
4850    }
4851
4852    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4853        if self
4854            .source_sensitive_functions
4855            .contains(&span_key(function.span))
4856        {
4857            return;
4858        }
4859        walk_mut::walk_arrow_function_expression(self, function);
4860    }
4861
4862    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4863        if self.with_statements.contains(&span_key(statement.span)) {
4864            return;
4865        }
4866        walk_mut::walk_with_statement(self, statement);
4867    }
4868
4869    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
4870        walk_mut::walk_call_expression(self, call);
4871        let property = match &call.callee {
4872            Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
4873            _ => None,
4874        };
4875        let mut callback_index = None;
4876        if matches!(property, Some("on" | "once" | "addListener"))
4877            && matches!(
4878                call.arguments.first(),
4879                Some(Argument::StringLiteral(event))
4880                    if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
4881            )
4882        {
4883            callback_index = Some(1);
4884        } else if Self::callee_is(&call.callee, "createServer") {
4885            callback_index = call.arguments.iter().rposition(Self::callback_candidate);
4886        }
4887        if let Some(index) = callback_index
4888            && let Some(argument) = call.arguments.get_mut(index)
4889        {
4890            self.wrap_argument(argument);
4891        }
4892    }
4893}
4894
4895struct CandidateNames<'s> {
4896    source: &'s str,
4897    allocated: Vec<String>,
4898    suffix: usize,
4899}
4900
4901impl<'s> CandidateNames<'s> {
4902    fn new(source: &'s str) -> Self {
4903        Self {
4904            source,
4905            allocated: Vec::new(),
4906            suffix: 0,
4907        }
4908    }
4909
4910    fn allocate(&mut self, base: &str) -> String {
4911        loop {
4912            let candidate = if self.suffix == 0 {
4913                base.to_string()
4914            } else {
4915                format!("{base}{}", self.suffix)
4916            };
4917            self.suffix += 1;
4918            if !self.source.contains(&candidate) && !self.allocated.contains(&candidate) {
4919                self.allocated.push(candidate.clone());
4920                return candidate;
4921            }
4922        }
4923    }
4924}
4925
4926struct ControlProbeV2Transformer<'a, 's> {
4927    ast: AstBuilder<'a>,
4928    decisions: &'s [CandidateDecision],
4929    mcdc_begin: String,
4930    mcdc_condition: String,
4931    mcdc_end: String,
4932    mcdc_end_v2: String,
4933    probe_file_v2: String,
4934    names: CandidateNames<'s>,
4935    scope_declarations: Vec<Vec<String>>,
4936    decision_index: usize,
4937    parameter_depth: usize,
4938    source_sensitive_functions: HashSet<SpanKey>,
4939    with_statements: HashSet<SpanKey>,
4940}
4941
4942#[derive(Clone, Copy)]
4943struct DecisionPlan {
4944    index: usize,
4945    condition_count: usize,
4946    inline_frame: bool,
4947}
4948
4949impl<'a> ControlProbeV2Transformer<'a, '_> {
4950    fn enter_declaration_scope(&mut self) {
4951        self.scope_declarations.push(Vec::new());
4952    }
4953
4954    fn leave_declaration_scope(&mut self) -> Vec<String> {
4955        self.scope_declarations
4956            .pop()
4957            .expect("program/function scope stack must remain balanced")
4958    }
4959
4960    fn allocate_scratch(&mut self, base: &str) -> String {
4961        let name = self.names.allocate(base);
4962        self.scope_declarations
4963            .last_mut()
4964            .expect("a control decision must be inside a program or function body")
4965            .push(name.clone());
4966        name
4967    }
4968
4969    fn scratch_for(&mut self, base: &str, inline: bool) -> String {
4970        if inline {
4971            self.names.allocate(base)
4972        } else {
4973            self.allocate_scratch(base)
4974        }
4975    }
4976
4977    fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
4978        let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
4979            self.ast.variable_declarator(
4980                Span::default(),
4981                VariableDeclarationKind::Let,
4982                self.ast
4983                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
4984                NONE,
4985                None,
4986                false,
4987            )
4988        }));
4989        let body = self.ast.alloc_function_body(
4990            Span::default(),
4991            self.ast.vec(),
4992            self.ast.vec_from_array([
4993                Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
4994                    Span::default(),
4995                    VariableDeclarationKind::Let,
4996                    declarations,
4997                    false,
4998                )),
4999                self.ast.statement_return(Span::default(), Some(expression)),
5000            ]),
5001        );
5002        let params = self.ast.alloc_formal_parameters(
5003            Span::default(),
5004            FormalParameterKind::ArrowFormalParameters,
5005            self.ast.vec(),
5006            NONE,
5007        );
5008        let arrow = self.ast.expression_arrow_function(
5009            Span::default(),
5010            false,
5011            false,
5012            NONE,
5013            params,
5014            NONE,
5015            body,
5016        );
5017        self.ast
5018            .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
5019    }
5020
5021    fn prepend_declarations(
5022        &self,
5023        names: Vec<String>,
5024        statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
5025    ) {
5026        if names.is_empty() {
5027            return;
5028        }
5029        let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
5030            self.ast.variable_declarator(
5031                Span::default(),
5032                VariableDeclarationKind::Let,
5033                self.ast
5034                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
5035                NONE,
5036                None,
5037                false,
5038            )
5039        }));
5040        statements.insert(
5041            0,
5042            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
5043                Span::default(),
5044                VariableDeclarationKind::Let,
5045                declarators,
5046                false,
5047            )),
5048        );
5049    }
5050
5051    fn identifier(&self, name: &str) -> Expression<'a> {
5052        self.ast
5053            .expression_identifier(Span::default(), self.ast.ident(name))
5054    }
5055
5056    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
5057        let target = self
5058            .ast
5059            .simple_assignment_target_assignment_target_identifier(
5060                Span::default(),
5061                self.ast.ident(name),
5062            );
5063        AssignmentTarget::from(target)
5064    }
5065
5066    fn number(&self, value: u64) -> Expression<'a> {
5067        self.ast.expression_numeric_literal(
5068            Span::default(),
5069            value as f64,
5070            None,
5071            NumberBase::Decimal,
5072        )
5073    }
5074
5075    fn string(&self, value: &str) -> Expression<'a> {
5076        self.ast
5077            .expression_string_literal(Span::default(), self.ast.str(value), None)
5078    }
5079
5080    fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
5081        self.ast.object_property_kind_object_property(
5082            Span::default(),
5083            PropertyKind::Init,
5084            self.ast
5085                .property_key_static_identifier(Span::default(), self.ast.ident(name)),
5086            value,
5087            false,
5088            false,
5089            false,
5090        )
5091    }
5092
5093    fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
5094        let conditions = self.ast.expression_array(
5095            Span::default(),
5096            self.ast.vec_from_iter(
5097                decision
5098                    .conditions
5099                    .iter()
5100                    .map(|condition| ArrayExpressionElement::from(self.string(condition))),
5101            ),
5102        );
5103        let properties = self.ast.vec_from_array([
5104            self.object_property("id", self.string(&decision.id)),
5105            self.object_property("file", self.string(&decision.file)),
5106            self.object_property("line", self.number(decision.line as u64)),
5107            self.object_property("column", self.number(decision.column as u64)),
5108            self.object_property("source", self.string(&decision.source)),
5109            self.object_property("conditions", conditions),
5110            self.object_property("kind", self.string(&decision.kind)),
5111        ]);
5112        Expression::ObjectExpression(
5113            self.ast
5114                .alloc_object_expression(Span::default(), properties),
5115        )
5116    }
5117
5118    fn instrument_condition(
5119        &self,
5120        expression: Expression<'a>,
5121        frame_name: &str,
5122        temporary_name: &str,
5123        index: usize,
5124    ) -> Expression<'a> {
5125        let assign_value = self.ast.expression_assignment(
5126            Span::default(),
5127            AssignmentOperator::Assign,
5128            self.assignment_target(temporary_name),
5129            expression,
5130        );
5131        let weight = 3_u64.pow(index as u32);
5132        let digit = self.ast.expression_conditional(
5133            Span::default(),
5134            self.identifier(temporary_name),
5135            self.number(weight * 2),
5136            self.number(weight),
5137        );
5138        let add_digit = self.ast.expression_assignment(
5139            Span::default(),
5140            AssignmentOperator::Addition,
5141            self.assignment_target(frame_name),
5142            digit,
5143        );
5144        self.ast.expression_sequence(
5145            Span::default(),
5146            self.ast
5147                .vec_from_array([assign_value, add_digit, self.identifier(temporary_name)]),
5148        )
5149    }
5150
5151    fn instrument_conditions(
5152        &self,
5153        expression: &mut Expression<'a>,
5154        frame_name: &str,
5155        temporary_names: &[String],
5156        next_index: &mut usize,
5157    ) {
5158        match expression {
5159            Expression::ParenthesizedExpression(parenthesized) => self.instrument_conditions(
5160                &mut parenthesized.expression,
5161                frame_name,
5162                temporary_names,
5163                next_index,
5164            ),
5165            Expression::LogicalExpression(logical)
5166                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5167            {
5168                self.instrument_conditions(
5169                    &mut logical.left,
5170                    frame_name,
5171                    temporary_names,
5172                    next_index,
5173                );
5174                self.instrument_conditions(
5175                    &mut logical.right,
5176                    frame_name,
5177                    temporary_names,
5178                    next_index,
5179                );
5180            }
5181            Expression::UnaryExpression(unary)
5182                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5183            {
5184                self.instrument_conditions(
5185                    &mut unary.argument,
5186                    frame_name,
5187                    temporary_names,
5188                    next_index,
5189                );
5190            }
5191            _ => {
5192                let index = *next_index;
5193                *next_index += 1;
5194                let original = expression.take_in(self.ast.allocator);
5195                *expression =
5196                    self.instrument_condition(original, frame_name, &temporary_names[index], index);
5197            }
5198        }
5199    }
5200
5201    fn instrument_condition_v1(
5202        &self,
5203        expression: Expression<'a>,
5204        frame_name: &str,
5205        index: usize,
5206    ) -> Expression<'a> {
5207        self.ast.expression_call(
5208            Span::default(),
5209            self.identifier(&self.mcdc_condition),
5210            NONE,
5211            self.ast.vec_from_array([
5212                Argument::from(self.identifier(frame_name)),
5213                Argument::from(self.number(index as u64)),
5214                Argument::from(expression),
5215            ]),
5216            false,
5217        )
5218    }
5219
5220    fn instrument_conditions_v1(
5221        &self,
5222        expression: &mut Expression<'a>,
5223        frame_name: &str,
5224        next_index: &mut usize,
5225    ) {
5226        match expression {
5227            Expression::ParenthesizedExpression(parenthesized) => {
5228                self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
5229            }
5230            Expression::LogicalExpression(logical)
5231                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5232            {
5233                self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
5234                self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
5235            }
5236            Expression::UnaryExpression(unary)
5237                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5238            {
5239                self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
5240            }
5241            _ => {
5242                let index = *next_index;
5243                *next_index += 1;
5244                let original = expression.take_in(self.ast.allocator);
5245                *expression = self.instrument_condition_v1(original, frame_name, index);
5246            }
5247        }
5248    }
5249
5250    fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
5251        let mut condition_spans = Vec::new();
5252        collect_conditions(test, &mut condition_spans);
5253        let plan = DecisionPlan {
5254            index: self.decision_index,
5255            condition_count: condition_spans.len(),
5256            inline_frame: self.parameter_depth > 0,
5257        };
5258        self.decision_index += 1;
5259        plan
5260    }
5261
5262    fn apply_decision(&mut self, test: &mut Expression<'a>, plan: DecisionPlan) {
5263        if plan.condition_count > 32 {
5264            let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
5265            let mut next_index = 0;
5266            self.instrument_conditions_v1(test, &frame_name, &mut next_index);
5267            debug_assert_eq!(next_index, plan.condition_count);
5268
5269            let decision = &self.decisions[plan.index];
5270            let begin = self.ast.expression_call(
5271                Span::default(),
5272                self.identifier(&self.mcdc_begin),
5273                NONE,
5274                self.ast.vec_from_array([
5275                    Argument::from(self.string(&decision.id)),
5276                    Argument::from(self.decision_meta(decision)),
5277                ]),
5278                false,
5279            );
5280            let assign_frame = self.ast.expression_assignment(
5281                Span::default(),
5282                AssignmentOperator::Assign,
5283                self.assignment_target(&frame_name),
5284                begin,
5285            );
5286            let instrumented = test.take_in(self.ast.allocator);
5287            let end = self.ast.expression_call(
5288                Span::default(),
5289                self.identifier(&self.mcdc_end),
5290                NONE,
5291                self.ast.vec_from_array([
5292                    Argument::from(self.identifier(&frame_name)),
5293                    Argument::from(instrumented),
5294                ]),
5295                false,
5296            );
5297            let observed = self.ast.expression_sequence(
5298                Span::default(),
5299                self.ast.vec_from_array([assign_frame, end]),
5300            );
5301            *test = if plan.inline_frame {
5302                self.wrap_inline_frame(observed, &[frame_name])
5303            } else {
5304                observed
5305            };
5306            return;
5307        }
5308
5309        let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
5310        let result_name = self.scratch_for("_supercovMcdcResult", plan.inline_frame);
5311        let temporary_names = (0..plan.condition_count)
5312            .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
5313            .collect::<Vec<_>>();
5314        let mut next_index = 0;
5315        self.instrument_conditions(test, &frame_name, &temporary_names, &mut next_index);
5316        debug_assert_eq!(next_index, plan.condition_count);
5317
5318        let original = test.take_in(self.ast.allocator);
5319        let assign_frame = self.ast.expression_assignment(
5320            Span::default(),
5321            AssignmentOperator::Assign,
5322            self.assignment_target(&frame_name),
5323            self.number(0),
5324        );
5325        let assign_result = self.ast.expression_assignment(
5326            Span::default(),
5327            AssignmentOperator::Assign,
5328            self.assignment_target(&result_name),
5329            original,
5330        );
5331        let arguments = self.ast.vec_from_array([
5332            Argument::from(self.identifier(&self.probe_file_v2)),
5333            Argument::from(self.number(plan.index as u64)),
5334            Argument::from(self.identifier(&frame_name)),
5335            Argument::from(self.identifier(&result_name)),
5336        ]);
5337        let record = self.ast.expression_call(
5338            Span::default(),
5339            self.identifier(&self.mcdc_end_v2),
5340            NONE,
5341            arguments,
5342            false,
5343        );
5344        let observed = self.ast.expression_sequence(
5345            Span::default(),
5346            self.ast.vec_from_array([
5347                assign_frame,
5348                assign_result,
5349                record,
5350                self.identifier(&result_name),
5351            ]),
5352        );
5353        *test = if plan.inline_frame {
5354            let mut names = vec![frame_name, result_name];
5355            names.extend(temporary_names);
5356            self.wrap_inline_frame(observed, &names)
5357        } else {
5358            observed
5359        };
5360    }
5361}
5362
5363impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
5364    fn visit_program(&mut self, program: &mut Program<'a>) {
5365        self.enter_declaration_scope();
5366        walk_mut::walk_program(self, program);
5367        let declarations = self.leave_declaration_scope();
5368        self.prepend_declarations(declarations, &mut program.body);
5369    }
5370
5371    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5372        self.enter_declaration_scope();
5373        walk_mut::walk_function_body(self, body);
5374        let declarations = self.leave_declaration_scope();
5375        self.prepend_declarations(declarations, &mut body.statements);
5376    }
5377
5378    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5379        if self
5380            .source_sensitive_functions
5381            .contains(&span_key(function.span))
5382        {
5383            return;
5384        }
5385        let outer_parameter_depth = self.parameter_depth;
5386        self.parameter_depth = 0;
5387        walk_mut::walk_function(self, function, flags);
5388        self.parameter_depth = outer_parameter_depth;
5389    }
5390
5391    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5392        if self
5393            .source_sensitive_functions
5394            .contains(&span_key(function.span))
5395        {
5396            return;
5397        }
5398        let outer_parameter_depth = self.parameter_depth;
5399        self.parameter_depth = 0;
5400        walk_mut::walk_arrow_function_expression(self, function);
5401        self.parameter_depth = outer_parameter_depth;
5402    }
5403
5404    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
5405        self.parameter_depth += 1;
5406        walk_mut::walk_formal_parameters(self, parameters);
5407        self.parameter_depth -= 1;
5408    }
5409
5410    fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
5411        if self.with_statements.contains(&span_key(statement.span)) {
5412            return;
5413        }
5414        walk_mut::walk_with_statement(self, statement);
5415    }
5416
5417    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
5418        let plan = decision_outcome_is_variable(&statement.test)
5419            .then(|| self.reserve_decision(&statement.test));
5420        self.visit_expression(&mut statement.test);
5421        if let Some(plan) = plan {
5422            self.apply_decision(&mut statement.test, plan);
5423        }
5424        self.visit_statement(&mut statement.consequent);
5425        if let Some(alternate) = &mut statement.alternate {
5426            self.visit_statement(alternate);
5427        }
5428    }
5429
5430    fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
5431        let plan = decision_outcome_is_variable(&expression.test)
5432            .then(|| self.reserve_decision(&expression.test));
5433        self.visit_expression(&mut expression.test);
5434        if let Some(plan) = plan {
5435            self.apply_decision(&mut expression.test, plan);
5436        }
5437        self.visit_expression(&mut expression.consequent);
5438        self.visit_expression(&mut expression.alternate);
5439    }
5440
5441    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
5442        let plan = decision_outcome_is_variable(&statement.test)
5443            .then(|| self.reserve_decision(&statement.test));
5444        self.visit_expression(&mut statement.test);
5445        if let Some(plan) = plan {
5446            self.apply_decision(&mut statement.test, plan);
5447        }
5448        self.visit_statement(&mut statement.body);
5449    }
5450
5451    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
5452        let plan = decision_outcome_is_variable(&statement.test)
5453            .then(|| self.reserve_decision(&statement.test));
5454        self.visit_statement(&mut statement.body);
5455        self.visit_expression(&mut statement.test);
5456        if let Some(plan) = plan {
5457            self.apply_decision(&mut statement.test, plan);
5458        }
5459    }
5460
5461    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
5462        let plan = statement
5463            .test
5464            .as_ref()
5465            .filter(|test| decision_outcome_is_variable(test))
5466            .map(|test| self.reserve_decision(test));
5467        if let Some(init) = &mut statement.init {
5468            self.visit_for_statement_init(init);
5469        }
5470        if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
5471            self.visit_expression(test);
5472            self.apply_decision(test, plan);
5473        }
5474        if let Some(update) = &mut statement.update {
5475            self.visit_expression(update);
5476        }
5477        self.visit_statement(&mut statement.body);
5478    }
5479}
5480
5481struct DecisionCollector<'s> {
5482    source: &'s str,
5483    file: &'s str,
5484    decisions: Vec<CandidateDecision>,
5485    decision_vector_counts: Vec<usize>,
5486    decision_logical_nodes: HashSet<SpanKey>,
5487    source_sensitive_functions: &'s HashSet<SpanKey>,
5488    with_statements: &'s HashSet<SpanKey>,
5489}
5490
5491impl DecisionCollector<'_> {
5492    fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
5493        let mut condition_spans = Vec::new();
5494        collect_conditions(test, &mut condition_spans);
5495        collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
5496        // Babel treats redundant parentheses as parser metadata rather than
5497        // part of the decision node. oxc preserves a ParenthesizedExpression,
5498        // so normalize it here to keep locations and stable IDs parser-neutral.
5499        let span = transparent_expression(test).span();
5500        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
5501        self.decisions.push(CandidateDecision {
5502            id: stable_id(self.source, self.file, "decision", span, kind),
5503            file: self.file.to_string(),
5504            line,
5505            column,
5506            source: source_slice(self.source, span).to_string(),
5507            conditions: condition_spans
5508                .iter()
5509                .map(|condition| source_slice(self.source, *condition).to_string())
5510                .collect(),
5511            kind: kind.to_string(),
5512        });
5513        self.decision_vector_counts.push(
5514            if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
5515                reachable_vector_count(test, &condition_spans)
5516            } else {
5517                0
5518            },
5519        );
5520    }
5521}
5522
5523/// MC/DC applies only when a decision can produce both Boolean outcomes.
5524/// This deliberately recognizes only semantics that are provable from syntax;
5525/// it never evaluates user code or assumes values for identifiers/calls.
5526fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
5527    syntactic_boolean_outcome(expression).is_none()
5528}
5529
5530fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
5531    match transparent_expression(expression) {
5532        Expression::BooleanLiteral(literal) => Some(literal.value),
5533        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
5534            syntactic_boolean_outcome(&unary.argument).map(|value| !value)
5535        }
5536        Expression::LogicalExpression(logical)
5537            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5538        {
5539            let left = syntactic_boolean_outcome(&logical.left);
5540            let right = syntactic_boolean_outcome(&logical.right);
5541            match logical.operator {
5542                LogicalOperator::And => match (left, right) {
5543                    (Some(false), _) | (_, Some(false)) => Some(false),
5544                    (Some(true), value) | (value, Some(true)) => value,
5545                    _ => None,
5546                },
5547                LogicalOperator::Or => match (left, right) {
5548                    (Some(true), _) | (_, Some(true)) => Some(true),
5549                    (Some(false), value) | (value, Some(false)) => value,
5550                    _ => None,
5551                },
5552                LogicalOperator::Coalesce => None,
5553            }
5554        }
5555        _ => None,
5556    }
5557}
5558
5559#[derive(Default)]
5560struct CoverageSurfaceScanner {
5561    found: bool,
5562}
5563
5564impl<'a> Visit<'a> for CoverageSurfaceScanner {
5565    fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
5566        self.found = true;
5567    }
5568
5569    fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
5570        self.found = true;
5571    }
5572
5573    fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
5574        self.found = true;
5575    }
5576
5577    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
5578        self.found = true;
5579    }
5580
5581    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
5582        self.found = true;
5583    }
5584
5585    fn visit_class(&mut self, _class: &Class<'a>) {
5586        self.found = true;
5587    }
5588
5589    fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
5590        if expression.operator.is_logical() {
5591            self.found = true;
5592        } else {
5593            walk::walk_assignment_expression(self, expression);
5594        }
5595    }
5596}
5597
5598fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
5599    fn visit_condition(expression: &Expression<'_>) -> bool {
5600        let mut scanner = CoverageSurfaceScanner::default();
5601        scanner.visit_expression(expression);
5602        !scanner.found
5603    }
5604    match transparent_expression(expression) {
5605        Expression::LogicalExpression(logical)
5606            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5607        {
5608            decision_conditions_are_transparent(&logical.left)
5609                && decision_conditions_are_transparent(&logical.right)
5610        }
5611        Expression::UnaryExpression(unary)
5612            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5613        {
5614            decision_conditions_are_transparent(&unary.argument)
5615        }
5616        condition => visit_condition(condition),
5617    }
5618}
5619
5620fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
5621    fn evaluate(
5622        expression: &Expression<'_>,
5623        assignment: usize,
5624        encoded: &mut usize,
5625        indices: &HashMap<SpanKey, usize>,
5626    ) -> bool {
5627        match transparent_expression(expression) {
5628            Expression::LogicalExpression(logical)
5629                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5630            {
5631                let left = evaluate(&logical.left, assignment, encoded, indices);
5632                if logical.operator == LogicalOperator::And {
5633                    left && evaluate(&logical.right, assignment, encoded, indices)
5634                } else {
5635                    left || evaluate(&logical.right, assignment, encoded, indices)
5636                }
5637            }
5638            Expression::UnaryExpression(unary)
5639                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5640            {
5641                !evaluate(&unary.argument, assignment, encoded, indices)
5642            }
5643            condition => {
5644                let index = indices
5645                    .get(&span_key(condition.span()))
5646                    .expect("decision condition index must remain stable");
5647                let value = assignment & (1 << index) != 0;
5648                *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
5649                value
5650            }
5651        }
5652    }
5653
5654    let indices = conditions
5655        .iter()
5656        .enumerate()
5657        .map(|(index, span)| (span_key(*span), index))
5658        .collect::<HashMap<_, _>>();
5659    let mut vectors = HashSet::new();
5660    for assignment in 0..(1_usize << conditions.len()) {
5661        let mut encoded = 0;
5662        let outcome = evaluate(expression, assignment, &mut encoded, &indices);
5663        vectors.insert(encoded * 2 + usize::from(outcome));
5664    }
5665    vectors.len()
5666}
5667
5668fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
5669    match expression {
5670        Expression::ParenthesizedExpression(parenthesized) => {
5671            collect_decision_logical_nodes(&parenthesized.expression, nodes);
5672        }
5673        Expression::LogicalExpression(logical)
5674            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5675        {
5676            nodes.insert(span_key(logical.span));
5677            collect_decision_logical_nodes(&logical.left, nodes);
5678            collect_decision_logical_nodes(&logical.right, nodes);
5679        }
5680        Expression::UnaryExpression(unary)
5681            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5682        {
5683            collect_decision_logical_nodes(&unary.argument, nodes);
5684        }
5685        _ => {}
5686    }
5687}
5688
5689#[derive(Default)]
5690struct LogicalBranchAnalysis {
5691    branches: Vec<CandidateBranch>,
5692    logical_targets: HashMap<SpanKey, (String, String)>,
5693}
5694
5695#[derive(Default)]
5696struct LogicalAssignmentAnalysis {
5697    branches: Vec<CandidateBranch>,
5698    targets: HashMap<SpanKey, (String, String)>,
5699}
5700
5701#[derive(Default)]
5702struct OptionalMemberAnalysis {
5703    branches: Vec<CandidateBranch>,
5704    targets: HashMap<SpanKey, (String, String)>,
5705}
5706
5707#[derive(Default)]
5708struct OptionalCallAnalysis {
5709    branches: Vec<CandidateBranch>,
5710    sites: HashMap<SpanKey, (String, String)>,
5711    roots: HashMap<SpanKey, Vec<SpanKey>>,
5712    limitations: Vec<CandidateLimitation>,
5713}
5714
5715#[derive(Clone)]
5716struct DefaultTarget {
5717    default_id: String,
5718    provided_id: String,
5719    inferred_name: Option<String>,
5720}
5721
5722#[derive(Default)]
5723struct DefaultAnalysis {
5724    branches: Vec<CandidateBranch>,
5725    parameter_targets: HashMap<SpanKey, DefaultTarget>,
5726    binding_targets: HashMap<SpanKey, DefaultTarget>,
5727    limitations: Vec<CandidateLimitation>,
5728}
5729
5730#[derive(Clone)]
5731struct ExtendedTarget {
5732    first_id: String,
5733    second_id: String,
5734}
5735
5736#[derive(Default)]
5737struct ExtendedAnalysis {
5738    branches: Vec<CandidateBranch>,
5739    try_targets: HashMap<SpanKey, ExtendedTarget>,
5740    loop_targets: HashMap<SpanKey, ExtendedTarget>,
5741}
5742
5743#[derive(Clone)]
5744struct SwitchTarget {
5745    case_ids: Vec<String>,
5746    no_match_id: Option<String>,
5747}
5748
5749#[derive(Default)]
5750struct SwitchAnalysis {
5751    branches: Vec<CandidateBranch>,
5752    targets: HashMap<SpanKey, SwitchTarget>,
5753}
5754
5755struct SwitchCollector<'s> {
5756    source: &'s str,
5757    file: &'s str,
5758    source_sensitive_functions: &'s HashSet<SpanKey>,
5759    unsafe_function_depth: usize,
5760    with_depth: usize,
5761    suppressed_depth: usize,
5762    suppressed_nodes: Vec<bool>,
5763    analysis: SwitchAnalysis,
5764}
5765
5766impl SwitchCollector<'_> {
5767    fn unsafe_context(&self) -> bool {
5768        self.unsafe_function_depth > 0 || self.with_depth > 0
5769    }
5770
5771    fn exit_source_function(&mut self, span: Span) {
5772        if self.source_sensitive_functions.contains(&span_key(span)) {
5773            self.unsafe_function_depth -= 1;
5774        }
5775    }
5776}
5777
5778impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
5779    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
5780        if self
5781            .source_sensitive_functions
5782            .contains(&span_key(node.span))
5783        {
5784            self.unsafe_function_depth += 1;
5785        }
5786    }
5787
5788    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
5789        self.exit_source_function(node.span);
5790    }
5791
5792    fn enter_arrow_function_expression(
5793        &mut self,
5794        node: &mut ArrowFunctionExpression<'a>,
5795        _context: &mut TraverseCtx<'a, ()>,
5796    ) {
5797        if self
5798            .source_sensitive_functions
5799            .contains(&span_key(node.span))
5800        {
5801            self.unsafe_function_depth += 1;
5802        }
5803    }
5804
5805    fn exit_arrow_function_expression(
5806        &mut self,
5807        node: &mut ArrowFunctionExpression<'a>,
5808        _context: &mut TraverseCtx<'a, ()>,
5809    ) {
5810        self.exit_source_function(node.span);
5811    }
5812
5813    fn enter_with_statement(
5814        &mut self,
5815        _node: &mut WithStatement<'a>,
5816        _context: &mut TraverseCtx<'a, ()>,
5817    ) {
5818        self.with_depth += 1;
5819    }
5820
5821    fn exit_with_statement(
5822        &mut self,
5823        _node: &mut WithStatement<'a>,
5824        _context: &mut TraverseCtx<'a, ()>,
5825    ) {
5826        self.with_depth -= 1;
5827    }
5828
5829    fn enter_switch_statement(
5830        &mut self,
5831        node: &mut SwitchStatement<'a>,
5832        _context: &mut TraverseCtx<'a, ()>,
5833    ) {
5834        let has_default = node.cases.iter().any(|case| case.test.is_none());
5835        let transformed = self.suppressed_depth == 0 && !self.unsafe_context();
5836        let suppresses = transformed && !has_default;
5837        self.suppressed_nodes.push(suppresses);
5838        if suppresses {
5839            self.suppressed_depth += 1;
5840        }
5841        if !transformed {
5842            return;
5843        }
5844        let id = stable_id(self.source, self.file, "switch", node.span, "");
5845        let mut case_ids = Vec::with_capacity(node.cases.len());
5846        let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
5847        for (index, case) in node.cases.iter().enumerate() {
5848            let alternative_id = format!("{id}:case:{index}");
5849            let label = case.test.as_ref().map_or_else(
5850                || "default".to_string(),
5851                |test| format!("case {}", source_slice(self.source, test.span())),
5852            );
5853            case_ids.push(alternative_id.clone());
5854            alternatives.push(CandidateBranchAlternative {
5855                id: alternative_id,
5856                label,
5857            });
5858        }
5859        let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
5860        if let Some(no_match_id) = &no_match_id {
5861            alternatives.push(CandidateBranchAlternative {
5862                id: no_match_id.clone(),
5863                label: "no matching case".to_string(),
5864            });
5865        }
5866        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
5867        self.analysis.branches.push(CandidateBranch {
5868            id,
5869            kind: "switch".to_string(),
5870            file: self.file.to_string(),
5871            line,
5872            column,
5873            source: source_slice(self.source, node.discriminant.span()).to_string(),
5874            alternatives,
5875        });
5876        self.analysis.targets.insert(
5877            span_key(node.span),
5878            SwitchTarget {
5879                case_ids,
5880                no_match_id,
5881            },
5882        );
5883    }
5884
5885    fn exit_switch_statement(
5886        &mut self,
5887        _node: &mut SwitchStatement<'a>,
5888        _context: &mut TraverseCtx<'a, ()>,
5889    ) {
5890        if self
5891            .suppressed_nodes
5892            .pop()
5893            .expect("switch collector stack must remain balanced")
5894        {
5895            self.suppressed_depth -= 1;
5896        }
5897    }
5898}
5899
5900fn collect_switch_branches<'a>(
5901    allocator: &'a Allocator,
5902    program: &mut Program<'a>,
5903    source: &str,
5904    file: &str,
5905    source_sensitive_functions: &HashSet<SpanKey>,
5906) -> SwitchAnalysis {
5907    let mut collector = SwitchCollector {
5908        source,
5909        file,
5910        source_sensitive_functions,
5911        unsafe_function_depth: 0,
5912        with_depth: 0,
5913        suppressed_depth: 0,
5914        suppressed_nodes: Vec::new(),
5915        analysis: SwitchAnalysis::default(),
5916    };
5917    traverse_mut(&mut collector, allocator, program, Default::default(), ());
5918    collector.analysis
5919}
5920
5921struct ExtendedCollector<'s> {
5922    source: &'s str,
5923    file: &'s str,
5924    source_sensitive_functions: &'s HashSet<SpanKey>,
5925    unsafe_function_depth: usize,
5926    with_depth: usize,
5927    analysis: ExtendedAnalysis,
5928}
5929
5930impl ExtendedCollector<'_> {
5931    fn unsafe_context(&self) -> bool {
5932        self.unsafe_function_depth > 0 || self.with_depth > 0
5933    }
5934
5935    fn enter_try(&mut self, node: &TryStatement<'_>) {
5936        let transformed = !self.unsafe_context() && node.handler.is_some();
5937        if !transformed {
5938            return;
5939        }
5940        let id = stable_id(self.source, self.file, "try-catch", node.span, "");
5941        let success_id = format!("{id}:success");
5942        let catch_id = format!("{id}:catch");
5943        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
5944        self.analysis.branches.push(CandidateBranch {
5945            id,
5946            kind: "try-catch".to_string(),
5947            file: self.file.to_string(),
5948            line,
5949            column,
5950            source: "try / catch".to_string(),
5951            alternatives: vec![
5952                CandidateBranchAlternative {
5953                    id: success_id.clone(),
5954                    label: "try completed without catch".to_string(),
5955                },
5956                CandidateBranchAlternative {
5957                    id: catch_id.clone(),
5958                    label: "catch entered".to_string(),
5959                },
5960            ],
5961        });
5962        self.analysis.try_targets.insert(
5963            span_key(node.span),
5964            ExtendedTarget {
5965                first_id: success_id,
5966                second_id: catch_id,
5967            },
5968        );
5969    }
5970
5971    fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
5972        let transformed = !self.unsafe_context();
5973        if !transformed {
5974            return;
5975        }
5976        let id = stable_id(self.source, self.file, kind, span, "");
5977        let zero_id = format!("{id}:zero");
5978        let entered_id = format!("{id}:entered");
5979        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
5980        self.analysis.branches.push(CandidateBranch {
5981            id,
5982            kind: kind.to_string(),
5983            file: self.file.to_string(),
5984            line,
5985            column,
5986            source: source_slice(self.source, right.span()).to_string(),
5987            alternatives: vec![
5988                CandidateBranchAlternative {
5989                    id: zero_id.clone(),
5990                    label: "zero iterations".to_string(),
5991                },
5992                CandidateBranchAlternative {
5993                    id: entered_id.clone(),
5994                    label: "one or more iterations".to_string(),
5995                },
5996            ],
5997        });
5998        self.analysis.loop_targets.insert(
5999            span_key(span),
6000            ExtendedTarget {
6001                first_id: zero_id,
6002                second_id: entered_id,
6003            },
6004        );
6005    }
6006
6007    fn exit_source_function(&mut self, span: Span) {
6008        if self.source_sensitive_functions.contains(&span_key(span)) {
6009            self.unsafe_function_depth -= 1;
6010        }
6011    }
6012}
6013
6014impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
6015    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6016        if self
6017            .source_sensitive_functions
6018            .contains(&span_key(node.span))
6019        {
6020            self.unsafe_function_depth += 1;
6021        }
6022    }
6023
6024    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6025        self.exit_source_function(node.span);
6026    }
6027
6028    fn enter_arrow_function_expression(
6029        &mut self,
6030        node: &mut ArrowFunctionExpression<'a>,
6031        _context: &mut TraverseCtx<'a, ()>,
6032    ) {
6033        if self
6034            .source_sensitive_functions
6035            .contains(&span_key(node.span))
6036        {
6037            self.unsafe_function_depth += 1;
6038        }
6039    }
6040
6041    fn exit_arrow_function_expression(
6042        &mut self,
6043        node: &mut ArrowFunctionExpression<'a>,
6044        _context: &mut TraverseCtx<'a, ()>,
6045    ) {
6046        self.exit_source_function(node.span);
6047    }
6048
6049    fn enter_with_statement(
6050        &mut self,
6051        _node: &mut WithStatement<'a>,
6052        _context: &mut TraverseCtx<'a, ()>,
6053    ) {
6054        self.with_depth += 1;
6055    }
6056
6057    fn exit_with_statement(
6058        &mut self,
6059        _node: &mut WithStatement<'a>,
6060        _context: &mut TraverseCtx<'a, ()>,
6061    ) {
6062        self.with_depth -= 1;
6063    }
6064
6065    fn enter_try_statement(
6066        &mut self,
6067        node: &mut TryStatement<'a>,
6068        _context: &mut TraverseCtx<'a, ()>,
6069    ) {
6070        self.enter_try(node);
6071    }
6072
6073    fn exit_try_statement(
6074        &mut self,
6075        _node: &mut TryStatement<'a>,
6076        _context: &mut TraverseCtx<'a, ()>,
6077    ) {
6078    }
6079
6080    fn enter_for_in_statement(
6081        &mut self,
6082        node: &mut ForInStatement<'a>,
6083        _context: &mut TraverseCtx<'a, ()>,
6084    ) {
6085        self.enter_loop(node.span, &node.right, "for-in");
6086    }
6087
6088    fn exit_for_in_statement(
6089        &mut self,
6090        _node: &mut ForInStatement<'a>,
6091        _context: &mut TraverseCtx<'a, ()>,
6092    ) {
6093    }
6094
6095    fn enter_for_of_statement(
6096        &mut self,
6097        node: &mut ForOfStatement<'a>,
6098        _context: &mut TraverseCtx<'a, ()>,
6099    ) {
6100        self.enter_loop(node.span, &node.right, "for-of");
6101    }
6102
6103    fn exit_for_of_statement(
6104        &mut self,
6105        _node: &mut ForOfStatement<'a>,
6106        _context: &mut TraverseCtx<'a, ()>,
6107    ) {
6108    }
6109}
6110
6111fn collect_extended_branches<'a>(
6112    allocator: &'a Allocator,
6113    program: &mut Program<'a>,
6114    source: &str,
6115    file: &str,
6116    source_sensitive_functions: &HashSet<SpanKey>,
6117) -> ExtendedAnalysis {
6118    let mut collector = ExtendedCollector {
6119        source,
6120        file,
6121        source_sensitive_functions,
6122        unsafe_function_depth: 0,
6123        with_depth: 0,
6124        analysis: ExtendedAnalysis::default(),
6125    };
6126    traverse_mut(&mut collector, allocator, program, Default::default(), ());
6127    collector.analysis
6128}
6129
6130struct DefaultCollector<'s> {
6131    source: &'s str,
6132    file: &'s str,
6133    source_sensitive_functions: &'s HashSet<SpanKey>,
6134    unsafe_function_depth: usize,
6135    with_depth: usize,
6136    analysis: DefaultAnalysis,
6137}
6138
6139impl DefaultCollector<'_> {
6140    fn unsafe_context(&self) -> bool {
6141        self.unsafe_function_depth > 0 || self.with_depth > 0
6142    }
6143
6144    fn target(
6145        &mut self,
6146        span: Span,
6147        left: &BindingPattern<'_>,
6148        right: &Expression<'_>,
6149    ) -> DefaultTarget {
6150        let id = stable_id(self.source, self.file, "default-value", span, "");
6151        let default_id = format!("{id}:default");
6152        let provided_id = format!("{id}:provided");
6153        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
6154        self.analysis.branches.push(CandidateBranch {
6155            id,
6156            kind: "default-value".to_string(),
6157            file: self.file.to_string(),
6158            line,
6159            column,
6160            source: source_slice(self.source, span).to_string(),
6161            alternatives: vec![
6162                CandidateBranchAlternative {
6163                    id: default_id.clone(),
6164                    label: "default evaluated".to_string(),
6165                },
6166                CandidateBranchAlternative {
6167                    id: provided_id.clone(),
6168                    label: "value provided".to_string(),
6169                },
6170            ],
6171        });
6172        DefaultTarget {
6173            default_id,
6174            provided_id,
6175            inferred_name: binding_identifier_name(left)
6176                .filter(|_| expression_is_anonymous_definition(right)),
6177        }
6178    }
6179
6180    fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
6181        match pattern {
6182            BindingPattern::AssignmentPattern(assignment) => {
6183                let target = self.target(assignment.span, &assignment.left, &assignment.right);
6184                if parameter {
6185                    self.analysis
6186                        .parameter_targets
6187                        .insert(span_key(assignment.span), target);
6188                } else {
6189                    self.analysis
6190                        .binding_targets
6191                        .insert(span_key(assignment.span), target);
6192                }
6193                self.collect_binding_pattern(&assignment.left, parameter);
6194            }
6195            BindingPattern::ObjectPattern(object) => {
6196                for property in &object.properties {
6197                    self.collect_binding_pattern(&property.value, parameter);
6198                }
6199                if let Some(rest) = &object.rest {
6200                    self.collect_binding_pattern(&rest.argument, parameter);
6201                }
6202            }
6203            BindingPattern::ArrayPattern(array) => {
6204                for element in array.elements.iter().flatten() {
6205                    self.collect_binding_pattern(element, parameter);
6206                }
6207                if let Some(rest) = &array.rest {
6208                    self.collect_binding_pattern(&rest.argument, parameter);
6209                }
6210            }
6211            BindingPattern::BindingIdentifier(_) => {}
6212        }
6213    }
6214
6215    fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
6216        for parameter in &parameters.items {
6217            if let Some(initializer) = &parameter.initializer {
6218                // Babel models a formal-parameter default as an AssignmentPattern whose
6219                // source range starts at the binding and ends at the initializer. oxc
6220                // keeps the initializer beside the BindingPattern and, for a TypeScript
6221                // parameter property, includes access/readonly modifiers in
6222                // FormalParameter::span. Keep the outer span as the transformation key,
6223                // but publish the same semantic default-expression range as Babel.
6224                let default_span =
6225                    Span::new(parameter.pattern.span().start, initializer.span().end);
6226                let target = self.target(default_span, &parameter.pattern, initializer);
6227                self.analysis
6228                    .parameter_targets
6229                    .insert(span_key(parameter.span), target);
6230            }
6231            self.collect_binding_pattern(&parameter.pattern, true);
6232        }
6233        if let Some(rest) = &parameters.rest {
6234            self.collect_binding_pattern(&rest.rest.argument, true);
6235        }
6236    }
6237
6238    fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
6239        match pattern {
6240            BindingPattern::AssignmentPattern(_) => true,
6241            BindingPattern::ObjectPattern(object) => {
6242                object
6243                    .properties
6244                    .iter()
6245                    .any(|property| Self::has_binding_default(&property.value))
6246                    || object
6247                        .rest
6248                        .as_ref()
6249                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
6250            }
6251            BindingPattern::ArrayPattern(array) => {
6252                array
6253                    .elements
6254                    .iter()
6255                    .flatten()
6256                    .any(Self::has_binding_default)
6257                    || array
6258                        .rest
6259                        .as_ref()
6260                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
6261            }
6262            BindingPattern::BindingIdentifier(_) => false,
6263        }
6264    }
6265
6266    fn exit_source_function(&mut self, span: Span) {
6267        if self.source_sensitive_functions.contains(&span_key(span)) {
6268            self.unsafe_function_depth -= 1;
6269        }
6270    }
6271}
6272
6273impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
6274    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6275        if self
6276            .source_sensitive_functions
6277            .contains(&span_key(node.span))
6278        {
6279            self.unsafe_function_depth += 1;
6280            return;
6281        }
6282        if !self.unsafe_context() && node.body.is_some() {
6283            self.collect_parameters(&node.params);
6284        }
6285    }
6286
6287    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6288        self.exit_source_function(node.span);
6289    }
6290
6291    fn enter_arrow_function_expression(
6292        &mut self,
6293        node: &mut ArrowFunctionExpression<'a>,
6294        _context: &mut TraverseCtx<'a, ()>,
6295    ) {
6296        if self
6297            .source_sensitive_functions
6298            .contains(&span_key(node.span))
6299        {
6300            self.unsafe_function_depth += 1;
6301            return;
6302        }
6303        if !self.unsafe_context() {
6304            self.collect_parameters(&node.params);
6305        }
6306    }
6307
6308    fn exit_arrow_function_expression(
6309        &mut self,
6310        node: &mut ArrowFunctionExpression<'a>,
6311        _context: &mut TraverseCtx<'a, ()>,
6312    ) {
6313        self.exit_source_function(node.span);
6314    }
6315
6316    fn enter_with_statement(
6317        &mut self,
6318        _node: &mut WithStatement<'a>,
6319        _context: &mut TraverseCtx<'a, ()>,
6320    ) {
6321        self.with_depth += 1;
6322    }
6323
6324    fn exit_with_statement(
6325        &mut self,
6326        _node: &mut WithStatement<'a>,
6327        _context: &mut TraverseCtx<'a, ()>,
6328    ) {
6329        self.with_depth -= 1;
6330    }
6331
6332    fn enter_variable_declaration(
6333        &mut self,
6334        node: &mut VariableDeclaration<'a>,
6335        context: &mut TraverseCtx<'a, ()>,
6336    ) {
6337        if self.unsafe_context() {
6338            return;
6339        }
6340        let has_default = node
6341            .declarations
6342            .iter()
6343            .any(|declaration| Self::has_binding_default(&declaration.id));
6344        if !has_default {
6345            return;
6346        }
6347        if matches!(
6348            context.ancestors().next(),
6349            Some(Ancestor::ForStatementInit(_))
6350        ) {
6351            let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6352            self.analysis.limitations.push(CandidateLimitation {
6353                id: stable_id(
6354                    self.source,
6355                    self.file,
6356                    "dynamic-code",
6357                    node.span,
6358                    "for-init-default",
6359                ),
6360                kind: "dynamic-code".to_string(),
6361                file: self.file.to_string(),
6362                line,
6363                column,
6364                source: source_slice(self.source, node.span).to_string(),
6365                reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
6366            });
6367            return;
6368        }
6369        for declaration in &node.declarations {
6370            self.collect_binding_pattern(&declaration.id, false);
6371        }
6372    }
6373}
6374
6375fn collect_default_branches<'a>(
6376    allocator: &'a Allocator,
6377    program: &mut Program<'a>,
6378    source: &str,
6379    file: &str,
6380    source_sensitive_functions: &HashSet<SpanKey>,
6381) -> DefaultAnalysis {
6382    let mut collector = DefaultCollector {
6383        source,
6384        file,
6385        source_sensitive_functions,
6386        unsafe_function_depth: 0,
6387        with_depth: 0,
6388        analysis: DefaultAnalysis::default(),
6389    };
6390    traverse_mut(&mut collector, allocator, program, Default::default(), ());
6391    collector.analysis
6392}
6393
6394struct OptionalCallCollector<'s> {
6395    source: &'s str,
6396    file: &'s str,
6397    source_sensitive_functions: &'s HashSet<SpanKey>,
6398    unsafe_function_depth: usize,
6399    with_depth: usize,
6400    chain_roots: Vec<SpanKey>,
6401    analysis: OptionalCallAnalysis,
6402}
6403
6404impl OptionalCallCollector<'_> {
6405    fn unsafe_context(&self) -> bool {
6406        self.unsafe_function_depth > 0 || self.with_depth > 0
6407    }
6408
6409    fn exit_source_function(&mut self, span: Span) {
6410        if self.source_sensitive_functions.contains(&span_key(span)) {
6411            self.unsafe_function_depth -= 1;
6412        }
6413    }
6414}
6415
6416impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
6417    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6418        if self
6419            .source_sensitive_functions
6420            .contains(&span_key(node.span))
6421        {
6422            self.unsafe_function_depth += 1;
6423        }
6424    }
6425
6426    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6427        self.exit_source_function(node.span);
6428    }
6429
6430    fn enter_arrow_function_expression(
6431        &mut self,
6432        node: &mut ArrowFunctionExpression<'a>,
6433        _context: &mut TraverseCtx<'a, ()>,
6434    ) {
6435        if self
6436            .source_sensitive_functions
6437            .contains(&span_key(node.span))
6438        {
6439            self.unsafe_function_depth += 1;
6440        }
6441    }
6442
6443    fn exit_arrow_function_expression(
6444        &mut self,
6445        node: &mut ArrowFunctionExpression<'a>,
6446        _context: &mut TraverseCtx<'a, ()>,
6447    ) {
6448        self.exit_source_function(node.span);
6449    }
6450
6451    fn enter_with_statement(
6452        &mut self,
6453        _node: &mut WithStatement<'a>,
6454        _context: &mut TraverseCtx<'a, ()>,
6455    ) {
6456        self.with_depth += 1;
6457    }
6458
6459    fn exit_with_statement(
6460        &mut self,
6461        _node: &mut WithStatement<'a>,
6462        _context: &mut TraverseCtx<'a, ()>,
6463    ) {
6464        self.with_depth -= 1;
6465    }
6466
6467    fn enter_chain_expression(
6468        &mut self,
6469        node: &mut ChainExpression<'a>,
6470        context: &mut TraverseCtx<'a, ()>,
6471    ) {
6472        let root = match context.ancestors().next() {
6473            Some(Ancestor::UnaryExpressionArgument(parent))
6474                if *parent.operator() == UnaryOperator::Delete =>
6475            {
6476                span_key(*parent.span())
6477            }
6478            _ => span_key(node.span),
6479        };
6480        self.chain_roots.push(root);
6481    }
6482
6483    fn exit_chain_expression(
6484        &mut self,
6485        _node: &mut ChainExpression<'a>,
6486        _context: &mut TraverseCtx<'a, ()>,
6487    ) {
6488        self.chain_roots.pop();
6489    }
6490
6491    fn enter_call_expression(
6492        &mut self,
6493        node: &mut CallExpression<'a>,
6494        _context: &mut TraverseCtx<'a, ()>,
6495    ) {
6496        if !node.optional || self.unsafe_context() {
6497            return;
6498        }
6499        let root = *self
6500            .chain_roots
6501            .last()
6502            .expect("an optional call must be enclosed by a chain expression");
6503        let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
6504        let short_id = format!("{id}:short");
6505        let continued_id = format!("{id}:continued");
6506        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6507        self.analysis.sites.insert(
6508            span_key(node.span),
6509            (short_id.clone(), continued_id.clone()),
6510        );
6511        self.analysis
6512            .roots
6513            .entry(root)
6514            .or_default()
6515            .push(span_key(node.span));
6516        self.analysis.branches.push(CandidateBranch {
6517            id,
6518            kind: "optional-chain".to_string(),
6519            file: self.file.to_string(),
6520            line,
6521            column,
6522            source: source_slice(self.source, node.span).to_string(),
6523            alternatives: vec![
6524                CandidateBranchAlternative {
6525                    id: short_id,
6526                    label: "nullish / short-circuited".to_string(),
6527                },
6528                CandidateBranchAlternative {
6529                    id: continued_id,
6530                    label: "non-nullish / continued".to_string(),
6531                },
6532            ],
6533        });
6534    }
6535}
6536
6537fn collect_optional_call_branches<'a>(
6538    allocator: &'a Allocator,
6539    program: &mut Program<'a>,
6540    source: &str,
6541    file: &str,
6542    source_sensitive_functions: &HashSet<SpanKey>,
6543) -> OptionalCallAnalysis {
6544    let mut collector = OptionalCallCollector {
6545        source,
6546        file,
6547        source_sensitive_functions,
6548        unsafe_function_depth: 0,
6549        with_depth: 0,
6550        chain_roots: Vec::new(),
6551        analysis: OptionalCallAnalysis::default(),
6552    };
6553    traverse_mut(&mut collector, allocator, program, Default::default(), ());
6554    collector.analysis
6555}
6556
6557struct OptionalMemberCollector<'s> {
6558    source: &'s str,
6559    file: &'s str,
6560    source_sensitive_functions: &'s HashSet<SpanKey>,
6561    unsafe_function_depth: usize,
6562    with_depth: usize,
6563    analysis: OptionalMemberAnalysis,
6564}
6565
6566impl OptionalMemberCollector<'_> {
6567    fn record(&mut self, span: Span, optional: bool) {
6568        if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
6569            return;
6570        }
6571        let id = stable_id(self.source, self.file, "optional-chain", span, "");
6572        let short_id = format!("{id}:short");
6573        let continued_id = format!("{id}:continued");
6574        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
6575        self.analysis
6576            .targets
6577            .insert(span_key(span), (short_id.clone(), continued_id.clone()));
6578        self.analysis.branches.push(CandidateBranch {
6579            id,
6580            kind: "optional-chain".to_string(),
6581            file: self.file.to_string(),
6582            line,
6583            column,
6584            source: source_slice(self.source, span).to_string(),
6585            alternatives: vec![
6586                CandidateBranchAlternative {
6587                    id: short_id,
6588                    label: "nullish / short-circuited".to_string(),
6589                },
6590                CandidateBranchAlternative {
6591                    id: continued_id,
6592                    label: "non-nullish / continued".to_string(),
6593                },
6594            ],
6595        });
6596    }
6597
6598    fn exit_source_function(&mut self, span: Span) {
6599        if self.source_sensitive_functions.contains(&span_key(span)) {
6600            self.unsafe_function_depth -= 1;
6601        }
6602    }
6603}
6604
6605impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
6606    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6607        if self
6608            .source_sensitive_functions
6609            .contains(&span_key(node.span))
6610        {
6611            self.unsafe_function_depth += 1;
6612        }
6613    }
6614
6615    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6616        self.exit_source_function(node.span);
6617    }
6618
6619    fn enter_arrow_function_expression(
6620        &mut self,
6621        node: &mut ArrowFunctionExpression<'a>,
6622        _context: &mut TraverseCtx<'a, ()>,
6623    ) {
6624        if self
6625            .source_sensitive_functions
6626            .contains(&span_key(node.span))
6627        {
6628            self.unsafe_function_depth += 1;
6629        }
6630    }
6631
6632    fn exit_arrow_function_expression(
6633        &mut self,
6634        node: &mut ArrowFunctionExpression<'a>,
6635        _context: &mut TraverseCtx<'a, ()>,
6636    ) {
6637        self.exit_source_function(node.span);
6638    }
6639
6640    fn enter_with_statement(
6641        &mut self,
6642        _node: &mut WithStatement<'a>,
6643        _context: &mut TraverseCtx<'a, ()>,
6644    ) {
6645        self.with_depth += 1;
6646    }
6647
6648    fn exit_with_statement(
6649        &mut self,
6650        _node: &mut WithStatement<'a>,
6651        _context: &mut TraverseCtx<'a, ()>,
6652    ) {
6653        self.with_depth -= 1;
6654    }
6655
6656    fn enter_computed_member_expression(
6657        &mut self,
6658        node: &mut ComputedMemberExpression<'a>,
6659        _context: &mut TraverseCtx<'a, ()>,
6660    ) {
6661        self.record(node.span, node.optional);
6662    }
6663
6664    fn enter_static_member_expression(
6665        &mut self,
6666        node: &mut StaticMemberExpression<'a>,
6667        _context: &mut TraverseCtx<'a, ()>,
6668    ) {
6669        self.record(node.span, node.optional);
6670    }
6671
6672    fn enter_private_field_expression(
6673        &mut self,
6674        node: &mut PrivateFieldExpression<'a>,
6675        _context: &mut TraverseCtx<'a, ()>,
6676    ) {
6677        self.record(node.span, node.optional);
6678    }
6679}
6680
6681fn collect_optional_member_branches<'a>(
6682    allocator: &'a Allocator,
6683    program: &mut Program<'a>,
6684    source: &str,
6685    file: &str,
6686    source_sensitive_functions: &HashSet<SpanKey>,
6687) -> OptionalMemberAnalysis {
6688    let mut collector = OptionalMemberCollector {
6689        source,
6690        file,
6691        source_sensitive_functions,
6692        unsafe_function_depth: 0,
6693        with_depth: 0,
6694        analysis: OptionalMemberAnalysis::default(),
6695    };
6696    traverse_mut(&mut collector, allocator, program, Default::default(), ());
6697    collector.analysis
6698}
6699
6700struct LogicalAssignmentCollector<'s> {
6701    source: &'s str,
6702    file: &'s str,
6703    source_sensitive_functions: &'s HashSet<SpanKey>,
6704    unsafe_function_depth: usize,
6705    with_depth: usize,
6706    analysis: LogicalAssignmentAnalysis,
6707}
6708
6709impl LogicalAssignmentCollector<'_> {
6710    fn exit_source_function(&mut self, span: Span) {
6711        if self.source_sensitive_functions.contains(&span_key(span)) {
6712            self.unsafe_function_depth -= 1;
6713        }
6714    }
6715}
6716
6717impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
6718    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6719        if self
6720            .source_sensitive_functions
6721            .contains(&span_key(node.span))
6722        {
6723            self.unsafe_function_depth += 1;
6724        }
6725    }
6726
6727    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6728        self.exit_source_function(node.span);
6729    }
6730
6731    fn enter_arrow_function_expression(
6732        &mut self,
6733        node: &mut ArrowFunctionExpression<'a>,
6734        _context: &mut TraverseCtx<'a, ()>,
6735    ) {
6736        if self
6737            .source_sensitive_functions
6738            .contains(&span_key(node.span))
6739        {
6740            self.unsafe_function_depth += 1;
6741        }
6742    }
6743
6744    fn exit_arrow_function_expression(
6745        &mut self,
6746        node: &mut ArrowFunctionExpression<'a>,
6747        _context: &mut TraverseCtx<'a, ()>,
6748    ) {
6749        self.exit_source_function(node.span);
6750    }
6751
6752    fn enter_with_statement(
6753        &mut self,
6754        _node: &mut WithStatement<'a>,
6755        _context: &mut TraverseCtx<'a, ()>,
6756    ) {
6757        self.with_depth += 1;
6758    }
6759
6760    fn exit_with_statement(
6761        &mut self,
6762        _node: &mut WithStatement<'a>,
6763        _context: &mut TraverseCtx<'a, ()>,
6764    ) {
6765        self.with_depth -= 1;
6766    }
6767
6768    fn exit_assignment_expression(
6769        &mut self,
6770        node: &mut AssignmentExpression<'a>,
6771        _context: &mut TraverseCtx<'a, ()>,
6772    ) {
6773        if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
6774            return;
6775        }
6776        let operator = match node.operator {
6777            AssignmentOperator::LogicalAnd => "&&=",
6778            AssignmentOperator::LogicalOr => "||=",
6779            AssignmentOperator::LogicalNullish => "??=",
6780            _ => unreachable!("logical assignment filter must be exhaustive"),
6781        };
6782        let id = stable_id(
6783            self.source,
6784            self.file,
6785            "logical-assignment",
6786            node.span,
6787            operator,
6788        );
6789        let short_id = format!("{id}:short");
6790        let right_id = format!("{id}:right");
6791        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6792        self.analysis
6793            .targets
6794            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
6795        self.analysis.branches.push(CandidateBranch {
6796            id,
6797            kind: "logical-assignment".to_string(),
6798            file: self.file.to_string(),
6799            line,
6800            column,
6801            source: source_slice(self.source, node.span).to_string(),
6802            alternatives: vec![
6803                CandidateBranchAlternative {
6804                    id: short_id,
6805                    label: "assignment skipped".to_string(),
6806                },
6807                CandidateBranchAlternative {
6808                    id: right_id,
6809                    label: "right evaluated / assigned".to_string(),
6810                },
6811            ],
6812        });
6813    }
6814}
6815
6816fn collect_logical_assignment_branches<'a>(
6817    allocator: &'a Allocator,
6818    program: &mut Program<'a>,
6819    source: &str,
6820    file: &str,
6821    source_sensitive_functions: &HashSet<SpanKey>,
6822) -> LogicalAssignmentAnalysis {
6823    let mut collector = LogicalAssignmentCollector {
6824        source,
6825        file,
6826        source_sensitive_functions,
6827        unsafe_function_depth: 0,
6828        with_depth: 0,
6829        analysis: LogicalAssignmentAnalysis::default(),
6830    };
6831    traverse_mut(&mut collector, allocator, program, Default::default(), ());
6832    collector.analysis
6833}
6834
6835struct LogicalBranchCollector<'s> {
6836    source: &'s str,
6837    file: &'s str,
6838    decision_logical_nodes: &'s HashSet<SpanKey>,
6839    source_sensitive_functions: &'s HashSet<SpanKey>,
6840    unsafe_function_depth: usize,
6841    with_depth: usize,
6842    analysis: LogicalBranchAnalysis,
6843}
6844
6845impl LogicalBranchCollector<'_> {
6846    fn exit_source_function(&mut self, span: Span) {
6847        if self.source_sensitive_functions.contains(&span_key(span)) {
6848            self.unsafe_function_depth -= 1;
6849        }
6850    }
6851}
6852
6853impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
6854    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6855        if self
6856            .source_sensitive_functions
6857            .contains(&span_key(node.span))
6858        {
6859            self.unsafe_function_depth += 1;
6860        }
6861    }
6862
6863    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6864        self.exit_source_function(node.span);
6865    }
6866
6867    fn enter_arrow_function_expression(
6868        &mut self,
6869        node: &mut ArrowFunctionExpression<'a>,
6870        _context: &mut TraverseCtx<'a, ()>,
6871    ) {
6872        if self
6873            .source_sensitive_functions
6874            .contains(&span_key(node.span))
6875        {
6876            self.unsafe_function_depth += 1;
6877        }
6878    }
6879
6880    fn exit_arrow_function_expression(
6881        &mut self,
6882        node: &mut ArrowFunctionExpression<'a>,
6883        _context: &mut TraverseCtx<'a, ()>,
6884    ) {
6885        self.exit_source_function(node.span);
6886    }
6887
6888    fn enter_with_statement(
6889        &mut self,
6890        _node: &mut WithStatement<'a>,
6891        _context: &mut TraverseCtx<'a, ()>,
6892    ) {
6893        self.with_depth += 1;
6894    }
6895
6896    fn exit_with_statement(
6897        &mut self,
6898        _node: &mut WithStatement<'a>,
6899        _context: &mut TraverseCtx<'a, ()>,
6900    ) {
6901        self.with_depth -= 1;
6902    }
6903
6904    fn exit_logical_expression(
6905        &mut self,
6906        node: &mut LogicalExpression<'a>,
6907        _context: &mut TraverseCtx<'a, ()>,
6908    ) {
6909        if self.unsafe_function_depth > 0
6910            || self.with_depth > 0
6911            || self.decision_logical_nodes.contains(&span_key(node.span))
6912        {
6913            return;
6914        }
6915        let operator = match node.operator {
6916            LogicalOperator::And => "&&",
6917            LogicalOperator::Or => "||",
6918            LogicalOperator::Coalesce => "??",
6919        };
6920        let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
6921        let short_id = format!("{id}:short");
6922        let right_id = format!("{id}:right");
6923        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6924        self.analysis
6925            .logical_targets
6926            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
6927        self.analysis.branches.push(CandidateBranch {
6928            id,
6929            kind: "logical-value".to_string(),
6930            file: self.file.to_string(),
6931            line,
6932            column,
6933            source: source_slice(self.source, node.span).to_string(),
6934            alternatives: vec![
6935                CandidateBranchAlternative {
6936                    id: short_id,
6937                    label: "short-circuit / left selected".to_string(),
6938                },
6939                CandidateBranchAlternative {
6940                    id: right_id,
6941                    label: "right evaluated / selected".to_string(),
6942                },
6943            ],
6944        });
6945    }
6946}
6947
6948fn collect_logical_value_branches<'a>(
6949    allocator: &'a Allocator,
6950    program: &mut Program<'a>,
6951    source: &str,
6952    file: &str,
6953    decision_logical_nodes: &HashSet<SpanKey>,
6954    source_sensitive_functions: &HashSet<SpanKey>,
6955) -> LogicalBranchAnalysis {
6956    let mut collector = LogicalBranchCollector {
6957        source,
6958        file,
6959        decision_logical_nodes,
6960        source_sensitive_functions,
6961        unsafe_function_depth: 0,
6962        with_depth: 0,
6963        analysis: LogicalBranchAnalysis::default(),
6964    };
6965    traverse_mut(&mut collector, allocator, program, Default::default(), ());
6966    collector.analysis
6967}
6968
6969impl<'a> Visit<'a> for DecisionCollector<'_> {
6970    fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
6971        if self
6972            .source_sensitive_functions
6973            .contains(&span_key(function.span))
6974        {
6975            return;
6976        }
6977        walk::walk_function(self, function, flags);
6978    }
6979
6980    fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
6981        if self
6982            .source_sensitive_functions
6983            .contains(&span_key(function.span))
6984        {
6985            return;
6986        }
6987        walk::walk_arrow_function_expression(self, function);
6988    }
6989
6990    fn visit_with_statement(&mut self, statement: &WithStatement<'a>) {
6991        if self.with_statements.contains(&span_key(statement.span)) {
6992            return;
6993        }
6994        walk::walk_with_statement(self, statement);
6995    }
6996
6997    fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
6998        if decision_outcome_is_variable(&statement.test) {
6999            self.record_decision(&statement.test, "if");
7000        }
7001        self.visit_expression(&statement.test);
7002        self.visit_statement(&statement.consequent);
7003        if let Some(alternate) = &statement.alternate {
7004            self.visit_statement(alternate);
7005        }
7006    }
7007
7008    fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
7009        if decision_outcome_is_variable(&expression.test) {
7010            self.record_decision(&expression.test, "ternary");
7011        }
7012        self.visit_expression(&expression.test);
7013        self.visit_expression(&expression.consequent);
7014        self.visit_expression(&expression.alternate);
7015    }
7016
7017    fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
7018        if decision_outcome_is_variable(&statement.test) {
7019            self.record_decision(&statement.test, "while");
7020        }
7021        self.visit_expression(&statement.test);
7022        self.visit_statement(&statement.body);
7023    }
7024
7025    fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
7026        if decision_outcome_is_variable(&statement.test) {
7027            self.record_decision(&statement.test, "do-while");
7028        }
7029        self.visit_statement(&statement.body);
7030        self.visit_expression(&statement.test);
7031    }
7032
7033    fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
7034        if let Some(test) = &statement.test
7035            && decision_outcome_is_variable(test)
7036        {
7037            self.record_decision(test, "for");
7038        }
7039        if let Some(init) = &statement.init {
7040            self.visit_for_statement_init(init);
7041        }
7042        if let Some(test) = &statement.test {
7043            self.visit_expression(test);
7044        }
7045        if let Some(update) = &statement.update {
7046            self.visit_expression(update);
7047        }
7048        self.visit_statement(&statement.body);
7049    }
7050}
7051
7052fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
7053    match expression {
7054        Expression::ParenthesizedExpression(parenthesized) => {
7055            transparent_expression(&parenthesized.expression)
7056        }
7057        _ => expression,
7058    }
7059}
7060
7061fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
7062    match expression {
7063        Expression::ParenthesizedExpression(parenthesized) => {
7064            has_compound_boolean_decision(&parenthesized.expression)
7065        }
7066        Expression::LogicalExpression(logical) => {
7067            matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
7068        }
7069        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
7070            has_compound_boolean_decision(&unary.argument)
7071        }
7072        _ => false,
7073    }
7074}
7075
7076fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
7077    match expression {
7078        Expression::ParenthesizedExpression(parenthesized) => {
7079            collect_conditions(&parenthesized.expression, conditions);
7080        }
7081        Expression::LogicalExpression(logical)
7082            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7083        {
7084            collect_conditions(&logical.left, conditions);
7085            collect_conditions(&logical.right, conditions);
7086        }
7087        Expression::UnaryExpression(unary)
7088            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7089        {
7090            collect_conditions(&unary.argument, conditions);
7091        }
7092        _ => conditions.push(expression.span()),
7093    }
7094}
7095
7096fn source_slice(source: &str, span: Span) -> &str {
7097    &source[span.start as usize..span.end as usize]
7098}
7099
7100pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
7101    let prefix = &source[..offset];
7102    let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
7103    let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
7104    let column = source[line_start..offset].encode_utf16().count() + 1;
7105    (line, column)
7106}
7107
7108fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
7109    let start = source[..span.start as usize].encode_utf16().count();
7110    let end = source[..span.end as usize].encode_utf16().count();
7111    let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
7112    let mut id = String::with_capacity(16);
7113    for byte in &digest[..8] {
7114        write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
7115    }
7116    id
7117}
7118
7119#[cfg(test)]
7120mod tests {
7121    use super::*;
7122
7123    const SOURCE: &str =
7124        "export function decide(a,b,c) {\n  if ((a && b) || !c) return 1;\n  return 0;\n}\n";
7125
7126    #[test]
7127    fn matches_the_frozen_if_decision_manifest_exactly() {
7128        let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
7129        assert!(!output.complete);
7130        assert_eq!(
7131            output.decisions,
7132            vec![CandidateDecision {
7133                id: "2f65989a5782c5bd".to_string(),
7134                file: "app/decide.ts".to_string(),
7135                line: 2,
7136                column: 7,
7137                source: "(a && b) || !c".to_string(),
7138                conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
7139                kind: "if".to_string(),
7140            }]
7141        );
7142    }
7143
7144    #[test]
7145    fn codegen_output_reparses_for_typescript_and_tsx() {
7146        for (file, source) in [
7147            (
7148                "component.tsx",
7149                "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
7150            ),
7151            ("module.ts", SOURCE),
7152        ] {
7153            let output = analyze_candidate(source, file).unwrap();
7154            let map = output.map.as_ref().expect("candidate source map");
7155            assert_eq!(map["version"], 3);
7156            assert_eq!(map["sources"][0], file);
7157            assert_eq!(map["sourcesContent"][0], source);
7158            assert!(
7159                map["mappings"]
7160                    .as_str()
7161                    .is_some_and(|value| !value.is_empty())
7162            );
7163            let allocator = Allocator::default();
7164            let source_type = SourceType::from_path(file).unwrap();
7165            let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
7166            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
7167        }
7168    }
7169
7170    #[test]
7171    fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
7172        for file in ["src/module.mjs", "src/script.cjs"] {
7173            let output = instrument_direct_candidate(
7174                "export const selected = value => value ? 1 : 0;",
7175                file,
7176            )
7177            .unwrap();
7178            assert!(
7179                output
7180                    .code
7181                    .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
7182            );
7183            assert!(!output.code.contains("virtual:supercov-runtime"));
7184            assert!(!output.code.contains("globalThis.__supercovRuntime"));
7185        }
7186    }
7187
7188    #[test]
7189    fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
7190        let source = concat!(
7191            "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
7192            "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
7193            "ordinaryHelper();\n",
7194        );
7195        let output = instrument_node_assertion_phases_with_runtime_hooks(
7196            source,
7197            "tests/runner.mjs",
7198            &[],
7199            Some("../.supercov/launchSupervisor.mjs"),
7200        )
7201        .unwrap();
7202        assert_eq!(output.assertions, 0);
7203        assert_eq!(output.capability_imports, 1);
7204        assert!(output.code.contains("wrapImportedCapability"));
7205        assert!(output.code.contains("__supercovRawImageBuilder"));
7206        assert!(output.code.contains("const ImageBuilder"));
7207        assert!(!output.code.contains("__supercovRawordinaryHelper"));
7208        assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
7209    }
7210
7211    #[test]
7212    fn capability_imports_accept_a_computed_guest_mount_path() {
7213        let source = concat!(
7214            "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
7215            "const guestRoot = resolve(tmpdir(), 'workspace');\n",
7216            "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
7217            "ordinaryHelper();\n",
7218        );
7219        let output = instrument_node_assertion_phases_with_runtime_hooks(
7220            source,
7221            "tests/runner.mjs",
7222            &[],
7223            Some("../.supercov/launchSupervisor.mjs"),
7224        )
7225        .unwrap();
7226        assert_eq!(output.capability_imports, 1);
7227        assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
7228        assert!(!output.code.contains("__supercovRawordinaryHelper"));
7229    }
7230
7231    #[test]
7232    fn capability_import_selection_follows_a_mapping_variable() {
7233        let source = concat!(
7234            "import { launch, unrelated } from './sdk.mjs';\n",
7235            "run();\n",
7236            "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
7237            "function run() { launch(options); unrelated(); }\n",
7238        );
7239        let output = instrument_node_assertion_phases_with_runtime_hooks(
7240            source,
7241            "tests/runner.mjs",
7242            &[],
7243            Some("../.supercov/launchSupervisor.mjs"),
7244        )
7245        .unwrap();
7246        assert_eq!(output.capability_imports, 1);
7247        assert!(output.code.contains("__supercovRawlaunch"));
7248        assert!(!output.code.contains("__supercovRawunrelated"));
7249    }
7250
7251    #[test]
7252    fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
7253        let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
7254        let output = instrument_node_assertion_phases_with_runtime_hooks(
7255            source,
7256            "src/main.mjs",
7257            &[],
7258            Some("../.supercov/launchSupervisor.mjs"),
7259        )
7260        .unwrap();
7261        assert_eq!(output.code, source);
7262        assert_eq!(output.capability_imports, 0);
7263    }
7264
7265    #[test]
7266    fn ordinary_mount_language_does_not_enable_capability_proxies() {
7267        let source = concat!(
7268            "import { render } from '@testing-library/react';\n",
7269            "it('mounts into a host element', () => render(<main />));\n",
7270        );
7271        let output = instrument_node_assertion_phases_with_runtime_hooks(
7272            source,
7273            "tests/component.test.tsx",
7274            &[],
7275            Some("../.supercov/launchSupervisor.mjs"),
7276        )
7277        .unwrap();
7278        assert_eq!(output.capability_imports, 0);
7279        assert!(!output.code.contains("wrapImportedCapability"));
7280    }
7281
7282    #[test]
7283    fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
7284        let source = concat!(
7285            "import { test, expect } from '@example/test-admin';\n",
7286            "test('visual snapshot', async ({ page }) => {\n",
7287            "  expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
7288            "});\n",
7289        );
7290        let output = instrument_node_assertion_phases_with_runtime_hooks(
7291            source,
7292            "tests/visual.spec.ts",
7293            &[],
7294            Some("../.supercov/launchSupervisor.mjs"),
7295        )
7296        .unwrap();
7297        assert_eq!(output.capability_imports, 0);
7298        assert!(!output.code.contains("wrapImportedCapability"));
7299    }
7300
7301    #[test]
7302    fn capability_rewrite_excludes_runtime_and_type_only_imports() {
7303        let source = concat!(
7304            "import type { Machine } from './types.ts';\n",
7305            "import { test } from 'node:test';\n",
7306            "import { runtime } from '../.supercov/runtime.mjs';\n",
7307            "console.log({ machine: true, test, runtime });\n",
7308        );
7309        let output = instrument_node_assertion_phases_with_runtime_hooks(
7310            source,
7311            "tests/runner.ts",
7312            &[],
7313            Some("../.supercov/launchSupervisor.mjs"),
7314        )
7315        .unwrap();
7316        assert_eq!(output.code, source);
7317        assert_eq!(output.capability_imports, 0);
7318    }
7319
7320    #[test]
7321    fn native_assertion_arguments_execute_inside_the_assertion_phase() {
7322        let source = concat!(
7323            "import assert from 'node:assert/strict';\n",
7324            "assert.equal(value(), 1);\n",
7325        );
7326        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7327        assert_eq!(output.assertions, 1);
7328        assert!(output.code.contains("withNodeAssertionPhase"));
7329        assert!(output.code.contains("node:assert/strict.equal"));
7330        assert!(output.code.contains("tests/value.test.mjs:2:1"));
7331        assert!(output.code.contains("assert.equal(value(), 1)"));
7332    }
7333
7334    #[test]
7335    fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
7336        let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
7337        let output = instrument_node_assertion_phases_with_runtime_imports(
7338            source,
7339            "tests/value.test.mjs",
7340            &[],
7341            None,
7342            Some("../.supercov/runtime.mjs"),
7343        )
7344        .unwrap();
7345        assert_eq!(output.assertions, 1);
7346        assert!(output.code.contains("withNodeAssertionPhase"));
7347        assert!(output.code.contains("import \"../.supercov/runtime.mjs\";"));
7348    }
7349
7350    #[test]
7351    fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
7352        let source = concat!(
7353            "import assert from 'node:assert/strict';\n",
7354            "export async function check() { assert.equal(await value(), 1); }\n",
7355        );
7356        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7357        assert_eq!(output.assertions, 1);
7358        assert!(output.code.contains("bindNodeAssertion"));
7359        assert!(output.code.contains("tests/value.test.mjs:2:33"));
7360        assert!(output.code.contains("await value()"));
7361        assert!(!output.code.contains("withNodeAssertionPhase"));
7362        let allocator = Allocator::default();
7363        let parsed = Parser::new(&allocator, &output.code, SourceType::mjs()).parse();
7364        assert!(parsed.errors.is_empty(), "{:?}", parsed.errors);
7365    }
7366
7367    #[test]
7368    fn awaited_assertion_bindings_leave_unsupported_calls_untouched() {
7369        for source in [
7370            "import assert from 'node:assert'; async function f() { assert?.equal(await value(), 1); }",
7371            "import assert from 'node:assert'; async function f() { assert.equal?.(await value(), 1); }",
7372            "import assert from 'node:assert'; function* f() { assert.equal(yield value(), 1); }",
7373            "import assert from 'node:assert'; async function f() { assert.rejects(await value()); }",
7374            "import assert from 'node:assert'; async function f(assert) { assert.equal(await value(), 1); }",
7375            "import { expect } from 'vitest'; async function f() { expect(await value()).toBe(1); }",
7376        ] {
7377            let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7378            assert_eq!(output.assertions, 0, "{source}");
7379            assert_eq!(output.code, source);
7380        }
7381    }
7382
7383    #[test]
7384    fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
7385        let source = concat!(
7386            "import { expect, test } from '@playwright/test';\n",
7387            "test('value', async () => { expect(await value()).toBe(1); });\n",
7388        );
7389        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
7390        assert_eq!(output.assertions, 0);
7391        assert_eq!(output.code, source);
7392    }
7393
7394    #[test]
7395    fn native_commonjs_assertion_bindings_are_attributed() {
7396        let source = concat!(
7397            "const assert = require('node:assert/strict');\n",
7398            "const { ok: verify } = require('assert');\n",
7399            "assert.equal(value(), 1);\n",
7400            "verify(other());\n",
7401        );
7402        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
7403        assert_eq!(output.assertions, 2);
7404        assert!(output.code.contains("node:assert/strict.equal"));
7405        assert!(output.code.contains("node:assert.ok"));
7406    }
7407
7408    #[test]
7409    fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
7410        let source = concat!(
7411            "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
7412            "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
7413        );
7414        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
7415        assert_eq!(output.assertions, 1);
7416        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
7417    }
7418
7419    #[test]
7420    fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
7421        let source = concat!(
7422            "import assert from 'node:assert/strict';\n",
7423            "function unrelated(assert) { assert.equal(effect(), 1); }\n",
7424            "assert.equal(value(), 1);\n",
7425        );
7426        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7427        assert_eq!(output.assertions, 1);
7428        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
7429    }
7430
7431    #[test]
7432    fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
7433        let source = concat!(
7434            "import test from 'node:test';\n",
7435            "import { expect as verify } from 'expect-library';\n",
7436            "test('value', () => verify(value()).not.toEqual(1));\n",
7437        );
7438        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7439        assert_eq!(output.assertions, 1);
7440        assert!(output.code.contains("expect.not.toEqual"));
7441        assert!(output.code.contains("verify(value()).not.toEqual(1)"));
7442    }
7443
7444    #[test]
7445    fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
7446        let source = concat!(
7447            "import { expect, test } from 'vitest';\n",
7448            "test('value', () => expect(value()).toBe(1));\n",
7449        );
7450        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7451        assert_eq!(output.assertions, 1);
7452        assert!(output.code.contains("expect.toBe"));
7453    }
7454
7455    #[test]
7456    fn jest_global_expect_is_attributed_next_to_its_test_globals() {
7457        // Jest injects `expect`, `test`, `it` and `describe` into the test
7458        // environment; none is imported. The bare `expect` is the assertion
7459        // exactly when the file reaches for those test globals too.
7460        let source = "test('value', () => expect(value()).toBe(1));\n";
7461        let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
7462        assert_eq!(output.assertions, 1);
7463        assert!(output.code.contains("expect.toBe"));
7464        // A stray global `expect` in ordinary code is not one.
7465        let plain = "const ok = expect(value()).toBe(1);\n";
7466        let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
7467        assert_eq!(output.assertions, 0);
7468        assert_eq!(output.code, plain);
7469    }
7470
7471    #[test]
7472    fn playwright_expect_matchers_are_left_to_the_runner_adapter() {
7473        let source = concat!(
7474            "import { expect, test } from '@playwright/test';\n",
7475            "test('value', () => expect(value()).toBe(1));\n",
7476        );
7477        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
7478        assert_eq!(output.assertions, 0);
7479        assert_eq!(output.code, source);
7480    }
7481
7482    #[test]
7483    fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
7484        let source = concat!(
7485            "import { browserTest, expect } from '@acme/browser-fixtures';\n",
7486            "browserTest('value', () => expect(value()).toBe(1));\n",
7487        );
7488        let output = instrument_node_assertion_phases_with_expect_modules(
7489            source,
7490            "tests/value.spec.mjs",
7491            &["@acme/browser-fixtures".into()],
7492        )
7493        .unwrap();
7494        assert_eq!(output.assertions, 1);
7495        assert!(output.code.contains("expect.toBe"));
7496    }
7497
7498    #[test]
7499    fn preserves_comment_payloads_byte_for_byte() {
7500        let comment = "/*---\ndescription: >\n  nested indentation is data\ninfo: |\n  first\n    second\n---*/";
7501        let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
7502        let output = instrument_candidate(&source, "app/comments.js").unwrap();
7503        assert!(output.code.contains(comment), "{}", output.code);
7504    }
7505
7506    #[test]
7507    fn source_map_destinations_follow_restored_comments() {
7508        let source = "function run() {\n  // first omitted comment\n  // second omitted comment\n  return 1;\n}\n";
7509        let output = analyze_candidate(source, "app/comment-map.js").unwrap();
7510        let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
7511        let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
7512        let return_token = map
7513            .get_tokens()
7514            .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
7515            .expect("return token mapping");
7516        let offset = Utf16LineIndex::new(&output.code).byte_offset(
7517            return_token.get_dst_line() as usize,
7518            return_token.get_dst_col() as usize,
7519        );
7520        assert!(
7521            output.code[offset..].starts_with("return"),
7522            "{}",
7523            output.code
7524        );
7525    }
7526
7527    #[test]
7528    fn restored_comments_never_split_a_keyword_from_its_argument() {
7529        // oxc drops the comment that led a parenthesised return argument; the
7530        // restore used to put it back right after `return`, on its own line,
7531        // and automatic semicolon insertion turned the statement into `return;`
7532        // with the JSX unreachable. Vite then removed the unreachable tree and
7533        // every declaration only it used, so a component rendered nothing under
7534        // measurement while passing plainly. Expression-bodied arrows are hit
7535        // too, since instrumentation rewrites them into `return` blocks.
7536        let source = concat!(
7537            "export const A = ({ open }) => {\n",
7538            "  return (\n",
7539            "    // leading comment before JSX\n",
7540            "    <div aria-expanded={open}>a</div>\n",
7541            "  )\n",
7542            "}\n",
7543            "export const B = ({ open }) => (\n",
7544            "  // comment in an expression body\n",
7545            "  <div aria-expanded={open}>b</div>\n",
7546            ")\n",
7547            "export function C(x) {\n",
7548            "  return ( // inline line comment\n",
7549            "    x + 1\n",
7550            "  )\n",
7551            "}\n",
7552            "export function D(x) {\n",
7553            "  throw (\n",
7554            "    /* block\n       comment */\n",
7555            "    new Error(String(x))\n",
7556            "  )\n",
7557            "}\n",
7558            "export function E(x) {\n",
7559            "  return ( /* inline block */ x + 2 )\n",
7560            "}\n",
7561        );
7562        let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
7563        for keyword in ["return ", "throw "] {
7564            for (index, _) in output.code.match_indices(keyword) {
7565                let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
7566                let block_with_line_break = rest.starts_with("/*")
7567                    && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
7568                assert!(
7569                    !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
7570                    "`{keyword}` is separated from its argument:\n{}",
7571                    output.code
7572                );
7573            }
7574        }
7575        for comment in [
7576            "// leading comment before JSX",
7577            "// comment in an expression body",
7578            "// inline line comment",
7579            "/* block\n       comment */",
7580            "/* inline block */",
7581        ] {
7582            assert!(
7583                output.code.contains(comment),
7584                "{comment} was lost:\n{}",
7585                output.code
7586            );
7587        }
7588        assert!(
7589            output
7590                .code
7591                .contains("return <div aria-expanded={open}>a</div>"),
7592            "{}",
7593            output.code
7594        );
7595        assert!(
7596            output
7597                .code
7598                .contains("return <div aria-expanded={open}>b</div>"),
7599            "{}",
7600            output.code
7601        );
7602        // The restored output must still parse to the same program shape.
7603        let allocator = Allocator::default();
7604        let reparsed = Parser::new(
7605            &allocator,
7606            &output.code,
7607            SourceType::from_path("app/components/List.tsx").unwrap(),
7608        )
7609        .parse();
7610        assert!(
7611            reparsed.errors.is_empty(),
7612            "{:?}\n{}",
7613            reparsed.errors,
7614            output.code
7615        );
7616    }
7617
7618    #[test]
7619    fn preserves_reference_order_across_every_control_decision_kind() {
7620        let source = "function run(a,b) {\n  const selected = a ? b : a;\n  while (a && b) break;\n  do { a = false; } while (a || b);\n  for (let i = 0; i < 1 && b; i++) work();\n  if (selected) return 1;\n  return 0;\n}";
7621        let output = instrument_candidate(source, "app/control.js").unwrap();
7622        assert_eq!(
7623            output
7624                .decisions
7625                .iter()
7626                .map(|decision| decision.kind.as_str())
7627                .collect::<Vec<_>>(),
7628            vec!["ternary", "while", "do-while", "for", "if"]
7629        );
7630    }
7631
7632    #[test]
7633    fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
7634        let source = concat!(
7635            "export function run(value) {\n",
7636            "  while (true) { break; }\n",
7637            "  do { value += 1; } while (false);\n",
7638            "  if (false) value += 2;\n",
7639            "  if (value || true) value += 3;\n",
7640            "  if (value && false) value += 4;\n",
7641            "  if (value) value += 5;\n",
7642            "  return value;\n",
7643            "}\n",
7644        );
7645        let output = analyze_candidate(source, "src/constants.js").unwrap();
7646        assert_eq!(output.decisions.len(), 1);
7647        assert_eq!(output.decisions[0].source, "value");
7648
7649        let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
7650        assert_eq!(instrumented.decisions.len(), 1);
7651        assert_eq!(instrumented.decisions[0].source, "value");
7652    }
7653
7654    #[test]
7655    fn leaves_compile_time_style_macro_arguments_as_source() {
7656        // StyleX evaluates these at build time and rejects a block-bodied
7657        // dynamic style; probing them broke a real project's Vite build.
7658        let source = concat!(
7659            "import * as stylex from '@stylexjs/stylex';\n",
7660            "const styles = stylex.create({\n",
7661            "  container: { display: 'flex' },\n",
7662            "  paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
7663            "  tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
7664            "});\n",
7665            "export function render(spacing: number) {\n",
7666            "  return stylex.props(styles.container, styles.paddingTop(spacing));\n",
7667            "}\n",
7668        );
7669        let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
7670        assert!(
7671            output.code.contains("=> ({ paddingTop: `${spacing}px` })")
7672                || output
7673                    .code
7674                    .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
7675            "dynamic style arrow must stay an expression body:\n{}",
7676            output.code
7677        );
7678        assert!(
7679            !output
7680                .decisions
7681                .iter()
7682                .any(|decision| decision.source.contains("dark")),
7683            "no decision may be collected inside the macro argument"
7684        );
7685        let function_points = output
7686            .points
7687            .iter()
7688            .filter(|point| point.kind == "function")
7689            .map(|point| point.source.as_str())
7690            .collect::<Vec<_>>();
7691        assert!(
7692            function_points
7693                .iter()
7694                .all(|source| source.contains("render")),
7695            "only the real function may carry a function point: {function_points:?}"
7696        );
7697        assert!(
7698            output.limitations.is_empty(),
7699            "compiled-away code is not a limitation: {:?}",
7700            output.limitations
7701        );
7702    }
7703
7704    #[test]
7705    fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
7706        let source = concat!(
7707            "declare global {\n",
7708            "  var Beacon: undefined | ((action: string) => void);\n",
7709            "}\n",
7710            "declare module 'virtual:feature' {\n",
7711            "  export const enabled: boolean;\n",
7712            "}\n",
7713            "declare const buildOnly: string;\n",
7714            "export const live = 1;\n",
7715        );
7716        let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
7717        let statements = output
7718            .points
7719            .iter()
7720            .filter(|point| point.kind == "statement")
7721            .map(|point| point.source.as_str())
7722            .collect::<Vec<_>>();
7723        assert_eq!(statements, ["const live = 1;"]);
7724        assert!(output.code.contains("const live = 1"));
7725    }
7726
7727    #[test]
7728    fn exposes_the_complete_probe_v2_instrumenter_contract() {
7729        let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
7730        assert!(output.complete);
7731        assert!(output.limitations.is_empty());
7732        assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
7733        let runtime = output.runtime.expect("candidate runtime binding");
7734        assert!(output.code.contains(&runtime.mcdc_end_v2));
7735        assert!(output.code.contains("_supercovMcdcFrame"));
7736        assert!(output.code.contains("_supercovMcdcResult"));
7737        assert!(output.code.contains("+= _supercovMcdcValue"));
7738        assert_eq!(output.decisions.len(), 1);
7739        assert!(output.points.iter().any(|point| point.kind == "statement"));
7740        assert!(output.points.iter().any(|point| point.kind == "function"));
7741
7742        let allocator = Allocator::default();
7743        let reparsed = Parser::new(
7744            &allocator,
7745            &output.code,
7746            SourceType::from_path("app/decide.ts").unwrap(),
7747        )
7748        .parse();
7749        assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
7750    }
7751
7752    #[test]
7753    fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
7754        let source = concat!(
7755            "import type { Session } from './types.ts';\n",
7756            "import { type Row } from './rows.ts';\n",
7757            "import './register.ts';\n",
7758            "const value = 1;\n",
7759        );
7760        let output = instrument_candidate(source, "app/imports.ts").unwrap();
7761        let statement_lines = output
7762            .points
7763            .iter()
7764            .filter(|point| point.kind == "statement")
7765            .map(|point| point.line)
7766            .collect::<Vec<_>>();
7767        assert_eq!(statement_lines, vec![3, 4]);
7768    }
7769
7770    #[test]
7771    fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
7772        let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
7773        let output = instrument_candidate(source, "app/collisions.js").unwrap();
7774        let runtime = output.runtime.expect("candidate runtime binding");
7775        assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
7776        assert!(!output.code.contains("let _supercovMcdcFrame1,"));
7777    }
7778
7779    #[test]
7780    fn instruments_wider_decisions_with_the_exact_v1_fallback() {
7781        let predicate = (0..33)
7782            .map(|index| format!("c{index}"))
7783            .collect::<Vec<_>>()
7784            .join(" && ");
7785        let source = format!("if ({predicate}) work();");
7786        let output = instrument_candidate(&source, "app/wide.js").unwrap();
7787        assert_eq!(output.decisions[0].conditions.len(), 33);
7788        let runtime = output.runtime.expect("candidate runtime binding");
7789        assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
7790        assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
7791        assert!(
7792            output
7793                .code
7794                .contains(&format!("{}(", runtime.mcdc_condition))
7795        );
7796        assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
7797    }
7798
7799    #[test]
7800    fn wraps_framework_request_exports_without_changing_the_public_api() {
7801        for (file, source, export_prefix) in [
7802            (
7803                "app/routes/example.ts",
7804                "export const loader = async ({ request }) => request.url;",
7805                "export const loader = ",
7806            ),
7807            (
7808                "app/routes/example.ts",
7809                "export async function action({ request }) { return request.method; }",
7810                "export const action = ",
7811            ),
7812            (
7813                "app/api/items/route.ts",
7814                "export function GET(request) { return Response.json({ url: request.url }); }",
7815                "export const GET = ",
7816            ),
7817            (
7818                "app/routes/example.ts",
7819                "export { generateAction as action } from './generateAction';",
7820                "export const action = ",
7821            ),
7822            (
7823                "app/entry.server.tsx",
7824                "export default async function handleRequest(request) { return request.url; }",
7825                "export default ",
7826            ),
7827        ] {
7828            let output = instrument_candidate(source, file).unwrap();
7829            let runtime = output.runtime.as_ref().expect("runtime binding");
7830            assert!(
7831                output
7832                    .code
7833                    .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
7834                "{file}: {}",
7835                output.code
7836            );
7837            assert!(
7838                output.code.contains(&format!(
7839                    "withRequestPhase as {}",
7840                    runtime.with_request_phase
7841                )),
7842                "{file}: {}",
7843                output.code
7844            );
7845            let allocator = Allocator::default();
7846            let reparsed = Parser::new(
7847                &allocator,
7848                &output.code,
7849                SourceType::from_path(file).unwrap(),
7850            )
7851            .parse();
7852            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
7853        }
7854    }
7855
7856    #[test]
7857    fn parse_failures_are_explicit_and_never_claim_completeness() {
7858        assert!(matches!(
7859            analyze_candidate("if (", "broken.js"),
7860            Err(CandidateError::Parse(errors)) if !errors.is_empty()
7861        ));
7862        assert!(matches!(
7863            analyze_candidate("let value = 1", "unknown.extension"),
7864            Err(CandidateError::UnknownSourceType(_))
7865        ));
7866    }
7867}