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 same = |index: usize| {
138            let original = &program.comments[index];
139            original.kind == emitted.kind
140                && equal_ignoring_whitespace(
141                    original.span.source_text(program.source_text),
142                    emitted_text,
143                )
144        };
145        // Comments come out in source order, with one exception: the printer
146        // writes a `@__NO_SIDE_EFFECTS__` annotation after the documentation
147        // comment it stood before. Looking only forward gave up there, and
148        // zod's `v4/core/api.ts` could not be instrumented at all. A comment
149        // the forward scan stepped over is still waiting behind it.
150        let index = match (original_index..program.comments.len()).find(|index| same(*index)) {
151            Some(index) => {
152                original_index = index + 1;
153                index
154            }
155            None => match (0..original_index).find(|index| !matched[*index] && same(*index)) {
156                Some(index) => index,
157                // The counts are usually equal here, so reporting them alone
158                // says the opposite of what happened: nothing was dropped, one
159                // comment was not recognised. Name it.
160                None => {
161                    return Err(CandidateError::CommentPreservation {
162                        expected: program.comments.len(),
163                        actual: reparsed.program.comments.len(),
164                        detail: format!(
165                            "no source comment matches the generated comment {emitted_text:?}                          (matched {} before it)",
166                            edits.len()
167                        ),
168                    });
169                }
170            },
171        };
172        let original = &program.comments[index];
173        matched[index] = true;
174        edits.push((
175            emitted.span.start as usize,
176            emitted.span.end as usize,
177            original.span.source_text(program.source_text).to_string(),
178        ));
179    }
180
181    let source_lines = Utf16LineIndex::new(program.source_text);
182    let generated_lines = Utf16LineIndex::new(generated);
183    let mut mappings = map
184        .get_tokens()
185        .filter_map(|token| {
186            token.get_source_id()?;
187            Some((
188                source_lines
189                    .byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
190                generated_lines
191                    .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
192            ))
193        })
194        .collect::<Vec<_>>();
195    mappings.sort_unstable_by_key(|(source, destination)| (*source, *destination));
196    let statements = statement_spans(&reparsed.program);
197    for (index, original) in program.comments.iter().enumerate() {
198        if matched[index] {
199            continue;
200        }
201        let anchor = if original.attached_to > 0 {
202            original.attached_to as usize
203        } else {
204            original.span.end as usize
205        };
206        let mapping_index = mappings.partition_point(|(source, _)| *source < anchor);
207        let mapped = mappings
208            .get(mapping_index)
209            .map_or(generated.len(), |(_, destination)| *destination);
210        let (destination, text) = place_restored_comment(
211            generated,
212            &statements,
213            mapped,
214            original,
215            original.span.source_text(program.source_text),
216        );
217        edits.push((destination, destination, text));
218    }
219    edits.sort_by_key(|(start, end, _)| (*start, *end));
220    let restored_len = edits
221        .iter()
222        .fold(generated.len(), |length, (start, end, text)| {
223            length + text.len() - (end - start)
224        });
225    let mut restored = String::with_capacity(restored_len);
226    let mut cursor = 0;
227    for (start, end, replacement) in &edits {
228        if *start < cursor {
229            return Err(CandidateError::CommentPreservation {
230                expected: program.comments.len(),
231                actual: reparsed.program.comments.len(),
232                detail: format!(
233                    "restored comments overlap: {replacement:?} would be placed at byte {start},                      behind byte {cursor} already written"
234                ),
235            });
236        }
237        restored.push_str(&generated[cursor..*start]);
238        restored.push_str(replacement);
239        cursor = *end;
240    }
241    restored.push_str(&generated[cursor..]);
242    let map = shift_source_map(map, generated, &restored, &edits);
243    Ok((restored, map))
244}
245
246/// Every statement span in a program, so a restored comment can be anchored
247/// to a statement boundary.
248fn statement_spans(program: &Program<'_>) -> Vec<Span> {
249    struct Spans(Vec<Span>);
250    impl<'a> Visit<'a> for Spans {
251        fn visit_statement(&mut self, statement: &Statement<'a>) {
252            self.0.push(statement.span());
253            walk::walk_statement(self, statement);
254        }
255    }
256    let mut spans = Spans(Vec::new());
257    spans.visit_program(program);
258    spans.0
259}
260
261/// Where a comment the generator dropped goes back in, and with what
262/// whitespace around it.
263///
264/// The mapped position is only a hint: it is where the node the comment was
265/// attached to landed, and that can be mid-line. A comment that carries a
266/// line break must not be dropped there. After `return`, `throw`, `break`,
267/// `continue` or `yield` the break lets automatic semicolon insertion end the
268/// statement early -- `return` + newline + JSX parsed as `return;` with the
269/// JSX unreachable, and the bundler then removed the whole tree, so a React
270/// component rendered nothing under measurement while passing plainly. A `//`
271/// comment inserted before more code on the same line comments that code out.
272/// Such a comment moves to the start of the innermost statement containing
273/// the position, where a line break is always inert. A single-line block
274/// comment is inert wherever it sits, so mid-line it stays inline.
275fn place_restored_comment(
276    generated: &str,
277    statements: &[Span],
278    mapped: usize,
279    comment: &Comment,
280    text: &str,
281) -> (usize, String) {
282    let line_prefix = |offset: usize| generated[..offset].rsplit('\n').next().unwrap_or("");
283    let at_line_start = line_prefix(mapped)
284        .chars()
285        .all(|character| character == ' ' || character == '\t');
286    if at_line_start {
287        let mut placed = String::new();
288        placed.push(if comment.preceded_by_newline() {
289            '\n'
290        } else {
291            ' '
292        });
293        placed.push_str(text);
294        placed.push(if comment.is_line() || comment.followed_by_newline() {
295            '\n'
296        } else {
297            ' '
298        });
299        return (mapped, placed);
300    }
301    if !comment.is_line() && !text.contains('\n') {
302        let leading = if generated[..mapped].ends_with([' ', '\t']) {
303            ""
304        } else {
305            " "
306        };
307        return (mapped, format!("{leading}{text} "));
308    }
309    let Some(statement) = statements
310        .iter()
311        .filter(|span| span.start as usize <= mapped && mapped < span.end as usize)
312        .max_by_key(|span| span.start)
313    else {
314        return (mapped, format!("\n{text}\n"));
315    };
316    let start = statement.start as usize;
317    let indentation: String = line_prefix(start)
318        .chars()
319        .take_while(|character| *character == ' ' || *character == '\t')
320        .collect();
321    (start, format!("{text}\n{indentation}"))
322}
323
324/// Whether two comments are the same comment, ignoring how they were laid out
325/// and the one escape a JavaScript code generator introduces into them.
326///
327/// A comment has no escape semantics -- its bytes are its text -- but codegen
328/// still rewrites `</script` to `<\/script` inside one, so the output can sit
329/// inside an HTML `<script>` element without closing it early. Measured
330/// against oxc: that sequence and no other. `</div`, `</style`, `<!--`, `-->`
331/// and a bare `/` are all emitted unchanged, as is a `<\/` the author wrote.
332///
333/// Comparing the two spellings as different text cost the whole file: the
334/// matcher below walks originals in order, so one comment it cannot recognise
335/// consumes every remaining original and fails the run.
336fn equal_ignoring_whitespace(left: &str, right: &str) -> bool {
337    fn significant(text: &str) -> impl Iterator<Item = char> + '_ {
338        let mut rest = text.char_indices();
339        std::iter::from_fn(move || {
340            loop {
341                let (index, character) = rest.next()?;
342                // Normalise both sides, so a `<\/` the author wrote still matches
343                // itself and only the codegen artefact is collapsed.
344                if character == '\\'
345                    && text[index + 1..].starts_with('/')
346                    && text[..index].ends_with('<')
347                {
348                    continue;
349                }
350                if !character.is_whitespace() {
351                    return Some(character);
352                }
353            }
354        })
355    }
356    significant(left).eq(significant(right))
357}
358
359/// Line starts, and for each line how to turn a UTF-16 column into bytes
360/// without walking the line from its start.
361///
362/// Every source-map token is converted, and walking the line made a file with
363/// long lines cost the product of its tokens and its line length: recording
364/// Actual stopped for more than half an hour inside one such file. An ASCII
365/// line's column is its byte offset; a line that is not keeps a checkpoint
366/// every `CHECKPOINT` UTF-16 units, so a lookup walks at most that far.
367struct Utf16LineIndex<'s> {
368    source: &'s str,
369    starts: Vec<usize>,
370    /// Per line: `None` when ASCII, otherwise `(utf16 column, byte offset)`
371    /// checkpoints from the line's start, the first being `(0, 0)`.
372    checkpoints: Vec<Option<Vec<(usize, usize)>>>,
373}
374
375const CHECKPOINT: usize = 256;
376
377impl<'s> Utf16LineIndex<'s> {
378    fn new(source: &'s str) -> Self {
379        let mut starts = Vec::with_capacity(source.lines().count() + 1);
380        starts.push(0);
381        starts.extend(
382            source
383                .char_indices()
384                .filter_map(|(offset, character)| (character == '\n').then_some(offset + 1)),
385        );
386        let checkpoints = (0..starts.len())
387            .map(|line| {
388                let end = starts.get(line + 1).copied().unwrap_or(source.len());
389                let text = &source[starts[line]..end];
390                if text.is_ascii() {
391                    return None;
392                }
393                let mut marks = vec![(0, 0)];
394                let mut column = 0;
395                for (offset, character) in text.char_indices() {
396                    if column >= marks.last().expect("a first mark").0 + CHECKPOINT {
397                        marks.push((column, offset));
398                    }
399                    column += character.len_utf16();
400                }
401                Some(marks)
402            })
403            .collect();
404        Self {
405            source,
406            starts,
407            checkpoints,
408        }
409    }
410
411    fn line_end(&self, line: usize) -> usize {
412        self.starts
413            .get(line + 1)
414            .copied()
415            .unwrap_or(self.source.len())
416    }
417
418    fn byte_offset(&self, target_line: usize, target_utf16_col: usize) -> usize {
419        let Some(&start) = self.starts.get(target_line) else {
420            return self.source.len();
421        };
422        let end = self.line_end(target_line);
423        match &self.checkpoints[target_line] {
424            None => start + target_utf16_col.min(end - start),
425            Some(marks) => {
426                let at = marks.partition_point(|(column, _)| *column <= target_utf16_col) - 1;
427                let (column, offset) = marks[at];
428                start
429                    + offset
430                    + utf16_col_to_byte(
431                        &self.source[start + offset..end],
432                        target_utf16_col - column,
433                    )
434            }
435        }
436    }
437
438    fn line_col(&self, byte_offset: usize) -> (u32, u32) {
439        let byte_offset = byte_offset.min(self.source.len());
440        let line = self.starts.partition_point(|start| *start <= byte_offset) - 1;
441        let start = self.starts[line];
442        let column = match &self.checkpoints[line] {
443            None => byte_offset - start,
444            Some(marks) => {
445                let at = marks.partition_point(|(_, offset)| *offset <= byte_offset - start) - 1;
446                let (column, offset) = marks[at];
447                column
448                    + self.source[start + offset..byte_offset]
449                        .chars()
450                        .map(char::len_utf16)
451                        .sum::<usize>()
452            }
453        };
454        (line as u32, column as u32)
455    }
456}
457
458fn utf16_col_to_byte(line: &str, target_utf16_col: usize) -> usize {
459    let mut column = 0;
460    for (offset, character) in line.char_indices() {
461        if column >= target_utf16_col {
462            return offset;
463        }
464        column += character.len_utf16();
465    }
466    line.len()
467}
468
469fn shift_source_map(
470    map: oxc_sourcemap::SourceMap,
471    generated: &str,
472    restored: &str,
473    edits: &[(usize, usize, String)],
474) -> oxc_sourcemap::SourceMap {
475    let generated_lines = Utf16LineIndex::new(generated);
476    let restored_lines = Utf16LineIndex::new(restored);
477    let mut edit_index = 0;
478    let mut shift = 0isize;
479    let tokens = map
480        .get_tokens()
481        .map(|token| {
482            let original_offset = generated_lines
483                .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
484            while let Some((start, end, replacement)) = edits.get(edit_index) {
485                if *end > original_offset {
486                    break;
487                }
488                shift += replacement.len() as isize - (*end - *start) as isize;
489                edit_index += 1;
490            }
491            let shifted_offset = edits
492                .get(edit_index)
493                .filter(|(start, end, _)| *start <= original_offset && original_offset < *end)
494                .map_or_else(
495                    || original_offset.saturating_add_signed(shift),
496                    |(start, _, _)| start.saturating_add_signed(shift),
497                );
498            let (dst_line, dst_col) = restored_lines.line_col(shifted_offset);
499            oxc_sourcemap::Token::new(
500                dst_line,
501                dst_col,
502                token.get_src_line(),
503                token.get_src_col(),
504                token.get_source_id(),
505                token.get_name_id(),
506            )
507        })
508        .collect::<Vec<_>>();
509    oxc_sourcemap::SourceMap::new(
510        map.get_file().cloned(),
511        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
512        map.get_source_root().map(str::to_string),
513        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
514        map.get_source_contents()
515            .map(|content| content.cloned())
516            .collect::<Vec<Option<Arc<str>>>>(),
517        tokens.into_boxed_slice(),
518        None,
519    )
520}
521
522/// Where a statement Supercov generated inside a function or block starts, in
523/// the source map: past the source's end, where no node of the program starts
524/// and no comment is attached (a comment ending the file is attached to its
525/// length, and oxc prints a comment where its position is printed), rewritten
526/// to an unmapped segment once the code is out. `None` when the source's last
527/// line has non-ASCII text, where oxc's column table has no entry that far;
528/// those files keep the generated statements unmapped-by-omission, as before.
529fn generated_statement_span(source: &str) -> Option<Span> {
530    let last_line = source.rfind('\n').map_or(0, |newline| newline + 1);
531    let length = u32::try_from(source.len()).ok()?;
532    (source[last_line..].is_ascii() && length < u32::MAX - 2)
533        .then(|| Span::new(length + 1, length + 2))
534}
535
536/// A coverage tool the tests run itself (c8, nyc, Node's `--test-coverage`,
537/// Jest, Vitest) reads the instrumented copy through its source map. oxc maps
538/// a node only when its start differs from the last one it mapped, and gives a
539/// generated node none, so a probe inherited the mapping before it: V8 counted
540/// a probe's `||` as a branch of the user's line, and the statement after a
541/// block Supercov added (whose brace took the statement's own start) lost its
542/// mapping, and with it its place in istanbul's report. Each such statement
543/// now starts at `generated_statement_span`, which this makes unmapped: the
544/// probe maps to nothing, and the user's statement after it maps again.
545///
546/// A block Supercov wraps around a statement takes that statement's span, so
547/// its closing brace maps to the statement's last character, where the
548/// statement's own last token or an inner block's brace often already mapped:
549/// the brace then went unmapped, and the enclosing `if` or function, ending
550/// there, left istanbul's report. An empty statement at the unmapped span
551/// before the brace lets it map again.
552struct GeneratedStatementSpans<'a, 's> {
553    ast: AstBuilder<'a>,
554    source: &'s str,
555    span: Span,
556    depth: usize,
557}
558
559impl GeneratedStatementSpans<'_, '_> {
560    /// A block or body Supercov made around the user's code, rather than one
561    /// the user wrote: its span starts somewhere other than a `{`.
562    fn borrowed(&self, span: Span) -> bool {
563        !span.is_empty()
564            && span != self.span
565            && self.source.as_bytes().get(span.start as usize) != Some(&b'{')
566    }
567}
568
569impl<'a> VisitMut<'a> for GeneratedStatementSpans<'a, '_> {
570    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
571        // Top-level ones stay unmapped by omission: the file's first mapping
572        // is then the user's, which Node's coverage takes for a range starting
573        // at the top of the file.
574        if self.depth > 0 && statement.span().is_empty() {
575            *statement.span_mut() = self.span;
576        }
577        self.depth += 1;
578        walk_mut::walk_statement(self, statement);
579        self.depth -= 1;
580    }
581
582    fn visit_block_statement(&mut self, block: &mut oxc_ast::ast::BlockStatement<'a>) {
583        if self.depth > 0 && block.span.is_empty() {
584            block.span = self.span;
585        } else if self.borrowed(block.span)
586            // Not after a jump: `allowUnreachableCode: false` would reject it
587            // in a TypeScript build of the instrumented copy.
588            && !matches!(
589                block.body.last(),
590                Some(
591                    Statement::ReturnStatement(_)
592                        | Statement::ThrowStatement(_)
593                        | Statement::BreakStatement(_)
594                        | Statement::ContinueStatement(_)
595                )
596            )
597        {
598            block.body.push(self.ast.statement_empty(self.span));
599        }
600        walk_mut::walk_block_statement(self, block);
601    }
602
603    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
604        // An arrow's expression body made a block: its brace maps just past
605        // the expression, where nothing inside it mapped.
606        if self.borrowed(body.span) && (body.span.end as usize) < self.source.len() {
607            body.span.end += 1;
608        }
609        self.depth += 1;
610        walk_mut::walk_function_body(self, body);
611        self.depth -= 1;
612    }
613}
614
615/// Node's own coverage looks a range's start up on the wrong line half the
616/// time when the range starts a line: with a source map, a line of the
617/// generated file ends, for it, where the next one starts, and its search
618/// takes whichever of the two it reaches first, which the length of the file
619/// decides. From the line before, it reads one column past that line's end,
620/// which gives the mapping before it: a top-level function was reported a
621/// line early, or lost its range and with it a branch, where the same file a
622/// line longer or shorter was reported right.
623///
624/// Only a function or class declaration at the top level starts a range at
625/// column 0. Each gets an empty statement on the line before it, which
626/// `map_closing_lines` maps to where the declaration starts: both lines then
627/// give the same answer, and nothing else is ever looked up on a line that
628/// holds only `;`.
629fn separate_top_level_declarations<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>, span: Span) {
630    let starts_a_range = |statement: &Statement<'a>| match statement {
631        Statement::FunctionDeclaration(function) => !function.declare,
632        Statement::ClassDeclaration(class) => !class.declare,
633        _ => false,
634    };
635    // Overload signatures stay next to their implementation.
636    let signature = |statement: &Statement<'a>| matches!(statement, Statement::FunctionDeclaration(function) if function.body.is_none());
637    let mut separated = ast.vec_with_capacity(program.body.len());
638    let mut after_signature = false;
639    for statement in program.body.take_in(ast.allocator) {
640        if starts_a_range(&statement) && !after_signature {
641            separated.push(ast.statement_empty(span));
642        }
643        after_signature = signature(&statement);
644        separated.push(statement);
645    }
646    program.body = separated;
647}
648
649fn mark_generated_statements<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>) {
650    let Some(span) = generated_statement_span(program.source_text) else {
651        return;
652    };
653    separate_top_level_declarations(ast, program, span);
654    GeneratedStatementSpans {
655        ast,
656        source: program.source_text,
657        span,
658        depth: 0,
659    }
660    .visit_program(program);
661}
662
663fn unmap_generated_statements(
664    map: oxc_sourcemap::SourceMap,
665    source: &str,
666) -> oxc_sourcemap::SourceMap {
667    let Some(span) = generated_statement_span(source) else {
668        return map;
669    };
670    let line = source.matches('\n').count() as u32;
671    let column = span.start - source.rfind('\n').map_or(0, |newline| newline as u32 + 1);
672    let is_generated = |token: &oxc_sourcemap::Token| {
673        token.get_src_line() == line && token.get_src_col() == column
674    };
675    if !map.get_tokens().any(|token| is_generated(&token)) {
676        return map;
677    }
678    let tokens = map
679        .get_tokens()
680        .map(|token| {
681            if is_generated(&token) {
682                oxc_sourcemap::Token::new(
683                    token.get_dst_line(),
684                    token.get_dst_col(),
685                    0,
686                    0,
687                    None,
688                    None,
689                )
690            } else {
691                token
692            }
693        })
694        .collect::<Vec<_>>();
695    oxc_sourcemap::SourceMap::new(
696        map.get_file().cloned(),
697        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
698        map.get_source_root().map(str::to_string),
699        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
700        map.get_source_contents()
701            .map(|content| content.cloned())
702            .collect::<Vec<Option<Arc<str>>>>(),
703        tokens.into_boxed_slice(),
704        None,
705    )
706}
707
708/// A line of the instrumented copy starting with a closing brace Supercov
709/// added (a wrapper's `try` block, `} finally {`) has no mapping at that
710/// brace, and istanbul, c8 and v8-to-istanbul look a position up on its own
711/// line only: a V8 range or a statement ending there was dropped from their
712/// reports. Node's own coverage takes the nearest mapping before it instead;
713/// so does each such brace now, with a copy of that mapping, unmapped if it
714/// was.
715///
716/// oxc maps where a node starts, never a statement's closing `;`, so a range
717/// ending with `break;` ended, for Node's own coverage, at the `b`: the line
718/// was no longer inside the range of the `case` no test reached, and read as
719/// covered. When the text after a line's last mapping is the source's own
720/// text after that position, ending in `;`, the `;` maps to the source's.
721fn map_closing_lines(
722    map: oxc_sourcemap::SourceMap,
723    code: &str,
724    source: &str,
725) -> oxc_sourcemap::SourceMap {
726    let source_lines = source.split('\n').collect::<Vec<_>>();
727    let tokens = map.get_tokens().collect::<Vec<_>>();
728    let mut patched = Vec::with_capacity(tokens.len());
729    let mut next = 0;
730    let mut previous: Option<oxc_sourcemap::Token> = None;
731    let mut added = false;
732    for (line, text) in code.split('\n').enumerate() {
733        let line = line as u32;
734        let first = next;
735        while next < tokens.len() && tokens[next].get_dst_line() == line {
736            next += 1;
737        }
738        // The line `separate_top_level_declarations` put before a
739        // declaration maps to where that declaration starts.
740        if text == ";"
741            && next == first + 1
742            && tokens[first].get_source_id().is_none()
743            && let Some(declaration) = tokens.get(next)
744            && declaration.get_dst_col() == 0
745            && declaration.get_source_id().is_some()
746        {
747            let separator = oxc_sourcemap::Token::new(
748                line,
749                0,
750                declaration.get_src_line(),
751                declaration.get_src_col(),
752                declaration.get_source_id(),
753                None,
754            );
755            patched.push(separator);
756            previous = Some(separator);
757            added = true;
758            continue;
759        }
760        let column = (text.len() - text.trim_start().len()) as u32;
761        if text.trim_start().starts_with(['}', ')', ']'])
762            && (first == next || tokens[first].get_dst_col() > column)
763            && let Some(previous) = &previous
764        {
765            patched.push(oxc_sourcemap::Token::new(
766                line,
767                column,
768                previous.get_src_line(),
769                previous.get_src_col(),
770                previous.get_source_id(),
771                None,
772            ));
773            added = true;
774        }
775        patched.extend_from_slice(&tokens[first..next]);
776        if next > first {
777            let last = tokens[next - 1];
778            previous = Some(last);
779            if let (Some(_), Some(original)) = (
780                last.get_source_id(),
781                source_lines.get(last.get_src_line() as usize),
782            ) && text.is_ascii()
783                && original.is_ascii()
784            {
785                let generated_rest = text
786                    .get(last.get_dst_col() as usize..)
787                    .unwrap_or("")
788                    .trim_end();
789                let original_rest = original
790                    .get(last.get_src_col() as usize..)
791                    .unwrap_or("")
792                    .trim_end();
793                if generated_rest.len() > 1
794                    && generated_rest.ends_with(';')
795                    && original_rest.starts_with(generated_rest)
796                {
797                    let offset = (generated_rest.len() - 1) as u32;
798                    let end = oxc_sourcemap::Token::new(
799                        line,
800                        last.get_dst_col() + offset,
801                        last.get_src_line(),
802                        last.get_src_col() + offset,
803                        last.get_source_id(),
804                        None,
805                    );
806                    patched.push(end);
807                    previous = Some(end);
808                    added = true;
809                }
810            }
811        }
812    }
813    patched.extend_from_slice(&tokens[next..]);
814    if !added {
815        return map;
816    }
817    oxc_sourcemap::SourceMap::new(
818        map.get_file().cloned(),
819        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
820        map.get_source_root().map(str::to_string),
821        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
822        map.get_source_contents()
823            .map(|content| content.cloned())
824            .collect::<Vec<Option<Arc<str>>>>(),
825        patched.into_boxed_slice(),
826        None,
827    )
828}
829
830fn generate_candidate(
831    program: &Program<'_>,
832    file: &str,
833) -> Result<(String, Option<serde_json::Value>), CandidateError> {
834    let options = CodegenOptions {
835        source_map_path: Some(Path::new(file).to_path_buf()),
836        ..CodegenOptions::default()
837    };
838    let generated = Codegen::new().with_options(options).build(program);
839    // Unmapped before comments are restored: a restored comment goes where
840    // the first mapping at or after it in the source was printed, and the
841    // generated statements' placeholder is past every comment.
842    let map = unmap_generated_statements(
843        generated.map.expect("source maps are enabled"),
844        program.source_text,
845    );
846    let (code, map) = restore_comment_text(program, &generated.code, map)?;
847    let map = map_closing_lines(map, &code, program.source_text);
848    let map = Some({
849        serde_json::from_str(&map.to_json_string())
850            .expect("oxc must serialize its own generated source map")
851    });
852    Ok((code, map))
853}
854
855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
856#[serde(rename_all = "camelCase")]
857pub struct CandidateRuntime {
858    pub coverage_hit: String,
859    pub mcdc_begin: String,
860    pub mcdc_condition: String,
861    pub mcdc_end: String,
862    pub register_probe_v2: String,
863    pub mcdc_end_v2: String,
864    pub coverage_hit_v2: String,
865    pub probe_file_v2: String,
866    pub parenthesized_assignment_value: String,
867    pub with_request_phase: String,
868    pub optional_select_v2: String,
869    pub select_short_v2: String,
870    pub select_right_v2: String,
871    pub select_named_right_v2: String,
872    pub select_assign_end_v2: String,
873    pub select_path_v2: String,
874    pub select_end2_v2: String,
875    pub select_end_v2: String,
876    pub select_next_v2: String,
877    pub rendered_value_v2: String,
878    pub optional_call_end_v2: String,
879    pub default_selected_v2: String,
880    pub default_entered_v2: String,
881}
882
883#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
884#[serde(rename_all = "camelCase")]
885pub struct CandidateLimitation {
886    pub id: String,
887    pub kind: String,
888    pub file: String,
889    pub line: usize,
890    pub column: usize,
891    pub source: String,
892    pub reason: String,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq)]
896pub enum CandidateError {
897    UnknownSourceType(String),
898    Parse(Vec<String>),
899    CommentPreservation {
900        expected: usize,
901        actual: usize,
902        detail: String,
903    },
904}
905
906impl std::fmt::Display for CandidateError {
907    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
908        match self {
909            Self::UnknownSourceType(file) => {
910                write!(formatter, "no JavaScript source type for {file}")
911            }
912            Self::Parse(messages) => write!(formatter, "{}", messages.join("; ")),
913            Self::CommentPreservation {
914                expected,
915                actual,
916                detail,
917            } => write!(
918                formatter,
919                "{expected} comments in the source, {actual} after instrumenting: {detail}"
920            ),
921        }
922    }
923}
924
925/// A parser error as a person reads it: the message, and where.
926fn parse_message(source: &str, message: &str, offset: Option<usize>) -> String {
927    match offset.filter(|offset| *offset <= source.len()) {
928        Some(offset) => {
929            let (line, column) = line_and_utf16_column(source, offset);
930            format!("{message} at line {line}, column {}", column + 1)
931        }
932        None => message.to_owned(),
933    }
934}
935
936#[derive(Debug, Clone, Copy, PartialEq, Eq)]
937enum RuntimeBinding {
938    ModuleImport,
939    DirectGlobal,
940}
941
942const NODE_ASSERT_MODULES: &[&str] = &[
943    "assert",
944    "assert/strict",
945    "node:assert",
946    "node:assert/strict",
947];
948const NODE_ASSERT_METHODS: &[&str] = &[
949    "deepEqual",
950    "deepStrictEqual",
951    "doesNotMatch",
952    "doesNotReject",
953    "doesNotThrow",
954    "equal",
955    "fail",
956    "ifError",
957    "match",
958    "notDeepEqual",
959    "notDeepStrictEqual",
960    "notEqual",
961    "notStrictEqual",
962    "ok",
963    "partialDeepStrictEqual",
964    "rejects",
965    "strictEqual",
966    "throws",
967];
968
969#[derive(Debug, Clone, PartialEq, Eq)]
970pub struct NodeAssertionInstrumentation {
971    pub code: String,
972    pub assertions: usize,
973    pub capability_imports: usize,
974}
975
976const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
977    &["@jest/globals", "@playwright/test", "playwright", "vitest"];
978
979fn capability_source_candidate(source: &str) -> bool {
980    // A capability wrapper is useful only when an argument can establish a
981    // host/guest workspace mapping. Generic words such as `snapshot`,
982    // `machine`, `acquire` and `spawn` are common in tests and application
983    // code; treating any of them as sufficient wrapped unrelated framework
984    // registration functions and changed their captured callsite.
985    let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
986        || (source.contains("hostRoot") && source.contains("guestRoot"));
987    // The guest path is commonly computed (for example
988    // `resolve(tmpdir(), "workspace")`), so requiring the literal
989    // `/workspace` loses genuine opaque launchers. The nested mount shape is
990    // already specific, and the AST pass below still restricts wrapping to
991    // the imported root actually called with that argument.
992    let mount_mapping = (source.contains("mounts") || source.contains("Mounts"))
993        && source.contains("source")
994        && source.contains("target");
995    direct_mapping || mount_mapping
996}
997
998fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
999    source == wrapper
1000        || source.starts_with("node:")
1001        || source.starts_with("virtual:supercov-")
1002        || source.contains(".supercov/")
1003        || CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
1004}
1005
1006fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
1007    match expression {
1008        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1009        Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
1010        Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
1011        Expression::CallExpression(call) => capability_callee_root(&call.callee),
1012        // `new GenericContainer(..).withBindMounts(..)`: the builder's class.
1013        Expression::NewExpression(construction) => capability_callee_root(&construction.callee),
1014        Expression::ParenthesizedExpression(parenthesized) => {
1015            capability_callee_root(&parenthesized.expression)
1016        }
1017        Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
1018        Expression::TSSatisfiesExpression(expression) => {
1019            capability_callee_root(&expression.expression)
1020        }
1021        Expression::TSNonNullExpression(expression) => {
1022            capability_callee_root(&expression.expression)
1023        }
1024        Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
1025        _ => None,
1026    }
1027}
1028
1029struct CapabilityCallCollector<'s> {
1030    source: &'s str,
1031    mapping_variables: HashSet<String>,
1032    roots: HashSet<String>,
1033}
1034
1035impl CapabilityCallCollector<'_> {
1036    fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
1037        if let Argument::Identifier(identifier) = argument
1038            && self.mapping_variables.contains(identifier.name.as_str())
1039        {
1040            return true;
1041        }
1042        capability_source_candidate(source_slice(self.source, argument.span()))
1043    }
1044}
1045
1046impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
1047    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
1048        if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
1049            (&declarator.id, &declarator.init)
1050            && capability_source_candidate(source_slice(self.source, initializer.span()))
1051        {
1052            self.mapping_variables.insert(identifier.name.to_string());
1053        }
1054        walk::walk_variable_declarator(self, declarator);
1055    }
1056
1057    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1058        // A builder method named for mounts (`withBindMounts`) takes the
1059        // mounts themselves, `[{ source, target }]`, with no `mounts` key.
1060        let mounting = matches!(&call.callee, Expression::StaticMemberExpression(member)
1061            if member.property.name.to_ascii_lowercase().contains("mount"));
1062        let mounted = |argument: &Argument<'_>| {
1063            let text = source_slice(self.source, argument.span());
1064            mounting && text.contains("source") && text.contains("target")
1065        };
1066        if call
1067            .arguments
1068            .iter()
1069            .any(|argument| self.argument_has_mapping(argument) || mounted(argument))
1070            && let Some(root) = capability_callee_root(&call.callee)
1071        {
1072            self.roots.insert(root);
1073        }
1074        walk::walk_call_expression(self, call);
1075    }
1076}
1077
1078fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
1079    let mut collector = CapabilityCallCollector {
1080        source,
1081        mapping_variables: HashSet::new(),
1082        roots: HashSet::new(),
1083    };
1084    // Mapping variables must be known before calls are inspected because a
1085    // declaration may appear after a function that closes over it.
1086    collector.visit_program(program);
1087    let mapping_variables = collector.mapping_variables.clone();
1088    collector.roots.clear();
1089    collector.mapping_variables = mapping_variables;
1090    collector.visit_program(program);
1091    collector.roots
1092}
1093
1094/// One change to a test file's text: `text` replaces `start..end`, an empty
1095/// range for an insertion. Insertions at one offset are ordered by `rank`, so
1096/// a wrapper that encloses another opens before it and closes after it.
1097struct TextEdit {
1098    start: usize,
1099    end: usize,
1100    rank: i64,
1101    text: String,
1102}
1103
1104impl TextEdit {
1105    fn insert(at: usize, rank: i64, text: String) -> Self {
1106        Self {
1107            start: at,
1108            end: at,
1109            rank,
1110            text,
1111        }
1112    }
1113
1114    fn replace(start: usize, end: usize, text: String) -> Self {
1115        Self {
1116            start,
1117            end,
1118            rank: 0,
1119            text,
1120        }
1121    }
1122}
1123
1124/// Apply edits to a test file, keeping every line of it where it was.
1125///
1126/// A test file used to be reprinted from its syntax tree to add assertion
1127/// wrappers, which moved lines and reflowed literals: a `@ts-expect-error`
1128/// above `period: { period: 'fortnight', amount: 1 }` then sat above a line
1129/// with no error, and the error surfaced on the next one (TS2578, TS2322).
1130/// Edits never add or remove a line break, so a directive, a comment and a
1131/// stack-trace line number all stay where the author put them. The source map
1132/// maps every copied run of the original back to itself.
1133fn apply_text_edits(
1134    source: &str,
1135    mut edits: Vec<TextEdit>,
1136    file: &str,
1137) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
1138    edits.sort_by_key(|edit| (edit.start, edit.end > edit.start, edit.rank));
1139    let mut builder = oxc_sourcemap::SourceMapBuilder::default();
1140    let source_id = builder.set_source_and_content(file, source);
1141    let mut code = String::with_capacity(source.len() + edits.len() * 64);
1142    let (mut line, mut column) = (0u32, 0u32);
1143    let (mut source_line, mut source_column) = (0u32, 0u32);
1144    let advance = |text: &str, line: &mut u32, column: &mut u32| {
1145        for unit in text.chars() {
1146            if unit == '\n' {
1147                *line += 1;
1148                *column = 0;
1149            } else {
1150                *column += unit.len_utf16() as u32;
1151            }
1152        }
1153    };
1154    let copy = |from: usize,
1155                to: usize,
1156                code: &mut String,
1157                line: &mut u32,
1158                column: &mut u32,
1159                source_line: &mut u32,
1160                source_column: &mut u32,
1161                builder: &mut oxc_sourcemap::SourceMapBuilder| {
1162        for (index, piece) in source[from..to].split('\n').enumerate() {
1163            if index > 0 {
1164                code.push('\n');
1165                *line += 1;
1166                *column = 0;
1167                *source_line += 1;
1168                *source_column = 0;
1169            }
1170            if !piece.is_empty() {
1171                builder.add_token(
1172                    *line,
1173                    *column,
1174                    *source_line,
1175                    *source_column,
1176                    Some(source_id),
1177                    None,
1178                );
1179                code.push_str(piece);
1180                let width = piece.encode_utf16().count() as u32;
1181                *column += width;
1182                *source_column += width;
1183            }
1184        }
1185    };
1186    let mut at = 0;
1187    for edit in edits {
1188        if edit.start < at || edit.end > source.len() || edit.start > edit.end {
1189            return Err(CandidateError::Parse(vec![format!(
1190                "overlapping assertion edits in {file}"
1191            )]));
1192        }
1193        copy(
1194            at,
1195            edit.start,
1196            &mut code,
1197            &mut line,
1198            &mut column,
1199            &mut source_line,
1200            &mut source_column,
1201            &mut builder,
1202        );
1203        let removed = &source[edit.start..edit.end];
1204        if edit.text.matches('\n').count() != removed.matches('\n').count() {
1205            return Err(CandidateError::Parse(vec![format!(
1206                "an assertion edit in {file} would move a line"
1207            )]));
1208        }
1209        code.push_str(&edit.text);
1210        advance(&edit.text, &mut line, &mut column);
1211        advance(removed, &mut source_line, &mut source_column);
1212        at = edit.end;
1213    }
1214    copy(
1215        at,
1216        source.len(),
1217        &mut code,
1218        &mut line,
1219        &mut column,
1220        &mut source_line,
1221        &mut source_column,
1222        &mut builder,
1223    );
1224    Ok((code, builder.into_sourcemap()))
1225}
1226
1227/// The runtime's assertion helpers as a TypeScript test sees them, declared
1228/// once at the end of each instrumented file.
1229///
1230/// Reading `globalThis.__SUPERCOV_DIRECT_RUNTIME__` without a declaration is
1231/// an implicit `any` index on `typeof globalThis`, which `tsc --strict` rejects
1232/// (TS7017) at every wrapped assertion; Actual's browser build failed on 66 of
1233/// them. A wrapped assertion keeps its own result type, and a bound matcher
1234/// keeps the method's own signature, overloads included, as `T[K]`: inferring
1235/// it through a conditional type made generic matchers `never` (TS2349,
1236/// TS2769). Every instrumented file declares the same type, which TypeScript
1237/// merges.
1238const ASSERTION_RUNTIME_TYPE: &str = "{ \
1239    withNodeAssertionPhase<T>(operation: string, source: string, callback: () => T): T; \
1240    openNodeAssertionPhase(operation: string, source: string): { close<T>(value: T): T; close(): void; fail(error: unknown): unknown }; \
1241    bindNodeAssertionPhase<T>(operation: string, source: string, target: T, property: null): T; \
1242    bindNodeAssertionPhase<T, K extends keyof T>(operation: string, source: string, target: T, property: K): T[K]; \
1243    bindNodeAssertionPhase(operation: string, source: string, target: unknown, property: PropertyKey): any; \
1244    }";
1245
1246const ASSERTION_RUNTIME: &str = "globalThis.__SUPERCOV_DIRECT_RUNTIME__";
1247
1248/// Where each recognised assertion is wrapped or bound, as text edits.
1249struct AssertionEdits<'s> {
1250    source: &'s str,
1251    sites: HashMap<SpanKey, (String, String, bool)>,
1252    edits: Vec<TextEdit>,
1253    /// In TypeScript the call runs in place, under these two names, instead
1254    /// of in a callback, which TypeScript narrows nothing into.
1255    in_place: Option<(String, String)>,
1256}
1257
1258impl AssertionEdits<'_> {
1259    fn opening(helper: &str, operation: &str, source: &str) -> String {
1260        format!(
1261            "{ASSERTION_RUNTIME}.{helper}({}, {}, ",
1262            serde_json::to_string(operation).expect("a string serializes"),
1263            serde_json::to_string(source).expect("a string serializes")
1264        )
1265    }
1266
1267    fn open(operation: &str, source: &str) -> String {
1268        format!(
1269            "{ASSERTION_RUNTIME}.openNodeAssertionPhase({}, {})",
1270            serde_json::to_string(operation).expect("a string serializes"),
1271            serde_json::to_string(source).expect("a string serializes")
1272        )
1273    }
1274
1275    /// Open the assertion phase around the call, before its arguments run.
1276    ///
1277    /// In TypeScript the call stays in its own flow: an immediately invoked
1278    /// arrow, which TypeScript narrows into, opens the phase as its argument.
1279    fn wrap(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1280        let (start, end) = (call.span.start as usize, call.span.end as usize);
1281        let width = (end - start) as i64;
1282        let (opening, closing) = match &self.in_place {
1283            Some((phase, error)) => (
1284                format!("(({phase}) => {{ try {{ return {phase}.close("),
1285                format!(
1286                    "); }} catch ({error}) {{ throw {phase}.fail({error}); }} }})({})",
1287                    Self::open(operation, source)
1288                ),
1289            ),
1290            None => (
1291                format!(
1292                    "{}() => (",
1293                    Self::opening("withNodeAssertionPhase", operation, source)
1294                ),
1295                "))".into(),
1296            ),
1297        };
1298        self.edits.push(TextEdit::insert(start, -width, opening));
1299        self.edits.push(TextEdit::insert(end, width, closing));
1300    }
1301
1302    /// Open the assertion phase around a TypeScript statement that is the
1303    /// call, leaving the call a statement: an `asserts` signature narrows
1304    /// only from an expression statement. `finally` closes the phase, so a
1305    /// call typed `never` leaves nothing after it unreachable.
1306    fn wrap_statement(
1307        &mut self,
1308        statement: &ExpressionStatement<'_>,
1309        operation: &str,
1310        source: &str,
1311    ) {
1312        let Some((phase, error)) = &self.in_place else {
1313            return;
1314        };
1315        let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1316        let width = (end - start) as i64;
1317        let opening = format!(
1318            "{{ const {phase} = {}; try {{ ",
1319            Self::open(operation, source)
1320        );
1321        let closing = format!(
1322            " }} catch ({error}) {{ throw {phase}.fail({error}); }} finally {{ {phase}.close(); }} }}"
1323        );
1324        self.edits.push(TextEdit::insert(start, -width, opening));
1325        self.edits.push(TextEdit::insert(end, width, closing));
1326    }
1327
1328    /// Bind a TypeScript statement's callee to a local typed as the callee
1329    /// itself and call the local. An `asserts` signature narrows only through
1330    /// an identifier or a qualified name, explicitly typed (TS2776, TS2775),
1331    /// and a bound call is neither: ts-node refused every such test file. The
1332    /// phase still opens only when the call is made, after its awaited
1333    /// operands, as with any bound call.
1334    fn bind_statement(
1335        &mut self,
1336        statement: &ExpressionStatement<'_>,
1337        call: &CallExpression<'_>,
1338        callee: &str,
1339        operation: &str,
1340        source: &str,
1341    ) {
1342        let Some((local, _)) = self.in_place.clone() else {
1343            return;
1344        };
1345        let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1346        let width = (end - start) as i64;
1347        self.edits.push(TextEdit::insert(
1348            start,
1349            -width,
1350            format!("{{ const {local}: typeof {callee} = "),
1351        ));
1352        self.bind(call, operation, source);
1353        self.edits.push(TextEdit::insert(
1354            call.callee.span().end as usize,
1355            width,
1356            format!("; {local}"),
1357        ));
1358        self.edits.push(TextEdit::insert(end, width, " }".into()));
1359    }
1360
1361    /// Bind the original callee and receiver, leaving await and yield operands
1362    /// in their own function: no async thunk, extra await or new microtask.
1363    fn bind(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1364        let callee = call.callee.span();
1365        let width = (callee.end - callee.start) as i64;
1366        let opening = Self::opening("bindNodeAssertionPhase", operation, source);
1367        self.edits
1368            .push(TextEdit::insert(callee.start as usize, -width, opening));
1369        match &call.callee {
1370            Expression::StaticMemberExpression(member) => {
1371                // `.name` becomes `, "name")`, keeping whatever whitespace and
1372                // line breaks sat between the receiver and the dot.
1373                let between = &self.source
1374                    [member.object.span().end as usize..member.property.span.start as usize];
1375                let dot = between.rfind('.').unwrap_or(0);
1376                let mut text = String::from(",");
1377                text.push_str(&between[..dot]);
1378                text.push(' ');
1379                text.push_str(&between[dot + 1..]);
1380                text.push_str(
1381                    &serde_json::to_string(member.property.name.as_str())
1382                        .expect("a string serializes"),
1383                );
1384                text.push(')');
1385                self.edits.push(TextEdit::replace(
1386                    member.object.span().end as usize,
1387                    member.property.span.end as usize,
1388                    text,
1389                ));
1390            }
1391            Expression::ComputedMemberExpression(member) => {
1392                let object_end = member.object.span().end as usize;
1393                let open = object_end + self.source[object_end..].find('[').unwrap_or(0);
1394                self.edits
1395                    .push(TextEdit::replace(open, open + 1, ",".into()));
1396                let close = member.span.end as usize - 1;
1397                self.edits
1398                    .push(TextEdit::replace(close, close + 1, ")".into()));
1399            }
1400            _ => {
1401                self.edits.push(TextEdit::insert(
1402                    callee.end as usize,
1403                    width,
1404                    ", null)".into(),
1405                ));
1406            }
1407        }
1408    }
1409}
1410
1411impl<'a> Visit<'a> for AssertionEdits<'_> {
1412    fn visit_expression_statement(&mut self, statement: &ExpressionStatement<'a>) {
1413        if self.in_place.is_some()
1414            && let Expression::CallExpression(call) = &statement.expression
1415            && let Some(&(_, _, bound)) = self.sites.get(&span_key(call.span))
1416        {
1417            let entity = entity_name(&call.callee);
1418            if !bound || entity.is_some() {
1419                let (operation, source, _) = self
1420                    .sites
1421                    .remove(&span_key(call.span))
1422                    .expect("the site was just found");
1423                match entity {
1424                    Some(callee) if bound => {
1425                        self.bind_statement(statement, call, &callee, &operation, &source)
1426                    }
1427                    _ => self.wrap_statement(statement, &operation, &source),
1428                }
1429            }
1430        }
1431        walk::walk_expression_statement(self, statement);
1432    }
1433
1434    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1435        if let Some((operation, source, bound)) = self.sites.remove(&span_key(call.span)) {
1436            if bound {
1437                self.bind(call, &operation, &source);
1438            } else {
1439                self.wrap(call, &operation, &source);
1440            }
1441        }
1442        walk::walk_call_expression(self, call);
1443    }
1444}
1445
1446/// A callee that is an identifier or a qualified name (`assert`,
1447/// `assert.strict.ok`), dotted and without whitespace: what `typeof` takes.
1448fn entity_name(callee: &Expression<'_>) -> Option<String> {
1449    match callee {
1450        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1451        Expression::StaticMemberExpression(member) if !member.optional => Some(format!(
1452            "{}.{}",
1453            entity_name(&member.object)?,
1454            member.property.name
1455        )),
1456        _ => None,
1457    }
1458}
1459
1460/// `transform_capability_imports` as text edits, for a test file.
1461fn capability_import_edits(
1462    program: &Program<'_>,
1463    source: &str,
1464    wrapper: &str,
1465) -> (Vec<TextEdit>, usize) {
1466    if !capability_source_candidate(source) || !program.source_type.is_module() {
1467        return (Vec::new(), 0);
1468    }
1469    let capability_roots = capability_import_roots(program, source);
1470    if capability_roots.is_empty() {
1471        return (Vec::new(), 0);
1472    }
1473    let mut names = CandidateNames::new(source);
1474    let wrapper_local = names.allocate("__supercovImportedCapability");
1475    let mut edits = Vec::new();
1476    let mut wrapped = 0;
1477    for statement in &program.body {
1478        let Statement::ImportDeclaration(declaration) = statement else {
1479            continue;
1480        };
1481        if declaration.import_kind == ImportOrExportKind::Type
1482            || excluded_capability_import(declaration.source.value.as_str(), wrapper)
1483        {
1484            continue;
1485        }
1486        let mut declarators = Vec::new();
1487        for specifier in declaration.specifiers.iter().flatten() {
1488            let (local, shorthand) = match specifier {
1489                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1490                    if specifier.import_kind == ImportOrExportKind::Type =>
1491                {
1492                    continue;
1493                }
1494                ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
1495                    &specifier.local,
1496                    specifier.imported.span() == specifier.local.span,
1497                ),
1498                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1499                    (&specifier.local, false)
1500                }
1501                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1502                    (&specifier.local, false)
1503                }
1504            };
1505            let original = local.name.to_string();
1506            if !capability_roots.contains(&original) {
1507                continue;
1508            }
1509            let raw = names.allocate(&format!("__supercovRaw{original}"));
1510            if shorthand {
1511                edits.push(TextEdit::insert(
1512                    local.span.end as usize,
1513                    0,
1514                    format!(" as {raw}"),
1515                ));
1516            } else {
1517                edits.push(TextEdit::replace(
1518                    local.span.start as usize,
1519                    local.span.end as usize,
1520                    raw.clone(),
1521                ));
1522            }
1523            declarators.push(format!("{original} = {wrapper_local}({raw})"));
1524            wrapped += 1;
1525        }
1526        if !declarators.is_empty() {
1527            edits.push(TextEdit::insert(
1528                declaration.span.end as usize,
1529                i64::MAX,
1530                format!("; const {};", declarators.join(", ")),
1531            ));
1532        }
1533    }
1534    if wrapped > 0 {
1535        // First among the imports, as the syntax-tree transform put it, on the
1536        // first statement's own line.
1537        let first = program
1538            .body
1539            .first()
1540            .map_or(0, |statement| statement.span().start as usize);
1541        edits.push(TextEdit::insert(
1542            first,
1543            i64::MIN,
1544            format!(
1545                "import {{ wrapImportedCapability as {wrapper_local} }} from {}; ",
1546                serde_json::to_string(wrapper).expect("a string serializes")
1547            ),
1548        ));
1549    }
1550    (edits, wrapped)
1551}
1552
1553/// Wrap value imports that can hide a remote execution capability. This is an
1554/// ahead-of-run Rust transform; the runtime shim only proxies the imported
1555/// value and never parses or rewrites source.
1556fn transform_capability_imports<'a>(
1557    allocator: &'a Allocator,
1558    program: &mut Program<'a>,
1559    source: &str,
1560    wrapper: &str,
1561) -> usize {
1562    if !capability_source_candidate(source) || !program.source_type.is_module() {
1563        return 0;
1564    }
1565    let capability_roots = capability_import_roots(program, source);
1566    if capability_roots.is_empty() {
1567        return 0;
1568    }
1569    let ast = AstBuilder::new(allocator);
1570    let mut names = CandidateNames::new(source);
1571    let wrapper_local = names.allocate("__supercovImportedCapability");
1572    let mut wrapped = 0;
1573    let mut output = ast.vec();
1574    for statement in program.body.take_in(allocator) {
1575        let Statement::ImportDeclaration(mut declaration) = statement else {
1576            output.push(statement);
1577            continue;
1578        };
1579        let module_name = declaration.source.value.as_str();
1580        if declaration.import_kind == ImportOrExportKind::Type
1581            || excluded_capability_import(module_name, wrapper)
1582        {
1583            output.push(Statement::ImportDeclaration(declaration));
1584            continue;
1585        }
1586        let mut declarations = ast.vec();
1587        for specifier in declaration.specifiers.iter_mut().flatten() {
1588            let local = match specifier {
1589                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1590                    if specifier.import_kind == ImportOrExportKind::Type =>
1591                {
1592                    continue;
1593                }
1594                ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
1595                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1596                    &mut specifier.local
1597                }
1598                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1599                    &mut specifier.local
1600                }
1601            };
1602            let original = local.name.to_string();
1603            if !capability_roots.contains(&original) {
1604                continue;
1605            }
1606            let raw = names.allocate(&format!("__supercovRaw{original}"));
1607            local.name = ast.ident(&raw);
1608            let wrapped_value = ast.expression_call(
1609                Span::default(),
1610                ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
1611                NONE,
1612                ast.vec1(Argument::from(
1613                    ast.expression_identifier(Span::default(), ast.ident(&raw)),
1614                )),
1615                false,
1616            );
1617            declarations.push(ast.variable_declarator(
1618                Span::default(),
1619                VariableDeclarationKind::Const,
1620                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
1621                NONE,
1622                Some(wrapped_value),
1623                false,
1624            ));
1625            wrapped += 1;
1626        }
1627        output.push(Statement::ImportDeclaration(declaration));
1628        if !declarations.is_empty() {
1629            output.push(Statement::VariableDeclaration(
1630                ast.alloc_variable_declaration(
1631                    Span::default(),
1632                    VariableDeclarationKind::Const,
1633                    declarations,
1634                    false,
1635                ),
1636            ));
1637        }
1638    }
1639    if wrapped > 0 {
1640        output.insert(
1641            0,
1642            Statement::ImportDeclaration(ast.alloc_import_declaration(
1643                Span::default(),
1644                Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
1645                    Span::default(),
1646                    ast.module_export_name_identifier_name(
1647                        Span::default(),
1648                        ast.ident("wrapImportedCapability"),
1649                    ),
1650                    ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
1651                    ImportOrExportKind::Value,
1652                ))),
1653                ast.string_literal(Span::default(), ast.str(wrapper), None),
1654                None,
1655                NONE,
1656                ImportOrExportKind::Value,
1657            )),
1658        );
1659    }
1660    program.body = output;
1661    wrapped
1662}
1663
1664fn canonical_assert_module(value: &str) -> Option<String> {
1665    NODE_ASSERT_MODULES.contains(&value).then(|| {
1666        if value.starts_with("node:") {
1667            value.into()
1668        } else {
1669            format!("node:{value}")
1670        }
1671    })
1672}
1673
1674fn required_assert_module(
1675    expression: &Expression<'_>,
1676    scoping: &oxc_semantic::Scoping,
1677) -> Option<String> {
1678    if let Expression::CallExpression(call) = expression
1679        && matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
1680        && matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
1681        && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
1682    {
1683        return canonical_assert_module(module.value.as_str());
1684    }
1685    if let Expression::StaticMemberExpression(member) = expression
1686        && member.property.name == "strict"
1687        && let Some(module) = required_assert_module(&member.object, scoping)
1688    {
1689        return Some(if module == "node:assert" {
1690            "node:assert/strict".into()
1691        } else {
1692            module
1693        });
1694    }
1695    None
1696}
1697
1698#[derive(Default)]
1699struct NodeAssertionBindings {
1700    objects: HashMap<SymbolId, String>,
1701    direct: HashMap<SymbolId, String>,
1702    expects: HashSet<SymbolId>,
1703    /// Jest (and Jasmine) inject `expect` as a global next to `test`, `it`
1704    /// and `describe`: a file that reaches for all of those unresolved is a
1705    /// test file whose bare `expect` is the assertion.
1706    global_expect: bool,
1707}
1708
1709fn bind_object(
1710    bindings: &mut NodeAssertionBindings,
1711    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
1712    module: String,
1713) {
1714    if let Some(symbol) = identifier.symbol_id.get() {
1715        bindings.objects.insert(symbol, module);
1716    }
1717}
1718
1719fn bind_direct(
1720    bindings: &mut NodeAssertionBindings,
1721    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
1722    operation: String,
1723) {
1724    if let Some(symbol) = identifier.symbol_id.get() {
1725        bindings.direct.insert(symbol, operation);
1726    }
1727}
1728
1729fn bind_expect(
1730    bindings: &mut NodeAssertionBindings,
1731    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
1732) {
1733    if let Some(symbol) = identifier.symbol_id.get() {
1734        bindings.expects.insert(symbol);
1735    }
1736}
1737
1738struct NodeAssertionBindingCollector<'s> {
1739    bindings: NodeAssertionBindings,
1740    scoping: &'s oxc_semantic::Scoping,
1741    allow_contextual_expect: bool,
1742    expect_modules: HashSet<String>,
1743}
1744
1745impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
1746    fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
1747        let module_name = declaration.source.value.as_str();
1748        let assert_module = canonical_assert_module(module_name);
1749        let expect_module =
1750            self.expect_modules.contains(module_name) || self.allow_contextual_expect;
1751        for specifier in declaration.specifiers.iter().flatten() {
1752            match specifier {
1753                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1754                    if let Some(module) = &assert_module {
1755                        bind_object(&mut self.bindings, &specifier.local, module.clone());
1756                    } else if expect_module && specifier.local.name == "expect" {
1757                        bind_expect(&mut self.bindings, &specifier.local);
1758                    }
1759                }
1760                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1761                    if let Some(module) = &assert_module {
1762                        bind_object(&mut self.bindings, &specifier.local, module.clone());
1763                    }
1764                }
1765                ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
1766                    let imported = specifier.imported.name().to_string();
1767                    if let Some(module) = &assert_module {
1768                        if imported == "strict" {
1769                            bind_object(
1770                                &mut self.bindings,
1771                                &specifier.local,
1772                                "node:assert/strict".into(),
1773                            );
1774                        } else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
1775                            bind_direct(
1776                                &mut self.bindings,
1777                                &specifier.local,
1778                                format!("{module}.{imported}"),
1779                            );
1780                        }
1781                    } else if expect_module && imported == "expect" {
1782                        bind_expect(&mut self.bindings, &specifier.local);
1783                    }
1784                }
1785            }
1786        }
1787        walk::walk_import_declaration(self, declaration);
1788    }
1789
1790    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
1791        // CommonJS named expect imports use the same lexical symbol identities
1792        // as ESM; a local/shadowed require is deliberately not recognized.
1793        if let Some(Expression::CallExpression(call)) = &declarator.init
1794            && matches!(&call.callee, Expression::Identifier(i) if i.name == "require" && referenced_symbol(i, self.scoping).is_none())
1795            && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
1796            && self.expect_modules.contains(module.value.as_str())
1797            && let BindingPattern::ObjectPattern(pattern) = &declarator.id
1798        {
1799            for property in &pattern.properties {
1800                if property.key.static_name().as_deref() == Some("expect")
1801                    && let BindingPattern::BindingIdentifier(local) = &property.value
1802                {
1803                    bind_expect(&mut self.bindings, local);
1804                }
1805            }
1806        }
1807        if let Some(module) = declarator
1808            .init
1809            .as_ref()
1810            .and_then(|expression| required_assert_module(expression, self.scoping))
1811        {
1812            match &declarator.id {
1813                BindingPattern::BindingIdentifier(identifier) => {
1814                    bind_object(&mut self.bindings, identifier, module);
1815                }
1816                BindingPattern::ObjectPattern(pattern) => {
1817                    for property in &pattern.properties {
1818                        let Some(imported) = property.key.static_name() else {
1819                            continue;
1820                        };
1821                        let BindingPattern::BindingIdentifier(local) = &property.value else {
1822                            continue;
1823                        };
1824                        if imported == "strict" {
1825                            bind_object(&mut self.bindings, local, "node:assert/strict".into());
1826                        } else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
1827                            bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
1828                        }
1829                    }
1830                }
1831                _ => {}
1832            }
1833        }
1834        walk::walk_variable_declarator(self, declarator);
1835    }
1836}
1837
1838fn node_assertion_bindings(
1839    program: &Program<'_>,
1840    scoping: &oxc_semantic::Scoping,
1841    extra_expect_modules: &[String],
1842) -> NodeAssertionBindings {
1843    let allow_contextual_expect = program.source_text.contains("node:test")
1844        && program
1845            .source_text
1846            .split(|character: char| !character.is_alphanumeric() && character != '_')
1847            .any(|word| word == "expect");
1848    let mut collector = NodeAssertionBindingCollector {
1849        bindings: NodeAssertionBindings::default(),
1850        scoping,
1851        allow_contextual_expect,
1852        expect_modules: ["vitest", "@jest/globals", "expect", "@playwright/test"]
1853            .into_iter()
1854            .map(str::to_owned)
1855            .chain(extra_expect_modules.iter().cloned())
1856            .collect(),
1857    };
1858    collector.visit_program(program);
1859    let unresolved = scoping.root_unresolved_references();
1860    let uses = |name: &str| unresolved.contains_key(name);
1861    collector.bindings.global_expect =
1862        uses("expect") && (uses("test") || uses("it") || uses("describe"));
1863    collector.bindings
1864}
1865
1866struct NodeAssertionSiteCollector<'s> {
1867    source: &'s str,
1868    file: &'s str,
1869    bindings: &'s NodeAssertionBindings,
1870    scoping: &'s oxc_semantic::Scoping,
1871    sites: HashMap<SpanKey, (String, String, bool)>,
1872}
1873
1874/// How a project's JavaScript is parsed: in the script or module mode its
1875/// extension implies, with JSX allowed. React code is routinely written in
1876/// `.js` and compiled by Babel, Webpack or Vite, so a `.js` file with JSX in it
1877/// is ordinary; rejecting it failed a whole run before any test started.
1878/// Enabling JSX changes nothing for JavaScript without it, because `<` cannot
1879/// begin an expression there. TypeScript is left alone: in `.ts` a leading `<`
1880/// is a type assertion, and only `.tsx` means JSX.
1881pub fn project_source_type(path: &Path) -> Result<SourceType, String> {
1882    let source_type = SourceType::from_path(path).map_err(|error| error.to_string())?;
1883    Ok(if source_type.is_typescript() {
1884        source_type
1885    } else {
1886        source_type.with_jsx(true)
1887    })
1888}
1889
1890fn referenced_symbol(
1891    identifier: &oxc_ast::ast::IdentifierReference<'_>,
1892    scoping: &oxc_semantic::Scoping,
1893) -> Option<SymbolId> {
1894    identifier
1895        .reference_id
1896        .get()
1897        .and_then(|reference| scoping.get_reference(reference).symbol_id())
1898}
1899
1900fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1901    match expression {
1902        Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1903        Expression::ComputedMemberExpression(member) => match &member.expression {
1904            Expression::StringLiteral(literal) => Some(literal.value.as_str()),
1905            _ => None,
1906        },
1907        _ => None,
1908    }
1909}
1910
1911fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
1912    match expression {
1913        Expression::StaticMemberExpression(member) => Some(&member.object),
1914        Expression::ComputedMemberExpression(member) => Some(&member.object),
1915        _ => None,
1916    }
1917}
1918
1919fn expect_operation(
1920    callee: &Expression<'_>,
1921    bindings: &NodeAssertionBindings,
1922    scoping: &oxc_semantic::Scoping,
1923) -> Option<String> {
1924    let mut current = callee;
1925    let mut matchers = Vec::new();
1926    while let Some(name) = assertion_member_name(current) {
1927        matchers.push(name.to_owned());
1928        current = assertion_member_object(current)?;
1929    }
1930    matchers.reverse();
1931    let matcher = matchers.last()?;
1932    if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
1933        return None;
1934    }
1935    let Expression::CallExpression(expect_call) = current else {
1936        return None;
1937    };
1938    // `expect(value)` is the usual root, but the assertion APIs wrap it:
1939    // `expect.element(locator)` in Vitest Browser Mode, `expect.soft(value)`,
1940    // `expect.poll(fn)`. The receiver is still expect and the matcher is still
1941    // the assertion, so requiring a bare identifier here made every UI
1942    // assertion in a browser-mode test invisible -- and an assertion Supercov
1943    // cannot see can never be mapped or earn credit.
1944    let mut root = &expect_call.callee;
1945    let mut prefix = Vec::new();
1946    while let Some(name) = assertion_member_name(root) {
1947        prefix.push(name.to_owned());
1948        root = assertion_member_object(root)?;
1949    }
1950    prefix.reverse();
1951    let Expression::Identifier(identifier) = root else {
1952        return None;
1953    };
1954    let recognized = match referenced_symbol(identifier, scoping) {
1955        Some(symbol) => bindings.expects.contains(&symbol),
1956        None => bindings.global_expect && identifier.name == "expect",
1957    };
1958    recognized.then(|| {
1959        let mut parts = prefix;
1960        parts.extend(matchers.iter().cloned());
1961        format!("expect.{}", parts.join("."))
1962    })
1963}
1964
1965#[derive(Default)]
1966struct AwaitYieldScanner {
1967    found: bool,
1968}
1969
1970impl<'a> Visit<'a> for AwaitYieldScanner {
1971    fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
1972        self.found = true;
1973    }
1974
1975    fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
1976        self.found = true;
1977    }
1978
1979    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
1980
1981    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
1982}
1983
1984impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
1985    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1986        let operation = match &call.callee {
1987            Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
1988                .and_then(|symbol| {
1989                    self.bindings.direct.get(&symbol).cloned().or_else(|| {
1990                        self.bindings
1991                            .objects
1992                            .get(&symbol)
1993                            .map(|module| format!("{module}.ok"))
1994                    })
1995                }),
1996            callee => {
1997                let member_operation = assertion_member_object(callee).and_then(|object| {
1998                    let Expression::Identifier(identifier) = object else {
1999                        return None;
2000                    };
2001                    let symbol = referenced_symbol(identifier, self.scoping)?;
2002                    let method = assertion_member_name(callee)?;
2003                    self.bindings
2004                        .objects
2005                        .get(&symbol)
2006                        .filter(|_| NODE_ASSERT_METHODS.contains(&method))
2007                        .map(|module| format!("{module}.{method}"))
2008                });
2009                member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
2010            }
2011        };
2012        if let Some(operation) = operation {
2013            let mut unsafe_argument = AwaitYieldScanner::default();
2014            unsafe_argument.visit_expression(&call.callee);
2015            for argument in &call.arguments {
2016                unsafe_argument.visit_argument(argument);
2017            }
2018            let optional = call.optional
2019                || matches!(&call.callee,
2020                Expression::StaticMemberExpression(m) if m.optional)
2021                || matches!(&call.callee, Expression::ComputedMemberExpression(m) if m.optional);
2022            // Never manufacture a passed occurrence when a call can
2023            // short-circuit before invoking a matcher.
2024            if optional {
2025                walk::walk_call_expression(self, call);
2026                return;
2027            }
2028            let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
2029            self.sites.insert(
2030                span_key(call.span),
2031                (
2032                    operation,
2033                    format!("{}:{line}:{column}", self.file),
2034                    unsafe_argument.found,
2035                ),
2036            );
2037        }
2038        walk::walk_call_expression(self, call);
2039    }
2040}
2041
2042/// Attribute native node:assert and node:test expect calls by opening the
2043/// assertion phase before argument evaluation when possible, or binding the
2044/// callee when arguments contain await/yield. Lexical symbol identity avoids
2045/// wrapping a shadowed or merely assert-shaped user binding.
2046pub fn instrument_node_assertion_phases(
2047    source: &str,
2048    file: &str,
2049) -> Result<NodeAssertionInstrumentation, CandidateError> {
2050    instrument_node_assertion_phases_with_expect_modules(source, file, &[])
2051}
2052
2053pub fn instrument_node_assertion_phases_with_expect_modules(
2054    source: &str,
2055    file: &str,
2056    extra_expect_modules: &[String],
2057) -> Result<NodeAssertionInstrumentation, CandidateError> {
2058    instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
2059}
2060
2061pub fn instrument_node_assertion_phases_with_runtime_hooks(
2062    source: &str,
2063    file: &str,
2064    extra_expect_modules: &[String],
2065    capability_wrapper: Option<&str>,
2066) -> Result<NodeAssertionInstrumentation, CandidateError> {
2067    instrument_node_assertion_phases_with_runtime_imports(
2068        source,
2069        file,
2070        extra_expect_modules,
2071        capability_wrapper,
2072        None,
2073    )
2074}
2075
2076/// Attribute assertions and, for an ESM test module, make the runtime
2077/// dependency explicit in the transformed module itself. This is required for
2078/// opaque runners which copy an already-instrumented workspace into another
2079/// process, container, or VM while constructing a fresh environment. Static
2080/// imports preserve ESM evaluation ordering and do not depend on runner-specific
2081/// environment forwarding.
2082/// The body of the function [`runtime_loader`] calls: where Node runs, it
2083/// requires the bootstrap relative to the file the stack's first frame names.
2084const 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);""#;
2085
2086/// How the statement [`runtime_loader`] writes starts.
2087pub(crate) const RUNTIME_LOADER_START: &str = "if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {";
2088
2089/// One statement that loads the run's bootstrap where no runtime is
2090/// installed. It has to be valid in a module, a CommonJS file and a browser,
2091/// pass a strict TypeScript check with or without Node's types, and survive
2092/// compilers -- so no `import`, no `import.meta`, and nothing but
2093/// `globalThis.__SUPERCOV_DIRECT_RUNTIME__`, `Function` and `Error` outside a
2094/// function body the type checker does not read. The file's own location
2095/// comes from a stack taken at its top level; `bootstrap` is relative to it.
2096pub(crate) fn runtime_loader(bootstrap: &str) -> String {
2097    let specifier = serde_json::to_string(bootstrap).expect("a path string serializes");
2098    format!(
2099        "{RUNTIME_LOADER_START} new Function(\"stack\", \"specifier\", {RUNTIME_LOADER_BODY})(new Error().stack, {specifier}); }} catch {{}}"
2100    )
2101}
2102
2103pub fn instrument_node_assertion_phases_with_runtime_imports(
2104    source: &str,
2105    file: &str,
2106    extra_expect_modules: &[String],
2107    capability_wrapper: Option<&str>,
2108    assertion_runtime: Option<&str>,
2109) -> Result<NodeAssertionInstrumentation, CandidateError> {
2110    let _positions = PositionScope::enter();
2111    let assertion_candidate = source.contains("assert") || source.contains("expect");
2112    let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
2113    if !assertion_candidate && !capability_candidate {
2114        return Ok(NodeAssertionInstrumentation {
2115            code: source.into(),
2116            assertions: 0,
2117            capability_imports: 0,
2118        });
2119    }
2120    let source_type =
2121        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
2122    let allocator = Allocator::default();
2123    let parsed = Parser::new(&allocator, source, source_type).parse();
2124    if !parsed.errors.is_empty() {
2125        return Err(CandidateError::Parse(
2126            parsed
2127                .errors
2128                .into_iter()
2129                .map(|error| error.to_string())
2130                .collect(),
2131        ));
2132    }
2133    let mut assertions = 0;
2134    let mut edits = Vec::new();
2135
2136    if assertion_candidate {
2137        let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
2138        let bindings =
2139            node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
2140        let mut collector = NodeAssertionSiteCollector {
2141            source,
2142            file,
2143            bindings: &bindings,
2144            scoping: semantic.scoping(),
2145            sites: HashMap::new(),
2146        };
2147        collector.visit_program(&parsed.program);
2148        assertions = collector.sites.len();
2149        if assertions > 0 {
2150            let in_place = source_type.is_typescript().then(|| {
2151                let mut names = CandidateNames::new(source);
2152                (
2153                    names.allocate("__supercovAssertionPhase"),
2154                    names.allocate("__supercovAssertionError"),
2155                )
2156            });
2157            let mut located = AssertionEdits {
2158                source,
2159                sites: collector.sites,
2160                edits: Vec::new(),
2161                in_place,
2162            };
2163            located.visit_program(&parsed.program);
2164            edits.extend(located.edits);
2165        }
2166    }
2167    let capability_imports = capability_wrapper.map_or(0, |wrapper| {
2168        let (found, wrapped) = capability_import_edits(&parsed.program, source, wrapper);
2169        edits.extend(found);
2170        wrapped
2171    });
2172    if assertions == 0 && capability_imports == 0 {
2173        return Ok(NodeAssertionInstrumentation {
2174            code: source.into(),
2175            assertions: 0,
2176            capability_imports: 0,
2177        });
2178    }
2179    let module = parsed.program.source_type.is_module();
2180    // The runtime the assertion phases call, loaded where nothing installed
2181    // it. A statement rather than an `import`: an import's relative path broke
2182    // once the test command compiled the file somewhere else (`tsc` into
2183    // `dist/`), and the test failed on a module that was not there. It goes
2184    // first, as the import was evaluated first, on the first line so every
2185    // other line stays where it was.
2186    if let Some(bootstrap) = assertion_runtime.filter(|_| assertions > 0) {
2187        let at = if source.starts_with("#!") {
2188            source
2189                .find('\n')
2190                .map_or(source.len(), |newline| newline + 1)
2191        } else {
2192            0
2193        };
2194        edits.push(TextEdit::insert(
2195            at,
2196            i64::MIN,
2197            format!("{} ", runtime_loader(bootstrap)),
2198        ));
2199    }
2200    // An inline map is resolved relative to the transformed file itself, and
2201    // assertion-only transforms replace the file in place, so its basename is
2202    // the exact source-map reference.
2203    let reference = Path::new(file)
2204        .file_name()
2205        .and_then(|name| name.to_str())
2206        .unwrap_or(file);
2207    let (mut code, map) = apply_text_edits(source, edits, reference)?;
2208    // Append instead of prepend so every line of user code stays where it
2209    // was. Import declarations are instantiated before module evaluation
2210    // regardless of their textual position.
2211    if assertions > 0 && source_type.is_typescript() {
2212        if module {
2213            code.push_str(&format!(
2214                "\ndeclare global {{ var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE}; }}\n"
2215            ));
2216        } else {
2217            code.push_str(&format!(
2218                "\ndeclare var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE};\n"
2219            ));
2220        }
2221    }
2222    let map = map.to_json_string();
2223    code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
2224    BASE64_STANDARD.encode_string(map, &mut code);
2225    code.push('\n');
2226    Ok(NodeAssertionInstrumentation {
2227        code,
2228        assertions,
2229        capability_imports,
2230    })
2231}
2232
2233type SpanKey = (u32, u32);
2234
2235#[derive(Default)]
2236struct SafetyAnalysis {
2237    source_sensitive_functions: HashSet<SpanKey>,
2238    semantic_limitations: Vec<CandidateLimitation>,
2239    dynamic_limitations: Vec<CandidateLimitation>,
2240}
2241
2242/// The bindings a file reads function source through, in the places
2243/// `observes_function_source` recognises a function written in place:
2244/// `name.toString()`, `String(name)`, `${name}`, `"(" + name + ")"`, and the
2245/// same through parentheses, casts, `?:`, `||`, `&&`, `,` and `=`. A function
2246/// bound to one of them is shipped as text -- to a worker, a `vm` context, a
2247/// browser page -- where no probe exists, so it is left as source like one
2248/// read in place. Bindings are symbols, not names: a local `total` compared
2249/// with a number says nothing about a function `total` elsewhere. Another
2250/// file's reads are not visible here.
2251struct SourceReads<'s> {
2252    scoping: &'s oxc_semantic::Scoping,
2253    symbols: HashSet<SymbolId>,
2254}
2255
2256impl SourceReads<'_> {
2257    fn read(&mut self, expression: &Expression<'_>) {
2258        match expression {
2259            Expression::Identifier(identifier) => {
2260                self.symbols
2261                    .extend(referenced_symbol(identifier, self.scoping));
2262            }
2263            Expression::ParenthesizedExpression(inner) => self.read(&inner.expression),
2264            Expression::TSAsExpression(inner) => self.read(&inner.expression),
2265            Expression::TSSatisfiesExpression(inner) => self.read(&inner.expression),
2266            Expression::TSTypeAssertion(inner) => self.read(&inner.expression),
2267            Expression::TSNonNullExpression(inner) => self.read(&inner.expression),
2268            Expression::ConditionalExpression(inner) => {
2269                self.read(&inner.consequent);
2270                self.read(&inner.alternate);
2271            }
2272            Expression::LogicalExpression(inner) => {
2273                self.read(&inner.left);
2274                self.read(&inner.right);
2275            }
2276            Expression::SequenceExpression(inner) => {
2277                if let Some(last) = inner.expressions.last() {
2278                    self.read(last);
2279                }
2280            }
2281            Expression::AssignmentExpression(inner) => self.read(&inner.right),
2282            _ => {}
2283        }
2284    }
2285}
2286
2287impl<'a> Visit<'a> for SourceReads<'_> {
2288    fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
2289        if member.property.name == "toString" {
2290            self.read(&member.object);
2291        }
2292        walk::walk_static_member_expression(self, member);
2293    }
2294
2295    fn visit_computed_member_expression(
2296        &mut self,
2297        member: &oxc_ast::ast::ComputedMemberExpression<'a>,
2298    ) {
2299        self.read(&member.expression);
2300        walk::walk_computed_member_expression(self, member);
2301    }
2302
2303    fn visit_object_property(&mut self, property: &oxc_ast::ast::ObjectProperty<'a>) {
2304        if property.computed
2305            && let Some(key) = property.key.as_expression()
2306        {
2307            self.read(key);
2308        }
2309        walk::walk_object_property(self, property);
2310    }
2311
2312    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
2313        if expression_is_identifier(&call.callee, "String")
2314            && let Some(argument) = call.arguments.first().and_then(Argument::as_expression)
2315        {
2316            self.read(argument);
2317        }
2318        walk::walk_call_expression(self, call);
2319    }
2320
2321    fn visit_template_literal(&mut self, template: &oxc_ast::ast::TemplateLiteral<'a>) {
2322        for expression in &template.expressions {
2323            self.read(expression);
2324        }
2325        walk::walk_template_literal(self, template);
2326    }
2327
2328    fn visit_binary_expression(&mut self, binary: &oxc_ast::ast::BinaryExpression<'a>) {
2329        if matches!(
2330            binary.operator,
2331            BinaryOperator::Addition
2332                | BinaryOperator::LessThan
2333                | BinaryOperator::LessEqualThan
2334                | BinaryOperator::GreaterThan
2335                | BinaryOperator::GreaterEqualThan
2336        ) {
2337            self.read(&binary.left);
2338            self.read(&binary.right);
2339        }
2340        walk::walk_binary_expression(self, binary);
2341    }
2342}
2343
2344/// The bindings a function is reached through where it is defined: its own
2345/// name, and the variable it initializes or is assigned to.
2346fn bound_symbols<State>(
2347    own: Option<&oxc_ast::ast::BindingIdentifier<'_>>,
2348    scoping: &oxc_semantic::Scoping,
2349    context: &TraverseCtx<'_, State>,
2350) -> Vec<SymbolId> {
2351    let mut symbols = own
2352        .and_then(|identifier| identifier.symbol_id.get())
2353        .into_iter()
2354        .collect::<Vec<_>>();
2355    match context.ancestors().next() {
2356        Some(Ancestor::VariableDeclaratorInit(parent)) => symbols.extend(
2357            parent
2358                .id()
2359                .get_binding_identifier()
2360                .and_then(|identifier| identifier.symbol_id.get()),
2361        ),
2362        Some(Ancestor::AssignmentExpressionRight(parent)) => {
2363            if let AssignmentTarget::AssignmentTargetIdentifier(target) = parent.left() {
2364                symbols.extend(referenced_symbol(target, scoping));
2365            }
2366        }
2367        _ => {}
2368    }
2369    symbols
2370}
2371
2372struct SafetyScanner<'s> {
2373    source: &'s str,
2374    file: &'s str,
2375    scoping: &'s oxc_semantic::Scoping,
2376    source_reads: HashSet<SymbolId>,
2377    source_sensitive_functions: HashSet<SpanKey>,
2378    function_limitations: Vec<CandidateLimitation>,
2379    with_limitations: Vec<CandidateLimitation>,
2380    dynamic_limitations: Vec<CandidateLimitation>,
2381    unsafe_function_depth: usize,
2382    with_depth: usize,
2383}
2384
2385#[derive(Clone, Copy, PartialEq, Eq)]
2386enum PointPass {
2387    Statements,
2388    Functions,
2389}
2390
2391struct PointCollector<'s> {
2392    source: &'s str,
2393    file: &'s str,
2394    pass: PointPass,
2395    points: Vec<CandidatePoint>,
2396    statement_targets: HashMap<SpanKey, Vec<String>>,
2397    function_targets: HashMap<SpanKey, String>,
2398    rendered_targets: HashMap<SpanKey, String>,
2399    source_sensitive_functions: &'s HashSet<SpanKey>,
2400    erased_imports: &'s HashSet<SpanKey>,
2401    unsafe_function_depth: usize,
2402    with_depth: usize,
2403    ambient_depth: usize,
2404}
2405
2406#[derive(Default)]
2407struct PointAnalysis {
2408    points: Vec<CandidatePoint>,
2409    statement_targets: HashMap<SpanKey, Vec<String>>,
2410    function_targets: HashMap<SpanKey, String>,
2411    rendered_targets: HashMap<SpanKey, String>,
2412}
2413
2414#[derive(Clone)]
2415struct PointTarget {
2416    index: usize,
2417}
2418
2419impl PointCollector<'_> {
2420    fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
2421        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
2422        CandidatePoint {
2423            id: stable_id(self.source, self.file, kind, span, ""),
2424            kind: kind.to_string(),
2425            file: self.file.to_string(),
2426            line,
2427            column,
2428            source: source_slice(self.source, span).to_string(),
2429            label,
2430        }
2431    }
2432
2433    fn unsafe_context(&self) -> bool {
2434        self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
2435    }
2436
2437    fn exit_function(&mut self, span: Span) {
2438        if self.source_sensitive_functions.contains(&span_key(span)) {
2439            self.unsafe_function_depth -= 1;
2440        }
2441    }
2442}
2443
2444fn function_label<State>(
2445    own_name: Option<&str>,
2446    context: &TraverseCtx<'_, State>,
2447) -> Option<String> {
2448    if let Some(name) = own_name {
2449        return Some(name.to_string());
2450    }
2451    match context.ancestors().next()? {
2452        Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
2453        Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
2454        Ancestor::VariableDeclaratorInit(parent) => parent
2455            .id()
2456            .get_binding_identifier()
2457            .map(|identifier| identifier.name.to_string()),
2458        _ => None,
2459    }
2460}
2461
2462fn property_label(key: &PropertyKey<'_>) -> Option<String> {
2463    key.static_name()
2464        .map(|name| name.into_owned())
2465        .or_else(|| match key {
2466            PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
2467            _ => None,
2468        })
2469}
2470
2471fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
2472    match context.ancestors().next() {
2473        Some(Ancestor::ObjectPropertyValue(parent))
2474            if *parent.method() || *parent.kind() != PropertyKind::Init =>
2475        {
2476            *parent.span()
2477        }
2478        Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
2479        _ => span,
2480    }
2481}
2482
2483fn type_only_import(statement: &Statement<'_>) -> bool {
2484    let Statement::ImportDeclaration(declaration) = statement else {
2485        return false;
2486    };
2487    // Inline `import { type T }` can leave an empty runtime import under
2488    // verbatimModuleSyntax. Only declaration-level `import type` is universal.
2489    declaration.import_kind == ImportOrExportKind::Type
2490}
2491
2492/// Local binding classification, not dependency/taint analysis. The caller
2493/// enables implicit elision only for a recognized compiler configuration.
2494fn erased_imports(program: &Program<'_>, elide_type_imports: bool) -> HashSet<SpanKey> {
2495    let semantic = SemanticBuilder::new().build(program).semantic;
2496    let scoping = semantic.scoping();
2497    program
2498        .body
2499        .iter()
2500        .filter_map(|statement| {
2501            let Statement::ImportDeclaration(declaration) = statement else {
2502                return None;
2503            };
2504            let erased = declaration.import_kind == ImportOrExportKind::Type
2505                || (elide_type_imports
2506                    && program.source_type.is_typescript()
2507                    && !program.source_type.is_jsx()
2508                    && declaration.specifiers.as_ref().is_some_and(|specifiers| {
2509                        !specifiers.is_empty()
2510                            && specifiers.iter().all(|specifier| {
2511                                let local = match specifier {
2512                                    ImportDeclarationSpecifier::ImportSpecifier(s) => {
2513                                        if s.import_kind == ImportOrExportKind::Type {
2514                                            return true;
2515                                        }
2516                                        &s.local
2517                                    }
2518                                    ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
2519                                        &s.local
2520                                    }
2521                                    ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
2522                                        &s.local
2523                                    }
2524                                };
2525                                local.symbol_id.get().is_some_and(|symbol| {
2526                                    let mut refs =
2527                                        scoping.get_resolved_references(symbol).peekable();
2528                                    // An unused binding may be needed by a JSX/decorator
2529                                    // transform. Only positively identified type uses count.
2530                                    refs.peek().is_some()
2531                                        && refs.all(|r| r.is_type() && !r.is_value())
2532                                })
2533                            })
2534                    }));
2535            erased.then_some(span_key(declaration.span))
2536        })
2537        .collect()
2538}
2539
2540fn executable_statement(statement: &Statement<'_>) -> bool {
2541    !matches!(
2542        statement,
2543        Statement::BlockStatement(_)
2544            | Statement::EmptyStatement(_)
2545            | Statement::FunctionDeclaration(_)
2546            | Statement::ExportNamedDeclaration(_)
2547            | Statement::ExportDefaultDeclaration(_)
2548    ) && !statement.is_typescript_syntax()
2549        && !type_only_import(statement)
2550}
2551
2552impl<'a> Traverse<'a, ()> for PointCollector<'_> {
2553    fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
2554        let mut ancestors = context.ancestors();
2555        let parent = ancestors.next();
2556        let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
2557            && matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
2558        if self.pass != PointPass::Statements
2559            || self.unsafe_context()
2560            || !executable_statement(node)
2561            || self.erased_imports.contains(&span_key(node.span()))
2562            || matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
2563            || expression_arrow_body
2564        {
2565            return;
2566        }
2567        let point = self.point(node.span(), "statement", None);
2568        self.statement_targets
2569            .entry(span_key(node.span()))
2570            .or_default()
2571            .push(point.id.clone());
2572        self.points.push(point);
2573    }
2574
2575    // A JSX tree is one statement, so everything rendered inside it -- an
2576    // `aria-label={label(state)}`, a child `{formatted(value)}` -- counted as
2577    // covered the moment the component rendered once, even when that
2578    // expression never evaluated. It is also the seam a UI assertion attaches
2579    // to: `toHaveAccessibleName` is about the attribute and `toHaveTextContent`
2580    // about the child, and with one statement for the whole tree neither could
2581    // be named apart from the other.
2582    //
2583    // Only expressions that can independently fail to evaluate earn a point.
2584    // A bare identifier or member chain cannot -- it is reached exactly when
2585    // the enclosing statement is -- and a function is already measured where
2586    // it is called rather than where it is created.
2587    fn enter_expression(&mut self, node: &mut Expression<'a>, context: &mut TraverseCtx<'a, ()>) {
2588        if self.pass != PointPass::Statements
2589            || self.unsafe_context()
2590            || !matches!(
2591                context.ancestors().next(),
2592                Some(Ancestor::JSXExpressionContainerExpression(_))
2593            )
2594            || !rendered_expression_is_measurable(node)
2595        {
2596            return;
2597        }
2598        let span = node.span();
2599        let point = self.point(span, "statement", Some("jsx-rendered-expression".into()));
2600        self.rendered_targets
2601            .insert(span_key(span), point.id.clone());
2602        self.points.push(point);
2603    }
2604
2605    fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
2606        if self.pass != PointPass::Statements
2607            || self.unsafe_context()
2608            || node.is_typescript_syntax()
2609            || matches!(node, Declaration::FunctionDeclaration(_))
2610            || !matches!(
2611                context.ancestors().next(),
2612                Some(Ancestor::ExportNamedDeclarationDeclaration(_))
2613                    | Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2614            )
2615        {
2616            return;
2617        }
2618        let point = self.point(node.span(), "statement", None);
2619        self.statement_targets
2620            .entry(span_key(node.span()))
2621            .or_default()
2622            .push(point.id.clone());
2623        self.points.push(point);
2624    }
2625
2626    fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
2627        // oxc represents `export default class` directly inside
2628        // ExportDefaultDeclarationKind rather than through Declaration, so it
2629        // does not reach enter_declaration. Babel treats the inner class as the
2630        // executable statement obligation (excluding the `export default`
2631        // prefix), which is also the location where the probe belongs.
2632        if self.pass != PointPass::Statements
2633            || self.unsafe_context()
2634            || node.declare
2635            || !matches!(
2636                context.ancestors().next(),
2637                Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2638            )
2639        {
2640            return;
2641        }
2642        let point = self.point(node.span, "statement", None);
2643        self.statement_targets
2644            .entry(span_key(node.span))
2645            .or_default()
2646            .push(point.id.clone());
2647        self.points.push(point);
2648    }
2649
2650    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
2651        let point_span = function_point_span(node.span, context);
2652        let label = if point_span == node.span {
2653            function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
2654        } else {
2655            None
2656        };
2657        if self
2658            .source_sensitive_functions
2659            .contains(&span_key(node.span))
2660        {
2661            self.unsafe_function_depth += 1;
2662            return;
2663        }
2664        if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
2665            let point = self.point(point_span, "function", label);
2666            self.function_targets
2667                .insert(span_key(node.span), point.id.clone());
2668            self.points.push(point);
2669        }
2670    }
2671
2672    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
2673        self.exit_function(node.span);
2674    }
2675
2676    fn enter_arrow_function_expression(
2677        &mut self,
2678        node: &mut ArrowFunctionExpression<'a>,
2679        context: &mut TraverseCtx<'a, ()>,
2680    ) {
2681        let point_span = function_point_span(node.span, context);
2682        let label = if point_span == node.span {
2683            function_label(None, context)
2684        } else {
2685            None
2686        };
2687        if self
2688            .source_sensitive_functions
2689            .contains(&span_key(node.span))
2690        {
2691            self.unsafe_function_depth += 1;
2692            return;
2693        }
2694        if self.pass == PointPass::Functions && !self.unsafe_context() {
2695            let point = self.point(point_span, "function", label);
2696            self.function_targets
2697                .insert(span_key(node.span), point.id.clone());
2698            self.points.push(point);
2699        }
2700    }
2701
2702    fn exit_arrow_function_expression(
2703        &mut self,
2704        node: &mut ArrowFunctionExpression<'a>,
2705        _context: &mut TraverseCtx<'a, ()>,
2706    ) {
2707        self.exit_function(node.span);
2708    }
2709
2710    fn enter_with_statement(
2711        &mut self,
2712        _node: &mut WithStatement<'a>,
2713        _context: &mut TraverseCtx<'a, ()>,
2714    ) {
2715        self.with_depth += 1;
2716    }
2717
2718    fn exit_with_statement(
2719        &mut self,
2720        _node: &mut WithStatement<'a>,
2721        _context: &mut TraverseCtx<'a, ()>,
2722    ) {
2723        self.with_depth -= 1;
2724    }
2725
2726    fn enter_ts_global_declaration(
2727        &mut self,
2728        _node: &mut TSGlobalDeclaration<'a>,
2729        _context: &mut TraverseCtx<'a, ()>,
2730    ) {
2731        self.ambient_depth += 1;
2732    }
2733
2734    fn exit_ts_global_declaration(
2735        &mut self,
2736        _node: &mut TSGlobalDeclaration<'a>,
2737        _context: &mut TraverseCtx<'a, ()>,
2738    ) {
2739        self.ambient_depth -= 1;
2740    }
2741
2742    fn enter_ts_module_declaration(
2743        &mut self,
2744        node: &mut TSModuleDeclaration<'a>,
2745        _context: &mut TraverseCtx<'a, ()>,
2746    ) {
2747        if node.declare {
2748            self.ambient_depth += 1;
2749        }
2750    }
2751
2752    fn exit_ts_module_declaration(
2753        &mut self,
2754        node: &mut TSModuleDeclaration<'a>,
2755        _context: &mut TraverseCtx<'a, ()>,
2756    ) {
2757        if node.declare {
2758            self.ambient_depth -= 1;
2759        }
2760    }
2761}
2762
2763/// Whether a JSX expression container holds something that can independently
2764/// fail to evaluate, and so is worth a coverage obligation of its own.
2765fn rendered_expression_is_measurable(expression: &Expression<'_>) -> bool {
2766    match expression {
2767        // Already measured where it is called.
2768        Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
2769        // Reached exactly when the enclosing statement is.
2770        Expression::Identifier(_)
2771        | Expression::ThisExpression(_)
2772        | Expression::StaticMemberExpression(_)
2773        | Expression::ComputedMemberExpression(_)
2774        | Expression::PrivateFieldExpression(_)
2775        | Expression::JSXElement(_)
2776        | Expression::JSXFragment(_) => false,
2777        // `user?.id` is a member access that happens to short-circuit; it is
2778        // reached with the statement like any other. `user?.load()` is not.
2779        Expression::ChainExpression(chain) => {
2780            matches!(chain.expression, ChainElement::CallExpression(_))
2781        }
2782        other => !other.is_literal(),
2783    }
2784}
2785
2786fn collect_points<'a>(
2787    allocator: &'a Allocator,
2788    program: &mut Program<'a>,
2789    source: &str,
2790    file: &str,
2791    source_sensitive_functions: &HashSet<SpanKey>,
2792    erased_imports: &HashSet<SpanKey>,
2793) -> PointAnalysis {
2794    let mut analysis = PointAnalysis::default();
2795    for pass in [PointPass::Statements, PointPass::Functions] {
2796        let mut collector = PointCollector {
2797            source,
2798            file,
2799            pass,
2800            points: Vec::new(),
2801            statement_targets: HashMap::new(),
2802            function_targets: HashMap::new(),
2803            rendered_targets: HashMap::new(),
2804            source_sensitive_functions,
2805            erased_imports,
2806            unsafe_function_depth: 0,
2807            with_depth: 0,
2808            ambient_depth: 0,
2809        };
2810        traverse_mut(&mut collector, allocator, program, Default::default(), ());
2811        analysis.points.extend(collector.points);
2812        analysis
2813            .statement_targets
2814            .extend(collector.statement_targets);
2815        analysis.function_targets.extend(collector.function_targets);
2816        analysis.rendered_targets.extend(collector.rendered_targets);
2817    }
2818    analysis
2819}
2820
2821impl<'s> SafetyScanner<'s> {
2822    fn new(source: &'s str, file: &'s str, scoping: &'s oxc_semantic::Scoping) -> Self {
2823        Self {
2824            source,
2825            file,
2826            scoping,
2827            source_reads: HashSet::new(),
2828            source_sensitive_functions: HashSet::new(),
2829            function_limitations: Vec::new(),
2830            with_limitations: Vec::new(),
2831            dynamic_limitations: Vec::new(),
2832            unsafe_function_depth: 0,
2833            with_depth: 0,
2834        }
2835    }
2836
2837    fn limitation(
2838        &self,
2839        span: Span,
2840        kind: &str,
2841        suffix: &str,
2842        reason: &str,
2843    ) -> CandidateLimitation {
2844        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
2845        CandidateLimitation {
2846            id: stable_id(self.source, self.file, kind, span, suffix),
2847            kind: kind.to_string(),
2848            file: self.file.to_string(),
2849            line,
2850            column,
2851            source: source_slice(self.source, span).to_string(),
2852            reason: reason.to_string(),
2853        }
2854    }
2855
2856    fn enter_source_sensitive_function<State>(
2857        &mut self,
2858        span: Span,
2859        bindings: Vec<SymbolId>,
2860        context: &TraverseCtx<'_, State>,
2861    ) {
2862        // A function handed to a compile-time style macro never runs: the
2863        // bundler's plugin evaluates it while building and replaces the whole
2864        // call. Probing its body has nothing to measure and breaks the build
2865        // (StyleX rejects a block-bodied dynamic style). Leave it as source,
2866        // with no limitation: there is no runtime behavior to lose.
2867        if compile_time_macro_argument(context) {
2868            self.source_sensitive_functions.insert(span_key(span));
2869            self.unsafe_function_depth += 1;
2870            return;
2871        }
2872        let sensitive = observes_function_source(span, context)
2873            || bindings
2874                .iter()
2875                .any(|symbol| self.source_reads.contains(symbol));
2876        if sensitive {
2877            self.source_sensitive_functions.insert(span_key(span));
2878            let limitation = self.limitation(
2879                span,
2880                "semantic-safety",
2881                "function-source",
2882                "function body is left uninstrumented because this expression observes or coerces Function source text",
2883            );
2884            self.function_limitations.push(limitation);
2885            self.unsafe_function_depth += 1;
2886        }
2887    }
2888
2889    fn exit_source_sensitive_function(&mut self, span: Span) {
2890        if self.source_sensitive_functions.contains(&span_key(span)) {
2891            self.unsafe_function_depth -= 1;
2892        }
2893    }
2894
2895    fn is_unsafe_context(&self) -> bool {
2896        self.unsafe_function_depth > 0 || self.with_depth > 0
2897    }
2898}
2899
2900impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
2901    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
2902        let bindings = bound_symbols(node.id.as_ref(), self.scoping, context);
2903        self.enter_source_sensitive_function(node.span, bindings, context);
2904    }
2905
2906    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
2907        self.exit_source_sensitive_function(node.span);
2908    }
2909
2910    fn enter_arrow_function_expression(
2911        &mut self,
2912        node: &mut ArrowFunctionExpression<'a>,
2913        context: &mut TraverseCtx<'a, ()>,
2914    ) {
2915        let bindings = bound_symbols(None, self.scoping, context);
2916        self.enter_source_sensitive_function(node.span, bindings, context);
2917    }
2918
2919    fn exit_arrow_function_expression(
2920        &mut self,
2921        node: &mut ArrowFunctionExpression<'a>,
2922        _context: &mut TraverseCtx<'a, ()>,
2923    ) {
2924        self.exit_source_sensitive_function(node.span);
2925    }
2926
2927    fn enter_with_statement(
2928        &mut self,
2929        node: &mut WithStatement<'a>,
2930        _context: &mut TraverseCtx<'a, ()>,
2931    ) {
2932        let limitation = self.limitation(
2933            node.span,
2934            "semantic-safety",
2935            "with-environment",
2936            "with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
2937        );
2938        self.with_limitations.push(limitation);
2939        self.with_depth += 1;
2940    }
2941
2942    fn exit_with_statement(
2943        &mut self,
2944        _node: &mut WithStatement<'a>,
2945        _context: &mut TraverseCtx<'a, ()>,
2946    ) {
2947        self.with_depth -= 1;
2948    }
2949
2950    fn enter_call_expression(
2951        &mut self,
2952        node: &mut CallExpression<'a>,
2953        _context: &mut TraverseCtx<'a, ()>,
2954    ) {
2955        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
2956            return;
2957        }
2958        let limitation = self.limitation(
2959            node.span,
2960            "dynamic-code",
2961            "eval",
2962            "eval-generated source has no stable pre-run coverage denominator",
2963        );
2964        self.dynamic_limitations.push(limitation);
2965    }
2966
2967    fn enter_new_expression(
2968        &mut self,
2969        node: &mut NewExpression<'a>,
2970        _context: &mut TraverseCtx<'a, ()>,
2971    ) {
2972        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
2973            return;
2974        }
2975        let limitation = self.limitation(
2976            node.span,
2977            "dynamic-code",
2978            "Function",
2979            "Function-generated source has no stable pre-run coverage denominator",
2980        );
2981        self.dynamic_limitations.push(limitation);
2982    }
2983}
2984
2985fn analyze_safety<'a>(
2986    allocator: &'a Allocator,
2987    program: &mut Program<'a>,
2988    source: &str,
2989    file: &str,
2990) -> SafetyAnalysis {
2991    // oxc_traverse uses resolved lexical scope IDs while walking ancestry.
2992    // Building semantics here initializes those IDs without changing the AST.
2993    let scoping = SemanticBuilder::new()
2994        .build(program)
2995        .semantic
2996        .into_scoping();
2997    let mut reads = SourceReads {
2998        scoping: &scoping,
2999        symbols: HashSet::new(),
3000    };
3001    reads.visit_program(program);
3002    let mut scanner = SafetyScanner::new(source, file, &scoping);
3003    scanner.source_reads = reads.symbols;
3004    traverse_mut(&mut scanner, allocator, program, Default::default(), ());
3005    let mut semantic_limitations = scanner.function_limitations;
3006    semantic_limitations.extend(scanner.with_limitations);
3007    SafetyAnalysis {
3008        source_sensitive_functions: scanner.source_sensitive_functions,
3009        semantic_limitations,
3010        dynamic_limitations: scanner.dynamic_limitations,
3011    }
3012}
3013
3014fn span_key(span: Span) -> SpanKey {
3015    (span.start, span.end)
3016}
3017
3018fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
3019    matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
3020}
3021
3022fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
3023    match pattern {
3024        BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
3025        _ => None,
3026    }
3027}
3028
3029fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
3030    match expression {
3031        Expression::ArrowFunctionExpression(_) => true,
3032        Expression::FunctionExpression(function) => function.id.is_none(),
3033        Expression::ClassExpression(class) => class.id.is_none(),
3034        Expression::ParenthesizedExpression(expression) => {
3035            expression_is_anonymous_definition(&expression.expression)
3036        }
3037        Expression::TSAsExpression(expression) => {
3038            expression_is_anonymous_definition(&expression.expression)
3039        }
3040        Expression::TSSatisfiesExpression(expression) => {
3041            expression_is_anonymous_definition(&expression.expression)
3042        }
3043        Expression::TSTypeAssertion(expression) => {
3044            expression_is_anonymous_definition(&expression.expression)
3045        }
3046        Expression::TSNonNullExpression(expression) => {
3047            expression_is_anonymous_definition(&expression.expression)
3048        }
3049        _ => false,
3050    }
3051}
3052
3053/// Takes a type assertion off an assignment target before the program is
3054/// printed.
3055///
3056/// The printer writes `(buffer[0] as string) += child` without its
3057/// parentheses, and `buffer[0] as string += child` does not parse: hono's
3058/// `src/jsx/base.ts` and `src/helper/html/index.ts` came out as files neither
3059/// TypeScript nor a bundler could read. An assertion on a target says nothing
3060/// at run time, and the instrumented file is not type-checked, so the target
3061/// is printed without it.
3062struct AssertedTargetTransformer<'a> {
3063    ast: AstBuilder<'a>,
3064}
3065
3066impl<'a> AssertedTargetTransformer<'a> {
3067    fn target(
3068        expression: Expression<'a>,
3069    ) -> Result<oxc_ast::ast::SimpleAssignmentTarget<'a>, Expression<'a>> {
3070        Ok(match expression {
3071            Expression::Identifier(identifier) => {
3072                oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(identifier)
3073            }
3074            Expression::ComputedMemberExpression(member) => {
3075                oxc_ast::ast::SimpleAssignmentTarget::ComputedMemberExpression(member)
3076            }
3077            Expression::StaticMemberExpression(member) => {
3078                oxc_ast::ast::SimpleAssignmentTarget::StaticMemberExpression(member)
3079            }
3080            Expression::PrivateFieldExpression(member) => {
3081                oxc_ast::ast::SimpleAssignmentTarget::PrivateFieldExpression(member)
3082            }
3083            Expression::TSAsExpression(asserted) => {
3084                oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted)
3085            }
3086            Expression::TSSatisfiesExpression(asserted) => {
3087                oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted)
3088            }
3089            Expression::TSNonNullExpression(asserted) => {
3090                oxc_ast::ast::SimpleAssignmentTarget::TSNonNullExpression(asserted)
3091            }
3092            Expression::TSTypeAssertion(asserted) => {
3093                oxc_ast::ast::SimpleAssignmentTarget::TSTypeAssertion(asserted)
3094            }
3095            Expression::ParenthesizedExpression(parenthesized) => {
3096                return Self::target(parenthesized.unbox().expression);
3097            }
3098            other => return Err(other),
3099        })
3100    }
3101}
3102
3103impl<'a> VisitMut<'a> for AssertedTargetTransformer<'a> {
3104    fn visit_simple_assignment_target(
3105        &mut self,
3106        target: &mut oxc_ast::ast::SimpleAssignmentTarget<'a>,
3107    ) {
3108        loop {
3109            let inner = match target {
3110                oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
3111                    &mut asserted.expression
3112                }
3113                oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
3114                    &mut asserted.expression
3115                }
3116                _ => break,
3117            };
3118            match Self::target(inner.take_in(self.ast.allocator)) {
3119                Ok(bare) => *target = bare,
3120                Err(kept) => {
3121                    // Not a target the grammar allows; print it as it came.
3122                    match target {
3123                        oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
3124                            asserted.expression = kept;
3125                        }
3126                        oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
3127                            asserted.expression = kept;
3128                        }
3129                        _ => unreachable!("the target was matched above"),
3130                    }
3131                    break;
3132                }
3133            }
3134        }
3135        walk_mut::walk_simple_assignment_target(self, target);
3136    }
3137}
3138
3139/// The module a directly run TypeScript file names its helpers' types from.
3140/// The frontend replaces it with the path to the runtime beside the file.
3141pub const RUNTIME_TYPES_MODULE: &str = "virtual:supercov-helper-types";
3142
3143/// `typeof import("…").helper`, as the annotation of the variable a helper is
3144/// bound to.
3145fn runtime_helper_type<'a>(
3146    allocator: &'a Allocator,
3147    source_type: SourceType,
3148    helper: &str,
3149) -> Option<oxc_allocator::Box<'a, oxc_ast::ast::TSTypeAnnotation<'a>>> {
3150    let text = allocator.alloc_str(&format!(
3151        "var helper: typeof import(\"{RUNTIME_TYPES_MODULE}\").{helper};"
3152    ));
3153    let mut parsed = Parser::new(allocator, text, source_type).parse();
3154    if !parsed.errors.is_empty() {
3155        return None;
3156    }
3157    match parsed.program.body.first_mut()? {
3158        Statement::VariableDeclaration(declaration) => {
3159            declaration.declarations.first_mut()?.type_annotation.take()
3160        }
3161        _ => None,
3162    }
3163}
3164
3165struct AssignmentNameSafetyTransformer<'a> {
3166    ast: AstBuilder<'a>,
3167    parenthesized_assignment_value: String,
3168}
3169
3170impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
3171    fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
3172        walk_mut::walk_assignment_expression(self, assignment);
3173        let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
3174            return;
3175        };
3176        if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
3177            || assignment.span.start == identifier.span.start
3178            || !expression_is_anonymous_definition(&assignment.right)
3179        {
3180            return;
3181        }
3182        let right = assignment.right.take_in(self.ast.allocator);
3183        assignment.right = self.ast.expression_call(
3184            Span::default(),
3185            self.ast.expression_identifier(
3186                Span::default(),
3187                self.ast.ident(&self.parenthesized_assignment_value),
3188            ),
3189            NONE,
3190            self.ast.vec_from_array([
3191                Argument::from(right),
3192                Argument::from(self.ast.expression_string_literal(
3193                    Span::default(),
3194                    identifier.name,
3195                    None,
3196                )),
3197            ]),
3198            false,
3199        );
3200    }
3201}
3202
3203/// Compile-time style macros whose arguments the bundler consumes at build
3204/// time. Only the namespaced form (`stylex.create(...)`) is recognized: it is
3205/// how the StyleX documentation and its Vite/Babel plugins expect the API to
3206/// be used, and it needs no binding resolution here.
3207const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
3208    "create",
3209    "createTheme",
3210    "defineConsts",
3211    "defineVars",
3212    "firstThatWorks",
3213    "keyframes",
3214    "positionTry",
3215    "viewTransitionClass",
3216];
3217
3218fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
3219    context.ancestors().any(|ancestor| {
3220        let Ancestor::CallExpressionArguments(parent) = ancestor else {
3221            return false;
3222        };
3223        let Expression::StaticMemberExpression(member) = parent.callee() else {
3224            return false;
3225        };
3226        let Expression::Identifier(object) = &member.object else {
3227            return false;
3228        };
3229        object.name == "stylex"
3230            && COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
3231    })
3232}
3233
3234fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
3235    let mut child_end = span.end;
3236    for ancestor in context.ancestors() {
3237        match ancestor {
3238            Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
3239            Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
3240            Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
3241            Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
3242            Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
3243            Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
3244            Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
3245            Ancestor::LogicalExpressionLeft(parent) => {
3246                child_end = parent.span().end;
3247            }
3248            Ancestor::LogicalExpressionRight(parent) => {
3249                child_end = parent.span().end;
3250            }
3251            Ancestor::SequenceExpressionExpressions(parent) => {
3252                if child_end != parent.span().end {
3253                    return false;
3254                }
3255                child_end = parent.span().end;
3256            }
3257            Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
3258            Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
3259            Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
3260            Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
3261            Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
3262            Ancestor::ComputedMemberExpressionExpression(_) => return true,
3263            Ancestor::StaticMemberExpressionObject(parent) => {
3264                return parent.property().name == "toString";
3265            }
3266            Ancestor::CallExpressionArguments(parent) => {
3267                return expression_is_identifier(parent.callee(), "String");
3268            }
3269            Ancestor::TemplateLiteralExpressions(_) => return true,
3270            Ancestor::BinaryExpressionLeft(parent) => {
3271                return matches!(
3272                    parent.operator(),
3273                    BinaryOperator::Addition
3274                        | BinaryOperator::LessThan
3275                        | BinaryOperator::LessEqualThan
3276                        | BinaryOperator::GreaterThan
3277                        | BinaryOperator::GreaterEqualThan
3278                );
3279            }
3280            Ancestor::BinaryExpressionRight(parent) => {
3281                return matches!(
3282                    parent.operator(),
3283                    BinaryOperator::Addition
3284                        | BinaryOperator::LessThan
3285                        | BinaryOperator::LessEqualThan
3286                        | BinaryOperator::GreaterThan
3287                        | BinaryOperator::GreaterEqualThan
3288                );
3289            }
3290            _ => return false,
3291        }
3292    }
3293    false
3294}
3295
3296pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3297    let _positions = PositionScope::enter();
3298    let elide_type_imports = false;
3299    let source_type =
3300        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3301    let allocator = Allocator::default();
3302    let mut parsed = Parser::new(&allocator, source, source_type).parse();
3303    if !parsed.errors.is_empty() {
3304        return Err(CandidateError::Parse(
3305            parsed
3306                .errors
3307                .iter()
3308                .map(|error| {
3309                    parse_message(
3310                        source,
3311                        &error.message,
3312                        error
3313                            .labels
3314                            .as_ref()
3315                            .and_then(|labels| labels.first())
3316                            .map(|label| label.offset()),
3317                    )
3318                })
3319                .collect(),
3320        ));
3321    }
3322
3323    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3324    let erased = erased_imports(&parsed.program, elide_type_imports);
3325    let excluded_statements = parsed
3326        .program
3327        .body
3328        .iter()
3329        .filter(|s| erased.contains(&span_key(s.span())))
3330        .map(|s| {
3331            let span = s.span();
3332            let (line, column) = line_and_utf16_column(source, span.start as usize);
3333            CandidatePoint {
3334                id: stable_id(source, file, "statement", span, ""),
3335                kind: "statement".into(),
3336                file: file.into(),
3337                line,
3338                column,
3339                source: source_slice(source, span).into(),
3340                label: Some("typescript-import-erasure".into()),
3341            }
3342        })
3343        .collect();
3344    let point_analysis = collect_points(
3345        &allocator,
3346        &mut parsed.program,
3347        source,
3348        file,
3349        &safety.source_sensitive_functions,
3350        &erased,
3351    );
3352    let mut collector = DecisionCollector {
3353        source,
3354        file,
3355        decisions: Vec::new(),
3356        decision_vector_counts: Vec::new(),
3357        decision_logical_nodes: HashSet::new(),
3358        source_sensitive_functions: &safety.source_sensitive_functions,
3359    };
3360    collector.visit_program(&parsed.program);
3361    let optional_analysis = collect_optional_member_branches(
3362        &allocator,
3363        &mut parsed.program,
3364        source,
3365        file,
3366        &safety.source_sensitive_functions,
3367    );
3368    let call_analysis = collect_optional_call_branches(
3369        &allocator,
3370        &mut parsed.program,
3371        source,
3372        file,
3373        &safety.source_sensitive_functions,
3374    );
3375    let assignment_analysis = collect_logical_assignment_branches(
3376        &allocator,
3377        &mut parsed.program,
3378        source,
3379        file,
3380        &safety.source_sensitive_functions,
3381    );
3382    let default_analysis = collect_default_branches(
3383        &allocator,
3384        &mut parsed.program,
3385        source,
3386        file,
3387        &safety.source_sensitive_functions,
3388    );
3389    let extended_analysis = collect_extended_branches(
3390        &allocator,
3391        &mut parsed.program,
3392        source,
3393        file,
3394        &safety.source_sensitive_functions,
3395    );
3396    let logical_analysis = collect_logical_value_branches(
3397        &allocator,
3398        &mut parsed.program,
3399        source,
3400        file,
3401        &collector.decision_logical_nodes,
3402        &safety.source_sensitive_functions,
3403    );
3404    let switch_analysis = collect_switch_branches(
3405        &allocator,
3406        &mut parsed.program,
3407        source,
3408        file,
3409        &safety.source_sensitive_functions,
3410    );
3411    let mut branches = optional_analysis.branches;
3412    branches.extend(call_analysis.branches);
3413    branches.extend(assignment_analysis.branches);
3414    branches.extend(default_analysis.branches);
3415    branches.extend(extended_analysis.branches);
3416    branches.extend(logical_analysis.branches);
3417    branches.extend(switch_analysis.branches);
3418    AssertedTargetTransformer {
3419        ast: AstBuilder::new(&allocator),
3420    }
3421    .visit_program(&mut parsed.program);
3422    let (generated, map) = generate_candidate(&parsed.program, file)?;
3423    Ok(CandidateOutput {
3424        engine: "rust-oxc".to_string(),
3425        complete: false,
3426        supported_surface: "control-decision-manifest-v1".to_string(),
3427        code: generated,
3428        map,
3429        decisions: collector.decisions,
3430        points: point_analysis.points,
3431        excluded_statements,
3432        branches,
3433        runtime: None,
3434        coverage_limitations: {
3435            let mut limitations = safety.semantic_limitations;
3436            limitations.extend(call_analysis.limitations);
3437            limitations.extend(default_analysis.limitations);
3438            limitations.extend(safety.dynamic_limitations);
3439            limitations
3440        },
3441        limitations: vec![
3442            "candidate emits metadata only; use the private differential transform for probes"
3443                .to_string(),
3444            "coverage points, value branches, and extended branch obligations are not included"
3445                .to_string(),
3446        ],
3447    })
3448}
3449
3450fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
3451    match value {
3452        serde_json::Value::Null => ast.expression_null_literal(Span::default()),
3453        serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
3454        serde_json::Value::Number(value) => ast.expression_numeric_literal(
3455            Span::default(),
3456            value
3457                .as_f64()
3458                .expect("coverage registration numbers must fit JavaScript"),
3459            None,
3460            NumberBase::Decimal,
3461        ),
3462        serde_json::Value::String(value) => {
3463            ast.expression_string_literal(Span::default(), ast.str(value), None)
3464        }
3465        serde_json::Value::Array(values) => ast.expression_array(
3466            Span::default(),
3467            ast.vec_from_iter(
3468                values
3469                    .iter()
3470                    .map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
3471            ),
3472        ),
3473        serde_json::Value::Object(properties) => ast.expression_object(
3474            Span::default(),
3475            ast.vec_from_iter(properties.iter().map(|(key, value)| {
3476                ast.object_property_kind_object_property(
3477                    Span::default(),
3478                    PropertyKind::Init,
3479                    ast.property_key_static_identifier(Span::default(), ast.ident(key)),
3480                    json_expression(ast, value),
3481                    false,
3482                    false,
3483                    false,
3484                )
3485            })),
3486        ),
3487    }
3488}
3489
3490/// Instrument JavaScript with the complete frozen v1 denominator and probe-v2
3491/// runtime ABI. The source-transform contract is complete and independently
3492/// conformance-tested; selection of the Rust engine remains private until the
3493/// Phase 4 CLI/orchestration cutover is complete.
3494pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3495    instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None, false)
3496}
3497
3498/// Emit code for an isolated source-executing workspace. Unlike the module-
3499/// import form, this has no virtual module dependency: the generated
3500/// Node preload installs the frozen runtime on this global before user modules
3501/// evaluate.
3502pub fn instrument_direct_candidate(
3503    source: &str,
3504    file: &str,
3505) -> Result<CandidateOutput, CandidateError> {
3506    instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None, false)
3507}
3508
3509pub fn instrument_with_import_policy(
3510    source: &str,
3511    file: &str,
3512    capability_wrapper: &str,
3513    elide_type_imports: bool,
3514) -> Result<CandidateOutput, CandidateError> {
3515    instrument_candidate_with_binding(
3516        source,
3517        file,
3518        RuntimeBinding::DirectGlobal,
3519        Some(capability_wrapper),
3520        elide_type_imports,
3521    )
3522}
3523
3524fn instrument_candidate_with_binding(
3525    source: &str,
3526    file: &str,
3527    runtime_binding: RuntimeBinding,
3528    capability_wrapper: Option<&str>,
3529    elide_type_imports: bool,
3530) -> Result<CandidateOutput, CandidateError> {
3531    let _positions = PositionScope::enter();
3532    let source_type =
3533        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3534    let allocator = Allocator::default();
3535    let mut parsed = Parser::new(&allocator, source, source_type).parse();
3536    if !parsed.errors.is_empty() {
3537        return Err(CandidateError::Parse(
3538            parsed
3539                .errors
3540                .iter()
3541                .map(|error| {
3542                    parse_message(
3543                        source,
3544                        &error.message,
3545                        error
3546                            .labels
3547                            .as_ref()
3548                            .and_then(|labels| labels.first())
3549                            .map(|label| label.offset()),
3550                    )
3551                })
3552                .collect(),
3553        ));
3554    }
3555
3556    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3557    let erased = erased_imports(&parsed.program, elide_type_imports);
3558    let excluded_statements = parsed
3559        .program
3560        .body
3561        .iter()
3562        .filter(|s| erased.contains(&span_key(s.span())))
3563        .map(|s| {
3564            let span = s.span();
3565            let (line, column) = line_and_utf16_column(source, span.start as usize);
3566            CandidatePoint {
3567                id: stable_id(source, file, "statement", span, ""),
3568                kind: "statement".into(),
3569                file: file.into(),
3570                line,
3571                column,
3572                source: source_slice(source, span).into(),
3573                label: Some("typescript-import-erasure".into()),
3574            }
3575        })
3576        .collect();
3577    let point_analysis = collect_points(
3578        &allocator,
3579        &mut parsed.program,
3580        source,
3581        file,
3582        &safety.source_sensitive_functions,
3583        &erased,
3584    );
3585    let mut collector = DecisionCollector {
3586        source,
3587        file,
3588        decisions: Vec::new(),
3589        decision_vector_counts: Vec::new(),
3590        decision_logical_nodes: HashSet::new(),
3591        source_sensitive_functions: &safety.source_sensitive_functions,
3592    };
3593    collector.visit_program(&parsed.program);
3594    let optional_analysis = collect_optional_member_branches(
3595        &allocator,
3596        &mut parsed.program,
3597        source,
3598        file,
3599        &safety.source_sensitive_functions,
3600    );
3601    let call_analysis = collect_optional_call_branches(
3602        &allocator,
3603        &mut parsed.program,
3604        source,
3605        file,
3606        &safety.source_sensitive_functions,
3607    );
3608    let assignment_analysis = collect_logical_assignment_branches(
3609        &allocator,
3610        &mut parsed.program,
3611        source,
3612        file,
3613        &safety.source_sensitive_functions,
3614    );
3615    let default_analysis = collect_default_branches(
3616        &allocator,
3617        &mut parsed.program,
3618        source,
3619        file,
3620        &safety.source_sensitive_functions,
3621    );
3622    let extended_analysis = collect_extended_branches(
3623        &allocator,
3624        &mut parsed.program,
3625        source,
3626        file,
3627        &safety.source_sensitive_functions,
3628    );
3629    let logical_analysis = collect_logical_value_branches(
3630        &allocator,
3631        &mut parsed.program,
3632        source,
3633        file,
3634        &collector.decision_logical_nodes,
3635        &safety.source_sensitive_functions,
3636    );
3637    let switch_analysis = collect_switch_branches(
3638        &allocator,
3639        &mut parsed.program,
3640        source,
3641        file,
3642        &safety.source_sensitive_functions,
3643    );
3644    let mut branches = optional_analysis.branches;
3645    branches.extend(call_analysis.branches.clone());
3646    branches.extend(assignment_analysis.branches);
3647    branches.extend(default_analysis.branches.clone());
3648    branches.extend(extended_analysis.branches.clone());
3649    branches.extend(logical_analysis.branches);
3650    branches.extend(switch_analysis.branches.clone());
3651
3652    // Direct-runtime output can be served as several classic scripts in one
3653    // document, and every classic script shares the page's global lexical
3654    // scope: two files declaring `const __supercovMcdcBegin` is a SyntaxError
3655    // on the second, and a shared probe-file binding would credit one file's
3656    // statements to another. Names are made unique per file, and declared with
3657    // `var` so a script included twice is still legal, as it was before.
3658    let (namespace, runtime_declaration_kind) = if runtime_binding == RuntimeBinding::DirectGlobal {
3659        (
3660            format!(
3661                "_{}",
3662                &stable_id("", file, "runtime", Span::default(), "")[..8]
3663            ),
3664            VariableDeclarationKind::Var,
3665        )
3666    } else {
3667        (String::new(), VariableDeclarationKind::Const)
3668    };
3669    let mut names = CandidateNames::within(source, &namespace);
3670    let mcdc_begin = names.allocate("__supercovMcdcBegin");
3671    let mcdc_condition = names.allocate("__supercovMcdcCondition");
3672    let mcdc_end = names.allocate("__supercovMcdcEnd");
3673    let coverage_hit = names.allocate("__supercovCoverageHit");
3674    let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
3675    let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
3676    let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
3677    let probe_file_v2 = names.allocate("__supercovProbeFileV2");
3678    let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
3679    let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
3680    let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
3681    let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
3682    let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
3683    let with_request_phase = names.allocate("__supercovWithRequestPhase");
3684    let optional_select_v2 = names.allocate("__supercovOptionalSelectV2");
3685    let select_short_v2 = names.allocate("__supercovSelectShortV2");
3686    let select_right_v2 = names.allocate("__supercovSelectRightV2");
3687    let select_named_right_v2 = names.allocate("__supercovSelectNamedRightV2");
3688    let select_assign_end_v2 = names.allocate("__supercovSelectAssignEndV2");
3689    let select_path_v2 = names.allocate("__supercovSelectPathV2");
3690    let select_end2_v2 = names.allocate("__supercovSelectEnd2V2");
3691    let select_end_v2 = names.allocate("__supercovSelectEndV2");
3692    let select_next_v2 = names.allocate("__supercovSelectNextV2");
3693    let rendered_value_v2 = names.allocate("__supercovRenderedValueV2");
3694    let optional_call_end_v2 = names.allocate("__supercovOptionalCallEndV2");
3695    let default_selected_v2 = names.allocate("__supercovDefaultSelectedV2");
3696    let default_entered_v2 = names.allocate("__supercovDefaultEnteredV2");
3697    let typescript = parsed.program.source_type.is_typescript();
3698    let ast = AstBuilder::new(&allocator);
3699    let mut assignment_name_safety = AssignmentNameSafetyTransformer {
3700        ast,
3701        parenthesized_assignment_value: parenthesized_assignment_value.clone(),
3702    };
3703    assignment_name_safety.visit_program(&mut parsed.program);
3704    let point_indices = point_analysis
3705        .points
3706        .iter()
3707        .enumerate()
3708        .map(|(index, point)| (point.id.clone(), index))
3709        .collect::<HashMap<_, _>>();
3710    // A default's two outcomes are V2 points after the statement points, and
3711    // its pending count a slot of the file's `pendingDefaults`, both in source
3712    // order. lru-cache's `set` and `get` destructure options with defaults, and
3713    // the string-keyed count behind each was a third of a hot loop.
3714    let mut default_sites = default_analysis
3715        .parameter_targets
3716        .iter()
3717        .chain(&default_analysis.binding_targets)
3718        .map(|(span, target)| (*span, target.default_id.clone(), target.provided_id.clone()))
3719        .collect::<Vec<_>>();
3720    default_sites.sort();
3721    let mut default_point_ids = Vec::with_capacity(default_sites.len() * 2);
3722    let default_slots = default_sites
3723        .into_iter()
3724        .enumerate()
3725        .map(|(slot, (_, default_id, provided_id))| {
3726            let default_point = point_analysis.points.len() + default_point_ids.len();
3727            default_point_ids.push(default_id.clone());
3728            default_point_ids.push(provided_id);
3729            (
3730                default_id,
3731                DefaultSlot {
3732                    slot,
3733                    default_point,
3734                    provided_point: default_point + 1,
3735                },
3736            )
3737        })
3738        .collect::<HashMap<_, _>>();
3739    let default_count = default_slots.len();
3740    let mut extra = ExtraPoints {
3741        base: point_analysis.points.len() + default_point_ids.len(),
3742        ids: Vec::new(),
3743    };
3744    let selection_first = extra.next();
3745    let selection_targets = extra.allocate(&logical_analysis.logical_targets, |(short, right)| {
3746        vec![short.clone(), short.clone(), right.clone(), right.clone()]
3747    });
3748    // A logical assignment's outcomes are a selection's four points too, next
3749    // to the others, so the runtime records its per-operand outcomes the same.
3750    let assignment_targets = extra.allocate(&assignment_analysis.targets, |(short, right)| {
3751        vec![short.clone(), short.clone(), right.clone(), right.clone()]
3752    });
3753    let selection_count = selection_targets.len() + assignment_targets.len();
3754    let optional_member_targets = extra
3755        .allocate(&optional_analysis.targets, |(short, continued)| {
3756            vec![short.clone(), continued.clone()]
3757        });
3758    let optional_call_sites = extra.allocate(&call_analysis.sites, |(short, continued)| {
3759        vec![short.clone(), continued.clone()]
3760    });
3761    let loop_targets = extra.allocate(&extended_analysis.loop_targets, |target| {
3762        vec![target.first_id.clone(), target.second_id.clone()]
3763    });
3764    let try_targets = extra.allocate(&extended_analysis.try_targets, |target| {
3765        vec![target.first_id.clone(), target.second_id.clone()]
3766    });
3767    let statement_targets = point_analysis
3768        .statement_targets
3769        .into_iter()
3770        .map(|(span, ids)| {
3771            (
3772                span,
3773                ids.into_iter()
3774                    .map(|id| PointTarget {
3775                        index: *point_indices
3776                            .get(&id)
3777                            .expect("statement point must have a global index"),
3778                    })
3779                    .collect(),
3780            )
3781        })
3782        .collect();
3783    let function_targets = point_analysis
3784        .function_targets
3785        .into_iter()
3786        .map(|(span, id)| {
3787            (
3788                span,
3789                PointTarget {
3790                    index: *point_indices
3791                        .get(&id)
3792                        .expect("function point must have a global index"),
3793                },
3794            )
3795        })
3796        .collect();
3797    let rendered_targets = point_analysis
3798        .rendered_targets
3799        .into_iter()
3800        .map(|(span, id)| {
3801            (
3802                span,
3803                PointTarget {
3804                    index: *point_indices
3805                        .get(&id)
3806                        .expect("rendered point must have a global index"),
3807                },
3808            )
3809        })
3810        .collect();
3811    let mut statement_transformer = StatementProbeTransformer {
3812        ast,
3813        coverage_hit_v2: coverage_hit_v2.clone(),
3814        probe_file_v2: probe_file_v2.clone(),
3815        targets: statement_targets,
3816        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3817    };
3818    statement_transformer.visit_program(&mut parsed.program);
3819    let mut function_transformer = FunctionProbeTransformer {
3820        ast,
3821        coverage_hit_v2: coverage_hit_v2.clone(),
3822        probe_file_v2: probe_file_v2.clone(),
3823        targets: function_targets,
3824        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3825    };
3826    function_transformer.visit_program(&mut parsed.program);
3827    let mut rendered_transformer = RenderedExpressionTransformer {
3828        ast,
3829        rendered_value_v2: rendered_value_v2.clone(),
3830        probe_file_v2: probe_file_v2.clone(),
3831        targets: rendered_targets,
3832        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3833    };
3834    rendered_transformer.visit_program(&mut parsed.program);
3835    // Made first so a member right after an optional call can read the
3836    // call's frame; it rewrites the program after the members.
3837    let mut call_transformer = OptionalCallTransformer::new(
3838        ast,
3839        source,
3840        optional_call_end_v2.clone(),
3841        probe_file_v2.clone(),
3842        typescript,
3843        optional_call_sites,
3844        call_analysis.roots,
3845        safety.source_sensitive_functions.clone(),
3846    );
3847    let call_frames = optional_analysis
3848        .after_call
3849        .iter()
3850        .filter_map(|(member, call)| {
3851            call_transformer
3852                .sites
3853                .get(call)
3854                .map(|site| (*member, site.frame.clone()))
3855        })
3856        .collect();
3857    let mut optional_transformer = OptionalMemberTransformer {
3858        ast,
3859        optional_select_v2: optional_select_v2.clone(),
3860        probe_file_v2: probe_file_v2.clone(),
3861        targets: optional_member_targets,
3862        links: optional_analysis.links,
3863        call_frames,
3864        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3865    };
3866    optional_transformer.visit_program(&mut parsed.program);
3867    call_transformer.visit_program(&mut parsed.program);
3868    let mut default_transformer = DefaultTransformer {
3869        ast,
3870        default_selected_v2: default_selected_v2.clone(),
3871        default_entered_v2: default_entered_v2.clone(),
3872        probe_file_v2: probe_file_v2.clone(),
3873        slots: default_slots,
3874        parameter_targets: default_analysis.parameter_targets,
3875        binding_targets: default_analysis.binding_targets,
3876        function_entries: Vec::new(),
3877        declaration_entries: HashMap::new(),
3878        active_declaration: Vec::new(),
3879        parameter_pattern_depth: 0,
3880        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3881        typescript,
3882    };
3883    default_transformer.visit_program(&mut parsed.program);
3884    let mut extended_transformer = ExtendedTransformer {
3885        ast,
3886        coverage_hit_v2: coverage_hit_v2.clone(),
3887        probe_file_v2: probe_file_v2.clone(),
3888        typescript,
3889        try_targets,
3890        loop_targets,
3891        names: CandidateNames::within(source, &namespace),
3892        scope_declarations: Vec::new(),
3893        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3894    };
3895    extended_transformer.visit_program(&mut parsed.program);
3896    let mut transformer = ControlProbeV2Transformer {
3897        ast,
3898        decisions: &collector.decisions,
3899        mcdc_begin: mcdc_begin.clone(),
3900        mcdc_condition: mcdc_condition.clone(),
3901        mcdc_end: mcdc_end.clone(),
3902        mcdc_end_v2: mcdc_end_v2.clone(),
3903        probe_file_v2: probe_file_v2.clone(),
3904        names,
3905        scope_declarations: Vec::new(),
3906        decision_index: 0,
3907        parameter_depth: 0,
3908        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3909        after_statement: None,
3910        label_body: false,
3911        inline_names: None,
3912    };
3913    transformer.visit_program(&mut parsed.program);
3914    let mut logical_transformer = LogicalValueTransformer {
3915        ast,
3916        select_short_v2: select_short_v2.clone(),
3917        select_right_v2: select_right_v2.clone(),
3918        select_named_right_v2: select_named_right_v2.clone(),
3919        select_assign_end_v2: select_assign_end_v2.clone(),
3920        select_path_v2: select_path_v2.clone(),
3921        select_end2_v2: select_end2_v2.clone(),
3922        select_end_v2: select_end_v2.clone(),
3923        select_next_v2: select_next_v2.clone(),
3924        coverage_hit_v2: coverage_hit_v2.clone(),
3925        probe_file_v2: probe_file_v2.clone(),
3926        typescript,
3927        names: CandidateNames::within(source, &namespace),
3928        scope_declarations: Vec::new(),
3929        frameless: 0,
3930        trees: Vec::new(),
3931        aliasing: false,
3932        after_declaration: Vec::new(),
3933        logical_targets: selection_targets,
3934        assignment_targets,
3935        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3936    };
3937    logical_transformer.visit_program(&mut parsed.program);
3938    let mut switch_transformer = SwitchTransformer {
3939        ast,
3940        coverage_hit: coverage_hit.clone(),
3941        targets: switch_analysis.targets,
3942        names: CandidateNames::within(source, &namespace),
3943        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3944    };
3945    switch_transformer.visit_program(&mut parsed.program);
3946    let mut route_transformer = RouteRequestPhaseTransformer {
3947        ast,
3948        file,
3949        with_request_phase: with_request_phase.clone(),
3950        used: false,
3951        names: CandidateNames::within(source, &namespace),
3952    };
3953    route_transformer.transform_program(&mut parsed.program);
3954    let mut request_transformer = RequestPhaseTransformer {
3955        ast,
3956        with_request_phase: with_request_phase.clone(),
3957        used: route_transformer.used,
3958        source_sensitive_functions: safety.source_sensitive_functions.clone(),
3959    };
3960    request_transformer.visit_program(&mut parsed.program);
3961    let uses_request_phase = request_transformer.used;
3962
3963    let registration = serde_json::json!({
3964        "decisions": &collector.decisions,
3965        "pointIds": point_analysis.points.iter().map(|point| &point.id).chain(&default_point_ids).chain(&extra.ids).collect::<Vec<_>>(),
3966        "decisionVectorCounts": &collector.decision_vector_counts,
3967        "defaultCount": default_count,
3968        "selectionPoints": [selection_first, selection_count],
3969        "selectionTrees": &logical_transformer.trees,
3970    });
3971    let registration_call = ast.expression_call(
3972        Span::default(),
3973        ast.expression_identifier(Span::default(), ast.ident(&register_probe_v2)),
3974        NONE,
3975        ast.vec1(Argument::from(json_expression(ast, &registration))),
3976        false,
3977    );
3978    parsed.program.body.insert(
3979        0,
3980        Statement::VariableDeclaration(ast.alloc_variable_declaration(
3981            Span::default(),
3982            runtime_declaration_kind,
3983            ast.vec1(ast.variable_declarator(
3984                Span::default(),
3985                runtime_declaration_kind,
3986                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
3987                NONE,
3988                Some(registration_call),
3989                false,
3990            )),
3991            false,
3992        )),
3993    );
3994    let mut runtime_imports = vec![
3995        ("mcdcBegin", &mcdc_begin),
3996        ("mcdcCondition", &mcdc_condition),
3997        ("mcdcEnd", &mcdc_end),
3998        ("coverageHit", &coverage_hit),
3999        ("registerProbeV2", &register_probe_v2),
4000        ("mcdcEndV2", &mcdc_end_v2),
4001        ("coverageHitV2", &coverage_hit_v2),
4002        (
4003            "parenthesizedAssignmentValue",
4004            &parenthesized_assignment_value,
4005        ),
4006        ("optionalSelectV2", &optional_select_v2),
4007        ("selectShortV2", &select_short_v2),
4008        ("selectRightV2", &select_right_v2),
4009        ("selectNamedRightV2", &select_named_right_v2),
4010        ("selectAssignEndV2", &select_assign_end_v2),
4011        ("selectPathV2", &select_path_v2),
4012        ("selectEnd2V2", &select_end2_v2),
4013        ("selectEndV2", &select_end_v2),
4014        ("selectNextV2", &select_next_v2),
4015        ("renderedValueV2", &rendered_value_v2),
4016        ("optionalCallEndV2", &optional_call_end_v2),
4017        ("defaultSelectedV2", &default_selected_v2),
4018        ("defaultEnteredV2", &default_entered_v2),
4019    ];
4020    if uses_request_phase {
4021        runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
4022    }
4023    if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
4024        // Read off an untyped global, every helper is `any` to TypeScript,
4025        // whatever the runtime's declarations say, and so is each value that
4026        // passes through one. A directly run TypeScript file names each
4027        // helper's type where it binds it. Declaring the global's type for
4028        // the program instead collided with the declaration every rewritten
4029        // test file carries for its assertion helpers.
4030        let typed = typescript && runtime_binding == RuntimeBinding::DirectGlobal;
4031        let declarators =
4032            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
4033                let annotation = typed
4034                    .then(|| runtime_helper_type(&allocator, parsed.program.source_type, imported))
4035                    .flatten();
4036                let global_runtime =
4037                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
4038                        Span::default(),
4039                        ast.expression_identifier(Span::default(), ast.ident("globalThis")),
4040                        ast.identifier_name(
4041                            Span::default(),
4042                            ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
4043                                "__SUPERCOV_DIRECT_RUNTIME__"
4044                            } else {
4045                                "__supercovRuntime"
4046                            }),
4047                        ),
4048                        false,
4049                    ));
4050                let runtime_helper =
4051                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
4052                        Span::default(),
4053                        global_runtime,
4054                        ast.identifier_name(Span::default(), ast.ident(imported)),
4055                        false,
4056                    ));
4057                ast.variable_declarator(
4058                    Span::default(),
4059                    runtime_declaration_kind,
4060                    ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
4061                    annotation,
4062                    Some(runtime_helper),
4063                    false,
4064                )
4065            }));
4066        parsed.program.body.insert(
4067            0,
4068            Statement::VariableDeclaration(ast.alloc_variable_declaration(
4069                Span::default(),
4070                runtime_declaration_kind,
4071                declarators,
4072                false,
4073            )),
4074        );
4075    } else {
4076        let import_specifiers =
4077            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
4078                ast.import_declaration_specifier_import_specifier(
4079                    Span::default(),
4080                    ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
4081                    ast.binding_identifier(Span::default(), ast.ident(local)),
4082                    oxc_ast::ast::ImportOrExportKind::Value,
4083                )
4084            }));
4085        parsed.program.body.insert(
4086            0,
4087            Statement::ImportDeclaration(ast.alloc_import_declaration(
4088                Span::default(),
4089                Some(import_specifiers),
4090                ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
4091                None,
4092                NONE,
4093                oxc_ast::ast::ImportOrExportKind::Value,
4094            )),
4095        );
4096    }
4097
4098    if let Some(wrapper) = capability_wrapper {
4099        transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
4100    }
4101    let limitations = Vec::new();
4102    mark_generated_statements(ast, &mut parsed.program);
4103    AssertedTargetTransformer {
4104        ast: AstBuilder::new(&allocator),
4105    }
4106    .visit_program(&mut parsed.program);
4107    let (code, map) = generate_candidate(&parsed.program, file)?;
4108    Ok(CandidateOutput {
4109        engine: "rust-oxc".to_string(),
4110        complete: true,
4111        supported_surface: "complete-js-instrumenter-v1".to_string(),
4112        code,
4113        map,
4114        decisions: collector.decisions,
4115        points: point_analysis.points,
4116        excluded_statements,
4117        branches,
4118        runtime: Some(CandidateRuntime {
4119            coverage_hit,
4120            mcdc_begin,
4121            mcdc_condition,
4122            mcdc_end,
4123            register_probe_v2,
4124            mcdc_end_v2,
4125            coverage_hit_v2,
4126            probe_file_v2,
4127            parenthesized_assignment_value,
4128            with_request_phase,
4129            optional_select_v2,
4130            select_short_v2,
4131            select_right_v2,
4132            select_named_right_v2,
4133            select_assign_end_v2,
4134            select_path_v2,
4135            select_end2_v2,
4136            select_end_v2,
4137            select_next_v2,
4138            rendered_value_v2,
4139            optional_call_end_v2,
4140            default_selected_v2,
4141            default_entered_v2,
4142        }),
4143        coverage_limitations: {
4144            let mut limitations = safety.semantic_limitations;
4145            limitations.extend(call_analysis.limitations);
4146            limitations.extend(default_analysis.limitations);
4147            limitations.extend(safety.dynamic_limitations);
4148            limitations
4149        },
4150        limitations,
4151    })
4152}
4153
4154/// `file.clock.fast && file.hitEpochs[index] === file.clock.epoch || hit(file, index);`
4155///
4156/// A statement probe that already fired in this context costs three loads and
4157/// a comparison, not a call: in minimatch's backtracking matcher the call per
4158/// statement was most of a tenfold slowdown, and its guard test failed. The
4159/// clock is `fast` only while an async hook keeps its epoch current; without
4160/// one the call decides, as before.
4161fn probe_v2_hit<'a>(ast: AstBuilder<'a>, hit: &str, file: &str, index: usize) -> Statement<'a> {
4162    ast.statement_expression(
4163        Span::default(),
4164        probe_v2_hit_expression(ast, hit, file, index),
4165    )
4166}
4167
4168/// The expression inside `probe_v2_hit`, for probes that sit in an expression:
4169/// a selection's outcome, a loop's first iteration, a caught exception.
4170fn probe_v2_hit_expression<'a>(
4171    ast: AstBuilder<'a>,
4172    hit: &str,
4173    file: &str,
4174    index: usize,
4175) -> Expression<'a> {
4176    let span = Span::default();
4177    let file_expression = || ast.expression_identifier(span, ast.ident(file));
4178    let member = |object: Expression<'a>, name: &str| -> Expression<'a> {
4179        Expression::from(ast.member_expression_static(
4180            span,
4181            object,
4182            ast.identifier_name(span, ast.ident(name)),
4183            false,
4184        ))
4185    };
4186    let index_literal =
4187        || ast.expression_numeric_literal(span, index as f64, None, NumberBase::Decimal);
4188    let fast = member(member(file_expression(), "clock"), "fast");
4189    let seen = ast.expression_binary(
4190        span,
4191        Expression::from(ast.member_expression_computed(
4192            span,
4193            member(file_expression(), "hitEpochs"),
4194            index_literal(),
4195            false,
4196        )),
4197        BinaryOperator::StrictEquality,
4198        member(member(file_expression(), "clock"), "epoch"),
4199    );
4200    let call = ast.expression_call(
4201        span,
4202        ast.expression_identifier(span, ast.ident(hit)),
4203        NONE,
4204        ast.vec_from_array([
4205            Argument::from(file_expression()),
4206            Argument::from(index_literal()),
4207        ]),
4208        false,
4209    );
4210    ast.expression_logical(
4211        span,
4212        ast.expression_logical(span, fast, LogicalOperator::And, seen),
4213        LogicalOperator::Or,
4214        call,
4215    )
4216}
4217
4218/// `let` declarations for probe temporaries at the top of a scope. In
4219/// TypeScript a temporary that holds user values is declared `any`, as the
4220/// runtime calls it replaces returned: `tsc --strict` would otherwise infer
4221/// its type across assignments and fail code that compiled before.
4222fn probe_temporaries<'a>(
4223    ast: AstBuilder<'a>,
4224    names: Vec<String>,
4225    typescript: bool,
4226) -> Statement<'a> {
4227    let declarations = ast.vec_from_iter(names.into_iter().map(|name| {
4228        let annotation = typescript.then(|| {
4229            ast.alloc_ts_type_annotation(Span::default(), ast.ts_type_any_keyword(Span::default()))
4230        });
4231        ast.variable_declarator(
4232            Span::default(),
4233            VariableDeclarationKind::Let,
4234            ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&name)),
4235            annotation,
4236            None,
4237            false,
4238        )
4239    }));
4240    Statement::VariableDeclaration(ast.alloc_variable_declaration(
4241        Span::default(),
4242        VariableDeclarationKind::Let,
4243        declarations,
4244        false,
4245    ))
4246}
4247
4248/// `T = value`, as an expression.
4249fn assign_temporary<'a>(ast: AstBuilder<'a>, name: &str, value: Expression<'a>) -> Expression<'a> {
4250    ast.expression_assignment(
4251        Span::default(),
4252        AssignmentOperator::Assign,
4253        AssignmentTarget::from(ast.simple_assignment_target_assignment_target_identifier(
4254            Span::default(),
4255            ast.ident(name),
4256        )),
4257        value,
4258    )
4259}
4260
4261fn numeric<'a>(ast: AstBuilder<'a>, value: usize) -> Expression<'a> {
4262    ast.expression_numeric_literal(Span::default(), value as f64, None, NumberBase::Decimal)
4263}
4264
4265/// Where each V2 point beyond the statement points begins, per site, in
4266/// source order: every probe kind records through `coverageHitV2` with the
4267/// ids it always recorded.
4268#[derive(Default)]
4269struct ExtraPoints {
4270    base: usize,
4271    ids: Vec<String>,
4272}
4273
4274impl ExtraPoints {
4275    fn allocate<T>(
4276        &mut self,
4277        targets: &HashMap<SpanKey, T>,
4278        ids: impl Fn(&T) -> Vec<String>,
4279    ) -> HashMap<SpanKey, usize> {
4280        let mut keys = targets.keys().copied().collect::<Vec<_>>();
4281        keys.sort();
4282        keys.into_iter()
4283            .map(|key| {
4284                let first = self.base + self.ids.len();
4285                self.ids.extend(ids(&targets[&key]));
4286                (key, first)
4287            })
4288            .collect()
4289    }
4290
4291    fn next(&self) -> usize {
4292        self.base + self.ids.len()
4293    }
4294}
4295
4296struct StatementProbeTransformer<'a> {
4297    ast: AstBuilder<'a>,
4298    coverage_hit_v2: String,
4299    probe_file_v2: String,
4300    targets: HashMap<SpanKey, Vec<PointTarget>>,
4301    source_sensitive_functions: HashSet<SpanKey>,
4302}
4303
4304impl<'a> StatementProbeTransformer<'a> {
4305    fn probe(&self, target: &PointTarget) -> Statement<'a> {
4306        probe_v2_hit(
4307            self.ast,
4308            &self.coverage_hit_v2,
4309            &self.probe_file_v2,
4310            target.index,
4311        )
4312    }
4313
4314    fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
4315        let mut ids = self
4316            .targets
4317            .remove(&span_key(statement.span()))
4318            .unwrap_or_default();
4319        if let Statement::ExportNamedDeclaration(export) = statement
4320            && let Some(declaration) = &export.declaration
4321            && let Some(nested) = self.targets.remove(&span_key(declaration.span()))
4322        {
4323            ids.extend(nested);
4324        }
4325        if let Statement::ExportDefaultDeclaration(export) = statement
4326            && let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
4327            && let Some(nested) = self.targets.remove(&span_key(class.span))
4328        {
4329            ids.extend(nested);
4330        }
4331        ids
4332    }
4333
4334    fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
4335        let ids = self.take_statement_ids(statement);
4336        if ids.is_empty() {
4337            return;
4338        }
4339        // The block takes the statement's span, so its braces map to the
4340        // statement's own start and end: a coverage tool mapping the copy back
4341        // (nyc, Jest) dropped a statement whose end was a brace of nowhere.
4342        let span = statement.span();
4343        let original = statement.take_in(self.ast.allocator);
4344        let mut body = self.ast.vec_with_capacity(ids.len() + 1);
4345        body.extend(ids.iter().map(|target| self.probe(target)));
4346        body.push(original);
4347        *statement = self.ast.statement_block(span, body);
4348    }
4349}
4350
4351impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
4352    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4353        let original = statements.take_in(self.ast.allocator);
4354        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
4355        for mut statement in original {
4356            let ids = self.take_statement_ids(&statement);
4357            self.visit_statement(&mut statement);
4358            instrumented.extend(ids.iter().map(|target| self.probe(target)));
4359            instrumented.push(statement);
4360        }
4361        *statements = instrumented;
4362    }
4363
4364    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4365        if self
4366            .source_sensitive_functions
4367            .contains(&span_key(function.span))
4368        {
4369            return;
4370        }
4371        walk_mut::walk_function(self, function, flags);
4372    }
4373
4374    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4375        if self
4376            .source_sensitive_functions
4377            .contains(&span_key(function.span))
4378        {
4379            return;
4380        }
4381        walk_mut::walk_arrow_function_expression(self, function);
4382    }
4383
4384    // A with body is left uninstrumented: its object environment can
4385    // intercept probe identifiers (see `enter_with_statement`).
4386    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4387
4388    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
4389        self.wrap_bare(&mut statement.consequent);
4390        if let Some(alternate) = &mut statement.alternate {
4391            self.wrap_bare(alternate);
4392        }
4393        walk_mut::walk_if_statement(self, statement);
4394    }
4395
4396    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
4397        self.wrap_bare(&mut statement.body);
4398        walk_mut::walk_while_statement(self, statement);
4399    }
4400
4401    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
4402        self.wrap_bare(&mut statement.body);
4403        walk_mut::walk_do_while_statement(self, statement);
4404    }
4405
4406    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
4407        self.wrap_bare(&mut statement.body);
4408        walk_mut::walk_for_statement(self, statement);
4409    }
4410
4411    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
4412        self.wrap_bare(&mut statement.body);
4413        walk_mut::walk_for_in_statement(self, statement);
4414    }
4415
4416    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
4417        self.wrap_bare(&mut statement.body);
4418        walk_mut::walk_for_of_statement(self, statement);
4419    }
4420}
4421
4422/// Wraps a rendered JSX expression in `renderedValueV2(file, i, expression)` so
4423/// the expression records its own evaluation. There is nowhere to put a
4424/// statement inside JSX, and a sequence expression is not available either --
4425/// JSX forbids a bare comma operator, and a container prints its expression
4426/// unparenthesised. A call carries the value through instead. It records after
4427/// the expression evaluates rather than before, as `optionalSelect` already
4428/// does, so an expression that throws is reported unevaluated rather than
4429/// covered.
4430struct RenderedExpressionTransformer<'a> {
4431    ast: AstBuilder<'a>,
4432    rendered_value_v2: String,
4433    probe_file_v2: String,
4434    targets: HashMap<SpanKey, PointTarget>,
4435    source_sensitive_functions: HashSet<SpanKey>,
4436}
4437
4438impl<'a> RenderedExpressionTransformer<'a> {
4439    fn probe(&self, target: &PointTarget, value: Expression<'a>) -> Expression<'a> {
4440        self.ast.expression_call(
4441            Span::default(),
4442            self.ast
4443                .expression_identifier(Span::default(), self.ast.ident(&self.rendered_value_v2)),
4444            NONE,
4445            self.ast.vec_from_array([
4446                Argument::from(
4447                    self.ast.expression_identifier(
4448                        Span::default(),
4449                        self.ast.ident(&self.probe_file_v2),
4450                    ),
4451                ),
4452                Argument::from(self.ast.expression_numeric_literal(
4453                    Span::default(),
4454                    target.index as f64,
4455                    None,
4456                    NumberBase::Decimal,
4457                )),
4458                Argument::from(value),
4459            ]),
4460            false,
4461        )
4462    }
4463}
4464
4465impl<'a> VisitMut<'a> for RenderedExpressionTransformer<'a> {
4466    fn visit_jsx_expression_container(&mut self, container: &mut JSXExpressionContainer<'a>) {
4467        walk_mut::walk_jsx_expression_container(self, container);
4468        if matches!(container.expression, JSXExpression::EmptyExpression(_)) {
4469            return;
4470        }
4471        let span = container.expression.span();
4472        if self.source_sensitive_functions.contains(&span_key(span)) {
4473            return;
4474        }
4475        let Some(target) = self.targets.remove(&span_key(span)) else {
4476            return;
4477        };
4478        let inner = container
4479            .expression
4480            .take_in(self.ast.allocator)
4481            .into_expression();
4482        container.expression = JSXExpression::from(self.probe(&target, inner));
4483    }
4484}
4485
4486struct FunctionProbeTransformer<'a> {
4487    ast: AstBuilder<'a>,
4488    coverage_hit_v2: String,
4489    probe_file_v2: String,
4490    targets: HashMap<SpanKey, PointTarget>,
4491    source_sensitive_functions: HashSet<SpanKey>,
4492}
4493
4494impl<'a> FunctionProbeTransformer<'a> {
4495    fn probe(&self, target: &PointTarget) -> Statement<'a> {
4496        probe_v2_hit(
4497            self.ast,
4498            &self.coverage_hit_v2,
4499            &self.probe_file_v2,
4500            target.index,
4501        )
4502    }
4503}
4504
4505impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
4506    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4507        if self
4508            .source_sensitive_functions
4509            .contains(&span_key(function.span))
4510        {
4511            return;
4512        }
4513        if let Some(target) = self.targets.remove(&span_key(function.span))
4514            && let Some(body) = &mut function.body
4515        {
4516            body.statements.insert(0, self.probe(&target));
4517        }
4518        walk_mut::walk_function(self, function, flags);
4519    }
4520
4521    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4522        if self
4523            .source_sensitive_functions
4524            .contains(&span_key(function.span))
4525        {
4526            return;
4527        }
4528        if let Some(target) = self.targets.remove(&span_key(function.span)) {
4529            let probe = self.probe(&target);
4530            if function.expression {
4531                let original = function
4532                    .body
4533                    .statements
4534                    .pop()
4535                    .expect("expression arrow must contain its expression statement");
4536                let Statement::ExpressionStatement(expression) = original else {
4537                    panic!("expression arrow body must be represented as an expression statement");
4538                };
4539                function.expression = false;
4540                function.body.statements.push(probe);
4541                function.body.statements.push(
4542                    self.ast
4543                        .statement_return(Span::default(), Some(expression.unbox().expression)),
4544                );
4545            } else {
4546                function.body.statements.insert(0, probe);
4547            }
4548        }
4549        walk_mut::walk_arrow_function_expression(self, function);
4550    }
4551}
4552
4553struct OptionalMemberTransformer<'a> {
4554    ast: AstBuilder<'a>,
4555    optional_select_v2: String,
4556    probe_file_v2: String,
4557    /// The short and continued outcomes are two V2 points from this index.
4558    targets: HashMap<SpanKey, usize>,
4559    links: HashMap<SpanKey, u8>,
4560    /// For a target right after an optional call, that call's frame.
4561    call_frames: HashMap<SpanKey, String>,
4562    source_sensitive_functions: HashSet<SpanKey>,
4563}
4564
4565impl<'a> OptionalMemberTransformer<'a> {
4566    /// `optionalSelectV2(file, first, object, links, call)`: records whether
4567    /// the object of `object?.member` was nullish and passes it through. In
4568    /// `a?.b?.c` the object of `?.c` is undefined when `a` is, though `?.c`
4569    /// never ran; `links`, and after an optional call that call's frame, let
4570    /// the runtime tell the two apart.
4571    fn instrument_operand(
4572        &self,
4573        operand: Expression<'a>,
4574        first: usize,
4575        links: u8,
4576        call_frame: Option<&str>,
4577    ) -> Expression<'a> {
4578        let mut arguments = self.ast.vec_from_array([
4579            Argument::from(
4580                self.ast
4581                    .expression_identifier(Span::default(), self.ast.ident(&self.probe_file_v2)),
4582            ),
4583            Argument::from(numeric(self.ast, first)),
4584            Argument::from(operand),
4585        ]);
4586        if links != 0 {
4587            arguments.push(Argument::from(numeric(self.ast, usize::from(links))));
4588        }
4589        if let Some(frame) = call_frame {
4590            arguments.push(Argument::from(
4591                self.ast
4592                    .expression_identifier(Span::default(), self.ast.ident(frame)),
4593            ));
4594        }
4595        self.ast.expression_call(
4596            Span::default(),
4597            self.ast
4598                .expression_identifier(Span::default(), self.ast.ident(&self.optional_select_v2)),
4599            NONE,
4600            arguments,
4601            false,
4602        )
4603    }
4604
4605    fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
4606        let Some(first) = self.targets.remove(&span_key(span)) else {
4607            return;
4608        };
4609        let mut links = self.links.get(&span_key(span)).copied().unwrap_or(0);
4610        let call_frame = self.call_frames.get(&span_key(span)).cloned();
4611        if call_frame.is_none() {
4612            // A call that is not measured leaves nothing to read.
4613            links &= !LINK_AFTER_CALL;
4614        }
4615        // `a?.b` narrows `a`, and TypeScript reads that through a comma's last
4616        // operand but not through a call: `if (user?.name) return user`
4617        // returned `User | undefined`. A name or `this` is read again after
4618        // the call that records it. Anything else may run code when read, a
4619        // getter or a proxy, and is read once, through the call.
4620        let again = match &*object {
4621            Expression::Identifier(identifier) => Some(
4622                self.ast
4623                    .expression_identifier(identifier.span, identifier.name),
4624            ),
4625            Expression::ThisExpression(this) => Some(self.ast.expression_this(this.span)),
4626            _ => None,
4627        };
4628        let mut operand = object.take_in(self.ast.allocator);
4629        if again.is_some() {
4630            *operand.span_mut() = Span::default();
4631        }
4632        let recorded = self.instrument_operand(operand, first, links, call_frame.as_deref());
4633        *object = match again {
4634            Some(again) => self
4635                .ast
4636                .expression_sequence(Span::default(), self.ast.vec_from_array([recorded, again])),
4637            None => recorded,
4638        };
4639    }
4640}
4641
4642impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
4643    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4644        if self
4645            .source_sensitive_functions
4646            .contains(&span_key(function.span))
4647        {
4648            return;
4649        }
4650        walk_mut::walk_function(self, function, flags);
4651    }
4652
4653    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4654        if self
4655            .source_sensitive_functions
4656            .contains(&span_key(function.span))
4657        {
4658            return;
4659        }
4660        walk_mut::walk_arrow_function_expression(self, function);
4661    }
4662
4663    // A with body is left uninstrumented: its object environment can
4664    // intercept probe identifiers (see `enter_with_statement`).
4665    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4666
4667    fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
4668        self.instrument_target(member.span, &mut member.object);
4669        walk_mut::walk_computed_member_expression(self, member);
4670    }
4671
4672    fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
4673        self.instrument_target(member.span, &mut member.object);
4674        walk_mut::walk_static_member_expression(self, member);
4675    }
4676
4677    fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
4678        self.instrument_target(member.span, &mut member.object);
4679        walk_mut::walk_private_field_expression(self, member);
4680    }
4681}
4682
4683/// A call site's state lives in a temporary of its own: 0 before the chain,
4684/// 1 once the callee is evaluated, 2 once the call goes ahead. Its short and
4685/// continued outcomes are two V2 points from `first`.
4686#[derive(Clone)]
4687struct OptionalCallSiteRuntime {
4688    frame: String,
4689    first: usize,
4690}
4691
4692struct OptionalCallTransformer<'a, 's> {
4693    ast: AstBuilder<'a>,
4694    optional_call_end_v2: String,
4695    probe_file_v2: String,
4696    typescript: bool,
4697    scope_declarations: Vec<Vec<String>>,
4698    sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
4699    roots: HashMap<SpanKey, Vec<SpanKey>>,
4700    source_sensitive_functions: HashSet<SpanKey>,
4701    _source: std::marker::PhantomData<&'s str>,
4702}
4703
4704impl<'a, 's> OptionalCallTransformer<'a, 's> {
4705    #[allow(clippy::too_many_arguments)]
4706    fn new(
4707        ast: AstBuilder<'a>,
4708        source: &'s str,
4709        optional_call_end_v2: String,
4710        probe_file_v2: String,
4711        typescript: bool,
4712        sites: HashMap<SpanKey, usize>,
4713        roots: HashMap<SpanKey, Vec<SpanKey>>,
4714        source_sensitive_functions: HashSet<SpanKey>,
4715    ) -> Self {
4716        let mut names = CandidateNames::new(source);
4717        let mut ordered = sites.into_iter().collect::<Vec<_>>();
4718        ordered.sort();
4719        let sites = ordered
4720            .into_iter()
4721            .map(|(key, first)| {
4722                (
4723                    key,
4724                    OptionalCallSiteRuntime {
4725                        frame: names.allocate("_optionalCall"),
4726                        first,
4727                    },
4728                )
4729            })
4730            .collect();
4731        Self {
4732            ast,
4733            optional_call_end_v2,
4734            probe_file_v2,
4735            typescript,
4736            scope_declarations: Vec::new(),
4737            sites,
4738            roots,
4739            source_sensitive_functions,
4740            _source: std::marker::PhantomData,
4741        }
4742    }
4743
4744    fn identifier(&self, name: &str) -> Expression<'a> {
4745        self.ast
4746            .expression_identifier(Span::default(), self.ast.ident(name))
4747    }
4748
4749    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
4750        self.ast.expression_call(
4751            Span::default(),
4752            self.identifier(name),
4753            NONE,
4754            arguments,
4755            false,
4756        )
4757    }
4758
4759    /// `(T = 1, value)`: the callee's position was evaluated.
4760    fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
4761        self.ast.expression_sequence(
4762            Span::default(),
4763            self.ast.vec_from_array([
4764                assign_temporary(self.ast, frame, numeric(self.ast, 1)),
4765                value,
4766            ]),
4767        )
4768    }
4769
4770    fn enter_scope(&mut self) {
4771        self.scope_declarations.push(Vec::new());
4772    }
4773
4774    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4775        let names = self
4776            .scope_declarations
4777            .pop()
4778            .expect("optional-call scope stack must remain balanced");
4779        if names.is_empty() {
4780            return;
4781        }
4782        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
4783    }
4784
4785    fn instrument_callee(
4786        &self,
4787        callee: Expression<'a>,
4788        site: &OptionalCallSiteRuntime,
4789    ) -> Expression<'a> {
4790        match callee {
4791            Expression::ComputedMemberExpression(mut member) => {
4792                let property = member.expression.take_in(self.ast.allocator);
4793                member.expression = self.reached(&site.frame, property);
4794                Expression::ComputedMemberExpression(member)
4795            }
4796            Expression::StaticMemberExpression(member) => {
4797                let member = member.unbox();
4798                let property = self.ast.expression_string_literal(
4799                    Span::default(),
4800                    self.ast.str(member.property.name.as_str()),
4801                    None,
4802                );
4803                Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
4804                    member.span,
4805                    member.object,
4806                    self.reached(&site.frame, property),
4807                    member.optional,
4808                ))
4809            }
4810            Expression::PrivateFieldExpression(mut member) => {
4811                let object = member.object.take_in(self.ast.allocator);
4812                member.object = self.reached(&site.frame, object);
4813                Expression::PrivateFieldExpression(member)
4814            }
4815            Expression::ChainExpression(mut chain) => {
4816                match &mut chain.expression {
4817                    ChainElement::ComputedMemberExpression(member) => {
4818                        let property = member.expression.take_in(self.ast.allocator);
4819                        member.expression = self.reached(&site.frame, property);
4820                    }
4821                    ChainElement::StaticMemberExpression(member) => {
4822                        let member = member.take_in(self.ast.allocator);
4823                        let property = self.ast.expression_string_literal(
4824                            Span::default(),
4825                            self.ast.str(member.property.name.as_str()),
4826                            None,
4827                        );
4828                        chain.expression = ChainElement::ComputedMemberExpression(
4829                            self.ast.alloc_computed_member_expression(
4830                                member.span,
4831                                member.object,
4832                                self.reached(&site.frame, property),
4833                                member.optional,
4834                            ),
4835                        );
4836                    }
4837                    ChainElement::PrivateFieldExpression(member) => {
4838                        let object = member.object.take_in(self.ast.allocator);
4839                        member.object = self.reached(&site.frame, object);
4840                    }
4841                    ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
4842                        return self.reached(&site.frame, Expression::ChainExpression(chain));
4843                    }
4844                }
4845                Expression::ChainExpression(chain)
4846            }
4847            Expression::ParenthesizedExpression(mut parenthesized) => {
4848                let inner = parenthesized.expression.take_in(self.ast.allocator);
4849                parenthesized.expression = self.instrument_callee(inner, site);
4850                Expression::ParenthesizedExpression(parenthesized)
4851            }
4852            Expression::TSAsExpression(mut wrapped) => {
4853                let inner = wrapped.expression.take_in(self.ast.allocator);
4854                wrapped.expression = self.instrument_callee(inner, site);
4855                Expression::TSAsExpression(wrapped)
4856            }
4857            Expression::TSSatisfiesExpression(mut wrapped) => {
4858                let inner = wrapped.expression.take_in(self.ast.allocator);
4859                wrapped.expression = self.instrument_callee(inner, site);
4860                Expression::TSSatisfiesExpression(wrapped)
4861            }
4862            Expression::TSTypeAssertion(mut wrapped) => {
4863                let inner = wrapped.expression.take_in(self.ast.allocator);
4864                wrapped.expression = self.instrument_callee(inner, site);
4865                Expression::TSTypeAssertion(wrapped)
4866            }
4867            Expression::TSNonNullExpression(mut wrapped) => {
4868                let inner = wrapped.expression.take_in(self.ast.allocator);
4869                wrapped.expression = self.instrument_callee(inner, site);
4870                Expression::TSNonNullExpression(wrapped)
4871            }
4872            // `a?.b()?.()`: the callee is what an earlier call in the same
4873            // chain returned. It is reached only when that call goes ahead; a
4874            // short-circuit before it never asked whether its result is
4875            // nullish.
4876            Expression::CallExpression(mut call) => {
4877                call.arguments.insert(0, self.marker_spread(&site.frame, 1));
4878                Expression::CallExpression(call)
4879            }
4880            other => self.reached(&site.frame, other),
4881        }
4882    }
4883
4884    fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
4885        let callee = call.callee.take_in(self.ast.allocator);
4886        call.callee = self.instrument_callee(callee, site);
4887        call.arguments.insert(0, self.marker_spread(&site.frame, 2));
4888    }
4889
4890    /// `...(T = value, file.none)`: runs only when the call goes ahead, before
4891    /// its own arguments, and spreads nothing.
4892    fn marker_spread(&self, frame: &str, value: usize) -> Argument<'a> {
4893        let none = Expression::from(
4894            self.ast.member_expression_static(
4895                Span::default(),
4896                self.identifier(&self.probe_file_v2),
4897                self.ast
4898                    .identifier_name(Span::default(), self.ast.ident("none")),
4899                false,
4900            ),
4901        );
4902        let marked = self.ast.expression_sequence(
4903            Span::default(),
4904            self.ast.vec_from_array([
4905                assign_temporary(self.ast, frame, numeric(self.ast, value)),
4906                none,
4907            ]),
4908        );
4909        Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), marked))
4910    }
4911
4912    fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
4913        let sites = site_keys
4914            .iter()
4915            .map(|key| {
4916                self.sites
4917                    .get(key)
4918                    .expect("optional-call root must reference a known site")
4919                    .clone()
4920            })
4921            .collect::<Vec<_>>();
4922        self.scope_declarations
4923            .last_mut()
4924            .expect("optional-call root must be inside a program or function")
4925            .extend(sites.iter().map(|site| site.frame.clone()));
4926
4927        // `optionalCallEndV2(file, first, chain, T)` reads T after the chain,
4928        // so it sees what that evaluation did.
4929        let original = expression.take_in(self.ast.allocator);
4930        let mut measured = original;
4931        for site in sites.iter().rev() {
4932            measured = self.call(
4933                &self.optional_call_end_v2,
4934                self.ast.vec_from_array([
4935                    Argument::from(self.identifier(&self.probe_file_v2)),
4936                    Argument::from(numeric(self.ast, site.first)),
4937                    Argument::from(measured),
4938                    Argument::from(self.identifier(&site.frame)),
4939                ]),
4940            );
4941        }
4942        let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
4943        for site in &sites {
4944            sequence.push(assign_temporary(
4945                self.ast,
4946                &site.frame,
4947                numeric(self.ast, 0),
4948            ));
4949        }
4950        sequence.push(measured);
4951        *expression = self.ast.expression_sequence(Span::default(), sequence);
4952    }
4953}
4954
4955impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
4956    fn visit_program(&mut self, program: &mut Program<'a>) {
4957        self.enter_scope();
4958        walk_mut::walk_program(self, program);
4959        self.leave_scope(&mut program.body);
4960    }
4961
4962    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
4963        self.enter_scope();
4964        walk_mut::walk_function_body(self, body);
4965        self.leave_scope(&mut body.statements);
4966    }
4967
4968    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4969        if self
4970            .source_sensitive_functions
4971            .contains(&span_key(function.span))
4972        {
4973            return;
4974        }
4975        walk_mut::walk_function(self, function, flags);
4976    }
4977
4978    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4979        if self
4980            .source_sensitive_functions
4981            .contains(&span_key(function.span))
4982        {
4983            return;
4984        }
4985        walk_mut::walk_arrow_function_expression(self, function);
4986    }
4987
4988    // A with body is left uninstrumented: its object environment can
4989    // intercept probe identifiers (see `enter_with_statement`).
4990    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4991
4992    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
4993        walk_mut::walk_call_expression(self, call);
4994        if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
4995            self.instrument_call(call, &site);
4996        }
4997    }
4998
4999    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
5000        let key = span_key(expression.span());
5001        walk_mut::walk_expression(self, expression);
5002        if let Some(site_keys) = self.roots.remove(&key) {
5003            self.wrap_root(expression, &site_keys);
5004        }
5005    }
5006}
5007
5008/// Where a default's pending count and its two outcomes live in the file's V2
5009/// probe state: a slot of `pendingDefaults`, and two indices of `pointIds`.
5010struct DefaultSlot {
5011    slot: usize,
5012    default_point: usize,
5013    provided_point: usize,
5014}
5015
5016struct DefaultTransformer<'a> {
5017    ast: AstBuilder<'a>,
5018    default_selected_v2: String,
5019    default_entered_v2: String,
5020    probe_file_v2: String,
5021    slots: HashMap<String, DefaultSlot>,
5022    parameter_targets: HashMap<SpanKey, DefaultTarget>,
5023    binding_targets: HashMap<SpanKey, DefaultTarget>,
5024    function_entries: Vec<Vec<Statement<'a>>>,
5025    declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
5026    active_declaration: Vec<SpanKey>,
5027    parameter_pattern_depth: usize,
5028    source_sensitive_functions: HashSet<SpanKey>,
5029    typescript: bool,
5030}
5031
5032impl<'a> DefaultTransformer<'a> {
5033    /// The type a default names by its own syntax: a literal's primitive, or
5034    /// the type it is asserted to (`"utf8" as E`).
5035    ///
5036    /// Under `isolatedDeclarations` a parameter needs no annotation while its
5037    /// default is such an expression. Wrapped in a call it does, and h3 failed
5038    /// to compile with `TS9011`, an error `@ts-nocheck` does not silence. The
5039    /// annotation says what TypeScript inferred already.
5040    fn default_type(&self, value: &Expression<'a>) -> Option<oxc_ast::ast::TSType<'a>> {
5041        let span = Span::default();
5042        let mut value = value;
5043        while let Expression::ParenthesizedExpression(parenthesized) = value {
5044            value = &parenthesized.expression;
5045        }
5046        match value {
5047            // `'x' as const` is the literal's own type, which no keyword names.
5048            Expression::TSAsExpression(asserted) => {
5049                (!asserted.type_annotation.is_const_type_reference())
5050                    .then(|| asserted.type_annotation.clone_in(self.ast.allocator))
5051            }
5052            Expression::TSTypeAssertion(asserted) => {
5053                (!asserted.type_annotation.is_const_type_reference())
5054                    .then(|| asserted.type_annotation.clone_in(self.ast.allocator))
5055            }
5056            Expression::BooleanLiteral(_) => Some(self.ast.ts_type_boolean_keyword(span)),
5057            Expression::NumericLiteral(_) => Some(self.ast.ts_type_number_keyword(span)),
5058            Expression::BigIntLiteral(_) => Some(self.ast.ts_type_big_int_keyword(span)),
5059            Expression::StringLiteral(_) => Some(self.ast.ts_type_string_keyword(span)),
5060            Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
5061                Some(self.ast.ts_type_string_keyword(span))
5062            }
5063            Expression::UnaryExpression(unary)
5064                if unary.operator == UnaryOperator::UnaryNegation
5065                    && matches!(unary.argument, Expression::NumericLiteral(_)) =>
5066            {
5067                Some(self.ast.ts_type_number_keyword(span))
5068            }
5069            _ => None,
5070        }
5071    }
5072
5073    fn identifier(&self, name: &str) -> Expression<'a> {
5074        self.ast
5075            .expression_identifier(Span::default(), self.ast.ident(name))
5076    }
5077
5078    fn string_argument(&self, value: &str) -> Argument<'a> {
5079        Argument::from(self.ast.expression_string_literal(
5080            Span::default(),
5081            self.ast.str(value),
5082            None,
5083        ))
5084    }
5085
5086    fn index_argument(&self, index: usize) -> Argument<'a> {
5087        Argument::from(self.ast.expression_numeric_literal(
5088            Span::default(),
5089            index as f64,
5090            None,
5091            NumberBase::Decimal,
5092        ))
5093    }
5094
5095    fn slot(&self, target: &DefaultTarget) -> &DefaultSlot {
5096        self.slots
5097            .get(&target.default_id)
5098            .expect("every default target has a V2 slot")
5099    }
5100
5101    /// `defaultSelectedV2(file, slot, value[, name])`: the default counts
5102    /// itself after its value evaluates, so a default that throws leaves no
5103    /// count behind.
5104    fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
5105        let mut arguments = self.ast.vec_from_array([
5106            Argument::from(self.identifier(&self.probe_file_v2)),
5107            self.index_argument(self.slot(target).slot),
5108            Argument::from(value),
5109        ]);
5110        if let Some(name) = &target.inferred_name {
5111            arguments.push(self.string_argument(name));
5112        }
5113        self.ast.expression_call(
5114            Span::default(),
5115            self.identifier(&self.default_selected_v2),
5116            NONE,
5117            arguments,
5118            false,
5119        )
5120    }
5121
5122    /// `defaultEnteredV2(file, slot, defaultPoint, providedPoint)`, once the
5123    /// parameters or the declaration are bound.
5124    fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
5125        let slot = self.slot(target);
5126        self.ast.statement_expression(
5127            Span::default(),
5128            self.ast.expression_call(
5129                Span::default(),
5130                self.identifier(&self.default_entered_v2),
5131                NONE,
5132                self.ast.vec_from_array([
5133                    Argument::from(self.identifier(&self.probe_file_v2)),
5134                    self.index_argument(slot.slot),
5135                    self.index_argument(slot.default_point),
5136                    self.index_argument(slot.provided_point),
5137                ]),
5138                false,
5139            ),
5140        )
5141    }
5142
5143    fn push_entry(&mut self, target: &DefaultTarget) {
5144        let entry = self.entered(target);
5145        if self.parameter_pattern_depth > 0 {
5146            self.function_entries
5147                .last_mut()
5148                .expect("parameter default must belong to a function")
5149                .push(entry);
5150        } else {
5151            let declaration = *self
5152                .active_declaration
5153                .last()
5154                .expect("binding default must belong to a declaration");
5155            self.declaration_entries
5156                .entry(declaration)
5157                .or_default()
5158                .push(entry);
5159        }
5160    }
5161
5162    fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
5163        if entries.is_empty() {
5164            return;
5165        }
5166        if let Statement::BlockStatement(block) = body {
5167            for (index, entry) in entries.into_iter().enumerate() {
5168                block.body.insert(index, entry);
5169            }
5170            return;
5171        }
5172        let span = body.span();
5173        let original = body.take_in(self.ast.allocator);
5174        let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
5175        statements.extend(entries);
5176        statements.push(original);
5177        *body = self.ast.statement_block(span, statements);
5178    }
5179
5180    fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
5181        match statement {
5182            Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
5183            Statement::ExportNamedDeclaration(export) => match &export.declaration {
5184                Some(Declaration::VariableDeclaration(declaration)) => {
5185                    Some(span_key(declaration.span))
5186                }
5187                _ => None,
5188            },
5189            _ => None,
5190        }
5191    }
5192
5193    fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
5194        match left {
5195            ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
5196            _ => None,
5197        }
5198    }
5199}
5200
5201impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
5202    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5203        let original = statements.take_in(self.ast.allocator);
5204        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
5205        for mut statement in original {
5206            let declaration = Self::variable_span(&statement);
5207            self.visit_statement(&mut statement);
5208            instrumented.push(statement);
5209            if let Some(declaration) = declaration
5210                && let Some(entries) = self.declaration_entries.remove(&declaration)
5211            {
5212                instrumented.extend(entries);
5213            }
5214        }
5215        *statements = instrumented;
5216    }
5217
5218    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5219        if self
5220            .source_sensitive_functions
5221            .contains(&span_key(function.span))
5222        {
5223            return;
5224        }
5225        self.function_entries.push(Vec::new());
5226        walk_mut::walk_function(self, function, flags);
5227        let entries = self
5228            .function_entries
5229            .pop()
5230            .expect("default function-entry stack must remain balanced");
5231        if let Some(body) = &mut function.body {
5232            for (index, entry) in entries.into_iter().enumerate() {
5233                body.statements.insert(index, entry);
5234            }
5235        }
5236    }
5237
5238    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5239        if self
5240            .source_sensitive_functions
5241            .contains(&span_key(function.span))
5242        {
5243            return;
5244        }
5245        self.function_entries.push(Vec::new());
5246        walk_mut::walk_arrow_function_expression(self, function);
5247        let entries = self
5248            .function_entries
5249            .pop()
5250            .expect("default arrow-entry stack must remain balanced");
5251        for (index, entry) in entries.into_iter().enumerate() {
5252            function.body.statements.insert(index, entry);
5253        }
5254    }
5255
5256    fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
5257        let outer = self.parameter_targets.remove(&span_key(parameter.span));
5258        if let Some(target) = &outer {
5259            let entry = self.entered(target);
5260            self.function_entries
5261                .last_mut()
5262                .expect("formal parameter must belong to a function")
5263                .push(entry);
5264        }
5265        self.visit_decorators(&mut parameter.decorators);
5266        self.parameter_pattern_depth += 1;
5267        self.visit_binding_pattern(&mut parameter.pattern);
5268        self.parameter_pattern_depth -= 1;
5269        if let Some(annotation) = &mut parameter.type_annotation {
5270            self.visit_ts_type_annotation(annotation);
5271        }
5272        if let Some(initializer) = &mut parameter.initializer {
5273            if self.typescript
5274                && outer.is_some()
5275                && parameter.type_annotation.is_none()
5276                && matches!(parameter.pattern, BindingPattern::BindingIdentifier(_))
5277            {
5278                let named = self.default_type(initializer);
5279                if let Some(named) = named {
5280                    parameter.type_annotation =
5281                        Some(self.ast.alloc_ts_type_annotation(Span::default(), named));
5282                }
5283            }
5284            self.visit_expression(initializer);
5285            if let Some(target) = outer {
5286                let value = initializer.take_in(self.ast.allocator);
5287                **initializer = self.selected(value, &target);
5288            }
5289        }
5290    }
5291
5292    fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
5293        let target = if self.parameter_pattern_depth > 0 {
5294            self.parameter_targets.remove(&span_key(assignment.span))
5295        } else {
5296            self.binding_targets.remove(&span_key(assignment.span))
5297        };
5298        if let Some(target) = &target {
5299            self.push_entry(target);
5300        }
5301        walk_mut::walk_assignment_pattern(self, assignment);
5302        if let Some(target) = target {
5303            let value = assignment.right.take_in(self.ast.allocator);
5304            assignment.right = self.selected(value, &target);
5305        }
5306    }
5307
5308    fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
5309        self.active_declaration.push(span_key(declaration.span));
5310        walk_mut::walk_variable_declaration(self, declaration);
5311        self.active_declaration
5312            .pop()
5313            .expect("default declaration stack must remain balanced");
5314    }
5315
5316    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
5317        let declaration = Self::loop_declaration_span(&statement.left);
5318        walk_mut::walk_for_in_statement(self, statement);
5319        if let Some(declaration) = declaration
5320            && let Some(entries) = self.declaration_entries.remove(&declaration)
5321        {
5322            self.prepend_entries(&mut statement.body, entries);
5323        }
5324    }
5325
5326    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
5327        let declaration = Self::loop_declaration_span(&statement.left);
5328        walk_mut::walk_for_of_statement(self, statement);
5329        if let Some(declaration) = declaration
5330            && let Some(entries) = self.declaration_entries.remove(&declaration)
5331        {
5332            self.prepend_entries(&mut statement.body, entries);
5333        }
5334    }
5335
5336    // A with body is left uninstrumented: its object environment can
5337    // intercept probe identifiers (see `enter_with_statement`).
5338    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5339}
5340
5341enum ExtendedKind {
5342    Try,
5343    Loop,
5344}
5345
5346/// A try's success and catch outcomes, and a loop's zero-iteration and
5347/// entered outcomes, are two V2 points from the index each target maps to. A
5348/// flag of the site's own is set once the second outcome fires: the frame
5349/// and the call per iteration before cost every enumeration loop.
5350struct ExtendedTransformer<'a, 's> {
5351    ast: AstBuilder<'a>,
5352    coverage_hit_v2: String,
5353    probe_file_v2: String,
5354    typescript: bool,
5355    try_targets: HashMap<SpanKey, usize>,
5356    loop_targets: HashMap<SpanKey, usize>,
5357    names: CandidateNames<'s>,
5358    scope_declarations: Vec<Vec<String>>,
5359    source_sensitive_functions: HashSet<SpanKey>,
5360}
5361
5362impl<'a> ExtendedTransformer<'a, '_> {
5363    fn identifier(&self, name: &str) -> Expression<'a> {
5364        self.ast
5365            .expression_identifier(Span::default(), self.ast.ident(name))
5366    }
5367
5368    fn enter_scope(&mut self) {
5369        self.scope_declarations.push(Vec::new());
5370    }
5371
5372    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5373        let names = self
5374            .scope_declarations
5375            .pop()
5376            .expect("extended scope stack must remain balanced");
5377        if names.is_empty() {
5378            return;
5379        }
5380        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5381    }
5382
5383    fn scratch(&mut self, base: &str) -> String {
5384        let name = self.names.allocate(base);
5385        self.scope_declarations
5386            .last_mut()
5387            .expect("extended branch must be inside a program or function")
5388            .push(name.clone());
5389        name
5390    }
5391
5392    fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
5393        match statement {
5394            Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
5395            Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5396            Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5397            Statement::LabeledStatement(node) => Self::target(&node.body),
5398            _ => None,
5399        }
5400    }
5401
5402    fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
5403        match statement {
5404            Statement::TryStatement(node) => Some(node),
5405            Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
5406            _ => None,
5407        }
5408    }
5409
5410    fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
5411        match statement {
5412            Statement::ForInStatement(node) => Some(&mut node.body),
5413            Statement::ForOfStatement(node) => Some(&mut node.body),
5414            Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
5415            _ => None,
5416        }
5417    }
5418
5419    fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
5420        if let Statement::BlockStatement(block) = body {
5421            block.body.insert(0, entry);
5422            return;
5423        }
5424        let span = body.span();
5425        let original = body.take_in(ast.allocator);
5426        *body = ast.statement_block(span, ast.vec_from_array([entry, original]));
5427    }
5428
5429    fn hit(&self, index: usize) -> Expression<'a> {
5430        probe_v2_hit_expression(self.ast, &self.coverage_hit_v2, &self.probe_file_v2, index)
5431    }
5432
5433    /// `T = 0;`
5434    fn reset(&self, flag: &str) -> Statement<'a> {
5435        self.ast.statement_expression(
5436            Span::default(),
5437            assign_temporary(self.ast, flag, numeric(self.ast, 0)),
5438        )
5439    }
5440
5441    /// `T || (T = 1, hit);` -- recorded the first time only.
5442    fn first_time(&self, flag: &str, index: usize) -> Statement<'a> {
5443        let record = self.ast.expression_sequence(
5444            Span::default(),
5445            self.ast.vec_from_array([
5446                assign_temporary(self.ast, flag, numeric(self.ast, 1)),
5447                self.hit(index),
5448            ]),
5449        );
5450        self.ast.statement_expression(
5451            Span::default(),
5452            self.ast.expression_logical(
5453                Span::default(),
5454                self.identifier(flag),
5455                LogicalOperator::Or,
5456                record,
5457            ),
5458        )
5459    }
5460
5461    /// `T || hit;` -- the other outcome, when the first never fired.
5462    fn otherwise(&self, flag: &str, index: usize) -> Statement<'a> {
5463        self.ast.statement_expression(
5464            Span::default(),
5465            self.ast.expression_logical(
5466                Span::default(),
5467                self.identifier(flag),
5468                LogicalOperator::Or,
5469                self.hit(index),
5470            ),
5471        )
5472    }
5473
5474    fn instrument_try(&mut self, statement: &mut Statement<'a>, first: usize) {
5475        // What Supercov adds around the statement takes its span (see
5476        // `wrap_bare`), so its braces map to the statement's own ends.
5477        let span = statement.span();
5478        let flag = self.scratch("_supercovTryFrame");
5479        let reset = self.reset(&flag);
5480        // Recorded the moment the catch starts, not when the construct is
5481        // left: `process.exit()` in a catch skips the `finally`.
5482        let caught = self.first_time(&flag, first + 1);
5483        let node = Self::inner_try(statement).expect("try target must remain a try statement");
5484        node.handler
5485            .as_mut()
5486            .expect("try coverage requires a catch handler")
5487            .body
5488            .body
5489            .insert(0, caught);
5490        let end = self.otherwise(&flag, first);
5491        if let Some(finalizer) = &mut node.finalizer {
5492            finalizer.body.insert(0, end);
5493        } else {
5494            // Its braces map to the statement's last character: to its start
5495            // they would take that mapping from the `try`, and unmapped the
5496            // `try` would end nowhere.
5497            node.finalizer = Some(
5498                self.ast
5499                    .alloc_block_statement(Span::new(span.end - 1, span.end), self.ast.vec1(end)),
5500            );
5501        }
5502        let original = statement.take_in(self.ast.allocator);
5503        *statement = self
5504            .ast
5505            .statement_block(span, self.ast.vec_from_array([reset, original]));
5506    }
5507
5508    fn instrument_loop(&mut self, statement: &mut Statement<'a>, first: usize) {
5509        let span = statement.span();
5510        let flag = self.scratch("_supercovLoopFrame");
5511        let reset = self.reset(&flag);
5512        let entered = self.first_time(&flag, first + 1);
5513        Self::prepend(
5514            Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
5515            entered,
5516            self.ast,
5517        );
5518        let original = statement.take_in(self.ast.allocator);
5519        let end = self.otherwise(&flag, first);
5520        // Only the outer block takes the loop's span: the `try` inside it
5521        // mapped to the same start would take that mapping from the loop.
5522        let wrapped = self.ast.statement_try(
5523            Span::default(),
5524            self.ast
5525                .block_statement(Span::default(), self.ast.vec1(original)),
5526            None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
5527            Some(
5528                self.ast
5529                    .block_statement(Span::default(), self.ast.vec1(end)),
5530            ),
5531        );
5532        *statement = self
5533            .ast
5534            .statement_block(span, self.ast.vec_from_array([reset, wrapped]));
5535    }
5536}
5537
5538impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
5539    fn visit_program(&mut self, program: &mut Program<'a>) {
5540        self.enter_scope();
5541        walk_mut::walk_program(self, program);
5542        self.leave_scope(&mut program.body);
5543    }
5544
5545    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5546        self.enter_scope();
5547        walk_mut::walk_function_body(self, body);
5548        self.leave_scope(&mut body.statements);
5549    }
5550
5551    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5552        if self
5553            .source_sensitive_functions
5554            .contains(&span_key(function.span))
5555        {
5556            return;
5557        }
5558        walk_mut::walk_function(self, function, flags);
5559    }
5560
5561    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5562        if self
5563            .source_sensitive_functions
5564            .contains(&span_key(function.span))
5565        {
5566            return;
5567        }
5568        walk_mut::walk_arrow_function_expression(self, function);
5569    }
5570
5571    // A with body is left uninstrumented: its object environment can
5572    // intercept probe identifiers (see `enter_with_statement`).
5573    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5574
5575    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
5576        let Some((kind, key)) = Self::target(statement) else {
5577            walk_mut::walk_statement(self, statement);
5578            return;
5579        };
5580        match kind {
5581            ExtendedKind::Try => {
5582                if let Some(target) = self.try_targets.remove(&key) {
5583                    walk_mut::walk_statement(self, statement);
5584                    self.instrument_try(statement, target);
5585                } else {
5586                    walk_mut::walk_statement(self, statement);
5587                }
5588            }
5589            ExtendedKind::Loop => {
5590                if let Some(target) = self.loop_targets.remove(&key) {
5591                    walk_mut::walk_statement(self, statement);
5592                    self.instrument_loop(statement, target);
5593                } else {
5594                    walk_mut::walk_statement(self, statement);
5595                }
5596            }
5597        }
5598    }
5599}
5600
5601/// A tree of selections used for its value, its leaves numbered in the order
5602/// they are evaluated.
5603enum SelectionShape {
5604    Leaf(usize),
5605    Selection {
5606        first: usize,
5607        kind: usize,
5608        left: Box<SelectionShape>,
5609        right: Box<SelectionShape>,
5610    },
5611}
5612
5613impl SelectionShape {
5614    fn leftmost(&self) -> usize {
5615        match self {
5616            Self::Leaf(leaf) => *leaf,
5617            Self::Selection { left, .. } => left.leftmost(),
5618        }
5619    }
5620
5621    /// The leaves whose value can be this subtree's when that value is
5622    /// truthy, or when it is not, each with the points it hits on its way up
5623    /// through the subtree: what `selectPathV2` records for the leaf's steps
5624    /// below the subtree's parent. A left side decides only by cutting its
5625    /// selection short, which `||` does on a truthy value, `&&` on a falsy
5626    /// one, and `??` on any that is not nullish.
5627    fn deciders(&self, truthy: bool, nullish: bool) -> Vec<(usize, Vec<usize>)> {
5628        match self {
5629            Self::Leaf(leaf) => vec![(*leaf, Vec::new())],
5630            Self::Selection {
5631                first,
5632                kind,
5633                left,
5634                right,
5635            } => {
5636                let mut deciders = Vec::new();
5637                let short = match kind {
5638                    0 => truthy,
5639                    1 => !truthy,
5640                    _ => !nullish,
5641                };
5642                if short {
5643                    for (leaf, mut hits) in left.deciders(truthy, nullish) {
5644                        hits.push(first + usize::from(truthy));
5645                        deciders.push((leaf, hits));
5646                    }
5647                }
5648                for (leaf, mut hits) in right.deciders(truthy, nullish) {
5649                    hits.push(first + 2 + usize::from(truthy));
5650                    deciders.push((leaf, hits));
5651                }
5652                deciders
5653            }
5654        }
5655    }
5656
5657    /// For each leaf but the first, the leaves that can have been evaluated
5658    /// just before it, with what each decided by then. Evaluation reaches the
5659    /// right side of a selection when its left side did not cut it short:
5660    /// truthy for `&&`, falsy for `||`, and nullish, so falsy, for `??`.
5661    fn before(&self, before: &mut Vec<Vec<(usize, Vec<usize>)>>) {
5662        let Self::Selection {
5663            kind, left, right, ..
5664        } = self
5665        else {
5666            return;
5667        };
5668        before[right.leftmost()] = left.deciders(*kind == 1, *kind == 2);
5669        left.before(before);
5670        right.before(before);
5671    }
5672}
5673
5674type SelectionTable = (Vec<Vec<usize>>, Vec<Vec<Vec<usize>>>);
5675
5676/// A tree whose leaves are named: the helper that records its value, the
5677/// selection or the table entry it records for, and the tree's frame.
5678struct SelectionSite {
5679    helper: String,
5680    index: usize,
5681    frame: String,
5682}
5683
5684struct LogicalValueTransformer<'a, 's> {
5685    ast: AstBuilder<'a>,
5686    select_short_v2: String,
5687    select_right_v2: String,
5688    select_named_right_v2: String,
5689    select_assign_end_v2: String,
5690    select_path_v2: String,
5691    select_end2_v2: String,
5692    select_end_v2: String,
5693    select_next_v2: String,
5694    coverage_hit_v2: String,
5695    probe_file_v2: String,
5696    typescript: bool,
5697    names: CandidateNames<'s>,
5698    scope_declarations: Vec<Vec<String>>,
5699    /// Inside a parameter default or a class member's initializer, where no
5700    /// function body can declare a tree's frame: a parameter list cannot see
5701    /// its function's body, and an initializer's enclosing scope is shared by
5702    /// every instance being constructed.
5703    frameless: usize,
5704    /// For each tree with more than two leaves, the runtime's table: the
5705    /// steps of each leaf, and for each leaf the points each leaf that can
5706    /// come before it has decided by then, where more than one can.
5707    trees: Vec<SelectionTable>,
5708    /// The statement being visited is a `const` declaration in a list of
5709    /// statements, where another can follow it.
5710    aliasing: bool,
5711    /// What records the values of the trees the declaration just visited
5712    /// keeps as its initializers, to go after it.
5713    after_declaration: Vec<Expression<'a>>,
5714    /// A selection's four outcomes -- short falsy, short truthy, right falsy,
5715    /// right truthy -- are consecutive V2 points from this index.
5716    logical_targets: HashMap<SpanKey, usize>,
5717    /// The same four points for `||=`, `&&=` and `??=`.
5718    assignment_targets: HashMap<SpanKey, usize>,
5719    source_sensitive_functions: HashSet<SpanKey>,
5720}
5721
5722impl<'a> LogicalValueTransformer<'a, '_> {
5723    fn identifier(&self, name: &str) -> Expression<'a> {
5724        self.ast
5725            .expression_identifier(Span::default(), self.ast.ident(name))
5726    }
5727
5728    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
5729        AssignmentTarget::from(
5730            self.ast
5731                .simple_assignment_target_assignment_target_identifier(
5732                    Span::default(),
5733                    self.ast.ident(name),
5734                ),
5735        )
5736    }
5737
5738    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
5739        self.ast.expression_call(
5740            Span::default(),
5741            self.identifier(name),
5742            NONE,
5743            arguments,
5744            false,
5745        )
5746    }
5747
5748    fn string_argument(&self, value: &str) -> Argument<'a> {
5749        Argument::from(self.ast.expression_string_literal(
5750            Span::default(),
5751            self.ast.str(value),
5752            None,
5753        ))
5754    }
5755
5756    fn enter_scope(&mut self) {
5757        self.scope_declarations.push(Vec::new());
5758    }
5759
5760    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5761        let names = self
5762            .scope_declarations
5763            .pop()
5764            .expect("logical-value scope stack must remain balanced");
5765        if names.is_empty() {
5766            return;
5767        }
5768        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5769    }
5770
5771    fn scratch(&mut self, base: &str) -> String {
5772        let name = self.names.allocate(base);
5773        self.scope_declarations
5774            .last_mut()
5775            .expect("logical expression must be inside a program or function")
5776            .push(name.clone());
5777        name
5778    }
5779
5780    fn helper(&self, name: &str, arguments: [Argument<'a>; 3]) -> Expression<'a> {
5781        let mut all = self
5782            .ast
5783            .vec_from_array([Argument::from(self.identifier(&self.probe_file_v2))]);
5784        all.extend(arguments);
5785        self.call(name, all)
5786    }
5787
5788    /// `a || b` as `selectShortV2(file, first, a, 0) || selectRightV2(file,
5789    /// first, b)`, and `&&` (1) and `??` (2) alike. The program's own operator
5790    /// stays, so a coverage tool the tests run -- nyc, c8, Jest, Vitest -- counts
5791    /// the branches it counts without Supercov, and neither call needs a frame
5792    /// or a temporary: the short side records its outcome when it decides the
5793    /// result, the right side records whether it came out truthy. Both are small
5794    /// enough for V8 to inline; the frame and three calls each evaluation cost
5795    /// before were a tenth of lru-cache's hot loop, with MC/DC.
5796    ///
5797    /// Nested selections, `(a && b) || c`, stay one tree of the program's
5798    /// operators with only the leaves wrapped: istanbul flattens a tree through
5799    /// its logical children only, and a call around `a && b` made two branches
5800    /// of its one. A leaf deeper than one selection records, through
5801    /// `selectPathV2`, every selection above it that its value decides.
5802    fn instrument_tree(&mut self, expression: &mut Expression<'a>, path: &[usize]) {
5803        let key = span_key(expression.span());
5804        let first = self
5805            .logical_targets
5806            .remove(&key)
5807            .expect("a selection tree starts at a selection");
5808        let Expression::LogicalExpression(logical) = expression else {
5809            panic!("logical-value target must remain a logical expression");
5810        };
5811        let kind = match logical.operator {
5812            LogicalOperator::Or => 0,
5813            LogicalOperator::And => 1,
5814            LogicalOperator::Coalesce => 2,
5815        };
5816        let mut left_path = vec![first * 4 + kind];
5817        left_path.extend_from_slice(path);
5818        let mut right_path = vec![first * 4 + 3];
5819        right_path.extend_from_slice(path);
5820        self.instrument_operand(&mut logical.left, &left_path);
5821        self.instrument_operand(&mut logical.right, &right_path);
5822    }
5823
5824    fn instrument_operand(&mut self, operand: &mut Expression<'a>, path: &[usize]) {
5825        let mut inner = &mut *operand;
5826        while let Expression::ParenthesizedExpression(parenthesized) = inner {
5827            inner = &mut parenthesized.expression;
5828        }
5829        if matches!(inner, Expression::LogicalExpression(_))
5830            && self.logical_targets.contains_key(&span_key(inner.span()))
5831        {
5832            self.instrument_tree(inner, path);
5833            return;
5834        }
5835        self.visit_expression(operand);
5836        let value = operand.take_in(self.ast.allocator);
5837        *operand = match *path {
5838            [step] if step % 4 == 3 => {
5839                let mut arguments = self.ast.vec_from_array([
5840                    Argument::from(self.identifier(&self.probe_file_v2)),
5841                    Argument::from(numeric(self.ast, step / 4)),
5842                ]);
5843                arguments.push(Argument::from(value));
5844                self.call(&self.select_right_v2, arguments)
5845            }
5846            [step] => self.helper(
5847                &self.select_short_v2,
5848                [
5849                    Argument::from(numeric(self.ast, step / 4)),
5850                    Argument::from(value),
5851                    Argument::from(numeric(self.ast, step % 4)),
5852                ],
5853            ),
5854            _ => {
5855                let mut arguments = self.ast.vec_from_array([
5856                    Argument::from(self.identifier(&self.probe_file_v2)),
5857                    Argument::from(value),
5858                ]);
5859                arguments.extend(
5860                    path.iter()
5861                        .map(|step| Argument::from(numeric(self.ast, *step))),
5862                );
5863                self.call(&self.select_path_v2, arguments)
5864            }
5865        };
5866    }
5867
5868    /// `x ||= y` as `(T = 0, selectAssignEndV2(file, first, x ||= selectRightV2(
5869    /// file, first, y, T = 1), T))`, and `&&=` and `??=` alike. The operator
5870    /// stays, so the target is read and written once, as the program does, and
5871    /// a frame per evaluation is gone. `T` is a temporary of the enclosing
5872    /// function, set after the right side evaluates, so a recursive call there
5873    /// has its own; the end records the short outcome when it is still 0.
5874    /// The shape of the tree at `expression`, taking its selections out of
5875    /// the targets, and each leaf's steps as `selectPathV2` takes them.
5876    fn shape(
5877        &mut self,
5878        expression: &Expression<'a>,
5879        path: &[usize],
5880        leaves: &mut Vec<Vec<usize>>,
5881    ) -> SelectionShape {
5882        let mut inner = expression;
5883        while let Expression::ParenthesizedExpression(parenthesized) = inner {
5884            inner = &parenthesized.expression;
5885        }
5886        if let Expression::LogicalExpression(logical) = inner
5887            && let Some(first) = self.logical_targets.remove(&span_key(logical.span))
5888        {
5889            let kind = match logical.operator {
5890                LogicalOperator::Or => 0,
5891                LogicalOperator::And => 1,
5892                LogicalOperator::Coalesce => 2,
5893            };
5894            let mut left_path = vec![first * 4 + kind];
5895            left_path.extend_from_slice(path);
5896            let mut right_path = vec![first * 4 + 3];
5897            right_path.extend_from_slice(path);
5898            let left = Box::new(self.shape(&logical.left, &left_path, leaves));
5899            let right = Box::new(self.shape(&logical.right, &right_path, leaves));
5900            return SelectionShape::Selection {
5901                first,
5902                kind,
5903                left,
5904                right,
5905            };
5906        }
5907        leaves.push(path.to_vec());
5908        SelectionShape::Leaf(leaves.len() - 1)
5909    }
5910
5911    fn hits(&self, points: &[usize]) -> Option<Expression<'a>> {
5912        let mut hits = self.ast.vec_from_iter(points.iter().map(|point| {
5913            self.call(
5914                &self.coverage_hit_v2,
5915                self.ast.vec_from_array([
5916                    Argument::from(self.identifier(&self.probe_file_v2)),
5917                    Argument::from(numeric(self.ast, *point)),
5918                ]),
5919            )
5920        }));
5921        match hits.len() {
5922            0 => None,
5923            1 => hits.pop(),
5924            _ => Some(self.ast.expression_sequence(Span::default(), hits)),
5925        }
5926    }
5927
5928    /// What the leaf evaluated before this one decided. Where only one leaf
5929    /// can come before, its points are hit there. Where several can, the
5930    /// frame says which, and the runtime reads its points from the tree's
5931    /// table: a choice written out here would be a branch of the user's line
5932    /// to a coverage tool the tests run.
5933    fn recorded_before(
5934        &self,
5935        frame: &str,
5936        tree: Option<usize>,
5937        leaf: usize,
5938        deciders: &[(usize, Vec<usize>)],
5939    ) -> Option<Expression<'a>> {
5940        if deciders.iter().all(|(_, points)| points.is_empty()) {
5941            return None;
5942        }
5943        if let [(_, points)] = deciders {
5944            return self.hits(points);
5945        }
5946        let tree = tree.expect("a tree with a choice of leaves has a table");
5947        Some(self.call(
5948            &self.select_next_v2,
5949            self.ast.vec_from_array([
5950                Argument::from(self.identifier(&self.probe_file_v2)),
5951                Argument::from(numeric(self.ast, tree)),
5952                Argument::from(self.identifier(frame)),
5953                Argument::from(numeric(self.ast, leaf)),
5954            ]),
5955        ))
5956    }
5957
5958    fn name_leaves(
5959        &mut self,
5960        expression: &mut Expression<'a>,
5961        shape: &SelectionShape,
5962        frame: &str,
5963        tree: Option<usize>,
5964        before: &[Vec<(usize, Vec<usize>)>],
5965    ) {
5966        let leaf = match shape {
5967            SelectionShape::Leaf(leaf) => *leaf,
5968            SelectionShape::Selection { left, right, .. } => {
5969                let mut inner = expression;
5970                while let Expression::ParenthesizedExpression(parenthesized) = inner {
5971                    inner = &mut parenthesized.expression;
5972                }
5973                let Expression::LogicalExpression(logical) = inner else {
5974                    panic!("a selection must remain a logical expression");
5975                };
5976                self.name_leaves(&mut logical.left, left, frame, tree, before);
5977                self.name_leaves(&mut logical.right, right, frame, tree, before);
5978                return;
5979            }
5980        };
5981        self.visit_expression(expression);
5982        let value = expression.take_in(self.ast.allocator);
5983        let mut steps = self.ast.vec_with_capacity(3);
5984        steps.extend(self.recorded_before(frame, tree, leaf, &before[leaf]));
5985        steps.push(self.ast.expression_assignment(
5986            Span::default(),
5987            AssignmentOperator::Assign,
5988            self.assignment_target(frame),
5989            numeric(self.ast, leaf),
5990        ));
5991        steps.push(value);
5992        *expression = self.ast.expression_sequence(Span::default(), steps);
5993    }
5994
5995    /// `a && a.b` as `selectEnd2V2(file, first, (_f = 0, a) && (_f = 1, a.b),
5996    /// _f)`: each leaf is still the program's own expression, as the last
5997    /// operand of a comma, so it has the type, the contextual type and the
5998    /// narrowing it has in the source. With a call around each leaf, as in
5999    /// `instrument_tree`, `a.b` was read from an `a` that nothing had
6000    /// narrowed: an error the instrumented copy does not report, and `any`
6001    /// when a member of `a`'s type has no `b`, for everything that reads the
6002    /// value.
6003    ///
6004    /// The frame names the leaf being evaluated. The call around the tree
6005    /// records the outcomes of the leaf that produced its value. What an
6006    /// earlier leaf decided is known from where evaluation went next, and is
6007    /// recorded there: the same points, in the same order, as before, a tree
6008    /// that throws half-way included.
6009    fn instrument_value_tree(&mut self, expression: &mut Expression<'a>) {
6010        let site = self.name_tree(expression);
6011        let tree = expression.take_in(self.ast.allocator);
6012        *expression = self.record_tree(&site, tree);
6013    }
6014
6015    /// The call that records a named tree's value, around that value.
6016    fn record_tree(&self, site: &SelectionSite, value: Expression<'a>) -> Expression<'a> {
6017        self.call(
6018            &site.helper,
6019            self.ast.vec_from_array([
6020                Argument::from(self.identifier(&self.probe_file_v2)),
6021                Argument::from(numeric(self.ast, site.index)),
6022                Argument::from(value),
6023                Argument::from(self.identifier(&site.frame)),
6024            ]),
6025        )
6026    }
6027
6028    /// Name the leaves of the tree at `expression`.
6029    fn name_tree(&mut self, expression: &mut Expression<'a>) -> SelectionSite {
6030        let mut leaves = Vec::new();
6031        let shape = self.shape(expression, &[], &mut leaves);
6032        let SelectionShape::Selection { first, .. } = shape else {
6033            panic!("a selection tree starts at a selection");
6034        };
6035        let mut before = vec![Vec::new(); leaves.len()];
6036        shape.before(&mut before);
6037        let frame = self.scratch("_supercovSelected");
6038        let tree = (leaves.len() > 2).then(|| {
6039            // The table for `selectNextV2`, by the leaf reached and the one
6040            // before it.
6041            let choices = before
6042                .iter()
6043                .map(|deciders| {
6044                    let mut by_leaf = Vec::new();
6045                    if deciders.len() > 1 {
6046                        for (leaf, points) in deciders {
6047                            if by_leaf.len() <= *leaf {
6048                                by_leaf.resize(*leaf + 1, Vec::new());
6049                            }
6050                            by_leaf[*leaf] = points.clone();
6051                        }
6052                    }
6053                    by_leaf
6054                })
6055                .collect();
6056            self.trees.push((leaves, choices));
6057            self.trees.len() - 1
6058        });
6059        self.name_leaves(expression, &shape, &frame, tree, &before);
6060        let (helper, index) = match tree {
6061            Some(tree) => (self.select_end_v2.clone(), tree),
6062            None => (self.select_end2_v2.clone(), first),
6063        };
6064        SelectionSite {
6065            helper,
6066            index,
6067            frame,
6068        }
6069    }
6070
6071    fn instrument_assignment(
6072        &mut self,
6073        assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
6074        first: usize,
6075    ) -> Expression<'a> {
6076        let assignment = assignment.unbox();
6077        let inferred_name = match &assignment.left {
6078            AssignmentTarget::AssignmentTargetIdentifier(identifier)
6079                if assignment.span.start == identifier.span.start
6080                    && expression_is_anonymous_definition(&assignment.right) =>
6081            {
6082                Some(identifier.name.to_string())
6083            }
6084            _ => None,
6085        };
6086        let right_evaluated = self.scratch("_supercovAssignedRight");
6087        let set = |value| {
6088            self.ast.expression_assignment(
6089                Span::default(),
6090                AssignmentOperator::Assign,
6091                self.assignment_target(&right_evaluated),
6092                numeric(self.ast, value),
6093            )
6094        };
6095        let (reset, mark) = (set(0), set(1));
6096        let mut right_arguments = self.ast.vec_from_array([
6097            Argument::from(self.identifier(&self.probe_file_v2)),
6098            Argument::from(numeric(self.ast, first)),
6099            Argument::from(assignment.right),
6100        ]);
6101        let right_helper = match inferred_name {
6102            Some(name) => {
6103                right_arguments.push(self.string_argument(&name));
6104                &self.select_named_right_v2
6105            }
6106            None => &self.select_right_v2,
6107        };
6108        right_arguments.push(Argument::from(mark));
6109        let right = self.call(right_helper, right_arguments);
6110        let measured_assignment = self.ast.expression_assignment(
6111            Span::default(),
6112            assignment.operator,
6113            assignment.left,
6114            right,
6115        );
6116        let end = self.call(
6117            &self.select_assign_end_v2,
6118            self.ast.vec_from_array([
6119                Argument::from(self.identifier(&self.probe_file_v2)),
6120                Argument::from(numeric(self.ast, first)),
6121                Argument::from(measured_assignment),
6122                Argument::from(self.identifier(&right_evaluated)),
6123            ]),
6124        );
6125        self.ast
6126            .expression_sequence(Span::default(), self.ast.vec_from_array([reset, end]))
6127    }
6128}
6129
6130impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
6131    fn visit_program(&mut self, program: &mut Program<'a>) {
6132        self.enter_scope();
6133        walk_mut::walk_program(self, program);
6134        self.leave_scope(&mut program.body);
6135    }
6136
6137    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
6138        let frameless = std::mem::take(&mut self.frameless);
6139        self.enter_scope();
6140        walk_mut::walk_function_body(self, body);
6141        self.leave_scope(&mut body.statements);
6142        self.frameless = frameless;
6143    }
6144
6145    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
6146        let mut index = 0;
6147        while index < statements.len() {
6148            let declaration = match &statements[index] {
6149                Statement::VariableDeclaration(declaration) => Some(&**declaration),
6150                Statement::ExportNamedDeclaration(export) => match &export.declaration {
6151                    Some(Declaration::VariableDeclaration(declaration)) => Some(&**declaration),
6152                    _ => None,
6153                },
6154                _ => None,
6155            };
6156            let aliasing = std::mem::replace(
6157                &mut self.aliasing,
6158                declaration.is_some_and(|declaration| declaration.kind.is_const()),
6159            );
6160            self.visit_statement(&mut statements[index]);
6161            self.aliasing = aliasing;
6162            for recorded in std::mem::take(&mut self.after_declaration) {
6163                index += 1;
6164                statements.insert(
6165                    index,
6166                    self.ast.statement_expression(Span::default(), recorded),
6167                );
6168            }
6169            index += 1;
6170        }
6171    }
6172
6173    // `const ok = a && b; if (ok) …` narrows `a` and `b`, but only while the
6174    // tree is the initializer itself: its value is recorded by a statement
6175    // after the declaration, from the constant.
6176    fn visit_variable_declarator(&mut self, declarator: &mut VariableDeclarator<'a>) {
6177        let aliasing = std::mem::take(&mut self.aliasing);
6178        if aliasing
6179            && self.frameless == 0
6180            && let BindingPattern::BindingIdentifier(name) = &declarator.id
6181            && let Some(initializer) = &mut declarator.init
6182        {
6183            let mut tree = &mut *initializer;
6184            while let Expression::ParenthesizedExpression(parenthesized) = tree {
6185                tree = &mut parenthesized.expression;
6186            }
6187            if matches!(tree, Expression::LogicalExpression(_))
6188                && self.logical_targets.contains_key(&span_key(tree.span()))
6189            {
6190                let site = self.name_tree(tree);
6191                let recorded = self.record_tree(&site, self.identifier(&name.name));
6192                self.after_declaration.push(recorded);
6193                self.aliasing = aliasing;
6194                return;
6195            }
6196        }
6197        walk_mut::walk_variable_declarator(self, declarator);
6198        self.aliasing = aliasing;
6199    }
6200
6201    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
6202        self.frameless += 1;
6203        walk_mut::walk_formal_parameters(self, parameters);
6204        self.frameless -= 1;
6205    }
6206
6207    fn visit_property_definition(&mut self, property: &mut oxc_ast::ast::PropertyDefinition<'a>) {
6208        self.frameless += 1;
6209        walk_mut::walk_property_definition(self, property);
6210        self.frameless -= 1;
6211    }
6212
6213    fn visit_accessor_property(&mut self, property: &mut oxc_ast::ast::AccessorProperty<'a>) {
6214        self.frameless += 1;
6215        walk_mut::walk_accessor_property(self, property);
6216        self.frameless -= 1;
6217    }
6218
6219    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6220        if self
6221            .source_sensitive_functions
6222            .contains(&span_key(function.span))
6223        {
6224            return;
6225        }
6226        walk_mut::walk_function(self, function, flags);
6227    }
6228
6229    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6230        if self
6231            .source_sensitive_functions
6232            .contains(&span_key(function.span))
6233        {
6234            return;
6235        }
6236        walk_mut::walk_arrow_function_expression(self, function);
6237    }
6238
6239    // A with body is left uninstrumented: its object environment can
6240    // intercept probe identifiers (see `enter_with_statement`).
6241    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6242
6243    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
6244        let key = span_key(expression.span());
6245        if matches!(expression, Expression::LogicalExpression(_))
6246            && self.logical_targets.contains_key(&key)
6247        {
6248            if self.frameless > 0 {
6249                self.instrument_tree(expression, &[]);
6250            } else {
6251                self.instrument_value_tree(expression);
6252            }
6253            return;
6254        }
6255        walk_mut::walk_expression(self, expression);
6256        if let Some(first) = self.assignment_targets.remove(&key) {
6257            let original = expression.take_in(self.ast.allocator);
6258            let Expression::AssignmentExpression(assignment) = original else {
6259                panic!("logical-assignment target must remain an assignment expression");
6260            };
6261            *expression = self.instrument_assignment(assignment, first);
6262        }
6263    }
6264}
6265
6266struct SwitchTransformer<'a, 's> {
6267    ast: AstBuilder<'a>,
6268    coverage_hit: String,
6269    targets: HashMap<SpanKey, SwitchTarget>,
6270    names: CandidateNames<'s>,
6271    source_sensitive_functions: HashSet<SpanKey>,
6272}
6273
6274impl<'a> SwitchTransformer<'a, '_> {
6275    fn identifier(&self, name: &str) -> Expression<'a> {
6276        self.ast
6277            .expression_identifier(Span::default(), self.ast.ident(name))
6278    }
6279
6280    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
6281        AssignmentTarget::from(
6282            self.ast
6283                .simple_assignment_target_assignment_target_identifier(
6284                    Span::default(),
6285                    self.ast.ident(name),
6286                ),
6287        )
6288    }
6289
6290    fn probe(&self, id: &str) -> Statement<'a> {
6291        self.ast.statement_expression(
6292            Span::default(),
6293            self.ast.expression_call(
6294                Span::default(),
6295                self.identifier(&self.coverage_hit),
6296                NONE,
6297                self.ast
6298                    .vec1(Argument::from(self.ast.expression_string_literal(
6299                        Span::default(),
6300                        self.ast.str(id),
6301                        None,
6302                    ))),
6303                false,
6304            ),
6305        )
6306    }
6307
6308    fn target(statement: &Statement<'a>) -> Option<SpanKey> {
6309        match statement {
6310            Statement::SwitchStatement(node) => Some(span_key(node.span)),
6311            Statement::LabeledStatement(node) => Self::target(&node.body),
6312            _ => None,
6313        }
6314    }
6315
6316    fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
6317        match statement {
6318            Statement::SwitchStatement(node) => Some(node),
6319            Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
6320            _ => None,
6321        }
6322    }
6323
6324    fn entered_assignment(&self, entered: &str) -> Statement<'a> {
6325        self.ast.statement_expression(
6326            Span::default(),
6327            self.ast.expression_assignment(
6328                Span::default(),
6329                AssignmentOperator::Assign,
6330                self.assignment_target(entered),
6331                self.ast.expression_boolean_literal(Span::default(), true),
6332            ),
6333        )
6334    }
6335
6336    /// Each case records only when the switch selected it: a case the one
6337    /// before falls into (`case 0: case 1:`, or a body with no `break`) ran
6338    /// without its test matching, and crediting it claimed a test of a value
6339    /// none had checked. `entered` says the switch has already selected a
6340    /// case; it also tells a switch with no default that nothing matched.
6341    fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
6342        let entered = self.names.allocate("_supercovSwitchEntered");
6343        let node = Self::inner_switch(statement).expect("switch target must remain a switch");
6344        for (index, case) in node.cases.iter_mut().enumerate() {
6345            let probe = self.probe(
6346                target
6347                    .case_ids
6348                    .get(index)
6349                    .expect("switch case target count must remain stable"),
6350            );
6351            let selected = self.ast.statement_if(
6352                Span::default(),
6353                self.ast.expression_unary(
6354                    Span::default(),
6355                    UnaryOperator::LogicalNot,
6356                    self.identifier(&entered),
6357                ),
6358                probe,
6359                None,
6360            );
6361            case.consequent.insert(0, self.entered_assignment(&entered));
6362            case.consequent.insert(0, selected);
6363        }
6364        let declaration =
6365            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
6366                Span::default(),
6367                VariableDeclarationKind::Let,
6368                self.ast.vec1(self.ast.variable_declarator(
6369                    Span::default(),
6370                    VariableDeclarationKind::Let,
6371                    self.ast.binding_pattern_binding_identifier(
6372                        Span::default(),
6373                        self.ast.ident(&entered),
6374                    ),
6375                    NONE,
6376                    Some(self.ast.expression_boolean_literal(Span::default(), false)),
6377                    false,
6378                )),
6379                false,
6380            ));
6381        let original = statement.take_in(self.ast.allocator);
6382        let mut body = self.ast.vec_from_array([declaration, original]);
6383        if let Some(no_match_id) = &target.no_match_id {
6384            body.push(self.ast.statement_if(
6385                Span::default(),
6386                self.ast.expression_unary(
6387                    Span::default(),
6388                    UnaryOperator::LogicalNot,
6389                    self.identifier(&entered),
6390                ),
6391                self.probe(no_match_id),
6392                None,
6393            ));
6394        }
6395        *statement = self.ast.statement_block(Span::default(), body);
6396    }
6397}
6398
6399impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
6400    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6401        if self
6402            .source_sensitive_functions
6403            .contains(&span_key(function.span))
6404        {
6405            return;
6406        }
6407        walk_mut::walk_function(self, function, flags);
6408    }
6409
6410    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6411        if self
6412            .source_sensitive_functions
6413            .contains(&span_key(function.span))
6414        {
6415            return;
6416        }
6417        walk_mut::walk_arrow_function_expression(self, function);
6418    }
6419
6420    // A with body is left uninstrumented: its object environment can
6421    // intercept probe identifiers (see `enter_with_statement`).
6422    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6423
6424    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
6425        let Some(key) = Self::target(statement) else {
6426            walk_mut::walk_statement(self, statement);
6427            return;
6428        };
6429        let Some(target) = self.targets.remove(&key) else {
6430            walk_mut::walk_statement(self, statement);
6431            return;
6432        };
6433        self.instrument(statement, &target);
6434        // The switch now sits in a block of its own; its target is spent, so
6435        // walking it reaches the switches inside its cases and nothing twice.
6436        walk_mut::walk_statement(self, statement);
6437    }
6438}
6439
6440struct RouteRequestPhaseTransformer<'a, 's> {
6441    ast: AstBuilder<'a>,
6442    file: &'s str,
6443    with_request_phase: String,
6444    used: bool,
6445    names: CandidateNames<'s>,
6446}
6447
6448impl<'a> RouteRequestPhaseTransformer<'a, '_> {
6449    fn is_remix_route(&self) -> bool {
6450        self.file.starts_with("app/routes/")
6451    }
6452
6453    fn is_next_route(&self) -> bool {
6454        let path = Path::new(self.file);
6455        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
6456            return false;
6457        };
6458        let is_route_module = [
6459            "route.js",
6460            "route.jsx",
6461            "route.ts",
6462            "route.tsx",
6463            "route.mjs",
6464            "route.mts",
6465            "route.cjs",
6466            "route.cts",
6467        ]
6468        .contains(&name);
6469        if !is_route_module {
6470            return false;
6471        }
6472        let components = path
6473            .components()
6474            .filter_map(|component| component.as_os_str().to_str())
6475            .collect::<Vec<_>>();
6476        components
6477            .windows(2)
6478            .any(|window| window[0] == "app" && window[1] != name)
6479    }
6480
6481    fn is_handler_name(&self, name: &str) -> bool {
6482        (self.is_remix_route() && matches!(name, "loader" | "action"))
6483            || (self.is_next_route()
6484                && matches!(
6485                    name,
6486                    "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
6487                ))
6488    }
6489
6490    fn identifier(&self, name: &str) -> Expression<'a> {
6491        self.ast
6492            .expression_identifier(Span::default(), self.ast.ident(name))
6493    }
6494
6495    fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
6496        self.ast.expression_call(
6497            Span::default(),
6498            self.identifier(&self.with_request_phase),
6499            NONE,
6500            self.ast.vec1(Argument::from(expression)),
6501            false,
6502        )
6503    }
6504
6505    fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
6506        Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
6507            Span::default(),
6508            Some(self.ast.declaration_variable(
6509                Span::default(),
6510                VariableDeclarationKind::Const,
6511                self.ast.vec1(self.ast.variable_declarator(
6512                    Span::default(),
6513                    VariableDeclarationKind::Const,
6514                    self.ast.binding_pattern_binding_identifier(
6515                        Span::default(),
6516                        self.ast.ident(exported_name),
6517                    ),
6518                    NONE,
6519                    Some(self.wrap_expression(self.identifier(original_name))),
6520                    false,
6521                )),
6522                false,
6523            )),
6524            self.ast.vec(),
6525            None,
6526            ImportOrExportKind::Value,
6527            NONE,
6528        ))
6529    }
6530
6531    fn transform_named_export(
6532        &mut self,
6533        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
6534        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6535    ) {
6536        if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
6537            for declarator in &mut declaration.declarations {
6538                let Some(name) = binding_identifier_name(&declarator.id) else {
6539                    continue;
6540                };
6541                if !self.is_handler_name(&name) {
6542                    continue;
6543                }
6544                if let Some(initializer) = &mut declarator.init {
6545                    let original = initializer.take_in(self.ast.allocator);
6546                    *initializer = self.wrap_expression(original);
6547                    self.used = true;
6548                }
6549            }
6550            output.push(Statement::ExportNamedDeclaration(export));
6551            return;
6552        }
6553
6554        if matches!(
6555            export.declaration,
6556            Some(Declaration::FunctionDeclaration(_))
6557        ) {
6558            let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
6559            else {
6560                unreachable!();
6561            };
6562            let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
6563                output.push(Statement::FunctionDeclaration(function));
6564                return;
6565            };
6566            if !self.is_handler_name(&exported_name) {
6567                export.declaration = Some(Declaration::FunctionDeclaration(function));
6568                output.push(Statement::ExportNamedDeclaration(export));
6569                return;
6570            }
6571            let original_name = self
6572                .names
6573                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6574            function.id = Some(
6575                self.ast
6576                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
6577            );
6578            output.push(Statement::FunctionDeclaration(function));
6579            output.push(self.wrapped_named_export(&exported_name, &original_name));
6580            self.used = true;
6581            return;
6582        }
6583
6584        if export.source.is_none() {
6585            output.push(Statement::ExportNamedDeclaration(export));
6586            return;
6587        }
6588        let source = export
6589            .source
6590            .as_ref()
6591            .expect("checked above")
6592            .clone_in(self.ast.allocator);
6593        let specifiers = export.specifiers.take_in(self.ast.allocator);
6594        let mut untouched = self.ast.vec();
6595        let mut handlers = Vec::new();
6596        for specifier in specifiers {
6597            let exported_name = specifier
6598                .exported
6599                .identifier_name()
6600                .map(|name| name.to_string());
6601            if exported_name
6602                .as_deref()
6603                .is_some_and(|name| self.is_handler_name(name))
6604            {
6605                handlers.push((exported_name.expect("checked above"), specifier));
6606            } else {
6607                untouched.push(specifier);
6608            }
6609        }
6610        if handlers.is_empty() {
6611            export.specifiers = untouched;
6612            output.push(Statement::ExportNamedDeclaration(export));
6613            return;
6614        }
6615        if !untouched.is_empty() {
6616            output.push(Statement::ExportNamedDeclaration(
6617                self.ast.alloc_export_named_declaration(
6618                    export.span,
6619                    None,
6620                    untouched,
6621                    Some(source.clone_in(self.ast.allocator)),
6622                    export.export_kind,
6623                    export.with_clause.take(),
6624                ),
6625            ));
6626        }
6627        for (exported_name, specifier) in handlers {
6628            let original_name = self
6629                .names
6630                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6631            output.push(Statement::ImportDeclaration(
6632                self.ast.alloc_import_declaration(
6633                    Span::default(),
6634                    Some(self.ast.vec1(
6635                        self.ast.import_declaration_specifier_import_specifier(
6636                            Span::default(),
6637                            specifier.local,
6638                            self.ast.binding_identifier(
6639                                Span::default(),
6640                                self.ast.ident(&original_name),
6641                            ),
6642                            ImportOrExportKind::Value,
6643                        ),
6644                    )),
6645                    source.clone_in(self.ast.allocator),
6646                    None,
6647                    NONE,
6648                    ImportOrExportKind::Value,
6649                ),
6650            ));
6651            output.push(self.wrapped_named_export(&exported_name, &original_name));
6652        }
6653        self.used = true;
6654    }
6655
6656    fn transform_default_export(
6657        &mut self,
6658        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
6659        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6660    ) {
6661        if let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
6662            export.declaration.take_in(self.ast.allocator)
6663        {
6664            let original_name = self
6665                .names
6666                .allocate("__supercovHandleRequestCoverageOriginal");
6667            function.id = Some(
6668                self.ast
6669                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
6670            );
6671            output.push(Statement::FunctionDeclaration(function));
6672            output.push(Statement::ExportDefaultDeclaration(
6673                self.ast.alloc_export_default_declaration(
6674                    Span::default(),
6675                    ExportDefaultDeclarationKind::from(
6676                        self.wrap_expression(self.identifier(&original_name)),
6677                    ),
6678                ),
6679            ));
6680            self.used = true;
6681            return;
6682        }
6683        if export.declaration.is_expression() {
6684            let original = export
6685                .declaration
6686                .take_in(self.ast.allocator)
6687                .into_expression();
6688            export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
6689            self.used = true;
6690        }
6691        output.push(Statement::ExportDefaultDeclaration(export));
6692    }
6693
6694    fn transform_program(&mut self, program: &mut Program<'a>) {
6695        let route_module = self.is_remix_route() || self.is_next_route();
6696        let server_entry = self.file.starts_with("app/entry.server.")
6697            && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
6698                &Path::new(self.file)
6699                    .extension()
6700                    .and_then(|value| value.to_str())
6701                    .unwrap_or(""),
6702            );
6703        if !route_module && !server_entry {
6704            return;
6705        }
6706        let original = program.body.take_in(self.ast.allocator);
6707        let mut output = self.ast.vec_with_capacity(original.len() + 4);
6708        for statement in original {
6709            match statement {
6710                Statement::ExportNamedDeclaration(export) if route_module => {
6711                    self.transform_named_export(export, &mut output);
6712                }
6713                Statement::ExportDefaultDeclaration(export) if server_entry => {
6714                    self.transform_default_export(export, &mut output);
6715                }
6716                statement => output.push(statement),
6717            }
6718        }
6719        program.body = output;
6720    }
6721}
6722
6723struct RequestPhaseTransformer<'a> {
6724    ast: AstBuilder<'a>,
6725    with_request_phase: String,
6726    used: bool,
6727    source_sensitive_functions: HashSet<SpanKey>,
6728}
6729
6730impl<'a> RequestPhaseTransformer<'a> {
6731    fn identifier(&self, name: &str) -> Expression<'a> {
6732        self.ast
6733            .expression_identifier(Span::default(), self.ast.ident(name))
6734    }
6735
6736    fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
6737        match callee {
6738            Expression::Identifier(identifier) => identifier.name == name,
6739            Expression::StaticMemberExpression(member) => member.property.name == name,
6740            _ => false,
6741        }
6742    }
6743
6744    fn callback_candidate(argument: &Argument<'a>) -> bool {
6745        matches!(
6746            argument,
6747            Argument::FunctionExpression(_)
6748                | Argument::ArrowFunctionExpression(_)
6749                | Argument::Identifier(_)
6750                | Argument::ComputedMemberExpression(_)
6751                | Argument::StaticMemberExpression(_)
6752                | Argument::PrivateFieldExpression(_)
6753        )
6754    }
6755
6756    fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
6757        matches!(
6758            argument,
6759            Argument::CallExpression(call)
6760                if expression_is_identifier(&call.callee, &self.with_request_phase)
6761        )
6762    }
6763
6764    fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
6765        if self.already_wrapped(argument) || !argument.is_expression() {
6766            return;
6767        }
6768        let expression = argument.to_expression_mut();
6769        let original = expression.take_in(self.ast.allocator);
6770        *expression = self.ast.expression_call(
6771            Span::default(),
6772            self.identifier(&self.with_request_phase),
6773            NONE,
6774            self.ast
6775                .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
6776                    |name| {
6777                        Argument::from(self.ast.expression_string_literal(
6778                            Span::default(),
6779                            self.ast.str(name),
6780                            None,
6781                        ))
6782                    },
6783                ))),
6784            false,
6785        );
6786        self.used = true;
6787    }
6788}
6789
6790impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
6791    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6792        if self
6793            .source_sensitive_functions
6794            .contains(&span_key(function.span))
6795        {
6796            return;
6797        }
6798        walk_mut::walk_function(self, function, flags);
6799    }
6800
6801    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6802        if self
6803            .source_sensitive_functions
6804            .contains(&span_key(function.span))
6805        {
6806            return;
6807        }
6808        walk_mut::walk_arrow_function_expression(self, function);
6809    }
6810
6811    // A with body is left uninstrumented: its object environment can
6812    // intercept probe identifiers (see `enter_with_statement`).
6813    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6814
6815    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
6816        walk_mut::walk_call_expression(self, call);
6817        let property = match &call.callee {
6818            Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
6819            _ => None,
6820        };
6821        let mut callback_index = None;
6822        if matches!(property, Some("on" | "once" | "addListener"))
6823            && matches!(
6824                call.arguments.first(),
6825                Some(Argument::StringLiteral(event))
6826                    if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
6827            )
6828        {
6829            callback_index = Some(1);
6830        } else if Self::callee_is(&call.callee, "createServer") {
6831            callback_index = call.arguments.iter().rposition(Self::callback_candidate);
6832        }
6833        let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
6834        if let Some(index) = callback_index
6835            && let Some(argument) = call.arguments.get_mut(index)
6836        {
6837            self.wrap_argument(argument, connection.then_some("connection"));
6838        }
6839    }
6840}
6841
6842struct CandidateNames<'s> {
6843    source: &'s str,
6844    /// Every identifier in the source that begins with `_`, which every
6845    /// generated name does. Asking the source whether it contains a name was
6846    /// a scan of the whole file per name, and names are allocated per
6847    /// function: the second quadratic cost in instrumenting a long file.
6848    underscored: HashSet<&'s str>,
6849    allocated: HashSet<String>,
6850    suffix: usize,
6851    /// Appended to every name, so output that shares a scope with other
6852    /// instrumented files cannot collide with theirs.
6853    namespace: String,
6854}
6855
6856impl<'s> CandidateNames<'s> {
6857    fn new(source: &'s str) -> Self {
6858        Self::within(source, "")
6859    }
6860
6861    fn within(source: &'s str, namespace: &str) -> Self {
6862        let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
6863        let underscored = source
6864            .split(|c: char| !identifier(c))
6865            .filter(|word| word.starts_with('_'))
6866            .collect();
6867        Self {
6868            source,
6869            underscored,
6870            allocated: HashSet::new(),
6871            suffix: 0,
6872            namespace: namespace.to_owned(),
6873        }
6874    }
6875
6876    fn taken(&self, candidate: &str) -> bool {
6877        self.allocated.contains(candidate)
6878            || if candidate.starts_with('_') {
6879                self.underscored.contains(candidate)
6880            } else {
6881                self.source.contains(candidate)
6882            }
6883    }
6884
6885    fn allocate(&mut self, base: &str) -> String {
6886        loop {
6887            let candidate = if self.suffix == 0 {
6888                format!("{base}{}", self.namespace)
6889            } else {
6890                format!("{base}{}{}", self.suffix, self.namespace)
6891            };
6892            self.suffix += 1;
6893            if !self.taken(&candidate) {
6894                self.allocated.insert(candidate.clone());
6895                return candidate;
6896            }
6897        }
6898    }
6899}
6900
6901struct ControlProbeV2Transformer<'a, 's> {
6902    ast: AstBuilder<'a>,
6903    decisions: &'s [CandidateDecision],
6904    mcdc_begin: String,
6905    mcdc_condition: String,
6906    mcdc_end: String,
6907    mcdc_end_v2: String,
6908    probe_file_v2: String,
6909    names: CandidateNames<'s>,
6910    scope_declarations: Vec<Vec<String>>,
6911    decision_index: usize,
6912    parameter_depth: usize,
6913    source_sensitive_functions: HashSet<SpanKey>,
6914    /// What goes around a statement whose false outcome is recorded after
6915    /// it: the statement that records it, and for a `do … while` one before
6916    /// the loop.
6917    after_statement: Option<(Option<Statement<'a>>, Statement<'a>)>,
6918    /// The statement being visited is the body of a label, which has to stay
6919    /// on it: what goes after the statement goes after the label.
6920    label_body: bool,
6921    /// The temporaries of a decision inside a parameter default, for the
6922    /// arrow that has to declare them around its conditional expression.
6923    inline_names: Option<Vec<String>>,
6924}
6925
6926#[derive(Clone, Copy)]
6927struct DecisionPlan {
6928    index: usize,
6929    condition_count: usize,
6930    inline_frame: bool,
6931}
6932
6933/// A decision whose conditions have been rewritten, and the temporaries its
6934/// outcome is recorded from.
6935struct AppliedDecision {
6936    index: usize,
6937    frame: String,
6938    values: Vec<String>,
6939    inline: bool,
6940}
6941
6942impl<'a> ControlProbeV2Transformer<'a, '_> {
6943    fn enter_declaration_scope(&mut self) {
6944        self.scope_declarations.push(Vec::new());
6945    }
6946
6947    fn leave_declaration_scope(&mut self) -> Vec<String> {
6948        self.scope_declarations
6949            .pop()
6950            .expect("program/function scope stack must remain balanced")
6951    }
6952
6953    fn allocate_scratch(&mut self, base: &str) -> String {
6954        let name = self.names.allocate(base);
6955        self.scope_declarations
6956            .last_mut()
6957            .expect("a control decision must be inside a program or function body")
6958            .push(name.clone());
6959        name
6960    }
6961
6962    fn scratch_for(&mut self, base: &str, inline: bool) -> String {
6963        if inline {
6964            self.names.allocate(base)
6965        } else {
6966            self.allocate_scratch(base)
6967        }
6968    }
6969
6970    fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
6971        let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
6972            self.ast.variable_declarator(
6973                Span::default(),
6974                VariableDeclarationKind::Let,
6975                self.ast
6976                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
6977                NONE,
6978                None,
6979                false,
6980            )
6981        }));
6982        let body = self.ast.alloc_function_body(
6983            Span::default(),
6984            self.ast.vec(),
6985            self.ast.vec_from_array([
6986                Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
6987                    Span::default(),
6988                    VariableDeclarationKind::Let,
6989                    declarations,
6990                    false,
6991                )),
6992                self.ast.statement_return(Span::default(), Some(expression)),
6993            ]),
6994        );
6995        let params = self.ast.alloc_formal_parameters(
6996            Span::default(),
6997            FormalParameterKind::ArrowFormalParameters,
6998            self.ast.vec(),
6999            NONE,
7000        );
7001        let arrow = self.ast.expression_arrow_function(
7002            Span::default(),
7003            false,
7004            false,
7005            NONE,
7006            params,
7007            NONE,
7008            body,
7009        );
7010        self.ast
7011            .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
7012    }
7013
7014    fn prepend_declarations(
7015        &self,
7016        names: Vec<String>,
7017        statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7018    ) {
7019        if names.is_empty() {
7020            return;
7021        }
7022        let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
7023            self.ast.variable_declarator(
7024                Span::default(),
7025                VariableDeclarationKind::Let,
7026                self.ast
7027                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
7028                NONE,
7029                None,
7030                false,
7031            )
7032        }));
7033        statements.insert(
7034            0,
7035            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7036                Span::default(),
7037                VariableDeclarationKind::Let,
7038                declarators,
7039                false,
7040            )),
7041        );
7042    }
7043
7044    fn identifier(&self, name: &str) -> Expression<'a> {
7045        self.ast
7046            .expression_identifier(Span::default(), self.ast.ident(name))
7047    }
7048
7049    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
7050        let target = self
7051            .ast
7052            .simple_assignment_target_assignment_target_identifier(
7053                Span::default(),
7054                self.ast.ident(name),
7055            );
7056        AssignmentTarget::from(target)
7057    }
7058
7059    fn number(&self, value: u64) -> Expression<'a> {
7060        self.ast.expression_numeric_literal(
7061            Span::default(),
7062            value as f64,
7063            None,
7064            NumberBase::Decimal,
7065        )
7066    }
7067
7068    fn string(&self, value: &str) -> Expression<'a> {
7069        self.ast
7070            .expression_string_literal(Span::default(), self.ast.str(value), None)
7071    }
7072
7073    fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
7074        self.ast.object_property_kind_object_property(
7075            Span::default(),
7076            PropertyKind::Init,
7077            self.ast
7078                .property_key_static_identifier(Span::default(), self.ast.ident(name)),
7079            value,
7080            false,
7081            false,
7082            false,
7083        )
7084    }
7085
7086    fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
7087        let conditions = self.ast.expression_array(
7088            Span::default(),
7089            self.ast.vec_from_iter(
7090                decision
7091                    .conditions
7092                    .iter()
7093                    .map(|condition| ArrayExpressionElement::from(self.string(condition))),
7094            ),
7095        );
7096        let properties = self.ast.vec_from_array([
7097            self.object_property("id", self.string(&decision.id)),
7098            self.object_property("file", self.string(&decision.file)),
7099            self.object_property("line", self.number(decision.line as u64)),
7100            self.object_property("column", self.number(decision.column as u64)),
7101            self.object_property("source", self.string(&decision.source)),
7102            self.object_property("conditions", conditions),
7103            self.object_property("kind", self.string(&decision.kind)),
7104        ]);
7105        Expression::ObjectExpression(
7106            self.ast
7107                .alloc_object_expression(Span::default(), properties),
7108        )
7109    }
7110
7111    fn assign(&self, name: &str, value: Expression<'a>) -> Expression<'a> {
7112        self.ast.expression_assignment(
7113            Span::default(),
7114            AssignmentOperator::Assign,
7115            self.assignment_target(name),
7116            value,
7117        )
7118    }
7119
7120    /// One condition of a decision, as `(_f += w, _v = C)`.
7121    ///
7122    /// The assignment comes last because TypeScript narrows through an
7123    /// assignment's right side and through a comma's last operand, and through
7124    /// nothing else here. The condition used to end in its temporary
7125    /// (`(_v = C, _f += …, _v)`), which narrows nothing: `if (client) return
7126    /// client` returned `Client | null`, and a file that Supercov never
7127    /// touched failed to compile against the widened type.
7128    ///
7129    /// The first condition carries the decision's bookkeeping, since a comma
7130    /// around the whole decision would hide its `&&` and `||` from the same
7131    /// analysis: the frame starts at this condition's weight, and the other
7132    /// conditions' temporaries are cleared, so one that is not evaluated adds
7133    /// nothing when the outcome is recorded.
7134    fn instrument_condition(
7135        &self,
7136        expression: Expression<'a>,
7137        applied: &AppliedDecision,
7138        index: usize,
7139    ) -> Expression<'a> {
7140        let mut steps = self.ast.vec();
7141        if index == 0 {
7142            steps.push(self.assign(&applied.frame, self.number(1)));
7143            for name in &applied.values[1..] {
7144                steps.push(self.assign(name, self.number(0)));
7145            }
7146        } else {
7147            steps.push(self.ast.expression_assignment(
7148                Span::default(),
7149                AssignmentOperator::Addition,
7150                self.assignment_target(&applied.frame),
7151                self.number(3_u64.pow(index as u32)),
7152            ));
7153        }
7154        steps.push(self.assign(&applied.values[index], expression));
7155        self.ast.expression_sequence(Span::default(), steps)
7156    }
7157
7158    fn instrument_conditions(
7159        &self,
7160        expression: &mut Expression<'a>,
7161        applied: &AppliedDecision,
7162        next_index: &mut usize,
7163    ) {
7164        match expression {
7165            Expression::ParenthesizedExpression(parenthesized) => {
7166                self.instrument_conditions(&mut parenthesized.expression, applied, next_index)
7167            }
7168            Expression::LogicalExpression(logical)
7169                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7170            {
7171                self.instrument_conditions(&mut logical.left, applied, next_index);
7172                self.instrument_conditions(&mut logical.right, applied, next_index);
7173            }
7174            Expression::UnaryExpression(unary)
7175                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7176            {
7177                self.instrument_conditions(&mut unary.argument, applied, next_index);
7178            }
7179            _ => {
7180                let index = *next_index;
7181                *next_index += 1;
7182                let original = expression.take_in(self.ast.allocator);
7183                *expression = self.instrument_condition(original, applied, index);
7184            }
7185        }
7186    }
7187
7188    /// The decision's vector in base 3, from its temporaries: nothing for a
7189    /// condition that was not evaluated, its weight for a false one, twice
7190    /// that for a true one. `_f` holds the weights of the conditions that
7191    /// were evaluated; `+!!_v` adds the second for each true one.
7192    fn encoded(&self, applied: &AppliedDecision) -> Expression<'a> {
7193        let mut sum = self.identifier(&applied.frame);
7194        for (index, name) in applied.values.iter().enumerate() {
7195            let truth = self.ast.expression_unary(
7196                Span::default(),
7197                UnaryOperator::UnaryPlus,
7198                self.ast.expression_unary(
7199                    Span::default(),
7200                    UnaryOperator::LogicalNot,
7201                    self.ast.expression_unary(
7202                        Span::default(),
7203                        UnaryOperator::LogicalNot,
7204                        self.identifier(name),
7205                    ),
7206                ),
7207            );
7208            let term = if index == 0 {
7209                truth
7210            } else {
7211                self.ast.expression_binary(
7212                    Span::default(),
7213                    self.number(3_u64.pow(index as u32)),
7214                    BinaryOperator::Multiplication,
7215                    truth,
7216                )
7217            };
7218            sum = self
7219                .ast
7220                .expression_binary(Span::default(), sum, BinaryOperator::Addition, term);
7221        }
7222        sum
7223    }
7224
7225    /// `mcdcEndV2(file, decision, vector, outcome)`, where the outcome is
7226    /// known from where the call stands: the branch that was taken.
7227    fn outcome(&self, applied: &AppliedDecision, outcome: bool) -> Expression<'a> {
7228        self.ast.expression_call(
7229            Span::default(),
7230            self.identifier(&self.mcdc_end_v2),
7231            NONE,
7232            self.ast.vec_from_array([
7233                Argument::from(self.identifier(&self.probe_file_v2)),
7234                Argument::from(self.number(applied.index as u64)),
7235                Argument::from(self.encoded(applied)),
7236                Argument::from(
7237                    self.ast
7238                        .expression_boolean_literal(Span::default(), outcome),
7239                ),
7240            ]),
7241            false,
7242        )
7243    }
7244
7245    /// Put `first` at the head of a branch or a loop body, making it a block
7246    /// if it is a single statement.
7247    fn prepend_statement(&self, body: &mut Statement<'a>, first: Statement<'a>) {
7248        if let Statement::BlockStatement(block) = body {
7249            block.body.insert(0, first);
7250            return;
7251        }
7252        let original = body.take_in(self.ast.allocator);
7253        *body = self
7254            .ast
7255            .statement_block(Span::default(), self.ast.vec_from_array([first, original]));
7256    }
7257
7258    /// What records the true outcome where the false one is recorded after
7259    /// the statement: `mcdcEndV2(…, true), _f = 0`. The frame is at least 1
7260    /// once a test has run, so cleared it says the outcome has been recorded.
7261    fn settled(&self, applied: &AppliedDecision) -> Expression<'a> {
7262        self.ast.expression_sequence(
7263            Span::default(),
7264            self.ast.vec_from_array([
7265                self.outcome(applied, true),
7266                self.assign(&applied.frame, self.number(0)),
7267            ]),
7268        )
7269    }
7270
7271    fn when_unsettled(&self, applied: &AppliedDecision, record: Expression<'a>) -> Statement<'a> {
7272        self.ast.statement_expression(
7273            Span::default(),
7274            self.ast.expression_logical(
7275                Span::default(),
7276                self.identifier(&applied.frame),
7277                LogicalOperator::And,
7278                record,
7279            ),
7280        )
7281    }
7282
7283    /// Record the false outcome after the statement, when nothing runs for
7284    /// it inside: an `if` without an `else`, a loop its test ended.
7285    ///
7286    /// An `else` made for the call would be a branch of its own to a coverage
7287    /// tool the tests run, and the `if` would end at a brace nobody wrote:
7288    /// istanbul lost the statement and its implicit `else` with it. A `break`
7289    /// leaves a loop from its body, where the outcome is already recorded.
7290    fn record_false_after(&mut self, applied: &AppliedDecision, before: Option<Statement<'a>>) {
7291        let after = self.when_unsettled(applied, self.outcome(applied, false));
7292        self.after_statement = Some((before, after));
7293    }
7294
7295    /// A loop's true outcome is recorded where its body starts.
7296    fn loop_outcomes(
7297        &mut self,
7298        body: &mut Statement<'a>,
7299        applied: &AppliedDecision,
7300        tested_after: bool,
7301    ) {
7302        if !tested_after {
7303            let entered = self
7304                .ast
7305                .statement_expression(Span::default(), self.settled(applied));
7306            self.prepend_statement(body, entered);
7307            self.record_false_after(applied, None);
7308            return;
7309        }
7310        // A `do … while` body runs before its test: the frame says whether a
7311        // test came before this iteration. It is cleared before the loop,
7312        // because a test that threw the last time the loop ran left it set.
7313        let entered = self.when_unsettled(applied, self.settled(applied));
7314        self.prepend_statement(body, entered);
7315        let before = self
7316            .ast
7317            .statement_expression(Span::default(), self.assign(&applied.frame, self.number(0)));
7318        self.record_false_after(applied, Some(before));
7319    }
7320
7321    fn instrument_condition_v1(
7322        &self,
7323        expression: Expression<'a>,
7324        frame_name: &str,
7325        index: usize,
7326    ) -> Expression<'a> {
7327        self.ast.expression_call(
7328            Span::default(),
7329            self.identifier(&self.mcdc_condition),
7330            NONE,
7331            self.ast.vec_from_array([
7332                Argument::from(self.identifier(frame_name)),
7333                Argument::from(self.number(index as u64)),
7334                Argument::from(expression),
7335            ]),
7336            false,
7337        )
7338    }
7339
7340    fn instrument_conditions_v1(
7341        &self,
7342        expression: &mut Expression<'a>,
7343        frame_name: &str,
7344        next_index: &mut usize,
7345    ) {
7346        match expression {
7347            Expression::ParenthesizedExpression(parenthesized) => {
7348                self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
7349            }
7350            Expression::LogicalExpression(logical)
7351                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7352            {
7353                self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
7354                self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
7355            }
7356            Expression::UnaryExpression(unary)
7357                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7358            {
7359                self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
7360            }
7361            _ => {
7362                let index = *next_index;
7363                *next_index += 1;
7364                let original = expression.take_in(self.ast.allocator);
7365                *expression = self.instrument_condition_v1(original, frame_name, index);
7366            }
7367        }
7368    }
7369
7370    fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
7371        let mut condition_spans = Vec::new();
7372        collect_conditions(test, &mut condition_spans);
7373        let plan = DecisionPlan {
7374            index: self.decision_index,
7375            condition_count: condition_spans.len(),
7376            inline_frame: self.parameter_depth > 0,
7377        };
7378        self.decision_index += 1;
7379        plan
7380    }
7381
7382    /// Rewrite a decision's conditions. What records the outcome is left to
7383    /// the caller, which knows the branches; a decision too wide for the
7384    /// encoding keeps the call-based form and returns nothing.
7385    fn apply_decision(
7386        &mut self,
7387        test: &mut Expression<'a>,
7388        plan: DecisionPlan,
7389    ) -> Option<AppliedDecision> {
7390        if plan.condition_count > 32 {
7391            let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
7392            let mut next_index = 0;
7393            self.instrument_conditions_v1(test, &frame_name, &mut next_index);
7394            debug_assert_eq!(next_index, plan.condition_count);
7395
7396            let decision = &self.decisions[plan.index];
7397            let begin = self.ast.expression_call(
7398                Span::default(),
7399                self.identifier(&self.mcdc_begin),
7400                NONE,
7401                self.ast.vec_from_array([
7402                    Argument::from(self.string(&decision.id)),
7403                    Argument::from(self.decision_meta(decision)),
7404                ]),
7405                false,
7406            );
7407            let assign_frame = self.ast.expression_assignment(
7408                Span::default(),
7409                AssignmentOperator::Assign,
7410                self.assignment_target(&frame_name),
7411                begin,
7412            );
7413            let instrumented = test.take_in(self.ast.allocator);
7414            let end = self.ast.expression_call(
7415                Span::default(),
7416                self.identifier(&self.mcdc_end),
7417                NONE,
7418                self.ast.vec_from_array([
7419                    Argument::from(self.identifier(&frame_name)),
7420                    Argument::from(instrumented),
7421                ]),
7422                false,
7423            );
7424            let observed = self.ast.expression_sequence(
7425                Span::default(),
7426                self.ast.vec_from_array([assign_frame, end]),
7427            );
7428            *test = if plan.inline_frame {
7429                self.wrap_inline_frame(observed, &[frame_name])
7430            } else {
7431                observed
7432            };
7433            return None;
7434        }
7435
7436        let applied = AppliedDecision {
7437            index: plan.index,
7438            frame: self.scratch_for("_supercovMcdcFrame", plan.inline_frame),
7439            values: (0..plan.condition_count)
7440                .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
7441                .collect(),
7442            inline: plan.inline_frame,
7443        };
7444        let mut next_index = 0;
7445        self.instrument_conditions(test, &applied, &mut next_index);
7446        debug_assert_eq!(next_index, plan.condition_count);
7447        Some(applied)
7448    }
7449}
7450
7451impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
7452    fn visit_program(&mut self, program: &mut Program<'a>) {
7453        self.enter_declaration_scope();
7454        walk_mut::walk_program(self, program);
7455        let declarations = self.leave_declaration_scope();
7456        self.prepend_declarations(declarations, &mut program.body);
7457    }
7458
7459    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
7460        self.enter_declaration_scope();
7461        walk_mut::walk_function_body(self, body);
7462        let declarations = self.leave_declaration_scope();
7463        self.prepend_declarations(declarations, &mut body.statements);
7464    }
7465
7466    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7467        if self
7468            .source_sensitive_functions
7469            .contains(&span_key(function.span))
7470        {
7471            return;
7472        }
7473        let outer_parameter_depth = self.parameter_depth;
7474        self.parameter_depth = 0;
7475        walk_mut::walk_function(self, function, flags);
7476        self.parameter_depth = outer_parameter_depth;
7477    }
7478
7479    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7480        if self
7481            .source_sensitive_functions
7482            .contains(&span_key(function.span))
7483        {
7484            return;
7485        }
7486        let outer_parameter_depth = self.parameter_depth;
7487        self.parameter_depth = 0;
7488        walk_mut::walk_arrow_function_expression(self, function);
7489        self.parameter_depth = outer_parameter_depth;
7490    }
7491
7492    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
7493        self.parameter_depth += 1;
7494        walk_mut::walk_formal_parameters(self, parameters);
7495        self.parameter_depth -= 1;
7496    }
7497
7498    // A with body is left uninstrumented: its object environment can
7499    // intercept probe identifiers (see `enter_with_statement`).
7500    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7501
7502    // A statement whose false outcome is recorded after it becomes a block
7503    // of the two, so it can stand wherever one statement can. A label stays
7504    // on its statement, where `break` and `continue` look for it, and the
7505    // block goes around the outermost label.
7506    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
7507        let label_body = self.label_body;
7508        self.label_body = matches!(statement, Statement::LabeledStatement(_));
7509        walk_mut::walk_statement(self, statement);
7510        self.label_body = false;
7511        if label_body {
7512            return;
7513        }
7514        if let Some((before, after)) = self.after_statement.take() {
7515            let decided = statement.take_in(self.ast.allocator);
7516            let mut recorded = self.ast.vec_with_capacity(3);
7517            recorded.extend(before);
7518            recorded.push(decided);
7519            recorded.push(after);
7520            *statement = self.ast.statement_block(Span::default(), recorded);
7521        }
7522    }
7523
7524    // A static block is a scope of its own for the temporaries: inside a
7525    // parameter default there is no function body between it and the
7526    // parameter list, and a statement cannot be wrapped in an arrow.
7527    fn visit_static_block(&mut self, block: &mut oxc_ast::ast::StaticBlock<'a>) {
7528        let outer_parameter_depth = self.parameter_depth;
7529        self.parameter_depth = 0;
7530        self.enter_declaration_scope();
7531        walk_mut::walk_static_block(self, block);
7532        let declarations = self.leave_declaration_scope();
7533        self.prepend_declarations(declarations, &mut block.body);
7534        self.parameter_depth = outer_parameter_depth;
7535    }
7536
7537    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
7538        walk_mut::walk_expression(self, expression);
7539        if let Some(names) = self.inline_names.take()
7540            && matches!(expression, Expression::ConditionalExpression(_))
7541        {
7542            let conditional = expression.take_in(self.ast.allocator);
7543            *expression = self.wrap_inline_frame(conditional, &names);
7544        }
7545    }
7546
7547    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
7548        let plan = decision_outcome_is_variable(&statement.test)
7549            .then(|| self.reserve_decision(&statement.test));
7550        self.visit_expression(&mut statement.test);
7551        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
7552        self.visit_statement(&mut statement.consequent);
7553        if let Some(alternate) = &mut statement.alternate {
7554            self.visit_statement(alternate);
7555        }
7556        let Some(applied) = applied else {
7557            return;
7558        };
7559        let Some(alternate) = &mut statement.alternate else {
7560            let taken = self
7561                .ast
7562                .statement_expression(Span::default(), self.settled(&applied));
7563            self.prepend_statement(&mut statement.consequent, taken);
7564            self.record_false_after(&applied, None);
7565            return;
7566        };
7567        let skipped = self
7568            .ast
7569            .statement_expression(Span::default(), self.outcome(&applied, false));
7570        self.prepend_statement(alternate, skipped);
7571        let taken = self
7572            .ast
7573            .statement_expression(Span::default(), self.outcome(&applied, true));
7574        self.prepend_statement(&mut statement.consequent, taken);
7575    }
7576
7577    fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
7578        let plan = decision_outcome_is_variable(&expression.test)
7579            .then(|| self.reserve_decision(&expression.test));
7580        self.visit_expression(&mut expression.test);
7581        let applied = plan.and_then(|plan| self.apply_decision(&mut expression.test, plan));
7582        self.visit_expression(&mut expression.consequent);
7583        self.visit_expression(&mut expression.alternate);
7584        let Some(applied) = applied else {
7585            return;
7586        };
7587        // A coverage tool the tests run reads where a branch starts from the
7588        // source map, and the printer maps neither a sequence nor a call:
7589        // the branch started at whatever token came before it. `void` is
7590        // mapped, and here it is mapped to the branch's own start.
7591        for (branch, outcome) in [
7592            (&mut expression.consequent, true),
7593            (&mut expression.alternate, false),
7594        ] {
7595            let value = branch.take_in(self.ast.allocator);
7596            let start = value.span().start;
7597            let record = self.ast.expression_unary(
7598                Span::new(start, start + 1),
7599                UnaryOperator::Void,
7600                self.outcome(&applied, outcome),
7601            );
7602            *branch = self
7603                .ast
7604                .expression_sequence(Span::default(), self.ast.vec_from_array([record, value]));
7605        }
7606        if applied.inline {
7607            let mut names = vec![applied.frame];
7608            names.extend(applied.values);
7609            self.inline_names = Some(names);
7610        }
7611    }
7612
7613    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
7614        let plan = decision_outcome_is_variable(&statement.test)
7615            .then(|| self.reserve_decision(&statement.test));
7616        self.visit_expression(&mut statement.test);
7617        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
7618        self.visit_statement(&mut statement.body);
7619        if let Some(applied) = applied {
7620            self.loop_outcomes(&mut statement.body, &applied, false);
7621        }
7622    }
7623
7624    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
7625        let plan = decision_outcome_is_variable(&statement.test)
7626            .then(|| self.reserve_decision(&statement.test));
7627        self.visit_statement(&mut statement.body);
7628        self.visit_expression(&mut statement.test);
7629        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
7630        if let Some(applied) = applied {
7631            self.loop_outcomes(&mut statement.body, &applied, true);
7632        }
7633    }
7634
7635    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
7636        let plan = statement
7637            .test
7638            .as_ref()
7639            .filter(|test| decision_outcome_is_variable(test))
7640            .map(|test| self.reserve_decision(test));
7641        if let Some(init) = &mut statement.init {
7642            self.visit_for_statement_init(init);
7643        }
7644        let mut applied = None;
7645        if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
7646            self.visit_expression(test);
7647            applied = self.apply_decision(test, plan);
7648        }
7649        if let Some(update) = &mut statement.update {
7650            self.visit_expression(update);
7651        }
7652        self.visit_statement(&mut statement.body);
7653        if let Some(applied) = applied {
7654            self.loop_outcomes(&mut statement.body, &applied, false);
7655        }
7656    }
7657}
7658
7659struct DecisionCollector<'s> {
7660    source: &'s str,
7661    file: &'s str,
7662    decisions: Vec<CandidateDecision>,
7663    decision_vector_counts: Vec<usize>,
7664    decision_logical_nodes: HashSet<SpanKey>,
7665    source_sensitive_functions: &'s HashSet<SpanKey>,
7666}
7667
7668impl DecisionCollector<'_> {
7669    fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
7670        let mut condition_spans = Vec::new();
7671        collect_conditions(test, &mut condition_spans);
7672        collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
7673        // Babel treats redundant parentheses as parser metadata rather than
7674        // part of the decision node. oxc preserves a ParenthesizedExpression,
7675        // so normalize it here to keep locations and stable IDs parser-neutral.
7676        let span = transparent_expression(test).span();
7677        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
7678        self.decisions.push(CandidateDecision {
7679            id: stable_id(self.source, self.file, "decision", span, kind),
7680            file: self.file.to_string(),
7681            line,
7682            column,
7683            source: source_slice(self.source, span).to_string(),
7684            conditions: condition_spans
7685                .iter()
7686                .map(|condition| source_slice(self.source, *condition).to_string())
7687                .collect(),
7688            kind: kind.to_string(),
7689        });
7690        self.decision_vector_counts.push(
7691            if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
7692                reachable_vector_count(test, &condition_spans)
7693            } else {
7694                0
7695            },
7696        );
7697    }
7698}
7699
7700/// MC/DC applies only when a decision can produce both Boolean outcomes.
7701/// This deliberately recognizes only semantics that are provable from syntax;
7702/// it never evaluates user code or assumes values for identifiers/calls.
7703fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
7704    syntactic_boolean_outcome(expression).is_none()
7705}
7706
7707fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
7708    match transparent_expression(expression) {
7709        Expression::BooleanLiteral(literal) => Some(literal.value),
7710        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
7711            syntactic_boolean_outcome(&unary.argument).map(|value| !value)
7712        }
7713        Expression::LogicalExpression(logical)
7714            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7715        {
7716            let left = syntactic_boolean_outcome(&logical.left);
7717            let right = syntactic_boolean_outcome(&logical.right);
7718            match logical.operator {
7719                LogicalOperator::And => match (left, right) {
7720                    (Some(false), _) | (_, Some(false)) => Some(false),
7721                    (Some(true), value) | (value, Some(true)) => value,
7722                    _ => None,
7723                },
7724                LogicalOperator::Or => match (left, right) {
7725                    (Some(true), _) | (_, Some(true)) => Some(true),
7726                    (Some(false), value) | (value, Some(false)) => value,
7727                    _ => None,
7728                },
7729                LogicalOperator::Coalesce => None,
7730            }
7731        }
7732        _ => None,
7733    }
7734}
7735
7736#[derive(Default)]
7737struct CoverageSurfaceScanner {
7738    found: bool,
7739}
7740
7741impl<'a> Visit<'a> for CoverageSurfaceScanner {
7742    fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
7743        self.found = true;
7744    }
7745
7746    fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
7747        self.found = true;
7748    }
7749
7750    fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
7751        self.found = true;
7752    }
7753
7754    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
7755        self.found = true;
7756    }
7757
7758    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
7759        self.found = true;
7760    }
7761
7762    fn visit_class(&mut self, _class: &Class<'a>) {
7763        self.found = true;
7764    }
7765
7766    fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
7767        if expression.operator.is_logical() {
7768            self.found = true;
7769        } else {
7770            walk::walk_assignment_expression(self, expression);
7771        }
7772    }
7773}
7774
7775fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
7776    fn visit_condition(expression: &Expression<'_>) -> bool {
7777        let mut scanner = CoverageSurfaceScanner::default();
7778        scanner.visit_expression(expression);
7779        !scanner.found
7780    }
7781    match transparent_expression(expression) {
7782        Expression::LogicalExpression(logical)
7783            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7784        {
7785            decision_conditions_are_transparent(&logical.left)
7786                && decision_conditions_are_transparent(&logical.right)
7787        }
7788        Expression::UnaryExpression(unary)
7789            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7790        {
7791            decision_conditions_are_transparent(&unary.argument)
7792        }
7793        condition => visit_condition(condition),
7794    }
7795}
7796
7797fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
7798    fn evaluate(
7799        expression: &Expression<'_>,
7800        assignment: usize,
7801        encoded: &mut usize,
7802        indices: &HashMap<SpanKey, usize>,
7803    ) -> bool {
7804        match transparent_expression(expression) {
7805            Expression::LogicalExpression(logical)
7806                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7807            {
7808                let left = evaluate(&logical.left, assignment, encoded, indices);
7809                if logical.operator == LogicalOperator::And {
7810                    left && evaluate(&logical.right, assignment, encoded, indices)
7811                } else {
7812                    left || evaluate(&logical.right, assignment, encoded, indices)
7813                }
7814            }
7815            Expression::UnaryExpression(unary)
7816                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7817            {
7818                !evaluate(&unary.argument, assignment, encoded, indices)
7819            }
7820            condition => {
7821                let index = indices
7822                    .get(&span_key(condition.span()))
7823                    .expect("decision condition index must remain stable");
7824                let value = assignment & (1 << index) != 0;
7825                *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
7826                value
7827            }
7828        }
7829    }
7830
7831    let indices = conditions
7832        .iter()
7833        .enumerate()
7834        .map(|(index, span)| (span_key(*span), index))
7835        .collect::<HashMap<_, _>>();
7836    let mut vectors = HashSet::new();
7837    for assignment in 0..(1_usize << conditions.len()) {
7838        let mut encoded = 0;
7839        let outcome = evaluate(expression, assignment, &mut encoded, &indices);
7840        vectors.insert(encoded * 2 + usize::from(outcome));
7841    }
7842    vectors.len()
7843}
7844
7845fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
7846    match expression {
7847        Expression::ParenthesizedExpression(parenthesized) => {
7848            collect_decision_logical_nodes(&parenthesized.expression, nodes);
7849        }
7850        Expression::LogicalExpression(logical)
7851            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7852        {
7853            nodes.insert(span_key(logical.span));
7854            collect_decision_logical_nodes(&logical.left, nodes);
7855            collect_decision_logical_nodes(&logical.right, nodes);
7856        }
7857        Expression::UnaryExpression(unary)
7858            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7859        {
7860            collect_decision_logical_nodes(&unary.argument, nodes);
7861        }
7862        _ => {}
7863    }
7864}
7865
7866#[derive(Default)]
7867struct LogicalBranchAnalysis {
7868    branches: Vec<CandidateBranch>,
7869    logical_targets: HashMap<SpanKey, (String, String)>,
7870}
7871
7872#[derive(Default)]
7873struct LogicalAssignmentAnalysis {
7874    branches: Vec<CandidateBranch>,
7875    targets: HashMap<SpanKey, (String, String)>,
7876}
7877
7878#[derive(Default)]
7879struct OptionalMemberAnalysis {
7880    branches: Vec<CandidateBranch>,
7881    targets: HashMap<SpanKey, (String, String)>,
7882    /// Each target's place in its chain, as `optionalSelectV2` reads it.
7883    links: HashMap<SpanKey, u8>,
7884    /// The optional call a target's object holds, for a target right after
7885    /// one: whether that call went ahead says whether the target ran.
7886    after_call: HashMap<SpanKey, SpanKey>,
7887}
7888
7889/// An optional link of the same chain runs before this one.
7890const LINK_AFTER_ANOTHER: u8 = 1;
7891/// An optional link of the same chain runs after this one.
7892const LINK_BEFORE_ANOTHER: u8 = 2;
7893/// The link before this one is an optional call.
7894const LINK_AFTER_CALL: u8 = 4;
7895
7896/// The optional link of the same chain an optional member's object holds.
7897enum LinkBelow {
7898    None,
7899    Member,
7900    Call(SpanKey),
7901}
7902
7903/// The earlier optional link of the same chain that `object`, the object of
7904/// an optional member, holds: through plain member accesses, plain calls and
7905/// `!`, which the chain's short-circuit skips along with the rest.
7906/// Parentheses end the chain.
7907fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
7908    let mut current = object;
7909    loop {
7910        current = match current {
7911            Expression::StaticMemberExpression(member) if member.optional => {
7912                return LinkBelow::Member;
7913            }
7914            Expression::ComputedMemberExpression(member) if member.optional => {
7915                return LinkBelow::Member;
7916            }
7917            Expression::PrivateFieldExpression(member) if member.optional => {
7918                return LinkBelow::Member;
7919            }
7920            Expression::CallExpression(call) if call.optional => {
7921                return LinkBelow::Call(span_key(call.span));
7922            }
7923            Expression::StaticMemberExpression(member) => &member.object,
7924            Expression::ComputedMemberExpression(member) => &member.object,
7925            Expression::PrivateFieldExpression(member) => &member.object,
7926            Expression::CallExpression(call) => &call.callee,
7927            Expression::TSNonNullExpression(inner) => &inner.expression,
7928            _ => return LinkBelow::None,
7929        };
7930    }
7931}
7932
7933/// The same walk outward: whether this member is the object, through the
7934/// same plain links, of a later optional member of its chain.
7935fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
7936    for ancestor in context.ancestors() {
7937        match ancestor {
7938            Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
7939            Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
7940            Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
7941            Ancestor::StaticMemberExpressionObject(_)
7942            | Ancestor::ComputedMemberExpressionObject(_)
7943            | Ancestor::PrivateFieldExpressionObject(_)
7944            | Ancestor::TSNonNullExpressionExpression(_) => {}
7945            Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
7946            _ => return false,
7947        }
7948    }
7949    false
7950}
7951
7952#[derive(Default)]
7953struct OptionalCallAnalysis {
7954    branches: Vec<CandidateBranch>,
7955    sites: HashMap<SpanKey, (String, String)>,
7956    roots: HashMap<SpanKey, Vec<SpanKey>>,
7957    limitations: Vec<CandidateLimitation>,
7958}
7959
7960#[derive(Clone)]
7961struct DefaultTarget {
7962    default_id: String,
7963    provided_id: String,
7964    inferred_name: Option<String>,
7965}
7966
7967#[derive(Default)]
7968struct DefaultAnalysis {
7969    branches: Vec<CandidateBranch>,
7970    parameter_targets: HashMap<SpanKey, DefaultTarget>,
7971    binding_targets: HashMap<SpanKey, DefaultTarget>,
7972    limitations: Vec<CandidateLimitation>,
7973}
7974
7975#[derive(Clone)]
7976struct ExtendedTarget {
7977    first_id: String,
7978    second_id: String,
7979}
7980
7981#[derive(Default)]
7982struct ExtendedAnalysis {
7983    branches: Vec<CandidateBranch>,
7984    try_targets: HashMap<SpanKey, ExtendedTarget>,
7985    loop_targets: HashMap<SpanKey, ExtendedTarget>,
7986}
7987
7988#[derive(Clone)]
7989struct SwitchTarget {
7990    case_ids: Vec<String>,
7991    no_match_id: Option<String>,
7992}
7993
7994#[derive(Default)]
7995struct SwitchAnalysis {
7996    branches: Vec<CandidateBranch>,
7997    targets: HashMap<SpanKey, SwitchTarget>,
7998}
7999
8000struct SwitchCollector<'s> {
8001    source: &'s str,
8002    file: &'s str,
8003    source_sensitive_functions: &'s HashSet<SpanKey>,
8004    unsafe_function_depth: usize,
8005    with_depth: usize,
8006    analysis: SwitchAnalysis,
8007}
8008
8009impl SwitchCollector<'_> {
8010    fn unsafe_context(&self) -> bool {
8011        self.unsafe_function_depth > 0 || self.with_depth > 0
8012    }
8013
8014    fn exit_source_function(&mut self, span: Span) {
8015        if self.source_sensitive_functions.contains(&span_key(span)) {
8016            self.unsafe_function_depth -= 1;
8017        }
8018    }
8019}
8020
8021impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
8022    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8023        if self
8024            .source_sensitive_functions
8025            .contains(&span_key(node.span))
8026        {
8027            self.unsafe_function_depth += 1;
8028        }
8029    }
8030
8031    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8032        self.exit_source_function(node.span);
8033    }
8034
8035    fn enter_arrow_function_expression(
8036        &mut self,
8037        node: &mut ArrowFunctionExpression<'a>,
8038        _context: &mut TraverseCtx<'a, ()>,
8039    ) {
8040        if self
8041            .source_sensitive_functions
8042            .contains(&span_key(node.span))
8043        {
8044            self.unsafe_function_depth += 1;
8045        }
8046    }
8047
8048    fn exit_arrow_function_expression(
8049        &mut self,
8050        node: &mut ArrowFunctionExpression<'a>,
8051        _context: &mut TraverseCtx<'a, ()>,
8052    ) {
8053        self.exit_source_function(node.span);
8054    }
8055
8056    fn enter_with_statement(
8057        &mut self,
8058        _node: &mut WithStatement<'a>,
8059        _context: &mut TraverseCtx<'a, ()>,
8060    ) {
8061        self.with_depth += 1;
8062    }
8063
8064    fn exit_with_statement(
8065        &mut self,
8066        _node: &mut WithStatement<'a>,
8067        _context: &mut TraverseCtx<'a, ()>,
8068    ) {
8069        self.with_depth -= 1;
8070    }
8071
8072    fn enter_switch_statement(
8073        &mut self,
8074        node: &mut SwitchStatement<'a>,
8075        _context: &mut TraverseCtx<'a, ()>,
8076    ) {
8077        let has_default = node.cases.iter().any(|case| case.test.is_none());
8078        if self.unsafe_context() {
8079            return;
8080        }
8081        let id = stable_id(self.source, self.file, "switch", node.span, "");
8082        let mut case_ids = Vec::with_capacity(node.cases.len());
8083        let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
8084        for (index, case) in node.cases.iter().enumerate() {
8085            let alternative_id = format!("{id}:case:{index}");
8086            let label = case.test.as_ref().map_or_else(
8087                || "default".to_string(),
8088                |test| format!("case {}", source_slice(self.source, test.span())),
8089            );
8090            case_ids.push(alternative_id.clone());
8091            alternatives.push(CandidateBranchAlternative {
8092                id: alternative_id,
8093                label,
8094            });
8095        }
8096        let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
8097        if let Some(no_match_id) = &no_match_id {
8098            alternatives.push(CandidateBranchAlternative {
8099                id: no_match_id.clone(),
8100                label: "no matching case".to_string(),
8101            });
8102        }
8103        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8104        self.analysis.branches.push(CandidateBranch {
8105            id,
8106            kind: "switch".to_string(),
8107            file: self.file.to_string(),
8108            line,
8109            column,
8110            source: source_slice(self.source, node.discriminant.span()).to_string(),
8111            alternatives,
8112        });
8113        self.analysis.targets.insert(
8114            span_key(node.span),
8115            SwitchTarget {
8116                case_ids,
8117                no_match_id,
8118            },
8119        );
8120    }
8121}
8122
8123fn collect_switch_branches<'a>(
8124    allocator: &'a Allocator,
8125    program: &mut Program<'a>,
8126    source: &str,
8127    file: &str,
8128    source_sensitive_functions: &HashSet<SpanKey>,
8129) -> SwitchAnalysis {
8130    let mut collector = SwitchCollector {
8131        source,
8132        file,
8133        source_sensitive_functions,
8134        unsafe_function_depth: 0,
8135        with_depth: 0,
8136        analysis: SwitchAnalysis::default(),
8137    };
8138    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8139    collector.analysis
8140}
8141
8142struct ExtendedCollector<'s> {
8143    source: &'s str,
8144    file: &'s str,
8145    source_sensitive_functions: &'s HashSet<SpanKey>,
8146    entered_loops: HashSet<SpanKey>,
8147    unsafe_function_depth: usize,
8148    with_depth: usize,
8149    analysis: ExtendedAnalysis,
8150}
8151
8152impl ExtendedCollector<'_> {
8153    fn unsafe_context(&self) -> bool {
8154        self.unsafe_function_depth > 0 || self.with_depth > 0
8155    }
8156
8157    fn enter_try(&mut self, node: &TryStatement<'_>) {
8158        let transformed = !self.unsafe_context() && node.handler.is_some();
8159        if !transformed {
8160            return;
8161        }
8162        let id = stable_id(self.source, self.file, "try-catch", node.span, "");
8163        let success_id = format!("{id}:success");
8164        let catch_id = format!("{id}:catch");
8165        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8166        self.analysis.branches.push(CandidateBranch {
8167            id,
8168            kind: "try-catch".to_string(),
8169            file: self.file.to_string(),
8170            line,
8171            column,
8172            source: "try / catch".to_string(),
8173            alternatives: vec![
8174                CandidateBranchAlternative {
8175                    id: success_id.clone(),
8176                    label: "try completed without catch".to_string(),
8177                },
8178                CandidateBranchAlternative {
8179                    id: catch_id.clone(),
8180                    label: "catch entered".to_string(),
8181                },
8182            ],
8183        });
8184        self.analysis.try_targets.insert(
8185            span_key(node.span),
8186            ExtendedTarget {
8187                first_id: success_id,
8188                second_id: catch_id,
8189            },
8190        );
8191    }
8192
8193    fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
8194        let transformed = !self.unsafe_context();
8195        if !transformed {
8196            return;
8197        }
8198        // One feasible outcome is not a branch. Recording it as one leaves a
8199        // permanently unreachable obligation, which is how a project ends up
8200        // below 100% with nothing to do about it.
8201        if self.entered_loops.contains(&span_key(span)) {
8202            return;
8203        }
8204        let id = stable_id(self.source, self.file, kind, span, "");
8205        let zero_id = format!("{id}:zero");
8206        let entered_id = format!("{id}:entered");
8207        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8208        self.analysis.branches.push(CandidateBranch {
8209            id,
8210            kind: kind.to_string(),
8211            file: self.file.to_string(),
8212            line,
8213            column,
8214            source: source_slice(self.source, right.span()).to_string(),
8215            alternatives: vec![
8216                CandidateBranchAlternative {
8217                    id: zero_id.clone(),
8218                    label: "zero iterations".to_string(),
8219                },
8220                CandidateBranchAlternative {
8221                    id: entered_id.clone(),
8222                    label: "one or more iterations".to_string(),
8223                },
8224            ],
8225        });
8226        self.analysis.loop_targets.insert(
8227            span_key(span),
8228            ExtendedTarget {
8229                first_id: zero_id,
8230                second_id: entered_id,
8231            },
8232        );
8233    }
8234
8235    fn exit_source_function(&mut self, span: Span) {
8236        if self.source_sensitive_functions.contains(&span_key(span)) {
8237            self.unsafe_function_depth -= 1;
8238        }
8239    }
8240}
8241
8242impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
8243    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8244        if self
8245            .source_sensitive_functions
8246            .contains(&span_key(node.span))
8247        {
8248            self.unsafe_function_depth += 1;
8249        }
8250    }
8251
8252    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8253        self.exit_source_function(node.span);
8254    }
8255
8256    fn enter_arrow_function_expression(
8257        &mut self,
8258        node: &mut ArrowFunctionExpression<'a>,
8259        _context: &mut TraverseCtx<'a, ()>,
8260    ) {
8261        if self
8262            .source_sensitive_functions
8263            .contains(&span_key(node.span))
8264        {
8265            self.unsafe_function_depth += 1;
8266        }
8267    }
8268
8269    fn exit_arrow_function_expression(
8270        &mut self,
8271        node: &mut ArrowFunctionExpression<'a>,
8272        _context: &mut TraverseCtx<'a, ()>,
8273    ) {
8274        self.exit_source_function(node.span);
8275    }
8276
8277    fn enter_with_statement(
8278        &mut self,
8279        _node: &mut WithStatement<'a>,
8280        _context: &mut TraverseCtx<'a, ()>,
8281    ) {
8282        self.with_depth += 1;
8283    }
8284
8285    fn exit_with_statement(
8286        &mut self,
8287        _node: &mut WithStatement<'a>,
8288        _context: &mut TraverseCtx<'a, ()>,
8289    ) {
8290        self.with_depth -= 1;
8291    }
8292
8293    fn enter_try_statement(
8294        &mut self,
8295        node: &mut TryStatement<'a>,
8296        _context: &mut TraverseCtx<'a, ()>,
8297    ) {
8298        self.enter_try(node);
8299    }
8300
8301    fn exit_try_statement(
8302        &mut self,
8303        _node: &mut TryStatement<'a>,
8304        _context: &mut TraverseCtx<'a, ()>,
8305    ) {
8306    }
8307
8308    fn enter_for_in_statement(
8309        &mut self,
8310        node: &mut ForInStatement<'a>,
8311        _context: &mut TraverseCtx<'a, ()>,
8312    ) {
8313        self.enter_loop(node.span, &node.right, "for-in");
8314    }
8315
8316    fn exit_for_in_statement(
8317        &mut self,
8318        _node: &mut ForInStatement<'a>,
8319        _context: &mut TraverseCtx<'a, ()>,
8320    ) {
8321    }
8322
8323    fn enter_for_of_statement(
8324        &mut self,
8325        node: &mut ForOfStatement<'a>,
8326        _context: &mut TraverseCtx<'a, ()>,
8327    ) {
8328        self.enter_loop(node.span, &node.right, "for-of");
8329    }
8330
8331    fn exit_for_of_statement(
8332        &mut self,
8333        _node: &mut ForOfStatement<'a>,
8334        _context: &mut TraverseCtx<'a, ()>,
8335    ) {
8336    }
8337}
8338
8339/// Loops that must run at least once.
8340///
8341/// A `for...of` over a non-empty array literal has no zero-iteration outcome
8342/// for a test to reach, so asking for one is asking for something impossible.
8343/// The author's only ways to satisfy it are to fake a test or delete the loop,
8344/// and a coverage tool should ask for neither.
8345///
8346/// This is decided from the syntax tree and symbol resolution alone. It stays
8347/// there deliberately: a type would answer more questions, but a type can be
8348/// asserted (`as Foo`) or optimistic (`xs[0]` is `T`, and `[][0]` is
8349/// `undefined`), and an obligation dropped on a wrong answer is a branch that
8350/// runs and is never reported again. Over-asking is recoverable; this is not.
8351fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
8352    let semantic = SemanticBuilder::new().build(program).semantic;
8353    let mut found = HashSet::new();
8354    for node in semantic.nodes() {
8355        if let AstKind::ForOfStatement(loop_node) = node.kind()
8356            && never_empty(&loop_node.right, &semantic)
8357        {
8358            found.insert(span_key(loop_node.span));
8359        }
8360    }
8361    found
8362}
8363
8364/// Whether an iterable cannot be empty: a non-empty array literal, or a
8365/// `const` bound to one that nothing else in the file can reach.
8366fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
8367    match transparent_expression(expression) {
8368        Expression::ArrayExpression(array) => has_definite_element(array),
8369        Expression::Identifier(identifier) => {
8370            let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
8371                return false;
8372            };
8373            // Nothing but this loop may reach the binding. A shrinking method
8374            // (`pop`, `splice`, `length = 0`) is an ordinary read of the
8375            // identifier, so counting references is what makes this sound --
8376            // recognising a list of mutating methods would not be.
8377            if semantic.scoping().get_resolved_references(symbol).count() != 1 {
8378                return false;
8379            }
8380            let declaration = semantic.scoping().symbol_declaration(symbol);
8381            let AstKind::VariableDeclarator(declarator) =
8382                semantic.nodes().get_node(declaration).kind()
8383            else {
8384                return false;
8385            };
8386            declarator.kind == VariableDeclarationKind::Const
8387                && declarator.init.as_ref().is_some_and(|init| {
8388                    matches!(transparent_expression(init), Expression::ArrayExpression(array)
8389                        if has_definite_element(array))
8390                })
8391        }
8392        _ => false,
8393    }
8394}
8395
8396/// An element that is certainly there. A spread may contribute nothing, so
8397/// `[...xs]` is not known to be non-empty; a hole is, since `[,]` has length 1
8398/// and yields once.
8399fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
8400    array
8401        .elements
8402        .iter()
8403        .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
8404}
8405
8406fn collect_extended_branches<'a>(
8407    allocator: &'a Allocator,
8408    program: &mut Program<'a>,
8409    source: &str,
8410    file: &str,
8411    source_sensitive_functions: &HashSet<SpanKey>,
8412) -> ExtendedAnalysis {
8413    let entered_loops = provably_entered_loops(program);
8414    let mut collector = ExtendedCollector {
8415        source,
8416        file,
8417        source_sensitive_functions,
8418        entered_loops,
8419        unsafe_function_depth: 0,
8420        with_depth: 0,
8421        analysis: ExtendedAnalysis::default(),
8422    };
8423    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8424    collector.analysis
8425}
8426
8427struct DefaultCollector<'s> {
8428    source: &'s str,
8429    file: &'s str,
8430    source_sensitive_functions: &'s HashSet<SpanKey>,
8431    unsafe_function_depth: usize,
8432    with_depth: usize,
8433    analysis: DefaultAnalysis,
8434}
8435
8436impl DefaultCollector<'_> {
8437    fn unsafe_context(&self) -> bool {
8438        self.unsafe_function_depth > 0 || self.with_depth > 0
8439    }
8440
8441    fn target(
8442        &mut self,
8443        span: Span,
8444        left: &BindingPattern<'_>,
8445        right: &Expression<'_>,
8446    ) -> DefaultTarget {
8447        let id = stable_id(self.source, self.file, "default-value", span, "");
8448        let default_id = format!("{id}:default");
8449        let provided_id = format!("{id}:provided");
8450        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8451        self.analysis.branches.push(CandidateBranch {
8452            id,
8453            kind: "default-value".to_string(),
8454            file: self.file.to_string(),
8455            line,
8456            column,
8457            source: source_slice(self.source, span).to_string(),
8458            alternatives: vec![
8459                CandidateBranchAlternative {
8460                    id: default_id.clone(),
8461                    label: "default evaluated".to_string(),
8462                },
8463                CandidateBranchAlternative {
8464                    id: provided_id.clone(),
8465                    label: "value provided".to_string(),
8466                },
8467            ],
8468        });
8469        DefaultTarget {
8470            default_id,
8471            provided_id,
8472            inferred_name: binding_identifier_name(left)
8473                .filter(|_| expression_is_anonymous_definition(right)),
8474        }
8475    }
8476
8477    fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
8478        match pattern {
8479            BindingPattern::AssignmentPattern(assignment) => {
8480                let target = self.target(assignment.span, &assignment.left, &assignment.right);
8481                if parameter {
8482                    self.analysis
8483                        .parameter_targets
8484                        .insert(span_key(assignment.span), target);
8485                } else {
8486                    self.analysis
8487                        .binding_targets
8488                        .insert(span_key(assignment.span), target);
8489                }
8490                self.collect_binding_pattern(&assignment.left, parameter);
8491            }
8492            BindingPattern::ObjectPattern(object) => {
8493                for property in &object.properties {
8494                    self.collect_binding_pattern(&property.value, parameter);
8495                }
8496                if let Some(rest) = &object.rest {
8497                    self.collect_binding_pattern(&rest.argument, parameter);
8498                }
8499            }
8500            BindingPattern::ArrayPattern(array) => {
8501                for element in array.elements.iter().flatten() {
8502                    self.collect_binding_pattern(element, parameter);
8503                }
8504                if let Some(rest) = &array.rest {
8505                    self.collect_binding_pattern(&rest.argument, parameter);
8506                }
8507            }
8508            BindingPattern::BindingIdentifier(_) => {}
8509        }
8510    }
8511
8512    fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
8513        for parameter in &parameters.items {
8514            if let Some(initializer) = &parameter.initializer {
8515                // Babel models a formal-parameter default as an AssignmentPattern whose
8516                // source range starts at the binding and ends at the initializer. oxc
8517                // keeps the initializer beside the BindingPattern and, for a TypeScript
8518                // parameter property, includes access/readonly modifiers in
8519                // FormalParameter::span. Keep the outer span as the transformation key,
8520                // but publish the same semantic default-expression range as Babel.
8521                let default_span =
8522                    Span::new(parameter.pattern.span().start, initializer.span().end);
8523                let target = self.target(default_span, &parameter.pattern, initializer);
8524                self.analysis
8525                    .parameter_targets
8526                    .insert(span_key(parameter.span), target);
8527            }
8528            self.collect_binding_pattern(&parameter.pattern, true);
8529        }
8530        if let Some(rest) = &parameters.rest {
8531            self.collect_binding_pattern(&rest.rest.argument, true);
8532        }
8533    }
8534
8535    fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
8536        match pattern {
8537            BindingPattern::AssignmentPattern(_) => true,
8538            BindingPattern::ObjectPattern(object) => {
8539                object
8540                    .properties
8541                    .iter()
8542                    .any(|property| Self::has_binding_default(&property.value))
8543                    || object
8544                        .rest
8545                        .as_ref()
8546                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
8547            }
8548            BindingPattern::ArrayPattern(array) => {
8549                array
8550                    .elements
8551                    .iter()
8552                    .flatten()
8553                    .any(Self::has_binding_default)
8554                    || array
8555                        .rest
8556                        .as_ref()
8557                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
8558            }
8559            BindingPattern::BindingIdentifier(_) => false,
8560        }
8561    }
8562
8563    fn exit_source_function(&mut self, span: Span) {
8564        if self.source_sensitive_functions.contains(&span_key(span)) {
8565            self.unsafe_function_depth -= 1;
8566        }
8567    }
8568}
8569
8570impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
8571    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8572        if self
8573            .source_sensitive_functions
8574            .contains(&span_key(node.span))
8575        {
8576            self.unsafe_function_depth += 1;
8577            return;
8578        }
8579        if !self.unsafe_context() && node.body.is_some() {
8580            self.collect_parameters(&node.params);
8581        }
8582    }
8583
8584    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8585        self.exit_source_function(node.span);
8586    }
8587
8588    fn enter_arrow_function_expression(
8589        &mut self,
8590        node: &mut ArrowFunctionExpression<'a>,
8591        _context: &mut TraverseCtx<'a, ()>,
8592    ) {
8593        if self
8594            .source_sensitive_functions
8595            .contains(&span_key(node.span))
8596        {
8597            self.unsafe_function_depth += 1;
8598            return;
8599        }
8600        if !self.unsafe_context() {
8601            self.collect_parameters(&node.params);
8602        }
8603    }
8604
8605    fn exit_arrow_function_expression(
8606        &mut self,
8607        node: &mut ArrowFunctionExpression<'a>,
8608        _context: &mut TraverseCtx<'a, ()>,
8609    ) {
8610        self.exit_source_function(node.span);
8611    }
8612
8613    fn enter_with_statement(
8614        &mut self,
8615        _node: &mut WithStatement<'a>,
8616        _context: &mut TraverseCtx<'a, ()>,
8617    ) {
8618        self.with_depth += 1;
8619    }
8620
8621    fn exit_with_statement(
8622        &mut self,
8623        _node: &mut WithStatement<'a>,
8624        _context: &mut TraverseCtx<'a, ()>,
8625    ) {
8626        self.with_depth -= 1;
8627    }
8628
8629    fn enter_variable_declaration(
8630        &mut self,
8631        node: &mut VariableDeclaration<'a>,
8632        context: &mut TraverseCtx<'a, ()>,
8633    ) {
8634        if self.unsafe_context() {
8635            return;
8636        }
8637        let has_default = node
8638            .declarations
8639            .iter()
8640            .any(|declaration| Self::has_binding_default(&declaration.id));
8641        if !has_default {
8642            return;
8643        }
8644        if matches!(
8645            context.ancestors().next(),
8646            Some(Ancestor::ForStatementInit(_))
8647        ) {
8648            let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8649            self.analysis.limitations.push(CandidateLimitation {
8650                id: stable_id(
8651                    self.source,
8652                    self.file,
8653                    "dynamic-code",
8654                    node.span,
8655                    "for-init-default",
8656                ),
8657                kind: "dynamic-code".to_string(),
8658                file: self.file.to_string(),
8659                line,
8660                column,
8661                source: source_slice(self.source, node.span).to_string(),
8662                reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
8663            });
8664            return;
8665        }
8666        for declaration in &node.declarations {
8667            self.collect_binding_pattern(&declaration.id, false);
8668        }
8669    }
8670}
8671
8672fn collect_default_branches<'a>(
8673    allocator: &'a Allocator,
8674    program: &mut Program<'a>,
8675    source: &str,
8676    file: &str,
8677    source_sensitive_functions: &HashSet<SpanKey>,
8678) -> DefaultAnalysis {
8679    let mut collector = DefaultCollector {
8680        source,
8681        file,
8682        source_sensitive_functions,
8683        unsafe_function_depth: 0,
8684        with_depth: 0,
8685        analysis: DefaultAnalysis::default(),
8686    };
8687    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8688    collector.analysis
8689}
8690
8691struct OptionalCallCollector<'s> {
8692    source: &'s str,
8693    file: &'s str,
8694    source_sensitive_functions: &'s HashSet<SpanKey>,
8695    unsafe_function_depth: usize,
8696    with_depth: usize,
8697    chain_roots: Vec<SpanKey>,
8698    analysis: OptionalCallAnalysis,
8699}
8700
8701impl OptionalCallCollector<'_> {
8702    fn unsafe_context(&self) -> bool {
8703        self.unsafe_function_depth > 0 || self.with_depth > 0
8704    }
8705
8706    fn exit_source_function(&mut self, span: Span) {
8707        if self.source_sensitive_functions.contains(&span_key(span)) {
8708            self.unsafe_function_depth -= 1;
8709        }
8710    }
8711}
8712
8713impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
8714    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8715        if self
8716            .source_sensitive_functions
8717            .contains(&span_key(node.span))
8718        {
8719            self.unsafe_function_depth += 1;
8720        }
8721    }
8722
8723    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8724        self.exit_source_function(node.span);
8725    }
8726
8727    fn enter_arrow_function_expression(
8728        &mut self,
8729        node: &mut ArrowFunctionExpression<'a>,
8730        _context: &mut TraverseCtx<'a, ()>,
8731    ) {
8732        if self
8733            .source_sensitive_functions
8734            .contains(&span_key(node.span))
8735        {
8736            self.unsafe_function_depth += 1;
8737        }
8738    }
8739
8740    fn exit_arrow_function_expression(
8741        &mut self,
8742        node: &mut ArrowFunctionExpression<'a>,
8743        _context: &mut TraverseCtx<'a, ()>,
8744    ) {
8745        self.exit_source_function(node.span);
8746    }
8747
8748    fn enter_with_statement(
8749        &mut self,
8750        _node: &mut WithStatement<'a>,
8751        _context: &mut TraverseCtx<'a, ()>,
8752    ) {
8753        self.with_depth += 1;
8754    }
8755
8756    fn exit_with_statement(
8757        &mut self,
8758        _node: &mut WithStatement<'a>,
8759        _context: &mut TraverseCtx<'a, ()>,
8760    ) {
8761        self.with_depth -= 1;
8762    }
8763
8764    fn enter_chain_expression(
8765        &mut self,
8766        node: &mut ChainExpression<'a>,
8767        context: &mut TraverseCtx<'a, ()>,
8768    ) {
8769        let root = match context.ancestors().next() {
8770            Some(Ancestor::UnaryExpressionArgument(parent))
8771                if *parent.operator() == UnaryOperator::Delete =>
8772            {
8773                span_key(*parent.span())
8774            }
8775            _ => span_key(node.span),
8776        };
8777        self.chain_roots.push(root);
8778    }
8779
8780    fn exit_chain_expression(
8781        &mut self,
8782        _node: &mut ChainExpression<'a>,
8783        _context: &mut TraverseCtx<'a, ()>,
8784    ) {
8785        self.chain_roots.pop();
8786    }
8787
8788    fn enter_call_expression(
8789        &mut self,
8790        node: &mut CallExpression<'a>,
8791        _context: &mut TraverseCtx<'a, ()>,
8792    ) {
8793        if !node.optional || self.unsafe_context() {
8794            return;
8795        }
8796        let root = *self
8797            .chain_roots
8798            .last()
8799            .expect("an optional call must be enclosed by a chain expression");
8800        let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
8801        let short_id = format!("{id}:short");
8802        let continued_id = format!("{id}:continued");
8803        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8804        self.analysis.sites.insert(
8805            span_key(node.span),
8806            (short_id.clone(), continued_id.clone()),
8807        );
8808        self.analysis
8809            .roots
8810            .entry(root)
8811            .or_default()
8812            .push(span_key(node.span));
8813        self.analysis.branches.push(CandidateBranch {
8814            id,
8815            kind: "optional-chain".to_string(),
8816            file: self.file.to_string(),
8817            line,
8818            column,
8819            source: source_slice(self.source, node.span).to_string(),
8820            alternatives: vec![
8821                CandidateBranchAlternative {
8822                    id: short_id,
8823                    label: "nullish / short-circuited".to_string(),
8824                },
8825                CandidateBranchAlternative {
8826                    id: continued_id,
8827                    label: "non-nullish / continued".to_string(),
8828                },
8829            ],
8830        });
8831    }
8832}
8833
8834fn collect_optional_call_branches<'a>(
8835    allocator: &'a Allocator,
8836    program: &mut Program<'a>,
8837    source: &str,
8838    file: &str,
8839    source_sensitive_functions: &HashSet<SpanKey>,
8840) -> OptionalCallAnalysis {
8841    let mut collector = OptionalCallCollector {
8842        source,
8843        file,
8844        source_sensitive_functions,
8845        unsafe_function_depth: 0,
8846        with_depth: 0,
8847        chain_roots: Vec::new(),
8848        analysis: OptionalCallAnalysis::default(),
8849    };
8850    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8851    collector.analysis
8852}
8853
8854struct OptionalMemberCollector<'s> {
8855    source: &'s str,
8856    file: &'s str,
8857    source_sensitive_functions: &'s HashSet<SpanKey>,
8858    unsafe_function_depth: usize,
8859    with_depth: usize,
8860    analysis: OptionalMemberAnalysis,
8861}
8862
8863impl OptionalMemberCollector<'_> {
8864    fn record<State>(
8865        &mut self,
8866        span: Span,
8867        optional: bool,
8868        object: &Expression<'_>,
8869        context: &TraverseCtx<'_, State>,
8870    ) {
8871        if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
8872            return;
8873        }
8874        let below = match optional_link_below(object) {
8875            LinkBelow::Member => LINK_AFTER_ANOTHER,
8876            LinkBelow::Call(call) => {
8877                self.analysis.after_call.insert(span_key(span), call);
8878                LINK_AFTER_CALL
8879            }
8880            LinkBelow::None => 0,
8881        };
8882        let links = below
8883            | if optional_link_above(context) {
8884                LINK_BEFORE_ANOTHER
8885            } else {
8886                0
8887            };
8888        if links != 0 {
8889            self.analysis.links.insert(span_key(span), links);
8890        }
8891        let id = stable_id(self.source, self.file, "optional-chain", span, "");
8892        let short_id = format!("{id}:short");
8893        let continued_id = format!("{id}:continued");
8894        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8895        self.analysis
8896            .targets
8897            .insert(span_key(span), (short_id.clone(), continued_id.clone()));
8898        self.analysis.branches.push(CandidateBranch {
8899            id,
8900            kind: "optional-chain".to_string(),
8901            file: self.file.to_string(),
8902            line,
8903            column,
8904            source: source_slice(self.source, span).to_string(),
8905            alternatives: vec![
8906                CandidateBranchAlternative {
8907                    id: short_id,
8908                    label: "nullish / short-circuited".to_string(),
8909                },
8910                CandidateBranchAlternative {
8911                    id: continued_id,
8912                    label: "non-nullish / continued".to_string(),
8913                },
8914            ],
8915        });
8916    }
8917
8918    fn exit_source_function(&mut self, span: Span) {
8919        if self.source_sensitive_functions.contains(&span_key(span)) {
8920            self.unsafe_function_depth -= 1;
8921        }
8922    }
8923}
8924
8925impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
8926    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8927        if self
8928            .source_sensitive_functions
8929            .contains(&span_key(node.span))
8930        {
8931            self.unsafe_function_depth += 1;
8932        }
8933    }
8934
8935    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8936        self.exit_source_function(node.span);
8937    }
8938
8939    fn enter_arrow_function_expression(
8940        &mut self,
8941        node: &mut ArrowFunctionExpression<'a>,
8942        _context: &mut TraverseCtx<'a, ()>,
8943    ) {
8944        if self
8945            .source_sensitive_functions
8946            .contains(&span_key(node.span))
8947        {
8948            self.unsafe_function_depth += 1;
8949        }
8950    }
8951
8952    fn exit_arrow_function_expression(
8953        &mut self,
8954        node: &mut ArrowFunctionExpression<'a>,
8955        _context: &mut TraverseCtx<'a, ()>,
8956    ) {
8957        self.exit_source_function(node.span);
8958    }
8959
8960    fn enter_with_statement(
8961        &mut self,
8962        _node: &mut WithStatement<'a>,
8963        _context: &mut TraverseCtx<'a, ()>,
8964    ) {
8965        self.with_depth += 1;
8966    }
8967
8968    fn exit_with_statement(
8969        &mut self,
8970        _node: &mut WithStatement<'a>,
8971        _context: &mut TraverseCtx<'a, ()>,
8972    ) {
8973        self.with_depth -= 1;
8974    }
8975
8976    fn enter_computed_member_expression(
8977        &mut self,
8978        node: &mut ComputedMemberExpression<'a>,
8979        context: &mut TraverseCtx<'a, ()>,
8980    ) {
8981        self.record(node.span, node.optional, &node.object, context);
8982    }
8983
8984    fn enter_static_member_expression(
8985        &mut self,
8986        node: &mut StaticMemberExpression<'a>,
8987        context: &mut TraverseCtx<'a, ()>,
8988    ) {
8989        self.record(node.span, node.optional, &node.object, context);
8990    }
8991
8992    fn enter_private_field_expression(
8993        &mut self,
8994        node: &mut PrivateFieldExpression<'a>,
8995        context: &mut TraverseCtx<'a, ()>,
8996    ) {
8997        self.record(node.span, node.optional, &node.object, context);
8998    }
8999}
9000
9001fn collect_optional_member_branches<'a>(
9002    allocator: &'a Allocator,
9003    program: &mut Program<'a>,
9004    source: &str,
9005    file: &str,
9006    source_sensitive_functions: &HashSet<SpanKey>,
9007) -> OptionalMemberAnalysis {
9008    let mut collector = OptionalMemberCollector {
9009        source,
9010        file,
9011        source_sensitive_functions,
9012        unsafe_function_depth: 0,
9013        with_depth: 0,
9014        analysis: OptionalMemberAnalysis::default(),
9015    };
9016    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9017    collector.analysis
9018}
9019
9020struct LogicalAssignmentCollector<'s> {
9021    source: &'s str,
9022    file: &'s str,
9023    source_sensitive_functions: &'s HashSet<SpanKey>,
9024    unsafe_function_depth: usize,
9025    with_depth: usize,
9026    analysis: LogicalAssignmentAnalysis,
9027}
9028
9029impl LogicalAssignmentCollector<'_> {
9030    fn exit_source_function(&mut self, span: Span) {
9031        if self.source_sensitive_functions.contains(&span_key(span)) {
9032            self.unsafe_function_depth -= 1;
9033        }
9034    }
9035}
9036
9037impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
9038    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9039        if self
9040            .source_sensitive_functions
9041            .contains(&span_key(node.span))
9042        {
9043            self.unsafe_function_depth += 1;
9044        }
9045    }
9046
9047    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9048        self.exit_source_function(node.span);
9049    }
9050
9051    fn enter_arrow_function_expression(
9052        &mut self,
9053        node: &mut ArrowFunctionExpression<'a>,
9054        _context: &mut TraverseCtx<'a, ()>,
9055    ) {
9056        if self
9057            .source_sensitive_functions
9058            .contains(&span_key(node.span))
9059        {
9060            self.unsafe_function_depth += 1;
9061        }
9062    }
9063
9064    fn exit_arrow_function_expression(
9065        &mut self,
9066        node: &mut ArrowFunctionExpression<'a>,
9067        _context: &mut TraverseCtx<'a, ()>,
9068    ) {
9069        self.exit_source_function(node.span);
9070    }
9071
9072    fn enter_with_statement(
9073        &mut self,
9074        _node: &mut WithStatement<'a>,
9075        _context: &mut TraverseCtx<'a, ()>,
9076    ) {
9077        self.with_depth += 1;
9078    }
9079
9080    fn exit_with_statement(
9081        &mut self,
9082        _node: &mut WithStatement<'a>,
9083        _context: &mut TraverseCtx<'a, ()>,
9084    ) {
9085        self.with_depth -= 1;
9086    }
9087
9088    fn exit_assignment_expression(
9089        &mut self,
9090        node: &mut AssignmentExpression<'a>,
9091        _context: &mut TraverseCtx<'a, ()>,
9092    ) {
9093        if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
9094            return;
9095        }
9096        let operator = match node.operator {
9097            AssignmentOperator::LogicalAnd => "&&=",
9098            AssignmentOperator::LogicalOr => "||=",
9099            AssignmentOperator::LogicalNullish => "??=",
9100            _ => unreachable!("logical assignment filter must be exhaustive"),
9101        };
9102        let id = stable_id(
9103            self.source,
9104            self.file,
9105            "logical-assignment",
9106            node.span,
9107            operator,
9108        );
9109        let short_id = format!("{id}:short");
9110        let right_id = format!("{id}:right");
9111        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9112        self.analysis
9113            .targets
9114            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9115        self.analysis.branches.push(CandidateBranch {
9116            id,
9117            kind: "logical-assignment".to_string(),
9118            file: self.file.to_string(),
9119            line,
9120            column,
9121            source: source_slice(self.source, node.span).to_string(),
9122            alternatives: vec![
9123                CandidateBranchAlternative {
9124                    id: short_id,
9125                    label: "assignment skipped".to_string(),
9126                },
9127                CandidateBranchAlternative {
9128                    id: right_id,
9129                    label: "right evaluated / assigned".to_string(),
9130                },
9131            ],
9132        });
9133    }
9134}
9135
9136fn collect_logical_assignment_branches<'a>(
9137    allocator: &'a Allocator,
9138    program: &mut Program<'a>,
9139    source: &str,
9140    file: &str,
9141    source_sensitive_functions: &HashSet<SpanKey>,
9142) -> LogicalAssignmentAnalysis {
9143    let mut collector = LogicalAssignmentCollector {
9144        source,
9145        file,
9146        source_sensitive_functions,
9147        unsafe_function_depth: 0,
9148        with_depth: 0,
9149        analysis: LogicalAssignmentAnalysis::default(),
9150    };
9151    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9152    collector.analysis
9153}
9154
9155struct LogicalBranchCollector<'s> {
9156    source: &'s str,
9157    file: &'s str,
9158    decision_logical_nodes: &'s HashSet<SpanKey>,
9159    source_sensitive_functions: &'s HashSet<SpanKey>,
9160    unsafe_function_depth: usize,
9161    with_depth: usize,
9162    analysis: LogicalBranchAnalysis,
9163}
9164
9165impl LogicalBranchCollector<'_> {
9166    fn exit_source_function(&mut self, span: Span) {
9167        if self.source_sensitive_functions.contains(&span_key(span)) {
9168            self.unsafe_function_depth -= 1;
9169        }
9170    }
9171}
9172
9173impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
9174    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9175        if self
9176            .source_sensitive_functions
9177            .contains(&span_key(node.span))
9178        {
9179            self.unsafe_function_depth += 1;
9180        }
9181    }
9182
9183    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9184        self.exit_source_function(node.span);
9185    }
9186
9187    fn enter_arrow_function_expression(
9188        &mut self,
9189        node: &mut ArrowFunctionExpression<'a>,
9190        _context: &mut TraverseCtx<'a, ()>,
9191    ) {
9192        if self
9193            .source_sensitive_functions
9194            .contains(&span_key(node.span))
9195        {
9196            self.unsafe_function_depth += 1;
9197        }
9198    }
9199
9200    fn exit_arrow_function_expression(
9201        &mut self,
9202        node: &mut ArrowFunctionExpression<'a>,
9203        _context: &mut TraverseCtx<'a, ()>,
9204    ) {
9205        self.exit_source_function(node.span);
9206    }
9207
9208    fn enter_with_statement(
9209        &mut self,
9210        _node: &mut WithStatement<'a>,
9211        _context: &mut TraverseCtx<'a, ()>,
9212    ) {
9213        self.with_depth += 1;
9214    }
9215
9216    fn exit_with_statement(
9217        &mut self,
9218        _node: &mut WithStatement<'a>,
9219        _context: &mut TraverseCtx<'a, ()>,
9220    ) {
9221        self.with_depth -= 1;
9222    }
9223
9224    fn exit_logical_expression(
9225        &mut self,
9226        node: &mut LogicalExpression<'a>,
9227        _context: &mut TraverseCtx<'a, ()>,
9228    ) {
9229        if self.unsafe_function_depth > 0
9230            || self.with_depth > 0
9231            || self.decision_logical_nodes.contains(&span_key(node.span))
9232        {
9233            return;
9234        }
9235        let operator = match node.operator {
9236            LogicalOperator::And => "&&",
9237            LogicalOperator::Or => "||",
9238            LogicalOperator::Coalesce => "??",
9239        };
9240        let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
9241        let short_id = format!("{id}:short");
9242        let right_id = format!("{id}:right");
9243        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9244        self.analysis
9245            .logical_targets
9246            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9247        self.analysis.branches.push(CandidateBranch {
9248            id,
9249            kind: "logical-value".to_string(),
9250            file: self.file.to_string(),
9251            line,
9252            column,
9253            source: source_slice(self.source, node.span).to_string(),
9254            alternatives: vec![
9255                CandidateBranchAlternative {
9256                    id: short_id,
9257                    label: "short-circuit / left selected".to_string(),
9258                },
9259                CandidateBranchAlternative {
9260                    id: right_id,
9261                    label: "right evaluated / selected".to_string(),
9262                },
9263            ],
9264        });
9265    }
9266}
9267
9268fn collect_logical_value_branches<'a>(
9269    allocator: &'a Allocator,
9270    program: &mut Program<'a>,
9271    source: &str,
9272    file: &str,
9273    decision_logical_nodes: &HashSet<SpanKey>,
9274    source_sensitive_functions: &HashSet<SpanKey>,
9275) -> LogicalBranchAnalysis {
9276    let mut collector = LogicalBranchCollector {
9277        source,
9278        file,
9279        decision_logical_nodes,
9280        source_sensitive_functions,
9281        unsafe_function_depth: 0,
9282        with_depth: 0,
9283        analysis: LogicalBranchAnalysis::default(),
9284    };
9285    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9286    collector.analysis
9287}
9288
9289impl<'a> Visit<'a> for DecisionCollector<'_> {
9290    fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
9291        if self
9292            .source_sensitive_functions
9293            .contains(&span_key(function.span))
9294        {
9295            return;
9296        }
9297        walk::walk_function(self, function, flags);
9298    }
9299
9300    fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
9301        if self
9302            .source_sensitive_functions
9303            .contains(&span_key(function.span))
9304        {
9305            return;
9306        }
9307        walk::walk_arrow_function_expression(self, function);
9308    }
9309
9310    // A with body is left uninstrumented: its object environment can
9311    // intercept probe identifiers (see `enter_with_statement`).
9312    fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
9313
9314    fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
9315        if decision_outcome_is_variable(&statement.test) {
9316            self.record_decision(&statement.test, "if");
9317        }
9318        self.visit_expression(&statement.test);
9319        self.visit_statement(&statement.consequent);
9320        if let Some(alternate) = &statement.alternate {
9321            self.visit_statement(alternate);
9322        }
9323    }
9324
9325    fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
9326        if decision_outcome_is_variable(&expression.test) {
9327            self.record_decision(&expression.test, "ternary");
9328        }
9329        self.visit_expression(&expression.test);
9330        self.visit_expression(&expression.consequent);
9331        self.visit_expression(&expression.alternate);
9332    }
9333
9334    fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
9335        if decision_outcome_is_variable(&statement.test) {
9336            self.record_decision(&statement.test, "while");
9337        }
9338        self.visit_expression(&statement.test);
9339        self.visit_statement(&statement.body);
9340    }
9341
9342    fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
9343        if decision_outcome_is_variable(&statement.test) {
9344            self.record_decision(&statement.test, "do-while");
9345        }
9346        self.visit_statement(&statement.body);
9347        self.visit_expression(&statement.test);
9348    }
9349
9350    fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
9351        if let Some(test) = &statement.test
9352            && decision_outcome_is_variable(test)
9353        {
9354            self.record_decision(test, "for");
9355        }
9356        if let Some(init) = &statement.init {
9357            self.visit_for_statement_init(init);
9358        }
9359        if let Some(test) = &statement.test {
9360            self.visit_expression(test);
9361        }
9362        if let Some(update) = &statement.update {
9363            self.visit_expression(update);
9364        }
9365        self.visit_statement(&statement.body);
9366    }
9367}
9368
9369fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
9370    match expression {
9371        Expression::ParenthesizedExpression(parenthesized) => {
9372            transparent_expression(&parenthesized.expression)
9373        }
9374        _ => expression,
9375    }
9376}
9377
9378fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
9379    match expression {
9380        Expression::ParenthesizedExpression(parenthesized) => {
9381            has_compound_boolean_decision(&parenthesized.expression)
9382        }
9383        Expression::LogicalExpression(logical) => {
9384            matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9385        }
9386        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
9387            has_compound_boolean_decision(&unary.argument)
9388        }
9389        _ => false,
9390    }
9391}
9392
9393fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
9394    match expression {
9395        Expression::ParenthesizedExpression(parenthesized) => {
9396            collect_conditions(&parenthesized.expression, conditions);
9397        }
9398        Expression::LogicalExpression(logical)
9399            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
9400        {
9401            collect_conditions(&logical.left, conditions);
9402            collect_conditions(&logical.right, conditions);
9403        }
9404        Expression::UnaryExpression(unary)
9405            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
9406        {
9407            collect_conditions(&unary.argument, conditions);
9408        }
9409        _ => conditions.push(expression.span()),
9410    }
9411}
9412
9413fn source_slice(source: &str, span: Span) -> &str {
9414    &source[span.start as usize..span.end as usize]
9415}
9416
9417/// Where every line of one source starts, in bytes and in UTF-16 units, so a
9418/// position is a binary search rather than a scan from the top of the file.
9419///
9420/// Every probe, decision and branch records its line, column and a UTF-16
9421/// offset, and each was counted from the first byte of the file: the work
9422/// grew with the square of the file's length. An 8,000-line file took 97
9423/// seconds to instrument on a debug build, a 24,000-line one did not finish
9424/// in ten minutes, and Actual's setup spent minutes before its first test.
9425struct SourcePositions {
9426    address: usize,
9427    length: usize,
9428    sample: u64,
9429    line_starts: Vec<usize>,
9430    utf16_before_line: Vec<usize>,
9431    /// Per line: `None` when ASCII, otherwise `(byte offset, utf16 column)`
9432    /// checkpoints from the line's start, so a long line that is not ASCII
9433    /// -- a table of currency symbols, say -- is not walked per position.
9434    checkpoints: Vec<Option<Vec<(usize, usize)>>>,
9435}
9436
9437impl SourcePositions {
9438    fn sample(source: &str) -> u64 {
9439        let bytes = source.as_bytes();
9440        let head = &bytes[..bytes.len().min(64)];
9441        let tail = &bytes[bytes.len().saturating_sub(64)..];
9442        head.iter()
9443            .chain(tail)
9444            .fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
9445                (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
9446            })
9447    }
9448
9449    fn new(source: &str) -> Self {
9450        let mut line_starts = vec![0];
9451        let mut utf16_before_line = vec![0];
9452        let mut checkpoints = Vec::new();
9453        let mut marks: Option<Vec<(usize, usize)>> = None;
9454        let mut utf16 = 0;
9455        let mut column = 0;
9456        let mut line_start = 0;
9457        for (index, character) in source.char_indices() {
9458            if !character.is_ascii() && marks.is_none() {
9459                marks = Some(vec![(0, 0)]);
9460            }
9461            if let Some(marks) = &mut marks
9462                && column >= marks.last().expect("a first mark").1 + CHECKPOINT
9463            {
9464                marks.push((index - line_start, column));
9465            }
9466            utf16 += character.len_utf16();
9467            column += character.len_utf16();
9468            if character == '\n' {
9469                line_start = index + 1;
9470                line_starts.push(line_start);
9471                utf16_before_line.push(utf16);
9472                checkpoints.push(marks.take());
9473                column = 0;
9474            }
9475        }
9476        checkpoints.push(marks);
9477        Self {
9478            address: source.as_ptr() as usize,
9479            length: source.len(),
9480            sample: Self::sample(source),
9481            line_starts,
9482            utf16_before_line,
9483            checkpoints,
9484        }
9485    }
9486
9487    fn describes(&self, source: &str) -> bool {
9488        self.address == source.as_ptr() as usize
9489            && self.length == source.len()
9490            && self.sample == Self::sample(source)
9491    }
9492
9493    /// Zero-based line, and the UTF-16 units from that line's start.
9494    fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
9495        let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
9496        let start = self.line_starts[line];
9497        let column = match &self.checkpoints[line] {
9498            None => offset - start,
9499            Some(marks) => {
9500                let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
9501                let (byte, column) = marks[at];
9502                column + source[start + byte..offset].encode_utf16().count()
9503            }
9504        };
9505        (line, column)
9506    }
9507}
9508
9509thread_local! {
9510    static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
9511        const { std::cell::RefCell::new(None) };
9512}
9513
9514/// While one of these is alive, positions in any source are computed from an
9515/// index built once for it. It lives for one instrumentation call, during
9516/// which the source it indexes is borrowed and cannot move; outside one,
9517/// positions are counted from the top as before.
9518pub(crate) struct PositionScope {
9519    owner: bool,
9520}
9521
9522impl PositionScope {
9523    pub(crate) fn enter() -> Self {
9524        let owner = POSITIONS.with(|positions| {
9525            let mut positions = positions.borrow_mut();
9526            let owner = positions.is_none();
9527            if owner {
9528                *positions = Some(Vec::new());
9529            }
9530            owner
9531        });
9532        Self { owner }
9533    }
9534}
9535
9536impl Drop for PositionScope {
9537    fn drop(&mut self) {
9538        if self.owner {
9539            POSITIONS.with(|positions| *positions.borrow_mut() = None);
9540        }
9541    }
9542}
9543
9544fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
9545    POSITIONS.with(|positions| {
9546        let mut positions = positions.borrow_mut();
9547        let known = positions.as_mut()?;
9548        if let Some(found) = known.iter().find(|index| index.describes(source)) {
9549            return Some(found.clone());
9550        }
9551        let built = std::rc::Rc::new(SourcePositions::new(source));
9552        known.push(built.clone());
9553        Some(built)
9554    })
9555}
9556
9557fn utf16_offset(source: &str, offset: usize) -> usize {
9558    match indexed(source) {
9559        Some(index) => {
9560            let (line, column) = index.locate(source, offset);
9561            index.utf16_before_line[line] + column
9562        }
9563        None => source[..offset].encode_utf16().count(),
9564    }
9565}
9566
9567pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
9568    if let Some(index) = indexed(source) {
9569        let (line, column) = index.locate(source, offset);
9570        return (line + 1, column + 1);
9571    }
9572    let prefix = &source[..offset];
9573    let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
9574    let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
9575    let column = source[line_start..offset].encode_utf16().count() + 1;
9576    (line, column)
9577}
9578
9579fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
9580    let start = utf16_offset(source, span.start as usize);
9581    let end = utf16_offset(source, span.end as usize);
9582    let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
9583    let mut id = String::with_capacity(16);
9584    for byte in &digest[..8] {
9585        write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
9586    }
9587    id
9588}
9589
9590#[cfg(test)]
9591mod tests {
9592    use super::*;
9593
9594    #[test]
9595    fn line_index_lookups_agree_with_walking_the_line() {
9596        let long = "é🚀x".repeat(700);
9597        let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
9598        let index = Utf16LineIndex::new(&source);
9599        for (line, text) in source.split_inclusive('\n').enumerate() {
9600            let start = index.starts[line];
9601            let width: usize = text.chars().map(char::len_utf16).sum();
9602            for column in 0..=width + 2 {
9603                assert_eq!(
9604                    index.byte_offset(line, column),
9605                    start + utf16_col_to_byte(text, column),
9606                    "line {line} column {column}"
9607                );
9608            }
9609            for (offset, _) in text.char_indices() {
9610                let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
9611                assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
9612            }
9613        }
9614    }
9615
9616    #[test]
9617    fn indexed_positions_agree_with_counting_from_the_top() {
9618        // The long line passes several checkpoints.
9619        let source = &format!(
9620            "const s = 'naïve 🚀';\n\nexport function f(a) {{\n  return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
9621            "é🚀x".repeat(400)
9622        );
9623        let source = source.as_str();
9624        let counted: Vec<_> = source
9625            .char_indices()
9626            .map(|(offset, _)| {
9627                (
9628                    line_and_utf16_column(source, offset),
9629                    stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
9630                )
9631            })
9632            .collect();
9633        let _positions = PositionScope::enter();
9634        for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
9635            assert_eq!(
9636                line_and_utf16_column(source, offset),
9637                position,
9638                "offset {offset}"
9639            );
9640            assert_eq!(
9641                stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
9642                id
9643            );
9644        }
9645    }
9646
9647    #[test]
9648    fn classic_scripts_share_application_globals_but_not_probe_bindings() {
9649        // TodoMVC's ES5 app loads several classic scripts into one document.
9650        // They share the global lexical scope, so a second
9651        // `const __supercovMcdcBegin` was a SyntaxError and no item was added.
9652        let a = instrument_direct_candidate(
9653            "var count = 0; function increment() { if (count >= 0) { count += 1; } }",
9654            "public/a.js",
9655        )
9656        .unwrap();
9657        let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
9658            .unwrap();
9659        let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
9660        let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
9661        let script = format!(
9662            "const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
9663             vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
9664             if (ctx.observed !== 1) throw Error('shared globals changed');\
9665             vm.runInContext({}, ctx);\
9666             if (ctx.observed !== 2) throw Error('a repeated script changed');",
9667            serde_json::to_string(runtime).unwrap(),
9668            serde_json::to_string(&a.code).unwrap(),
9669            serde_json::to_string(&b.code).unwrap(),
9670            serde_json::to_string(&b.code).unwrap()
9671        );
9672        // Through stdin: the script carries the whole runtime, longer than a
9673        // Windows command line may be.
9674        let mut node = std::process::Command::new("node")
9675            .arg("-")
9676            .stdin(std::process::Stdio::piped())
9677            .stdout(std::process::Stdio::piped())
9678            .stderr(std::process::Stdio::piped())
9679            .spawn()
9680            .expect("Node.js for the JavaScript regression");
9681        std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
9682        let result = node.wait_with_output().unwrap();
9683        assert!(
9684            result.status.success(),
9685            "{}",
9686            String::from_utf8_lossy(&result.stderr)
9687        );
9688    }
9689
9690    #[test]
9691    fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
9692        // TodoMVC's React app keeps JSX in `src/index.js`; the run failed on
9693        // `Unexpected token` before any test started.
9694        for file in [
9695            "src/index.js",
9696            "src/index.jsx",
9697            "src/index.mjs",
9698            "src/index.cjs",
9699            "src/index.tsx",
9700        ] {
9701            let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
9702            let output = instrument_candidate(source, file).unwrap();
9703            assert!(!output.points.is_empty(), "{file}");
9704            assert!(output.code.contains("<div>"), "{file}");
9705        }
9706        // TypeScript keeps its angle-bracket assertion; JSX would misread it.
9707        assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
9708        // The script or module mode an extension implies is kept.
9709        for file in ["a.js", "a.cjs", "a.mjs"] {
9710            let before = SourceType::from_path(file).unwrap();
9711            let after = project_source_type(Path::new(file)).unwrap();
9712            assert_eq!(after.module_kind(), before.module_kind(), "{file}");
9713            assert!(after.is_jsx(), "{file}");
9714        }
9715    }
9716
9717    const SOURCE: &str =
9718        "export function decide(a,b,c) {\n  if ((a && b) || !c) return 1;\n  return 0;\n}\n";
9719
9720    #[test]
9721    fn matches_the_frozen_if_decision_manifest_exactly() {
9722        let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
9723        assert!(!output.complete);
9724        assert_eq!(
9725            output.decisions,
9726            vec![CandidateDecision {
9727                id: "2f65989a5782c5bd".to_string(),
9728                file: "app/decide.ts".to_string(),
9729                line: 2,
9730                column: 7,
9731                source: "(a && b) || !c".to_string(),
9732                conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
9733                kind: "if".to_string(),
9734            }]
9735        );
9736    }
9737
9738    #[test]
9739    fn codegen_output_reparses_for_typescript_and_tsx() {
9740        for (file, source) in [
9741            (
9742                "component.tsx",
9743                "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
9744            ),
9745            ("module.ts", SOURCE),
9746        ] {
9747            let output = analyze_candidate(source, file).unwrap();
9748            let map = output.map.as_ref().expect("candidate source map");
9749            assert_eq!(map["version"], 3);
9750            assert_eq!(map["sources"][0], file);
9751            assert_eq!(map["sourcesContent"][0], source);
9752            assert!(
9753                map["mappings"]
9754                    .as_str()
9755                    .is_some_and(|value| !value.is_empty())
9756            );
9757            let allocator = Allocator::default();
9758            let source_type = SourceType::from_path(file).unwrap();
9759            let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
9760            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
9761        }
9762    }
9763
9764    #[test]
9765    fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
9766        for file in ["src/module.mjs", "src/script.cjs"] {
9767            let output = instrument_direct_candidate(
9768                "export const selected = value => value ? 1 : 0;",
9769                file,
9770            )
9771            .unwrap();
9772            assert!(
9773                output
9774                    .code
9775                    .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
9776            );
9777            assert!(!output.code.contains("virtual:supercov-runtime"));
9778            assert!(!output.code.contains("globalThis.__supercovRuntime"));
9779        }
9780    }
9781
9782    #[test]
9783    fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
9784        let source = concat!(
9785            "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
9786            "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
9787            "ordinaryHelper();\n",
9788        );
9789        let output = instrument_node_assertion_phases_with_runtime_hooks(
9790            source,
9791            "tests/runner.mjs",
9792            &[],
9793            Some("../.supercov/launchSupervisor.mjs"),
9794        )
9795        .unwrap();
9796        assert_eq!(output.assertions, 0);
9797        assert_eq!(output.capability_imports, 1);
9798        assert!(output.code.contains("wrapImportedCapability"));
9799        assert!(output.code.contains("__supercovRawImageBuilder"));
9800        assert!(output.code.contains("const ImageBuilder"));
9801        assert!(!output.code.contains("__supercovRawordinaryHelper"));
9802        assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
9803    }
9804
9805    #[test]
9806    fn capability_imports_accept_a_computed_guest_mount_path() {
9807        let source = concat!(
9808            "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
9809            "const guestRoot = resolve(tmpdir(), 'workspace');\n",
9810            "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
9811            "ordinaryHelper();\n",
9812        );
9813        let output = instrument_node_assertion_phases_with_runtime_hooks(
9814            source,
9815            "tests/runner.mjs",
9816            &[],
9817            Some("../.supercov/launchSupervisor.mjs"),
9818        )
9819        .unwrap();
9820        assert_eq!(output.capability_imports, 1);
9821        assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
9822        assert!(!output.code.contains("__supercovRawordinaryHelper"));
9823    }
9824
9825    #[test]
9826    fn capability_import_selection_follows_a_mapping_variable() {
9827        let source = concat!(
9828            "import { launch, unrelated } from './sdk.mjs';\n",
9829            "run();\n",
9830            "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
9831            "function run() { launch(options); unrelated(); }\n",
9832        );
9833        let output = instrument_node_assertion_phases_with_runtime_hooks(
9834            source,
9835            "tests/runner.mjs",
9836            &[],
9837            Some("../.supercov/launchSupervisor.mjs"),
9838        )
9839        .unwrap();
9840        assert_eq!(output.capability_imports, 1);
9841        assert!(output.code.contains("__supercovRawlaunch"));
9842        assert!(!output.code.contains("__supercovRawunrelated"));
9843    }
9844
9845    #[test]
9846    fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
9847        let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
9848        let output = instrument_node_assertion_phases_with_runtime_hooks(
9849            source,
9850            "src/main.mjs",
9851            &[],
9852            Some("../.supercov/launchSupervisor.mjs"),
9853        )
9854        .unwrap();
9855        assert_eq!(output.code, source);
9856        assert_eq!(output.capability_imports, 0);
9857    }
9858
9859    #[test]
9860    fn ordinary_mount_language_does_not_enable_capability_proxies() {
9861        let source = concat!(
9862            "import { render } from '@testing-library/react';\n",
9863            "it('mounts into a host element', () => render(<main />));\n",
9864        );
9865        let output = instrument_node_assertion_phases_with_runtime_hooks(
9866            source,
9867            "tests/component.test.tsx",
9868            &[],
9869            Some("../.supercov/launchSupervisor.mjs"),
9870        )
9871        .unwrap();
9872        assert_eq!(output.capability_imports, 0);
9873        assert!(!output.code.contains("wrapImportedCapability"));
9874    }
9875
9876    #[test]
9877    fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
9878        let source = concat!(
9879            "import { test, expect } from '@example/test-admin';\n",
9880            "test('visual snapshot', async ({ page }) => {\n",
9881            "  expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
9882            "});\n",
9883        );
9884        let output = instrument_node_assertion_phases_with_runtime_hooks(
9885            source,
9886            "tests/visual.spec.ts",
9887            &[],
9888            Some("../.supercov/launchSupervisor.mjs"),
9889        )
9890        .unwrap();
9891        assert_eq!(output.capability_imports, 0);
9892        assert!(!output.code.contains("wrapImportedCapability"));
9893    }
9894
9895    #[test]
9896    fn capability_rewrite_excludes_runtime_and_type_only_imports() {
9897        let source = concat!(
9898            "import type { Machine } from './types.ts';\n",
9899            "import { test } from 'node:test';\n",
9900            "import { runtime } from '../.supercov/runtime.mjs';\n",
9901            "console.log({ machine: true, test, runtime });\n",
9902        );
9903        let output = instrument_node_assertion_phases_with_runtime_hooks(
9904            source,
9905            "tests/runner.ts",
9906            &[],
9907            Some("../.supercov/launchSupervisor.mjs"),
9908        )
9909        .unwrap();
9910        assert_eq!(output.code, source);
9911        assert_eq!(output.capability_imports, 0);
9912    }
9913
9914    #[test]
9915    fn native_assertion_arguments_execute_inside_the_assertion_phase() {
9916        let source = concat!(
9917            "import assert from 'node:assert/strict';\n",
9918            "assert.equal(value(), 1);\n",
9919        );
9920        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9921        assert_eq!(output.assertions, 1);
9922        assert!(output.code.contains("withNodeAssertionPhase"));
9923        assert!(output.code.contains("node:assert/strict.equal"));
9924        assert!(output.code.contains("tests/value.test.mjs:2:1"));
9925        assert!(output.code.contains("assert.equal(value(), 1)"));
9926    }
9927
9928    #[test]
9929    fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
9930        let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
9931        let output = instrument_node_assertion_phases_with_runtime_imports(
9932            source,
9933            "tests/value.test.mjs",
9934            &[],
9935            None,
9936            Some("../.supercov/node_modules/bootstrap.cjs"),
9937        )
9938        .unwrap();
9939        assert_eq!(output.assertions, 1);
9940        // The loader shares the first line, so the assertion stays on line 2.
9941        let lines = output.code.lines().collect::<Vec<_>>();
9942        assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
9943        assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
9944        assert!(lines[0].ends_with("import assert from 'node:assert';"));
9945        assert!(lines[1].contains("withNodeAssertionPhase"));
9946        let allocator = Allocator::default();
9947        assert!(
9948            Parser::new(&allocator, &output.code, SourceType::mjs())
9949                .parse()
9950                .errors
9951                .is_empty()
9952        );
9953    }
9954
9955    #[test]
9956    fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
9957        let source = concat!(
9958            "import assert from 'node:assert/strict';\n",
9959            "export async function check() { assert.equal(await value(), 1); }\n",
9960        );
9961        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9962        assert_eq!(output.assertions, 1);
9963        assert!(output.code.contains("bindNodeAssertionPhase"));
9964        assert!(output.code.contains("await value()"));
9965        let allocator = Allocator::default();
9966        assert!(
9967            Parser::new(&allocator, &output.code, SourceType::mjs())
9968                .parse()
9969                .errors
9970                .is_empty()
9971        );
9972    }
9973
9974    #[test]
9975    fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
9976        let source = concat!(
9977            "import assert from 'node:assert/strict';\n",
9978            "export async function check(user?: string) {\n",
9979            "  assert.ok(user, await value());\n",
9980            "  assert\n",
9981            "    .equal(await value(), 1)\n",
9982            "}\n",
9983        );
9984        let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
9985        assert_eq!(output.assertions, 2);
9986        let code = &output.code;
9987        let lines: Vec<&str> = code.lines().collect();
9988        assert!(
9989            lines[2].starts_with(
9990                "  { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
9991            ),
9992            "{code}"
9993        );
9994        assert!(
9995            lines[2]
9996                .ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
9997            "{code}"
9998        );
9999        assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
10000        assert_eq!(
10001            lines[4], "     \"equal\"); __supercovAssertionPhase(await value(), 1) }",
10002            "{code}"
10003        );
10004        assert_eq!(lines[5], "}", "{code}");
10005        let allocator = Allocator::default();
10006        assert!(
10007            Parser::new(&allocator, code, SourceType::ts())
10008                .parse()
10009                .errors
10010                .is_empty()
10011        );
10012    }
10013
10014    #[test]
10015    fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
10016        let source = concat!(
10017            "import { expect, test } from '@playwright/test';\n",
10018            "test('value', async () => { expect(await value()).toBe(1); });\n",
10019        );
10020        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
10021        assert_eq!(output.assertions, 1);
10022        assert!(output.code.contains("bindNodeAssertionPhase"));
10023        assert!(output.code.contains("await value()"));
10024        let allocator = Allocator::default();
10025        assert!(
10026            Parser::new(&allocator, &output.code, SourceType::mjs())
10027                .parse()
10028                .errors
10029                .is_empty()
10030        );
10031    }
10032
10033    #[test]
10034    fn native_commonjs_assertion_bindings_are_attributed() {
10035        let source = concat!(
10036            "const assert = require('node:assert/strict');\n",
10037            "const { ok: verify } = require('assert');\n",
10038            "assert.equal(value(), 1);\n",
10039            "verify(other());\n",
10040        );
10041        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10042        assert_eq!(output.assertions, 2);
10043        assert!(output.code.contains("node:assert/strict.equal"));
10044        assert!(output.code.contains("node:assert.ok"));
10045    }
10046
10047    #[test]
10048    fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
10049        let source = concat!(
10050            "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
10051            "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
10052        );
10053        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10054        assert_eq!(output.assertions, 1);
10055        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10056    }
10057
10058    #[test]
10059    fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
10060        let source = concat!(
10061            "import assert from 'node:assert/strict';\n",
10062            "function unrelated(assert) { assert.equal(effect(), 1); }\n",
10063            "assert.equal(value(), 1);\n",
10064        );
10065        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10066        assert_eq!(output.assertions, 1);
10067        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10068    }
10069
10070    #[test]
10071    fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
10072        let source = concat!(
10073            "import test from 'node:test';\n",
10074            "import { expect as verify } from 'expect-library';\n",
10075            "test('value', () => verify(value()).not.toEqual(1));\n",
10076        );
10077        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10078        assert_eq!(output.assertions, 1);
10079        assert!(output.code.contains("expect.not.toEqual"));
10080        assert!(output.code.contains("verify(value()).not.toEqual(1)"));
10081    }
10082
10083    #[test]
10084    fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
10085        let source = concat!(
10086            "import { expect, test } from 'vitest';\n",
10087            "test('value', () => expect(value()).toBe(1));\n",
10088        );
10089        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10090        assert_eq!(output.assertions, 1);
10091        assert!(output.code.contains("expect.toBe"));
10092    }
10093
10094    #[test]
10095    fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
10096        // Vitest Browser Mode asserts through `expect.element(locator)`, and
10097        // `expect.soft`/`expect.poll` wrap the same way. The chain root is a
10098        // member call rather than a bare `expect` identifier, which is the
10099        // shape that used to leave every UI assertion unrecognised.
10100        for (call, operation) in [
10101            (
10102                "expect.element(button).toHaveAccessibleName('Copy')",
10103                "expect.element.toHaveAccessibleName",
10104            ),
10105            ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
10106            ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
10107        ] {
10108            let source = format!(
10109                "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
10110            );
10111            let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
10112            assert_eq!(output.assertions, 1, "{call}");
10113            assert!(output.code.contains(operation), "{call} -> {operation}");
10114        }
10115    }
10116
10117    #[test]
10118    fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
10119        // A JSX tree is one statement, so an attribute and a child that call
10120        // the same function had no way to be told apart -- which is exactly
10121        // what a UI assertion needs, `toHaveAccessibleName` naming the
10122        // attribute and `toHaveTextContent` the child.
10123        let source = concat!(
10124            "export function Button({ label, copied }) {\n",
10125            "  return (\n",
10126            "    <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
10127            "      {format(copied)}\n",
10128            "    </button>\n",
10129            "  );\n",
10130            "}\n",
10131        );
10132        let output = instrument_candidate(source, "src/button.tsx").unwrap();
10133        let rendered = output
10134            .points
10135            .iter()
10136            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10137            .collect::<Vec<_>>();
10138        // The attribute and the child are separate obligations at separate
10139        // positions, even though they read identically.
10140        assert_eq!(rendered.len(), 2, "{:?}", output.points);
10141        assert!(
10142            rendered
10143                .iter()
10144                .all(|point| point.source == "format(copied)")
10145        );
10146        assert_eq!(rendered[0].line, 3);
10147        assert_eq!(rendered[1].line, 4);
10148        assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
10149        // `{label}` is reached exactly when the tree is, and the handler is
10150        // measured where it is called; neither earns an obligation.
10151        assert!(!output.code.contains("title={__supercovRenderedValueV2"));
10152        // JSX forbids a bare comma operator, so the probe has to carry the
10153        // value through a call rather than a sequence expression.
10154        assert!(!output.code.contains("), format(copied)}"));
10155        assert!(output.code.contains(", format(copied))}"));
10156    }
10157
10158    #[test]
10159    fn instrumented_jsx_stays_parseable_through_every_container_shape() {
10160        // A JSX container cannot hold a bare comma operator, and a malformed
10161        // rewrite would surface only as the project's own build failing. Each
10162        // shape below reaches the rewrite by a different route.
10163        let source = concat!(
10164            "export function Panel({ items, user, error, tone }) {\n",
10165            "  return (\n",
10166            "    <>\n",
10167            "      <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
10168            "      <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
10169            "        {error ? <Alert text={message(error)} /> : null}\n",
10170            "        {items.map((item) => (\n",
10171            "          <Item key={item.id} label={format(item)} />\n",
10172            "        ))}\n",
10173            "        {count(items) > 0 && <Footer total={sum(items)} />}\n",
10174            "      </div>\n",
10175            "    </>\n",
10176            "  );\n",
10177            "}\n",
10178        );
10179        let output = instrument_candidate(source, "src/panel.tsx").unwrap();
10180        // The instrumented output must itself parse.
10181        instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
10182        // A spread attribute is not a container and is never rewritten.
10183        assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
10184        let rendered = output
10185            .points
10186            .iter()
10187            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10188            .map(|point| point.source.as_str())
10189            .collect::<Vec<_>>();
10190        assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
10191        assert!(
10192            rendered.contains(&"cx(\'a\', tone && \'b\')"),
10193            "{rendered:?}"
10194        );
10195        assert!(rendered.contains(&"format(item)"), "{rendered:?}");
10196        assert!(rendered.contains(&"message(error)"), "{rendered:?}");
10197        // `user?.id` short-circuits but is still a member access reached with
10198        // the statement, and a key that is a bare member chain is too.
10199        assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
10200        assert!(!rendered.contains(&"item.id"), "{rendered:?}");
10201    }
10202
10203    #[test]
10204    fn jest_global_expect_is_attributed_next_to_its_test_globals() {
10205        // Jest injects `expect`, `test`, `it` and `describe` into the test
10206        // environment; none is imported. The bare `expect` is the assertion
10207        // exactly when the file reaches for those test globals too.
10208        let source = "test('value', () => expect(value()).toBe(1));\n";
10209        let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
10210        assert_eq!(output.assertions, 1);
10211        assert!(output.code.contains("expect.toBe"));
10212        // A stray global `expect` in ordinary code is not one.
10213        let plain = "const ok = expect(value()).toBe(1);\n";
10214        let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
10215        assert_eq!(output.assertions, 0);
10216        assert_eq!(output.code, plain);
10217    }
10218
10219    #[test]
10220    fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
10221        let source = concat!(
10222            "import { expect, test } from '@playwright/test';\n",
10223            "test('value', () => expect(value()).toBe(1));\n",
10224        );
10225        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
10226        assert_eq!(output.assertions, 1);
10227        assert!(output.code.contains("expect.toBe"));
10228    }
10229
10230    #[test]
10231    fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
10232        let source = concat!(
10233            "import { browserTest, expect } from '@acme/browser-fixtures';\n",
10234            "browserTest('value', () => expect(value()).toBe(1));\n",
10235        );
10236        let output = instrument_node_assertion_phases_with_expect_modules(
10237            source,
10238            "tests/value.spec.mjs",
10239            &["@acme/browser-fixtures".into()],
10240        )
10241        .unwrap();
10242        assert_eq!(output.assertions, 1);
10243        assert!(output.code.contains("expect.toBe"));
10244    }
10245
10246    #[test]
10247    fn preserves_comment_payloads_byte_for_byte() {
10248        let comment = "/*---\ndescription: >\n  nested indentation is data\ninfo: |\n  first\n    second\n---*/";
10249        let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
10250        let output = instrument_candidate(&source, "app/comments.js").unwrap();
10251        assert!(output.code.contains(comment), "{}", output.code);
10252    }
10253
10254    #[test]
10255    fn source_map_destinations_follow_restored_comments() {
10256        let source = "function run() {\n  // first omitted comment\n  // second omitted comment\n  return 1;\n}\n";
10257        let output = analyze_candidate(source, "app/comment-map.js").unwrap();
10258        let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
10259        let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
10260        let return_token = map
10261            .get_tokens()
10262            .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
10263            .expect("return token mapping");
10264        let offset = Utf16LineIndex::new(&output.code).byte_offset(
10265            return_token.get_dst_line() as usize,
10266            return_token.get_dst_col() as usize,
10267        );
10268        assert!(
10269            output.code[offset..].starts_with("return"),
10270            "{}",
10271            output.code
10272        );
10273    }
10274
10275    #[test]
10276    fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
10277        // B10. A code generator rewrites `</script` to `<\/script` inside a
10278        // comment so the output cannot close an HTML <script> element early.
10279        // The restore compared the two spellings as different text, and because
10280        // it walks the source comments in order, one comment it could not
10281        // recognise consumed every remaining one and failed the file. That cost
10282        // a real project its coverage silently, and later its whole run.
10283        //
10284        // Prose about HTML is ordinary in web code, which is how this reached a
10285        // shipped release and stayed for months: the file was otherwise plain
10286        // TypeScript.
10287        for source in [
10288            "// </script>\nexport const x = 1;\n",
10289            "// </script\nexport const x = 1;\n",
10290            "// </SCRIPT>\nexport const x = 1;\n",
10291            "// see the </script> tag here\nexport const x = 1;\n",
10292            "/* block </script> here */\nexport const x = 1;\n",
10293            "/** jsdoc </script> */\nexport const x = 1;\n",
10294            // A run of comments, as the reported file had.
10295            "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
10296        ] {
10297            let output = instrument_candidate(source, "src/probe.ts")
10298                .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
10299            let original = source.lines().next().expect("a comment");
10300            assert!(
10301                output.code.contains(original),
10302                "comment was not restored verbatim: {original:?} missing from {:?}",
10303                output.code
10304            );
10305        }
10306        // The escape is the generator's, not the author's: a backslash the
10307        // author wrote survives as written rather than being collapsed.
10308        let written = "// author wrote <\\/script>\nexport const x = 1;\n";
10309        let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
10310        assert!(
10311            output.code.contains("// author wrote <\\/script>"),
10312            "{:?}",
10313            output.code
10314        );
10315        // Only that sequence is normalised. Nothing else a comment may contain
10316        // is treated as equal to a different spelling.
10317        assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
10318        assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
10319        assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
10320    }
10321
10322    #[test]
10323    fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
10324        // The counts in this error are usually equal, so reporting them alone
10325        // says the opposite of what happened -- nothing was dropped, one
10326        // comment was not recognised. The reported 116-vs-112 sent the first
10327        // reader looking for four missing comments that were never the cause.
10328        let error = CandidateError::CommentPreservation {
10329            expected: 116,
10330            actual: 112,
10331            detail:
10332                "no source comment matches the generated comment \"// x\" (matched 97 before it)"
10333                    .to_owned(),
10334        };
10335        let rendered = format!("{error:?}");
10336        assert!(rendered.contains("no source comment matches"), "{rendered}");
10337        assert!(rendered.contains("matched 97 before it"), "{rendered}");
10338    }
10339
10340    #[test]
10341    fn a_wrapped_default_keeps_the_type_its_syntax_names() {
10342        // Wrapped in a call, `strict = false` is no longer a default
10343        // `isolatedDeclarations` can read a type from, and h3 failed with
10344        // TS9011. The annotation says what was inferred before.
10345        let source = concat!(
10346            "export function f(a: number, strict = false, limit = 10, name = `x`, big = 1n, low = -1) { return a; }\n",
10347            "export function g<E extends 'utf8' | false = 'utf8'>(encoding = 'utf8' as E, list = [1], kept: string = 'a') { return encoding; }\n",
10348            "export const h = ({ open = false }, mode = 'x' as const) => open;\n",
10349        );
10350        let output = instrument_candidate(source, "src/defaults.ts").unwrap();
10351        for annotated in [
10352            "strict: boolean = ",
10353            "limit: number = ",
10354            "name: string = ",
10355            "big: bigint = ",
10356            "low: number = ",
10357            "encoding: E = ",
10358            "kept: string = ",
10359        ] {
10360            assert!(
10361                output.code.contains(annotated),
10362                "{annotated}\n{}",
10363                output.code
10364            );
10365        }
10366        // Nothing is said where the syntax names no type: an array, a
10367        // destructured parameter, `as const`.
10368        for bare in ["list = ", "mode = "] {
10369            assert!(output.code.contains(bare), "{bare}\n{}", output.code);
10370        }
10371        assert!(!output.code.contains("list:"), "{}", output.code);
10372        assert!(!output.code.contains("mode:"), "{}", output.code);
10373        // A JavaScript file has no annotations to give.
10374        let script = instrument_candidate(
10375            "export function f(strict = false) { return strict; }\n",
10376            "src/defaults.js",
10377        )
10378        .unwrap();
10379        assert!(!script.code.contains("strict:"), "{}", script.code);
10380    }
10381
10382    #[test]
10383    fn an_asserted_assignment_target_is_printed_as_a_target() {
10384        // `(buffer[0] as string) += child` came out as
10385        // `buffer[0] as string += child`, which does not parse: two of hono's
10386        // files could not be read by TypeScript or by a bundler.
10387        let source = concat!(
10388            "export function join(buffer: [unknown], child: string, n: { v?: number }) {\n",
10389            "  ;(buffer[0] as string) += child\n",
10390            "  ;(buffer[0] as string) = child\n",
10391            "  ;(n.v as number)++\n",
10392            "  ;(n.v satisfies number | undefined) = 1\n",
10393            "  ;[(buffer[0] as string)] = [child]\n",
10394            "  for ((buffer[0] as string) of [child]) {}\n",
10395            "  return buffer\n",
10396            "}\n",
10397        );
10398        let output = instrument_candidate(source, "src/targets.ts").unwrap();
10399        assert!(!output.code.contains(" as string"), "{}", output.code);
10400        assert!(!output.code.contains("satisfies"), "{}", output.code);
10401        assert!(
10402            output.code.contains("buffer[0] += child"),
10403            "{}",
10404            output.code
10405        );
10406        assert!(output.code.contains("n.v++"), "{}", output.code);
10407        let allocator = Allocator::default();
10408        let reparsed = Parser::new(&allocator, &output.code, SourceType::ts()).parse();
10409        assert!(
10410            reparsed.errors.is_empty(),
10411            "{:?}\n{}",
10412            reparsed.errors,
10413            output.code
10414        );
10415    }
10416
10417    #[test]
10418    fn a_comment_the_printer_moves_is_still_recognised() {
10419        // The printer writes a `@__NO_SIDE_EFFECTS__` annotation after the
10420        // documentation comment it stood before. The restore looked only
10421        // forward for each printed comment, did not find the annotation, and
10422        // zod's `v4/core/api.ts` could not be instrumented.
10423        let source = concat!(
10424            "// first\n",
10425            "// @__NO_SIDE_EFFECTS__\n",
10426            "/** documented\n */\n",
10427            "export function f(a: number) {\n",
10428            "  return a > 1 ? a : 0;\n",
10429            "}\n",
10430            "\n",
10431            "// second\n",
10432            "export type T = number;\n",
10433        );
10434        let output = instrument_candidate(source, "src/annotated.ts").unwrap();
10435        for comment in [
10436            "// first",
10437            "// @__NO_SIDE_EFFECTS__",
10438            "/** documented\n */",
10439            "// second",
10440        ] {
10441            assert_eq!(
10442                output.code.matches(comment).count(),
10443                1,
10444                "{comment}\n{}",
10445                output.code
10446            );
10447        }
10448    }
10449
10450    #[test]
10451    fn restored_comments_never_split_a_keyword_from_its_argument() {
10452        // oxc drops the comment that led a parenthesised return argument; the
10453        // restore used to put it back right after `return`, on its own line,
10454        // and automatic semicolon insertion turned the statement into `return;`
10455        // with the JSX unreachable. Vite then removed the unreachable tree and
10456        // every declaration only it used, so a component rendered nothing under
10457        // measurement while passing plainly. Expression-bodied arrows are hit
10458        // too, since instrumentation rewrites them into `return` blocks.
10459        let source = concat!(
10460            "export const A = ({ open }) => {\n",
10461            "  return (\n",
10462            "    // leading comment before JSX\n",
10463            "    <div aria-expanded={open}>a</div>\n",
10464            "  )\n",
10465            "}\n",
10466            "export const B = ({ open }) => (\n",
10467            "  // comment in an expression body\n",
10468            "  <div aria-expanded={open}>b</div>\n",
10469            ")\n",
10470            "export function C(x) {\n",
10471            "  return ( // inline line comment\n",
10472            "    x + 1\n",
10473            "  )\n",
10474            "}\n",
10475            "export function D(x) {\n",
10476            "  throw (\n",
10477            "    /* block\n       comment */\n",
10478            "    new Error(String(x))\n",
10479            "  )\n",
10480            "}\n",
10481            "export function E(x) {\n",
10482            "  return ( /* inline block */ x + 2 )\n",
10483            "}\n",
10484        );
10485        let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
10486        for keyword in ["return ", "throw "] {
10487            for (index, _) in output.code.match_indices(keyword) {
10488                let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
10489                let block_with_line_break = rest.starts_with("/*")
10490                    && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
10491                assert!(
10492                    !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
10493                    "`{keyword}` is separated from its argument:\n{}",
10494                    output.code
10495                );
10496            }
10497        }
10498        for comment in [
10499            "// leading comment before JSX",
10500            "// comment in an expression body",
10501            "// inline line comment",
10502            "/* block\n       comment */",
10503            "/* inline block */",
10504        ] {
10505            assert!(
10506                output.code.contains(comment),
10507                "{comment} was lost:\n{}",
10508                output.code
10509            );
10510        }
10511        assert!(
10512            output
10513                .code
10514                .contains("return <div aria-expanded={open}>a</div>"),
10515            "{}",
10516            output.code
10517        );
10518        assert!(
10519            output
10520                .code
10521                .contains("return <div aria-expanded={open}>b</div>"),
10522            "{}",
10523            output.code
10524        );
10525        // The restored output must still parse to the same program shape.
10526        let allocator = Allocator::default();
10527        let reparsed = Parser::new(
10528            &allocator,
10529            &output.code,
10530            SourceType::from_path("app/components/List.tsx").unwrap(),
10531        )
10532        .parse();
10533        assert!(
10534            reparsed.errors.is_empty(),
10535            "{:?}\n{}",
10536            reparsed.errors,
10537            output.code
10538        );
10539    }
10540
10541    #[test]
10542    fn preserves_reference_order_across_every_control_decision_kind() {
10543        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}";
10544        let output = instrument_candidate(source, "app/control.js").unwrap();
10545        assert_eq!(
10546            output
10547                .decisions
10548                .iter()
10549                .map(|decision| decision.kind.as_str())
10550                .collect::<Vec<_>>(),
10551            vec!["ternary", "while", "do-while", "for", "if"]
10552        );
10553    }
10554
10555    #[test]
10556    fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
10557        // B11. `for (const x of [a, b])` cannot iterate zero times, so the
10558        // outcome was an obligation no test could ever discharge -- the author
10559        // could only fake a test or delete the loop.
10560        let entered = |source: &str| {
10561            let output = analyze_candidate(source, "src/loops.ts").unwrap();
10562            output
10563                .branches
10564                .iter()
10565                .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
10566                .count()
10567        };
10568        // Folded: the iterable is certainly non-empty.
10569        for source in [
10570            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
10571            "export function f(g) { for (const x of [1]) g(x); }\n",
10572            "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
10573            // A hole still yields once: [,].length === 1.
10574            "export function f(g) { for (const x of [,]) g(x); }\n",
10575            // A spread may contribute nothing, but a definite element is enough.
10576            "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
10577        ] {
10578            assert_eq!(entered(source), 0, "should be folded: {source}");
10579        }
10580        // Kept: the loop can genuinely run zero times.
10581        for source in [
10582            "export function f(g, ys) { for (const x of ys) g(x); }\n",
10583            "export function f(g) { for (const x of []) g(x); }\n",
10584            "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
10585            "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
10586            // Anything else may reach the binding, including a shrinking call.
10587            "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
10588            "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
10589            "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
10590            // A different binding of the same name must not be borrowed.
10591            concat!(
10592                "const xs = [1, 2];\n",
10593                "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
10594            ),
10595            // for-in is about keys, not elements, and is left alone.
10596            "export function f(g) { for (const k in [1, 2]) g(k); }\n",
10597        ] {
10598            assert_eq!(entered(source), 1, "should be kept: {source}");
10599        }
10600        // The loop body is still measured; only the impossible outcome is gone.
10601        let output = analyze_candidate(
10602            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
10603            "src/loops.ts",
10604        )
10605        .unwrap();
10606        assert!(
10607            output
10608                .points
10609                .iter()
10610                .any(|point| point.source.contains("g(x)")),
10611            "the body must still be an obligation"
10612        );
10613    }
10614
10615    #[test]
10616    fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
10617        let source = concat!(
10618            "export function run(value) {\n",
10619            "  while (true) { break; }\n",
10620            "  do { value += 1; } while (false);\n",
10621            "  if (false) value += 2;\n",
10622            "  if (value || true) value += 3;\n",
10623            "  if (value && false) value += 4;\n",
10624            "  if (value) value += 5;\n",
10625            "  return value;\n",
10626            "}\n",
10627        );
10628        let output = analyze_candidate(source, "src/constants.js").unwrap();
10629        assert_eq!(output.decisions.len(), 1);
10630        assert_eq!(output.decisions[0].source, "value");
10631
10632        let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
10633        assert_eq!(instrumented.decisions.len(), 1);
10634        assert_eq!(instrumented.decisions[0].source, "value");
10635    }
10636
10637    #[test]
10638    fn leaves_compile_time_style_macro_arguments_as_source() {
10639        // StyleX evaluates these at build time and rejects a block-bodied
10640        // dynamic style; probing them broke a real project's Vite build.
10641        let source = concat!(
10642            "import * as stylex from '@stylexjs/stylex';\n",
10643            "const styles = stylex.create({\n",
10644            "  container: { display: 'flex' },\n",
10645            "  paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
10646            "  tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
10647            "});\n",
10648            "export function render(spacing: number) {\n",
10649            "  return stylex.props(styles.container, styles.paddingTop(spacing));\n",
10650            "}\n",
10651        );
10652        let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
10653        assert!(
10654            output.code.contains("=> ({ paddingTop: `${spacing}px` })")
10655                || output
10656                    .code
10657                    .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
10658            "dynamic style arrow must stay an expression body:\n{}",
10659            output.code
10660        );
10661        assert!(
10662            !output
10663                .decisions
10664                .iter()
10665                .any(|decision| decision.source.contains("dark")),
10666            "no decision may be collected inside the macro argument"
10667        );
10668        let function_points = output
10669            .points
10670            .iter()
10671            .filter(|point| point.kind == "function")
10672            .map(|point| point.source.as_str())
10673            .collect::<Vec<_>>();
10674        assert!(
10675            function_points
10676                .iter()
10677                .all(|source| source.contains("render")),
10678            "only the real function may carry a function point: {function_points:?}"
10679        );
10680        assert!(
10681            output.limitations.is_empty(),
10682            "compiled-away code is not a limitation: {:?}",
10683            output.limitations
10684        );
10685    }
10686
10687    #[test]
10688    fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
10689        let source = concat!(
10690            "declare global {\n",
10691            "  var Beacon: undefined | ((action: string) => void);\n",
10692            "}\n",
10693            "declare module 'virtual:feature' {\n",
10694            "  export const enabled: boolean;\n",
10695            "}\n",
10696            "declare const buildOnly: string;\n",
10697            "export const live = 1;\n",
10698        );
10699        let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
10700        let statements = output
10701            .points
10702            .iter()
10703            .filter(|point| point.kind == "statement")
10704            .map(|point| point.source.as_str())
10705            .collect::<Vec<_>>();
10706        assert_eq!(statements, ["const live = 1;"]);
10707        assert!(output.code.contains("const live = 1"));
10708    }
10709
10710    /// The instrumented code with what records a decision's outcome written
10711    /// `END(`, and the decisions' temporaries named `t0`, `t1`, … in the order
10712    /// they are declared.
10713    fn decision_shape(source: &str, file: &str) -> String {
10714        let output = instrument_candidate(source, file).unwrap();
10715        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
10716        let call = format!("{}(", runtime.mcdc_end_v2);
10717        let start = output.code.find(&call).expect("an outcome is recorded") + call.len();
10718        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
10719        let code = output
10720            .code
10721            .replace(&format!("{call}{probe_file}, "), "END(");
10722        let mut names: Vec<&str> = Vec::new();
10723        let mut shaped = String::new();
10724        let mut rest = code.as_str();
10725        while let Some(at) = rest.find("_supercovMcdc") {
10726            shaped.push_str(&rest[..at]);
10727            let tail = &rest[at..];
10728            // The runtime's own helpers are bound to `__supercovMcdc…`.
10729            if shaped.ends_with('_') {
10730                shaped.push_str("_supercovMcdc");
10731                rest = &tail["_supercovMcdc".len()..];
10732                continue;
10733            }
10734            let length = tail
10735                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
10736                .unwrap_or(tail.len());
10737            let name = &tail[..length];
10738            let index = names
10739                .iter()
10740                .position(|known| *known == name)
10741                .unwrap_or_else(|| {
10742                    names.push(name);
10743                    names.len() - 1
10744                });
10745            shaped.push_str(&format!("t{index}"));
10746            rest = &tail[length..];
10747        }
10748        shaped.push_str(rest);
10749        shaped
10750    }
10751
10752    #[test]
10753    fn a_decision_stays_the_test_it_was() {
10754        // TypeScript narrows through `&&` and `||` only where they are the
10755        // condition itself, through an assignment's right side, and through a
10756        // comma's last operand. The decision used to be the right side of an
10757        // assignment inside a comma, and each condition ended in its
10758        // temporary: `if (client) return client` returned `Client | null`,
10759        // and a file Supercov never touched failed to compile against it.
10760        let shaped = decision_shape(
10761            concat!(
10762                "export function pick(client: { id: number } | null, strict: boolean) {\n",
10763                "  if (client && strict) return client;\n",
10764                "  return null;\n",
10765                "}\n",
10766                "export function size(list: string[] | null) {\n",
10767                "  if (!list) { return 0; } else { return list.length; }\n",
10768                "}\n",
10769                "export function first(list: string[] | null) {\n",
10770                "  return list ? list[0] : undefined;\n",
10771                "}\n",
10772            ),
10773            "src/pick.ts",
10774        );
10775        for expected in [
10776            // The bookkeeping rides inside the first condition, and every
10777            // condition ends in the assignment of its own value.
10778            "if ((t0 = 1, t2 = 0, t1 = client) && (t0 += 3, t2 = strict)) {\n\t\t\tEND(0, t0 + +!!t1 + 3 * +!!t2, true), t0 = 0;",
10779            // Without an `else` there is nowhere inside the statement for the
10780            // false outcome, and an `else` made for it would be a branch to
10781            // a coverage tool the tests run: it is recorded after the `if`,
10782            // when the frame says it has not been.
10783            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1 + 3 * +!!t2, false);\n\t}",
10784            "if (t3 = 1, t4 = !list) {\n\t\tEND(1, t3 + +!!t4, true);",
10785            "} else {\n\t\tEND(1, t3 + +!!t4, false);",
10786            "return (t5 = 1, t6 = list) ? (void END(2, t5 + +!!t6, true), list[0]) : (void END(2, t5 + +!!t6, false), undefined);",
10787        ] {
10788            assert!(shaped.contains(expected), "{expected}\n{shaped}");
10789        }
10790    }
10791
10792    #[test]
10793    fn a_loop_records_its_last_test_after_itself() {
10794        // A loop's true outcome is recorded where its body starts. Nothing
10795        // runs when the test is false, so the statement after the loop
10796        // records that, and only when the test, not a `break`, ended it.
10797        let shaped = decision_shape(
10798            concat!(
10799                "export function scan(limit: number) {\n",
10800                "  let i = 0;\n",
10801                "  walk: while (i < limit) { i++; if (i > 5) break walk; }\n",
10802                "  do i--; while (i > 0);\n",
10803                "  for (let j = 0; j < limit; j++) i += j;\n",
10804                "  return i;\n",
10805                "}\n",
10806            ),
10807            "src/scan.ts",
10808        );
10809        for expected in [
10810            // The label stays on its loop, for `continue walk`.
10811            "{\n\t\twalk: while (t0 = 1, t1 = i < limit) {\n\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
10812            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1, false);\n\t}",
10813            // A `do … while` body runs before its test: the frame says whether
10814            // one came before this iteration, and starts cleared.
10815            "{\n\t\tt4 = 0;\n\t\tdo {\n\t\t\tt4 && (END(2, t4 + +!!t5, true), t4 = 0);",
10816            "} while (t4 = 1, t5 = i > 0);\n\t\tt4 && END(2, t4 + +!!t5, false);\n\t}",
10817            "for (let j = 0; t6 = 1, t7 = j < limit; j++) {\n\t\t\tEND(3, t6 + +!!t7, true), t6 = 0;",
10818            "\t\t}\n\t\tt6 && END(3, t6 + +!!t7, false);\n\t}",
10819        ] {
10820            assert!(shaped.contains(expected), "{expected}\n{shaped}");
10821        }
10822    }
10823
10824    #[test]
10825    fn a_decision_without_a_function_body_around_it_declares_its_own_temporaries() {
10826        let shaped = decision_shape(
10827            concat!(
10828                "export function pick(\n",
10829                "  n: number,\n",
10830                "  kind = class { static label = 'none'; static { if (n > 1) this.label = 'some'; } },\n",
10831                "  tag = n > 5 ? 'high' : 'low',\n",
10832                ") {\n",
10833                "  return [kind.label, tag];\n",
10834                "}\n",
10835            ),
10836            "src/defaults.ts",
10837        );
10838        for expected in [
10839            "static {\n\t\tlet t0, t1;\n",
10840            "if (t0 = 1, t1 = n > 1) {\n\t\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
10841            // A parameter default has no body to declare in: the arrow holds
10842            // the whole conditional expression, so its branches still see
10843            // what the test narrowed.
10844            "(() => {\n\tlet t2, t3;\n\treturn (t2 = 1, t3 = n > 5) ? (void END(1, t2 + +!!t3, true), \"high\") : (void END(1, t2 + +!!t3, false), \"low\");\n})()",
10845        ] {
10846            assert!(shaped.contains(expected), "{expected}\n{shaped}");
10847        }
10848    }
10849
10850    #[test]
10851    fn a_top_level_declaration_has_a_line_of_its_own_before_it() {
10852        // See `separate_top_level_declarations`. An overload signature stays
10853        // next to its implementation.
10854        let output = instrument_candidate(
10855            concat!(
10856                "const one = 1;\n",
10857                "function plain() { return one; }\n",
10858                "class Box {}\n",
10859                "function over(value: string): string;\n",
10860                "function over(value: number): number;\n",
10861                "function over(value: any) { return value; }\n",
10862                "export function exported() { return plain(); }\n",
10863            ),
10864            "src/declarations.ts",
10865        )
10866        .unwrap();
10867        for expected in [
10868            "\n;\nfunction plain() {",
10869            "\n;\nclass Box {",
10870            "\n;\nfunction over(value: string): string;\nfunction over(value: number): number;\nfunction over(value: any) {",
10871        ] {
10872            assert!(
10873                output.code.contains(expected),
10874                "{expected}\n{}",
10875                output.code
10876            );
10877        }
10878        assert!(
10879            !output.code.contains(";\nexport function"),
10880            "{}",
10881            output.code
10882        );
10883    }
10884
10885    #[test]
10886    fn exposes_the_complete_probe_v2_instrumenter_contract() {
10887        let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
10888        assert!(output.complete);
10889        assert!(output.limitations.is_empty());
10890        assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
10891        let runtime = output.runtime.expect("candidate runtime binding");
10892        assert!(output.code.contains(&runtime.mcdc_end_v2));
10893        assert!(output.code.contains("_supercovMcdcFrame"));
10894        assert!(output.code.contains("_supercovMcdcValue"));
10895        assert_eq!(output.decisions.len(), 1);
10896        assert!(output.points.iter().any(|point| point.kind == "statement"));
10897        assert!(output.points.iter().any(|point| point.kind == "function"));
10898
10899        let allocator = Allocator::default();
10900        let reparsed = Parser::new(
10901            &allocator,
10902            &output.code,
10903            SourceType::from_path("app/decide.ts").unwrap(),
10904        )
10905        .parse();
10906        assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
10907    }
10908
10909    #[test]
10910    fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
10911        let source = concat!(
10912            "import type { Session } from './types.ts';\n",
10913            "import { type Row } from './rows.ts';\n",
10914            "import './register.ts';\n",
10915            "const value = 1;\n",
10916        );
10917        let output = instrument_candidate(source, "app/imports.ts").unwrap();
10918        let statement_lines = output
10919            .points
10920            .iter()
10921            .filter(|point| point.kind == "statement")
10922            .map(|point| point.line)
10923            .collect::<Vec<_>>();
10924        assert_eq!(statement_lines, vec![2, 3, 4]);
10925    }
10926
10927    #[test]
10928    fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
10929        let source = concat!(
10930            "import { Logger } from './types.js';\n",
10931            "import DefaultType from './default.js';\n",
10932            "import * as Types from './namespace.js';\n",
10933            "import { type Inline } from './inline.js';\n",
10934            "import { run, type Config } from './mixed.js';\n",
10935            "import './register.js';\n",
10936            "import {} from './empty.js';\n",
10937            "import { unused } from './unused.js';\n",
10938            "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
10939        );
10940        let preserved =
10941            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", false)
10942                .unwrap();
10943        let erased =
10944            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true).unwrap();
10945        assert_eq!(
10946            erased
10947                .excluded_statements
10948                .iter()
10949                .map(|p| p.line)
10950                .collect::<Vec<_>>(),
10951            vec![1, 2, 3, 4]
10952        );
10953        let excluded = erased
10954            .excluded_statements
10955            .iter()
10956            .map(|p| &p.id)
10957            .collect::<HashSet<_>>();
10958        let expected = preserved
10959            .points
10960            .iter()
10961            .filter(|p| !excluded.contains(&p.id))
10962            .collect::<Vec<_>>();
10963        assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
10964        for line in [5, 6, 7, 8] {
10965            assert!(erased.points.iter().any(|p| p.line == line));
10966        }
10967        // JavaScript value imports and type queries with runtime uses remain.
10968        let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
10969        let mixed = instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true)
10970            .unwrap();
10971        assert!(mixed.excluded_statements.is_empty());
10972    }
10973
10974    /// A line's first mapping: `None` for no segment, `Some(None)` for an
10975    /// unmapped one, else the source line and column it names.
10976    type FirstMapping = Option<Option<(u32, u32)>>;
10977
10978    /// The instrumented copy's lines with their first mapping.
10979    fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
10980        let map = oxc_sourcemap::SourceMap::from_json_string(
10981            &output
10982                .map
10983                .as_ref()
10984                .expect("instrumented copy has a map")
10985                .to_string(),
10986        )
10987        .unwrap();
10988        let tokens = map.get_tokens().collect::<Vec<_>>();
10989        output
10990            .code
10991            .split('\n')
10992            .enumerate()
10993            .map(|(line, text)| {
10994                let first = tokens
10995                    .iter()
10996                    .find(|token| token.get_dst_line() == line as u32)
10997                    .map(|token| {
10998                        token
10999                            .get_source_id()
11000                            .map(|_| (token.get_src_line(), token.get_src_col()))
11001                    });
11002                (text.trim().to_string(), first)
11003            })
11004            .collect()
11005    }
11006
11007    #[test]
11008    fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
11009        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";
11010        let output = instrument_candidate(source, "app/f.js").unwrap();
11011        let lines = first_mappings(&output);
11012        let runtime = output.runtime.as_ref().unwrap();
11013        for (text, first) in &lines {
11014            // A probe maps to nothing: a coverage tool reading the copy
11015            // through its map counts no branch of the user's for its `||`.
11016            if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
11017                && let Some(first) = first
11018            {
11019                assert_eq!(*first, None, "{text}");
11020            }
11021        }
11022        let at = |needle: &str| {
11023            lines
11024                .iter()
11025                .find(|(text, _)| text.starts_with(needle))
11026                .unwrap_or_else(|| panic!("{needle} in {}", output.code))
11027                .1
11028        };
11029        // Each statement after a block Supercov added maps at its own start,
11030        // where the block's brace used to take that mapping from it.
11031        assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
11032        assert_eq!(at("continue;"), Some(Some((4, 25))));
11033        assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
11034        // The arrow's body, made a block, returns what the expression it was
11035        // evaluated: the line maps where that expression starts.
11036        assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
11037        // Every line of the program starting with a closing brace maps where
11038        // it starts, so a tool that looks a position up on its own line finds
11039        // a mapping.
11040        let program = lines
11041            .iter()
11042            .position(|(text, _)| text.starts_with("function f("))
11043            .unwrap();
11044        for (text, first) in &lines[program..] {
11045            if text.starts_with('}') {
11046                assert!(first.is_some(), "{text} in {}", output.code);
11047            }
11048        }
11049    }
11050
11051    /// The instrumented code with the calls that record a value tree written
11052    /// `END2(`, `END(` and `HIT(`, and the trees' frames named `s0`, `s1`, …
11053    fn value_shape(source: &str, file: &str) -> String {
11054        let output = instrument_candidate(source, file).unwrap();
11055        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11056        let call = format!("{}(", runtime.select_end2_v2);
11057        let start = output.code.find(&call).expect("a value tree is recorded") + call.len();
11058        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11059        let mut code = output.code.clone();
11060        for (helper, short) in [
11061            (&runtime.select_end2_v2, "END2("),
11062            (&runtime.select_end_v2, "END("),
11063            (&runtime.select_next_v2, "NEXT("),
11064            (&runtime.coverage_hit_v2, "HIT("),
11065            (&runtime.select_right_v2, "RIGHT("),
11066            (&runtime.select_short_v2, "SHORT("),
11067        ] {
11068            code = code.replace(&format!("{helper}({probe_file}, "), short);
11069        }
11070        let mut names: Vec<&str> = Vec::new();
11071        let mut shaped = String::new();
11072        let mut rest = code.as_str();
11073        while let Some(at) = rest.find("_supercovSelected") {
11074            shaped.push_str(&rest[..at]);
11075            let tail = &rest[at..];
11076            let length = tail
11077                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
11078                .unwrap_or(tail.len());
11079            let name = &tail[..length];
11080            let index = names
11081                .iter()
11082                .position(|known| *known == name)
11083                .unwrap_or_else(|| {
11084                    names.push(name);
11085                    names.len() - 1
11086                });
11087            shaped.push_str(&format!("s{index}"));
11088            rest = &tail[length..];
11089        }
11090        shaped.push_str(rest);
11091        shaped
11092    }
11093
11094    /// Every tree of up to `leaves` leaves over the three operators, with
11095    /// its selections' points 10 apart.
11096    fn selection_shapes(
11097        leaves: usize,
11098        next_leaf: &mut usize,
11099        next_first: &mut usize,
11100    ) -> Vec<(SelectionShape, usize, usize)> {
11101        if leaves == 1 {
11102            let shape = SelectionShape::Leaf(*next_leaf);
11103            return vec![(shape, *next_leaf + 1, *next_first)];
11104        }
11105        let mut shapes = Vec::new();
11106        for left_leaves in 1..leaves {
11107            for kind in 0..3 {
11108                let first = *next_first;
11109                let mut after_first = first + 10;
11110                for (left, leaf_after_left, first_after_left) in
11111                    selection_shapes(left_leaves, next_leaf, &mut after_first)
11112                {
11113                    let mut leaf = leaf_after_left;
11114                    let mut first_right = first_after_left;
11115                    for (right, leaf_after, first_after) in
11116                        selection_shapes(leaves - left_leaves, &mut leaf, &mut first_right)
11117                    {
11118                        shapes.push((
11119                            SelectionShape::Selection {
11120                                first,
11121                                kind,
11122                                left: Box::new(copy_shape(&left)),
11123                                right: Box::new(right),
11124                            },
11125                            leaf_after,
11126                            first_after,
11127                        ));
11128                    }
11129                }
11130            }
11131        }
11132        shapes
11133    }
11134
11135    fn copy_shape(shape: &SelectionShape) -> SelectionShape {
11136        match shape {
11137            SelectionShape::Leaf(leaf) => SelectionShape::Leaf(*leaf),
11138            SelectionShape::Selection {
11139                first,
11140                kind,
11141                left,
11142                right,
11143            } => SelectionShape::Selection {
11144                first: *first,
11145                kind: *kind,
11146                left: Box::new(copy_shape(left)),
11147                right: Box::new(copy_shape(right)),
11148            },
11149        }
11150    }
11151
11152    /// A leaf's steps, as `LogicalValueTransformer::shape` builds them.
11153    fn leaf_steps(shape: &SelectionShape, path: &[usize], steps: &mut Vec<Vec<usize>>) {
11154        match shape {
11155            SelectionShape::Leaf(_) => steps.push(path.to_vec()),
11156            SelectionShape::Selection {
11157                first,
11158                kind,
11159                left,
11160                right,
11161            } => {
11162                let mut left_path = vec![first * 4 + kind];
11163                left_path.extend_from_slice(path);
11164                let mut right_path = vec![first * 4 + 3];
11165                right_path.extend_from_slice(path);
11166                leaf_steps(left, &left_path, steps);
11167                leaf_steps(right, &right_path, steps);
11168            }
11169        }
11170    }
11171
11172    /// Evaluate a tree over values that are truthy (2), falsy (1) or nullish
11173    /// (0): its value, and the leaves evaluated, in order.
11174    fn evaluate(shape: &SelectionShape, values: &[u8], evaluated: &mut Vec<usize>) -> u8 {
11175        match shape {
11176            SelectionShape::Leaf(leaf) => {
11177                evaluated.push(*leaf);
11178                values[*leaf]
11179            }
11180            SelectionShape::Selection {
11181                kind, left, right, ..
11182            } => {
11183                let value = evaluate(left, values, evaluated);
11184                let short = match kind {
11185                    0 => value == 2,
11186                    1 => value != 2,
11187                    _ => value != 0,
11188                };
11189                if short {
11190                    value
11191                } else {
11192                    evaluate(right, values, evaluated)
11193                }
11194            }
11195        }
11196    }
11197
11198    /// What the runtime records for a leaf's steps and its value.
11199    fn record_steps(steps: &[usize], value: u8, recorded: &mut Vec<usize>) {
11200        for step in steps {
11201            let (first, code) = (step / 4, step % 4);
11202            let truthy = usize::from(value == 2);
11203            if code == 3 {
11204                recorded.push(first + 2 + truthy);
11205            } else if match code {
11206                0 => value == 2,
11207                1 => value != 2,
11208                _ => value != 0,
11209            } {
11210                recorded.push(first + truthy);
11211            } else {
11212                break;
11213            }
11214        }
11215    }
11216
11217    #[test]
11218    fn a_value_tree_records_what_a_call_around_each_leaf_recorded() {
11219        // The points a leaf decided are written out where evaluation goes
11220        // on from it, from the tree's shape alone. Every tree of up to five
11221        // leaves, under every combination of truthy, falsy and nullish
11222        // values, records the points `selectPathV2` recorded around each
11223        // leaf, in the same order.
11224        let mut checked = 0;
11225        for leaves in 2..=5 {
11226            for (shape, ..) in selection_shapes(leaves, &mut 0, &mut 0) {
11227                let mut steps = Vec::new();
11228                leaf_steps(&shape, &[], &mut steps);
11229                let mut before = vec![Vec::new(); leaves];
11230                shape.before(&mut before);
11231                for combination in 0..3_usize.pow(leaves as u32) {
11232                    let values = (0..leaves)
11233                        .map(|leaf| (combination / 3_usize.pow(leaf as u32) % 3) as u8)
11234                        .collect::<Vec<_>>();
11235                    let mut evaluated = Vec::new();
11236                    evaluate(&shape, &values, &mut evaluated);
11237                    let mut each_leaf = Vec::new();
11238                    for leaf in &evaluated {
11239                        record_steps(&steps[*leaf], values[*leaf], &mut each_leaf);
11240                    }
11241                    let mut tree = Vec::new();
11242                    for pair in evaluated.windows(2) {
11243                        // As `recorded_before` writes it: the only leaf that
11244                        // can come before, or the one the frame names.
11245                        let deciders = &before[pair[1]];
11246                        let (_, points) = match &deciders[..] {
11247                            [only] => only,
11248                            several => several
11249                                .iter()
11250                                .find(|(leaf, _)| *leaf == pair[0])
11251                                .expect("the leaf before is one the table has"),
11252                        };
11253                        tree.extend(points);
11254                    }
11255                    let last = *evaluated.last().unwrap();
11256                    record_steps(&steps[last], values[last], &mut tree);
11257                    assert_eq!(tree, each_leaf, "{values:?} over {steps:?}");
11258                    assert!(
11259                        evaluated
11260                            .windows(2)
11261                            .all(|pair| before[pair[1]].iter().any(|(leaf, _)| *leaf == pair[0])),
11262                        "{values:?} over {steps:?}: {before:?}"
11263                    );
11264                    checked += 1;
11265                }
11266            }
11267        }
11268        assert!(checked > 100_000, "{checked}");
11269    }
11270
11271    #[test]
11272    fn a_constant_keeps_its_tree_and_a_named_chain_its_name() {
11273        let shaped = value_shape(
11274            concat!(
11275                "export function text(value: string | number) {\n",
11276                "  const isText = typeof value === 'string' && value.length > 0;\n",
11277                "  if (isText) return value;\n",
11278                "  return null;\n",
11279                "}\n",
11280                "export const both = text(1) ?? text('a');\n",
11281                "export class Owner {\n",
11282                "  pet?: { name: string };\n",
11283                "  named(other?: Owner) { return [other?.pet, this?.pet, this.pet?.name]; }\n",
11284                "}\n",
11285            ),
11286            "src/alias.ts",
11287        );
11288        for expected in [
11289            // TypeScript narrows through a constant only when the condition
11290            // is its initializer: the value is recorded by the statement
11291            // after, from the constant.
11292            "const isText = (s1 = 0, typeof value === \"string\") && (s1 = 1, value.length > 0);\n\tEND2(9, isText, s1);\n",
11293            "export const both = (s0 = 0, text(1)) ?? (s0 = 1, text(\"a\"));\nEND2(13, both, s0);\n",
11294            // A name and `this` are read again after the call that records
11295            // them, so the chain narrows them.
11296            ", other)?.pet",
11297            ", this)?.pet",
11298        ] {
11299            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11300        }
11301        // A member may run a getter: it is read once, through the call.
11302        assert!(shaped.contains(", this.pet)?.name"), "{shaped}");
11303        assert!(!shaped.contains(", this.pet), this.pet)"), "{shaped}");
11304    }
11305
11306    #[test]
11307    fn a_value_tree_keeps_its_leaves_the_programs_own_expressions() {
11308        // With a call around each leaf, `user.name` was read from a `user`
11309        // nothing had narrowed: `any`, when a member of its type has no
11310        // `name`, for everything that reads the value. A leaf is the last
11311        // operand of a comma now, and the call goes around the tree.
11312        let shaped = value_shape(
11313            concat!(
11314                "export function name(user: { name: string } | false) {\n",
11315                "  return user && user.name;\n",
11316                "}\n",
11317                "export function ok(total: number, name: string, force: boolean) {\n",
11318                "  return (total > 0 && name !== 'x') || !!force;\n",
11319                "}\n",
11320                "export function pick(a: number | null, b: number | null, c: number) {\n",
11321                "  return a ?? b ?? c;\n",
11322                "}\n",
11323                "export function held(list: string[] | null, fallback = list || []) {\n",
11324                "  return fallback;\n",
11325                "}\n",
11326            ),
11327            "src/values.ts",
11328        );
11329        for expected in [
11330            "let s0: any;\n",
11331            "return END2(10, (s0 = 0, user) && (s0 = 1, user.name), s0);",
11332            // More than two leaves: the tree's own operators stay between
11333            // them, so a coverage tool the tests run reads one branch, and
11334            // what `total > 0` or `name !== 'x'` decided is recorded where
11335            // evaluation went on to `!!force`, by which of them it left:
11336            // the runtime looks that up, because a choice written out here
11337            // would be a branch of this line to that tool.
11338            "return END(0, (s1 = 0, total > 0) && (s1 = 1, name !== \"x\") || (NEXT(0, s1, 2), s1 = 2, !!force), s1);",
11339            // Only `b` can come before `c`: a first `??` that kept `a` ends
11340            // the second.
11341            "return END(1, (s2 = 0, a) ?? (s2 = 1, b) ?? (HIT(24), s2 = 2, c), s2);",
11342            // A parameter list cannot see a frame its function's body
11343            // declares: the leaves keep their calls there.
11344            "SHORT(30, list, 0) || RIGHT(30, [])",
11345        ] {
11346            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11347        }
11348    }
11349
11350    #[test]
11351    fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
11352        let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
11353        for file in ["app/fill.js", "app/fill.ts"] {
11354            let output = instrument_candidate(source, file).unwrap();
11355            let runtime = output.runtime.expect("candidate runtime binding");
11356            // No frame per evaluation: the target is written by the program's
11357            // own operator, once.
11358            assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
11359            assert!(output.code.contains("||= "), "{}", output.code);
11360            assert!(output.code.contains("??= "), "{}", output.code);
11361            assert!(output.code.contains("&&= "), "{}", output.code);
11362            assert_eq!(
11363                output
11364                    .code
11365                    .matches(&format!("{}(", runtime.select_assign_end_v2))
11366                    .count(),
11367                3
11368            );
11369            assert_eq!(
11370                output
11371                    .code
11372                    .matches(&format!("{}(", runtime.select_named_right_v2))
11373                    .count(),
11374                1
11375            );
11376            let allocator = Allocator::default();
11377            let reparsed = Parser::new(
11378                &allocator,
11379                &output.code,
11380                SourceType::from_path(file).unwrap(),
11381            )
11382            .parse();
11383            assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
11384        }
11385    }
11386
11387    #[test]
11388    fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
11389        let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
11390        let output = instrument_candidate(source, "app/collisions.js").unwrap();
11391        let runtime = output.runtime.expect("candidate runtime binding");
11392        assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
11393        assert!(!output.code.contains("let _supercovMcdcFrame1,"));
11394    }
11395
11396    #[test]
11397    fn instruments_wider_decisions_with_the_exact_v1_fallback() {
11398        let predicate = (0..33)
11399            .map(|index| format!("c{index}"))
11400            .collect::<Vec<_>>()
11401            .join(" && ");
11402        let source = format!("if ({predicate}) work();");
11403        let output = instrument_candidate(&source, "app/wide.js").unwrap();
11404        assert_eq!(output.decisions[0].conditions.len(), 33);
11405        let runtime = output.runtime.expect("candidate runtime binding");
11406        assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
11407        assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
11408        assert!(
11409            output
11410                .code
11411                .contains(&format!("{}(", runtime.mcdc_condition))
11412        );
11413        assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
11414    }
11415
11416    #[test]
11417    fn wraps_framework_request_exports_without_changing_the_public_api() {
11418        for (file, source, export_prefix) in [
11419            (
11420                "app/routes/example.ts",
11421                "export const loader = async ({ request }) => request.url;",
11422                "export const loader = ",
11423            ),
11424            (
11425                "app/routes/example.ts",
11426                "export async function action({ request }) { return request.method; }",
11427                "export const action = ",
11428            ),
11429            (
11430                "app/api/items/route.ts",
11431                "export function GET(request) { return Response.json({ url: request.url }); }",
11432                "export const GET = ",
11433            ),
11434            (
11435                "app/routes/example.ts",
11436                "export { generateAction as action } from './generateAction';",
11437                "export const action = ",
11438            ),
11439            (
11440                "app/entry.server.tsx",
11441                "export default async function handleRequest(request) { return request.url; }",
11442                "export default ",
11443            ),
11444        ] {
11445            let output = instrument_candidate(source, file).unwrap();
11446            let runtime = output.runtime.as_ref().expect("runtime binding");
11447            assert!(
11448                output
11449                    .code
11450                    .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
11451                "{file}: {}",
11452                output.code
11453            );
11454            assert!(
11455                output.code.contains(&format!(
11456                    "withRequestPhase as {}",
11457                    runtime.with_request_phase
11458                )),
11459                "{file}: {}",
11460                output.code
11461            );
11462            let allocator = Allocator::default();
11463            let reparsed = Parser::new(
11464                &allocator,
11465                &output.code,
11466                SourceType::from_path(file).unwrap(),
11467            )
11468            .parse();
11469            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
11470        }
11471    }
11472
11473    #[test]
11474    fn parse_failures_are_explicit_and_never_claim_completeness() {
11475        assert!(matches!(
11476            analyze_candidate("if (", "broken.js"),
11477            Err(CandidateError::Parse(errors)) if !errors.is_empty()
11478        ));
11479        assert!(matches!(
11480            analyze_candidate("let value = 1", "unknown.extension"),
11481            Err(CandidateError::UnknownSourceType(_))
11482        ));
11483    }
11484}