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