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