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        // Anywhere under `app/`, `app/route.ts` (the handler for `/`) too.
6838        components[..components.len().saturating_sub(1)].contains(&"app")
6839    }
6840
6841    fn is_handler_name(&self, name: &str) -> bool {
6842        (self.is_remix_route() && matches!(name, "loader" | "action"))
6843            || (self.is_next_route()
6844                && matches!(
6845                    name,
6846                    "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
6847                ))
6848    }
6849
6850    fn identifier(&self, name: &str) -> Expression<'a> {
6851        self.ast
6852            .expression_identifier(Span::default(), self.ast.ident(name))
6853    }
6854
6855    fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
6856        self.ast.expression_call(
6857            Span::default(),
6858            self.identifier(&self.with_request_phase),
6859            NONE,
6860            self.ast.vec1(Argument::from(expression)),
6861            false,
6862        )
6863    }
6864
6865    fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
6866        Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
6867            Span::default(),
6868            Some(self.ast.declaration_variable(
6869                Span::default(),
6870                VariableDeclarationKind::Const,
6871                self.ast.vec1(self.ast.variable_declarator(
6872                    Span::default(),
6873                    VariableDeclarationKind::Const,
6874                    self.ast.binding_pattern_binding_identifier(
6875                        Span::default(),
6876                        self.ast.ident(exported_name),
6877                    ),
6878                    NONE,
6879                    Some(self.wrap_expression(self.identifier(original_name))),
6880                    false,
6881                )),
6882                false,
6883            )),
6884            self.ast.vec(),
6885            None,
6886            ImportOrExportKind::Value,
6887            NONE,
6888        ))
6889    }
6890
6891    fn transform_named_export(
6892        &mut self,
6893        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
6894        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6895    ) {
6896        if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
6897            for declarator in &mut declaration.declarations {
6898                let Some(name) = binding_identifier_name(&declarator.id) else {
6899                    continue;
6900                };
6901                if !self.is_handler_name(&name) {
6902                    continue;
6903                }
6904                if let Some(initializer) = &mut declarator.init {
6905                    let original = initializer.take_in(self.ast.allocator);
6906                    *initializer = self.wrap_expression(original);
6907                    self.used = true;
6908                }
6909            }
6910            output.push(Statement::ExportNamedDeclaration(export));
6911            return;
6912        }
6913
6914        if matches!(
6915            export.declaration,
6916            Some(Declaration::FunctionDeclaration(_))
6917        ) {
6918            let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
6919            else {
6920                unreachable!();
6921            };
6922            let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
6923                output.push(Statement::FunctionDeclaration(function));
6924                return;
6925            };
6926            if !self.is_handler_name(&exported_name) {
6927                export.declaration = Some(Declaration::FunctionDeclaration(function));
6928                output.push(Statement::ExportNamedDeclaration(export));
6929                return;
6930            }
6931            let original_name = self
6932                .names
6933                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6934            function.id = Some(
6935                self.ast
6936                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
6937            );
6938            output.push(Statement::FunctionDeclaration(function));
6939            output.push(self.wrapped_named_export(&exported_name, &original_name));
6940            self.used = true;
6941            return;
6942        }
6943
6944        if export.source.is_none() {
6945            output.push(Statement::ExportNamedDeclaration(export));
6946            return;
6947        }
6948        let source = export
6949            .source
6950            .as_ref()
6951            .expect("checked above")
6952            .clone_in(self.ast.allocator);
6953        let specifiers = export.specifiers.take_in(self.ast.allocator);
6954        let mut untouched = self.ast.vec();
6955        let mut handlers = Vec::new();
6956        for specifier in specifiers {
6957            let exported_name = specifier
6958                .exported
6959                .identifier_name()
6960                .map(|name| name.to_string());
6961            if exported_name
6962                .as_deref()
6963                .is_some_and(|name| self.is_handler_name(name))
6964            {
6965                handlers.push((exported_name.expect("checked above"), specifier));
6966            } else {
6967                untouched.push(specifier);
6968            }
6969        }
6970        if handlers.is_empty() {
6971            export.specifiers = untouched;
6972            output.push(Statement::ExportNamedDeclaration(export));
6973            return;
6974        }
6975        if !untouched.is_empty() {
6976            output.push(Statement::ExportNamedDeclaration(
6977                self.ast.alloc_export_named_declaration(
6978                    export.span,
6979                    None,
6980                    untouched,
6981                    Some(source.clone_in(self.ast.allocator)),
6982                    export.export_kind,
6983                    export.with_clause.take(),
6984                ),
6985            ));
6986        }
6987        for (exported_name, specifier) in handlers {
6988            let original_name = self
6989                .names
6990                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6991            output.push(Statement::ImportDeclaration(
6992                self.ast.alloc_import_declaration(
6993                    Span::default(),
6994                    Some(self.ast.vec1(
6995                        self.ast.import_declaration_specifier_import_specifier(
6996                            Span::default(),
6997                            specifier.local,
6998                            self.ast.binding_identifier(
6999                                Span::default(),
7000                                self.ast.ident(&original_name),
7001                            ),
7002                            ImportOrExportKind::Value,
7003                        ),
7004                    )),
7005                    source.clone_in(self.ast.allocator),
7006                    None,
7007                    NONE,
7008                    ImportOrExportKind::Value,
7009                ),
7010            ));
7011            output.push(self.wrapped_named_export(&exported_name, &original_name));
7012        }
7013        self.used = true;
7014    }
7015
7016    fn transform_default_export(
7017        &mut self,
7018        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
7019        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7020    ) {
7021        // Looked at before it is taken: taking it leaves a placeholder `null`,
7022        // and a server entry that exported its handler by name (`export
7023        // default handleRequest;`) exported `withRequestPhase(null)`.
7024        if matches!(
7025            export.declaration,
7026            ExportDefaultDeclarationKind::FunctionDeclaration(_)
7027        ) {
7028            let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
7029                export.declaration.take_in(self.ast.allocator)
7030            else {
7031                unreachable!();
7032            };
7033            let original_name = self
7034                .names
7035                .allocate("__supercovHandleRequestCoverageOriginal");
7036            function.id = Some(
7037                self.ast
7038                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
7039            );
7040            output.push(Statement::FunctionDeclaration(function));
7041            output.push(Statement::ExportDefaultDeclaration(
7042                self.ast.alloc_export_default_declaration(
7043                    Span::default(),
7044                    ExportDefaultDeclarationKind::from(
7045                        self.wrap_expression(self.identifier(&original_name)),
7046                    ),
7047                ),
7048            ));
7049            self.used = true;
7050            return;
7051        }
7052        if export.declaration.is_expression() {
7053            let original = export
7054                .declaration
7055                .take_in(self.ast.allocator)
7056                .into_expression();
7057            export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
7058            self.used = true;
7059        }
7060        output.push(Statement::ExportDefaultDeclaration(export));
7061    }
7062
7063    fn transform_program(&mut self, program: &mut Program<'a>) {
7064        let route_module = self.is_remix_route() || self.is_next_route();
7065        let server_entry = self.file.starts_with("app/entry.server.")
7066            && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
7067                &Path::new(self.file)
7068                    .extension()
7069                    .and_then(|value| value.to_str())
7070                    .unwrap_or(""),
7071            );
7072        if !route_module && !server_entry {
7073            return;
7074        }
7075        let original = program.body.take_in(self.ast.allocator);
7076        let mut output = self.ast.vec_with_capacity(original.len() + 4);
7077        for statement in original {
7078            match statement {
7079                Statement::ExportNamedDeclaration(export) if route_module => {
7080                    self.transform_named_export(export, &mut output);
7081                }
7082                Statement::ExportDefaultDeclaration(export) if server_entry => {
7083                    self.transform_default_export(export, &mut output);
7084                }
7085                statement => output.push(statement),
7086            }
7087        }
7088        program.body = output;
7089    }
7090}
7091
7092struct RequestPhaseTransformer<'a> {
7093    ast: AstBuilder<'a>,
7094    with_request_phase: String,
7095    used: bool,
7096    source_sensitive_functions: HashSet<SpanKey>,
7097}
7098
7099impl<'a> RequestPhaseTransformer<'a> {
7100    fn identifier(&self, name: &str) -> Expression<'a> {
7101        self.ast
7102            .expression_identifier(Span::default(), self.ast.ident(name))
7103    }
7104
7105    fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
7106        match callee {
7107            Expression::Identifier(identifier) => identifier.name == name,
7108            Expression::StaticMemberExpression(member) => member.property.name == name,
7109            _ => false,
7110        }
7111    }
7112
7113    fn callback_candidate(argument: &Argument<'a>) -> bool {
7114        matches!(
7115            argument,
7116            Argument::FunctionExpression(_)
7117                | Argument::ArrowFunctionExpression(_)
7118                | Argument::Identifier(_)
7119                | Argument::ComputedMemberExpression(_)
7120                | Argument::StaticMemberExpression(_)
7121                | Argument::PrivateFieldExpression(_)
7122        )
7123    }
7124
7125    fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
7126        matches!(
7127            argument,
7128            Argument::CallExpression(call)
7129                if expression_is_identifier(&call.callee, &self.with_request_phase)
7130        )
7131    }
7132
7133    fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
7134        if self.already_wrapped(argument) || !argument.is_expression() {
7135            return;
7136        }
7137        let expression = argument.to_expression_mut();
7138        let original = expression.take_in(self.ast.allocator);
7139        *expression = self.ast.expression_call(
7140            Span::default(),
7141            self.identifier(&self.with_request_phase),
7142            NONE,
7143            self.ast
7144                .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
7145                    |name| {
7146                        Argument::from(self.ast.expression_string_literal(
7147                            Span::default(),
7148                            self.ast.str(name),
7149                            None,
7150                        ))
7151                    },
7152                ))),
7153            false,
7154        );
7155        self.used = true;
7156    }
7157}
7158
7159impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
7160    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7161        if self
7162            .source_sensitive_functions
7163            .contains(&span_key(function.span))
7164        {
7165            return;
7166        }
7167        walk_mut::walk_function(self, function, flags);
7168    }
7169
7170    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7171        if self
7172            .source_sensitive_functions
7173            .contains(&span_key(function.span))
7174        {
7175            return;
7176        }
7177        walk_mut::walk_arrow_function_expression(self, function);
7178    }
7179
7180    // A with body is left uninstrumented: its object environment can
7181    // intercept probe identifiers (see `enter_with_statement`).
7182    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7183
7184    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
7185        walk_mut::walk_call_expression(self, call);
7186        let property = match &call.callee {
7187            Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
7188            _ => None,
7189        };
7190        let mut callback_index = None;
7191        if matches!(property, Some("on" | "once" | "addListener"))
7192            && matches!(
7193                call.arguments.first(),
7194                Some(Argument::StringLiteral(event))
7195                    if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
7196            )
7197        {
7198            callback_index = Some(1);
7199        } else if Self::callee_is(&call.callee, "createServer") {
7200            callback_index = call.arguments.iter().rposition(Self::callback_candidate);
7201        }
7202        let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
7203        if let Some(index) = callback_index
7204            && let Some(argument) = call.arguments.get_mut(index)
7205        {
7206            self.wrap_argument(argument, connection.then_some("connection"));
7207        }
7208    }
7209}
7210
7211struct CandidateNames<'s> {
7212    source: &'s str,
7213    /// Every identifier in the source that begins with `_`, which every
7214    /// generated name does. Asking the source whether it contains a name was
7215    /// a scan of the whole file per name, and names are allocated per
7216    /// function: the second quadratic cost in instrumenting a long file.
7217    underscored: HashSet<&'s str>,
7218    allocated: HashSet<String>,
7219    suffix: usize,
7220    /// Appended to every name, so output that shares a scope with other
7221    /// instrumented files cannot collide with theirs.
7222    namespace: String,
7223}
7224
7225impl<'s> CandidateNames<'s> {
7226    fn new(source: &'s str) -> Self {
7227        Self::within(source, "")
7228    }
7229
7230    fn within(source: &'s str, namespace: &str) -> Self {
7231        let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
7232        let underscored = source
7233            .split(|c: char| !identifier(c))
7234            .filter(|word| word.starts_with('_'))
7235            .collect();
7236        Self {
7237            source,
7238            underscored,
7239            allocated: HashSet::new(),
7240            suffix: 0,
7241            namespace: namespace.to_owned(),
7242        }
7243    }
7244
7245    fn taken(&self, candidate: &str) -> bool {
7246        self.allocated.contains(candidate)
7247            || if candidate.starts_with('_') {
7248                self.underscored.contains(candidate)
7249            } else {
7250                self.source.contains(candidate)
7251            }
7252    }
7253
7254    fn allocate(&mut self, base: &str) -> String {
7255        loop {
7256            let candidate = if self.suffix == 0 {
7257                format!("{base}{}", self.namespace)
7258            } else {
7259                format!("{base}{}{}", self.suffix, self.namespace)
7260            };
7261            self.suffix += 1;
7262            if !self.taken(&candidate) {
7263                self.allocated.insert(candidate.clone());
7264                return candidate;
7265            }
7266        }
7267    }
7268}
7269
7270struct ControlProbeV2Transformer<'a, 's> {
7271    ast: AstBuilder<'a>,
7272    decisions: &'s [CandidateDecision],
7273    /// Expressions used as values that are decisions, by span, each with the
7274    /// root of the tree whose evaluation records it.
7275    value_decisions: &'s HashMap<SpanKey, SpanKey>,
7276    /// Decisions applied below a tree root and not yet recorded there, with
7277    /// the shape their outcome is read back from.
7278    pending_values: HashMap<SpanKey, Vec<(AppliedDecision, ValueShape)>>,
7279    mcdc_begin: String,
7280    mcdc_condition: String,
7281    mcdc_end: String,
7282    mcdc_end_v2: String,
7283    probe_file_v2: String,
7284    names: CandidateNames<'s>,
7285    scope_declarations: Vec<Vec<String>>,
7286    decision_index: usize,
7287    parameter_depth: usize,
7288    source_sensitive_functions: HashSet<SpanKey>,
7289    /// What goes around a statement whose false outcome is recorded after
7290    /// it: the statement that records it, and for a `do … while` one before
7291    /// the loop.
7292    after_statement: Option<(Option<Statement<'a>>, Statement<'a>)>,
7293    /// The statement being visited is the body of a label, which has to stay
7294    /// on it: what goes after the statement goes after the label.
7295    label_body: bool,
7296    /// The temporaries of a decision inside a parameter default, for the
7297    /// arrow that has to declare them around its conditional expression.
7298    inline_names: Option<Vec<String>>,
7299}
7300
7301#[derive(Clone, Copy)]
7302struct DecisionPlan {
7303    index: usize,
7304    condition_count: usize,
7305    inline_frame: bool,
7306}
7307
7308/// A decision whose conditions have been rewritten, and the temporaries its
7309/// outcome is recorded from.
7310struct AppliedDecision {
7311    index: usize,
7312    frame: String,
7313    values: Vec<String>,
7314    inline: bool,
7315}
7316
7317impl<'a> ControlProbeV2Transformer<'a, '_> {
7318    fn enter_declaration_scope(&mut self) {
7319        self.scope_declarations.push(Vec::new());
7320    }
7321
7322    fn leave_declaration_scope(&mut self) -> Vec<String> {
7323        self.scope_declarations
7324            .pop()
7325            .expect("program/function scope stack must remain balanced")
7326    }
7327
7328    fn allocate_scratch(&mut self, base: &str) -> String {
7329        let name = self.names.allocate(base);
7330        self.scope_declarations
7331            .last_mut()
7332            .expect("a control decision must be inside a program or function body")
7333            .push(name.clone());
7334        name
7335    }
7336
7337    fn scratch_for(&mut self, base: &str, inline: bool) -> String {
7338        if inline {
7339            self.names.allocate(base)
7340        } else {
7341            self.allocate_scratch(base)
7342        }
7343    }
7344
7345    fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
7346        let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
7347            self.ast.variable_declarator(
7348                Span::default(),
7349                VariableDeclarationKind::Let,
7350                self.ast
7351                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
7352                NONE,
7353                None,
7354                false,
7355            )
7356        }));
7357        let body = self.ast.alloc_function_body(
7358            Span::default(),
7359            self.ast.vec(),
7360            self.ast.vec_from_array([
7361                Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7362                    Span::default(),
7363                    VariableDeclarationKind::Let,
7364                    declarations,
7365                    false,
7366                )),
7367                self.ast.statement_return(Span::default(), Some(expression)),
7368            ]),
7369        );
7370        let params = self.ast.alloc_formal_parameters(
7371            Span::default(),
7372            FormalParameterKind::ArrowFormalParameters,
7373            self.ast.vec(),
7374            NONE,
7375        );
7376        let arrow = self.ast.expression_arrow_function(
7377            Span::default(),
7378            false,
7379            false,
7380            NONE,
7381            params,
7382            NONE,
7383            body,
7384        );
7385        self.ast
7386            .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
7387    }
7388
7389    fn prepend_declarations(
7390        &self,
7391        names: Vec<String>,
7392        statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7393    ) {
7394        if names.is_empty() {
7395            return;
7396        }
7397        let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
7398            self.ast.variable_declarator(
7399                Span::default(),
7400                VariableDeclarationKind::Let,
7401                self.ast
7402                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
7403                NONE,
7404                None,
7405                false,
7406            )
7407        }));
7408        statements.insert(
7409            0,
7410            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7411                Span::default(),
7412                VariableDeclarationKind::Let,
7413                declarators,
7414                false,
7415            )),
7416        );
7417    }
7418
7419    fn identifier(&self, name: &str) -> Expression<'a> {
7420        self.ast
7421            .expression_identifier(Span::default(), self.ast.ident(name))
7422    }
7423
7424    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
7425        let target = self
7426            .ast
7427            .simple_assignment_target_assignment_target_identifier(
7428                Span::default(),
7429                self.ast.ident(name),
7430            );
7431        AssignmentTarget::from(target)
7432    }
7433
7434    fn number(&self, value: u64) -> Expression<'a> {
7435        self.ast.expression_numeric_literal(
7436            Span::default(),
7437            value as f64,
7438            None,
7439            NumberBase::Decimal,
7440        )
7441    }
7442
7443    fn string(&self, value: &str) -> Expression<'a> {
7444        self.ast
7445            .expression_string_literal(Span::default(), self.ast.str(value), None)
7446    }
7447
7448    fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
7449        self.ast.object_property_kind_object_property(
7450            Span::default(),
7451            PropertyKind::Init,
7452            self.ast
7453                .property_key_static_identifier(Span::default(), self.ast.ident(name)),
7454            value,
7455            false,
7456            false,
7457            false,
7458        )
7459    }
7460
7461    fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
7462        let conditions = self.ast.expression_array(
7463            Span::default(),
7464            self.ast.vec_from_iter(
7465                decision
7466                    .conditions
7467                    .iter()
7468                    .map(|condition| ArrayExpressionElement::from(self.string(condition))),
7469            ),
7470        );
7471        let properties = self.ast.vec_from_array([
7472            self.object_property("id", self.string(&decision.id)),
7473            self.object_property("file", self.string(&decision.file)),
7474            self.object_property("line", self.number(decision.line as u64)),
7475            self.object_property("column", self.number(decision.column as u64)),
7476            self.object_property("source", self.string(&decision.source)),
7477            self.object_property("conditions", conditions),
7478            self.object_property("kind", self.string(&decision.kind)),
7479        ]);
7480        Expression::ObjectExpression(
7481            self.ast
7482                .alloc_object_expression(Span::default(), properties),
7483        )
7484    }
7485
7486    fn assign(&self, name: &str, value: Expression<'a>) -> Expression<'a> {
7487        self.ast.expression_assignment(
7488            Span::default(),
7489            AssignmentOperator::Assign,
7490            self.assignment_target(name),
7491            value,
7492        )
7493    }
7494
7495    /// One condition of a decision, as `(_f += w, _v = C)`.
7496    ///
7497    /// The assignment comes last because TypeScript narrows through an
7498    /// assignment's right side and through a comma's last operand, and through
7499    /// nothing else here. The condition used to end in its temporary
7500    /// (`(_v = C, _f += …, _v)`), which narrows nothing: `if (client) return
7501    /// client` returned `Client | null`, and a file that Supercov never
7502    /// touched failed to compile against the widened type.
7503    ///
7504    /// The first condition carries the decision's bookkeeping, since a comma
7505    /// around the whole decision would hide its `&&` and `||` from the same
7506    /// analysis: the frame starts at this condition's weight, and the other
7507    /// conditions' temporaries are cleared, so one that is not evaluated adds
7508    /// nothing when the outcome is recorded.
7509    fn instrument_condition(
7510        &self,
7511        expression: Expression<'a>,
7512        applied: &AppliedDecision,
7513        index: usize,
7514    ) -> Expression<'a> {
7515        let mut steps = self.ast.vec();
7516        if index == 0 {
7517            steps.push(self.assign(&applied.frame, self.number(1)));
7518            for name in &applied.values[1..] {
7519                steps.push(self.assign(name, self.number(0)));
7520            }
7521        } else {
7522            steps.push(self.ast.expression_assignment(
7523                Span::default(),
7524                AssignmentOperator::Addition,
7525                self.assignment_target(&applied.frame),
7526                self.number(3_u64.pow(index as u32)),
7527            ));
7528        }
7529        steps.push(self.assign(&applied.values[index], expression));
7530        self.ast.expression_sequence(Span::default(), steps)
7531    }
7532
7533    fn instrument_conditions(
7534        &self,
7535        expression: &mut Expression<'a>,
7536        applied: &AppliedDecision,
7537        next_index: &mut usize,
7538    ) {
7539        match expression {
7540            Expression::ParenthesizedExpression(parenthesized) => {
7541                self.instrument_conditions(&mut parenthesized.expression, applied, next_index)
7542            }
7543            Expression::LogicalExpression(logical)
7544                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7545            {
7546                self.instrument_conditions(&mut logical.left, applied, next_index);
7547                self.instrument_conditions(&mut logical.right, applied, next_index);
7548            }
7549            Expression::UnaryExpression(unary)
7550                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7551            {
7552                self.instrument_conditions(&mut unary.argument, applied, next_index);
7553            }
7554            _ => {
7555                let index = *next_index;
7556                *next_index += 1;
7557                let original = expression.take_in(self.ast.allocator);
7558                *expression = self.instrument_condition(original, applied, index);
7559            }
7560        }
7561    }
7562
7563    /// The decision's vector in base 3, from its temporaries: nothing for a
7564    /// condition that was not evaluated, its weight for a false one, twice
7565    /// that for a true one. `_f` holds the weights of the conditions that
7566    /// were evaluated; `+!!_v` adds the second for each true one.
7567    fn encoded(&self, applied: &AppliedDecision) -> Expression<'a> {
7568        let mut sum = self.identifier(&applied.frame);
7569        for (index, name) in applied.values.iter().enumerate() {
7570            let truth = self.ast.expression_unary(
7571                Span::default(),
7572                UnaryOperator::UnaryPlus,
7573                self.ast.expression_unary(
7574                    Span::default(),
7575                    UnaryOperator::LogicalNot,
7576                    self.ast.expression_unary(
7577                        Span::default(),
7578                        UnaryOperator::LogicalNot,
7579                        self.identifier(name),
7580                    ),
7581                ),
7582            );
7583            let term = if index == 0 {
7584                truth
7585            } else {
7586                self.ast.expression_binary(
7587                    Span::default(),
7588                    self.number(3_u64.pow(index as u32)),
7589                    BinaryOperator::Multiplication,
7590                    truth,
7591                )
7592            };
7593            sum = self
7594                .ast
7595                .expression_binary(Span::default(), sum, BinaryOperator::Addition, term);
7596        }
7597        sum
7598    }
7599
7600    /// `mcdcEndV2(file, decision, vector, outcome)`, where the outcome is
7601    /// known from where the call stands: the branch that was taken.
7602    /// The decisions used as values in one `&&`/`||` tree, recorded once the
7603    /// tree has been evaluated: `(_f = 0, _v0 = 0, .., _r = tree, end(..),
7604    /// _r)`. The tree stays whole -- a coverage tool the tests run counts it
7605    /// as one branch, and TypeScript narrows across it as written -- and the
7606    /// assignment keeps its value and its type. Each outcome is read back
7607    /// from the decision's own temporaries with `&`, `|` and `^`, which no
7608    /// tool counts as a branch; a decision the evaluation did not reach was
7609    /// reset to nothing, and the runtime records no vector for it.
7610    fn record_value_outcomes(
7611        &mut self,
7612        expression: &mut Expression<'a>,
7613        pending: Vec<(AppliedDecision, ValueShape)>,
7614    ) {
7615        let inline = pending.iter().any(|(applied, _)| applied.inline);
7616        let result = self.scratch_for("_supercovMcdcResult", inline);
7617        let mut steps = self.ast.vec();
7618        for (applied, _) in &pending {
7619            steps.push(self.assign(&applied.frame, self.number(0)));
7620            for name in &applied.values {
7621                steps.push(self.assign(name, self.number(0)));
7622            }
7623        }
7624        let value = expression.take_in(self.ast.allocator);
7625        steps.push(self.assign(&result, value));
7626        let mut names = Vec::new();
7627        for (applied, shape) in &pending {
7628            let truth = self.ast.expression_unary(
7629                Span::default(),
7630                UnaryOperator::LogicalNot,
7631                self.ast.expression_unary(
7632                    Span::default(),
7633                    UnaryOperator::LogicalNot,
7634                    self.shape_bits(shape, applied),
7635                ),
7636            );
7637            steps.push(self.ast.expression_call(
7638                Span::default(),
7639                self.identifier(&self.mcdc_end_v2),
7640                NONE,
7641                self.ast.vec_from_array([
7642                    Argument::from(self.identifier(&self.probe_file_v2)),
7643                    Argument::from(self.number(applied.index as u64)),
7644                    Argument::from(self.encoded(applied)),
7645                    Argument::from(truth),
7646                ]),
7647                false,
7648            ));
7649            if applied.inline {
7650                names.push(applied.frame.clone());
7651                names.extend(applied.values.iter().cloned());
7652            }
7653        }
7654        steps.push(self.identifier(&result));
7655        let observed = self.ast.expression_parenthesized(
7656            Span::default(),
7657            self.ast.expression_sequence(Span::default(), steps),
7658        );
7659        *expression = if inline {
7660            names.push(result);
7661            self.wrap_inline_frame(observed, &names)
7662        } else {
7663            observed
7664        };
7665    }
7666
7667    /// A decision's outcome from its temporaries, as 0 or 1: a condition that
7668    /// was not evaluated reads 0, which is never what decided the outcome.
7669    fn shape_bits(&self, shape: &ValueShape, applied: &AppliedDecision) -> Expression<'a> {
7670        let binary = |left, operator, right| {
7671            self.ast.expression_parenthesized(
7672                Span::default(),
7673                self.ast
7674                    .expression_binary(Span::default(), left, operator, right),
7675            )
7676        };
7677        match shape {
7678            ValueShape::Condition(index) => self.ast.expression_unary(
7679                Span::default(),
7680                UnaryOperator::UnaryPlus,
7681                self.ast.expression_unary(
7682                    Span::default(),
7683                    UnaryOperator::LogicalNot,
7684                    self.ast.expression_unary(
7685                        Span::default(),
7686                        UnaryOperator::LogicalNot,
7687                        self.identifier(&applied.values[*index]),
7688                    ),
7689                ),
7690            ),
7691            ValueShape::And(left, right) => binary(
7692                self.shape_bits(left, applied),
7693                BinaryOperator::BitwiseAnd,
7694                self.shape_bits(right, applied),
7695            ),
7696            ValueShape::Or(left, right) => binary(
7697                self.shape_bits(left, applied),
7698                BinaryOperator::BitwiseOR,
7699                self.shape_bits(right, applied),
7700            ),
7701            ValueShape::Not(inner) => binary(
7702                self.number(1),
7703                BinaryOperator::BitwiseXOR,
7704                self.shape_bits(inner, applied),
7705            ),
7706        }
7707    }
7708
7709    fn outcome(&self, applied: &AppliedDecision, outcome: bool) -> Expression<'a> {
7710        self.ast.expression_call(
7711            Span::default(),
7712            self.identifier(&self.mcdc_end_v2),
7713            NONE,
7714            self.ast.vec_from_array([
7715                Argument::from(self.identifier(&self.probe_file_v2)),
7716                Argument::from(self.number(applied.index as u64)),
7717                Argument::from(self.encoded(applied)),
7718                Argument::from(
7719                    self.ast
7720                        .expression_boolean_literal(Span::default(), outcome),
7721                ),
7722            ]),
7723            false,
7724        )
7725    }
7726
7727    /// Put `first` at the head of a branch or a loop body, making it a block
7728    /// if it is a single statement.
7729    fn prepend_statement(&self, body: &mut Statement<'a>, first: Statement<'a>) {
7730        if let Statement::BlockStatement(block) = body {
7731            block.body.insert(0, first);
7732            return;
7733        }
7734        let original = body.take_in(self.ast.allocator);
7735        *body = self
7736            .ast
7737            .statement_block(Span::default(), self.ast.vec_from_array([first, original]));
7738    }
7739
7740    /// What records the true outcome where the false one is recorded after
7741    /// the statement: `mcdcEndV2(…, true), _f = 0`. The frame is at least 1
7742    /// once a test has run, so cleared it says the outcome has been recorded.
7743    fn settled(&self, applied: &AppliedDecision) -> Expression<'a> {
7744        self.ast.expression_sequence(
7745            Span::default(),
7746            self.ast.vec_from_array([
7747                self.outcome(applied, true),
7748                self.assign(&applied.frame, self.number(0)),
7749            ]),
7750        )
7751    }
7752
7753    fn when_unsettled(&self, applied: &AppliedDecision, record: Expression<'a>) -> Statement<'a> {
7754        self.ast.statement_expression(
7755            Span::default(),
7756            self.ast.expression_logical(
7757                Span::default(),
7758                self.identifier(&applied.frame),
7759                LogicalOperator::And,
7760                record,
7761            ),
7762        )
7763    }
7764
7765    /// Record the false outcome after the statement, when nothing runs for
7766    /// it inside: an `if` without an `else`, a loop its test ended.
7767    ///
7768    /// An `else` made for the call would be a branch of its own to a coverage
7769    /// tool the tests run, and the `if` would end at a brace nobody wrote:
7770    /// istanbul lost the statement and its implicit `else` with it. A `break`
7771    /// leaves a loop from its body, where the outcome is already recorded.
7772    fn record_false_after(&mut self, applied: &AppliedDecision, before: Option<Statement<'a>>) {
7773        let after = self.when_unsettled(applied, self.outcome(applied, false));
7774        self.after_statement = Some((before, after));
7775    }
7776
7777    /// A loop's true outcome is recorded where its body starts.
7778    fn loop_outcomes(
7779        &mut self,
7780        body: &mut Statement<'a>,
7781        applied: &AppliedDecision,
7782        tested_after: bool,
7783    ) {
7784        if !tested_after {
7785            let entered = self
7786                .ast
7787                .statement_expression(Span::default(), self.settled(applied));
7788            self.prepend_statement(body, entered);
7789            self.record_false_after(applied, None);
7790            return;
7791        }
7792        // A `do … while` body runs before its test: the frame says whether a
7793        // test came before this iteration. It is cleared before the loop,
7794        // because a test that threw the last time the loop ran left it set.
7795        let entered = self.when_unsettled(applied, self.settled(applied));
7796        self.prepend_statement(body, entered);
7797        let before = self
7798            .ast
7799            .statement_expression(Span::default(), self.assign(&applied.frame, self.number(0)));
7800        self.record_false_after(applied, Some(before));
7801    }
7802
7803    fn instrument_condition_v1(
7804        &self,
7805        expression: Expression<'a>,
7806        frame_name: &str,
7807        index: usize,
7808    ) -> Expression<'a> {
7809        self.ast.expression_call(
7810            Span::default(),
7811            self.identifier(&self.mcdc_condition),
7812            NONE,
7813            self.ast.vec_from_array([
7814                Argument::from(self.identifier(frame_name)),
7815                Argument::from(self.number(index as u64)),
7816                Argument::from(expression),
7817            ]),
7818            false,
7819        )
7820    }
7821
7822    fn instrument_conditions_v1(
7823        &self,
7824        expression: &mut Expression<'a>,
7825        frame_name: &str,
7826        next_index: &mut usize,
7827    ) {
7828        match expression {
7829            Expression::ParenthesizedExpression(parenthesized) => {
7830                self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
7831            }
7832            Expression::LogicalExpression(logical)
7833                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7834            {
7835                self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
7836                self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
7837            }
7838            Expression::UnaryExpression(unary)
7839                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7840            {
7841                self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
7842            }
7843            _ => {
7844                let index = *next_index;
7845                *next_index += 1;
7846                let original = expression.take_in(self.ast.allocator);
7847                *expression = self.instrument_condition_v1(original, frame_name, index);
7848            }
7849        }
7850    }
7851
7852    fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
7853        let mut condition_spans = Vec::new();
7854        collect_conditions(test, &mut condition_spans);
7855        let plan = DecisionPlan {
7856            index: self.decision_index,
7857            condition_count: condition_spans.len(),
7858            inline_frame: self.parameter_depth > 0,
7859        };
7860        self.decision_index += 1;
7861        plan
7862    }
7863
7864    /// Rewrite a decision's conditions. What records the outcome is left to
7865    /// the caller, which knows the branches; a decision too wide for the
7866    /// encoding keeps the call-based form and returns nothing.
7867    fn apply_decision(
7868        &mut self,
7869        test: &mut Expression<'a>,
7870        plan: DecisionPlan,
7871    ) -> Option<AppliedDecision> {
7872        if plan.condition_count > 32 {
7873            let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
7874            let mut next_index = 0;
7875            self.instrument_conditions_v1(test, &frame_name, &mut next_index);
7876            debug_assert_eq!(next_index, plan.condition_count);
7877
7878            let decision = &self.decisions[plan.index];
7879            let begin = self.ast.expression_call(
7880                Span::default(),
7881                self.identifier(&self.mcdc_begin),
7882                NONE,
7883                self.ast.vec_from_array([
7884                    Argument::from(self.string(&decision.id)),
7885                    Argument::from(self.decision_meta(decision)),
7886                ]),
7887                false,
7888            );
7889            let assign_frame = self.ast.expression_assignment(
7890                Span::default(),
7891                AssignmentOperator::Assign,
7892                self.assignment_target(&frame_name),
7893                begin,
7894            );
7895            let instrumented = test.take_in(self.ast.allocator);
7896            let end = self.ast.expression_call(
7897                Span::default(),
7898                self.identifier(&self.mcdc_end),
7899                NONE,
7900                self.ast.vec_from_array([
7901                    Argument::from(self.identifier(&frame_name)),
7902                    Argument::from(instrumented),
7903                ]),
7904                false,
7905            );
7906            let observed = self.ast.expression_sequence(
7907                Span::default(),
7908                self.ast.vec_from_array([assign_frame, end]),
7909            );
7910            *test = if plan.inline_frame {
7911                self.wrap_inline_frame(observed, &[frame_name])
7912            } else {
7913                observed
7914            };
7915            return None;
7916        }
7917
7918        let applied = AppliedDecision {
7919            index: plan.index,
7920            frame: self.scratch_for("_supercovMcdcFrame", plan.inline_frame),
7921            values: (0..plan.condition_count)
7922                .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
7923                .collect(),
7924            inline: plan.inline_frame,
7925        };
7926        let mut next_index = 0;
7927        self.instrument_conditions(test, &applied, &mut next_index);
7928        debug_assert_eq!(
7929            next_index,
7930            plan.condition_count,
7931            "decision at {:?}",
7932            test.span()
7933        );
7934        Some(applied)
7935    }
7936}
7937
7938impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
7939    fn visit_program(&mut self, program: &mut Program<'a>) {
7940        self.enter_declaration_scope();
7941        walk_mut::walk_program(self, program);
7942        let declarations = self.leave_declaration_scope();
7943        self.prepend_declarations(declarations, &mut program.body);
7944    }
7945
7946    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
7947        self.enter_declaration_scope();
7948        walk_mut::walk_function_body(self, body);
7949        let declarations = self.leave_declaration_scope();
7950        self.prepend_declarations(declarations, &mut body.statements);
7951    }
7952
7953    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7954        if self
7955            .source_sensitive_functions
7956            .contains(&span_key(function.span))
7957        {
7958            return;
7959        }
7960        let outer_parameter_depth = self.parameter_depth;
7961        self.parameter_depth = 0;
7962        walk_mut::walk_function(self, function, flags);
7963        self.parameter_depth = outer_parameter_depth;
7964    }
7965
7966    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7967        if self
7968            .source_sensitive_functions
7969            .contains(&span_key(function.span))
7970        {
7971            return;
7972        }
7973        let outer_parameter_depth = self.parameter_depth;
7974        self.parameter_depth = 0;
7975        walk_mut::walk_arrow_function_expression(self, function);
7976        self.parameter_depth = outer_parameter_depth;
7977    }
7978
7979    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
7980        self.parameter_depth += 1;
7981        walk_mut::walk_formal_parameters(self, parameters);
7982        self.parameter_depth -= 1;
7983    }
7984
7985    // A with body is left uninstrumented: its object environment can
7986    // intercept probe identifiers (see `enter_with_statement`).
7987    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7988
7989    // A statement whose false outcome is recorded after it becomes a block
7990    // of the two, so it can stand wherever one statement can. A label stays
7991    // on its statement, where `break` and `continue` look for it, and the
7992    // block goes around the outermost label.
7993    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
7994        let label_body = self.label_body;
7995        self.label_body = matches!(statement, Statement::LabeledStatement(_));
7996        walk_mut::walk_statement(self, statement);
7997        self.label_body = false;
7998        if label_body {
7999            return;
8000        }
8001        if let Some((before, after)) = self.after_statement.take() {
8002            let decided = statement.take_in(self.ast.allocator);
8003            let mut recorded = self.ast.vec_with_capacity(3);
8004            recorded.extend(before);
8005            recorded.push(decided);
8006            recorded.push(after);
8007            *statement = self.ast.statement_block(Span::default(), recorded);
8008        }
8009    }
8010
8011    // A static block is a scope of its own for the temporaries: inside a
8012    // parameter default there is no function body between it and the
8013    // parameter list, and a statement cannot be wrapped in an arrow.
8014    fn visit_static_block(&mut self, block: &mut oxc_ast::ast::StaticBlock<'a>) {
8015        let outer_parameter_depth = self.parameter_depth;
8016        self.parameter_depth = 0;
8017        self.enter_declaration_scope();
8018        walk_mut::walk_static_block(self, block);
8019        let declarations = self.leave_declaration_scope();
8020        self.prepend_declarations(declarations, &mut block.body);
8021        self.parameter_depth = outer_parameter_depth;
8022    }
8023
8024    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
8025        let key = span_key(expression.span());
8026        let value = self.value_decisions.get(&key).copied().map(|root| {
8027            let mut next = 0;
8028            (
8029                root,
8030                self.reserve_decision(expression),
8031                value_shape(expression, &mut next),
8032            )
8033        });
8034        let is_root = self.value_decisions.values().any(|root| *root == key);
8035        walk_mut::walk_expression(self, expression);
8036        if let Some(names) = self.inline_names.take()
8037            && matches!(expression, Expression::ConditionalExpression(_))
8038        {
8039            let conditional = expression.take_in(self.ast.allocator);
8040            *expression = self.wrap_inline_frame(conditional, &names);
8041        }
8042        if let Some((root, plan, shape)) = value
8043            && let Some(applied) = self.apply_decision(expression, plan)
8044        {
8045            self.pending_values
8046                .entry(root)
8047                .or_default()
8048                .push((applied, shape));
8049        }
8050        if is_root && let Some(pending) = self.pending_values.remove(&key) {
8051            self.record_value_outcomes(expression, pending);
8052        }
8053    }
8054
8055    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
8056        let plan = decision_outcome_is_variable(&statement.test)
8057            .then(|| self.reserve_decision(&statement.test));
8058        self.visit_expression(&mut statement.test);
8059        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8060        self.visit_statement(&mut statement.consequent);
8061        if let Some(alternate) = &mut statement.alternate {
8062            self.visit_statement(alternate);
8063        }
8064        let Some(applied) = applied else {
8065            return;
8066        };
8067        let Some(alternate) = &mut statement.alternate else {
8068            let taken = self
8069                .ast
8070                .statement_expression(Span::default(), self.settled(&applied));
8071            self.prepend_statement(&mut statement.consequent, taken);
8072            self.record_false_after(&applied, None);
8073            return;
8074        };
8075        let skipped = self
8076            .ast
8077            .statement_expression(Span::default(), self.outcome(&applied, false));
8078        self.prepend_statement(alternate, skipped);
8079        let taken = self
8080            .ast
8081            .statement_expression(Span::default(), self.outcome(&applied, true));
8082        self.prepend_statement(&mut statement.consequent, taken);
8083    }
8084
8085    fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
8086        let plan = decision_outcome_is_variable(&expression.test)
8087            .then(|| self.reserve_decision(&expression.test));
8088        self.visit_expression(&mut expression.test);
8089        let applied = plan.and_then(|plan| self.apply_decision(&mut expression.test, plan));
8090        self.visit_expression(&mut expression.consequent);
8091        self.visit_expression(&mut expression.alternate);
8092        let Some(applied) = applied else {
8093            return;
8094        };
8095        // A coverage tool the tests run reads where a branch starts from the
8096        // source map, and the printer maps neither a sequence nor a call:
8097        // the branch started at whatever token came before it. `void` is
8098        // mapped, and here it is mapped to the branch's own start.
8099        for (branch, outcome) in [
8100            (&mut expression.consequent, true),
8101            (&mut expression.alternate, false),
8102        ] {
8103            let value = branch.take_in(self.ast.allocator);
8104            let start = value.span().start;
8105            let record = self.ast.expression_unary(
8106                Span::new(start, start + 1),
8107                UnaryOperator::Void,
8108                self.outcome(&applied, outcome),
8109            );
8110            *branch = self
8111                .ast
8112                .expression_sequence(Span::default(), self.ast.vec_from_array([record, value]));
8113        }
8114        if applied.inline {
8115            let mut names = vec![applied.frame];
8116            names.extend(applied.values);
8117            self.inline_names = Some(names);
8118        }
8119    }
8120
8121    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
8122        let plan = decision_outcome_is_variable(&statement.test)
8123            .then(|| self.reserve_decision(&statement.test));
8124        self.visit_expression(&mut statement.test);
8125        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8126        self.visit_statement(&mut statement.body);
8127        if let Some(applied) = applied {
8128            self.loop_outcomes(&mut statement.body, &applied, false);
8129        }
8130    }
8131
8132    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
8133        let plan = decision_outcome_is_variable(&statement.test)
8134            .then(|| self.reserve_decision(&statement.test));
8135        self.visit_statement(&mut statement.body);
8136        self.visit_expression(&mut statement.test);
8137        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8138        if let Some(applied) = applied {
8139            self.loop_outcomes(&mut statement.body, &applied, true);
8140        }
8141    }
8142
8143    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
8144        let plan = statement
8145            .test
8146            .as_ref()
8147            .filter(|test| decision_outcome_is_variable(test))
8148            .map(|test| self.reserve_decision(test));
8149        if let Some(init) = &mut statement.init {
8150            self.visit_for_statement_init(init);
8151        }
8152        let mut applied = None;
8153        if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
8154            self.visit_expression(test);
8155            applied = self.apply_decision(test, plan);
8156        }
8157        if let Some(update) = &mut statement.update {
8158            self.visit_expression(update);
8159        }
8160        self.visit_statement(&mut statement.body);
8161        if let Some(applied) = applied {
8162            self.loop_outcomes(&mut statement.body, &applied, false);
8163        }
8164    }
8165}
8166
8167struct DecisionCollector<'s> {
8168    source: &'s str,
8169    file: &'s str,
8170    decisions: Vec<CandidateDecision>,
8171    decision_vector_counts: Vec<usize>,
8172    decision_logical_nodes: HashSet<SpanKey>,
8173    source_sensitive_functions: &'s HashSet<SpanKey>,
8174    /// The expressions recorded as decisions where they are used as values,
8175    /// for the transformer to find by span, each with the root of the `&&`/`||`
8176    /// tree it sits in: the outcome is recorded once that root has been
8177    /// evaluated, so the tree stays one tree.
8178    value_decisions: HashMap<SpanKey, SpanKey>,
8179    /// The root of the `&&`/`||`/`??` tree being visited, through parentheses.
8180    logical_root: Option<SpanKey>,
8181    /// The operators inside a decision used as a value. They stay in their
8182    /// value tree -- its branches and the one tree a coverage tool reads --
8183    /// and only stop a decision being found inside the decision.
8184    value_logical_nodes: HashSet<SpanKey>,
8185}
8186
8187impl DecisionCollector<'_> {
8188    fn in_a_decision(&self, span: Span) -> bool {
8189        let key = span_key(span);
8190        self.decision_logical_nodes.contains(&key) || self.value_logical_nodes.contains(&key)
8191    }
8192
8193    fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
8194        let mut condition_spans = Vec::new();
8195        collect_conditions(test, &mut condition_spans);
8196        if kind == "value" {
8197            collect_decision_logical_nodes(test, &mut self.value_logical_nodes);
8198        } else {
8199            collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
8200        }
8201        // Babel treats redundant parentheses as parser metadata rather than
8202        // part of the decision node. oxc preserves a ParenthesizedExpression,
8203        // so normalize it here to keep locations and stable IDs parser-neutral.
8204        let span = transparent_expression(test).span();
8205        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8206        self.decisions.push(CandidateDecision {
8207            id: stable_id(self.source, self.file, "decision", span, kind),
8208            file: self.file.to_string(),
8209            line,
8210            column,
8211            source: source_slice(self.source, span).to_string(),
8212            conditions: condition_spans
8213                .iter()
8214                .map(|condition| source_slice(self.source, *condition).to_string())
8215                .collect(),
8216            kind: kind.to_string(),
8217        });
8218        self.decision_vector_counts.push(
8219            if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
8220                reachable_vector_count(test, &condition_spans)
8221            } else {
8222                0
8223            },
8224        );
8225    }
8226}
8227
8228/// The `&&` or `||` under any parentheses and `!`s: `!!(a && b)` is the
8229/// decision `a && b` negated twice.
8230fn compound_root<'e, 'a>(expression: &'e Expression<'a>) -> Option<&'e LogicalExpression<'a>> {
8231    match expression {
8232        Expression::ParenthesizedExpression(parenthesized) => {
8233            compound_root(&parenthesized.expression)
8234        }
8235        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8236            compound_root(&unary.argument)
8237        }
8238        Expression::LogicalExpression(logical)
8239            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8240        {
8241            Some(logical)
8242        }
8243        _ => None,
8244    }
8245}
8246
8247/// MC/DC applies only when a decision can produce both Boolean outcomes.
8248/// This deliberately recognizes only semantics that are provable from syntax;
8249/// it never evaluates user code or assumes values for identifiers/calls.
8250fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
8251    syntactic_boolean_outcome(expression).is_none()
8252}
8253
8254fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
8255    match transparent_expression(expression) {
8256        Expression::BooleanLiteral(literal) => Some(literal.value),
8257        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8258            syntactic_boolean_outcome(&unary.argument).map(|value| !value)
8259        }
8260        Expression::LogicalExpression(logical)
8261            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8262        {
8263            let left = syntactic_boolean_outcome(&logical.left);
8264            let right = syntactic_boolean_outcome(&logical.right);
8265            match logical.operator {
8266                LogicalOperator::And => match (left, right) {
8267                    (Some(false), _) | (_, Some(false)) => Some(false),
8268                    (Some(true), value) | (value, Some(true)) => value,
8269                    _ => None,
8270                },
8271                LogicalOperator::Or => match (left, right) {
8272                    (Some(true), _) | (_, Some(true)) => Some(true),
8273                    (Some(false), value) | (value, Some(false)) => value,
8274                    _ => None,
8275                },
8276                LogicalOperator::Coalesce => None,
8277            }
8278        }
8279        _ => None,
8280    }
8281}
8282
8283#[derive(Default)]
8284struct CoverageSurfaceScanner {
8285    found: bool,
8286}
8287
8288impl<'a> Visit<'a> for CoverageSurfaceScanner {
8289    fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
8290        self.found = true;
8291    }
8292
8293    fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
8294        self.found = true;
8295    }
8296
8297    fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
8298        self.found = true;
8299    }
8300
8301    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
8302        self.found = true;
8303    }
8304
8305    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
8306        self.found = true;
8307    }
8308
8309    fn visit_class(&mut self, _class: &Class<'a>) {
8310        self.found = true;
8311    }
8312
8313    fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
8314        if expression.operator.is_logical() {
8315            self.found = true;
8316        } else {
8317            walk::walk_assignment_expression(self, expression);
8318        }
8319    }
8320}
8321
8322fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
8323    fn visit_condition(expression: &Expression<'_>) -> bool {
8324        let mut scanner = CoverageSurfaceScanner::default();
8325        scanner.visit_expression(expression);
8326        !scanner.found
8327    }
8328    match transparent_expression(expression) {
8329        Expression::LogicalExpression(logical)
8330            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8331        {
8332            decision_conditions_are_transparent(&logical.left)
8333                && decision_conditions_are_transparent(&logical.right)
8334        }
8335        Expression::UnaryExpression(unary)
8336            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8337        {
8338            decision_conditions_are_transparent(&unary.argument)
8339        }
8340        condition => visit_condition(condition),
8341    }
8342}
8343
8344fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
8345    fn evaluate(
8346        expression: &Expression<'_>,
8347        assignment: usize,
8348        encoded: &mut usize,
8349        indices: &HashMap<SpanKey, usize>,
8350    ) -> bool {
8351        match transparent_expression(expression) {
8352            Expression::LogicalExpression(logical)
8353                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8354            {
8355                let left = evaluate(&logical.left, assignment, encoded, indices);
8356                if logical.operator == LogicalOperator::And {
8357                    left && evaluate(&logical.right, assignment, encoded, indices)
8358                } else {
8359                    left || evaluate(&logical.right, assignment, encoded, indices)
8360                }
8361            }
8362            Expression::UnaryExpression(unary)
8363                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8364            {
8365                !evaluate(&unary.argument, assignment, encoded, indices)
8366            }
8367            condition => {
8368                let index = indices
8369                    .get(&span_key(condition.span()))
8370                    .expect("decision condition index must remain stable");
8371                let value = assignment & (1 << index) != 0;
8372                *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
8373                value
8374            }
8375        }
8376    }
8377
8378    let indices = conditions
8379        .iter()
8380        .enumerate()
8381        .map(|(index, span)| (span_key(*span), index))
8382        .collect::<HashMap<_, _>>();
8383    let mut vectors = HashSet::new();
8384    for assignment in 0..(1_usize << conditions.len()) {
8385        let mut encoded = 0;
8386        let outcome = evaluate(expression, assignment, &mut encoded, &indices);
8387        vectors.insert(encoded * 2 + usize::from(outcome));
8388    }
8389    vectors.len()
8390}
8391
8392fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
8393    match expression {
8394        Expression::ParenthesizedExpression(parenthesized) => {
8395            collect_decision_logical_nodes(&parenthesized.expression, nodes);
8396        }
8397        Expression::LogicalExpression(logical)
8398            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8399        {
8400            nodes.insert(span_key(logical.span));
8401            collect_decision_logical_nodes(&logical.left, nodes);
8402            collect_decision_logical_nodes(&logical.right, nodes);
8403        }
8404        Expression::UnaryExpression(unary)
8405            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8406        {
8407            collect_decision_logical_nodes(&unary.argument, nodes);
8408        }
8409        _ => {}
8410    }
8411}
8412
8413#[derive(Default)]
8414struct LogicalBranchAnalysis {
8415    branches: Vec<CandidateBranch>,
8416    logical_targets: HashMap<SpanKey, (String, String)>,
8417}
8418
8419#[derive(Default)]
8420struct LogicalAssignmentAnalysis {
8421    branches: Vec<CandidateBranch>,
8422    targets: HashMap<SpanKey, (String, String)>,
8423}
8424
8425#[derive(Default)]
8426struct OptionalMemberAnalysis {
8427    branches: Vec<CandidateBranch>,
8428    targets: HashMap<SpanKey, (String, String)>,
8429    /// Each target's place in its chain, as `optionalSelectV2` reads it.
8430    links: HashMap<SpanKey, u8>,
8431    /// The optional call a target's object holds, for a target right after
8432    /// one: whether that call went ahead says whether the target ran.
8433    after_call: HashMap<SpanKey, SpanKey>,
8434}
8435
8436/// An optional link of the same chain runs before this one.
8437const LINK_AFTER_ANOTHER: u8 = 1;
8438/// An optional link of the same chain runs after this one.
8439const LINK_BEFORE_ANOTHER: u8 = 2;
8440/// The link before this one is an optional call.
8441const LINK_AFTER_CALL: u8 = 4;
8442
8443/// The optional link of the same chain an optional member's object holds.
8444enum LinkBelow {
8445    None,
8446    Member,
8447    Call(SpanKey),
8448}
8449
8450/// The earlier optional link of the same chain that `object`, the object of
8451/// an optional member, holds: through plain member accesses, plain calls and
8452/// `!`, which the chain's short-circuit skips along with the rest.
8453/// Parentheses end the chain.
8454fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
8455    let mut current = object;
8456    loop {
8457        current = match current {
8458            Expression::StaticMemberExpression(member) if member.optional => {
8459                return LinkBelow::Member;
8460            }
8461            Expression::ComputedMemberExpression(member) if member.optional => {
8462                return LinkBelow::Member;
8463            }
8464            Expression::PrivateFieldExpression(member) if member.optional => {
8465                return LinkBelow::Member;
8466            }
8467            Expression::CallExpression(call) if call.optional => {
8468                return LinkBelow::Call(span_key(call.span));
8469            }
8470            Expression::StaticMemberExpression(member) => &member.object,
8471            Expression::ComputedMemberExpression(member) => &member.object,
8472            Expression::PrivateFieldExpression(member) => &member.object,
8473            Expression::CallExpression(call) => &call.callee,
8474            Expression::TSNonNullExpression(inner) => &inner.expression,
8475            _ => return LinkBelow::None,
8476        };
8477    }
8478}
8479
8480/// The same walk outward: whether this member is the object, through the
8481/// same plain links, of a later optional member of its chain.
8482fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
8483    for ancestor in context.ancestors() {
8484        match ancestor {
8485            Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
8486            Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
8487            Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
8488            Ancestor::StaticMemberExpressionObject(_)
8489            | Ancestor::ComputedMemberExpressionObject(_)
8490            | Ancestor::PrivateFieldExpressionObject(_)
8491            | Ancestor::TSNonNullExpressionExpression(_) => {}
8492            Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
8493            _ => return false,
8494        }
8495    }
8496    false
8497}
8498
8499#[derive(Default)]
8500struct OptionalCallAnalysis {
8501    branches: Vec<CandidateBranch>,
8502    sites: HashMap<SpanKey, (String, String)>,
8503    roots: HashMap<SpanKey, Vec<SpanKey>>,
8504    limitations: Vec<CandidateLimitation>,
8505}
8506
8507#[derive(Clone)]
8508struct DefaultTarget {
8509    default_id: String,
8510    provided_id: String,
8511    inferred_name: Option<String>,
8512}
8513
8514#[derive(Default)]
8515struct DefaultAnalysis {
8516    branches: Vec<CandidateBranch>,
8517    parameter_targets: HashMap<SpanKey, DefaultTarget>,
8518    binding_targets: HashMap<SpanKey, DefaultTarget>,
8519    limitations: Vec<CandidateLimitation>,
8520}
8521
8522#[derive(Clone)]
8523struct ExtendedTarget {
8524    first_id: String,
8525    second_id: String,
8526}
8527
8528#[derive(Default)]
8529struct ExtendedAnalysis {
8530    branches: Vec<CandidateBranch>,
8531    try_targets: HashMap<SpanKey, ExtendedTarget>,
8532    loop_targets: HashMap<SpanKey, ExtendedTarget>,
8533}
8534
8535#[derive(Clone)]
8536struct SwitchTarget {
8537    case_ids: Vec<String>,
8538    no_match_id: Option<String>,
8539}
8540
8541#[derive(Default)]
8542struct SwitchAnalysis {
8543    branches: Vec<CandidateBranch>,
8544    targets: HashMap<SpanKey, SwitchTarget>,
8545}
8546
8547struct SwitchCollector<'s> {
8548    source: &'s str,
8549    file: &'s str,
8550    source_sensitive_functions: &'s HashSet<SpanKey>,
8551    unsafe_function_depth: usize,
8552    with_depth: usize,
8553    analysis: SwitchAnalysis,
8554}
8555
8556impl SwitchCollector<'_> {
8557    fn unsafe_context(&self) -> bool {
8558        self.unsafe_function_depth > 0 || self.with_depth > 0
8559    }
8560
8561    fn exit_source_function(&mut self, span: Span) {
8562        if self.source_sensitive_functions.contains(&span_key(span)) {
8563            self.unsafe_function_depth -= 1;
8564        }
8565    }
8566}
8567
8568impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
8569    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8570        if self
8571            .source_sensitive_functions
8572            .contains(&span_key(node.span))
8573        {
8574            self.unsafe_function_depth += 1;
8575        }
8576    }
8577
8578    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8579        self.exit_source_function(node.span);
8580    }
8581
8582    fn enter_arrow_function_expression(
8583        &mut self,
8584        node: &mut ArrowFunctionExpression<'a>,
8585        _context: &mut TraverseCtx<'a, ()>,
8586    ) {
8587        if self
8588            .source_sensitive_functions
8589            .contains(&span_key(node.span))
8590        {
8591            self.unsafe_function_depth += 1;
8592        }
8593    }
8594
8595    fn exit_arrow_function_expression(
8596        &mut self,
8597        node: &mut ArrowFunctionExpression<'a>,
8598        _context: &mut TraverseCtx<'a, ()>,
8599    ) {
8600        self.exit_source_function(node.span);
8601    }
8602
8603    fn enter_with_statement(
8604        &mut self,
8605        _node: &mut WithStatement<'a>,
8606        _context: &mut TraverseCtx<'a, ()>,
8607    ) {
8608        self.with_depth += 1;
8609    }
8610
8611    fn exit_with_statement(
8612        &mut self,
8613        _node: &mut WithStatement<'a>,
8614        _context: &mut TraverseCtx<'a, ()>,
8615    ) {
8616        self.with_depth -= 1;
8617    }
8618
8619    fn enter_switch_statement(
8620        &mut self,
8621        node: &mut SwitchStatement<'a>,
8622        _context: &mut TraverseCtx<'a, ()>,
8623    ) {
8624        let has_default = node.cases.iter().any(|case| case.test.is_none());
8625        if self.unsafe_context() {
8626            return;
8627        }
8628        let id = stable_id(self.source, self.file, "switch", node.span, "");
8629        let mut case_ids = Vec::with_capacity(node.cases.len());
8630        let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
8631        for (index, case) in node.cases.iter().enumerate() {
8632            let alternative_id = format!("{id}:case:{index}");
8633            let label = case.test.as_ref().map_or_else(
8634                || "default".to_string(),
8635                |test| format!("case {}", source_slice(self.source, test.span())),
8636            );
8637            case_ids.push(alternative_id.clone());
8638            alternatives.push(CandidateBranchAlternative {
8639                id: alternative_id,
8640                label,
8641            });
8642        }
8643        let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
8644        if let Some(no_match_id) = &no_match_id {
8645            alternatives.push(CandidateBranchAlternative {
8646                id: no_match_id.clone(),
8647                label: "no matching case".to_string(),
8648            });
8649        }
8650        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8651        self.analysis.branches.push(CandidateBranch {
8652            id,
8653            kind: "switch".to_string(),
8654            file: self.file.to_string(),
8655            line,
8656            column,
8657            source: source_slice(self.source, node.discriminant.span()).to_string(),
8658            alternatives,
8659        });
8660        self.analysis.targets.insert(
8661            span_key(node.span),
8662            SwitchTarget {
8663                case_ids,
8664                no_match_id,
8665            },
8666        );
8667    }
8668}
8669
8670fn collect_switch_branches<'a>(
8671    allocator: &'a Allocator,
8672    program: &mut Program<'a>,
8673    source: &str,
8674    file: &str,
8675    source_sensitive_functions: &HashSet<SpanKey>,
8676) -> SwitchAnalysis {
8677    let mut collector = SwitchCollector {
8678        source,
8679        file,
8680        source_sensitive_functions,
8681        unsafe_function_depth: 0,
8682        with_depth: 0,
8683        analysis: SwitchAnalysis::default(),
8684    };
8685    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8686    collector.analysis
8687}
8688
8689struct ExtendedCollector<'s> {
8690    source: &'s str,
8691    file: &'s str,
8692    source_sensitive_functions: &'s HashSet<SpanKey>,
8693    entered_loops: HashSet<SpanKey>,
8694    unsafe_function_depth: usize,
8695    with_depth: usize,
8696    analysis: ExtendedAnalysis,
8697}
8698
8699impl ExtendedCollector<'_> {
8700    fn unsafe_context(&self) -> bool {
8701        self.unsafe_function_depth > 0 || self.with_depth > 0
8702    }
8703
8704    fn enter_try(&mut self, node: &TryStatement<'_>) {
8705        let transformed = !self.unsafe_context() && node.handler.is_some();
8706        if !transformed {
8707            return;
8708        }
8709        let id = stable_id(self.source, self.file, "try-catch", node.span, "");
8710        let success_id = format!("{id}:success");
8711        let catch_id = format!("{id}:catch");
8712        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8713        self.analysis.branches.push(CandidateBranch {
8714            id,
8715            kind: "try-catch".to_string(),
8716            file: self.file.to_string(),
8717            line,
8718            column,
8719            source: "try / catch".to_string(),
8720            alternatives: vec![
8721                CandidateBranchAlternative {
8722                    id: success_id.clone(),
8723                    label: "try completed without catch".to_string(),
8724                },
8725                CandidateBranchAlternative {
8726                    id: catch_id.clone(),
8727                    label: "catch entered".to_string(),
8728                },
8729            ],
8730        });
8731        self.analysis.try_targets.insert(
8732            span_key(node.span),
8733            ExtendedTarget {
8734                first_id: success_id,
8735                second_id: catch_id,
8736            },
8737        );
8738    }
8739
8740    fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
8741        let transformed = !self.unsafe_context();
8742        if !transformed {
8743            return;
8744        }
8745        // One feasible outcome is not a branch. Recording it as one leaves a
8746        // permanently unreachable obligation, which is how a project ends up
8747        // below 100% with nothing to do about it.
8748        if self.entered_loops.contains(&span_key(span)) {
8749            return;
8750        }
8751        let id = stable_id(self.source, self.file, kind, span, "");
8752        let zero_id = format!("{id}:zero");
8753        let entered_id = format!("{id}:entered");
8754        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8755        self.analysis.branches.push(CandidateBranch {
8756            id,
8757            kind: kind.to_string(),
8758            file: self.file.to_string(),
8759            line,
8760            column,
8761            source: source_slice(self.source, right.span()).to_string(),
8762            alternatives: vec![
8763                CandidateBranchAlternative {
8764                    id: zero_id.clone(),
8765                    label: "zero iterations".to_string(),
8766                },
8767                CandidateBranchAlternative {
8768                    id: entered_id.clone(),
8769                    label: "one or more iterations".to_string(),
8770                },
8771            ],
8772        });
8773        self.analysis.loop_targets.insert(
8774            span_key(span),
8775            ExtendedTarget {
8776                first_id: zero_id,
8777                second_id: entered_id,
8778            },
8779        );
8780    }
8781
8782    fn exit_source_function(&mut self, span: Span) {
8783        if self.source_sensitive_functions.contains(&span_key(span)) {
8784            self.unsafe_function_depth -= 1;
8785        }
8786    }
8787}
8788
8789impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
8790    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8791        if self
8792            .source_sensitive_functions
8793            .contains(&span_key(node.span))
8794        {
8795            self.unsafe_function_depth += 1;
8796        }
8797    }
8798
8799    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8800        self.exit_source_function(node.span);
8801    }
8802
8803    fn enter_arrow_function_expression(
8804        &mut self,
8805        node: &mut ArrowFunctionExpression<'a>,
8806        _context: &mut TraverseCtx<'a, ()>,
8807    ) {
8808        if self
8809            .source_sensitive_functions
8810            .contains(&span_key(node.span))
8811        {
8812            self.unsafe_function_depth += 1;
8813        }
8814    }
8815
8816    fn exit_arrow_function_expression(
8817        &mut self,
8818        node: &mut ArrowFunctionExpression<'a>,
8819        _context: &mut TraverseCtx<'a, ()>,
8820    ) {
8821        self.exit_source_function(node.span);
8822    }
8823
8824    fn enter_with_statement(
8825        &mut self,
8826        _node: &mut WithStatement<'a>,
8827        _context: &mut TraverseCtx<'a, ()>,
8828    ) {
8829        self.with_depth += 1;
8830    }
8831
8832    fn exit_with_statement(
8833        &mut self,
8834        _node: &mut WithStatement<'a>,
8835        _context: &mut TraverseCtx<'a, ()>,
8836    ) {
8837        self.with_depth -= 1;
8838    }
8839
8840    fn enter_try_statement(
8841        &mut self,
8842        node: &mut TryStatement<'a>,
8843        _context: &mut TraverseCtx<'a, ()>,
8844    ) {
8845        self.enter_try(node);
8846    }
8847
8848    fn exit_try_statement(
8849        &mut self,
8850        _node: &mut TryStatement<'a>,
8851        _context: &mut TraverseCtx<'a, ()>,
8852    ) {
8853    }
8854
8855    fn enter_for_in_statement(
8856        &mut self,
8857        node: &mut ForInStatement<'a>,
8858        _context: &mut TraverseCtx<'a, ()>,
8859    ) {
8860        self.enter_loop(node.span, &node.right, "for-in");
8861    }
8862
8863    fn exit_for_in_statement(
8864        &mut self,
8865        _node: &mut ForInStatement<'a>,
8866        _context: &mut TraverseCtx<'a, ()>,
8867    ) {
8868    }
8869
8870    fn enter_for_of_statement(
8871        &mut self,
8872        node: &mut ForOfStatement<'a>,
8873        _context: &mut TraverseCtx<'a, ()>,
8874    ) {
8875        self.enter_loop(node.span, &node.right, "for-of");
8876    }
8877
8878    fn exit_for_of_statement(
8879        &mut self,
8880        _node: &mut ForOfStatement<'a>,
8881        _context: &mut TraverseCtx<'a, ()>,
8882    ) {
8883    }
8884}
8885
8886/// Loops that must run at least once.
8887///
8888/// A `for...of` over a non-empty array literal has no zero-iteration outcome
8889/// for a test to reach, so asking for one is asking for something impossible.
8890/// The author's only ways to satisfy it are to fake a test or delete the loop,
8891/// and a coverage tool should ask for neither.
8892///
8893/// This is decided from the syntax tree and symbol resolution alone. It stays
8894/// there deliberately: a type would answer more questions, but a type can be
8895/// asserted (`as Foo`) or optimistic (`xs[0]` is `T`, and `[][0]` is
8896/// `undefined`), and an obligation dropped on a wrong answer is a branch that
8897/// runs and is never reported again. Over-asking is recoverable; this is not.
8898fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
8899    let semantic = SemanticBuilder::new().build(program).semantic;
8900    let mut found = HashSet::new();
8901    for node in semantic.nodes() {
8902        if let AstKind::ForOfStatement(loop_node) = node.kind()
8903            && never_empty(&loop_node.right, &semantic)
8904        {
8905            found.insert(span_key(loop_node.span));
8906        }
8907    }
8908    found
8909}
8910
8911/// Whether an iterable cannot be empty: a non-empty array literal, or a
8912/// `const` bound to one that nothing else in the file can reach.
8913fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
8914    match transparent_expression(expression) {
8915        Expression::ArrayExpression(array) => has_definite_element(array),
8916        Expression::Identifier(identifier) => {
8917            let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
8918                return false;
8919            };
8920            // Nothing but this loop may reach the binding. A shrinking method
8921            // (`pop`, `splice`, `length = 0`) is an ordinary read of the
8922            // identifier, so counting references is what makes this sound --
8923            // recognising a list of mutating methods would not be.
8924            if semantic.scoping().get_resolved_references(symbol).count() != 1 {
8925                return false;
8926            }
8927            let declaration = semantic.scoping().symbol_declaration(symbol);
8928            let AstKind::VariableDeclarator(declarator) =
8929                semantic.nodes().get_node(declaration).kind()
8930            else {
8931                return false;
8932            };
8933            declarator.kind == VariableDeclarationKind::Const
8934                && declarator.init.as_ref().is_some_and(|init| {
8935                    matches!(transparent_expression(init), Expression::ArrayExpression(array)
8936                        if has_definite_element(array))
8937                })
8938        }
8939        _ => false,
8940    }
8941}
8942
8943/// An element that is certainly there. A spread may contribute nothing, so
8944/// `[...xs]` is not known to be non-empty; a hole is, since `[,]` has length 1
8945/// and yields once.
8946fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
8947    array
8948        .elements
8949        .iter()
8950        .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
8951}
8952
8953fn collect_extended_branches<'a>(
8954    allocator: &'a Allocator,
8955    program: &mut Program<'a>,
8956    source: &str,
8957    file: &str,
8958    source_sensitive_functions: &HashSet<SpanKey>,
8959) -> ExtendedAnalysis {
8960    let entered_loops = provably_entered_loops(program);
8961    let mut collector = ExtendedCollector {
8962        source,
8963        file,
8964        source_sensitive_functions,
8965        entered_loops,
8966        unsafe_function_depth: 0,
8967        with_depth: 0,
8968        analysis: ExtendedAnalysis::default(),
8969    };
8970    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8971    collector.analysis
8972}
8973
8974struct DefaultCollector<'s> {
8975    source: &'s str,
8976    file: &'s str,
8977    source_sensitive_functions: &'s HashSet<SpanKey>,
8978    unsafe_function_depth: usize,
8979    with_depth: usize,
8980    analysis: DefaultAnalysis,
8981}
8982
8983impl DefaultCollector<'_> {
8984    fn unsafe_context(&self) -> bool {
8985        self.unsafe_function_depth > 0 || self.with_depth > 0
8986    }
8987
8988    fn target(
8989        &mut self,
8990        span: Span,
8991        left: &BindingPattern<'_>,
8992        right: &Expression<'_>,
8993    ) -> DefaultTarget {
8994        let id = stable_id(self.source, self.file, "default-value", span, "");
8995        let default_id = format!("{id}:default");
8996        let provided_id = format!("{id}:provided");
8997        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8998        self.analysis.branches.push(CandidateBranch {
8999            id,
9000            kind: "default-value".to_string(),
9001            file: self.file.to_string(),
9002            line,
9003            column,
9004            source: source_slice(self.source, span).to_string(),
9005            alternatives: vec![
9006                CandidateBranchAlternative {
9007                    id: default_id.clone(),
9008                    label: "default evaluated".to_string(),
9009                },
9010                CandidateBranchAlternative {
9011                    id: provided_id.clone(),
9012                    label: "value provided".to_string(),
9013                },
9014            ],
9015        });
9016        DefaultTarget {
9017            default_id,
9018            provided_id,
9019            inferred_name: binding_identifier_name(left)
9020                .filter(|_| expression_is_anonymous_definition(right)),
9021        }
9022    }
9023
9024    fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
9025        match pattern {
9026            BindingPattern::AssignmentPattern(assignment) => {
9027                let target = self.target(assignment.span, &assignment.left, &assignment.right);
9028                if parameter {
9029                    self.analysis
9030                        .parameter_targets
9031                        .insert(span_key(assignment.span), target);
9032                } else {
9033                    self.analysis
9034                        .binding_targets
9035                        .insert(span_key(assignment.span), target);
9036                }
9037                self.collect_binding_pattern(&assignment.left, parameter);
9038            }
9039            BindingPattern::ObjectPattern(object) => {
9040                for property in &object.properties {
9041                    self.collect_binding_pattern(&property.value, parameter);
9042                }
9043                if let Some(rest) = &object.rest {
9044                    self.collect_binding_pattern(&rest.argument, parameter);
9045                }
9046            }
9047            BindingPattern::ArrayPattern(array) => {
9048                for element in array.elements.iter().flatten() {
9049                    self.collect_binding_pattern(element, parameter);
9050                }
9051                if let Some(rest) = &array.rest {
9052                    self.collect_binding_pattern(&rest.argument, parameter);
9053                }
9054            }
9055            BindingPattern::BindingIdentifier(_) => {}
9056        }
9057    }
9058
9059    fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
9060        for parameter in &parameters.items {
9061            if let Some(initializer) = &parameter.initializer {
9062                // Babel models a formal-parameter default as an AssignmentPattern whose
9063                // source range starts at the binding and ends at the initializer. oxc
9064                // keeps the initializer beside the BindingPattern and, for a TypeScript
9065                // parameter property, includes access/readonly modifiers in
9066                // FormalParameter::span. Keep the outer span as the transformation key,
9067                // but publish the same semantic default-expression range as Babel.
9068                let default_span =
9069                    Span::new(parameter.pattern.span().start, initializer.span().end);
9070                let target = self.target(default_span, &parameter.pattern, initializer);
9071                self.analysis
9072                    .parameter_targets
9073                    .insert(span_key(parameter.span), target);
9074            }
9075            self.collect_binding_pattern(&parameter.pattern, true);
9076        }
9077        if let Some(rest) = &parameters.rest {
9078            self.collect_binding_pattern(&rest.rest.argument, true);
9079        }
9080    }
9081
9082    fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
9083        match pattern {
9084            BindingPattern::AssignmentPattern(_) => true,
9085            BindingPattern::ObjectPattern(object) => {
9086                object
9087                    .properties
9088                    .iter()
9089                    .any(|property| Self::has_binding_default(&property.value))
9090                    || object
9091                        .rest
9092                        .as_ref()
9093                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9094            }
9095            BindingPattern::ArrayPattern(array) => {
9096                array
9097                    .elements
9098                    .iter()
9099                    .flatten()
9100                    .any(Self::has_binding_default)
9101                    || array
9102                        .rest
9103                        .as_ref()
9104                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9105            }
9106            BindingPattern::BindingIdentifier(_) => false,
9107        }
9108    }
9109
9110    fn exit_source_function(&mut self, span: Span) {
9111        if self.source_sensitive_functions.contains(&span_key(span)) {
9112            self.unsafe_function_depth -= 1;
9113        }
9114    }
9115}
9116
9117impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
9118    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9119        if self
9120            .source_sensitive_functions
9121            .contains(&span_key(node.span))
9122        {
9123            self.unsafe_function_depth += 1;
9124            return;
9125        }
9126        if !self.unsafe_context() && node.body.is_some() {
9127            self.collect_parameters(&node.params);
9128        }
9129    }
9130
9131    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9132        self.exit_source_function(node.span);
9133    }
9134
9135    fn enter_arrow_function_expression(
9136        &mut self,
9137        node: &mut ArrowFunctionExpression<'a>,
9138        _context: &mut TraverseCtx<'a, ()>,
9139    ) {
9140        if self
9141            .source_sensitive_functions
9142            .contains(&span_key(node.span))
9143        {
9144            self.unsafe_function_depth += 1;
9145            return;
9146        }
9147        if !self.unsafe_context() {
9148            self.collect_parameters(&node.params);
9149        }
9150    }
9151
9152    fn exit_arrow_function_expression(
9153        &mut self,
9154        node: &mut ArrowFunctionExpression<'a>,
9155        _context: &mut TraverseCtx<'a, ()>,
9156    ) {
9157        self.exit_source_function(node.span);
9158    }
9159
9160    fn enter_with_statement(
9161        &mut self,
9162        _node: &mut WithStatement<'a>,
9163        _context: &mut TraverseCtx<'a, ()>,
9164    ) {
9165        self.with_depth += 1;
9166    }
9167
9168    fn exit_with_statement(
9169        &mut self,
9170        _node: &mut WithStatement<'a>,
9171        _context: &mut TraverseCtx<'a, ()>,
9172    ) {
9173        self.with_depth -= 1;
9174    }
9175
9176    fn enter_variable_declaration(
9177        &mut self,
9178        node: &mut VariableDeclaration<'a>,
9179        context: &mut TraverseCtx<'a, ()>,
9180    ) {
9181        if self.unsafe_context() {
9182            return;
9183        }
9184        let has_default = node
9185            .declarations
9186            .iter()
9187            .any(|declaration| Self::has_binding_default(&declaration.id));
9188        if !has_default {
9189            return;
9190        }
9191        if matches!(
9192            context.ancestors().next(),
9193            Some(Ancestor::ForStatementInit(_))
9194        ) {
9195            let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9196            self.analysis.limitations.push(CandidateLimitation {
9197                id: stable_id(
9198                    self.source,
9199                    self.file,
9200                    "dynamic-code",
9201                    node.span,
9202                    "for-init-default",
9203                ),
9204                kind: "dynamic-code".to_string(),
9205                file: self.file.to_string(),
9206                line,
9207                column,
9208                source: source_slice(self.source, node.span).to_string(),
9209                reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
9210            });
9211            return;
9212        }
9213        for declaration in &node.declarations {
9214            self.collect_binding_pattern(&declaration.id, false);
9215        }
9216    }
9217}
9218
9219fn collect_default_branches<'a>(
9220    allocator: &'a Allocator,
9221    program: &mut Program<'a>,
9222    source: &str,
9223    file: &str,
9224    source_sensitive_functions: &HashSet<SpanKey>,
9225) -> DefaultAnalysis {
9226    let mut collector = DefaultCollector {
9227        source,
9228        file,
9229        source_sensitive_functions,
9230        unsafe_function_depth: 0,
9231        with_depth: 0,
9232        analysis: DefaultAnalysis::default(),
9233    };
9234    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9235    collector.analysis
9236}
9237
9238struct OptionalCallCollector<'s> {
9239    source: &'s str,
9240    file: &'s str,
9241    source_sensitive_functions: &'s HashSet<SpanKey>,
9242    unsafe_function_depth: usize,
9243    with_depth: usize,
9244    chain_roots: Vec<SpanKey>,
9245    analysis: OptionalCallAnalysis,
9246}
9247
9248impl OptionalCallCollector<'_> {
9249    fn unsafe_context(&self) -> bool {
9250        self.unsafe_function_depth > 0 || self.with_depth > 0
9251    }
9252
9253    fn exit_source_function(&mut self, span: Span) {
9254        if self.source_sensitive_functions.contains(&span_key(span)) {
9255            self.unsafe_function_depth -= 1;
9256        }
9257    }
9258}
9259
9260impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
9261    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9262        if self
9263            .source_sensitive_functions
9264            .contains(&span_key(node.span))
9265        {
9266            self.unsafe_function_depth += 1;
9267        }
9268    }
9269
9270    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9271        self.exit_source_function(node.span);
9272    }
9273
9274    fn enter_arrow_function_expression(
9275        &mut self,
9276        node: &mut ArrowFunctionExpression<'a>,
9277        _context: &mut TraverseCtx<'a, ()>,
9278    ) {
9279        if self
9280            .source_sensitive_functions
9281            .contains(&span_key(node.span))
9282        {
9283            self.unsafe_function_depth += 1;
9284        }
9285    }
9286
9287    fn exit_arrow_function_expression(
9288        &mut self,
9289        node: &mut ArrowFunctionExpression<'a>,
9290        _context: &mut TraverseCtx<'a, ()>,
9291    ) {
9292        self.exit_source_function(node.span);
9293    }
9294
9295    fn enter_with_statement(
9296        &mut self,
9297        _node: &mut WithStatement<'a>,
9298        _context: &mut TraverseCtx<'a, ()>,
9299    ) {
9300        self.with_depth += 1;
9301    }
9302
9303    fn exit_with_statement(
9304        &mut self,
9305        _node: &mut WithStatement<'a>,
9306        _context: &mut TraverseCtx<'a, ()>,
9307    ) {
9308        self.with_depth -= 1;
9309    }
9310
9311    fn enter_chain_expression(
9312        &mut self,
9313        node: &mut ChainExpression<'a>,
9314        context: &mut TraverseCtx<'a, ()>,
9315    ) {
9316        let root = match context.ancestors().next() {
9317            Some(Ancestor::UnaryExpressionArgument(parent))
9318                if *parent.operator() == UnaryOperator::Delete =>
9319            {
9320                span_key(*parent.span())
9321            }
9322            _ => span_key(node.span),
9323        };
9324        self.chain_roots.push(root);
9325    }
9326
9327    fn exit_chain_expression(
9328        &mut self,
9329        _node: &mut ChainExpression<'a>,
9330        _context: &mut TraverseCtx<'a, ()>,
9331    ) {
9332        self.chain_roots.pop();
9333    }
9334
9335    fn enter_call_expression(
9336        &mut self,
9337        node: &mut CallExpression<'a>,
9338        _context: &mut TraverseCtx<'a, ()>,
9339    ) {
9340        if !node.optional || self.unsafe_context() {
9341            return;
9342        }
9343        let root = *self
9344            .chain_roots
9345            .last()
9346            .expect("an optional call must be enclosed by a chain expression");
9347        let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
9348        let short_id = format!("{id}:short");
9349        let continued_id = format!("{id}:continued");
9350        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9351        self.analysis.sites.insert(
9352            span_key(node.span),
9353            (short_id.clone(), continued_id.clone()),
9354        );
9355        self.analysis
9356            .roots
9357            .entry(root)
9358            .or_default()
9359            .push(span_key(node.span));
9360        self.analysis.branches.push(CandidateBranch {
9361            id,
9362            kind: "optional-chain".to_string(),
9363            file: self.file.to_string(),
9364            line,
9365            column,
9366            source: source_slice(self.source, node.span).to_string(),
9367            alternatives: vec![
9368                CandidateBranchAlternative {
9369                    id: short_id,
9370                    label: "nullish / short-circuited".to_string(),
9371                },
9372                CandidateBranchAlternative {
9373                    id: continued_id,
9374                    label: "non-nullish / continued".to_string(),
9375                },
9376            ],
9377        });
9378    }
9379}
9380
9381fn collect_optional_call_branches<'a>(
9382    allocator: &'a Allocator,
9383    program: &mut Program<'a>,
9384    source: &str,
9385    file: &str,
9386    source_sensitive_functions: &HashSet<SpanKey>,
9387) -> OptionalCallAnalysis {
9388    let mut collector = OptionalCallCollector {
9389        source,
9390        file,
9391        source_sensitive_functions,
9392        unsafe_function_depth: 0,
9393        with_depth: 0,
9394        chain_roots: Vec::new(),
9395        analysis: OptionalCallAnalysis::default(),
9396    };
9397    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9398    collector.analysis
9399}
9400
9401struct OptionalMemberCollector<'s> {
9402    source: &'s str,
9403    file: &'s str,
9404    source_sensitive_functions: &'s HashSet<SpanKey>,
9405    unsafe_function_depth: usize,
9406    with_depth: usize,
9407    analysis: OptionalMemberAnalysis,
9408}
9409
9410impl OptionalMemberCollector<'_> {
9411    fn record<State>(
9412        &mut self,
9413        span: Span,
9414        optional: bool,
9415        object: &Expression<'_>,
9416        context: &TraverseCtx<'_, State>,
9417    ) {
9418        if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
9419            return;
9420        }
9421        let below = match optional_link_below(object) {
9422            LinkBelow::Member => LINK_AFTER_ANOTHER,
9423            LinkBelow::Call(call) => {
9424                self.analysis.after_call.insert(span_key(span), call);
9425                LINK_AFTER_CALL
9426            }
9427            LinkBelow::None => 0,
9428        };
9429        let links = below
9430            | if optional_link_above(context) {
9431                LINK_BEFORE_ANOTHER
9432            } else {
9433                0
9434            };
9435        if links != 0 {
9436            self.analysis.links.insert(span_key(span), links);
9437        }
9438        let id = stable_id(self.source, self.file, "optional-chain", span, "");
9439        let short_id = format!("{id}:short");
9440        let continued_id = format!("{id}:continued");
9441        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
9442        self.analysis
9443            .targets
9444            .insert(span_key(span), (short_id.clone(), continued_id.clone()));
9445        self.analysis.branches.push(CandidateBranch {
9446            id,
9447            kind: "optional-chain".to_string(),
9448            file: self.file.to_string(),
9449            line,
9450            column,
9451            source: source_slice(self.source, span).to_string(),
9452            alternatives: vec![
9453                CandidateBranchAlternative {
9454                    id: short_id,
9455                    label: "nullish / short-circuited".to_string(),
9456                },
9457                CandidateBranchAlternative {
9458                    id: continued_id,
9459                    label: "non-nullish / continued".to_string(),
9460                },
9461            ],
9462        });
9463    }
9464
9465    fn exit_source_function(&mut self, span: Span) {
9466        if self.source_sensitive_functions.contains(&span_key(span)) {
9467            self.unsafe_function_depth -= 1;
9468        }
9469    }
9470}
9471
9472impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
9473    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9474        if self
9475            .source_sensitive_functions
9476            .contains(&span_key(node.span))
9477        {
9478            self.unsafe_function_depth += 1;
9479        }
9480    }
9481
9482    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9483        self.exit_source_function(node.span);
9484    }
9485
9486    fn enter_arrow_function_expression(
9487        &mut self,
9488        node: &mut ArrowFunctionExpression<'a>,
9489        _context: &mut TraverseCtx<'a, ()>,
9490    ) {
9491        if self
9492            .source_sensitive_functions
9493            .contains(&span_key(node.span))
9494        {
9495            self.unsafe_function_depth += 1;
9496        }
9497    }
9498
9499    fn exit_arrow_function_expression(
9500        &mut self,
9501        node: &mut ArrowFunctionExpression<'a>,
9502        _context: &mut TraverseCtx<'a, ()>,
9503    ) {
9504        self.exit_source_function(node.span);
9505    }
9506
9507    fn enter_with_statement(
9508        &mut self,
9509        _node: &mut WithStatement<'a>,
9510        _context: &mut TraverseCtx<'a, ()>,
9511    ) {
9512        self.with_depth += 1;
9513    }
9514
9515    fn exit_with_statement(
9516        &mut self,
9517        _node: &mut WithStatement<'a>,
9518        _context: &mut TraverseCtx<'a, ()>,
9519    ) {
9520        self.with_depth -= 1;
9521    }
9522
9523    fn enter_computed_member_expression(
9524        &mut self,
9525        node: &mut ComputedMemberExpression<'a>,
9526        context: &mut TraverseCtx<'a, ()>,
9527    ) {
9528        self.record(node.span, node.optional, &node.object, context);
9529    }
9530
9531    fn enter_static_member_expression(
9532        &mut self,
9533        node: &mut StaticMemberExpression<'a>,
9534        context: &mut TraverseCtx<'a, ()>,
9535    ) {
9536        self.record(node.span, node.optional, &node.object, context);
9537    }
9538
9539    fn enter_private_field_expression(
9540        &mut self,
9541        node: &mut PrivateFieldExpression<'a>,
9542        context: &mut TraverseCtx<'a, ()>,
9543    ) {
9544        self.record(node.span, node.optional, &node.object, context);
9545    }
9546}
9547
9548fn collect_optional_member_branches<'a>(
9549    allocator: &'a Allocator,
9550    program: &mut Program<'a>,
9551    source: &str,
9552    file: &str,
9553    source_sensitive_functions: &HashSet<SpanKey>,
9554) -> OptionalMemberAnalysis {
9555    let mut collector = OptionalMemberCollector {
9556        source,
9557        file,
9558        source_sensitive_functions,
9559        unsafe_function_depth: 0,
9560        with_depth: 0,
9561        analysis: OptionalMemberAnalysis::default(),
9562    };
9563    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9564    collector.analysis
9565}
9566
9567struct LogicalAssignmentCollector<'s> {
9568    source: &'s str,
9569    file: &'s str,
9570    source_sensitive_functions: &'s HashSet<SpanKey>,
9571    unsafe_function_depth: usize,
9572    with_depth: usize,
9573    analysis: LogicalAssignmentAnalysis,
9574}
9575
9576impl LogicalAssignmentCollector<'_> {
9577    fn exit_source_function(&mut self, span: Span) {
9578        if self.source_sensitive_functions.contains(&span_key(span)) {
9579            self.unsafe_function_depth -= 1;
9580        }
9581    }
9582}
9583
9584impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
9585    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9586        if self
9587            .source_sensitive_functions
9588            .contains(&span_key(node.span))
9589        {
9590            self.unsafe_function_depth += 1;
9591        }
9592    }
9593
9594    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9595        self.exit_source_function(node.span);
9596    }
9597
9598    fn enter_arrow_function_expression(
9599        &mut self,
9600        node: &mut ArrowFunctionExpression<'a>,
9601        _context: &mut TraverseCtx<'a, ()>,
9602    ) {
9603        if self
9604            .source_sensitive_functions
9605            .contains(&span_key(node.span))
9606        {
9607            self.unsafe_function_depth += 1;
9608        }
9609    }
9610
9611    fn exit_arrow_function_expression(
9612        &mut self,
9613        node: &mut ArrowFunctionExpression<'a>,
9614        _context: &mut TraverseCtx<'a, ()>,
9615    ) {
9616        self.exit_source_function(node.span);
9617    }
9618
9619    fn enter_with_statement(
9620        &mut self,
9621        _node: &mut WithStatement<'a>,
9622        _context: &mut TraverseCtx<'a, ()>,
9623    ) {
9624        self.with_depth += 1;
9625    }
9626
9627    fn exit_with_statement(
9628        &mut self,
9629        _node: &mut WithStatement<'a>,
9630        _context: &mut TraverseCtx<'a, ()>,
9631    ) {
9632        self.with_depth -= 1;
9633    }
9634
9635    fn exit_assignment_expression(
9636        &mut self,
9637        node: &mut AssignmentExpression<'a>,
9638        _context: &mut TraverseCtx<'a, ()>,
9639    ) {
9640        if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
9641            return;
9642        }
9643        let operator = match node.operator {
9644            AssignmentOperator::LogicalAnd => "&&=",
9645            AssignmentOperator::LogicalOr => "||=",
9646            AssignmentOperator::LogicalNullish => "??=",
9647            _ => unreachable!("logical assignment filter must be exhaustive"),
9648        };
9649        let id = stable_id(
9650            self.source,
9651            self.file,
9652            "logical-assignment",
9653            node.span,
9654            operator,
9655        );
9656        let short_id = format!("{id}:short");
9657        let right_id = format!("{id}:right");
9658        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9659        self.analysis
9660            .targets
9661            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9662        self.analysis.branches.push(CandidateBranch {
9663            id,
9664            kind: "logical-assignment".to_string(),
9665            file: self.file.to_string(),
9666            line,
9667            column,
9668            source: source_slice(self.source, node.span).to_string(),
9669            alternatives: vec![
9670                CandidateBranchAlternative {
9671                    id: short_id,
9672                    label: "assignment skipped".to_string(),
9673                },
9674                CandidateBranchAlternative {
9675                    id: right_id,
9676                    label: "right evaluated / assigned".to_string(),
9677                },
9678            ],
9679        });
9680    }
9681}
9682
9683fn collect_logical_assignment_branches<'a>(
9684    allocator: &'a Allocator,
9685    program: &mut Program<'a>,
9686    source: &str,
9687    file: &str,
9688    source_sensitive_functions: &HashSet<SpanKey>,
9689) -> LogicalAssignmentAnalysis {
9690    let mut collector = LogicalAssignmentCollector {
9691        source,
9692        file,
9693        source_sensitive_functions,
9694        unsafe_function_depth: 0,
9695        with_depth: 0,
9696        analysis: LogicalAssignmentAnalysis::default(),
9697    };
9698    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9699    collector.analysis
9700}
9701
9702struct LogicalBranchCollector<'s> {
9703    source: &'s str,
9704    file: &'s str,
9705    decision_logical_nodes: &'s HashSet<SpanKey>,
9706    source_sensitive_functions: &'s HashSet<SpanKey>,
9707    unsafe_function_depth: usize,
9708    with_depth: usize,
9709    analysis: LogicalBranchAnalysis,
9710}
9711
9712impl LogicalBranchCollector<'_> {
9713    fn exit_source_function(&mut self, span: Span) {
9714        if self.source_sensitive_functions.contains(&span_key(span)) {
9715            self.unsafe_function_depth -= 1;
9716        }
9717    }
9718}
9719
9720impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
9721    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9722        if self
9723            .source_sensitive_functions
9724            .contains(&span_key(node.span))
9725        {
9726            self.unsafe_function_depth += 1;
9727        }
9728    }
9729
9730    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9731        self.exit_source_function(node.span);
9732    }
9733
9734    fn enter_arrow_function_expression(
9735        &mut self,
9736        node: &mut ArrowFunctionExpression<'a>,
9737        _context: &mut TraverseCtx<'a, ()>,
9738    ) {
9739        if self
9740            .source_sensitive_functions
9741            .contains(&span_key(node.span))
9742        {
9743            self.unsafe_function_depth += 1;
9744        }
9745    }
9746
9747    fn exit_arrow_function_expression(
9748        &mut self,
9749        node: &mut ArrowFunctionExpression<'a>,
9750        _context: &mut TraverseCtx<'a, ()>,
9751    ) {
9752        self.exit_source_function(node.span);
9753    }
9754
9755    fn enter_with_statement(
9756        &mut self,
9757        _node: &mut WithStatement<'a>,
9758        _context: &mut TraverseCtx<'a, ()>,
9759    ) {
9760        self.with_depth += 1;
9761    }
9762
9763    fn exit_with_statement(
9764        &mut self,
9765        _node: &mut WithStatement<'a>,
9766        _context: &mut TraverseCtx<'a, ()>,
9767    ) {
9768        self.with_depth -= 1;
9769    }
9770
9771    fn exit_logical_expression(
9772        &mut self,
9773        node: &mut LogicalExpression<'a>,
9774        _context: &mut TraverseCtx<'a, ()>,
9775    ) {
9776        if self.unsafe_function_depth > 0
9777            || self.with_depth > 0
9778            || self.decision_logical_nodes.contains(&span_key(node.span))
9779        {
9780            return;
9781        }
9782        let operator = match node.operator {
9783            LogicalOperator::And => "&&",
9784            LogicalOperator::Or => "||",
9785            LogicalOperator::Coalesce => "??",
9786        };
9787        let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
9788        let short_id = format!("{id}:short");
9789        let right_id = format!("{id}:right");
9790        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9791        self.analysis
9792            .logical_targets
9793            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9794        self.analysis.branches.push(CandidateBranch {
9795            id,
9796            kind: "logical-value".to_string(),
9797            file: self.file.to_string(),
9798            line,
9799            column,
9800            source: source_slice(self.source, node.span).to_string(),
9801            alternatives: vec![
9802                CandidateBranchAlternative {
9803                    id: short_id,
9804                    label: "short-circuit / left selected".to_string(),
9805                },
9806                CandidateBranchAlternative {
9807                    id: right_id,
9808                    label: "right evaluated / selected".to_string(),
9809                },
9810            ],
9811        });
9812    }
9813}
9814
9815fn collect_logical_value_branches<'a>(
9816    allocator: &'a Allocator,
9817    program: &mut Program<'a>,
9818    source: &str,
9819    file: &str,
9820    decision_logical_nodes: &HashSet<SpanKey>,
9821    source_sensitive_functions: &HashSet<SpanKey>,
9822) -> LogicalBranchAnalysis {
9823    let mut collector = LogicalBranchCollector {
9824        source,
9825        file,
9826        decision_logical_nodes,
9827        source_sensitive_functions,
9828        unsafe_function_depth: 0,
9829        with_depth: 0,
9830        analysis: LogicalBranchAnalysis::default(),
9831    };
9832    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9833    collector.analysis
9834}
9835
9836impl<'a> Visit<'a> for DecisionCollector<'_> {
9837    fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
9838        if self
9839            .source_sensitive_functions
9840            .contains(&span_key(function.span))
9841        {
9842            return;
9843        }
9844        walk::walk_function(self, function, flags);
9845    }
9846
9847    fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
9848        if self
9849            .source_sensitive_functions
9850            .contains(&span_key(function.span))
9851        {
9852            return;
9853        }
9854        walk::walk_arrow_function_expression(self, function);
9855    }
9856
9857    // A with body is left uninstrumented: its object environment can
9858    // intercept probe identifiers (see `enter_with_statement`).
9859    fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
9860
9861    /// `&&` and `||` used as a value: the left operand is always read as a
9862    /// boolean, whatever the operator returns, so a compound one is a decision
9863    /// as much as an `if`'s test is -- `(a && b) || fallback`, `a || b ||
9864    /// "default"`. What the operator returns from its right is a value, not a
9865    /// condition, and is judged as one in its turn. A `!` reads its operand as
9866    /// a boolean too.
9867    fn visit_expression(&mut self, expression: &Expression<'a>) {
9868        let outer_root = self.logical_root;
9869        // A coverage tool the tests run reads an `&&`/`||`/`??` tree as one
9870        // branch, whatever parentheses are in it; a `!` or anything else is a
9871        // leaf of it.
9872        self.logical_root = match expression {
9873            Expression::LogicalExpression(logical) => {
9874                Some(outer_root.unwrap_or(span_key(logical.span)))
9875            }
9876            Expression::ParenthesizedExpression(_) => outer_root,
9877            _ => None,
9878        };
9879        let candidate = match expression {
9880            Expression::LogicalExpression(logical)
9881                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9882                    && !self.in_a_decision(logical.span) =>
9883            {
9884                Some(&logical.left)
9885            }
9886            Expression::UnaryExpression(unary)
9887                if unary.operator.is_not()
9888                    && has_compound_boolean_decision(&unary.argument)
9889                    && !compound_root(&unary.argument)
9890                        .is_some_and(|root| self.in_a_decision(root.span)) =>
9891            {
9892                Some(expression)
9893            }
9894            _ => None,
9895        };
9896        if let Some(decision) = candidate
9897            && has_compound_boolean_decision(decision)
9898            && decision_outcome_is_variable(decision)
9899        {
9900            let mut conditions = Vec::new();
9901            collect_conditions(decision, &mut conditions);
9902            if conditions.len() <= 32 {
9903                // A `!` outside any tree is its own root.
9904                let root = self
9905                    .logical_root
9906                    .or(outer_root)
9907                    .unwrap_or(span_key(decision.span()));
9908                self.value_decisions.insert(span_key(decision.span()), root);
9909                self.record_decision(decision, "value");
9910            }
9911        }
9912        walk::walk_expression(self, expression);
9913        self.logical_root = outer_root;
9914    }
9915
9916    fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
9917        if decision_outcome_is_variable(&statement.test) {
9918            self.record_decision(&statement.test, "if");
9919        }
9920        self.visit_expression(&statement.test);
9921        self.visit_statement(&statement.consequent);
9922        if let Some(alternate) = &statement.alternate {
9923            self.visit_statement(alternate);
9924        }
9925    }
9926
9927    fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
9928        if decision_outcome_is_variable(&expression.test) {
9929            self.record_decision(&expression.test, "ternary");
9930        }
9931        self.visit_expression(&expression.test);
9932        self.visit_expression(&expression.consequent);
9933        self.visit_expression(&expression.alternate);
9934    }
9935
9936    fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
9937        if decision_outcome_is_variable(&statement.test) {
9938            self.record_decision(&statement.test, "while");
9939        }
9940        self.visit_expression(&statement.test);
9941        self.visit_statement(&statement.body);
9942    }
9943
9944    fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
9945        if decision_outcome_is_variable(&statement.test) {
9946            self.record_decision(&statement.test, "do-while");
9947        }
9948        self.visit_statement(&statement.body);
9949        self.visit_expression(&statement.test);
9950    }
9951
9952    fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
9953        if let Some(test) = &statement.test
9954            && decision_outcome_is_variable(test)
9955        {
9956            self.record_decision(test, "for");
9957        }
9958        if let Some(init) = &statement.init {
9959            self.visit_for_statement_init(init);
9960        }
9961        if let Some(test) = &statement.test {
9962            self.visit_expression(test);
9963        }
9964        if let Some(update) = &statement.update {
9965            self.visit_expression(update);
9966        }
9967        self.visit_statement(&statement.body);
9968    }
9969}
9970
9971fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
9972    match expression {
9973        Expression::ParenthesizedExpression(parenthesized) => {
9974            transparent_expression(&parenthesized.expression)
9975        }
9976        _ => expression,
9977    }
9978}
9979
9980fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
9981    match expression {
9982        Expression::ParenthesizedExpression(parenthesized) => {
9983            has_compound_boolean_decision(&parenthesized.expression)
9984        }
9985        Expression::LogicalExpression(logical) => {
9986            matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9987        }
9988        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
9989            has_compound_boolean_decision(&unary.argument)
9990        }
9991        _ => false,
9992    }
9993}
9994
9995/// A decision's and/or/not structure over its conditions, numbered as
9996/// `collect_conditions` numbers them.
9997enum ValueShape {
9998    Condition(usize),
9999    And(Box<ValueShape>, Box<ValueShape>),
10000    Or(Box<ValueShape>, Box<ValueShape>),
10001    Not(Box<ValueShape>),
10002}
10003
10004fn value_shape(expression: &Expression<'_>, next: &mut usize) -> ValueShape {
10005    match expression {
10006        Expression::ParenthesizedExpression(parenthesized) => {
10007            value_shape(&parenthesized.expression, next)
10008        }
10009        Expression::LogicalExpression(logical)
10010            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10011        {
10012            let left = Box::new(value_shape(&logical.left, next));
10013            let right = Box::new(value_shape(&logical.right, next));
10014            if logical.operator == LogicalOperator::And {
10015                ValueShape::And(left, right)
10016            } else {
10017                ValueShape::Or(left, right)
10018            }
10019        }
10020        Expression::UnaryExpression(unary)
10021            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10022        {
10023            ValueShape::Not(Box::new(value_shape(&unary.argument, next)))
10024        }
10025        _ => {
10026            let index = *next;
10027            *next += 1;
10028            ValueShape::Condition(index)
10029        }
10030    }
10031}
10032
10033fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
10034    match expression {
10035        Expression::ParenthesizedExpression(parenthesized) => {
10036            collect_conditions(&parenthesized.expression, conditions);
10037        }
10038        Expression::LogicalExpression(logical)
10039            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10040        {
10041            collect_conditions(&logical.left, conditions);
10042            collect_conditions(&logical.right, conditions);
10043        }
10044        Expression::UnaryExpression(unary)
10045            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10046        {
10047            collect_conditions(&unary.argument, conditions);
10048        }
10049        _ => conditions.push(expression.span()),
10050    }
10051}
10052
10053fn source_slice(source: &str, span: Span) -> &str {
10054    &source[span.start as usize..span.end as usize]
10055}
10056
10057/// Where every line of one source starts, in bytes and in UTF-16 units, so a
10058/// position is a binary search rather than a scan from the top of the file.
10059///
10060/// Every probe, decision and branch records its line, column and a UTF-16
10061/// offset, and each was counted from the first byte of the file: the work
10062/// grew with the square of the file's length. An 8,000-line file took 97
10063/// seconds to instrument on a debug build, a 24,000-line one did not finish
10064/// in ten minutes, and Actual's setup spent minutes before its first test.
10065struct SourcePositions {
10066    address: usize,
10067    length: usize,
10068    sample: u64,
10069    line_starts: Vec<usize>,
10070    utf16_before_line: Vec<usize>,
10071    /// Per line: `None` when ASCII, otherwise `(byte offset, utf16 column)`
10072    /// checkpoints from the line's start, so a long line that is not ASCII
10073    /// -- a table of currency symbols, say -- is not walked per position.
10074    checkpoints: Vec<Option<Vec<(usize, usize)>>>,
10075}
10076
10077impl SourcePositions {
10078    fn sample(source: &str) -> u64 {
10079        let bytes = source.as_bytes();
10080        let head = &bytes[..bytes.len().min(64)];
10081        let tail = &bytes[bytes.len().saturating_sub(64)..];
10082        head.iter()
10083            .chain(tail)
10084            .fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
10085                (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
10086            })
10087    }
10088
10089    fn new(source: &str) -> Self {
10090        let mut line_starts = vec![0];
10091        let mut utf16_before_line = vec![0];
10092        let mut checkpoints = Vec::new();
10093        let mut marks: Option<Vec<(usize, usize)>> = None;
10094        let mut utf16 = 0;
10095        let mut column = 0;
10096        let mut line_start = 0;
10097        for (index, character) in source.char_indices() {
10098            if !character.is_ascii() && marks.is_none() {
10099                marks = Some(vec![(0, 0)]);
10100            }
10101            if let Some(marks) = &mut marks
10102                && column >= marks.last().expect("a first mark").1 + CHECKPOINT
10103            {
10104                marks.push((index - line_start, column));
10105            }
10106            utf16 += character.len_utf16();
10107            column += character.len_utf16();
10108            if character == '\n' {
10109                line_start = index + 1;
10110                line_starts.push(line_start);
10111                utf16_before_line.push(utf16);
10112                checkpoints.push(marks.take());
10113                column = 0;
10114            }
10115        }
10116        checkpoints.push(marks);
10117        Self {
10118            address: source.as_ptr() as usize,
10119            length: source.len(),
10120            sample: Self::sample(source),
10121            line_starts,
10122            utf16_before_line,
10123            checkpoints,
10124        }
10125    }
10126
10127    fn describes(&self, source: &str) -> bool {
10128        self.address == source.as_ptr() as usize
10129            && self.length == source.len()
10130            && self.sample == Self::sample(source)
10131    }
10132
10133    /// Zero-based line, and the UTF-16 units from that line's start.
10134    fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
10135        let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
10136        let start = self.line_starts[line];
10137        let column = match &self.checkpoints[line] {
10138            None => offset - start,
10139            Some(marks) => {
10140                let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
10141                let (byte, column) = marks[at];
10142                column + source[start + byte..offset].encode_utf16().count()
10143            }
10144        };
10145        (line, column)
10146    }
10147}
10148
10149thread_local! {
10150    static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
10151        const { std::cell::RefCell::new(None) };
10152}
10153
10154/// While one of these is alive, positions in any source are computed from an
10155/// index built once for it. It lives for one instrumentation call, during
10156/// which the source it indexes is borrowed and cannot move; outside one,
10157/// positions are counted from the top as before.
10158pub(crate) struct PositionScope {
10159    owner: bool,
10160}
10161
10162impl PositionScope {
10163    pub(crate) fn enter() -> Self {
10164        let owner = POSITIONS.with(|positions| {
10165            let mut positions = positions.borrow_mut();
10166            let owner = positions.is_none();
10167            if owner {
10168                *positions = Some(Vec::new());
10169            }
10170            owner
10171        });
10172        Self { owner }
10173    }
10174}
10175
10176impl Drop for PositionScope {
10177    fn drop(&mut self) {
10178        if self.owner {
10179            POSITIONS.with(|positions| *positions.borrow_mut() = None);
10180        }
10181    }
10182}
10183
10184fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
10185    POSITIONS.with(|positions| {
10186        let mut positions = positions.borrow_mut();
10187        let known = positions.as_mut()?;
10188        if let Some(found) = known.iter().find(|index| index.describes(source)) {
10189            return Some(found.clone());
10190        }
10191        let built = std::rc::Rc::new(SourcePositions::new(source));
10192        known.push(built.clone());
10193        Some(built)
10194    })
10195}
10196
10197fn utf16_offset(source: &str, offset: usize) -> usize {
10198    match indexed(source) {
10199        Some(index) => {
10200            let (line, column) = index.locate(source, offset);
10201            index.utf16_before_line[line] + column
10202        }
10203        None => source[..offset].encode_utf16().count(),
10204    }
10205}
10206
10207pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
10208    if let Some(index) = indexed(source) {
10209        let (line, column) = index.locate(source, offset);
10210        return (line + 1, column + 1);
10211    }
10212    let prefix = &source[..offset];
10213    let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
10214    let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
10215    let column = source[line_start..offset].encode_utf16().count() + 1;
10216    (line, column)
10217}
10218
10219fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
10220    let start = utf16_offset(source, span.start as usize);
10221    let end = utf16_offset(source, span.end as usize);
10222    let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
10223    let mut id = String::with_capacity(16);
10224    for byte in &digest[..8] {
10225        write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
10226    }
10227    id
10228}
10229
10230#[cfg(test)]
10231mod tests {
10232    use super::*;
10233
10234    #[test]
10235    fn line_index_lookups_agree_with_walking_the_line() {
10236        let long = "é🚀x".repeat(700);
10237        let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
10238        let index = Utf16LineIndex::new(&source);
10239        for (line, text) in source.split_inclusive('\n').enumerate() {
10240            let start = index.starts[line];
10241            let width: usize = text.chars().map(char::len_utf16).sum();
10242            for column in 0..=width + 2 {
10243                assert_eq!(
10244                    index.byte_offset(line, column),
10245                    start + utf16_col_to_byte(text, column),
10246                    "line {line} column {column}"
10247                );
10248            }
10249            for (offset, _) in text.char_indices() {
10250                let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
10251                assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
10252            }
10253        }
10254    }
10255
10256    #[test]
10257    fn indexed_positions_agree_with_counting_from_the_top() {
10258        // The long line passes several checkpoints.
10259        let source = &format!(
10260            "const s = 'naïve 🚀';\n\nexport function f(a) {{\n  return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
10261            "é🚀x".repeat(400)
10262        );
10263        let source = source.as_str();
10264        let counted: Vec<_> = source
10265            .char_indices()
10266            .map(|(offset, _)| {
10267                (
10268                    line_and_utf16_column(source, offset),
10269                    stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10270                )
10271            })
10272            .collect();
10273        let _positions = PositionScope::enter();
10274        for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
10275            assert_eq!(
10276                line_and_utf16_column(source, offset),
10277                position,
10278                "offset {offset}"
10279            );
10280            assert_eq!(
10281                stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10282                id
10283            );
10284        }
10285    }
10286
10287    #[test]
10288    fn classic_scripts_share_application_globals_but_not_probe_bindings() {
10289        // TodoMVC's ES5 app loads several classic scripts into one document.
10290        // They share the global lexical scope, so a second
10291        // `const __supercovMcdcBegin` was a SyntaxError and no item was added.
10292        let a = instrument_direct_candidate(
10293            "var count = 0; function increment() { if (count >= 0) { count += 1; } }",
10294            "public/a.js",
10295        )
10296        .unwrap();
10297        let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
10298            .unwrap();
10299        let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
10300        let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
10301        let script = format!(
10302            "const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
10303             vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
10304             if (ctx.observed !== 1) throw Error('shared globals changed');\
10305             vm.runInContext({}, ctx);\
10306             if (ctx.observed !== 2) throw Error('a repeated script changed');",
10307            serde_json::to_string(runtime).unwrap(),
10308            serde_json::to_string(&a.code).unwrap(),
10309            serde_json::to_string(&b.code).unwrap(),
10310            serde_json::to_string(&b.code).unwrap()
10311        );
10312        // Through stdin: the script carries the whole runtime, longer than a
10313        // Windows command line may be.
10314        let mut node = std::process::Command::new("node")
10315            .arg("-")
10316            .stdin(std::process::Stdio::piped())
10317            .stdout(std::process::Stdio::piped())
10318            .stderr(std::process::Stdio::piped())
10319            .spawn()
10320            .expect("Node.js for the JavaScript regression");
10321        std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
10322        let result = node.wait_with_output().unwrap();
10323        assert!(
10324            result.status.success(),
10325            "{}",
10326            String::from_utf8_lossy(&result.stderr)
10327        );
10328    }
10329
10330    #[test]
10331    fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
10332        // TodoMVC's React app keeps JSX in `src/index.js`; the run failed on
10333        // `Unexpected token` before any test started.
10334        for file in [
10335            "src/index.js",
10336            "src/index.jsx",
10337            "src/index.mjs",
10338            "src/index.cjs",
10339            "src/index.tsx",
10340        ] {
10341            let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
10342            let output = instrument_candidate(source, file).unwrap();
10343            assert!(!output.points.is_empty(), "{file}");
10344            assert!(output.code.contains("<div>"), "{file}");
10345        }
10346        // TypeScript keeps its angle-bracket assertion; JSX would misread it.
10347        assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
10348        // The script or module mode an extension implies is kept.
10349        for file in ["a.js", "a.cjs", "a.mjs"] {
10350            let before = SourceType::from_path(file).unwrap();
10351            let after = project_source_type(Path::new(file)).unwrap();
10352            assert_eq!(after.module_kind(), before.module_kind(), "{file}");
10353            assert!(after.is_jsx(), "{file}");
10354        }
10355    }
10356
10357    const SOURCE: &str =
10358        "export function decide(a,b,c) {\n  if ((a && b) || !c) return 1;\n  return 0;\n}\n";
10359
10360    #[test]
10361    fn matches_the_frozen_if_decision_manifest_exactly() {
10362        let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
10363        assert!(!output.complete);
10364        assert_eq!(
10365            output.decisions,
10366            vec![CandidateDecision {
10367                id: "2f65989a5782c5bd".to_string(),
10368                file: "app/decide.ts".to_string(),
10369                line: 2,
10370                column: 7,
10371                source: "(a && b) || !c".to_string(),
10372                conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
10373                kind: "if".to_string(),
10374            }]
10375        );
10376    }
10377
10378    #[test]
10379    fn a_test_files_unchanged_text_keeps_its_columns() {
10380        // One mapping for a line put every frame of a test file in column 1,
10381        // and one inside a wrapped assertion at the statement's start: Vitest
10382        // printed `:2:3` where it prints `:2:22` without Supercov.
10383        let source = "import { expect } from 'vitest';\n  expect(value(1)).toBe(2);\n";
10384        let wrapper = "wrap(() => ";
10385        let edits = vec![
10386            TextEdit {
10387                start: source.find("expect(value").unwrap(),
10388                end: source.find("expect(value").unwrap(),
10389                text: wrapper.into(),
10390                rank: 0,
10391            },
10392            TextEdit {
10393                start: source.rfind(';').unwrap(),
10394                end: source.rfind(';').unwrap(),
10395                text: ")".into(),
10396                rank: 0,
10397            },
10398        ];
10399        let (code, map) = apply_text_edits(source, edits, "a.test.js").unwrap();
10400        assert_eq!(
10401            code,
10402            "import { expect } from 'vitest';\n  wrap(() => expect(value(1)).toBe(2));\n"
10403        );
10404        let original = |line: u32, column: u32| {
10405            map.get_tokens()
10406                .filter(|token| token.get_dst_line() == line && token.get_dst_col() <= column)
10407                .max_by_key(|token| token.get_dst_col())
10408                .map(|token| (token.get_src_line(), token.get_src_col()))
10409        };
10410        // `toBe`, past the text the wrapper added.
10411        let matcher = code.lines().nth(1).unwrap().find("toBe").unwrap() as u32;
10412        assert_eq!(original(1, matcher), Some((1, 19)));
10413        // A word and a mark of a line nothing was added to.
10414        assert_eq!(original(0, 9), Some((0, 9)));
10415        assert_eq!(original(0, 7), Some((0, 7)));
10416        // The text the wrapper added maps to where the line began.
10417        assert_eq!(original(1, 4), Some((1, 0)));
10418        // An identifier carries its name, which a compiler reading this map
10419        // keeps and Jest prints (`at toBe (…)`); a keyword carries none.
10420        let name_at = |line: u32, column: u32| {
10421            map.get_tokens()
10422                .find(|token| token.get_dst_line() == line && token.get_dst_col() == column)
10423                .and_then(|token| token.get_name_id())
10424                .and_then(|id| map.get_name(id))
10425                .map(|name| name.to_string())
10426        };
10427        assert_eq!(name_at(1, matcher).as_deref(), Some("toBe"));
10428        assert_eq!(name_at(0, 9).as_deref(), Some("expect"));
10429        assert_eq!(name_at(0, 18), None, "`from` is a keyword");
10430    }
10431
10432    #[test]
10433    fn a_server_entry_keeps_what_it_exports_by_default() {
10434        // Taken before it was looked at, anything but a function declaration
10435        // was replaced by a placeholder `null`: `export default handleRequest;`
10436        // exported `withRequestPhase(null)`, and a class exported `null`.
10437        for (source, export) in [
10438            (
10439                "function handleRequest(r) { return r; }\nexport default handleRequest;\n",
10440                "export default __supercovWithRequestPhase13(handleRequest);",
10441            ),
10442            (
10443                "export default class Entry {}\n",
10444                "export default class Entry {}",
10445            ),
10446        ] {
10447            let output = instrument_candidate_with_binding(
10448                source,
10449                "app/entry.server.ts",
10450                RuntimeBinding::ModuleImport,
10451                None,
10452                false,
10453            )
10454            .unwrap();
10455            assert!(
10456                output.code.lines().any(|line| line == export),
10457                "{export}\n{}",
10458                output.code
10459            );
10460        }
10461    }
10462
10463    #[test]
10464    fn a_method_read_through_a_property_keeps_its_text() {
10465        // `box.method.toString()` names no binding; any method of that name in
10466        // the file is taken as the one read, and others keep their probes.
10467        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";
10468        let output = instrument_direct_candidate(source, "src/methods.js").unwrap();
10469        for text in [
10470            "method(v) { return v  * 2; }",
10471            "area(w) { return w  * w; }",
10472            "() =>  new Shape()",
10473        ] {
10474            assert!(output.code.contains(text), "{text}\n{}", output.code);
10475        }
10476        assert!(
10477            !output.code.contains("untouched(v) { return v  + 1; }"),
10478            "{}",
10479            output.code
10480        );
10481    }
10482
10483    #[test]
10484    fn a_function_whose_source_is_read_keeps_its_text_exactly() {
10485        // Left without probes, it was still printed again: `f.toString()`
10486        // read `function f() {\n\treturn 1;\n}` under measurement.
10487        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";
10488        let output = instrument_direct_candidate(source, "src/text.js").unwrap();
10489        for text in [
10490            "function* g() { yield 1; }",
10491            "(a,  b) =>   a + b",
10492            "async function later(x) {\n  // kept\n  if (x) {\n      return x;\n  }\n  return 0;\n}",
10493        ] {
10494            assert!(output.code.contains(text), "{text}\n{}", output.code);
10495        }
10496        // A function nobody reads the source of is measured as before.
10497        assert!(
10498            !output
10499                .code
10500                .contains("function counted(n) { return n > 1 ? 'many' : 'one'; }"),
10501            "{}",
10502            output.code
10503        );
10504        // Typed source keeps its types as written.
10505        let typed = "export function twice(value: number): number { return value  * 2; }\nexport const text = twice.toString();\n";
10506        let output = instrument_direct_candidate(typed, "src/typed.ts").unwrap();
10507        assert!(
10508            output
10509                .code
10510                .contains("function twice(value: number): number { return value  * 2; }"),
10511            "{}",
10512            output.code
10513        );
10514    }
10515
10516    #[test]
10517    fn codegen_output_reparses_for_typescript_and_tsx() {
10518        for (file, source) in [
10519            (
10520                "component.tsx",
10521                "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
10522            ),
10523            ("module.ts", SOURCE),
10524        ] {
10525            let output = analyze_candidate(source, file).unwrap();
10526            let map = output.map.as_ref().expect("candidate source map");
10527            assert_eq!(map["version"], 3);
10528            assert_eq!(map["sources"][0], file);
10529            assert_eq!(map["sourcesContent"][0], source);
10530            assert!(
10531                map["mappings"]
10532                    .as_str()
10533                    .is_some_and(|value| !value.is_empty())
10534            );
10535            let allocator = Allocator::default();
10536            let source_type = SourceType::from_path(file).unwrap();
10537            let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
10538            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
10539        }
10540    }
10541
10542    #[test]
10543    fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
10544        for file in ["src/module.mjs", "src/script.cjs"] {
10545            let output = instrument_direct_candidate(
10546                "export const selected = value => value ? 1 : 0;",
10547                file,
10548            )
10549            .unwrap();
10550            assert!(
10551                output
10552                    .code
10553                    .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
10554            );
10555            assert!(!output.code.contains("virtual:supercov-runtime"));
10556            assert!(!output.code.contains("globalThis.__supercovRuntime"));
10557        }
10558    }
10559
10560    #[test]
10561    fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
10562        let source = concat!(
10563            "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10564            "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
10565            "ordinaryHelper();\n",
10566        );
10567        let output = instrument_node_assertion_phases_with_runtime_hooks(
10568            source,
10569            "tests/runner.mjs",
10570            &[],
10571            Some("../.supercov/launchSupervisor.mjs"),
10572        )
10573        .unwrap();
10574        assert_eq!(output.assertions, 0);
10575        assert_eq!(output.capability_imports, 1);
10576        assert!(output.code.contains("wrapImportedCapability"));
10577        assert!(output.code.contains("__supercovRawImageBuilder"));
10578        assert!(output.code.contains("const ImageBuilder"));
10579        assert!(!output.code.contains("__supercovRawordinaryHelper"));
10580        assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
10581    }
10582
10583    #[test]
10584    fn capability_imports_accept_a_computed_guest_mount_path() {
10585        let source = concat!(
10586            "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10587            "const guestRoot = resolve(tmpdir(), 'workspace');\n",
10588            "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
10589            "ordinaryHelper();\n",
10590        );
10591        let output = instrument_node_assertion_phases_with_runtime_hooks(
10592            source,
10593            "tests/runner.mjs",
10594            &[],
10595            Some("../.supercov/launchSupervisor.mjs"),
10596        )
10597        .unwrap();
10598        assert_eq!(output.capability_imports, 1);
10599        assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
10600        assert!(!output.code.contains("__supercovRawordinaryHelper"));
10601    }
10602
10603    #[test]
10604    fn capability_import_selection_follows_a_mapping_variable() {
10605        let source = concat!(
10606            "import { launch, unrelated } from './sdk.mjs';\n",
10607            "run();\n",
10608            "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
10609            "function run() { launch(options); unrelated(); }\n",
10610        );
10611        let output = instrument_node_assertion_phases_with_runtime_hooks(
10612            source,
10613            "tests/runner.mjs",
10614            &[],
10615            Some("../.supercov/launchSupervisor.mjs"),
10616        )
10617        .unwrap();
10618        assert_eq!(output.capability_imports, 1);
10619        assert!(output.code.contains("__supercovRawlaunch"));
10620        assert!(!output.code.contains("__supercovRawunrelated"));
10621    }
10622
10623    #[test]
10624    fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
10625        let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
10626        let output = instrument_node_assertion_phases_with_runtime_hooks(
10627            source,
10628            "src/main.mjs",
10629            &[],
10630            Some("../.supercov/launchSupervisor.mjs"),
10631        )
10632        .unwrap();
10633        assert_eq!(output.code, source);
10634        assert_eq!(output.capability_imports, 0);
10635    }
10636
10637    #[test]
10638    fn ordinary_mount_language_does_not_enable_capability_proxies() {
10639        let source = concat!(
10640            "import { render } from '@testing-library/react';\n",
10641            "it('mounts into a host element', () => render(<main />));\n",
10642        );
10643        let output = instrument_node_assertion_phases_with_runtime_hooks(
10644            source,
10645            "tests/component.test.tsx",
10646            &[],
10647            Some("../.supercov/launchSupervisor.mjs"),
10648        )
10649        .unwrap();
10650        assert_eq!(output.capability_imports, 0);
10651        assert!(!output.code.contains("wrapImportedCapability"));
10652    }
10653
10654    #[test]
10655    fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
10656        let source = concat!(
10657            "import { test, expect } from '@example/test-admin';\n",
10658            "test('visual snapshot', async ({ page }) => {\n",
10659            "  expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
10660            "});\n",
10661        );
10662        let output = instrument_node_assertion_phases_with_runtime_hooks(
10663            source,
10664            "tests/visual.spec.ts",
10665            &[],
10666            Some("../.supercov/launchSupervisor.mjs"),
10667        )
10668        .unwrap();
10669        assert_eq!(output.capability_imports, 0);
10670        assert!(!output.code.contains("wrapImportedCapability"));
10671    }
10672
10673    #[test]
10674    fn capability_rewrite_excludes_runtime_and_type_only_imports() {
10675        let source = concat!(
10676            "import type { Machine } from './types.ts';\n",
10677            "import { test } from 'node:test';\n",
10678            "import { runtime } from '../.supercov/runtime.mjs';\n",
10679            "console.log({ machine: true, test, runtime });\n",
10680        );
10681        let output = instrument_node_assertion_phases_with_runtime_hooks(
10682            source,
10683            "tests/runner.ts",
10684            &[],
10685            Some("../.supercov/launchSupervisor.mjs"),
10686        )
10687        .unwrap();
10688        assert_eq!(output.code, source);
10689        assert_eq!(output.capability_imports, 0);
10690    }
10691
10692    #[test]
10693    fn native_assertion_arguments_execute_inside_the_assertion_phase() {
10694        let source = concat!(
10695            "import assert from 'node:assert/strict';\n",
10696            "assert.equal(value(), 1);\n",
10697        );
10698        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10699        assert_eq!(output.assertions, 1);
10700        assert!(output.code.contains("withNodeAssertionPhase"));
10701        assert!(output.code.contains("node:assert/strict.equal"));
10702        assert!(output.code.contains("tests/value.test.mjs:2:1"));
10703        assert!(output.code.contains("assert.equal(value(), 1)"));
10704    }
10705
10706    #[test]
10707    fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
10708        let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
10709        let output = instrument_node_assertion_phases_with_runtime_imports(
10710            source,
10711            "tests/value.test.mjs",
10712            &[],
10713            None,
10714            Some("../.supercov/node_modules/bootstrap.cjs"),
10715        )
10716        .unwrap();
10717        assert_eq!(output.assertions, 1);
10718        // The loader shares the first line, so the assertion stays on line 2.
10719        let lines = output.code.lines().collect::<Vec<_>>();
10720        assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
10721        assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
10722        assert!(lines[0].ends_with("import assert from 'node:assert';"));
10723        assert!(lines[1].contains("withNodeAssertionPhase"));
10724        let allocator = Allocator::default();
10725        assert!(
10726            Parser::new(&allocator, &output.code, SourceType::mjs())
10727                .parse()
10728                .errors
10729                .is_empty()
10730        );
10731    }
10732
10733    #[test]
10734    fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
10735        let source = concat!(
10736            "import assert from 'node:assert/strict';\n",
10737            "export async function check() { assert.equal(await value(), 1); }\n",
10738        );
10739        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10740        assert_eq!(output.assertions, 1);
10741        assert!(output.code.contains("bindNodeAssertionPhase"));
10742        assert!(output.code.contains("await value()"));
10743        let allocator = Allocator::default();
10744        assert!(
10745            Parser::new(&allocator, &output.code, SourceType::mjs())
10746                .parse()
10747                .errors
10748                .is_empty()
10749        );
10750    }
10751
10752    #[test]
10753    fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
10754        let source = concat!(
10755            "import assert from 'node:assert/strict';\n",
10756            "export async function check(user?: string) {\n",
10757            "  assert.ok(user, await value());\n",
10758            "  assert\n",
10759            "    .equal(await value(), 1)\n",
10760            "}\n",
10761        );
10762        let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
10763        assert_eq!(output.assertions, 2);
10764        let code = &output.code;
10765        let lines: Vec<&str> = code.lines().collect();
10766        assert!(
10767            lines[2].starts_with(
10768                "  { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
10769            ),
10770            "{code}"
10771        );
10772        assert!(
10773            lines[2]
10774                .ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
10775            "{code}"
10776        );
10777        assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
10778        assert_eq!(
10779            lines[4], "     \"equal\"); __supercovAssertionPhase(await value(), 1) }",
10780            "{code}"
10781        );
10782        assert_eq!(lines[5], "}", "{code}");
10783        let allocator = Allocator::default();
10784        assert!(
10785            Parser::new(&allocator, code, SourceType::ts())
10786                .parse()
10787                .errors
10788                .is_empty()
10789        );
10790    }
10791
10792    #[test]
10793    fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
10794        let source = concat!(
10795            "import { expect, test } from '@playwright/test';\n",
10796            "test('value', async () => { expect(await value()).toBe(1); });\n",
10797        );
10798        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
10799        assert_eq!(output.assertions, 1);
10800        assert!(output.code.contains("bindNodeAssertionPhase"));
10801        assert!(output.code.contains("await value()"));
10802        let allocator = Allocator::default();
10803        assert!(
10804            Parser::new(&allocator, &output.code, SourceType::mjs())
10805                .parse()
10806                .errors
10807                .is_empty()
10808        );
10809    }
10810
10811    #[test]
10812    fn native_commonjs_assertion_bindings_are_attributed() {
10813        let source = concat!(
10814            "const assert = require('node:assert/strict');\n",
10815            "const { ok: verify } = require('assert');\n",
10816            "assert.equal(value(), 1);\n",
10817            "verify(other());\n",
10818        );
10819        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10820        assert_eq!(output.assertions, 2);
10821        assert!(output.code.contains("node:assert/strict.equal"));
10822        assert!(output.code.contains("node:assert.ok"));
10823    }
10824
10825    #[test]
10826    fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
10827        let source = concat!(
10828            "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
10829            "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
10830        );
10831        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10832        assert_eq!(output.assertions, 1);
10833        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10834    }
10835
10836    #[test]
10837    fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
10838        let source = concat!(
10839            "import assert from 'node:assert/strict';\n",
10840            "function unrelated(assert) { assert.equal(effect(), 1); }\n",
10841            "assert.equal(value(), 1);\n",
10842        );
10843        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10844        assert_eq!(output.assertions, 1);
10845        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10846    }
10847
10848    #[test]
10849    fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
10850        let source = concat!(
10851            "import test from 'node:test';\n",
10852            "import { expect as verify } from 'expect-library';\n",
10853            "test('value', () => verify(value()).not.toEqual(1));\n",
10854        );
10855        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10856        assert_eq!(output.assertions, 1);
10857        assert!(output.code.contains("expect.not.toEqual"));
10858        assert!(output.code.contains("verify(value()).not.toEqual(1)"));
10859    }
10860
10861    #[test]
10862    fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
10863        let source = concat!(
10864            "import { expect, test } from 'vitest';\n",
10865            "test('value', () => expect(value()).toBe(1));\n",
10866        );
10867        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10868        assert_eq!(output.assertions, 1);
10869        assert!(output.code.contains("expect.toBe"));
10870    }
10871
10872    #[test]
10873    fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
10874        // Vitest Browser Mode asserts through `expect.element(locator)`, and
10875        // `expect.soft`/`expect.poll` wrap the same way. The chain root is a
10876        // member call rather than a bare `expect` identifier, which is the
10877        // shape that used to leave every UI assertion unrecognised.
10878        for (call, operation) in [
10879            (
10880                "expect.element(button).toHaveAccessibleName('Copy')",
10881                "expect.element.toHaveAccessibleName",
10882            ),
10883            ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
10884            ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
10885        ] {
10886            let source = format!(
10887                "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
10888            );
10889            let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
10890            assert_eq!(output.assertions, 1, "{call}");
10891            assert!(output.code.contains(operation), "{call} -> {operation}");
10892        }
10893    }
10894
10895    #[test]
10896    fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
10897        // A JSX tree is one statement, so an attribute and a child that call
10898        // the same function had no way to be told apart -- which is exactly
10899        // what a UI assertion needs, `toHaveAccessibleName` naming the
10900        // attribute and `toHaveTextContent` the child.
10901        let source = concat!(
10902            "export function Button({ label, copied }) {\n",
10903            "  return (\n",
10904            "    <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
10905            "      {format(copied)}\n",
10906            "    </button>\n",
10907            "  );\n",
10908            "}\n",
10909        );
10910        let output = instrument_candidate(source, "src/button.tsx").unwrap();
10911        let rendered = output
10912            .points
10913            .iter()
10914            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10915            .collect::<Vec<_>>();
10916        // The attribute and the child are separate obligations at separate
10917        // positions, even though they read identically.
10918        assert_eq!(rendered.len(), 2, "{:?}", output.points);
10919        assert!(
10920            rendered
10921                .iter()
10922                .all(|point| point.source == "format(copied)")
10923        );
10924        assert_eq!(rendered[0].line, 3);
10925        assert_eq!(rendered[1].line, 4);
10926        assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
10927        // `{label}` is reached exactly when the tree is, and the handler is
10928        // measured where it is called; neither earns an obligation.
10929        assert!(!output.code.contains("title={__supercovRenderedValueV2"));
10930        // JSX forbids a bare comma operator, so the probe has to carry the
10931        // value through a call rather than a sequence expression.
10932        assert!(!output.code.contains("), format(copied)}"));
10933        assert!(output.code.contains(", format(copied))}"));
10934    }
10935
10936    #[test]
10937    fn instrumented_jsx_stays_parseable_through_every_container_shape() {
10938        // A JSX container cannot hold a bare comma operator, and a malformed
10939        // rewrite would surface only as the project's own build failing. Each
10940        // shape below reaches the rewrite by a different route.
10941        let source = concat!(
10942            "export function Panel({ items, user, error, tone }) {\n",
10943            "  return (\n",
10944            "    <>\n",
10945            "      <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
10946            "      <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
10947            "        {error ? <Alert text={message(error)} /> : null}\n",
10948            "        {items.map((item) => (\n",
10949            "          <Item key={item.id} label={format(item)} />\n",
10950            "        ))}\n",
10951            "        {count(items) > 0 && <Footer total={sum(items)} />}\n",
10952            "      </div>\n",
10953            "    </>\n",
10954            "  );\n",
10955            "}\n",
10956        );
10957        let output = instrument_candidate(source, "src/panel.tsx").unwrap();
10958        // The instrumented output must itself parse.
10959        instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
10960        // A spread attribute is not a container and is never rewritten.
10961        assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
10962        let rendered = output
10963            .points
10964            .iter()
10965            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10966            .map(|point| point.source.as_str())
10967            .collect::<Vec<_>>();
10968        assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
10969        assert!(
10970            rendered.contains(&"cx(\'a\', tone && \'b\')"),
10971            "{rendered:?}"
10972        );
10973        assert!(rendered.contains(&"format(item)"), "{rendered:?}");
10974        assert!(rendered.contains(&"message(error)"), "{rendered:?}");
10975        // `user?.id` short-circuits but is still a member access reached with
10976        // the statement, and a key that is a bare member chain is too.
10977        assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
10978        assert!(!rendered.contains(&"item.id"), "{rendered:?}");
10979    }
10980
10981    #[test]
10982    fn jest_global_expect_is_attributed_next_to_its_test_globals() {
10983        // Jest injects `expect`, `test`, `it` and `describe` into the test
10984        // environment; none is imported. The bare `expect` is the assertion
10985        // exactly when the file reaches for those test globals too.
10986        let source = "test('value', () => expect(value()).toBe(1));\n";
10987        let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
10988        assert_eq!(output.assertions, 1);
10989        assert!(output.code.contains("expect.toBe"));
10990        // A stray global `expect` in ordinary code is not one.
10991        let plain = "const ok = expect(value()).toBe(1);\n";
10992        let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
10993        assert_eq!(output.assertions, 0);
10994        assert_eq!(output.code, plain);
10995    }
10996
10997    #[test]
10998    fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
10999        let source = concat!(
11000            "import { expect, test } from '@playwright/test';\n",
11001            "test('value', () => expect(value()).toBe(1));\n",
11002        );
11003        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
11004        assert_eq!(output.assertions, 1);
11005        assert!(output.code.contains("expect.toBe"));
11006    }
11007
11008    /// Each way a test file reaches an assertion, and the look-alikes that
11009    /// are not one: another library's `expect`, a `require` of a name only
11010    /// known at run time, a local `expect`, and calls that can short-circuit
11011    /// before the assertion runs.
11012    #[test]
11013    fn an_assertion_is_recognised_however_it_was_reached_and_only_then() {
11014        for (file, source, assertions) in [
11015            (
11016                "t/a.test.js",
11017                "describe('x', () => { it('y', () => expect(a()).toBe(1)); });\n",
11018                1,
11019            ),
11020            ("t/b.test.js", "it('y', () => expect(a()).toBe(1));\n", 1),
11021            (
11022                "t/p.test.js",
11023                "describe('x', function () { expect(a()).toBe(1); });\n",
11024                1,
11025            ),
11026            // A global `expect` with none of the test globals beside it.
11027            ("t/q.test.js", "expect(a()).toBe(1);\n", 0),
11028            (
11029                "t/c.test.cjs",
11030                "const { expect } = require('vitest');\nexpect(a()).toBe(1);\n",
11031                1,
11032            ),
11033            (
11034                "t/d.test.cjs",
11035                "const { expect: check } = require('@jest/globals');\ncheck(a()).toBe(1);\n",
11036                1,
11037            ),
11038            (
11039                "t/i.test.mjs",
11040                "import { expect as e } from 'vitest';\ne(a()).toBe(1);\n",
11041                1,
11042            ),
11043            (
11044                "t/n.test.cjs",
11045                "const { strict } = require('node:assert');\nstrict.equal(a(), 1);\n",
11046                1,
11047            ),
11048            (
11049                "t/s.test.cjs",
11050                "const assert = require('node:assert').strict;\nassert.equal(a(), 1);\n",
11051                1,
11052            ),
11053            // An export of `node:assert` that is not an assertion.
11054            (
11055                "t/h.test.mjs",
11056                "import { ok, AssertionError } from 'node:assert';\nok(a());\nnew AssertionError({});\n",
11057                1,
11058            ),
11059            (
11060                "t/j.test.cjs",
11061                "const { equal, AssertionError } = require('assert');\nequal(a(), 1);\nnew AssertionError({});\n",
11062                1,
11063            ),
11064            (
11065                "t/e.test.cjs",
11066                "const { expect } = require('chai');\nexpect(a()).to.equal(1);\n",
11067                0,
11068            ),
11069            (
11070                "t/f.test.cjs",
11071                "const name = 'node:assert';\nconst assert = require(name);\nassert.ok(a());\n",
11072                0,
11073            ),
11074            (
11075                "t/o.test.cjs",
11076                "const [first] = require('node:assert');\nfirst(a());\n",
11077                0,
11078            ),
11079            (
11080                "t/m.test.mjs",
11081                "import { test } from 'node:test';\nconst expect = (v) => ({ toBe() {} });\ntest('x', () => expect(a()).toBe(1));\n",
11082                0,
11083            ),
11084            (
11085                "t/k.test.mjs",
11086                "import assert from 'node:assert';\nassert?.ok(a());\nassert.ok?.(b());\n",
11087                0,
11088            ),
11089        ] {
11090            let output = instrument_node_assertion_phases(source, file).unwrap();
11091            assert_eq!(output.assertions, assertions, "{file}");
11092            assert_eq!(
11093                output.code.matches("withNodeAssertionPhase").count(),
11094                assertions,
11095                "{file}"
11096            );
11097        }
11098    }
11099
11100    /// A sandbox call is wrapped only when its argument maps the host's
11101    /// workspace to the guest's, and never for Supercov's own or a test
11102    /// framework's modules.
11103    #[test]
11104    fn only_a_host_to_guest_mapping_makes_a_capability_candidate() {
11105        assert!(capability_source_candidate(
11106            "run({ hostPath: a, guestPath: b })"
11107        ));
11108        assert!(capability_source_candidate(
11109            "run({ hostRoot: a, guestRoot: b })"
11110        ));
11111        assert!(capability_source_candidate(
11112            "start({ Mounts: [{ source: a, target: b }] })"
11113        ));
11114        assert!(capability_source_candidate(
11115            "start({ mounts: [{ source: a, target: b }] })"
11116        ));
11117        for unrelated in [
11118            "run({ hostPath: a })",
11119            "run({ hostRoot: a, guestPath: b })",
11120            "start({ mounts: [{ source: a }] })",
11121            "start({ source: a, target: b })",
11122            "snapshot(machine.spawn())",
11123        ] {
11124            assert!(!capability_source_candidate(unrelated), "{unrelated}");
11125        }
11126        let wrapper = "./.supercov/capability.mjs";
11127        for excluded in [
11128            wrapper,
11129            "node:child_process",
11130            "virtual:supercov-runtime",
11131            "../.supercov/node_modules/x.mjs",
11132            "vitest",
11133            "@playwright/test",
11134        ] {
11135            assert!(excluded_capability_import(excluded, wrapper), "{excluded}");
11136        }
11137        assert!(!excluded_capability_import("e2b", wrapper));
11138    }
11139
11140    #[test]
11141    fn an_arrow_that_returns_an_assertion_still_returns_it() {
11142        // `await loginError(page)` waits for the matcher only if the arrow
11143        // still returns it: made a block, it returned nothing at once.
11144        let source = concat!(
11145            "import { expect, test } from '@playwright/test';\n",
11146            "const loginError = (page: Page, error: string) => expect(page).toHaveURL(error);\n",
11147            "test('value', () => expect(value()).toBe(1));\n",
11148        );
11149        for file in ["tests/login.spec.ts", "tests/login.spec.js"] {
11150            let source = if file.ends_with(".js") {
11151                source.replace(": Page", "").replace(": string", "")
11152            } else {
11153                source.into()
11154            };
11155            let output = instrument_node_assertion_phases(&source, file).unwrap();
11156            assert_eq!(output.assertions, 2, "{file}");
11157            let code = output.code.split("\n//# sourceMappingURL").next().unwrap();
11158            assert!(!code.contains("=> { const"), "{file}:\n{code}");
11159            assert!(!code.contains("finally"), "{file}:\n{code}");
11160            assert!(
11161                code.contains("expect(page).toHaveURL(error)"),
11162                "{file}:\n{code}"
11163            );
11164        }
11165    }
11166
11167    #[test]
11168    fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
11169        let source = concat!(
11170            "import { browserTest, expect } from '@acme/browser-fixtures';\n",
11171            "browserTest('value', () => expect(value()).toBe(1));\n",
11172        );
11173        let output = instrument_node_assertion_phases_with_expect_modules(
11174            source,
11175            "tests/value.spec.mjs",
11176            &["@acme/browser-fixtures".into()],
11177        )
11178        .unwrap();
11179        assert_eq!(output.assertions, 1);
11180        assert!(output.code.contains("expect.toBe"));
11181    }
11182
11183    #[test]
11184    fn preserves_comment_payloads_byte_for_byte() {
11185        let comment = "/*---\ndescription: >\n  nested indentation is data\ninfo: |\n  first\n    second\n---*/";
11186        let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
11187        let output = instrument_candidate(&source, "app/comments.js").unwrap();
11188        assert!(output.code.contains(comment), "{}", output.code);
11189    }
11190
11191    #[test]
11192    fn source_map_destinations_follow_restored_comments() {
11193        let source = "function run() {\n  // first omitted comment\n  // second omitted comment\n  return 1;\n}\n";
11194        let output = analyze_candidate(source, "app/comment-map.js").unwrap();
11195        let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
11196        let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
11197        let return_token = map
11198            .get_tokens()
11199            .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
11200            .expect("return token mapping");
11201        let offset = Utf16LineIndex::new(&output.code).byte_offset(
11202            return_token.get_dst_line() as usize,
11203            return_token.get_dst_col() as usize,
11204        );
11205        assert!(
11206            output.code[offset..].starts_with("return"),
11207            "{}",
11208            output.code
11209        );
11210    }
11211
11212    #[test]
11213    fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
11214        // B10. A code generator rewrites `</script` to `<\/script` inside a
11215        // comment so the output cannot close an HTML <script> element early.
11216        // The restore compared the two spellings as different text, and because
11217        // it walks the source comments in order, one comment it could not
11218        // recognise consumed every remaining one and failed the file. That cost
11219        // a real project its coverage silently, and later its whole run.
11220        //
11221        // Prose about HTML is ordinary in web code, which is how this reached a
11222        // shipped release and stayed for months: the file was otherwise plain
11223        // TypeScript.
11224        for source in [
11225            "// </script>\nexport const x = 1;\n",
11226            "// </script\nexport const x = 1;\n",
11227            "// </SCRIPT>\nexport const x = 1;\n",
11228            "// see the </script> tag here\nexport const x = 1;\n",
11229            "/* block </script> here */\nexport const x = 1;\n",
11230            "/** jsdoc </script> */\nexport const x = 1;\n",
11231            // A run of comments, as the reported file had.
11232            "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
11233        ] {
11234            let output = instrument_candidate(source, "src/probe.ts")
11235                .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
11236            let original = source.lines().next().expect("a comment");
11237            assert!(
11238                output.code.contains(original),
11239                "comment was not restored verbatim: {original:?} missing from {:?}",
11240                output.code
11241            );
11242        }
11243        // The escape is the generator's, not the author's: a backslash the
11244        // author wrote survives as written rather than being collapsed.
11245        let written = "// author wrote <\\/script>\nexport const x = 1;\n";
11246        let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
11247        assert!(
11248            output.code.contains("// author wrote <\\/script>"),
11249            "{:?}",
11250            output.code
11251        );
11252        // Only that sequence is normalised. Nothing else a comment may contain
11253        // is treated as equal to a different spelling.
11254        assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
11255        assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
11256        assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
11257    }
11258
11259    #[test]
11260    fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
11261        // The counts in this error are usually equal, so reporting them alone
11262        // says the opposite of what happened -- nothing was dropped, one
11263        // comment was not recognised. The reported 116-vs-112 sent the first
11264        // reader looking for four missing comments that were never the cause.
11265        let error = CandidateError::CommentPreservation {
11266            expected: 116,
11267            actual: 112,
11268            detail:
11269                "no source comment matches the generated comment \"// x\" (matched 97 before it)"
11270                    .to_owned(),
11271        };
11272        let rendered = format!("{error:?}");
11273        assert!(rendered.contains("no source comment matches"), "{rendered}");
11274        assert!(rendered.contains("matched 97 before it"), "{rendered}");
11275    }
11276
11277    #[test]
11278    fn a_wrapped_default_keeps_the_type_its_syntax_names() {
11279        // Wrapped in a call, `strict = false` is no longer a default
11280        // `isolatedDeclarations` can read a type from, and h3 failed with
11281        // TS9011. The annotation says what was inferred before.
11282        let source = concat!(
11283            "export function f(a: number, strict = false, limit = 10, name = `x`, big = 1n, low = -1) { return a; }\n",
11284            "export function g<E extends 'utf8' | false = 'utf8'>(encoding = 'utf8' as E, list = [1], kept: string = 'a') { return encoding; }\n",
11285            "export const h = ({ open = false }, mode = 'x' as const) => open;\n",
11286        );
11287        let output = instrument_candidate(source, "src/defaults.ts").unwrap();
11288        for annotated in [
11289            "strict: boolean = ",
11290            "limit: number = ",
11291            "name: string = ",
11292            "big: bigint = ",
11293            "low: number = ",
11294            "encoding: E = ",
11295            "kept: string = ",
11296        ] {
11297            assert!(
11298                output.code.contains(annotated),
11299                "{annotated}\n{}",
11300                output.code
11301            );
11302        }
11303        // Nothing is said where the syntax names no type: an array, a
11304        // destructured parameter, `as const`.
11305        for bare in ["list = ", "mode = "] {
11306            assert!(output.code.contains(bare), "{bare}\n{}", output.code);
11307        }
11308        assert!(!output.code.contains("list:"), "{}", output.code);
11309        assert!(!output.code.contains("mode:"), "{}", output.code);
11310        // A JavaScript file has no annotations to give.
11311        let script = instrument_candidate(
11312            "export function f(strict = false) { return strict; }\n",
11313            "src/defaults.js",
11314        )
11315        .unwrap();
11316        assert!(!script.code.contains("strict:"), "{}", script.code);
11317    }
11318
11319    #[test]
11320    fn an_asserted_assignment_target_is_printed_as_a_target() {
11321        // `(buffer[0] as string) += child` came out as
11322        // `buffer[0] as string += child`, which does not parse: two of hono's
11323        // files could not be read by TypeScript or by a bundler.
11324        let source = concat!(
11325            "export function join(buffer: [unknown], child: string, n: { v?: number }) {\n",
11326            "  ;(buffer[0] as string) += child\n",
11327            "  ;(buffer[0] as string) = child\n",
11328            "  ;(n.v as number)++\n",
11329            "  ;(n.v satisfies number | undefined) = 1\n",
11330            "  ;[(buffer[0] as string)] = [child]\n",
11331            "  for ((buffer[0] as string) of [child]) {}\n",
11332            "  return buffer\n",
11333            "}\n",
11334        );
11335        let output = instrument_candidate(source, "src/targets.ts").unwrap();
11336        assert!(!output.code.contains(" as string"), "{}", output.code);
11337        assert!(!output.code.contains("satisfies"), "{}", output.code);
11338        assert!(
11339            output.code.contains("buffer[0] += child"),
11340            "{}",
11341            output.code
11342        );
11343        assert!(output.code.contains("n.v++"), "{}", output.code);
11344        let allocator = Allocator::default();
11345        let reparsed = Parser::new(&allocator, &output.code, SourceType::ts()).parse();
11346        assert!(
11347            reparsed.errors.is_empty(),
11348            "{:?}\n{}",
11349            reparsed.errors,
11350            output.code
11351        );
11352    }
11353
11354    #[test]
11355    fn a_comment_the_printer_moves_is_still_recognised() {
11356        // The printer writes a `@__NO_SIDE_EFFECTS__` annotation after the
11357        // documentation comment it stood before. The restore looked only
11358        // forward for each printed comment, did not find the annotation, and
11359        // zod's `v4/core/api.ts` could not be instrumented.
11360        let source = concat!(
11361            "// first\n",
11362            "// @__NO_SIDE_EFFECTS__\n",
11363            "/** documented\n */\n",
11364            "export function f(a: number) {\n",
11365            "  return a > 1 ? a : 0;\n",
11366            "}\n",
11367            "\n",
11368            "// second\n",
11369            "export type T = number;\n",
11370        );
11371        let output = instrument_candidate(source, "src/annotated.ts").unwrap();
11372        for comment in [
11373            "// first",
11374            "// @__NO_SIDE_EFFECTS__",
11375            "/** documented\n */",
11376            "// second",
11377        ] {
11378            assert_eq!(
11379                output.code.matches(comment).count(),
11380                1,
11381                "{comment}\n{}",
11382                output.code
11383            );
11384        }
11385    }
11386
11387    #[test]
11388    fn restored_comments_never_split_a_keyword_from_its_argument() {
11389        // oxc drops the comment that led a parenthesised return argument; the
11390        // restore used to put it back right after `return`, on its own line,
11391        // and automatic semicolon insertion turned the statement into `return;`
11392        // with the JSX unreachable. Vite then removed the unreachable tree and
11393        // every declaration only it used, so a component rendered nothing under
11394        // measurement while passing plainly. Expression-bodied arrows are hit
11395        // too, since instrumentation rewrites them into `return` blocks.
11396        let source = concat!(
11397            "export const A = ({ open }) => {\n",
11398            "  return (\n",
11399            "    // leading comment before JSX\n",
11400            "    <div aria-expanded={open}>a</div>\n",
11401            "  )\n",
11402            "}\n",
11403            "export const B = ({ open }) => (\n",
11404            "  // comment in an expression body\n",
11405            "  <div aria-expanded={open}>b</div>\n",
11406            ")\n",
11407            "export function C(x) {\n",
11408            "  return ( // inline line comment\n",
11409            "    x + 1\n",
11410            "  )\n",
11411            "}\n",
11412            "export function D(x) {\n",
11413            "  throw (\n",
11414            "    /* block\n       comment */\n",
11415            "    new Error(String(x))\n",
11416            "  )\n",
11417            "}\n",
11418            "export function E(x) {\n",
11419            "  return ( /* inline block */ x + 2 )\n",
11420            "}\n",
11421        );
11422        let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
11423        for keyword in ["return ", "throw "] {
11424            for (index, _) in output.code.match_indices(keyword) {
11425                let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
11426                let block_with_line_break = rest.starts_with("/*")
11427                    && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
11428                assert!(
11429                    !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
11430                    "`{keyword}` is separated from its argument:\n{}",
11431                    output.code
11432                );
11433            }
11434        }
11435        for comment in [
11436            "// leading comment before JSX",
11437            "// comment in an expression body",
11438            "// inline line comment",
11439            "/* block\n       comment */",
11440            "/* inline block */",
11441        ] {
11442            assert!(
11443                output.code.contains(comment),
11444                "{comment} was lost:\n{}",
11445                output.code
11446            );
11447        }
11448        assert!(
11449            output
11450                .code
11451                .contains("return <div aria-expanded={open}>a</div>"),
11452            "{}",
11453            output.code
11454        );
11455        assert!(
11456            output
11457                .code
11458                .contains("return <div aria-expanded={open}>b</div>"),
11459            "{}",
11460            output.code
11461        );
11462        // The restored output must still parse to the same program shape.
11463        let allocator = Allocator::default();
11464        let reparsed = Parser::new(
11465            &allocator,
11466            &output.code,
11467            SourceType::from_path("app/components/List.tsx").unwrap(),
11468        )
11469        .parse();
11470        assert!(
11471            reparsed.errors.is_empty(),
11472            "{:?}\n{}",
11473            reparsed.errors,
11474            output.code
11475        );
11476    }
11477
11478    #[test]
11479    fn preserves_reference_order_across_every_control_decision_kind() {
11480        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}";
11481        let output = instrument_candidate(source, "app/control.js").unwrap();
11482        assert_eq!(
11483            output
11484                .decisions
11485                .iter()
11486                .map(|decision| decision.kind.as_str())
11487                .collect::<Vec<_>>(),
11488            vec!["ternary", "while", "do-while", "for", "if"]
11489        );
11490    }
11491
11492    #[test]
11493    fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
11494        // B11. `for (const x of [a, b])` cannot iterate zero times, so the
11495        // outcome was an obligation no test could ever discharge -- the author
11496        // could only fake a test or delete the loop.
11497        let entered = |source: &str| {
11498            let output = analyze_candidate(source, "src/loops.ts").unwrap();
11499            output
11500                .branches
11501                .iter()
11502                .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
11503                .count()
11504        };
11505        // Folded: the iterable is certainly non-empty.
11506        for source in [
11507            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11508            "export function f(g) { for (const x of [1]) g(x); }\n",
11509            "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
11510            // A hole still yields once: [,].length === 1.
11511            "export function f(g) { for (const x of [,]) g(x); }\n",
11512            // A spread may contribute nothing, but a definite element is enough.
11513            "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
11514        ] {
11515            assert_eq!(entered(source), 0, "should be folded: {source}");
11516        }
11517        // Kept: the loop can genuinely run zero times.
11518        for source in [
11519            "export function f(g, ys) { for (const x of ys) g(x); }\n",
11520            "export function f(g) { for (const x of []) g(x); }\n",
11521            "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
11522            "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
11523            // Anything else may reach the binding, including a shrinking call.
11524            "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
11525            "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
11526            "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
11527            // A different binding of the same name must not be borrowed.
11528            concat!(
11529                "const xs = [1, 2];\n",
11530                "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
11531            ),
11532            // for-in is about keys, not elements, and is left alone.
11533            "export function f(g) { for (const k in [1, 2]) g(k); }\n",
11534        ] {
11535            assert_eq!(entered(source), 1, "should be kept: {source}");
11536        }
11537        // The loop body is still measured; only the impossible outcome is gone.
11538        let output = analyze_candidate(
11539            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11540            "src/loops.ts",
11541        )
11542        .unwrap();
11543        assert!(
11544            output
11545                .points
11546                .iter()
11547                .any(|point| point.source.contains("g(x)")),
11548            "the body must still be an obligation"
11549        );
11550    }
11551
11552    #[test]
11553    fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
11554        let source = concat!(
11555            "export function run(value) {\n",
11556            "  while (true) { break; }\n",
11557            "  do { value += 1; } while (false);\n",
11558            "  if (false) value += 2;\n",
11559            "  if (value || true) value += 3;\n",
11560            "  if (value && false) value += 4;\n",
11561            "  if (value) value += 5;\n",
11562            "  return value;\n",
11563            "}\n",
11564        );
11565        let output = analyze_candidate(source, "src/constants.js").unwrap();
11566        assert_eq!(output.decisions.len(), 1);
11567        assert_eq!(output.decisions[0].source, "value");
11568
11569        let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
11570        assert_eq!(instrumented.decisions.len(), 1);
11571        assert_eq!(instrumented.decisions[0].source, "value");
11572    }
11573
11574    #[test]
11575    fn leaves_compile_time_style_macro_arguments_as_source() {
11576        // StyleX evaluates these at build time and rejects a block-bodied
11577        // dynamic style; probing them broke a real project's Vite build.
11578        let source = concat!(
11579            "import * as stylex from '@stylexjs/stylex';\n",
11580            "const styles = stylex.create({\n",
11581            "  container: { display: 'flex' },\n",
11582            "  paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
11583            "  tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
11584            "});\n",
11585            "export function render(spacing: number) {\n",
11586            "  return stylex.props(styles.container, styles.paddingTop(spacing));\n",
11587            "}\n",
11588        );
11589        let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
11590        assert!(
11591            output.code.contains("=> ({ paddingTop: `${spacing}px` })")
11592                || output
11593                    .code
11594                    .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
11595            "dynamic style arrow must stay an expression body:\n{}",
11596            output.code
11597        );
11598        assert!(
11599            !output
11600                .decisions
11601                .iter()
11602                .any(|decision| decision.source.contains("dark")),
11603            "no decision may be collected inside the macro argument"
11604        );
11605        let function_points = output
11606            .points
11607            .iter()
11608            .filter(|point| point.kind == "function")
11609            .map(|point| point.source.as_str())
11610            .collect::<Vec<_>>();
11611        assert!(
11612            function_points
11613                .iter()
11614                .all(|source| source.contains("render")),
11615            "only the real function may carry a function point: {function_points:?}"
11616        );
11617        assert!(
11618            output.limitations.is_empty(),
11619            "compiled-away code is not a limitation: {:?}",
11620            output.limitations
11621        );
11622    }
11623
11624    #[test]
11625    fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
11626        let source = concat!(
11627            "declare global {\n",
11628            "  var Beacon: undefined | ((action: string) => void);\n",
11629            "}\n",
11630            "declare module 'virtual:feature' {\n",
11631            "  export const enabled: boolean;\n",
11632            "}\n",
11633            "declare const buildOnly: string;\n",
11634            "export const live = 1;\n",
11635        );
11636        let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
11637        let statements = output
11638            .points
11639            .iter()
11640            .filter(|point| point.kind == "statement")
11641            .map(|point| point.source.as_str())
11642            .collect::<Vec<_>>();
11643        assert_eq!(statements, ["const live = 1;"]);
11644        assert!(output.code.contains("const live = 1"));
11645    }
11646
11647    /// The instrumented code with what records a decision's outcome written
11648    /// `END(`, and the decisions' temporaries named `t0`, `t1`, … in the order
11649    /// they are declared.
11650    fn decision_shape(source: &str, file: &str) -> String {
11651        let output = instrument_candidate(source, file).unwrap();
11652        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11653        let call = format!("{}(", runtime.mcdc_end_v2);
11654        let start = output.code.find(&call).expect("an outcome is recorded") + call.len();
11655        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11656        let code = output
11657            .code
11658            .replace(&format!("{call}{probe_file}, "), "END(");
11659        let mut names: Vec<&str> = Vec::new();
11660        let mut shaped = String::new();
11661        let mut rest = code.as_str();
11662        while let Some(at) = rest.find("_supercovMcdc") {
11663            shaped.push_str(&rest[..at]);
11664            let tail = &rest[at..];
11665            // The runtime's own helpers are bound to `__supercovMcdc…`.
11666            if shaped.ends_with('_') {
11667                shaped.push_str("_supercovMcdc");
11668                rest = &tail["_supercovMcdc".len()..];
11669                continue;
11670            }
11671            let length = tail
11672                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
11673                .unwrap_or(tail.len());
11674            let name = &tail[..length];
11675            let index = names
11676                .iter()
11677                .position(|known| *known == name)
11678                .unwrap_or_else(|| {
11679                    names.push(name);
11680                    names.len() - 1
11681                });
11682            shaped.push_str(&format!("t{index}"));
11683            rest = &tail[length..];
11684        }
11685        shaped.push_str(rest);
11686        shaped
11687    }
11688
11689    #[test]
11690    fn a_decision_stays_the_test_it_was() {
11691        // TypeScript narrows through `&&` and `||` only where they are the
11692        // condition itself, through an assignment's right side, and through a
11693        // comma's last operand. The decision used to be the right side of an
11694        // assignment inside a comma, and each condition ended in its
11695        // temporary: `if (client) return client` returned `Client | null`,
11696        // and a file Supercov never touched failed to compile against it.
11697        let shaped = decision_shape(
11698            concat!(
11699                "export function pick(client: { id: number } | null, strict: boolean) {\n",
11700                "  if (client && strict) return client;\n",
11701                "  return null;\n",
11702                "}\n",
11703                "export function size(list: string[] | null) {\n",
11704                "  if (!list) { return 0; } else { return list.length; }\n",
11705                "}\n",
11706                "export function first(list: string[] | null) {\n",
11707                "  return list ? list[0] : undefined;\n",
11708                "}\n",
11709            ),
11710            "src/pick.ts",
11711        );
11712        for expected in [
11713            // The bookkeeping rides inside the first condition, and every
11714            // condition ends in the assignment of its own value.
11715            "if ((t0 = 1, t2 = 0, t1 = client) && (t0 += 3, t2 = strict)) {\n\t\t\tEND(0, t0 + +!!t1 + 3 * +!!t2, true), t0 = 0;",
11716            // Without an `else` there is nowhere inside the statement for the
11717            // false outcome, and an `else` made for it would be a branch to
11718            // a coverage tool the tests run: it is recorded after the `if`,
11719            // when the frame says it has not been.
11720            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1 + 3 * +!!t2, false);\n\t}",
11721            "if (t3 = 1, t4 = !list) {\n\t\tEND(1, t3 + +!!t4, true);",
11722            "} else {\n\t\tEND(1, t3 + +!!t4, false);",
11723            "return (t5 = 1, t6 = list) ? (void END(2, t5 + +!!t6, true), list[0]) : (void END(2, t5 + +!!t6, false), undefined);",
11724        ] {
11725            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11726        }
11727    }
11728
11729    #[test]
11730    fn a_loop_records_its_last_test_after_itself() {
11731        // A loop's true outcome is recorded where its body starts. Nothing
11732        // runs when the test is false, so the statement after the loop
11733        // records that, and only when the test, not a `break`, ended it.
11734        let shaped = decision_shape(
11735            concat!(
11736                "export function scan(limit: number) {\n",
11737                "  let i = 0;\n",
11738                "  walk: while (i < limit) { i++; if (i > 5) break walk; }\n",
11739                "  do i--; while (i > 0);\n",
11740                "  for (let j = 0; j < limit; j++) i += j;\n",
11741                "  return i;\n",
11742                "}\n",
11743            ),
11744            "src/scan.ts",
11745        );
11746        for expected in [
11747            // The label stays on its loop, for `continue walk`.
11748            "{\n\t\twalk: while (t0 = 1, t1 = i < limit) {\n\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11749            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1, false);\n\t}",
11750            // A `do … while` body runs before its test: the frame says whether
11751            // one came before this iteration, and starts cleared.
11752            "{\n\t\tt4 = 0;\n\t\tdo {\n\t\t\tt4 && (END(2, t4 + +!!t5, true), t4 = 0);",
11753            "} while (t4 = 1, t5 = i > 0);\n\t\tt4 && END(2, t4 + +!!t5, false);\n\t}",
11754            "for (let j = 0; t6 = 1, t7 = j < limit; j++) {\n\t\t\tEND(3, t6 + +!!t7, true), t6 = 0;",
11755            "\t\t}\n\t\tt6 && END(3, t6 + +!!t7, false);\n\t}",
11756        ] {
11757            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11758        }
11759    }
11760
11761    #[test]
11762    fn a_decision_without_a_function_body_around_it_declares_its_own_temporaries() {
11763        let shaped = decision_shape(
11764            concat!(
11765                "export function pick(\n",
11766                "  n: number,\n",
11767                "  kind = class { static label = 'none'; static { if (n > 1) this.label = 'some'; } },\n",
11768                "  tag = n > 5 ? 'high' : 'low',\n",
11769                ") {\n",
11770                "  return [kind.label, tag];\n",
11771                "}\n",
11772            ),
11773            "src/defaults.ts",
11774        );
11775        for expected in [
11776            "static {\n\t\tlet t0, t1;\n",
11777            "if (t0 = 1, t1 = n > 1) {\n\t\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11778            // A parameter default has no body to declare in: the arrow holds
11779            // the whole conditional expression, so its branches still see
11780            // what the test narrowed.
11781            "(() => {\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})()",
11782        ] {
11783            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11784        }
11785    }
11786
11787    #[test]
11788    fn a_top_level_declaration_has_a_line_of_its_own_before_it() {
11789        // See `separate_top_level_declarations`. An overload signature stays
11790        // next to its implementation.
11791        let output = instrument_candidate(
11792            concat!(
11793                "const one = 1;\n",
11794                "function plain() { return one; }\n",
11795                "class Box {}\n",
11796                "function over(value: string): string;\n",
11797                "function over(value: number): number;\n",
11798                "function over(value: any) { return value; }\n",
11799                "export function exported() { return plain(); }\n",
11800            ),
11801            "src/declarations.ts",
11802        )
11803        .unwrap();
11804        for expected in [
11805            "\n;\nfunction plain() {",
11806            "\n;\nclass Box {",
11807            "\n;\nfunction over(value: string): string;\nfunction over(value: number): number;\nfunction over(value: any) {",
11808        ] {
11809            assert!(
11810                output.code.contains(expected),
11811                "{expected}\n{}",
11812                output.code
11813            );
11814        }
11815        assert!(
11816            !output.code.contains(";\nexport function"),
11817            "{}",
11818            output.code
11819        );
11820    }
11821
11822    #[test]
11823    fn exposes_the_complete_probe_v2_instrumenter_contract() {
11824        let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
11825        assert!(output.complete);
11826        assert!(output.limitations.is_empty());
11827        assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
11828        let runtime = output.runtime.expect("candidate runtime binding");
11829        assert!(output.code.contains(&runtime.mcdc_end_v2));
11830        assert!(output.code.contains("_supercovMcdcFrame"));
11831        assert!(output.code.contains("_supercovMcdcValue"));
11832        assert_eq!(output.decisions.len(), 1);
11833        assert!(output.points.iter().any(|point| point.kind == "statement"));
11834        assert!(output.points.iter().any(|point| point.kind == "function"));
11835
11836        let allocator = Allocator::default();
11837        let reparsed = Parser::new(
11838            &allocator,
11839            &output.code,
11840            SourceType::from_path("app/decide.ts").unwrap(),
11841        )
11842        .parse();
11843        assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
11844    }
11845
11846    #[test]
11847    fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
11848        let source = concat!(
11849            "import type { Session } from './types.ts';\n",
11850            "import { type Row } from './rows.ts';\n",
11851            "import './register.ts';\n",
11852            "const value = 1;\n",
11853        );
11854        let output = instrument_candidate(source, "app/imports.ts").unwrap();
11855        let statement_lines = output
11856            .points
11857            .iter()
11858            .filter(|point| point.kind == "statement")
11859            .map(|point| point.line)
11860            .collect::<Vec<_>>();
11861        assert_eq!(statement_lines, vec![2, 3, 4]);
11862    }
11863
11864    #[test]
11865    fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
11866        let source = concat!(
11867            "import { Logger } from './types.js';\n",
11868            "import DefaultType from './default.js';\n",
11869            "import * as Types from './namespace.js';\n",
11870            "import { type Inline } from './inline.js';\n",
11871            "import { run, type Config } from './mixed.js';\n",
11872            "import './register.js';\n",
11873            "import {} from './empty.js';\n",
11874            "import { unused } from './unused.js';\n",
11875            "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
11876        );
11877        let preserved =
11878            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", false)
11879                .unwrap();
11880        let erased =
11881            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true).unwrap();
11882        assert_eq!(
11883            erased
11884                .excluded_statements
11885                .iter()
11886                .map(|p| p.line)
11887                .collect::<Vec<_>>(),
11888            vec![1, 2, 3, 4]
11889        );
11890        let excluded = erased
11891            .excluded_statements
11892            .iter()
11893            .map(|p| &p.id)
11894            .collect::<HashSet<_>>();
11895        let expected = preserved
11896            .points
11897            .iter()
11898            .filter(|p| !excluded.contains(&p.id))
11899            .collect::<Vec<_>>();
11900        assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
11901        for line in [5, 6, 7, 8] {
11902            assert!(erased.points.iter().any(|p| p.line == line));
11903        }
11904        // JavaScript value imports and type queries with runtime uses remain.
11905        let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
11906        let mixed = instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true)
11907            .unwrap();
11908        assert!(mixed.excluded_statements.is_empty());
11909    }
11910
11911    /// A line's first mapping: `None` for no segment, `Some(None)` for an
11912    /// unmapped one, else the source line and column it names.
11913    type FirstMapping = Option<Option<(u32, u32)>>;
11914
11915    /// The instrumented copy's lines with their first mapping.
11916    fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
11917        let map = oxc_sourcemap::SourceMap::from_json_string(
11918            &output
11919                .map
11920                .as_ref()
11921                .expect("instrumented copy has a map")
11922                .to_string(),
11923        )
11924        .unwrap();
11925        let tokens = map.get_tokens().collect::<Vec<_>>();
11926        output
11927            .code
11928            .split('\n')
11929            .enumerate()
11930            .map(|(line, text)| {
11931                let first = tokens
11932                    .iter()
11933                    .find(|token| token.get_dst_line() == line as u32)
11934                    .map(|token| {
11935                        token
11936                            .get_source_id()
11937                            .map(|_| (token.get_src_line(), token.get_src_col()))
11938                    });
11939                (text.trim().to_string(), first)
11940            })
11941            .collect()
11942    }
11943
11944    #[test]
11945    fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
11946        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";
11947        let output = instrument_candidate(source, "app/f.js").unwrap();
11948        let lines = first_mappings(&output);
11949        let runtime = output.runtime.as_ref().unwrap();
11950        for (text, first) in &lines {
11951            // A probe maps to nothing: a coverage tool reading the copy
11952            // through its map counts no branch of the user's for its `||`.
11953            if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
11954                && let Some(first) = first
11955            {
11956                assert_eq!(*first, None, "{text}");
11957            }
11958        }
11959        let at = |needle: &str| {
11960            lines
11961                .iter()
11962                .find(|(text, _)| text.starts_with(needle))
11963                .unwrap_or_else(|| panic!("{needle} in {}", output.code))
11964                .1
11965        };
11966        // Each statement after a block Supercov added maps at its own start,
11967        // where the block's brace used to take that mapping from it.
11968        assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
11969        assert_eq!(at("continue;"), Some(Some((4, 25))));
11970        assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
11971        // The arrow's body, made a block, returns what the expression it was
11972        // evaluated: the line maps where that expression starts.
11973        assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
11974        // Every line of the program starting with a closing brace maps where
11975        // it starts, so a tool that looks a position up on its own line finds
11976        // a mapping.
11977        let program = lines
11978            .iter()
11979            .position(|(text, _)| text.starts_with("function f("))
11980            .unwrap();
11981        for (text, first) in &lines[program..] {
11982            if text.starts_with('}') {
11983                assert!(first.is_some(), "{text} in {}", output.code);
11984            }
11985        }
11986    }
11987
11988    /// The instrumented code with the calls that record a value tree written
11989    /// `END2(`, `END(` and `HIT(`, and the trees' frames named `s0`, `s1`, …
11990    fn value_shape(source: &str, file: &str) -> String {
11991        let output = instrument_candidate(source, file).unwrap();
11992        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11993        let call = format!("{}(", runtime.select_end2_v2);
11994        let start = output.code.find(&call).expect("a value tree is recorded") + call.len();
11995        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11996        let mut code = output.code.clone();
11997        for (helper, short) in [
11998            (&runtime.select_end2_v2, "END2("),
11999            (&runtime.select_end_v2, "END("),
12000            (&runtime.select_next_v2, "NEXT("),
12001            (&runtime.coverage_hit_v2, "HIT("),
12002            (&runtime.select_right_v2, "RIGHT("),
12003            (&runtime.select_short_v2, "SHORT("),
12004        ] {
12005            code = code.replace(&format!("{helper}({probe_file}, "), short);
12006        }
12007        let mut names: Vec<&str> = Vec::new();
12008        let mut shaped = String::new();
12009        let mut rest = code.as_str();
12010        while let Some(at) = rest.find("_supercovSelected") {
12011            shaped.push_str(&rest[..at]);
12012            let tail = &rest[at..];
12013            let length = tail
12014                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
12015                .unwrap_or(tail.len());
12016            let name = &tail[..length];
12017            let index = names
12018                .iter()
12019                .position(|known| *known == name)
12020                .unwrap_or_else(|| {
12021                    names.push(name);
12022                    names.len() - 1
12023                });
12024            shaped.push_str(&format!("s{index}"));
12025            rest = &tail[length..];
12026        }
12027        shaped.push_str(rest);
12028        shaped
12029    }
12030
12031    /// Every tree of up to `leaves` leaves over the three operators, with
12032    /// its selections' points 10 apart.
12033    fn selection_shapes(
12034        leaves: usize,
12035        next_leaf: &mut usize,
12036        next_first: &mut usize,
12037    ) -> Vec<(SelectionShape, usize, usize)> {
12038        if leaves == 1 {
12039            let shape = SelectionShape::Leaf(*next_leaf);
12040            return vec![(shape, *next_leaf + 1, *next_first)];
12041        }
12042        let mut shapes = Vec::new();
12043        for left_leaves in 1..leaves {
12044            for kind in 0..3 {
12045                let first = *next_first;
12046                let mut after_first = first + 10;
12047                for (left, leaf_after_left, first_after_left) in
12048                    selection_shapes(left_leaves, next_leaf, &mut after_first)
12049                {
12050                    let mut leaf = leaf_after_left;
12051                    let mut first_right = first_after_left;
12052                    for (right, leaf_after, first_after) in
12053                        selection_shapes(leaves - left_leaves, &mut leaf, &mut first_right)
12054                    {
12055                        shapes.push((
12056                            SelectionShape::Selection {
12057                                first,
12058                                kind,
12059                                left: Box::new(copy_shape(&left)),
12060                                right: Box::new(right),
12061                            },
12062                            leaf_after,
12063                            first_after,
12064                        ));
12065                    }
12066                }
12067            }
12068        }
12069        shapes
12070    }
12071
12072    fn copy_shape(shape: &SelectionShape) -> SelectionShape {
12073        match shape {
12074            SelectionShape::Leaf(leaf) => SelectionShape::Leaf(*leaf),
12075            SelectionShape::Selection {
12076                first,
12077                kind,
12078                left,
12079                right,
12080            } => SelectionShape::Selection {
12081                first: *first,
12082                kind: *kind,
12083                left: Box::new(copy_shape(left)),
12084                right: Box::new(copy_shape(right)),
12085            },
12086        }
12087    }
12088
12089    /// A leaf's steps, as `LogicalValueTransformer::shape` builds them.
12090    fn leaf_steps(shape: &SelectionShape, path: &[usize], steps: &mut Vec<Vec<usize>>) {
12091        match shape {
12092            SelectionShape::Leaf(_) => steps.push(path.to_vec()),
12093            SelectionShape::Selection {
12094                first,
12095                kind,
12096                left,
12097                right,
12098            } => {
12099                let mut left_path = vec![first * 4 + kind];
12100                left_path.extend_from_slice(path);
12101                let mut right_path = vec![first * 4 + 3];
12102                right_path.extend_from_slice(path);
12103                leaf_steps(left, &left_path, steps);
12104                leaf_steps(right, &right_path, steps);
12105            }
12106        }
12107    }
12108
12109    /// Evaluate a tree over values that are truthy (2), falsy (1) or nullish
12110    /// (0): its value, and the leaves evaluated, in order.
12111    fn evaluate(shape: &SelectionShape, values: &[u8], evaluated: &mut Vec<usize>) -> u8 {
12112        match shape {
12113            SelectionShape::Leaf(leaf) => {
12114                evaluated.push(*leaf);
12115                values[*leaf]
12116            }
12117            SelectionShape::Selection {
12118                kind, left, right, ..
12119            } => {
12120                let value = evaluate(left, values, evaluated);
12121                let short = match kind {
12122                    0 => value == 2,
12123                    1 => value != 2,
12124                    _ => value != 0,
12125                };
12126                if short {
12127                    value
12128                } else {
12129                    evaluate(right, values, evaluated)
12130                }
12131            }
12132        }
12133    }
12134
12135    /// What the runtime records for a leaf's steps and its value.
12136    fn record_steps(steps: &[usize], value: u8, recorded: &mut Vec<usize>) {
12137        for step in steps {
12138            let (first, code) = (step / 4, step % 4);
12139            let truthy = usize::from(value == 2);
12140            if code == 3 {
12141                recorded.push(first + 2 + truthy);
12142            } else if match code {
12143                0 => value == 2,
12144                1 => value != 2,
12145                _ => value != 0,
12146            } {
12147                recorded.push(first + truthy);
12148            } else {
12149                break;
12150            }
12151        }
12152    }
12153
12154    #[test]
12155    fn a_value_tree_records_what_a_call_around_each_leaf_recorded() {
12156        // The points a leaf decided are written out where evaluation goes
12157        // on from it, from the tree's shape alone. Every tree of up to five
12158        // leaves, under every combination of truthy, falsy and nullish
12159        // values, records the points `selectPathV2` recorded around each
12160        // leaf, in the same order.
12161        let mut checked = 0;
12162        for leaves in 2..=5 {
12163            for (shape, ..) in selection_shapes(leaves, &mut 0, &mut 0) {
12164                let mut steps = Vec::new();
12165                leaf_steps(&shape, &[], &mut steps);
12166                let mut before = vec![Vec::new(); leaves];
12167                shape.before(&mut before);
12168                for combination in 0..3_usize.pow(leaves as u32) {
12169                    let values = (0..leaves)
12170                        .map(|leaf| (combination / 3_usize.pow(leaf as u32) % 3) as u8)
12171                        .collect::<Vec<_>>();
12172                    let mut evaluated = Vec::new();
12173                    evaluate(&shape, &values, &mut evaluated);
12174                    let mut each_leaf = Vec::new();
12175                    for leaf in &evaluated {
12176                        record_steps(&steps[*leaf], values[*leaf], &mut each_leaf);
12177                    }
12178                    let mut tree = Vec::new();
12179                    for pair in evaluated.windows(2) {
12180                        // As `recorded_before` writes it: the only leaf that
12181                        // can come before, or the one the frame names.
12182                        let deciders = &before[pair[1]];
12183                        let (_, points) = match &deciders[..] {
12184                            [only] => only,
12185                            several => several
12186                                .iter()
12187                                .find(|(leaf, _)| *leaf == pair[0])
12188                                .expect("the leaf before is one the table has"),
12189                        };
12190                        tree.extend(points);
12191                    }
12192                    let last = *evaluated.last().unwrap();
12193                    record_steps(&steps[last], values[last], &mut tree);
12194                    assert_eq!(tree, each_leaf, "{values:?} over {steps:?}");
12195                    assert!(
12196                        evaluated
12197                            .windows(2)
12198                            .all(|pair| before[pair[1]].iter().any(|(leaf, _)| *leaf == pair[0])),
12199                        "{values:?} over {steps:?}: {before:?}"
12200                    );
12201                    checked += 1;
12202                }
12203            }
12204        }
12205        assert!(checked > 100_000, "{checked}");
12206    }
12207
12208    #[test]
12209    fn a_constant_keeps_its_tree_and_a_named_chain_its_name() {
12210        let shaped = value_shape(
12211            concat!(
12212                "export function text(value: string | number) {\n",
12213                "  const isText = typeof value === 'string' && value.length > 0;\n",
12214                "  if (isText) return value;\n",
12215                "  return null;\n",
12216                "}\n",
12217                "export const both = text(1) ?? text('a');\n",
12218                "export class Owner {\n",
12219                "  pet?: { name: string };\n",
12220                "  named(other?: Owner) { return [other?.pet, this?.pet, this.pet?.name]; }\n",
12221                "}\n",
12222            ),
12223            "src/alias.ts",
12224        );
12225        for expected in [
12226            // TypeScript narrows through a constant only when the condition
12227            // is its initializer: the value is recorded by the statement
12228            // after, from the constant.
12229            "const isText = (s1 = 0, typeof value === \"string\") && (s1 = 1, value.length > 0);\n\tEND2(9, isText, s1);\n",
12230            "export const both = (s0 = 0, text(1)) ?? (s0 = 1, text(\"a\"));\nEND2(13, both, s0);\n",
12231            // A name and `this` are read again after the call that records
12232            // them, so the chain narrows them.
12233            ", other)?.pet",
12234            ", this)?.pet",
12235        ] {
12236            assert!(shaped.contains(expected), "{expected}\n{shaped}");
12237        }
12238        // A member may run a getter: it is read once, through the call.
12239        assert!(shaped.contains(", this.pet)?.name"), "{shaped}");
12240        assert!(!shaped.contains(", this.pet), this.pet)"), "{shaped}");
12241    }
12242
12243    #[test]
12244    fn a_value_tree_keeps_its_leaves_the_programs_own_expressions() {
12245        // With a call around each leaf, `user.name` was read from a `user`
12246        // nothing had narrowed: `any`, when a member of its type has no
12247        // `name`, for everything that reads the value. A leaf is the last
12248        // operand of a comma now, and the call goes around the tree.
12249        let shaped = value_shape(
12250            concat!(
12251                "export function name(user: { name: string } | false) {\n",
12252                "  return user && user.name;\n",
12253                "}\n",
12254                "export function ok(total: number, name: string) {\n",
12255                "  return (total > 0 && name !== 'x') || !!name;\n",
12256                "}\n",
12257                "export function decided(total: number, name: string, force: boolean) {\n",
12258                "  return (total > 0 && name !== 'x') || force;\n",
12259                "}\n",
12260                "export function pick(a: number | null, b: number | null, c: number) {\n",
12261                "  return a ?? b ?? c;\n",
12262                "}\n",
12263                "export function held(list: string[] | null, fallback = list || []) {\n",
12264                "  return fallback;\n",
12265                "}\n",
12266            ),
12267            "src/values.ts",
12268        );
12269        for expected in [
12270            "let s0: any;\n",
12271            "return END2(12, (s0 = 0, user) && (s0 = 1, user.name), s0);",
12272            // More than two leaves: the tree's own operators stay between
12273            // them, so a coverage tool the tests run reads one branch, and
12274            // what `total > 0` or `name !== 'x'` decided is recorded where
12275            // evaluation went on to `!!name`, by which of them it left:
12276            // the runtime looks that up, because a choice written out here
12277            // would be a branch of this line to that tool. The compound left
12278            // operand is a decision too: its conditions are recorded in the
12279            // leaves and its outcome after the tree, from their values with
12280            // `&`, so the tree stays one tree and narrows as written.
12281            "(s1 = 0, _supercovMcdcFrame27 = 1, _supercovMcdcValue29 = 0, _supercovMcdcValue28 = total > 0) && (s1 = 1, _supercovMcdcFrame27 += 3, _supercovMcdcValue29 = name !== \"x\") || (NEXT(0, s1, 2), s1 = 2, !!name), s1)",
12282            "!!(+!!_supercovMcdcValue28 & +!!_supercovMcdcValue29)), _supercovMcdcResult30;",
12283            "|| (NEXT(1, s2, 2), s2 = 2, force), s2)",
12284            "kind: \"value\"",
12285            // Only `b` can come before `c`: a first `??` that kept `a` ends
12286            // the second.
12287            "return END(2, (s3 = 0, a) ?? (s3 = 1, b) ?? (HIT(34), s3 = 2, c), s3);",
12288            // A parameter list cannot see a frame its function's body
12289            // declares: the leaves keep their calls there.
12290            "SHORT(40, list, 0) || RIGHT(40, [])",
12291        ] {
12292            assert!(shaped.contains(expected), "{expected}\n{shaped}");
12293        }
12294    }
12295
12296    #[test]
12297    fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
12298        let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
12299        for file in ["app/fill.js", "app/fill.ts"] {
12300            let output = instrument_candidate(source, file).unwrap();
12301            let runtime = output.runtime.expect("candidate runtime binding");
12302            // No frame per evaluation: the target is written by the program's
12303            // own operator, once.
12304            assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
12305            assert!(output.code.contains("||= "), "{}", output.code);
12306            assert!(output.code.contains("??= "), "{}", output.code);
12307            assert!(output.code.contains("&&= "), "{}", output.code);
12308            assert_eq!(
12309                output
12310                    .code
12311                    .matches(&format!("{}(", runtime.select_assign_end_v2))
12312                    .count(),
12313                3
12314            );
12315            assert_eq!(
12316                output
12317                    .code
12318                    .matches(&format!("{}(", runtime.select_named_right_v2))
12319                    .count(),
12320                1
12321            );
12322            let allocator = Allocator::default();
12323            let reparsed = Parser::new(
12324                &allocator,
12325                &output.code,
12326                SourceType::from_path(file).unwrap(),
12327            )
12328            .parse();
12329            assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
12330        }
12331    }
12332
12333    #[test]
12334    fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
12335        let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
12336        let output = instrument_candidate(source, "app/collisions.js").unwrap();
12337        let runtime = output.runtime.expect("candidate runtime binding");
12338        assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
12339        assert!(!output.code.contains("let _supercovMcdcFrame1,"));
12340    }
12341
12342    #[test]
12343    fn instruments_wider_decisions_with_the_exact_v1_fallback() {
12344        let predicate = (0..33)
12345            .map(|index| format!("c{index}"))
12346            .collect::<Vec<_>>()
12347            .join(" && ");
12348        let source = format!("if ({predicate}) work();");
12349        let output = instrument_candidate(&source, "app/wide.js").unwrap();
12350        assert_eq!(output.decisions[0].conditions.len(), 33);
12351        let runtime = output.runtime.expect("candidate runtime binding");
12352        assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
12353        assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
12354        assert!(
12355            output
12356                .code
12357                .contains(&format!("{}(", runtime.mcdc_condition))
12358        );
12359        assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
12360    }
12361
12362    /// A server's request, upgrade and connection callbacks run in the
12363    /// request's phase; a spread or a missing callback is left as written.
12364    #[test]
12365    fn server_callbacks_run_in_the_request_phase_however_they_are_passed() {
12366        let source = "import http from 'node:http';\nconst handlers = [() => {}];\nconst server = http.createServer({}, function (req, res) { res.end('a'); });\nserver.on('request', ...handlers);\nserver.on('request');\nserver.on('connection', (socket) => socket.setNoDelay());\nserver.once('upgrade', onUpgrade);\nserver.on('close', () => {});\nfunction onUpgrade() {}\n";
12367        let output = instrument_candidate(source, "src/server.mjs").unwrap();
12368        let runtime = output.runtime.as_ref().expect("runtime binding");
12369        let phase = &runtime.with_request_phase;
12370        let line = |start: &str| {
12371            output
12372                .code
12373                .lines()
12374                .find(|line| line.starts_with(start))
12375                .unwrap_or_else(|| panic!("no {start} in\n{}", output.code))
12376                .to_owned()
12377        };
12378        assert!(line("const server").contains(&format!("{{}}, {phase}(function(req, res)")));
12379        assert_eq!(
12380            line("server.on(\"request\", ..."),
12381            "server.on(\"request\", ...handlers);"
12382        );
12383        assert_eq!(line("server.on(\"request\")"), "server.on(\"request\");");
12384        assert!(line("server.on(\"connection\"").contains(&format!("{phase}((socket)")));
12385        // A connection callback is told it is one.
12386        assert_eq!(
12387            output.code.matches("\"connection\")").count(),
12388            1,
12389            "{}",
12390            output.code
12391        );
12392        assert_eq!(
12393            line("server.once"),
12394            format!("server.once(\"upgrade\", {phase}(onUpgrade));")
12395        );
12396        assert!(!line("server.on(\"close\"").contains(phase.as_str()));
12397    }
12398
12399    #[test]
12400    fn leaves_look_alike_route_exports_alone() {
12401        for (file, source) in [
12402            // Not under `app/`.
12403            (
12404                "lib/route.ts",
12405                "export function GET(request) { return request.url; }",
12406            ),
12407            // Under `app/`, but not a route module or not a method name.
12408            (
12409                "app/api/items/handler.ts",
12410                "export function GET(request) { return request.url; }",
12411            ),
12412            (
12413                "app/route.ts",
12414                "export function helper(request) { return request.url; }",
12415            ),
12416        ] {
12417            let output = instrument_candidate(source, file).unwrap();
12418            assert!(
12419                !output.code.contains("withRequestPhase"),
12420                "{file}: {}",
12421                output.code
12422            );
12423        }
12424    }
12425
12426    #[test]
12427    fn wraps_framework_request_exports_without_changing_the_public_api() {
12428        for (file, source, export_prefix) in [
12429            (
12430                "app/routes/example.ts",
12431                "export const loader = async ({ request }) => request.url;",
12432                "export const loader = ",
12433            ),
12434            (
12435                "app/routes/example.ts",
12436                "export async function action({ request }) { return request.method; }",
12437                "export const action = ",
12438            ),
12439            (
12440                "app/api/items/route.ts",
12441                "export function GET(request) { return Response.json({ url: request.url }); }",
12442                "export const GET = ",
12443            ),
12444            // The root route handler serves `/`.
12445            (
12446                "app/route.ts",
12447                "export function GET(request) { return Response.json({ url: request.url }); }",
12448                "export const GET = ",
12449            ),
12450            (
12451                "src/app/api/items/route.ts",
12452                "export async function POST(request) { return Response.json(await request.json()); }",
12453                "export const POST = ",
12454            ),
12455            (
12456                "app/routes/example.ts",
12457                "export { generateAction as action } from './generateAction';",
12458                "export const action = ",
12459            ),
12460            (
12461                "app/entry.server.tsx",
12462                "export default async function handleRequest(request) { return request.url; }",
12463                "export default ",
12464            ),
12465        ] {
12466            let output = instrument_candidate(source, file).unwrap();
12467            let runtime = output.runtime.as_ref().expect("runtime binding");
12468            assert!(
12469                output
12470                    .code
12471                    .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
12472                "{file}: {}",
12473                output.code
12474            );
12475            assert!(
12476                output.code.contains(&format!(
12477                    "withRequestPhase as {}",
12478                    runtime.with_request_phase
12479                )),
12480                "{file}: {}",
12481                output.code
12482            );
12483            let allocator = Allocator::default();
12484            let reparsed = Parser::new(
12485                &allocator,
12486                &output.code,
12487                SourceType::from_path(file).unwrap(),
12488            )
12489            .parse();
12490            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
12491        }
12492    }
12493
12494    #[test]
12495    fn parse_failures_are_explicit_and_never_claim_completeness() {
12496        assert!(matches!(
12497            analyze_candidate("if (", "broken.js"),
12498            Err(CandidateError::Parse(errors)) if !errors.is_empty()
12499        ));
12500        assert!(matches!(
12501            analyze_candidate("let value = 1", "unknown.extension"),
12502            Err(CandidateError::UnknownSourceType(_))
12503        ));
12504    }
12505}