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