Skip to main content

supercov_engine/
js_instrumenter.rs

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