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