Skip to main content

supercov_engine/
js_instrumenter.rs

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