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