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