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