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/// Puts back, as written, every function whose source text the program reads.
523///
524/// Such a function is left without probes, but the whole file is printed
525/// again from its tree, and the printer lays a function out its own way:
526/// `function f() { return 1; }` came back as `function f() {\n\treturn 1;\n}`,
527/// so `f.toString()` differed under measurement. A function of the output is
528/// matched to its source by the first position inside it the source map says
529/// was printed from somewhere (an arrow's is a parameter, not its start).
530/// Inside a restored function each line maps to its own line of the source.
531fn restore_function_source(
532    program: &Program<'_>,
533    sensitive: &HashSet<SpanKey>,
534    generated: &str,
535    map: oxc_sourcemap::SourceMap,
536) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
537    if sensitive.is_empty() {
538        return Ok((generated.to_string(), map));
539    }
540    let allocator = Allocator::default();
541    let reparsed = Parser::new(&allocator, generated, program.source_type).parse();
542    if !reparsed.errors.is_empty() {
543        return Err(CandidateError::Parse(
544            reparsed
545                .errors
546                .into_iter()
547                .map(|error| error.to_string())
548                .collect(),
549        ));
550    }
551    struct Functions(Vec<Span>);
552    impl<'a> Visit<'a> for Functions {
553        fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
554            self.0.push(function.span);
555            walk::walk_function(self, function, flags);
556        }
557        fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) {
558            self.0.push(arrow.span);
559            walk::walk_arrow_function_expression(self, arrow);
560        }
561    }
562    let mut functions = Functions(Vec::new());
563    functions.visit_program(&reparsed.program);
564    functions.0.sort_by_key(|span| span.start);
565
566    let source_lines = Utf16LineIndex::new(program.source_text);
567    let generated_lines = Utf16LineIndex::new(generated);
568    let mut printed_from = map
569        .get_tokens()
570        .filter_map(|token| {
571            token.get_source_id()?;
572            Some((
573                generated_lines
574                    .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
575                source_lines
576                    .byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
577            ))
578        })
579        .collect::<Vec<_>>();
580    printed_from.sort_unstable();
581    let mut used = HashSet::new();
582    // (generated start, generated end, source start, source end)
583    let mut edits = Vec::<(usize, usize, usize, usize)>::new();
584    for span in &functions.0 {
585        let (start, end) = (span.start as usize, span.end as usize);
586        if edits.last().is_some_and(|edit| start < edit.1) {
587            continue;
588        }
589        let first = printed_from.partition_point(|(generated, _)| *generated < start);
590        let Some(&(_, source)) = printed_from
591            .get(first)
592            .filter(|(generated, _)| *generated < end)
593        else {
594            continue;
595        };
596        // The innermost source-read function holding that position.
597        let Some(&(source_start, source_end)) = sensitive
598            .iter()
599            .filter(|(from, to)| *from as usize <= source && source < *to as usize)
600            .min_by_key(|(from, to)| to - from)
601        else {
602            continue;
603        };
604        if !used.insert((source_start, source_end)) {
605            continue;
606        }
607        edits.push((start, end, source_start as usize, source_end as usize));
608    }
609    if edits.is_empty() {
610        return Ok((generated.to_string(), map));
611    }
612
613    let mut restored = String::with_capacity(generated.len());
614    let mut cursor = 0;
615    // Where each edit landed in the restored text.
616    let mut placed = Vec::with_capacity(edits.len());
617    for (start, end, source_start, source_end) in &edits {
618        restored.push_str(&generated[cursor..*start]);
619        placed.push(restored.len());
620        restored.push_str(&program.source_text[*source_start..*source_end]);
621        cursor = *end;
622    }
623    restored.push_str(&generated[cursor..]);
624
625    let restored_lines = Utf16LineIndex::new(&restored);
626    let shifted = |offset: usize| {
627        let before = edits.partition_point(|edit| edit.1 <= offset);
628        let shift = edits[..before]
629            .iter()
630            .map(|(start, end, source_start, source_end)| {
631                (source_end - source_start) as isize - (end - start) as isize
632            })
633            .sum::<isize>();
634        offset.saturating_add_signed(shift)
635    };
636    let mut tokens = Vec::new();
637    for token in map.get_tokens() {
638        let offset = generated_lines
639            .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
640        // What the printer wrote inside a restored function is gone.
641        if edits
642            .iter()
643            .any(|(start, end, _, _)| *start < offset && offset < *end)
644        {
645            continue;
646        }
647        let (dst_line, dst_col) = restored_lines.line_col(shifted(offset));
648        tokens.push(oxc_sourcemap::Token::new(
649            dst_line,
650            dst_col,
651            token.get_src_line(),
652            token.get_src_col(),
653            token.get_source_id(),
654            token.get_name_id(),
655        ));
656    }
657    let source_id = map.get_tokens().find_map(|token| token.get_source_id());
658    for ((_, _, source_start, source_end), at) in edits.iter().zip(&placed) {
659        let text = &program.source_text[*source_start..*source_end];
660        let mut line_start = 0;
661        for line in text.split_inclusive('\n') {
662            if line_start > 0 {
663                let indent = line.len() - line.trim_start_matches([' ', '\t']).len();
664                let (dst_line, dst_col) = restored_lines.line_col(at + line_start + indent);
665                let (src_line, src_col) = source_lines.line_col(source_start + line_start + indent);
666                tokens.push(oxc_sourcemap::Token::new(
667                    dst_line, dst_col, src_line, src_col, source_id, None,
668                ));
669            }
670            line_start += line.len();
671        }
672    }
673    tokens.sort_by_key(|token| (token.get_dst_line(), token.get_dst_col()));
674    let map = oxc_sourcemap::SourceMap::new(
675        map.get_file().cloned(),
676        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
677        map.get_source_root().map(str::to_string),
678        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
679        map.get_source_contents()
680            .map(|content| content.cloned())
681            .collect::<Vec<Option<Arc<str>>>>(),
682        tokens.into_boxed_slice(),
683        None,
684    );
685    Ok((restored, map))
686}
687
688/// Where a statement Supercov generated inside a function or block starts, in
689/// the source map: past the source's end, where no node of the program starts
690/// and no comment is attached (a comment ending the file is attached to its
691/// length, and oxc prints a comment where its position is printed), rewritten
692/// to an unmapped segment once the code is out. `None` when the source's last
693/// line has non-ASCII text, where oxc's column table has no entry that far;
694/// those files keep the generated statements unmapped-by-omission, as before.
695fn generated_statement_span(source: &str) -> Option<Span> {
696    let last_line = source.rfind('\n').map_or(0, |newline| newline + 1);
697    let length = u32::try_from(source.len()).ok()?;
698    (source[last_line..].is_ascii() && length < u32::MAX - 2)
699        .then(|| Span::new(length + 1, length + 2))
700}
701
702/// A coverage tool the tests run itself (c8, nyc, Node's `--test-coverage`,
703/// Jest, Vitest) reads the instrumented copy through its source map. oxc maps
704/// a node only when its start differs from the last one it mapped, and gives a
705/// generated node none, so a probe inherited the mapping before it: V8 counted
706/// a probe's `||` as a branch of the user's line, and the statement after a
707/// block Supercov added (whose brace took the statement's own start) lost its
708/// mapping, and with it its place in istanbul's report. Each such statement
709/// now starts at `generated_statement_span`, which this makes unmapped: the
710/// probe maps to nothing, and the user's statement after it maps again.
711///
712/// A block Supercov wraps around a statement takes that statement's span, so
713/// its closing brace maps to the statement's last character, where the
714/// statement's own last token or an inner block's brace often already mapped:
715/// the brace then went unmapped, and the enclosing `if` or function, ending
716/// there, left istanbul's report. An empty statement at the unmapped span
717/// before the brace lets it map again.
718struct GeneratedStatementSpans<'a, 's> {
719    ast: AstBuilder<'a>,
720    source: &'s str,
721    span: Span,
722    depth: usize,
723}
724
725impl GeneratedStatementSpans<'_, '_> {
726    /// A block or body Supercov made around the user's code, rather than one
727    /// the user wrote: its span starts somewhere other than a `{`.
728    fn borrowed(&self, span: Span) -> bool {
729        !span.is_empty()
730            && span != self.span
731            && self.source.as_bytes().get(span.start as usize) != Some(&b'{')
732    }
733}
734
735impl<'a> VisitMut<'a> for GeneratedStatementSpans<'a, '_> {
736    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
737        // Top-level ones stay unmapped by omission: the file's first mapping
738        // is then the user's, which Node's coverage takes for a range starting
739        // at the top of the file.
740        if self.depth > 0 && statement.span().is_empty() {
741            *statement.span_mut() = self.span;
742        }
743        self.depth += 1;
744        walk_mut::walk_statement(self, statement);
745        self.depth -= 1;
746    }
747
748    fn visit_block_statement(&mut self, block: &mut oxc_ast::ast::BlockStatement<'a>) {
749        if self.depth > 0 && block.span.is_empty() {
750            block.span = self.span;
751        } else if self.borrowed(block.span)
752            // Not after a jump: `allowUnreachableCode: false` would reject it
753            // in a TypeScript build of the instrumented copy.
754            && !matches!(
755                block.body.last(),
756                Some(
757                    Statement::ReturnStatement(_)
758                        | Statement::ThrowStatement(_)
759                        | Statement::BreakStatement(_)
760                        | Statement::ContinueStatement(_)
761                )
762            )
763        {
764            block.body.push(self.ast.statement_empty(self.span));
765        }
766        walk_mut::walk_block_statement(self, block);
767    }
768
769    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
770        // An arrow's expression body made a block: its brace maps just past
771        // the expression, where nothing inside it mapped.
772        if self.borrowed(body.span) && (body.span.end as usize) < self.source.len() {
773            body.span.end += 1;
774        }
775        self.depth += 1;
776        walk_mut::walk_function_body(self, body);
777        self.depth -= 1;
778    }
779}
780
781/// Node's own coverage looks a range's start up on the wrong line half the
782/// time when the range starts a line: with a source map, a line of the
783/// generated file ends, for it, where the next one starts, and its search
784/// takes whichever of the two it reaches first, which the length of the file
785/// decides. From the line before, it reads one column past that line's end,
786/// which gives the mapping before it: a top-level function was reported a
787/// line early, or lost its range and with it a branch, where the same file a
788/// line longer or shorter was reported right.
789///
790/// Only a function or class declaration at the top level starts a range at
791/// column 0. Each gets an empty statement on the line before it, which
792/// `map_closing_lines` maps to where the declaration starts: both lines then
793/// give the same answer, and nothing else is ever looked up on a line that
794/// holds only `;`.
795fn separate_top_level_declarations<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>, span: Span) {
796    let starts_a_range = |statement: &Statement<'a>| match statement {
797        Statement::FunctionDeclaration(function) => !function.declare,
798        Statement::ClassDeclaration(class) => !class.declare,
799        _ => false,
800    };
801    // Overload signatures stay next to their implementation.
802    let signature = |statement: &Statement<'a>| matches!(statement, Statement::FunctionDeclaration(function) if function.body.is_none());
803    let mut separated = ast.vec_with_capacity(program.body.len());
804    let mut after_signature = false;
805    for statement in program.body.take_in(ast.allocator) {
806        if starts_a_range(&statement) && !after_signature {
807            separated.push(ast.statement_empty(span));
808        }
809        after_signature = signature(&statement);
810        separated.push(statement);
811    }
812    program.body = separated;
813}
814
815fn mark_generated_statements<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>) {
816    let Some(span) = generated_statement_span(program.source_text) else {
817        return;
818    };
819    separate_top_level_declarations(ast, program, span);
820    GeneratedStatementSpans {
821        ast,
822        source: program.source_text,
823        span,
824        depth: 0,
825    }
826    .visit_program(program);
827}
828
829fn unmap_generated_statements(
830    map: oxc_sourcemap::SourceMap,
831    source: &str,
832) -> oxc_sourcemap::SourceMap {
833    let Some(span) = generated_statement_span(source) else {
834        return map;
835    };
836    let line = source.matches('\n').count() as u32;
837    let column = span.start - source.rfind('\n').map_or(0, |newline| newline as u32 + 1);
838    let is_generated = |token: &oxc_sourcemap::Token| {
839        token.get_src_line() == line && token.get_src_col() == column
840    };
841    if !map.get_tokens().any(|token| is_generated(&token)) {
842        return map;
843    }
844    let tokens = map
845        .get_tokens()
846        .map(|token| {
847            if is_generated(&token) {
848                oxc_sourcemap::Token::new(
849                    token.get_dst_line(),
850                    token.get_dst_col(),
851                    0,
852                    0,
853                    None,
854                    None,
855                )
856            } else {
857                token
858            }
859        })
860        .collect::<Vec<_>>();
861    oxc_sourcemap::SourceMap::new(
862        map.get_file().cloned(),
863        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
864        map.get_source_root().map(str::to_string),
865        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
866        map.get_source_contents()
867            .map(|content| content.cloned())
868            .collect::<Vec<Option<Arc<str>>>>(),
869        tokens.into_boxed_slice(),
870        None,
871    )
872}
873
874/// A line of the instrumented copy starting with a closing brace Supercov
875/// added (a wrapper's `try` block, `} finally {`) has no mapping at that
876/// brace, and istanbul, c8 and v8-to-istanbul look a position up on its own
877/// line only: a V8 range or a statement ending there was dropped from their
878/// reports. Node's own coverage takes the nearest mapping before it instead;
879/// so does each such brace now, with a copy of that mapping, unmapped if it
880/// was.
881///
882/// oxc maps where a node starts, never a statement's closing `;`, so a range
883/// ending with `break;` ended, for Node's own coverage, at the `b`: the line
884/// was no longer inside the range of the `case` no test reached, and read as
885/// covered. When the text after a line's last mapping is the source's own
886/// text after that position, ending in `;`, the `;` maps to the source's.
887fn map_closing_lines(
888    map: oxc_sourcemap::SourceMap,
889    code: &str,
890    source: &str,
891) -> oxc_sourcemap::SourceMap {
892    let source_lines = source.split('\n').collect::<Vec<_>>();
893    let tokens = map.get_tokens().collect::<Vec<_>>();
894    let mut patched = Vec::with_capacity(tokens.len());
895    let mut next = 0;
896    let mut previous: Option<oxc_sourcemap::Token> = None;
897    let mut added = false;
898    for (line, text) in code.split('\n').enumerate() {
899        let line = line as u32;
900        let first = next;
901        while next < tokens.len() && tokens[next].get_dst_line() == line {
902            next += 1;
903        }
904        // The line `separate_top_level_declarations` put before a
905        // declaration maps to where that declaration starts.
906        if text == ";"
907            && next == first + 1
908            && tokens[first].get_source_id().is_none()
909            && let Some(declaration) = tokens.get(next)
910            && declaration.get_dst_col() == 0
911            && declaration.get_source_id().is_some()
912        {
913            let separator = oxc_sourcemap::Token::new(
914                line,
915                0,
916                declaration.get_src_line(),
917                declaration.get_src_col(),
918                declaration.get_source_id(),
919                None,
920            );
921            patched.push(separator);
922            previous = Some(separator);
923            added = true;
924            continue;
925        }
926        let column = (text.len() - text.trim_start().len()) as u32;
927        if text.trim_start().starts_with(['}', ')', ']'])
928            && (first == next || tokens[first].get_dst_col() > column)
929            && let Some(previous) = &previous
930        {
931            patched.push(oxc_sourcemap::Token::new(
932                line,
933                column,
934                previous.get_src_line(),
935                previous.get_src_col(),
936                previous.get_source_id(),
937                None,
938            ));
939            added = true;
940        }
941        patched.extend_from_slice(&tokens[first..next]);
942        if next > first {
943            let last = tokens[next - 1];
944            previous = Some(last);
945            if let (Some(_), Some(original)) = (
946                last.get_source_id(),
947                source_lines.get(last.get_src_line() as usize),
948            ) && text.is_ascii()
949                && original.is_ascii()
950            {
951                let generated_rest = text
952                    .get(last.get_dst_col() as usize..)
953                    .unwrap_or("")
954                    .trim_end();
955                let original_rest = original
956                    .get(last.get_src_col() as usize..)
957                    .unwrap_or("")
958                    .trim_end();
959                if generated_rest.len() > 1
960                    && generated_rest.ends_with(';')
961                    && original_rest.starts_with(generated_rest)
962                {
963                    let offset = (generated_rest.len() - 1) as u32;
964                    let end = oxc_sourcemap::Token::new(
965                        line,
966                        last.get_dst_col() + offset,
967                        last.get_src_line(),
968                        last.get_src_col() + offset,
969                        last.get_source_id(),
970                        None,
971                    );
972                    patched.push(end);
973                    previous = Some(end);
974                    added = true;
975                }
976            }
977        }
978    }
979    patched.extend_from_slice(&tokens[next..]);
980    if !added {
981        return map;
982    }
983    oxc_sourcemap::SourceMap::new(
984        map.get_file().cloned(),
985        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
986        map.get_source_root().map(str::to_string),
987        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
988        map.get_source_contents()
989            .map(|content| content.cloned())
990            .collect::<Vec<Option<Arc<str>>>>(),
991        patched.into_boxed_slice(),
992        None,
993    )
994}
995
996fn generate_candidate(
997    program: &Program<'_>,
998    file: &str,
999    source_sensitive_functions: &HashSet<SpanKey>,
1000) -> Result<(String, Option<serde_json::Value>), CandidateError> {
1001    let options = CodegenOptions {
1002        source_map_path: Some(Path::new(file).to_path_buf()),
1003        ..CodegenOptions::default()
1004    };
1005    let generated = Codegen::new().with_options(options).build(program);
1006    // Unmapped before comments are restored: a restored comment goes where
1007    // the first mapping at or after it in the source was printed, and the
1008    // generated statements' placeholder is past every comment.
1009    let map = unmap_generated_statements(
1010        generated.map.expect("source maps are enabled"),
1011        program.source_text,
1012    );
1013    let (code, map) = restore_comment_text(program, &generated.code, map)?;
1014    let (code, map) = restore_function_source(program, source_sensitive_functions, &code, map)?;
1015    let map = map_closing_lines(map, &code, program.source_text);
1016    let map = Some({
1017        serde_json::from_str(&map.to_json_string())
1018            .expect("oxc must serialize its own generated source map")
1019    });
1020    Ok((code, map))
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct CandidateRuntime {
1026    pub coverage_hit: String,
1027    pub mcdc_begin: String,
1028    pub mcdc_condition: String,
1029    pub mcdc_end: String,
1030    pub register_probe_v2: String,
1031    pub mcdc_end_v2: String,
1032    pub coverage_hit_v2: String,
1033    pub probe_file_v2: String,
1034    pub parenthesized_assignment_value: String,
1035    pub with_request_phase: String,
1036    pub optional_select_v2: String,
1037    pub select_short_v2: String,
1038    pub select_right_v2: String,
1039    pub select_named_right_v2: String,
1040    pub select_assign_end_v2: String,
1041    pub select_path_v2: String,
1042    pub select_end2_v2: String,
1043    pub select_end_v2: String,
1044    pub select_next_v2: String,
1045    pub rendered_value_v2: String,
1046    pub optional_call_end_v2: String,
1047    pub default_selected_v2: String,
1048    pub default_entered_v2: String,
1049}
1050
1051#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1052#[serde(rename_all = "camelCase")]
1053pub struct CandidateLimitation {
1054    pub id: String,
1055    pub kind: String,
1056    pub file: String,
1057    pub line: usize,
1058    pub column: usize,
1059    pub source: String,
1060    pub reason: String,
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Eq)]
1064pub enum CandidateError {
1065    UnknownSourceType(String),
1066    Parse(Vec<String>),
1067    CommentPreservation {
1068        expected: usize,
1069        actual: usize,
1070        detail: String,
1071    },
1072}
1073
1074impl std::fmt::Display for CandidateError {
1075    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1076        match self {
1077            Self::UnknownSourceType(file) => {
1078                write!(formatter, "no JavaScript source type for {file}")
1079            }
1080            Self::Parse(messages) => write!(formatter, "{}", messages.join("; ")),
1081            Self::CommentPreservation {
1082                expected,
1083                actual,
1084                detail,
1085            } => write!(
1086                formatter,
1087                "{expected} comments in the source, {actual} after instrumenting: {detail}"
1088            ),
1089        }
1090    }
1091}
1092
1093/// A parser error as a person reads it: the message, and where.
1094fn parse_message(source: &str, message: &str, offset: Option<usize>) -> String {
1095    match offset.filter(|offset| *offset <= source.len()) {
1096        Some(offset) => {
1097            let (line, column) = line_and_utf16_column(source, offset);
1098            format!("{message} at line {line}, column {}", column + 1)
1099        }
1100        None => message.to_owned(),
1101    }
1102}
1103
1104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1105enum RuntimeBinding {
1106    ModuleImport,
1107    DirectGlobal,
1108}
1109
1110const NODE_ASSERT_MODULES: &[&str] = &[
1111    "assert",
1112    "assert/strict",
1113    "node:assert",
1114    "node:assert/strict",
1115];
1116const NODE_ASSERT_METHODS: &[&str] = &[
1117    "deepEqual",
1118    "deepStrictEqual",
1119    "doesNotMatch",
1120    "doesNotReject",
1121    "doesNotThrow",
1122    "equal",
1123    "fail",
1124    "ifError",
1125    "match",
1126    "notDeepEqual",
1127    "notDeepStrictEqual",
1128    "notEqual",
1129    "notStrictEqual",
1130    "ok",
1131    "partialDeepStrictEqual",
1132    "rejects",
1133    "strictEqual",
1134    "throws",
1135];
1136
1137#[derive(Debug, Clone, PartialEq, Eq)]
1138pub struct NodeAssertionInstrumentation {
1139    pub code: String,
1140    pub assertions: usize,
1141    pub capability_imports: usize,
1142}
1143
1144const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
1145    &["@jest/globals", "@playwright/test", "playwright", "vitest"];
1146
1147fn capability_source_candidate(source: &str) -> bool {
1148    // A capability wrapper is useful only when an argument can establish a
1149    // host/guest workspace mapping. Generic words such as `snapshot`,
1150    // `machine`, `acquire` and `spawn` are common in tests and application
1151    // code; treating any of them as sufficient wrapped unrelated framework
1152    // registration functions and changed their captured callsite.
1153    let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
1154        || (source.contains("hostRoot") && source.contains("guestRoot"));
1155    // The guest path is commonly computed (for example
1156    // `resolve(tmpdir(), "workspace")`), so requiring the literal
1157    // `/workspace` loses genuine opaque launchers. The nested mount shape is
1158    // already specific, and the AST pass below still restricts wrapping to
1159    // the imported root actually called with that argument.
1160    let mount_mapping = (source.contains("mounts") || source.contains("Mounts"))
1161        && source.contains("source")
1162        && source.contains("target");
1163    direct_mapping || mount_mapping
1164}
1165
1166fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
1167    source == wrapper
1168        || source.starts_with("node:")
1169        || source.starts_with("virtual:supercov-")
1170        || source.contains(".supercov/")
1171        || CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
1172}
1173
1174fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
1175    match expression {
1176        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1177        Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
1178        Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
1179        Expression::CallExpression(call) => capability_callee_root(&call.callee),
1180        // `new GenericContainer(..).withBindMounts(..)`: the builder's class.
1181        Expression::NewExpression(construction) => capability_callee_root(&construction.callee),
1182        Expression::ParenthesizedExpression(parenthesized) => {
1183            capability_callee_root(&parenthesized.expression)
1184        }
1185        Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
1186        Expression::TSSatisfiesExpression(expression) => {
1187            capability_callee_root(&expression.expression)
1188        }
1189        Expression::TSNonNullExpression(expression) => {
1190            capability_callee_root(&expression.expression)
1191        }
1192        Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
1193        _ => None,
1194    }
1195}
1196
1197struct CapabilityCallCollector<'s> {
1198    source: &'s str,
1199    mapping_variables: HashSet<String>,
1200    roots: HashSet<String>,
1201}
1202
1203impl CapabilityCallCollector<'_> {
1204    fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
1205        if let Argument::Identifier(identifier) = argument
1206            && self.mapping_variables.contains(identifier.name.as_str())
1207        {
1208            return true;
1209        }
1210        capability_source_candidate(source_slice(self.source, argument.span()))
1211    }
1212}
1213
1214impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
1215    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
1216        if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
1217            (&declarator.id, &declarator.init)
1218            && capability_source_candidate(source_slice(self.source, initializer.span()))
1219        {
1220            self.mapping_variables.insert(identifier.name.to_string());
1221        }
1222        walk::walk_variable_declarator(self, declarator);
1223    }
1224
1225    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1226        // A builder method named for mounts (`withBindMounts`) takes the
1227        // mounts themselves, `[{ source, target }]`, with no `mounts` key.
1228        let mounting = matches!(&call.callee, Expression::StaticMemberExpression(member)
1229            if member.property.name.to_ascii_lowercase().contains("mount"));
1230        let mounted = |argument: &Argument<'_>| {
1231            let text = source_slice(self.source, argument.span());
1232            mounting && text.contains("source") && text.contains("target")
1233        };
1234        if call
1235            .arguments
1236            .iter()
1237            .any(|argument| self.argument_has_mapping(argument) || mounted(argument))
1238            && let Some(root) = capability_callee_root(&call.callee)
1239        {
1240            self.roots.insert(root);
1241        }
1242        walk::walk_call_expression(self, call);
1243    }
1244}
1245
1246fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
1247    let mut collector = CapabilityCallCollector {
1248        source,
1249        mapping_variables: HashSet::new(),
1250        roots: HashSet::new(),
1251    };
1252    // Mapping variables must be known before calls are inspected because a
1253    // declaration may appear after a function that closes over it.
1254    collector.visit_program(program);
1255    let mapping_variables = collector.mapping_variables.clone();
1256    collector.roots.clear();
1257    collector.mapping_variables = mapping_variables;
1258    collector.visit_program(program);
1259    collector.roots
1260}
1261
1262/// One change to a test file's text: `text` replaces `start..end`, an empty
1263/// range for an insertion. Insertions at one offset are ordered by `rank`, so
1264/// a wrapper that encloses another opens before it and closes after it.
1265struct TextEdit {
1266    start: usize,
1267    end: usize,
1268    rank: i64,
1269    text: String,
1270}
1271
1272impl TextEdit {
1273    fn insert(at: usize, rank: i64, text: String) -> Self {
1274        Self {
1275            start: at,
1276            end: at,
1277            rank,
1278            text,
1279        }
1280    }
1281
1282    fn replace(start: usize, end: usize, text: String) -> Self {
1283        Self {
1284            start,
1285            end,
1286            rank: 0,
1287            text,
1288        }
1289    }
1290}
1291
1292/// Apply edits to a test file, keeping every line of it where it was.
1293///
1294/// A test file used to be reprinted from its syntax tree to add assertion
1295/// wrappers, which moved lines and reflowed literals: a `@ts-expect-error`
1296/// above `period: { period: 'fortnight', amount: 1 }` then sat above a line
1297/// with no error, and the error surfaced on the next one (TS2578, TS2322).
1298/// Edits never add or remove a line break, so a directive, a comment and a
1299/// stack-trace line number all stay where the author put them. The source map
1300/// maps every copied run of the original back to itself.
1301/// What a character is to a position in a line: part of a word, a mark of
1302/// its own, or the space between.
1303#[derive(Clone, Copy, PartialEq, Eq)]
1304enum TokenClass {
1305    Word,
1306    Mark,
1307    Space,
1308}
1309
1310impl TokenClass {
1311    fn of(unit: char) -> Self {
1312        if unit.is_whitespace() {
1313            Self::Space
1314        } else if unit.is_alphanumeric() || unit == '_' || unit == '$' {
1315            Self::Word
1316        } else {
1317            Self::Mark
1318        }
1319    }
1320}
1321
1322/// Whether a word can be the name of something: a compiler names a segment
1323/// for an identifier, never for a keyword or a number. Naming `await` printed
1324/// a test's own frame as `at Object.await`.
1325fn names_something(word: &str) -> bool {
1326    const KEYWORDS: &[&str] = &[
1327        "as",
1328        "async",
1329        "await",
1330        "break",
1331        "case",
1332        "catch",
1333        "class",
1334        "const",
1335        "continue",
1336        "debugger",
1337        "default",
1338        "delete",
1339        "do",
1340        "else",
1341        "enum",
1342        "export",
1343        "extends",
1344        "false",
1345        "finally",
1346        "for",
1347        "from",
1348        "function",
1349        "if",
1350        "implements",
1351        "import",
1352        "in",
1353        "instanceof",
1354        "interface",
1355        "let",
1356        "new",
1357        "null",
1358        "of",
1359        "package",
1360        "private",
1361        "protected",
1362        "public",
1363        "return",
1364        "static",
1365        "super",
1366        "switch",
1367        "this",
1368        "throw",
1369        "true",
1370        "try",
1371        "type",
1372        "typeof",
1373        "var",
1374        "void",
1375        "while",
1376        "with",
1377        "yield",
1378    ];
1379    !word.starts_with(|first: char| first.is_ascii_digit()) && !KEYWORDS.contains(&word)
1380}
1381
1382fn apply_text_edits(
1383    source: &str,
1384    mut edits: Vec<TextEdit>,
1385    file: &str,
1386) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
1387    edits.sort_by_key(|edit| (edit.start, edit.end > edit.start, edit.rank));
1388    let mut builder = oxc_sourcemap::SourceMapBuilder::default();
1389    let source_id = builder.set_source_and_content(file, source);
1390    let mut code = String::with_capacity(source.len() + edits.len() * 64);
1391    let (mut line, mut column) = (0u32, 0u32);
1392    let (mut source_line, mut source_column) = (0u32, 0u32);
1393    let advance = |text: &str, line: &mut u32, column: &mut u32| {
1394        for unit in text.chars() {
1395            if unit == '\n' {
1396                *line += 1;
1397                *column = 0;
1398            } else {
1399                *column += unit.len_utf16() as u32;
1400            }
1401        }
1402    };
1403    let copy = |from: usize,
1404                to: usize,
1405                code: &mut String,
1406                line: &mut u32,
1407                column: &mut u32,
1408                source_line: &mut u32,
1409                source_column: &mut u32,
1410                builder: &mut oxc_sourcemap::SourceMapBuilder| {
1411        for (index, piece) in source[from..to].split('\n').enumerate() {
1412            if index > 0 {
1413                code.push('\n');
1414                *line += 1;
1415                *column = 0;
1416                *source_line += 1;
1417                *source_column = 0;
1418            }
1419            if !piece.is_empty() {
1420                builder.add_token(
1421                    *line,
1422                    *column,
1423                    *source_line,
1424                    *source_column,
1425                    Some(source_id),
1426                    None,
1427                );
1428                // A position reads as its segment's start, so one mapping for
1429                // a line put every frame of a test file in column 1, and the
1430                // one inside a wrapped assertion at the statement's start:
1431                // Vitest printed `:12:7` where it prints `:12:35` without
1432                // Supercov. Each word and each mark starts a segment.
1433                // A word also carries its own text as the segment's name:
1434                // a compiler that reads this map keeps a name only where the
1435                // map it reads has one, and Jest names a frame by it. With
1436                // @swc/jest `at toBe (a.test.ts:12:35)` lost its `toBe`.
1437                let (mut offset, mut before) = (0u32, TokenClass::Space);
1438                for (index, unit) in piece.char_indices() {
1439                    let class = TokenClass::of(unit);
1440                    if offset > 0
1441                        && class != TokenClass::Space
1442                        && (class != before || class == TokenClass::Mark)
1443                    {
1444                        let name = (class == TokenClass::Word)
1445                            .then(|| {
1446                                let rest = &piece[index..];
1447                                let end = rest
1448                                    .find(|next| TokenClass::of(next) != TokenClass::Word)
1449                                    .unwrap_or(rest.len());
1450                                &rest[..end]
1451                            })
1452                            .filter(|word| names_something(word))
1453                            .map(|word| builder.add_name(word));
1454                        builder.add_token(
1455                            *line,
1456                            *column + offset,
1457                            *source_line,
1458                            *source_column + offset,
1459                            Some(source_id),
1460                            name,
1461                        );
1462                    }
1463                    before = class;
1464                    offset += unit.len_utf16() as u32;
1465                }
1466                code.push_str(piece);
1467                *column += offset;
1468                *source_column += offset;
1469            }
1470        }
1471    };
1472    let mut at = 0;
1473    for edit in edits {
1474        if edit.start < at || edit.end > source.len() || edit.start > edit.end {
1475            return Err(CandidateError::Parse(vec![format!(
1476                "overlapping assertion edits in {file}"
1477            )]));
1478        }
1479        copy(
1480            at,
1481            edit.start,
1482            &mut code,
1483            &mut line,
1484            &mut column,
1485            &mut source_line,
1486            &mut source_column,
1487            &mut builder,
1488        );
1489        let removed = &source[edit.start..edit.end];
1490        if edit.text.matches('\n').count() != removed.matches('\n').count() {
1491            return Err(CandidateError::Parse(vec![format!(
1492                "an assertion edit in {file} would move a line"
1493            )]));
1494        }
1495        code.push_str(&edit.text);
1496        advance(&edit.text, &mut line, &mut column);
1497        advance(removed, &mut source_line, &mut source_column);
1498        at = edit.end;
1499    }
1500    copy(
1501        at,
1502        source.len(),
1503        &mut code,
1504        &mut line,
1505        &mut column,
1506        &mut source_line,
1507        &mut source_column,
1508        &mut builder,
1509    );
1510    Ok((code, builder.into_sourcemap()))
1511}
1512
1513/// The runtime's assertion helpers as a TypeScript test sees them, declared
1514/// once at the end of each instrumented file.
1515///
1516/// Reading `globalThis.__SUPERCOV_DIRECT_RUNTIME__` without a declaration is
1517/// an implicit `any` index on `typeof globalThis`, which `tsc --strict` rejects
1518/// (TS7017) at every wrapped assertion; Actual's browser build failed on 66 of
1519/// them. A wrapped assertion keeps its own result type, and a bound matcher
1520/// keeps the method's own signature, overloads included, as `T[K]`: inferring
1521/// it through a conditional type made generic matchers `never` (TS2349,
1522/// TS2769). Every instrumented file declares the same type, which TypeScript
1523/// merges.
1524const ASSERTION_RUNTIME_TYPE: &str = "{ \
1525    withNodeAssertionPhase<T>(operation: string, source: string, callback: () => T): T; \
1526    openNodeAssertionPhase(operation: string, source: string): { close<T>(value: T): T; close(): void; fail(error: unknown): unknown }; \
1527    bindNodeAssertionPhase<T>(operation: string, source: string, target: T, property: null): T; \
1528    bindNodeAssertionPhase<T, K extends keyof T>(operation: string, source: string, target: T, property: K): T[K]; \
1529    bindNodeAssertionPhase(operation: string, source: string, target: unknown, property: PropertyKey): any; \
1530    }";
1531
1532const ASSERTION_RUNTIME: &str = "globalThis.__SUPERCOV_DIRECT_RUNTIME__";
1533
1534/// Where each recognised assertion is wrapped or bound, as text edits.
1535struct AssertionEdits<'s> {
1536    source: &'s str,
1537    sites: HashMap<SpanKey, (String, String, bool)>,
1538    edits: Vec<TextEdit>,
1539    /// In TypeScript the call runs in place, under these two names, instead
1540    /// of in a callback, which TypeScript narrows nothing into.
1541    in_place: Option<(String, String)>,
1542    /// The statement that is the body of the arrow being walked, written
1543    /// without braces (`(page) => expect(page).toHaveURL(url)`). It is the
1544    /// arrow's value, so it stays an expression: wrapped as a statement it
1545    /// became a block that returned nothing.
1546    arrow_body: Option<SpanKey>,
1547}
1548
1549impl AssertionEdits<'_> {
1550    fn opening(helper: &str, operation: &str, source: &str) -> String {
1551        format!(
1552            "{ASSERTION_RUNTIME}.{helper}({}, {}, ",
1553            serde_json::to_string(operation).expect("a string serializes"),
1554            serde_json::to_string(source).expect("a string serializes")
1555        )
1556    }
1557
1558    fn open(operation: &str, source: &str) -> String {
1559        format!(
1560            "{ASSERTION_RUNTIME}.openNodeAssertionPhase({}, {})",
1561            serde_json::to_string(operation).expect("a string serializes"),
1562            serde_json::to_string(source).expect("a string serializes")
1563        )
1564    }
1565
1566    /// Open the assertion phase around the call, before its arguments run.
1567    ///
1568    /// In TypeScript the call stays in its own flow: an immediately invoked
1569    /// arrow, which TypeScript narrows into, opens the phase as its argument.
1570    fn wrap(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1571        let (start, end) = (call.span.start as usize, call.span.end as usize);
1572        let width = (end - start) as i64;
1573        let (opening, closing) = match &self.in_place {
1574            Some((phase, error)) => (
1575                format!("(({phase}) => {{ try {{ return {phase}.close("),
1576                format!(
1577                    "); }} catch ({error}) {{ throw {phase}.fail({error}); }} }})({})",
1578                    Self::open(operation, source)
1579                ),
1580            ),
1581            None => (
1582                format!(
1583                    "{}() => (",
1584                    Self::opening("withNodeAssertionPhase", operation, source)
1585                ),
1586                "))".into(),
1587            ),
1588        };
1589        self.edits.push(TextEdit::insert(start, -width, opening));
1590        self.edits.push(TextEdit::insert(end, width, closing));
1591    }
1592
1593    /// Open the assertion phase around a TypeScript statement that is the
1594    /// call, leaving the call a statement: an `asserts` signature narrows
1595    /// only from an expression statement. `finally` closes the phase, so a
1596    /// call typed `never` leaves nothing after it unreachable.
1597    fn wrap_statement(
1598        &mut self,
1599        statement: &ExpressionStatement<'_>,
1600        operation: &str,
1601        source: &str,
1602    ) {
1603        let Some((phase, error)) = &self.in_place else {
1604            return;
1605        };
1606        let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1607        let width = (end - start) as i64;
1608        let opening = format!(
1609            "{{ const {phase} = {}; try {{ ",
1610            Self::open(operation, source)
1611        );
1612        let closing = format!(
1613            " }} catch ({error}) {{ throw {phase}.fail({error}); }} finally {{ {phase}.close(); }} }}"
1614        );
1615        self.edits.push(TextEdit::insert(start, -width, opening));
1616        self.edits.push(TextEdit::insert(end, width, closing));
1617    }
1618
1619    /// Bind a TypeScript statement's callee to a local typed as the callee
1620    /// itself and call the local. An `asserts` signature narrows only through
1621    /// an identifier or a qualified name, explicitly typed (TS2776, TS2775),
1622    /// and a bound call is neither: ts-node refused every such test file. The
1623    /// phase still opens only when the call is made, after its awaited
1624    /// operands, as with any bound call.
1625    fn bind_statement(
1626        &mut self,
1627        statement: &ExpressionStatement<'_>,
1628        call: &CallExpression<'_>,
1629        callee: &str,
1630        operation: &str,
1631        source: &str,
1632    ) {
1633        let Some((local, _)) = self.in_place.clone() else {
1634            return;
1635        };
1636        let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1637        let width = (end - start) as i64;
1638        self.edits.push(TextEdit::insert(
1639            start,
1640            -width,
1641            format!("{{ const {local}: typeof {callee} = "),
1642        ));
1643        self.bind(call, operation, source);
1644        self.edits.push(TextEdit::insert(
1645            call.callee.span().end as usize,
1646            width,
1647            format!("; {local}"),
1648        ));
1649        self.edits.push(TextEdit::insert(end, width, " }".into()));
1650    }
1651
1652    /// Bind the original callee and receiver, leaving await and yield operands
1653    /// in their own function: no async thunk, extra await or new microtask.
1654    fn bind(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1655        let callee = call.callee.span();
1656        let width = (callee.end - callee.start) as i64;
1657        let opening = Self::opening("bindNodeAssertionPhase", operation, source);
1658        self.edits
1659            .push(TextEdit::insert(callee.start as usize, -width, opening));
1660        match &call.callee {
1661            Expression::StaticMemberExpression(member) => {
1662                // `.name` becomes `, "name")`, keeping whatever whitespace and
1663                // line breaks sat between the receiver and the dot.
1664                let between = &self.source
1665                    [member.object.span().end as usize..member.property.span.start as usize];
1666                let dot = between.rfind('.').unwrap_or(0);
1667                let mut text = String::from(",");
1668                text.push_str(&between[..dot]);
1669                text.push(' ');
1670                text.push_str(&between[dot + 1..]);
1671                text.push_str(
1672                    &serde_json::to_string(member.property.name.as_str())
1673                        .expect("a string serializes"),
1674                );
1675                text.push(')');
1676                self.edits.push(TextEdit::replace(
1677                    member.object.span().end as usize,
1678                    member.property.span.end as usize,
1679                    text,
1680                ));
1681            }
1682            Expression::ComputedMemberExpression(member) => {
1683                let object_end = member.object.span().end as usize;
1684                let open = object_end + self.source[object_end..].find('[').unwrap_or(0);
1685                self.edits
1686                    .push(TextEdit::replace(open, open + 1, ",".into()));
1687                let close = member.span.end as usize - 1;
1688                self.edits
1689                    .push(TextEdit::replace(close, close + 1, ")".into()));
1690            }
1691            _ => {
1692                self.edits.push(TextEdit::insert(
1693                    callee.end as usize,
1694                    width,
1695                    ", null)".into(),
1696                ));
1697            }
1698        }
1699    }
1700}
1701
1702impl<'a> Visit<'a> for AssertionEdits<'_> {
1703    fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) {
1704        let body = arrow
1705            .expression
1706            .then(|| arrow.body.statements.first())
1707            .flatten()
1708            .map(|statement| span_key(statement.span()));
1709        let outer = std::mem::replace(&mut self.arrow_body, body);
1710        walk::walk_arrow_function_expression(self, arrow);
1711        self.arrow_body = outer;
1712    }
1713
1714    fn visit_expression_statement(&mut self, statement: &ExpressionStatement<'a>) {
1715        if self.in_place.is_some()
1716            && self.arrow_body != Some(span_key(statement.span))
1717            && let Expression::CallExpression(call) = &statement.expression
1718            && let Some(&(_, _, bound)) = self.sites.get(&span_key(call.span))
1719        {
1720            let entity = entity_name(&call.callee);
1721            if !bound || entity.is_some() {
1722                let (operation, source, _) = self
1723                    .sites
1724                    .remove(&span_key(call.span))
1725                    .expect("the site was just found");
1726                match entity {
1727                    Some(callee) if bound => {
1728                        self.bind_statement(statement, call, &callee, &operation, &source)
1729                    }
1730                    _ => self.wrap_statement(statement, &operation, &source),
1731                }
1732            }
1733        }
1734        walk::walk_expression_statement(self, statement);
1735    }
1736
1737    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1738        if let Some((operation, source, bound)) = self.sites.remove(&span_key(call.span)) {
1739            if bound {
1740                self.bind(call, &operation, &source);
1741            } else {
1742                self.wrap(call, &operation, &source);
1743            }
1744        }
1745        walk::walk_call_expression(self, call);
1746    }
1747}
1748
1749/// A callee that is an identifier or a qualified name (`assert`,
1750/// `assert.strict.ok`), dotted and without whitespace: what `typeof` takes.
1751fn entity_name(callee: &Expression<'_>) -> Option<String> {
1752    match callee {
1753        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1754        Expression::StaticMemberExpression(member) if !member.optional => Some(format!(
1755            "{}.{}",
1756            entity_name(&member.object)?,
1757            member.property.name
1758        )),
1759        _ => None,
1760    }
1761}
1762
1763/// `transform_capability_imports` as text edits, for a test file.
1764fn capability_import_edits(
1765    program: &Program<'_>,
1766    source: &str,
1767    wrapper: &str,
1768) -> (Vec<TextEdit>, usize) {
1769    if !capability_source_candidate(source) || !program.source_type.is_module() {
1770        return (Vec::new(), 0);
1771    }
1772    let capability_roots = capability_import_roots(program, source);
1773    if capability_roots.is_empty() {
1774        return (Vec::new(), 0);
1775    }
1776    let mut names = CandidateNames::new(source);
1777    let wrapper_local = names.allocate("__supercovImportedCapability");
1778    let mut edits = Vec::new();
1779    let mut wrapped = 0;
1780    for statement in &program.body {
1781        let Statement::ImportDeclaration(declaration) = statement else {
1782            continue;
1783        };
1784        if declaration.import_kind == ImportOrExportKind::Type
1785            || excluded_capability_import(declaration.source.value.as_str(), wrapper)
1786        {
1787            continue;
1788        }
1789        let mut declarators = Vec::new();
1790        for specifier in declaration.specifiers.iter().flatten() {
1791            let (local, shorthand) = match specifier {
1792                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1793                    if specifier.import_kind == ImportOrExportKind::Type =>
1794                {
1795                    continue;
1796                }
1797                ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
1798                    &specifier.local,
1799                    specifier.imported.span() == specifier.local.span,
1800                ),
1801                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1802                    (&specifier.local, false)
1803                }
1804                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1805                    (&specifier.local, false)
1806                }
1807            };
1808            let original = local.name.to_string();
1809            if !capability_roots.contains(&original) {
1810                continue;
1811            }
1812            let raw = names.allocate(&format!("__supercovRaw{original}"));
1813            if shorthand {
1814                edits.push(TextEdit::insert(
1815                    local.span.end as usize,
1816                    0,
1817                    format!(" as {raw}"),
1818                ));
1819            } else {
1820                edits.push(TextEdit::replace(
1821                    local.span.start as usize,
1822                    local.span.end as usize,
1823                    raw.clone(),
1824                ));
1825            }
1826            declarators.push(format!("{original} = {wrapper_local}({raw})"));
1827            wrapped += 1;
1828        }
1829        if !declarators.is_empty() {
1830            edits.push(TextEdit::insert(
1831                declaration.span.end as usize,
1832                i64::MAX,
1833                format!("; const {};", declarators.join(", ")),
1834            ));
1835        }
1836    }
1837    if wrapped > 0 {
1838        // First among the imports, as the syntax-tree transform put it, on the
1839        // first statement's own line.
1840        let first = program
1841            .body
1842            .first()
1843            .map_or(0, |statement| statement.span().start as usize);
1844        edits.push(TextEdit::insert(
1845            first,
1846            i64::MIN,
1847            format!(
1848                "import {{ wrapImportedCapability as {wrapper_local} }} from {}; ",
1849                serde_json::to_string(wrapper).expect("a string serializes")
1850            ),
1851        ));
1852    }
1853    (edits, wrapped)
1854}
1855
1856/// Wrap value imports that can hide a remote execution capability. This is an
1857/// ahead-of-run Rust transform; the runtime shim only proxies the imported
1858/// value and never parses or rewrites source.
1859fn transform_capability_imports<'a>(
1860    allocator: &'a Allocator,
1861    program: &mut Program<'a>,
1862    source: &str,
1863    wrapper: &str,
1864) -> usize {
1865    if !capability_source_candidate(source) || !program.source_type.is_module() {
1866        return 0;
1867    }
1868    let capability_roots = capability_import_roots(program, source);
1869    if capability_roots.is_empty() {
1870        return 0;
1871    }
1872    let ast = AstBuilder::new(allocator);
1873    let mut names = CandidateNames::new(source);
1874    let wrapper_local = names.allocate("__supercovImportedCapability");
1875    let mut wrapped = 0;
1876    let mut output = ast.vec();
1877    for statement in program.body.take_in(allocator) {
1878        let Statement::ImportDeclaration(mut declaration) = statement else {
1879            output.push(statement);
1880            continue;
1881        };
1882        let module_name = declaration.source.value.as_str();
1883        if declaration.import_kind == ImportOrExportKind::Type
1884            || excluded_capability_import(module_name, wrapper)
1885        {
1886            output.push(Statement::ImportDeclaration(declaration));
1887            continue;
1888        }
1889        let mut declarations = ast.vec();
1890        for specifier in declaration.specifiers.iter_mut().flatten() {
1891            let local = match specifier {
1892                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1893                    if specifier.import_kind == ImportOrExportKind::Type =>
1894                {
1895                    continue;
1896                }
1897                ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
1898                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1899                    &mut specifier.local
1900                }
1901                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1902                    &mut specifier.local
1903                }
1904            };
1905            let original = local.name.to_string();
1906            if !capability_roots.contains(&original) {
1907                continue;
1908            }
1909            let raw = names.allocate(&format!("__supercovRaw{original}"));
1910            local.name = ast.ident(&raw);
1911            let wrapped_value = ast.expression_call(
1912                Span::default(),
1913                ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
1914                NONE,
1915                ast.vec1(Argument::from(
1916                    ast.expression_identifier(Span::default(), ast.ident(&raw)),
1917                )),
1918                false,
1919            );
1920            declarations.push(ast.variable_declarator(
1921                Span::default(),
1922                VariableDeclarationKind::Const,
1923                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
1924                NONE,
1925                Some(wrapped_value),
1926                false,
1927            ));
1928            wrapped += 1;
1929        }
1930        output.push(Statement::ImportDeclaration(declaration));
1931        if !declarations.is_empty() {
1932            output.push(Statement::VariableDeclaration(
1933                ast.alloc_variable_declaration(
1934                    Span::default(),
1935                    VariableDeclarationKind::Const,
1936                    declarations,
1937                    false,
1938                ),
1939            ));
1940        }
1941    }
1942    if wrapped > 0 {
1943        output.insert(
1944            0,
1945            Statement::ImportDeclaration(ast.alloc_import_declaration(
1946                Span::default(),
1947                Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
1948                    Span::default(),
1949                    ast.module_export_name_identifier_name(
1950                        Span::default(),
1951                        ast.ident("wrapImportedCapability"),
1952                    ),
1953                    ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
1954                    ImportOrExportKind::Value,
1955                ))),
1956                ast.string_literal(Span::default(), ast.str(wrapper), None),
1957                None,
1958                NONE,
1959                ImportOrExportKind::Value,
1960            )),
1961        );
1962    }
1963    program.body = output;
1964    wrapped
1965}
1966
1967fn canonical_assert_module(value: &str) -> Option<String> {
1968    NODE_ASSERT_MODULES.contains(&value).then(|| {
1969        if value.starts_with("node:") {
1970            value.into()
1971        } else {
1972            format!("node:{value}")
1973        }
1974    })
1975}
1976
1977fn required_assert_module(
1978    expression: &Expression<'_>,
1979    scoping: &oxc_semantic::Scoping,
1980) -> Option<String> {
1981    if let Expression::CallExpression(call) = expression
1982        && matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
1983        && matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
1984        && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
1985    {
1986        return canonical_assert_module(module.value.as_str());
1987    }
1988    if let Expression::StaticMemberExpression(member) = expression
1989        && member.property.name == "strict"
1990        && let Some(module) = required_assert_module(&member.object, scoping)
1991    {
1992        return Some(if module == "node:assert" {
1993            "node:assert/strict".into()
1994        } else {
1995            module
1996        });
1997    }
1998    None
1999}
2000
2001#[derive(Default)]
2002struct NodeAssertionBindings {
2003    objects: HashMap<SymbolId, String>,
2004    direct: HashMap<SymbolId, String>,
2005    expects: HashSet<SymbolId>,
2006    /// Jest (and Jasmine) inject `expect` as a global next to `test`, `it`
2007    /// and `describe`: a file that reaches for all of those unresolved is a
2008    /// test file whose bare `expect` is the assertion.
2009    global_expect: bool,
2010}
2011
2012fn bind_object(
2013    bindings: &mut NodeAssertionBindings,
2014    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
2015    module: String,
2016) {
2017    if let Some(symbol) = identifier.symbol_id.get() {
2018        bindings.objects.insert(symbol, module);
2019    }
2020}
2021
2022fn bind_direct(
2023    bindings: &mut NodeAssertionBindings,
2024    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
2025    operation: String,
2026) {
2027    if let Some(symbol) = identifier.symbol_id.get() {
2028        bindings.direct.insert(symbol, operation);
2029    }
2030}
2031
2032fn bind_expect(
2033    bindings: &mut NodeAssertionBindings,
2034    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
2035) {
2036    if let Some(symbol) = identifier.symbol_id.get() {
2037        bindings.expects.insert(symbol);
2038    }
2039}
2040
2041struct NodeAssertionBindingCollector<'s> {
2042    bindings: NodeAssertionBindings,
2043    scoping: &'s oxc_semantic::Scoping,
2044    allow_contextual_expect: bool,
2045    expect_modules: HashSet<String>,
2046}
2047
2048impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
2049    fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
2050        let module_name = declaration.source.value.as_str();
2051        let assert_module = canonical_assert_module(module_name);
2052        let expect_module =
2053            self.expect_modules.contains(module_name) || self.allow_contextual_expect;
2054        for specifier in declaration.specifiers.iter().flatten() {
2055            match specifier {
2056                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
2057                    if let Some(module) = &assert_module {
2058                        bind_object(&mut self.bindings, &specifier.local, module.clone());
2059                    } else if expect_module && specifier.local.name == "expect" {
2060                        bind_expect(&mut self.bindings, &specifier.local);
2061                    }
2062                }
2063                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
2064                    if let Some(module) = &assert_module {
2065                        bind_object(&mut self.bindings, &specifier.local, module.clone());
2066                    }
2067                }
2068                ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
2069                    let imported = specifier.imported.name().to_string();
2070                    if let Some(module) = &assert_module {
2071                        if imported == "strict" {
2072                            bind_object(
2073                                &mut self.bindings,
2074                                &specifier.local,
2075                                "node:assert/strict".into(),
2076                            );
2077                        } else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
2078                            bind_direct(
2079                                &mut self.bindings,
2080                                &specifier.local,
2081                                format!("{module}.{imported}"),
2082                            );
2083                        }
2084                    } else if expect_module && imported == "expect" {
2085                        bind_expect(&mut self.bindings, &specifier.local);
2086                    }
2087                }
2088            }
2089        }
2090        walk::walk_import_declaration(self, declaration);
2091    }
2092
2093    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
2094        // CommonJS named expect imports use the same lexical symbol identities
2095        // as ESM; a local/shadowed require is deliberately not recognized.
2096        if let Some(Expression::CallExpression(call)) = &declarator.init
2097            && matches!(&call.callee, Expression::Identifier(i) if i.name == "require" && referenced_symbol(i, self.scoping).is_none())
2098            && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
2099            && self.expect_modules.contains(module.value.as_str())
2100            && let BindingPattern::ObjectPattern(pattern) = &declarator.id
2101        {
2102            for property in &pattern.properties {
2103                if property.key.static_name().as_deref() == Some("expect")
2104                    && let BindingPattern::BindingIdentifier(local) = &property.value
2105                {
2106                    bind_expect(&mut self.bindings, local);
2107                }
2108            }
2109        }
2110        if let Some(module) = declarator
2111            .init
2112            .as_ref()
2113            .and_then(|expression| required_assert_module(expression, self.scoping))
2114        {
2115            match &declarator.id {
2116                BindingPattern::BindingIdentifier(identifier) => {
2117                    bind_object(&mut self.bindings, identifier, module);
2118                }
2119                BindingPattern::ObjectPattern(pattern) => {
2120                    for property in &pattern.properties {
2121                        let Some(imported) = property.key.static_name() else {
2122                            continue;
2123                        };
2124                        let BindingPattern::BindingIdentifier(local) = &property.value else {
2125                            continue;
2126                        };
2127                        if imported == "strict" {
2128                            bind_object(&mut self.bindings, local, "node:assert/strict".into());
2129                        } else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
2130                            bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
2131                        }
2132                    }
2133                }
2134                _ => {}
2135            }
2136        }
2137        walk::walk_variable_declarator(self, declarator);
2138    }
2139}
2140
2141fn node_assertion_bindings(
2142    program: &Program<'_>,
2143    scoping: &oxc_semantic::Scoping,
2144    extra_expect_modules: &[String],
2145) -> NodeAssertionBindings {
2146    let allow_contextual_expect = program.source_text.contains("node:test")
2147        && program
2148            .source_text
2149            .split(|character: char| !character.is_alphanumeric() && character != '_')
2150            .any(|word| word == "expect");
2151    let mut collector = NodeAssertionBindingCollector {
2152        bindings: NodeAssertionBindings::default(),
2153        scoping,
2154        allow_contextual_expect,
2155        expect_modules: ["vitest", "@jest/globals", "expect", "@playwright/test"]
2156            .into_iter()
2157            .map(str::to_owned)
2158            .chain(extra_expect_modules.iter().cloned())
2159            .collect(),
2160    };
2161    collector.visit_program(program);
2162    let unresolved = scoping.root_unresolved_references();
2163    let uses = |name: &str| unresolved.contains_key(name);
2164    collector.bindings.global_expect =
2165        uses("expect") && (uses("test") || uses("it") || uses("describe"));
2166    collector.bindings
2167}
2168
2169struct NodeAssertionSiteCollector<'s> {
2170    source: &'s str,
2171    file: &'s str,
2172    bindings: &'s NodeAssertionBindings,
2173    scoping: &'s oxc_semantic::Scoping,
2174    sites: HashMap<SpanKey, (String, String, bool)>,
2175}
2176
2177/// How a project's JavaScript is parsed: in the script or module mode its
2178/// extension implies, with JSX allowed. React code is routinely written in
2179/// `.js` and compiled by Babel, Webpack or Vite, so a `.js` file with JSX in it
2180/// is ordinary; rejecting it failed a whole run before any test started.
2181/// Enabling JSX changes nothing for JavaScript without it, because `<` cannot
2182/// begin an expression there. TypeScript is left alone: in `.ts` a leading `<`
2183/// is a type assertion, and only `.tsx` means JSX.
2184pub fn project_source_type(path: &Path) -> Result<SourceType, String> {
2185    let source_type = SourceType::from_path(path).map_err(|error| error.to_string())?;
2186    Ok(if source_type.is_typescript() {
2187        source_type
2188    } else {
2189        source_type.with_jsx(true)
2190    })
2191}
2192
2193fn referenced_symbol(
2194    identifier: &oxc_ast::ast::IdentifierReference<'_>,
2195    scoping: &oxc_semantic::Scoping,
2196) -> Option<SymbolId> {
2197    identifier
2198        .reference_id
2199        .get()
2200        .and_then(|reference| scoping.get_reference(reference).symbol_id())
2201}
2202
2203fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
2204    match expression {
2205        Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
2206        Expression::ComputedMemberExpression(member) => match &member.expression {
2207            Expression::StringLiteral(literal) => Some(literal.value.as_str()),
2208            _ => None,
2209        },
2210        _ => None,
2211    }
2212}
2213
2214fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
2215    match expression {
2216        Expression::StaticMemberExpression(member) => Some(&member.object),
2217        Expression::ComputedMemberExpression(member) => Some(&member.object),
2218        _ => None,
2219    }
2220}
2221
2222fn expect_operation(
2223    callee: &Expression<'_>,
2224    bindings: &NodeAssertionBindings,
2225    scoping: &oxc_semantic::Scoping,
2226) -> Option<String> {
2227    let mut current = callee;
2228    let mut matchers = Vec::new();
2229    while let Some(name) = assertion_member_name(current) {
2230        matchers.push(name.to_owned());
2231        current = assertion_member_object(current)?;
2232    }
2233    matchers.reverse();
2234    let matcher = matchers.last()?;
2235    if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
2236        return None;
2237    }
2238    let Expression::CallExpression(expect_call) = current else {
2239        return None;
2240    };
2241    // `expect(value)` is the usual root, but the assertion APIs wrap it:
2242    // `expect.element(locator)` in Vitest Browser Mode, `expect.soft(value)`,
2243    // `expect.poll(fn)`. The receiver is still expect and the matcher is still
2244    // the assertion, so requiring a bare identifier here made every UI
2245    // assertion in a browser-mode test invisible -- and an assertion Supercov
2246    // cannot see can never be mapped or earn credit.
2247    let mut root = &expect_call.callee;
2248    let mut prefix = Vec::new();
2249    while let Some(name) = assertion_member_name(root) {
2250        prefix.push(name.to_owned());
2251        root = assertion_member_object(root)?;
2252    }
2253    prefix.reverse();
2254    let Expression::Identifier(identifier) = root else {
2255        return None;
2256    };
2257    let recognized = match referenced_symbol(identifier, scoping) {
2258        Some(symbol) => bindings.expects.contains(&symbol),
2259        None => bindings.global_expect && identifier.name == "expect",
2260    };
2261    recognized.then(|| {
2262        let mut parts = prefix;
2263        parts.extend(matchers.iter().cloned());
2264        format!("expect.{}", parts.join("."))
2265    })
2266}
2267
2268#[derive(Default)]
2269struct AwaitYieldScanner {
2270    found: bool,
2271}
2272
2273impl<'a> Visit<'a> for AwaitYieldScanner {
2274    fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
2275        self.found = true;
2276    }
2277
2278    fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
2279        self.found = true;
2280    }
2281
2282    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
2283
2284    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
2285}
2286
2287impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
2288    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
2289        let operation = match &call.callee {
2290            Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
2291                .and_then(|symbol| {
2292                    self.bindings.direct.get(&symbol).cloned().or_else(|| {
2293                        self.bindings
2294                            .objects
2295                            .get(&symbol)
2296                            .map(|module| format!("{module}.ok"))
2297                    })
2298                }),
2299            callee => {
2300                let member_operation = assertion_member_object(callee).and_then(|object| {
2301                    let Expression::Identifier(identifier) = object else {
2302                        return None;
2303                    };
2304                    let symbol = referenced_symbol(identifier, self.scoping)?;
2305                    let method = assertion_member_name(callee)?;
2306                    self.bindings
2307                        .objects
2308                        .get(&symbol)
2309                        .filter(|_| NODE_ASSERT_METHODS.contains(&method))
2310                        .map(|module| format!("{module}.{method}"))
2311                });
2312                member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
2313            }
2314        };
2315        if let Some(operation) = operation {
2316            let mut unsafe_argument = AwaitYieldScanner::default();
2317            unsafe_argument.visit_expression(&call.callee);
2318            for argument in &call.arguments {
2319                unsafe_argument.visit_argument(argument);
2320            }
2321            let optional = call.optional
2322                || matches!(&call.callee,
2323                Expression::StaticMemberExpression(m) if m.optional)
2324                || matches!(&call.callee, Expression::ComputedMemberExpression(m) if m.optional);
2325            // Never manufacture a passed occurrence when a call can
2326            // short-circuit before invoking a matcher.
2327            if optional {
2328                walk::walk_call_expression(self, call);
2329                return;
2330            }
2331            let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
2332            self.sites.insert(
2333                span_key(call.span),
2334                (
2335                    operation,
2336                    format!("{}:{line}:{column}", self.file),
2337                    unsafe_argument.found,
2338                ),
2339            );
2340        }
2341        walk::walk_call_expression(self, call);
2342    }
2343}
2344
2345/// Attribute native node:assert and node:test expect calls by opening the
2346/// assertion phase before argument evaluation when possible, or binding the
2347/// callee when arguments contain await/yield. Lexical symbol identity avoids
2348/// wrapping a shadowed or merely assert-shaped user binding.
2349pub fn instrument_node_assertion_phases(
2350    source: &str,
2351    file: &str,
2352) -> Result<NodeAssertionInstrumentation, CandidateError> {
2353    instrument_node_assertion_phases_with_expect_modules(source, file, &[])
2354}
2355
2356pub fn instrument_node_assertion_phases_with_expect_modules(
2357    source: &str,
2358    file: &str,
2359    extra_expect_modules: &[String],
2360) -> Result<NodeAssertionInstrumentation, CandidateError> {
2361    instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
2362}
2363
2364pub fn instrument_node_assertion_phases_with_runtime_hooks(
2365    source: &str,
2366    file: &str,
2367    extra_expect_modules: &[String],
2368    capability_wrapper: Option<&str>,
2369) -> Result<NodeAssertionInstrumentation, CandidateError> {
2370    instrument_node_assertion_phases_with_runtime_imports(
2371        source,
2372        file,
2373        extra_expect_modules,
2374        capability_wrapper,
2375        None,
2376    )
2377}
2378
2379/// Attribute assertions and, for an ESM test module, make the runtime
2380/// dependency explicit in the transformed module itself. This is required for
2381/// opaque runners which copy an already-instrumented workspace into another
2382/// process, container, or VM while constructing a fresh environment. Static
2383/// imports preserve ESM evaluation ordering and do not depend on runner-specific
2384/// environment forwarding.
2385/// The body of the function [`runtime_loader`] calls: where Node runs, it
2386/// requires the bootstrap relative to the file the stack's first frame names.
2387const 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);""#;
2388
2389/// How the statement [`runtime_loader`] writes starts.
2390pub(crate) const RUNTIME_LOADER_START: &str = "if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {";
2391
2392/// One statement that loads the run's bootstrap where no runtime is
2393/// installed. It has to be valid in a module, a CommonJS file and a browser,
2394/// pass a strict TypeScript check with or without Node's types, and survive
2395/// compilers -- so no `import`, no `import.meta`, and nothing but
2396/// `globalThis.__SUPERCOV_DIRECT_RUNTIME__`, `Function` and `Error` outside a
2397/// function body the type checker does not read. The file's own location
2398/// comes from a stack taken at its top level; `bootstrap` is relative to it.
2399pub(crate) fn runtime_loader(bootstrap: &str) -> String {
2400    let specifier = serde_json::to_string(bootstrap).expect("a path string serializes");
2401    format!(
2402        "{RUNTIME_LOADER_START} new Function(\"stack\", \"specifier\", {RUNTIME_LOADER_BODY})(new Error().stack, {specifier}); }} catch {{}}"
2403    )
2404}
2405
2406pub fn instrument_node_assertion_phases_with_runtime_imports(
2407    source: &str,
2408    file: &str,
2409    extra_expect_modules: &[String],
2410    capability_wrapper: Option<&str>,
2411    assertion_runtime: Option<&str>,
2412) -> Result<NodeAssertionInstrumentation, CandidateError> {
2413    let _positions = PositionScope::enter();
2414    let assertion_candidate = source.contains("assert") || source.contains("expect");
2415    let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
2416    if !assertion_candidate && !capability_candidate {
2417        return Ok(NodeAssertionInstrumentation {
2418            code: source.into(),
2419            assertions: 0,
2420            capability_imports: 0,
2421        });
2422    }
2423    let source_type =
2424        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
2425    let allocator = Allocator::default();
2426    let parsed = Parser::new(&allocator, source, source_type).parse();
2427    if !parsed.errors.is_empty() {
2428        return Err(CandidateError::Parse(
2429            parsed
2430                .errors
2431                .into_iter()
2432                .map(|error| error.to_string())
2433                .collect(),
2434        ));
2435    }
2436    let mut assertions = 0;
2437    let mut edits = Vec::new();
2438
2439    if assertion_candidate {
2440        let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
2441        let bindings =
2442            node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
2443        let mut collector = NodeAssertionSiteCollector {
2444            source,
2445            file,
2446            bindings: &bindings,
2447            scoping: semantic.scoping(),
2448            sites: HashMap::new(),
2449        };
2450        collector.visit_program(&parsed.program);
2451        assertions = collector.sites.len();
2452        if assertions > 0 {
2453            let in_place = source_type.is_typescript().then(|| {
2454                let mut names = CandidateNames::new(source);
2455                (
2456                    names.allocate("__supercovAssertionPhase"),
2457                    names.allocate("__supercovAssertionError"),
2458                )
2459            });
2460            let mut located = AssertionEdits {
2461                source,
2462                sites: collector.sites,
2463                edits: Vec::new(),
2464                in_place,
2465                arrow_body: None,
2466            };
2467            located.visit_program(&parsed.program);
2468            edits.extend(located.edits);
2469        }
2470    }
2471    let capability_imports = capability_wrapper.map_or(0, |wrapper| {
2472        let (found, wrapped) = capability_import_edits(&parsed.program, source, wrapper);
2473        edits.extend(found);
2474        wrapped
2475    });
2476    if assertions == 0 && capability_imports == 0 {
2477        return Ok(NodeAssertionInstrumentation {
2478            code: source.into(),
2479            assertions: 0,
2480            capability_imports: 0,
2481        });
2482    }
2483    let module = parsed.program.source_type.is_module();
2484    // The runtime the assertion phases call, loaded where nothing installed
2485    // it. A statement rather than an `import`: an import's relative path broke
2486    // once the test command compiled the file somewhere else (`tsc` into
2487    // `dist/`), and the test failed on a module that was not there. It goes
2488    // first, as the import was evaluated first, on the first line so every
2489    // other line stays where it was.
2490    if let Some(bootstrap) = assertion_runtime.filter(|_| assertions > 0) {
2491        let at = if source.starts_with("#!") {
2492            source
2493                .find('\n')
2494                .map_or(source.len(), |newline| newline + 1)
2495        } else {
2496            0
2497        };
2498        edits.push(TextEdit::insert(
2499            at,
2500            i64::MIN,
2501            format!("{} ", runtime_loader(bootstrap)),
2502        ));
2503    }
2504    // An inline map is resolved relative to the transformed file itself, and
2505    // assertion-only transforms replace the file in place, so its basename is
2506    // the exact source-map reference.
2507    let reference = Path::new(file)
2508        .file_name()
2509        .and_then(|name| name.to_str())
2510        .unwrap_or(file);
2511    let (mut code, map) = apply_text_edits(source, edits, reference)?;
2512    // Append instead of prepend so every line of user code stays where it
2513    // was. Import declarations are instantiated before module evaluation
2514    // regardless of their textual position.
2515    if assertions > 0 && source_type.is_typescript() {
2516        if module {
2517            code.push_str(&format!(
2518                "\ndeclare global {{ var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE}; }}\n"
2519            ));
2520        } else {
2521            code.push_str(&format!(
2522                "\ndeclare var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE};\n"
2523            ));
2524        }
2525    }
2526    let map = map.to_json_string();
2527    code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
2528    BASE64_STANDARD.encode_string(map, &mut code);
2529    code.push('\n');
2530    Ok(NodeAssertionInstrumentation {
2531        code,
2532        assertions,
2533        capability_imports,
2534    })
2535}
2536
2537type SpanKey = (u32, u32);
2538
2539#[derive(Default)]
2540struct SafetyAnalysis {
2541    source_sensitive_functions: HashSet<SpanKey>,
2542    semantic_limitations: Vec<CandidateLimitation>,
2543    dynamic_limitations: Vec<CandidateLimitation>,
2544}
2545
2546/// The bindings a file reads function source through, in the places
2547/// `observes_function_source` recognises a function written in place:
2548/// `name.toString()`, `String(name)`, `${name}`, `"(" + name + ")"`, and the
2549/// same through parentheses, casts, `?:`, `||`, `&&`, `,` and `=`. A function
2550/// bound to one of them is shipped as text -- to a worker, a `vm` context, a
2551/// browser page -- where no probe exists, so it is left as source like one
2552/// read in place. Bindings are symbols, not names: a local `total` compared
2553/// with a number says nothing about a function `total` elsewhere. Another
2554/// file's reads are not visible here.
2555struct SourceReads<'s> {
2556    scoping: &'s oxc_semantic::Scoping,
2557    symbols: HashSet<SymbolId>,
2558    /// Property names a method's source is read through: `box.method` in
2559    /// `box.method.toString()` or `String(box.method)`. A property path has
2560    /// no symbol to follow, so any method of that name in the file is taken
2561    /// as the one read. Only the two explicit forms count: `"Total: " +
2562    /// cart.total` reads a value almost always, and would cost a method
2563    /// `total` its probes.
2564    properties: HashSet<String>,
2565}
2566
2567impl SourceReads<'_> {
2568    fn read_property(&mut self, expression: &Expression<'_>) {
2569        match expression {
2570            Expression::StaticMemberExpression(member) => {
2571                self.properties.insert(member.property.name.to_string());
2572            }
2573            Expression::ParenthesizedExpression(inner) => self.read_property(&inner.expression),
2574            Expression::TSAsExpression(inner) => self.read_property(&inner.expression),
2575            Expression::TSNonNullExpression(inner) => self.read_property(&inner.expression),
2576            Expression::ConditionalExpression(inner) => {
2577                self.read_property(&inner.consequent);
2578                self.read_property(&inner.alternate);
2579            }
2580            Expression::LogicalExpression(inner) => {
2581                self.read_property(&inner.left);
2582                self.read_property(&inner.right);
2583            }
2584            _ => {}
2585        }
2586    }
2587
2588    fn read(&mut self, expression: &Expression<'_>) {
2589        match expression {
2590            Expression::Identifier(identifier) => {
2591                self.symbols
2592                    .extend(referenced_symbol(identifier, self.scoping));
2593            }
2594            Expression::ParenthesizedExpression(inner) => self.read(&inner.expression),
2595            Expression::TSAsExpression(inner) => self.read(&inner.expression),
2596            Expression::TSSatisfiesExpression(inner) => self.read(&inner.expression),
2597            Expression::TSTypeAssertion(inner) => self.read(&inner.expression),
2598            Expression::TSNonNullExpression(inner) => self.read(&inner.expression),
2599            Expression::ConditionalExpression(inner) => {
2600                self.read(&inner.consequent);
2601                self.read(&inner.alternate);
2602            }
2603            Expression::LogicalExpression(inner) => {
2604                self.read(&inner.left);
2605                self.read(&inner.right);
2606            }
2607            Expression::SequenceExpression(inner) => {
2608                if let Some(last) = inner.expressions.last() {
2609                    self.read(last);
2610                }
2611            }
2612            Expression::AssignmentExpression(inner) => self.read(&inner.right),
2613            _ => {}
2614        }
2615    }
2616}
2617
2618impl<'a> Visit<'a> for SourceReads<'_> {
2619    fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
2620        if member.property.name == "toString" {
2621            self.read(&member.object);
2622            self.read_property(&member.object);
2623        }
2624        walk::walk_static_member_expression(self, member);
2625    }
2626
2627    fn visit_computed_member_expression(
2628        &mut self,
2629        member: &oxc_ast::ast::ComputedMemberExpression<'a>,
2630    ) {
2631        self.read(&member.expression);
2632        walk::walk_computed_member_expression(self, member);
2633    }
2634
2635    fn visit_object_property(&mut self, property: &oxc_ast::ast::ObjectProperty<'a>) {
2636        if property.computed
2637            && let Some(key) = property.key.as_expression()
2638        {
2639            self.read(key);
2640        }
2641        walk::walk_object_property(self, property);
2642    }
2643
2644    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
2645        if expression_is_identifier(&call.callee, "String")
2646            && let Some(argument) = call.arguments.first().and_then(Argument::as_expression)
2647        {
2648            self.read(argument);
2649            self.read_property(argument);
2650        }
2651        walk::walk_call_expression(self, call);
2652    }
2653
2654    fn visit_template_literal(&mut self, template: &oxc_ast::ast::TemplateLiteral<'a>) {
2655        for expression in &template.expressions {
2656            self.read(expression);
2657        }
2658        walk::walk_template_literal(self, template);
2659    }
2660
2661    fn visit_binary_expression(&mut self, binary: &oxc_ast::ast::BinaryExpression<'a>) {
2662        if matches!(
2663            binary.operator,
2664            BinaryOperator::Addition
2665                | BinaryOperator::LessThan
2666                | BinaryOperator::LessEqualThan
2667                | BinaryOperator::GreaterThan
2668                | BinaryOperator::GreaterEqualThan
2669        ) {
2670            self.read(&binary.left);
2671            self.read(&binary.right);
2672        }
2673        walk::walk_binary_expression(self, binary);
2674    }
2675}
2676
2677/// The property a function is the value of: an object's method or property,
2678/// a class's method or field.
2679fn property_name_of_function<State>(context: &TraverseCtx<'_, State>) -> Option<String> {
2680    let key = match context.parent() {
2681        Ancestor::ObjectPropertyValue(property) => property.key(),
2682        Ancestor::MethodDefinitionValue(method) => method.key(),
2683        Ancestor::PropertyDefinitionValue(property) => property.key(),
2684        _ => return None,
2685    };
2686    key.static_name().map(|name| name.to_string())
2687}
2688
2689/// The bindings a function is reached through where it is defined: its own
2690/// name, and the variable it initializes or is assigned to.
2691fn bound_symbols<State>(
2692    own: Option<&oxc_ast::ast::BindingIdentifier<'_>>,
2693    scoping: &oxc_semantic::Scoping,
2694    context: &TraverseCtx<'_, State>,
2695) -> Vec<SymbolId> {
2696    let mut symbols = own
2697        .and_then(|identifier| identifier.symbol_id.get())
2698        .into_iter()
2699        .collect::<Vec<_>>();
2700    match context.ancestors().next() {
2701        Some(Ancestor::VariableDeclaratorInit(parent)) => symbols.extend(
2702            parent
2703                .id()
2704                .get_binding_identifier()
2705                .and_then(|identifier| identifier.symbol_id.get()),
2706        ),
2707        Some(Ancestor::AssignmentExpressionRight(parent)) => {
2708            if let AssignmentTarget::AssignmentTargetIdentifier(target) = parent.left() {
2709                symbols.extend(referenced_symbol(target, scoping));
2710            }
2711        }
2712        _ => {}
2713    }
2714    symbols
2715}
2716
2717struct SafetyScanner<'s> {
2718    source: &'s str,
2719    file: &'s str,
2720    scoping: &'s oxc_semantic::Scoping,
2721    source_reads: HashSet<SymbolId>,
2722    source_read_properties: HashSet<String>,
2723    source_sensitive_functions: HashSet<SpanKey>,
2724    function_limitations: Vec<CandidateLimitation>,
2725    with_limitations: Vec<CandidateLimitation>,
2726    dynamic_limitations: Vec<CandidateLimitation>,
2727    unsafe_function_depth: usize,
2728    with_depth: usize,
2729}
2730
2731#[derive(Clone, Copy, PartialEq, Eq)]
2732enum PointPass {
2733    Statements,
2734    Functions,
2735}
2736
2737struct PointCollector<'s> {
2738    source: &'s str,
2739    file: &'s str,
2740    pass: PointPass,
2741    points: Vec<CandidatePoint>,
2742    statement_targets: HashMap<SpanKey, Vec<String>>,
2743    function_targets: HashMap<SpanKey, String>,
2744    rendered_targets: HashMap<SpanKey, String>,
2745    source_sensitive_functions: &'s HashSet<SpanKey>,
2746    erased_imports: &'s HashSet<SpanKey>,
2747    unsafe_function_depth: usize,
2748    with_depth: usize,
2749    ambient_depth: usize,
2750}
2751
2752#[derive(Default)]
2753struct PointAnalysis {
2754    points: Vec<CandidatePoint>,
2755    statement_targets: HashMap<SpanKey, Vec<String>>,
2756    function_targets: HashMap<SpanKey, String>,
2757    rendered_targets: HashMap<SpanKey, String>,
2758}
2759
2760#[derive(Clone)]
2761struct PointTarget {
2762    index: usize,
2763}
2764
2765impl PointCollector<'_> {
2766    fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
2767        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
2768        CandidatePoint {
2769            id: stable_id(self.source, self.file, kind, span, ""),
2770            kind: kind.to_string(),
2771            file: self.file.to_string(),
2772            line,
2773            column,
2774            source: source_slice(self.source, span).to_string(),
2775            label,
2776        }
2777    }
2778
2779    fn unsafe_context(&self) -> bool {
2780        self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
2781    }
2782
2783    fn exit_function(&mut self, span: Span) {
2784        if self.source_sensitive_functions.contains(&span_key(span)) {
2785            self.unsafe_function_depth -= 1;
2786        }
2787    }
2788}
2789
2790fn function_label<State>(
2791    own_name: Option<&str>,
2792    context: &TraverseCtx<'_, State>,
2793) -> Option<String> {
2794    if let Some(name) = own_name {
2795        return Some(name.to_string());
2796    }
2797    match context.ancestors().next()? {
2798        Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
2799        Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
2800        Ancestor::VariableDeclaratorInit(parent) => parent
2801            .id()
2802            .get_binding_identifier()
2803            .map(|identifier| identifier.name.to_string()),
2804        _ => None,
2805    }
2806}
2807
2808fn property_label(key: &PropertyKey<'_>) -> Option<String> {
2809    key.static_name()
2810        .map(|name| name.into_owned())
2811        .or_else(|| match key {
2812            PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
2813            _ => None,
2814        })
2815}
2816
2817fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
2818    match context.ancestors().next() {
2819        Some(Ancestor::ObjectPropertyValue(parent))
2820            if *parent.method() || *parent.kind() != PropertyKind::Init =>
2821        {
2822            *parent.span()
2823        }
2824        Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
2825        _ => span,
2826    }
2827}
2828
2829fn type_only_import(statement: &Statement<'_>) -> bool {
2830    let Statement::ImportDeclaration(declaration) = statement else {
2831        return false;
2832    };
2833    // Inline `import { type T }` can leave an empty runtime import under
2834    // verbatimModuleSyntax. Only declaration-level `import type` is universal.
2835    declaration.import_kind == ImportOrExportKind::Type
2836}
2837
2838/// Local binding classification, not dependency/taint analysis. The caller
2839/// enables implicit elision only for a recognized compiler configuration.
2840fn erased_imports(program: &Program<'_>, elide_type_imports: bool) -> HashSet<SpanKey> {
2841    let semantic = SemanticBuilder::new().build(program).semantic;
2842    let scoping = semantic.scoping();
2843    program
2844        .body
2845        .iter()
2846        .filter_map(|statement| {
2847            let Statement::ImportDeclaration(declaration) = statement else {
2848                return None;
2849            };
2850            let erased = declaration.import_kind == ImportOrExportKind::Type
2851                || (elide_type_imports
2852                    && program.source_type.is_typescript()
2853                    && !program.source_type.is_jsx()
2854                    && declaration.specifiers.as_ref().is_some_and(|specifiers| {
2855                        !specifiers.is_empty()
2856                            && specifiers.iter().all(|specifier| {
2857                                let local = match specifier {
2858                                    ImportDeclarationSpecifier::ImportSpecifier(s) => {
2859                                        if s.import_kind == ImportOrExportKind::Type {
2860                                            return true;
2861                                        }
2862                                        &s.local
2863                                    }
2864                                    ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
2865                                        &s.local
2866                                    }
2867                                    ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
2868                                        &s.local
2869                                    }
2870                                };
2871                                local.symbol_id.get().is_some_and(|symbol| {
2872                                    let mut refs =
2873                                        scoping.get_resolved_references(symbol).peekable();
2874                                    // An unused binding may be needed by a JSX/decorator
2875                                    // transform. Only positively identified type uses count.
2876                                    refs.peek().is_some()
2877                                        && refs.all(|r| r.is_type() && !r.is_value())
2878                                })
2879                            })
2880                    }));
2881            erased.then_some(span_key(declaration.span))
2882        })
2883        .collect()
2884}
2885
2886fn executable_statement(statement: &Statement<'_>) -> bool {
2887    !matches!(
2888        statement,
2889        Statement::BlockStatement(_)
2890            | Statement::EmptyStatement(_)
2891            | Statement::FunctionDeclaration(_)
2892            | Statement::ExportNamedDeclaration(_)
2893            | Statement::ExportDefaultDeclaration(_)
2894    ) && !statement.is_typescript_syntax()
2895        && !type_only_import(statement)
2896}
2897
2898impl<'a> Traverse<'a, ()> for PointCollector<'_> {
2899    fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
2900        let mut ancestors = context.ancestors();
2901        let parent = ancestors.next();
2902        let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
2903            && matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
2904        if self.pass != PointPass::Statements
2905            || self.unsafe_context()
2906            || !executable_statement(node)
2907            || self.erased_imports.contains(&span_key(node.span()))
2908            || matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
2909            || expression_arrow_body
2910        {
2911            return;
2912        }
2913        let point = self.point(node.span(), "statement", None);
2914        self.statement_targets
2915            .entry(span_key(node.span()))
2916            .or_default()
2917            .push(point.id.clone());
2918        self.points.push(point);
2919    }
2920
2921    // A JSX tree is one statement, so everything rendered inside it -- an
2922    // `aria-label={label(state)}`, a child `{formatted(value)}` -- counted as
2923    // covered the moment the component rendered once, even when that
2924    // expression never evaluated. It is also the seam a UI assertion attaches
2925    // to: `toHaveAccessibleName` is about the attribute and `toHaveTextContent`
2926    // about the child, and with one statement for the whole tree neither could
2927    // be named apart from the other.
2928    //
2929    // Only expressions that can independently fail to evaluate earn a point.
2930    // A bare identifier or member chain cannot -- it is reached exactly when
2931    // the enclosing statement is -- and a function is already measured where
2932    // it is called rather than where it is created.
2933    fn enter_expression(&mut self, node: &mut Expression<'a>, context: &mut TraverseCtx<'a, ()>) {
2934        if self.pass != PointPass::Statements
2935            || self.unsafe_context()
2936            || !matches!(
2937                context.ancestors().next(),
2938                Some(Ancestor::JSXExpressionContainerExpression(_))
2939            )
2940            || !rendered_expression_is_measurable(node)
2941        {
2942            return;
2943        }
2944        let span = node.span();
2945        let point = self.point(span, "statement", Some("jsx-rendered-expression".into()));
2946        self.rendered_targets
2947            .insert(span_key(span), point.id.clone());
2948        self.points.push(point);
2949    }
2950
2951    fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
2952        if self.pass != PointPass::Statements
2953            || self.unsafe_context()
2954            || node.is_typescript_syntax()
2955            || matches!(node, Declaration::FunctionDeclaration(_))
2956            || !matches!(
2957                context.ancestors().next(),
2958                Some(Ancestor::ExportNamedDeclarationDeclaration(_))
2959                    | Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2960            )
2961        {
2962            return;
2963        }
2964        let point = self.point(node.span(), "statement", None);
2965        self.statement_targets
2966            .entry(span_key(node.span()))
2967            .or_default()
2968            .push(point.id.clone());
2969        self.points.push(point);
2970    }
2971
2972    fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
2973        // oxc represents `export default class` directly inside
2974        // ExportDefaultDeclarationKind rather than through Declaration, so it
2975        // does not reach enter_declaration. Babel treats the inner class as the
2976        // executable statement obligation (excluding the `export default`
2977        // prefix), which is also the location where the probe belongs.
2978        if self.pass != PointPass::Statements
2979            || self.unsafe_context()
2980            || node.declare
2981            || !matches!(
2982                context.ancestors().next(),
2983                Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2984            )
2985        {
2986            return;
2987        }
2988        let point = self.point(node.span, "statement", None);
2989        self.statement_targets
2990            .entry(span_key(node.span))
2991            .or_default()
2992            .push(point.id.clone());
2993        self.points.push(point);
2994    }
2995
2996    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
2997        let point_span = function_point_span(node.span, context);
2998        let label = if point_span == node.span {
2999            function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
3000        } else {
3001            None
3002        };
3003        if self
3004            .source_sensitive_functions
3005            .contains(&span_key(node.span))
3006        {
3007            self.unsafe_function_depth += 1;
3008            return;
3009        }
3010        if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
3011            let point = self.point(point_span, "function", label);
3012            self.function_targets
3013                .insert(span_key(node.span), point.id.clone());
3014            self.points.push(point);
3015        }
3016    }
3017
3018    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
3019        self.exit_function(node.span);
3020    }
3021
3022    fn enter_arrow_function_expression(
3023        &mut self,
3024        node: &mut ArrowFunctionExpression<'a>,
3025        context: &mut TraverseCtx<'a, ()>,
3026    ) {
3027        let point_span = function_point_span(node.span, context);
3028        let label = if point_span == node.span {
3029            function_label(None, context)
3030        } else {
3031            None
3032        };
3033        if self
3034            .source_sensitive_functions
3035            .contains(&span_key(node.span))
3036        {
3037            self.unsafe_function_depth += 1;
3038            return;
3039        }
3040        if self.pass == PointPass::Functions && !self.unsafe_context() {
3041            let point = self.point(point_span, "function", label);
3042            self.function_targets
3043                .insert(span_key(node.span), point.id.clone());
3044            self.points.push(point);
3045        }
3046    }
3047
3048    fn exit_arrow_function_expression(
3049        &mut self,
3050        node: &mut ArrowFunctionExpression<'a>,
3051        _context: &mut TraverseCtx<'a, ()>,
3052    ) {
3053        self.exit_function(node.span);
3054    }
3055
3056    fn enter_with_statement(
3057        &mut self,
3058        _node: &mut WithStatement<'a>,
3059        _context: &mut TraverseCtx<'a, ()>,
3060    ) {
3061        self.with_depth += 1;
3062    }
3063
3064    fn exit_with_statement(
3065        &mut self,
3066        _node: &mut WithStatement<'a>,
3067        _context: &mut TraverseCtx<'a, ()>,
3068    ) {
3069        self.with_depth -= 1;
3070    }
3071
3072    fn enter_ts_global_declaration(
3073        &mut self,
3074        _node: &mut TSGlobalDeclaration<'a>,
3075        _context: &mut TraverseCtx<'a, ()>,
3076    ) {
3077        self.ambient_depth += 1;
3078    }
3079
3080    fn exit_ts_global_declaration(
3081        &mut self,
3082        _node: &mut TSGlobalDeclaration<'a>,
3083        _context: &mut TraverseCtx<'a, ()>,
3084    ) {
3085        self.ambient_depth -= 1;
3086    }
3087
3088    fn enter_ts_module_declaration(
3089        &mut self,
3090        node: &mut TSModuleDeclaration<'a>,
3091        _context: &mut TraverseCtx<'a, ()>,
3092    ) {
3093        if node.declare {
3094            self.ambient_depth += 1;
3095        }
3096    }
3097
3098    fn exit_ts_module_declaration(
3099        &mut self,
3100        node: &mut TSModuleDeclaration<'a>,
3101        _context: &mut TraverseCtx<'a, ()>,
3102    ) {
3103        if node.declare {
3104            self.ambient_depth -= 1;
3105        }
3106    }
3107}
3108
3109/// Whether a JSX expression container holds something that can independently
3110/// fail to evaluate, and so is worth a coverage obligation of its own.
3111fn rendered_expression_is_measurable(expression: &Expression<'_>) -> bool {
3112    match expression {
3113        // Already measured where it is called.
3114        Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
3115        // Reached exactly when the enclosing statement is.
3116        Expression::Identifier(_)
3117        | Expression::ThisExpression(_)
3118        | Expression::StaticMemberExpression(_)
3119        | Expression::ComputedMemberExpression(_)
3120        | Expression::PrivateFieldExpression(_)
3121        | Expression::JSXElement(_)
3122        | Expression::JSXFragment(_) => false,
3123        // `user?.id` is a member access that happens to short-circuit; it is
3124        // reached with the statement like any other. `user?.load()` is not.
3125        Expression::ChainExpression(chain) => {
3126            matches!(chain.expression, ChainElement::CallExpression(_))
3127        }
3128        other => !other.is_literal(),
3129    }
3130}
3131
3132fn collect_points<'a>(
3133    allocator: &'a Allocator,
3134    program: &mut Program<'a>,
3135    source: &str,
3136    file: &str,
3137    source_sensitive_functions: &HashSet<SpanKey>,
3138    erased_imports: &HashSet<SpanKey>,
3139) -> PointAnalysis {
3140    let mut analysis = PointAnalysis::default();
3141    for pass in [PointPass::Statements, PointPass::Functions] {
3142        let mut collector = PointCollector {
3143            source,
3144            file,
3145            pass,
3146            points: Vec::new(),
3147            statement_targets: HashMap::new(),
3148            function_targets: HashMap::new(),
3149            rendered_targets: HashMap::new(),
3150            source_sensitive_functions,
3151            erased_imports,
3152            unsafe_function_depth: 0,
3153            with_depth: 0,
3154            ambient_depth: 0,
3155        };
3156        traverse_mut(&mut collector, allocator, program, Default::default(), ());
3157        analysis.points.extend(collector.points);
3158        analysis
3159            .statement_targets
3160            .extend(collector.statement_targets);
3161        analysis.function_targets.extend(collector.function_targets);
3162        analysis.rendered_targets.extend(collector.rendered_targets);
3163    }
3164    analysis
3165}
3166
3167impl<'s> SafetyScanner<'s> {
3168    fn new(source: &'s str, file: &'s str, scoping: &'s oxc_semantic::Scoping) -> Self {
3169        Self {
3170            source,
3171            file,
3172            scoping,
3173            source_reads: HashSet::new(),
3174            source_read_properties: HashSet::new(),
3175            source_sensitive_functions: HashSet::new(),
3176            function_limitations: Vec::new(),
3177            with_limitations: Vec::new(),
3178            dynamic_limitations: Vec::new(),
3179            unsafe_function_depth: 0,
3180            with_depth: 0,
3181        }
3182    }
3183
3184    fn limitation(
3185        &self,
3186        span: Span,
3187        kind: &str,
3188        suffix: &str,
3189        reason: &str,
3190    ) -> CandidateLimitation {
3191        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
3192        CandidateLimitation {
3193            id: stable_id(self.source, self.file, kind, span, suffix),
3194            kind: kind.to_string(),
3195            file: self.file.to_string(),
3196            line,
3197            column,
3198            source: source_slice(self.source, span).to_string(),
3199            reason: reason.to_string(),
3200        }
3201    }
3202
3203    fn enter_source_sensitive_function<State>(
3204        &mut self,
3205        span: Span,
3206        bindings: Vec<SymbolId>,
3207        context: &TraverseCtx<'_, State>,
3208    ) {
3209        // A function handed to a compile-time style macro never runs: the
3210        // bundler's plugin evaluates it while building and replaces the whole
3211        // call. Probing its body has nothing to measure and breaks the build
3212        // (StyleX rejects a block-bodied dynamic style). Leave it as source,
3213        // with no limitation: there is no runtime behavior to lose.
3214        if compile_time_macro_argument(context) {
3215            self.source_sensitive_functions.insert(span_key(span));
3216            self.unsafe_function_depth += 1;
3217            return;
3218        }
3219        let sensitive = observes_function_source(span, context)
3220            || bindings
3221                .iter()
3222                .any(|symbol| self.source_reads.contains(symbol))
3223            || property_name_of_function(context)
3224                .is_some_and(|name| self.source_read_properties.contains(&name));
3225        if sensitive {
3226            self.source_sensitive_functions.insert(span_key(span));
3227            let limitation = self.limitation(
3228                span,
3229                "semantic-safety",
3230                "function-source",
3231                "function body is left uninstrumented because this expression observes or coerces Function source text",
3232            );
3233            self.function_limitations.push(limitation);
3234            self.unsafe_function_depth += 1;
3235        }
3236    }
3237
3238    fn exit_source_sensitive_function(&mut self, span: Span) {
3239        if self.source_sensitive_functions.contains(&span_key(span)) {
3240            self.unsafe_function_depth -= 1;
3241        }
3242    }
3243
3244    fn is_unsafe_context(&self) -> bool {
3245        self.unsafe_function_depth > 0 || self.with_depth > 0
3246    }
3247}
3248
3249impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
3250    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
3251        let bindings = bound_symbols(node.id.as_ref(), self.scoping, context);
3252        self.enter_source_sensitive_function(node.span, bindings, context);
3253    }
3254
3255    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
3256        self.exit_source_sensitive_function(node.span);
3257    }
3258
3259    fn enter_arrow_function_expression(
3260        &mut self,
3261        node: &mut ArrowFunctionExpression<'a>,
3262        context: &mut TraverseCtx<'a, ()>,
3263    ) {
3264        let bindings = bound_symbols(None, self.scoping, context);
3265        self.enter_source_sensitive_function(node.span, bindings, context);
3266    }
3267
3268    fn exit_arrow_function_expression(
3269        &mut self,
3270        node: &mut ArrowFunctionExpression<'a>,
3271        _context: &mut TraverseCtx<'a, ()>,
3272    ) {
3273        self.exit_source_sensitive_function(node.span);
3274    }
3275
3276    fn enter_with_statement(
3277        &mut self,
3278        node: &mut WithStatement<'a>,
3279        _context: &mut TraverseCtx<'a, ()>,
3280    ) {
3281        let limitation = self.limitation(
3282            node.span,
3283            "semantic-safety",
3284            "with-environment",
3285            "with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
3286        );
3287        self.with_limitations.push(limitation);
3288        self.with_depth += 1;
3289    }
3290
3291    fn exit_with_statement(
3292        &mut self,
3293        _node: &mut WithStatement<'a>,
3294        _context: &mut TraverseCtx<'a, ()>,
3295    ) {
3296        self.with_depth -= 1;
3297    }
3298
3299    fn enter_call_expression(
3300        &mut self,
3301        node: &mut CallExpression<'a>,
3302        _context: &mut TraverseCtx<'a, ()>,
3303    ) {
3304        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
3305            return;
3306        }
3307        let limitation = self.limitation(
3308            node.span,
3309            "dynamic-code",
3310            "eval",
3311            "eval-generated source has no stable pre-run coverage denominator",
3312        );
3313        self.dynamic_limitations.push(limitation);
3314    }
3315
3316    fn enter_new_expression(
3317        &mut self,
3318        node: &mut NewExpression<'a>,
3319        _context: &mut TraverseCtx<'a, ()>,
3320    ) {
3321        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
3322            return;
3323        }
3324        let limitation = self.limitation(
3325            node.span,
3326            "dynamic-code",
3327            "Function",
3328            "Function-generated source has no stable pre-run coverage denominator",
3329        );
3330        self.dynamic_limitations.push(limitation);
3331    }
3332}
3333
3334fn analyze_safety<'a>(
3335    allocator: &'a Allocator,
3336    program: &mut Program<'a>,
3337    source: &str,
3338    file: &str,
3339) -> SafetyAnalysis {
3340    // oxc_traverse uses resolved lexical scope IDs while walking ancestry.
3341    // Building semantics here initializes those IDs without changing the AST.
3342    let scoping = SemanticBuilder::new()
3343        .build(program)
3344        .semantic
3345        .into_scoping();
3346    let mut reads = SourceReads {
3347        scoping: &scoping,
3348        symbols: HashSet::new(),
3349        properties: HashSet::new(),
3350    };
3351    reads.visit_program(program);
3352    let mut scanner = SafetyScanner::new(source, file, &scoping);
3353    scanner.source_reads = reads.symbols;
3354    scanner.source_read_properties = reads.properties;
3355    traverse_mut(&mut scanner, allocator, program, Default::default(), ());
3356    let mut semantic_limitations = scanner.function_limitations;
3357    semantic_limitations.extend(scanner.with_limitations);
3358    SafetyAnalysis {
3359        source_sensitive_functions: scanner.source_sensitive_functions,
3360        semantic_limitations,
3361        dynamic_limitations: scanner.dynamic_limitations,
3362    }
3363}
3364
3365fn span_key(span: Span) -> SpanKey {
3366    (span.start, span.end)
3367}
3368
3369fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
3370    matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
3371}
3372
3373fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
3374    match pattern {
3375        BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
3376        _ => None,
3377    }
3378}
3379
3380fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
3381    match expression {
3382        Expression::ArrowFunctionExpression(_) => true,
3383        Expression::FunctionExpression(function) => function.id.is_none(),
3384        Expression::ClassExpression(class) => class.id.is_none(),
3385        Expression::ParenthesizedExpression(expression) => {
3386            expression_is_anonymous_definition(&expression.expression)
3387        }
3388        Expression::TSAsExpression(expression) => {
3389            expression_is_anonymous_definition(&expression.expression)
3390        }
3391        Expression::TSSatisfiesExpression(expression) => {
3392            expression_is_anonymous_definition(&expression.expression)
3393        }
3394        Expression::TSTypeAssertion(expression) => {
3395            expression_is_anonymous_definition(&expression.expression)
3396        }
3397        Expression::TSNonNullExpression(expression) => {
3398            expression_is_anonymous_definition(&expression.expression)
3399        }
3400        _ => false,
3401    }
3402}
3403
3404/// Takes a type assertion off an assignment target before the program is
3405/// printed.
3406///
3407/// The printer writes `(buffer[0] as string) += child` without its
3408/// parentheses, and `buffer[0] as string += child` does not parse: hono's
3409/// `src/jsx/base.ts` and `src/helper/html/index.ts` came out as files neither
3410/// TypeScript nor a bundler could read. An assertion on a target says nothing
3411/// at run time, and the instrumented file is not type-checked, so the target
3412/// is printed without it.
3413struct AssertedTargetTransformer<'a> {
3414    ast: AstBuilder<'a>,
3415}
3416
3417impl<'a> AssertedTargetTransformer<'a> {
3418    fn target(
3419        expression: Expression<'a>,
3420    ) -> Result<oxc_ast::ast::SimpleAssignmentTarget<'a>, Expression<'a>> {
3421        Ok(match expression {
3422            Expression::Identifier(identifier) => {
3423                oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(identifier)
3424            }
3425            Expression::ComputedMemberExpression(member) => {
3426                oxc_ast::ast::SimpleAssignmentTarget::ComputedMemberExpression(member)
3427            }
3428            Expression::StaticMemberExpression(member) => {
3429                oxc_ast::ast::SimpleAssignmentTarget::StaticMemberExpression(member)
3430            }
3431            Expression::PrivateFieldExpression(member) => {
3432                oxc_ast::ast::SimpleAssignmentTarget::PrivateFieldExpression(member)
3433            }
3434            Expression::TSAsExpression(asserted) => {
3435                oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted)
3436            }
3437            Expression::TSSatisfiesExpression(asserted) => {
3438                oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted)
3439            }
3440            Expression::TSNonNullExpression(asserted) => {
3441                oxc_ast::ast::SimpleAssignmentTarget::TSNonNullExpression(asserted)
3442            }
3443            Expression::TSTypeAssertion(asserted) => {
3444                oxc_ast::ast::SimpleAssignmentTarget::TSTypeAssertion(asserted)
3445            }
3446            Expression::ParenthesizedExpression(parenthesized) => {
3447                return Self::target(parenthesized.unbox().expression);
3448            }
3449            other => return Err(other),
3450        })
3451    }
3452}
3453
3454impl<'a> VisitMut<'a> for AssertedTargetTransformer<'a> {
3455    fn visit_simple_assignment_target(
3456        &mut self,
3457        target: &mut oxc_ast::ast::SimpleAssignmentTarget<'a>,
3458    ) {
3459        loop {
3460            let inner = match target {
3461                oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
3462                    &mut asserted.expression
3463                }
3464                oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
3465                    &mut asserted.expression
3466                }
3467                _ => break,
3468            };
3469            match Self::target(inner.take_in(self.ast.allocator)) {
3470                Ok(bare) => *target = bare,
3471                Err(kept) => {
3472                    // Not a target the grammar allows; print it as it came.
3473                    match target {
3474                        oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
3475                            asserted.expression = kept;
3476                        }
3477                        oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
3478                            asserted.expression = kept;
3479                        }
3480                        _ => unreachable!("the target was matched above"),
3481                    }
3482                    break;
3483                }
3484            }
3485        }
3486        walk_mut::walk_simple_assignment_target(self, target);
3487    }
3488}
3489
3490/// The module a directly run TypeScript file names its helpers' types from.
3491/// The frontend replaces it with the path to the runtime beside the file.
3492pub const RUNTIME_TYPES_MODULE: &str = "virtual:supercov-helper-types";
3493
3494/// `typeof import("…").helper`, as the annotation of the variable a helper is
3495/// bound to.
3496fn runtime_helper_type<'a>(
3497    allocator: &'a Allocator,
3498    source_type: SourceType,
3499    helper: &str,
3500) -> Option<oxc_allocator::Box<'a, oxc_ast::ast::TSTypeAnnotation<'a>>> {
3501    let text = allocator.alloc_str(&format!(
3502        "var helper: typeof import(\"{RUNTIME_TYPES_MODULE}\").{helper};"
3503    ));
3504    let mut parsed = Parser::new(allocator, text, source_type).parse();
3505    if !parsed.errors.is_empty() {
3506        return None;
3507    }
3508    match parsed.program.body.first_mut()? {
3509        Statement::VariableDeclaration(declaration) => {
3510            declaration.declarations.first_mut()?.type_annotation.take()
3511        }
3512        _ => None,
3513    }
3514}
3515
3516struct AssignmentNameSafetyTransformer<'a> {
3517    ast: AstBuilder<'a>,
3518    parenthesized_assignment_value: String,
3519}
3520
3521impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
3522    fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
3523        walk_mut::walk_assignment_expression(self, assignment);
3524        let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
3525            return;
3526        };
3527        if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
3528            || assignment.span.start == identifier.span.start
3529            || !expression_is_anonymous_definition(&assignment.right)
3530        {
3531            return;
3532        }
3533        let right = assignment.right.take_in(self.ast.allocator);
3534        assignment.right = self.ast.expression_call(
3535            Span::default(),
3536            self.ast.expression_identifier(
3537                Span::default(),
3538                self.ast.ident(&self.parenthesized_assignment_value),
3539            ),
3540            NONE,
3541            self.ast.vec_from_array([
3542                Argument::from(right),
3543                Argument::from(self.ast.expression_string_literal(
3544                    Span::default(),
3545                    identifier.name,
3546                    None,
3547                )),
3548            ]),
3549            false,
3550        );
3551    }
3552}
3553
3554/// Compile-time style macros whose arguments the bundler consumes at build
3555/// time. Only the namespaced form (`stylex.create(...)`) is recognized: it is
3556/// how the StyleX documentation and its Vite/Babel plugins expect the API to
3557/// be used, and it needs no binding resolution here.
3558const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
3559    "create",
3560    "createTheme",
3561    "defineConsts",
3562    "defineVars",
3563    "firstThatWorks",
3564    "keyframes",
3565    "positionTry",
3566    "viewTransitionClass",
3567];
3568
3569fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
3570    context.ancestors().any(|ancestor| {
3571        let Ancestor::CallExpressionArguments(parent) = ancestor else {
3572            return false;
3573        };
3574        let Expression::StaticMemberExpression(member) = parent.callee() else {
3575            return false;
3576        };
3577        let Expression::Identifier(object) = &member.object else {
3578            return false;
3579        };
3580        object.name == "stylex"
3581            && COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
3582    })
3583}
3584
3585fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
3586    let mut child_end = span.end;
3587    for ancestor in context.ancestors() {
3588        match ancestor {
3589            Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
3590            Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
3591            Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
3592            Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
3593            Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
3594            Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
3595            Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
3596            Ancestor::LogicalExpressionLeft(parent) => {
3597                child_end = parent.span().end;
3598            }
3599            Ancestor::LogicalExpressionRight(parent) => {
3600                child_end = parent.span().end;
3601            }
3602            Ancestor::SequenceExpressionExpressions(parent) => {
3603                if child_end != parent.span().end {
3604                    return false;
3605                }
3606                child_end = parent.span().end;
3607            }
3608            Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
3609            Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
3610            Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
3611            Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
3612            Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
3613            Ancestor::ComputedMemberExpressionExpression(_) => return true,
3614            Ancestor::StaticMemberExpressionObject(parent) => {
3615                return parent.property().name == "toString";
3616            }
3617            Ancestor::CallExpressionArguments(parent) => {
3618                return expression_is_identifier(parent.callee(), "String");
3619            }
3620            Ancestor::TemplateLiteralExpressions(_) => return true,
3621            Ancestor::BinaryExpressionLeft(parent) => {
3622                return matches!(
3623                    parent.operator(),
3624                    BinaryOperator::Addition
3625                        | BinaryOperator::LessThan
3626                        | BinaryOperator::LessEqualThan
3627                        | BinaryOperator::GreaterThan
3628                        | BinaryOperator::GreaterEqualThan
3629                );
3630            }
3631            Ancestor::BinaryExpressionRight(parent) => {
3632                return matches!(
3633                    parent.operator(),
3634                    BinaryOperator::Addition
3635                        | BinaryOperator::LessThan
3636                        | BinaryOperator::LessEqualThan
3637                        | BinaryOperator::GreaterThan
3638                        | BinaryOperator::GreaterEqualThan
3639                );
3640            }
3641            _ => return false,
3642        }
3643    }
3644    false
3645}
3646
3647pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3648    let _positions = PositionScope::enter();
3649    let elide_type_imports = false;
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        value_decisions: HashMap::new(),
3711        logical_root: None,
3712        value_logical_nodes: HashSet::new(),
3713    };
3714    collector.visit_program(&parsed.program);
3715    let optional_analysis = collect_optional_member_branches(
3716        &allocator,
3717        &mut parsed.program,
3718        source,
3719        file,
3720        &safety.source_sensitive_functions,
3721    );
3722    let call_analysis = collect_optional_call_branches(
3723        &allocator,
3724        &mut parsed.program,
3725        source,
3726        file,
3727        &safety.source_sensitive_functions,
3728    );
3729    let assignment_analysis = collect_logical_assignment_branches(
3730        &allocator,
3731        &mut parsed.program,
3732        source,
3733        file,
3734        &safety.source_sensitive_functions,
3735    );
3736    let default_analysis = collect_default_branches(
3737        &allocator,
3738        &mut parsed.program,
3739        source,
3740        file,
3741        &safety.source_sensitive_functions,
3742    );
3743    let extended_analysis = collect_extended_branches(
3744        &allocator,
3745        &mut parsed.program,
3746        source,
3747        file,
3748        &safety.source_sensitive_functions,
3749    );
3750    let logical_analysis = collect_logical_value_branches(
3751        &allocator,
3752        &mut parsed.program,
3753        source,
3754        file,
3755        &collector.decision_logical_nodes,
3756        &safety.source_sensitive_functions,
3757    );
3758    let switch_analysis = collect_switch_branches(
3759        &allocator,
3760        &mut parsed.program,
3761        source,
3762        file,
3763        &safety.source_sensitive_functions,
3764    );
3765    let mut branches = optional_analysis.branches;
3766    branches.extend(call_analysis.branches);
3767    branches.extend(assignment_analysis.branches);
3768    branches.extend(default_analysis.branches);
3769    branches.extend(extended_analysis.branches);
3770    branches.extend(logical_analysis.branches);
3771    branches.extend(switch_analysis.branches);
3772    AssertedTargetTransformer {
3773        ast: AstBuilder::new(&allocator),
3774    }
3775    .visit_program(&mut parsed.program);
3776    let (generated, map) =
3777        generate_candidate(&parsed.program, file, &safety.source_sensitive_functions)?;
3778    Ok(CandidateOutput {
3779        engine: "rust-oxc".to_string(),
3780        complete: false,
3781        supported_surface: "control-decision-manifest-v1".to_string(),
3782        code: generated,
3783        map,
3784        decisions: collector.decisions,
3785        points: point_analysis.points,
3786        excluded_statements,
3787        branches,
3788        runtime: None,
3789        coverage_limitations: {
3790            let mut limitations = safety.semantic_limitations;
3791            limitations.extend(call_analysis.limitations);
3792            limitations.extend(default_analysis.limitations);
3793            limitations.extend(safety.dynamic_limitations);
3794            limitations
3795        },
3796        limitations: vec![
3797            "candidate emits metadata only; use the private differential transform for probes"
3798                .to_string(),
3799            "coverage points, value branches, and extended branch obligations are not included"
3800                .to_string(),
3801        ],
3802    })
3803}
3804
3805fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
3806    match value {
3807        serde_json::Value::Null => ast.expression_null_literal(Span::default()),
3808        serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
3809        serde_json::Value::Number(value) => ast.expression_numeric_literal(
3810            Span::default(),
3811            value
3812                .as_f64()
3813                .expect("coverage registration numbers must fit JavaScript"),
3814            None,
3815            NumberBase::Decimal,
3816        ),
3817        serde_json::Value::String(value) => {
3818            ast.expression_string_literal(Span::default(), ast.str(value), None)
3819        }
3820        serde_json::Value::Array(values) => ast.expression_array(
3821            Span::default(),
3822            ast.vec_from_iter(
3823                values
3824                    .iter()
3825                    .map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
3826            ),
3827        ),
3828        serde_json::Value::Object(properties) => ast.expression_object(
3829            Span::default(),
3830            ast.vec_from_iter(properties.iter().map(|(key, value)| {
3831                ast.object_property_kind_object_property(
3832                    Span::default(),
3833                    PropertyKind::Init,
3834                    ast.property_key_static_identifier(Span::default(), ast.ident(key)),
3835                    json_expression(ast, value),
3836                    false,
3837                    false,
3838                    false,
3839                )
3840            })),
3841        ),
3842    }
3843}
3844
3845/// Instrument JavaScript with the complete frozen v1 denominator and probe-v2
3846/// runtime ABI. The source-transform contract is complete and independently
3847/// conformance-tested; selection of the Rust engine remains private until the
3848/// Phase 4 CLI/orchestration cutover is complete.
3849pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3850    instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None, false)
3851}
3852
3853/// Emit code for an isolated source-executing workspace. Unlike the module-
3854/// import form, this has no virtual module dependency: the generated
3855/// Node preload installs the frozen runtime on this global before user modules
3856/// evaluate.
3857pub fn instrument_direct_candidate(
3858    source: &str,
3859    file: &str,
3860) -> Result<CandidateOutput, CandidateError> {
3861    instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None, false)
3862}
3863
3864pub fn instrument_with_import_policy(
3865    source: &str,
3866    file: &str,
3867    capability_wrapper: &str,
3868    elide_type_imports: bool,
3869) -> Result<CandidateOutput, CandidateError> {
3870    instrument_candidate_with_binding(
3871        source,
3872        file,
3873        RuntimeBinding::DirectGlobal,
3874        Some(capability_wrapper),
3875        elide_type_imports,
3876    )
3877}
3878
3879fn instrument_candidate_with_binding(
3880    source: &str,
3881    file: &str,
3882    runtime_binding: RuntimeBinding,
3883    capability_wrapper: Option<&str>,
3884    elide_type_imports: bool,
3885) -> Result<CandidateOutput, CandidateError> {
3886    let _positions = PositionScope::enter();
3887    let source_type =
3888        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3889    let allocator = Allocator::default();
3890    let mut parsed = Parser::new(&allocator, source, source_type).parse();
3891    if !parsed.errors.is_empty() {
3892        return Err(CandidateError::Parse(
3893            parsed
3894                .errors
3895                .iter()
3896                .map(|error| {
3897                    parse_message(
3898                        source,
3899                        &error.message,
3900                        error
3901                            .labels
3902                            .as_ref()
3903                            .and_then(|labels| labels.first())
3904                            .map(|label| label.offset()),
3905                    )
3906                })
3907                .collect(),
3908        ));
3909    }
3910
3911    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3912    let erased = erased_imports(&parsed.program, elide_type_imports);
3913    let excluded_statements = parsed
3914        .program
3915        .body
3916        .iter()
3917        .filter(|s| erased.contains(&span_key(s.span())))
3918        .map(|s| {
3919            let span = s.span();
3920            let (line, column) = line_and_utf16_column(source, span.start as usize);
3921            CandidatePoint {
3922                id: stable_id(source, file, "statement", span, ""),
3923                kind: "statement".into(),
3924                file: file.into(),
3925                line,
3926                column,
3927                source: source_slice(source, span).into(),
3928                label: Some("typescript-import-erasure".into()),
3929            }
3930        })
3931        .collect();
3932    let point_analysis = collect_points(
3933        &allocator,
3934        &mut parsed.program,
3935        source,
3936        file,
3937        &safety.source_sensitive_functions,
3938        &erased,
3939    );
3940    let mut collector = DecisionCollector {
3941        source,
3942        file,
3943        decisions: Vec::new(),
3944        decision_vector_counts: Vec::new(),
3945        decision_logical_nodes: HashSet::new(),
3946        source_sensitive_functions: &safety.source_sensitive_functions,
3947        value_decisions: HashMap::new(),
3948        logical_root: None,
3949        value_logical_nodes: HashSet::new(),
3950    };
3951    collector.visit_program(&parsed.program);
3952    let optional_analysis = collect_optional_member_branches(
3953        &allocator,
3954        &mut parsed.program,
3955        source,
3956        file,
3957        &safety.source_sensitive_functions,
3958    );
3959    let call_analysis = collect_optional_call_branches(
3960        &allocator,
3961        &mut parsed.program,
3962        source,
3963        file,
3964        &safety.source_sensitive_functions,
3965    );
3966    let assignment_analysis = collect_logical_assignment_branches(
3967        &allocator,
3968        &mut parsed.program,
3969        source,
3970        file,
3971        &safety.source_sensitive_functions,
3972    );
3973    let default_analysis = collect_default_branches(
3974        &allocator,
3975        &mut parsed.program,
3976        source,
3977        file,
3978        &safety.source_sensitive_functions,
3979    );
3980    let extended_analysis = collect_extended_branches(
3981        &allocator,
3982        &mut parsed.program,
3983        source,
3984        file,
3985        &safety.source_sensitive_functions,
3986    );
3987    let logical_analysis = collect_logical_value_branches(
3988        &allocator,
3989        &mut parsed.program,
3990        source,
3991        file,
3992        &collector.decision_logical_nodes,
3993        &safety.source_sensitive_functions,
3994    );
3995    let switch_analysis = collect_switch_branches(
3996        &allocator,
3997        &mut parsed.program,
3998        source,
3999        file,
4000        &safety.source_sensitive_functions,
4001    );
4002    let mut branches = optional_analysis.branches;
4003    branches.extend(call_analysis.branches.clone());
4004    branches.extend(assignment_analysis.branches);
4005    branches.extend(default_analysis.branches.clone());
4006    branches.extend(extended_analysis.branches.clone());
4007    branches.extend(logical_analysis.branches);
4008    branches.extend(switch_analysis.branches.clone());
4009
4010    // Direct-runtime output can be served as several classic scripts in one
4011    // document, and every classic script shares the page's global lexical
4012    // scope: two files declaring `const __supercovMcdcBegin` is a SyntaxError
4013    // on the second, and a shared probe-file binding would credit one file's
4014    // statements to another. Names are made unique per file, and declared with
4015    // `var` so a script included twice is still legal, as it was before.
4016    let (namespace, runtime_declaration_kind) = if runtime_binding == RuntimeBinding::DirectGlobal {
4017        (
4018            format!(
4019                "_{}",
4020                &stable_id("", file, "runtime", Span::default(), "")[..8]
4021            ),
4022            VariableDeclarationKind::Var,
4023        )
4024    } else {
4025        (String::new(), VariableDeclarationKind::Const)
4026    };
4027    let mut names = CandidateNames::within(source, &namespace);
4028    let mcdc_begin = names.allocate("__supercovMcdcBegin");
4029    let mcdc_condition = names.allocate("__supercovMcdcCondition");
4030    let mcdc_end = names.allocate("__supercovMcdcEnd");
4031    let coverage_hit = names.allocate("__supercovCoverageHit");
4032    let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
4033    let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
4034    let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
4035    let probe_file_v2 = names.allocate("__supercovProbeFileV2");
4036    let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
4037    let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
4038    let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
4039    let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
4040    let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
4041    let with_request_phase = names.allocate("__supercovWithRequestPhase");
4042    let optional_select_v2 = names.allocate("__supercovOptionalSelectV2");
4043    let select_short_v2 = names.allocate("__supercovSelectShortV2");
4044    let select_right_v2 = names.allocate("__supercovSelectRightV2");
4045    let select_named_right_v2 = names.allocate("__supercovSelectNamedRightV2");
4046    let select_assign_end_v2 = names.allocate("__supercovSelectAssignEndV2");
4047    let select_path_v2 = names.allocate("__supercovSelectPathV2");
4048    let select_end2_v2 = names.allocate("__supercovSelectEnd2V2");
4049    let select_end_v2 = names.allocate("__supercovSelectEndV2");
4050    let select_next_v2 = names.allocate("__supercovSelectNextV2");
4051    let rendered_value_v2 = names.allocate("__supercovRenderedValueV2");
4052    let optional_call_end_v2 = names.allocate("__supercovOptionalCallEndV2");
4053    let default_selected_v2 = names.allocate("__supercovDefaultSelectedV2");
4054    let default_entered_v2 = names.allocate("__supercovDefaultEnteredV2");
4055    let typescript = parsed.program.source_type.is_typescript();
4056    let ast = AstBuilder::new(&allocator);
4057    let mut assignment_name_safety = AssignmentNameSafetyTransformer {
4058        ast,
4059        parenthesized_assignment_value: parenthesized_assignment_value.clone(),
4060    };
4061    assignment_name_safety.visit_program(&mut parsed.program);
4062    let point_indices = point_analysis
4063        .points
4064        .iter()
4065        .enumerate()
4066        .map(|(index, point)| (point.id.clone(), index))
4067        .collect::<HashMap<_, _>>();
4068    // A default's two outcomes are V2 points after the statement points, and
4069    // its pending count a slot of the file's `pendingDefaults`, both in source
4070    // order. lru-cache's `set` and `get` destructure options with defaults, and
4071    // the string-keyed count behind each was a third of a hot loop.
4072    let mut default_sites = default_analysis
4073        .parameter_targets
4074        .iter()
4075        .chain(&default_analysis.binding_targets)
4076        .map(|(span, target)| (*span, target.default_id.clone(), target.provided_id.clone()))
4077        .collect::<Vec<_>>();
4078    default_sites.sort();
4079    let mut default_point_ids = Vec::with_capacity(default_sites.len() * 2);
4080    let default_slots = default_sites
4081        .into_iter()
4082        .enumerate()
4083        .map(|(slot, (_, default_id, provided_id))| {
4084            let default_point = point_analysis.points.len() + default_point_ids.len();
4085            default_point_ids.push(default_id.clone());
4086            default_point_ids.push(provided_id);
4087            (
4088                default_id,
4089                DefaultSlot {
4090                    slot,
4091                    default_point,
4092                    provided_point: default_point + 1,
4093                },
4094            )
4095        })
4096        .collect::<HashMap<_, _>>();
4097    let default_count = default_slots.len();
4098    let mut extra = ExtraPoints {
4099        base: point_analysis.points.len() + default_point_ids.len(),
4100        ids: Vec::new(),
4101    };
4102    let selection_first = extra.next();
4103    let selection_targets = extra.allocate(&logical_analysis.logical_targets, |(short, right)| {
4104        vec![short.clone(), short.clone(), right.clone(), right.clone()]
4105    });
4106    // A logical assignment's outcomes are a selection's four points too, next
4107    // to the others, so the runtime records its per-operand outcomes the same.
4108    let assignment_targets = extra.allocate(&assignment_analysis.targets, |(short, right)| {
4109        vec![short.clone(), short.clone(), right.clone(), right.clone()]
4110    });
4111    let selection_count = selection_targets.len() + assignment_targets.len();
4112    let optional_member_targets = extra
4113        .allocate(&optional_analysis.targets, |(short, continued)| {
4114            vec![short.clone(), continued.clone()]
4115        });
4116    let optional_call_sites = extra.allocate(&call_analysis.sites, |(short, continued)| {
4117        vec![short.clone(), continued.clone()]
4118    });
4119    let loop_targets = extra.allocate(&extended_analysis.loop_targets, |target| {
4120        vec![target.first_id.clone(), target.second_id.clone()]
4121    });
4122    let try_targets = extra.allocate(&extended_analysis.try_targets, |target| {
4123        vec![target.first_id.clone(), target.second_id.clone()]
4124    });
4125    let statement_targets = point_analysis
4126        .statement_targets
4127        .into_iter()
4128        .map(|(span, ids)| {
4129            (
4130                span,
4131                ids.into_iter()
4132                    .map(|id| PointTarget {
4133                        index: *point_indices
4134                            .get(&id)
4135                            .expect("statement point must have a global index"),
4136                    })
4137                    .collect(),
4138            )
4139        })
4140        .collect();
4141    let function_targets = point_analysis
4142        .function_targets
4143        .into_iter()
4144        .map(|(span, id)| {
4145            (
4146                span,
4147                PointTarget {
4148                    index: *point_indices
4149                        .get(&id)
4150                        .expect("function point must have a global index"),
4151                },
4152            )
4153        })
4154        .collect();
4155    let rendered_targets = point_analysis
4156        .rendered_targets
4157        .into_iter()
4158        .map(|(span, id)| {
4159            (
4160                span,
4161                PointTarget {
4162                    index: *point_indices
4163                        .get(&id)
4164                        .expect("rendered point must have a global index"),
4165                },
4166            )
4167        })
4168        .collect();
4169    let mut statement_transformer = StatementProbeTransformer {
4170        ast,
4171        coverage_hit_v2: coverage_hit_v2.clone(),
4172        probe_file_v2: probe_file_v2.clone(),
4173        targets: statement_targets,
4174        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4175    };
4176    statement_transformer.visit_program(&mut parsed.program);
4177    let mut function_transformer = FunctionProbeTransformer {
4178        ast,
4179        coverage_hit_v2: coverage_hit_v2.clone(),
4180        probe_file_v2: probe_file_v2.clone(),
4181        targets: function_targets,
4182        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4183    };
4184    function_transformer.visit_program(&mut parsed.program);
4185    let mut rendered_transformer = RenderedExpressionTransformer {
4186        ast,
4187        rendered_value_v2: rendered_value_v2.clone(),
4188        probe_file_v2: probe_file_v2.clone(),
4189        targets: rendered_targets,
4190        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4191    };
4192    rendered_transformer.visit_program(&mut parsed.program);
4193    // Made first so a member right after an optional call can read the
4194    // call's frame; it rewrites the program after the members.
4195    let mut call_transformer = OptionalCallTransformer::new(
4196        ast,
4197        source,
4198        optional_call_end_v2.clone(),
4199        probe_file_v2.clone(),
4200        typescript,
4201        optional_call_sites,
4202        call_analysis.roots,
4203        safety.source_sensitive_functions.clone(),
4204    );
4205    let call_frames = optional_analysis
4206        .after_call
4207        .iter()
4208        .filter_map(|(member, call)| {
4209            call_transformer
4210                .sites
4211                .get(call)
4212                .map(|site| (*member, site.frame.clone()))
4213        })
4214        .collect();
4215    let mut optional_transformer = OptionalMemberTransformer {
4216        ast,
4217        optional_select_v2: optional_select_v2.clone(),
4218        probe_file_v2: probe_file_v2.clone(),
4219        targets: optional_member_targets,
4220        links: optional_analysis.links,
4221        call_frames,
4222        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4223    };
4224    optional_transformer.visit_program(&mut parsed.program);
4225    call_transformer.visit_program(&mut parsed.program);
4226    let mut default_transformer = DefaultTransformer {
4227        ast,
4228        default_selected_v2: default_selected_v2.clone(),
4229        default_entered_v2: default_entered_v2.clone(),
4230        probe_file_v2: probe_file_v2.clone(),
4231        slots: default_slots,
4232        parameter_targets: default_analysis.parameter_targets,
4233        binding_targets: default_analysis.binding_targets,
4234        function_entries: Vec::new(),
4235        declaration_entries: HashMap::new(),
4236        active_declaration: Vec::new(),
4237        parameter_pattern_depth: 0,
4238        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4239        typescript,
4240    };
4241    default_transformer.visit_program(&mut parsed.program);
4242    let mut extended_transformer = ExtendedTransformer {
4243        ast,
4244        coverage_hit_v2: coverage_hit_v2.clone(),
4245        probe_file_v2: probe_file_v2.clone(),
4246        typescript,
4247        try_targets,
4248        loop_targets,
4249        names: CandidateNames::within(source, &namespace),
4250        scope_declarations: Vec::new(),
4251        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4252    };
4253    extended_transformer.visit_program(&mut parsed.program);
4254    let mut transformer = ControlProbeV2Transformer {
4255        ast,
4256        decisions: &collector.decisions,
4257        value_decisions: &collector.value_decisions,
4258        pending_values: HashMap::new(),
4259        mcdc_begin: mcdc_begin.clone(),
4260        mcdc_condition: mcdc_condition.clone(),
4261        mcdc_end: mcdc_end.clone(),
4262        mcdc_end_v2: mcdc_end_v2.clone(),
4263        probe_file_v2: probe_file_v2.clone(),
4264        names,
4265        scope_declarations: Vec::new(),
4266        decision_index: 0,
4267        parameter_depth: 0,
4268        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4269        after_statement: None,
4270        label_body: false,
4271        inline_names: None,
4272    };
4273    transformer.visit_program(&mut parsed.program);
4274    let mut logical_transformer = LogicalValueTransformer {
4275        ast,
4276        select_short_v2: select_short_v2.clone(),
4277        select_right_v2: select_right_v2.clone(),
4278        select_named_right_v2: select_named_right_v2.clone(),
4279        select_assign_end_v2: select_assign_end_v2.clone(),
4280        select_path_v2: select_path_v2.clone(),
4281        select_end2_v2: select_end2_v2.clone(),
4282        select_end_v2: select_end_v2.clone(),
4283        select_next_v2: select_next_v2.clone(),
4284        coverage_hit_v2: coverage_hit_v2.clone(),
4285        probe_file_v2: probe_file_v2.clone(),
4286        typescript,
4287        names: CandidateNames::within(source, &namespace),
4288        scope_declarations: Vec::new(),
4289        frameless: 0,
4290        trees: Vec::new(),
4291        aliasing: false,
4292        after_declaration: Vec::new(),
4293        logical_targets: selection_targets,
4294        assignment_targets,
4295        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4296    };
4297    logical_transformer.visit_program(&mut parsed.program);
4298    let mut switch_transformer = SwitchTransformer {
4299        ast,
4300        coverage_hit: coverage_hit.clone(),
4301        targets: switch_analysis.targets,
4302        names: CandidateNames::within(source, &namespace),
4303        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4304    };
4305    switch_transformer.visit_program(&mut parsed.program);
4306    let mut route_transformer = RouteRequestPhaseTransformer {
4307        ast,
4308        file,
4309        with_request_phase: with_request_phase.clone(),
4310        used: false,
4311        names: CandidateNames::within(source, &namespace),
4312    };
4313    route_transformer.transform_program(&mut parsed.program);
4314    let mut request_transformer = RequestPhaseTransformer {
4315        ast,
4316        with_request_phase: with_request_phase.clone(),
4317        used: route_transformer.used,
4318        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4319    };
4320    request_transformer.visit_program(&mut parsed.program);
4321    let uses_request_phase = request_transformer.used;
4322
4323    let registration = serde_json::json!({
4324        "decisions": &collector.decisions,
4325        "pointIds": point_analysis.points.iter().map(|point| &point.id).chain(&default_point_ids).chain(&extra.ids).collect::<Vec<_>>(),
4326        "decisionVectorCounts": &collector.decision_vector_counts,
4327        "defaultCount": default_count,
4328        "selectionPoints": [selection_first, selection_count],
4329        "selectionTrees": &logical_transformer.trees,
4330    });
4331    let registration_call = ast.expression_call(
4332        Span::default(),
4333        ast.expression_identifier(Span::default(), ast.ident(&register_probe_v2)),
4334        NONE,
4335        ast.vec1(Argument::from(json_expression(ast, &registration))),
4336        false,
4337    );
4338    parsed.program.body.insert(
4339        0,
4340        Statement::VariableDeclaration(ast.alloc_variable_declaration(
4341            Span::default(),
4342            runtime_declaration_kind,
4343            ast.vec1(ast.variable_declarator(
4344                Span::default(),
4345                runtime_declaration_kind,
4346                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
4347                NONE,
4348                Some(registration_call),
4349                false,
4350            )),
4351            false,
4352        )),
4353    );
4354    let mut runtime_imports = vec![
4355        ("mcdcBegin", &mcdc_begin),
4356        ("mcdcCondition", &mcdc_condition),
4357        ("mcdcEnd", &mcdc_end),
4358        ("coverageHit", &coverage_hit),
4359        ("registerProbeV2", &register_probe_v2),
4360        ("mcdcEndV2", &mcdc_end_v2),
4361        ("coverageHitV2", &coverage_hit_v2),
4362        (
4363            "parenthesizedAssignmentValue",
4364            &parenthesized_assignment_value,
4365        ),
4366        ("optionalSelectV2", &optional_select_v2),
4367        ("selectShortV2", &select_short_v2),
4368        ("selectRightV2", &select_right_v2),
4369        ("selectNamedRightV2", &select_named_right_v2),
4370        ("selectAssignEndV2", &select_assign_end_v2),
4371        ("selectPathV2", &select_path_v2),
4372        ("selectEnd2V2", &select_end2_v2),
4373        ("selectEndV2", &select_end_v2),
4374        ("selectNextV2", &select_next_v2),
4375        ("renderedValueV2", &rendered_value_v2),
4376        ("optionalCallEndV2", &optional_call_end_v2),
4377        ("defaultSelectedV2", &default_selected_v2),
4378        ("defaultEnteredV2", &default_entered_v2),
4379    ];
4380    if uses_request_phase {
4381        runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
4382    }
4383    if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
4384        // Read off an untyped global, every helper is `any` to TypeScript,
4385        // whatever the runtime's declarations say, and so is each value that
4386        // passes through one. A directly run TypeScript file names each
4387        // helper's type where it binds it. Declaring the global's type for
4388        // the program instead collided with the declaration every rewritten
4389        // test file carries for its assertion helpers.
4390        let typed = typescript && runtime_binding == RuntimeBinding::DirectGlobal;
4391        let declarators =
4392            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
4393                let annotation = typed
4394                    .then(|| runtime_helper_type(&allocator, parsed.program.source_type, imported))
4395                    .flatten();
4396                let global_runtime =
4397                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
4398                        Span::default(),
4399                        ast.expression_identifier(Span::default(), ast.ident("globalThis")),
4400                        ast.identifier_name(
4401                            Span::default(),
4402                            ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
4403                                "__SUPERCOV_DIRECT_RUNTIME__"
4404                            } else {
4405                                "__supercovRuntime"
4406                            }),
4407                        ),
4408                        false,
4409                    ));
4410                let runtime_helper =
4411                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
4412                        Span::default(),
4413                        global_runtime,
4414                        ast.identifier_name(Span::default(), ast.ident(imported)),
4415                        false,
4416                    ));
4417                ast.variable_declarator(
4418                    Span::default(),
4419                    runtime_declaration_kind,
4420                    ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
4421                    annotation,
4422                    Some(runtime_helper),
4423                    false,
4424                )
4425            }));
4426        parsed.program.body.insert(
4427            0,
4428            Statement::VariableDeclaration(ast.alloc_variable_declaration(
4429                Span::default(),
4430                runtime_declaration_kind,
4431                declarators,
4432                false,
4433            )),
4434        );
4435    } else {
4436        let import_specifiers =
4437            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
4438                ast.import_declaration_specifier_import_specifier(
4439                    Span::default(),
4440                    ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
4441                    ast.binding_identifier(Span::default(), ast.ident(local)),
4442                    oxc_ast::ast::ImportOrExportKind::Value,
4443                )
4444            }));
4445        parsed.program.body.insert(
4446            0,
4447            Statement::ImportDeclaration(ast.alloc_import_declaration(
4448                Span::default(),
4449                Some(import_specifiers),
4450                ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
4451                None,
4452                NONE,
4453                oxc_ast::ast::ImportOrExportKind::Value,
4454            )),
4455        );
4456    }
4457
4458    if let Some(wrapper) = capability_wrapper {
4459        transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
4460    }
4461    let limitations = Vec::new();
4462    mark_generated_statements(ast, &mut parsed.program);
4463    AssertedTargetTransformer {
4464        ast: AstBuilder::new(&allocator),
4465    }
4466    .visit_program(&mut parsed.program);
4467    let (code, map) =
4468        generate_candidate(&parsed.program, file, &safety.source_sensitive_functions)?;
4469    Ok(CandidateOutput {
4470        engine: "rust-oxc".to_string(),
4471        complete: true,
4472        supported_surface: "complete-js-instrumenter-v1".to_string(),
4473        code,
4474        map,
4475        decisions: collector.decisions,
4476        points: point_analysis.points,
4477        excluded_statements,
4478        branches,
4479        runtime: Some(CandidateRuntime {
4480            coverage_hit,
4481            mcdc_begin,
4482            mcdc_condition,
4483            mcdc_end,
4484            register_probe_v2,
4485            mcdc_end_v2,
4486            coverage_hit_v2,
4487            probe_file_v2,
4488            parenthesized_assignment_value,
4489            with_request_phase,
4490            optional_select_v2,
4491            select_short_v2,
4492            select_right_v2,
4493            select_named_right_v2,
4494            select_assign_end_v2,
4495            select_path_v2,
4496            select_end2_v2,
4497            select_end_v2,
4498            select_next_v2,
4499            rendered_value_v2,
4500            optional_call_end_v2,
4501            default_selected_v2,
4502            default_entered_v2,
4503        }),
4504        coverage_limitations: {
4505            let mut limitations = safety.semantic_limitations;
4506            limitations.extend(call_analysis.limitations);
4507            limitations.extend(default_analysis.limitations);
4508            limitations.extend(safety.dynamic_limitations);
4509            limitations
4510        },
4511        limitations,
4512    })
4513}
4514
4515/// `file.clock.fast && file.hitEpochs[index] === file.clock.epoch || hit(file, index);`
4516///
4517/// A statement probe that already fired in this context costs three loads and
4518/// a comparison, not a call: in minimatch's backtracking matcher the call per
4519/// statement was most of a tenfold slowdown, and its guard test failed. The
4520/// clock is `fast` only while an async hook keeps its epoch current; without
4521/// one the call decides, as before.
4522fn probe_v2_hit<'a>(ast: AstBuilder<'a>, hit: &str, file: &str, index: usize) -> Statement<'a> {
4523    ast.statement_expression(
4524        Span::default(),
4525        probe_v2_hit_expression(ast, hit, file, index),
4526    )
4527}
4528
4529/// The expression inside `probe_v2_hit`, for probes that sit in an expression:
4530/// a selection's outcome, a loop's first iteration, a caught exception.
4531fn probe_v2_hit_expression<'a>(
4532    ast: AstBuilder<'a>,
4533    hit: &str,
4534    file: &str,
4535    index: usize,
4536) -> Expression<'a> {
4537    let span = Span::default();
4538    let file_expression = || ast.expression_identifier(span, ast.ident(file));
4539    let member = |object: Expression<'a>, name: &str| -> Expression<'a> {
4540        Expression::from(ast.member_expression_static(
4541            span,
4542            object,
4543            ast.identifier_name(span, ast.ident(name)),
4544            false,
4545        ))
4546    };
4547    let index_literal =
4548        || ast.expression_numeric_literal(span, index as f64, None, NumberBase::Decimal);
4549    let fast = member(member(file_expression(), "clock"), "fast");
4550    let seen = ast.expression_binary(
4551        span,
4552        Expression::from(ast.member_expression_computed(
4553            span,
4554            member(file_expression(), "hitEpochs"),
4555            index_literal(),
4556            false,
4557        )),
4558        BinaryOperator::StrictEquality,
4559        member(member(file_expression(), "clock"), "epoch"),
4560    );
4561    let call = ast.expression_call(
4562        span,
4563        ast.expression_identifier(span, ast.ident(hit)),
4564        NONE,
4565        ast.vec_from_array([
4566            Argument::from(file_expression()),
4567            Argument::from(index_literal()),
4568        ]),
4569        false,
4570    );
4571    ast.expression_logical(
4572        span,
4573        ast.expression_logical(span, fast, LogicalOperator::And, seen),
4574        LogicalOperator::Or,
4575        call,
4576    )
4577}
4578
4579/// `let` declarations for probe temporaries at the top of a scope. In
4580/// TypeScript a temporary that holds user values is declared `any`, as the
4581/// runtime calls it replaces returned: `tsc --strict` would otherwise infer
4582/// its type across assignments and fail code that compiled before.
4583fn probe_temporaries<'a>(
4584    ast: AstBuilder<'a>,
4585    names: Vec<String>,
4586    typescript: bool,
4587) -> Statement<'a> {
4588    let declarations = ast.vec_from_iter(names.into_iter().map(|name| {
4589        let annotation = typescript.then(|| {
4590            ast.alloc_ts_type_annotation(Span::default(), ast.ts_type_any_keyword(Span::default()))
4591        });
4592        ast.variable_declarator(
4593            Span::default(),
4594            VariableDeclarationKind::Let,
4595            ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&name)),
4596            annotation,
4597            None,
4598            false,
4599        )
4600    }));
4601    Statement::VariableDeclaration(ast.alloc_variable_declaration(
4602        Span::default(),
4603        VariableDeclarationKind::Let,
4604        declarations,
4605        false,
4606    ))
4607}
4608
4609/// `T = value`, as an expression.
4610fn assign_temporary<'a>(ast: AstBuilder<'a>, name: &str, value: Expression<'a>) -> Expression<'a> {
4611    ast.expression_assignment(
4612        Span::default(),
4613        AssignmentOperator::Assign,
4614        AssignmentTarget::from(ast.simple_assignment_target_assignment_target_identifier(
4615            Span::default(),
4616            ast.ident(name),
4617        )),
4618        value,
4619    )
4620}
4621
4622fn numeric<'a>(ast: AstBuilder<'a>, value: usize) -> Expression<'a> {
4623    ast.expression_numeric_literal(Span::default(), value as f64, None, NumberBase::Decimal)
4624}
4625
4626/// Where each V2 point beyond the statement points begins, per site, in
4627/// source order: every probe kind records through `coverageHitV2` with the
4628/// ids it always recorded.
4629#[derive(Default)]
4630struct ExtraPoints {
4631    base: usize,
4632    ids: Vec<String>,
4633}
4634
4635impl ExtraPoints {
4636    fn allocate<T>(
4637        &mut self,
4638        targets: &HashMap<SpanKey, T>,
4639        ids: impl Fn(&T) -> Vec<String>,
4640    ) -> HashMap<SpanKey, usize> {
4641        let mut keys = targets.keys().copied().collect::<Vec<_>>();
4642        keys.sort();
4643        keys.into_iter()
4644            .map(|key| {
4645                let first = self.base + self.ids.len();
4646                self.ids.extend(ids(&targets[&key]));
4647                (key, first)
4648            })
4649            .collect()
4650    }
4651
4652    fn next(&self) -> usize {
4653        self.base + self.ids.len()
4654    }
4655}
4656
4657struct StatementProbeTransformer<'a> {
4658    ast: AstBuilder<'a>,
4659    coverage_hit_v2: String,
4660    probe_file_v2: String,
4661    targets: HashMap<SpanKey, Vec<PointTarget>>,
4662    source_sensitive_functions: HashSet<SpanKey>,
4663}
4664
4665impl<'a> StatementProbeTransformer<'a> {
4666    fn probe(&self, target: &PointTarget) -> Statement<'a> {
4667        probe_v2_hit(
4668            self.ast,
4669            &self.coverage_hit_v2,
4670            &self.probe_file_v2,
4671            target.index,
4672        )
4673    }
4674
4675    fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
4676        let mut ids = self
4677            .targets
4678            .remove(&span_key(statement.span()))
4679            .unwrap_or_default();
4680        if let Statement::ExportNamedDeclaration(export) = statement
4681            && let Some(declaration) = &export.declaration
4682            && let Some(nested) = self.targets.remove(&span_key(declaration.span()))
4683        {
4684            ids.extend(nested);
4685        }
4686        if let Statement::ExportDefaultDeclaration(export) = statement
4687            && let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
4688            && let Some(nested) = self.targets.remove(&span_key(class.span))
4689        {
4690            ids.extend(nested);
4691        }
4692        ids
4693    }
4694
4695    fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
4696        let ids = self.take_statement_ids(statement);
4697        if ids.is_empty() {
4698            return;
4699        }
4700        // The block takes the statement's span, so its braces map to the
4701        // statement's own start and end: a coverage tool mapping the copy back
4702        // (nyc, Jest) dropped a statement whose end was a brace of nowhere.
4703        let span = statement.span();
4704        let original = statement.take_in(self.ast.allocator);
4705        let mut body = self.ast.vec_with_capacity(ids.len() + 1);
4706        body.extend(ids.iter().map(|target| self.probe(target)));
4707        body.push(original);
4708        *statement = self.ast.statement_block(span, body);
4709    }
4710}
4711
4712impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
4713    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4714        let original = statements.take_in(self.ast.allocator);
4715        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
4716        for mut statement in original {
4717            let ids = self.take_statement_ids(&statement);
4718            self.visit_statement(&mut statement);
4719            instrumented.extend(ids.iter().map(|target| self.probe(target)));
4720            instrumented.push(statement);
4721        }
4722        *statements = instrumented;
4723    }
4724
4725    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4726        if self
4727            .source_sensitive_functions
4728            .contains(&span_key(function.span))
4729        {
4730            return;
4731        }
4732        walk_mut::walk_function(self, function, flags);
4733    }
4734
4735    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4736        if self
4737            .source_sensitive_functions
4738            .contains(&span_key(function.span))
4739        {
4740            return;
4741        }
4742        walk_mut::walk_arrow_function_expression(self, function);
4743    }
4744
4745    // A with body is left uninstrumented: its object environment can
4746    // intercept probe identifiers (see `enter_with_statement`).
4747    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4748
4749    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
4750        self.wrap_bare(&mut statement.consequent);
4751        if let Some(alternate) = &mut statement.alternate {
4752            self.wrap_bare(alternate);
4753        }
4754        walk_mut::walk_if_statement(self, statement);
4755    }
4756
4757    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
4758        self.wrap_bare(&mut statement.body);
4759        walk_mut::walk_while_statement(self, statement);
4760    }
4761
4762    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
4763        self.wrap_bare(&mut statement.body);
4764        walk_mut::walk_do_while_statement(self, statement);
4765    }
4766
4767    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
4768        self.wrap_bare(&mut statement.body);
4769        walk_mut::walk_for_statement(self, statement);
4770    }
4771
4772    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
4773        self.wrap_bare(&mut statement.body);
4774        walk_mut::walk_for_in_statement(self, statement);
4775    }
4776
4777    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
4778        self.wrap_bare(&mut statement.body);
4779        walk_mut::walk_for_of_statement(self, statement);
4780    }
4781}
4782
4783/// Wraps a rendered JSX expression in `renderedValueV2(file, i, expression)` so
4784/// the expression records its own evaluation. There is nowhere to put a
4785/// statement inside JSX, and a sequence expression is not available either --
4786/// JSX forbids a bare comma operator, and a container prints its expression
4787/// unparenthesised. A call carries the value through instead. It records after
4788/// the expression evaluates rather than before, as `optionalSelect` already
4789/// does, so an expression that throws is reported unevaluated rather than
4790/// covered.
4791struct RenderedExpressionTransformer<'a> {
4792    ast: AstBuilder<'a>,
4793    rendered_value_v2: String,
4794    probe_file_v2: String,
4795    targets: HashMap<SpanKey, PointTarget>,
4796    source_sensitive_functions: HashSet<SpanKey>,
4797}
4798
4799impl<'a> RenderedExpressionTransformer<'a> {
4800    fn probe(&self, target: &PointTarget, value: Expression<'a>) -> Expression<'a> {
4801        self.ast.expression_call(
4802            Span::default(),
4803            self.ast
4804                .expression_identifier(Span::default(), self.ast.ident(&self.rendered_value_v2)),
4805            NONE,
4806            self.ast.vec_from_array([
4807                Argument::from(
4808                    self.ast.expression_identifier(
4809                        Span::default(),
4810                        self.ast.ident(&self.probe_file_v2),
4811                    ),
4812                ),
4813                Argument::from(self.ast.expression_numeric_literal(
4814                    Span::default(),
4815                    target.index as f64,
4816                    None,
4817                    NumberBase::Decimal,
4818                )),
4819                Argument::from(value),
4820            ]),
4821            false,
4822        )
4823    }
4824}
4825
4826impl<'a> VisitMut<'a> for RenderedExpressionTransformer<'a> {
4827    fn visit_jsx_expression_container(&mut self, container: &mut JSXExpressionContainer<'a>) {
4828        walk_mut::walk_jsx_expression_container(self, container);
4829        if matches!(container.expression, JSXExpression::EmptyExpression(_)) {
4830            return;
4831        }
4832        let span = container.expression.span();
4833        if self.source_sensitive_functions.contains(&span_key(span)) {
4834            return;
4835        }
4836        let Some(target) = self.targets.remove(&span_key(span)) else {
4837            return;
4838        };
4839        let inner = container
4840            .expression
4841            .take_in(self.ast.allocator)
4842            .into_expression();
4843        container.expression = JSXExpression::from(self.probe(&target, inner));
4844    }
4845}
4846
4847struct FunctionProbeTransformer<'a> {
4848    ast: AstBuilder<'a>,
4849    coverage_hit_v2: String,
4850    probe_file_v2: String,
4851    targets: HashMap<SpanKey, PointTarget>,
4852    source_sensitive_functions: HashSet<SpanKey>,
4853}
4854
4855impl<'a> FunctionProbeTransformer<'a> {
4856    fn probe(&self, target: &PointTarget) -> Statement<'a> {
4857        probe_v2_hit(
4858            self.ast,
4859            &self.coverage_hit_v2,
4860            &self.probe_file_v2,
4861            target.index,
4862        )
4863    }
4864}
4865
4866impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
4867    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4868        if self
4869            .source_sensitive_functions
4870            .contains(&span_key(function.span))
4871        {
4872            return;
4873        }
4874        if let Some(target) = self.targets.remove(&span_key(function.span))
4875            && let Some(body) = &mut function.body
4876        {
4877            body.statements.insert(0, self.probe(&target));
4878        }
4879        walk_mut::walk_function(self, function, flags);
4880    }
4881
4882    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4883        if self
4884            .source_sensitive_functions
4885            .contains(&span_key(function.span))
4886        {
4887            return;
4888        }
4889        if let Some(target) = self.targets.remove(&span_key(function.span)) {
4890            let probe = self.probe(&target);
4891            if function.expression {
4892                let original = function
4893                    .body
4894                    .statements
4895                    .pop()
4896                    .expect("expression arrow must contain its expression statement");
4897                let Statement::ExpressionStatement(expression) = original else {
4898                    panic!("expression arrow body must be represented as an expression statement");
4899                };
4900                function.expression = false;
4901                function.body.statements.push(probe);
4902                function.body.statements.push(
4903                    self.ast
4904                        .statement_return(Span::default(), Some(expression.unbox().expression)),
4905                );
4906            } else {
4907                function.body.statements.insert(0, probe);
4908            }
4909        }
4910        walk_mut::walk_arrow_function_expression(self, function);
4911    }
4912}
4913
4914struct OptionalMemberTransformer<'a> {
4915    ast: AstBuilder<'a>,
4916    optional_select_v2: String,
4917    probe_file_v2: String,
4918    /// The short and continued outcomes are two V2 points from this index.
4919    targets: HashMap<SpanKey, usize>,
4920    links: HashMap<SpanKey, u8>,
4921    /// For a target right after an optional call, that call's frame.
4922    call_frames: HashMap<SpanKey, String>,
4923    source_sensitive_functions: HashSet<SpanKey>,
4924}
4925
4926impl<'a> OptionalMemberTransformer<'a> {
4927    /// `optionalSelectV2(file, first, object, links, call)`: records whether
4928    /// the object of `object?.member` was nullish and passes it through. In
4929    /// `a?.b?.c` the object of `?.c` is undefined when `a` is, though `?.c`
4930    /// never ran; `links`, and after an optional call that call's frame, let
4931    /// the runtime tell the two apart.
4932    fn instrument_operand(
4933        &self,
4934        operand: Expression<'a>,
4935        first: usize,
4936        links: u8,
4937        call_frame: Option<&str>,
4938    ) -> Expression<'a> {
4939        let mut arguments = self.ast.vec_from_array([
4940            Argument::from(
4941                self.ast
4942                    .expression_identifier(Span::default(), self.ast.ident(&self.probe_file_v2)),
4943            ),
4944            Argument::from(numeric(self.ast, first)),
4945            Argument::from(operand),
4946        ]);
4947        if links != 0 {
4948            arguments.push(Argument::from(numeric(self.ast, usize::from(links))));
4949        }
4950        if let Some(frame) = call_frame {
4951            arguments.push(Argument::from(
4952                self.ast
4953                    .expression_identifier(Span::default(), self.ast.ident(frame)),
4954            ));
4955        }
4956        self.ast.expression_call(
4957            Span::default(),
4958            self.ast
4959                .expression_identifier(Span::default(), self.ast.ident(&self.optional_select_v2)),
4960            NONE,
4961            arguments,
4962            false,
4963        )
4964    }
4965
4966    fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
4967        let Some(first) = self.targets.remove(&span_key(span)) else {
4968            return;
4969        };
4970        let mut links = self.links.get(&span_key(span)).copied().unwrap_or(0);
4971        let call_frame = self.call_frames.get(&span_key(span)).cloned();
4972        if call_frame.is_none() {
4973            // A call that is not measured leaves nothing to read.
4974            links &= !LINK_AFTER_CALL;
4975        }
4976        // `a?.b` narrows `a`, and TypeScript reads that through a comma's last
4977        // operand but not through a call: `if (user?.name) return user`
4978        // returned `User | undefined`. A name or `this` is read again after
4979        // the call that records it. Anything else may run code when read, a
4980        // getter or a proxy, and is read once, through the call.
4981        let again = match &*object {
4982            Expression::Identifier(identifier) => Some(
4983                self.ast
4984                    .expression_identifier(identifier.span, identifier.name),
4985            ),
4986            Expression::ThisExpression(this) => Some(self.ast.expression_this(this.span)),
4987            _ => None,
4988        };
4989        let mut operand = object.take_in(self.ast.allocator);
4990        if again.is_some() {
4991            *operand.span_mut() = Span::default();
4992        }
4993        let recorded = self.instrument_operand(operand, first, links, call_frame.as_deref());
4994        *object = match again {
4995            Some(again) => self
4996                .ast
4997                .expression_sequence(Span::default(), self.ast.vec_from_array([recorded, again])),
4998            None => recorded,
4999        };
5000    }
5001}
5002
5003impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
5004    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5005        if self
5006            .source_sensitive_functions
5007            .contains(&span_key(function.span))
5008        {
5009            return;
5010        }
5011        walk_mut::walk_function(self, function, flags);
5012    }
5013
5014    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5015        if self
5016            .source_sensitive_functions
5017            .contains(&span_key(function.span))
5018        {
5019            return;
5020        }
5021        walk_mut::walk_arrow_function_expression(self, function);
5022    }
5023
5024    // A with body is left uninstrumented: its object environment can
5025    // intercept probe identifiers (see `enter_with_statement`).
5026    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5027
5028    fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
5029        self.instrument_target(member.span, &mut member.object);
5030        walk_mut::walk_computed_member_expression(self, member);
5031    }
5032
5033    fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
5034        self.instrument_target(member.span, &mut member.object);
5035        walk_mut::walk_static_member_expression(self, member);
5036    }
5037
5038    fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
5039        self.instrument_target(member.span, &mut member.object);
5040        walk_mut::walk_private_field_expression(self, member);
5041    }
5042}
5043
5044/// A call site's state lives in a temporary of its own: 0 before the chain,
5045/// 1 once the callee is evaluated, 2 once the call goes ahead. Its short and
5046/// continued outcomes are two V2 points from `first`.
5047#[derive(Clone)]
5048struct OptionalCallSiteRuntime {
5049    frame: String,
5050    first: usize,
5051}
5052
5053struct OptionalCallTransformer<'a, 's> {
5054    ast: AstBuilder<'a>,
5055    optional_call_end_v2: String,
5056    probe_file_v2: String,
5057    typescript: bool,
5058    scope_declarations: Vec<Vec<String>>,
5059    sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
5060    roots: HashMap<SpanKey, Vec<SpanKey>>,
5061    source_sensitive_functions: HashSet<SpanKey>,
5062    _source: std::marker::PhantomData<&'s str>,
5063}
5064
5065impl<'a, 's> OptionalCallTransformer<'a, 's> {
5066    #[allow(clippy::too_many_arguments)]
5067    fn new(
5068        ast: AstBuilder<'a>,
5069        source: &'s str,
5070        optional_call_end_v2: String,
5071        probe_file_v2: String,
5072        typescript: bool,
5073        sites: HashMap<SpanKey, usize>,
5074        roots: HashMap<SpanKey, Vec<SpanKey>>,
5075        source_sensitive_functions: HashSet<SpanKey>,
5076    ) -> Self {
5077        let mut names = CandidateNames::new(source);
5078        let mut ordered = sites.into_iter().collect::<Vec<_>>();
5079        ordered.sort();
5080        let sites = ordered
5081            .into_iter()
5082            .map(|(key, first)| {
5083                (
5084                    key,
5085                    OptionalCallSiteRuntime {
5086                        frame: names.allocate("_optionalCall"),
5087                        first,
5088                    },
5089                )
5090            })
5091            .collect();
5092        Self {
5093            ast,
5094            optional_call_end_v2,
5095            probe_file_v2,
5096            typescript,
5097            scope_declarations: Vec::new(),
5098            sites,
5099            roots,
5100            source_sensitive_functions,
5101            _source: std::marker::PhantomData,
5102        }
5103    }
5104
5105    fn identifier(&self, name: &str) -> Expression<'a> {
5106        self.ast
5107            .expression_identifier(Span::default(), self.ast.ident(name))
5108    }
5109
5110    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
5111        self.ast.expression_call(
5112            Span::default(),
5113            self.identifier(name),
5114            NONE,
5115            arguments,
5116            false,
5117        )
5118    }
5119
5120    /// `(T = 1, value)`: the callee's position was evaluated.
5121    fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
5122        self.ast.expression_sequence(
5123            Span::default(),
5124            self.ast.vec_from_array([
5125                assign_temporary(self.ast, frame, numeric(self.ast, 1)),
5126                value,
5127            ]),
5128        )
5129    }
5130
5131    fn enter_scope(&mut self) {
5132        self.scope_declarations.push(Vec::new());
5133    }
5134
5135    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5136        let names = self
5137            .scope_declarations
5138            .pop()
5139            .expect("optional-call scope stack must remain balanced");
5140        if names.is_empty() {
5141            return;
5142        }
5143        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5144    }
5145
5146    fn instrument_callee(
5147        &self,
5148        callee: Expression<'a>,
5149        site: &OptionalCallSiteRuntime,
5150    ) -> Expression<'a> {
5151        match callee {
5152            Expression::ComputedMemberExpression(mut member) => {
5153                let property = member.expression.take_in(self.ast.allocator);
5154                member.expression = self.reached(&site.frame, property);
5155                Expression::ComputedMemberExpression(member)
5156            }
5157            Expression::StaticMemberExpression(member) => {
5158                let member = member.unbox();
5159                let property = self.ast.expression_string_literal(
5160                    Span::default(),
5161                    self.ast.str(member.property.name.as_str()),
5162                    None,
5163                );
5164                Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
5165                    member.span,
5166                    member.object,
5167                    self.reached(&site.frame, property),
5168                    member.optional,
5169                ))
5170            }
5171            Expression::PrivateFieldExpression(mut member) => {
5172                let object = member.object.take_in(self.ast.allocator);
5173                member.object = self.reached(&site.frame, object);
5174                Expression::PrivateFieldExpression(member)
5175            }
5176            Expression::ChainExpression(mut chain) => {
5177                match &mut chain.expression {
5178                    ChainElement::ComputedMemberExpression(member) => {
5179                        let property = member.expression.take_in(self.ast.allocator);
5180                        member.expression = self.reached(&site.frame, property);
5181                    }
5182                    ChainElement::StaticMemberExpression(member) => {
5183                        let member = member.take_in(self.ast.allocator);
5184                        let property = self.ast.expression_string_literal(
5185                            Span::default(),
5186                            self.ast.str(member.property.name.as_str()),
5187                            None,
5188                        );
5189                        chain.expression = ChainElement::ComputedMemberExpression(
5190                            self.ast.alloc_computed_member_expression(
5191                                member.span,
5192                                member.object,
5193                                self.reached(&site.frame, property),
5194                                member.optional,
5195                            ),
5196                        );
5197                    }
5198                    ChainElement::PrivateFieldExpression(member) => {
5199                        let object = member.object.take_in(self.ast.allocator);
5200                        member.object = self.reached(&site.frame, object);
5201                    }
5202                    ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
5203                        return self.reached(&site.frame, Expression::ChainExpression(chain));
5204                    }
5205                }
5206                Expression::ChainExpression(chain)
5207            }
5208            Expression::ParenthesizedExpression(mut parenthesized) => {
5209                let inner = parenthesized.expression.take_in(self.ast.allocator);
5210                parenthesized.expression = self.instrument_callee(inner, site);
5211                Expression::ParenthesizedExpression(parenthesized)
5212            }
5213            Expression::TSAsExpression(mut wrapped) => {
5214                let inner = wrapped.expression.take_in(self.ast.allocator);
5215                wrapped.expression = self.instrument_callee(inner, site);
5216                Expression::TSAsExpression(wrapped)
5217            }
5218            Expression::TSSatisfiesExpression(mut wrapped) => {
5219                let inner = wrapped.expression.take_in(self.ast.allocator);
5220                wrapped.expression = self.instrument_callee(inner, site);
5221                Expression::TSSatisfiesExpression(wrapped)
5222            }
5223            Expression::TSTypeAssertion(mut wrapped) => {
5224                let inner = wrapped.expression.take_in(self.ast.allocator);
5225                wrapped.expression = self.instrument_callee(inner, site);
5226                Expression::TSTypeAssertion(wrapped)
5227            }
5228            Expression::TSNonNullExpression(mut wrapped) => {
5229                let inner = wrapped.expression.take_in(self.ast.allocator);
5230                wrapped.expression = self.instrument_callee(inner, site);
5231                Expression::TSNonNullExpression(wrapped)
5232            }
5233            // `a?.b()?.()`: the callee is what an earlier call in the same
5234            // chain returned. It is reached only when that call goes ahead; a
5235            // short-circuit before it never asked whether its result is
5236            // nullish.
5237            Expression::CallExpression(mut call) => {
5238                call.arguments.insert(0, self.marker_spread(&site.frame, 1));
5239                Expression::CallExpression(call)
5240            }
5241            other => self.reached(&site.frame, other),
5242        }
5243    }
5244
5245    fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
5246        let callee = call.callee.take_in(self.ast.allocator);
5247        call.callee = self.instrument_callee(callee, site);
5248        call.arguments.insert(0, self.marker_spread(&site.frame, 2));
5249    }
5250
5251    /// `...(T = value, file.none)`: runs only when the call goes ahead, before
5252    /// its own arguments, and spreads nothing.
5253    fn marker_spread(&self, frame: &str, value: usize) -> Argument<'a> {
5254        let none = Expression::from(
5255            self.ast.member_expression_static(
5256                Span::default(),
5257                self.identifier(&self.probe_file_v2),
5258                self.ast
5259                    .identifier_name(Span::default(), self.ast.ident("none")),
5260                false,
5261            ),
5262        );
5263        let marked = self.ast.expression_sequence(
5264            Span::default(),
5265            self.ast.vec_from_array([
5266                assign_temporary(self.ast, frame, numeric(self.ast, value)),
5267                none,
5268            ]),
5269        );
5270        Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), marked))
5271    }
5272
5273    fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
5274        let sites = site_keys
5275            .iter()
5276            .map(|key| {
5277                self.sites
5278                    .get(key)
5279                    .expect("optional-call root must reference a known site")
5280                    .clone()
5281            })
5282            .collect::<Vec<_>>();
5283        self.scope_declarations
5284            .last_mut()
5285            .expect("optional-call root must be inside a program or function")
5286            .extend(sites.iter().map(|site| site.frame.clone()));
5287
5288        // `optionalCallEndV2(file, first, chain, T)` reads T after the chain,
5289        // so it sees what that evaluation did.
5290        let original = expression.take_in(self.ast.allocator);
5291        let mut measured = original;
5292        for site in sites.iter().rev() {
5293            measured = self.call(
5294                &self.optional_call_end_v2,
5295                self.ast.vec_from_array([
5296                    Argument::from(self.identifier(&self.probe_file_v2)),
5297                    Argument::from(numeric(self.ast, site.first)),
5298                    Argument::from(measured),
5299                    Argument::from(self.identifier(&site.frame)),
5300                ]),
5301            );
5302        }
5303        let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
5304        for site in &sites {
5305            sequence.push(assign_temporary(
5306                self.ast,
5307                &site.frame,
5308                numeric(self.ast, 0),
5309            ));
5310        }
5311        sequence.push(measured);
5312        *expression = self.ast.expression_sequence(Span::default(), sequence);
5313    }
5314}
5315
5316impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
5317    fn visit_program(&mut self, program: &mut Program<'a>) {
5318        self.enter_scope();
5319        walk_mut::walk_program(self, program);
5320        self.leave_scope(&mut program.body);
5321    }
5322
5323    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5324        self.enter_scope();
5325        walk_mut::walk_function_body(self, body);
5326        self.leave_scope(&mut body.statements);
5327    }
5328
5329    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5330        if self
5331            .source_sensitive_functions
5332            .contains(&span_key(function.span))
5333        {
5334            return;
5335        }
5336        walk_mut::walk_function(self, function, flags);
5337    }
5338
5339    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5340        if self
5341            .source_sensitive_functions
5342            .contains(&span_key(function.span))
5343        {
5344            return;
5345        }
5346        walk_mut::walk_arrow_function_expression(self, function);
5347    }
5348
5349    // A with body is left uninstrumented: its object environment can
5350    // intercept probe identifiers (see `enter_with_statement`).
5351    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5352
5353    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
5354        walk_mut::walk_call_expression(self, call);
5355        if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
5356            self.instrument_call(call, &site);
5357        }
5358    }
5359
5360    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
5361        let key = span_key(expression.span());
5362        walk_mut::walk_expression(self, expression);
5363        if let Some(site_keys) = self.roots.remove(&key) {
5364            self.wrap_root(expression, &site_keys);
5365        }
5366    }
5367}
5368
5369/// Where a default's pending count and its two outcomes live in the file's V2
5370/// probe state: a slot of `pendingDefaults`, and two indices of `pointIds`.
5371struct DefaultSlot {
5372    slot: usize,
5373    default_point: usize,
5374    provided_point: usize,
5375}
5376
5377struct DefaultTransformer<'a> {
5378    ast: AstBuilder<'a>,
5379    default_selected_v2: String,
5380    default_entered_v2: String,
5381    probe_file_v2: String,
5382    slots: HashMap<String, DefaultSlot>,
5383    parameter_targets: HashMap<SpanKey, DefaultTarget>,
5384    binding_targets: HashMap<SpanKey, DefaultTarget>,
5385    function_entries: Vec<Vec<Statement<'a>>>,
5386    declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
5387    active_declaration: Vec<SpanKey>,
5388    parameter_pattern_depth: usize,
5389    source_sensitive_functions: HashSet<SpanKey>,
5390    typescript: bool,
5391}
5392
5393impl<'a> DefaultTransformer<'a> {
5394    /// The type a default names by its own syntax: a literal's primitive, or
5395    /// the type it is asserted to (`"utf8" as E`).
5396    ///
5397    /// Under `isolatedDeclarations` a parameter needs no annotation while its
5398    /// default is such an expression. Wrapped in a call it does, and h3 failed
5399    /// to compile with `TS9011`, an error `@ts-nocheck` does not silence. The
5400    /// annotation says what TypeScript inferred already.
5401    fn default_type(&self, value: &Expression<'a>) -> Option<oxc_ast::ast::TSType<'a>> {
5402        let span = Span::default();
5403        let mut value = value;
5404        while let Expression::ParenthesizedExpression(parenthesized) = value {
5405            value = &parenthesized.expression;
5406        }
5407        match value {
5408            // `'x' as const` is the literal's own type, which no keyword names.
5409            Expression::TSAsExpression(asserted) => {
5410                (!asserted.type_annotation.is_const_type_reference())
5411                    .then(|| asserted.type_annotation.clone_in(self.ast.allocator))
5412            }
5413            Expression::TSTypeAssertion(asserted) => {
5414                (!asserted.type_annotation.is_const_type_reference())
5415                    .then(|| asserted.type_annotation.clone_in(self.ast.allocator))
5416            }
5417            Expression::BooleanLiteral(_) => Some(self.ast.ts_type_boolean_keyword(span)),
5418            Expression::NumericLiteral(_) => Some(self.ast.ts_type_number_keyword(span)),
5419            Expression::BigIntLiteral(_) => Some(self.ast.ts_type_big_int_keyword(span)),
5420            Expression::StringLiteral(_) => Some(self.ast.ts_type_string_keyword(span)),
5421            Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
5422                Some(self.ast.ts_type_string_keyword(span))
5423            }
5424            Expression::UnaryExpression(unary)
5425                if unary.operator == UnaryOperator::UnaryNegation
5426                    && matches!(unary.argument, Expression::NumericLiteral(_)) =>
5427            {
5428                Some(self.ast.ts_type_number_keyword(span))
5429            }
5430            _ => None,
5431        }
5432    }
5433
5434    fn identifier(&self, name: &str) -> Expression<'a> {
5435        self.ast
5436            .expression_identifier(Span::default(), self.ast.ident(name))
5437    }
5438
5439    fn string_argument(&self, value: &str) -> Argument<'a> {
5440        Argument::from(self.ast.expression_string_literal(
5441            Span::default(),
5442            self.ast.str(value),
5443            None,
5444        ))
5445    }
5446
5447    fn index_argument(&self, index: usize) -> Argument<'a> {
5448        Argument::from(self.ast.expression_numeric_literal(
5449            Span::default(),
5450            index as f64,
5451            None,
5452            NumberBase::Decimal,
5453        ))
5454    }
5455
5456    fn slot(&self, target: &DefaultTarget) -> &DefaultSlot {
5457        self.slots
5458            .get(&target.default_id)
5459            .expect("every default target has a V2 slot")
5460    }
5461
5462    /// `defaultSelectedV2(file, slot, value[, name])`: the default counts
5463    /// itself after its value evaluates, so a default that throws leaves no
5464    /// count behind.
5465    fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
5466        let mut arguments = self.ast.vec_from_array([
5467            Argument::from(self.identifier(&self.probe_file_v2)),
5468            self.index_argument(self.slot(target).slot),
5469            Argument::from(value),
5470        ]);
5471        if let Some(name) = &target.inferred_name {
5472            arguments.push(self.string_argument(name));
5473        }
5474        self.ast.expression_call(
5475            Span::default(),
5476            self.identifier(&self.default_selected_v2),
5477            NONE,
5478            arguments,
5479            false,
5480        )
5481    }
5482
5483    /// `defaultEnteredV2(file, slot, defaultPoint, providedPoint)`, once the
5484    /// parameters or the declaration are bound.
5485    fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
5486        let slot = self.slot(target);
5487        self.ast.statement_expression(
5488            Span::default(),
5489            self.ast.expression_call(
5490                Span::default(),
5491                self.identifier(&self.default_entered_v2),
5492                NONE,
5493                self.ast.vec_from_array([
5494                    Argument::from(self.identifier(&self.probe_file_v2)),
5495                    self.index_argument(slot.slot),
5496                    self.index_argument(slot.default_point),
5497                    self.index_argument(slot.provided_point),
5498                ]),
5499                false,
5500            ),
5501        )
5502    }
5503
5504    fn push_entry(&mut self, target: &DefaultTarget) {
5505        let entry = self.entered(target);
5506        if self.parameter_pattern_depth > 0 {
5507            self.function_entries
5508                .last_mut()
5509                .expect("parameter default must belong to a function")
5510                .push(entry);
5511        } else {
5512            let declaration = *self
5513                .active_declaration
5514                .last()
5515                .expect("binding default must belong to a declaration");
5516            self.declaration_entries
5517                .entry(declaration)
5518                .or_default()
5519                .push(entry);
5520        }
5521    }
5522
5523    fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
5524        if entries.is_empty() {
5525            return;
5526        }
5527        if let Statement::BlockStatement(block) = body {
5528            for (index, entry) in entries.into_iter().enumerate() {
5529                block.body.insert(index, entry);
5530            }
5531            return;
5532        }
5533        let span = body.span();
5534        let original = body.take_in(self.ast.allocator);
5535        let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
5536        statements.extend(entries);
5537        statements.push(original);
5538        *body = self.ast.statement_block(span, statements);
5539    }
5540
5541    fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
5542        match statement {
5543            Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
5544            Statement::ExportNamedDeclaration(export) => match &export.declaration {
5545                Some(Declaration::VariableDeclaration(declaration)) => {
5546                    Some(span_key(declaration.span))
5547                }
5548                _ => None,
5549            },
5550            _ => None,
5551        }
5552    }
5553
5554    fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
5555        match left {
5556            ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
5557            _ => None,
5558        }
5559    }
5560}
5561
5562impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
5563    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5564        let original = statements.take_in(self.ast.allocator);
5565        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
5566        for mut statement in original {
5567            let declaration = Self::variable_span(&statement);
5568            self.visit_statement(&mut statement);
5569            instrumented.push(statement);
5570            if let Some(declaration) = declaration
5571                && let Some(entries) = self.declaration_entries.remove(&declaration)
5572            {
5573                instrumented.extend(entries);
5574            }
5575        }
5576        *statements = instrumented;
5577    }
5578
5579    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5580        if self
5581            .source_sensitive_functions
5582            .contains(&span_key(function.span))
5583        {
5584            return;
5585        }
5586        self.function_entries.push(Vec::new());
5587        walk_mut::walk_function(self, function, flags);
5588        let entries = self
5589            .function_entries
5590            .pop()
5591            .expect("default function-entry stack must remain balanced");
5592        if let Some(body) = &mut function.body {
5593            for (index, entry) in entries.into_iter().enumerate() {
5594                body.statements.insert(index, entry);
5595            }
5596        }
5597    }
5598
5599    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5600        if self
5601            .source_sensitive_functions
5602            .contains(&span_key(function.span))
5603        {
5604            return;
5605        }
5606        self.function_entries.push(Vec::new());
5607        walk_mut::walk_arrow_function_expression(self, function);
5608        let entries = self
5609            .function_entries
5610            .pop()
5611            .expect("default arrow-entry stack must remain balanced");
5612        for (index, entry) in entries.into_iter().enumerate() {
5613            function.body.statements.insert(index, entry);
5614        }
5615    }
5616
5617    fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
5618        let outer = self.parameter_targets.remove(&span_key(parameter.span));
5619        if let Some(target) = &outer {
5620            let entry = self.entered(target);
5621            self.function_entries
5622                .last_mut()
5623                .expect("formal parameter must belong to a function")
5624                .push(entry);
5625        }
5626        self.visit_decorators(&mut parameter.decorators);
5627        self.parameter_pattern_depth += 1;
5628        self.visit_binding_pattern(&mut parameter.pattern);
5629        self.parameter_pattern_depth -= 1;
5630        if let Some(annotation) = &mut parameter.type_annotation {
5631            self.visit_ts_type_annotation(annotation);
5632        }
5633        if let Some(initializer) = &mut parameter.initializer {
5634            if self.typescript
5635                && outer.is_some()
5636                && parameter.type_annotation.is_none()
5637                && matches!(parameter.pattern, BindingPattern::BindingIdentifier(_))
5638            {
5639                let named = self.default_type(initializer);
5640                if let Some(named) = named {
5641                    parameter.type_annotation =
5642                        Some(self.ast.alloc_ts_type_annotation(Span::default(), named));
5643                }
5644            }
5645            self.visit_expression(initializer);
5646            if let Some(target) = outer {
5647                let value = initializer.take_in(self.ast.allocator);
5648                **initializer = self.selected(value, &target);
5649            }
5650        }
5651    }
5652
5653    fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
5654        let target = if self.parameter_pattern_depth > 0 {
5655            self.parameter_targets.remove(&span_key(assignment.span))
5656        } else {
5657            self.binding_targets.remove(&span_key(assignment.span))
5658        };
5659        if let Some(target) = &target {
5660            self.push_entry(target);
5661        }
5662        walk_mut::walk_assignment_pattern(self, assignment);
5663        if let Some(target) = target {
5664            let value = assignment.right.take_in(self.ast.allocator);
5665            assignment.right = self.selected(value, &target);
5666        }
5667    }
5668
5669    fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
5670        self.active_declaration.push(span_key(declaration.span));
5671        walk_mut::walk_variable_declaration(self, declaration);
5672        self.active_declaration
5673            .pop()
5674            .expect("default declaration stack must remain balanced");
5675    }
5676
5677    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
5678        let declaration = Self::loop_declaration_span(&statement.left);
5679        walk_mut::walk_for_in_statement(self, statement);
5680        if let Some(declaration) = declaration
5681            && let Some(entries) = self.declaration_entries.remove(&declaration)
5682        {
5683            self.prepend_entries(&mut statement.body, entries);
5684        }
5685    }
5686
5687    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
5688        let declaration = Self::loop_declaration_span(&statement.left);
5689        walk_mut::walk_for_of_statement(self, statement);
5690        if let Some(declaration) = declaration
5691            && let Some(entries) = self.declaration_entries.remove(&declaration)
5692        {
5693            self.prepend_entries(&mut statement.body, entries);
5694        }
5695    }
5696
5697    // A with body is left uninstrumented: its object environment can
5698    // intercept probe identifiers (see `enter_with_statement`).
5699    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5700}
5701
5702enum ExtendedKind {
5703    Try,
5704    Loop,
5705}
5706
5707/// A try's success and catch outcomes, and a loop's zero-iteration and
5708/// entered outcomes, are two V2 points from the index each target maps to. A
5709/// flag of the site's own is set once the second outcome fires: the frame
5710/// and the call per iteration before cost every enumeration loop.
5711struct ExtendedTransformer<'a, 's> {
5712    ast: AstBuilder<'a>,
5713    coverage_hit_v2: String,
5714    probe_file_v2: String,
5715    typescript: bool,
5716    try_targets: HashMap<SpanKey, usize>,
5717    loop_targets: HashMap<SpanKey, usize>,
5718    names: CandidateNames<'s>,
5719    scope_declarations: Vec<Vec<String>>,
5720    source_sensitive_functions: HashSet<SpanKey>,
5721}
5722
5723impl<'a> ExtendedTransformer<'a, '_> {
5724    fn identifier(&self, name: &str) -> Expression<'a> {
5725        self.ast
5726            .expression_identifier(Span::default(), self.ast.ident(name))
5727    }
5728
5729    fn enter_scope(&mut self) {
5730        self.scope_declarations.push(Vec::new());
5731    }
5732
5733    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5734        let names = self
5735            .scope_declarations
5736            .pop()
5737            .expect("extended scope stack must remain balanced");
5738        if names.is_empty() {
5739            return;
5740        }
5741        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5742    }
5743
5744    fn scratch(&mut self, base: &str) -> String {
5745        let name = self.names.allocate(base);
5746        self.scope_declarations
5747            .last_mut()
5748            .expect("extended branch must be inside a program or function")
5749            .push(name.clone());
5750        name
5751    }
5752
5753    fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
5754        match statement {
5755            Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
5756            Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5757            Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5758            Statement::LabeledStatement(node) => Self::target(&node.body),
5759            _ => None,
5760        }
5761    }
5762
5763    fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
5764        match statement {
5765            Statement::TryStatement(node) => Some(node),
5766            Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
5767            _ => None,
5768        }
5769    }
5770
5771    fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
5772        match statement {
5773            Statement::ForInStatement(node) => Some(&mut node.body),
5774            Statement::ForOfStatement(node) => Some(&mut node.body),
5775            Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
5776            _ => None,
5777        }
5778    }
5779
5780    fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
5781        if let Statement::BlockStatement(block) = body {
5782            block.body.insert(0, entry);
5783            return;
5784        }
5785        let span = body.span();
5786        let original = body.take_in(ast.allocator);
5787        *body = ast.statement_block(span, ast.vec_from_array([entry, original]));
5788    }
5789
5790    fn hit(&self, index: usize) -> Expression<'a> {
5791        probe_v2_hit_expression(self.ast, &self.coverage_hit_v2, &self.probe_file_v2, index)
5792    }
5793
5794    /// `T = 0;`
5795    fn reset(&self, flag: &str) -> Statement<'a> {
5796        self.ast.statement_expression(
5797            Span::default(),
5798            assign_temporary(self.ast, flag, numeric(self.ast, 0)),
5799        )
5800    }
5801
5802    /// `T || (T = 1, hit);` -- recorded the first time only.
5803    fn first_time(&self, flag: &str, index: usize) -> Statement<'a> {
5804        let record = self.ast.expression_sequence(
5805            Span::default(),
5806            self.ast.vec_from_array([
5807                assign_temporary(self.ast, flag, numeric(self.ast, 1)),
5808                self.hit(index),
5809            ]),
5810        );
5811        self.ast.statement_expression(
5812            Span::default(),
5813            self.ast.expression_logical(
5814                Span::default(),
5815                self.identifier(flag),
5816                LogicalOperator::Or,
5817                record,
5818            ),
5819        )
5820    }
5821
5822    /// `T || hit;` -- the other outcome, when the first never fired.
5823    fn otherwise(&self, flag: &str, index: usize) -> Statement<'a> {
5824        self.ast.statement_expression(
5825            Span::default(),
5826            self.ast.expression_logical(
5827                Span::default(),
5828                self.identifier(flag),
5829                LogicalOperator::Or,
5830                self.hit(index),
5831            ),
5832        )
5833    }
5834
5835    fn instrument_try(&mut self, statement: &mut Statement<'a>, first: usize) {
5836        // What Supercov adds around the statement takes its span (see
5837        // `wrap_bare`), so its braces map to the statement's own ends.
5838        let span = statement.span();
5839        let flag = self.scratch("_supercovTryFrame");
5840        let reset = self.reset(&flag);
5841        // Recorded the moment the catch starts, not when the construct is
5842        // left: `process.exit()` in a catch skips the `finally`.
5843        let caught = self.first_time(&flag, first + 1);
5844        let node = Self::inner_try(statement).expect("try target must remain a try statement");
5845        node.handler
5846            .as_mut()
5847            .expect("try coverage requires a catch handler")
5848            .body
5849            .body
5850            .insert(0, caught);
5851        let end = self.otherwise(&flag, first);
5852        if let Some(finalizer) = &mut node.finalizer {
5853            finalizer.body.insert(0, end);
5854        } else {
5855            // Its braces map to the statement's last character: to its start
5856            // they would take that mapping from the `try`, and unmapped the
5857            // `try` would end nowhere.
5858            node.finalizer = Some(
5859                self.ast
5860                    .alloc_block_statement(Span::new(span.end - 1, span.end), self.ast.vec1(end)),
5861            );
5862        }
5863        let original = statement.take_in(self.ast.allocator);
5864        *statement = self
5865            .ast
5866            .statement_block(span, self.ast.vec_from_array([reset, original]));
5867    }
5868
5869    fn instrument_loop(&mut self, statement: &mut Statement<'a>, first: usize) {
5870        let span = statement.span();
5871        let flag = self.scratch("_supercovLoopFrame");
5872        let reset = self.reset(&flag);
5873        let entered = self.first_time(&flag, first + 1);
5874        Self::prepend(
5875            Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
5876            entered,
5877            self.ast,
5878        );
5879        let original = statement.take_in(self.ast.allocator);
5880        let end = self.otherwise(&flag, first);
5881        // Only the outer block takes the loop's span: the `try` inside it
5882        // mapped to the same start would take that mapping from the loop.
5883        let wrapped = self.ast.statement_try(
5884            Span::default(),
5885            self.ast
5886                .block_statement(Span::default(), self.ast.vec1(original)),
5887            None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
5888            Some(
5889                self.ast
5890                    .block_statement(Span::default(), self.ast.vec1(end)),
5891            ),
5892        );
5893        *statement = self
5894            .ast
5895            .statement_block(span, self.ast.vec_from_array([reset, wrapped]));
5896    }
5897}
5898
5899impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
5900    fn visit_program(&mut self, program: &mut Program<'a>) {
5901        self.enter_scope();
5902        walk_mut::walk_program(self, program);
5903        self.leave_scope(&mut program.body);
5904    }
5905
5906    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5907        self.enter_scope();
5908        walk_mut::walk_function_body(self, body);
5909        self.leave_scope(&mut body.statements);
5910    }
5911
5912    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5913        if self
5914            .source_sensitive_functions
5915            .contains(&span_key(function.span))
5916        {
5917            return;
5918        }
5919        walk_mut::walk_function(self, function, flags);
5920    }
5921
5922    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5923        if self
5924            .source_sensitive_functions
5925            .contains(&span_key(function.span))
5926        {
5927            return;
5928        }
5929        walk_mut::walk_arrow_function_expression(self, function);
5930    }
5931
5932    // A with body is left uninstrumented: its object environment can
5933    // intercept probe identifiers (see `enter_with_statement`).
5934    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5935
5936    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
5937        let Some((kind, key)) = Self::target(statement) else {
5938            walk_mut::walk_statement(self, statement);
5939            return;
5940        };
5941        match kind {
5942            ExtendedKind::Try => {
5943                if let Some(target) = self.try_targets.remove(&key) {
5944                    walk_mut::walk_statement(self, statement);
5945                    self.instrument_try(statement, target);
5946                } else {
5947                    walk_mut::walk_statement(self, statement);
5948                }
5949            }
5950            ExtendedKind::Loop => {
5951                if let Some(target) = self.loop_targets.remove(&key) {
5952                    walk_mut::walk_statement(self, statement);
5953                    self.instrument_loop(statement, target);
5954                } else {
5955                    walk_mut::walk_statement(self, statement);
5956                }
5957            }
5958        }
5959    }
5960}
5961
5962/// A tree of selections used for its value, its leaves numbered in the order
5963/// they are evaluated.
5964enum SelectionShape {
5965    Leaf(usize),
5966    Selection {
5967        first: usize,
5968        kind: usize,
5969        left: Box<SelectionShape>,
5970        right: Box<SelectionShape>,
5971    },
5972}
5973
5974impl SelectionShape {
5975    fn leftmost(&self) -> usize {
5976        match self {
5977            Self::Leaf(leaf) => *leaf,
5978            Self::Selection { left, .. } => left.leftmost(),
5979        }
5980    }
5981
5982    /// The leaves whose value can be this subtree's when that value is
5983    /// truthy, or when it is not, each with the points it hits on its way up
5984    /// through the subtree: what `selectPathV2` records for the leaf's steps
5985    /// below the subtree's parent. A left side decides only by cutting its
5986    /// selection short, which `||` does on a truthy value, `&&` on a falsy
5987    /// one, and `??` on any that is not nullish.
5988    fn deciders(&self, truthy: bool, nullish: bool) -> Vec<(usize, Vec<usize>)> {
5989        match self {
5990            Self::Leaf(leaf) => vec![(*leaf, Vec::new())],
5991            Self::Selection {
5992                first,
5993                kind,
5994                left,
5995                right,
5996            } => {
5997                let mut deciders = Vec::new();
5998                let short = match kind {
5999                    0 => truthy,
6000                    1 => !truthy,
6001                    _ => !nullish,
6002                };
6003                if short {
6004                    for (leaf, mut hits) in left.deciders(truthy, nullish) {
6005                        hits.push(first + usize::from(truthy));
6006                        deciders.push((leaf, hits));
6007                    }
6008                }
6009                for (leaf, mut hits) in right.deciders(truthy, nullish) {
6010                    hits.push(first + 2 + usize::from(truthy));
6011                    deciders.push((leaf, hits));
6012                }
6013                deciders
6014            }
6015        }
6016    }
6017
6018    /// For each leaf but the first, the leaves that can have been evaluated
6019    /// just before it, with what each decided by then. Evaluation reaches the
6020    /// right side of a selection when its left side did not cut it short:
6021    /// truthy for `&&`, falsy for `||`, and nullish, so falsy, for `??`.
6022    fn before(&self, before: &mut Vec<Vec<(usize, Vec<usize>)>>) {
6023        let Self::Selection {
6024            kind, left, right, ..
6025        } = self
6026        else {
6027            return;
6028        };
6029        before[right.leftmost()] = left.deciders(*kind == 1, *kind == 2);
6030        left.before(before);
6031        right.before(before);
6032    }
6033}
6034
6035type SelectionTable = (Vec<Vec<usize>>, Vec<Vec<Vec<usize>>>);
6036
6037/// A tree whose leaves are named: the helper that records its value, the
6038/// selection or the table entry it records for, and the tree's frame.
6039struct SelectionSite {
6040    helper: String,
6041    index: usize,
6042    frame: String,
6043}
6044
6045struct LogicalValueTransformer<'a, 's> {
6046    ast: AstBuilder<'a>,
6047    select_short_v2: String,
6048    select_right_v2: String,
6049    select_named_right_v2: String,
6050    select_assign_end_v2: String,
6051    select_path_v2: String,
6052    select_end2_v2: String,
6053    select_end_v2: String,
6054    select_next_v2: String,
6055    coverage_hit_v2: String,
6056    probe_file_v2: String,
6057    typescript: bool,
6058    names: CandidateNames<'s>,
6059    scope_declarations: Vec<Vec<String>>,
6060    /// Inside a parameter default or a class member's initializer, where no
6061    /// function body can declare a tree's frame: a parameter list cannot see
6062    /// its function's body, and an initializer's enclosing scope is shared by
6063    /// every instance being constructed.
6064    frameless: usize,
6065    /// For each tree with more than two leaves, the runtime's table: the
6066    /// steps of each leaf, and for each leaf the points each leaf that can
6067    /// come before it has decided by then, where more than one can.
6068    trees: Vec<SelectionTable>,
6069    /// The statement being visited is a `const` declaration in a list of
6070    /// statements, where another can follow it.
6071    aliasing: bool,
6072    /// What records the values of the trees the declaration just visited
6073    /// keeps as its initializers, to go after it.
6074    after_declaration: Vec<Expression<'a>>,
6075    /// A selection's four outcomes -- short falsy, short truthy, right falsy,
6076    /// right truthy -- are consecutive V2 points from this index.
6077    logical_targets: HashMap<SpanKey, usize>,
6078    /// The same four points for `||=`, `&&=` and `??=`.
6079    assignment_targets: HashMap<SpanKey, usize>,
6080    source_sensitive_functions: HashSet<SpanKey>,
6081}
6082
6083impl<'a> LogicalValueTransformer<'a, '_> {
6084    fn identifier(&self, name: &str) -> Expression<'a> {
6085        self.ast
6086            .expression_identifier(Span::default(), self.ast.ident(name))
6087    }
6088
6089    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
6090        AssignmentTarget::from(
6091            self.ast
6092                .simple_assignment_target_assignment_target_identifier(
6093                    Span::default(),
6094                    self.ast.ident(name),
6095                ),
6096        )
6097    }
6098
6099    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
6100        self.ast.expression_call(
6101            Span::default(),
6102            self.identifier(name),
6103            NONE,
6104            arguments,
6105            false,
6106        )
6107    }
6108
6109    fn string_argument(&self, value: &str) -> Argument<'a> {
6110        Argument::from(self.ast.expression_string_literal(
6111            Span::default(),
6112            self.ast.str(value),
6113            None,
6114        ))
6115    }
6116
6117    fn enter_scope(&mut self) {
6118        self.scope_declarations.push(Vec::new());
6119    }
6120
6121    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
6122        let names = self
6123            .scope_declarations
6124            .pop()
6125            .expect("logical-value scope stack must remain balanced");
6126        if names.is_empty() {
6127            return;
6128        }
6129        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
6130    }
6131
6132    fn scratch(&mut self, base: &str) -> String {
6133        let name = self.names.allocate(base);
6134        self.scope_declarations
6135            .last_mut()
6136            .expect("logical expression must be inside a program or function")
6137            .push(name.clone());
6138        name
6139    }
6140
6141    fn helper(&self, name: &str, arguments: [Argument<'a>; 3]) -> Expression<'a> {
6142        let mut all = self
6143            .ast
6144            .vec_from_array([Argument::from(self.identifier(&self.probe_file_v2))]);
6145        all.extend(arguments);
6146        self.call(name, all)
6147    }
6148
6149    /// `a || b` as `selectShortV2(file, first, a, 0) || selectRightV2(file,
6150    /// first, b)`, and `&&` (1) and `??` (2) alike. The program's own operator
6151    /// stays, so a coverage tool the tests run -- nyc, c8, Jest, Vitest -- counts
6152    /// the branches it counts without Supercov, and neither call needs a frame
6153    /// or a temporary: the short side records its outcome when it decides the
6154    /// result, the right side records whether it came out truthy. Both are small
6155    /// enough for V8 to inline; the frame and three calls each evaluation cost
6156    /// before were a tenth of lru-cache's hot loop, with MC/DC.
6157    ///
6158    /// Nested selections, `(a && b) || c`, stay one tree of the program's
6159    /// operators with only the leaves wrapped: istanbul flattens a tree through
6160    /// its logical children only, and a call around `a && b` made two branches
6161    /// of its one. A leaf deeper than one selection records, through
6162    /// `selectPathV2`, every selection above it that its value decides.
6163    fn instrument_tree(&mut self, expression: &mut Expression<'a>, path: &[usize]) {
6164        let key = span_key(expression.span());
6165        let first = self
6166            .logical_targets
6167            .remove(&key)
6168            .expect("a selection tree starts at a selection");
6169        let Expression::LogicalExpression(logical) = expression else {
6170            panic!("logical-value target must remain a logical expression");
6171        };
6172        let kind = match logical.operator {
6173            LogicalOperator::Or => 0,
6174            LogicalOperator::And => 1,
6175            LogicalOperator::Coalesce => 2,
6176        };
6177        let mut left_path = vec![first * 4 + kind];
6178        left_path.extend_from_slice(path);
6179        let mut right_path = vec![first * 4 + 3];
6180        right_path.extend_from_slice(path);
6181        self.instrument_operand(&mut logical.left, &left_path);
6182        self.instrument_operand(&mut logical.right, &right_path);
6183    }
6184
6185    fn instrument_operand(&mut self, operand: &mut Expression<'a>, path: &[usize]) {
6186        let mut inner = &mut *operand;
6187        while let Expression::ParenthesizedExpression(parenthesized) = inner {
6188            inner = &mut parenthesized.expression;
6189        }
6190        if matches!(inner, Expression::LogicalExpression(_))
6191            && self.logical_targets.contains_key(&span_key(inner.span()))
6192        {
6193            self.instrument_tree(inner, path);
6194            return;
6195        }
6196        self.visit_expression(operand);
6197        let value = operand.take_in(self.ast.allocator);
6198        *operand = match *path {
6199            [step] if step % 4 == 3 => {
6200                let mut arguments = self.ast.vec_from_array([
6201                    Argument::from(self.identifier(&self.probe_file_v2)),
6202                    Argument::from(numeric(self.ast, step / 4)),
6203                ]);
6204                arguments.push(Argument::from(value));
6205                self.call(&self.select_right_v2, arguments)
6206            }
6207            [step] => self.helper(
6208                &self.select_short_v2,
6209                [
6210                    Argument::from(numeric(self.ast, step / 4)),
6211                    Argument::from(value),
6212                    Argument::from(numeric(self.ast, step % 4)),
6213                ],
6214            ),
6215            _ => {
6216                let mut arguments = self.ast.vec_from_array([
6217                    Argument::from(self.identifier(&self.probe_file_v2)),
6218                    Argument::from(value),
6219                ]);
6220                arguments.extend(
6221                    path.iter()
6222                        .map(|step| Argument::from(numeric(self.ast, *step))),
6223                );
6224                self.call(&self.select_path_v2, arguments)
6225            }
6226        };
6227    }
6228
6229    /// `x ||= y` as `(T = 0, selectAssignEndV2(file, first, x ||= selectRightV2(
6230    /// file, first, y, T = 1), T))`, and `&&=` and `??=` alike. The operator
6231    /// stays, so the target is read and written once, as the program does, and
6232    /// a frame per evaluation is gone. `T` is a temporary of the enclosing
6233    /// function, set after the right side evaluates, so a recursive call there
6234    /// has its own; the end records the short outcome when it is still 0.
6235    /// The shape of the tree at `expression`, taking its selections out of
6236    /// the targets, and each leaf's steps as `selectPathV2` takes them.
6237    fn shape(
6238        &mut self,
6239        expression: &Expression<'a>,
6240        path: &[usize],
6241        leaves: &mut Vec<Vec<usize>>,
6242    ) -> SelectionShape {
6243        let mut inner = expression;
6244        while let Expression::ParenthesizedExpression(parenthesized) = inner {
6245            inner = &parenthesized.expression;
6246        }
6247        if let Expression::LogicalExpression(logical) = inner
6248            && let Some(first) = self.logical_targets.remove(&span_key(logical.span))
6249        {
6250            let kind = match logical.operator {
6251                LogicalOperator::Or => 0,
6252                LogicalOperator::And => 1,
6253                LogicalOperator::Coalesce => 2,
6254            };
6255            let mut left_path = vec![first * 4 + kind];
6256            left_path.extend_from_slice(path);
6257            let mut right_path = vec![first * 4 + 3];
6258            right_path.extend_from_slice(path);
6259            let left = Box::new(self.shape(&logical.left, &left_path, leaves));
6260            let right = Box::new(self.shape(&logical.right, &right_path, leaves));
6261            return SelectionShape::Selection {
6262                first,
6263                kind,
6264                left,
6265                right,
6266            };
6267        }
6268        leaves.push(path.to_vec());
6269        SelectionShape::Leaf(leaves.len() - 1)
6270    }
6271
6272    fn hits(&self, points: &[usize]) -> Option<Expression<'a>> {
6273        let mut hits = self.ast.vec_from_iter(points.iter().map(|point| {
6274            self.call(
6275                &self.coverage_hit_v2,
6276                self.ast.vec_from_array([
6277                    Argument::from(self.identifier(&self.probe_file_v2)),
6278                    Argument::from(numeric(self.ast, *point)),
6279                ]),
6280            )
6281        }));
6282        match hits.len() {
6283            0 => None,
6284            1 => hits.pop(),
6285            _ => Some(self.ast.expression_sequence(Span::default(), hits)),
6286        }
6287    }
6288
6289    /// What the leaf evaluated before this one decided. Where only one leaf
6290    /// can come before, its points are hit there. Where several can, the
6291    /// frame says which, and the runtime reads its points from the tree's
6292    /// table: a choice written out here would be a branch of the user's line
6293    /// to a coverage tool the tests run.
6294    fn recorded_before(
6295        &self,
6296        frame: &str,
6297        tree: Option<usize>,
6298        leaf: usize,
6299        deciders: &[(usize, Vec<usize>)],
6300    ) -> Option<Expression<'a>> {
6301        if deciders.iter().all(|(_, points)| points.is_empty()) {
6302            return None;
6303        }
6304        if let [(_, points)] = deciders {
6305            return self.hits(points);
6306        }
6307        let tree = tree.expect("a tree with a choice of leaves has a table");
6308        Some(self.call(
6309            &self.select_next_v2,
6310            self.ast.vec_from_array([
6311                Argument::from(self.identifier(&self.probe_file_v2)),
6312                Argument::from(numeric(self.ast, tree)),
6313                Argument::from(self.identifier(frame)),
6314                Argument::from(numeric(self.ast, leaf)),
6315            ]),
6316        ))
6317    }
6318
6319    fn name_leaves(
6320        &mut self,
6321        expression: &mut Expression<'a>,
6322        shape: &SelectionShape,
6323        frame: &str,
6324        tree: Option<usize>,
6325        before: &[Vec<(usize, Vec<usize>)>],
6326    ) {
6327        let leaf = match shape {
6328            SelectionShape::Leaf(leaf) => *leaf,
6329            SelectionShape::Selection { left, right, .. } => {
6330                let mut inner = expression;
6331                while let Expression::ParenthesizedExpression(parenthesized) = inner {
6332                    inner = &mut parenthesized.expression;
6333                }
6334                let Expression::LogicalExpression(logical) = inner else {
6335                    panic!("a selection must remain a logical expression");
6336                };
6337                self.name_leaves(&mut logical.left, left, frame, tree, before);
6338                self.name_leaves(&mut logical.right, right, frame, tree, before);
6339                return;
6340            }
6341        };
6342        self.visit_expression(expression);
6343        let value = expression.take_in(self.ast.allocator);
6344        let mut steps = self.ast.vec_with_capacity(3);
6345        steps.extend(self.recorded_before(frame, tree, leaf, &before[leaf]));
6346        steps.push(self.ast.expression_assignment(
6347            Span::default(),
6348            AssignmentOperator::Assign,
6349            self.assignment_target(frame),
6350            numeric(self.ast, leaf),
6351        ));
6352        steps.push(value);
6353        *expression = self.ast.expression_sequence(Span::default(), steps);
6354    }
6355
6356    /// `a && a.b` as `selectEnd2V2(file, first, (_f = 0, a) && (_f = 1, a.b),
6357    /// _f)`: each leaf is still the program's own expression, as the last
6358    /// operand of a comma, so it has the type, the contextual type and the
6359    /// narrowing it has in the source. With a call around each leaf, as in
6360    /// `instrument_tree`, `a.b` was read from an `a` that nothing had
6361    /// narrowed: an error the instrumented copy does not report, and `any`
6362    /// when a member of `a`'s type has no `b`, for everything that reads the
6363    /// value.
6364    ///
6365    /// The frame names the leaf being evaluated. The call around the tree
6366    /// records the outcomes of the leaf that produced its value. What an
6367    /// earlier leaf decided is known from where evaluation went next, and is
6368    /// recorded there: the same points, in the same order, as before, a tree
6369    /// that throws half-way included.
6370    fn instrument_value_tree(&mut self, expression: &mut Expression<'a>) {
6371        let site = self.name_tree(expression);
6372        let tree = expression.take_in(self.ast.allocator);
6373        *expression = self.record_tree(&site, tree);
6374    }
6375
6376    /// The call that records a named tree's value, around that value.
6377    fn record_tree(&self, site: &SelectionSite, value: Expression<'a>) -> Expression<'a> {
6378        self.call(
6379            &site.helper,
6380            self.ast.vec_from_array([
6381                Argument::from(self.identifier(&self.probe_file_v2)),
6382                Argument::from(numeric(self.ast, site.index)),
6383                Argument::from(value),
6384                Argument::from(self.identifier(&site.frame)),
6385            ]),
6386        )
6387    }
6388
6389    /// Name the leaves of the tree at `expression`.
6390    fn name_tree(&mut self, expression: &mut Expression<'a>) -> SelectionSite {
6391        let mut leaves = Vec::new();
6392        let shape = self.shape(expression, &[], &mut leaves);
6393        let SelectionShape::Selection { first, .. } = shape else {
6394            panic!("a selection tree starts at a selection");
6395        };
6396        let mut before = vec![Vec::new(); leaves.len()];
6397        shape.before(&mut before);
6398        let frame = self.scratch("_supercovSelected");
6399        let tree = (leaves.len() > 2).then(|| {
6400            // The table for `selectNextV2`, by the leaf reached and the one
6401            // before it.
6402            let choices = before
6403                .iter()
6404                .map(|deciders| {
6405                    let mut by_leaf = Vec::new();
6406                    if deciders.len() > 1 {
6407                        for (leaf, points) in deciders {
6408                            if by_leaf.len() <= *leaf {
6409                                by_leaf.resize(*leaf + 1, Vec::new());
6410                            }
6411                            by_leaf[*leaf] = points.clone();
6412                        }
6413                    }
6414                    by_leaf
6415                })
6416                .collect();
6417            self.trees.push((leaves, choices));
6418            self.trees.len() - 1
6419        });
6420        self.name_leaves(expression, &shape, &frame, tree, &before);
6421        let (helper, index) = match tree {
6422            Some(tree) => (self.select_end_v2.clone(), tree),
6423            None => (self.select_end2_v2.clone(), first),
6424        };
6425        SelectionSite {
6426            helper,
6427            index,
6428            frame,
6429        }
6430    }
6431
6432    fn instrument_assignment(
6433        &mut self,
6434        assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
6435        first: usize,
6436    ) -> Expression<'a> {
6437        let assignment = assignment.unbox();
6438        let inferred_name = match &assignment.left {
6439            AssignmentTarget::AssignmentTargetIdentifier(identifier)
6440                if assignment.span.start == identifier.span.start
6441                    && expression_is_anonymous_definition(&assignment.right) =>
6442            {
6443                Some(identifier.name.to_string())
6444            }
6445            _ => None,
6446        };
6447        let right_evaluated = self.scratch("_supercovAssignedRight");
6448        let set = |value| {
6449            self.ast.expression_assignment(
6450                Span::default(),
6451                AssignmentOperator::Assign,
6452                self.assignment_target(&right_evaluated),
6453                numeric(self.ast, value),
6454            )
6455        };
6456        let (reset, mark) = (set(0), set(1));
6457        let mut right_arguments = self.ast.vec_from_array([
6458            Argument::from(self.identifier(&self.probe_file_v2)),
6459            Argument::from(numeric(self.ast, first)),
6460            Argument::from(assignment.right),
6461        ]);
6462        let right_helper = match inferred_name {
6463            Some(name) => {
6464                right_arguments.push(self.string_argument(&name));
6465                &self.select_named_right_v2
6466            }
6467            None => &self.select_right_v2,
6468        };
6469        right_arguments.push(Argument::from(mark));
6470        let right = self.call(right_helper, right_arguments);
6471        let measured_assignment = self.ast.expression_assignment(
6472            Span::default(),
6473            assignment.operator,
6474            assignment.left,
6475            right,
6476        );
6477        let end = self.call(
6478            &self.select_assign_end_v2,
6479            self.ast.vec_from_array([
6480                Argument::from(self.identifier(&self.probe_file_v2)),
6481                Argument::from(numeric(self.ast, first)),
6482                Argument::from(measured_assignment),
6483                Argument::from(self.identifier(&right_evaluated)),
6484            ]),
6485        );
6486        self.ast
6487            .expression_sequence(Span::default(), self.ast.vec_from_array([reset, end]))
6488    }
6489}
6490
6491impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
6492    fn visit_program(&mut self, program: &mut Program<'a>) {
6493        self.enter_scope();
6494        walk_mut::walk_program(self, program);
6495        self.leave_scope(&mut program.body);
6496    }
6497
6498    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
6499        let frameless = std::mem::take(&mut self.frameless);
6500        self.enter_scope();
6501        walk_mut::walk_function_body(self, body);
6502        self.leave_scope(&mut body.statements);
6503        self.frameless = frameless;
6504    }
6505
6506    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
6507        let mut index = 0;
6508        while index < statements.len() {
6509            let declaration = match &statements[index] {
6510                Statement::VariableDeclaration(declaration) => Some(&**declaration),
6511                Statement::ExportNamedDeclaration(export) => match &export.declaration {
6512                    Some(Declaration::VariableDeclaration(declaration)) => Some(&**declaration),
6513                    _ => None,
6514                },
6515                _ => None,
6516            };
6517            let aliasing = std::mem::replace(
6518                &mut self.aliasing,
6519                declaration.is_some_and(|declaration| declaration.kind.is_const()),
6520            );
6521            self.visit_statement(&mut statements[index]);
6522            self.aliasing = aliasing;
6523            for recorded in std::mem::take(&mut self.after_declaration) {
6524                index += 1;
6525                statements.insert(
6526                    index,
6527                    self.ast.statement_expression(Span::default(), recorded),
6528                );
6529            }
6530            index += 1;
6531        }
6532    }
6533
6534    // `const ok = a && b; if (ok) …` narrows `a` and `b`, but only while the
6535    // tree is the initializer itself: its value is recorded by a statement
6536    // after the declaration, from the constant.
6537    fn visit_variable_declarator(&mut self, declarator: &mut VariableDeclarator<'a>) {
6538        let aliasing = std::mem::take(&mut self.aliasing);
6539        if aliasing
6540            && self.frameless == 0
6541            && let BindingPattern::BindingIdentifier(name) = &declarator.id
6542            && let Some(initializer) = &mut declarator.init
6543        {
6544            let mut tree = &mut *initializer;
6545            while let Expression::ParenthesizedExpression(parenthesized) = tree {
6546                tree = &mut parenthesized.expression;
6547            }
6548            if matches!(tree, Expression::LogicalExpression(_))
6549                && self.logical_targets.contains_key(&span_key(tree.span()))
6550            {
6551                let site = self.name_tree(tree);
6552                let recorded = self.record_tree(&site, self.identifier(&name.name));
6553                self.after_declaration.push(recorded);
6554                self.aliasing = aliasing;
6555                return;
6556            }
6557        }
6558        walk_mut::walk_variable_declarator(self, declarator);
6559        self.aliasing = aliasing;
6560    }
6561
6562    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
6563        self.frameless += 1;
6564        walk_mut::walk_formal_parameters(self, parameters);
6565        self.frameless -= 1;
6566    }
6567
6568    fn visit_property_definition(&mut self, property: &mut oxc_ast::ast::PropertyDefinition<'a>) {
6569        self.frameless += 1;
6570        walk_mut::walk_property_definition(self, property);
6571        self.frameless -= 1;
6572    }
6573
6574    fn visit_accessor_property(&mut self, property: &mut oxc_ast::ast::AccessorProperty<'a>) {
6575        self.frameless += 1;
6576        walk_mut::walk_accessor_property(self, property);
6577        self.frameless -= 1;
6578    }
6579
6580    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6581        if self
6582            .source_sensitive_functions
6583            .contains(&span_key(function.span))
6584        {
6585            return;
6586        }
6587        walk_mut::walk_function(self, function, flags);
6588    }
6589
6590    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6591        if self
6592            .source_sensitive_functions
6593            .contains(&span_key(function.span))
6594        {
6595            return;
6596        }
6597        walk_mut::walk_arrow_function_expression(self, function);
6598    }
6599
6600    // A with body is left uninstrumented: its object environment can
6601    // intercept probe identifiers (see `enter_with_statement`).
6602    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6603
6604    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
6605        let key = span_key(expression.span());
6606        if matches!(expression, Expression::LogicalExpression(_))
6607            && self.logical_targets.contains_key(&key)
6608        {
6609            if self.frameless > 0 {
6610                self.instrument_tree(expression, &[]);
6611            } else {
6612                self.instrument_value_tree(expression);
6613            }
6614            return;
6615        }
6616        walk_mut::walk_expression(self, expression);
6617        if let Some(first) = self.assignment_targets.remove(&key) {
6618            let original = expression.take_in(self.ast.allocator);
6619            let Expression::AssignmentExpression(assignment) = original else {
6620                panic!("logical-assignment target must remain an assignment expression");
6621            };
6622            *expression = self.instrument_assignment(assignment, first);
6623        }
6624    }
6625}
6626
6627struct SwitchTransformer<'a, 's> {
6628    ast: AstBuilder<'a>,
6629    coverage_hit: String,
6630    targets: HashMap<SpanKey, SwitchTarget>,
6631    names: CandidateNames<'s>,
6632    source_sensitive_functions: HashSet<SpanKey>,
6633}
6634
6635impl<'a> SwitchTransformer<'a, '_> {
6636    fn identifier(&self, name: &str) -> Expression<'a> {
6637        self.ast
6638            .expression_identifier(Span::default(), self.ast.ident(name))
6639    }
6640
6641    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
6642        AssignmentTarget::from(
6643            self.ast
6644                .simple_assignment_target_assignment_target_identifier(
6645                    Span::default(),
6646                    self.ast.ident(name),
6647                ),
6648        )
6649    }
6650
6651    fn probe(&self, id: &str) -> Statement<'a> {
6652        self.ast.statement_expression(
6653            Span::default(),
6654            self.ast.expression_call(
6655                Span::default(),
6656                self.identifier(&self.coverage_hit),
6657                NONE,
6658                self.ast
6659                    .vec1(Argument::from(self.ast.expression_string_literal(
6660                        Span::default(),
6661                        self.ast.str(id),
6662                        None,
6663                    ))),
6664                false,
6665            ),
6666        )
6667    }
6668
6669    fn target(statement: &Statement<'a>) -> Option<SpanKey> {
6670        match statement {
6671            Statement::SwitchStatement(node) => Some(span_key(node.span)),
6672            Statement::LabeledStatement(node) => Self::target(&node.body),
6673            _ => None,
6674        }
6675    }
6676
6677    fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
6678        match statement {
6679            Statement::SwitchStatement(node) => Some(node),
6680            Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
6681            _ => None,
6682        }
6683    }
6684
6685    fn entered_assignment(&self, entered: &str) -> Statement<'a> {
6686        self.ast.statement_expression(
6687            Span::default(),
6688            self.ast.expression_assignment(
6689                Span::default(),
6690                AssignmentOperator::Assign,
6691                self.assignment_target(entered),
6692                self.ast.expression_boolean_literal(Span::default(), true),
6693            ),
6694        )
6695    }
6696
6697    /// Each case records only when the switch selected it: a case the one
6698    /// before falls into (`case 0: case 1:`, or a body with no `break`) ran
6699    /// without its test matching, and crediting it claimed a test of a value
6700    /// none had checked. `entered` says the switch has already selected a
6701    /// case; it also tells a switch with no default that nothing matched.
6702    fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
6703        let entered = self.names.allocate("_supercovSwitchEntered");
6704        let node = Self::inner_switch(statement).expect("switch target must remain a switch");
6705        for (index, case) in node.cases.iter_mut().enumerate() {
6706            let probe = self.probe(
6707                target
6708                    .case_ids
6709                    .get(index)
6710                    .expect("switch case target count must remain stable"),
6711            );
6712            let selected = self.ast.statement_if(
6713                Span::default(),
6714                self.ast.expression_unary(
6715                    Span::default(),
6716                    UnaryOperator::LogicalNot,
6717                    self.identifier(&entered),
6718                ),
6719                probe,
6720                None,
6721            );
6722            case.consequent.insert(0, self.entered_assignment(&entered));
6723            case.consequent.insert(0, selected);
6724        }
6725        let declaration =
6726            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
6727                Span::default(),
6728                VariableDeclarationKind::Let,
6729                self.ast.vec1(self.ast.variable_declarator(
6730                    Span::default(),
6731                    VariableDeclarationKind::Let,
6732                    self.ast.binding_pattern_binding_identifier(
6733                        Span::default(),
6734                        self.ast.ident(&entered),
6735                    ),
6736                    NONE,
6737                    Some(self.ast.expression_boolean_literal(Span::default(), false)),
6738                    false,
6739                )),
6740                false,
6741            ));
6742        let original = statement.take_in(self.ast.allocator);
6743        let mut body = self.ast.vec_from_array([declaration, original]);
6744        if let Some(no_match_id) = &target.no_match_id {
6745            body.push(self.ast.statement_if(
6746                Span::default(),
6747                self.ast.expression_unary(
6748                    Span::default(),
6749                    UnaryOperator::LogicalNot,
6750                    self.identifier(&entered),
6751                ),
6752                self.probe(no_match_id),
6753                None,
6754            ));
6755        }
6756        *statement = self.ast.statement_block(Span::default(), body);
6757    }
6758}
6759
6760impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
6761    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6762        if self
6763            .source_sensitive_functions
6764            .contains(&span_key(function.span))
6765        {
6766            return;
6767        }
6768        walk_mut::walk_function(self, function, flags);
6769    }
6770
6771    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6772        if self
6773            .source_sensitive_functions
6774            .contains(&span_key(function.span))
6775        {
6776            return;
6777        }
6778        walk_mut::walk_arrow_function_expression(self, function);
6779    }
6780
6781    // A with body is left uninstrumented: its object environment can
6782    // intercept probe identifiers (see `enter_with_statement`).
6783    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6784
6785    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
6786        let Some(key) = Self::target(statement) else {
6787            walk_mut::walk_statement(self, statement);
6788            return;
6789        };
6790        let Some(target) = self.targets.remove(&key) else {
6791            walk_mut::walk_statement(self, statement);
6792            return;
6793        };
6794        self.instrument(statement, &target);
6795        // The switch now sits in a block of its own; its target is spent, so
6796        // walking it reaches the switches inside its cases and nothing twice.
6797        walk_mut::walk_statement(self, statement);
6798    }
6799}
6800
6801struct RouteRequestPhaseTransformer<'a, 's> {
6802    ast: AstBuilder<'a>,
6803    file: &'s str,
6804    with_request_phase: String,
6805    used: bool,
6806    names: CandidateNames<'s>,
6807}
6808
6809impl<'a> RouteRequestPhaseTransformer<'a, '_> {
6810    fn is_remix_route(&self) -> bool {
6811        self.file.starts_with("app/routes/")
6812    }
6813
6814    fn is_next_route(&self) -> bool {
6815        let path = Path::new(self.file);
6816        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
6817            return false;
6818        };
6819        let is_route_module = [
6820            "route.js",
6821            "route.jsx",
6822            "route.ts",
6823            "route.tsx",
6824            "route.mjs",
6825            "route.mts",
6826            "route.cjs",
6827            "route.cts",
6828        ]
6829        .contains(&name);
6830        if !is_route_module {
6831            return false;
6832        }
6833        let components = path
6834            .components()
6835            .filter_map(|component| component.as_os_str().to_str())
6836            .collect::<Vec<_>>();
6837        components
6838            .windows(2)
6839            .any(|window| window[0] == "app" && window[1] != name)
6840    }
6841
6842    fn is_handler_name(&self, name: &str) -> bool {
6843        (self.is_remix_route() && matches!(name, "loader" | "action"))
6844            || (self.is_next_route()
6845                && matches!(
6846                    name,
6847                    "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
6848                ))
6849    }
6850
6851    fn identifier(&self, name: &str) -> Expression<'a> {
6852        self.ast
6853            .expression_identifier(Span::default(), self.ast.ident(name))
6854    }
6855
6856    fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
6857        self.ast.expression_call(
6858            Span::default(),
6859            self.identifier(&self.with_request_phase),
6860            NONE,
6861            self.ast.vec1(Argument::from(expression)),
6862            false,
6863        )
6864    }
6865
6866    fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
6867        Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
6868            Span::default(),
6869            Some(self.ast.declaration_variable(
6870                Span::default(),
6871                VariableDeclarationKind::Const,
6872                self.ast.vec1(self.ast.variable_declarator(
6873                    Span::default(),
6874                    VariableDeclarationKind::Const,
6875                    self.ast.binding_pattern_binding_identifier(
6876                        Span::default(),
6877                        self.ast.ident(exported_name),
6878                    ),
6879                    NONE,
6880                    Some(self.wrap_expression(self.identifier(original_name))),
6881                    false,
6882                )),
6883                false,
6884            )),
6885            self.ast.vec(),
6886            None,
6887            ImportOrExportKind::Value,
6888            NONE,
6889        ))
6890    }
6891
6892    fn transform_named_export(
6893        &mut self,
6894        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
6895        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6896    ) {
6897        if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
6898            for declarator in &mut declaration.declarations {
6899                let Some(name) = binding_identifier_name(&declarator.id) else {
6900                    continue;
6901                };
6902                if !self.is_handler_name(&name) {
6903                    continue;
6904                }
6905                if let Some(initializer) = &mut declarator.init {
6906                    let original = initializer.take_in(self.ast.allocator);
6907                    *initializer = self.wrap_expression(original);
6908                    self.used = true;
6909                }
6910            }
6911            output.push(Statement::ExportNamedDeclaration(export));
6912            return;
6913        }
6914
6915        if matches!(
6916            export.declaration,
6917            Some(Declaration::FunctionDeclaration(_))
6918        ) {
6919            let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
6920            else {
6921                unreachable!();
6922            };
6923            let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
6924                output.push(Statement::FunctionDeclaration(function));
6925                return;
6926            };
6927            if !self.is_handler_name(&exported_name) {
6928                export.declaration = Some(Declaration::FunctionDeclaration(function));
6929                output.push(Statement::ExportNamedDeclaration(export));
6930                return;
6931            }
6932            let original_name = self
6933                .names
6934                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6935            function.id = Some(
6936                self.ast
6937                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
6938            );
6939            output.push(Statement::FunctionDeclaration(function));
6940            output.push(self.wrapped_named_export(&exported_name, &original_name));
6941            self.used = true;
6942            return;
6943        }
6944
6945        if export.source.is_none() {
6946            output.push(Statement::ExportNamedDeclaration(export));
6947            return;
6948        }
6949        let source = export
6950            .source
6951            .as_ref()
6952            .expect("checked above")
6953            .clone_in(self.ast.allocator);
6954        let specifiers = export.specifiers.take_in(self.ast.allocator);
6955        let mut untouched = self.ast.vec();
6956        let mut handlers = Vec::new();
6957        for specifier in specifiers {
6958            let exported_name = specifier
6959                .exported
6960                .identifier_name()
6961                .map(|name| name.to_string());
6962            if exported_name
6963                .as_deref()
6964                .is_some_and(|name| self.is_handler_name(name))
6965            {
6966                handlers.push((exported_name.expect("checked above"), specifier));
6967            } else {
6968                untouched.push(specifier);
6969            }
6970        }
6971        if handlers.is_empty() {
6972            export.specifiers = untouched;
6973            output.push(Statement::ExportNamedDeclaration(export));
6974            return;
6975        }
6976        if !untouched.is_empty() {
6977            output.push(Statement::ExportNamedDeclaration(
6978                self.ast.alloc_export_named_declaration(
6979                    export.span,
6980                    None,
6981                    untouched,
6982                    Some(source.clone_in(self.ast.allocator)),
6983                    export.export_kind,
6984                    export.with_clause.take(),
6985                ),
6986            ));
6987        }
6988        for (exported_name, specifier) in handlers {
6989            let original_name = self
6990                .names
6991                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6992            output.push(Statement::ImportDeclaration(
6993                self.ast.alloc_import_declaration(
6994                    Span::default(),
6995                    Some(self.ast.vec1(
6996                        self.ast.import_declaration_specifier_import_specifier(
6997                            Span::default(),
6998                            specifier.local,
6999                            self.ast.binding_identifier(
7000                                Span::default(),
7001                                self.ast.ident(&original_name),
7002                            ),
7003                            ImportOrExportKind::Value,
7004                        ),
7005                    )),
7006                    source.clone_in(self.ast.allocator),
7007                    None,
7008                    NONE,
7009                    ImportOrExportKind::Value,
7010                ),
7011            ));
7012            output.push(self.wrapped_named_export(&exported_name, &original_name));
7013        }
7014        self.used = true;
7015    }
7016
7017    fn transform_default_export(
7018        &mut self,
7019        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
7020        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7021    ) {
7022        // Looked at before it is taken: taking it leaves a placeholder `null`,
7023        // and a server entry that exported its handler by name (`export
7024        // default handleRequest;`) exported `withRequestPhase(null)`.
7025        if matches!(
7026            export.declaration,
7027            ExportDefaultDeclarationKind::FunctionDeclaration(_)
7028        ) {
7029            let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
7030                export.declaration.take_in(self.ast.allocator)
7031            else {
7032                unreachable!();
7033            };
7034            let original_name = self
7035                .names
7036                .allocate("__supercovHandleRequestCoverageOriginal");
7037            function.id = Some(
7038                self.ast
7039                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
7040            );
7041            output.push(Statement::FunctionDeclaration(function));
7042            output.push(Statement::ExportDefaultDeclaration(
7043                self.ast.alloc_export_default_declaration(
7044                    Span::default(),
7045                    ExportDefaultDeclarationKind::from(
7046                        self.wrap_expression(self.identifier(&original_name)),
7047                    ),
7048                ),
7049            ));
7050            self.used = true;
7051            return;
7052        }
7053        if export.declaration.is_expression() {
7054            let original = export
7055                .declaration
7056                .take_in(self.ast.allocator)
7057                .into_expression();
7058            export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
7059            self.used = true;
7060        }
7061        output.push(Statement::ExportDefaultDeclaration(export));
7062    }
7063
7064    fn transform_program(&mut self, program: &mut Program<'a>) {
7065        let route_module = self.is_remix_route() || self.is_next_route();
7066        let server_entry = self.file.starts_with("app/entry.server.")
7067            && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
7068                &Path::new(self.file)
7069                    .extension()
7070                    .and_then(|value| value.to_str())
7071                    .unwrap_or(""),
7072            );
7073        if !route_module && !server_entry {
7074            return;
7075        }
7076        let original = program.body.take_in(self.ast.allocator);
7077        let mut output = self.ast.vec_with_capacity(original.len() + 4);
7078        for statement in original {
7079            match statement {
7080                Statement::ExportNamedDeclaration(export) if route_module => {
7081                    self.transform_named_export(export, &mut output);
7082                }
7083                Statement::ExportDefaultDeclaration(export) if server_entry => {
7084                    self.transform_default_export(export, &mut output);
7085                }
7086                statement => output.push(statement),
7087            }
7088        }
7089        program.body = output;
7090    }
7091}
7092
7093struct RequestPhaseTransformer<'a> {
7094    ast: AstBuilder<'a>,
7095    with_request_phase: String,
7096    used: bool,
7097    source_sensitive_functions: HashSet<SpanKey>,
7098}
7099
7100impl<'a> RequestPhaseTransformer<'a> {
7101    fn identifier(&self, name: &str) -> Expression<'a> {
7102        self.ast
7103            .expression_identifier(Span::default(), self.ast.ident(name))
7104    }
7105
7106    fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
7107        match callee {
7108            Expression::Identifier(identifier) => identifier.name == name,
7109            Expression::StaticMemberExpression(member) => member.property.name == name,
7110            _ => false,
7111        }
7112    }
7113
7114    fn callback_candidate(argument: &Argument<'a>) -> bool {
7115        matches!(
7116            argument,
7117            Argument::FunctionExpression(_)
7118                | Argument::ArrowFunctionExpression(_)
7119                | Argument::Identifier(_)
7120                | Argument::ComputedMemberExpression(_)
7121                | Argument::StaticMemberExpression(_)
7122                | Argument::PrivateFieldExpression(_)
7123        )
7124    }
7125
7126    fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
7127        matches!(
7128            argument,
7129            Argument::CallExpression(call)
7130                if expression_is_identifier(&call.callee, &self.with_request_phase)
7131        )
7132    }
7133
7134    fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
7135        if self.already_wrapped(argument) || !argument.is_expression() {
7136            return;
7137        }
7138        let expression = argument.to_expression_mut();
7139        let original = expression.take_in(self.ast.allocator);
7140        *expression = self.ast.expression_call(
7141            Span::default(),
7142            self.identifier(&self.with_request_phase),
7143            NONE,
7144            self.ast
7145                .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
7146                    |name| {
7147                        Argument::from(self.ast.expression_string_literal(
7148                            Span::default(),
7149                            self.ast.str(name),
7150                            None,
7151                        ))
7152                    },
7153                ))),
7154            false,
7155        );
7156        self.used = true;
7157    }
7158}
7159
7160impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
7161    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7162        if self
7163            .source_sensitive_functions
7164            .contains(&span_key(function.span))
7165        {
7166            return;
7167        }
7168        walk_mut::walk_function(self, function, flags);
7169    }
7170
7171    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7172        if self
7173            .source_sensitive_functions
7174            .contains(&span_key(function.span))
7175        {
7176            return;
7177        }
7178        walk_mut::walk_arrow_function_expression(self, function);
7179    }
7180
7181    // A with body is left uninstrumented: its object environment can
7182    // intercept probe identifiers (see `enter_with_statement`).
7183    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7184
7185    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
7186        walk_mut::walk_call_expression(self, call);
7187        let property = match &call.callee {
7188            Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
7189            _ => None,
7190        };
7191        let mut callback_index = None;
7192        if matches!(property, Some("on" | "once" | "addListener"))
7193            && matches!(
7194                call.arguments.first(),
7195                Some(Argument::StringLiteral(event))
7196                    if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
7197            )
7198        {
7199            callback_index = Some(1);
7200        } else if Self::callee_is(&call.callee, "createServer") {
7201            callback_index = call.arguments.iter().rposition(Self::callback_candidate);
7202        }
7203        let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
7204        if let Some(index) = callback_index
7205            && let Some(argument) = call.arguments.get_mut(index)
7206        {
7207            self.wrap_argument(argument, connection.then_some("connection"));
7208        }
7209    }
7210}
7211
7212struct CandidateNames<'s> {
7213    source: &'s str,
7214    /// Every identifier in the source that begins with `_`, which every
7215    /// generated name does. Asking the source whether it contains a name was
7216    /// a scan of the whole file per name, and names are allocated per
7217    /// function: the second quadratic cost in instrumenting a long file.
7218    underscored: HashSet<&'s str>,
7219    allocated: HashSet<String>,
7220    suffix: usize,
7221    /// Appended to every name, so output that shares a scope with other
7222    /// instrumented files cannot collide with theirs.
7223    namespace: String,
7224}
7225
7226impl<'s> CandidateNames<'s> {
7227    fn new(source: &'s str) -> Self {
7228        Self::within(source, "")
7229    }
7230
7231    fn within(source: &'s str, namespace: &str) -> Self {
7232        let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
7233        let underscored = source
7234            .split(|c: char| !identifier(c))
7235            .filter(|word| word.starts_with('_'))
7236            .collect();
7237        Self {
7238            source,
7239            underscored,
7240            allocated: HashSet::new(),
7241            suffix: 0,
7242            namespace: namespace.to_owned(),
7243        }
7244    }
7245
7246    fn taken(&self, candidate: &str) -> bool {
7247        self.allocated.contains(candidate)
7248            || if candidate.starts_with('_') {
7249                self.underscored.contains(candidate)
7250            } else {
7251                self.source.contains(candidate)
7252            }
7253    }
7254
7255    fn allocate(&mut self, base: &str) -> String {
7256        loop {
7257            let candidate = if self.suffix == 0 {
7258                format!("{base}{}", self.namespace)
7259            } else {
7260                format!("{base}{}{}", self.suffix, self.namespace)
7261            };
7262            self.suffix += 1;
7263            if !self.taken(&candidate) {
7264                self.allocated.insert(candidate.clone());
7265                return candidate;
7266            }
7267        }
7268    }
7269}
7270
7271struct ControlProbeV2Transformer<'a, 's> {
7272    ast: AstBuilder<'a>,
7273    decisions: &'s [CandidateDecision],
7274    /// Expressions used as values that are decisions, by span, each with the
7275    /// root of the tree whose evaluation records it.
7276    value_decisions: &'s HashMap<SpanKey, SpanKey>,
7277    /// Decisions applied below a tree root and not yet recorded there, with
7278    /// the shape their outcome is read back from.
7279    pending_values: HashMap<SpanKey, Vec<(AppliedDecision, ValueShape)>>,
7280    mcdc_begin: String,
7281    mcdc_condition: String,
7282    mcdc_end: String,
7283    mcdc_end_v2: String,
7284    probe_file_v2: String,
7285    names: CandidateNames<'s>,
7286    scope_declarations: Vec<Vec<String>>,
7287    decision_index: usize,
7288    parameter_depth: usize,
7289    source_sensitive_functions: HashSet<SpanKey>,
7290    /// What goes around a statement whose false outcome is recorded after
7291    /// it: the statement that records it, and for a `do … while` one before
7292    /// the loop.
7293    after_statement: Option<(Option<Statement<'a>>, Statement<'a>)>,
7294    /// The statement being visited is the body of a label, which has to stay
7295    /// on it: what goes after the statement goes after the label.
7296    label_body: bool,
7297    /// The temporaries of a decision inside a parameter default, for the
7298    /// arrow that has to declare them around its conditional expression.
7299    inline_names: Option<Vec<String>>,
7300}
7301
7302#[derive(Clone, Copy)]
7303struct DecisionPlan {
7304    index: usize,
7305    condition_count: usize,
7306    inline_frame: bool,
7307}
7308
7309/// A decision whose conditions have been rewritten, and the temporaries its
7310/// outcome is recorded from.
7311struct AppliedDecision {
7312    index: usize,
7313    frame: String,
7314    values: Vec<String>,
7315    inline: bool,
7316}
7317
7318impl<'a> ControlProbeV2Transformer<'a, '_> {
7319    fn enter_declaration_scope(&mut self) {
7320        self.scope_declarations.push(Vec::new());
7321    }
7322
7323    fn leave_declaration_scope(&mut self) -> Vec<String> {
7324        self.scope_declarations
7325            .pop()
7326            .expect("program/function scope stack must remain balanced")
7327    }
7328
7329    fn allocate_scratch(&mut self, base: &str) -> String {
7330        let name = self.names.allocate(base);
7331        self.scope_declarations
7332            .last_mut()
7333            .expect("a control decision must be inside a program or function body")
7334            .push(name.clone());
7335        name
7336    }
7337
7338    fn scratch_for(&mut self, base: &str, inline: bool) -> String {
7339        if inline {
7340            self.names.allocate(base)
7341        } else {
7342            self.allocate_scratch(base)
7343        }
7344    }
7345
7346    fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
7347        let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
7348            self.ast.variable_declarator(
7349                Span::default(),
7350                VariableDeclarationKind::Let,
7351                self.ast
7352                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
7353                NONE,
7354                None,
7355                false,
7356            )
7357        }));
7358        let body = self.ast.alloc_function_body(
7359            Span::default(),
7360            self.ast.vec(),
7361            self.ast.vec_from_array([
7362                Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7363                    Span::default(),
7364                    VariableDeclarationKind::Let,
7365                    declarations,
7366                    false,
7367                )),
7368                self.ast.statement_return(Span::default(), Some(expression)),
7369            ]),
7370        );
7371        let params = self.ast.alloc_formal_parameters(
7372            Span::default(),
7373            FormalParameterKind::ArrowFormalParameters,
7374            self.ast.vec(),
7375            NONE,
7376        );
7377        let arrow = self.ast.expression_arrow_function(
7378            Span::default(),
7379            false,
7380            false,
7381            NONE,
7382            params,
7383            NONE,
7384            body,
7385        );
7386        self.ast
7387            .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
7388    }
7389
7390    fn prepend_declarations(
7391        &self,
7392        names: Vec<String>,
7393        statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7394    ) {
7395        if names.is_empty() {
7396            return;
7397        }
7398        let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
7399            self.ast.variable_declarator(
7400                Span::default(),
7401                VariableDeclarationKind::Let,
7402                self.ast
7403                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
7404                NONE,
7405                None,
7406                false,
7407            )
7408        }));
7409        statements.insert(
7410            0,
7411            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7412                Span::default(),
7413                VariableDeclarationKind::Let,
7414                declarators,
7415                false,
7416            )),
7417        );
7418    }
7419
7420    fn identifier(&self, name: &str) -> Expression<'a> {
7421        self.ast
7422            .expression_identifier(Span::default(), self.ast.ident(name))
7423    }
7424
7425    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
7426        let target = self
7427            .ast
7428            .simple_assignment_target_assignment_target_identifier(
7429                Span::default(),
7430                self.ast.ident(name),
7431            );
7432        AssignmentTarget::from(target)
7433    }
7434
7435    fn number(&self, value: u64) -> Expression<'a> {
7436        self.ast.expression_numeric_literal(
7437            Span::default(),
7438            value as f64,
7439            None,
7440            NumberBase::Decimal,
7441        )
7442    }
7443
7444    fn string(&self, value: &str) -> Expression<'a> {
7445        self.ast
7446            .expression_string_literal(Span::default(), self.ast.str(value), None)
7447    }
7448
7449    fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
7450        self.ast.object_property_kind_object_property(
7451            Span::default(),
7452            PropertyKind::Init,
7453            self.ast
7454                .property_key_static_identifier(Span::default(), self.ast.ident(name)),
7455            value,
7456            false,
7457            false,
7458            false,
7459        )
7460    }
7461
7462    fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
7463        let conditions = self.ast.expression_array(
7464            Span::default(),
7465            self.ast.vec_from_iter(
7466                decision
7467                    .conditions
7468                    .iter()
7469                    .map(|condition| ArrayExpressionElement::from(self.string(condition))),
7470            ),
7471        );
7472        let properties = self.ast.vec_from_array([
7473            self.object_property("id", self.string(&decision.id)),
7474            self.object_property("file", self.string(&decision.file)),
7475            self.object_property("line", self.number(decision.line as u64)),
7476            self.object_property("column", self.number(decision.column as u64)),
7477            self.object_property("source", self.string(&decision.source)),
7478            self.object_property("conditions", conditions),
7479            self.object_property("kind", self.string(&decision.kind)),
7480        ]);
7481        Expression::ObjectExpression(
7482            self.ast
7483                .alloc_object_expression(Span::default(), properties),
7484        )
7485    }
7486
7487    fn assign(&self, name: &str, value: Expression<'a>) -> Expression<'a> {
7488        self.ast.expression_assignment(
7489            Span::default(),
7490            AssignmentOperator::Assign,
7491            self.assignment_target(name),
7492            value,
7493        )
7494    }
7495
7496    /// One condition of a decision, as `(_f += w, _v = C)`.
7497    ///
7498    /// The assignment comes last because TypeScript narrows through an
7499    /// assignment's right side and through a comma's last operand, and through
7500    /// nothing else here. The condition used to end in its temporary
7501    /// (`(_v = C, _f += …, _v)`), which narrows nothing: `if (client) return
7502    /// client` returned `Client | null`, and a file that Supercov never
7503    /// touched failed to compile against the widened type.
7504    ///
7505    /// The first condition carries the decision's bookkeeping, since a comma
7506    /// around the whole decision would hide its `&&` and `||` from the same
7507    /// analysis: the frame starts at this condition's weight, and the other
7508    /// conditions' temporaries are cleared, so one that is not evaluated adds
7509    /// nothing when the outcome is recorded.
7510    fn instrument_condition(
7511        &self,
7512        expression: Expression<'a>,
7513        applied: &AppliedDecision,
7514        index: usize,
7515    ) -> Expression<'a> {
7516        let mut steps = self.ast.vec();
7517        if index == 0 {
7518            steps.push(self.assign(&applied.frame, self.number(1)));
7519            for name in &applied.values[1..] {
7520                steps.push(self.assign(name, self.number(0)));
7521            }
7522        } else {
7523            steps.push(self.ast.expression_assignment(
7524                Span::default(),
7525                AssignmentOperator::Addition,
7526                self.assignment_target(&applied.frame),
7527                self.number(3_u64.pow(index as u32)),
7528            ));
7529        }
7530        steps.push(self.assign(&applied.values[index], expression));
7531        self.ast.expression_sequence(Span::default(), steps)
7532    }
7533
7534    fn instrument_conditions(
7535        &self,
7536        expression: &mut Expression<'a>,
7537        applied: &AppliedDecision,
7538        next_index: &mut usize,
7539    ) {
7540        match expression {
7541            Expression::ParenthesizedExpression(parenthesized) => {
7542                self.instrument_conditions(&mut parenthesized.expression, applied, next_index)
7543            }
7544            Expression::LogicalExpression(logical)
7545                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7546            {
7547                self.instrument_conditions(&mut logical.left, applied, next_index);
7548                self.instrument_conditions(&mut logical.right, applied, next_index);
7549            }
7550            Expression::UnaryExpression(unary)
7551                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7552            {
7553                self.instrument_conditions(&mut unary.argument, applied, next_index);
7554            }
7555            _ => {
7556                let index = *next_index;
7557                *next_index += 1;
7558                let original = expression.take_in(self.ast.allocator);
7559                *expression = self.instrument_condition(original, applied, index);
7560            }
7561        }
7562    }
7563
7564    /// The decision's vector in base 3, from its temporaries: nothing for a
7565    /// condition that was not evaluated, its weight for a false one, twice
7566    /// that for a true one. `_f` holds the weights of the conditions that
7567    /// were evaluated; `+!!_v` adds the second for each true one.
7568    fn encoded(&self, applied: &AppliedDecision) -> Expression<'a> {
7569        let mut sum = self.identifier(&applied.frame);
7570        for (index, name) in applied.values.iter().enumerate() {
7571            let truth = self.ast.expression_unary(
7572                Span::default(),
7573                UnaryOperator::UnaryPlus,
7574                self.ast.expression_unary(
7575                    Span::default(),
7576                    UnaryOperator::LogicalNot,
7577                    self.ast.expression_unary(
7578                        Span::default(),
7579                        UnaryOperator::LogicalNot,
7580                        self.identifier(name),
7581                    ),
7582                ),
7583            );
7584            let term = if index == 0 {
7585                truth
7586            } else {
7587                self.ast.expression_binary(
7588                    Span::default(),
7589                    self.number(3_u64.pow(index as u32)),
7590                    BinaryOperator::Multiplication,
7591                    truth,
7592                )
7593            };
7594            sum = self
7595                .ast
7596                .expression_binary(Span::default(), sum, BinaryOperator::Addition, term);
7597        }
7598        sum
7599    }
7600
7601    /// `mcdcEndV2(file, decision, vector, outcome)`, where the outcome is
7602    /// known from where the call stands: the branch that was taken.
7603    /// The decisions used as values in one `&&`/`||` tree, recorded once the
7604    /// tree has been evaluated: `(_f = 0, _v0 = 0, .., _r = tree, end(..),
7605    /// _r)`. The tree stays whole -- a coverage tool the tests run counts it
7606    /// as one branch, and TypeScript narrows across it as written -- and the
7607    /// assignment keeps its value and its type. Each outcome is read back
7608    /// from the decision's own temporaries with `&`, `|` and `^`, which no
7609    /// tool counts as a branch; a decision the evaluation did not reach was
7610    /// reset to nothing, and the runtime records no vector for it.
7611    fn record_value_outcomes(
7612        &mut self,
7613        expression: &mut Expression<'a>,
7614        pending: Vec<(AppliedDecision, ValueShape)>,
7615    ) {
7616        let inline = pending.iter().any(|(applied, _)| applied.inline);
7617        let result = self.scratch_for("_supercovMcdcResult", inline);
7618        let mut steps = self.ast.vec();
7619        for (applied, _) in &pending {
7620            steps.push(self.assign(&applied.frame, self.number(0)));
7621            for name in &applied.values {
7622                steps.push(self.assign(name, self.number(0)));
7623            }
7624        }
7625        let value = expression.take_in(self.ast.allocator);
7626        steps.push(self.assign(&result, value));
7627        let mut names = Vec::new();
7628        for (applied, shape) in &pending {
7629            let truth = self.ast.expression_unary(
7630                Span::default(),
7631                UnaryOperator::LogicalNot,
7632                self.ast.expression_unary(
7633                    Span::default(),
7634                    UnaryOperator::LogicalNot,
7635                    self.shape_bits(shape, applied),
7636                ),
7637            );
7638            steps.push(self.ast.expression_call(
7639                Span::default(),
7640                self.identifier(&self.mcdc_end_v2),
7641                NONE,
7642                self.ast.vec_from_array([
7643                    Argument::from(self.identifier(&self.probe_file_v2)),
7644                    Argument::from(self.number(applied.index as u64)),
7645                    Argument::from(self.encoded(applied)),
7646                    Argument::from(truth),
7647                ]),
7648                false,
7649            ));
7650            if applied.inline {
7651                names.push(applied.frame.clone());
7652                names.extend(applied.values.iter().cloned());
7653            }
7654        }
7655        steps.push(self.identifier(&result));
7656        let observed = self.ast.expression_parenthesized(
7657            Span::default(),
7658            self.ast.expression_sequence(Span::default(), steps),
7659        );
7660        *expression = if inline {
7661            names.push(result);
7662            self.wrap_inline_frame(observed, &names)
7663        } else {
7664            observed
7665        };
7666    }
7667
7668    /// A decision's outcome from its temporaries, as 0 or 1: a condition that
7669    /// was not evaluated reads 0, which is never what decided the outcome.
7670    fn shape_bits(&self, shape: &ValueShape, applied: &AppliedDecision) -> Expression<'a> {
7671        let binary = |left, operator, right| {
7672            self.ast.expression_parenthesized(
7673                Span::default(),
7674                self.ast
7675                    .expression_binary(Span::default(), left, operator, right),
7676            )
7677        };
7678        match shape {
7679            ValueShape::Condition(index) => self.ast.expression_unary(
7680                Span::default(),
7681                UnaryOperator::UnaryPlus,
7682                self.ast.expression_unary(
7683                    Span::default(),
7684                    UnaryOperator::LogicalNot,
7685                    self.ast.expression_unary(
7686                        Span::default(),
7687                        UnaryOperator::LogicalNot,
7688                        self.identifier(&applied.values[*index]),
7689                    ),
7690                ),
7691            ),
7692            ValueShape::And(left, right) => binary(
7693                self.shape_bits(left, applied),
7694                BinaryOperator::BitwiseAnd,
7695                self.shape_bits(right, applied),
7696            ),
7697            ValueShape::Or(left, right) => binary(
7698                self.shape_bits(left, applied),
7699                BinaryOperator::BitwiseOR,
7700                self.shape_bits(right, applied),
7701            ),
7702            ValueShape::Not(inner) => binary(
7703                self.number(1),
7704                BinaryOperator::BitwiseXOR,
7705                self.shape_bits(inner, applied),
7706            ),
7707        }
7708    }
7709
7710    fn outcome(&self, applied: &AppliedDecision, outcome: bool) -> Expression<'a> {
7711        self.ast.expression_call(
7712            Span::default(),
7713            self.identifier(&self.mcdc_end_v2),
7714            NONE,
7715            self.ast.vec_from_array([
7716                Argument::from(self.identifier(&self.probe_file_v2)),
7717                Argument::from(self.number(applied.index as u64)),
7718                Argument::from(self.encoded(applied)),
7719                Argument::from(
7720                    self.ast
7721                        .expression_boolean_literal(Span::default(), outcome),
7722                ),
7723            ]),
7724            false,
7725        )
7726    }
7727
7728    /// Put `first` at the head of a branch or a loop body, making it a block
7729    /// if it is a single statement.
7730    fn prepend_statement(&self, body: &mut Statement<'a>, first: Statement<'a>) {
7731        if let Statement::BlockStatement(block) = body {
7732            block.body.insert(0, first);
7733            return;
7734        }
7735        let original = body.take_in(self.ast.allocator);
7736        *body = self
7737            .ast
7738            .statement_block(Span::default(), self.ast.vec_from_array([first, original]));
7739    }
7740
7741    /// What records the true outcome where the false one is recorded after
7742    /// the statement: `mcdcEndV2(…, true), _f = 0`. The frame is at least 1
7743    /// once a test has run, so cleared it says the outcome has been recorded.
7744    fn settled(&self, applied: &AppliedDecision) -> Expression<'a> {
7745        self.ast.expression_sequence(
7746            Span::default(),
7747            self.ast.vec_from_array([
7748                self.outcome(applied, true),
7749                self.assign(&applied.frame, self.number(0)),
7750            ]),
7751        )
7752    }
7753
7754    fn when_unsettled(&self, applied: &AppliedDecision, record: Expression<'a>) -> Statement<'a> {
7755        self.ast.statement_expression(
7756            Span::default(),
7757            self.ast.expression_logical(
7758                Span::default(),
7759                self.identifier(&applied.frame),
7760                LogicalOperator::And,
7761                record,
7762            ),
7763        )
7764    }
7765
7766    /// Record the false outcome after the statement, when nothing runs for
7767    /// it inside: an `if` without an `else`, a loop its test ended.
7768    ///
7769    /// An `else` made for the call would be a branch of its own to a coverage
7770    /// tool the tests run, and the `if` would end at a brace nobody wrote:
7771    /// istanbul lost the statement and its implicit `else` with it. A `break`
7772    /// leaves a loop from its body, where the outcome is already recorded.
7773    fn record_false_after(&mut self, applied: &AppliedDecision, before: Option<Statement<'a>>) {
7774        let after = self.when_unsettled(applied, self.outcome(applied, false));
7775        self.after_statement = Some((before, after));
7776    }
7777
7778    /// A loop's true outcome is recorded where its body starts.
7779    fn loop_outcomes(
7780        &mut self,
7781        body: &mut Statement<'a>,
7782        applied: &AppliedDecision,
7783        tested_after: bool,
7784    ) {
7785        if !tested_after {
7786            let entered = self
7787                .ast
7788                .statement_expression(Span::default(), self.settled(applied));
7789            self.prepend_statement(body, entered);
7790            self.record_false_after(applied, None);
7791            return;
7792        }
7793        // A `do … while` body runs before its test: the frame says whether a
7794        // test came before this iteration. It is cleared before the loop,
7795        // because a test that threw the last time the loop ran left it set.
7796        let entered = self.when_unsettled(applied, self.settled(applied));
7797        self.prepend_statement(body, entered);
7798        let before = self
7799            .ast
7800            .statement_expression(Span::default(), self.assign(&applied.frame, self.number(0)));
7801        self.record_false_after(applied, Some(before));
7802    }
7803
7804    fn instrument_condition_v1(
7805        &self,
7806        expression: Expression<'a>,
7807        frame_name: &str,
7808        index: usize,
7809    ) -> Expression<'a> {
7810        self.ast.expression_call(
7811            Span::default(),
7812            self.identifier(&self.mcdc_condition),
7813            NONE,
7814            self.ast.vec_from_array([
7815                Argument::from(self.identifier(frame_name)),
7816                Argument::from(self.number(index as u64)),
7817                Argument::from(expression),
7818            ]),
7819            false,
7820        )
7821    }
7822
7823    fn instrument_conditions_v1(
7824        &self,
7825        expression: &mut Expression<'a>,
7826        frame_name: &str,
7827        next_index: &mut usize,
7828    ) {
7829        match expression {
7830            Expression::ParenthesizedExpression(parenthesized) => {
7831                self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
7832            }
7833            Expression::LogicalExpression(logical)
7834                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7835            {
7836                self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
7837                self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
7838            }
7839            Expression::UnaryExpression(unary)
7840                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7841            {
7842                self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
7843            }
7844            _ => {
7845                let index = *next_index;
7846                *next_index += 1;
7847                let original = expression.take_in(self.ast.allocator);
7848                *expression = self.instrument_condition_v1(original, frame_name, index);
7849            }
7850        }
7851    }
7852
7853    fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
7854        let mut condition_spans = Vec::new();
7855        collect_conditions(test, &mut condition_spans);
7856        let plan = DecisionPlan {
7857            index: self.decision_index,
7858            condition_count: condition_spans.len(),
7859            inline_frame: self.parameter_depth > 0,
7860        };
7861        self.decision_index += 1;
7862        plan
7863    }
7864
7865    /// Rewrite a decision's conditions. What records the outcome is left to
7866    /// the caller, which knows the branches; a decision too wide for the
7867    /// encoding keeps the call-based form and returns nothing.
7868    fn apply_decision(
7869        &mut self,
7870        test: &mut Expression<'a>,
7871        plan: DecisionPlan,
7872    ) -> Option<AppliedDecision> {
7873        if plan.condition_count > 32 {
7874            let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
7875            let mut next_index = 0;
7876            self.instrument_conditions_v1(test, &frame_name, &mut next_index);
7877            debug_assert_eq!(next_index, plan.condition_count);
7878
7879            let decision = &self.decisions[plan.index];
7880            let begin = self.ast.expression_call(
7881                Span::default(),
7882                self.identifier(&self.mcdc_begin),
7883                NONE,
7884                self.ast.vec_from_array([
7885                    Argument::from(self.string(&decision.id)),
7886                    Argument::from(self.decision_meta(decision)),
7887                ]),
7888                false,
7889            );
7890            let assign_frame = self.ast.expression_assignment(
7891                Span::default(),
7892                AssignmentOperator::Assign,
7893                self.assignment_target(&frame_name),
7894                begin,
7895            );
7896            let instrumented = test.take_in(self.ast.allocator);
7897            let end = self.ast.expression_call(
7898                Span::default(),
7899                self.identifier(&self.mcdc_end),
7900                NONE,
7901                self.ast.vec_from_array([
7902                    Argument::from(self.identifier(&frame_name)),
7903                    Argument::from(instrumented),
7904                ]),
7905                false,
7906            );
7907            let observed = self.ast.expression_sequence(
7908                Span::default(),
7909                self.ast.vec_from_array([assign_frame, end]),
7910            );
7911            *test = if plan.inline_frame {
7912                self.wrap_inline_frame(observed, &[frame_name])
7913            } else {
7914                observed
7915            };
7916            return None;
7917        }
7918
7919        let applied = AppliedDecision {
7920            index: plan.index,
7921            frame: self.scratch_for("_supercovMcdcFrame", plan.inline_frame),
7922            values: (0..plan.condition_count)
7923                .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
7924                .collect(),
7925            inline: plan.inline_frame,
7926        };
7927        let mut next_index = 0;
7928        self.instrument_conditions(test, &applied, &mut next_index);
7929        debug_assert_eq!(
7930            next_index,
7931            plan.condition_count,
7932            "decision at {:?}",
7933            test.span()
7934        );
7935        Some(applied)
7936    }
7937}
7938
7939impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
7940    fn visit_program(&mut self, program: &mut Program<'a>) {
7941        self.enter_declaration_scope();
7942        walk_mut::walk_program(self, program);
7943        let declarations = self.leave_declaration_scope();
7944        self.prepend_declarations(declarations, &mut program.body);
7945    }
7946
7947    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
7948        self.enter_declaration_scope();
7949        walk_mut::walk_function_body(self, body);
7950        let declarations = self.leave_declaration_scope();
7951        self.prepend_declarations(declarations, &mut body.statements);
7952    }
7953
7954    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7955        if self
7956            .source_sensitive_functions
7957            .contains(&span_key(function.span))
7958        {
7959            return;
7960        }
7961        let outer_parameter_depth = self.parameter_depth;
7962        self.parameter_depth = 0;
7963        walk_mut::walk_function(self, function, flags);
7964        self.parameter_depth = outer_parameter_depth;
7965    }
7966
7967    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7968        if self
7969            .source_sensitive_functions
7970            .contains(&span_key(function.span))
7971        {
7972            return;
7973        }
7974        let outer_parameter_depth = self.parameter_depth;
7975        self.parameter_depth = 0;
7976        walk_mut::walk_arrow_function_expression(self, function);
7977        self.parameter_depth = outer_parameter_depth;
7978    }
7979
7980    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
7981        self.parameter_depth += 1;
7982        walk_mut::walk_formal_parameters(self, parameters);
7983        self.parameter_depth -= 1;
7984    }
7985
7986    // A with body is left uninstrumented: its object environment can
7987    // intercept probe identifiers (see `enter_with_statement`).
7988    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7989
7990    // A statement whose false outcome is recorded after it becomes a block
7991    // of the two, so it can stand wherever one statement can. A label stays
7992    // on its statement, where `break` and `continue` look for it, and the
7993    // block goes around the outermost label.
7994    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
7995        let label_body = self.label_body;
7996        self.label_body = matches!(statement, Statement::LabeledStatement(_));
7997        walk_mut::walk_statement(self, statement);
7998        self.label_body = false;
7999        if label_body {
8000            return;
8001        }
8002        if let Some((before, after)) = self.after_statement.take() {
8003            let decided = statement.take_in(self.ast.allocator);
8004            let mut recorded = self.ast.vec_with_capacity(3);
8005            recorded.extend(before);
8006            recorded.push(decided);
8007            recorded.push(after);
8008            *statement = self.ast.statement_block(Span::default(), recorded);
8009        }
8010    }
8011
8012    // A static block is a scope of its own for the temporaries: inside a
8013    // parameter default there is no function body between it and the
8014    // parameter list, and a statement cannot be wrapped in an arrow.
8015    fn visit_static_block(&mut self, block: &mut oxc_ast::ast::StaticBlock<'a>) {
8016        let outer_parameter_depth = self.parameter_depth;
8017        self.parameter_depth = 0;
8018        self.enter_declaration_scope();
8019        walk_mut::walk_static_block(self, block);
8020        let declarations = self.leave_declaration_scope();
8021        self.prepend_declarations(declarations, &mut block.body);
8022        self.parameter_depth = outer_parameter_depth;
8023    }
8024
8025    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
8026        let key = span_key(expression.span());
8027        let value = self.value_decisions.get(&key).copied().map(|root| {
8028            let mut next = 0;
8029            (
8030                root,
8031                self.reserve_decision(expression),
8032                value_shape(expression, &mut next),
8033            )
8034        });
8035        let is_root = self.value_decisions.values().any(|root| *root == key);
8036        walk_mut::walk_expression(self, expression);
8037        if let Some(names) = self.inline_names.take()
8038            && matches!(expression, Expression::ConditionalExpression(_))
8039        {
8040            let conditional = expression.take_in(self.ast.allocator);
8041            *expression = self.wrap_inline_frame(conditional, &names);
8042        }
8043        if let Some((root, plan, shape)) = value
8044            && let Some(applied) = self.apply_decision(expression, plan)
8045        {
8046            self.pending_values
8047                .entry(root)
8048                .or_default()
8049                .push((applied, shape));
8050        }
8051        if is_root && let Some(pending) = self.pending_values.remove(&key) {
8052            self.record_value_outcomes(expression, pending);
8053        }
8054    }
8055
8056    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
8057        let plan = decision_outcome_is_variable(&statement.test)
8058            .then(|| self.reserve_decision(&statement.test));
8059        self.visit_expression(&mut statement.test);
8060        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8061        self.visit_statement(&mut statement.consequent);
8062        if let Some(alternate) = &mut statement.alternate {
8063            self.visit_statement(alternate);
8064        }
8065        let Some(applied) = applied else {
8066            return;
8067        };
8068        let Some(alternate) = &mut statement.alternate else {
8069            let taken = self
8070                .ast
8071                .statement_expression(Span::default(), self.settled(&applied));
8072            self.prepend_statement(&mut statement.consequent, taken);
8073            self.record_false_after(&applied, None);
8074            return;
8075        };
8076        let skipped = self
8077            .ast
8078            .statement_expression(Span::default(), self.outcome(&applied, false));
8079        self.prepend_statement(alternate, skipped);
8080        let taken = self
8081            .ast
8082            .statement_expression(Span::default(), self.outcome(&applied, true));
8083        self.prepend_statement(&mut statement.consequent, taken);
8084    }
8085
8086    fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
8087        let plan = decision_outcome_is_variable(&expression.test)
8088            .then(|| self.reserve_decision(&expression.test));
8089        self.visit_expression(&mut expression.test);
8090        let applied = plan.and_then(|plan| self.apply_decision(&mut expression.test, plan));
8091        self.visit_expression(&mut expression.consequent);
8092        self.visit_expression(&mut expression.alternate);
8093        let Some(applied) = applied else {
8094            return;
8095        };
8096        // A coverage tool the tests run reads where a branch starts from the
8097        // source map, and the printer maps neither a sequence nor a call:
8098        // the branch started at whatever token came before it. `void` is
8099        // mapped, and here it is mapped to the branch's own start.
8100        for (branch, outcome) in [
8101            (&mut expression.consequent, true),
8102            (&mut expression.alternate, false),
8103        ] {
8104            let value = branch.take_in(self.ast.allocator);
8105            let start = value.span().start;
8106            let record = self.ast.expression_unary(
8107                Span::new(start, start + 1),
8108                UnaryOperator::Void,
8109                self.outcome(&applied, outcome),
8110            );
8111            *branch = self
8112                .ast
8113                .expression_sequence(Span::default(), self.ast.vec_from_array([record, value]));
8114        }
8115        if applied.inline {
8116            let mut names = vec![applied.frame];
8117            names.extend(applied.values);
8118            self.inline_names = Some(names);
8119        }
8120    }
8121
8122    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
8123        let plan = decision_outcome_is_variable(&statement.test)
8124            .then(|| self.reserve_decision(&statement.test));
8125        self.visit_expression(&mut statement.test);
8126        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8127        self.visit_statement(&mut statement.body);
8128        if let Some(applied) = applied {
8129            self.loop_outcomes(&mut statement.body, &applied, false);
8130        }
8131    }
8132
8133    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
8134        let plan = decision_outcome_is_variable(&statement.test)
8135            .then(|| self.reserve_decision(&statement.test));
8136        self.visit_statement(&mut statement.body);
8137        self.visit_expression(&mut statement.test);
8138        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8139        if let Some(applied) = applied {
8140            self.loop_outcomes(&mut statement.body, &applied, true);
8141        }
8142    }
8143
8144    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
8145        let plan = statement
8146            .test
8147            .as_ref()
8148            .filter(|test| decision_outcome_is_variable(test))
8149            .map(|test| self.reserve_decision(test));
8150        if let Some(init) = &mut statement.init {
8151            self.visit_for_statement_init(init);
8152        }
8153        let mut applied = None;
8154        if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
8155            self.visit_expression(test);
8156            applied = self.apply_decision(test, plan);
8157        }
8158        if let Some(update) = &mut statement.update {
8159            self.visit_expression(update);
8160        }
8161        self.visit_statement(&mut statement.body);
8162        if let Some(applied) = applied {
8163            self.loop_outcomes(&mut statement.body, &applied, false);
8164        }
8165    }
8166}
8167
8168struct DecisionCollector<'s> {
8169    source: &'s str,
8170    file: &'s str,
8171    decisions: Vec<CandidateDecision>,
8172    decision_vector_counts: Vec<usize>,
8173    decision_logical_nodes: HashSet<SpanKey>,
8174    source_sensitive_functions: &'s HashSet<SpanKey>,
8175    /// The expressions recorded as decisions where they are used as values,
8176    /// for the transformer to find by span, each with the root of the `&&`/`||`
8177    /// tree it sits in: the outcome is recorded once that root has been
8178    /// evaluated, so the tree stays one tree.
8179    value_decisions: HashMap<SpanKey, SpanKey>,
8180    /// The root of the `&&`/`||`/`??` tree being visited, through parentheses.
8181    logical_root: Option<SpanKey>,
8182    /// The operators inside a decision used as a value. They stay in their
8183    /// value tree -- its branches and the one tree a coverage tool reads --
8184    /// and only stop a decision being found inside the decision.
8185    value_logical_nodes: HashSet<SpanKey>,
8186}
8187
8188impl DecisionCollector<'_> {
8189    fn in_a_decision(&self, span: Span) -> bool {
8190        let key = span_key(span);
8191        self.decision_logical_nodes.contains(&key) || self.value_logical_nodes.contains(&key)
8192    }
8193
8194    fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
8195        let mut condition_spans = Vec::new();
8196        collect_conditions(test, &mut condition_spans);
8197        if kind == "value" {
8198            collect_decision_logical_nodes(test, &mut self.value_logical_nodes);
8199        } else {
8200            collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
8201        }
8202        // Babel treats redundant parentheses as parser metadata rather than
8203        // part of the decision node. oxc preserves a ParenthesizedExpression,
8204        // so normalize it here to keep locations and stable IDs parser-neutral.
8205        let span = transparent_expression(test).span();
8206        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8207        self.decisions.push(CandidateDecision {
8208            id: stable_id(self.source, self.file, "decision", span, kind),
8209            file: self.file.to_string(),
8210            line,
8211            column,
8212            source: source_slice(self.source, span).to_string(),
8213            conditions: condition_spans
8214                .iter()
8215                .map(|condition| source_slice(self.source, *condition).to_string())
8216                .collect(),
8217            kind: kind.to_string(),
8218        });
8219        self.decision_vector_counts.push(
8220            if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
8221                reachable_vector_count(test, &condition_spans)
8222            } else {
8223                0
8224            },
8225        );
8226    }
8227}
8228
8229/// The `&&` or `||` under any parentheses and `!`s: `!!(a && b)` is the
8230/// decision `a && b` negated twice.
8231fn compound_root<'e, 'a>(expression: &'e Expression<'a>) -> Option<&'e LogicalExpression<'a>> {
8232    match expression {
8233        Expression::ParenthesizedExpression(parenthesized) => {
8234            compound_root(&parenthesized.expression)
8235        }
8236        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8237            compound_root(&unary.argument)
8238        }
8239        Expression::LogicalExpression(logical)
8240            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8241        {
8242            Some(logical)
8243        }
8244        _ => None,
8245    }
8246}
8247
8248/// MC/DC applies only when a decision can produce both Boolean outcomes.
8249/// This deliberately recognizes only semantics that are provable from syntax;
8250/// it never evaluates user code or assumes values for identifiers/calls.
8251fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
8252    syntactic_boolean_outcome(expression).is_none()
8253}
8254
8255fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
8256    match transparent_expression(expression) {
8257        Expression::BooleanLiteral(literal) => Some(literal.value),
8258        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8259            syntactic_boolean_outcome(&unary.argument).map(|value| !value)
8260        }
8261        Expression::LogicalExpression(logical)
8262            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8263        {
8264            let left = syntactic_boolean_outcome(&logical.left);
8265            let right = syntactic_boolean_outcome(&logical.right);
8266            match logical.operator {
8267                LogicalOperator::And => match (left, right) {
8268                    (Some(false), _) | (_, Some(false)) => Some(false),
8269                    (Some(true), value) | (value, Some(true)) => value,
8270                    _ => None,
8271                },
8272                LogicalOperator::Or => match (left, right) {
8273                    (Some(true), _) | (_, Some(true)) => Some(true),
8274                    (Some(false), value) | (value, Some(false)) => value,
8275                    _ => None,
8276                },
8277                LogicalOperator::Coalesce => None,
8278            }
8279        }
8280        _ => None,
8281    }
8282}
8283
8284#[derive(Default)]
8285struct CoverageSurfaceScanner {
8286    found: bool,
8287}
8288
8289impl<'a> Visit<'a> for CoverageSurfaceScanner {
8290    fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
8291        self.found = true;
8292    }
8293
8294    fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
8295        self.found = true;
8296    }
8297
8298    fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
8299        self.found = true;
8300    }
8301
8302    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
8303        self.found = true;
8304    }
8305
8306    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
8307        self.found = true;
8308    }
8309
8310    fn visit_class(&mut self, _class: &Class<'a>) {
8311        self.found = true;
8312    }
8313
8314    fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
8315        if expression.operator.is_logical() {
8316            self.found = true;
8317        } else {
8318            walk::walk_assignment_expression(self, expression);
8319        }
8320    }
8321}
8322
8323fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
8324    fn visit_condition(expression: &Expression<'_>) -> bool {
8325        let mut scanner = CoverageSurfaceScanner::default();
8326        scanner.visit_expression(expression);
8327        !scanner.found
8328    }
8329    match transparent_expression(expression) {
8330        Expression::LogicalExpression(logical)
8331            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8332        {
8333            decision_conditions_are_transparent(&logical.left)
8334                && decision_conditions_are_transparent(&logical.right)
8335        }
8336        Expression::UnaryExpression(unary)
8337            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8338        {
8339            decision_conditions_are_transparent(&unary.argument)
8340        }
8341        condition => visit_condition(condition),
8342    }
8343}
8344
8345fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
8346    fn evaluate(
8347        expression: &Expression<'_>,
8348        assignment: usize,
8349        encoded: &mut usize,
8350        indices: &HashMap<SpanKey, usize>,
8351    ) -> bool {
8352        match transparent_expression(expression) {
8353            Expression::LogicalExpression(logical)
8354                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8355            {
8356                let left = evaluate(&logical.left, assignment, encoded, indices);
8357                if logical.operator == LogicalOperator::And {
8358                    left && evaluate(&logical.right, assignment, encoded, indices)
8359                } else {
8360                    left || evaluate(&logical.right, assignment, encoded, indices)
8361                }
8362            }
8363            Expression::UnaryExpression(unary)
8364                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8365            {
8366                !evaluate(&unary.argument, assignment, encoded, indices)
8367            }
8368            condition => {
8369                let index = indices
8370                    .get(&span_key(condition.span()))
8371                    .expect("decision condition index must remain stable");
8372                let value = assignment & (1 << index) != 0;
8373                *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
8374                value
8375            }
8376        }
8377    }
8378
8379    let indices = conditions
8380        .iter()
8381        .enumerate()
8382        .map(|(index, span)| (span_key(*span), index))
8383        .collect::<HashMap<_, _>>();
8384    let mut vectors = HashSet::new();
8385    for assignment in 0..(1_usize << conditions.len()) {
8386        let mut encoded = 0;
8387        let outcome = evaluate(expression, assignment, &mut encoded, &indices);
8388        vectors.insert(encoded * 2 + usize::from(outcome));
8389    }
8390    vectors.len()
8391}
8392
8393fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
8394    match expression {
8395        Expression::ParenthesizedExpression(parenthesized) => {
8396            collect_decision_logical_nodes(&parenthesized.expression, nodes);
8397        }
8398        Expression::LogicalExpression(logical)
8399            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8400        {
8401            nodes.insert(span_key(logical.span));
8402            collect_decision_logical_nodes(&logical.left, nodes);
8403            collect_decision_logical_nodes(&logical.right, nodes);
8404        }
8405        Expression::UnaryExpression(unary)
8406            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8407        {
8408            collect_decision_logical_nodes(&unary.argument, nodes);
8409        }
8410        _ => {}
8411    }
8412}
8413
8414#[derive(Default)]
8415struct LogicalBranchAnalysis {
8416    branches: Vec<CandidateBranch>,
8417    logical_targets: HashMap<SpanKey, (String, String)>,
8418}
8419
8420#[derive(Default)]
8421struct LogicalAssignmentAnalysis {
8422    branches: Vec<CandidateBranch>,
8423    targets: HashMap<SpanKey, (String, String)>,
8424}
8425
8426#[derive(Default)]
8427struct OptionalMemberAnalysis {
8428    branches: Vec<CandidateBranch>,
8429    targets: HashMap<SpanKey, (String, String)>,
8430    /// Each target's place in its chain, as `optionalSelectV2` reads it.
8431    links: HashMap<SpanKey, u8>,
8432    /// The optional call a target's object holds, for a target right after
8433    /// one: whether that call went ahead says whether the target ran.
8434    after_call: HashMap<SpanKey, SpanKey>,
8435}
8436
8437/// An optional link of the same chain runs before this one.
8438const LINK_AFTER_ANOTHER: u8 = 1;
8439/// An optional link of the same chain runs after this one.
8440const LINK_BEFORE_ANOTHER: u8 = 2;
8441/// The link before this one is an optional call.
8442const LINK_AFTER_CALL: u8 = 4;
8443
8444/// The optional link of the same chain an optional member's object holds.
8445enum LinkBelow {
8446    None,
8447    Member,
8448    Call(SpanKey),
8449}
8450
8451/// The earlier optional link of the same chain that `object`, the object of
8452/// an optional member, holds: through plain member accesses, plain calls and
8453/// `!`, which the chain's short-circuit skips along with the rest.
8454/// Parentheses end the chain.
8455fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
8456    let mut current = object;
8457    loop {
8458        current = match current {
8459            Expression::StaticMemberExpression(member) if member.optional => {
8460                return LinkBelow::Member;
8461            }
8462            Expression::ComputedMemberExpression(member) if member.optional => {
8463                return LinkBelow::Member;
8464            }
8465            Expression::PrivateFieldExpression(member) if member.optional => {
8466                return LinkBelow::Member;
8467            }
8468            Expression::CallExpression(call) if call.optional => {
8469                return LinkBelow::Call(span_key(call.span));
8470            }
8471            Expression::StaticMemberExpression(member) => &member.object,
8472            Expression::ComputedMemberExpression(member) => &member.object,
8473            Expression::PrivateFieldExpression(member) => &member.object,
8474            Expression::CallExpression(call) => &call.callee,
8475            Expression::TSNonNullExpression(inner) => &inner.expression,
8476            _ => return LinkBelow::None,
8477        };
8478    }
8479}
8480
8481/// The same walk outward: whether this member is the object, through the
8482/// same plain links, of a later optional member of its chain.
8483fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
8484    for ancestor in context.ancestors() {
8485        match ancestor {
8486            Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
8487            Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
8488            Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
8489            Ancestor::StaticMemberExpressionObject(_)
8490            | Ancestor::ComputedMemberExpressionObject(_)
8491            | Ancestor::PrivateFieldExpressionObject(_)
8492            | Ancestor::TSNonNullExpressionExpression(_) => {}
8493            Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
8494            _ => return false,
8495        }
8496    }
8497    false
8498}
8499
8500#[derive(Default)]
8501struct OptionalCallAnalysis {
8502    branches: Vec<CandidateBranch>,
8503    sites: HashMap<SpanKey, (String, String)>,
8504    roots: HashMap<SpanKey, Vec<SpanKey>>,
8505    limitations: Vec<CandidateLimitation>,
8506}
8507
8508#[derive(Clone)]
8509struct DefaultTarget {
8510    default_id: String,
8511    provided_id: String,
8512    inferred_name: Option<String>,
8513}
8514
8515#[derive(Default)]
8516struct DefaultAnalysis {
8517    branches: Vec<CandidateBranch>,
8518    parameter_targets: HashMap<SpanKey, DefaultTarget>,
8519    binding_targets: HashMap<SpanKey, DefaultTarget>,
8520    limitations: Vec<CandidateLimitation>,
8521}
8522
8523#[derive(Clone)]
8524struct ExtendedTarget {
8525    first_id: String,
8526    second_id: String,
8527}
8528
8529#[derive(Default)]
8530struct ExtendedAnalysis {
8531    branches: Vec<CandidateBranch>,
8532    try_targets: HashMap<SpanKey, ExtendedTarget>,
8533    loop_targets: HashMap<SpanKey, ExtendedTarget>,
8534}
8535
8536#[derive(Clone)]
8537struct SwitchTarget {
8538    case_ids: Vec<String>,
8539    no_match_id: Option<String>,
8540}
8541
8542#[derive(Default)]
8543struct SwitchAnalysis {
8544    branches: Vec<CandidateBranch>,
8545    targets: HashMap<SpanKey, SwitchTarget>,
8546}
8547
8548struct SwitchCollector<'s> {
8549    source: &'s str,
8550    file: &'s str,
8551    source_sensitive_functions: &'s HashSet<SpanKey>,
8552    unsafe_function_depth: usize,
8553    with_depth: usize,
8554    analysis: SwitchAnalysis,
8555}
8556
8557impl SwitchCollector<'_> {
8558    fn unsafe_context(&self) -> bool {
8559        self.unsafe_function_depth > 0 || self.with_depth > 0
8560    }
8561
8562    fn exit_source_function(&mut self, span: Span) {
8563        if self.source_sensitive_functions.contains(&span_key(span)) {
8564            self.unsafe_function_depth -= 1;
8565        }
8566    }
8567}
8568
8569impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
8570    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8571        if self
8572            .source_sensitive_functions
8573            .contains(&span_key(node.span))
8574        {
8575            self.unsafe_function_depth += 1;
8576        }
8577    }
8578
8579    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8580        self.exit_source_function(node.span);
8581    }
8582
8583    fn enter_arrow_function_expression(
8584        &mut self,
8585        node: &mut ArrowFunctionExpression<'a>,
8586        _context: &mut TraverseCtx<'a, ()>,
8587    ) {
8588        if self
8589            .source_sensitive_functions
8590            .contains(&span_key(node.span))
8591        {
8592            self.unsafe_function_depth += 1;
8593        }
8594    }
8595
8596    fn exit_arrow_function_expression(
8597        &mut self,
8598        node: &mut ArrowFunctionExpression<'a>,
8599        _context: &mut TraverseCtx<'a, ()>,
8600    ) {
8601        self.exit_source_function(node.span);
8602    }
8603
8604    fn enter_with_statement(
8605        &mut self,
8606        _node: &mut WithStatement<'a>,
8607        _context: &mut TraverseCtx<'a, ()>,
8608    ) {
8609        self.with_depth += 1;
8610    }
8611
8612    fn exit_with_statement(
8613        &mut self,
8614        _node: &mut WithStatement<'a>,
8615        _context: &mut TraverseCtx<'a, ()>,
8616    ) {
8617        self.with_depth -= 1;
8618    }
8619
8620    fn enter_switch_statement(
8621        &mut self,
8622        node: &mut SwitchStatement<'a>,
8623        _context: &mut TraverseCtx<'a, ()>,
8624    ) {
8625        let has_default = node.cases.iter().any(|case| case.test.is_none());
8626        if self.unsafe_context() {
8627            return;
8628        }
8629        let id = stable_id(self.source, self.file, "switch", node.span, "");
8630        let mut case_ids = Vec::with_capacity(node.cases.len());
8631        let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
8632        for (index, case) in node.cases.iter().enumerate() {
8633            let alternative_id = format!("{id}:case:{index}");
8634            let label = case.test.as_ref().map_or_else(
8635                || "default".to_string(),
8636                |test| format!("case {}", source_slice(self.source, test.span())),
8637            );
8638            case_ids.push(alternative_id.clone());
8639            alternatives.push(CandidateBranchAlternative {
8640                id: alternative_id,
8641                label,
8642            });
8643        }
8644        let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
8645        if let Some(no_match_id) = &no_match_id {
8646            alternatives.push(CandidateBranchAlternative {
8647                id: no_match_id.clone(),
8648                label: "no matching case".to_string(),
8649            });
8650        }
8651        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8652        self.analysis.branches.push(CandidateBranch {
8653            id,
8654            kind: "switch".to_string(),
8655            file: self.file.to_string(),
8656            line,
8657            column,
8658            source: source_slice(self.source, node.discriminant.span()).to_string(),
8659            alternatives,
8660        });
8661        self.analysis.targets.insert(
8662            span_key(node.span),
8663            SwitchTarget {
8664                case_ids,
8665                no_match_id,
8666            },
8667        );
8668    }
8669}
8670
8671fn collect_switch_branches<'a>(
8672    allocator: &'a Allocator,
8673    program: &mut Program<'a>,
8674    source: &str,
8675    file: &str,
8676    source_sensitive_functions: &HashSet<SpanKey>,
8677) -> SwitchAnalysis {
8678    let mut collector = SwitchCollector {
8679        source,
8680        file,
8681        source_sensitive_functions,
8682        unsafe_function_depth: 0,
8683        with_depth: 0,
8684        analysis: SwitchAnalysis::default(),
8685    };
8686    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8687    collector.analysis
8688}
8689
8690struct ExtendedCollector<'s> {
8691    source: &'s str,
8692    file: &'s str,
8693    source_sensitive_functions: &'s HashSet<SpanKey>,
8694    entered_loops: HashSet<SpanKey>,
8695    unsafe_function_depth: usize,
8696    with_depth: usize,
8697    analysis: ExtendedAnalysis,
8698}
8699
8700impl ExtendedCollector<'_> {
8701    fn unsafe_context(&self) -> bool {
8702        self.unsafe_function_depth > 0 || self.with_depth > 0
8703    }
8704
8705    fn enter_try(&mut self, node: &TryStatement<'_>) {
8706        let transformed = !self.unsafe_context() && node.handler.is_some();
8707        if !transformed {
8708            return;
8709        }
8710        let id = stable_id(self.source, self.file, "try-catch", node.span, "");
8711        let success_id = format!("{id}:success");
8712        let catch_id = format!("{id}:catch");
8713        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8714        self.analysis.branches.push(CandidateBranch {
8715            id,
8716            kind: "try-catch".to_string(),
8717            file: self.file.to_string(),
8718            line,
8719            column,
8720            source: "try / catch".to_string(),
8721            alternatives: vec![
8722                CandidateBranchAlternative {
8723                    id: success_id.clone(),
8724                    label: "try completed without catch".to_string(),
8725                },
8726                CandidateBranchAlternative {
8727                    id: catch_id.clone(),
8728                    label: "catch entered".to_string(),
8729                },
8730            ],
8731        });
8732        self.analysis.try_targets.insert(
8733            span_key(node.span),
8734            ExtendedTarget {
8735                first_id: success_id,
8736                second_id: catch_id,
8737            },
8738        );
8739    }
8740
8741    fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
8742        let transformed = !self.unsafe_context();
8743        if !transformed {
8744            return;
8745        }
8746        // One feasible outcome is not a branch. Recording it as one leaves a
8747        // permanently unreachable obligation, which is how a project ends up
8748        // below 100% with nothing to do about it.
8749        if self.entered_loops.contains(&span_key(span)) {
8750            return;
8751        }
8752        let id = stable_id(self.source, self.file, kind, span, "");
8753        let zero_id = format!("{id}:zero");
8754        let entered_id = format!("{id}:entered");
8755        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8756        self.analysis.branches.push(CandidateBranch {
8757            id,
8758            kind: kind.to_string(),
8759            file: self.file.to_string(),
8760            line,
8761            column,
8762            source: source_slice(self.source, right.span()).to_string(),
8763            alternatives: vec![
8764                CandidateBranchAlternative {
8765                    id: zero_id.clone(),
8766                    label: "zero iterations".to_string(),
8767                },
8768                CandidateBranchAlternative {
8769                    id: entered_id.clone(),
8770                    label: "one or more iterations".to_string(),
8771                },
8772            ],
8773        });
8774        self.analysis.loop_targets.insert(
8775            span_key(span),
8776            ExtendedTarget {
8777                first_id: zero_id,
8778                second_id: entered_id,
8779            },
8780        );
8781    }
8782
8783    fn exit_source_function(&mut self, span: Span) {
8784        if self.source_sensitive_functions.contains(&span_key(span)) {
8785            self.unsafe_function_depth -= 1;
8786        }
8787    }
8788}
8789
8790impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
8791    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8792        if self
8793            .source_sensitive_functions
8794            .contains(&span_key(node.span))
8795        {
8796            self.unsafe_function_depth += 1;
8797        }
8798    }
8799
8800    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8801        self.exit_source_function(node.span);
8802    }
8803
8804    fn enter_arrow_function_expression(
8805        &mut self,
8806        node: &mut ArrowFunctionExpression<'a>,
8807        _context: &mut TraverseCtx<'a, ()>,
8808    ) {
8809        if self
8810            .source_sensitive_functions
8811            .contains(&span_key(node.span))
8812        {
8813            self.unsafe_function_depth += 1;
8814        }
8815    }
8816
8817    fn exit_arrow_function_expression(
8818        &mut self,
8819        node: &mut ArrowFunctionExpression<'a>,
8820        _context: &mut TraverseCtx<'a, ()>,
8821    ) {
8822        self.exit_source_function(node.span);
8823    }
8824
8825    fn enter_with_statement(
8826        &mut self,
8827        _node: &mut WithStatement<'a>,
8828        _context: &mut TraverseCtx<'a, ()>,
8829    ) {
8830        self.with_depth += 1;
8831    }
8832
8833    fn exit_with_statement(
8834        &mut self,
8835        _node: &mut WithStatement<'a>,
8836        _context: &mut TraverseCtx<'a, ()>,
8837    ) {
8838        self.with_depth -= 1;
8839    }
8840
8841    fn enter_try_statement(
8842        &mut self,
8843        node: &mut TryStatement<'a>,
8844        _context: &mut TraverseCtx<'a, ()>,
8845    ) {
8846        self.enter_try(node);
8847    }
8848
8849    fn exit_try_statement(
8850        &mut self,
8851        _node: &mut TryStatement<'a>,
8852        _context: &mut TraverseCtx<'a, ()>,
8853    ) {
8854    }
8855
8856    fn enter_for_in_statement(
8857        &mut self,
8858        node: &mut ForInStatement<'a>,
8859        _context: &mut TraverseCtx<'a, ()>,
8860    ) {
8861        self.enter_loop(node.span, &node.right, "for-in");
8862    }
8863
8864    fn exit_for_in_statement(
8865        &mut self,
8866        _node: &mut ForInStatement<'a>,
8867        _context: &mut TraverseCtx<'a, ()>,
8868    ) {
8869    }
8870
8871    fn enter_for_of_statement(
8872        &mut self,
8873        node: &mut ForOfStatement<'a>,
8874        _context: &mut TraverseCtx<'a, ()>,
8875    ) {
8876        self.enter_loop(node.span, &node.right, "for-of");
8877    }
8878
8879    fn exit_for_of_statement(
8880        &mut self,
8881        _node: &mut ForOfStatement<'a>,
8882        _context: &mut TraverseCtx<'a, ()>,
8883    ) {
8884    }
8885}
8886
8887/// Loops that must run at least once.
8888///
8889/// A `for...of` over a non-empty array literal has no zero-iteration outcome
8890/// for a test to reach, so asking for one is asking for something impossible.
8891/// The author's only ways to satisfy it are to fake a test or delete the loop,
8892/// and a coverage tool should ask for neither.
8893///
8894/// This is decided from the syntax tree and symbol resolution alone. It stays
8895/// there deliberately: a type would answer more questions, but a type can be
8896/// asserted (`as Foo`) or optimistic (`xs[0]` is `T`, and `[][0]` is
8897/// `undefined`), and an obligation dropped on a wrong answer is a branch that
8898/// runs and is never reported again. Over-asking is recoverable; this is not.
8899fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
8900    let semantic = SemanticBuilder::new().build(program).semantic;
8901    let mut found = HashSet::new();
8902    for node in semantic.nodes() {
8903        if let AstKind::ForOfStatement(loop_node) = node.kind()
8904            && never_empty(&loop_node.right, &semantic)
8905        {
8906            found.insert(span_key(loop_node.span));
8907        }
8908    }
8909    found
8910}
8911
8912/// Whether an iterable cannot be empty: a non-empty array literal, or a
8913/// `const` bound to one that nothing else in the file can reach.
8914fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
8915    match transparent_expression(expression) {
8916        Expression::ArrayExpression(array) => has_definite_element(array),
8917        Expression::Identifier(identifier) => {
8918            let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
8919                return false;
8920            };
8921            // Nothing but this loop may reach the binding. A shrinking method
8922            // (`pop`, `splice`, `length = 0`) is an ordinary read of the
8923            // identifier, so counting references is what makes this sound --
8924            // recognising a list of mutating methods would not be.
8925            if semantic.scoping().get_resolved_references(symbol).count() != 1 {
8926                return false;
8927            }
8928            let declaration = semantic.scoping().symbol_declaration(symbol);
8929            let AstKind::VariableDeclarator(declarator) =
8930                semantic.nodes().get_node(declaration).kind()
8931            else {
8932                return false;
8933            };
8934            declarator.kind == VariableDeclarationKind::Const
8935                && declarator.init.as_ref().is_some_and(|init| {
8936                    matches!(transparent_expression(init), Expression::ArrayExpression(array)
8937                        if has_definite_element(array))
8938                })
8939        }
8940        _ => false,
8941    }
8942}
8943
8944/// An element that is certainly there. A spread may contribute nothing, so
8945/// `[...xs]` is not known to be non-empty; a hole is, since `[,]` has length 1
8946/// and yields once.
8947fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
8948    array
8949        .elements
8950        .iter()
8951        .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
8952}
8953
8954fn collect_extended_branches<'a>(
8955    allocator: &'a Allocator,
8956    program: &mut Program<'a>,
8957    source: &str,
8958    file: &str,
8959    source_sensitive_functions: &HashSet<SpanKey>,
8960) -> ExtendedAnalysis {
8961    let entered_loops = provably_entered_loops(program);
8962    let mut collector = ExtendedCollector {
8963        source,
8964        file,
8965        source_sensitive_functions,
8966        entered_loops,
8967        unsafe_function_depth: 0,
8968        with_depth: 0,
8969        analysis: ExtendedAnalysis::default(),
8970    };
8971    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8972    collector.analysis
8973}
8974
8975struct DefaultCollector<'s> {
8976    source: &'s str,
8977    file: &'s str,
8978    source_sensitive_functions: &'s HashSet<SpanKey>,
8979    unsafe_function_depth: usize,
8980    with_depth: usize,
8981    analysis: DefaultAnalysis,
8982}
8983
8984impl DefaultCollector<'_> {
8985    fn unsafe_context(&self) -> bool {
8986        self.unsafe_function_depth > 0 || self.with_depth > 0
8987    }
8988
8989    fn target(
8990        &mut self,
8991        span: Span,
8992        left: &BindingPattern<'_>,
8993        right: &Expression<'_>,
8994    ) -> DefaultTarget {
8995        let id = stable_id(self.source, self.file, "default-value", span, "");
8996        let default_id = format!("{id}:default");
8997        let provided_id = format!("{id}:provided");
8998        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8999        self.analysis.branches.push(CandidateBranch {
9000            id,
9001            kind: "default-value".to_string(),
9002            file: self.file.to_string(),
9003            line,
9004            column,
9005            source: source_slice(self.source, span).to_string(),
9006            alternatives: vec![
9007                CandidateBranchAlternative {
9008                    id: default_id.clone(),
9009                    label: "default evaluated".to_string(),
9010                },
9011                CandidateBranchAlternative {
9012                    id: provided_id.clone(),
9013                    label: "value provided".to_string(),
9014                },
9015            ],
9016        });
9017        DefaultTarget {
9018            default_id,
9019            provided_id,
9020            inferred_name: binding_identifier_name(left)
9021                .filter(|_| expression_is_anonymous_definition(right)),
9022        }
9023    }
9024
9025    fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
9026        match pattern {
9027            BindingPattern::AssignmentPattern(assignment) => {
9028                let target = self.target(assignment.span, &assignment.left, &assignment.right);
9029                if parameter {
9030                    self.analysis
9031                        .parameter_targets
9032                        .insert(span_key(assignment.span), target);
9033                } else {
9034                    self.analysis
9035                        .binding_targets
9036                        .insert(span_key(assignment.span), target);
9037                }
9038                self.collect_binding_pattern(&assignment.left, parameter);
9039            }
9040            BindingPattern::ObjectPattern(object) => {
9041                for property in &object.properties {
9042                    self.collect_binding_pattern(&property.value, parameter);
9043                }
9044                if let Some(rest) = &object.rest {
9045                    self.collect_binding_pattern(&rest.argument, parameter);
9046                }
9047            }
9048            BindingPattern::ArrayPattern(array) => {
9049                for element in array.elements.iter().flatten() {
9050                    self.collect_binding_pattern(element, parameter);
9051                }
9052                if let Some(rest) = &array.rest {
9053                    self.collect_binding_pattern(&rest.argument, parameter);
9054                }
9055            }
9056            BindingPattern::BindingIdentifier(_) => {}
9057        }
9058    }
9059
9060    fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
9061        for parameter in &parameters.items {
9062            if let Some(initializer) = &parameter.initializer {
9063                // Babel models a formal-parameter default as an AssignmentPattern whose
9064                // source range starts at the binding and ends at the initializer. oxc
9065                // keeps the initializer beside the BindingPattern and, for a TypeScript
9066                // parameter property, includes access/readonly modifiers in
9067                // FormalParameter::span. Keep the outer span as the transformation key,
9068                // but publish the same semantic default-expression range as Babel.
9069                let default_span =
9070                    Span::new(parameter.pattern.span().start, initializer.span().end);
9071                let target = self.target(default_span, &parameter.pattern, initializer);
9072                self.analysis
9073                    .parameter_targets
9074                    .insert(span_key(parameter.span), target);
9075            }
9076            self.collect_binding_pattern(&parameter.pattern, true);
9077        }
9078        if let Some(rest) = &parameters.rest {
9079            self.collect_binding_pattern(&rest.rest.argument, true);
9080        }
9081    }
9082
9083    fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
9084        match pattern {
9085            BindingPattern::AssignmentPattern(_) => true,
9086            BindingPattern::ObjectPattern(object) => {
9087                object
9088                    .properties
9089                    .iter()
9090                    .any(|property| Self::has_binding_default(&property.value))
9091                    || object
9092                        .rest
9093                        .as_ref()
9094                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9095            }
9096            BindingPattern::ArrayPattern(array) => {
9097                array
9098                    .elements
9099                    .iter()
9100                    .flatten()
9101                    .any(Self::has_binding_default)
9102                    || array
9103                        .rest
9104                        .as_ref()
9105                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9106            }
9107            BindingPattern::BindingIdentifier(_) => false,
9108        }
9109    }
9110
9111    fn exit_source_function(&mut self, span: Span) {
9112        if self.source_sensitive_functions.contains(&span_key(span)) {
9113            self.unsafe_function_depth -= 1;
9114        }
9115    }
9116}
9117
9118impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
9119    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9120        if self
9121            .source_sensitive_functions
9122            .contains(&span_key(node.span))
9123        {
9124            self.unsafe_function_depth += 1;
9125            return;
9126        }
9127        if !self.unsafe_context() && node.body.is_some() {
9128            self.collect_parameters(&node.params);
9129        }
9130    }
9131
9132    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9133        self.exit_source_function(node.span);
9134    }
9135
9136    fn enter_arrow_function_expression(
9137        &mut self,
9138        node: &mut ArrowFunctionExpression<'a>,
9139        _context: &mut TraverseCtx<'a, ()>,
9140    ) {
9141        if self
9142            .source_sensitive_functions
9143            .contains(&span_key(node.span))
9144        {
9145            self.unsafe_function_depth += 1;
9146            return;
9147        }
9148        if !self.unsafe_context() {
9149            self.collect_parameters(&node.params);
9150        }
9151    }
9152
9153    fn exit_arrow_function_expression(
9154        &mut self,
9155        node: &mut ArrowFunctionExpression<'a>,
9156        _context: &mut TraverseCtx<'a, ()>,
9157    ) {
9158        self.exit_source_function(node.span);
9159    }
9160
9161    fn enter_with_statement(
9162        &mut self,
9163        _node: &mut WithStatement<'a>,
9164        _context: &mut TraverseCtx<'a, ()>,
9165    ) {
9166        self.with_depth += 1;
9167    }
9168
9169    fn exit_with_statement(
9170        &mut self,
9171        _node: &mut WithStatement<'a>,
9172        _context: &mut TraverseCtx<'a, ()>,
9173    ) {
9174        self.with_depth -= 1;
9175    }
9176
9177    fn enter_variable_declaration(
9178        &mut self,
9179        node: &mut VariableDeclaration<'a>,
9180        context: &mut TraverseCtx<'a, ()>,
9181    ) {
9182        if self.unsafe_context() {
9183            return;
9184        }
9185        let has_default = node
9186            .declarations
9187            .iter()
9188            .any(|declaration| Self::has_binding_default(&declaration.id));
9189        if !has_default {
9190            return;
9191        }
9192        if matches!(
9193            context.ancestors().next(),
9194            Some(Ancestor::ForStatementInit(_))
9195        ) {
9196            let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9197            self.analysis.limitations.push(CandidateLimitation {
9198                id: stable_id(
9199                    self.source,
9200                    self.file,
9201                    "dynamic-code",
9202                    node.span,
9203                    "for-init-default",
9204                ),
9205                kind: "dynamic-code".to_string(),
9206                file: self.file.to_string(),
9207                line,
9208                column,
9209                source: source_slice(self.source, node.span).to_string(),
9210                reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
9211            });
9212            return;
9213        }
9214        for declaration in &node.declarations {
9215            self.collect_binding_pattern(&declaration.id, false);
9216        }
9217    }
9218}
9219
9220fn collect_default_branches<'a>(
9221    allocator: &'a Allocator,
9222    program: &mut Program<'a>,
9223    source: &str,
9224    file: &str,
9225    source_sensitive_functions: &HashSet<SpanKey>,
9226) -> DefaultAnalysis {
9227    let mut collector = DefaultCollector {
9228        source,
9229        file,
9230        source_sensitive_functions,
9231        unsafe_function_depth: 0,
9232        with_depth: 0,
9233        analysis: DefaultAnalysis::default(),
9234    };
9235    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9236    collector.analysis
9237}
9238
9239struct OptionalCallCollector<'s> {
9240    source: &'s str,
9241    file: &'s str,
9242    source_sensitive_functions: &'s HashSet<SpanKey>,
9243    unsafe_function_depth: usize,
9244    with_depth: usize,
9245    chain_roots: Vec<SpanKey>,
9246    analysis: OptionalCallAnalysis,
9247}
9248
9249impl OptionalCallCollector<'_> {
9250    fn unsafe_context(&self) -> bool {
9251        self.unsafe_function_depth > 0 || self.with_depth > 0
9252    }
9253
9254    fn exit_source_function(&mut self, span: Span) {
9255        if self.source_sensitive_functions.contains(&span_key(span)) {
9256            self.unsafe_function_depth -= 1;
9257        }
9258    }
9259}
9260
9261impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
9262    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9263        if self
9264            .source_sensitive_functions
9265            .contains(&span_key(node.span))
9266        {
9267            self.unsafe_function_depth += 1;
9268        }
9269    }
9270
9271    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9272        self.exit_source_function(node.span);
9273    }
9274
9275    fn enter_arrow_function_expression(
9276        &mut self,
9277        node: &mut ArrowFunctionExpression<'a>,
9278        _context: &mut TraverseCtx<'a, ()>,
9279    ) {
9280        if self
9281            .source_sensitive_functions
9282            .contains(&span_key(node.span))
9283        {
9284            self.unsafe_function_depth += 1;
9285        }
9286    }
9287
9288    fn exit_arrow_function_expression(
9289        &mut self,
9290        node: &mut ArrowFunctionExpression<'a>,
9291        _context: &mut TraverseCtx<'a, ()>,
9292    ) {
9293        self.exit_source_function(node.span);
9294    }
9295
9296    fn enter_with_statement(
9297        &mut self,
9298        _node: &mut WithStatement<'a>,
9299        _context: &mut TraverseCtx<'a, ()>,
9300    ) {
9301        self.with_depth += 1;
9302    }
9303
9304    fn exit_with_statement(
9305        &mut self,
9306        _node: &mut WithStatement<'a>,
9307        _context: &mut TraverseCtx<'a, ()>,
9308    ) {
9309        self.with_depth -= 1;
9310    }
9311
9312    fn enter_chain_expression(
9313        &mut self,
9314        node: &mut ChainExpression<'a>,
9315        context: &mut TraverseCtx<'a, ()>,
9316    ) {
9317        let root = match context.ancestors().next() {
9318            Some(Ancestor::UnaryExpressionArgument(parent))
9319                if *parent.operator() == UnaryOperator::Delete =>
9320            {
9321                span_key(*parent.span())
9322            }
9323            _ => span_key(node.span),
9324        };
9325        self.chain_roots.push(root);
9326    }
9327
9328    fn exit_chain_expression(
9329        &mut self,
9330        _node: &mut ChainExpression<'a>,
9331        _context: &mut TraverseCtx<'a, ()>,
9332    ) {
9333        self.chain_roots.pop();
9334    }
9335
9336    fn enter_call_expression(
9337        &mut self,
9338        node: &mut CallExpression<'a>,
9339        _context: &mut TraverseCtx<'a, ()>,
9340    ) {
9341        if !node.optional || self.unsafe_context() {
9342            return;
9343        }
9344        let root = *self
9345            .chain_roots
9346            .last()
9347            .expect("an optional call must be enclosed by a chain expression");
9348        let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
9349        let short_id = format!("{id}:short");
9350        let continued_id = format!("{id}:continued");
9351        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9352        self.analysis.sites.insert(
9353            span_key(node.span),
9354            (short_id.clone(), continued_id.clone()),
9355        );
9356        self.analysis
9357            .roots
9358            .entry(root)
9359            .or_default()
9360            .push(span_key(node.span));
9361        self.analysis.branches.push(CandidateBranch {
9362            id,
9363            kind: "optional-chain".to_string(),
9364            file: self.file.to_string(),
9365            line,
9366            column,
9367            source: source_slice(self.source, node.span).to_string(),
9368            alternatives: vec![
9369                CandidateBranchAlternative {
9370                    id: short_id,
9371                    label: "nullish / short-circuited".to_string(),
9372                },
9373                CandidateBranchAlternative {
9374                    id: continued_id,
9375                    label: "non-nullish / continued".to_string(),
9376                },
9377            ],
9378        });
9379    }
9380}
9381
9382fn collect_optional_call_branches<'a>(
9383    allocator: &'a Allocator,
9384    program: &mut Program<'a>,
9385    source: &str,
9386    file: &str,
9387    source_sensitive_functions: &HashSet<SpanKey>,
9388) -> OptionalCallAnalysis {
9389    let mut collector = OptionalCallCollector {
9390        source,
9391        file,
9392        source_sensitive_functions,
9393        unsafe_function_depth: 0,
9394        with_depth: 0,
9395        chain_roots: Vec::new(),
9396        analysis: OptionalCallAnalysis::default(),
9397    };
9398    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9399    collector.analysis
9400}
9401
9402struct OptionalMemberCollector<'s> {
9403    source: &'s str,
9404    file: &'s str,
9405    source_sensitive_functions: &'s HashSet<SpanKey>,
9406    unsafe_function_depth: usize,
9407    with_depth: usize,
9408    analysis: OptionalMemberAnalysis,
9409}
9410
9411impl OptionalMemberCollector<'_> {
9412    fn record<State>(
9413        &mut self,
9414        span: Span,
9415        optional: bool,
9416        object: &Expression<'_>,
9417        context: &TraverseCtx<'_, State>,
9418    ) {
9419        if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
9420            return;
9421        }
9422        let below = match optional_link_below(object) {
9423            LinkBelow::Member => LINK_AFTER_ANOTHER,
9424            LinkBelow::Call(call) => {
9425                self.analysis.after_call.insert(span_key(span), call);
9426                LINK_AFTER_CALL
9427            }
9428            LinkBelow::None => 0,
9429        };
9430        let links = below
9431            | if optional_link_above(context) {
9432                LINK_BEFORE_ANOTHER
9433            } else {
9434                0
9435            };
9436        if links != 0 {
9437            self.analysis.links.insert(span_key(span), links);
9438        }
9439        let id = stable_id(self.source, self.file, "optional-chain", span, "");
9440        let short_id = format!("{id}:short");
9441        let continued_id = format!("{id}:continued");
9442        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
9443        self.analysis
9444            .targets
9445            .insert(span_key(span), (short_id.clone(), continued_id.clone()));
9446        self.analysis.branches.push(CandidateBranch {
9447            id,
9448            kind: "optional-chain".to_string(),
9449            file: self.file.to_string(),
9450            line,
9451            column,
9452            source: source_slice(self.source, span).to_string(),
9453            alternatives: vec![
9454                CandidateBranchAlternative {
9455                    id: short_id,
9456                    label: "nullish / short-circuited".to_string(),
9457                },
9458                CandidateBranchAlternative {
9459                    id: continued_id,
9460                    label: "non-nullish / continued".to_string(),
9461                },
9462            ],
9463        });
9464    }
9465
9466    fn exit_source_function(&mut self, span: Span) {
9467        if self.source_sensitive_functions.contains(&span_key(span)) {
9468            self.unsafe_function_depth -= 1;
9469        }
9470    }
9471}
9472
9473impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
9474    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9475        if self
9476            .source_sensitive_functions
9477            .contains(&span_key(node.span))
9478        {
9479            self.unsafe_function_depth += 1;
9480        }
9481    }
9482
9483    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9484        self.exit_source_function(node.span);
9485    }
9486
9487    fn enter_arrow_function_expression(
9488        &mut self,
9489        node: &mut ArrowFunctionExpression<'a>,
9490        _context: &mut TraverseCtx<'a, ()>,
9491    ) {
9492        if self
9493            .source_sensitive_functions
9494            .contains(&span_key(node.span))
9495        {
9496            self.unsafe_function_depth += 1;
9497        }
9498    }
9499
9500    fn exit_arrow_function_expression(
9501        &mut self,
9502        node: &mut ArrowFunctionExpression<'a>,
9503        _context: &mut TraverseCtx<'a, ()>,
9504    ) {
9505        self.exit_source_function(node.span);
9506    }
9507
9508    fn enter_with_statement(
9509        &mut self,
9510        _node: &mut WithStatement<'a>,
9511        _context: &mut TraverseCtx<'a, ()>,
9512    ) {
9513        self.with_depth += 1;
9514    }
9515
9516    fn exit_with_statement(
9517        &mut self,
9518        _node: &mut WithStatement<'a>,
9519        _context: &mut TraverseCtx<'a, ()>,
9520    ) {
9521        self.with_depth -= 1;
9522    }
9523
9524    fn enter_computed_member_expression(
9525        &mut self,
9526        node: &mut ComputedMemberExpression<'a>,
9527        context: &mut TraverseCtx<'a, ()>,
9528    ) {
9529        self.record(node.span, node.optional, &node.object, context);
9530    }
9531
9532    fn enter_static_member_expression(
9533        &mut self,
9534        node: &mut StaticMemberExpression<'a>,
9535        context: &mut TraverseCtx<'a, ()>,
9536    ) {
9537        self.record(node.span, node.optional, &node.object, context);
9538    }
9539
9540    fn enter_private_field_expression(
9541        &mut self,
9542        node: &mut PrivateFieldExpression<'a>,
9543        context: &mut TraverseCtx<'a, ()>,
9544    ) {
9545        self.record(node.span, node.optional, &node.object, context);
9546    }
9547}
9548
9549fn collect_optional_member_branches<'a>(
9550    allocator: &'a Allocator,
9551    program: &mut Program<'a>,
9552    source: &str,
9553    file: &str,
9554    source_sensitive_functions: &HashSet<SpanKey>,
9555) -> OptionalMemberAnalysis {
9556    let mut collector = OptionalMemberCollector {
9557        source,
9558        file,
9559        source_sensitive_functions,
9560        unsafe_function_depth: 0,
9561        with_depth: 0,
9562        analysis: OptionalMemberAnalysis::default(),
9563    };
9564    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9565    collector.analysis
9566}
9567
9568struct LogicalAssignmentCollector<'s> {
9569    source: &'s str,
9570    file: &'s str,
9571    source_sensitive_functions: &'s HashSet<SpanKey>,
9572    unsafe_function_depth: usize,
9573    with_depth: usize,
9574    analysis: LogicalAssignmentAnalysis,
9575}
9576
9577impl LogicalAssignmentCollector<'_> {
9578    fn exit_source_function(&mut self, span: Span) {
9579        if self.source_sensitive_functions.contains(&span_key(span)) {
9580            self.unsafe_function_depth -= 1;
9581        }
9582    }
9583}
9584
9585impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
9586    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9587        if self
9588            .source_sensitive_functions
9589            .contains(&span_key(node.span))
9590        {
9591            self.unsafe_function_depth += 1;
9592        }
9593    }
9594
9595    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9596        self.exit_source_function(node.span);
9597    }
9598
9599    fn enter_arrow_function_expression(
9600        &mut self,
9601        node: &mut ArrowFunctionExpression<'a>,
9602        _context: &mut TraverseCtx<'a, ()>,
9603    ) {
9604        if self
9605            .source_sensitive_functions
9606            .contains(&span_key(node.span))
9607        {
9608            self.unsafe_function_depth += 1;
9609        }
9610    }
9611
9612    fn exit_arrow_function_expression(
9613        &mut self,
9614        node: &mut ArrowFunctionExpression<'a>,
9615        _context: &mut TraverseCtx<'a, ()>,
9616    ) {
9617        self.exit_source_function(node.span);
9618    }
9619
9620    fn enter_with_statement(
9621        &mut self,
9622        _node: &mut WithStatement<'a>,
9623        _context: &mut TraverseCtx<'a, ()>,
9624    ) {
9625        self.with_depth += 1;
9626    }
9627
9628    fn exit_with_statement(
9629        &mut self,
9630        _node: &mut WithStatement<'a>,
9631        _context: &mut TraverseCtx<'a, ()>,
9632    ) {
9633        self.with_depth -= 1;
9634    }
9635
9636    fn exit_assignment_expression(
9637        &mut self,
9638        node: &mut AssignmentExpression<'a>,
9639        _context: &mut TraverseCtx<'a, ()>,
9640    ) {
9641        if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
9642            return;
9643        }
9644        let operator = match node.operator {
9645            AssignmentOperator::LogicalAnd => "&&=",
9646            AssignmentOperator::LogicalOr => "||=",
9647            AssignmentOperator::LogicalNullish => "??=",
9648            _ => unreachable!("logical assignment filter must be exhaustive"),
9649        };
9650        let id = stable_id(
9651            self.source,
9652            self.file,
9653            "logical-assignment",
9654            node.span,
9655            operator,
9656        );
9657        let short_id = format!("{id}:short");
9658        let right_id = format!("{id}:right");
9659        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9660        self.analysis
9661            .targets
9662            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9663        self.analysis.branches.push(CandidateBranch {
9664            id,
9665            kind: "logical-assignment".to_string(),
9666            file: self.file.to_string(),
9667            line,
9668            column,
9669            source: source_slice(self.source, node.span).to_string(),
9670            alternatives: vec![
9671                CandidateBranchAlternative {
9672                    id: short_id,
9673                    label: "assignment skipped".to_string(),
9674                },
9675                CandidateBranchAlternative {
9676                    id: right_id,
9677                    label: "right evaluated / assigned".to_string(),
9678                },
9679            ],
9680        });
9681    }
9682}
9683
9684fn collect_logical_assignment_branches<'a>(
9685    allocator: &'a Allocator,
9686    program: &mut Program<'a>,
9687    source: &str,
9688    file: &str,
9689    source_sensitive_functions: &HashSet<SpanKey>,
9690) -> LogicalAssignmentAnalysis {
9691    let mut collector = LogicalAssignmentCollector {
9692        source,
9693        file,
9694        source_sensitive_functions,
9695        unsafe_function_depth: 0,
9696        with_depth: 0,
9697        analysis: LogicalAssignmentAnalysis::default(),
9698    };
9699    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9700    collector.analysis
9701}
9702
9703struct LogicalBranchCollector<'s> {
9704    source: &'s str,
9705    file: &'s str,
9706    decision_logical_nodes: &'s HashSet<SpanKey>,
9707    source_sensitive_functions: &'s HashSet<SpanKey>,
9708    unsafe_function_depth: usize,
9709    with_depth: usize,
9710    analysis: LogicalBranchAnalysis,
9711}
9712
9713impl LogicalBranchCollector<'_> {
9714    fn exit_source_function(&mut self, span: Span) {
9715        if self.source_sensitive_functions.contains(&span_key(span)) {
9716            self.unsafe_function_depth -= 1;
9717        }
9718    }
9719}
9720
9721impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
9722    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9723        if self
9724            .source_sensitive_functions
9725            .contains(&span_key(node.span))
9726        {
9727            self.unsafe_function_depth += 1;
9728        }
9729    }
9730
9731    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9732        self.exit_source_function(node.span);
9733    }
9734
9735    fn enter_arrow_function_expression(
9736        &mut self,
9737        node: &mut ArrowFunctionExpression<'a>,
9738        _context: &mut TraverseCtx<'a, ()>,
9739    ) {
9740        if self
9741            .source_sensitive_functions
9742            .contains(&span_key(node.span))
9743        {
9744            self.unsafe_function_depth += 1;
9745        }
9746    }
9747
9748    fn exit_arrow_function_expression(
9749        &mut self,
9750        node: &mut ArrowFunctionExpression<'a>,
9751        _context: &mut TraverseCtx<'a, ()>,
9752    ) {
9753        self.exit_source_function(node.span);
9754    }
9755
9756    fn enter_with_statement(
9757        &mut self,
9758        _node: &mut WithStatement<'a>,
9759        _context: &mut TraverseCtx<'a, ()>,
9760    ) {
9761        self.with_depth += 1;
9762    }
9763
9764    fn exit_with_statement(
9765        &mut self,
9766        _node: &mut WithStatement<'a>,
9767        _context: &mut TraverseCtx<'a, ()>,
9768    ) {
9769        self.with_depth -= 1;
9770    }
9771
9772    fn exit_logical_expression(
9773        &mut self,
9774        node: &mut LogicalExpression<'a>,
9775        _context: &mut TraverseCtx<'a, ()>,
9776    ) {
9777        if self.unsafe_function_depth > 0
9778            || self.with_depth > 0
9779            || self.decision_logical_nodes.contains(&span_key(node.span))
9780        {
9781            return;
9782        }
9783        let operator = match node.operator {
9784            LogicalOperator::And => "&&",
9785            LogicalOperator::Or => "||",
9786            LogicalOperator::Coalesce => "??",
9787        };
9788        let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
9789        let short_id = format!("{id}:short");
9790        let right_id = format!("{id}:right");
9791        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9792        self.analysis
9793            .logical_targets
9794            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9795        self.analysis.branches.push(CandidateBranch {
9796            id,
9797            kind: "logical-value".to_string(),
9798            file: self.file.to_string(),
9799            line,
9800            column,
9801            source: source_slice(self.source, node.span).to_string(),
9802            alternatives: vec![
9803                CandidateBranchAlternative {
9804                    id: short_id,
9805                    label: "short-circuit / left selected".to_string(),
9806                },
9807                CandidateBranchAlternative {
9808                    id: right_id,
9809                    label: "right evaluated / selected".to_string(),
9810                },
9811            ],
9812        });
9813    }
9814}
9815
9816fn collect_logical_value_branches<'a>(
9817    allocator: &'a Allocator,
9818    program: &mut Program<'a>,
9819    source: &str,
9820    file: &str,
9821    decision_logical_nodes: &HashSet<SpanKey>,
9822    source_sensitive_functions: &HashSet<SpanKey>,
9823) -> LogicalBranchAnalysis {
9824    let mut collector = LogicalBranchCollector {
9825        source,
9826        file,
9827        decision_logical_nodes,
9828        source_sensitive_functions,
9829        unsafe_function_depth: 0,
9830        with_depth: 0,
9831        analysis: LogicalBranchAnalysis::default(),
9832    };
9833    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9834    collector.analysis
9835}
9836
9837impl<'a> Visit<'a> for DecisionCollector<'_> {
9838    fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
9839        if self
9840            .source_sensitive_functions
9841            .contains(&span_key(function.span))
9842        {
9843            return;
9844        }
9845        walk::walk_function(self, function, flags);
9846    }
9847
9848    fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
9849        if self
9850            .source_sensitive_functions
9851            .contains(&span_key(function.span))
9852        {
9853            return;
9854        }
9855        walk::walk_arrow_function_expression(self, function);
9856    }
9857
9858    // A with body is left uninstrumented: its object environment can
9859    // intercept probe identifiers (see `enter_with_statement`).
9860    fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
9861
9862    /// `&&` and `||` used as a value: the left operand is always read as a
9863    /// boolean, whatever the operator returns, so a compound one is a decision
9864    /// as much as an `if`'s test is -- `(a && b) || fallback`, `a || b ||
9865    /// "default"`. What the operator returns from its right is a value, not a
9866    /// condition, and is judged as one in its turn. A `!` reads its operand as
9867    /// a boolean too.
9868    fn visit_expression(&mut self, expression: &Expression<'a>) {
9869        let outer_root = self.logical_root;
9870        // A coverage tool the tests run reads an `&&`/`||`/`??` tree as one
9871        // branch, whatever parentheses are in it; a `!` or anything else is a
9872        // leaf of it.
9873        self.logical_root = match expression {
9874            Expression::LogicalExpression(logical) => {
9875                Some(outer_root.unwrap_or(span_key(logical.span)))
9876            }
9877            Expression::ParenthesizedExpression(_) => outer_root,
9878            _ => None,
9879        };
9880        let candidate = match expression {
9881            Expression::LogicalExpression(logical)
9882                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9883                    && !self.in_a_decision(logical.span) =>
9884            {
9885                Some(&logical.left)
9886            }
9887            Expression::UnaryExpression(unary)
9888                if unary.operator.is_not()
9889                    && has_compound_boolean_decision(&unary.argument)
9890                    && !compound_root(&unary.argument)
9891                        .is_some_and(|root| self.in_a_decision(root.span)) =>
9892            {
9893                Some(expression)
9894            }
9895            _ => None,
9896        };
9897        if let Some(decision) = candidate
9898            && has_compound_boolean_decision(decision)
9899            && decision_outcome_is_variable(decision)
9900        {
9901            let mut conditions = Vec::new();
9902            collect_conditions(decision, &mut conditions);
9903            if conditions.len() <= 32 {
9904                // A `!` outside any tree is its own root.
9905                let root = self
9906                    .logical_root
9907                    .or(outer_root)
9908                    .unwrap_or(span_key(decision.span()));
9909                self.value_decisions.insert(span_key(decision.span()), root);
9910                self.record_decision(decision, "value");
9911            }
9912        }
9913        walk::walk_expression(self, expression);
9914        self.logical_root = outer_root;
9915    }
9916
9917    fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
9918        if decision_outcome_is_variable(&statement.test) {
9919            self.record_decision(&statement.test, "if");
9920        }
9921        self.visit_expression(&statement.test);
9922        self.visit_statement(&statement.consequent);
9923        if let Some(alternate) = &statement.alternate {
9924            self.visit_statement(alternate);
9925        }
9926    }
9927
9928    fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
9929        if decision_outcome_is_variable(&expression.test) {
9930            self.record_decision(&expression.test, "ternary");
9931        }
9932        self.visit_expression(&expression.test);
9933        self.visit_expression(&expression.consequent);
9934        self.visit_expression(&expression.alternate);
9935    }
9936
9937    fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
9938        if decision_outcome_is_variable(&statement.test) {
9939            self.record_decision(&statement.test, "while");
9940        }
9941        self.visit_expression(&statement.test);
9942        self.visit_statement(&statement.body);
9943    }
9944
9945    fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
9946        if decision_outcome_is_variable(&statement.test) {
9947            self.record_decision(&statement.test, "do-while");
9948        }
9949        self.visit_statement(&statement.body);
9950        self.visit_expression(&statement.test);
9951    }
9952
9953    fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
9954        if let Some(test) = &statement.test
9955            && decision_outcome_is_variable(test)
9956        {
9957            self.record_decision(test, "for");
9958        }
9959        if let Some(init) = &statement.init {
9960            self.visit_for_statement_init(init);
9961        }
9962        if let Some(test) = &statement.test {
9963            self.visit_expression(test);
9964        }
9965        if let Some(update) = &statement.update {
9966            self.visit_expression(update);
9967        }
9968        self.visit_statement(&statement.body);
9969    }
9970}
9971
9972fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
9973    match expression {
9974        Expression::ParenthesizedExpression(parenthesized) => {
9975            transparent_expression(&parenthesized.expression)
9976        }
9977        _ => expression,
9978    }
9979}
9980
9981fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
9982    match expression {
9983        Expression::ParenthesizedExpression(parenthesized) => {
9984            has_compound_boolean_decision(&parenthesized.expression)
9985        }
9986        Expression::LogicalExpression(logical) => {
9987            matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9988        }
9989        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
9990            has_compound_boolean_decision(&unary.argument)
9991        }
9992        _ => false,
9993    }
9994}
9995
9996/// A decision's and/or/not structure over its conditions, numbered as
9997/// `collect_conditions` numbers them.
9998enum ValueShape {
9999    Condition(usize),
10000    And(Box<ValueShape>, Box<ValueShape>),
10001    Or(Box<ValueShape>, Box<ValueShape>),
10002    Not(Box<ValueShape>),
10003}
10004
10005fn value_shape(expression: &Expression<'_>, next: &mut usize) -> ValueShape {
10006    match expression {
10007        Expression::ParenthesizedExpression(parenthesized) => {
10008            value_shape(&parenthesized.expression, next)
10009        }
10010        Expression::LogicalExpression(logical)
10011            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10012        {
10013            let left = Box::new(value_shape(&logical.left, next));
10014            let right = Box::new(value_shape(&logical.right, next));
10015            if logical.operator == LogicalOperator::And {
10016                ValueShape::And(left, right)
10017            } else {
10018                ValueShape::Or(left, right)
10019            }
10020        }
10021        Expression::UnaryExpression(unary)
10022            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10023        {
10024            ValueShape::Not(Box::new(value_shape(&unary.argument, next)))
10025        }
10026        _ => {
10027            let index = *next;
10028            *next += 1;
10029            ValueShape::Condition(index)
10030        }
10031    }
10032}
10033
10034fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
10035    match expression {
10036        Expression::ParenthesizedExpression(parenthesized) => {
10037            collect_conditions(&parenthesized.expression, conditions);
10038        }
10039        Expression::LogicalExpression(logical)
10040            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10041        {
10042            collect_conditions(&logical.left, conditions);
10043            collect_conditions(&logical.right, conditions);
10044        }
10045        Expression::UnaryExpression(unary)
10046            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10047        {
10048            collect_conditions(&unary.argument, conditions);
10049        }
10050        _ => conditions.push(expression.span()),
10051    }
10052}
10053
10054fn source_slice(source: &str, span: Span) -> &str {
10055    &source[span.start as usize..span.end as usize]
10056}
10057
10058/// Where every line of one source starts, in bytes and in UTF-16 units, so a
10059/// position is a binary search rather than a scan from the top of the file.
10060///
10061/// Every probe, decision and branch records its line, column and a UTF-16
10062/// offset, and each was counted from the first byte of the file: the work
10063/// grew with the square of the file's length. An 8,000-line file took 97
10064/// seconds to instrument on a debug build, a 24,000-line one did not finish
10065/// in ten minutes, and Actual's setup spent minutes before its first test.
10066struct SourcePositions {
10067    address: usize,
10068    length: usize,
10069    sample: u64,
10070    line_starts: Vec<usize>,
10071    utf16_before_line: Vec<usize>,
10072    /// Per line: `None` when ASCII, otherwise `(byte offset, utf16 column)`
10073    /// checkpoints from the line's start, so a long line that is not ASCII
10074    /// -- a table of currency symbols, say -- is not walked per position.
10075    checkpoints: Vec<Option<Vec<(usize, usize)>>>,
10076}
10077
10078impl SourcePositions {
10079    fn sample(source: &str) -> u64 {
10080        let bytes = source.as_bytes();
10081        let head = &bytes[..bytes.len().min(64)];
10082        let tail = &bytes[bytes.len().saturating_sub(64)..];
10083        head.iter()
10084            .chain(tail)
10085            .fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
10086                (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
10087            })
10088    }
10089
10090    fn new(source: &str) -> Self {
10091        let mut line_starts = vec![0];
10092        let mut utf16_before_line = vec![0];
10093        let mut checkpoints = Vec::new();
10094        let mut marks: Option<Vec<(usize, usize)>> = None;
10095        let mut utf16 = 0;
10096        let mut column = 0;
10097        let mut line_start = 0;
10098        for (index, character) in source.char_indices() {
10099            if !character.is_ascii() && marks.is_none() {
10100                marks = Some(vec![(0, 0)]);
10101            }
10102            if let Some(marks) = &mut marks
10103                && column >= marks.last().expect("a first mark").1 + CHECKPOINT
10104            {
10105                marks.push((index - line_start, column));
10106            }
10107            utf16 += character.len_utf16();
10108            column += character.len_utf16();
10109            if character == '\n' {
10110                line_start = index + 1;
10111                line_starts.push(line_start);
10112                utf16_before_line.push(utf16);
10113                checkpoints.push(marks.take());
10114                column = 0;
10115            }
10116        }
10117        checkpoints.push(marks);
10118        Self {
10119            address: source.as_ptr() as usize,
10120            length: source.len(),
10121            sample: Self::sample(source),
10122            line_starts,
10123            utf16_before_line,
10124            checkpoints,
10125        }
10126    }
10127
10128    fn describes(&self, source: &str) -> bool {
10129        self.address == source.as_ptr() as usize
10130            && self.length == source.len()
10131            && self.sample == Self::sample(source)
10132    }
10133
10134    /// Zero-based line, and the UTF-16 units from that line's start.
10135    fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
10136        let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
10137        let start = self.line_starts[line];
10138        let column = match &self.checkpoints[line] {
10139            None => offset - start,
10140            Some(marks) => {
10141                let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
10142                let (byte, column) = marks[at];
10143                column + source[start + byte..offset].encode_utf16().count()
10144            }
10145        };
10146        (line, column)
10147    }
10148}
10149
10150thread_local! {
10151    static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
10152        const { std::cell::RefCell::new(None) };
10153}
10154
10155/// While one of these is alive, positions in any source are computed from an
10156/// index built once for it. It lives for one instrumentation call, during
10157/// which the source it indexes is borrowed and cannot move; outside one,
10158/// positions are counted from the top as before.
10159pub(crate) struct PositionScope {
10160    owner: bool,
10161}
10162
10163impl PositionScope {
10164    pub(crate) fn enter() -> Self {
10165        let owner = POSITIONS.with(|positions| {
10166            let mut positions = positions.borrow_mut();
10167            let owner = positions.is_none();
10168            if owner {
10169                *positions = Some(Vec::new());
10170            }
10171            owner
10172        });
10173        Self { owner }
10174    }
10175}
10176
10177impl Drop for PositionScope {
10178    fn drop(&mut self) {
10179        if self.owner {
10180            POSITIONS.with(|positions| *positions.borrow_mut() = None);
10181        }
10182    }
10183}
10184
10185fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
10186    POSITIONS.with(|positions| {
10187        let mut positions = positions.borrow_mut();
10188        let known = positions.as_mut()?;
10189        if let Some(found) = known.iter().find(|index| index.describes(source)) {
10190            return Some(found.clone());
10191        }
10192        let built = std::rc::Rc::new(SourcePositions::new(source));
10193        known.push(built.clone());
10194        Some(built)
10195    })
10196}
10197
10198fn utf16_offset(source: &str, offset: usize) -> usize {
10199    match indexed(source) {
10200        Some(index) => {
10201            let (line, column) = index.locate(source, offset);
10202            index.utf16_before_line[line] + column
10203        }
10204        None => source[..offset].encode_utf16().count(),
10205    }
10206}
10207
10208pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
10209    if let Some(index) = indexed(source) {
10210        let (line, column) = index.locate(source, offset);
10211        return (line + 1, column + 1);
10212    }
10213    let prefix = &source[..offset];
10214    let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
10215    let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
10216    let column = source[line_start..offset].encode_utf16().count() + 1;
10217    (line, column)
10218}
10219
10220fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
10221    let start = utf16_offset(source, span.start as usize);
10222    let end = utf16_offset(source, span.end as usize);
10223    let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
10224    let mut id = String::with_capacity(16);
10225    for byte in &digest[..8] {
10226        write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
10227    }
10228    id
10229}
10230
10231#[cfg(test)]
10232mod tests {
10233    use super::*;
10234
10235    #[test]
10236    fn line_index_lookups_agree_with_walking_the_line() {
10237        let long = "é🚀x".repeat(700);
10238        let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
10239        let index = Utf16LineIndex::new(&source);
10240        for (line, text) in source.split_inclusive('\n').enumerate() {
10241            let start = index.starts[line];
10242            let width: usize = text.chars().map(char::len_utf16).sum();
10243            for column in 0..=width + 2 {
10244                assert_eq!(
10245                    index.byte_offset(line, column),
10246                    start + utf16_col_to_byte(text, column),
10247                    "line {line} column {column}"
10248                );
10249            }
10250            for (offset, _) in text.char_indices() {
10251                let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
10252                assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
10253            }
10254        }
10255    }
10256
10257    #[test]
10258    fn indexed_positions_agree_with_counting_from_the_top() {
10259        // The long line passes several checkpoints.
10260        let source = &format!(
10261            "const s = 'naïve 🚀';\n\nexport function f(a) {{\n  return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
10262            "é🚀x".repeat(400)
10263        );
10264        let source = source.as_str();
10265        let counted: Vec<_> = source
10266            .char_indices()
10267            .map(|(offset, _)| {
10268                (
10269                    line_and_utf16_column(source, offset),
10270                    stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10271                )
10272            })
10273            .collect();
10274        let _positions = PositionScope::enter();
10275        for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
10276            assert_eq!(
10277                line_and_utf16_column(source, offset),
10278                position,
10279                "offset {offset}"
10280            );
10281            assert_eq!(
10282                stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10283                id
10284            );
10285        }
10286    }
10287
10288    #[test]
10289    fn classic_scripts_share_application_globals_but_not_probe_bindings() {
10290        // TodoMVC's ES5 app loads several classic scripts into one document.
10291        // They share the global lexical scope, so a second
10292        // `const __supercovMcdcBegin` was a SyntaxError and no item was added.
10293        let a = instrument_direct_candidate(
10294            "var count = 0; function increment() { if (count >= 0) { count += 1; } }",
10295            "public/a.js",
10296        )
10297        .unwrap();
10298        let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
10299            .unwrap();
10300        let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
10301        let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
10302        let script = format!(
10303            "const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
10304             vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
10305             if (ctx.observed !== 1) throw Error('shared globals changed');\
10306             vm.runInContext({}, ctx);\
10307             if (ctx.observed !== 2) throw Error('a repeated script changed');",
10308            serde_json::to_string(runtime).unwrap(),
10309            serde_json::to_string(&a.code).unwrap(),
10310            serde_json::to_string(&b.code).unwrap(),
10311            serde_json::to_string(&b.code).unwrap()
10312        );
10313        // Through stdin: the script carries the whole runtime, longer than a
10314        // Windows command line may be.
10315        let mut node = std::process::Command::new("node")
10316            .arg("-")
10317            .stdin(std::process::Stdio::piped())
10318            .stdout(std::process::Stdio::piped())
10319            .stderr(std::process::Stdio::piped())
10320            .spawn()
10321            .expect("Node.js for the JavaScript regression");
10322        std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
10323        let result = node.wait_with_output().unwrap();
10324        assert!(
10325            result.status.success(),
10326            "{}",
10327            String::from_utf8_lossy(&result.stderr)
10328        );
10329    }
10330
10331    #[test]
10332    fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
10333        // TodoMVC's React app keeps JSX in `src/index.js`; the run failed on
10334        // `Unexpected token` before any test started.
10335        for file in [
10336            "src/index.js",
10337            "src/index.jsx",
10338            "src/index.mjs",
10339            "src/index.cjs",
10340            "src/index.tsx",
10341        ] {
10342            let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
10343            let output = instrument_candidate(source, file).unwrap();
10344            assert!(!output.points.is_empty(), "{file}");
10345            assert!(output.code.contains("<div>"), "{file}");
10346        }
10347        // TypeScript keeps its angle-bracket assertion; JSX would misread it.
10348        assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
10349        // The script or module mode an extension implies is kept.
10350        for file in ["a.js", "a.cjs", "a.mjs"] {
10351            let before = SourceType::from_path(file).unwrap();
10352            let after = project_source_type(Path::new(file)).unwrap();
10353            assert_eq!(after.module_kind(), before.module_kind(), "{file}");
10354            assert!(after.is_jsx(), "{file}");
10355        }
10356    }
10357
10358    const SOURCE: &str =
10359        "export function decide(a,b,c) {\n  if ((a && b) || !c) return 1;\n  return 0;\n}\n";
10360
10361    #[test]
10362    fn matches_the_frozen_if_decision_manifest_exactly() {
10363        let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
10364        assert!(!output.complete);
10365        assert_eq!(
10366            output.decisions,
10367            vec![CandidateDecision {
10368                id: "2f65989a5782c5bd".to_string(),
10369                file: "app/decide.ts".to_string(),
10370                line: 2,
10371                column: 7,
10372                source: "(a && b) || !c".to_string(),
10373                conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
10374                kind: "if".to_string(),
10375            }]
10376        );
10377    }
10378
10379    #[test]
10380    fn a_test_files_unchanged_text_keeps_its_columns() {
10381        // One mapping for a line put every frame of a test file in column 1,
10382        // and one inside a wrapped assertion at the statement's start: Vitest
10383        // printed `:2:3` where it prints `:2:22` without Supercov.
10384        let source = "import { expect } from 'vitest';\n  expect(value(1)).toBe(2);\n";
10385        let wrapper = "wrap(() => ";
10386        let edits = vec![
10387            TextEdit {
10388                start: source.find("expect(value").unwrap(),
10389                end: source.find("expect(value").unwrap(),
10390                text: wrapper.into(),
10391                rank: 0,
10392            },
10393            TextEdit {
10394                start: source.rfind(';').unwrap(),
10395                end: source.rfind(';').unwrap(),
10396                text: ")".into(),
10397                rank: 0,
10398            },
10399        ];
10400        let (code, map) = apply_text_edits(source, edits, "a.test.js").unwrap();
10401        assert_eq!(
10402            code,
10403            "import { expect } from 'vitest';\n  wrap(() => expect(value(1)).toBe(2));\n"
10404        );
10405        let original = |line: u32, column: u32| {
10406            map.get_tokens()
10407                .filter(|token| token.get_dst_line() == line && token.get_dst_col() <= column)
10408                .max_by_key(|token| token.get_dst_col())
10409                .map(|token| (token.get_src_line(), token.get_src_col()))
10410        };
10411        // `toBe`, past the text the wrapper added.
10412        let matcher = code.lines().nth(1).unwrap().find("toBe").unwrap() as u32;
10413        assert_eq!(original(1, matcher), Some((1, 19)));
10414        // A word and a mark of a line nothing was added to.
10415        assert_eq!(original(0, 9), Some((0, 9)));
10416        assert_eq!(original(0, 7), Some((0, 7)));
10417        // The text the wrapper added maps to where the line began.
10418        assert_eq!(original(1, 4), Some((1, 0)));
10419        // An identifier carries its name, which a compiler reading this map
10420        // keeps and Jest prints (`at toBe (…)`); a keyword carries none.
10421        let name_at = |line: u32, column: u32| {
10422            map.get_tokens()
10423                .find(|token| token.get_dst_line() == line && token.get_dst_col() == column)
10424                .and_then(|token| token.get_name_id())
10425                .and_then(|id| map.get_name(id))
10426                .map(|name| name.to_string())
10427        };
10428        assert_eq!(name_at(1, matcher).as_deref(), Some("toBe"));
10429        assert_eq!(name_at(0, 9).as_deref(), Some("expect"));
10430        assert_eq!(name_at(0, 18), None, "`from` is a keyword");
10431    }
10432
10433    #[test]
10434    fn a_server_entry_keeps_what_it_exports_by_default() {
10435        // Taken before it was looked at, anything but a function declaration
10436        // was replaced by a placeholder `null`: `export default handleRequest;`
10437        // exported `withRequestPhase(null)`, and a class exported `null`.
10438        for (source, export) in [
10439            (
10440                "function handleRequest(r) { return r; }\nexport default handleRequest;\n",
10441                "export default __supercovWithRequestPhase13(handleRequest);",
10442            ),
10443            (
10444                "export default class Entry {}\n",
10445                "export default class Entry {}",
10446            ),
10447        ] {
10448            let output = instrument_candidate_with_binding(
10449                source,
10450                "app/entry.server.ts",
10451                RuntimeBinding::ModuleImport,
10452                None,
10453                false,
10454            )
10455            .unwrap();
10456            assert!(
10457                output.code.lines().any(|line| line == export),
10458                "{export}\n{}",
10459                output.code
10460            );
10461        }
10462    }
10463
10464    #[test]
10465    fn a_method_read_through_a_property_keeps_its_text() {
10466        // `box.method.toString()` names no binding; any method of that name in
10467        // the file is taken as the one read, and others keep their probes.
10468        let source = "export const box = { method(v) { return v  * 2; } };\nexport class Shape { area(w) { return w  * w; } static make = () =>  new Shape(); }\nexport const other = { untouched(v) { return v  + 1; } };\nexport const texts = [box.method.toString(), String(Shape.prototype.area), Shape.make.toString()];\n";
10469        let output = instrument_direct_candidate(source, "src/methods.js").unwrap();
10470        for text in [
10471            "method(v) { return v  * 2; }",
10472            "area(w) { return w  * w; }",
10473            "() =>  new Shape()",
10474        ] {
10475            assert!(output.code.contains(text), "{text}\n{}", output.code);
10476        }
10477        assert!(
10478            !output.code.contains("untouched(v) { return v  + 1; }"),
10479            "{}",
10480            output.code
10481        );
10482    }
10483
10484    #[test]
10485    fn a_function_whose_source_is_read_keeps_its_text_exactly() {
10486        // Left without probes, it was still printed again: `f.toString()`
10487        // read `function f() {\n\treturn 1;\n}` under measurement.
10488        let source = "export function* g() { yield 1; }\nexport const add = (a,  b) =>   a + b;\nexport async function later(x) {\n  // kept\n  if (x) {\n      return x;\n  }\n  return 0;\n}\nexport function counted(n) { return n > 1 ? 'many' : 'one'; }\nexport const shown = () => [g.toString(), String(add), `${later}`];\n";
10489        let output = instrument_direct_candidate(source, "src/text.js").unwrap();
10490        for text in [
10491            "function* g() { yield 1; }",
10492            "(a,  b) =>   a + b",
10493            "async function later(x) {\n  // kept\n  if (x) {\n      return x;\n  }\n  return 0;\n}",
10494        ] {
10495            assert!(output.code.contains(text), "{text}\n{}", output.code);
10496        }
10497        // A function nobody reads the source of is measured as before.
10498        assert!(
10499            !output
10500                .code
10501                .contains("function counted(n) { return n > 1 ? 'many' : 'one'; }"),
10502            "{}",
10503            output.code
10504        );
10505        // Typed source keeps its types as written.
10506        let typed = "export function twice(value: number): number { return value  * 2; }\nexport const text = twice.toString();\n";
10507        let output = instrument_direct_candidate(typed, "src/typed.ts").unwrap();
10508        assert!(
10509            output
10510                .code
10511                .contains("function twice(value: number): number { return value  * 2; }"),
10512            "{}",
10513            output.code
10514        );
10515    }
10516
10517    #[test]
10518    fn codegen_output_reparses_for_typescript_and_tsx() {
10519        for (file, source) in [
10520            (
10521                "component.tsx",
10522                "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
10523            ),
10524            ("module.ts", SOURCE),
10525        ] {
10526            let output = analyze_candidate(source, file).unwrap();
10527            let map = output.map.as_ref().expect("candidate source map");
10528            assert_eq!(map["version"], 3);
10529            assert_eq!(map["sources"][0], file);
10530            assert_eq!(map["sourcesContent"][0], source);
10531            assert!(
10532                map["mappings"]
10533                    .as_str()
10534                    .is_some_and(|value| !value.is_empty())
10535            );
10536            let allocator = Allocator::default();
10537            let source_type = SourceType::from_path(file).unwrap();
10538            let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
10539            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
10540        }
10541    }
10542
10543    #[test]
10544    fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
10545        for file in ["src/module.mjs", "src/script.cjs"] {
10546            let output = instrument_direct_candidate(
10547                "export const selected = value => value ? 1 : 0;",
10548                file,
10549            )
10550            .unwrap();
10551            assert!(
10552                output
10553                    .code
10554                    .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
10555            );
10556            assert!(!output.code.contains("virtual:supercov-runtime"));
10557            assert!(!output.code.contains("globalThis.__supercovRuntime"));
10558        }
10559    }
10560
10561    #[test]
10562    fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
10563        let source = concat!(
10564            "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10565            "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
10566            "ordinaryHelper();\n",
10567        );
10568        let output = instrument_node_assertion_phases_with_runtime_hooks(
10569            source,
10570            "tests/runner.mjs",
10571            &[],
10572            Some("../.supercov/launchSupervisor.mjs"),
10573        )
10574        .unwrap();
10575        assert_eq!(output.assertions, 0);
10576        assert_eq!(output.capability_imports, 1);
10577        assert!(output.code.contains("wrapImportedCapability"));
10578        assert!(output.code.contains("__supercovRawImageBuilder"));
10579        assert!(output.code.contains("const ImageBuilder"));
10580        assert!(!output.code.contains("__supercovRawordinaryHelper"));
10581        assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
10582    }
10583
10584    #[test]
10585    fn capability_imports_accept_a_computed_guest_mount_path() {
10586        let source = concat!(
10587            "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10588            "const guestRoot = resolve(tmpdir(), 'workspace');\n",
10589            "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
10590            "ordinaryHelper();\n",
10591        );
10592        let output = instrument_node_assertion_phases_with_runtime_hooks(
10593            source,
10594            "tests/runner.mjs",
10595            &[],
10596            Some("../.supercov/launchSupervisor.mjs"),
10597        )
10598        .unwrap();
10599        assert_eq!(output.capability_imports, 1);
10600        assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
10601        assert!(!output.code.contains("__supercovRawordinaryHelper"));
10602    }
10603
10604    #[test]
10605    fn capability_import_selection_follows_a_mapping_variable() {
10606        let source = concat!(
10607            "import { launch, unrelated } from './sdk.mjs';\n",
10608            "run();\n",
10609            "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
10610            "function run() { launch(options); unrelated(); }\n",
10611        );
10612        let output = instrument_node_assertion_phases_with_runtime_hooks(
10613            source,
10614            "tests/runner.mjs",
10615            &[],
10616            Some("../.supercov/launchSupervisor.mjs"),
10617        )
10618        .unwrap();
10619        assert_eq!(output.capability_imports, 1);
10620        assert!(output.code.contains("__supercovRawlaunch"));
10621        assert!(!output.code.contains("__supercovRawunrelated"));
10622    }
10623
10624    #[test]
10625    fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
10626        let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
10627        let output = instrument_node_assertion_phases_with_runtime_hooks(
10628            source,
10629            "src/main.mjs",
10630            &[],
10631            Some("../.supercov/launchSupervisor.mjs"),
10632        )
10633        .unwrap();
10634        assert_eq!(output.code, source);
10635        assert_eq!(output.capability_imports, 0);
10636    }
10637
10638    #[test]
10639    fn ordinary_mount_language_does_not_enable_capability_proxies() {
10640        let source = concat!(
10641            "import { render } from '@testing-library/react';\n",
10642            "it('mounts into a host element', () => render(<main />));\n",
10643        );
10644        let output = instrument_node_assertion_phases_with_runtime_hooks(
10645            source,
10646            "tests/component.test.tsx",
10647            &[],
10648            Some("../.supercov/launchSupervisor.mjs"),
10649        )
10650        .unwrap();
10651        assert_eq!(output.capability_imports, 0);
10652        assert!(!output.code.contains("wrapImportedCapability"));
10653    }
10654
10655    #[test]
10656    fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
10657        let source = concat!(
10658            "import { test, expect } from '@example/test-admin';\n",
10659            "test('visual snapshot', async ({ page }) => {\n",
10660            "  expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
10661            "});\n",
10662        );
10663        let output = instrument_node_assertion_phases_with_runtime_hooks(
10664            source,
10665            "tests/visual.spec.ts",
10666            &[],
10667            Some("../.supercov/launchSupervisor.mjs"),
10668        )
10669        .unwrap();
10670        assert_eq!(output.capability_imports, 0);
10671        assert!(!output.code.contains("wrapImportedCapability"));
10672    }
10673
10674    #[test]
10675    fn capability_rewrite_excludes_runtime_and_type_only_imports() {
10676        let source = concat!(
10677            "import type { Machine } from './types.ts';\n",
10678            "import { test } from 'node:test';\n",
10679            "import { runtime } from '../.supercov/runtime.mjs';\n",
10680            "console.log({ machine: true, test, runtime });\n",
10681        );
10682        let output = instrument_node_assertion_phases_with_runtime_hooks(
10683            source,
10684            "tests/runner.ts",
10685            &[],
10686            Some("../.supercov/launchSupervisor.mjs"),
10687        )
10688        .unwrap();
10689        assert_eq!(output.code, source);
10690        assert_eq!(output.capability_imports, 0);
10691    }
10692
10693    #[test]
10694    fn native_assertion_arguments_execute_inside_the_assertion_phase() {
10695        let source = concat!(
10696            "import assert from 'node:assert/strict';\n",
10697            "assert.equal(value(), 1);\n",
10698        );
10699        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10700        assert_eq!(output.assertions, 1);
10701        assert!(output.code.contains("withNodeAssertionPhase"));
10702        assert!(output.code.contains("node:assert/strict.equal"));
10703        assert!(output.code.contains("tests/value.test.mjs:2:1"));
10704        assert!(output.code.contains("assert.equal(value(), 1)"));
10705    }
10706
10707    #[test]
10708    fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
10709        let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
10710        let output = instrument_node_assertion_phases_with_runtime_imports(
10711            source,
10712            "tests/value.test.mjs",
10713            &[],
10714            None,
10715            Some("../.supercov/node_modules/bootstrap.cjs"),
10716        )
10717        .unwrap();
10718        assert_eq!(output.assertions, 1);
10719        // The loader shares the first line, so the assertion stays on line 2.
10720        let lines = output.code.lines().collect::<Vec<_>>();
10721        assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
10722        assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
10723        assert!(lines[0].ends_with("import assert from 'node:assert';"));
10724        assert!(lines[1].contains("withNodeAssertionPhase"));
10725        let allocator = Allocator::default();
10726        assert!(
10727            Parser::new(&allocator, &output.code, SourceType::mjs())
10728                .parse()
10729                .errors
10730                .is_empty()
10731        );
10732    }
10733
10734    #[test]
10735    fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
10736        let source = concat!(
10737            "import assert from 'node:assert/strict';\n",
10738            "export async function check() { assert.equal(await value(), 1); }\n",
10739        );
10740        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10741        assert_eq!(output.assertions, 1);
10742        assert!(output.code.contains("bindNodeAssertionPhase"));
10743        assert!(output.code.contains("await value()"));
10744        let allocator = Allocator::default();
10745        assert!(
10746            Parser::new(&allocator, &output.code, SourceType::mjs())
10747                .parse()
10748                .errors
10749                .is_empty()
10750        );
10751    }
10752
10753    #[test]
10754    fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
10755        let source = concat!(
10756            "import assert from 'node:assert/strict';\n",
10757            "export async function check(user?: string) {\n",
10758            "  assert.ok(user, await value());\n",
10759            "  assert\n",
10760            "    .equal(await value(), 1)\n",
10761            "}\n",
10762        );
10763        let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
10764        assert_eq!(output.assertions, 2);
10765        let code = &output.code;
10766        let lines: Vec<&str> = code.lines().collect();
10767        assert!(
10768            lines[2].starts_with(
10769                "  { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
10770            ),
10771            "{code}"
10772        );
10773        assert!(
10774            lines[2]
10775                .ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
10776            "{code}"
10777        );
10778        assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
10779        assert_eq!(
10780            lines[4], "     \"equal\"); __supercovAssertionPhase(await value(), 1) }",
10781            "{code}"
10782        );
10783        assert_eq!(lines[5], "}", "{code}");
10784        let allocator = Allocator::default();
10785        assert!(
10786            Parser::new(&allocator, code, SourceType::ts())
10787                .parse()
10788                .errors
10789                .is_empty()
10790        );
10791    }
10792
10793    #[test]
10794    fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
10795        let source = concat!(
10796            "import { expect, test } from '@playwright/test';\n",
10797            "test('value', async () => { expect(await value()).toBe(1); });\n",
10798        );
10799        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
10800        assert_eq!(output.assertions, 1);
10801        assert!(output.code.contains("bindNodeAssertionPhase"));
10802        assert!(output.code.contains("await value()"));
10803        let allocator = Allocator::default();
10804        assert!(
10805            Parser::new(&allocator, &output.code, SourceType::mjs())
10806                .parse()
10807                .errors
10808                .is_empty()
10809        );
10810    }
10811
10812    #[test]
10813    fn native_commonjs_assertion_bindings_are_attributed() {
10814        let source = concat!(
10815            "const assert = require('node:assert/strict');\n",
10816            "const { ok: verify } = require('assert');\n",
10817            "assert.equal(value(), 1);\n",
10818            "verify(other());\n",
10819        );
10820        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10821        assert_eq!(output.assertions, 2);
10822        assert!(output.code.contains("node:assert/strict.equal"));
10823        assert!(output.code.contains("node:assert.ok"));
10824    }
10825
10826    #[test]
10827    fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
10828        let source = concat!(
10829            "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
10830            "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
10831        );
10832        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10833        assert_eq!(output.assertions, 1);
10834        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10835    }
10836
10837    #[test]
10838    fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
10839        let source = concat!(
10840            "import assert from 'node:assert/strict';\n",
10841            "function unrelated(assert) { assert.equal(effect(), 1); }\n",
10842            "assert.equal(value(), 1);\n",
10843        );
10844        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10845        assert_eq!(output.assertions, 1);
10846        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10847    }
10848
10849    #[test]
10850    fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
10851        let source = concat!(
10852            "import test from 'node:test';\n",
10853            "import { expect as verify } from 'expect-library';\n",
10854            "test('value', () => verify(value()).not.toEqual(1));\n",
10855        );
10856        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10857        assert_eq!(output.assertions, 1);
10858        assert!(output.code.contains("expect.not.toEqual"));
10859        assert!(output.code.contains("verify(value()).not.toEqual(1)"));
10860    }
10861
10862    #[test]
10863    fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
10864        let source = concat!(
10865            "import { expect, test } from 'vitest';\n",
10866            "test('value', () => expect(value()).toBe(1));\n",
10867        );
10868        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10869        assert_eq!(output.assertions, 1);
10870        assert!(output.code.contains("expect.toBe"));
10871    }
10872
10873    #[test]
10874    fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
10875        // Vitest Browser Mode asserts through `expect.element(locator)`, and
10876        // `expect.soft`/`expect.poll` wrap the same way. The chain root is a
10877        // member call rather than a bare `expect` identifier, which is the
10878        // shape that used to leave every UI assertion unrecognised.
10879        for (call, operation) in [
10880            (
10881                "expect.element(button).toHaveAccessibleName('Copy')",
10882                "expect.element.toHaveAccessibleName",
10883            ),
10884            ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
10885            ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
10886        ] {
10887            let source = format!(
10888                "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
10889            );
10890            let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
10891            assert_eq!(output.assertions, 1, "{call}");
10892            assert!(output.code.contains(operation), "{call} -> {operation}");
10893        }
10894    }
10895
10896    #[test]
10897    fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
10898        // A JSX tree is one statement, so an attribute and a child that call
10899        // the same function had no way to be told apart -- which is exactly
10900        // what a UI assertion needs, `toHaveAccessibleName` naming the
10901        // attribute and `toHaveTextContent` the child.
10902        let source = concat!(
10903            "export function Button({ label, copied }) {\n",
10904            "  return (\n",
10905            "    <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
10906            "      {format(copied)}\n",
10907            "    </button>\n",
10908            "  );\n",
10909            "}\n",
10910        );
10911        let output = instrument_candidate(source, "src/button.tsx").unwrap();
10912        let rendered = output
10913            .points
10914            .iter()
10915            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10916            .collect::<Vec<_>>();
10917        // The attribute and the child are separate obligations at separate
10918        // positions, even though they read identically.
10919        assert_eq!(rendered.len(), 2, "{:?}", output.points);
10920        assert!(
10921            rendered
10922                .iter()
10923                .all(|point| point.source == "format(copied)")
10924        );
10925        assert_eq!(rendered[0].line, 3);
10926        assert_eq!(rendered[1].line, 4);
10927        assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
10928        // `{label}` is reached exactly when the tree is, and the handler is
10929        // measured where it is called; neither earns an obligation.
10930        assert!(!output.code.contains("title={__supercovRenderedValueV2"));
10931        // JSX forbids a bare comma operator, so the probe has to carry the
10932        // value through a call rather than a sequence expression.
10933        assert!(!output.code.contains("), format(copied)}"));
10934        assert!(output.code.contains(", format(copied))}"));
10935    }
10936
10937    #[test]
10938    fn instrumented_jsx_stays_parseable_through_every_container_shape() {
10939        // A JSX container cannot hold a bare comma operator, and a malformed
10940        // rewrite would surface only as the project's own build failing. Each
10941        // shape below reaches the rewrite by a different route.
10942        let source = concat!(
10943            "export function Panel({ items, user, error, tone }) {\n",
10944            "  return (\n",
10945            "    <>\n",
10946            "      <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
10947            "      <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
10948            "        {error ? <Alert text={message(error)} /> : null}\n",
10949            "        {items.map((item) => (\n",
10950            "          <Item key={item.id} label={format(item)} />\n",
10951            "        ))}\n",
10952            "        {count(items) > 0 && <Footer total={sum(items)} />}\n",
10953            "      </div>\n",
10954            "    </>\n",
10955            "  );\n",
10956            "}\n",
10957        );
10958        let output = instrument_candidate(source, "src/panel.tsx").unwrap();
10959        // The instrumented output must itself parse.
10960        instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
10961        // A spread attribute is not a container and is never rewritten.
10962        assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
10963        let rendered = output
10964            .points
10965            .iter()
10966            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10967            .map(|point| point.source.as_str())
10968            .collect::<Vec<_>>();
10969        assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
10970        assert!(
10971            rendered.contains(&"cx(\'a\', tone && \'b\')"),
10972            "{rendered:?}"
10973        );
10974        assert!(rendered.contains(&"format(item)"), "{rendered:?}");
10975        assert!(rendered.contains(&"message(error)"), "{rendered:?}");
10976        // `user?.id` short-circuits but is still a member access reached with
10977        // the statement, and a key that is a bare member chain is too.
10978        assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
10979        assert!(!rendered.contains(&"item.id"), "{rendered:?}");
10980    }
10981
10982    #[test]
10983    fn jest_global_expect_is_attributed_next_to_its_test_globals() {
10984        // Jest injects `expect`, `test`, `it` and `describe` into the test
10985        // environment; none is imported. The bare `expect` is the assertion
10986        // exactly when the file reaches for those test globals too.
10987        let source = "test('value', () => expect(value()).toBe(1));\n";
10988        let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
10989        assert_eq!(output.assertions, 1);
10990        assert!(output.code.contains("expect.toBe"));
10991        // A stray global `expect` in ordinary code is not one.
10992        let plain = "const ok = expect(value()).toBe(1);\n";
10993        let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
10994        assert_eq!(output.assertions, 0);
10995        assert_eq!(output.code, plain);
10996    }
10997
10998    #[test]
10999    fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
11000        let source = concat!(
11001            "import { expect, test } from '@playwright/test';\n",
11002            "test('value', () => expect(value()).toBe(1));\n",
11003        );
11004        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
11005        assert_eq!(output.assertions, 1);
11006        assert!(output.code.contains("expect.toBe"));
11007    }
11008
11009    #[test]
11010    fn an_arrow_that_returns_an_assertion_still_returns_it() {
11011        // `await loginError(page)` waits for the matcher only if the arrow
11012        // still returns it: made a block, it returned nothing at once.
11013        let source = concat!(
11014            "import { expect, test } from '@playwright/test';\n",
11015            "const loginError = (page: Page, error: string) => expect(page).toHaveURL(error);\n",
11016            "test('value', () => expect(value()).toBe(1));\n",
11017        );
11018        for file in ["tests/login.spec.ts", "tests/login.spec.js"] {
11019            let source = if file.ends_with(".js") {
11020                source.replace(": Page", "").replace(": string", "")
11021            } else {
11022                source.into()
11023            };
11024            let output = instrument_node_assertion_phases(&source, file).unwrap();
11025            assert_eq!(output.assertions, 2, "{file}");
11026            let code = output.code.split("\n//# sourceMappingURL").next().unwrap();
11027            assert!(!code.contains("=> { const"), "{file}:\n{code}");
11028            assert!(!code.contains("finally"), "{file}:\n{code}");
11029            assert!(
11030                code.contains("expect(page).toHaveURL(error)"),
11031                "{file}:\n{code}"
11032            );
11033        }
11034    }
11035
11036    #[test]
11037    fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
11038        let source = concat!(
11039            "import { browserTest, expect } from '@acme/browser-fixtures';\n",
11040            "browserTest('value', () => expect(value()).toBe(1));\n",
11041        );
11042        let output = instrument_node_assertion_phases_with_expect_modules(
11043            source,
11044            "tests/value.spec.mjs",
11045            &["@acme/browser-fixtures".into()],
11046        )
11047        .unwrap();
11048        assert_eq!(output.assertions, 1);
11049        assert!(output.code.contains("expect.toBe"));
11050    }
11051
11052    #[test]
11053    fn preserves_comment_payloads_byte_for_byte() {
11054        let comment = "/*---\ndescription: >\n  nested indentation is data\ninfo: |\n  first\n    second\n---*/";
11055        let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
11056        let output = instrument_candidate(&source, "app/comments.js").unwrap();
11057        assert!(output.code.contains(comment), "{}", output.code);
11058    }
11059
11060    #[test]
11061    fn source_map_destinations_follow_restored_comments() {
11062        let source = "function run() {\n  // first omitted comment\n  // second omitted comment\n  return 1;\n}\n";
11063        let output = analyze_candidate(source, "app/comment-map.js").unwrap();
11064        let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
11065        let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
11066        let return_token = map
11067            .get_tokens()
11068            .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
11069            .expect("return token mapping");
11070        let offset = Utf16LineIndex::new(&output.code).byte_offset(
11071            return_token.get_dst_line() as usize,
11072            return_token.get_dst_col() as usize,
11073        );
11074        assert!(
11075            output.code[offset..].starts_with("return"),
11076            "{}",
11077            output.code
11078        );
11079    }
11080
11081    #[test]
11082    fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
11083        // B10. A code generator rewrites `</script` to `<\/script` inside a
11084        // comment so the output cannot close an HTML <script> element early.
11085        // The restore compared the two spellings as different text, and because
11086        // it walks the source comments in order, one comment it could not
11087        // recognise consumed every remaining one and failed the file. That cost
11088        // a real project its coverage silently, and later its whole run.
11089        //
11090        // Prose about HTML is ordinary in web code, which is how this reached a
11091        // shipped release and stayed for months: the file was otherwise plain
11092        // TypeScript.
11093        for source in [
11094            "// </script>\nexport const x = 1;\n",
11095            "// </script\nexport const x = 1;\n",
11096            "// </SCRIPT>\nexport const x = 1;\n",
11097            "// see the </script> tag here\nexport const x = 1;\n",
11098            "/* block </script> here */\nexport const x = 1;\n",
11099            "/** jsdoc </script> */\nexport const x = 1;\n",
11100            // A run of comments, as the reported file had.
11101            "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
11102        ] {
11103            let output = instrument_candidate(source, "src/probe.ts")
11104                .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
11105            let original = source.lines().next().expect("a comment");
11106            assert!(
11107                output.code.contains(original),
11108                "comment was not restored verbatim: {original:?} missing from {:?}",
11109                output.code
11110            );
11111        }
11112        // The escape is the generator's, not the author's: a backslash the
11113        // author wrote survives as written rather than being collapsed.
11114        let written = "// author wrote <\\/script>\nexport const x = 1;\n";
11115        let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
11116        assert!(
11117            output.code.contains("// author wrote <\\/script>"),
11118            "{:?}",
11119            output.code
11120        );
11121        // Only that sequence is normalised. Nothing else a comment may contain
11122        // is treated as equal to a different spelling.
11123        assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
11124        assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
11125        assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
11126    }
11127
11128    #[test]
11129    fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
11130        // The counts in this error are usually equal, so reporting them alone
11131        // says the opposite of what happened -- nothing was dropped, one
11132        // comment was not recognised. The reported 116-vs-112 sent the first
11133        // reader looking for four missing comments that were never the cause.
11134        let error = CandidateError::CommentPreservation {
11135            expected: 116,
11136            actual: 112,
11137            detail:
11138                "no source comment matches the generated comment \"// x\" (matched 97 before it)"
11139                    .to_owned(),
11140        };
11141        let rendered = format!("{error:?}");
11142        assert!(rendered.contains("no source comment matches"), "{rendered}");
11143        assert!(rendered.contains("matched 97 before it"), "{rendered}");
11144    }
11145
11146    #[test]
11147    fn a_wrapped_default_keeps_the_type_its_syntax_names() {
11148        // Wrapped in a call, `strict = false` is no longer a default
11149        // `isolatedDeclarations` can read a type from, and h3 failed with
11150        // TS9011. The annotation says what was inferred before.
11151        let source = concat!(
11152            "export function f(a: number, strict = false, limit = 10, name = `x`, big = 1n, low = -1) { return a; }\n",
11153            "export function g<E extends 'utf8' | false = 'utf8'>(encoding = 'utf8' as E, list = [1], kept: string = 'a') { return encoding; }\n",
11154            "export const h = ({ open = false }, mode = 'x' as const) => open;\n",
11155        );
11156        let output = instrument_candidate(source, "src/defaults.ts").unwrap();
11157        for annotated in [
11158            "strict: boolean = ",
11159            "limit: number = ",
11160            "name: string = ",
11161            "big: bigint = ",
11162            "low: number = ",
11163            "encoding: E = ",
11164            "kept: string = ",
11165        ] {
11166            assert!(
11167                output.code.contains(annotated),
11168                "{annotated}\n{}",
11169                output.code
11170            );
11171        }
11172        // Nothing is said where the syntax names no type: an array, a
11173        // destructured parameter, `as const`.
11174        for bare in ["list = ", "mode = "] {
11175            assert!(output.code.contains(bare), "{bare}\n{}", output.code);
11176        }
11177        assert!(!output.code.contains("list:"), "{}", output.code);
11178        assert!(!output.code.contains("mode:"), "{}", output.code);
11179        // A JavaScript file has no annotations to give.
11180        let script = instrument_candidate(
11181            "export function f(strict = false) { return strict; }\n",
11182            "src/defaults.js",
11183        )
11184        .unwrap();
11185        assert!(!script.code.contains("strict:"), "{}", script.code);
11186    }
11187
11188    #[test]
11189    fn an_asserted_assignment_target_is_printed_as_a_target() {
11190        // `(buffer[0] as string) += child` came out as
11191        // `buffer[0] as string += child`, which does not parse: two of hono's
11192        // files could not be read by TypeScript or by a bundler.
11193        let source = concat!(
11194            "export function join(buffer: [unknown], child: string, n: { v?: number }) {\n",
11195            "  ;(buffer[0] as string) += child\n",
11196            "  ;(buffer[0] as string) = child\n",
11197            "  ;(n.v as number)++\n",
11198            "  ;(n.v satisfies number | undefined) = 1\n",
11199            "  ;[(buffer[0] as string)] = [child]\n",
11200            "  for ((buffer[0] as string) of [child]) {}\n",
11201            "  return buffer\n",
11202            "}\n",
11203        );
11204        let output = instrument_candidate(source, "src/targets.ts").unwrap();
11205        assert!(!output.code.contains(" as string"), "{}", output.code);
11206        assert!(!output.code.contains("satisfies"), "{}", output.code);
11207        assert!(
11208            output.code.contains("buffer[0] += child"),
11209            "{}",
11210            output.code
11211        );
11212        assert!(output.code.contains("n.v++"), "{}", output.code);
11213        let allocator = Allocator::default();
11214        let reparsed = Parser::new(&allocator, &output.code, SourceType::ts()).parse();
11215        assert!(
11216            reparsed.errors.is_empty(),
11217            "{:?}\n{}",
11218            reparsed.errors,
11219            output.code
11220        );
11221    }
11222
11223    #[test]
11224    fn a_comment_the_printer_moves_is_still_recognised() {
11225        // The printer writes a `@__NO_SIDE_EFFECTS__` annotation after the
11226        // documentation comment it stood before. The restore looked only
11227        // forward for each printed comment, did not find the annotation, and
11228        // zod's `v4/core/api.ts` could not be instrumented.
11229        let source = concat!(
11230            "// first\n",
11231            "// @__NO_SIDE_EFFECTS__\n",
11232            "/** documented\n */\n",
11233            "export function f(a: number) {\n",
11234            "  return a > 1 ? a : 0;\n",
11235            "}\n",
11236            "\n",
11237            "// second\n",
11238            "export type T = number;\n",
11239        );
11240        let output = instrument_candidate(source, "src/annotated.ts").unwrap();
11241        for comment in [
11242            "// first",
11243            "// @__NO_SIDE_EFFECTS__",
11244            "/** documented\n */",
11245            "// second",
11246        ] {
11247            assert_eq!(
11248                output.code.matches(comment).count(),
11249                1,
11250                "{comment}\n{}",
11251                output.code
11252            );
11253        }
11254    }
11255
11256    #[test]
11257    fn restored_comments_never_split_a_keyword_from_its_argument() {
11258        // oxc drops the comment that led a parenthesised return argument; the
11259        // restore used to put it back right after `return`, on its own line,
11260        // and automatic semicolon insertion turned the statement into `return;`
11261        // with the JSX unreachable. Vite then removed the unreachable tree and
11262        // every declaration only it used, so a component rendered nothing under
11263        // measurement while passing plainly. Expression-bodied arrows are hit
11264        // too, since instrumentation rewrites them into `return` blocks.
11265        let source = concat!(
11266            "export const A = ({ open }) => {\n",
11267            "  return (\n",
11268            "    // leading comment before JSX\n",
11269            "    <div aria-expanded={open}>a</div>\n",
11270            "  )\n",
11271            "}\n",
11272            "export const B = ({ open }) => (\n",
11273            "  // comment in an expression body\n",
11274            "  <div aria-expanded={open}>b</div>\n",
11275            ")\n",
11276            "export function C(x) {\n",
11277            "  return ( // inline line comment\n",
11278            "    x + 1\n",
11279            "  )\n",
11280            "}\n",
11281            "export function D(x) {\n",
11282            "  throw (\n",
11283            "    /* block\n       comment */\n",
11284            "    new Error(String(x))\n",
11285            "  )\n",
11286            "}\n",
11287            "export function E(x) {\n",
11288            "  return ( /* inline block */ x + 2 )\n",
11289            "}\n",
11290        );
11291        let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
11292        for keyword in ["return ", "throw "] {
11293            for (index, _) in output.code.match_indices(keyword) {
11294                let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
11295                let block_with_line_break = rest.starts_with("/*")
11296                    && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
11297                assert!(
11298                    !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
11299                    "`{keyword}` is separated from its argument:\n{}",
11300                    output.code
11301                );
11302            }
11303        }
11304        for comment in [
11305            "// leading comment before JSX",
11306            "// comment in an expression body",
11307            "// inline line comment",
11308            "/* block\n       comment */",
11309            "/* inline block */",
11310        ] {
11311            assert!(
11312                output.code.contains(comment),
11313                "{comment} was lost:\n{}",
11314                output.code
11315            );
11316        }
11317        assert!(
11318            output
11319                .code
11320                .contains("return <div aria-expanded={open}>a</div>"),
11321            "{}",
11322            output.code
11323        );
11324        assert!(
11325            output
11326                .code
11327                .contains("return <div aria-expanded={open}>b</div>"),
11328            "{}",
11329            output.code
11330        );
11331        // The restored output must still parse to the same program shape.
11332        let allocator = Allocator::default();
11333        let reparsed = Parser::new(
11334            &allocator,
11335            &output.code,
11336            SourceType::from_path("app/components/List.tsx").unwrap(),
11337        )
11338        .parse();
11339        assert!(
11340            reparsed.errors.is_empty(),
11341            "{:?}\n{}",
11342            reparsed.errors,
11343            output.code
11344        );
11345    }
11346
11347    #[test]
11348    fn preserves_reference_order_across_every_control_decision_kind() {
11349        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}";
11350        let output = instrument_candidate(source, "app/control.js").unwrap();
11351        assert_eq!(
11352            output
11353                .decisions
11354                .iter()
11355                .map(|decision| decision.kind.as_str())
11356                .collect::<Vec<_>>(),
11357            vec!["ternary", "while", "do-while", "for", "if"]
11358        );
11359    }
11360
11361    #[test]
11362    fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
11363        // B11. `for (const x of [a, b])` cannot iterate zero times, so the
11364        // outcome was an obligation no test could ever discharge -- the author
11365        // could only fake a test or delete the loop.
11366        let entered = |source: &str| {
11367            let output = analyze_candidate(source, "src/loops.ts").unwrap();
11368            output
11369                .branches
11370                .iter()
11371                .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
11372                .count()
11373        };
11374        // Folded: the iterable is certainly non-empty.
11375        for source in [
11376            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11377            "export function f(g) { for (const x of [1]) g(x); }\n",
11378            "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
11379            // A hole still yields once: [,].length === 1.
11380            "export function f(g) { for (const x of [,]) g(x); }\n",
11381            // A spread may contribute nothing, but a definite element is enough.
11382            "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
11383        ] {
11384            assert_eq!(entered(source), 0, "should be folded: {source}");
11385        }
11386        // Kept: the loop can genuinely run zero times.
11387        for source in [
11388            "export function f(g, ys) { for (const x of ys) g(x); }\n",
11389            "export function f(g) { for (const x of []) g(x); }\n",
11390            "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
11391            "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
11392            // Anything else may reach the binding, including a shrinking call.
11393            "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
11394            "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
11395            "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
11396            // A different binding of the same name must not be borrowed.
11397            concat!(
11398                "const xs = [1, 2];\n",
11399                "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
11400            ),
11401            // for-in is about keys, not elements, and is left alone.
11402            "export function f(g) { for (const k in [1, 2]) g(k); }\n",
11403        ] {
11404            assert_eq!(entered(source), 1, "should be kept: {source}");
11405        }
11406        // The loop body is still measured; only the impossible outcome is gone.
11407        let output = analyze_candidate(
11408            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11409            "src/loops.ts",
11410        )
11411        .unwrap();
11412        assert!(
11413            output
11414                .points
11415                .iter()
11416                .any(|point| point.source.contains("g(x)")),
11417            "the body must still be an obligation"
11418        );
11419    }
11420
11421    #[test]
11422    fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
11423        let source = concat!(
11424            "export function run(value) {\n",
11425            "  while (true) { break; }\n",
11426            "  do { value += 1; } while (false);\n",
11427            "  if (false) value += 2;\n",
11428            "  if (value || true) value += 3;\n",
11429            "  if (value && false) value += 4;\n",
11430            "  if (value) value += 5;\n",
11431            "  return value;\n",
11432            "}\n",
11433        );
11434        let output = analyze_candidate(source, "src/constants.js").unwrap();
11435        assert_eq!(output.decisions.len(), 1);
11436        assert_eq!(output.decisions[0].source, "value");
11437
11438        let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
11439        assert_eq!(instrumented.decisions.len(), 1);
11440        assert_eq!(instrumented.decisions[0].source, "value");
11441    }
11442
11443    #[test]
11444    fn leaves_compile_time_style_macro_arguments_as_source() {
11445        // StyleX evaluates these at build time and rejects a block-bodied
11446        // dynamic style; probing them broke a real project's Vite build.
11447        let source = concat!(
11448            "import * as stylex from '@stylexjs/stylex';\n",
11449            "const styles = stylex.create({\n",
11450            "  container: { display: 'flex' },\n",
11451            "  paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
11452            "  tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
11453            "});\n",
11454            "export function render(spacing: number) {\n",
11455            "  return stylex.props(styles.container, styles.paddingTop(spacing));\n",
11456            "}\n",
11457        );
11458        let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
11459        assert!(
11460            output.code.contains("=> ({ paddingTop: `${spacing}px` })")
11461                || output
11462                    .code
11463                    .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
11464            "dynamic style arrow must stay an expression body:\n{}",
11465            output.code
11466        );
11467        assert!(
11468            !output
11469                .decisions
11470                .iter()
11471                .any(|decision| decision.source.contains("dark")),
11472            "no decision may be collected inside the macro argument"
11473        );
11474        let function_points = output
11475            .points
11476            .iter()
11477            .filter(|point| point.kind == "function")
11478            .map(|point| point.source.as_str())
11479            .collect::<Vec<_>>();
11480        assert!(
11481            function_points
11482                .iter()
11483                .all(|source| source.contains("render")),
11484            "only the real function may carry a function point: {function_points:?}"
11485        );
11486        assert!(
11487            output.limitations.is_empty(),
11488            "compiled-away code is not a limitation: {:?}",
11489            output.limitations
11490        );
11491    }
11492
11493    #[test]
11494    fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
11495        let source = concat!(
11496            "declare global {\n",
11497            "  var Beacon: undefined | ((action: string) => void);\n",
11498            "}\n",
11499            "declare module 'virtual:feature' {\n",
11500            "  export const enabled: boolean;\n",
11501            "}\n",
11502            "declare const buildOnly: string;\n",
11503            "export const live = 1;\n",
11504        );
11505        let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
11506        let statements = output
11507            .points
11508            .iter()
11509            .filter(|point| point.kind == "statement")
11510            .map(|point| point.source.as_str())
11511            .collect::<Vec<_>>();
11512        assert_eq!(statements, ["const live = 1;"]);
11513        assert!(output.code.contains("const live = 1"));
11514    }
11515
11516    /// The instrumented code with what records a decision's outcome written
11517    /// `END(`, and the decisions' temporaries named `t0`, `t1`, … in the order
11518    /// they are declared.
11519    fn decision_shape(source: &str, file: &str) -> String {
11520        let output = instrument_candidate(source, file).unwrap();
11521        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11522        let call = format!("{}(", runtime.mcdc_end_v2);
11523        let start = output.code.find(&call).expect("an outcome is recorded") + call.len();
11524        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11525        let code = output
11526            .code
11527            .replace(&format!("{call}{probe_file}, "), "END(");
11528        let mut names: Vec<&str> = Vec::new();
11529        let mut shaped = String::new();
11530        let mut rest = code.as_str();
11531        while let Some(at) = rest.find("_supercovMcdc") {
11532            shaped.push_str(&rest[..at]);
11533            let tail = &rest[at..];
11534            // The runtime's own helpers are bound to `__supercovMcdc…`.
11535            if shaped.ends_with('_') {
11536                shaped.push_str("_supercovMcdc");
11537                rest = &tail["_supercovMcdc".len()..];
11538                continue;
11539            }
11540            let length = tail
11541                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
11542                .unwrap_or(tail.len());
11543            let name = &tail[..length];
11544            let index = names
11545                .iter()
11546                .position(|known| *known == name)
11547                .unwrap_or_else(|| {
11548                    names.push(name);
11549                    names.len() - 1
11550                });
11551            shaped.push_str(&format!("t{index}"));
11552            rest = &tail[length..];
11553        }
11554        shaped.push_str(rest);
11555        shaped
11556    }
11557
11558    #[test]
11559    fn a_decision_stays_the_test_it_was() {
11560        // TypeScript narrows through `&&` and `||` only where they are the
11561        // condition itself, through an assignment's right side, and through a
11562        // comma's last operand. The decision used to be the right side of an
11563        // assignment inside a comma, and each condition ended in its
11564        // temporary: `if (client) return client` returned `Client | null`,
11565        // and a file Supercov never touched failed to compile against it.
11566        let shaped = decision_shape(
11567            concat!(
11568                "export function pick(client: { id: number } | null, strict: boolean) {\n",
11569                "  if (client && strict) return client;\n",
11570                "  return null;\n",
11571                "}\n",
11572                "export function size(list: string[] | null) {\n",
11573                "  if (!list) { return 0; } else { return list.length; }\n",
11574                "}\n",
11575                "export function first(list: string[] | null) {\n",
11576                "  return list ? list[0] : undefined;\n",
11577                "}\n",
11578            ),
11579            "src/pick.ts",
11580        );
11581        for expected in [
11582            // The bookkeeping rides inside the first condition, and every
11583            // condition ends in the assignment of its own value.
11584            "if ((t0 = 1, t2 = 0, t1 = client) && (t0 += 3, t2 = strict)) {\n\t\t\tEND(0, t0 + +!!t1 + 3 * +!!t2, true), t0 = 0;",
11585            // Without an `else` there is nowhere inside the statement for the
11586            // false outcome, and an `else` made for it would be a branch to
11587            // a coverage tool the tests run: it is recorded after the `if`,
11588            // when the frame says it has not been.
11589            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1 + 3 * +!!t2, false);\n\t}",
11590            "if (t3 = 1, t4 = !list) {\n\t\tEND(1, t3 + +!!t4, true);",
11591            "} else {\n\t\tEND(1, t3 + +!!t4, false);",
11592            "return (t5 = 1, t6 = list) ? (void END(2, t5 + +!!t6, true), list[0]) : (void END(2, t5 + +!!t6, false), undefined);",
11593        ] {
11594            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11595        }
11596    }
11597
11598    #[test]
11599    fn a_loop_records_its_last_test_after_itself() {
11600        // A loop's true outcome is recorded where its body starts. Nothing
11601        // runs when the test is false, so the statement after the loop
11602        // records that, and only when the test, not a `break`, ended it.
11603        let shaped = decision_shape(
11604            concat!(
11605                "export function scan(limit: number) {\n",
11606                "  let i = 0;\n",
11607                "  walk: while (i < limit) { i++; if (i > 5) break walk; }\n",
11608                "  do i--; while (i > 0);\n",
11609                "  for (let j = 0; j < limit; j++) i += j;\n",
11610                "  return i;\n",
11611                "}\n",
11612            ),
11613            "src/scan.ts",
11614        );
11615        for expected in [
11616            // The label stays on its loop, for `continue walk`.
11617            "{\n\t\twalk: while (t0 = 1, t1 = i < limit) {\n\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11618            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1, false);\n\t}",
11619            // A `do … while` body runs before its test: the frame says whether
11620            // one came before this iteration, and starts cleared.
11621            "{\n\t\tt4 = 0;\n\t\tdo {\n\t\t\tt4 && (END(2, t4 + +!!t5, true), t4 = 0);",
11622            "} while (t4 = 1, t5 = i > 0);\n\t\tt4 && END(2, t4 + +!!t5, false);\n\t}",
11623            "for (let j = 0; t6 = 1, t7 = j < limit; j++) {\n\t\t\tEND(3, t6 + +!!t7, true), t6 = 0;",
11624            "\t\t}\n\t\tt6 && END(3, t6 + +!!t7, false);\n\t}",
11625        ] {
11626            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11627        }
11628    }
11629
11630    #[test]
11631    fn a_decision_without_a_function_body_around_it_declares_its_own_temporaries() {
11632        let shaped = decision_shape(
11633            concat!(
11634                "export function pick(\n",
11635                "  n: number,\n",
11636                "  kind = class { static label = 'none'; static { if (n > 1) this.label = 'some'; } },\n",
11637                "  tag = n > 5 ? 'high' : 'low',\n",
11638                ") {\n",
11639                "  return [kind.label, tag];\n",
11640                "}\n",
11641            ),
11642            "src/defaults.ts",
11643        );
11644        for expected in [
11645            "static {\n\t\tlet t0, t1;\n",
11646            "if (t0 = 1, t1 = n > 1) {\n\t\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11647            // A parameter default has no body to declare in: the arrow holds
11648            // the whole conditional expression, so its branches still see
11649            // what the test narrowed.
11650            "(() => {\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})()",
11651        ] {
11652            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11653        }
11654    }
11655
11656    #[test]
11657    fn a_top_level_declaration_has_a_line_of_its_own_before_it() {
11658        // See `separate_top_level_declarations`. An overload signature stays
11659        // next to its implementation.
11660        let output = instrument_candidate(
11661            concat!(
11662                "const one = 1;\n",
11663                "function plain() { return one; }\n",
11664                "class Box {}\n",
11665                "function over(value: string): string;\n",
11666                "function over(value: number): number;\n",
11667                "function over(value: any) { return value; }\n",
11668                "export function exported() { return plain(); }\n",
11669            ),
11670            "src/declarations.ts",
11671        )
11672        .unwrap();
11673        for expected in [
11674            "\n;\nfunction plain() {",
11675            "\n;\nclass Box {",
11676            "\n;\nfunction over(value: string): string;\nfunction over(value: number): number;\nfunction over(value: any) {",
11677        ] {
11678            assert!(
11679                output.code.contains(expected),
11680                "{expected}\n{}",
11681                output.code
11682            );
11683        }
11684        assert!(
11685            !output.code.contains(";\nexport function"),
11686            "{}",
11687            output.code
11688        );
11689    }
11690
11691    #[test]
11692    fn exposes_the_complete_probe_v2_instrumenter_contract() {
11693        let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
11694        assert!(output.complete);
11695        assert!(output.limitations.is_empty());
11696        assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
11697        let runtime = output.runtime.expect("candidate runtime binding");
11698        assert!(output.code.contains(&runtime.mcdc_end_v2));
11699        assert!(output.code.contains("_supercovMcdcFrame"));
11700        assert!(output.code.contains("_supercovMcdcValue"));
11701        assert_eq!(output.decisions.len(), 1);
11702        assert!(output.points.iter().any(|point| point.kind == "statement"));
11703        assert!(output.points.iter().any(|point| point.kind == "function"));
11704
11705        let allocator = Allocator::default();
11706        let reparsed = Parser::new(
11707            &allocator,
11708            &output.code,
11709            SourceType::from_path("app/decide.ts").unwrap(),
11710        )
11711        .parse();
11712        assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
11713    }
11714
11715    #[test]
11716    fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
11717        let source = concat!(
11718            "import type { Session } from './types.ts';\n",
11719            "import { type Row } from './rows.ts';\n",
11720            "import './register.ts';\n",
11721            "const value = 1;\n",
11722        );
11723        let output = instrument_candidate(source, "app/imports.ts").unwrap();
11724        let statement_lines = output
11725            .points
11726            .iter()
11727            .filter(|point| point.kind == "statement")
11728            .map(|point| point.line)
11729            .collect::<Vec<_>>();
11730        assert_eq!(statement_lines, vec![2, 3, 4]);
11731    }
11732
11733    #[test]
11734    fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
11735        let source = concat!(
11736            "import { Logger } from './types.js';\n",
11737            "import DefaultType from './default.js';\n",
11738            "import * as Types from './namespace.js';\n",
11739            "import { type Inline } from './inline.js';\n",
11740            "import { run, type Config } from './mixed.js';\n",
11741            "import './register.js';\n",
11742            "import {} from './empty.js';\n",
11743            "import { unused } from './unused.js';\n",
11744            "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
11745        );
11746        let preserved =
11747            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", false)
11748                .unwrap();
11749        let erased =
11750            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true).unwrap();
11751        assert_eq!(
11752            erased
11753                .excluded_statements
11754                .iter()
11755                .map(|p| p.line)
11756                .collect::<Vec<_>>(),
11757            vec![1, 2, 3, 4]
11758        );
11759        let excluded = erased
11760            .excluded_statements
11761            .iter()
11762            .map(|p| &p.id)
11763            .collect::<HashSet<_>>();
11764        let expected = preserved
11765            .points
11766            .iter()
11767            .filter(|p| !excluded.contains(&p.id))
11768            .collect::<Vec<_>>();
11769        assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
11770        for line in [5, 6, 7, 8] {
11771            assert!(erased.points.iter().any(|p| p.line == line));
11772        }
11773        // JavaScript value imports and type queries with runtime uses remain.
11774        let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
11775        let mixed = instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true)
11776            .unwrap();
11777        assert!(mixed.excluded_statements.is_empty());
11778    }
11779
11780    /// A line's first mapping: `None` for no segment, `Some(None)` for an
11781    /// unmapped one, else the source line and column it names.
11782    type FirstMapping = Option<Option<(u32, u32)>>;
11783
11784    /// The instrumented copy's lines with their first mapping.
11785    fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
11786        let map = oxc_sourcemap::SourceMap::from_json_string(
11787            &output
11788                .map
11789                .as_ref()
11790                .expect("instrumented copy has a map")
11791                .to_string(),
11792        )
11793        .unwrap();
11794        let tokens = map.get_tokens().collect::<Vec<_>>();
11795        output
11796            .code
11797            .split('\n')
11798            .enumerate()
11799            .map(|(line, text)| {
11800                let first = tokens
11801                    .iter()
11802                    .find(|token| token.get_dst_line() == line as u32)
11803                    .map(|token| {
11804                        token
11805                            .get_source_id()
11806                            .map(|_| (token.get_src_line(), token.get_src_col()))
11807                    });
11808                (text.trim().to_string(), first)
11809            })
11810            .collect()
11811    }
11812
11813    #[test]
11814    fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
11815        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";
11816        let output = instrument_candidate(source, "app/f.js").unwrap();
11817        let lines = first_mappings(&output);
11818        let runtime = output.runtime.as_ref().unwrap();
11819        for (text, first) in &lines {
11820            // A probe maps to nothing: a coverage tool reading the copy
11821            // through its map counts no branch of the user's for its `||`.
11822            if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
11823                && let Some(first) = first
11824            {
11825                assert_eq!(*first, None, "{text}");
11826            }
11827        }
11828        let at = |needle: &str| {
11829            lines
11830                .iter()
11831                .find(|(text, _)| text.starts_with(needle))
11832                .unwrap_or_else(|| panic!("{needle} in {}", output.code))
11833                .1
11834        };
11835        // Each statement after a block Supercov added maps at its own start,
11836        // where the block's brace used to take that mapping from it.
11837        assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
11838        assert_eq!(at("continue;"), Some(Some((4, 25))));
11839        assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
11840        // The arrow's body, made a block, returns what the expression it was
11841        // evaluated: the line maps where that expression starts.
11842        assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
11843        // Every line of the program starting with a closing brace maps where
11844        // it starts, so a tool that looks a position up on its own line finds
11845        // a mapping.
11846        let program = lines
11847            .iter()
11848            .position(|(text, _)| text.starts_with("function f("))
11849            .unwrap();
11850        for (text, first) in &lines[program..] {
11851            if text.starts_with('}') {
11852                assert!(first.is_some(), "{text} in {}", output.code);
11853            }
11854        }
11855    }
11856
11857    /// The instrumented code with the calls that record a value tree written
11858    /// `END2(`, `END(` and `HIT(`, and the trees' frames named `s0`, `s1`, …
11859    fn value_shape(source: &str, file: &str) -> String {
11860        let output = instrument_candidate(source, file).unwrap();
11861        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11862        let call = format!("{}(", runtime.select_end2_v2);
11863        let start = output.code.find(&call).expect("a value tree is recorded") + call.len();
11864        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11865        let mut code = output.code.clone();
11866        for (helper, short) in [
11867            (&runtime.select_end2_v2, "END2("),
11868            (&runtime.select_end_v2, "END("),
11869            (&runtime.select_next_v2, "NEXT("),
11870            (&runtime.coverage_hit_v2, "HIT("),
11871            (&runtime.select_right_v2, "RIGHT("),
11872            (&runtime.select_short_v2, "SHORT("),
11873        ] {
11874            code = code.replace(&format!("{helper}({probe_file}, "), short);
11875        }
11876        let mut names: Vec<&str> = Vec::new();
11877        let mut shaped = String::new();
11878        let mut rest = code.as_str();
11879        while let Some(at) = rest.find("_supercovSelected") {
11880            shaped.push_str(&rest[..at]);
11881            let tail = &rest[at..];
11882            let length = tail
11883                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
11884                .unwrap_or(tail.len());
11885            let name = &tail[..length];
11886            let index = names
11887                .iter()
11888                .position(|known| *known == name)
11889                .unwrap_or_else(|| {
11890                    names.push(name);
11891                    names.len() - 1
11892                });
11893            shaped.push_str(&format!("s{index}"));
11894            rest = &tail[length..];
11895        }
11896        shaped.push_str(rest);
11897        shaped
11898    }
11899
11900    /// Every tree of up to `leaves` leaves over the three operators, with
11901    /// its selections' points 10 apart.
11902    fn selection_shapes(
11903        leaves: usize,
11904        next_leaf: &mut usize,
11905        next_first: &mut usize,
11906    ) -> Vec<(SelectionShape, usize, usize)> {
11907        if leaves == 1 {
11908            let shape = SelectionShape::Leaf(*next_leaf);
11909            return vec![(shape, *next_leaf + 1, *next_first)];
11910        }
11911        let mut shapes = Vec::new();
11912        for left_leaves in 1..leaves {
11913            for kind in 0..3 {
11914                let first = *next_first;
11915                let mut after_first = first + 10;
11916                for (left, leaf_after_left, first_after_left) in
11917                    selection_shapes(left_leaves, next_leaf, &mut after_first)
11918                {
11919                    let mut leaf = leaf_after_left;
11920                    let mut first_right = first_after_left;
11921                    for (right, leaf_after, first_after) in
11922                        selection_shapes(leaves - left_leaves, &mut leaf, &mut first_right)
11923                    {
11924                        shapes.push((
11925                            SelectionShape::Selection {
11926                                first,
11927                                kind,
11928                                left: Box::new(copy_shape(&left)),
11929                                right: Box::new(right),
11930                            },
11931                            leaf_after,
11932                            first_after,
11933                        ));
11934                    }
11935                }
11936            }
11937        }
11938        shapes
11939    }
11940
11941    fn copy_shape(shape: &SelectionShape) -> SelectionShape {
11942        match shape {
11943            SelectionShape::Leaf(leaf) => SelectionShape::Leaf(*leaf),
11944            SelectionShape::Selection {
11945                first,
11946                kind,
11947                left,
11948                right,
11949            } => SelectionShape::Selection {
11950                first: *first,
11951                kind: *kind,
11952                left: Box::new(copy_shape(left)),
11953                right: Box::new(copy_shape(right)),
11954            },
11955        }
11956    }
11957
11958    /// A leaf's steps, as `LogicalValueTransformer::shape` builds them.
11959    fn leaf_steps(shape: &SelectionShape, path: &[usize], steps: &mut Vec<Vec<usize>>) {
11960        match shape {
11961            SelectionShape::Leaf(_) => steps.push(path.to_vec()),
11962            SelectionShape::Selection {
11963                first,
11964                kind,
11965                left,
11966                right,
11967            } => {
11968                let mut left_path = vec![first * 4 + kind];
11969                left_path.extend_from_slice(path);
11970                let mut right_path = vec![first * 4 + 3];
11971                right_path.extend_from_slice(path);
11972                leaf_steps(left, &left_path, steps);
11973                leaf_steps(right, &right_path, steps);
11974            }
11975        }
11976    }
11977
11978    /// Evaluate a tree over values that are truthy (2), falsy (1) or nullish
11979    /// (0): its value, and the leaves evaluated, in order.
11980    fn evaluate(shape: &SelectionShape, values: &[u8], evaluated: &mut Vec<usize>) -> u8 {
11981        match shape {
11982            SelectionShape::Leaf(leaf) => {
11983                evaluated.push(*leaf);
11984                values[*leaf]
11985            }
11986            SelectionShape::Selection {
11987                kind, left, right, ..
11988            } => {
11989                let value = evaluate(left, values, evaluated);
11990                let short = match kind {
11991                    0 => value == 2,
11992                    1 => value != 2,
11993                    _ => value != 0,
11994                };
11995                if short {
11996                    value
11997                } else {
11998                    evaluate(right, values, evaluated)
11999                }
12000            }
12001        }
12002    }
12003
12004    /// What the runtime records for a leaf's steps and its value.
12005    fn record_steps(steps: &[usize], value: u8, recorded: &mut Vec<usize>) {
12006        for step in steps {
12007            let (first, code) = (step / 4, step % 4);
12008            let truthy = usize::from(value == 2);
12009            if code == 3 {
12010                recorded.push(first + 2 + truthy);
12011            } else if match code {
12012                0 => value == 2,
12013                1 => value != 2,
12014                _ => value != 0,
12015            } {
12016                recorded.push(first + truthy);
12017            } else {
12018                break;
12019            }
12020        }
12021    }
12022
12023    #[test]
12024    fn a_value_tree_records_what_a_call_around_each_leaf_recorded() {
12025        // The points a leaf decided are written out where evaluation goes
12026        // on from it, from the tree's shape alone. Every tree of up to five
12027        // leaves, under every combination of truthy, falsy and nullish
12028        // values, records the points `selectPathV2` recorded around each
12029        // leaf, in the same order.
12030        let mut checked = 0;
12031        for leaves in 2..=5 {
12032            for (shape, ..) in selection_shapes(leaves, &mut 0, &mut 0) {
12033                let mut steps = Vec::new();
12034                leaf_steps(&shape, &[], &mut steps);
12035                let mut before = vec![Vec::new(); leaves];
12036                shape.before(&mut before);
12037                for combination in 0..3_usize.pow(leaves as u32) {
12038                    let values = (0..leaves)
12039                        .map(|leaf| (combination / 3_usize.pow(leaf as u32) % 3) as u8)
12040                        .collect::<Vec<_>>();
12041                    let mut evaluated = Vec::new();
12042                    evaluate(&shape, &values, &mut evaluated);
12043                    let mut each_leaf = Vec::new();
12044                    for leaf in &evaluated {
12045                        record_steps(&steps[*leaf], values[*leaf], &mut each_leaf);
12046                    }
12047                    let mut tree = Vec::new();
12048                    for pair in evaluated.windows(2) {
12049                        // As `recorded_before` writes it: the only leaf that
12050                        // can come before, or the one the frame names.
12051                        let deciders = &before[pair[1]];
12052                        let (_, points) = match &deciders[..] {
12053                            [only] => only,
12054                            several => several
12055                                .iter()
12056                                .find(|(leaf, _)| *leaf == pair[0])
12057                                .expect("the leaf before is one the table has"),
12058                        };
12059                        tree.extend(points);
12060                    }
12061                    let last = *evaluated.last().unwrap();
12062                    record_steps(&steps[last], values[last], &mut tree);
12063                    assert_eq!(tree, each_leaf, "{values:?} over {steps:?}");
12064                    assert!(
12065                        evaluated
12066                            .windows(2)
12067                            .all(|pair| before[pair[1]].iter().any(|(leaf, _)| *leaf == pair[0])),
12068                        "{values:?} over {steps:?}: {before:?}"
12069                    );
12070                    checked += 1;
12071                }
12072            }
12073        }
12074        assert!(checked > 100_000, "{checked}");
12075    }
12076
12077    #[test]
12078    fn a_constant_keeps_its_tree_and_a_named_chain_its_name() {
12079        let shaped = value_shape(
12080            concat!(
12081                "export function text(value: string | number) {\n",
12082                "  const isText = typeof value === 'string' && value.length > 0;\n",
12083                "  if (isText) return value;\n",
12084                "  return null;\n",
12085                "}\n",
12086                "export const both = text(1) ?? text('a');\n",
12087                "export class Owner {\n",
12088                "  pet?: { name: string };\n",
12089                "  named(other?: Owner) { return [other?.pet, this?.pet, this.pet?.name]; }\n",
12090                "}\n",
12091            ),
12092            "src/alias.ts",
12093        );
12094        for expected in [
12095            // TypeScript narrows through a constant only when the condition
12096            // is its initializer: the value is recorded by the statement
12097            // after, from the constant.
12098            "const isText = (s1 = 0, typeof value === \"string\") && (s1 = 1, value.length > 0);\n\tEND2(9, isText, s1);\n",
12099            "export const both = (s0 = 0, text(1)) ?? (s0 = 1, text(\"a\"));\nEND2(13, both, s0);\n",
12100            // A name and `this` are read again after the call that records
12101            // them, so the chain narrows them.
12102            ", other)?.pet",
12103            ", this)?.pet",
12104        ] {
12105            assert!(shaped.contains(expected), "{expected}\n{shaped}");
12106        }
12107        // A member may run a getter: it is read once, through the call.
12108        assert!(shaped.contains(", this.pet)?.name"), "{shaped}");
12109        assert!(!shaped.contains(", this.pet), this.pet)"), "{shaped}");
12110    }
12111
12112    #[test]
12113    fn a_value_tree_keeps_its_leaves_the_programs_own_expressions() {
12114        // With a call around each leaf, `user.name` was read from a `user`
12115        // nothing had narrowed: `any`, when a member of its type has no
12116        // `name`, for everything that reads the value. A leaf is the last
12117        // operand of a comma now, and the call goes around the tree.
12118        let shaped = value_shape(
12119            concat!(
12120                "export function name(user: { name: string } | false) {\n",
12121                "  return user && user.name;\n",
12122                "}\n",
12123                "export function ok(total: number, name: string) {\n",
12124                "  return (total > 0 && name !== 'x') || !!name;\n",
12125                "}\n",
12126                "export function decided(total: number, name: string, force: boolean) {\n",
12127                "  return (total > 0 && name !== 'x') || force;\n",
12128                "}\n",
12129                "export function pick(a: number | null, b: number | null, c: number) {\n",
12130                "  return a ?? b ?? c;\n",
12131                "}\n",
12132                "export function held(list: string[] | null, fallback = list || []) {\n",
12133                "  return fallback;\n",
12134                "}\n",
12135            ),
12136            "src/values.ts",
12137        );
12138        for expected in [
12139            "let s0: any;\n",
12140            "return END2(12, (s0 = 0, user) && (s0 = 1, user.name), s0);",
12141            // More than two leaves: the tree's own operators stay between
12142            // them, so a coverage tool the tests run reads one branch, and
12143            // what `total > 0` or `name !== 'x'` decided is recorded where
12144            // evaluation went on to `!!name`, by which of them it left:
12145            // the runtime looks that up, because a choice written out here
12146            // would be a branch of this line to that tool. The compound left
12147            // operand is a decision too: its conditions are recorded in the
12148            // leaves and its outcome after the tree, from their values with
12149            // `&`, so the tree stays one tree and narrows as written.
12150            "(s1 = 0, _supercovMcdcFrame27 = 1, _supercovMcdcValue29 = 0, _supercovMcdcValue28 = total > 0) && (s1 = 1, _supercovMcdcFrame27 += 3, _supercovMcdcValue29 = name !== \"x\") || (NEXT(0, s1, 2), s1 = 2, !!name), s1)",
12151            "!!(+!!_supercovMcdcValue28 & +!!_supercovMcdcValue29)), _supercovMcdcResult30;",
12152            "|| (NEXT(1, s2, 2), s2 = 2, force), s2)",
12153            "kind: \"value\"",
12154            // Only `b` can come before `c`: a first `??` that kept `a` ends
12155            // the second.
12156            "return END(2, (s3 = 0, a) ?? (s3 = 1, b) ?? (HIT(34), s3 = 2, c), s3);",
12157            // A parameter list cannot see a frame its function's body
12158            // declares: the leaves keep their calls there.
12159            "SHORT(40, list, 0) || RIGHT(40, [])",
12160        ] {
12161            assert!(shaped.contains(expected), "{expected}\n{shaped}");
12162        }
12163    }
12164
12165    #[test]
12166    fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
12167        let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
12168        for file in ["app/fill.js", "app/fill.ts"] {
12169            let output = instrument_candidate(source, file).unwrap();
12170            let runtime = output.runtime.expect("candidate runtime binding");
12171            // No frame per evaluation: the target is written by the program's
12172            // own operator, once.
12173            assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
12174            assert!(output.code.contains("||= "), "{}", output.code);
12175            assert!(output.code.contains("??= "), "{}", output.code);
12176            assert!(output.code.contains("&&= "), "{}", output.code);
12177            assert_eq!(
12178                output
12179                    .code
12180                    .matches(&format!("{}(", runtime.select_assign_end_v2))
12181                    .count(),
12182                3
12183            );
12184            assert_eq!(
12185                output
12186                    .code
12187                    .matches(&format!("{}(", runtime.select_named_right_v2))
12188                    .count(),
12189                1
12190            );
12191            let allocator = Allocator::default();
12192            let reparsed = Parser::new(
12193                &allocator,
12194                &output.code,
12195                SourceType::from_path(file).unwrap(),
12196            )
12197            .parse();
12198            assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
12199        }
12200    }
12201
12202    #[test]
12203    fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
12204        let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
12205        let output = instrument_candidate(source, "app/collisions.js").unwrap();
12206        let runtime = output.runtime.expect("candidate runtime binding");
12207        assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
12208        assert!(!output.code.contains("let _supercovMcdcFrame1,"));
12209    }
12210
12211    #[test]
12212    fn instruments_wider_decisions_with_the_exact_v1_fallback() {
12213        let predicate = (0..33)
12214            .map(|index| format!("c{index}"))
12215            .collect::<Vec<_>>()
12216            .join(" && ");
12217        let source = format!("if ({predicate}) work();");
12218        let output = instrument_candidate(&source, "app/wide.js").unwrap();
12219        assert_eq!(output.decisions[0].conditions.len(), 33);
12220        let runtime = output.runtime.expect("candidate runtime binding");
12221        assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
12222        assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
12223        assert!(
12224            output
12225                .code
12226                .contains(&format!("{}(", runtime.mcdc_condition))
12227        );
12228        assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
12229    }
12230
12231    #[test]
12232    fn wraps_framework_request_exports_without_changing_the_public_api() {
12233        for (file, source, export_prefix) in [
12234            (
12235                "app/routes/example.ts",
12236                "export const loader = async ({ request }) => request.url;",
12237                "export const loader = ",
12238            ),
12239            (
12240                "app/routes/example.ts",
12241                "export async function action({ request }) { return request.method; }",
12242                "export const action = ",
12243            ),
12244            (
12245                "app/api/items/route.ts",
12246                "export function GET(request) { return Response.json({ url: request.url }); }",
12247                "export const GET = ",
12248            ),
12249            (
12250                "app/routes/example.ts",
12251                "export { generateAction as action } from './generateAction';",
12252                "export const action = ",
12253            ),
12254            (
12255                "app/entry.server.tsx",
12256                "export default async function handleRequest(request) { return request.url; }",
12257                "export default ",
12258            ),
12259        ] {
12260            let output = instrument_candidate(source, file).unwrap();
12261            let runtime = output.runtime.as_ref().expect("runtime binding");
12262            assert!(
12263                output
12264                    .code
12265                    .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
12266                "{file}: {}",
12267                output.code
12268            );
12269            assert!(
12270                output.code.contains(&format!(
12271                    "withRequestPhase as {}",
12272                    runtime.with_request_phase
12273                )),
12274                "{file}: {}",
12275                output.code
12276            );
12277            let allocator = Allocator::default();
12278            let reparsed = Parser::new(
12279                &allocator,
12280                &output.code,
12281                SourceType::from_path(file).unwrap(),
12282            )
12283            .parse();
12284            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
12285        }
12286    }
12287
12288    #[test]
12289    fn parse_failures_are_explicit_and_never_claim_completeness() {
12290        assert!(matches!(
12291            analyze_candidate("if (", "broken.js"),
12292            Err(CandidateError::Parse(errors)) if !errors.is_empty()
12293        ));
12294        assert!(matches!(
12295            analyze_candidate("let value = 1", "unknown.extension"),
12296            Err(CandidateError::UnknownSourceType(_))
12297        ));
12298    }
12299}