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