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