Skip to main content

supercov_engine/
js_instrumenter.rs

1//! First oxc-backed vertical slice of the Rust JavaScript instrumenter.
2//!
3//! This candidate reports and instruments the complete frozen JavaScript
4//! denominator, including semantic-safety boundaries and exact wide-decision
5//! fallback. It remains private until its generated runtime import, evidence
6//! transport, and attribution behavior are defined by Supercov's frozen contracts.
7
8use std::{
9    collections::{HashMap, HashSet},
10    fmt::Write,
11    path::Path,
12    sync::Arc,
13};
14
15use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
16use oxc_allocator::{Allocator, CloneIn, TakeIn};
17use oxc_ast::{
18    AstBuilder, AstKind, NONE,
19    ast::{
20        Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentExpression,
21        AssignmentPattern, AssignmentTarget, BindingPattern, CallExpression, CatchClause,
22        ChainElement, ChainExpression, Class, Comment, ComputedMemberExpression,
23        ConditionalExpression, Declaration, DoWhileStatement, ExportDefaultDeclarationKind,
24        Expression, ExpressionStatement, ForInStatement, ForOfStatement, ForStatement,
25        ForStatementLeft, FormalParameter, FormalParameterKind, FormalParameters, Function,
26        FunctionBody, IfStatement, ImportDeclarationSpecifier, ImportOrExportKind, JSXExpression,
27        JSXExpressionContainer, LogicalExpression, NewExpression, ObjectPropertyKind,
28        PrivateFieldExpression, Program, PropertyKey, PropertyKind, Statement,
29        StaticMemberExpression, SwitchStatement, TSGlobalDeclaration, TSModuleDeclaration,
30        TryStatement, VariableDeclaration, VariableDeclarationKind, VariableDeclarator,
31        WhileStatement, WithStatement,
32    },
33};
34use oxc_ast_visit::{Visit, VisitMut, walk, walk_mut};
35use oxc_codegen::{Codegen, CodegenOptions};
36use oxc_parser::Parser;
37use oxc_semantic::SemanticBuilder;
38use oxc_span::{GetSpan, GetSpanMut, SourceType, Span};
39use oxc_syntax::{
40    number::NumberBase,
41    operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator},
42    scope::ScopeFlags,
43    symbol::SymbolId,
44};
45use oxc_traverse::{Ancestor, Traverse, TraverseCtx, traverse_mut};
46use serde::{Deserialize, Serialize};
47use sha2::{Digest, Sha256};
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct CandidateDecision {
52    pub id: String,
53    pub file: String,
54    pub line: usize,
55    pub column: usize,
56    pub source: String,
57    pub conditions: Vec<String>,
58    pub kind: String,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct CandidatePoint {
64    pub id: String,
65    pub kind: String,
66    pub file: String,
67    pub line: usize,
68    pub column: usize,
69    pub source: String,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub label: Option<String>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct CandidateBranchAlternative {
77    pub id: String,
78    pub label: String,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct CandidateBranch {
84    pub id: String,
85    pub kind: String,
86    pub file: String,
87    pub line: usize,
88    pub column: usize,
89    pub source: String,
90    pub alternatives: Vec<CandidateBranchAlternative>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct CandidateOutput {
96    pub engine: String,
97    pub complete: bool,
98    pub supported_surface: String,
99    pub code: String,
100    pub map: Option<serde_json::Value>,
101    pub decisions: Vec<CandidateDecision>,
102    pub points: Vec<CandidatePoint>,
103    /// Source statements erased by the selected TypeScript import policy.
104    #[serde(default, skip_serializing_if = "Vec::is_empty")]
105    pub excluded_statements: Vec<CandidatePoint>,
106    pub branches: Vec<CandidateBranch>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub runtime: Option<CandidateRuntime>,
109    pub coverage_limitations: Vec<CandidateLimitation>,
110    pub limitations: Vec<String>,
111}
112
113fn restore_comment_text(
114    program: &Program<'_>,
115    generated: &str,
116    map: oxc_sourcemap::SourceMap,
117) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
118    if program.comments.is_empty() {
119        return Ok((generated.to_string(), map));
120    }
121    let allocator = Allocator::default();
122    let reparsed = Parser::new(&allocator, generated, program.source_type).parse();
123    if !reparsed.errors.is_empty() {
124        return Err(CandidateError::Parse(
125            reparsed
126                .errors
127                .into_iter()
128                .map(|error| error.to_string())
129                .collect(),
130        ));
131    }
132    let mut matched = vec![false; program.comments.len()];
133    let mut original_index = 0;
134    let mut edits = Vec::<(usize, usize, String)>::new();
135    for emitted in &reparsed.program.comments {
136        let emitted_text = emitted.span.source_text(generated);
137        let same = |index: usize| {
138            let original = &program.comments[index];
139            original.kind == emitted.kind
140                && equal_ignoring_whitespace(
141                    original.span.source_text(program.source_text),
142                    emitted_text,
143                )
144        };
145        // Comments come out in source order, with one exception: the printer
146        // writes a `@__NO_SIDE_EFFECTS__` annotation after the documentation
147        // comment it stood before. Looking only forward gave up there, and
148        // zod's `v4/core/api.ts` could not be instrumented at all. A comment
149        // the forward scan stepped over is still waiting behind it.
150        let index = match (original_index..program.comments.len()).find(|index| same(*index)) {
151            Some(index) => {
152                original_index = index + 1;
153                index
154            }
155            None => match (0..original_index).find(|index| !matched[*index] && same(*index)) {
156                Some(index) => index,
157                // The counts are usually equal here, so reporting them alone
158                // says the opposite of what happened: nothing was dropped, one
159                // comment was not recognised. Name it.
160                None => {
161                    return Err(CandidateError::CommentPreservation {
162                        expected: program.comments.len(),
163                        actual: reparsed.program.comments.len(),
164                        detail: format!(
165                            "no source comment matches the generated comment {emitted_text:?}                          (matched {} before it)",
166                            edits.len()
167                        ),
168                    });
169                }
170            },
171        };
172        let original = &program.comments[index];
173        matched[index] = true;
174        edits.push((
175            emitted.span.start as usize,
176            emitted.span.end as usize,
177            original.span.source_text(program.source_text).to_string(),
178        ));
179    }
180
181    let source_lines = Utf16LineIndex::new(program.source_text);
182    let generated_lines = Utf16LineIndex::new(generated);
183    let mut mappings = map
184        .get_tokens()
185        .filter_map(|token| {
186            token.get_source_id()?;
187            Some((
188                source_lines
189                    .byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
190                generated_lines
191                    .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
192            ))
193        })
194        .collect::<Vec<_>>();
195    mappings.sort_unstable_by_key(|(source, destination)| (*source, *destination));
196    let statements = statement_spans(&reparsed.program);
197    for (index, original) in program.comments.iter().enumerate() {
198        if matched[index] {
199            continue;
200        }
201        let anchor = if original.attached_to > 0 {
202            original.attached_to as usize
203        } else {
204            original.span.end as usize
205        };
206        let mapping_index = mappings.partition_point(|(source, _)| *source < anchor);
207        let mapped = mappings
208            .get(mapping_index)
209            .map_or(generated.len(), |(_, destination)| *destination);
210        let (destination, text) = place_restored_comment(
211            generated,
212            &statements,
213            mapped,
214            original,
215            original.span.source_text(program.source_text),
216        );
217        edits.push((destination, destination, text));
218    }
219    edits.sort_by_key(|(start, end, _)| (*start, *end));
220    let restored_len = edits
221        .iter()
222        .fold(generated.len(), |length, (start, end, text)| {
223            length + text.len() - (end - start)
224        });
225    let mut restored = String::with_capacity(restored_len);
226    let mut cursor = 0;
227    for (start, end, replacement) in &edits {
228        if *start < cursor {
229            return Err(CandidateError::CommentPreservation {
230                expected: program.comments.len(),
231                actual: reparsed.program.comments.len(),
232                detail: format!(
233                    "restored comments overlap: {replacement:?} would be placed at byte {start},                      behind byte {cursor} already written"
234                ),
235            });
236        }
237        restored.push_str(&generated[cursor..*start]);
238        restored.push_str(replacement);
239        cursor = *end;
240    }
241    restored.push_str(&generated[cursor..]);
242    let map = shift_source_map(map, generated, &restored, &edits);
243    Ok((restored, map))
244}
245
246/// Every statement span in a program, so a restored comment can be anchored
247/// to a statement boundary.
248fn statement_spans(program: &Program<'_>) -> Vec<Span> {
249    struct Spans(Vec<Span>);
250    impl<'a> Visit<'a> for Spans {
251        fn visit_statement(&mut self, statement: &Statement<'a>) {
252            self.0.push(statement.span());
253            walk::walk_statement(self, statement);
254        }
255    }
256    let mut spans = Spans(Vec::new());
257    spans.visit_program(program);
258    spans.0
259}
260
261/// Where a comment the generator dropped goes back in, and with what
262/// whitespace around it.
263///
264/// The mapped position is only a hint: it is where the node the comment was
265/// attached to landed, and that can be mid-line. A comment that carries a
266/// line break must not be dropped there. After `return`, `throw`, `break`,
267/// `continue` or `yield` the break lets automatic semicolon insertion end the
268/// statement early -- `return` + newline + JSX parsed as `return;` with the
269/// JSX unreachable, and the bundler then removed the whole tree, so a React
270/// component rendered nothing under measurement while passing plainly. A `//`
271/// comment inserted before more code on the same line comments that code out.
272/// Such a comment moves to the start of the innermost statement containing
273/// the position, where a line break is always inert. A single-line block
274/// comment is inert wherever it sits, so mid-line it stays inline.
275fn place_restored_comment(
276    generated: &str,
277    statements: &[Span],
278    mapped: usize,
279    comment: &Comment,
280    text: &str,
281) -> (usize, String) {
282    let line_prefix = |offset: usize| generated[..offset].rsplit('\n').next().unwrap_or("");
283    let at_line_start = line_prefix(mapped)
284        .chars()
285        .all(|character| character == ' ' || character == '\t');
286    if at_line_start {
287        let mut placed = String::new();
288        placed.push(if comment.preceded_by_newline() {
289            '\n'
290        } else {
291            ' '
292        });
293        placed.push_str(text);
294        placed.push(if comment.is_line() || comment.followed_by_newline() {
295            '\n'
296        } else {
297            ' '
298        });
299        return (mapped, placed);
300    }
301    if !comment.is_line() && !text.contains('\n') {
302        let leading = if generated[..mapped].ends_with([' ', '\t']) {
303            ""
304        } else {
305            " "
306        };
307        return (mapped, format!("{leading}{text} "));
308    }
309    let Some(statement) = statements
310        .iter()
311        .filter(|span| span.start as usize <= mapped && mapped < span.end as usize)
312        .max_by_key(|span| span.start)
313    else {
314        return (mapped, format!("\n{text}\n"));
315    };
316    let start = statement.start as usize;
317    let indentation: String = line_prefix(start)
318        .chars()
319        .take_while(|character| *character == ' ' || *character == '\t')
320        .collect();
321    (start, format!("{text}\n{indentation}"))
322}
323
324/// Whether two comments are the same comment, ignoring how they were laid out
325/// and the one escape a JavaScript code generator introduces into them.
326///
327/// A comment has no escape semantics -- its bytes are its text -- but codegen
328/// still rewrites `</script` to `<\/script` inside one, so the output can sit
329/// inside an HTML `<script>` element without closing it early. Measured
330/// against oxc: that sequence and no other. `</div`, `</style`, `<!--`, `-->`
331/// and a bare `/` are all emitted unchanged, as is a `<\/` the author wrote.
332///
333/// Comparing the two spellings as different text cost the whole file: the
334/// matcher below walks originals in order, so one comment it cannot recognise
335/// consumes every remaining original and fails the run.
336fn equal_ignoring_whitespace(left: &str, right: &str) -> bool {
337    fn significant(text: &str) -> impl Iterator<Item = char> + '_ {
338        let mut rest = text.char_indices();
339        std::iter::from_fn(move || {
340            loop {
341                let (index, character) = rest.next()?;
342                // Normalise both sides, so a `<\/` the author wrote still matches
343                // itself and only the codegen artefact is collapsed.
344                if character == '\\'
345                    && text[index + 1..].starts_with('/')
346                    && text[..index].ends_with('<')
347                {
348                    continue;
349                }
350                if !character.is_whitespace() {
351                    return Some(character);
352                }
353            }
354        })
355    }
356    significant(left).eq(significant(right))
357}
358
359/// Line starts, and for each line how to turn a UTF-16 column into bytes
360/// without walking the line from its start.
361///
362/// Every source-map token is converted, and walking the line made a file with
363/// long lines cost the product of its tokens and its line length: recording
364/// Actual stopped for more than half an hour inside one such file. An ASCII
365/// line's column is its byte offset; a line that is not keeps a checkpoint
366/// every `CHECKPOINT` UTF-16 units, so a lookup walks at most that far.
367struct Utf16LineIndex<'s> {
368    source: &'s str,
369    starts: Vec<usize>,
370    /// Per line: `None` when ASCII, otherwise `(utf16 column, byte offset)`
371    /// checkpoints from the line's start, the first being `(0, 0)`.
372    checkpoints: Vec<Option<Vec<(usize, usize)>>>,
373}
374
375const CHECKPOINT: usize = 256;
376
377impl<'s> Utf16LineIndex<'s> {
378    fn new(source: &'s str) -> Self {
379        let mut starts = Vec::with_capacity(source.lines().count() + 1);
380        starts.push(0);
381        starts.extend(
382            source
383                .char_indices()
384                .filter_map(|(offset, character)| (character == '\n').then_some(offset + 1)),
385        );
386        let checkpoints = (0..starts.len())
387            .map(|line| {
388                let end = starts.get(line + 1).copied().unwrap_or(source.len());
389                let text = &source[starts[line]..end];
390                if text.is_ascii() {
391                    return None;
392                }
393                let mut marks = vec![(0, 0)];
394                let mut column = 0;
395                for (offset, character) in text.char_indices() {
396                    if column >= marks.last().expect("a first mark").0 + CHECKPOINT {
397                        marks.push((column, offset));
398                    }
399                    column += character.len_utf16();
400                }
401                Some(marks)
402            })
403            .collect();
404        Self {
405            source,
406            starts,
407            checkpoints,
408        }
409    }
410
411    fn line_end(&self, line: usize) -> usize {
412        self.starts
413            .get(line + 1)
414            .copied()
415            .unwrap_or(self.source.len())
416    }
417
418    fn byte_offset(&self, target_line: usize, target_utf16_col: usize) -> usize {
419        let Some(&start) = self.starts.get(target_line) else {
420            return self.source.len();
421        };
422        let end = self.line_end(target_line);
423        match &self.checkpoints[target_line] {
424            None => start + target_utf16_col.min(end - start),
425            Some(marks) => {
426                let at = marks.partition_point(|(column, _)| *column <= target_utf16_col) - 1;
427                let (column, offset) = marks[at];
428                start
429                    + offset
430                    + utf16_col_to_byte(
431                        &self.source[start + offset..end],
432                        target_utf16_col - column,
433                    )
434            }
435        }
436    }
437
438    fn line_col(&self, byte_offset: usize) -> (u32, u32) {
439        let byte_offset = byte_offset.min(self.source.len());
440        let line = self.starts.partition_point(|start| *start <= byte_offset) - 1;
441        let start = self.starts[line];
442        let column = match &self.checkpoints[line] {
443            None => byte_offset - start,
444            Some(marks) => {
445                let at = marks.partition_point(|(_, offset)| *offset <= byte_offset - start) - 1;
446                let (column, offset) = marks[at];
447                column
448                    + self.source[start + offset..byte_offset]
449                        .chars()
450                        .map(char::len_utf16)
451                        .sum::<usize>()
452            }
453        };
454        (line as u32, column as u32)
455    }
456}
457
458fn utf16_col_to_byte(line: &str, target_utf16_col: usize) -> usize {
459    let mut column = 0;
460    for (offset, character) in line.char_indices() {
461        if column >= target_utf16_col {
462            return offset;
463        }
464        column += character.len_utf16();
465    }
466    line.len()
467}
468
469fn shift_source_map(
470    map: oxc_sourcemap::SourceMap,
471    generated: &str,
472    restored: &str,
473    edits: &[(usize, usize, String)],
474) -> oxc_sourcemap::SourceMap {
475    let generated_lines = Utf16LineIndex::new(generated);
476    let restored_lines = Utf16LineIndex::new(restored);
477    let mut edit_index = 0;
478    let mut shift = 0isize;
479    let tokens = map
480        .get_tokens()
481        .map(|token| {
482            let original_offset = generated_lines
483                .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
484            while let Some((start, end, replacement)) = edits.get(edit_index) {
485                if *end > original_offset {
486                    break;
487                }
488                shift += replacement.len() as isize - (*end - *start) as isize;
489                edit_index += 1;
490            }
491            let shifted_offset = edits
492                .get(edit_index)
493                .filter(|(start, end, _)| *start <= original_offset && original_offset < *end)
494                .map_or_else(
495                    || original_offset.saturating_add_signed(shift),
496                    |(start, _, _)| start.saturating_add_signed(shift),
497                );
498            let (dst_line, dst_col) = restored_lines.line_col(shifted_offset);
499            oxc_sourcemap::Token::new(
500                dst_line,
501                dst_col,
502                token.get_src_line(),
503                token.get_src_col(),
504                token.get_source_id(),
505                token.get_name_id(),
506            )
507        })
508        .collect::<Vec<_>>();
509    oxc_sourcemap::SourceMap::new(
510        map.get_file().cloned(),
511        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
512        map.get_source_root().map(str::to_string),
513        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
514        map.get_source_contents()
515            .map(|content| content.cloned())
516            .collect::<Vec<Option<Arc<str>>>>(),
517        tokens.into_boxed_slice(),
518        None,
519    )
520}
521
522/// Puts back, as written, every function whose source text the program reads.
523///
524/// Such a function is left without probes, but the whole file is printed
525/// again from its tree, and the printer lays a function out its own way:
526/// `function f() { return 1; }` came back as `function f() {\n\treturn 1;\n}`,
527/// so `f.toString()` differed under measurement. A function of the output is
528/// matched to its source by the first position inside it the source map says
529/// was printed from somewhere (an arrow's is a parameter, not its start).
530/// Inside a restored function each line maps to its own line of the source.
531fn restore_function_source(
532    program: &Program<'_>,
533    sensitive: &HashSet<SpanKey>,
534    generated: &str,
535    map: oxc_sourcemap::SourceMap,
536) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
537    if sensitive.is_empty() {
538        return Ok((generated.to_string(), map));
539    }
540    let allocator = Allocator::default();
541    let reparsed = Parser::new(&allocator, generated, program.source_type).parse();
542    if !reparsed.errors.is_empty() {
543        return Err(CandidateError::Parse(
544            reparsed
545                .errors
546                .into_iter()
547                .map(|error| error.to_string())
548                .collect(),
549        ));
550    }
551    struct Functions(Vec<Span>);
552    impl<'a> Visit<'a> for Functions {
553        fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
554            self.0.push(function.span);
555            walk::walk_function(self, function, flags);
556        }
557        fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) {
558            self.0.push(arrow.span);
559            walk::walk_arrow_function_expression(self, arrow);
560        }
561    }
562    let mut functions = Functions(Vec::new());
563    functions.visit_program(&reparsed.program);
564    functions.0.sort_by_key(|span| span.start);
565
566    let source_lines = Utf16LineIndex::new(program.source_text);
567    let generated_lines = Utf16LineIndex::new(generated);
568    let mut printed_from = map
569        .get_tokens()
570        .filter_map(|token| {
571            token.get_source_id()?;
572            Some((
573                generated_lines
574                    .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
575                source_lines
576                    .byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
577            ))
578        })
579        .collect::<Vec<_>>();
580    printed_from.sort_unstable();
581    let mut used = HashSet::new();
582    // (generated start, generated end, source start, source end)
583    let mut edits = Vec::<(usize, usize, usize, usize)>::new();
584    for span in &functions.0 {
585        let (start, end) = (span.start as usize, span.end as usize);
586        if edits.last().is_some_and(|edit| start < edit.1) {
587            continue;
588        }
589        let first = printed_from.partition_point(|(generated, _)| *generated < start);
590        let Some(&(_, source)) = printed_from
591            .get(first)
592            .filter(|(generated, _)| *generated < end)
593        else {
594            continue;
595        };
596        // The innermost source-read function holding that position.
597        let Some(&(source_start, source_end)) = sensitive
598            .iter()
599            .filter(|(from, to)| *from as usize <= source && source < *to as usize)
600            .min_by_key(|(from, to)| to - from)
601        else {
602            continue;
603        };
604        if !used.insert((source_start, source_end)) {
605            continue;
606        }
607        edits.push((start, end, source_start as usize, source_end as usize));
608    }
609    if edits.is_empty() {
610        return Ok((generated.to_string(), map));
611    }
612
613    let mut restored = String::with_capacity(generated.len());
614    let mut cursor = 0;
615    // Where each edit landed in the restored text.
616    let mut placed = Vec::with_capacity(edits.len());
617    for (start, end, source_start, source_end) in &edits {
618        restored.push_str(&generated[cursor..*start]);
619        placed.push(restored.len());
620        restored.push_str(&program.source_text[*source_start..*source_end]);
621        cursor = *end;
622    }
623    restored.push_str(&generated[cursor..]);
624
625    let restored_lines = Utf16LineIndex::new(&restored);
626    let shifted = |offset: usize| {
627        let before = edits.partition_point(|edit| edit.1 <= offset);
628        let shift = edits[..before]
629            .iter()
630            .map(|(start, end, source_start, source_end)| {
631                (source_end - source_start) as isize - (end - start) as isize
632            })
633            .sum::<isize>();
634        offset.saturating_add_signed(shift)
635    };
636    let mut tokens = Vec::new();
637    for token in map.get_tokens() {
638        let offset = generated_lines
639            .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
640        // What the printer wrote inside a restored function is gone.
641        if edits
642            .iter()
643            .any(|(start, end, _, _)| *start < offset && offset < *end)
644        {
645            continue;
646        }
647        let (dst_line, dst_col) = restored_lines.line_col(shifted(offset));
648        tokens.push(oxc_sourcemap::Token::new(
649            dst_line,
650            dst_col,
651            token.get_src_line(),
652            token.get_src_col(),
653            token.get_source_id(),
654            token.get_name_id(),
655        ));
656    }
657    let source_id = map.get_tokens().find_map(|token| token.get_source_id());
658    for ((_, _, source_start, source_end), at) in edits.iter().zip(&placed) {
659        let text = &program.source_text[*source_start..*source_end];
660        let mut line_start = 0;
661        for line in text.split_inclusive('\n') {
662            if line_start > 0 {
663                let indent = line.len() - line.trim_start_matches([' ', '\t']).len();
664                let (dst_line, dst_col) = restored_lines.line_col(at + line_start + indent);
665                let (src_line, src_col) = source_lines.line_col(source_start + line_start + indent);
666                tokens.push(oxc_sourcemap::Token::new(
667                    dst_line, dst_col, src_line, src_col, source_id, None,
668                ));
669            }
670            line_start += line.len();
671        }
672    }
673    tokens.sort_by_key(|token| (token.get_dst_line(), token.get_dst_col()));
674    let map = oxc_sourcemap::SourceMap::new(
675        map.get_file().cloned(),
676        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
677        map.get_source_root().map(str::to_string),
678        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
679        map.get_source_contents()
680            .map(|content| content.cloned())
681            .collect::<Vec<Option<Arc<str>>>>(),
682        tokens.into_boxed_slice(),
683        None,
684    );
685    Ok((restored, map))
686}
687
688/// Where a statement Supercov generated inside a function or block starts, in
689/// the source map: past the source's end, where no node of the program starts
690/// and no comment is attached (a comment ending the file is attached to its
691/// length, and oxc prints a comment where its position is printed), rewritten
692/// to an unmapped segment once the code is out. `None` when the source's last
693/// line has non-ASCII text, where oxc's column table has no entry that far;
694/// those files keep the generated statements unmapped-by-omission, as before.
695fn generated_statement_span(source: &str) -> Option<Span> {
696    let last_line = source.rfind('\n').map_or(0, |newline| newline + 1);
697    let length = u32::try_from(source.len()).ok()?;
698    (source[last_line..].is_ascii() && length < u32::MAX - 2)
699        .then(|| Span::new(length + 1, length + 2))
700}
701
702/// A coverage tool the tests run itself (c8, nyc, Node's `--test-coverage`,
703/// Jest, Vitest) reads the instrumented copy through its source map. oxc maps
704/// a node only when its start differs from the last one it mapped, and gives a
705/// generated node none, so a probe inherited the mapping before it: V8 counted
706/// a probe's `||` as a branch of the user's line, and the statement after a
707/// block Supercov added (whose brace took the statement's own start) lost its
708/// mapping, and with it its place in istanbul's report. Each such statement
709/// now starts at `generated_statement_span`, which this makes unmapped: the
710/// probe maps to nothing, and the user's statement after it maps again.
711///
712/// A block Supercov wraps around a statement takes that statement's span, so
713/// its closing brace maps to the statement's last character, where the
714/// statement's own last token or an inner block's brace often already mapped:
715/// the brace then went unmapped, and the enclosing `if` or function, ending
716/// there, left istanbul's report. An empty statement at the unmapped span
717/// before the brace lets it map again.
718struct GeneratedStatementSpans<'a, 's> {
719    ast: AstBuilder<'a>,
720    source: &'s str,
721    span: Span,
722    depth: usize,
723}
724
725impl GeneratedStatementSpans<'_, '_> {
726    /// A block or body Supercov made around the user's code, rather than one
727    /// the user wrote: its span starts somewhere other than a `{`.
728    fn borrowed(&self, span: Span) -> bool {
729        !span.is_empty()
730            && span != self.span
731            && self.source.as_bytes().get(span.start as usize) != Some(&b'{')
732    }
733}
734
735impl<'a> VisitMut<'a> for GeneratedStatementSpans<'a, '_> {
736    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
737        // Top-level ones stay unmapped by omission: the file's first mapping
738        // is then the user's, which Node's coverage takes for a range starting
739        // at the top of the file.
740        if self.depth > 0 && statement.span().is_empty() {
741            *statement.span_mut() = self.span;
742        }
743        self.depth += 1;
744        walk_mut::walk_statement(self, statement);
745        self.depth -= 1;
746    }
747
748    fn visit_block_statement(&mut self, block: &mut oxc_ast::ast::BlockStatement<'a>) {
749        if self.depth > 0 && block.span.is_empty() {
750            block.span = self.span;
751        } else if self.borrowed(block.span)
752            // Not after a jump: `allowUnreachableCode: false` would reject it
753            // in a TypeScript build of the instrumented copy.
754            && !matches!(
755                block.body.last(),
756                Some(
757                    Statement::ReturnStatement(_)
758                        | Statement::ThrowStatement(_)
759                        | Statement::BreakStatement(_)
760                        | Statement::ContinueStatement(_)
761                )
762            )
763        {
764            block.body.push(self.ast.statement_empty(self.span));
765        }
766        walk_mut::walk_block_statement(self, block);
767    }
768
769    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
770        // An arrow's expression body made a block: its brace maps just past
771        // the expression, where nothing inside it mapped.
772        if self.borrowed(body.span) && (body.span.end as usize) < self.source.len() {
773            body.span.end += 1;
774        }
775        self.depth += 1;
776        walk_mut::walk_function_body(self, body);
777        self.depth -= 1;
778    }
779}
780
781/// Node's own coverage looks a range's start up on the wrong line half the
782/// time when the range starts a line: with a source map, a line of the
783/// generated file ends, for it, where the next one starts, and its search
784/// takes whichever of the two it reaches first, which the length of the file
785/// decides. From the line before, it reads one column past that line's end,
786/// which gives the mapping before it: a top-level function was reported a
787/// line early, or lost its range and with it a branch, where the same file a
788/// line longer or shorter was reported right.
789///
790/// Only a function or class declaration at the top level starts a range at
791/// column 0. Each gets an empty statement on the line before it, which
792/// `map_closing_lines` maps to where the declaration starts: both lines then
793/// give the same answer, and nothing else is ever looked up on a line that
794/// holds only `;`.
795fn separate_top_level_declarations<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>, span: Span) {
796    let starts_a_range = |statement: &Statement<'a>| match statement {
797        Statement::FunctionDeclaration(function) => !function.declare,
798        Statement::ClassDeclaration(class) => !class.declare,
799        _ => false,
800    };
801    // Overload signatures stay next to their implementation.
802    let signature = |statement: &Statement<'a>| matches!(statement, Statement::FunctionDeclaration(function) if function.body.is_none());
803    let mut separated = ast.vec_with_capacity(program.body.len());
804    let mut after_signature = false;
805    for statement in program.body.take_in(ast.allocator) {
806        if starts_a_range(&statement) && !after_signature {
807            separated.push(ast.statement_empty(span));
808        }
809        after_signature = signature(&statement);
810        separated.push(statement);
811    }
812    program.body = separated;
813}
814
815fn mark_generated_statements<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>) {
816    let Some(span) = generated_statement_span(program.source_text) else {
817        return;
818    };
819    separate_top_level_declarations(ast, program, span);
820    GeneratedStatementSpans {
821        ast,
822        source: program.source_text,
823        span,
824        depth: 0,
825    }
826    .visit_program(program);
827}
828
829fn unmap_generated_statements(
830    map: oxc_sourcemap::SourceMap,
831    source: &str,
832) -> oxc_sourcemap::SourceMap {
833    let Some(span) = generated_statement_span(source) else {
834        return map;
835    };
836    let line = source.matches('\n').count() as u32;
837    let column = span.start - source.rfind('\n').map_or(0, |newline| newline as u32 + 1);
838    let is_generated = |token: &oxc_sourcemap::Token| {
839        token.get_src_line() == line && token.get_src_col() == column
840    };
841    if !map.get_tokens().any(|token| is_generated(&token)) {
842        return map;
843    }
844    let tokens = map
845        .get_tokens()
846        .map(|token| {
847            if is_generated(&token) {
848                oxc_sourcemap::Token::new(
849                    token.get_dst_line(),
850                    token.get_dst_col(),
851                    0,
852                    0,
853                    None,
854                    None,
855                )
856            } else {
857                token
858            }
859        })
860        .collect::<Vec<_>>();
861    oxc_sourcemap::SourceMap::new(
862        map.get_file().cloned(),
863        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
864        map.get_source_root().map(str::to_string),
865        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
866        map.get_source_contents()
867            .map(|content| content.cloned())
868            .collect::<Vec<Option<Arc<str>>>>(),
869        tokens.into_boxed_slice(),
870        None,
871    )
872}
873
874/// A line of the instrumented copy starting with a closing brace Supercov
875/// added (a wrapper's `try` block, `} finally {`) has no mapping at that
876/// brace, and istanbul, c8 and v8-to-istanbul look a position up on its own
877/// line only: a V8 range or a statement ending there was dropped from their
878/// reports. Node's own coverage takes the nearest mapping before it instead;
879/// so does each such brace now, with a copy of that mapping, unmapped if it
880/// was.
881///
882/// oxc maps where a node starts, never a statement's closing `;`, so a range
883/// ending with `break;` ended, for Node's own coverage, at the `b`: the line
884/// was no longer inside the range of the `case` no test reached, and read as
885/// covered. When the text after a line's last mapping is the source's own
886/// text after that position, ending in `;`, the `;` maps to the source's.
887fn map_closing_lines(
888    map: oxc_sourcemap::SourceMap,
889    code: &str,
890    source: &str,
891) -> oxc_sourcemap::SourceMap {
892    let source_lines = source.split('\n').collect::<Vec<_>>();
893    let tokens = map.get_tokens().collect::<Vec<_>>();
894    let mut patched = Vec::with_capacity(tokens.len());
895    let mut next = 0;
896    let mut previous: Option<oxc_sourcemap::Token> = None;
897    let mut added = false;
898    for (line, text) in code.split('\n').enumerate() {
899        let line = line as u32;
900        let first = next;
901        while next < tokens.len() && tokens[next].get_dst_line() == line {
902            next += 1;
903        }
904        // The line `separate_top_level_declarations` put before a
905        // declaration maps to where that declaration starts.
906        if text == ";"
907            && next == first + 1
908            && tokens[first].get_source_id().is_none()
909            && let Some(declaration) = tokens.get(next)
910            && declaration.get_dst_col() == 0
911            && declaration.get_source_id().is_some()
912        {
913            let separator = oxc_sourcemap::Token::new(
914                line,
915                0,
916                declaration.get_src_line(),
917                declaration.get_src_col(),
918                declaration.get_source_id(),
919                None,
920            );
921            patched.push(separator);
922            previous = Some(separator);
923            added = true;
924            continue;
925        }
926        let column = (text.len() - text.trim_start().len()) as u32;
927        if text.trim_start().starts_with(['}', ')', ']'])
928            && (first == next || tokens[first].get_dst_col() > column)
929            && let Some(previous) = &previous
930        {
931            patched.push(oxc_sourcemap::Token::new(
932                line,
933                column,
934                previous.get_src_line(),
935                previous.get_src_col(),
936                previous.get_source_id(),
937                None,
938            ));
939            added = true;
940        }
941        patched.extend_from_slice(&tokens[first..next]);
942        if next > first {
943            let last = tokens[next - 1];
944            previous = Some(last);
945            if let (Some(_), Some(original)) = (
946                last.get_source_id(),
947                source_lines.get(last.get_src_line() as usize),
948            ) && text.is_ascii()
949                && original.is_ascii()
950            {
951                let generated_rest = text
952                    .get(last.get_dst_col() as usize..)
953                    .unwrap_or("")
954                    .trim_end();
955                let original_rest = original
956                    .get(last.get_src_col() as usize..)
957                    .unwrap_or("")
958                    .trim_end();
959                if generated_rest.len() > 1
960                    && generated_rest.ends_with(';')
961                    && original_rest.starts_with(generated_rest)
962                {
963                    let offset = (generated_rest.len() - 1) as u32;
964                    let end = oxc_sourcemap::Token::new(
965                        line,
966                        last.get_dst_col() + offset,
967                        last.get_src_line(),
968                        last.get_src_col() + offset,
969                        last.get_source_id(),
970                        None,
971                    );
972                    patched.push(end);
973                    previous = Some(end);
974                    added = true;
975                }
976            }
977        }
978    }
979    patched.extend_from_slice(&tokens[next..]);
980    if !added {
981        return map;
982    }
983    oxc_sourcemap::SourceMap::new(
984        map.get_file().cloned(),
985        map.get_names().cloned().collect::<Vec<Arc<str>>>(),
986        map.get_source_root().map(str::to_string),
987        map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
988        map.get_source_contents()
989            .map(|content| content.cloned())
990            .collect::<Vec<Option<Arc<str>>>>(),
991        patched.into_boxed_slice(),
992        None,
993    )
994}
995
996fn generate_candidate(
997    program: &Program<'_>,
998    file: &str,
999    source_sensitive_functions: &HashSet<SpanKey>,
1000) -> Result<(String, Option<serde_json::Value>), CandidateError> {
1001    let options = CodegenOptions {
1002        source_map_path: Some(Path::new(file).to_path_buf()),
1003        ..CodegenOptions::default()
1004    };
1005    let generated = Codegen::new().with_options(options).build(program);
1006    // Unmapped before comments are restored: a restored comment goes where
1007    // the first mapping at or after it in the source was printed, and the
1008    // generated statements' placeholder is past every comment.
1009    let map = unmap_generated_statements(
1010        generated.map.expect("source maps are enabled"),
1011        program.source_text,
1012    );
1013    let (code, map) = restore_comment_text(program, &generated.code, map)?;
1014    let (code, map) = restore_function_source(program, source_sensitive_functions, &code, map)?;
1015    let map = map_closing_lines(map, &code, program.source_text);
1016    let map = Some({
1017        serde_json::from_str(&map.to_json_string())
1018            .expect("oxc must serialize its own generated source map")
1019    });
1020    Ok((code, map))
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct CandidateRuntime {
1026    pub coverage_hit: String,
1027    pub mcdc_begin: String,
1028    pub mcdc_condition: String,
1029    pub mcdc_end: String,
1030    pub register_probe_v2: String,
1031    pub mcdc_end_v2: String,
1032    pub coverage_hit_v2: String,
1033    pub probe_file_v2: String,
1034    pub parenthesized_assignment_value: String,
1035    pub with_request_phase: String,
1036    pub optional_select_v2: String,
1037    pub select_short_v2: String,
1038    pub select_right_v2: String,
1039    pub select_named_right_v2: String,
1040    pub select_assign_end_v2: String,
1041    pub select_path_v2: String,
1042    pub select_end2_v2: String,
1043    pub select_end_v2: String,
1044    pub select_next_v2: String,
1045    pub rendered_value_v2: String,
1046    pub optional_call_end_v2: String,
1047    pub default_selected_v2: String,
1048    pub default_entered_v2: String,
1049}
1050
1051#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1052#[serde(rename_all = "camelCase")]
1053pub struct CandidateLimitation {
1054    pub id: String,
1055    pub kind: String,
1056    pub file: String,
1057    pub line: usize,
1058    pub column: usize,
1059    pub source: String,
1060    pub reason: String,
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Eq)]
1064pub enum CandidateError {
1065    UnknownSourceType(String),
1066    Parse(Vec<String>),
1067    CommentPreservation {
1068        expected: usize,
1069        actual: usize,
1070        detail: String,
1071    },
1072}
1073
1074impl std::fmt::Display for CandidateError {
1075    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1076        match self {
1077            Self::UnknownSourceType(file) => {
1078                write!(formatter, "no JavaScript source type for {file}")
1079            }
1080            Self::Parse(messages) => write!(formatter, "{}", messages.join("; ")),
1081            Self::CommentPreservation {
1082                expected,
1083                actual,
1084                detail,
1085            } => write!(
1086                formatter,
1087                "{expected} comments in the source, {actual} after instrumenting: {detail}"
1088            ),
1089        }
1090    }
1091}
1092
1093/// A parser error as a person reads it: the message, and where.
1094fn parse_message(source: &str, message: &str, offset: Option<usize>) -> String {
1095    match offset.filter(|offset| *offset <= source.len()) {
1096        Some(offset) => {
1097            let (line, column) = line_and_utf16_column(source, offset);
1098            format!("{message} at line {line}, column {}", column + 1)
1099        }
1100        None => message.to_owned(),
1101    }
1102}
1103
1104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1105enum RuntimeBinding {
1106    ModuleImport,
1107    DirectGlobal,
1108}
1109
1110const NODE_ASSERT_MODULES: &[&str] = &[
1111    "assert",
1112    "assert/strict",
1113    "node:assert",
1114    "node:assert/strict",
1115];
1116const NODE_ASSERT_METHODS: &[&str] = &[
1117    "deepEqual",
1118    "deepStrictEqual",
1119    "doesNotMatch",
1120    "doesNotReject",
1121    "doesNotThrow",
1122    "equal",
1123    "fail",
1124    "ifError",
1125    "match",
1126    "notDeepEqual",
1127    "notDeepStrictEqual",
1128    "notEqual",
1129    "notStrictEqual",
1130    "ok",
1131    "partialDeepStrictEqual",
1132    "rejects",
1133    "strictEqual",
1134    "throws",
1135];
1136
1137#[derive(Debug, Clone, PartialEq, Eq)]
1138pub struct NodeAssertionInstrumentation {
1139    pub code: String,
1140    pub assertions: usize,
1141    pub capability_imports: usize,
1142}
1143
1144const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
1145    &["@jest/globals", "@playwright/test", "playwright", "vitest"];
1146
1147fn capability_source_candidate(source: &str) -> bool {
1148    // A capability wrapper is useful only when an argument can establish a
1149    // host/guest workspace mapping. Generic words such as `snapshot`,
1150    // `machine`, `acquire` and `spawn` are common in tests and application
1151    // code; treating any of them as sufficient wrapped unrelated framework
1152    // registration functions and changed their captured callsite.
1153    let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
1154        || (source.contains("hostRoot") && source.contains("guestRoot"));
1155    // The guest path is commonly computed (for example
1156    // `resolve(tmpdir(), "workspace")`), so requiring the literal
1157    // `/workspace` loses genuine opaque launchers. The nested mount shape is
1158    // already specific, and the AST pass below still restricts wrapping to
1159    // the imported root actually called with that argument.
1160    let mount_mapping = (source.contains("mounts") || source.contains("Mounts"))
1161        && source.contains("source")
1162        && source.contains("target");
1163    direct_mapping || mount_mapping
1164}
1165
1166fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
1167    source == wrapper
1168        || source.starts_with("node:")
1169        || source.starts_with("virtual:supercov-")
1170        || source.contains(".supercov/")
1171        || CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
1172}
1173
1174fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
1175    match expression {
1176        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1177        Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
1178        Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
1179        Expression::CallExpression(call) => capability_callee_root(&call.callee),
1180        // `new GenericContainer(..).withBindMounts(..)`: the builder's class.
1181        Expression::NewExpression(construction) => capability_callee_root(&construction.callee),
1182        Expression::ParenthesizedExpression(parenthesized) => {
1183            capability_callee_root(&parenthesized.expression)
1184        }
1185        Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
1186        Expression::TSSatisfiesExpression(expression) => {
1187            capability_callee_root(&expression.expression)
1188        }
1189        Expression::TSNonNullExpression(expression) => {
1190            capability_callee_root(&expression.expression)
1191        }
1192        Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
1193        _ => None,
1194    }
1195}
1196
1197struct CapabilityCallCollector<'s> {
1198    source: &'s str,
1199    mapping_variables: HashSet<String>,
1200    roots: HashSet<String>,
1201}
1202
1203impl CapabilityCallCollector<'_> {
1204    fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
1205        if let Argument::Identifier(identifier) = argument
1206            && self.mapping_variables.contains(identifier.name.as_str())
1207        {
1208            return true;
1209        }
1210        capability_source_candidate(source_slice(self.source, argument.span()))
1211    }
1212}
1213
1214impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
1215    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
1216        if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
1217            (&declarator.id, &declarator.init)
1218            && capability_source_candidate(source_slice(self.source, initializer.span()))
1219        {
1220            self.mapping_variables.insert(identifier.name.to_string());
1221        }
1222        walk::walk_variable_declarator(self, declarator);
1223    }
1224
1225    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1226        // A builder method named for mounts (`withBindMounts`) takes the
1227        // mounts themselves, `[{ source, target }]`, with no `mounts` key.
1228        let mounting = matches!(&call.callee, Expression::StaticMemberExpression(member)
1229            if member.property.name.to_ascii_lowercase().contains("mount"));
1230        let mounted = |argument: &Argument<'_>| {
1231            let text = source_slice(self.source, argument.span());
1232            mounting && text.contains("source") && text.contains("target")
1233        };
1234        if call
1235            .arguments
1236            .iter()
1237            .any(|argument| self.argument_has_mapping(argument) || mounted(argument))
1238            && let Some(root) = capability_callee_root(&call.callee)
1239        {
1240            self.roots.insert(root);
1241        }
1242        walk::walk_call_expression(self, call);
1243    }
1244}
1245
1246fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
1247    let mut collector = CapabilityCallCollector {
1248        source,
1249        mapping_variables: HashSet::new(),
1250        roots: HashSet::new(),
1251    };
1252    // Mapping variables must be known before calls are inspected because a
1253    // declaration may appear after a function that closes over it.
1254    collector.visit_program(program);
1255    let mapping_variables = collector.mapping_variables.clone();
1256    collector.roots.clear();
1257    collector.mapping_variables = mapping_variables;
1258    collector.visit_program(program);
1259    collector.roots
1260}
1261
1262/// One change to a test file's text: `text` replaces `start..end`, an empty
1263/// range for an insertion. Insertions at one offset are ordered by `rank`, so
1264/// a wrapper that encloses another opens before it and closes after it.
1265struct TextEdit {
1266    start: usize,
1267    end: usize,
1268    rank: i64,
1269    text: String,
1270}
1271
1272impl TextEdit {
1273    fn insert(at: usize, rank: i64, text: String) -> Self {
1274        Self {
1275            start: at,
1276            end: at,
1277            rank,
1278            text,
1279        }
1280    }
1281
1282    fn replace(start: usize, end: usize, text: String) -> Self {
1283        Self {
1284            start,
1285            end,
1286            rank: 0,
1287            text,
1288        }
1289    }
1290}
1291
1292/// Apply edits to a test file, keeping every line of it where it was.
1293///
1294/// A test file used to be reprinted from its syntax tree to add assertion
1295/// wrappers, which moved lines and reflowed literals: a `@ts-expect-error`
1296/// above `period: { period: 'fortnight', amount: 1 }` then sat above a line
1297/// with no error, and the error surfaced on the next one (TS2578, TS2322).
1298/// Edits never add or remove a line break, so a directive, a comment and a
1299/// stack-trace line number all stay where the author put them. The source map
1300/// maps every copied run of the original back to itself.
1301/// What a character is to a position in a line: part of a word, a mark of
1302/// its own, or the space between.
1303#[derive(Clone, Copy, PartialEq, Eq)]
1304enum TokenClass {
1305    Word,
1306    Mark,
1307    Space,
1308}
1309
1310impl TokenClass {
1311    fn of(unit: char) -> Self {
1312        if unit.is_whitespace() {
1313            Self::Space
1314        } else if unit.is_alphanumeric() || unit == '_' || unit == '$' {
1315            Self::Word
1316        } else {
1317            Self::Mark
1318        }
1319    }
1320}
1321
1322/// Whether a word can be the name of something: a compiler names a segment
1323/// for an identifier, never for a keyword or a number. Naming `await` printed
1324/// a test's own frame as `at Object.await`.
1325fn names_something(word: &str) -> bool {
1326    const KEYWORDS: &[&str] = &[
1327        "as",
1328        "async",
1329        "await",
1330        "break",
1331        "case",
1332        "catch",
1333        "class",
1334        "const",
1335        "continue",
1336        "debugger",
1337        "default",
1338        "delete",
1339        "do",
1340        "else",
1341        "enum",
1342        "export",
1343        "extends",
1344        "false",
1345        "finally",
1346        "for",
1347        "from",
1348        "function",
1349        "if",
1350        "implements",
1351        "import",
1352        "in",
1353        "instanceof",
1354        "interface",
1355        "let",
1356        "new",
1357        "null",
1358        "of",
1359        "package",
1360        "private",
1361        "protected",
1362        "public",
1363        "return",
1364        "static",
1365        "super",
1366        "switch",
1367        "this",
1368        "throw",
1369        "true",
1370        "try",
1371        "type",
1372        "typeof",
1373        "var",
1374        "void",
1375        "while",
1376        "with",
1377        "yield",
1378    ];
1379    !word.starts_with(|first: char| first.is_ascii_digit()) && !KEYWORDS.contains(&word)
1380}
1381
1382fn apply_text_edits(
1383    source: &str,
1384    mut edits: Vec<TextEdit>,
1385    file: &str,
1386) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
1387    edits.sort_by_key(|edit| (edit.start, edit.end > edit.start, edit.rank));
1388    let mut builder = oxc_sourcemap::SourceMapBuilder::default();
1389    let source_id = builder.set_source_and_content(file, source);
1390    let mut code = String::with_capacity(source.len() + edits.len() * 64);
1391    let (mut line, mut column) = (0u32, 0u32);
1392    let (mut source_line, mut source_column) = (0u32, 0u32);
1393    let advance = |text: &str, line: &mut u32, column: &mut u32| {
1394        for unit in text.chars() {
1395            if unit == '\n' {
1396                *line += 1;
1397                *column = 0;
1398            } else {
1399                *column += unit.len_utf16() as u32;
1400            }
1401        }
1402    };
1403    let copy = |from: usize,
1404                to: usize,
1405                code: &mut String,
1406                line: &mut u32,
1407                column: &mut u32,
1408                source_line: &mut u32,
1409                source_column: &mut u32,
1410                builder: &mut oxc_sourcemap::SourceMapBuilder| {
1411        for (index, piece) in source[from..to].split('\n').enumerate() {
1412            if index > 0 {
1413                code.push('\n');
1414                *line += 1;
1415                *column = 0;
1416                *source_line += 1;
1417                *source_column = 0;
1418            }
1419            if !piece.is_empty() {
1420                builder.add_token(
1421                    *line,
1422                    *column,
1423                    *source_line,
1424                    *source_column,
1425                    Some(source_id),
1426                    None,
1427                );
1428                // A position reads as its segment's start, so one mapping for
1429                // a line put every frame of a test file in column 1, and the
1430                // one inside a wrapped assertion at the statement's start:
1431                // Vitest printed `:12:7` where it prints `:12:35` without
1432                // Supercov. Each word and each mark starts a segment.
1433                // A word also carries its own text as the segment's name:
1434                // a compiler that reads this map keeps a name only where the
1435                // map it reads has one, and Jest names a frame by it. With
1436                // @swc/jest `at toBe (a.test.ts:12:35)` lost its `toBe`.
1437                let (mut offset, mut before) = (0u32, TokenClass::Space);
1438                for (index, unit) in piece.char_indices() {
1439                    let class = TokenClass::of(unit);
1440                    if offset > 0
1441                        && class != TokenClass::Space
1442                        && (class != before || class == TokenClass::Mark)
1443                    {
1444                        let name = (class == TokenClass::Word)
1445                            .then(|| {
1446                                let rest = &piece[index..];
1447                                let end = rest
1448                                    .find(|next| TokenClass::of(next) != TokenClass::Word)
1449                                    .unwrap_or(rest.len());
1450                                &rest[..end]
1451                            })
1452                            .filter(|word| names_something(word))
1453                            .map(|word| builder.add_name(word));
1454                        builder.add_token(
1455                            *line,
1456                            *column + offset,
1457                            *source_line,
1458                            *source_column + offset,
1459                            Some(source_id),
1460                            name,
1461                        );
1462                    }
1463                    before = class;
1464                    offset += unit.len_utf16() as u32;
1465                }
1466                code.push_str(piece);
1467                *column += offset;
1468                *source_column += offset;
1469            }
1470        }
1471    };
1472    let mut at = 0;
1473    for edit in edits {
1474        if edit.start < at || edit.end > source.len() || edit.start > edit.end {
1475            return Err(CandidateError::Parse(vec![format!(
1476                "overlapping assertion edits in {file}"
1477            )]));
1478        }
1479        copy(
1480            at,
1481            edit.start,
1482            &mut code,
1483            &mut line,
1484            &mut column,
1485            &mut source_line,
1486            &mut source_column,
1487            &mut builder,
1488        );
1489        let removed = &source[edit.start..edit.end];
1490        if edit.text.matches('\n').count() != removed.matches('\n').count() {
1491            return Err(CandidateError::Parse(vec![format!(
1492                "an assertion edit in {file} would move a line"
1493            )]));
1494        }
1495        code.push_str(&edit.text);
1496        advance(&edit.text, &mut line, &mut column);
1497        advance(removed, &mut source_line, &mut source_column);
1498        at = edit.end;
1499    }
1500    copy(
1501        at,
1502        source.len(),
1503        &mut code,
1504        &mut line,
1505        &mut column,
1506        &mut source_line,
1507        &mut source_column,
1508        &mut builder,
1509    );
1510    Ok((code, builder.into_sourcemap()))
1511}
1512
1513/// The runtime's assertion helpers as a TypeScript test sees them, declared
1514/// once at the end of each instrumented file.
1515///
1516/// Reading `globalThis.__SUPERCOV_DIRECT_RUNTIME__` without a declaration is
1517/// an implicit `any` index on `typeof globalThis`, which `tsc --strict` rejects
1518/// (TS7017) at every wrapped assertion; Actual's browser build failed on 66 of
1519/// them. A wrapped assertion keeps its own result type, and a bound matcher
1520/// keeps the method's own signature, overloads included, as `T[K]`: inferring
1521/// it through a conditional type made generic matchers `never` (TS2349,
1522/// TS2769). Every instrumented file declares the same type, which TypeScript
1523/// merges.
1524const ASSERTION_RUNTIME_TYPE: &str = "{ \
1525    withNodeAssertionPhase<T>(operation: string, source: string, callback: () => T): T; \
1526    openNodeAssertionPhase(operation: string, source: string): { close<T>(value: T): T; close(): void; fail(error: unknown): unknown }; \
1527    bindNodeAssertionPhase<T>(operation: string, source: string, target: T, property: null): T; \
1528    bindNodeAssertionPhase<T, K extends keyof T>(operation: string, source: string, target: T, property: K): T[K]; \
1529    bindNodeAssertionPhase(operation: string, source: string, target: unknown, property: PropertyKey): any; \
1530    }";
1531
1532const ASSERTION_RUNTIME: &str = "globalThis.__SUPERCOV_DIRECT_RUNTIME__";
1533
1534/// Where each recognised assertion is wrapped or bound, as text edits.
1535struct AssertionEdits<'s> {
1536    source: &'s str,
1537    sites: HashMap<SpanKey, (String, String, bool)>,
1538    edits: Vec<TextEdit>,
1539    /// In TypeScript the call runs in place, under these two names, instead
1540    /// of in a callback, which TypeScript narrows nothing into.
1541    in_place: Option<(String, String)>,
1542    /// The statement that is the body of the arrow being walked, written
1543    /// without braces (`(page) => expect(page).toHaveURL(url)`). It is the
1544    /// arrow's value, so it stays an expression: wrapped as a statement it
1545    /// became a block that returned nothing.
1546    arrow_body: Option<SpanKey>,
1547}
1548
1549impl AssertionEdits<'_> {
1550    fn opening(helper: &str, operation: &str, source: &str) -> String {
1551        format!(
1552            "{ASSERTION_RUNTIME}.{helper}({}, {}, ",
1553            serde_json::to_string(operation).expect("a string serializes"),
1554            serde_json::to_string(source).expect("a string serializes")
1555        )
1556    }
1557
1558    fn open(operation: &str, source: &str) -> String {
1559        format!(
1560            "{ASSERTION_RUNTIME}.openNodeAssertionPhase({}, {})",
1561            serde_json::to_string(operation).expect("a string serializes"),
1562            serde_json::to_string(source).expect("a string serializes")
1563        )
1564    }
1565
1566    /// Open the assertion phase around the call, before its arguments run.
1567    ///
1568    /// In TypeScript the call stays in its own flow: an immediately invoked
1569    /// arrow, which TypeScript narrows into, opens the phase as its argument.
1570    fn wrap(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1571        let (start, end) = (call.span.start as usize, call.span.end as usize);
1572        let width = (end - start) as i64;
1573        let (opening, closing) = match &self.in_place {
1574            Some((phase, error)) => (
1575                format!("(({phase}) => {{ try {{ return {phase}.close("),
1576                format!(
1577                    "); }} catch ({error}) {{ throw {phase}.fail({error}); }} }})({})",
1578                    Self::open(operation, source)
1579                ),
1580            ),
1581            None => (
1582                format!(
1583                    "{}() => (",
1584                    Self::opening("withNodeAssertionPhase", operation, source)
1585                ),
1586                "))".into(),
1587            ),
1588        };
1589        self.edits.push(TextEdit::insert(start, -width, opening));
1590        self.edits.push(TextEdit::insert(end, width, closing));
1591    }
1592
1593    /// Open the assertion phase around a TypeScript statement that is the
1594    /// call, leaving the call a statement: an `asserts` signature narrows
1595    /// only from an expression statement. `finally` closes the phase, so a
1596    /// call typed `never` leaves nothing after it unreachable.
1597    fn wrap_statement(
1598        &mut self,
1599        statement: &ExpressionStatement<'_>,
1600        operation: &str,
1601        source: &str,
1602    ) {
1603        let Some((phase, error)) = &self.in_place else {
1604            return;
1605        };
1606        let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1607        let width = (end - start) as i64;
1608        let opening = format!(
1609            "{{ const {phase} = {}; try {{ ",
1610            Self::open(operation, source)
1611        );
1612        let closing = format!(
1613            " }} catch ({error}) {{ throw {phase}.fail({error}); }} finally {{ {phase}.close(); }} }}"
1614        );
1615        self.edits.push(TextEdit::insert(start, -width, opening));
1616        self.edits.push(TextEdit::insert(end, width, closing));
1617    }
1618
1619    /// Bind a TypeScript statement's callee to a local typed as the callee
1620    /// itself and call the local. An `asserts` signature narrows only through
1621    /// an identifier or a qualified name, explicitly typed (TS2776, TS2775),
1622    /// and a bound call is neither: ts-node refused every such test file. The
1623    /// phase still opens only when the call is made, after its awaited
1624    /// operands, as with any bound call.
1625    fn bind_statement(
1626        &mut self,
1627        statement: &ExpressionStatement<'_>,
1628        call: &CallExpression<'_>,
1629        callee: &str,
1630        operation: &str,
1631        source: &str,
1632    ) {
1633        let Some((local, _)) = self.in_place.clone() else {
1634            return;
1635        };
1636        let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1637        let width = (end - start) as i64;
1638        self.edits.push(TextEdit::insert(
1639            start,
1640            -width,
1641            format!("{{ const {local}: typeof {callee} = "),
1642        ));
1643        self.bind(call, operation, source);
1644        self.edits.push(TextEdit::insert(
1645            call.callee.span().end as usize,
1646            width,
1647            format!("; {local}"),
1648        ));
1649        self.edits.push(TextEdit::insert(end, width, " }".into()));
1650    }
1651
1652    /// Bind the original callee and receiver, leaving await and yield operands
1653    /// in their own function: no async thunk, extra await or new microtask.
1654    fn bind(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1655        let callee = call.callee.span();
1656        let width = (callee.end - callee.start) as i64;
1657        let opening = Self::opening("bindNodeAssertionPhase", operation, source);
1658        self.edits
1659            .push(TextEdit::insert(callee.start as usize, -width, opening));
1660        match &call.callee {
1661            Expression::StaticMemberExpression(member) => {
1662                // `.name` becomes `, "name")`, keeping whatever whitespace and
1663                // line breaks sat between the receiver and the dot.
1664                let between = &self.source
1665                    [member.object.span().end as usize..member.property.span.start as usize];
1666                let dot = between.rfind('.').unwrap_or(0);
1667                let mut text = String::from(",");
1668                text.push_str(&between[..dot]);
1669                text.push(' ');
1670                text.push_str(&between[dot + 1..]);
1671                text.push_str(
1672                    &serde_json::to_string(member.property.name.as_str())
1673                        .expect("a string serializes"),
1674                );
1675                text.push(')');
1676                self.edits.push(TextEdit::replace(
1677                    member.object.span().end as usize,
1678                    member.property.span.end as usize,
1679                    text,
1680                ));
1681            }
1682            Expression::ComputedMemberExpression(member) => {
1683                let object_end = member.object.span().end as usize;
1684                let open = object_end + self.source[object_end..].find('[').unwrap_or(0);
1685                self.edits
1686                    .push(TextEdit::replace(open, open + 1, ",".into()));
1687                let close = member.span.end as usize - 1;
1688                self.edits
1689                    .push(TextEdit::replace(close, close + 1, ")".into()));
1690            }
1691            _ => {
1692                self.edits.push(TextEdit::insert(
1693                    callee.end as usize,
1694                    width,
1695                    ", null)".into(),
1696                ));
1697            }
1698        }
1699    }
1700}
1701
1702impl<'a> Visit<'a> for AssertionEdits<'_> {
1703    fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) {
1704        let body = arrow
1705            .expression
1706            .then(|| arrow.body.statements.first())
1707            .flatten()
1708            .map(|statement| span_key(statement.span()));
1709        let outer = std::mem::replace(&mut self.arrow_body, body);
1710        walk::walk_arrow_function_expression(self, arrow);
1711        self.arrow_body = outer;
1712    }
1713
1714    fn visit_expression_statement(&mut self, statement: &ExpressionStatement<'a>) {
1715        if self.in_place.is_some()
1716            && self.arrow_body != Some(span_key(statement.span))
1717            && let Expression::CallExpression(call) = &statement.expression
1718            && let Some(&(_, _, bound)) = self.sites.get(&span_key(call.span))
1719        {
1720            let entity = entity_name(&call.callee);
1721            if !bound || entity.is_some() {
1722                let (operation, source, _) = self
1723                    .sites
1724                    .remove(&span_key(call.span))
1725                    .expect("the site was just found");
1726                match entity {
1727                    Some(callee) if bound => {
1728                        self.bind_statement(statement, call, &callee, &operation, &source)
1729                    }
1730                    _ => self.wrap_statement(statement, &operation, &source),
1731                }
1732            }
1733        }
1734        walk::walk_expression_statement(self, statement);
1735    }
1736
1737    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1738        if let Some((operation, source, bound)) = self.sites.remove(&span_key(call.span)) {
1739            if bound {
1740                self.bind(call, &operation, &source);
1741            } else {
1742                self.wrap(call, &operation, &source);
1743            }
1744        }
1745        walk::walk_call_expression(self, call);
1746    }
1747}
1748
1749/// A callee that is an identifier or a qualified name (`assert`,
1750/// `assert.strict.ok`), dotted and without whitespace: what `typeof` takes.
1751fn entity_name(callee: &Expression<'_>) -> Option<String> {
1752    match callee {
1753        Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1754        Expression::StaticMemberExpression(member) if !member.optional => Some(format!(
1755            "{}.{}",
1756            entity_name(&member.object)?,
1757            member.property.name
1758        )),
1759        _ => None,
1760    }
1761}
1762
1763/// `transform_capability_imports` as text edits, for a test file.
1764fn capability_import_edits(
1765    program: &Program<'_>,
1766    source: &str,
1767    wrapper: &str,
1768) -> (Vec<TextEdit>, usize) {
1769    if !capability_source_candidate(source) || !program.source_type.is_module() {
1770        return (Vec::new(), 0);
1771    }
1772    let capability_roots = capability_import_roots(program, source);
1773    if capability_roots.is_empty() {
1774        return (Vec::new(), 0);
1775    }
1776    let mut names = CandidateNames::new(source);
1777    let wrapper_local = names.allocate("__supercovImportedCapability");
1778    let mut edits = Vec::new();
1779    let mut wrapped = 0;
1780    for statement in &program.body {
1781        let Statement::ImportDeclaration(declaration) = statement else {
1782            continue;
1783        };
1784        if declaration.import_kind == ImportOrExportKind::Type
1785            || excluded_capability_import(declaration.source.value.as_str(), wrapper)
1786        {
1787            continue;
1788        }
1789        let mut declarators = Vec::new();
1790        for specifier in declaration.specifiers.iter().flatten() {
1791            let (local, shorthand) = match specifier {
1792                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1793                    if specifier.import_kind == ImportOrExportKind::Type =>
1794                {
1795                    continue;
1796                }
1797                ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
1798                    &specifier.local,
1799                    specifier.imported.span() == specifier.local.span,
1800                ),
1801                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1802                    (&specifier.local, false)
1803                }
1804                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1805                    (&specifier.local, false)
1806                }
1807            };
1808            let original = local.name.to_string();
1809            if !capability_roots.contains(&original) {
1810                continue;
1811            }
1812            let raw = names.allocate(&format!("__supercovRaw{original}"));
1813            if shorthand {
1814                edits.push(TextEdit::insert(
1815                    local.span.end as usize,
1816                    0,
1817                    format!(" as {raw}"),
1818                ));
1819            } else {
1820                edits.push(TextEdit::replace(
1821                    local.span.start as usize,
1822                    local.span.end as usize,
1823                    raw.clone(),
1824                ));
1825            }
1826            declarators.push(format!("{original} = {wrapper_local}({raw})"));
1827            wrapped += 1;
1828        }
1829        if !declarators.is_empty() {
1830            edits.push(TextEdit::insert(
1831                declaration.span.end as usize,
1832                i64::MAX,
1833                format!("; const {};", declarators.join(", ")),
1834            ));
1835        }
1836    }
1837    if wrapped > 0 {
1838        // First among the imports, as the syntax-tree transform put it, on the
1839        // first statement's own line.
1840        let first = program
1841            .body
1842            .first()
1843            .map_or(0, |statement| statement.span().start as usize);
1844        edits.push(TextEdit::insert(
1845            first,
1846            i64::MIN,
1847            format!(
1848                "import {{ wrapImportedCapability as {wrapper_local} }} from {}; ",
1849                serde_json::to_string(wrapper).expect("a string serializes")
1850            ),
1851        ));
1852    }
1853    (edits, wrapped)
1854}
1855
1856/// Wrap value imports that can hide a remote execution capability. This is an
1857/// ahead-of-run Rust transform; the runtime shim only proxies the imported
1858/// value and never parses or rewrites source.
1859fn transform_capability_imports<'a>(
1860    allocator: &'a Allocator,
1861    program: &mut Program<'a>,
1862    source: &str,
1863    wrapper: &str,
1864) -> usize {
1865    if !capability_source_candidate(source) || !program.source_type.is_module() {
1866        return 0;
1867    }
1868    let capability_roots = capability_import_roots(program, source);
1869    if capability_roots.is_empty() {
1870        return 0;
1871    }
1872    let ast = AstBuilder::new(allocator);
1873    let mut names = CandidateNames::new(source);
1874    let wrapper_local = names.allocate("__supercovImportedCapability");
1875    let mut wrapped = 0;
1876    let mut output = ast.vec();
1877    for statement in program.body.take_in(allocator) {
1878        let Statement::ImportDeclaration(mut declaration) = statement else {
1879            output.push(statement);
1880            continue;
1881        };
1882        let module_name = declaration.source.value.as_str();
1883        if declaration.import_kind == ImportOrExportKind::Type
1884            || excluded_capability_import(module_name, wrapper)
1885        {
1886            output.push(Statement::ImportDeclaration(declaration));
1887            continue;
1888        }
1889        let mut declarations = ast.vec();
1890        for specifier in declaration.specifiers.iter_mut().flatten() {
1891            let local = match specifier {
1892                ImportDeclarationSpecifier::ImportSpecifier(specifier)
1893                    if specifier.import_kind == ImportOrExportKind::Type =>
1894                {
1895                    continue;
1896                }
1897                ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
1898                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1899                    &mut specifier.local
1900                }
1901                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1902                    &mut specifier.local
1903                }
1904            };
1905            let original = local.name.to_string();
1906            if !capability_roots.contains(&original) {
1907                continue;
1908            }
1909            let raw = names.allocate(&format!("__supercovRaw{original}"));
1910            local.name = ast.ident(&raw);
1911            let wrapped_value = ast.expression_call(
1912                Span::default(),
1913                ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
1914                NONE,
1915                ast.vec1(Argument::from(
1916                    ast.expression_identifier(Span::default(), ast.ident(&raw)),
1917                )),
1918                false,
1919            );
1920            declarations.push(ast.variable_declarator(
1921                Span::default(),
1922                VariableDeclarationKind::Const,
1923                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
1924                NONE,
1925                Some(wrapped_value),
1926                false,
1927            ));
1928            wrapped += 1;
1929        }
1930        output.push(Statement::ImportDeclaration(declaration));
1931        if !declarations.is_empty() {
1932            output.push(Statement::VariableDeclaration(
1933                ast.alloc_variable_declaration(
1934                    Span::default(),
1935                    VariableDeclarationKind::Const,
1936                    declarations,
1937                    false,
1938                ),
1939            ));
1940        }
1941    }
1942    if wrapped > 0 {
1943        output.insert(
1944            0,
1945            Statement::ImportDeclaration(ast.alloc_import_declaration(
1946                Span::default(),
1947                Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
1948                    Span::default(),
1949                    ast.module_export_name_identifier_name(
1950                        Span::default(),
1951                        ast.ident("wrapImportedCapability"),
1952                    ),
1953                    ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
1954                    ImportOrExportKind::Value,
1955                ))),
1956                ast.string_literal(Span::default(), ast.str(wrapper), None),
1957                None,
1958                NONE,
1959                ImportOrExportKind::Value,
1960            )),
1961        );
1962    }
1963    program.body = output;
1964    wrapped
1965}
1966
1967fn canonical_assert_module(value: &str) -> Option<String> {
1968    NODE_ASSERT_MODULES.contains(&value).then(|| {
1969        if value.starts_with("node:") {
1970            value.into()
1971        } else {
1972            format!("node:{value}")
1973        }
1974    })
1975}
1976
1977fn required_assert_module(
1978    expression: &Expression<'_>,
1979    scoping: &oxc_semantic::Scoping,
1980) -> Option<String> {
1981    if let Expression::CallExpression(call) = expression
1982        && matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
1983        && matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
1984        && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
1985    {
1986        return canonical_assert_module(module.value.as_str());
1987    }
1988    if let Expression::StaticMemberExpression(member) = expression
1989        && member.property.name == "strict"
1990        && let Some(module) = required_assert_module(&member.object, scoping)
1991    {
1992        return Some(if module == "node:assert" {
1993            "node:assert/strict".into()
1994        } else {
1995            module
1996        });
1997    }
1998    None
1999}
2000
2001#[derive(Default)]
2002struct NodeAssertionBindings {
2003    objects: HashMap<SymbolId, String>,
2004    direct: HashMap<SymbolId, String>,
2005    expects: HashSet<SymbolId>,
2006    /// Jest (and Jasmine) inject `expect` as a global next to `test`, `it`
2007    /// and `describe`: a file that reaches for all of those unresolved is a
2008    /// test file whose bare `expect` is the assertion.
2009    global_expect: bool,
2010}
2011
2012fn bind_object(
2013    bindings: &mut NodeAssertionBindings,
2014    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
2015    module: String,
2016) {
2017    if let Some(symbol) = identifier.symbol_id.get() {
2018        bindings.objects.insert(symbol, module);
2019    }
2020}
2021
2022fn bind_direct(
2023    bindings: &mut NodeAssertionBindings,
2024    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
2025    operation: String,
2026) {
2027    if let Some(symbol) = identifier.symbol_id.get() {
2028        bindings.direct.insert(symbol, operation);
2029    }
2030}
2031
2032fn bind_expect(
2033    bindings: &mut NodeAssertionBindings,
2034    identifier: &oxc_ast::ast::BindingIdentifier<'_>,
2035) {
2036    if let Some(symbol) = identifier.symbol_id.get() {
2037        bindings.expects.insert(symbol);
2038    }
2039}
2040
2041struct NodeAssertionBindingCollector<'s> {
2042    bindings: NodeAssertionBindings,
2043    scoping: &'s oxc_semantic::Scoping,
2044    allow_contextual_expect: bool,
2045    expect_modules: HashSet<String>,
2046}
2047
2048impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
2049    fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
2050        let module_name = declaration.source.value.as_str();
2051        let assert_module = canonical_assert_module(module_name);
2052        let expect_module =
2053            self.expect_modules.contains(module_name) || self.allow_contextual_expect;
2054        for specifier in declaration.specifiers.iter().flatten() {
2055            match specifier {
2056                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
2057                    if let Some(module) = &assert_module {
2058                        bind_object(&mut self.bindings, &specifier.local, module.clone());
2059                    } else if expect_module && specifier.local.name == "expect" {
2060                        bind_expect(&mut self.bindings, &specifier.local);
2061                    }
2062                }
2063                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
2064                    if let Some(module) = &assert_module {
2065                        bind_object(&mut self.bindings, &specifier.local, module.clone());
2066                    }
2067                }
2068                ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
2069                    let imported = specifier.imported.name().to_string();
2070                    if let Some(module) = &assert_module {
2071                        if imported == "strict" {
2072                            bind_object(
2073                                &mut self.bindings,
2074                                &specifier.local,
2075                                "node:assert/strict".into(),
2076                            );
2077                        } else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
2078                            bind_direct(
2079                                &mut self.bindings,
2080                                &specifier.local,
2081                                format!("{module}.{imported}"),
2082                            );
2083                        }
2084                    } else if expect_module && imported == "expect" {
2085                        bind_expect(&mut self.bindings, &specifier.local);
2086                    }
2087                }
2088            }
2089        }
2090        walk::walk_import_declaration(self, declaration);
2091    }
2092
2093    fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
2094        // CommonJS named expect imports use the same lexical symbol identities
2095        // as ESM; a local/shadowed require is deliberately not recognized.
2096        if let Some(Expression::CallExpression(call)) = &declarator.init
2097            && matches!(&call.callee, Expression::Identifier(i) if i.name == "require" && referenced_symbol(i, self.scoping).is_none())
2098            && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
2099            && self.expect_modules.contains(module.value.as_str())
2100            && let BindingPattern::ObjectPattern(pattern) = &declarator.id
2101        {
2102            for property in &pattern.properties {
2103                if property.key.static_name().as_deref() == Some("expect")
2104                    && let BindingPattern::BindingIdentifier(local) = &property.value
2105                {
2106                    bind_expect(&mut self.bindings, local);
2107                }
2108            }
2109        }
2110        if let Some(module) = declarator
2111            .init
2112            .as_ref()
2113            .and_then(|expression| required_assert_module(expression, self.scoping))
2114        {
2115            match &declarator.id {
2116                BindingPattern::BindingIdentifier(identifier) => {
2117                    bind_object(&mut self.bindings, identifier, module);
2118                }
2119                BindingPattern::ObjectPattern(pattern) => {
2120                    for property in &pattern.properties {
2121                        let Some(imported) = property.key.static_name() else {
2122                            continue;
2123                        };
2124                        let BindingPattern::BindingIdentifier(local) = &property.value else {
2125                            continue;
2126                        };
2127                        if imported == "strict" {
2128                            bind_object(&mut self.bindings, local, "node:assert/strict".into());
2129                        } else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
2130                            bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
2131                        }
2132                    }
2133                }
2134                _ => {}
2135            }
2136        }
2137        walk::walk_variable_declarator(self, declarator);
2138    }
2139}
2140
2141fn node_assertion_bindings(
2142    program: &Program<'_>,
2143    scoping: &oxc_semantic::Scoping,
2144    extra_expect_modules: &[String],
2145) -> NodeAssertionBindings {
2146    let allow_contextual_expect = program.source_text.contains("node:test")
2147        && program
2148            .source_text
2149            .split(|character: char| !character.is_alphanumeric() && character != '_')
2150            .any(|word| word == "expect");
2151    let mut collector = NodeAssertionBindingCollector {
2152        bindings: NodeAssertionBindings::default(),
2153        scoping,
2154        allow_contextual_expect,
2155        expect_modules: ["vitest", "@jest/globals", "expect", "@playwright/test"]
2156            .into_iter()
2157            .map(str::to_owned)
2158            .chain(extra_expect_modules.iter().cloned())
2159            .collect(),
2160    };
2161    collector.visit_program(program);
2162    let unresolved = scoping.root_unresolved_references();
2163    let uses = |name: &str| unresolved.contains_key(name);
2164    collector.bindings.global_expect =
2165        uses("expect") && (uses("test") || uses("it") || uses("describe"));
2166    collector.bindings
2167}
2168
2169struct NodeAssertionSiteCollector<'s> {
2170    source: &'s str,
2171    file: &'s str,
2172    bindings: &'s NodeAssertionBindings,
2173    scoping: &'s oxc_semantic::Scoping,
2174    sites: HashMap<SpanKey, (String, String, bool)>,
2175}
2176
2177/// How a project's JavaScript is parsed: in the script or module mode its
2178/// extension implies, with JSX allowed. React code is routinely written in
2179/// `.js` and compiled by Babel, Webpack or Vite, so a `.js` file with JSX in it
2180/// is ordinary; rejecting it failed a whole run before any test started.
2181/// Enabling JSX changes nothing for JavaScript without it, because `<` cannot
2182/// begin an expression there. TypeScript is left alone: in `.ts` a leading `<`
2183/// is a type assertion, and only `.tsx` means JSX.
2184pub fn project_source_type(path: &Path) -> Result<SourceType, String> {
2185    let source_type = SourceType::from_path(path).map_err(|error| error.to_string())?;
2186    Ok(if source_type.is_typescript() {
2187        source_type
2188    } else {
2189        source_type.with_jsx(true)
2190    })
2191}
2192
2193fn referenced_symbol(
2194    identifier: &oxc_ast::ast::IdentifierReference<'_>,
2195    scoping: &oxc_semantic::Scoping,
2196) -> Option<SymbolId> {
2197    identifier
2198        .reference_id
2199        .get()
2200        .and_then(|reference| scoping.get_reference(reference).symbol_id())
2201}
2202
2203fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
2204    match expression {
2205        Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
2206        Expression::ComputedMemberExpression(member) => match &member.expression {
2207            Expression::StringLiteral(literal) => Some(literal.value.as_str()),
2208            _ => None,
2209        },
2210        _ => None,
2211    }
2212}
2213
2214fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
2215    match expression {
2216        Expression::StaticMemberExpression(member) => Some(&member.object),
2217        Expression::ComputedMemberExpression(member) => Some(&member.object),
2218        _ => None,
2219    }
2220}
2221
2222fn expect_operation(
2223    callee: &Expression<'_>,
2224    bindings: &NodeAssertionBindings,
2225    scoping: &oxc_semantic::Scoping,
2226) -> Option<String> {
2227    let mut current = callee;
2228    let mut matchers = Vec::new();
2229    while let Some(name) = assertion_member_name(current) {
2230        matchers.push(name.to_owned());
2231        current = assertion_member_object(current)?;
2232    }
2233    matchers.reverse();
2234    let matcher = matchers.last()?;
2235    if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
2236        return None;
2237    }
2238    let Expression::CallExpression(expect_call) = current else {
2239        return None;
2240    };
2241    // `expect(value)` is the usual root, but the assertion APIs wrap it:
2242    // `expect.element(locator)` in Vitest Browser Mode, `expect.soft(value)`,
2243    // `expect.poll(fn)`. The receiver is still expect and the matcher is still
2244    // the assertion, so requiring a bare identifier here made every UI
2245    // assertion in a browser-mode test invisible -- and an assertion Supercov
2246    // cannot see can never be mapped or earn credit.
2247    let mut root = &expect_call.callee;
2248    let mut prefix = Vec::new();
2249    while let Some(name) = assertion_member_name(root) {
2250        prefix.push(name.to_owned());
2251        root = assertion_member_object(root)?;
2252    }
2253    prefix.reverse();
2254    let Expression::Identifier(identifier) = root else {
2255        return None;
2256    };
2257    let recognized = match referenced_symbol(identifier, scoping) {
2258        Some(symbol) => bindings.expects.contains(&symbol),
2259        None => bindings.global_expect && identifier.name == "expect",
2260    };
2261    recognized.then(|| {
2262        let mut parts = prefix;
2263        parts.extend(matchers.iter().cloned());
2264        format!("expect.{}", parts.join("."))
2265    })
2266}
2267
2268#[derive(Default)]
2269struct AwaitYieldScanner {
2270    found: bool,
2271}
2272
2273impl<'a> Visit<'a> for AwaitYieldScanner {
2274    fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
2275        self.found = true;
2276    }
2277
2278    fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
2279        self.found = true;
2280    }
2281
2282    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
2283
2284    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
2285}
2286
2287impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
2288    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
2289        let operation = match &call.callee {
2290            Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
2291                .and_then(|symbol| {
2292                    self.bindings.direct.get(&symbol).cloned().or_else(|| {
2293                        self.bindings
2294                            .objects
2295                            .get(&symbol)
2296                            .map(|module| format!("{module}.ok"))
2297                    })
2298                }),
2299            callee => {
2300                let member_operation = assertion_member_object(callee).and_then(|object| {
2301                    let Expression::Identifier(identifier) = object else {
2302                        return None;
2303                    };
2304                    let symbol = referenced_symbol(identifier, self.scoping)?;
2305                    let method = assertion_member_name(callee)?;
2306                    self.bindings
2307                        .objects
2308                        .get(&symbol)
2309                        .filter(|_| NODE_ASSERT_METHODS.contains(&method))
2310                        .map(|module| format!("{module}.{method}"))
2311                });
2312                member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
2313            }
2314        };
2315        if let Some(operation) = operation {
2316            let mut unsafe_argument = AwaitYieldScanner::default();
2317            unsafe_argument.visit_expression(&call.callee);
2318            for argument in &call.arguments {
2319                unsafe_argument.visit_argument(argument);
2320            }
2321            let optional = call.optional
2322                || matches!(&call.callee,
2323                Expression::StaticMemberExpression(m) if m.optional)
2324                || matches!(&call.callee, Expression::ComputedMemberExpression(m) if m.optional);
2325            // Never manufacture a passed occurrence when a call can
2326            // short-circuit before invoking a matcher.
2327            if optional {
2328                walk::walk_call_expression(self, call);
2329                return;
2330            }
2331            let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
2332            self.sites.insert(
2333                span_key(call.span),
2334                (
2335                    operation,
2336                    format!("{}:{line}:{column}", self.file),
2337                    unsafe_argument.found,
2338                ),
2339            );
2340        }
2341        walk::walk_call_expression(self, call);
2342    }
2343}
2344
2345/// Attribute native node:assert and node:test expect calls by opening the
2346/// assertion phase before argument evaluation when possible, or binding the
2347/// callee when arguments contain await/yield. Lexical symbol identity avoids
2348/// wrapping a shadowed or merely assert-shaped user binding.
2349pub fn instrument_node_assertion_phases(
2350    source: &str,
2351    file: &str,
2352) -> Result<NodeAssertionInstrumentation, CandidateError> {
2353    instrument_node_assertion_phases_with_expect_modules(source, file, &[])
2354}
2355
2356pub fn instrument_node_assertion_phases_with_expect_modules(
2357    source: &str,
2358    file: &str,
2359    extra_expect_modules: &[String],
2360) -> Result<NodeAssertionInstrumentation, CandidateError> {
2361    instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
2362}
2363
2364pub fn instrument_node_assertion_phases_with_runtime_hooks(
2365    source: &str,
2366    file: &str,
2367    extra_expect_modules: &[String],
2368    capability_wrapper: Option<&str>,
2369) -> Result<NodeAssertionInstrumentation, CandidateError> {
2370    instrument_node_assertion_phases_with_runtime_imports(
2371        source,
2372        file,
2373        extra_expect_modules,
2374        capability_wrapper,
2375        None,
2376    )
2377}
2378
2379/// Attribute assertions and, for an ESM test module, make the runtime
2380/// dependency explicit in the transformed module itself. This is required for
2381/// opaque runners which copy an already-instrumented workspace into another
2382/// process, container, or VM while constructing a fresh environment. Static
2383/// imports preserve ESM evaluation ordering and do not depend on runner-specific
2384/// environment forwarding.
2385/// The body of the function [`runtime_loader`] calls: where Node runs, it
2386/// requires the bootstrap relative to the file the stack's first frame names.
2387const RUNTIME_LOADER_BODY: &str = r#""const host = globalThis.process; if (!host || !host.getBuiltinModule) return; const frame = /(?:file:\\/\\/)?((?:\\/|[A-Za-z]:[\\\\/])[^\\s()]+?)(?=:\\d+:\\d+)/.exec(String(stack)); if (!frame) return; host.getBuiltinModule(\"node:module\").createRequire(frame[0].startsWith(\"file:\") ? frame[0] : frame[1])(specifier);""#;
2388
2389/// How the statement [`runtime_loader`] writes starts.
2390pub(crate) const RUNTIME_LOADER_START: &str = "if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {";
2391
2392/// One statement that loads the run's bootstrap where no runtime is
2393/// installed. It has to be valid in a module, a CommonJS file and a browser,
2394/// pass a strict TypeScript check with or without Node's types, and survive
2395/// compilers -- so no `import`, no `import.meta`, and nothing but
2396/// `globalThis.__SUPERCOV_DIRECT_RUNTIME__`, `Function` and `Error` outside a
2397/// function body the type checker does not read. The file's own location
2398/// comes from a stack taken at its top level; `bootstrap` is relative to it.
2399/// The offset just past a block comment that opens the source at `from`
2400/// (after any whitespace), or `from` when none does.
2401pub(crate) fn after_leading_block_comment(source: &str, from: usize) -> usize {
2402    let rest = &source[from..];
2403    let start = from + (rest.len() - rest.trim_start().len());
2404    if !source[start..].starts_with("/*") {
2405        return from;
2406    }
2407    source[start + 2..]
2408        .find("*/")
2409        .map_or(from, |end| start + 2 + end + 2)
2410}
2411
2412pub(crate) fn runtime_loader(bootstrap: &str) -> String {
2413    let specifier = serde_json::to_string(bootstrap).expect("a path string serializes");
2414    format!(
2415        "{RUNTIME_LOADER_START} new Function(\"stack\", \"specifier\", {RUNTIME_LOADER_BODY})(new Error().stack, {specifier}); }} catch {{}}"
2416    )
2417}
2418
2419pub fn instrument_node_assertion_phases_with_runtime_imports(
2420    source: &str,
2421    file: &str,
2422    extra_expect_modules: &[String],
2423    capability_wrapper: Option<&str>,
2424    assertion_runtime: Option<&str>,
2425) -> Result<NodeAssertionInstrumentation, CandidateError> {
2426    let _positions = PositionScope::enter();
2427    let assertion_candidate = source.contains("assert") || source.contains("expect");
2428    let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
2429    if !assertion_candidate && !capability_candidate {
2430        return Ok(NodeAssertionInstrumentation {
2431            code: source.into(),
2432            assertions: 0,
2433            capability_imports: 0,
2434        });
2435    }
2436    let source_type =
2437        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
2438    let allocator = Allocator::default();
2439    let parsed = Parser::new(&allocator, source, source_type).parse();
2440    if !parsed.errors.is_empty() {
2441        return Err(CandidateError::Parse(
2442            parsed
2443                .errors
2444                .into_iter()
2445                .map(|error| error.to_string())
2446                .collect(),
2447        ));
2448    }
2449    let mut assertions = 0;
2450    let mut edits = Vec::new();
2451
2452    if assertion_candidate {
2453        let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
2454        let bindings =
2455            node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
2456        let mut collector = NodeAssertionSiteCollector {
2457            source,
2458            file,
2459            bindings: &bindings,
2460            scoping: semantic.scoping(),
2461            sites: HashMap::new(),
2462        };
2463        collector.visit_program(&parsed.program);
2464        assertions = collector.sites.len();
2465        if assertions > 0 {
2466            let in_place = source_type.is_typescript().then(|| {
2467                let mut names = CandidateNames::new(source);
2468                (
2469                    names.allocate("__supercovAssertionPhase"),
2470                    names.allocate("__supercovAssertionError"),
2471                )
2472            });
2473            let mut located = AssertionEdits {
2474                source,
2475                sites: collector.sites,
2476                edits: Vec::new(),
2477                in_place,
2478                arrow_body: None,
2479            };
2480            located.visit_program(&parsed.program);
2481            edits.extend(located.edits);
2482        }
2483    }
2484    let capability_imports = capability_wrapper.map_or(0, |wrapper| {
2485        let (found, wrapped) = capability_import_edits(&parsed.program, source, wrapper);
2486        edits.extend(found);
2487        wrapped
2488    });
2489    if assertions == 0 && capability_imports == 0 {
2490        return Ok(NodeAssertionInstrumentation {
2491            code: source.into(),
2492            assertions: 0,
2493            capability_imports: 0,
2494        });
2495    }
2496    let module = parsed.program.source_type.is_module();
2497    // The runtime the assertion phases call, loaded where nothing installed
2498    // it. A statement rather than an `import`: an import's relative path broke
2499    // once the test command compiled the file somewhere else (`tsc` into
2500    // `dist/`), and the test failed on a module that was not there. It goes
2501    // first, as the import was evaluated first, on the first line so every
2502    // other line stays where it was.
2503    // After a leading comment too: Jest reads a test's docblock pragmas
2504    // (`/** @jest-environment jsdom */`) only from a comment the file opens
2505    // with, so a loader in front of it ran the test in the wrong environment
2506    // ("window is not defined").
2507    if let Some(bootstrap) = assertion_runtime.filter(|_| assertions > 0) {
2508        let mut at = if source.starts_with("#!") {
2509            source
2510                .find('\n')
2511                .map_or(source.len(), |newline| newline + 1)
2512        } else {
2513            0
2514        };
2515        at = after_leading_block_comment(source, at);
2516        let space = if source[..at].ends_with("*/") {
2517            " "
2518        } else {
2519            ""
2520        };
2521        edits.push(TextEdit::insert(
2522            at,
2523            i64::MIN,
2524            format!("{space}{} ", runtime_loader(bootstrap)),
2525        ));
2526    }
2527    // An inline map is resolved relative to the transformed file itself, and
2528    // assertion-only transforms replace the file in place, so its basename is
2529    // the exact source-map reference.
2530    let reference = Path::new(file)
2531        .file_name()
2532        .and_then(|name| name.to_str())
2533        .unwrap_or(file);
2534    let (mut code, map) = apply_text_edits(source, edits, reference)?;
2535    // Append instead of prepend so every line of user code stays where it
2536    // was. Import declarations are instantiated before module evaluation
2537    // regardless of their textual position.
2538    if assertions > 0 && source_type.is_typescript() {
2539        if module {
2540            code.push_str(&format!(
2541                "\ndeclare global {{ var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE}; }}\n"
2542            ));
2543        } else {
2544            code.push_str(&format!(
2545                "\ndeclare var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE};\n"
2546            ));
2547        }
2548    }
2549    let map = map.to_json_string();
2550    code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
2551    BASE64_STANDARD.encode_string(map, &mut code);
2552    code.push('\n');
2553    Ok(NodeAssertionInstrumentation {
2554        code,
2555        assertions,
2556        capability_imports,
2557    })
2558}
2559
2560type SpanKey = (u32, u32);
2561
2562#[derive(Default)]
2563struct SafetyAnalysis {
2564    source_sensitive_functions: HashSet<SpanKey>,
2565    semantic_limitations: Vec<CandidateLimitation>,
2566    dynamic_limitations: Vec<CandidateLimitation>,
2567}
2568
2569/// The bindings a file reads function source through, in the places
2570/// `observes_function_source` recognises a function written in place:
2571/// `name.toString()`, `String(name)`, `${name}`, `"(" + name + ")"`, and the
2572/// same through parentheses, casts, `?:`, `||`, `&&`, `,` and `=`. A function
2573/// bound to one of them is shipped as text -- to a worker, a `vm` context, a
2574/// browser page -- where no probe exists, so it is left as source like one
2575/// read in place. Bindings are symbols, not names: a local `total` compared
2576/// with a number says nothing about a function `total` elsewhere. Another
2577/// file's reads are not visible here.
2578struct SourceReads<'s> {
2579    scoping: &'s oxc_semantic::Scoping,
2580    symbols: HashSet<SymbolId>,
2581    /// Property names a method's source is read through: `box.method` in
2582    /// `box.method.toString()` or `String(box.method)`. A property path has
2583    /// no symbol to follow, so any method of that name in the file is taken
2584    /// as the one read. Only the two explicit forms count: `"Total: " +
2585    /// cart.total` reads a value almost always, and would cost a method
2586    /// `total` its probes.
2587    properties: HashSet<String>,
2588}
2589
2590impl SourceReads<'_> {
2591    fn read_property(&mut self, expression: &Expression<'_>) {
2592        match expression {
2593            Expression::StaticMemberExpression(member) => {
2594                self.properties.insert(member.property.name.to_string());
2595            }
2596            Expression::ParenthesizedExpression(inner) => self.read_property(&inner.expression),
2597            Expression::TSAsExpression(inner) => self.read_property(&inner.expression),
2598            Expression::TSNonNullExpression(inner) => self.read_property(&inner.expression),
2599            Expression::ConditionalExpression(inner) => {
2600                self.read_property(&inner.consequent);
2601                self.read_property(&inner.alternate);
2602            }
2603            Expression::LogicalExpression(inner) => {
2604                self.read_property(&inner.left);
2605                self.read_property(&inner.right);
2606            }
2607            _ => {}
2608        }
2609    }
2610
2611    fn read(&mut self, expression: &Expression<'_>) {
2612        match expression {
2613            Expression::Identifier(identifier) => {
2614                self.symbols
2615                    .extend(referenced_symbol(identifier, self.scoping));
2616            }
2617            Expression::ParenthesizedExpression(inner) => self.read(&inner.expression),
2618            Expression::TSAsExpression(inner) => self.read(&inner.expression),
2619            Expression::TSSatisfiesExpression(inner) => self.read(&inner.expression),
2620            Expression::TSTypeAssertion(inner) => self.read(&inner.expression),
2621            Expression::TSNonNullExpression(inner) => self.read(&inner.expression),
2622            Expression::ConditionalExpression(inner) => {
2623                self.read(&inner.consequent);
2624                self.read(&inner.alternate);
2625            }
2626            Expression::LogicalExpression(inner) => {
2627                self.read(&inner.left);
2628                self.read(&inner.right);
2629            }
2630            Expression::SequenceExpression(inner) => {
2631                if let Some(last) = inner.expressions.last() {
2632                    self.read(last);
2633                }
2634            }
2635            Expression::AssignmentExpression(inner) => self.read(&inner.right),
2636            _ => {}
2637        }
2638    }
2639}
2640
2641impl<'a> Visit<'a> for SourceReads<'_> {
2642    fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
2643        if member.property.name == "toString" {
2644            self.read(&member.object);
2645            self.read_property(&member.object);
2646        }
2647        walk::walk_static_member_expression(self, member);
2648    }
2649
2650    fn visit_computed_member_expression(
2651        &mut self,
2652        member: &oxc_ast::ast::ComputedMemberExpression<'a>,
2653    ) {
2654        self.read(&member.expression);
2655        walk::walk_computed_member_expression(self, member);
2656    }
2657
2658    fn visit_object_property(&mut self, property: &oxc_ast::ast::ObjectProperty<'a>) {
2659        if property.computed
2660            && let Some(key) = property.key.as_expression()
2661        {
2662            self.read(key);
2663        }
2664        walk::walk_object_property(self, property);
2665    }
2666
2667    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
2668        if expression_is_identifier(&call.callee, "String")
2669            && let Some(argument) = call.arguments.first().and_then(Argument::as_expression)
2670        {
2671            self.read(argument);
2672            self.read_property(argument);
2673        }
2674        walk::walk_call_expression(self, call);
2675    }
2676
2677    fn visit_template_literal(&mut self, template: &oxc_ast::ast::TemplateLiteral<'a>) {
2678        for expression in &template.expressions {
2679            self.read(expression);
2680        }
2681        walk::walk_template_literal(self, template);
2682    }
2683
2684    fn visit_binary_expression(&mut self, binary: &oxc_ast::ast::BinaryExpression<'a>) {
2685        if matches!(
2686            binary.operator,
2687            BinaryOperator::Addition
2688                | BinaryOperator::LessThan
2689                | BinaryOperator::LessEqualThan
2690                | BinaryOperator::GreaterThan
2691                | BinaryOperator::GreaterEqualThan
2692        ) {
2693            self.read(&binary.left);
2694            self.read(&binary.right);
2695        }
2696        walk::walk_binary_expression(self, binary);
2697    }
2698}
2699
2700/// The property a function is the value of: an object's method or property,
2701/// a class's method or field.
2702fn property_name_of_function<State>(context: &TraverseCtx<'_, State>) -> Option<String> {
2703    let key = match context.parent() {
2704        Ancestor::ObjectPropertyValue(property) => property.key(),
2705        Ancestor::MethodDefinitionValue(method) => method.key(),
2706        Ancestor::PropertyDefinitionValue(property) => property.key(),
2707        _ => return None,
2708    };
2709    key.static_name().map(|name| name.to_string())
2710}
2711
2712/// The bindings a function is reached through where it is defined: its own
2713/// name, and the variable it initializes or is assigned to.
2714fn bound_symbols<State>(
2715    own: Option<&oxc_ast::ast::BindingIdentifier<'_>>,
2716    scoping: &oxc_semantic::Scoping,
2717    context: &TraverseCtx<'_, State>,
2718) -> Vec<SymbolId> {
2719    let mut symbols = own
2720        .and_then(|identifier| identifier.symbol_id.get())
2721        .into_iter()
2722        .collect::<Vec<_>>();
2723    match context.ancestors().next() {
2724        Some(Ancestor::VariableDeclaratorInit(parent)) => symbols.extend(
2725            parent
2726                .id()
2727                .get_binding_identifier()
2728                .and_then(|identifier| identifier.symbol_id.get()),
2729        ),
2730        Some(Ancestor::AssignmentExpressionRight(parent)) => {
2731            if let AssignmentTarget::AssignmentTargetIdentifier(target) = parent.left() {
2732                symbols.extend(referenced_symbol(target, scoping));
2733            }
2734        }
2735        _ => {}
2736    }
2737    symbols
2738}
2739
2740struct SafetyScanner<'s> {
2741    source: &'s str,
2742    file: &'s str,
2743    scoping: &'s oxc_semantic::Scoping,
2744    source_reads: HashSet<SymbolId>,
2745    source_read_properties: HashSet<String>,
2746    source_sensitive_functions: HashSet<SpanKey>,
2747    function_limitations: Vec<CandidateLimitation>,
2748    with_limitations: Vec<CandidateLimitation>,
2749    dynamic_limitations: Vec<CandidateLimitation>,
2750    unsafe_function_depth: usize,
2751    with_depth: usize,
2752}
2753
2754#[derive(Clone, Copy, PartialEq, Eq)]
2755enum PointPass {
2756    Statements,
2757    Functions,
2758}
2759
2760struct PointCollector<'s> {
2761    source: &'s str,
2762    file: &'s str,
2763    pass: PointPass,
2764    points: Vec<CandidatePoint>,
2765    statement_targets: HashMap<SpanKey, Vec<String>>,
2766    function_targets: HashMap<SpanKey, String>,
2767    rendered_targets: HashMap<SpanKey, String>,
2768    source_sensitive_functions: &'s HashSet<SpanKey>,
2769    erased_imports: &'s HashSet<SpanKey>,
2770    unsafe_function_depth: usize,
2771    with_depth: usize,
2772    ambient_depth: usize,
2773}
2774
2775#[derive(Default)]
2776struct PointAnalysis {
2777    points: Vec<CandidatePoint>,
2778    statement_targets: HashMap<SpanKey, Vec<String>>,
2779    function_targets: HashMap<SpanKey, String>,
2780    rendered_targets: HashMap<SpanKey, String>,
2781}
2782
2783#[derive(Clone)]
2784struct PointTarget {
2785    index: usize,
2786}
2787
2788impl PointCollector<'_> {
2789    fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
2790        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
2791        CandidatePoint {
2792            id: stable_id(self.source, self.file, kind, span, ""),
2793            kind: kind.to_string(),
2794            file: self.file.to_string(),
2795            line,
2796            column,
2797            source: source_slice(self.source, span).to_string(),
2798            label,
2799        }
2800    }
2801
2802    fn unsafe_context(&self) -> bool {
2803        self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
2804    }
2805
2806    fn exit_function(&mut self, span: Span) {
2807        if self.source_sensitive_functions.contains(&span_key(span)) {
2808            self.unsafe_function_depth -= 1;
2809        }
2810    }
2811}
2812
2813fn function_label<State>(
2814    own_name: Option<&str>,
2815    context: &TraverseCtx<'_, State>,
2816) -> Option<String> {
2817    if let Some(name) = own_name {
2818        return Some(name.to_string());
2819    }
2820    match context.ancestors().next()? {
2821        Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
2822        Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
2823        Ancestor::VariableDeclaratorInit(parent) => parent
2824            .id()
2825            .get_binding_identifier()
2826            .map(|identifier| identifier.name.to_string()),
2827        _ => None,
2828    }
2829}
2830
2831fn property_label(key: &PropertyKey<'_>) -> Option<String> {
2832    key.static_name()
2833        .map(|name| name.into_owned())
2834        .or_else(|| match key {
2835            PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
2836            _ => None,
2837        })
2838}
2839
2840fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
2841    match context.ancestors().next() {
2842        Some(Ancestor::ObjectPropertyValue(parent))
2843            if *parent.method() || *parent.kind() != PropertyKind::Init =>
2844        {
2845            *parent.span()
2846        }
2847        Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
2848        _ => span,
2849    }
2850}
2851
2852fn type_only_import(statement: &Statement<'_>) -> bool {
2853    let Statement::ImportDeclaration(declaration) = statement else {
2854        return false;
2855    };
2856    // Inline `import { type T }` can leave an empty runtime import under
2857    // verbatimModuleSyntax. Only declaration-level `import type` is universal.
2858    declaration.import_kind == ImportOrExportKind::Type
2859}
2860
2861/// Local binding classification, not dependency/taint analysis. The caller
2862/// enables implicit elision only for a recognized compiler configuration.
2863fn erased_imports(program: &Program<'_>, elide_type_imports: bool) -> HashSet<SpanKey> {
2864    let semantic = SemanticBuilder::new().build(program).semantic;
2865    let scoping = semantic.scoping();
2866    program
2867        .body
2868        .iter()
2869        .filter_map(|statement| {
2870            let Statement::ImportDeclaration(declaration) = statement else {
2871                return None;
2872            };
2873            let erased = declaration.import_kind == ImportOrExportKind::Type
2874                || (elide_type_imports
2875                    && program.source_type.is_typescript()
2876                    && !program.source_type.is_jsx()
2877                    && declaration.specifiers.as_ref().is_some_and(|specifiers| {
2878                        !specifiers.is_empty()
2879                            && specifiers.iter().all(|specifier| {
2880                                let local = match specifier {
2881                                    ImportDeclarationSpecifier::ImportSpecifier(s) => {
2882                                        if s.import_kind == ImportOrExportKind::Type {
2883                                            return true;
2884                                        }
2885                                        &s.local
2886                                    }
2887                                    ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
2888                                        &s.local
2889                                    }
2890                                    ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
2891                                        &s.local
2892                                    }
2893                                };
2894                                local.symbol_id.get().is_some_and(|symbol| {
2895                                    let mut refs =
2896                                        scoping.get_resolved_references(symbol).peekable();
2897                                    // An unused binding may be needed by a JSX/decorator
2898                                    // transform. Only positively identified type uses count.
2899                                    refs.peek().is_some()
2900                                        && refs.all(|r| r.is_type() && !r.is_value())
2901                                })
2902                            })
2903                    }));
2904            erased.then_some(span_key(declaration.span))
2905        })
2906        .collect()
2907}
2908
2909fn executable_statement(statement: &Statement<'_>) -> bool {
2910    !matches!(
2911        statement,
2912        Statement::BlockStatement(_)
2913            | Statement::EmptyStatement(_)
2914            | Statement::FunctionDeclaration(_)
2915            | Statement::ExportNamedDeclaration(_)
2916            | Statement::ExportDefaultDeclaration(_)
2917    ) && !statement.is_typescript_syntax()
2918        && !type_only_import(statement)
2919}
2920
2921impl<'a> Traverse<'a, ()> for PointCollector<'_> {
2922    fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
2923        let mut ancestors = context.ancestors();
2924        let parent = ancestors.next();
2925        let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
2926            && matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
2927        if self.pass != PointPass::Statements
2928            || self.unsafe_context()
2929            || !executable_statement(node)
2930            || self.erased_imports.contains(&span_key(node.span()))
2931            || matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
2932            || expression_arrow_body
2933        {
2934            return;
2935        }
2936        let point = self.point(node.span(), "statement", None);
2937        self.statement_targets
2938            .entry(span_key(node.span()))
2939            .or_default()
2940            .push(point.id.clone());
2941        self.points.push(point);
2942    }
2943
2944    // A JSX tree is one statement, so everything rendered inside it -- an
2945    // `aria-label={label(state)}`, a child `{formatted(value)}` -- counted as
2946    // covered the moment the component rendered once, even when that
2947    // expression never evaluated. It is also the seam a UI assertion attaches
2948    // to: `toHaveAccessibleName` is about the attribute and `toHaveTextContent`
2949    // about the child, and with one statement for the whole tree neither could
2950    // be named apart from the other.
2951    //
2952    // Only expressions that can independently fail to evaluate earn a point.
2953    // A bare identifier or member chain cannot -- it is reached exactly when
2954    // the enclosing statement is -- and a function is already measured where
2955    // it is called rather than where it is created.
2956    fn enter_expression(&mut self, node: &mut Expression<'a>, context: &mut TraverseCtx<'a, ()>) {
2957        if self.pass != PointPass::Statements
2958            || self.unsafe_context()
2959            || !matches!(
2960                context.ancestors().next(),
2961                Some(Ancestor::JSXExpressionContainerExpression(_))
2962            )
2963            || !rendered_expression_is_measurable(node)
2964        {
2965            return;
2966        }
2967        let span = node.span();
2968        let point = self.point(span, "statement", Some("jsx-rendered-expression".into()));
2969        self.rendered_targets
2970            .insert(span_key(span), point.id.clone());
2971        self.points.push(point);
2972    }
2973
2974    fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
2975        if self.pass != PointPass::Statements
2976            || self.unsafe_context()
2977            || node.is_typescript_syntax()
2978            || matches!(node, Declaration::FunctionDeclaration(_))
2979            || !matches!(
2980                context.ancestors().next(),
2981                Some(Ancestor::ExportNamedDeclarationDeclaration(_))
2982                    | Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2983            )
2984        {
2985            return;
2986        }
2987        let point = self.point(node.span(), "statement", None);
2988        self.statement_targets
2989            .entry(span_key(node.span()))
2990            .or_default()
2991            .push(point.id.clone());
2992        self.points.push(point);
2993    }
2994
2995    fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
2996        // oxc represents `export default class` directly inside
2997        // ExportDefaultDeclarationKind rather than through Declaration, so it
2998        // does not reach enter_declaration. Babel treats the inner class as the
2999        // executable statement obligation (excluding the `export default`
3000        // prefix), which is also the location where the probe belongs.
3001        if self.pass != PointPass::Statements
3002            || self.unsafe_context()
3003            || node.declare
3004            || !matches!(
3005                context.ancestors().next(),
3006                Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
3007            )
3008        {
3009            return;
3010        }
3011        let point = self.point(node.span, "statement", None);
3012        self.statement_targets
3013            .entry(span_key(node.span))
3014            .or_default()
3015            .push(point.id.clone());
3016        self.points.push(point);
3017    }
3018
3019    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
3020        let point_span = function_point_span(node.span, context);
3021        let label = if point_span == node.span {
3022            function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
3023        } else {
3024            None
3025        };
3026        if self
3027            .source_sensitive_functions
3028            .contains(&span_key(node.span))
3029        {
3030            self.unsafe_function_depth += 1;
3031            return;
3032        }
3033        if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
3034            let point = self.point(point_span, "function", label);
3035            self.function_targets
3036                .insert(span_key(node.span), point.id.clone());
3037            self.points.push(point);
3038        }
3039    }
3040
3041    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
3042        self.exit_function(node.span);
3043    }
3044
3045    fn enter_arrow_function_expression(
3046        &mut self,
3047        node: &mut ArrowFunctionExpression<'a>,
3048        context: &mut TraverseCtx<'a, ()>,
3049    ) {
3050        let point_span = function_point_span(node.span, context);
3051        let label = if point_span == node.span {
3052            function_label(None, context)
3053        } else {
3054            None
3055        };
3056        if self
3057            .source_sensitive_functions
3058            .contains(&span_key(node.span))
3059        {
3060            self.unsafe_function_depth += 1;
3061            return;
3062        }
3063        if self.pass == PointPass::Functions && !self.unsafe_context() {
3064            let point = self.point(point_span, "function", label);
3065            self.function_targets
3066                .insert(span_key(node.span), point.id.clone());
3067            self.points.push(point);
3068        }
3069    }
3070
3071    fn exit_arrow_function_expression(
3072        &mut self,
3073        node: &mut ArrowFunctionExpression<'a>,
3074        _context: &mut TraverseCtx<'a, ()>,
3075    ) {
3076        self.exit_function(node.span);
3077    }
3078
3079    fn enter_with_statement(
3080        &mut self,
3081        _node: &mut WithStatement<'a>,
3082        _context: &mut TraverseCtx<'a, ()>,
3083    ) {
3084        self.with_depth += 1;
3085    }
3086
3087    fn exit_with_statement(
3088        &mut self,
3089        _node: &mut WithStatement<'a>,
3090        _context: &mut TraverseCtx<'a, ()>,
3091    ) {
3092        self.with_depth -= 1;
3093    }
3094
3095    fn enter_ts_global_declaration(
3096        &mut self,
3097        _node: &mut TSGlobalDeclaration<'a>,
3098        _context: &mut TraverseCtx<'a, ()>,
3099    ) {
3100        self.ambient_depth += 1;
3101    }
3102
3103    fn exit_ts_global_declaration(
3104        &mut self,
3105        _node: &mut TSGlobalDeclaration<'a>,
3106        _context: &mut TraverseCtx<'a, ()>,
3107    ) {
3108        self.ambient_depth -= 1;
3109    }
3110
3111    fn enter_ts_module_declaration(
3112        &mut self,
3113        node: &mut TSModuleDeclaration<'a>,
3114        _context: &mut TraverseCtx<'a, ()>,
3115    ) {
3116        if node.declare {
3117            self.ambient_depth += 1;
3118        }
3119    }
3120
3121    fn exit_ts_module_declaration(
3122        &mut self,
3123        node: &mut TSModuleDeclaration<'a>,
3124        _context: &mut TraverseCtx<'a, ()>,
3125    ) {
3126        if node.declare {
3127            self.ambient_depth -= 1;
3128        }
3129    }
3130}
3131
3132/// Whether a JSX expression container holds something that can independently
3133/// fail to evaluate, and so is worth a coverage obligation of its own.
3134fn rendered_expression_is_measurable(expression: &Expression<'_>) -> bool {
3135    match expression {
3136        // Already measured where it is called.
3137        Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
3138        // Reached exactly when the enclosing statement is.
3139        Expression::Identifier(_)
3140        | Expression::ThisExpression(_)
3141        | Expression::StaticMemberExpression(_)
3142        | Expression::ComputedMemberExpression(_)
3143        | Expression::PrivateFieldExpression(_)
3144        | Expression::JSXElement(_)
3145        | Expression::JSXFragment(_) => false,
3146        // `user?.id` is a member access that happens to short-circuit; it is
3147        // reached with the statement like any other. `user?.load()` is not.
3148        Expression::ChainExpression(chain) => {
3149            matches!(chain.expression, ChainElement::CallExpression(_))
3150        }
3151        other => !other.is_literal(),
3152    }
3153}
3154
3155fn collect_points<'a>(
3156    allocator: &'a Allocator,
3157    program: &mut Program<'a>,
3158    source: &str,
3159    file: &str,
3160    source_sensitive_functions: &HashSet<SpanKey>,
3161    erased_imports: &HashSet<SpanKey>,
3162) -> PointAnalysis {
3163    let mut analysis = PointAnalysis::default();
3164    for pass in [PointPass::Statements, PointPass::Functions] {
3165        let mut collector = PointCollector {
3166            source,
3167            file,
3168            pass,
3169            points: Vec::new(),
3170            statement_targets: HashMap::new(),
3171            function_targets: HashMap::new(),
3172            rendered_targets: HashMap::new(),
3173            source_sensitive_functions,
3174            erased_imports,
3175            unsafe_function_depth: 0,
3176            with_depth: 0,
3177            ambient_depth: 0,
3178        };
3179        traverse_mut(&mut collector, allocator, program, Default::default(), ());
3180        analysis.points.extend(collector.points);
3181        analysis
3182            .statement_targets
3183            .extend(collector.statement_targets);
3184        analysis.function_targets.extend(collector.function_targets);
3185        analysis.rendered_targets.extend(collector.rendered_targets);
3186    }
3187    analysis
3188}
3189
3190impl<'s> SafetyScanner<'s> {
3191    fn new(source: &'s str, file: &'s str, scoping: &'s oxc_semantic::Scoping) -> Self {
3192        Self {
3193            source,
3194            file,
3195            scoping,
3196            source_reads: HashSet::new(),
3197            source_read_properties: HashSet::new(),
3198            source_sensitive_functions: HashSet::new(),
3199            function_limitations: Vec::new(),
3200            with_limitations: Vec::new(),
3201            dynamic_limitations: Vec::new(),
3202            unsafe_function_depth: 0,
3203            with_depth: 0,
3204        }
3205    }
3206
3207    fn limitation(
3208        &self,
3209        span: Span,
3210        kind: &str,
3211        suffix: &str,
3212        reason: &str,
3213    ) -> CandidateLimitation {
3214        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
3215        CandidateLimitation {
3216            id: stable_id(self.source, self.file, kind, span, suffix),
3217            kind: kind.to_string(),
3218            file: self.file.to_string(),
3219            line,
3220            column,
3221            source: source_slice(self.source, span).to_string(),
3222            reason: reason.to_string(),
3223        }
3224    }
3225
3226    fn enter_source_sensitive_function<State>(
3227        &mut self,
3228        span: Span,
3229        bindings: Vec<SymbolId>,
3230        context: &TraverseCtx<'_, State>,
3231    ) {
3232        // A function handed to a compile-time style macro never runs: the
3233        // bundler's plugin evaluates it while building and replaces the whole
3234        // call. Probing its body has nothing to measure and breaks the build
3235        // (StyleX rejects a block-bodied dynamic style). Leave it as source,
3236        // with no limitation: there is no runtime behavior to lose.
3237        if compile_time_macro_argument(context) {
3238            self.source_sensitive_functions.insert(span_key(span));
3239            self.unsafe_function_depth += 1;
3240            return;
3241        }
3242        let sensitive = observes_function_source(span, context)
3243            || bindings
3244                .iter()
3245                .any(|symbol| self.source_reads.contains(symbol))
3246            || property_name_of_function(context)
3247                .is_some_and(|name| self.source_read_properties.contains(&name));
3248        if sensitive {
3249            self.source_sensitive_functions.insert(span_key(span));
3250            let limitation = self.limitation(
3251                span,
3252                "semantic-safety",
3253                "function-source",
3254                "function body is left uninstrumented because this expression observes or coerces Function source text",
3255            );
3256            self.function_limitations.push(limitation);
3257            self.unsafe_function_depth += 1;
3258        }
3259    }
3260
3261    fn exit_source_sensitive_function(&mut self, span: Span) {
3262        if self.source_sensitive_functions.contains(&span_key(span)) {
3263            self.unsafe_function_depth -= 1;
3264        }
3265    }
3266
3267    fn is_unsafe_context(&self) -> bool {
3268        self.unsafe_function_depth > 0 || self.with_depth > 0
3269    }
3270}
3271
3272impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
3273    fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
3274        let bindings = bound_symbols(node.id.as_ref(), self.scoping, context);
3275        self.enter_source_sensitive_function(node.span, bindings, context);
3276    }
3277
3278    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
3279        self.exit_source_sensitive_function(node.span);
3280    }
3281
3282    fn enter_arrow_function_expression(
3283        &mut self,
3284        node: &mut ArrowFunctionExpression<'a>,
3285        context: &mut TraverseCtx<'a, ()>,
3286    ) {
3287        let bindings = bound_symbols(None, self.scoping, context);
3288        self.enter_source_sensitive_function(node.span, bindings, context);
3289    }
3290
3291    fn exit_arrow_function_expression(
3292        &mut self,
3293        node: &mut ArrowFunctionExpression<'a>,
3294        _context: &mut TraverseCtx<'a, ()>,
3295    ) {
3296        self.exit_source_sensitive_function(node.span);
3297    }
3298
3299    fn enter_with_statement(
3300        &mut self,
3301        node: &mut WithStatement<'a>,
3302        _context: &mut TraverseCtx<'a, ()>,
3303    ) {
3304        let limitation = self.limitation(
3305            node.span,
3306            "semantic-safety",
3307            "with-environment",
3308            "with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
3309        );
3310        self.with_limitations.push(limitation);
3311        self.with_depth += 1;
3312    }
3313
3314    fn exit_with_statement(
3315        &mut self,
3316        _node: &mut WithStatement<'a>,
3317        _context: &mut TraverseCtx<'a, ()>,
3318    ) {
3319        self.with_depth -= 1;
3320    }
3321
3322    fn enter_call_expression(
3323        &mut self,
3324        node: &mut CallExpression<'a>,
3325        _context: &mut TraverseCtx<'a, ()>,
3326    ) {
3327        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
3328            return;
3329        }
3330        let limitation = self.limitation(
3331            node.span,
3332            "dynamic-code",
3333            "eval",
3334            "eval-generated source has no stable pre-run coverage denominator",
3335        );
3336        self.dynamic_limitations.push(limitation);
3337    }
3338
3339    fn enter_new_expression(
3340        &mut self,
3341        node: &mut NewExpression<'a>,
3342        _context: &mut TraverseCtx<'a, ()>,
3343    ) {
3344        if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
3345            return;
3346        }
3347        let limitation = self.limitation(
3348            node.span,
3349            "dynamic-code",
3350            "Function",
3351            "Function-generated source has no stable pre-run coverage denominator",
3352        );
3353        self.dynamic_limitations.push(limitation);
3354    }
3355}
3356
3357fn analyze_safety<'a>(
3358    allocator: &'a Allocator,
3359    program: &mut Program<'a>,
3360    source: &str,
3361    file: &str,
3362) -> SafetyAnalysis {
3363    // oxc_traverse uses resolved lexical scope IDs while walking ancestry.
3364    // Building semantics here initializes those IDs without changing the AST.
3365    let scoping = SemanticBuilder::new()
3366        .build(program)
3367        .semantic
3368        .into_scoping();
3369    let mut reads = SourceReads {
3370        scoping: &scoping,
3371        symbols: HashSet::new(),
3372        properties: HashSet::new(),
3373    };
3374    reads.visit_program(program);
3375    let mut scanner = SafetyScanner::new(source, file, &scoping);
3376    scanner.source_reads = reads.symbols;
3377    scanner.source_read_properties = reads.properties;
3378    traverse_mut(&mut scanner, allocator, program, Default::default(), ());
3379    let mut semantic_limitations = scanner.function_limitations;
3380    semantic_limitations.extend(scanner.with_limitations);
3381    SafetyAnalysis {
3382        source_sensitive_functions: scanner.source_sensitive_functions,
3383        semantic_limitations,
3384        dynamic_limitations: scanner.dynamic_limitations,
3385    }
3386}
3387
3388fn span_key(span: Span) -> SpanKey {
3389    (span.start, span.end)
3390}
3391
3392fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
3393    matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
3394}
3395
3396fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
3397    match pattern {
3398        BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
3399        _ => None,
3400    }
3401}
3402
3403fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
3404    match expression {
3405        Expression::ArrowFunctionExpression(_) => true,
3406        Expression::FunctionExpression(function) => function.id.is_none(),
3407        Expression::ClassExpression(class) => class.id.is_none(),
3408        Expression::ParenthesizedExpression(expression) => {
3409            expression_is_anonymous_definition(&expression.expression)
3410        }
3411        Expression::TSAsExpression(expression) => {
3412            expression_is_anonymous_definition(&expression.expression)
3413        }
3414        Expression::TSSatisfiesExpression(expression) => {
3415            expression_is_anonymous_definition(&expression.expression)
3416        }
3417        Expression::TSTypeAssertion(expression) => {
3418            expression_is_anonymous_definition(&expression.expression)
3419        }
3420        Expression::TSNonNullExpression(expression) => {
3421            expression_is_anonymous_definition(&expression.expression)
3422        }
3423        _ => false,
3424    }
3425}
3426
3427/// Takes a type assertion off an assignment target before the program is
3428/// printed.
3429///
3430/// The printer writes `(buffer[0] as string) += child` without its
3431/// parentheses, and `buffer[0] as string += child` does not parse: hono's
3432/// `src/jsx/base.ts` and `src/helper/html/index.ts` came out as files neither
3433/// TypeScript nor a bundler could read. An assertion on a target says nothing
3434/// at run time, and the instrumented file is not type-checked, so the target
3435/// is printed without it.
3436struct AssertedTargetTransformer<'a> {
3437    ast: AstBuilder<'a>,
3438}
3439
3440impl<'a> AssertedTargetTransformer<'a> {
3441    fn target(
3442        expression: Expression<'a>,
3443    ) -> Result<oxc_ast::ast::SimpleAssignmentTarget<'a>, Expression<'a>> {
3444        Ok(match expression {
3445            Expression::Identifier(identifier) => {
3446                oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(identifier)
3447            }
3448            Expression::ComputedMemberExpression(member) => {
3449                oxc_ast::ast::SimpleAssignmentTarget::ComputedMemberExpression(member)
3450            }
3451            Expression::StaticMemberExpression(member) => {
3452                oxc_ast::ast::SimpleAssignmentTarget::StaticMemberExpression(member)
3453            }
3454            Expression::PrivateFieldExpression(member) => {
3455                oxc_ast::ast::SimpleAssignmentTarget::PrivateFieldExpression(member)
3456            }
3457            Expression::TSAsExpression(asserted) => {
3458                oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted)
3459            }
3460            Expression::TSSatisfiesExpression(asserted) => {
3461                oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted)
3462            }
3463            Expression::TSNonNullExpression(asserted) => {
3464                oxc_ast::ast::SimpleAssignmentTarget::TSNonNullExpression(asserted)
3465            }
3466            Expression::TSTypeAssertion(asserted) => {
3467                oxc_ast::ast::SimpleAssignmentTarget::TSTypeAssertion(asserted)
3468            }
3469            Expression::ParenthesizedExpression(parenthesized) => {
3470                return Self::target(parenthesized.unbox().expression);
3471            }
3472            other => return Err(other),
3473        })
3474    }
3475}
3476
3477impl<'a> VisitMut<'a> for AssertedTargetTransformer<'a> {
3478    fn visit_simple_assignment_target(
3479        &mut self,
3480        target: &mut oxc_ast::ast::SimpleAssignmentTarget<'a>,
3481    ) {
3482        loop {
3483            let inner = match target {
3484                oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
3485                    &mut asserted.expression
3486                }
3487                oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
3488                    &mut asserted.expression
3489                }
3490                _ => break,
3491            };
3492            match Self::target(inner.take_in(self.ast.allocator)) {
3493                Ok(bare) => *target = bare,
3494                Err(kept) => {
3495                    // Not a target the grammar allows; print it as it came.
3496                    match target {
3497                        oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
3498                            asserted.expression = kept;
3499                        }
3500                        oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
3501                            asserted.expression = kept;
3502                        }
3503                        _ => unreachable!("the target was matched above"),
3504                    }
3505                    break;
3506                }
3507            }
3508        }
3509        walk_mut::walk_simple_assignment_target(self, target);
3510    }
3511}
3512
3513/// The module a directly run TypeScript file names its helpers' types from.
3514/// The frontend replaces it with the path to the runtime beside the file.
3515pub const RUNTIME_TYPES_MODULE: &str = "virtual:supercov-helper-types";
3516
3517/// `typeof import("…").helper`, as the annotation of the variable a helper is
3518/// bound to.
3519fn runtime_helper_type<'a>(
3520    allocator: &'a Allocator,
3521    source_type: SourceType,
3522    helper: &str,
3523) -> Option<oxc_allocator::Box<'a, oxc_ast::ast::TSTypeAnnotation<'a>>> {
3524    let text = allocator.alloc_str(&format!(
3525        "var helper: typeof import(\"{RUNTIME_TYPES_MODULE}\").{helper};"
3526    ));
3527    let mut parsed = Parser::new(allocator, text, source_type).parse();
3528    if !parsed.errors.is_empty() {
3529        return None;
3530    }
3531    match parsed.program.body.first_mut()? {
3532        Statement::VariableDeclaration(declaration) => {
3533            declaration.declarations.first_mut()?.type_annotation.take()
3534        }
3535        _ => None,
3536    }
3537}
3538
3539struct AssignmentNameSafetyTransformer<'a> {
3540    ast: AstBuilder<'a>,
3541    parenthesized_assignment_value: String,
3542}
3543
3544impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
3545    fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
3546        walk_mut::walk_assignment_expression(self, assignment);
3547        let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
3548            return;
3549        };
3550        if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
3551            || assignment.span.start == identifier.span.start
3552            || !expression_is_anonymous_definition(&assignment.right)
3553        {
3554            return;
3555        }
3556        let right = assignment.right.take_in(self.ast.allocator);
3557        assignment.right = self.ast.expression_call(
3558            Span::default(),
3559            self.ast.expression_identifier(
3560                Span::default(),
3561                self.ast.ident(&self.parenthesized_assignment_value),
3562            ),
3563            NONE,
3564            self.ast.vec_from_array([
3565                Argument::from(right),
3566                Argument::from(self.ast.expression_string_literal(
3567                    Span::default(),
3568                    identifier.name,
3569                    None,
3570                )),
3571            ]),
3572            false,
3573        );
3574    }
3575}
3576
3577/// Compile-time style macros whose arguments the bundler consumes at build
3578/// time. Only the namespaced form (`stylex.create(...)`) is recognized: it is
3579/// how the StyleX documentation and its Vite/Babel plugins expect the API to
3580/// be used, and it needs no binding resolution here.
3581const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
3582    "create",
3583    "createTheme",
3584    "defineConsts",
3585    "defineVars",
3586    "firstThatWorks",
3587    "keyframes",
3588    "positionTry",
3589    "viewTransitionClass",
3590];
3591
3592fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
3593    context.ancestors().any(|ancestor| {
3594        let Ancestor::CallExpressionArguments(parent) = ancestor else {
3595            return false;
3596        };
3597        let Expression::StaticMemberExpression(member) = parent.callee() else {
3598            return false;
3599        };
3600        let Expression::Identifier(object) = &member.object else {
3601            return false;
3602        };
3603        object.name == "stylex"
3604            && COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
3605    })
3606}
3607
3608fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
3609    let mut child_end = span.end;
3610    for ancestor in context.ancestors() {
3611        match ancestor {
3612            Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
3613            Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
3614            Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
3615            Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
3616            Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
3617            Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
3618            Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
3619            Ancestor::LogicalExpressionLeft(parent) => {
3620                child_end = parent.span().end;
3621            }
3622            Ancestor::LogicalExpressionRight(parent) => {
3623                child_end = parent.span().end;
3624            }
3625            Ancestor::SequenceExpressionExpressions(parent) => {
3626                if child_end != parent.span().end {
3627                    return false;
3628                }
3629                child_end = parent.span().end;
3630            }
3631            Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
3632            Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
3633            Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
3634            Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
3635            Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
3636            Ancestor::ComputedMemberExpressionExpression(_) => return true,
3637            Ancestor::StaticMemberExpressionObject(parent) => {
3638                return parent.property().name == "toString";
3639            }
3640            Ancestor::CallExpressionArguments(parent) => {
3641                return expression_is_identifier(parent.callee(), "String");
3642            }
3643            Ancestor::TemplateLiteralExpressions(_) => return true,
3644            Ancestor::BinaryExpressionLeft(parent) => {
3645                return matches!(
3646                    parent.operator(),
3647                    BinaryOperator::Addition
3648                        | BinaryOperator::LessThan
3649                        | BinaryOperator::LessEqualThan
3650                        | BinaryOperator::GreaterThan
3651                        | BinaryOperator::GreaterEqualThan
3652                );
3653            }
3654            Ancestor::BinaryExpressionRight(parent) => {
3655                return matches!(
3656                    parent.operator(),
3657                    BinaryOperator::Addition
3658                        | BinaryOperator::LessThan
3659                        | BinaryOperator::LessEqualThan
3660                        | BinaryOperator::GreaterThan
3661                        | BinaryOperator::GreaterEqualThan
3662                );
3663            }
3664            _ => return false,
3665        }
3666    }
3667    false
3668}
3669
3670pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3671    let _positions = PositionScope::enter();
3672    let elide_type_imports = false;
3673    let source_type =
3674        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3675    let allocator = Allocator::default();
3676    let mut parsed = Parser::new(&allocator, source, source_type).parse();
3677    if !parsed.errors.is_empty() {
3678        return Err(CandidateError::Parse(
3679            parsed
3680                .errors
3681                .iter()
3682                .map(|error| {
3683                    parse_message(
3684                        source,
3685                        &error.message,
3686                        error
3687                            .labels
3688                            .as_ref()
3689                            .and_then(|labels| labels.first())
3690                            .map(|label| label.offset()),
3691                    )
3692                })
3693                .collect(),
3694        ));
3695    }
3696
3697    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3698    let erased = erased_imports(&parsed.program, elide_type_imports);
3699    let excluded_statements = parsed
3700        .program
3701        .body
3702        .iter()
3703        .filter(|s| erased.contains(&span_key(s.span())))
3704        .map(|s| {
3705            let span = s.span();
3706            let (line, column) = line_and_utf16_column(source, span.start as usize);
3707            CandidatePoint {
3708                id: stable_id(source, file, "statement", span, ""),
3709                kind: "statement".into(),
3710                file: file.into(),
3711                line,
3712                column,
3713                source: source_slice(source, span).into(),
3714                label: Some("typescript-import-erasure".into()),
3715            }
3716        })
3717        .collect();
3718    let point_analysis = collect_points(
3719        &allocator,
3720        &mut parsed.program,
3721        source,
3722        file,
3723        &safety.source_sensitive_functions,
3724        &erased,
3725    );
3726    let mut collector = DecisionCollector {
3727        source,
3728        file,
3729        decisions: Vec::new(),
3730        decision_vector_counts: Vec::new(),
3731        decision_logical_nodes: HashSet::new(),
3732        source_sensitive_functions: &safety.source_sensitive_functions,
3733        value_decisions: HashMap::new(),
3734        logical_root: None,
3735        value_logical_nodes: HashSet::new(),
3736    };
3737    collector.visit_program(&parsed.program);
3738    let optional_analysis = collect_optional_member_branches(
3739        &allocator,
3740        &mut parsed.program,
3741        source,
3742        file,
3743        &safety.source_sensitive_functions,
3744    );
3745    let call_analysis = collect_optional_call_branches(
3746        &allocator,
3747        &mut parsed.program,
3748        source,
3749        file,
3750        &safety.source_sensitive_functions,
3751    );
3752    let assignment_analysis = collect_logical_assignment_branches(
3753        &allocator,
3754        &mut parsed.program,
3755        source,
3756        file,
3757        &safety.source_sensitive_functions,
3758    );
3759    let default_analysis = collect_default_branches(
3760        &allocator,
3761        &mut parsed.program,
3762        source,
3763        file,
3764        &safety.source_sensitive_functions,
3765    );
3766    let extended_analysis = collect_extended_branches(
3767        &allocator,
3768        &mut parsed.program,
3769        source,
3770        file,
3771        &safety.source_sensitive_functions,
3772    );
3773    let logical_analysis = collect_logical_value_branches(
3774        &allocator,
3775        &mut parsed.program,
3776        source,
3777        file,
3778        &collector.decision_logical_nodes,
3779        &safety.source_sensitive_functions,
3780    );
3781    let switch_analysis = collect_switch_branches(
3782        &allocator,
3783        &mut parsed.program,
3784        source,
3785        file,
3786        &safety.source_sensitive_functions,
3787    );
3788    let mut branches = optional_analysis.branches;
3789    branches.extend(call_analysis.branches);
3790    branches.extend(assignment_analysis.branches);
3791    branches.extend(default_analysis.branches);
3792    branches.extend(extended_analysis.branches);
3793    branches.extend(logical_analysis.branches);
3794    branches.extend(switch_analysis.branches);
3795    AssertedTargetTransformer {
3796        ast: AstBuilder::new(&allocator),
3797    }
3798    .visit_program(&mut parsed.program);
3799    let (generated, map) =
3800        generate_candidate(&parsed.program, file, &safety.source_sensitive_functions)?;
3801    Ok(CandidateOutput {
3802        engine: "rust-oxc".to_string(),
3803        complete: false,
3804        supported_surface: "control-decision-manifest-v1".to_string(),
3805        code: generated,
3806        map,
3807        decisions: collector.decisions,
3808        points: point_analysis.points,
3809        excluded_statements,
3810        branches,
3811        runtime: None,
3812        coverage_limitations: {
3813            let mut limitations = safety.semantic_limitations;
3814            limitations.extend(call_analysis.limitations);
3815            limitations.extend(default_analysis.limitations);
3816            limitations.extend(safety.dynamic_limitations);
3817            limitations
3818        },
3819        limitations: vec![
3820            "candidate emits metadata only; use the private differential transform for probes"
3821                .to_string(),
3822            "coverage points, value branches, and extended branch obligations are not included"
3823                .to_string(),
3824        ],
3825    })
3826}
3827
3828fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
3829    match value {
3830        serde_json::Value::Null => ast.expression_null_literal(Span::default()),
3831        serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
3832        serde_json::Value::Number(value) => ast.expression_numeric_literal(
3833            Span::default(),
3834            value
3835                .as_f64()
3836                .expect("coverage registration numbers must fit JavaScript"),
3837            None,
3838            NumberBase::Decimal,
3839        ),
3840        serde_json::Value::String(value) => {
3841            ast.expression_string_literal(Span::default(), ast.str(value), None)
3842        }
3843        serde_json::Value::Array(values) => ast.expression_array(
3844            Span::default(),
3845            ast.vec_from_iter(
3846                values
3847                    .iter()
3848                    .map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
3849            ),
3850        ),
3851        serde_json::Value::Object(properties) => ast.expression_object(
3852            Span::default(),
3853            ast.vec_from_iter(properties.iter().map(|(key, value)| {
3854                ast.object_property_kind_object_property(
3855                    Span::default(),
3856                    PropertyKind::Init,
3857                    ast.property_key_static_identifier(Span::default(), ast.ident(key)),
3858                    json_expression(ast, value),
3859                    false,
3860                    false,
3861                    false,
3862                )
3863            })),
3864        ),
3865    }
3866}
3867
3868/// Instrument JavaScript with the complete frozen v1 denominator and probe-v2
3869/// runtime ABI. The source-transform contract is complete and independently
3870/// conformance-tested; selection of the Rust engine remains private until the
3871/// Phase 4 CLI/orchestration cutover is complete.
3872pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3873    instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None, false)
3874}
3875
3876/// Emit code for an isolated source-executing workspace. Unlike the module-
3877/// import form, this has no virtual module dependency: the generated
3878/// Node preload installs the frozen runtime on this global before user modules
3879/// evaluate.
3880pub fn instrument_direct_candidate(
3881    source: &str,
3882    file: &str,
3883) -> Result<CandidateOutput, CandidateError> {
3884    instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None, false)
3885}
3886
3887pub fn instrument_with_import_policy(
3888    source: &str,
3889    file: &str,
3890    capability_wrapper: &str,
3891    elide_type_imports: bool,
3892) -> Result<CandidateOutput, CandidateError> {
3893    instrument_candidate_with_binding(
3894        source,
3895        file,
3896        RuntimeBinding::DirectGlobal,
3897        Some(capability_wrapper),
3898        elide_type_imports,
3899    )
3900}
3901
3902fn instrument_candidate_with_binding(
3903    source: &str,
3904    file: &str,
3905    runtime_binding: RuntimeBinding,
3906    capability_wrapper: Option<&str>,
3907    elide_type_imports: bool,
3908) -> Result<CandidateOutput, CandidateError> {
3909    let _positions = PositionScope::enter();
3910    let source_type =
3911        project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3912    let allocator = Allocator::default();
3913    let mut parsed = Parser::new(&allocator, source, source_type).parse();
3914    if !parsed.errors.is_empty() {
3915        return Err(CandidateError::Parse(
3916            parsed
3917                .errors
3918                .iter()
3919                .map(|error| {
3920                    parse_message(
3921                        source,
3922                        &error.message,
3923                        error
3924                            .labels
3925                            .as_ref()
3926                            .and_then(|labels| labels.first())
3927                            .map(|label| label.offset()),
3928                    )
3929                })
3930                .collect(),
3931        ));
3932    }
3933
3934    let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3935    let erased = erased_imports(&parsed.program, elide_type_imports);
3936    let excluded_statements = parsed
3937        .program
3938        .body
3939        .iter()
3940        .filter(|s| erased.contains(&span_key(s.span())))
3941        .map(|s| {
3942            let span = s.span();
3943            let (line, column) = line_and_utf16_column(source, span.start as usize);
3944            CandidatePoint {
3945                id: stable_id(source, file, "statement", span, ""),
3946                kind: "statement".into(),
3947                file: file.into(),
3948                line,
3949                column,
3950                source: source_slice(source, span).into(),
3951                label: Some("typescript-import-erasure".into()),
3952            }
3953        })
3954        .collect();
3955    let point_analysis = collect_points(
3956        &allocator,
3957        &mut parsed.program,
3958        source,
3959        file,
3960        &safety.source_sensitive_functions,
3961        &erased,
3962    );
3963    let mut collector = DecisionCollector {
3964        source,
3965        file,
3966        decisions: Vec::new(),
3967        decision_vector_counts: Vec::new(),
3968        decision_logical_nodes: HashSet::new(),
3969        source_sensitive_functions: &safety.source_sensitive_functions,
3970        value_decisions: HashMap::new(),
3971        logical_root: None,
3972        value_logical_nodes: HashSet::new(),
3973    };
3974    collector.visit_program(&parsed.program);
3975    let optional_analysis = collect_optional_member_branches(
3976        &allocator,
3977        &mut parsed.program,
3978        source,
3979        file,
3980        &safety.source_sensitive_functions,
3981    );
3982    let call_analysis = collect_optional_call_branches(
3983        &allocator,
3984        &mut parsed.program,
3985        source,
3986        file,
3987        &safety.source_sensitive_functions,
3988    );
3989    let assignment_analysis = collect_logical_assignment_branches(
3990        &allocator,
3991        &mut parsed.program,
3992        source,
3993        file,
3994        &safety.source_sensitive_functions,
3995    );
3996    let default_analysis = collect_default_branches(
3997        &allocator,
3998        &mut parsed.program,
3999        source,
4000        file,
4001        &safety.source_sensitive_functions,
4002    );
4003    let extended_analysis = collect_extended_branches(
4004        &allocator,
4005        &mut parsed.program,
4006        source,
4007        file,
4008        &safety.source_sensitive_functions,
4009    );
4010    let logical_analysis = collect_logical_value_branches(
4011        &allocator,
4012        &mut parsed.program,
4013        source,
4014        file,
4015        &collector.decision_logical_nodes,
4016        &safety.source_sensitive_functions,
4017    );
4018    let switch_analysis = collect_switch_branches(
4019        &allocator,
4020        &mut parsed.program,
4021        source,
4022        file,
4023        &safety.source_sensitive_functions,
4024    );
4025    let mut branches = optional_analysis.branches;
4026    branches.extend(call_analysis.branches.clone());
4027    branches.extend(assignment_analysis.branches);
4028    branches.extend(default_analysis.branches.clone());
4029    branches.extend(extended_analysis.branches.clone());
4030    branches.extend(logical_analysis.branches);
4031    branches.extend(switch_analysis.branches.clone());
4032
4033    // Direct-runtime output can be served as several classic scripts in one
4034    // document, and every classic script shares the page's global lexical
4035    // scope: two files declaring `const __supercovMcdcBegin` is a SyntaxError
4036    // on the second, and a shared probe-file binding would credit one file's
4037    // statements to another. Names are made unique per file, and declared with
4038    // `var` so a script included twice is still legal, as it was before.
4039    let (namespace, runtime_declaration_kind) = if runtime_binding == RuntimeBinding::DirectGlobal {
4040        (
4041            format!(
4042                "_{}",
4043                &stable_id("", file, "runtime", Span::default(), "")[..8]
4044            ),
4045            VariableDeclarationKind::Var,
4046        )
4047    } else {
4048        (String::new(), VariableDeclarationKind::Const)
4049    };
4050    let mut names = CandidateNames::within(source, &namespace);
4051    let mcdc_begin = names.allocate("__supercovMcdcBegin");
4052    let mcdc_condition = names.allocate("__supercovMcdcCondition");
4053    let mcdc_end = names.allocate("__supercovMcdcEnd");
4054    let coverage_hit = names.allocate("__supercovCoverageHit");
4055    let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
4056    let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
4057    let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
4058    let probe_file_v2 = names.allocate("__supercovProbeFileV2");
4059    let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
4060    let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
4061    let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
4062    let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
4063    let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
4064    let with_request_phase = names.allocate("__supercovWithRequestPhase");
4065    let optional_select_v2 = names.allocate("__supercovOptionalSelectV2");
4066    let select_short_v2 = names.allocate("__supercovSelectShortV2");
4067    let select_right_v2 = names.allocate("__supercovSelectRightV2");
4068    let select_named_right_v2 = names.allocate("__supercovSelectNamedRightV2");
4069    let select_assign_end_v2 = names.allocate("__supercovSelectAssignEndV2");
4070    let select_path_v2 = names.allocate("__supercovSelectPathV2");
4071    let select_end2_v2 = names.allocate("__supercovSelectEnd2V2");
4072    let select_end_v2 = names.allocate("__supercovSelectEndV2");
4073    let select_next_v2 = names.allocate("__supercovSelectNextV2");
4074    let rendered_value_v2 = names.allocate("__supercovRenderedValueV2");
4075    let optional_call_end_v2 = names.allocate("__supercovOptionalCallEndV2");
4076    let default_selected_v2 = names.allocate("__supercovDefaultSelectedV2");
4077    let default_entered_v2 = names.allocate("__supercovDefaultEnteredV2");
4078    let typescript = parsed.program.source_type.is_typescript();
4079    let ast = AstBuilder::new(&allocator);
4080    let mut assignment_name_safety = AssignmentNameSafetyTransformer {
4081        ast,
4082        parenthesized_assignment_value: parenthesized_assignment_value.clone(),
4083    };
4084    assignment_name_safety.visit_program(&mut parsed.program);
4085    let point_indices = point_analysis
4086        .points
4087        .iter()
4088        .enumerate()
4089        .map(|(index, point)| (point.id.clone(), index))
4090        .collect::<HashMap<_, _>>();
4091    // A default's two outcomes are V2 points after the statement points, and
4092    // its pending count a slot of the file's `pendingDefaults`, both in source
4093    // order. lru-cache's `set` and `get` destructure options with defaults, and
4094    // the string-keyed count behind each was a third of a hot loop.
4095    let mut default_sites = default_analysis
4096        .parameter_targets
4097        .iter()
4098        .chain(&default_analysis.binding_targets)
4099        .map(|(span, target)| (*span, target.default_id.clone(), target.provided_id.clone()))
4100        .collect::<Vec<_>>();
4101    default_sites.sort();
4102    let mut default_point_ids = Vec::with_capacity(default_sites.len() * 2);
4103    let default_slots = default_sites
4104        .into_iter()
4105        .enumerate()
4106        .map(|(slot, (_, default_id, provided_id))| {
4107            let default_point = point_analysis.points.len() + default_point_ids.len();
4108            default_point_ids.push(default_id.clone());
4109            default_point_ids.push(provided_id);
4110            (
4111                default_id,
4112                DefaultSlot {
4113                    slot,
4114                    default_point,
4115                    provided_point: default_point + 1,
4116                },
4117            )
4118        })
4119        .collect::<HashMap<_, _>>();
4120    let default_count = default_slots.len();
4121    let mut extra = ExtraPoints {
4122        base: point_analysis.points.len() + default_point_ids.len(),
4123        ids: Vec::new(),
4124    };
4125    let selection_first = extra.next();
4126    let selection_targets = extra.allocate(&logical_analysis.logical_targets, |(short, right)| {
4127        vec![short.clone(), short.clone(), right.clone(), right.clone()]
4128    });
4129    // A logical assignment's outcomes are a selection's four points too, next
4130    // to the others, so the runtime records its per-operand outcomes the same.
4131    let assignment_targets = extra.allocate(&assignment_analysis.targets, |(short, right)| {
4132        vec![short.clone(), short.clone(), right.clone(), right.clone()]
4133    });
4134    let selection_count = selection_targets.len() + assignment_targets.len();
4135    let optional_member_targets = extra
4136        .allocate(&optional_analysis.targets, |(short, continued)| {
4137            vec![short.clone(), continued.clone()]
4138        });
4139    let optional_call_sites = extra.allocate(&call_analysis.sites, |(short, continued)| {
4140        vec![short.clone(), continued.clone()]
4141    });
4142    let loop_targets = extra.allocate(&extended_analysis.loop_targets, |target| {
4143        vec![target.first_id.clone(), target.second_id.clone()]
4144    });
4145    let try_targets = extra.allocate(&extended_analysis.try_targets, |target| {
4146        vec![target.first_id.clone(), target.second_id.clone()]
4147    });
4148    let statement_targets = point_analysis
4149        .statement_targets
4150        .into_iter()
4151        .map(|(span, ids)| {
4152            (
4153                span,
4154                ids.into_iter()
4155                    .map(|id| PointTarget {
4156                        index: *point_indices
4157                            .get(&id)
4158                            .expect("statement point must have a global index"),
4159                    })
4160                    .collect(),
4161            )
4162        })
4163        .collect();
4164    let function_targets = point_analysis
4165        .function_targets
4166        .into_iter()
4167        .map(|(span, id)| {
4168            (
4169                span,
4170                PointTarget {
4171                    index: *point_indices
4172                        .get(&id)
4173                        .expect("function point must have a global index"),
4174                },
4175            )
4176        })
4177        .collect();
4178    let rendered_targets = point_analysis
4179        .rendered_targets
4180        .into_iter()
4181        .map(|(span, id)| {
4182            (
4183                span,
4184                PointTarget {
4185                    index: *point_indices
4186                        .get(&id)
4187                        .expect("rendered point must have a global index"),
4188                },
4189            )
4190        })
4191        .collect();
4192    let mut statement_transformer = StatementProbeTransformer {
4193        ast,
4194        coverage_hit_v2: coverage_hit_v2.clone(),
4195        probe_file_v2: probe_file_v2.clone(),
4196        targets: statement_targets,
4197        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4198    };
4199    statement_transformer.visit_program(&mut parsed.program);
4200    let mut function_transformer = FunctionProbeTransformer {
4201        ast,
4202        coverage_hit_v2: coverage_hit_v2.clone(),
4203        probe_file_v2: probe_file_v2.clone(),
4204        targets: function_targets,
4205        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4206    };
4207    function_transformer.visit_program(&mut parsed.program);
4208    let mut rendered_transformer = RenderedExpressionTransformer {
4209        ast,
4210        rendered_value_v2: rendered_value_v2.clone(),
4211        probe_file_v2: probe_file_v2.clone(),
4212        targets: rendered_targets,
4213        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4214    };
4215    rendered_transformer.visit_program(&mut parsed.program);
4216    // Made first so a member right after an optional call can read the
4217    // call's frame; it rewrites the program after the members.
4218    let mut call_transformer = OptionalCallTransformer::new(
4219        ast,
4220        source,
4221        optional_call_end_v2.clone(),
4222        probe_file_v2.clone(),
4223        typescript,
4224        optional_call_sites,
4225        call_analysis.roots,
4226        safety.source_sensitive_functions.clone(),
4227    );
4228    let call_frames = optional_analysis
4229        .after_call
4230        .iter()
4231        .filter_map(|(member, call)| {
4232            call_transformer
4233                .sites
4234                .get(call)
4235                .map(|site| (*member, site.frame.clone()))
4236        })
4237        .collect();
4238    let mut optional_transformer = OptionalMemberTransformer {
4239        ast,
4240        optional_select_v2: optional_select_v2.clone(),
4241        probe_file_v2: probe_file_v2.clone(),
4242        targets: optional_member_targets,
4243        links: optional_analysis.links,
4244        call_frames,
4245        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4246    };
4247    optional_transformer.visit_program(&mut parsed.program);
4248    call_transformer.visit_program(&mut parsed.program);
4249    let mut default_transformer = DefaultTransformer {
4250        ast,
4251        default_selected_v2: default_selected_v2.clone(),
4252        default_entered_v2: default_entered_v2.clone(),
4253        probe_file_v2: probe_file_v2.clone(),
4254        slots: default_slots,
4255        parameter_targets: default_analysis.parameter_targets,
4256        binding_targets: default_analysis.binding_targets,
4257        function_entries: Vec::new(),
4258        declaration_entries: HashMap::new(),
4259        active_declaration: Vec::new(),
4260        parameter_pattern_depth: 0,
4261        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4262        typescript,
4263    };
4264    default_transformer.visit_program(&mut parsed.program);
4265    let mut extended_transformer = ExtendedTransformer {
4266        ast,
4267        coverage_hit_v2: coverage_hit_v2.clone(),
4268        probe_file_v2: probe_file_v2.clone(),
4269        typescript,
4270        try_targets,
4271        loop_targets,
4272        names: CandidateNames::within(source, &namespace),
4273        scope_declarations: Vec::new(),
4274        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4275    };
4276    extended_transformer.visit_program(&mut parsed.program);
4277    let mut transformer = ControlProbeV2Transformer {
4278        ast,
4279        decisions: &collector.decisions,
4280        value_decisions: &collector.value_decisions,
4281        pending_values: HashMap::new(),
4282        mcdc_begin: mcdc_begin.clone(),
4283        mcdc_condition: mcdc_condition.clone(),
4284        mcdc_end: mcdc_end.clone(),
4285        mcdc_end_v2: mcdc_end_v2.clone(),
4286        probe_file_v2: probe_file_v2.clone(),
4287        names,
4288        scope_declarations: Vec::new(),
4289        decision_index: 0,
4290        parameter_depth: 0,
4291        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4292        after_statement: None,
4293        label_body: false,
4294        inline_names: None,
4295    };
4296    transformer.visit_program(&mut parsed.program);
4297    let mut logical_transformer = LogicalValueTransformer {
4298        ast,
4299        select_short_v2: select_short_v2.clone(),
4300        select_right_v2: select_right_v2.clone(),
4301        select_named_right_v2: select_named_right_v2.clone(),
4302        select_assign_end_v2: select_assign_end_v2.clone(),
4303        select_path_v2: select_path_v2.clone(),
4304        select_end2_v2: select_end2_v2.clone(),
4305        select_end_v2: select_end_v2.clone(),
4306        select_next_v2: select_next_v2.clone(),
4307        coverage_hit_v2: coverage_hit_v2.clone(),
4308        probe_file_v2: probe_file_v2.clone(),
4309        typescript,
4310        names: CandidateNames::within(source, &namespace),
4311        scope_declarations: Vec::new(),
4312        frameless: 0,
4313        trees: Vec::new(),
4314        aliasing: false,
4315        after_declaration: Vec::new(),
4316        logical_targets: selection_targets,
4317        assignment_targets,
4318        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4319    };
4320    logical_transformer.visit_program(&mut parsed.program);
4321    let mut switch_transformer = SwitchTransformer {
4322        ast,
4323        coverage_hit: coverage_hit.clone(),
4324        targets: switch_analysis.targets,
4325        names: CandidateNames::within(source, &namespace),
4326        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4327    };
4328    switch_transformer.visit_program(&mut parsed.program);
4329    let mut route_transformer = RouteRequestPhaseTransformer {
4330        ast,
4331        file,
4332        with_request_phase: with_request_phase.clone(),
4333        used: false,
4334        names: CandidateNames::within(source, &namespace),
4335    };
4336    route_transformer.transform_program(&mut parsed.program);
4337    let mut request_transformer = RequestPhaseTransformer {
4338        ast,
4339        with_request_phase: with_request_phase.clone(),
4340        used: route_transformer.used,
4341        source_sensitive_functions: safety.source_sensitive_functions.clone(),
4342    };
4343    request_transformer.visit_program(&mut parsed.program);
4344    let uses_request_phase = request_transformer.used;
4345
4346    let registration = serde_json::json!({
4347        "decisions": &collector.decisions,
4348        "pointIds": point_analysis.points.iter().map(|point| &point.id).chain(&default_point_ids).chain(&extra.ids).collect::<Vec<_>>(),
4349        "decisionVectorCounts": &collector.decision_vector_counts,
4350        "defaultCount": default_count,
4351        "selectionPoints": [selection_first, selection_count],
4352        "selectionTrees": &logical_transformer.trees,
4353    });
4354    let registration_call = ast.expression_call(
4355        Span::default(),
4356        ast.expression_identifier(Span::default(), ast.ident(&register_probe_v2)),
4357        NONE,
4358        ast.vec1(Argument::from(json_expression(ast, &registration))),
4359        false,
4360    );
4361    parsed.program.body.insert(
4362        0,
4363        Statement::VariableDeclaration(ast.alloc_variable_declaration(
4364            Span::default(),
4365            runtime_declaration_kind,
4366            ast.vec1(ast.variable_declarator(
4367                Span::default(),
4368                runtime_declaration_kind,
4369                ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
4370                NONE,
4371                Some(registration_call),
4372                false,
4373            )),
4374            false,
4375        )),
4376    );
4377    let mut runtime_imports = vec![
4378        ("mcdcBegin", &mcdc_begin),
4379        ("mcdcCondition", &mcdc_condition),
4380        ("mcdcEnd", &mcdc_end),
4381        ("coverageHit", &coverage_hit),
4382        ("registerProbeV2", &register_probe_v2),
4383        ("mcdcEndV2", &mcdc_end_v2),
4384        ("coverageHitV2", &coverage_hit_v2),
4385        (
4386            "parenthesizedAssignmentValue",
4387            &parenthesized_assignment_value,
4388        ),
4389        ("optionalSelectV2", &optional_select_v2),
4390        ("selectShortV2", &select_short_v2),
4391        ("selectRightV2", &select_right_v2),
4392        ("selectNamedRightV2", &select_named_right_v2),
4393        ("selectAssignEndV2", &select_assign_end_v2),
4394        ("selectPathV2", &select_path_v2),
4395        ("selectEnd2V2", &select_end2_v2),
4396        ("selectEndV2", &select_end_v2),
4397        ("selectNextV2", &select_next_v2),
4398        ("renderedValueV2", &rendered_value_v2),
4399        ("optionalCallEndV2", &optional_call_end_v2),
4400        ("defaultSelectedV2", &default_selected_v2),
4401        ("defaultEnteredV2", &default_entered_v2),
4402    ];
4403    if uses_request_phase {
4404        runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
4405    }
4406    if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
4407        // Read off an untyped global, every helper is `any` to TypeScript,
4408        // whatever the runtime's declarations say, and so is each value that
4409        // passes through one. A directly run TypeScript file names each
4410        // helper's type where it binds it. Declaring the global's type for
4411        // the program instead collided with the declaration every rewritten
4412        // test file carries for its assertion helpers.
4413        let typed = typescript && runtime_binding == RuntimeBinding::DirectGlobal;
4414        let declarators =
4415            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
4416                let annotation = typed
4417                    .then(|| runtime_helper_type(&allocator, parsed.program.source_type, imported))
4418                    .flatten();
4419                let global_runtime =
4420                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
4421                        Span::default(),
4422                        ast.expression_identifier(Span::default(), ast.ident("globalThis")),
4423                        ast.identifier_name(
4424                            Span::default(),
4425                            ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
4426                                "__SUPERCOV_DIRECT_RUNTIME__"
4427                            } else {
4428                                "__supercovRuntime"
4429                            }),
4430                        ),
4431                        false,
4432                    ));
4433                let runtime_helper =
4434                    Expression::StaticMemberExpression(ast.alloc_static_member_expression(
4435                        Span::default(),
4436                        global_runtime,
4437                        ast.identifier_name(Span::default(), ast.ident(imported)),
4438                        false,
4439                    ));
4440                ast.variable_declarator(
4441                    Span::default(),
4442                    runtime_declaration_kind,
4443                    ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
4444                    annotation,
4445                    Some(runtime_helper),
4446                    false,
4447                )
4448            }));
4449        parsed.program.body.insert(
4450            0,
4451            Statement::VariableDeclaration(ast.alloc_variable_declaration(
4452                Span::default(),
4453                runtime_declaration_kind,
4454                declarators,
4455                false,
4456            )),
4457        );
4458    } else {
4459        let import_specifiers =
4460            ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
4461                ast.import_declaration_specifier_import_specifier(
4462                    Span::default(),
4463                    ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
4464                    ast.binding_identifier(Span::default(), ast.ident(local)),
4465                    oxc_ast::ast::ImportOrExportKind::Value,
4466                )
4467            }));
4468        parsed.program.body.insert(
4469            0,
4470            Statement::ImportDeclaration(ast.alloc_import_declaration(
4471                Span::default(),
4472                Some(import_specifiers),
4473                ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
4474                None,
4475                NONE,
4476                oxc_ast::ast::ImportOrExportKind::Value,
4477            )),
4478        );
4479    }
4480
4481    if let Some(wrapper) = capability_wrapper {
4482        transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
4483    }
4484    let limitations = Vec::new();
4485    mark_generated_statements(ast, &mut parsed.program);
4486    AssertedTargetTransformer {
4487        ast: AstBuilder::new(&allocator),
4488    }
4489    .visit_program(&mut parsed.program);
4490    let (code, map) =
4491        generate_candidate(&parsed.program, file, &safety.source_sensitive_functions)?;
4492    Ok(CandidateOutput {
4493        engine: "rust-oxc".to_string(),
4494        complete: true,
4495        supported_surface: "complete-js-instrumenter-v1".to_string(),
4496        code,
4497        map,
4498        decisions: collector.decisions,
4499        points: point_analysis.points,
4500        excluded_statements,
4501        branches,
4502        runtime: Some(CandidateRuntime {
4503            coverage_hit,
4504            mcdc_begin,
4505            mcdc_condition,
4506            mcdc_end,
4507            register_probe_v2,
4508            mcdc_end_v2,
4509            coverage_hit_v2,
4510            probe_file_v2,
4511            parenthesized_assignment_value,
4512            with_request_phase,
4513            optional_select_v2,
4514            select_short_v2,
4515            select_right_v2,
4516            select_named_right_v2,
4517            select_assign_end_v2,
4518            select_path_v2,
4519            select_end2_v2,
4520            select_end_v2,
4521            select_next_v2,
4522            rendered_value_v2,
4523            optional_call_end_v2,
4524            default_selected_v2,
4525            default_entered_v2,
4526        }),
4527        coverage_limitations: {
4528            let mut limitations = safety.semantic_limitations;
4529            limitations.extend(call_analysis.limitations);
4530            limitations.extend(default_analysis.limitations);
4531            limitations.extend(safety.dynamic_limitations);
4532            limitations
4533        },
4534        limitations,
4535    })
4536}
4537
4538/// `file.clock.fast && file.hitEpochs[index] === file.clock.epoch || hit(file, index);`
4539///
4540/// A statement probe that already fired in this context costs three loads and
4541/// a comparison, not a call: in minimatch's backtracking matcher the call per
4542/// statement was most of a tenfold slowdown, and its guard test failed. The
4543/// clock is `fast` only while an async hook keeps its epoch current; without
4544/// one the call decides, as before.
4545fn probe_v2_hit<'a>(ast: AstBuilder<'a>, hit: &str, file: &str, index: usize) -> Statement<'a> {
4546    ast.statement_expression(
4547        Span::default(),
4548        probe_v2_hit_expression(ast, hit, file, index),
4549    )
4550}
4551
4552/// The expression inside `probe_v2_hit`, for probes that sit in an expression:
4553/// a selection's outcome, a loop's first iteration, a caught exception.
4554fn probe_v2_hit_expression<'a>(
4555    ast: AstBuilder<'a>,
4556    hit: &str,
4557    file: &str,
4558    index: usize,
4559) -> Expression<'a> {
4560    let span = Span::default();
4561    let file_expression = || ast.expression_identifier(span, ast.ident(file));
4562    let member = |object: Expression<'a>, name: &str| -> Expression<'a> {
4563        Expression::from(ast.member_expression_static(
4564            span,
4565            object,
4566            ast.identifier_name(span, ast.ident(name)),
4567            false,
4568        ))
4569    };
4570    let index_literal =
4571        || ast.expression_numeric_literal(span, index as f64, None, NumberBase::Decimal);
4572    let fast = member(member(file_expression(), "clock"), "fast");
4573    let seen = ast.expression_binary(
4574        span,
4575        Expression::from(ast.member_expression_computed(
4576            span,
4577            member(file_expression(), "hitEpochs"),
4578            index_literal(),
4579            false,
4580        )),
4581        BinaryOperator::StrictEquality,
4582        member(member(file_expression(), "clock"), "epoch"),
4583    );
4584    let call = ast.expression_call(
4585        span,
4586        ast.expression_identifier(span, ast.ident(hit)),
4587        NONE,
4588        ast.vec_from_array([
4589            Argument::from(file_expression()),
4590            Argument::from(index_literal()),
4591        ]),
4592        false,
4593    );
4594    ast.expression_logical(
4595        span,
4596        ast.expression_logical(span, fast, LogicalOperator::And, seen),
4597        LogicalOperator::Or,
4598        call,
4599    )
4600}
4601
4602/// `let` declarations for probe temporaries at the top of a scope. In
4603/// TypeScript a temporary that holds user values is declared `any`, as the
4604/// runtime calls it replaces returned: `tsc --strict` would otherwise infer
4605/// its type across assignments and fail code that compiled before.
4606fn probe_temporaries<'a>(
4607    ast: AstBuilder<'a>,
4608    names: Vec<String>,
4609    typescript: bool,
4610) -> Statement<'a> {
4611    let declarations = ast.vec_from_iter(names.into_iter().map(|name| {
4612        let annotation = typescript.then(|| {
4613            ast.alloc_ts_type_annotation(Span::default(), ast.ts_type_any_keyword(Span::default()))
4614        });
4615        ast.variable_declarator(
4616            Span::default(),
4617            VariableDeclarationKind::Let,
4618            ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&name)),
4619            annotation,
4620            None,
4621            false,
4622        )
4623    }));
4624    Statement::VariableDeclaration(ast.alloc_variable_declaration(
4625        Span::default(),
4626        VariableDeclarationKind::Let,
4627        declarations,
4628        false,
4629    ))
4630}
4631
4632/// `T = value`, as an expression.
4633fn assign_temporary<'a>(ast: AstBuilder<'a>, name: &str, value: Expression<'a>) -> Expression<'a> {
4634    ast.expression_assignment(
4635        Span::default(),
4636        AssignmentOperator::Assign,
4637        AssignmentTarget::from(ast.simple_assignment_target_assignment_target_identifier(
4638            Span::default(),
4639            ast.ident(name),
4640        )),
4641        value,
4642    )
4643}
4644
4645fn numeric<'a>(ast: AstBuilder<'a>, value: usize) -> Expression<'a> {
4646    ast.expression_numeric_literal(Span::default(), value as f64, None, NumberBase::Decimal)
4647}
4648
4649/// Where each V2 point beyond the statement points begins, per site, in
4650/// source order: every probe kind records through `coverageHitV2` with the
4651/// ids it always recorded.
4652#[derive(Default)]
4653struct ExtraPoints {
4654    base: usize,
4655    ids: Vec<String>,
4656}
4657
4658impl ExtraPoints {
4659    fn allocate<T>(
4660        &mut self,
4661        targets: &HashMap<SpanKey, T>,
4662        ids: impl Fn(&T) -> Vec<String>,
4663    ) -> HashMap<SpanKey, usize> {
4664        let mut keys = targets.keys().copied().collect::<Vec<_>>();
4665        keys.sort();
4666        keys.into_iter()
4667            .map(|key| {
4668                let first = self.base + self.ids.len();
4669                self.ids.extend(ids(&targets[&key]));
4670                (key, first)
4671            })
4672            .collect()
4673    }
4674
4675    fn next(&self) -> usize {
4676        self.base + self.ids.len()
4677    }
4678}
4679
4680struct StatementProbeTransformer<'a> {
4681    ast: AstBuilder<'a>,
4682    coverage_hit_v2: String,
4683    probe_file_v2: String,
4684    targets: HashMap<SpanKey, Vec<PointTarget>>,
4685    source_sensitive_functions: HashSet<SpanKey>,
4686}
4687
4688impl<'a> StatementProbeTransformer<'a> {
4689    fn probe(&self, target: &PointTarget) -> Statement<'a> {
4690        probe_v2_hit(
4691            self.ast,
4692            &self.coverage_hit_v2,
4693            &self.probe_file_v2,
4694            target.index,
4695        )
4696    }
4697
4698    fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
4699        let mut ids = self
4700            .targets
4701            .remove(&span_key(statement.span()))
4702            .unwrap_or_default();
4703        if let Statement::ExportNamedDeclaration(export) = statement
4704            && let Some(declaration) = &export.declaration
4705            && let Some(nested) = self.targets.remove(&span_key(declaration.span()))
4706        {
4707            ids.extend(nested);
4708        }
4709        if let Statement::ExportDefaultDeclaration(export) = statement
4710            && let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
4711            && let Some(nested) = self.targets.remove(&span_key(class.span))
4712        {
4713            ids.extend(nested);
4714        }
4715        ids
4716    }
4717
4718    fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
4719        let ids = self.take_statement_ids(statement);
4720        if ids.is_empty() {
4721            return;
4722        }
4723        // The block takes the statement's span, so its braces map to the
4724        // statement's own start and end: a coverage tool mapping the copy back
4725        // (nyc, Jest) dropped a statement whose end was a brace of nowhere.
4726        let span = statement.span();
4727        let original = statement.take_in(self.ast.allocator);
4728        let mut body = self.ast.vec_with_capacity(ids.len() + 1);
4729        body.extend(ids.iter().map(|target| self.probe(target)));
4730        body.push(original);
4731        *statement = self.ast.statement_block(span, body);
4732    }
4733}
4734
4735impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
4736    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4737        let original = statements.take_in(self.ast.allocator);
4738        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
4739        for mut statement in original {
4740            let ids = self.take_statement_ids(&statement);
4741            self.visit_statement(&mut statement);
4742            instrumented.extend(ids.iter().map(|target| self.probe(target)));
4743            instrumented.push(statement);
4744        }
4745        *statements = instrumented;
4746    }
4747
4748    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4749        if self
4750            .source_sensitive_functions
4751            .contains(&span_key(function.span))
4752        {
4753            return;
4754        }
4755        walk_mut::walk_function(self, function, flags);
4756    }
4757
4758    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4759        if self
4760            .source_sensitive_functions
4761            .contains(&span_key(function.span))
4762        {
4763            return;
4764        }
4765        walk_mut::walk_arrow_function_expression(self, function);
4766    }
4767
4768    // A with body is left uninstrumented: its object environment can
4769    // intercept probe identifiers (see `enter_with_statement`).
4770    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4771
4772    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
4773        self.wrap_bare(&mut statement.consequent);
4774        if let Some(alternate) = &mut statement.alternate {
4775            self.wrap_bare(alternate);
4776        }
4777        walk_mut::walk_if_statement(self, statement);
4778    }
4779
4780    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
4781        self.wrap_bare(&mut statement.body);
4782        walk_mut::walk_while_statement(self, statement);
4783    }
4784
4785    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
4786        self.wrap_bare(&mut statement.body);
4787        walk_mut::walk_do_while_statement(self, statement);
4788    }
4789
4790    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
4791        self.wrap_bare(&mut statement.body);
4792        walk_mut::walk_for_statement(self, statement);
4793    }
4794
4795    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
4796        self.wrap_bare(&mut statement.body);
4797        walk_mut::walk_for_in_statement(self, statement);
4798    }
4799
4800    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
4801        self.wrap_bare(&mut statement.body);
4802        walk_mut::walk_for_of_statement(self, statement);
4803    }
4804}
4805
4806/// Wraps a rendered JSX expression in `renderedValueV2(file, i, expression)` so
4807/// the expression records its own evaluation. There is nowhere to put a
4808/// statement inside JSX, and a sequence expression is not available either --
4809/// JSX forbids a bare comma operator, and a container prints its expression
4810/// unparenthesised. A call carries the value through instead. It records after
4811/// the expression evaluates rather than before, as `optionalSelect` already
4812/// does, so an expression that throws is reported unevaluated rather than
4813/// covered.
4814struct RenderedExpressionTransformer<'a> {
4815    ast: AstBuilder<'a>,
4816    rendered_value_v2: String,
4817    probe_file_v2: String,
4818    targets: HashMap<SpanKey, PointTarget>,
4819    source_sensitive_functions: HashSet<SpanKey>,
4820}
4821
4822impl<'a> RenderedExpressionTransformer<'a> {
4823    fn probe(&self, target: &PointTarget, value: Expression<'a>) -> Expression<'a> {
4824        self.ast.expression_call(
4825            Span::default(),
4826            self.ast
4827                .expression_identifier(Span::default(), self.ast.ident(&self.rendered_value_v2)),
4828            NONE,
4829            self.ast.vec_from_array([
4830                Argument::from(
4831                    self.ast.expression_identifier(
4832                        Span::default(),
4833                        self.ast.ident(&self.probe_file_v2),
4834                    ),
4835                ),
4836                Argument::from(self.ast.expression_numeric_literal(
4837                    Span::default(),
4838                    target.index as f64,
4839                    None,
4840                    NumberBase::Decimal,
4841                )),
4842                Argument::from(value),
4843            ]),
4844            false,
4845        )
4846    }
4847}
4848
4849impl<'a> VisitMut<'a> for RenderedExpressionTransformer<'a> {
4850    fn visit_jsx_expression_container(&mut self, container: &mut JSXExpressionContainer<'a>) {
4851        walk_mut::walk_jsx_expression_container(self, container);
4852        if matches!(container.expression, JSXExpression::EmptyExpression(_)) {
4853            return;
4854        }
4855        let span = container.expression.span();
4856        if self.source_sensitive_functions.contains(&span_key(span)) {
4857            return;
4858        }
4859        let Some(target) = self.targets.remove(&span_key(span)) else {
4860            return;
4861        };
4862        let inner = container
4863            .expression
4864            .take_in(self.ast.allocator)
4865            .into_expression();
4866        container.expression = JSXExpression::from(self.probe(&target, inner));
4867    }
4868}
4869
4870struct FunctionProbeTransformer<'a> {
4871    ast: AstBuilder<'a>,
4872    coverage_hit_v2: String,
4873    probe_file_v2: String,
4874    targets: HashMap<SpanKey, PointTarget>,
4875    source_sensitive_functions: HashSet<SpanKey>,
4876}
4877
4878impl<'a> FunctionProbeTransformer<'a> {
4879    fn probe(&self, target: &PointTarget) -> Statement<'a> {
4880        probe_v2_hit(
4881            self.ast,
4882            &self.coverage_hit_v2,
4883            &self.probe_file_v2,
4884            target.index,
4885        )
4886    }
4887}
4888
4889impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
4890    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4891        if self
4892            .source_sensitive_functions
4893            .contains(&span_key(function.span))
4894        {
4895            return;
4896        }
4897        if let Some(target) = self.targets.remove(&span_key(function.span))
4898            && let Some(body) = &mut function.body
4899        {
4900            body.statements.insert(0, self.probe(&target));
4901        }
4902        walk_mut::walk_function(self, function, flags);
4903    }
4904
4905    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4906        if self
4907            .source_sensitive_functions
4908            .contains(&span_key(function.span))
4909        {
4910            return;
4911        }
4912        if let Some(target) = self.targets.remove(&span_key(function.span)) {
4913            let probe = self.probe(&target);
4914            if function.expression {
4915                let original = function
4916                    .body
4917                    .statements
4918                    .pop()
4919                    .expect("expression arrow must contain its expression statement");
4920                let Statement::ExpressionStatement(expression) = original else {
4921                    panic!("expression arrow body must be represented as an expression statement");
4922                };
4923                function.expression = false;
4924                function.body.statements.push(probe);
4925                function.body.statements.push(
4926                    self.ast
4927                        .statement_return(Span::default(), Some(expression.unbox().expression)),
4928                );
4929            } else {
4930                function.body.statements.insert(0, probe);
4931            }
4932        }
4933        walk_mut::walk_arrow_function_expression(self, function);
4934    }
4935}
4936
4937struct OptionalMemberTransformer<'a> {
4938    ast: AstBuilder<'a>,
4939    optional_select_v2: String,
4940    probe_file_v2: String,
4941    /// The short and continued outcomes are two V2 points from this index.
4942    targets: HashMap<SpanKey, usize>,
4943    links: HashMap<SpanKey, u8>,
4944    /// For a target right after an optional call, that call's frame.
4945    call_frames: HashMap<SpanKey, String>,
4946    source_sensitive_functions: HashSet<SpanKey>,
4947}
4948
4949impl<'a> OptionalMemberTransformer<'a> {
4950    /// `optionalSelectV2(file, first, object, links, call)`: records whether
4951    /// the object of `object?.member` was nullish and passes it through. In
4952    /// `a?.b?.c` the object of `?.c` is undefined when `a` is, though `?.c`
4953    /// never ran; `links`, and after an optional call that call's frame, let
4954    /// the runtime tell the two apart.
4955    fn instrument_operand(
4956        &self,
4957        operand: Expression<'a>,
4958        first: usize,
4959        links: u8,
4960        call_frame: Option<&str>,
4961    ) -> Expression<'a> {
4962        let mut arguments = self.ast.vec_from_array([
4963            Argument::from(
4964                self.ast
4965                    .expression_identifier(Span::default(), self.ast.ident(&self.probe_file_v2)),
4966            ),
4967            Argument::from(numeric(self.ast, first)),
4968            Argument::from(operand),
4969        ]);
4970        if links != 0 {
4971            arguments.push(Argument::from(numeric(self.ast, usize::from(links))));
4972        }
4973        if let Some(frame) = call_frame {
4974            arguments.push(Argument::from(
4975                self.ast
4976                    .expression_identifier(Span::default(), self.ast.ident(frame)),
4977            ));
4978        }
4979        self.ast.expression_call(
4980            Span::default(),
4981            self.ast
4982                .expression_identifier(Span::default(), self.ast.ident(&self.optional_select_v2)),
4983            NONE,
4984            arguments,
4985            false,
4986        )
4987    }
4988
4989    fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
4990        let Some(first) = self.targets.remove(&span_key(span)) else {
4991            return;
4992        };
4993        let mut links = self.links.get(&span_key(span)).copied().unwrap_or(0);
4994        let call_frame = self.call_frames.get(&span_key(span)).cloned();
4995        if call_frame.is_none() {
4996            // A call that is not measured leaves nothing to read.
4997            links &= !LINK_AFTER_CALL;
4998        }
4999        // `a?.b` narrows `a`, and TypeScript reads that through a comma's last
5000        // operand but not through a call: `if (user?.name) return user`
5001        // returned `User | undefined`. A name or `this` is read again after
5002        // the call that records it. Anything else may run code when read, a
5003        // getter or a proxy, and is read once, through the call.
5004        let again = match &*object {
5005            Expression::Identifier(identifier) => Some(
5006                self.ast
5007                    .expression_identifier(identifier.span, identifier.name),
5008            ),
5009            Expression::ThisExpression(this) => Some(self.ast.expression_this(this.span)),
5010            _ => None,
5011        };
5012        let mut operand = object.take_in(self.ast.allocator);
5013        if again.is_some() {
5014            *operand.span_mut() = Span::default();
5015        }
5016        let recorded = self.instrument_operand(operand, first, links, call_frame.as_deref());
5017        *object = match again {
5018            Some(again) => self
5019                .ast
5020                .expression_sequence(Span::default(), self.ast.vec_from_array([recorded, again])),
5021            None => recorded,
5022        };
5023    }
5024}
5025
5026impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
5027    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5028        if self
5029            .source_sensitive_functions
5030            .contains(&span_key(function.span))
5031        {
5032            return;
5033        }
5034        walk_mut::walk_function(self, function, flags);
5035    }
5036
5037    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5038        if self
5039            .source_sensitive_functions
5040            .contains(&span_key(function.span))
5041        {
5042            return;
5043        }
5044        walk_mut::walk_arrow_function_expression(self, function);
5045    }
5046
5047    // A with body is left uninstrumented: its object environment can
5048    // intercept probe identifiers (see `enter_with_statement`).
5049    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5050
5051    fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
5052        self.instrument_target(member.span, &mut member.object);
5053        walk_mut::walk_computed_member_expression(self, member);
5054    }
5055
5056    fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
5057        self.instrument_target(member.span, &mut member.object);
5058        walk_mut::walk_static_member_expression(self, member);
5059    }
5060
5061    fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
5062        self.instrument_target(member.span, &mut member.object);
5063        walk_mut::walk_private_field_expression(self, member);
5064    }
5065}
5066
5067/// A call site's state lives in a temporary of its own: 0 before the chain,
5068/// 1 once the callee is evaluated, 2 once the call goes ahead. Its short and
5069/// continued outcomes are two V2 points from `first`.
5070#[derive(Clone)]
5071struct OptionalCallSiteRuntime {
5072    frame: String,
5073    first: usize,
5074}
5075
5076struct OptionalCallTransformer<'a, 's> {
5077    ast: AstBuilder<'a>,
5078    optional_call_end_v2: String,
5079    probe_file_v2: String,
5080    typescript: bool,
5081    scope_declarations: Vec<Vec<String>>,
5082    sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
5083    roots: HashMap<SpanKey, Vec<SpanKey>>,
5084    source_sensitive_functions: HashSet<SpanKey>,
5085    _source: std::marker::PhantomData<&'s str>,
5086}
5087
5088impl<'a, 's> OptionalCallTransformer<'a, 's> {
5089    #[allow(clippy::too_many_arguments)]
5090    fn new(
5091        ast: AstBuilder<'a>,
5092        source: &'s str,
5093        optional_call_end_v2: String,
5094        probe_file_v2: String,
5095        typescript: bool,
5096        sites: HashMap<SpanKey, usize>,
5097        roots: HashMap<SpanKey, Vec<SpanKey>>,
5098        source_sensitive_functions: HashSet<SpanKey>,
5099    ) -> Self {
5100        let mut names = CandidateNames::new(source);
5101        let mut ordered = sites.into_iter().collect::<Vec<_>>();
5102        ordered.sort();
5103        let sites = ordered
5104            .into_iter()
5105            .map(|(key, first)| {
5106                (
5107                    key,
5108                    OptionalCallSiteRuntime {
5109                        frame: names.allocate("_optionalCall"),
5110                        first,
5111                    },
5112                )
5113            })
5114            .collect();
5115        Self {
5116            ast,
5117            optional_call_end_v2,
5118            probe_file_v2,
5119            typescript,
5120            scope_declarations: Vec::new(),
5121            sites,
5122            roots,
5123            source_sensitive_functions,
5124            _source: std::marker::PhantomData,
5125        }
5126    }
5127
5128    fn identifier(&self, name: &str) -> Expression<'a> {
5129        self.ast
5130            .expression_identifier(Span::default(), self.ast.ident(name))
5131    }
5132
5133    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
5134        self.ast.expression_call(
5135            Span::default(),
5136            self.identifier(name),
5137            NONE,
5138            arguments,
5139            false,
5140        )
5141    }
5142
5143    /// `(T = 1, value)`: the callee's position was evaluated.
5144    fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
5145        self.ast.expression_sequence(
5146            Span::default(),
5147            self.ast.vec_from_array([
5148                assign_temporary(self.ast, frame, numeric(self.ast, 1)),
5149                value,
5150            ]),
5151        )
5152    }
5153
5154    fn enter_scope(&mut self) {
5155        self.scope_declarations.push(Vec::new());
5156    }
5157
5158    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5159        let names = self
5160            .scope_declarations
5161            .pop()
5162            .expect("optional-call scope stack must remain balanced");
5163        if names.is_empty() {
5164            return;
5165        }
5166        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5167    }
5168
5169    fn instrument_callee(
5170        &self,
5171        callee: Expression<'a>,
5172        site: &OptionalCallSiteRuntime,
5173    ) -> Expression<'a> {
5174        match callee {
5175            Expression::ComputedMemberExpression(mut member) => {
5176                let property = member.expression.take_in(self.ast.allocator);
5177                member.expression = self.reached(&site.frame, property);
5178                Expression::ComputedMemberExpression(member)
5179            }
5180            Expression::StaticMemberExpression(member) => {
5181                let member = member.unbox();
5182                let property = self.ast.expression_string_literal(
5183                    Span::default(),
5184                    self.ast.str(member.property.name.as_str()),
5185                    None,
5186                );
5187                Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
5188                    member.span,
5189                    member.object,
5190                    self.reached(&site.frame, property),
5191                    member.optional,
5192                ))
5193            }
5194            Expression::PrivateFieldExpression(mut member) => {
5195                let object = member.object.take_in(self.ast.allocator);
5196                member.object = self.reached(&site.frame, object);
5197                Expression::PrivateFieldExpression(member)
5198            }
5199            Expression::ChainExpression(mut chain) => {
5200                match &mut chain.expression {
5201                    ChainElement::ComputedMemberExpression(member) => {
5202                        let property = member.expression.take_in(self.ast.allocator);
5203                        member.expression = self.reached(&site.frame, property);
5204                    }
5205                    ChainElement::StaticMemberExpression(member) => {
5206                        let member = member.take_in(self.ast.allocator);
5207                        let property = self.ast.expression_string_literal(
5208                            Span::default(),
5209                            self.ast.str(member.property.name.as_str()),
5210                            None,
5211                        );
5212                        chain.expression = ChainElement::ComputedMemberExpression(
5213                            self.ast.alloc_computed_member_expression(
5214                                member.span,
5215                                member.object,
5216                                self.reached(&site.frame, property),
5217                                member.optional,
5218                            ),
5219                        );
5220                    }
5221                    ChainElement::PrivateFieldExpression(member) => {
5222                        let object = member.object.take_in(self.ast.allocator);
5223                        member.object = self.reached(&site.frame, object);
5224                    }
5225                    ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
5226                        return self.reached(&site.frame, Expression::ChainExpression(chain));
5227                    }
5228                }
5229                Expression::ChainExpression(chain)
5230            }
5231            Expression::ParenthesizedExpression(mut parenthesized) => {
5232                let inner = parenthesized.expression.take_in(self.ast.allocator);
5233                parenthesized.expression = self.instrument_callee(inner, site);
5234                Expression::ParenthesizedExpression(parenthesized)
5235            }
5236            Expression::TSAsExpression(mut wrapped) => {
5237                let inner = wrapped.expression.take_in(self.ast.allocator);
5238                wrapped.expression = self.instrument_callee(inner, site);
5239                Expression::TSAsExpression(wrapped)
5240            }
5241            Expression::TSSatisfiesExpression(mut wrapped) => {
5242                let inner = wrapped.expression.take_in(self.ast.allocator);
5243                wrapped.expression = self.instrument_callee(inner, site);
5244                Expression::TSSatisfiesExpression(wrapped)
5245            }
5246            Expression::TSTypeAssertion(mut wrapped) => {
5247                let inner = wrapped.expression.take_in(self.ast.allocator);
5248                wrapped.expression = self.instrument_callee(inner, site);
5249                Expression::TSTypeAssertion(wrapped)
5250            }
5251            Expression::TSNonNullExpression(mut wrapped) => {
5252                let inner = wrapped.expression.take_in(self.ast.allocator);
5253                wrapped.expression = self.instrument_callee(inner, site);
5254                Expression::TSNonNullExpression(wrapped)
5255            }
5256            // `a?.b()?.()`: the callee is what an earlier call in the same
5257            // chain returned. It is reached only when that call goes ahead; a
5258            // short-circuit before it never asked whether its result is
5259            // nullish.
5260            Expression::CallExpression(mut call) => {
5261                call.arguments.insert(0, self.marker_spread(&site.frame, 1));
5262                Expression::CallExpression(call)
5263            }
5264            other => self.reached(&site.frame, other),
5265        }
5266    }
5267
5268    fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
5269        let callee = call.callee.take_in(self.ast.allocator);
5270        call.callee = self.instrument_callee(callee, site);
5271        call.arguments.insert(0, self.marker_spread(&site.frame, 2));
5272    }
5273
5274    /// `...(T = value, file.none)`: runs only when the call goes ahead, before
5275    /// its own arguments, and spreads nothing.
5276    fn marker_spread(&self, frame: &str, value: usize) -> Argument<'a> {
5277        let none = Expression::from(
5278            self.ast.member_expression_static(
5279                Span::default(),
5280                self.identifier(&self.probe_file_v2),
5281                self.ast
5282                    .identifier_name(Span::default(), self.ast.ident("none")),
5283                false,
5284            ),
5285        );
5286        let marked = self.ast.expression_sequence(
5287            Span::default(),
5288            self.ast.vec_from_array([
5289                assign_temporary(self.ast, frame, numeric(self.ast, value)),
5290                none,
5291            ]),
5292        );
5293        Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), marked))
5294    }
5295
5296    fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
5297        let sites = site_keys
5298            .iter()
5299            .map(|key| {
5300                self.sites
5301                    .get(key)
5302                    .expect("optional-call root must reference a known site")
5303                    .clone()
5304            })
5305            .collect::<Vec<_>>();
5306        self.scope_declarations
5307            .last_mut()
5308            .expect("optional-call root must be inside a program or function")
5309            .extend(sites.iter().map(|site| site.frame.clone()));
5310
5311        // `optionalCallEndV2(file, first, chain, T)` reads T after the chain,
5312        // so it sees what that evaluation did.
5313        let original = expression.take_in(self.ast.allocator);
5314        let mut measured = original;
5315        for site in sites.iter().rev() {
5316            measured = self.call(
5317                &self.optional_call_end_v2,
5318                self.ast.vec_from_array([
5319                    Argument::from(self.identifier(&self.probe_file_v2)),
5320                    Argument::from(numeric(self.ast, site.first)),
5321                    Argument::from(measured),
5322                    Argument::from(self.identifier(&site.frame)),
5323                ]),
5324            );
5325        }
5326        let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
5327        for site in &sites {
5328            sequence.push(assign_temporary(
5329                self.ast,
5330                &site.frame,
5331                numeric(self.ast, 0),
5332            ));
5333        }
5334        sequence.push(measured);
5335        *expression = self.ast.expression_sequence(Span::default(), sequence);
5336    }
5337}
5338
5339impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
5340    fn visit_program(&mut self, program: &mut Program<'a>) {
5341        self.enter_scope();
5342        walk_mut::walk_program(self, program);
5343        self.leave_scope(&mut program.body);
5344    }
5345
5346    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5347        self.enter_scope();
5348        walk_mut::walk_function_body(self, body);
5349        self.leave_scope(&mut body.statements);
5350    }
5351
5352    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5353        if self
5354            .source_sensitive_functions
5355            .contains(&span_key(function.span))
5356        {
5357            return;
5358        }
5359        walk_mut::walk_function(self, function, flags);
5360    }
5361
5362    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5363        if self
5364            .source_sensitive_functions
5365            .contains(&span_key(function.span))
5366        {
5367            return;
5368        }
5369        walk_mut::walk_arrow_function_expression(self, function);
5370    }
5371
5372    // A with body is left uninstrumented: its object environment can
5373    // intercept probe identifiers (see `enter_with_statement`).
5374    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5375
5376    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
5377        walk_mut::walk_call_expression(self, call);
5378        if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
5379            self.instrument_call(call, &site);
5380        }
5381    }
5382
5383    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
5384        let key = span_key(expression.span());
5385        walk_mut::walk_expression(self, expression);
5386        if let Some(site_keys) = self.roots.remove(&key) {
5387            self.wrap_root(expression, &site_keys);
5388        }
5389    }
5390}
5391
5392/// Where a default's pending count and its two outcomes live in the file's V2
5393/// probe state: a slot of `pendingDefaults`, and two indices of `pointIds`.
5394struct DefaultSlot {
5395    slot: usize,
5396    default_point: usize,
5397    provided_point: usize,
5398}
5399
5400struct DefaultTransformer<'a> {
5401    ast: AstBuilder<'a>,
5402    default_selected_v2: String,
5403    default_entered_v2: String,
5404    probe_file_v2: String,
5405    slots: HashMap<String, DefaultSlot>,
5406    parameter_targets: HashMap<SpanKey, DefaultTarget>,
5407    binding_targets: HashMap<SpanKey, DefaultTarget>,
5408    function_entries: Vec<Vec<Statement<'a>>>,
5409    declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
5410    active_declaration: Vec<SpanKey>,
5411    parameter_pattern_depth: usize,
5412    source_sensitive_functions: HashSet<SpanKey>,
5413    typescript: bool,
5414}
5415
5416impl<'a> DefaultTransformer<'a> {
5417    /// The type a default names by its own syntax: a literal's primitive, or
5418    /// the type it is asserted to (`"utf8" as E`).
5419    ///
5420    /// Under `isolatedDeclarations` a parameter needs no annotation while its
5421    /// default is such an expression. Wrapped in a call it does, and h3 failed
5422    /// to compile with `TS9011`, an error `@ts-nocheck` does not silence. The
5423    /// annotation says what TypeScript inferred already.
5424    fn default_type(&self, value: &Expression<'a>) -> Option<oxc_ast::ast::TSType<'a>> {
5425        let span = Span::default();
5426        let mut value = value;
5427        while let Expression::ParenthesizedExpression(parenthesized) = value {
5428            value = &parenthesized.expression;
5429        }
5430        match value {
5431            // `'x' as const` is the literal's own type, which no keyword names.
5432            Expression::TSAsExpression(asserted) => {
5433                (!asserted.type_annotation.is_const_type_reference())
5434                    .then(|| asserted.type_annotation.clone_in(self.ast.allocator))
5435            }
5436            Expression::TSTypeAssertion(asserted) => {
5437                (!asserted.type_annotation.is_const_type_reference())
5438                    .then(|| asserted.type_annotation.clone_in(self.ast.allocator))
5439            }
5440            Expression::BooleanLiteral(_) => Some(self.ast.ts_type_boolean_keyword(span)),
5441            Expression::NumericLiteral(_) => Some(self.ast.ts_type_number_keyword(span)),
5442            Expression::BigIntLiteral(_) => Some(self.ast.ts_type_big_int_keyword(span)),
5443            Expression::StringLiteral(_) => Some(self.ast.ts_type_string_keyword(span)),
5444            Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
5445                Some(self.ast.ts_type_string_keyword(span))
5446            }
5447            Expression::UnaryExpression(unary)
5448                if unary.operator == UnaryOperator::UnaryNegation
5449                    && matches!(unary.argument, Expression::NumericLiteral(_)) =>
5450            {
5451                Some(self.ast.ts_type_number_keyword(span))
5452            }
5453            _ => None,
5454        }
5455    }
5456
5457    fn identifier(&self, name: &str) -> Expression<'a> {
5458        self.ast
5459            .expression_identifier(Span::default(), self.ast.ident(name))
5460    }
5461
5462    fn string_argument(&self, value: &str) -> Argument<'a> {
5463        Argument::from(self.ast.expression_string_literal(
5464            Span::default(),
5465            self.ast.str(value),
5466            None,
5467        ))
5468    }
5469
5470    fn index_argument(&self, index: usize) -> Argument<'a> {
5471        Argument::from(self.ast.expression_numeric_literal(
5472            Span::default(),
5473            index as f64,
5474            None,
5475            NumberBase::Decimal,
5476        ))
5477    }
5478
5479    fn slot(&self, target: &DefaultTarget) -> &DefaultSlot {
5480        self.slots
5481            .get(&target.default_id)
5482            .expect("every default target has a V2 slot")
5483    }
5484
5485    /// `defaultSelectedV2(file, slot, value[, name])`: the default counts
5486    /// itself after its value evaluates, so a default that throws leaves no
5487    /// count behind.
5488    fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
5489        let mut arguments = self.ast.vec_from_array([
5490            Argument::from(self.identifier(&self.probe_file_v2)),
5491            self.index_argument(self.slot(target).slot),
5492            Argument::from(value),
5493        ]);
5494        if let Some(name) = &target.inferred_name {
5495            arguments.push(self.string_argument(name));
5496        }
5497        self.ast.expression_call(
5498            Span::default(),
5499            self.identifier(&self.default_selected_v2),
5500            NONE,
5501            arguments,
5502            false,
5503        )
5504    }
5505
5506    /// `defaultEnteredV2(file, slot, defaultPoint, providedPoint)`, once the
5507    /// parameters or the declaration are bound.
5508    fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
5509        let slot = self.slot(target);
5510        self.ast.statement_expression(
5511            Span::default(),
5512            self.ast.expression_call(
5513                Span::default(),
5514                self.identifier(&self.default_entered_v2),
5515                NONE,
5516                self.ast.vec_from_array([
5517                    Argument::from(self.identifier(&self.probe_file_v2)),
5518                    self.index_argument(slot.slot),
5519                    self.index_argument(slot.default_point),
5520                    self.index_argument(slot.provided_point),
5521                ]),
5522                false,
5523            ),
5524        )
5525    }
5526
5527    fn push_entry(&mut self, target: &DefaultTarget) {
5528        let entry = self.entered(target);
5529        if self.parameter_pattern_depth > 0 {
5530            self.function_entries
5531                .last_mut()
5532                .expect("parameter default must belong to a function")
5533                .push(entry);
5534        } else {
5535            let declaration = *self
5536                .active_declaration
5537                .last()
5538                .expect("binding default must belong to a declaration");
5539            self.declaration_entries
5540                .entry(declaration)
5541                .or_default()
5542                .push(entry);
5543        }
5544    }
5545
5546    fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
5547        if entries.is_empty() {
5548            return;
5549        }
5550        if let Statement::BlockStatement(block) = body {
5551            for (index, entry) in entries.into_iter().enumerate() {
5552                block.body.insert(index, entry);
5553            }
5554            return;
5555        }
5556        let span = body.span();
5557        let original = body.take_in(self.ast.allocator);
5558        let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
5559        statements.extend(entries);
5560        statements.push(original);
5561        *body = self.ast.statement_block(span, statements);
5562    }
5563
5564    fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
5565        match statement {
5566            Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
5567            Statement::ExportNamedDeclaration(export) => match &export.declaration {
5568                Some(Declaration::VariableDeclaration(declaration)) => {
5569                    Some(span_key(declaration.span))
5570                }
5571                _ => None,
5572            },
5573            _ => None,
5574        }
5575    }
5576
5577    fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
5578        match left {
5579            ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
5580            _ => None,
5581        }
5582    }
5583}
5584
5585impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
5586    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5587        let original = statements.take_in(self.ast.allocator);
5588        let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
5589        for mut statement in original {
5590            let declaration = Self::variable_span(&statement);
5591            self.visit_statement(&mut statement);
5592            instrumented.push(statement);
5593            if let Some(declaration) = declaration
5594                && let Some(entries) = self.declaration_entries.remove(&declaration)
5595            {
5596                instrumented.extend(entries);
5597            }
5598        }
5599        *statements = instrumented;
5600    }
5601
5602    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5603        if self
5604            .source_sensitive_functions
5605            .contains(&span_key(function.span))
5606        {
5607            return;
5608        }
5609        self.function_entries.push(Vec::new());
5610        walk_mut::walk_function(self, function, flags);
5611        let entries = self
5612            .function_entries
5613            .pop()
5614            .expect("default function-entry stack must remain balanced");
5615        if let Some(body) = &mut function.body {
5616            for (index, entry) in entries.into_iter().enumerate() {
5617                body.statements.insert(index, entry);
5618            }
5619        }
5620    }
5621
5622    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5623        if self
5624            .source_sensitive_functions
5625            .contains(&span_key(function.span))
5626        {
5627            return;
5628        }
5629        self.function_entries.push(Vec::new());
5630        walk_mut::walk_arrow_function_expression(self, function);
5631        let entries = self
5632            .function_entries
5633            .pop()
5634            .expect("default arrow-entry stack must remain balanced");
5635        for (index, entry) in entries.into_iter().enumerate() {
5636            function.body.statements.insert(index, entry);
5637        }
5638    }
5639
5640    fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
5641        let outer = self.parameter_targets.remove(&span_key(parameter.span));
5642        if let Some(target) = &outer {
5643            let entry = self.entered(target);
5644            self.function_entries
5645                .last_mut()
5646                .expect("formal parameter must belong to a function")
5647                .push(entry);
5648        }
5649        self.visit_decorators(&mut parameter.decorators);
5650        self.parameter_pattern_depth += 1;
5651        self.visit_binding_pattern(&mut parameter.pattern);
5652        self.parameter_pattern_depth -= 1;
5653        if let Some(annotation) = &mut parameter.type_annotation {
5654            self.visit_ts_type_annotation(annotation);
5655        }
5656        if let Some(initializer) = &mut parameter.initializer {
5657            if self.typescript
5658                && outer.is_some()
5659                && parameter.type_annotation.is_none()
5660                && matches!(parameter.pattern, BindingPattern::BindingIdentifier(_))
5661            {
5662                let named = self.default_type(initializer);
5663                if let Some(named) = named {
5664                    parameter.type_annotation =
5665                        Some(self.ast.alloc_ts_type_annotation(Span::default(), named));
5666                }
5667            }
5668            self.visit_expression(initializer);
5669            if let Some(target) = outer {
5670                let value = initializer.take_in(self.ast.allocator);
5671                **initializer = self.selected(value, &target);
5672            }
5673        }
5674    }
5675
5676    fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
5677        let target = if self.parameter_pattern_depth > 0 {
5678            self.parameter_targets.remove(&span_key(assignment.span))
5679        } else {
5680            self.binding_targets.remove(&span_key(assignment.span))
5681        };
5682        if let Some(target) = &target {
5683            self.push_entry(target);
5684        }
5685        walk_mut::walk_assignment_pattern(self, assignment);
5686        if let Some(target) = target {
5687            let value = assignment.right.take_in(self.ast.allocator);
5688            assignment.right = self.selected(value, &target);
5689        }
5690    }
5691
5692    fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
5693        self.active_declaration.push(span_key(declaration.span));
5694        walk_mut::walk_variable_declaration(self, declaration);
5695        self.active_declaration
5696            .pop()
5697            .expect("default declaration stack must remain balanced");
5698    }
5699
5700    fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
5701        let declaration = Self::loop_declaration_span(&statement.left);
5702        walk_mut::walk_for_in_statement(self, statement);
5703        if let Some(declaration) = declaration
5704            && let Some(entries) = self.declaration_entries.remove(&declaration)
5705        {
5706            self.prepend_entries(&mut statement.body, entries);
5707        }
5708    }
5709
5710    fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
5711        let declaration = Self::loop_declaration_span(&statement.left);
5712        walk_mut::walk_for_of_statement(self, statement);
5713        if let Some(declaration) = declaration
5714            && let Some(entries) = self.declaration_entries.remove(&declaration)
5715        {
5716            self.prepend_entries(&mut statement.body, entries);
5717        }
5718    }
5719
5720    // A with body is left uninstrumented: its object environment can
5721    // intercept probe identifiers (see `enter_with_statement`).
5722    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5723}
5724
5725enum ExtendedKind {
5726    Try,
5727    Loop,
5728}
5729
5730/// A try's success and catch outcomes, and a loop's zero-iteration and
5731/// entered outcomes, are two V2 points from the index each target maps to. A
5732/// flag of the site's own is set once the second outcome fires: the frame
5733/// and the call per iteration before cost every enumeration loop.
5734struct ExtendedTransformer<'a, 's> {
5735    ast: AstBuilder<'a>,
5736    coverage_hit_v2: String,
5737    probe_file_v2: String,
5738    typescript: bool,
5739    try_targets: HashMap<SpanKey, usize>,
5740    loop_targets: HashMap<SpanKey, usize>,
5741    names: CandidateNames<'s>,
5742    scope_declarations: Vec<Vec<String>>,
5743    source_sensitive_functions: HashSet<SpanKey>,
5744}
5745
5746impl<'a> ExtendedTransformer<'a, '_> {
5747    fn identifier(&self, name: &str) -> Expression<'a> {
5748        self.ast
5749            .expression_identifier(Span::default(), self.ast.ident(name))
5750    }
5751
5752    fn enter_scope(&mut self) {
5753        self.scope_declarations.push(Vec::new());
5754    }
5755
5756    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5757        let names = self
5758            .scope_declarations
5759            .pop()
5760            .expect("extended scope stack must remain balanced");
5761        if names.is_empty() {
5762            return;
5763        }
5764        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5765    }
5766
5767    fn scratch(&mut self, base: &str) -> String {
5768        let name = self.names.allocate(base);
5769        self.scope_declarations
5770            .last_mut()
5771            .expect("extended branch must be inside a program or function")
5772            .push(name.clone());
5773        name
5774    }
5775
5776    fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
5777        match statement {
5778            Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
5779            Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5780            Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5781            Statement::LabeledStatement(node) => Self::target(&node.body),
5782            _ => None,
5783        }
5784    }
5785
5786    fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
5787        match statement {
5788            Statement::TryStatement(node) => Some(node),
5789            Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
5790            _ => None,
5791        }
5792    }
5793
5794    fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
5795        match statement {
5796            Statement::ForInStatement(node) => Some(&mut node.body),
5797            Statement::ForOfStatement(node) => Some(&mut node.body),
5798            Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
5799            _ => None,
5800        }
5801    }
5802
5803    fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
5804        if let Statement::BlockStatement(block) = body {
5805            block.body.insert(0, entry);
5806            return;
5807        }
5808        let span = body.span();
5809        let original = body.take_in(ast.allocator);
5810        *body = ast.statement_block(span, ast.vec_from_array([entry, original]));
5811    }
5812
5813    fn hit(&self, index: usize) -> Expression<'a> {
5814        probe_v2_hit_expression(self.ast, &self.coverage_hit_v2, &self.probe_file_v2, index)
5815    }
5816
5817    /// `T = 0;`
5818    fn reset(&self, flag: &str) -> Statement<'a> {
5819        self.ast.statement_expression(
5820            Span::default(),
5821            assign_temporary(self.ast, flag, numeric(self.ast, 0)),
5822        )
5823    }
5824
5825    /// `T || (T = 1, hit);` -- recorded the first time only.
5826    fn first_time(&self, flag: &str, index: usize) -> Statement<'a> {
5827        let record = self.ast.expression_sequence(
5828            Span::default(),
5829            self.ast.vec_from_array([
5830                assign_temporary(self.ast, flag, numeric(self.ast, 1)),
5831                self.hit(index),
5832            ]),
5833        );
5834        self.ast.statement_expression(
5835            Span::default(),
5836            self.ast.expression_logical(
5837                Span::default(),
5838                self.identifier(flag),
5839                LogicalOperator::Or,
5840                record,
5841            ),
5842        )
5843    }
5844
5845    /// `T || hit;` -- the other outcome, when the first never fired.
5846    fn otherwise(&self, flag: &str, index: usize) -> Statement<'a> {
5847        self.ast.statement_expression(
5848            Span::default(),
5849            self.ast.expression_logical(
5850                Span::default(),
5851                self.identifier(flag),
5852                LogicalOperator::Or,
5853                self.hit(index),
5854            ),
5855        )
5856    }
5857
5858    fn instrument_try(&mut self, statement: &mut Statement<'a>, first: usize) {
5859        // What Supercov adds around the statement takes its span (see
5860        // `wrap_bare`), so its braces map to the statement's own ends.
5861        let span = statement.span();
5862        let flag = self.scratch("_supercovTryFrame");
5863        let reset = self.reset(&flag);
5864        // Recorded the moment the catch starts, not when the construct is
5865        // left: `process.exit()` in a catch skips the `finally`.
5866        let caught = self.first_time(&flag, first + 1);
5867        let node = Self::inner_try(statement).expect("try target must remain a try statement");
5868        node.handler
5869            .as_mut()
5870            .expect("try coverage requires a catch handler")
5871            .body
5872            .body
5873            .insert(0, caught);
5874        let end = self.otherwise(&flag, first);
5875        if let Some(finalizer) = &mut node.finalizer {
5876            finalizer.body.insert(0, end);
5877        } else {
5878            // Its braces map to the statement's last character: to its start
5879            // they would take that mapping from the `try`, and unmapped the
5880            // `try` would end nowhere.
5881            node.finalizer = Some(
5882                self.ast
5883                    .alloc_block_statement(Span::new(span.end - 1, span.end), self.ast.vec1(end)),
5884            );
5885        }
5886        let original = statement.take_in(self.ast.allocator);
5887        *statement = self
5888            .ast
5889            .statement_block(span, self.ast.vec_from_array([reset, original]));
5890    }
5891
5892    fn instrument_loop(&mut self, statement: &mut Statement<'a>, first: usize) {
5893        let span = statement.span();
5894        let flag = self.scratch("_supercovLoopFrame");
5895        let reset = self.reset(&flag);
5896        let entered = self.first_time(&flag, first + 1);
5897        Self::prepend(
5898            Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
5899            entered,
5900            self.ast,
5901        );
5902        let original = statement.take_in(self.ast.allocator);
5903        let end = self.otherwise(&flag, first);
5904        // Only the outer block takes the loop's span: the `try` inside it
5905        // mapped to the same start would take that mapping from the loop.
5906        let wrapped = self.ast.statement_try(
5907            Span::default(),
5908            self.ast
5909                .block_statement(Span::default(), self.ast.vec1(original)),
5910            None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
5911            Some(
5912                self.ast
5913                    .block_statement(Span::default(), self.ast.vec1(end)),
5914            ),
5915        );
5916        *statement = self
5917            .ast
5918            .statement_block(span, self.ast.vec_from_array([reset, wrapped]));
5919    }
5920}
5921
5922impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
5923    fn visit_program(&mut self, program: &mut Program<'a>) {
5924        self.enter_scope();
5925        walk_mut::walk_program(self, program);
5926        self.leave_scope(&mut program.body);
5927    }
5928
5929    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5930        self.enter_scope();
5931        walk_mut::walk_function_body(self, body);
5932        self.leave_scope(&mut body.statements);
5933    }
5934
5935    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5936        if self
5937            .source_sensitive_functions
5938            .contains(&span_key(function.span))
5939        {
5940            return;
5941        }
5942        walk_mut::walk_function(self, function, flags);
5943    }
5944
5945    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5946        if self
5947            .source_sensitive_functions
5948            .contains(&span_key(function.span))
5949        {
5950            return;
5951        }
5952        walk_mut::walk_arrow_function_expression(self, function);
5953    }
5954
5955    // A with body is left uninstrumented: its object environment can
5956    // intercept probe identifiers (see `enter_with_statement`).
5957    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5958
5959    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
5960        let Some((kind, key)) = Self::target(statement) else {
5961            walk_mut::walk_statement(self, statement);
5962            return;
5963        };
5964        match kind {
5965            ExtendedKind::Try => {
5966                if let Some(target) = self.try_targets.remove(&key) {
5967                    walk_mut::walk_statement(self, statement);
5968                    self.instrument_try(statement, target);
5969                } else {
5970                    walk_mut::walk_statement(self, statement);
5971                }
5972            }
5973            ExtendedKind::Loop => {
5974                if let Some(target) = self.loop_targets.remove(&key) {
5975                    walk_mut::walk_statement(self, statement);
5976                    self.instrument_loop(statement, target);
5977                } else {
5978                    walk_mut::walk_statement(self, statement);
5979                }
5980            }
5981        }
5982    }
5983}
5984
5985/// A tree of selections used for its value, its leaves numbered in the order
5986/// they are evaluated.
5987enum SelectionShape {
5988    Leaf(usize),
5989    Selection {
5990        first: usize,
5991        kind: usize,
5992        left: Box<SelectionShape>,
5993        right: Box<SelectionShape>,
5994    },
5995}
5996
5997impl SelectionShape {
5998    fn leftmost(&self) -> usize {
5999        match self {
6000            Self::Leaf(leaf) => *leaf,
6001            Self::Selection { left, .. } => left.leftmost(),
6002        }
6003    }
6004
6005    /// The leaves whose value can be this subtree's when that value is
6006    /// truthy, or when it is not, each with the points it hits on its way up
6007    /// through the subtree: what `selectPathV2` records for the leaf's steps
6008    /// below the subtree's parent. A left side decides only by cutting its
6009    /// selection short, which `||` does on a truthy value, `&&` on a falsy
6010    /// one, and `??` on any that is not nullish.
6011    fn deciders(&self, truthy: bool, nullish: bool) -> Vec<(usize, Vec<usize>)> {
6012        match self {
6013            Self::Leaf(leaf) => vec![(*leaf, Vec::new())],
6014            Self::Selection {
6015                first,
6016                kind,
6017                left,
6018                right,
6019            } => {
6020                let mut deciders = Vec::new();
6021                let short = match kind {
6022                    0 => truthy,
6023                    1 => !truthy,
6024                    _ => !nullish,
6025                };
6026                if short {
6027                    for (leaf, mut hits) in left.deciders(truthy, nullish) {
6028                        hits.push(first + usize::from(truthy));
6029                        deciders.push((leaf, hits));
6030                    }
6031                }
6032                for (leaf, mut hits) in right.deciders(truthy, nullish) {
6033                    hits.push(first + 2 + usize::from(truthy));
6034                    deciders.push((leaf, hits));
6035                }
6036                deciders
6037            }
6038        }
6039    }
6040
6041    /// For each leaf but the first, the leaves that can have been evaluated
6042    /// just before it, with what each decided by then. Evaluation reaches the
6043    /// right side of a selection when its left side did not cut it short:
6044    /// truthy for `&&`, falsy for `||`, and nullish, so falsy, for `??`.
6045    fn before(&self, before: &mut Vec<Vec<(usize, Vec<usize>)>>) {
6046        let Self::Selection {
6047            kind, left, right, ..
6048        } = self
6049        else {
6050            return;
6051        };
6052        before[right.leftmost()] = left.deciders(*kind == 1, *kind == 2);
6053        left.before(before);
6054        right.before(before);
6055    }
6056}
6057
6058type SelectionTable = (Vec<Vec<usize>>, Vec<Vec<Vec<usize>>>);
6059
6060/// A tree whose leaves are named: the helper that records its value, the
6061/// selection or the table entry it records for, and the tree's frame.
6062struct SelectionSite {
6063    helper: String,
6064    index: usize,
6065    frame: String,
6066}
6067
6068struct LogicalValueTransformer<'a, 's> {
6069    ast: AstBuilder<'a>,
6070    select_short_v2: String,
6071    select_right_v2: String,
6072    select_named_right_v2: String,
6073    select_assign_end_v2: String,
6074    select_path_v2: String,
6075    select_end2_v2: String,
6076    select_end_v2: String,
6077    select_next_v2: String,
6078    coverage_hit_v2: String,
6079    probe_file_v2: String,
6080    typescript: bool,
6081    names: CandidateNames<'s>,
6082    scope_declarations: Vec<Vec<String>>,
6083    /// Inside a parameter default or a class member's initializer, where no
6084    /// function body can declare a tree's frame: a parameter list cannot see
6085    /// its function's body, and an initializer's enclosing scope is shared by
6086    /// every instance being constructed.
6087    frameless: usize,
6088    /// For each tree with more than two leaves, the runtime's table: the
6089    /// steps of each leaf, and for each leaf the points each leaf that can
6090    /// come before it has decided by then, where more than one can.
6091    trees: Vec<SelectionTable>,
6092    /// The statement being visited is a `const` declaration in a list of
6093    /// statements, where another can follow it.
6094    aliasing: bool,
6095    /// What records the values of the trees the declaration just visited
6096    /// keeps as its initializers, to go after it.
6097    after_declaration: Vec<Expression<'a>>,
6098    /// A selection's four outcomes -- short falsy, short truthy, right falsy,
6099    /// right truthy -- are consecutive V2 points from this index.
6100    logical_targets: HashMap<SpanKey, usize>,
6101    /// The same four points for `||=`, `&&=` and `??=`.
6102    assignment_targets: HashMap<SpanKey, usize>,
6103    source_sensitive_functions: HashSet<SpanKey>,
6104}
6105
6106impl<'a> LogicalValueTransformer<'a, '_> {
6107    fn identifier(&self, name: &str) -> Expression<'a> {
6108        self.ast
6109            .expression_identifier(Span::default(), self.ast.ident(name))
6110    }
6111
6112    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
6113        AssignmentTarget::from(
6114            self.ast
6115                .simple_assignment_target_assignment_target_identifier(
6116                    Span::default(),
6117                    self.ast.ident(name),
6118                ),
6119        )
6120    }
6121
6122    fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
6123        self.ast.expression_call(
6124            Span::default(),
6125            self.identifier(name),
6126            NONE,
6127            arguments,
6128            false,
6129        )
6130    }
6131
6132    fn string_argument(&self, value: &str) -> Argument<'a> {
6133        Argument::from(self.ast.expression_string_literal(
6134            Span::default(),
6135            self.ast.str(value),
6136            None,
6137        ))
6138    }
6139
6140    fn enter_scope(&mut self) {
6141        self.scope_declarations.push(Vec::new());
6142    }
6143
6144    fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
6145        let names = self
6146            .scope_declarations
6147            .pop()
6148            .expect("logical-value scope stack must remain balanced");
6149        if names.is_empty() {
6150            return;
6151        }
6152        statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
6153    }
6154
6155    fn scratch(&mut self, base: &str) -> String {
6156        let name = self.names.allocate(base);
6157        self.scope_declarations
6158            .last_mut()
6159            .expect("logical expression must be inside a program or function")
6160            .push(name.clone());
6161        name
6162    }
6163
6164    fn helper(&self, name: &str, arguments: [Argument<'a>; 3]) -> Expression<'a> {
6165        let mut all = self
6166            .ast
6167            .vec_from_array([Argument::from(self.identifier(&self.probe_file_v2))]);
6168        all.extend(arguments);
6169        self.call(name, all)
6170    }
6171
6172    /// `a || b` as `selectShortV2(file, first, a, 0) || selectRightV2(file,
6173    /// first, b)`, and `&&` (1) and `??` (2) alike. The program's own operator
6174    /// stays, so a coverage tool the tests run -- nyc, c8, Jest, Vitest -- counts
6175    /// the branches it counts without Supercov, and neither call needs a frame
6176    /// or a temporary: the short side records its outcome when it decides the
6177    /// result, the right side records whether it came out truthy. Both are small
6178    /// enough for V8 to inline; the frame and three calls each evaluation cost
6179    /// before were a tenth of lru-cache's hot loop, with MC/DC.
6180    ///
6181    /// Nested selections, `(a && b) || c`, stay one tree of the program's
6182    /// operators with only the leaves wrapped: istanbul flattens a tree through
6183    /// its logical children only, and a call around `a && b` made two branches
6184    /// of its one. A leaf deeper than one selection records, through
6185    /// `selectPathV2`, every selection above it that its value decides.
6186    fn instrument_tree(&mut self, expression: &mut Expression<'a>, path: &[usize]) {
6187        let key = span_key(expression.span());
6188        let first = self
6189            .logical_targets
6190            .remove(&key)
6191            .expect("a selection tree starts at a selection");
6192        let Expression::LogicalExpression(logical) = expression else {
6193            panic!("logical-value target must remain a logical expression");
6194        };
6195        let kind = match logical.operator {
6196            LogicalOperator::Or => 0,
6197            LogicalOperator::And => 1,
6198            LogicalOperator::Coalesce => 2,
6199        };
6200        let mut left_path = vec![first * 4 + kind];
6201        left_path.extend_from_slice(path);
6202        let mut right_path = vec![first * 4 + 3];
6203        right_path.extend_from_slice(path);
6204        self.instrument_operand(&mut logical.left, &left_path);
6205        self.instrument_operand(&mut logical.right, &right_path);
6206    }
6207
6208    fn instrument_operand(&mut self, operand: &mut Expression<'a>, path: &[usize]) {
6209        let mut inner = &mut *operand;
6210        while let Expression::ParenthesizedExpression(parenthesized) = inner {
6211            inner = &mut parenthesized.expression;
6212        }
6213        if matches!(inner, Expression::LogicalExpression(_))
6214            && self.logical_targets.contains_key(&span_key(inner.span()))
6215        {
6216            self.instrument_tree(inner, path);
6217            return;
6218        }
6219        self.visit_expression(operand);
6220        let value = operand.take_in(self.ast.allocator);
6221        *operand = match *path {
6222            [step] if step % 4 == 3 => {
6223                let mut arguments = self.ast.vec_from_array([
6224                    Argument::from(self.identifier(&self.probe_file_v2)),
6225                    Argument::from(numeric(self.ast, step / 4)),
6226                ]);
6227                arguments.push(Argument::from(value));
6228                self.call(&self.select_right_v2, arguments)
6229            }
6230            [step] => self.helper(
6231                &self.select_short_v2,
6232                [
6233                    Argument::from(numeric(self.ast, step / 4)),
6234                    Argument::from(value),
6235                    Argument::from(numeric(self.ast, step % 4)),
6236                ],
6237            ),
6238            _ => {
6239                let mut arguments = self.ast.vec_from_array([
6240                    Argument::from(self.identifier(&self.probe_file_v2)),
6241                    Argument::from(value),
6242                ]);
6243                arguments.extend(
6244                    path.iter()
6245                        .map(|step| Argument::from(numeric(self.ast, *step))),
6246                );
6247                self.call(&self.select_path_v2, arguments)
6248            }
6249        };
6250    }
6251
6252    /// `x ||= y` as `(T = 0, selectAssignEndV2(file, first, x ||= selectRightV2(
6253    /// file, first, y, T = 1), T))`, and `&&=` and `??=` alike. The operator
6254    /// stays, so the target is read and written once, as the program does, and
6255    /// a frame per evaluation is gone. `T` is a temporary of the enclosing
6256    /// function, set after the right side evaluates, so a recursive call there
6257    /// has its own; the end records the short outcome when it is still 0.
6258    /// The shape of the tree at `expression`, taking its selections out of
6259    /// the targets, and each leaf's steps as `selectPathV2` takes them.
6260    fn shape(
6261        &mut self,
6262        expression: &Expression<'a>,
6263        path: &[usize],
6264        leaves: &mut Vec<Vec<usize>>,
6265    ) -> SelectionShape {
6266        let mut inner = expression;
6267        while let Expression::ParenthesizedExpression(parenthesized) = inner {
6268            inner = &parenthesized.expression;
6269        }
6270        if let Expression::LogicalExpression(logical) = inner
6271            && let Some(first) = self.logical_targets.remove(&span_key(logical.span))
6272        {
6273            let kind = match logical.operator {
6274                LogicalOperator::Or => 0,
6275                LogicalOperator::And => 1,
6276                LogicalOperator::Coalesce => 2,
6277            };
6278            let mut left_path = vec![first * 4 + kind];
6279            left_path.extend_from_slice(path);
6280            let mut right_path = vec![first * 4 + 3];
6281            right_path.extend_from_slice(path);
6282            let left = Box::new(self.shape(&logical.left, &left_path, leaves));
6283            let right = Box::new(self.shape(&logical.right, &right_path, leaves));
6284            return SelectionShape::Selection {
6285                first,
6286                kind,
6287                left,
6288                right,
6289            };
6290        }
6291        leaves.push(path.to_vec());
6292        SelectionShape::Leaf(leaves.len() - 1)
6293    }
6294
6295    fn hits(&self, points: &[usize]) -> Option<Expression<'a>> {
6296        let mut hits = self.ast.vec_from_iter(points.iter().map(|point| {
6297            self.call(
6298                &self.coverage_hit_v2,
6299                self.ast.vec_from_array([
6300                    Argument::from(self.identifier(&self.probe_file_v2)),
6301                    Argument::from(numeric(self.ast, *point)),
6302                ]),
6303            )
6304        }));
6305        match hits.len() {
6306            0 => None,
6307            1 => hits.pop(),
6308            _ => Some(self.ast.expression_sequence(Span::default(), hits)),
6309        }
6310    }
6311
6312    /// What the leaf evaluated before this one decided. Where only one leaf
6313    /// can come before, its points are hit there. Where several can, the
6314    /// frame says which, and the runtime reads its points from the tree's
6315    /// table: a choice written out here would be a branch of the user's line
6316    /// to a coverage tool the tests run.
6317    fn recorded_before(
6318        &self,
6319        frame: &str,
6320        tree: Option<usize>,
6321        leaf: usize,
6322        deciders: &[(usize, Vec<usize>)],
6323    ) -> Option<Expression<'a>> {
6324        if deciders.iter().all(|(_, points)| points.is_empty()) {
6325            return None;
6326        }
6327        if let [(_, points)] = deciders {
6328            return self.hits(points);
6329        }
6330        let tree = tree.expect("a tree with a choice of leaves has a table");
6331        Some(self.call(
6332            &self.select_next_v2,
6333            self.ast.vec_from_array([
6334                Argument::from(self.identifier(&self.probe_file_v2)),
6335                Argument::from(numeric(self.ast, tree)),
6336                Argument::from(self.identifier(frame)),
6337                Argument::from(numeric(self.ast, leaf)),
6338            ]),
6339        ))
6340    }
6341
6342    fn name_leaves(
6343        &mut self,
6344        expression: &mut Expression<'a>,
6345        shape: &SelectionShape,
6346        frame: &str,
6347        tree: Option<usize>,
6348        before: &[Vec<(usize, Vec<usize>)>],
6349    ) {
6350        let leaf = match shape {
6351            SelectionShape::Leaf(leaf) => *leaf,
6352            SelectionShape::Selection { left, right, .. } => {
6353                let mut inner = expression;
6354                while let Expression::ParenthesizedExpression(parenthesized) = inner {
6355                    inner = &mut parenthesized.expression;
6356                }
6357                let Expression::LogicalExpression(logical) = inner else {
6358                    panic!("a selection must remain a logical expression");
6359                };
6360                self.name_leaves(&mut logical.left, left, frame, tree, before);
6361                self.name_leaves(&mut logical.right, right, frame, tree, before);
6362                return;
6363            }
6364        };
6365        self.visit_expression(expression);
6366        let value = expression.take_in(self.ast.allocator);
6367        let mut steps = self.ast.vec_with_capacity(3);
6368        steps.extend(self.recorded_before(frame, tree, leaf, &before[leaf]));
6369        steps.push(self.ast.expression_assignment(
6370            Span::default(),
6371            AssignmentOperator::Assign,
6372            self.assignment_target(frame),
6373            numeric(self.ast, leaf),
6374        ));
6375        steps.push(value);
6376        *expression = self.ast.expression_sequence(Span::default(), steps);
6377    }
6378
6379    /// `a && a.b` as `selectEnd2V2(file, first, (_f = 0, a) && (_f = 1, a.b),
6380    /// _f)`: each leaf is still the program's own expression, as the last
6381    /// operand of a comma, so it has the type, the contextual type and the
6382    /// narrowing it has in the source. With a call around each leaf, as in
6383    /// `instrument_tree`, `a.b` was read from an `a` that nothing had
6384    /// narrowed: an error the instrumented copy does not report, and `any`
6385    /// when a member of `a`'s type has no `b`, for everything that reads the
6386    /// value.
6387    ///
6388    /// The frame names the leaf being evaluated. The call around the tree
6389    /// records the outcomes of the leaf that produced its value. What an
6390    /// earlier leaf decided is known from where evaluation went next, and is
6391    /// recorded there: the same points, in the same order, as before, a tree
6392    /// that throws half-way included.
6393    fn instrument_value_tree(&mut self, expression: &mut Expression<'a>) {
6394        let site = self.name_tree(expression);
6395        let tree = expression.take_in(self.ast.allocator);
6396        *expression = self.record_tree(&site, tree);
6397    }
6398
6399    /// The call that records a named tree's value, around that value.
6400    fn record_tree(&self, site: &SelectionSite, value: Expression<'a>) -> Expression<'a> {
6401        self.call(
6402            &site.helper,
6403            self.ast.vec_from_array([
6404                Argument::from(self.identifier(&self.probe_file_v2)),
6405                Argument::from(numeric(self.ast, site.index)),
6406                Argument::from(value),
6407                Argument::from(self.identifier(&site.frame)),
6408            ]),
6409        )
6410    }
6411
6412    /// Name the leaves of the tree at `expression`.
6413    fn name_tree(&mut self, expression: &mut Expression<'a>) -> SelectionSite {
6414        let mut leaves = Vec::new();
6415        let shape = self.shape(expression, &[], &mut leaves);
6416        let SelectionShape::Selection { first, .. } = shape else {
6417            panic!("a selection tree starts at a selection");
6418        };
6419        let mut before = vec![Vec::new(); leaves.len()];
6420        shape.before(&mut before);
6421        let frame = self.scratch("_supercovSelected");
6422        let tree = (leaves.len() > 2).then(|| {
6423            // The table for `selectNextV2`, by the leaf reached and the one
6424            // before it.
6425            let choices = before
6426                .iter()
6427                .map(|deciders| {
6428                    let mut by_leaf = Vec::new();
6429                    if deciders.len() > 1 {
6430                        for (leaf, points) in deciders {
6431                            if by_leaf.len() <= *leaf {
6432                                by_leaf.resize(*leaf + 1, Vec::new());
6433                            }
6434                            by_leaf[*leaf] = points.clone();
6435                        }
6436                    }
6437                    by_leaf
6438                })
6439                .collect();
6440            self.trees.push((leaves, choices));
6441            self.trees.len() - 1
6442        });
6443        self.name_leaves(expression, &shape, &frame, tree, &before);
6444        let (helper, index) = match tree {
6445            Some(tree) => (self.select_end_v2.clone(), tree),
6446            None => (self.select_end2_v2.clone(), first),
6447        };
6448        SelectionSite {
6449            helper,
6450            index,
6451            frame,
6452        }
6453    }
6454
6455    fn instrument_assignment(
6456        &mut self,
6457        assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
6458        first: usize,
6459    ) -> Expression<'a> {
6460        let assignment = assignment.unbox();
6461        let inferred_name = match &assignment.left {
6462            AssignmentTarget::AssignmentTargetIdentifier(identifier)
6463                if assignment.span.start == identifier.span.start
6464                    && expression_is_anonymous_definition(&assignment.right) =>
6465            {
6466                Some(identifier.name.to_string())
6467            }
6468            _ => None,
6469        };
6470        let right_evaluated = self.scratch("_supercovAssignedRight");
6471        let set = |value| {
6472            self.ast.expression_assignment(
6473                Span::default(),
6474                AssignmentOperator::Assign,
6475                self.assignment_target(&right_evaluated),
6476                numeric(self.ast, value),
6477            )
6478        };
6479        let (reset, mark) = (set(0), set(1));
6480        let mut right_arguments = self.ast.vec_from_array([
6481            Argument::from(self.identifier(&self.probe_file_v2)),
6482            Argument::from(numeric(self.ast, first)),
6483            Argument::from(assignment.right),
6484        ]);
6485        let right_helper = match inferred_name {
6486            Some(name) => {
6487                right_arguments.push(self.string_argument(&name));
6488                &self.select_named_right_v2
6489            }
6490            None => &self.select_right_v2,
6491        };
6492        right_arguments.push(Argument::from(mark));
6493        let right = self.call(right_helper, right_arguments);
6494        let measured_assignment = self.ast.expression_assignment(
6495            Span::default(),
6496            assignment.operator,
6497            assignment.left,
6498            right,
6499        );
6500        let end = self.call(
6501            &self.select_assign_end_v2,
6502            self.ast.vec_from_array([
6503                Argument::from(self.identifier(&self.probe_file_v2)),
6504                Argument::from(numeric(self.ast, first)),
6505                Argument::from(measured_assignment),
6506                Argument::from(self.identifier(&right_evaluated)),
6507            ]),
6508        );
6509        self.ast
6510            .expression_sequence(Span::default(), self.ast.vec_from_array([reset, end]))
6511    }
6512}
6513
6514impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
6515    fn visit_program(&mut self, program: &mut Program<'a>) {
6516        self.enter_scope();
6517        walk_mut::walk_program(self, program);
6518        self.leave_scope(&mut program.body);
6519    }
6520
6521    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
6522        let frameless = std::mem::take(&mut self.frameless);
6523        self.enter_scope();
6524        walk_mut::walk_function_body(self, body);
6525        self.leave_scope(&mut body.statements);
6526        self.frameless = frameless;
6527    }
6528
6529    fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
6530        let mut index = 0;
6531        while index < statements.len() {
6532            let declaration = match &statements[index] {
6533                Statement::VariableDeclaration(declaration) => Some(&**declaration),
6534                Statement::ExportNamedDeclaration(export) => match &export.declaration {
6535                    Some(Declaration::VariableDeclaration(declaration)) => Some(&**declaration),
6536                    _ => None,
6537                },
6538                _ => None,
6539            };
6540            let aliasing = std::mem::replace(
6541                &mut self.aliasing,
6542                declaration.is_some_and(|declaration| declaration.kind.is_const()),
6543            );
6544            self.visit_statement(&mut statements[index]);
6545            self.aliasing = aliasing;
6546            for recorded in std::mem::take(&mut self.after_declaration) {
6547                index += 1;
6548                statements.insert(
6549                    index,
6550                    self.ast.statement_expression(Span::default(), recorded),
6551                );
6552            }
6553            index += 1;
6554        }
6555    }
6556
6557    // `const ok = a && b; if (ok) …` narrows `a` and `b`, but only while the
6558    // tree is the initializer itself: its value is recorded by a statement
6559    // after the declaration, from the constant.
6560    fn visit_variable_declarator(&mut self, declarator: &mut VariableDeclarator<'a>) {
6561        let aliasing = std::mem::take(&mut self.aliasing);
6562        if aliasing
6563            && self.frameless == 0
6564            && let BindingPattern::BindingIdentifier(name) = &declarator.id
6565            && let Some(initializer) = &mut declarator.init
6566        {
6567            let mut tree = &mut *initializer;
6568            while let Expression::ParenthesizedExpression(parenthesized) = tree {
6569                tree = &mut parenthesized.expression;
6570            }
6571            if matches!(tree, Expression::LogicalExpression(_))
6572                && self.logical_targets.contains_key(&span_key(tree.span()))
6573            {
6574                let site = self.name_tree(tree);
6575                let recorded = self.record_tree(&site, self.identifier(&name.name));
6576                self.after_declaration.push(recorded);
6577                self.aliasing = aliasing;
6578                return;
6579            }
6580        }
6581        walk_mut::walk_variable_declarator(self, declarator);
6582        self.aliasing = aliasing;
6583    }
6584
6585    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
6586        self.frameless += 1;
6587        walk_mut::walk_formal_parameters(self, parameters);
6588        self.frameless -= 1;
6589    }
6590
6591    fn visit_property_definition(&mut self, property: &mut oxc_ast::ast::PropertyDefinition<'a>) {
6592        self.frameless += 1;
6593        walk_mut::walk_property_definition(self, property);
6594        self.frameless -= 1;
6595    }
6596
6597    fn visit_accessor_property(&mut self, property: &mut oxc_ast::ast::AccessorProperty<'a>) {
6598        self.frameless += 1;
6599        walk_mut::walk_accessor_property(self, property);
6600        self.frameless -= 1;
6601    }
6602
6603    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6604        if self
6605            .source_sensitive_functions
6606            .contains(&span_key(function.span))
6607        {
6608            return;
6609        }
6610        walk_mut::walk_function(self, function, flags);
6611    }
6612
6613    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6614        if self
6615            .source_sensitive_functions
6616            .contains(&span_key(function.span))
6617        {
6618            return;
6619        }
6620        walk_mut::walk_arrow_function_expression(self, function);
6621    }
6622
6623    // A with body is left uninstrumented: its object environment can
6624    // intercept probe identifiers (see `enter_with_statement`).
6625    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6626
6627    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
6628        let key = span_key(expression.span());
6629        if matches!(expression, Expression::LogicalExpression(_))
6630            && self.logical_targets.contains_key(&key)
6631        {
6632            if self.frameless > 0 {
6633                self.instrument_tree(expression, &[]);
6634            } else {
6635                self.instrument_value_tree(expression);
6636            }
6637            return;
6638        }
6639        walk_mut::walk_expression(self, expression);
6640        if let Some(first) = self.assignment_targets.remove(&key) {
6641            let original = expression.take_in(self.ast.allocator);
6642            let Expression::AssignmentExpression(assignment) = original else {
6643                panic!("logical-assignment target must remain an assignment expression");
6644            };
6645            *expression = self.instrument_assignment(assignment, first);
6646        }
6647    }
6648}
6649
6650struct SwitchTransformer<'a, 's> {
6651    ast: AstBuilder<'a>,
6652    coverage_hit: String,
6653    targets: HashMap<SpanKey, SwitchTarget>,
6654    names: CandidateNames<'s>,
6655    source_sensitive_functions: HashSet<SpanKey>,
6656}
6657
6658impl<'a> SwitchTransformer<'a, '_> {
6659    fn identifier(&self, name: &str) -> Expression<'a> {
6660        self.ast
6661            .expression_identifier(Span::default(), self.ast.ident(name))
6662    }
6663
6664    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
6665        AssignmentTarget::from(
6666            self.ast
6667                .simple_assignment_target_assignment_target_identifier(
6668                    Span::default(),
6669                    self.ast.ident(name),
6670                ),
6671        )
6672    }
6673
6674    fn probe(&self, id: &str) -> Statement<'a> {
6675        self.ast.statement_expression(
6676            Span::default(),
6677            self.ast.expression_call(
6678                Span::default(),
6679                self.identifier(&self.coverage_hit),
6680                NONE,
6681                self.ast
6682                    .vec1(Argument::from(self.ast.expression_string_literal(
6683                        Span::default(),
6684                        self.ast.str(id),
6685                        None,
6686                    ))),
6687                false,
6688            ),
6689        )
6690    }
6691
6692    fn target(statement: &Statement<'a>) -> Option<SpanKey> {
6693        match statement {
6694            Statement::SwitchStatement(node) => Some(span_key(node.span)),
6695            Statement::LabeledStatement(node) => Self::target(&node.body),
6696            _ => None,
6697        }
6698    }
6699
6700    fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
6701        match statement {
6702            Statement::SwitchStatement(node) => Some(node),
6703            Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
6704            _ => None,
6705        }
6706    }
6707
6708    fn entered_assignment(&self, entered: &str) -> Statement<'a> {
6709        self.ast.statement_expression(
6710            Span::default(),
6711            self.ast.expression_assignment(
6712                Span::default(),
6713                AssignmentOperator::Assign,
6714                self.assignment_target(entered),
6715                self.ast.expression_boolean_literal(Span::default(), true),
6716            ),
6717        )
6718    }
6719
6720    /// Each case records only when the switch selected it: a case the one
6721    /// before falls into (`case 0: case 1:`, or a body with no `break`) ran
6722    /// without its test matching, and crediting it claimed a test of a value
6723    /// none had checked. `entered` says the switch has already selected a
6724    /// case; it also tells a switch with no default that nothing matched.
6725    fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
6726        let entered = self.names.allocate("_supercovSwitchEntered");
6727        let node = Self::inner_switch(statement).expect("switch target must remain a switch");
6728        for (index, case) in node.cases.iter_mut().enumerate() {
6729            let probe = self.probe(
6730                target
6731                    .case_ids
6732                    .get(index)
6733                    .expect("switch case target count must remain stable"),
6734            );
6735            let selected = self.ast.statement_if(
6736                Span::default(),
6737                self.ast.expression_unary(
6738                    Span::default(),
6739                    UnaryOperator::LogicalNot,
6740                    self.identifier(&entered),
6741                ),
6742                probe,
6743                None,
6744            );
6745            case.consequent.insert(0, self.entered_assignment(&entered));
6746            case.consequent.insert(0, selected);
6747        }
6748        let declaration =
6749            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
6750                Span::default(),
6751                VariableDeclarationKind::Let,
6752                self.ast.vec1(self.ast.variable_declarator(
6753                    Span::default(),
6754                    VariableDeclarationKind::Let,
6755                    self.ast.binding_pattern_binding_identifier(
6756                        Span::default(),
6757                        self.ast.ident(&entered),
6758                    ),
6759                    NONE,
6760                    Some(self.ast.expression_boolean_literal(Span::default(), false)),
6761                    false,
6762                )),
6763                false,
6764            ));
6765        let original = statement.take_in(self.ast.allocator);
6766        let mut body = self.ast.vec_from_array([declaration, original]);
6767        if let Some(no_match_id) = &target.no_match_id {
6768            body.push(self.ast.statement_if(
6769                Span::default(),
6770                self.ast.expression_unary(
6771                    Span::default(),
6772                    UnaryOperator::LogicalNot,
6773                    self.identifier(&entered),
6774                ),
6775                self.probe(no_match_id),
6776                None,
6777            ));
6778        }
6779        *statement = self.ast.statement_block(Span::default(), body);
6780    }
6781}
6782
6783impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
6784    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6785        if self
6786            .source_sensitive_functions
6787            .contains(&span_key(function.span))
6788        {
6789            return;
6790        }
6791        walk_mut::walk_function(self, function, flags);
6792    }
6793
6794    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6795        if self
6796            .source_sensitive_functions
6797            .contains(&span_key(function.span))
6798        {
6799            return;
6800        }
6801        walk_mut::walk_arrow_function_expression(self, function);
6802    }
6803
6804    // A with body is left uninstrumented: its object environment can
6805    // intercept probe identifiers (see `enter_with_statement`).
6806    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6807
6808    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
6809        let Some(key) = Self::target(statement) else {
6810            walk_mut::walk_statement(self, statement);
6811            return;
6812        };
6813        let Some(target) = self.targets.remove(&key) else {
6814            walk_mut::walk_statement(self, statement);
6815            return;
6816        };
6817        self.instrument(statement, &target);
6818        // The switch now sits in a block of its own; its target is spent, so
6819        // walking it reaches the switches inside its cases and nothing twice.
6820        walk_mut::walk_statement(self, statement);
6821    }
6822}
6823
6824struct RouteRequestPhaseTransformer<'a, 's> {
6825    ast: AstBuilder<'a>,
6826    file: &'s str,
6827    with_request_phase: String,
6828    used: bool,
6829    names: CandidateNames<'s>,
6830}
6831
6832impl<'a> RouteRequestPhaseTransformer<'a, '_> {
6833    fn is_remix_route(&self) -> bool {
6834        self.file.starts_with("app/routes/")
6835    }
6836
6837    fn is_next_route(&self) -> bool {
6838        let path = Path::new(self.file);
6839        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
6840            return false;
6841        };
6842        let is_route_module = [
6843            "route.js",
6844            "route.jsx",
6845            "route.ts",
6846            "route.tsx",
6847            "route.mjs",
6848            "route.mts",
6849            "route.cjs",
6850            "route.cts",
6851        ]
6852        .contains(&name);
6853        if !is_route_module {
6854            return false;
6855        }
6856        let components = path
6857            .components()
6858            .filter_map(|component| component.as_os_str().to_str())
6859            .collect::<Vec<_>>();
6860        // Anywhere under `app/`, `app/route.ts` (the handler for `/`) too.
6861        components[..components.len().saturating_sub(1)].contains(&"app")
6862    }
6863
6864    fn is_handler_name(&self, name: &str) -> bool {
6865        (self.is_remix_route() && matches!(name, "loader" | "action"))
6866            || (self.is_next_route()
6867                && matches!(
6868                    name,
6869                    "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
6870                ))
6871    }
6872
6873    fn identifier(&self, name: &str) -> Expression<'a> {
6874        self.ast
6875            .expression_identifier(Span::default(), self.ast.ident(name))
6876    }
6877
6878    fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
6879        self.ast.expression_call(
6880            Span::default(),
6881            self.identifier(&self.with_request_phase),
6882            NONE,
6883            self.ast.vec1(Argument::from(expression)),
6884            false,
6885        )
6886    }
6887
6888    fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
6889        Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
6890            Span::default(),
6891            Some(self.ast.declaration_variable(
6892                Span::default(),
6893                VariableDeclarationKind::Const,
6894                self.ast.vec1(self.ast.variable_declarator(
6895                    Span::default(),
6896                    VariableDeclarationKind::Const,
6897                    self.ast.binding_pattern_binding_identifier(
6898                        Span::default(),
6899                        self.ast.ident(exported_name),
6900                    ),
6901                    NONE,
6902                    Some(self.wrap_expression(self.identifier(original_name))),
6903                    false,
6904                )),
6905                false,
6906            )),
6907            self.ast.vec(),
6908            None,
6909            ImportOrExportKind::Value,
6910            NONE,
6911        ))
6912    }
6913
6914    fn transform_named_export(
6915        &mut self,
6916        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
6917        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6918    ) {
6919        if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
6920            for declarator in &mut declaration.declarations {
6921                let Some(name) = binding_identifier_name(&declarator.id) else {
6922                    continue;
6923                };
6924                if !self.is_handler_name(&name) {
6925                    continue;
6926                }
6927                if let Some(initializer) = &mut declarator.init {
6928                    let original = initializer.take_in(self.ast.allocator);
6929                    *initializer = self.wrap_expression(original);
6930                    self.used = true;
6931                }
6932            }
6933            output.push(Statement::ExportNamedDeclaration(export));
6934            return;
6935        }
6936
6937        if matches!(
6938            export.declaration,
6939            Some(Declaration::FunctionDeclaration(_))
6940        ) {
6941            let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
6942            else {
6943                unreachable!();
6944            };
6945            let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
6946                output.push(Statement::FunctionDeclaration(function));
6947                return;
6948            };
6949            if !self.is_handler_name(&exported_name) {
6950                export.declaration = Some(Declaration::FunctionDeclaration(function));
6951                output.push(Statement::ExportNamedDeclaration(export));
6952                return;
6953            }
6954            let original_name = self
6955                .names
6956                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6957            function.id = Some(
6958                self.ast
6959                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
6960            );
6961            output.push(Statement::FunctionDeclaration(function));
6962            output.push(self.wrapped_named_export(&exported_name, &original_name));
6963            self.used = true;
6964            return;
6965        }
6966
6967        if export.source.is_none() {
6968            output.push(Statement::ExportNamedDeclaration(export));
6969            return;
6970        }
6971        let source = export
6972            .source
6973            .as_ref()
6974            .expect("checked above")
6975            .clone_in(self.ast.allocator);
6976        let specifiers = export.specifiers.take_in(self.ast.allocator);
6977        let mut untouched = self.ast.vec();
6978        let mut handlers = Vec::new();
6979        for specifier in specifiers {
6980            let exported_name = specifier
6981                .exported
6982                .identifier_name()
6983                .map(|name| name.to_string());
6984            if exported_name
6985                .as_deref()
6986                .is_some_and(|name| self.is_handler_name(name))
6987            {
6988                handlers.push((exported_name.expect("checked above"), specifier));
6989            } else {
6990                untouched.push(specifier);
6991            }
6992        }
6993        if handlers.is_empty() {
6994            export.specifiers = untouched;
6995            output.push(Statement::ExportNamedDeclaration(export));
6996            return;
6997        }
6998        if !untouched.is_empty() {
6999            output.push(Statement::ExportNamedDeclaration(
7000                self.ast.alloc_export_named_declaration(
7001                    export.span,
7002                    None,
7003                    untouched,
7004                    Some(source.clone_in(self.ast.allocator)),
7005                    export.export_kind,
7006                    export.with_clause.take(),
7007                ),
7008            ));
7009        }
7010        for (exported_name, specifier) in handlers {
7011            let original_name = self
7012                .names
7013                .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
7014            output.push(Statement::ImportDeclaration(
7015                self.ast.alloc_import_declaration(
7016                    Span::default(),
7017                    Some(self.ast.vec1(
7018                        self.ast.import_declaration_specifier_import_specifier(
7019                            Span::default(),
7020                            specifier.local,
7021                            self.ast.binding_identifier(
7022                                Span::default(),
7023                                self.ast.ident(&original_name),
7024                            ),
7025                            ImportOrExportKind::Value,
7026                        ),
7027                    )),
7028                    source.clone_in(self.ast.allocator),
7029                    None,
7030                    NONE,
7031                    ImportOrExportKind::Value,
7032                ),
7033            ));
7034            output.push(self.wrapped_named_export(&exported_name, &original_name));
7035        }
7036        self.used = true;
7037    }
7038
7039    fn transform_default_export(
7040        &mut self,
7041        mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
7042        output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7043    ) {
7044        // Looked at before it is taken: taking it leaves a placeholder `null`,
7045        // and a server entry that exported its handler by name (`export
7046        // default handleRequest;`) exported `withRequestPhase(null)`.
7047        if matches!(
7048            export.declaration,
7049            ExportDefaultDeclarationKind::FunctionDeclaration(_)
7050        ) {
7051            let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
7052                export.declaration.take_in(self.ast.allocator)
7053            else {
7054                unreachable!();
7055            };
7056            let original_name = self
7057                .names
7058                .allocate("__supercovHandleRequestCoverageOriginal");
7059            function.id = Some(
7060                self.ast
7061                    .binding_identifier(Span::default(), self.ast.ident(&original_name)),
7062            );
7063            output.push(Statement::FunctionDeclaration(function));
7064            output.push(Statement::ExportDefaultDeclaration(
7065                self.ast.alloc_export_default_declaration(
7066                    Span::default(),
7067                    ExportDefaultDeclarationKind::from(
7068                        self.wrap_expression(self.identifier(&original_name)),
7069                    ),
7070                ),
7071            ));
7072            self.used = true;
7073            return;
7074        }
7075        if export.declaration.is_expression() {
7076            let original = export
7077                .declaration
7078                .take_in(self.ast.allocator)
7079                .into_expression();
7080            export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
7081            self.used = true;
7082        }
7083        output.push(Statement::ExportDefaultDeclaration(export));
7084    }
7085
7086    fn transform_program(&mut self, program: &mut Program<'a>) {
7087        let route_module = self.is_remix_route() || self.is_next_route();
7088        let server_entry = self.file.starts_with("app/entry.server.")
7089            && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
7090                &Path::new(self.file)
7091                    .extension()
7092                    .and_then(|value| value.to_str())
7093                    .unwrap_or(""),
7094            );
7095        if !route_module && !server_entry {
7096            return;
7097        }
7098        let original = program.body.take_in(self.ast.allocator);
7099        let mut output = self.ast.vec_with_capacity(original.len() + 4);
7100        for statement in original {
7101            match statement {
7102                Statement::ExportNamedDeclaration(export) if route_module => {
7103                    self.transform_named_export(export, &mut output);
7104                }
7105                Statement::ExportDefaultDeclaration(export) if server_entry => {
7106                    self.transform_default_export(export, &mut output);
7107                }
7108                statement => output.push(statement),
7109            }
7110        }
7111        program.body = output;
7112    }
7113}
7114
7115struct RequestPhaseTransformer<'a> {
7116    ast: AstBuilder<'a>,
7117    with_request_phase: String,
7118    used: bool,
7119    source_sensitive_functions: HashSet<SpanKey>,
7120}
7121
7122impl<'a> RequestPhaseTransformer<'a> {
7123    fn identifier(&self, name: &str) -> Expression<'a> {
7124        self.ast
7125            .expression_identifier(Span::default(), self.ast.ident(name))
7126    }
7127
7128    fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
7129        match callee {
7130            Expression::Identifier(identifier) => identifier.name == name,
7131            Expression::StaticMemberExpression(member) => member.property.name == name,
7132            _ => false,
7133        }
7134    }
7135
7136    fn callback_candidate(argument: &Argument<'a>) -> bool {
7137        matches!(
7138            argument,
7139            Argument::FunctionExpression(_)
7140                | Argument::ArrowFunctionExpression(_)
7141                | Argument::Identifier(_)
7142                | Argument::ComputedMemberExpression(_)
7143                | Argument::StaticMemberExpression(_)
7144                | Argument::PrivateFieldExpression(_)
7145        )
7146    }
7147
7148    fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
7149        matches!(
7150            argument,
7151            Argument::CallExpression(call)
7152                if expression_is_identifier(&call.callee, &self.with_request_phase)
7153        )
7154    }
7155
7156    fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
7157        if self.already_wrapped(argument) || !argument.is_expression() {
7158            return;
7159        }
7160        let expression = argument.to_expression_mut();
7161        let original = expression.take_in(self.ast.allocator);
7162        *expression = self.ast.expression_call(
7163            Span::default(),
7164            self.identifier(&self.with_request_phase),
7165            NONE,
7166            self.ast
7167                .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
7168                    |name| {
7169                        Argument::from(self.ast.expression_string_literal(
7170                            Span::default(),
7171                            self.ast.str(name),
7172                            None,
7173                        ))
7174                    },
7175                ))),
7176            false,
7177        );
7178        self.used = true;
7179    }
7180}
7181
7182impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
7183    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7184        if self
7185            .source_sensitive_functions
7186            .contains(&span_key(function.span))
7187        {
7188            return;
7189        }
7190        walk_mut::walk_function(self, function, flags);
7191    }
7192
7193    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7194        if self
7195            .source_sensitive_functions
7196            .contains(&span_key(function.span))
7197        {
7198            return;
7199        }
7200        walk_mut::walk_arrow_function_expression(self, function);
7201    }
7202
7203    // A with body is left uninstrumented: its object environment can
7204    // intercept probe identifiers (see `enter_with_statement`).
7205    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7206
7207    fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
7208        walk_mut::walk_call_expression(self, call);
7209        let property = match &call.callee {
7210            Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
7211            _ => None,
7212        };
7213        let mut callback_index = None;
7214        if matches!(property, Some("on" | "once" | "addListener"))
7215            && matches!(
7216                call.arguments.first(),
7217                Some(Argument::StringLiteral(event))
7218                    if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
7219            )
7220        {
7221            callback_index = Some(1);
7222        } else if Self::callee_is(&call.callee, "createServer") {
7223            callback_index = call.arguments.iter().rposition(Self::callback_candidate);
7224        }
7225        let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
7226        if let Some(index) = callback_index
7227            && let Some(argument) = call.arguments.get_mut(index)
7228        {
7229            self.wrap_argument(argument, connection.then_some("connection"));
7230        }
7231    }
7232}
7233
7234struct CandidateNames<'s> {
7235    source: &'s str,
7236    /// Every identifier in the source that begins with `_`, which every
7237    /// generated name does. Asking the source whether it contains a name was
7238    /// a scan of the whole file per name, and names are allocated per
7239    /// function: the second quadratic cost in instrumenting a long file.
7240    underscored: HashSet<&'s str>,
7241    allocated: HashSet<String>,
7242    suffix: usize,
7243    /// Appended to every name, so output that shares a scope with other
7244    /// instrumented files cannot collide with theirs.
7245    namespace: String,
7246}
7247
7248impl<'s> CandidateNames<'s> {
7249    fn new(source: &'s str) -> Self {
7250        Self::within(source, "")
7251    }
7252
7253    fn within(source: &'s str, namespace: &str) -> Self {
7254        let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
7255        let underscored = source
7256            .split(|c: char| !identifier(c))
7257            .filter(|word| word.starts_with('_'))
7258            .collect();
7259        Self {
7260            source,
7261            underscored,
7262            allocated: HashSet::new(),
7263            suffix: 0,
7264            namespace: namespace.to_owned(),
7265        }
7266    }
7267
7268    fn taken(&self, candidate: &str) -> bool {
7269        self.allocated.contains(candidate)
7270            || if candidate.starts_with('_') {
7271                self.underscored.contains(candidate)
7272            } else {
7273                self.source.contains(candidate)
7274            }
7275    }
7276
7277    fn allocate(&mut self, base: &str) -> String {
7278        loop {
7279            let candidate = if self.suffix == 0 {
7280                format!("{base}{}", self.namespace)
7281            } else {
7282                format!("{base}{}{}", self.suffix, self.namespace)
7283            };
7284            self.suffix += 1;
7285            if !self.taken(&candidate) {
7286                self.allocated.insert(candidate.clone());
7287                return candidate;
7288            }
7289        }
7290    }
7291}
7292
7293struct ControlProbeV2Transformer<'a, 's> {
7294    ast: AstBuilder<'a>,
7295    decisions: &'s [CandidateDecision],
7296    /// Expressions used as values that are decisions, by span, each with the
7297    /// root of the tree whose evaluation records it.
7298    value_decisions: &'s HashMap<SpanKey, SpanKey>,
7299    /// Decisions applied below a tree root and not yet recorded there, with
7300    /// the shape their outcome is read back from.
7301    pending_values: HashMap<SpanKey, Vec<(AppliedDecision, ValueShape)>>,
7302    mcdc_begin: String,
7303    mcdc_condition: String,
7304    mcdc_end: String,
7305    mcdc_end_v2: String,
7306    probe_file_v2: String,
7307    names: CandidateNames<'s>,
7308    scope_declarations: Vec<Vec<String>>,
7309    decision_index: usize,
7310    parameter_depth: usize,
7311    source_sensitive_functions: HashSet<SpanKey>,
7312    /// What goes around a statement whose false outcome is recorded after
7313    /// it: the statement that records it, and for a `do … while` one before
7314    /// the loop.
7315    after_statement: Option<(Option<Statement<'a>>, Statement<'a>)>,
7316    /// The statement being visited is the body of a label, which has to stay
7317    /// on it: what goes after the statement goes after the label.
7318    label_body: bool,
7319    /// The temporaries of a decision inside a parameter default, for the
7320    /// arrow that has to declare them around its conditional expression.
7321    inline_names: Option<Vec<String>>,
7322}
7323
7324#[derive(Clone, Copy)]
7325struct DecisionPlan {
7326    index: usize,
7327    condition_count: usize,
7328    inline_frame: bool,
7329}
7330
7331/// A decision whose conditions have been rewritten, and the temporaries its
7332/// outcome is recorded from.
7333struct AppliedDecision {
7334    index: usize,
7335    frame: String,
7336    values: Vec<String>,
7337    inline: bool,
7338}
7339
7340impl<'a> ControlProbeV2Transformer<'a, '_> {
7341    fn enter_declaration_scope(&mut self) {
7342        self.scope_declarations.push(Vec::new());
7343    }
7344
7345    fn leave_declaration_scope(&mut self) -> Vec<String> {
7346        self.scope_declarations
7347            .pop()
7348            .expect("program/function scope stack must remain balanced")
7349    }
7350
7351    fn allocate_scratch(&mut self, base: &str) -> String {
7352        let name = self.names.allocate(base);
7353        self.scope_declarations
7354            .last_mut()
7355            .expect("a control decision must be inside a program or function body")
7356            .push(name.clone());
7357        name
7358    }
7359
7360    fn scratch_for(&mut self, base: &str, inline: bool) -> String {
7361        if inline {
7362            self.names.allocate(base)
7363        } else {
7364            self.allocate_scratch(base)
7365        }
7366    }
7367
7368    fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
7369        let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
7370            self.ast.variable_declarator(
7371                Span::default(),
7372                VariableDeclarationKind::Let,
7373                self.ast
7374                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
7375                NONE,
7376                None,
7377                false,
7378            )
7379        }));
7380        let body = self.ast.alloc_function_body(
7381            Span::default(),
7382            self.ast.vec(),
7383            self.ast.vec_from_array([
7384                Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7385                    Span::default(),
7386                    VariableDeclarationKind::Let,
7387                    declarations,
7388                    false,
7389                )),
7390                self.ast.statement_return(Span::default(), Some(expression)),
7391            ]),
7392        );
7393        let params = self.ast.alloc_formal_parameters(
7394            Span::default(),
7395            FormalParameterKind::ArrowFormalParameters,
7396            self.ast.vec(),
7397            NONE,
7398        );
7399        let arrow = self.ast.expression_arrow_function(
7400            Span::default(),
7401            false,
7402            false,
7403            NONE,
7404            params,
7405            NONE,
7406            body,
7407        );
7408        self.ast
7409            .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
7410    }
7411
7412    fn prepend_declarations(
7413        &self,
7414        names: Vec<String>,
7415        statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7416    ) {
7417        if names.is_empty() {
7418            return;
7419        }
7420        let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
7421            self.ast.variable_declarator(
7422                Span::default(),
7423                VariableDeclarationKind::Let,
7424                self.ast
7425                    .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
7426                NONE,
7427                None,
7428                false,
7429            )
7430        }));
7431        statements.insert(
7432            0,
7433            Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7434                Span::default(),
7435                VariableDeclarationKind::Let,
7436                declarators,
7437                false,
7438            )),
7439        );
7440    }
7441
7442    fn identifier(&self, name: &str) -> Expression<'a> {
7443        self.ast
7444            .expression_identifier(Span::default(), self.ast.ident(name))
7445    }
7446
7447    fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
7448        let target = self
7449            .ast
7450            .simple_assignment_target_assignment_target_identifier(
7451                Span::default(),
7452                self.ast.ident(name),
7453            );
7454        AssignmentTarget::from(target)
7455    }
7456
7457    fn number(&self, value: u64) -> Expression<'a> {
7458        self.ast.expression_numeric_literal(
7459            Span::default(),
7460            value as f64,
7461            None,
7462            NumberBase::Decimal,
7463        )
7464    }
7465
7466    fn string(&self, value: &str) -> Expression<'a> {
7467        self.ast
7468            .expression_string_literal(Span::default(), self.ast.str(value), None)
7469    }
7470
7471    fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
7472        self.ast.object_property_kind_object_property(
7473            Span::default(),
7474            PropertyKind::Init,
7475            self.ast
7476                .property_key_static_identifier(Span::default(), self.ast.ident(name)),
7477            value,
7478            false,
7479            false,
7480            false,
7481        )
7482    }
7483
7484    fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
7485        let conditions = self.ast.expression_array(
7486            Span::default(),
7487            self.ast.vec_from_iter(
7488                decision
7489                    .conditions
7490                    .iter()
7491                    .map(|condition| ArrayExpressionElement::from(self.string(condition))),
7492            ),
7493        );
7494        let properties = self.ast.vec_from_array([
7495            self.object_property("id", self.string(&decision.id)),
7496            self.object_property("file", self.string(&decision.file)),
7497            self.object_property("line", self.number(decision.line as u64)),
7498            self.object_property("column", self.number(decision.column as u64)),
7499            self.object_property("source", self.string(&decision.source)),
7500            self.object_property("conditions", conditions),
7501            self.object_property("kind", self.string(&decision.kind)),
7502        ]);
7503        Expression::ObjectExpression(
7504            self.ast
7505                .alloc_object_expression(Span::default(), properties),
7506        )
7507    }
7508
7509    fn assign(&self, name: &str, value: Expression<'a>) -> Expression<'a> {
7510        self.ast.expression_assignment(
7511            Span::default(),
7512            AssignmentOperator::Assign,
7513            self.assignment_target(name),
7514            value,
7515        )
7516    }
7517
7518    /// One condition of a decision, as `(_f += w, _v = C)`.
7519    ///
7520    /// The assignment comes last because TypeScript narrows through an
7521    /// assignment's right side and through a comma's last operand, and through
7522    /// nothing else here. The condition used to end in its temporary
7523    /// (`(_v = C, _f += …, _v)`), which narrows nothing: `if (client) return
7524    /// client` returned `Client | null`, and a file that Supercov never
7525    /// touched failed to compile against the widened type.
7526    ///
7527    /// The first condition carries the decision's bookkeeping, since a comma
7528    /// around the whole decision would hide its `&&` and `||` from the same
7529    /// analysis: the frame starts at this condition's weight, and the other
7530    /// conditions' temporaries are cleared, so one that is not evaluated adds
7531    /// nothing when the outcome is recorded.
7532    fn instrument_condition(
7533        &self,
7534        expression: Expression<'a>,
7535        applied: &AppliedDecision,
7536        index: usize,
7537    ) -> Expression<'a> {
7538        let mut steps = self.ast.vec();
7539        if index == 0 {
7540            steps.push(self.assign(&applied.frame, self.number(1)));
7541            for name in &applied.values[1..] {
7542                steps.push(self.assign(name, self.number(0)));
7543            }
7544        } else {
7545            steps.push(self.ast.expression_assignment(
7546                Span::default(),
7547                AssignmentOperator::Addition,
7548                self.assignment_target(&applied.frame),
7549                self.number(3_u64.pow(index as u32)),
7550            ));
7551        }
7552        steps.push(self.assign(&applied.values[index], expression));
7553        self.ast.expression_sequence(Span::default(), steps)
7554    }
7555
7556    fn instrument_conditions(
7557        &self,
7558        expression: &mut Expression<'a>,
7559        applied: &AppliedDecision,
7560        next_index: &mut usize,
7561    ) {
7562        match expression {
7563            Expression::ParenthesizedExpression(parenthesized) => {
7564                self.instrument_conditions(&mut parenthesized.expression, applied, next_index)
7565            }
7566            Expression::LogicalExpression(logical)
7567                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7568            {
7569                self.instrument_conditions(&mut logical.left, applied, next_index);
7570                self.instrument_conditions(&mut logical.right, applied, next_index);
7571            }
7572            Expression::UnaryExpression(unary)
7573                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7574            {
7575                self.instrument_conditions(&mut unary.argument, applied, next_index);
7576            }
7577            _ => {
7578                let index = *next_index;
7579                *next_index += 1;
7580                let original = expression.take_in(self.ast.allocator);
7581                *expression = self.instrument_condition(original, applied, index);
7582            }
7583        }
7584    }
7585
7586    /// The decision's vector in base 3, from its temporaries: nothing for a
7587    /// condition that was not evaluated, its weight for a false one, twice
7588    /// that for a true one. `_f` holds the weights of the conditions that
7589    /// were evaluated; `+!!_v` adds the second for each true one.
7590    fn encoded(&self, applied: &AppliedDecision) -> Expression<'a> {
7591        let mut sum = self.identifier(&applied.frame);
7592        for (index, name) in applied.values.iter().enumerate() {
7593            let truth = self.ast.expression_unary(
7594                Span::default(),
7595                UnaryOperator::UnaryPlus,
7596                self.ast.expression_unary(
7597                    Span::default(),
7598                    UnaryOperator::LogicalNot,
7599                    self.ast.expression_unary(
7600                        Span::default(),
7601                        UnaryOperator::LogicalNot,
7602                        self.identifier(name),
7603                    ),
7604                ),
7605            );
7606            let term = if index == 0 {
7607                truth
7608            } else {
7609                self.ast.expression_binary(
7610                    Span::default(),
7611                    self.number(3_u64.pow(index as u32)),
7612                    BinaryOperator::Multiplication,
7613                    truth,
7614                )
7615            };
7616            sum = self
7617                .ast
7618                .expression_binary(Span::default(), sum, BinaryOperator::Addition, term);
7619        }
7620        sum
7621    }
7622
7623    /// `mcdcEndV2(file, decision, vector, outcome)`, where the outcome is
7624    /// known from where the call stands: the branch that was taken.
7625    /// The decisions used as values in one `&&`/`||` tree, recorded once the
7626    /// tree has been evaluated: `(_f = 0, _v0 = 0, .., _r = tree, end(..),
7627    /// _r)`. The tree stays whole -- a coverage tool the tests run counts it
7628    /// as one branch, and TypeScript narrows across it as written -- and the
7629    /// assignment keeps its value and its type. Each outcome is read back
7630    /// from the decision's own temporaries with `&`, `|` and `^`, which no
7631    /// tool counts as a branch; a decision the evaluation did not reach was
7632    /// reset to nothing, and the runtime records no vector for it.
7633    fn record_value_outcomes(
7634        &mut self,
7635        expression: &mut Expression<'a>,
7636        pending: Vec<(AppliedDecision, ValueShape)>,
7637    ) {
7638        let inline = pending.iter().any(|(applied, _)| applied.inline);
7639        let result = self.scratch_for("_supercovMcdcResult", inline);
7640        let mut steps = self.ast.vec();
7641        for (applied, _) in &pending {
7642            steps.push(self.assign(&applied.frame, self.number(0)));
7643            for name in &applied.values {
7644                steps.push(self.assign(name, self.number(0)));
7645            }
7646        }
7647        let value = expression.take_in(self.ast.allocator);
7648        steps.push(self.assign(&result, value));
7649        let mut names = Vec::new();
7650        for (applied, shape) in &pending {
7651            let truth = self.ast.expression_unary(
7652                Span::default(),
7653                UnaryOperator::LogicalNot,
7654                self.ast.expression_unary(
7655                    Span::default(),
7656                    UnaryOperator::LogicalNot,
7657                    self.shape_bits(shape, applied),
7658                ),
7659            );
7660            steps.push(self.ast.expression_call(
7661                Span::default(),
7662                self.identifier(&self.mcdc_end_v2),
7663                NONE,
7664                self.ast.vec_from_array([
7665                    Argument::from(self.identifier(&self.probe_file_v2)),
7666                    Argument::from(self.number(applied.index as u64)),
7667                    Argument::from(self.encoded(applied)),
7668                    Argument::from(truth),
7669                ]),
7670                false,
7671            ));
7672            if applied.inline {
7673                names.push(applied.frame.clone());
7674                names.extend(applied.values.iter().cloned());
7675            }
7676        }
7677        steps.push(self.identifier(&result));
7678        let observed = self.ast.expression_parenthesized(
7679            Span::default(),
7680            self.ast.expression_sequence(Span::default(), steps),
7681        );
7682        *expression = if inline {
7683            names.push(result);
7684            self.wrap_inline_frame(observed, &names)
7685        } else {
7686            observed
7687        };
7688    }
7689
7690    /// A decision's outcome from its temporaries, as 0 or 1: a condition that
7691    /// was not evaluated reads 0, which is never what decided the outcome.
7692    fn shape_bits(&self, shape: &ValueShape, applied: &AppliedDecision) -> Expression<'a> {
7693        let binary = |left, operator, right| {
7694            self.ast.expression_parenthesized(
7695                Span::default(),
7696                self.ast
7697                    .expression_binary(Span::default(), left, operator, right),
7698            )
7699        };
7700        match shape {
7701            ValueShape::Condition(index) => self.ast.expression_unary(
7702                Span::default(),
7703                UnaryOperator::UnaryPlus,
7704                self.ast.expression_unary(
7705                    Span::default(),
7706                    UnaryOperator::LogicalNot,
7707                    self.ast.expression_unary(
7708                        Span::default(),
7709                        UnaryOperator::LogicalNot,
7710                        self.identifier(&applied.values[*index]),
7711                    ),
7712                ),
7713            ),
7714            ValueShape::And(left, right) => binary(
7715                self.shape_bits(left, applied),
7716                BinaryOperator::BitwiseAnd,
7717                self.shape_bits(right, applied),
7718            ),
7719            ValueShape::Or(left, right) => binary(
7720                self.shape_bits(left, applied),
7721                BinaryOperator::BitwiseOR,
7722                self.shape_bits(right, applied),
7723            ),
7724            ValueShape::Not(inner) => binary(
7725                self.number(1),
7726                BinaryOperator::BitwiseXOR,
7727                self.shape_bits(inner, applied),
7728            ),
7729        }
7730    }
7731
7732    fn outcome(&self, applied: &AppliedDecision, outcome: bool) -> Expression<'a> {
7733        self.ast.expression_call(
7734            Span::default(),
7735            self.identifier(&self.mcdc_end_v2),
7736            NONE,
7737            self.ast.vec_from_array([
7738                Argument::from(self.identifier(&self.probe_file_v2)),
7739                Argument::from(self.number(applied.index as u64)),
7740                Argument::from(self.encoded(applied)),
7741                Argument::from(
7742                    self.ast
7743                        .expression_boolean_literal(Span::default(), outcome),
7744                ),
7745            ]),
7746            false,
7747        )
7748    }
7749
7750    /// Put `first` at the head of a branch or a loop body, making it a block
7751    /// if it is a single statement.
7752    fn prepend_statement(&self, body: &mut Statement<'a>, first: Statement<'a>) {
7753        if let Statement::BlockStatement(block) = body {
7754            block.body.insert(0, first);
7755            return;
7756        }
7757        let original = body.take_in(self.ast.allocator);
7758        *body = self
7759            .ast
7760            .statement_block(Span::default(), self.ast.vec_from_array([first, original]));
7761    }
7762
7763    /// What records the true outcome where the false one is recorded after
7764    /// the statement: `mcdcEndV2(…, true), _f = 0`. The frame is at least 1
7765    /// once a test has run, so cleared it says the outcome has been recorded.
7766    fn settled(&self, applied: &AppliedDecision) -> Expression<'a> {
7767        self.ast.expression_sequence(
7768            Span::default(),
7769            self.ast.vec_from_array([
7770                self.outcome(applied, true),
7771                self.assign(&applied.frame, self.number(0)),
7772            ]),
7773        )
7774    }
7775
7776    fn when_unsettled(&self, applied: &AppliedDecision, record: Expression<'a>) -> Statement<'a> {
7777        self.ast.statement_expression(
7778            Span::default(),
7779            self.ast.expression_logical(
7780                Span::default(),
7781                self.identifier(&applied.frame),
7782                LogicalOperator::And,
7783                record,
7784            ),
7785        )
7786    }
7787
7788    /// Record the false outcome after the statement, when nothing runs for
7789    /// it inside: an `if` without an `else`, a loop its test ended.
7790    ///
7791    /// An `else` made for the call would be a branch of its own to a coverage
7792    /// tool the tests run, and the `if` would end at a brace nobody wrote:
7793    /// istanbul lost the statement and its implicit `else` with it. A `break`
7794    /// leaves a loop from its body, where the outcome is already recorded.
7795    fn record_false_after(&mut self, applied: &AppliedDecision, before: Option<Statement<'a>>) {
7796        let after = self.when_unsettled(applied, self.outcome(applied, false));
7797        self.after_statement = Some((before, after));
7798    }
7799
7800    /// A loop's true outcome is recorded where its body starts.
7801    fn loop_outcomes(
7802        &mut self,
7803        body: &mut Statement<'a>,
7804        applied: &AppliedDecision,
7805        tested_after: bool,
7806    ) {
7807        if !tested_after {
7808            let entered = self
7809                .ast
7810                .statement_expression(Span::default(), self.settled(applied));
7811            self.prepend_statement(body, entered);
7812            self.record_false_after(applied, None);
7813            return;
7814        }
7815        // A `do … while` body runs before its test: the frame says whether a
7816        // test came before this iteration. It is cleared before the loop,
7817        // because a test that threw the last time the loop ran left it set.
7818        let entered = self.when_unsettled(applied, self.settled(applied));
7819        self.prepend_statement(body, entered);
7820        let before = self
7821            .ast
7822            .statement_expression(Span::default(), self.assign(&applied.frame, self.number(0)));
7823        self.record_false_after(applied, Some(before));
7824    }
7825
7826    fn instrument_condition_v1(
7827        &self,
7828        expression: Expression<'a>,
7829        frame_name: &str,
7830        index: usize,
7831    ) -> Expression<'a> {
7832        self.ast.expression_call(
7833            Span::default(),
7834            self.identifier(&self.mcdc_condition),
7835            NONE,
7836            self.ast.vec_from_array([
7837                Argument::from(self.identifier(frame_name)),
7838                Argument::from(self.number(index as u64)),
7839                Argument::from(expression),
7840            ]),
7841            false,
7842        )
7843    }
7844
7845    fn instrument_conditions_v1(
7846        &self,
7847        expression: &mut Expression<'a>,
7848        frame_name: &str,
7849        next_index: &mut usize,
7850    ) {
7851        match expression {
7852            Expression::ParenthesizedExpression(parenthesized) => {
7853                self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
7854            }
7855            Expression::LogicalExpression(logical)
7856                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7857            {
7858                self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
7859                self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
7860            }
7861            Expression::UnaryExpression(unary)
7862                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7863            {
7864                self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
7865            }
7866            _ => {
7867                let index = *next_index;
7868                *next_index += 1;
7869                let original = expression.take_in(self.ast.allocator);
7870                *expression = self.instrument_condition_v1(original, frame_name, index);
7871            }
7872        }
7873    }
7874
7875    fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
7876        let mut condition_spans = Vec::new();
7877        collect_conditions(test, &mut condition_spans);
7878        let plan = DecisionPlan {
7879            index: self.decision_index,
7880            condition_count: condition_spans.len(),
7881            inline_frame: self.parameter_depth > 0,
7882        };
7883        self.decision_index += 1;
7884        plan
7885    }
7886
7887    /// Rewrite a decision's conditions. What records the outcome is left to
7888    /// the caller, which knows the branches; a decision too wide for the
7889    /// encoding keeps the call-based form and returns nothing.
7890    fn apply_decision(
7891        &mut self,
7892        test: &mut Expression<'a>,
7893        plan: DecisionPlan,
7894    ) -> Option<AppliedDecision> {
7895        if plan.condition_count > 32 {
7896            let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
7897            let mut next_index = 0;
7898            self.instrument_conditions_v1(test, &frame_name, &mut next_index);
7899            debug_assert_eq!(next_index, plan.condition_count);
7900
7901            let decision = &self.decisions[plan.index];
7902            let begin = self.ast.expression_call(
7903                Span::default(),
7904                self.identifier(&self.mcdc_begin),
7905                NONE,
7906                self.ast.vec_from_array([
7907                    Argument::from(self.string(&decision.id)),
7908                    Argument::from(self.decision_meta(decision)),
7909                ]),
7910                false,
7911            );
7912            let assign_frame = self.ast.expression_assignment(
7913                Span::default(),
7914                AssignmentOperator::Assign,
7915                self.assignment_target(&frame_name),
7916                begin,
7917            );
7918            let instrumented = test.take_in(self.ast.allocator);
7919            let end = self.ast.expression_call(
7920                Span::default(),
7921                self.identifier(&self.mcdc_end),
7922                NONE,
7923                self.ast.vec_from_array([
7924                    Argument::from(self.identifier(&frame_name)),
7925                    Argument::from(instrumented),
7926                ]),
7927                false,
7928            );
7929            let observed = self.ast.expression_sequence(
7930                Span::default(),
7931                self.ast.vec_from_array([assign_frame, end]),
7932            );
7933            *test = if plan.inline_frame {
7934                self.wrap_inline_frame(observed, &[frame_name])
7935            } else {
7936                observed
7937            };
7938            return None;
7939        }
7940
7941        let applied = AppliedDecision {
7942            index: plan.index,
7943            frame: self.scratch_for("_supercovMcdcFrame", plan.inline_frame),
7944            values: (0..plan.condition_count)
7945                .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
7946                .collect(),
7947            inline: plan.inline_frame,
7948        };
7949        let mut next_index = 0;
7950        self.instrument_conditions(test, &applied, &mut next_index);
7951        debug_assert_eq!(
7952            next_index,
7953            plan.condition_count,
7954            "decision at {:?}",
7955            test.span()
7956        );
7957        Some(applied)
7958    }
7959}
7960
7961impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
7962    fn visit_program(&mut self, program: &mut Program<'a>) {
7963        self.enter_declaration_scope();
7964        walk_mut::walk_program(self, program);
7965        let declarations = self.leave_declaration_scope();
7966        self.prepend_declarations(declarations, &mut program.body);
7967    }
7968
7969    fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
7970        self.enter_declaration_scope();
7971        walk_mut::walk_function_body(self, body);
7972        let declarations = self.leave_declaration_scope();
7973        self.prepend_declarations(declarations, &mut body.statements);
7974    }
7975
7976    fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7977        if self
7978            .source_sensitive_functions
7979            .contains(&span_key(function.span))
7980        {
7981            return;
7982        }
7983        let outer_parameter_depth = self.parameter_depth;
7984        self.parameter_depth = 0;
7985        walk_mut::walk_function(self, function, flags);
7986        self.parameter_depth = outer_parameter_depth;
7987    }
7988
7989    fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7990        if self
7991            .source_sensitive_functions
7992            .contains(&span_key(function.span))
7993        {
7994            return;
7995        }
7996        let outer_parameter_depth = self.parameter_depth;
7997        self.parameter_depth = 0;
7998        walk_mut::walk_arrow_function_expression(self, function);
7999        self.parameter_depth = outer_parameter_depth;
8000    }
8001
8002    fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
8003        self.parameter_depth += 1;
8004        walk_mut::walk_formal_parameters(self, parameters);
8005        self.parameter_depth -= 1;
8006    }
8007
8008    // A with body is left uninstrumented: its object environment can
8009    // intercept probe identifiers (see `enter_with_statement`).
8010    fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
8011
8012    // A statement whose false outcome is recorded after it becomes a block
8013    // of the two, so it can stand wherever one statement can. A label stays
8014    // on its statement, where `break` and `continue` look for it, and the
8015    // block goes around the outermost label.
8016    fn visit_statement(&mut self, statement: &mut Statement<'a>) {
8017        let label_body = self.label_body;
8018        self.label_body = matches!(statement, Statement::LabeledStatement(_));
8019        walk_mut::walk_statement(self, statement);
8020        self.label_body = false;
8021        if label_body {
8022            return;
8023        }
8024        if let Some((before, after)) = self.after_statement.take() {
8025            let decided = statement.take_in(self.ast.allocator);
8026            let mut recorded = self.ast.vec_with_capacity(3);
8027            recorded.extend(before);
8028            recorded.push(decided);
8029            recorded.push(after);
8030            *statement = self.ast.statement_block(Span::default(), recorded);
8031        }
8032    }
8033
8034    // A static block is a scope of its own for the temporaries: inside a
8035    // parameter default there is no function body between it and the
8036    // parameter list, and a statement cannot be wrapped in an arrow.
8037    fn visit_static_block(&mut self, block: &mut oxc_ast::ast::StaticBlock<'a>) {
8038        let outer_parameter_depth = self.parameter_depth;
8039        self.parameter_depth = 0;
8040        self.enter_declaration_scope();
8041        walk_mut::walk_static_block(self, block);
8042        let declarations = self.leave_declaration_scope();
8043        self.prepend_declarations(declarations, &mut block.body);
8044        self.parameter_depth = outer_parameter_depth;
8045    }
8046
8047    fn visit_expression(&mut self, expression: &mut Expression<'a>) {
8048        let key = span_key(expression.span());
8049        let value = self.value_decisions.get(&key).copied().map(|root| {
8050            let mut next = 0;
8051            (
8052                root,
8053                self.reserve_decision(expression),
8054                value_shape(expression, &mut next),
8055            )
8056        });
8057        let is_root = self.value_decisions.values().any(|root| *root == key);
8058        walk_mut::walk_expression(self, expression);
8059        if let Some(names) = self.inline_names.take()
8060            && matches!(expression, Expression::ConditionalExpression(_))
8061        {
8062            let conditional = expression.take_in(self.ast.allocator);
8063            *expression = self.wrap_inline_frame(conditional, &names);
8064        }
8065        if let Some((root, plan, shape)) = value
8066            && let Some(applied) = self.apply_decision(expression, plan)
8067        {
8068            self.pending_values
8069                .entry(root)
8070                .or_default()
8071                .push((applied, shape));
8072        }
8073        if is_root && let Some(pending) = self.pending_values.remove(&key) {
8074            self.record_value_outcomes(expression, pending);
8075        }
8076    }
8077
8078    fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
8079        let plan = decision_outcome_is_variable(&statement.test)
8080            .then(|| self.reserve_decision(&statement.test));
8081        self.visit_expression(&mut statement.test);
8082        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8083        self.visit_statement(&mut statement.consequent);
8084        if let Some(alternate) = &mut statement.alternate {
8085            self.visit_statement(alternate);
8086        }
8087        let Some(applied) = applied else {
8088            return;
8089        };
8090        let Some(alternate) = &mut statement.alternate else {
8091            let taken = self
8092                .ast
8093                .statement_expression(Span::default(), self.settled(&applied));
8094            self.prepend_statement(&mut statement.consequent, taken);
8095            self.record_false_after(&applied, None);
8096            return;
8097        };
8098        let skipped = self
8099            .ast
8100            .statement_expression(Span::default(), self.outcome(&applied, false));
8101        self.prepend_statement(alternate, skipped);
8102        let taken = self
8103            .ast
8104            .statement_expression(Span::default(), self.outcome(&applied, true));
8105        self.prepend_statement(&mut statement.consequent, taken);
8106    }
8107
8108    fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
8109        let plan = decision_outcome_is_variable(&expression.test)
8110            .then(|| self.reserve_decision(&expression.test));
8111        self.visit_expression(&mut expression.test);
8112        let applied = plan.and_then(|plan| self.apply_decision(&mut expression.test, plan));
8113        self.visit_expression(&mut expression.consequent);
8114        self.visit_expression(&mut expression.alternate);
8115        let Some(applied) = applied else {
8116            return;
8117        };
8118        // A coverage tool the tests run reads where a branch starts from the
8119        // source map, and the printer maps neither a sequence nor a call:
8120        // the branch started at whatever token came before it. `void` is
8121        // mapped, and here it is mapped to the branch's own start.
8122        for (branch, outcome) in [
8123            (&mut expression.consequent, true),
8124            (&mut expression.alternate, false),
8125        ] {
8126            let value = branch.take_in(self.ast.allocator);
8127            let start = value.span().start;
8128            let record = self.ast.expression_unary(
8129                Span::new(start, start + 1),
8130                UnaryOperator::Void,
8131                self.outcome(&applied, outcome),
8132            );
8133            *branch = self
8134                .ast
8135                .expression_sequence(Span::default(), self.ast.vec_from_array([record, value]));
8136        }
8137        if applied.inline {
8138            let mut names = vec![applied.frame];
8139            names.extend(applied.values);
8140            self.inline_names = Some(names);
8141        }
8142    }
8143
8144    fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
8145        let plan = decision_outcome_is_variable(&statement.test)
8146            .then(|| self.reserve_decision(&statement.test));
8147        self.visit_expression(&mut statement.test);
8148        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8149        self.visit_statement(&mut statement.body);
8150        if let Some(applied) = applied {
8151            self.loop_outcomes(&mut statement.body, &applied, false);
8152        }
8153    }
8154
8155    fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
8156        let plan = decision_outcome_is_variable(&statement.test)
8157            .then(|| self.reserve_decision(&statement.test));
8158        self.visit_statement(&mut statement.body);
8159        self.visit_expression(&mut statement.test);
8160        let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8161        if let Some(applied) = applied {
8162            self.loop_outcomes(&mut statement.body, &applied, true);
8163        }
8164    }
8165
8166    fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
8167        let plan = statement
8168            .test
8169            .as_ref()
8170            .filter(|test| decision_outcome_is_variable(test))
8171            .map(|test| self.reserve_decision(test));
8172        if let Some(init) = &mut statement.init {
8173            self.visit_for_statement_init(init);
8174        }
8175        let mut applied = None;
8176        if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
8177            self.visit_expression(test);
8178            applied = self.apply_decision(test, plan);
8179        }
8180        if let Some(update) = &mut statement.update {
8181            self.visit_expression(update);
8182        }
8183        self.visit_statement(&mut statement.body);
8184        if let Some(applied) = applied {
8185            self.loop_outcomes(&mut statement.body, &applied, false);
8186        }
8187    }
8188}
8189
8190struct DecisionCollector<'s> {
8191    source: &'s str,
8192    file: &'s str,
8193    decisions: Vec<CandidateDecision>,
8194    decision_vector_counts: Vec<usize>,
8195    decision_logical_nodes: HashSet<SpanKey>,
8196    source_sensitive_functions: &'s HashSet<SpanKey>,
8197    /// The expressions recorded as decisions where they are used as values,
8198    /// for the transformer to find by span, each with the root of the `&&`/`||`
8199    /// tree it sits in: the outcome is recorded once that root has been
8200    /// evaluated, so the tree stays one tree.
8201    value_decisions: HashMap<SpanKey, SpanKey>,
8202    /// The root of the `&&`/`||`/`??` tree being visited, through parentheses.
8203    logical_root: Option<SpanKey>,
8204    /// The operators inside a decision used as a value. They stay in their
8205    /// value tree -- its branches and the one tree a coverage tool reads --
8206    /// and only stop a decision being found inside the decision.
8207    value_logical_nodes: HashSet<SpanKey>,
8208}
8209
8210impl DecisionCollector<'_> {
8211    fn in_a_decision(&self, span: Span) -> bool {
8212        let key = span_key(span);
8213        self.decision_logical_nodes.contains(&key) || self.value_logical_nodes.contains(&key)
8214    }
8215
8216    fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
8217        let mut condition_spans = Vec::new();
8218        collect_conditions(test, &mut condition_spans);
8219        if kind == "value" {
8220            collect_decision_logical_nodes(test, &mut self.value_logical_nodes);
8221        } else {
8222            collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
8223        }
8224        // Babel treats redundant parentheses as parser metadata rather than
8225        // part of the decision node. oxc preserves a ParenthesizedExpression,
8226        // so normalize it here to keep locations and stable IDs parser-neutral.
8227        let span = transparent_expression(test).span();
8228        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8229        self.decisions.push(CandidateDecision {
8230            id: stable_id(self.source, self.file, "decision", span, kind),
8231            file: self.file.to_string(),
8232            line,
8233            column,
8234            source: source_slice(self.source, span).to_string(),
8235            conditions: condition_spans
8236                .iter()
8237                .map(|condition| source_slice(self.source, *condition).to_string())
8238                .collect(),
8239            kind: kind.to_string(),
8240        });
8241        self.decision_vector_counts.push(
8242            if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
8243                reachable_vector_count(test, &condition_spans)
8244            } else {
8245                0
8246            },
8247        );
8248    }
8249}
8250
8251/// The `&&` or `||` under any parentheses and `!`s: `!!(a && b)` is the
8252/// decision `a && b` negated twice.
8253fn compound_root<'e, 'a>(expression: &'e Expression<'a>) -> Option<&'e LogicalExpression<'a>> {
8254    match expression {
8255        Expression::ParenthesizedExpression(parenthesized) => {
8256            compound_root(&parenthesized.expression)
8257        }
8258        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8259            compound_root(&unary.argument)
8260        }
8261        Expression::LogicalExpression(logical)
8262            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8263        {
8264            Some(logical)
8265        }
8266        _ => None,
8267    }
8268}
8269
8270/// MC/DC applies only when a decision can produce both Boolean outcomes.
8271/// This deliberately recognizes only semantics that are provable from syntax;
8272/// it never evaluates user code or assumes values for identifiers/calls.
8273fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
8274    syntactic_boolean_outcome(expression).is_none()
8275}
8276
8277fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
8278    match transparent_expression(expression) {
8279        Expression::BooleanLiteral(literal) => Some(literal.value),
8280        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8281            syntactic_boolean_outcome(&unary.argument).map(|value| !value)
8282        }
8283        Expression::LogicalExpression(logical)
8284            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8285        {
8286            let left = syntactic_boolean_outcome(&logical.left);
8287            let right = syntactic_boolean_outcome(&logical.right);
8288            match logical.operator {
8289                LogicalOperator::And => match (left, right) {
8290                    (Some(false), _) | (_, Some(false)) => Some(false),
8291                    (Some(true), value) | (value, Some(true)) => value,
8292                    _ => None,
8293                },
8294                LogicalOperator::Or => match (left, right) {
8295                    (Some(true), _) | (_, Some(true)) => Some(true),
8296                    (Some(false), value) | (value, Some(false)) => value,
8297                    _ => None,
8298                },
8299                LogicalOperator::Coalesce => None,
8300            }
8301        }
8302        _ => None,
8303    }
8304}
8305
8306#[derive(Default)]
8307struct CoverageSurfaceScanner {
8308    found: bool,
8309}
8310
8311impl<'a> Visit<'a> for CoverageSurfaceScanner {
8312    fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
8313        self.found = true;
8314    }
8315
8316    fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
8317        self.found = true;
8318    }
8319
8320    fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
8321        self.found = true;
8322    }
8323
8324    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
8325        self.found = true;
8326    }
8327
8328    fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
8329        self.found = true;
8330    }
8331
8332    fn visit_class(&mut self, _class: &Class<'a>) {
8333        self.found = true;
8334    }
8335
8336    fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
8337        if expression.operator.is_logical() {
8338            self.found = true;
8339        } else {
8340            walk::walk_assignment_expression(self, expression);
8341        }
8342    }
8343}
8344
8345fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
8346    fn visit_condition(expression: &Expression<'_>) -> bool {
8347        let mut scanner = CoverageSurfaceScanner::default();
8348        scanner.visit_expression(expression);
8349        !scanner.found
8350    }
8351    match transparent_expression(expression) {
8352        Expression::LogicalExpression(logical)
8353            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8354        {
8355            decision_conditions_are_transparent(&logical.left)
8356                && decision_conditions_are_transparent(&logical.right)
8357        }
8358        Expression::UnaryExpression(unary)
8359            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8360        {
8361            decision_conditions_are_transparent(&unary.argument)
8362        }
8363        condition => visit_condition(condition),
8364    }
8365}
8366
8367fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
8368    fn evaluate(
8369        expression: &Expression<'_>,
8370        assignment: usize,
8371        encoded: &mut usize,
8372        indices: &HashMap<SpanKey, usize>,
8373    ) -> bool {
8374        match transparent_expression(expression) {
8375            Expression::LogicalExpression(logical)
8376                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8377            {
8378                let left = evaluate(&logical.left, assignment, encoded, indices);
8379                if logical.operator == LogicalOperator::And {
8380                    left && evaluate(&logical.right, assignment, encoded, indices)
8381                } else {
8382                    left || evaluate(&logical.right, assignment, encoded, indices)
8383                }
8384            }
8385            Expression::UnaryExpression(unary)
8386                if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8387            {
8388                !evaluate(&unary.argument, assignment, encoded, indices)
8389            }
8390            condition => {
8391                let index = indices
8392                    .get(&span_key(condition.span()))
8393                    .expect("decision condition index must remain stable");
8394                let value = assignment & (1 << index) != 0;
8395                *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
8396                value
8397            }
8398        }
8399    }
8400
8401    let indices = conditions
8402        .iter()
8403        .enumerate()
8404        .map(|(index, span)| (span_key(*span), index))
8405        .collect::<HashMap<_, _>>();
8406    let mut vectors = HashSet::new();
8407    for assignment in 0..(1_usize << conditions.len()) {
8408        let mut encoded = 0;
8409        let outcome = evaluate(expression, assignment, &mut encoded, &indices);
8410        vectors.insert(encoded * 2 + usize::from(outcome));
8411    }
8412    vectors.len()
8413}
8414
8415fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
8416    match expression {
8417        Expression::ParenthesizedExpression(parenthesized) => {
8418            collect_decision_logical_nodes(&parenthesized.expression, nodes);
8419        }
8420        Expression::LogicalExpression(logical)
8421            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8422        {
8423            nodes.insert(span_key(logical.span));
8424            collect_decision_logical_nodes(&logical.left, nodes);
8425            collect_decision_logical_nodes(&logical.right, nodes);
8426        }
8427        Expression::UnaryExpression(unary)
8428            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8429        {
8430            collect_decision_logical_nodes(&unary.argument, nodes);
8431        }
8432        _ => {}
8433    }
8434}
8435
8436#[derive(Default)]
8437struct LogicalBranchAnalysis {
8438    branches: Vec<CandidateBranch>,
8439    logical_targets: HashMap<SpanKey, (String, String)>,
8440}
8441
8442#[derive(Default)]
8443struct LogicalAssignmentAnalysis {
8444    branches: Vec<CandidateBranch>,
8445    targets: HashMap<SpanKey, (String, String)>,
8446}
8447
8448#[derive(Default)]
8449struct OptionalMemberAnalysis {
8450    branches: Vec<CandidateBranch>,
8451    targets: HashMap<SpanKey, (String, String)>,
8452    /// Each target's place in its chain, as `optionalSelectV2` reads it.
8453    links: HashMap<SpanKey, u8>,
8454    /// The optional call a target's object holds, for a target right after
8455    /// one: whether that call went ahead says whether the target ran.
8456    after_call: HashMap<SpanKey, SpanKey>,
8457}
8458
8459/// An optional link of the same chain runs before this one.
8460const LINK_AFTER_ANOTHER: u8 = 1;
8461/// An optional link of the same chain runs after this one.
8462const LINK_BEFORE_ANOTHER: u8 = 2;
8463/// The link before this one is an optional call.
8464const LINK_AFTER_CALL: u8 = 4;
8465
8466/// The optional link of the same chain an optional member's object holds.
8467enum LinkBelow {
8468    None,
8469    Member,
8470    Call(SpanKey),
8471}
8472
8473/// The earlier optional link of the same chain that `object`, the object of
8474/// an optional member, holds: through plain member accesses, plain calls and
8475/// `!`, which the chain's short-circuit skips along with the rest.
8476/// Parentheses end the chain.
8477fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
8478    let mut current = object;
8479    loop {
8480        current = match current {
8481            Expression::StaticMemberExpression(member) if member.optional => {
8482                return LinkBelow::Member;
8483            }
8484            Expression::ComputedMemberExpression(member) if member.optional => {
8485                return LinkBelow::Member;
8486            }
8487            Expression::PrivateFieldExpression(member) if member.optional => {
8488                return LinkBelow::Member;
8489            }
8490            Expression::CallExpression(call) if call.optional => {
8491                return LinkBelow::Call(span_key(call.span));
8492            }
8493            Expression::StaticMemberExpression(member) => &member.object,
8494            Expression::ComputedMemberExpression(member) => &member.object,
8495            Expression::PrivateFieldExpression(member) => &member.object,
8496            Expression::CallExpression(call) => &call.callee,
8497            Expression::TSNonNullExpression(inner) => &inner.expression,
8498            _ => return LinkBelow::None,
8499        };
8500    }
8501}
8502
8503/// The same walk outward: whether this member is the object, through the
8504/// same plain links, of a later optional member of its chain.
8505fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
8506    for ancestor in context.ancestors() {
8507        match ancestor {
8508            Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
8509            Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
8510            Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
8511            Ancestor::StaticMemberExpressionObject(_)
8512            | Ancestor::ComputedMemberExpressionObject(_)
8513            | Ancestor::PrivateFieldExpressionObject(_)
8514            | Ancestor::TSNonNullExpressionExpression(_) => {}
8515            Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
8516            _ => return false,
8517        }
8518    }
8519    false
8520}
8521
8522#[derive(Default)]
8523struct OptionalCallAnalysis {
8524    branches: Vec<CandidateBranch>,
8525    sites: HashMap<SpanKey, (String, String)>,
8526    roots: HashMap<SpanKey, Vec<SpanKey>>,
8527    limitations: Vec<CandidateLimitation>,
8528}
8529
8530#[derive(Clone)]
8531struct DefaultTarget {
8532    default_id: String,
8533    provided_id: String,
8534    inferred_name: Option<String>,
8535}
8536
8537#[derive(Default)]
8538struct DefaultAnalysis {
8539    branches: Vec<CandidateBranch>,
8540    parameter_targets: HashMap<SpanKey, DefaultTarget>,
8541    binding_targets: HashMap<SpanKey, DefaultTarget>,
8542    limitations: Vec<CandidateLimitation>,
8543}
8544
8545#[derive(Clone)]
8546struct ExtendedTarget {
8547    first_id: String,
8548    second_id: String,
8549}
8550
8551#[derive(Default)]
8552struct ExtendedAnalysis {
8553    branches: Vec<CandidateBranch>,
8554    try_targets: HashMap<SpanKey, ExtendedTarget>,
8555    loop_targets: HashMap<SpanKey, ExtendedTarget>,
8556}
8557
8558#[derive(Clone)]
8559struct SwitchTarget {
8560    case_ids: Vec<String>,
8561    no_match_id: Option<String>,
8562}
8563
8564#[derive(Default)]
8565struct SwitchAnalysis {
8566    branches: Vec<CandidateBranch>,
8567    targets: HashMap<SpanKey, SwitchTarget>,
8568}
8569
8570struct SwitchCollector<'s> {
8571    source: &'s str,
8572    file: &'s str,
8573    source_sensitive_functions: &'s HashSet<SpanKey>,
8574    unsafe_function_depth: usize,
8575    with_depth: usize,
8576    analysis: SwitchAnalysis,
8577}
8578
8579impl SwitchCollector<'_> {
8580    fn unsafe_context(&self) -> bool {
8581        self.unsafe_function_depth > 0 || self.with_depth > 0
8582    }
8583
8584    fn exit_source_function(&mut self, span: Span) {
8585        if self.source_sensitive_functions.contains(&span_key(span)) {
8586            self.unsafe_function_depth -= 1;
8587        }
8588    }
8589}
8590
8591impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
8592    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8593        if self
8594            .source_sensitive_functions
8595            .contains(&span_key(node.span))
8596        {
8597            self.unsafe_function_depth += 1;
8598        }
8599    }
8600
8601    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8602        self.exit_source_function(node.span);
8603    }
8604
8605    fn enter_arrow_function_expression(
8606        &mut self,
8607        node: &mut ArrowFunctionExpression<'a>,
8608        _context: &mut TraverseCtx<'a, ()>,
8609    ) {
8610        if self
8611            .source_sensitive_functions
8612            .contains(&span_key(node.span))
8613        {
8614            self.unsafe_function_depth += 1;
8615        }
8616    }
8617
8618    fn exit_arrow_function_expression(
8619        &mut self,
8620        node: &mut ArrowFunctionExpression<'a>,
8621        _context: &mut TraverseCtx<'a, ()>,
8622    ) {
8623        self.exit_source_function(node.span);
8624    }
8625
8626    fn enter_with_statement(
8627        &mut self,
8628        _node: &mut WithStatement<'a>,
8629        _context: &mut TraverseCtx<'a, ()>,
8630    ) {
8631        self.with_depth += 1;
8632    }
8633
8634    fn exit_with_statement(
8635        &mut self,
8636        _node: &mut WithStatement<'a>,
8637        _context: &mut TraverseCtx<'a, ()>,
8638    ) {
8639        self.with_depth -= 1;
8640    }
8641
8642    fn enter_switch_statement(
8643        &mut self,
8644        node: &mut SwitchStatement<'a>,
8645        _context: &mut TraverseCtx<'a, ()>,
8646    ) {
8647        let has_default = node.cases.iter().any(|case| case.test.is_none());
8648        if self.unsafe_context() {
8649            return;
8650        }
8651        let id = stable_id(self.source, self.file, "switch", node.span, "");
8652        let mut case_ids = Vec::with_capacity(node.cases.len());
8653        let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
8654        for (index, case) in node.cases.iter().enumerate() {
8655            let alternative_id = format!("{id}:case:{index}");
8656            let label = case.test.as_ref().map_or_else(
8657                || "default".to_string(),
8658                |test| format!("case {}", source_slice(self.source, test.span())),
8659            );
8660            case_ids.push(alternative_id.clone());
8661            alternatives.push(CandidateBranchAlternative {
8662                id: alternative_id,
8663                label,
8664            });
8665        }
8666        let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
8667        if let Some(no_match_id) = &no_match_id {
8668            alternatives.push(CandidateBranchAlternative {
8669                id: no_match_id.clone(),
8670                label: "no matching case".to_string(),
8671            });
8672        }
8673        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8674        self.analysis.branches.push(CandidateBranch {
8675            id,
8676            kind: "switch".to_string(),
8677            file: self.file.to_string(),
8678            line,
8679            column,
8680            source: source_slice(self.source, node.discriminant.span()).to_string(),
8681            alternatives,
8682        });
8683        self.analysis.targets.insert(
8684            span_key(node.span),
8685            SwitchTarget {
8686                case_ids,
8687                no_match_id,
8688            },
8689        );
8690    }
8691}
8692
8693fn collect_switch_branches<'a>(
8694    allocator: &'a Allocator,
8695    program: &mut Program<'a>,
8696    source: &str,
8697    file: &str,
8698    source_sensitive_functions: &HashSet<SpanKey>,
8699) -> SwitchAnalysis {
8700    let mut collector = SwitchCollector {
8701        source,
8702        file,
8703        source_sensitive_functions,
8704        unsafe_function_depth: 0,
8705        with_depth: 0,
8706        analysis: SwitchAnalysis::default(),
8707    };
8708    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8709    collector.analysis
8710}
8711
8712struct ExtendedCollector<'s> {
8713    source: &'s str,
8714    file: &'s str,
8715    source_sensitive_functions: &'s HashSet<SpanKey>,
8716    entered_loops: HashSet<SpanKey>,
8717    unsafe_function_depth: usize,
8718    with_depth: usize,
8719    analysis: ExtendedAnalysis,
8720}
8721
8722impl ExtendedCollector<'_> {
8723    fn unsafe_context(&self) -> bool {
8724        self.unsafe_function_depth > 0 || self.with_depth > 0
8725    }
8726
8727    fn enter_try(&mut self, node: &TryStatement<'_>) {
8728        let transformed = !self.unsafe_context() && node.handler.is_some();
8729        if !transformed {
8730            return;
8731        }
8732        let id = stable_id(self.source, self.file, "try-catch", node.span, "");
8733        let success_id = format!("{id}:success");
8734        let catch_id = format!("{id}:catch");
8735        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8736        self.analysis.branches.push(CandidateBranch {
8737            id,
8738            kind: "try-catch".to_string(),
8739            file: self.file.to_string(),
8740            line,
8741            column,
8742            source: "try / catch".to_string(),
8743            alternatives: vec![
8744                CandidateBranchAlternative {
8745                    id: success_id.clone(),
8746                    label: "try completed without catch".to_string(),
8747                },
8748                CandidateBranchAlternative {
8749                    id: catch_id.clone(),
8750                    label: "catch entered".to_string(),
8751                },
8752            ],
8753        });
8754        self.analysis.try_targets.insert(
8755            span_key(node.span),
8756            ExtendedTarget {
8757                first_id: success_id,
8758                second_id: catch_id,
8759            },
8760        );
8761    }
8762
8763    fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
8764        let transformed = !self.unsafe_context();
8765        if !transformed {
8766            return;
8767        }
8768        // One feasible outcome is not a branch. Recording it as one leaves a
8769        // permanently unreachable obligation, which is how a project ends up
8770        // below 100% with nothing to do about it.
8771        if self.entered_loops.contains(&span_key(span)) {
8772            return;
8773        }
8774        let id = stable_id(self.source, self.file, kind, span, "");
8775        let zero_id = format!("{id}:zero");
8776        let entered_id = format!("{id}:entered");
8777        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8778        self.analysis.branches.push(CandidateBranch {
8779            id,
8780            kind: kind.to_string(),
8781            file: self.file.to_string(),
8782            line,
8783            column,
8784            source: source_slice(self.source, right.span()).to_string(),
8785            alternatives: vec![
8786                CandidateBranchAlternative {
8787                    id: zero_id.clone(),
8788                    label: "zero iterations".to_string(),
8789                },
8790                CandidateBranchAlternative {
8791                    id: entered_id.clone(),
8792                    label: "one or more iterations".to_string(),
8793                },
8794            ],
8795        });
8796        self.analysis.loop_targets.insert(
8797            span_key(span),
8798            ExtendedTarget {
8799                first_id: zero_id,
8800                second_id: entered_id,
8801            },
8802        );
8803    }
8804
8805    fn exit_source_function(&mut self, span: Span) {
8806        if self.source_sensitive_functions.contains(&span_key(span)) {
8807            self.unsafe_function_depth -= 1;
8808        }
8809    }
8810}
8811
8812impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
8813    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8814        if self
8815            .source_sensitive_functions
8816            .contains(&span_key(node.span))
8817        {
8818            self.unsafe_function_depth += 1;
8819        }
8820    }
8821
8822    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8823        self.exit_source_function(node.span);
8824    }
8825
8826    fn enter_arrow_function_expression(
8827        &mut self,
8828        node: &mut ArrowFunctionExpression<'a>,
8829        _context: &mut TraverseCtx<'a, ()>,
8830    ) {
8831        if self
8832            .source_sensitive_functions
8833            .contains(&span_key(node.span))
8834        {
8835            self.unsafe_function_depth += 1;
8836        }
8837    }
8838
8839    fn exit_arrow_function_expression(
8840        &mut self,
8841        node: &mut ArrowFunctionExpression<'a>,
8842        _context: &mut TraverseCtx<'a, ()>,
8843    ) {
8844        self.exit_source_function(node.span);
8845    }
8846
8847    fn enter_with_statement(
8848        &mut self,
8849        _node: &mut WithStatement<'a>,
8850        _context: &mut TraverseCtx<'a, ()>,
8851    ) {
8852        self.with_depth += 1;
8853    }
8854
8855    fn exit_with_statement(
8856        &mut self,
8857        _node: &mut WithStatement<'a>,
8858        _context: &mut TraverseCtx<'a, ()>,
8859    ) {
8860        self.with_depth -= 1;
8861    }
8862
8863    fn enter_try_statement(
8864        &mut self,
8865        node: &mut TryStatement<'a>,
8866        _context: &mut TraverseCtx<'a, ()>,
8867    ) {
8868        self.enter_try(node);
8869    }
8870
8871    fn exit_try_statement(
8872        &mut self,
8873        _node: &mut TryStatement<'a>,
8874        _context: &mut TraverseCtx<'a, ()>,
8875    ) {
8876    }
8877
8878    fn enter_for_in_statement(
8879        &mut self,
8880        node: &mut ForInStatement<'a>,
8881        _context: &mut TraverseCtx<'a, ()>,
8882    ) {
8883        self.enter_loop(node.span, &node.right, "for-in");
8884    }
8885
8886    fn exit_for_in_statement(
8887        &mut self,
8888        _node: &mut ForInStatement<'a>,
8889        _context: &mut TraverseCtx<'a, ()>,
8890    ) {
8891    }
8892
8893    fn enter_for_of_statement(
8894        &mut self,
8895        node: &mut ForOfStatement<'a>,
8896        _context: &mut TraverseCtx<'a, ()>,
8897    ) {
8898        self.enter_loop(node.span, &node.right, "for-of");
8899    }
8900
8901    fn exit_for_of_statement(
8902        &mut self,
8903        _node: &mut ForOfStatement<'a>,
8904        _context: &mut TraverseCtx<'a, ()>,
8905    ) {
8906    }
8907}
8908
8909/// Loops that must run at least once.
8910///
8911/// A `for...of` over a non-empty array literal has no zero-iteration outcome
8912/// for a test to reach, so asking for one is asking for something impossible.
8913/// The author's only ways to satisfy it are to fake a test or delete the loop,
8914/// and a coverage tool should ask for neither.
8915///
8916/// This is decided from the syntax tree and symbol resolution alone. It stays
8917/// there deliberately: a type would answer more questions, but a type can be
8918/// asserted (`as Foo`) or optimistic (`xs[0]` is `T`, and `[][0]` is
8919/// `undefined`), and an obligation dropped on a wrong answer is a branch that
8920/// runs and is never reported again. Over-asking is recoverable; this is not.
8921fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
8922    let semantic = SemanticBuilder::new().build(program).semantic;
8923    let mut found = HashSet::new();
8924    for node in semantic.nodes() {
8925        if let AstKind::ForOfStatement(loop_node) = node.kind()
8926            && never_empty(&loop_node.right, &semantic)
8927        {
8928            found.insert(span_key(loop_node.span));
8929        }
8930    }
8931    found
8932}
8933
8934/// Whether an iterable cannot be empty: a non-empty array literal, or a
8935/// `const` bound to one that nothing else in the file can reach.
8936fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
8937    match transparent_expression(expression) {
8938        Expression::ArrayExpression(array) => has_definite_element(array),
8939        Expression::Identifier(identifier) => {
8940            let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
8941                return false;
8942            };
8943            // Nothing but this loop may reach the binding. A shrinking method
8944            // (`pop`, `splice`, `length = 0`) is an ordinary read of the
8945            // identifier, so counting references is what makes this sound --
8946            // recognising a list of mutating methods would not be.
8947            if semantic.scoping().get_resolved_references(symbol).count() != 1 {
8948                return false;
8949            }
8950            let declaration = semantic.scoping().symbol_declaration(symbol);
8951            let AstKind::VariableDeclarator(declarator) =
8952                semantic.nodes().get_node(declaration).kind()
8953            else {
8954                return false;
8955            };
8956            declarator.kind == VariableDeclarationKind::Const
8957                && declarator.init.as_ref().is_some_and(|init| {
8958                    matches!(transparent_expression(init), Expression::ArrayExpression(array)
8959                        if has_definite_element(array))
8960                })
8961        }
8962        _ => false,
8963    }
8964}
8965
8966/// An element that is certainly there. A spread may contribute nothing, so
8967/// `[...xs]` is not known to be non-empty; a hole is, since `[,]` has length 1
8968/// and yields once.
8969fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
8970    array
8971        .elements
8972        .iter()
8973        .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
8974}
8975
8976fn collect_extended_branches<'a>(
8977    allocator: &'a Allocator,
8978    program: &mut Program<'a>,
8979    source: &str,
8980    file: &str,
8981    source_sensitive_functions: &HashSet<SpanKey>,
8982) -> ExtendedAnalysis {
8983    let entered_loops = provably_entered_loops(program);
8984    let mut collector = ExtendedCollector {
8985        source,
8986        file,
8987        source_sensitive_functions,
8988        entered_loops,
8989        unsafe_function_depth: 0,
8990        with_depth: 0,
8991        analysis: ExtendedAnalysis::default(),
8992    };
8993    traverse_mut(&mut collector, allocator, program, Default::default(), ());
8994    collector.analysis
8995}
8996
8997struct DefaultCollector<'s> {
8998    source: &'s str,
8999    file: &'s str,
9000    source_sensitive_functions: &'s HashSet<SpanKey>,
9001    unsafe_function_depth: usize,
9002    with_depth: usize,
9003    analysis: DefaultAnalysis,
9004}
9005
9006impl DefaultCollector<'_> {
9007    fn unsafe_context(&self) -> bool {
9008        self.unsafe_function_depth > 0 || self.with_depth > 0
9009    }
9010
9011    fn target(
9012        &mut self,
9013        span: Span,
9014        left: &BindingPattern<'_>,
9015        right: &Expression<'_>,
9016    ) -> DefaultTarget {
9017        let id = stable_id(self.source, self.file, "default-value", span, "");
9018        let default_id = format!("{id}:default");
9019        let provided_id = format!("{id}:provided");
9020        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
9021        self.analysis.branches.push(CandidateBranch {
9022            id,
9023            kind: "default-value".to_string(),
9024            file: self.file.to_string(),
9025            line,
9026            column,
9027            source: source_slice(self.source, span).to_string(),
9028            alternatives: vec![
9029                CandidateBranchAlternative {
9030                    id: default_id.clone(),
9031                    label: "default evaluated".to_string(),
9032                },
9033                CandidateBranchAlternative {
9034                    id: provided_id.clone(),
9035                    label: "value provided".to_string(),
9036                },
9037            ],
9038        });
9039        DefaultTarget {
9040            default_id,
9041            provided_id,
9042            inferred_name: binding_identifier_name(left)
9043                .filter(|_| expression_is_anonymous_definition(right)),
9044        }
9045    }
9046
9047    fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
9048        match pattern {
9049            BindingPattern::AssignmentPattern(assignment) => {
9050                let target = self.target(assignment.span, &assignment.left, &assignment.right);
9051                if parameter {
9052                    self.analysis
9053                        .parameter_targets
9054                        .insert(span_key(assignment.span), target);
9055                } else {
9056                    self.analysis
9057                        .binding_targets
9058                        .insert(span_key(assignment.span), target);
9059                }
9060                self.collect_binding_pattern(&assignment.left, parameter);
9061            }
9062            BindingPattern::ObjectPattern(object) => {
9063                for property in &object.properties {
9064                    self.collect_binding_pattern(&property.value, parameter);
9065                }
9066                if let Some(rest) = &object.rest {
9067                    self.collect_binding_pattern(&rest.argument, parameter);
9068                }
9069            }
9070            BindingPattern::ArrayPattern(array) => {
9071                for element in array.elements.iter().flatten() {
9072                    self.collect_binding_pattern(element, parameter);
9073                }
9074                if let Some(rest) = &array.rest {
9075                    self.collect_binding_pattern(&rest.argument, parameter);
9076                }
9077            }
9078            BindingPattern::BindingIdentifier(_) => {}
9079        }
9080    }
9081
9082    fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
9083        for parameter in &parameters.items {
9084            if let Some(initializer) = &parameter.initializer {
9085                // Babel models a formal-parameter default as an AssignmentPattern whose
9086                // source range starts at the binding and ends at the initializer. oxc
9087                // keeps the initializer beside the BindingPattern and, for a TypeScript
9088                // parameter property, includes access/readonly modifiers in
9089                // FormalParameter::span. Keep the outer span as the transformation key,
9090                // but publish the same semantic default-expression range as Babel.
9091                let default_span =
9092                    Span::new(parameter.pattern.span().start, initializer.span().end);
9093                let target = self.target(default_span, &parameter.pattern, initializer);
9094                self.analysis
9095                    .parameter_targets
9096                    .insert(span_key(parameter.span), target);
9097            }
9098            self.collect_binding_pattern(&parameter.pattern, true);
9099        }
9100        if let Some(rest) = &parameters.rest {
9101            self.collect_binding_pattern(&rest.rest.argument, true);
9102        }
9103    }
9104
9105    fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
9106        match pattern {
9107            BindingPattern::AssignmentPattern(_) => true,
9108            BindingPattern::ObjectPattern(object) => {
9109                object
9110                    .properties
9111                    .iter()
9112                    .any(|property| Self::has_binding_default(&property.value))
9113                    || object
9114                        .rest
9115                        .as_ref()
9116                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9117            }
9118            BindingPattern::ArrayPattern(array) => {
9119                array
9120                    .elements
9121                    .iter()
9122                    .flatten()
9123                    .any(Self::has_binding_default)
9124                    || array
9125                        .rest
9126                        .as_ref()
9127                        .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9128            }
9129            BindingPattern::BindingIdentifier(_) => false,
9130        }
9131    }
9132
9133    fn exit_source_function(&mut self, span: Span) {
9134        if self.source_sensitive_functions.contains(&span_key(span)) {
9135            self.unsafe_function_depth -= 1;
9136        }
9137    }
9138}
9139
9140impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
9141    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9142        if self
9143            .source_sensitive_functions
9144            .contains(&span_key(node.span))
9145        {
9146            self.unsafe_function_depth += 1;
9147            return;
9148        }
9149        if !self.unsafe_context() && node.body.is_some() {
9150            self.collect_parameters(&node.params);
9151        }
9152    }
9153
9154    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9155        self.exit_source_function(node.span);
9156    }
9157
9158    fn enter_arrow_function_expression(
9159        &mut self,
9160        node: &mut ArrowFunctionExpression<'a>,
9161        _context: &mut TraverseCtx<'a, ()>,
9162    ) {
9163        if self
9164            .source_sensitive_functions
9165            .contains(&span_key(node.span))
9166        {
9167            self.unsafe_function_depth += 1;
9168            return;
9169        }
9170        if !self.unsafe_context() {
9171            self.collect_parameters(&node.params);
9172        }
9173    }
9174
9175    fn exit_arrow_function_expression(
9176        &mut self,
9177        node: &mut ArrowFunctionExpression<'a>,
9178        _context: &mut TraverseCtx<'a, ()>,
9179    ) {
9180        self.exit_source_function(node.span);
9181    }
9182
9183    fn enter_with_statement(
9184        &mut self,
9185        _node: &mut WithStatement<'a>,
9186        _context: &mut TraverseCtx<'a, ()>,
9187    ) {
9188        self.with_depth += 1;
9189    }
9190
9191    fn exit_with_statement(
9192        &mut self,
9193        _node: &mut WithStatement<'a>,
9194        _context: &mut TraverseCtx<'a, ()>,
9195    ) {
9196        self.with_depth -= 1;
9197    }
9198
9199    fn enter_variable_declaration(
9200        &mut self,
9201        node: &mut VariableDeclaration<'a>,
9202        context: &mut TraverseCtx<'a, ()>,
9203    ) {
9204        if self.unsafe_context() {
9205            return;
9206        }
9207        let has_default = node
9208            .declarations
9209            .iter()
9210            .any(|declaration| Self::has_binding_default(&declaration.id));
9211        if !has_default {
9212            return;
9213        }
9214        if matches!(
9215            context.ancestors().next(),
9216            Some(Ancestor::ForStatementInit(_))
9217        ) {
9218            let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9219            self.analysis.limitations.push(CandidateLimitation {
9220                id: stable_id(
9221                    self.source,
9222                    self.file,
9223                    "dynamic-code",
9224                    node.span,
9225                    "for-init-default",
9226                ),
9227                kind: "dynamic-code".to_string(),
9228                file: self.file.to_string(),
9229                line,
9230                column,
9231                source: source_slice(self.source, node.span).to_string(),
9232                reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
9233            });
9234            return;
9235        }
9236        for declaration in &node.declarations {
9237            self.collect_binding_pattern(&declaration.id, false);
9238        }
9239    }
9240}
9241
9242fn collect_default_branches<'a>(
9243    allocator: &'a Allocator,
9244    program: &mut Program<'a>,
9245    source: &str,
9246    file: &str,
9247    source_sensitive_functions: &HashSet<SpanKey>,
9248) -> DefaultAnalysis {
9249    let mut collector = DefaultCollector {
9250        source,
9251        file,
9252        source_sensitive_functions,
9253        unsafe_function_depth: 0,
9254        with_depth: 0,
9255        analysis: DefaultAnalysis::default(),
9256    };
9257    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9258    collector.analysis
9259}
9260
9261struct OptionalCallCollector<'s> {
9262    source: &'s str,
9263    file: &'s str,
9264    source_sensitive_functions: &'s HashSet<SpanKey>,
9265    unsafe_function_depth: usize,
9266    with_depth: usize,
9267    chain_roots: Vec<SpanKey>,
9268    analysis: OptionalCallAnalysis,
9269}
9270
9271impl OptionalCallCollector<'_> {
9272    fn unsafe_context(&self) -> bool {
9273        self.unsafe_function_depth > 0 || self.with_depth > 0
9274    }
9275
9276    fn exit_source_function(&mut self, span: Span) {
9277        if self.source_sensitive_functions.contains(&span_key(span)) {
9278            self.unsafe_function_depth -= 1;
9279        }
9280    }
9281}
9282
9283impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
9284    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9285        if self
9286            .source_sensitive_functions
9287            .contains(&span_key(node.span))
9288        {
9289            self.unsafe_function_depth += 1;
9290        }
9291    }
9292
9293    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9294        self.exit_source_function(node.span);
9295    }
9296
9297    fn enter_arrow_function_expression(
9298        &mut self,
9299        node: &mut ArrowFunctionExpression<'a>,
9300        _context: &mut TraverseCtx<'a, ()>,
9301    ) {
9302        if self
9303            .source_sensitive_functions
9304            .contains(&span_key(node.span))
9305        {
9306            self.unsafe_function_depth += 1;
9307        }
9308    }
9309
9310    fn exit_arrow_function_expression(
9311        &mut self,
9312        node: &mut ArrowFunctionExpression<'a>,
9313        _context: &mut TraverseCtx<'a, ()>,
9314    ) {
9315        self.exit_source_function(node.span);
9316    }
9317
9318    fn enter_with_statement(
9319        &mut self,
9320        _node: &mut WithStatement<'a>,
9321        _context: &mut TraverseCtx<'a, ()>,
9322    ) {
9323        self.with_depth += 1;
9324    }
9325
9326    fn exit_with_statement(
9327        &mut self,
9328        _node: &mut WithStatement<'a>,
9329        _context: &mut TraverseCtx<'a, ()>,
9330    ) {
9331        self.with_depth -= 1;
9332    }
9333
9334    fn enter_chain_expression(
9335        &mut self,
9336        node: &mut ChainExpression<'a>,
9337        context: &mut TraverseCtx<'a, ()>,
9338    ) {
9339        let root = match context.ancestors().next() {
9340            Some(Ancestor::UnaryExpressionArgument(parent))
9341                if *parent.operator() == UnaryOperator::Delete =>
9342            {
9343                span_key(*parent.span())
9344            }
9345            _ => span_key(node.span),
9346        };
9347        self.chain_roots.push(root);
9348    }
9349
9350    fn exit_chain_expression(
9351        &mut self,
9352        _node: &mut ChainExpression<'a>,
9353        _context: &mut TraverseCtx<'a, ()>,
9354    ) {
9355        self.chain_roots.pop();
9356    }
9357
9358    fn enter_call_expression(
9359        &mut self,
9360        node: &mut CallExpression<'a>,
9361        _context: &mut TraverseCtx<'a, ()>,
9362    ) {
9363        if !node.optional || self.unsafe_context() {
9364            return;
9365        }
9366        let root = *self
9367            .chain_roots
9368            .last()
9369            .expect("an optional call must be enclosed by a chain expression");
9370        let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
9371        let short_id = format!("{id}:short");
9372        let continued_id = format!("{id}:continued");
9373        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9374        self.analysis.sites.insert(
9375            span_key(node.span),
9376            (short_id.clone(), continued_id.clone()),
9377        );
9378        self.analysis
9379            .roots
9380            .entry(root)
9381            .or_default()
9382            .push(span_key(node.span));
9383        self.analysis.branches.push(CandidateBranch {
9384            id,
9385            kind: "optional-chain".to_string(),
9386            file: self.file.to_string(),
9387            line,
9388            column,
9389            source: source_slice(self.source, node.span).to_string(),
9390            alternatives: vec![
9391                CandidateBranchAlternative {
9392                    id: short_id,
9393                    label: "nullish / short-circuited".to_string(),
9394                },
9395                CandidateBranchAlternative {
9396                    id: continued_id,
9397                    label: "non-nullish / continued".to_string(),
9398                },
9399            ],
9400        });
9401    }
9402}
9403
9404fn collect_optional_call_branches<'a>(
9405    allocator: &'a Allocator,
9406    program: &mut Program<'a>,
9407    source: &str,
9408    file: &str,
9409    source_sensitive_functions: &HashSet<SpanKey>,
9410) -> OptionalCallAnalysis {
9411    let mut collector = OptionalCallCollector {
9412        source,
9413        file,
9414        source_sensitive_functions,
9415        unsafe_function_depth: 0,
9416        with_depth: 0,
9417        chain_roots: Vec::new(),
9418        analysis: OptionalCallAnalysis::default(),
9419    };
9420    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9421    collector.analysis
9422}
9423
9424struct OptionalMemberCollector<'s> {
9425    source: &'s str,
9426    file: &'s str,
9427    source_sensitive_functions: &'s HashSet<SpanKey>,
9428    unsafe_function_depth: usize,
9429    with_depth: usize,
9430    analysis: OptionalMemberAnalysis,
9431}
9432
9433impl OptionalMemberCollector<'_> {
9434    fn record<State>(
9435        &mut self,
9436        span: Span,
9437        optional: bool,
9438        object: &Expression<'_>,
9439        context: &TraverseCtx<'_, State>,
9440    ) {
9441        if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
9442            return;
9443        }
9444        let below = match optional_link_below(object) {
9445            LinkBelow::Member => LINK_AFTER_ANOTHER,
9446            LinkBelow::Call(call) => {
9447                self.analysis.after_call.insert(span_key(span), call);
9448                LINK_AFTER_CALL
9449            }
9450            LinkBelow::None => 0,
9451        };
9452        let links = below
9453            | if optional_link_above(context) {
9454                LINK_BEFORE_ANOTHER
9455            } else {
9456                0
9457            };
9458        if links != 0 {
9459            self.analysis.links.insert(span_key(span), links);
9460        }
9461        let id = stable_id(self.source, self.file, "optional-chain", span, "");
9462        let short_id = format!("{id}:short");
9463        let continued_id = format!("{id}:continued");
9464        let (line, column) = line_and_utf16_column(self.source, span.start as usize);
9465        self.analysis
9466            .targets
9467            .insert(span_key(span), (short_id.clone(), continued_id.clone()));
9468        self.analysis.branches.push(CandidateBranch {
9469            id,
9470            kind: "optional-chain".to_string(),
9471            file: self.file.to_string(),
9472            line,
9473            column,
9474            source: source_slice(self.source, span).to_string(),
9475            alternatives: vec![
9476                CandidateBranchAlternative {
9477                    id: short_id,
9478                    label: "nullish / short-circuited".to_string(),
9479                },
9480                CandidateBranchAlternative {
9481                    id: continued_id,
9482                    label: "non-nullish / continued".to_string(),
9483                },
9484            ],
9485        });
9486    }
9487
9488    fn exit_source_function(&mut self, span: Span) {
9489        if self.source_sensitive_functions.contains(&span_key(span)) {
9490            self.unsafe_function_depth -= 1;
9491        }
9492    }
9493}
9494
9495impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
9496    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9497        if self
9498            .source_sensitive_functions
9499            .contains(&span_key(node.span))
9500        {
9501            self.unsafe_function_depth += 1;
9502        }
9503    }
9504
9505    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9506        self.exit_source_function(node.span);
9507    }
9508
9509    fn enter_arrow_function_expression(
9510        &mut self,
9511        node: &mut ArrowFunctionExpression<'a>,
9512        _context: &mut TraverseCtx<'a, ()>,
9513    ) {
9514        if self
9515            .source_sensitive_functions
9516            .contains(&span_key(node.span))
9517        {
9518            self.unsafe_function_depth += 1;
9519        }
9520    }
9521
9522    fn exit_arrow_function_expression(
9523        &mut self,
9524        node: &mut ArrowFunctionExpression<'a>,
9525        _context: &mut TraverseCtx<'a, ()>,
9526    ) {
9527        self.exit_source_function(node.span);
9528    }
9529
9530    fn enter_with_statement(
9531        &mut self,
9532        _node: &mut WithStatement<'a>,
9533        _context: &mut TraverseCtx<'a, ()>,
9534    ) {
9535        self.with_depth += 1;
9536    }
9537
9538    fn exit_with_statement(
9539        &mut self,
9540        _node: &mut WithStatement<'a>,
9541        _context: &mut TraverseCtx<'a, ()>,
9542    ) {
9543        self.with_depth -= 1;
9544    }
9545
9546    fn enter_computed_member_expression(
9547        &mut self,
9548        node: &mut ComputedMemberExpression<'a>,
9549        context: &mut TraverseCtx<'a, ()>,
9550    ) {
9551        self.record(node.span, node.optional, &node.object, context);
9552    }
9553
9554    fn enter_static_member_expression(
9555        &mut self,
9556        node: &mut StaticMemberExpression<'a>,
9557        context: &mut TraverseCtx<'a, ()>,
9558    ) {
9559        self.record(node.span, node.optional, &node.object, context);
9560    }
9561
9562    fn enter_private_field_expression(
9563        &mut self,
9564        node: &mut PrivateFieldExpression<'a>,
9565        context: &mut TraverseCtx<'a, ()>,
9566    ) {
9567        self.record(node.span, node.optional, &node.object, context);
9568    }
9569}
9570
9571fn collect_optional_member_branches<'a>(
9572    allocator: &'a Allocator,
9573    program: &mut Program<'a>,
9574    source: &str,
9575    file: &str,
9576    source_sensitive_functions: &HashSet<SpanKey>,
9577) -> OptionalMemberAnalysis {
9578    let mut collector = OptionalMemberCollector {
9579        source,
9580        file,
9581        source_sensitive_functions,
9582        unsafe_function_depth: 0,
9583        with_depth: 0,
9584        analysis: OptionalMemberAnalysis::default(),
9585    };
9586    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9587    collector.analysis
9588}
9589
9590struct LogicalAssignmentCollector<'s> {
9591    source: &'s str,
9592    file: &'s str,
9593    source_sensitive_functions: &'s HashSet<SpanKey>,
9594    unsafe_function_depth: usize,
9595    with_depth: usize,
9596    analysis: LogicalAssignmentAnalysis,
9597}
9598
9599impl LogicalAssignmentCollector<'_> {
9600    fn exit_source_function(&mut self, span: Span) {
9601        if self.source_sensitive_functions.contains(&span_key(span)) {
9602            self.unsafe_function_depth -= 1;
9603        }
9604    }
9605}
9606
9607impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
9608    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9609        if self
9610            .source_sensitive_functions
9611            .contains(&span_key(node.span))
9612        {
9613            self.unsafe_function_depth += 1;
9614        }
9615    }
9616
9617    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9618        self.exit_source_function(node.span);
9619    }
9620
9621    fn enter_arrow_function_expression(
9622        &mut self,
9623        node: &mut ArrowFunctionExpression<'a>,
9624        _context: &mut TraverseCtx<'a, ()>,
9625    ) {
9626        if self
9627            .source_sensitive_functions
9628            .contains(&span_key(node.span))
9629        {
9630            self.unsafe_function_depth += 1;
9631        }
9632    }
9633
9634    fn exit_arrow_function_expression(
9635        &mut self,
9636        node: &mut ArrowFunctionExpression<'a>,
9637        _context: &mut TraverseCtx<'a, ()>,
9638    ) {
9639        self.exit_source_function(node.span);
9640    }
9641
9642    fn enter_with_statement(
9643        &mut self,
9644        _node: &mut WithStatement<'a>,
9645        _context: &mut TraverseCtx<'a, ()>,
9646    ) {
9647        self.with_depth += 1;
9648    }
9649
9650    fn exit_with_statement(
9651        &mut self,
9652        _node: &mut WithStatement<'a>,
9653        _context: &mut TraverseCtx<'a, ()>,
9654    ) {
9655        self.with_depth -= 1;
9656    }
9657
9658    fn exit_assignment_expression(
9659        &mut self,
9660        node: &mut AssignmentExpression<'a>,
9661        _context: &mut TraverseCtx<'a, ()>,
9662    ) {
9663        if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
9664            return;
9665        }
9666        let operator = match node.operator {
9667            AssignmentOperator::LogicalAnd => "&&=",
9668            AssignmentOperator::LogicalOr => "||=",
9669            AssignmentOperator::LogicalNullish => "??=",
9670            _ => unreachable!("logical assignment filter must be exhaustive"),
9671        };
9672        let id = stable_id(
9673            self.source,
9674            self.file,
9675            "logical-assignment",
9676            node.span,
9677            operator,
9678        );
9679        let short_id = format!("{id}:short");
9680        let right_id = format!("{id}:right");
9681        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9682        self.analysis
9683            .targets
9684            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9685        self.analysis.branches.push(CandidateBranch {
9686            id,
9687            kind: "logical-assignment".to_string(),
9688            file: self.file.to_string(),
9689            line,
9690            column,
9691            source: source_slice(self.source, node.span).to_string(),
9692            alternatives: vec![
9693                CandidateBranchAlternative {
9694                    id: short_id,
9695                    label: "assignment skipped".to_string(),
9696                },
9697                CandidateBranchAlternative {
9698                    id: right_id,
9699                    label: "right evaluated / assigned".to_string(),
9700                },
9701            ],
9702        });
9703    }
9704}
9705
9706fn collect_logical_assignment_branches<'a>(
9707    allocator: &'a Allocator,
9708    program: &mut Program<'a>,
9709    source: &str,
9710    file: &str,
9711    source_sensitive_functions: &HashSet<SpanKey>,
9712) -> LogicalAssignmentAnalysis {
9713    let mut collector = LogicalAssignmentCollector {
9714        source,
9715        file,
9716        source_sensitive_functions,
9717        unsafe_function_depth: 0,
9718        with_depth: 0,
9719        analysis: LogicalAssignmentAnalysis::default(),
9720    };
9721    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9722    collector.analysis
9723}
9724
9725struct LogicalBranchCollector<'s> {
9726    source: &'s str,
9727    file: &'s str,
9728    decision_logical_nodes: &'s HashSet<SpanKey>,
9729    source_sensitive_functions: &'s HashSet<SpanKey>,
9730    unsafe_function_depth: usize,
9731    with_depth: usize,
9732    analysis: LogicalBranchAnalysis,
9733}
9734
9735impl LogicalBranchCollector<'_> {
9736    fn exit_source_function(&mut self, span: Span) {
9737        if self.source_sensitive_functions.contains(&span_key(span)) {
9738            self.unsafe_function_depth -= 1;
9739        }
9740    }
9741}
9742
9743impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
9744    fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9745        if self
9746            .source_sensitive_functions
9747            .contains(&span_key(node.span))
9748        {
9749            self.unsafe_function_depth += 1;
9750        }
9751    }
9752
9753    fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9754        self.exit_source_function(node.span);
9755    }
9756
9757    fn enter_arrow_function_expression(
9758        &mut self,
9759        node: &mut ArrowFunctionExpression<'a>,
9760        _context: &mut TraverseCtx<'a, ()>,
9761    ) {
9762        if self
9763            .source_sensitive_functions
9764            .contains(&span_key(node.span))
9765        {
9766            self.unsafe_function_depth += 1;
9767        }
9768    }
9769
9770    fn exit_arrow_function_expression(
9771        &mut self,
9772        node: &mut ArrowFunctionExpression<'a>,
9773        _context: &mut TraverseCtx<'a, ()>,
9774    ) {
9775        self.exit_source_function(node.span);
9776    }
9777
9778    fn enter_with_statement(
9779        &mut self,
9780        _node: &mut WithStatement<'a>,
9781        _context: &mut TraverseCtx<'a, ()>,
9782    ) {
9783        self.with_depth += 1;
9784    }
9785
9786    fn exit_with_statement(
9787        &mut self,
9788        _node: &mut WithStatement<'a>,
9789        _context: &mut TraverseCtx<'a, ()>,
9790    ) {
9791        self.with_depth -= 1;
9792    }
9793
9794    fn exit_logical_expression(
9795        &mut self,
9796        node: &mut LogicalExpression<'a>,
9797        _context: &mut TraverseCtx<'a, ()>,
9798    ) {
9799        if self.unsafe_function_depth > 0
9800            || self.with_depth > 0
9801            || self.decision_logical_nodes.contains(&span_key(node.span))
9802        {
9803            return;
9804        }
9805        let operator = match node.operator {
9806            LogicalOperator::And => "&&",
9807            LogicalOperator::Or => "||",
9808            LogicalOperator::Coalesce => "??",
9809        };
9810        let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
9811        let short_id = format!("{id}:short");
9812        let right_id = format!("{id}:right");
9813        let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9814        self.analysis
9815            .logical_targets
9816            .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9817        self.analysis.branches.push(CandidateBranch {
9818            id,
9819            kind: "logical-value".to_string(),
9820            file: self.file.to_string(),
9821            line,
9822            column,
9823            source: source_slice(self.source, node.span).to_string(),
9824            alternatives: vec![
9825                CandidateBranchAlternative {
9826                    id: short_id,
9827                    label: "short-circuit / left selected".to_string(),
9828                },
9829                CandidateBranchAlternative {
9830                    id: right_id,
9831                    label: "right evaluated / selected".to_string(),
9832                },
9833            ],
9834        });
9835    }
9836}
9837
9838fn collect_logical_value_branches<'a>(
9839    allocator: &'a Allocator,
9840    program: &mut Program<'a>,
9841    source: &str,
9842    file: &str,
9843    decision_logical_nodes: &HashSet<SpanKey>,
9844    source_sensitive_functions: &HashSet<SpanKey>,
9845) -> LogicalBranchAnalysis {
9846    let mut collector = LogicalBranchCollector {
9847        source,
9848        file,
9849        decision_logical_nodes,
9850        source_sensitive_functions,
9851        unsafe_function_depth: 0,
9852        with_depth: 0,
9853        analysis: LogicalBranchAnalysis::default(),
9854    };
9855    traverse_mut(&mut collector, allocator, program, Default::default(), ());
9856    collector.analysis
9857}
9858
9859impl<'a> Visit<'a> for DecisionCollector<'_> {
9860    fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
9861        if self
9862            .source_sensitive_functions
9863            .contains(&span_key(function.span))
9864        {
9865            return;
9866        }
9867        walk::walk_function(self, function, flags);
9868    }
9869
9870    fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
9871        if self
9872            .source_sensitive_functions
9873            .contains(&span_key(function.span))
9874        {
9875            return;
9876        }
9877        walk::walk_arrow_function_expression(self, function);
9878    }
9879
9880    // A with body is left uninstrumented: its object environment can
9881    // intercept probe identifiers (see `enter_with_statement`).
9882    fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
9883
9884    /// `&&` and `||` used as a value: the left operand is always read as a
9885    /// boolean, whatever the operator returns, so a compound one is a decision
9886    /// as much as an `if`'s test is -- `(a && b) || fallback`, `a || b ||
9887    /// "default"`. What the operator returns from its right is a value, not a
9888    /// condition, and is judged as one in its turn. A `!` reads its operand as
9889    /// a boolean too.
9890    fn visit_expression(&mut self, expression: &Expression<'a>) {
9891        let outer_root = self.logical_root;
9892        // A coverage tool the tests run reads an `&&`/`||`/`??` tree as one
9893        // branch, whatever parentheses are in it; a `!` or anything else is a
9894        // leaf of it.
9895        self.logical_root = match expression {
9896            Expression::LogicalExpression(logical) => {
9897                Some(outer_root.unwrap_or(span_key(logical.span)))
9898            }
9899            Expression::ParenthesizedExpression(_) => outer_root,
9900            _ => None,
9901        };
9902        let candidate = match expression {
9903            Expression::LogicalExpression(logical)
9904                if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9905                    && !self.in_a_decision(logical.span) =>
9906            {
9907                Some(&logical.left)
9908            }
9909            Expression::UnaryExpression(unary)
9910                if unary.operator.is_not()
9911                    && has_compound_boolean_decision(&unary.argument)
9912                    && !compound_root(&unary.argument)
9913                        .is_some_and(|root| self.in_a_decision(root.span)) =>
9914            {
9915                Some(expression)
9916            }
9917            _ => None,
9918        };
9919        if let Some(decision) = candidate
9920            && has_compound_boolean_decision(decision)
9921            && decision_outcome_is_variable(decision)
9922        {
9923            let mut conditions = Vec::new();
9924            collect_conditions(decision, &mut conditions);
9925            if conditions.len() <= 32 {
9926                // A `!` outside any tree is its own root.
9927                let root = self
9928                    .logical_root
9929                    .or(outer_root)
9930                    .unwrap_or(span_key(decision.span()));
9931                self.value_decisions.insert(span_key(decision.span()), root);
9932                self.record_decision(decision, "value");
9933            }
9934        }
9935        walk::walk_expression(self, expression);
9936        self.logical_root = outer_root;
9937    }
9938
9939    fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
9940        if decision_outcome_is_variable(&statement.test) {
9941            self.record_decision(&statement.test, "if");
9942        }
9943        self.visit_expression(&statement.test);
9944        self.visit_statement(&statement.consequent);
9945        if let Some(alternate) = &statement.alternate {
9946            self.visit_statement(alternate);
9947        }
9948    }
9949
9950    fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
9951        if decision_outcome_is_variable(&expression.test) {
9952            self.record_decision(&expression.test, "ternary");
9953        }
9954        self.visit_expression(&expression.test);
9955        self.visit_expression(&expression.consequent);
9956        self.visit_expression(&expression.alternate);
9957    }
9958
9959    fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
9960        if decision_outcome_is_variable(&statement.test) {
9961            self.record_decision(&statement.test, "while");
9962        }
9963        self.visit_expression(&statement.test);
9964        self.visit_statement(&statement.body);
9965    }
9966
9967    fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
9968        if decision_outcome_is_variable(&statement.test) {
9969            self.record_decision(&statement.test, "do-while");
9970        }
9971        self.visit_statement(&statement.body);
9972        self.visit_expression(&statement.test);
9973    }
9974
9975    fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
9976        if let Some(test) = &statement.test
9977            && decision_outcome_is_variable(test)
9978        {
9979            self.record_decision(test, "for");
9980        }
9981        if let Some(init) = &statement.init {
9982            self.visit_for_statement_init(init);
9983        }
9984        if let Some(test) = &statement.test {
9985            self.visit_expression(test);
9986        }
9987        if let Some(update) = &statement.update {
9988            self.visit_expression(update);
9989        }
9990        self.visit_statement(&statement.body);
9991    }
9992}
9993
9994fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
9995    match expression {
9996        Expression::ParenthesizedExpression(parenthesized) => {
9997            transparent_expression(&parenthesized.expression)
9998        }
9999        _ => expression,
10000    }
10001}
10002
10003fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
10004    match expression {
10005        Expression::ParenthesizedExpression(parenthesized) => {
10006            has_compound_boolean_decision(&parenthesized.expression)
10007        }
10008        Expression::LogicalExpression(logical) => {
10009            matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
10010        }
10011        Expression::UnaryExpression(unary) if unary.operator.is_not() => {
10012            has_compound_boolean_decision(&unary.argument)
10013        }
10014        _ => false,
10015    }
10016}
10017
10018/// A decision's and/or/not structure over its conditions, numbered as
10019/// `collect_conditions` numbers them.
10020enum ValueShape {
10021    Condition(usize),
10022    And(Box<ValueShape>, Box<ValueShape>),
10023    Or(Box<ValueShape>, Box<ValueShape>),
10024    Not(Box<ValueShape>),
10025}
10026
10027fn value_shape(expression: &Expression<'_>, next: &mut usize) -> ValueShape {
10028    match expression {
10029        Expression::ParenthesizedExpression(parenthesized) => {
10030            value_shape(&parenthesized.expression, next)
10031        }
10032        Expression::LogicalExpression(logical)
10033            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10034        {
10035            let left = Box::new(value_shape(&logical.left, next));
10036            let right = Box::new(value_shape(&logical.right, next));
10037            if logical.operator == LogicalOperator::And {
10038                ValueShape::And(left, right)
10039            } else {
10040                ValueShape::Or(left, right)
10041            }
10042        }
10043        Expression::UnaryExpression(unary)
10044            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10045        {
10046            ValueShape::Not(Box::new(value_shape(&unary.argument, next)))
10047        }
10048        _ => {
10049            let index = *next;
10050            *next += 1;
10051            ValueShape::Condition(index)
10052        }
10053    }
10054}
10055
10056fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
10057    match expression {
10058        Expression::ParenthesizedExpression(parenthesized) => {
10059            collect_conditions(&parenthesized.expression, conditions);
10060        }
10061        Expression::LogicalExpression(logical)
10062            if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10063        {
10064            collect_conditions(&logical.left, conditions);
10065            collect_conditions(&logical.right, conditions);
10066        }
10067        Expression::UnaryExpression(unary)
10068            if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10069        {
10070            collect_conditions(&unary.argument, conditions);
10071        }
10072        _ => conditions.push(expression.span()),
10073    }
10074}
10075
10076fn source_slice(source: &str, span: Span) -> &str {
10077    &source[span.start as usize..span.end as usize]
10078}
10079
10080/// Where every line of one source starts, in bytes and in UTF-16 units, so a
10081/// position is a binary search rather than a scan from the top of the file.
10082///
10083/// Every probe, decision and branch records its line, column and a UTF-16
10084/// offset, and each was counted from the first byte of the file: the work
10085/// grew with the square of the file's length. An 8,000-line file took 97
10086/// seconds to instrument on a debug build, a 24,000-line one did not finish
10087/// in ten minutes, and Actual's setup spent minutes before its first test.
10088struct SourcePositions {
10089    address: usize,
10090    length: usize,
10091    sample: u64,
10092    line_starts: Vec<usize>,
10093    utf16_before_line: Vec<usize>,
10094    /// Per line: `None` when ASCII, otherwise `(byte offset, utf16 column)`
10095    /// checkpoints from the line's start, so a long line that is not ASCII
10096    /// -- a table of currency symbols, say -- is not walked per position.
10097    checkpoints: Vec<Option<Vec<(usize, usize)>>>,
10098}
10099
10100impl SourcePositions {
10101    fn sample(source: &str) -> u64 {
10102        let bytes = source.as_bytes();
10103        let head = &bytes[..bytes.len().min(64)];
10104        let tail = &bytes[bytes.len().saturating_sub(64)..];
10105        head.iter()
10106            .chain(tail)
10107            .fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
10108                (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
10109            })
10110    }
10111
10112    fn new(source: &str) -> Self {
10113        let mut line_starts = vec![0];
10114        let mut utf16_before_line = vec![0];
10115        let mut checkpoints = Vec::new();
10116        let mut marks: Option<Vec<(usize, usize)>> = None;
10117        let mut utf16 = 0;
10118        let mut column = 0;
10119        let mut line_start = 0;
10120        for (index, character) in source.char_indices() {
10121            if !character.is_ascii() && marks.is_none() {
10122                marks = Some(vec![(0, 0)]);
10123            }
10124            if let Some(marks) = &mut marks
10125                && column >= marks.last().expect("a first mark").1 + CHECKPOINT
10126            {
10127                marks.push((index - line_start, column));
10128            }
10129            utf16 += character.len_utf16();
10130            column += character.len_utf16();
10131            if character == '\n' {
10132                line_start = index + 1;
10133                line_starts.push(line_start);
10134                utf16_before_line.push(utf16);
10135                checkpoints.push(marks.take());
10136                column = 0;
10137            }
10138        }
10139        checkpoints.push(marks);
10140        Self {
10141            address: source.as_ptr() as usize,
10142            length: source.len(),
10143            sample: Self::sample(source),
10144            line_starts,
10145            utf16_before_line,
10146            checkpoints,
10147        }
10148    }
10149
10150    fn describes(&self, source: &str) -> bool {
10151        self.address == source.as_ptr() as usize
10152            && self.length == source.len()
10153            && self.sample == Self::sample(source)
10154    }
10155
10156    /// Zero-based line, and the UTF-16 units from that line's start.
10157    fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
10158        let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
10159        let start = self.line_starts[line];
10160        let column = match &self.checkpoints[line] {
10161            None => offset - start,
10162            Some(marks) => {
10163                let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
10164                let (byte, column) = marks[at];
10165                column + source[start + byte..offset].encode_utf16().count()
10166            }
10167        };
10168        (line, column)
10169    }
10170}
10171
10172thread_local! {
10173    static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
10174        const { std::cell::RefCell::new(None) };
10175}
10176
10177/// While one of these is alive, positions in any source are computed from an
10178/// index built once for it. It lives for one instrumentation call, during
10179/// which the source it indexes is borrowed and cannot move; outside one,
10180/// positions are counted from the top as before.
10181pub(crate) struct PositionScope {
10182    owner: bool,
10183}
10184
10185impl PositionScope {
10186    pub(crate) fn enter() -> Self {
10187        let owner = POSITIONS.with(|positions| {
10188            let mut positions = positions.borrow_mut();
10189            let owner = positions.is_none();
10190            if owner {
10191                *positions = Some(Vec::new());
10192            }
10193            owner
10194        });
10195        Self { owner }
10196    }
10197}
10198
10199impl Drop for PositionScope {
10200    fn drop(&mut self) {
10201        if self.owner {
10202            POSITIONS.with(|positions| *positions.borrow_mut() = None);
10203        }
10204    }
10205}
10206
10207fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
10208    POSITIONS.with(|positions| {
10209        let mut positions = positions.borrow_mut();
10210        let known = positions.as_mut()?;
10211        if let Some(found) = known.iter().find(|index| index.describes(source)) {
10212            return Some(found.clone());
10213        }
10214        let built = std::rc::Rc::new(SourcePositions::new(source));
10215        known.push(built.clone());
10216        Some(built)
10217    })
10218}
10219
10220fn utf16_offset(source: &str, offset: usize) -> usize {
10221    match indexed(source) {
10222        Some(index) => {
10223            let (line, column) = index.locate(source, offset);
10224            index.utf16_before_line[line] + column
10225        }
10226        None => source[..offset].encode_utf16().count(),
10227    }
10228}
10229
10230pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
10231    if let Some(index) = indexed(source) {
10232        let (line, column) = index.locate(source, offset);
10233        return (line + 1, column + 1);
10234    }
10235    let prefix = &source[..offset];
10236    let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
10237    let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
10238    let column = source[line_start..offset].encode_utf16().count() + 1;
10239    (line, column)
10240}
10241
10242fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
10243    let start = utf16_offset(source, span.start as usize);
10244    let end = utf16_offset(source, span.end as usize);
10245    let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
10246    let mut id = String::with_capacity(16);
10247    for byte in &digest[..8] {
10248        write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
10249    }
10250    id
10251}
10252
10253#[cfg(test)]
10254mod tests {
10255    use super::*;
10256
10257    #[test]
10258    fn line_index_lookups_agree_with_walking_the_line() {
10259        let long = "é🚀x".repeat(700);
10260        let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
10261        let index = Utf16LineIndex::new(&source);
10262        for (line, text) in source.split_inclusive('\n').enumerate() {
10263            let start = index.starts[line];
10264            let width: usize = text.chars().map(char::len_utf16).sum();
10265            for column in 0..=width + 2 {
10266                assert_eq!(
10267                    index.byte_offset(line, column),
10268                    start + utf16_col_to_byte(text, column),
10269                    "line {line} column {column}"
10270                );
10271            }
10272            for (offset, _) in text.char_indices() {
10273                let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
10274                assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
10275            }
10276        }
10277    }
10278
10279    #[test]
10280    fn indexed_positions_agree_with_counting_from_the_top() {
10281        // The long line passes several checkpoints.
10282        let source = &format!(
10283            "const s = 'naïve 🚀';\n\nexport function f(a) {{\n  return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
10284            "é🚀x".repeat(400)
10285        );
10286        let source = source.as_str();
10287        let counted: Vec<_> = source
10288            .char_indices()
10289            .map(|(offset, _)| {
10290                (
10291                    line_and_utf16_column(source, offset),
10292                    stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10293                )
10294            })
10295            .collect();
10296        let _positions = PositionScope::enter();
10297        for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
10298            assert_eq!(
10299                line_and_utf16_column(source, offset),
10300                position,
10301                "offset {offset}"
10302            );
10303            assert_eq!(
10304                stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10305                id
10306            );
10307        }
10308    }
10309
10310    #[test]
10311    fn classic_scripts_share_application_globals_but_not_probe_bindings() {
10312        // TodoMVC's ES5 app loads several classic scripts into one document.
10313        // They share the global lexical scope, so a second
10314        // `const __supercovMcdcBegin` was a SyntaxError and no item was added.
10315        let a = instrument_direct_candidate(
10316            "var count = 0; function increment() { if (count >= 0) { count += 1; } }",
10317            "public/a.js",
10318        )
10319        .unwrap();
10320        let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
10321            .unwrap();
10322        let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
10323        let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
10324        let script = format!(
10325            "const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
10326             vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
10327             if (ctx.observed !== 1) throw Error('shared globals changed');\
10328             vm.runInContext({}, ctx);\
10329             if (ctx.observed !== 2) throw Error('a repeated script changed');",
10330            serde_json::to_string(runtime).unwrap(),
10331            serde_json::to_string(&a.code).unwrap(),
10332            serde_json::to_string(&b.code).unwrap(),
10333            serde_json::to_string(&b.code).unwrap()
10334        );
10335        // Through stdin: the script carries the whole runtime, longer than a
10336        // Windows command line may be.
10337        let mut node = std::process::Command::new("node")
10338            .arg("-")
10339            .stdin(std::process::Stdio::piped())
10340            .stdout(std::process::Stdio::piped())
10341            .stderr(std::process::Stdio::piped())
10342            .spawn()
10343            .expect("Node.js for the JavaScript regression");
10344        std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
10345        let result = node.wait_with_output().unwrap();
10346        assert!(
10347            result.status.success(),
10348            "{}",
10349            String::from_utf8_lossy(&result.stderr)
10350        );
10351    }
10352
10353    #[test]
10354    fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
10355        // TodoMVC's React app keeps JSX in `src/index.js`; the run failed on
10356        // `Unexpected token` before any test started.
10357        for file in [
10358            "src/index.js",
10359            "src/index.jsx",
10360            "src/index.mjs",
10361            "src/index.cjs",
10362            "src/index.tsx",
10363        ] {
10364            let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
10365            let output = instrument_candidate(source, file).unwrap();
10366            assert!(!output.points.is_empty(), "{file}");
10367            assert!(output.code.contains("<div>"), "{file}");
10368        }
10369        // TypeScript keeps its angle-bracket assertion; JSX would misread it.
10370        assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
10371        // The script or module mode an extension implies is kept.
10372        for file in ["a.js", "a.cjs", "a.mjs"] {
10373            let before = SourceType::from_path(file).unwrap();
10374            let after = project_source_type(Path::new(file)).unwrap();
10375            assert_eq!(after.module_kind(), before.module_kind(), "{file}");
10376            assert!(after.is_jsx(), "{file}");
10377        }
10378    }
10379
10380    const SOURCE: &str =
10381        "export function decide(a,b,c) {\n  if ((a && b) || !c) return 1;\n  return 0;\n}\n";
10382
10383    #[test]
10384    fn matches_the_frozen_if_decision_manifest_exactly() {
10385        let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
10386        assert!(!output.complete);
10387        assert_eq!(
10388            output.decisions,
10389            vec![CandidateDecision {
10390                id: "2f65989a5782c5bd".to_string(),
10391                file: "app/decide.ts".to_string(),
10392                line: 2,
10393                column: 7,
10394                source: "(a && b) || !c".to_string(),
10395                conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
10396                kind: "if".to_string(),
10397            }]
10398        );
10399    }
10400
10401    #[test]
10402    fn a_test_files_unchanged_text_keeps_its_columns() {
10403        // One mapping for a line put every frame of a test file in column 1,
10404        // and one inside a wrapped assertion at the statement's start: Vitest
10405        // printed `:2:3` where it prints `:2:22` without Supercov.
10406        let source = "import { expect } from 'vitest';\n  expect(value(1)).toBe(2);\n";
10407        let wrapper = "wrap(() => ";
10408        let edits = vec![
10409            TextEdit {
10410                start: source.find("expect(value").unwrap(),
10411                end: source.find("expect(value").unwrap(),
10412                text: wrapper.into(),
10413                rank: 0,
10414            },
10415            TextEdit {
10416                start: source.rfind(';').unwrap(),
10417                end: source.rfind(';').unwrap(),
10418                text: ")".into(),
10419                rank: 0,
10420            },
10421        ];
10422        let (code, map) = apply_text_edits(source, edits, "a.test.js").unwrap();
10423        assert_eq!(
10424            code,
10425            "import { expect } from 'vitest';\n  wrap(() => expect(value(1)).toBe(2));\n"
10426        );
10427        let original = |line: u32, column: u32| {
10428            map.get_tokens()
10429                .filter(|token| token.get_dst_line() == line && token.get_dst_col() <= column)
10430                .max_by_key(|token| token.get_dst_col())
10431                .map(|token| (token.get_src_line(), token.get_src_col()))
10432        };
10433        // `toBe`, past the text the wrapper added.
10434        let matcher = code.lines().nth(1).unwrap().find("toBe").unwrap() as u32;
10435        assert_eq!(original(1, matcher), Some((1, 19)));
10436        // A word and a mark of a line nothing was added to.
10437        assert_eq!(original(0, 9), Some((0, 9)));
10438        assert_eq!(original(0, 7), Some((0, 7)));
10439        // The text the wrapper added maps to where the line began.
10440        assert_eq!(original(1, 4), Some((1, 0)));
10441        // An identifier carries its name, which a compiler reading this map
10442        // keeps and Jest prints (`at toBe (…)`); a keyword carries none.
10443        let name_at = |line: u32, column: u32| {
10444            map.get_tokens()
10445                .find(|token| token.get_dst_line() == line && token.get_dst_col() == column)
10446                .and_then(|token| token.get_name_id())
10447                .and_then(|id| map.get_name(id))
10448                .map(|name| name.to_string())
10449        };
10450        assert_eq!(name_at(1, matcher).as_deref(), Some("toBe"));
10451        assert_eq!(name_at(0, 9).as_deref(), Some("expect"));
10452        assert_eq!(name_at(0, 18), None, "`from` is a keyword");
10453    }
10454
10455    #[test]
10456    fn a_server_entry_keeps_what_it_exports_by_default() {
10457        // Taken before it was looked at, anything but a function declaration
10458        // was replaced by a placeholder `null`: `export default handleRequest;`
10459        // exported `withRequestPhase(null)`, and a class exported `null`.
10460        for (source, export) in [
10461            (
10462                "function handleRequest(r) { return r; }\nexport default handleRequest;\n",
10463                "export default __supercovWithRequestPhase13(handleRequest);",
10464            ),
10465            (
10466                "export default class Entry {}\n",
10467                "export default class Entry {}",
10468            ),
10469        ] {
10470            let output = instrument_candidate_with_binding(
10471                source,
10472                "app/entry.server.ts",
10473                RuntimeBinding::ModuleImport,
10474                None,
10475                false,
10476            )
10477            .unwrap();
10478            assert!(
10479                output.code.lines().any(|line| line == export),
10480                "{export}\n{}",
10481                output.code
10482            );
10483        }
10484    }
10485
10486    #[test]
10487    fn a_method_read_through_a_property_keeps_its_text() {
10488        // `box.method.toString()` names no binding; any method of that name in
10489        // the file is taken as the one read, and others keep their probes.
10490        let source = "export const box = { method(v) { return v  * 2; } };\nexport class Shape { area(w) { return w  * w; } static make = () =>  new Shape(); }\nexport const other = { untouched(v) { return v  + 1; } };\nexport const texts = [box.method.toString(), String(Shape.prototype.area), Shape.make.toString()];\n";
10491        let output = instrument_direct_candidate(source, "src/methods.js").unwrap();
10492        for text in [
10493            "method(v) { return v  * 2; }",
10494            "area(w) { return w  * w; }",
10495            "() =>  new Shape()",
10496        ] {
10497            assert!(output.code.contains(text), "{text}\n{}", output.code);
10498        }
10499        assert!(
10500            !output.code.contains("untouched(v) { return v  + 1; }"),
10501            "{}",
10502            output.code
10503        );
10504    }
10505
10506    #[test]
10507    fn a_function_whose_source_is_read_keeps_its_text_exactly() {
10508        // Left without probes, it was still printed again: `f.toString()`
10509        // read `function f() {\n\treturn 1;\n}` under measurement.
10510        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";
10511        let output = instrument_direct_candidate(source, "src/text.js").unwrap();
10512        for text in [
10513            "function* g() { yield 1; }",
10514            "(a,  b) =>   a + b",
10515            "async function later(x) {\n  // kept\n  if (x) {\n      return x;\n  }\n  return 0;\n}",
10516        ] {
10517            assert!(output.code.contains(text), "{text}\n{}", output.code);
10518        }
10519        // A function nobody reads the source of is measured as before.
10520        assert!(
10521            !output
10522                .code
10523                .contains("function counted(n) { return n > 1 ? 'many' : 'one'; }"),
10524            "{}",
10525            output.code
10526        );
10527        // Typed source keeps its types as written.
10528        let typed = "export function twice(value: number): number { return value  * 2; }\nexport const text = twice.toString();\n";
10529        let output = instrument_direct_candidate(typed, "src/typed.ts").unwrap();
10530        assert!(
10531            output
10532                .code
10533                .contains("function twice(value: number): number { return value  * 2; }"),
10534            "{}",
10535            output.code
10536        );
10537    }
10538
10539    #[test]
10540    fn codegen_output_reparses_for_typescript_and_tsx() {
10541        for (file, source) in [
10542            (
10543                "component.tsx",
10544                "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
10545            ),
10546            ("module.ts", SOURCE),
10547        ] {
10548            let output = analyze_candidate(source, file).unwrap();
10549            let map = output.map.as_ref().expect("candidate source map");
10550            assert_eq!(map["version"], 3);
10551            assert_eq!(map["sources"][0], file);
10552            assert_eq!(map["sourcesContent"][0], source);
10553            assert!(
10554                map["mappings"]
10555                    .as_str()
10556                    .is_some_and(|value| !value.is_empty())
10557            );
10558            let allocator = Allocator::default();
10559            let source_type = SourceType::from_path(file).unwrap();
10560            let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
10561            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
10562        }
10563    }
10564
10565    #[test]
10566    fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
10567        for file in ["src/module.mjs", "src/script.cjs"] {
10568            let output = instrument_direct_candidate(
10569                "export const selected = value => value ? 1 : 0;",
10570                file,
10571            )
10572            .unwrap();
10573            assert!(
10574                output
10575                    .code
10576                    .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
10577            );
10578            assert!(!output.code.contains("virtual:supercov-runtime"));
10579            assert!(!output.code.contains("globalThis.__supercovRuntime"));
10580        }
10581    }
10582
10583    #[test]
10584    fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
10585        let source = concat!(
10586            "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10587            "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
10588            "ordinaryHelper();\n",
10589        );
10590        let output = instrument_node_assertion_phases_with_runtime_hooks(
10591            source,
10592            "tests/runner.mjs",
10593            &[],
10594            Some("../.supercov/launchSupervisor.mjs"),
10595        )
10596        .unwrap();
10597        assert_eq!(output.assertions, 0);
10598        assert_eq!(output.capability_imports, 1);
10599        assert!(output.code.contains("wrapImportedCapability"));
10600        assert!(output.code.contains("__supercovRawImageBuilder"));
10601        assert!(output.code.contains("const ImageBuilder"));
10602        assert!(!output.code.contains("__supercovRawordinaryHelper"));
10603        assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
10604    }
10605
10606    #[test]
10607    fn capability_imports_accept_a_computed_guest_mount_path() {
10608        let source = concat!(
10609            "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10610            "const guestRoot = resolve(tmpdir(), 'workspace');\n",
10611            "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
10612            "ordinaryHelper();\n",
10613        );
10614        let output = instrument_node_assertion_phases_with_runtime_hooks(
10615            source,
10616            "tests/runner.mjs",
10617            &[],
10618            Some("../.supercov/launchSupervisor.mjs"),
10619        )
10620        .unwrap();
10621        assert_eq!(output.capability_imports, 1);
10622        assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
10623        assert!(!output.code.contains("__supercovRawordinaryHelper"));
10624    }
10625
10626    #[test]
10627    fn capability_import_selection_follows_a_mapping_variable() {
10628        let source = concat!(
10629            "import { launch, unrelated } from './sdk.mjs';\n",
10630            "run();\n",
10631            "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
10632            "function run() { launch(options); unrelated(); }\n",
10633        );
10634        let output = instrument_node_assertion_phases_with_runtime_hooks(
10635            source,
10636            "tests/runner.mjs",
10637            &[],
10638            Some("../.supercov/launchSupervisor.mjs"),
10639        )
10640        .unwrap();
10641        assert_eq!(output.capability_imports, 1);
10642        assert!(output.code.contains("__supercovRawlaunch"));
10643        assert!(!output.code.contains("__supercovRawunrelated"));
10644    }
10645
10646    #[test]
10647    fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
10648        let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
10649        let output = instrument_node_assertion_phases_with_runtime_hooks(
10650            source,
10651            "src/main.mjs",
10652            &[],
10653            Some("../.supercov/launchSupervisor.mjs"),
10654        )
10655        .unwrap();
10656        assert_eq!(output.code, source);
10657        assert_eq!(output.capability_imports, 0);
10658    }
10659
10660    #[test]
10661    fn ordinary_mount_language_does_not_enable_capability_proxies() {
10662        let source = concat!(
10663            "import { render } from '@testing-library/react';\n",
10664            "it('mounts into a host element', () => render(<main />));\n",
10665        );
10666        let output = instrument_node_assertion_phases_with_runtime_hooks(
10667            source,
10668            "tests/component.test.tsx",
10669            &[],
10670            Some("../.supercov/launchSupervisor.mjs"),
10671        )
10672        .unwrap();
10673        assert_eq!(output.capability_imports, 0);
10674        assert!(!output.code.contains("wrapImportedCapability"));
10675    }
10676
10677    #[test]
10678    fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
10679        let source = concat!(
10680            "import { test, expect } from '@example/test-admin';\n",
10681            "test('visual snapshot', async ({ page }) => {\n",
10682            "  expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
10683            "});\n",
10684        );
10685        let output = instrument_node_assertion_phases_with_runtime_hooks(
10686            source,
10687            "tests/visual.spec.ts",
10688            &[],
10689            Some("../.supercov/launchSupervisor.mjs"),
10690        )
10691        .unwrap();
10692        assert_eq!(output.capability_imports, 0);
10693        assert!(!output.code.contains("wrapImportedCapability"));
10694    }
10695
10696    #[test]
10697    fn capability_rewrite_excludes_runtime_and_type_only_imports() {
10698        let source = concat!(
10699            "import type { Machine } from './types.ts';\n",
10700            "import { test } from 'node:test';\n",
10701            "import { runtime } from '../.supercov/runtime.mjs';\n",
10702            "console.log({ machine: true, test, runtime });\n",
10703        );
10704        let output = instrument_node_assertion_phases_with_runtime_hooks(
10705            source,
10706            "tests/runner.ts",
10707            &[],
10708            Some("../.supercov/launchSupervisor.mjs"),
10709        )
10710        .unwrap();
10711        assert_eq!(output.code, source);
10712        assert_eq!(output.capability_imports, 0);
10713    }
10714
10715    #[test]
10716    fn native_assertion_arguments_execute_inside_the_assertion_phase() {
10717        let source = concat!(
10718            "import assert from 'node:assert/strict';\n",
10719            "assert.equal(value(), 1);\n",
10720        );
10721        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10722        assert_eq!(output.assertions, 1);
10723        assert!(output.code.contains("withNodeAssertionPhase"));
10724        assert!(output.code.contains("node:assert/strict.equal"));
10725        assert!(output.code.contains("tests/value.test.mjs:2:1"));
10726        assert!(output.code.contains("assert.equal(value(), 1)"));
10727    }
10728
10729    #[test]
10730    fn a_docblock_the_test_opens_with_stays_first_for_jest_to_read() {
10731        let source = "/** @jest-environment jsdom */\n\nimport assert from 'node:assert';\nassert.equal(value(), 1);\n";
10732        let output = instrument_node_assertion_phases_with_runtime_imports(
10733            source,
10734            "tests/view.test.ts",
10735            &[],
10736            None,
10737            Some("../.supercov/node_modules/bootstrap.cjs"),
10738        )
10739        .unwrap();
10740        let lines = output.code.lines().collect::<Vec<_>>();
10741        assert!(
10742            lines[0].starts_with(
10743                "/** @jest-environment jsdom */ if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"
10744            ),
10745            "{}",
10746            lines[0]
10747        );
10748        assert!(
10749            lines[3].contains("assert.equal(value(), 1)"),
10750            "lines stay where they were"
10751        );
10752    }
10753
10754    #[test]
10755    fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
10756        let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
10757        let output = instrument_node_assertion_phases_with_runtime_imports(
10758            source,
10759            "tests/value.test.mjs",
10760            &[],
10761            None,
10762            Some("../.supercov/node_modules/bootstrap.cjs"),
10763        )
10764        .unwrap();
10765        assert_eq!(output.assertions, 1);
10766        // The loader shares the first line, so the assertion stays on line 2.
10767        let lines = output.code.lines().collect::<Vec<_>>();
10768        assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
10769        assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
10770        assert!(lines[0].ends_with("import assert from 'node:assert';"));
10771        assert!(lines[1].contains("withNodeAssertionPhase"));
10772        let allocator = Allocator::default();
10773        assert!(
10774            Parser::new(&allocator, &output.code, SourceType::mjs())
10775                .parse()
10776                .errors
10777                .is_empty()
10778        );
10779    }
10780
10781    #[test]
10782    fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
10783        let source = concat!(
10784            "import assert from 'node:assert/strict';\n",
10785            "export async function check() { assert.equal(await value(), 1); }\n",
10786        );
10787        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10788        assert_eq!(output.assertions, 1);
10789        assert!(output.code.contains("bindNodeAssertionPhase"));
10790        assert!(output.code.contains("await value()"));
10791        let allocator = Allocator::default();
10792        assert!(
10793            Parser::new(&allocator, &output.code, SourceType::mjs())
10794                .parse()
10795                .errors
10796                .is_empty()
10797        );
10798    }
10799
10800    #[test]
10801    fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
10802        let source = concat!(
10803            "import assert from 'node:assert/strict';\n",
10804            "export async function check(user?: string) {\n",
10805            "  assert.ok(user, await value());\n",
10806            "  assert\n",
10807            "    .equal(await value(), 1)\n",
10808            "}\n",
10809        );
10810        let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
10811        assert_eq!(output.assertions, 2);
10812        let code = &output.code;
10813        let lines: Vec<&str> = code.lines().collect();
10814        assert!(
10815            lines[2].starts_with(
10816                "  { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
10817            ),
10818            "{code}"
10819        );
10820        assert!(
10821            lines[2]
10822                .ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
10823            "{code}"
10824        );
10825        assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
10826        assert_eq!(
10827            lines[4], "     \"equal\"); __supercovAssertionPhase(await value(), 1) }",
10828            "{code}"
10829        );
10830        assert_eq!(lines[5], "}", "{code}");
10831        let allocator = Allocator::default();
10832        assert!(
10833            Parser::new(&allocator, code, SourceType::ts())
10834                .parse()
10835                .errors
10836                .is_empty()
10837        );
10838    }
10839
10840    #[test]
10841    fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
10842        let source = concat!(
10843            "import { expect, test } from '@playwright/test';\n",
10844            "test('value', async () => { expect(await value()).toBe(1); });\n",
10845        );
10846        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
10847        assert_eq!(output.assertions, 1);
10848        assert!(output.code.contains("bindNodeAssertionPhase"));
10849        assert!(output.code.contains("await value()"));
10850        let allocator = Allocator::default();
10851        assert!(
10852            Parser::new(&allocator, &output.code, SourceType::mjs())
10853                .parse()
10854                .errors
10855                .is_empty()
10856        );
10857    }
10858
10859    #[test]
10860    fn native_commonjs_assertion_bindings_are_attributed() {
10861        let source = concat!(
10862            "const assert = require('node:assert/strict');\n",
10863            "const { ok: verify } = require('assert');\n",
10864            "assert.equal(value(), 1);\n",
10865            "verify(other());\n",
10866        );
10867        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10868        assert_eq!(output.assertions, 2);
10869        assert!(output.code.contains("node:assert/strict.equal"));
10870        assert!(output.code.contains("node:assert.ok"));
10871    }
10872
10873    #[test]
10874    fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
10875        let source = concat!(
10876            "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
10877            "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
10878        );
10879        let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10880        assert_eq!(output.assertions, 1);
10881        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10882    }
10883
10884    #[test]
10885    fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
10886        let source = concat!(
10887            "import assert from 'node:assert/strict';\n",
10888            "function unrelated(assert) { assert.equal(effect(), 1); }\n",
10889            "assert.equal(value(), 1);\n",
10890        );
10891        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10892        assert_eq!(output.assertions, 1);
10893        assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10894    }
10895
10896    #[test]
10897    fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
10898        let source = concat!(
10899            "import test from 'node:test';\n",
10900            "import { expect as verify } from 'expect-library';\n",
10901            "test('value', () => verify(value()).not.toEqual(1));\n",
10902        );
10903        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10904        assert_eq!(output.assertions, 1);
10905        assert!(output.code.contains("expect.not.toEqual"));
10906        assert!(output.code.contains("verify(value()).not.toEqual(1)"));
10907    }
10908
10909    #[test]
10910    fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
10911        let source = concat!(
10912            "import { expect, test } from 'vitest';\n",
10913            "test('value', () => expect(value()).toBe(1));\n",
10914        );
10915        let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10916        assert_eq!(output.assertions, 1);
10917        assert!(output.code.contains("expect.toBe"));
10918    }
10919
10920    #[test]
10921    fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
10922        // Vitest Browser Mode asserts through `expect.element(locator)`, and
10923        // `expect.soft`/`expect.poll` wrap the same way. The chain root is a
10924        // member call rather than a bare `expect` identifier, which is the
10925        // shape that used to leave every UI assertion unrecognised.
10926        for (call, operation) in [
10927            (
10928                "expect.element(button).toHaveAccessibleName('Copy')",
10929                "expect.element.toHaveAccessibleName",
10930            ),
10931            ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
10932            ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
10933        ] {
10934            let source = format!(
10935                "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
10936            );
10937            let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
10938            assert_eq!(output.assertions, 1, "{call}");
10939            assert!(output.code.contains(operation), "{call} -> {operation}");
10940        }
10941    }
10942
10943    #[test]
10944    fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
10945        // A JSX tree is one statement, so an attribute and a child that call
10946        // the same function had no way to be told apart -- which is exactly
10947        // what a UI assertion needs, `toHaveAccessibleName` naming the
10948        // attribute and `toHaveTextContent` the child.
10949        let source = concat!(
10950            "export function Button({ label, copied }) {\n",
10951            "  return (\n",
10952            "    <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
10953            "      {format(copied)}\n",
10954            "    </button>\n",
10955            "  );\n",
10956            "}\n",
10957        );
10958        let output = instrument_candidate(source, "src/button.tsx").unwrap();
10959        let rendered = output
10960            .points
10961            .iter()
10962            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10963            .collect::<Vec<_>>();
10964        // The attribute and the child are separate obligations at separate
10965        // positions, even though they read identically.
10966        assert_eq!(rendered.len(), 2, "{:?}", output.points);
10967        assert!(
10968            rendered
10969                .iter()
10970                .all(|point| point.source == "format(copied)")
10971        );
10972        assert_eq!(rendered[0].line, 3);
10973        assert_eq!(rendered[1].line, 4);
10974        assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
10975        // `{label}` is reached exactly when the tree is, and the handler is
10976        // measured where it is called; neither earns an obligation.
10977        assert!(!output.code.contains("title={__supercovRenderedValueV2"));
10978        // JSX forbids a bare comma operator, so the probe has to carry the
10979        // value through a call rather than a sequence expression.
10980        assert!(!output.code.contains("), format(copied)}"));
10981        assert!(output.code.contains(", format(copied))}"));
10982    }
10983
10984    #[test]
10985    fn instrumented_jsx_stays_parseable_through_every_container_shape() {
10986        // A JSX container cannot hold a bare comma operator, and a malformed
10987        // rewrite would surface only as the project's own build failing. Each
10988        // shape below reaches the rewrite by a different route.
10989        let source = concat!(
10990            "export function Panel({ items, user, error, tone }) {\n",
10991            "  return (\n",
10992            "    <>\n",
10993            "      <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
10994            "      <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
10995            "        {error ? <Alert text={message(error)} /> : null}\n",
10996            "        {items.map((item) => (\n",
10997            "          <Item key={item.id} label={format(item)} />\n",
10998            "        ))}\n",
10999            "        {count(items) > 0 && <Footer total={sum(items)} />}\n",
11000            "      </div>\n",
11001            "    </>\n",
11002            "  );\n",
11003            "}\n",
11004        );
11005        let output = instrument_candidate(source, "src/panel.tsx").unwrap();
11006        // The instrumented output must itself parse.
11007        instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
11008        // A spread attribute is not a container and is never rewritten.
11009        assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
11010        let rendered = output
11011            .points
11012            .iter()
11013            .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
11014            .map(|point| point.source.as_str())
11015            .collect::<Vec<_>>();
11016        assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
11017        assert!(
11018            rendered.contains(&"cx(\'a\', tone && \'b\')"),
11019            "{rendered:?}"
11020        );
11021        assert!(rendered.contains(&"format(item)"), "{rendered:?}");
11022        assert!(rendered.contains(&"message(error)"), "{rendered:?}");
11023        // `user?.id` short-circuits but is still a member access reached with
11024        // the statement, and a key that is a bare member chain is too.
11025        assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
11026        assert!(!rendered.contains(&"item.id"), "{rendered:?}");
11027    }
11028
11029    #[test]
11030    fn jest_global_expect_is_attributed_next_to_its_test_globals() {
11031        // Jest injects `expect`, `test`, `it` and `describe` into the test
11032        // environment; none is imported. The bare `expect` is the assertion
11033        // exactly when the file reaches for those test globals too.
11034        let source = "test('value', () => expect(value()).toBe(1));\n";
11035        let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
11036        assert_eq!(output.assertions, 1);
11037        assert!(output.code.contains("expect.toBe"));
11038        // A stray global `expect` in ordinary code is not one.
11039        let plain = "const ok = expect(value()).toBe(1);\n";
11040        let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
11041        assert_eq!(output.assertions, 0);
11042        assert_eq!(output.code, plain);
11043    }
11044
11045    #[test]
11046    fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
11047        let source = concat!(
11048            "import { expect, test } from '@playwright/test';\n",
11049            "test('value', () => expect(value()).toBe(1));\n",
11050        );
11051        let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
11052        assert_eq!(output.assertions, 1);
11053        assert!(output.code.contains("expect.toBe"));
11054    }
11055
11056    /// Each way a test file reaches an assertion, and the look-alikes that
11057    /// are not one: another library's `expect`, a `require` of a name only
11058    /// known at run time, a local `expect`, and calls that can short-circuit
11059    /// before the assertion runs.
11060    #[test]
11061    fn an_assertion_is_recognised_however_it_was_reached_and_only_then() {
11062        for (file, source, assertions) in [
11063            (
11064                "t/a.test.js",
11065                "describe('x', () => { it('y', () => expect(a()).toBe(1)); });\n",
11066                1,
11067            ),
11068            ("t/b.test.js", "it('y', () => expect(a()).toBe(1));\n", 1),
11069            (
11070                "t/p.test.js",
11071                "describe('x', function () { expect(a()).toBe(1); });\n",
11072                1,
11073            ),
11074            // A global `expect` with none of the test globals beside it.
11075            ("t/q.test.js", "expect(a()).toBe(1);\n", 0),
11076            (
11077                "t/c.test.cjs",
11078                "const { expect } = require('vitest');\nexpect(a()).toBe(1);\n",
11079                1,
11080            ),
11081            (
11082                "t/d.test.cjs",
11083                "const { expect: check } = require('@jest/globals');\ncheck(a()).toBe(1);\n",
11084                1,
11085            ),
11086            (
11087                "t/i.test.mjs",
11088                "import { expect as e } from 'vitest';\ne(a()).toBe(1);\n",
11089                1,
11090            ),
11091            (
11092                "t/n.test.cjs",
11093                "const { strict } = require('node:assert');\nstrict.equal(a(), 1);\n",
11094                1,
11095            ),
11096            (
11097                "t/s.test.cjs",
11098                "const assert = require('node:assert').strict;\nassert.equal(a(), 1);\n",
11099                1,
11100            ),
11101            // An export of `node:assert` that is not an assertion.
11102            (
11103                "t/h.test.mjs",
11104                "import { ok, AssertionError } from 'node:assert';\nok(a());\nnew AssertionError({});\n",
11105                1,
11106            ),
11107            (
11108                "t/j.test.cjs",
11109                "const { equal, AssertionError } = require('assert');\nequal(a(), 1);\nnew AssertionError({});\n",
11110                1,
11111            ),
11112            (
11113                "t/e.test.cjs",
11114                "const { expect } = require('chai');\nexpect(a()).to.equal(1);\n",
11115                0,
11116            ),
11117            (
11118                "t/f.test.cjs",
11119                "const name = 'node:assert';\nconst assert = require(name);\nassert.ok(a());\n",
11120                0,
11121            ),
11122            (
11123                "t/o.test.cjs",
11124                "const [first] = require('node:assert');\nfirst(a());\n",
11125                0,
11126            ),
11127            (
11128                "t/m.test.mjs",
11129                "import { test } from 'node:test';\nconst expect = (v) => ({ toBe() {} });\ntest('x', () => expect(a()).toBe(1));\n",
11130                0,
11131            ),
11132            (
11133                "t/k.test.mjs",
11134                "import assert from 'node:assert';\nassert?.ok(a());\nassert.ok?.(b());\n",
11135                0,
11136            ),
11137        ] {
11138            let output = instrument_node_assertion_phases(source, file).unwrap();
11139            assert_eq!(output.assertions, assertions, "{file}");
11140            assert_eq!(
11141                output.code.matches("withNodeAssertionPhase").count(),
11142                assertions,
11143                "{file}"
11144            );
11145        }
11146    }
11147
11148    /// A sandbox call is wrapped only when its argument maps the host's
11149    /// workspace to the guest's, and never for Supercov's own or a test
11150    /// framework's modules.
11151    #[test]
11152    fn only_a_host_to_guest_mapping_makes_a_capability_candidate() {
11153        assert!(capability_source_candidate(
11154            "run({ hostPath: a, guestPath: b })"
11155        ));
11156        assert!(capability_source_candidate(
11157            "run({ hostRoot: a, guestRoot: b })"
11158        ));
11159        assert!(capability_source_candidate(
11160            "start({ Mounts: [{ source: a, target: b }] })"
11161        ));
11162        assert!(capability_source_candidate(
11163            "start({ mounts: [{ source: a, target: b }] })"
11164        ));
11165        for unrelated in [
11166            "run({ hostPath: a })",
11167            "run({ hostRoot: a, guestPath: b })",
11168            "start({ mounts: [{ source: a }] })",
11169            "start({ source: a, target: b })",
11170            "snapshot(machine.spawn())",
11171        ] {
11172            assert!(!capability_source_candidate(unrelated), "{unrelated}");
11173        }
11174        let wrapper = "./.supercov/capability.mjs";
11175        for excluded in [
11176            wrapper,
11177            "node:child_process",
11178            "virtual:supercov-runtime",
11179            "../.supercov/node_modules/x.mjs",
11180            "vitest",
11181            "@playwright/test",
11182        ] {
11183            assert!(excluded_capability_import(excluded, wrapper), "{excluded}");
11184        }
11185        assert!(!excluded_capability_import("e2b", wrapper));
11186    }
11187
11188    #[test]
11189    fn an_arrow_that_returns_an_assertion_still_returns_it() {
11190        // `await loginError(page)` waits for the matcher only if the arrow
11191        // still returns it: made a block, it returned nothing at once.
11192        let source = concat!(
11193            "import { expect, test } from '@playwright/test';\n",
11194            "const loginError = (page: Page, error: string) => expect(page).toHaveURL(error);\n",
11195            "test('value', () => expect(value()).toBe(1));\n",
11196        );
11197        for file in ["tests/login.spec.ts", "tests/login.spec.js"] {
11198            let source = if file.ends_with(".js") {
11199                source.replace(": Page", "").replace(": string", "")
11200            } else {
11201                source.into()
11202            };
11203            let output = instrument_node_assertion_phases(&source, file).unwrap();
11204            assert_eq!(output.assertions, 2, "{file}");
11205            let code = output.code.split("\n//# sourceMappingURL").next().unwrap();
11206            assert!(!code.contains("=> { const"), "{file}:\n{code}");
11207            assert!(!code.contains("finally"), "{file}:\n{code}");
11208            assert!(
11209                code.contains("expect(page).toHaveURL(error)"),
11210                "{file}:\n{code}"
11211            );
11212        }
11213    }
11214
11215    #[test]
11216    fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
11217        let source = concat!(
11218            "import { browserTest, expect } from '@acme/browser-fixtures';\n",
11219            "browserTest('value', () => expect(value()).toBe(1));\n",
11220        );
11221        let output = instrument_node_assertion_phases_with_expect_modules(
11222            source,
11223            "tests/value.spec.mjs",
11224            &["@acme/browser-fixtures".into()],
11225        )
11226        .unwrap();
11227        assert_eq!(output.assertions, 1);
11228        assert!(output.code.contains("expect.toBe"));
11229    }
11230
11231    #[test]
11232    fn preserves_comment_payloads_byte_for_byte() {
11233        let comment = "/*---\ndescription: >\n  nested indentation is data\ninfo: |\n  first\n    second\n---*/";
11234        let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
11235        let output = instrument_candidate(&source, "app/comments.js").unwrap();
11236        assert!(output.code.contains(comment), "{}", output.code);
11237    }
11238
11239    #[test]
11240    fn source_map_destinations_follow_restored_comments() {
11241        let source = "function run() {\n  // first omitted comment\n  // second omitted comment\n  return 1;\n}\n";
11242        let output = analyze_candidate(source, "app/comment-map.js").unwrap();
11243        let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
11244        let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
11245        let return_token = map
11246            .get_tokens()
11247            .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
11248            .expect("return token mapping");
11249        let offset = Utf16LineIndex::new(&output.code).byte_offset(
11250            return_token.get_dst_line() as usize,
11251            return_token.get_dst_col() as usize,
11252        );
11253        assert!(
11254            output.code[offset..].starts_with("return"),
11255            "{}",
11256            output.code
11257        );
11258    }
11259
11260    #[test]
11261    fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
11262        // B10. A code generator rewrites `</script` to `<\/script` inside a
11263        // comment so the output cannot close an HTML <script> element early.
11264        // The restore compared the two spellings as different text, and because
11265        // it walks the source comments in order, one comment it could not
11266        // recognise consumed every remaining one and failed the file. That cost
11267        // a real project its coverage silently, and later its whole run.
11268        //
11269        // Prose about HTML is ordinary in web code, which is how this reached a
11270        // shipped release and stayed for months: the file was otherwise plain
11271        // TypeScript.
11272        for source in [
11273            "// </script>\nexport const x = 1;\n",
11274            "// </script\nexport const x = 1;\n",
11275            "// </SCRIPT>\nexport const x = 1;\n",
11276            "// see the </script> tag here\nexport const x = 1;\n",
11277            "/* block </script> here */\nexport const x = 1;\n",
11278            "/** jsdoc </script> */\nexport const x = 1;\n",
11279            // A run of comments, as the reported file had.
11280            "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
11281        ] {
11282            let output = instrument_candidate(source, "src/probe.ts")
11283                .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
11284            let original = source.lines().next().expect("a comment");
11285            assert!(
11286                output.code.contains(original),
11287                "comment was not restored verbatim: {original:?} missing from {:?}",
11288                output.code
11289            );
11290        }
11291        // The escape is the generator's, not the author's: a backslash the
11292        // author wrote survives as written rather than being collapsed.
11293        let written = "// author wrote <\\/script>\nexport const x = 1;\n";
11294        let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
11295        assert!(
11296            output.code.contains("// author wrote <\\/script>"),
11297            "{:?}",
11298            output.code
11299        );
11300        // Only that sequence is normalised. Nothing else a comment may contain
11301        // is treated as equal to a different spelling.
11302        assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
11303        assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
11304        assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
11305    }
11306
11307    #[test]
11308    fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
11309        // The counts in this error are usually equal, so reporting them alone
11310        // says the opposite of what happened -- nothing was dropped, one
11311        // comment was not recognised. The reported 116-vs-112 sent the first
11312        // reader looking for four missing comments that were never the cause.
11313        let error = CandidateError::CommentPreservation {
11314            expected: 116,
11315            actual: 112,
11316            detail:
11317                "no source comment matches the generated comment \"// x\" (matched 97 before it)"
11318                    .to_owned(),
11319        };
11320        let rendered = format!("{error:?}");
11321        assert!(rendered.contains("no source comment matches"), "{rendered}");
11322        assert!(rendered.contains("matched 97 before it"), "{rendered}");
11323    }
11324
11325    #[test]
11326    fn a_wrapped_default_keeps_the_type_its_syntax_names() {
11327        // Wrapped in a call, `strict = false` is no longer a default
11328        // `isolatedDeclarations` can read a type from, and h3 failed with
11329        // TS9011. The annotation says what was inferred before.
11330        let source = concat!(
11331            "export function f(a: number, strict = false, limit = 10, name = `x`, big = 1n, low = -1) { return a; }\n",
11332            "export function g<E extends 'utf8' | false = 'utf8'>(encoding = 'utf8' as E, list = [1], kept: string = 'a') { return encoding; }\n",
11333            "export const h = ({ open = false }, mode = 'x' as const) => open;\n",
11334        );
11335        let output = instrument_candidate(source, "src/defaults.ts").unwrap();
11336        for annotated in [
11337            "strict: boolean = ",
11338            "limit: number = ",
11339            "name: string = ",
11340            "big: bigint = ",
11341            "low: number = ",
11342            "encoding: E = ",
11343            "kept: string = ",
11344        ] {
11345            assert!(
11346                output.code.contains(annotated),
11347                "{annotated}\n{}",
11348                output.code
11349            );
11350        }
11351        // Nothing is said where the syntax names no type: an array, a
11352        // destructured parameter, `as const`.
11353        for bare in ["list = ", "mode = "] {
11354            assert!(output.code.contains(bare), "{bare}\n{}", output.code);
11355        }
11356        assert!(!output.code.contains("list:"), "{}", output.code);
11357        assert!(!output.code.contains("mode:"), "{}", output.code);
11358        // A JavaScript file has no annotations to give.
11359        let script = instrument_candidate(
11360            "export function f(strict = false) { return strict; }\n",
11361            "src/defaults.js",
11362        )
11363        .unwrap();
11364        assert!(!script.code.contains("strict:"), "{}", script.code);
11365    }
11366
11367    #[test]
11368    fn an_asserted_assignment_target_is_printed_as_a_target() {
11369        // `(buffer[0] as string) += child` came out as
11370        // `buffer[0] as string += child`, which does not parse: two of hono's
11371        // files could not be read by TypeScript or by a bundler.
11372        let source = concat!(
11373            "export function join(buffer: [unknown], child: string, n: { v?: number }) {\n",
11374            "  ;(buffer[0] as string) += child\n",
11375            "  ;(buffer[0] as string) = child\n",
11376            "  ;(n.v as number)++\n",
11377            "  ;(n.v satisfies number | undefined) = 1\n",
11378            "  ;[(buffer[0] as string)] = [child]\n",
11379            "  for ((buffer[0] as string) of [child]) {}\n",
11380            "  return buffer\n",
11381            "}\n",
11382        );
11383        let output = instrument_candidate(source, "src/targets.ts").unwrap();
11384        assert!(!output.code.contains(" as string"), "{}", output.code);
11385        assert!(!output.code.contains("satisfies"), "{}", output.code);
11386        assert!(
11387            output.code.contains("buffer[0] += child"),
11388            "{}",
11389            output.code
11390        );
11391        assert!(output.code.contains("n.v++"), "{}", output.code);
11392        let allocator = Allocator::default();
11393        let reparsed = Parser::new(&allocator, &output.code, SourceType::ts()).parse();
11394        assert!(
11395            reparsed.errors.is_empty(),
11396            "{:?}\n{}",
11397            reparsed.errors,
11398            output.code
11399        );
11400    }
11401
11402    #[test]
11403    fn a_comment_the_printer_moves_is_still_recognised() {
11404        // The printer writes a `@__NO_SIDE_EFFECTS__` annotation after the
11405        // documentation comment it stood before. The restore looked only
11406        // forward for each printed comment, did not find the annotation, and
11407        // zod's `v4/core/api.ts` could not be instrumented.
11408        let source = concat!(
11409            "// first\n",
11410            "// @__NO_SIDE_EFFECTS__\n",
11411            "/** documented\n */\n",
11412            "export function f(a: number) {\n",
11413            "  return a > 1 ? a : 0;\n",
11414            "}\n",
11415            "\n",
11416            "// second\n",
11417            "export type T = number;\n",
11418        );
11419        let output = instrument_candidate(source, "src/annotated.ts").unwrap();
11420        for comment in [
11421            "// first",
11422            "// @__NO_SIDE_EFFECTS__",
11423            "/** documented\n */",
11424            "// second",
11425        ] {
11426            assert_eq!(
11427                output.code.matches(comment).count(),
11428                1,
11429                "{comment}\n{}",
11430                output.code
11431            );
11432        }
11433    }
11434
11435    #[test]
11436    fn restored_comments_never_split_a_keyword_from_its_argument() {
11437        // oxc drops the comment that led a parenthesised return argument; the
11438        // restore used to put it back right after `return`, on its own line,
11439        // and automatic semicolon insertion turned the statement into `return;`
11440        // with the JSX unreachable. Vite then removed the unreachable tree and
11441        // every declaration only it used, so a component rendered nothing under
11442        // measurement while passing plainly. Expression-bodied arrows are hit
11443        // too, since instrumentation rewrites them into `return` blocks.
11444        let source = concat!(
11445            "export const A = ({ open }) => {\n",
11446            "  return (\n",
11447            "    // leading comment before JSX\n",
11448            "    <div aria-expanded={open}>a</div>\n",
11449            "  )\n",
11450            "}\n",
11451            "export const B = ({ open }) => (\n",
11452            "  // comment in an expression body\n",
11453            "  <div aria-expanded={open}>b</div>\n",
11454            ")\n",
11455            "export function C(x) {\n",
11456            "  return ( // inline line comment\n",
11457            "    x + 1\n",
11458            "  )\n",
11459            "}\n",
11460            "export function D(x) {\n",
11461            "  throw (\n",
11462            "    /* block\n       comment */\n",
11463            "    new Error(String(x))\n",
11464            "  )\n",
11465            "}\n",
11466            "export function E(x) {\n",
11467            "  return ( /* inline block */ x + 2 )\n",
11468            "}\n",
11469        );
11470        let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
11471        for keyword in ["return ", "throw "] {
11472            for (index, _) in output.code.match_indices(keyword) {
11473                let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
11474                let block_with_line_break = rest.starts_with("/*")
11475                    && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
11476                assert!(
11477                    !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
11478                    "`{keyword}` is separated from its argument:\n{}",
11479                    output.code
11480                );
11481            }
11482        }
11483        for comment in [
11484            "// leading comment before JSX",
11485            "// comment in an expression body",
11486            "// inline line comment",
11487            "/* block\n       comment */",
11488            "/* inline block */",
11489        ] {
11490            assert!(
11491                output.code.contains(comment),
11492                "{comment} was lost:\n{}",
11493                output.code
11494            );
11495        }
11496        assert!(
11497            output
11498                .code
11499                .contains("return <div aria-expanded={open}>a</div>"),
11500            "{}",
11501            output.code
11502        );
11503        assert!(
11504            output
11505                .code
11506                .contains("return <div aria-expanded={open}>b</div>"),
11507            "{}",
11508            output.code
11509        );
11510        // The restored output must still parse to the same program shape.
11511        let allocator = Allocator::default();
11512        let reparsed = Parser::new(
11513            &allocator,
11514            &output.code,
11515            SourceType::from_path("app/components/List.tsx").unwrap(),
11516        )
11517        .parse();
11518        assert!(
11519            reparsed.errors.is_empty(),
11520            "{:?}\n{}",
11521            reparsed.errors,
11522            output.code
11523        );
11524    }
11525
11526    #[test]
11527    fn preserves_reference_order_across_every_control_decision_kind() {
11528        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}";
11529        let output = instrument_candidate(source, "app/control.js").unwrap();
11530        assert_eq!(
11531            output
11532                .decisions
11533                .iter()
11534                .map(|decision| decision.kind.as_str())
11535                .collect::<Vec<_>>(),
11536            vec!["ternary", "while", "do-while", "for", "if"]
11537        );
11538    }
11539
11540    #[test]
11541    fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
11542        // B11. `for (const x of [a, b])` cannot iterate zero times, so the
11543        // outcome was an obligation no test could ever discharge -- the author
11544        // could only fake a test or delete the loop.
11545        let entered = |source: &str| {
11546            let output = analyze_candidate(source, "src/loops.ts").unwrap();
11547            output
11548                .branches
11549                .iter()
11550                .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
11551                .count()
11552        };
11553        // Folded: the iterable is certainly non-empty.
11554        for source in [
11555            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11556            "export function f(g) { for (const x of [1]) g(x); }\n",
11557            "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
11558            // A hole still yields once: [,].length === 1.
11559            "export function f(g) { for (const x of [,]) g(x); }\n",
11560            // A spread may contribute nothing, but a definite element is enough.
11561            "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
11562        ] {
11563            assert_eq!(entered(source), 0, "should be folded: {source}");
11564        }
11565        // Kept: the loop can genuinely run zero times.
11566        for source in [
11567            "export function f(g, ys) { for (const x of ys) g(x); }\n",
11568            "export function f(g) { for (const x of []) g(x); }\n",
11569            "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
11570            "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
11571            // Anything else may reach the binding, including a shrinking call.
11572            "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
11573            "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
11574            "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
11575            // A different binding of the same name must not be borrowed.
11576            concat!(
11577                "const xs = [1, 2];\n",
11578                "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
11579            ),
11580            // for-in is about keys, not elements, and is left alone.
11581            "export function f(g) { for (const k in [1, 2]) g(k); }\n",
11582        ] {
11583            assert_eq!(entered(source), 1, "should be kept: {source}");
11584        }
11585        // The loop body is still measured; only the impossible outcome is gone.
11586        let output = analyze_candidate(
11587            "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11588            "src/loops.ts",
11589        )
11590        .unwrap();
11591        assert!(
11592            output
11593                .points
11594                .iter()
11595                .any(|point| point.source.contains("g(x)")),
11596            "the body must still be an obligation"
11597        );
11598    }
11599
11600    #[test]
11601    fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
11602        let source = concat!(
11603            "export function run(value) {\n",
11604            "  while (true) { break; }\n",
11605            "  do { value += 1; } while (false);\n",
11606            "  if (false) value += 2;\n",
11607            "  if (value || true) value += 3;\n",
11608            "  if (value && false) value += 4;\n",
11609            "  if (value) value += 5;\n",
11610            "  return value;\n",
11611            "}\n",
11612        );
11613        let output = analyze_candidate(source, "src/constants.js").unwrap();
11614        assert_eq!(output.decisions.len(), 1);
11615        assert_eq!(output.decisions[0].source, "value");
11616
11617        let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
11618        assert_eq!(instrumented.decisions.len(), 1);
11619        assert_eq!(instrumented.decisions[0].source, "value");
11620    }
11621
11622    #[test]
11623    fn leaves_compile_time_style_macro_arguments_as_source() {
11624        // StyleX evaluates these at build time and rejects a block-bodied
11625        // dynamic style; probing them broke a real project's Vite build.
11626        let source = concat!(
11627            "import * as stylex from '@stylexjs/stylex';\n",
11628            "const styles = stylex.create({\n",
11629            "  container: { display: 'flex' },\n",
11630            "  paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
11631            "  tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
11632            "});\n",
11633            "export function render(spacing: number) {\n",
11634            "  return stylex.props(styles.container, styles.paddingTop(spacing));\n",
11635            "}\n",
11636        );
11637        let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
11638        assert!(
11639            output.code.contains("=> ({ paddingTop: `${spacing}px` })")
11640                || output
11641                    .code
11642                    .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
11643            "dynamic style arrow must stay an expression body:\n{}",
11644            output.code
11645        );
11646        assert!(
11647            !output
11648                .decisions
11649                .iter()
11650                .any(|decision| decision.source.contains("dark")),
11651            "no decision may be collected inside the macro argument"
11652        );
11653        let function_points = output
11654            .points
11655            .iter()
11656            .filter(|point| point.kind == "function")
11657            .map(|point| point.source.as_str())
11658            .collect::<Vec<_>>();
11659        assert!(
11660            function_points
11661                .iter()
11662                .all(|source| source.contains("render")),
11663            "only the real function may carry a function point: {function_points:?}"
11664        );
11665        assert!(
11666            output.limitations.is_empty(),
11667            "compiled-away code is not a limitation: {:?}",
11668            output.limitations
11669        );
11670    }
11671
11672    #[test]
11673    fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
11674        let source = concat!(
11675            "declare global {\n",
11676            "  var Beacon: undefined | ((action: string) => void);\n",
11677            "}\n",
11678            "declare module 'virtual:feature' {\n",
11679            "  export const enabled: boolean;\n",
11680            "}\n",
11681            "declare const buildOnly: string;\n",
11682            "export const live = 1;\n",
11683        );
11684        let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
11685        let statements = output
11686            .points
11687            .iter()
11688            .filter(|point| point.kind == "statement")
11689            .map(|point| point.source.as_str())
11690            .collect::<Vec<_>>();
11691        assert_eq!(statements, ["const live = 1;"]);
11692        assert!(output.code.contains("const live = 1"));
11693    }
11694
11695    /// The instrumented code with what records a decision's outcome written
11696    /// `END(`, and the decisions' temporaries named `t0`, `t1`, … in the order
11697    /// they are declared.
11698    fn decision_shape(source: &str, file: &str) -> String {
11699        let output = instrument_candidate(source, file).unwrap();
11700        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11701        let call = format!("{}(", runtime.mcdc_end_v2);
11702        let start = output.code.find(&call).expect("an outcome is recorded") + call.len();
11703        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11704        let code = output
11705            .code
11706            .replace(&format!("{call}{probe_file}, "), "END(");
11707        let mut names: Vec<&str> = Vec::new();
11708        let mut shaped = String::new();
11709        let mut rest = code.as_str();
11710        while let Some(at) = rest.find("_supercovMcdc") {
11711            shaped.push_str(&rest[..at]);
11712            let tail = &rest[at..];
11713            // The runtime's own helpers are bound to `__supercovMcdc…`.
11714            if shaped.ends_with('_') {
11715                shaped.push_str("_supercovMcdc");
11716                rest = &tail["_supercovMcdc".len()..];
11717                continue;
11718            }
11719            let length = tail
11720                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
11721                .unwrap_or(tail.len());
11722            let name = &tail[..length];
11723            let index = names
11724                .iter()
11725                .position(|known| *known == name)
11726                .unwrap_or_else(|| {
11727                    names.push(name);
11728                    names.len() - 1
11729                });
11730            shaped.push_str(&format!("t{index}"));
11731            rest = &tail[length..];
11732        }
11733        shaped.push_str(rest);
11734        shaped
11735    }
11736
11737    #[test]
11738    fn a_decision_stays_the_test_it_was() {
11739        // TypeScript narrows through `&&` and `||` only where they are the
11740        // condition itself, through an assignment's right side, and through a
11741        // comma's last operand. The decision used to be the right side of an
11742        // assignment inside a comma, and each condition ended in its
11743        // temporary: `if (client) return client` returned `Client | null`,
11744        // and a file Supercov never touched failed to compile against it.
11745        let shaped = decision_shape(
11746            concat!(
11747                "export function pick(client: { id: number } | null, strict: boolean) {\n",
11748                "  if (client && strict) return client;\n",
11749                "  return null;\n",
11750                "}\n",
11751                "export function size(list: string[] | null) {\n",
11752                "  if (!list) { return 0; } else { return list.length; }\n",
11753                "}\n",
11754                "export function first(list: string[] | null) {\n",
11755                "  return list ? list[0] : undefined;\n",
11756                "}\n",
11757            ),
11758            "src/pick.ts",
11759        );
11760        for expected in [
11761            // The bookkeeping rides inside the first condition, and every
11762            // condition ends in the assignment of its own value.
11763            "if ((t0 = 1, t2 = 0, t1 = client) && (t0 += 3, t2 = strict)) {\n\t\t\tEND(0, t0 + +!!t1 + 3 * +!!t2, true), t0 = 0;",
11764            // Without an `else` there is nowhere inside the statement for the
11765            // false outcome, and an `else` made for it would be a branch to
11766            // a coverage tool the tests run: it is recorded after the `if`,
11767            // when the frame says it has not been.
11768            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1 + 3 * +!!t2, false);\n\t}",
11769            "if (t3 = 1, t4 = !list) {\n\t\tEND(1, t3 + +!!t4, true);",
11770            "} else {\n\t\tEND(1, t3 + +!!t4, false);",
11771            "return (t5 = 1, t6 = list) ? (void END(2, t5 + +!!t6, true), list[0]) : (void END(2, t5 + +!!t6, false), undefined);",
11772        ] {
11773            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11774        }
11775    }
11776
11777    #[test]
11778    fn a_loop_records_its_last_test_after_itself() {
11779        // A loop's true outcome is recorded where its body starts. Nothing
11780        // runs when the test is false, so the statement after the loop
11781        // records that, and only when the test, not a `break`, ended it.
11782        let shaped = decision_shape(
11783            concat!(
11784                "export function scan(limit: number) {\n",
11785                "  let i = 0;\n",
11786                "  walk: while (i < limit) { i++; if (i > 5) break walk; }\n",
11787                "  do i--; while (i > 0);\n",
11788                "  for (let j = 0; j < limit; j++) i += j;\n",
11789                "  return i;\n",
11790                "}\n",
11791            ),
11792            "src/scan.ts",
11793        );
11794        for expected in [
11795            // The label stays on its loop, for `continue walk`.
11796            "{\n\t\twalk: while (t0 = 1, t1 = i < limit) {\n\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11797            "\t\t}\n\t\tt0 && END(0, t0 + +!!t1, false);\n\t}",
11798            // A `do … while` body runs before its test: the frame says whether
11799            // one came before this iteration, and starts cleared.
11800            "{\n\t\tt4 = 0;\n\t\tdo {\n\t\t\tt4 && (END(2, t4 + +!!t5, true), t4 = 0);",
11801            "} while (t4 = 1, t5 = i > 0);\n\t\tt4 && END(2, t4 + +!!t5, false);\n\t}",
11802            "for (let j = 0; t6 = 1, t7 = j < limit; j++) {\n\t\t\tEND(3, t6 + +!!t7, true), t6 = 0;",
11803            "\t\t}\n\t\tt6 && END(3, t6 + +!!t7, false);\n\t}",
11804        ] {
11805            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11806        }
11807    }
11808
11809    #[test]
11810    fn a_decision_without_a_function_body_around_it_declares_its_own_temporaries() {
11811        let shaped = decision_shape(
11812            concat!(
11813                "export function pick(\n",
11814                "  n: number,\n",
11815                "  kind = class { static label = 'none'; static { if (n > 1) this.label = 'some'; } },\n",
11816                "  tag = n > 5 ? 'high' : 'low',\n",
11817                ") {\n",
11818                "  return [kind.label, tag];\n",
11819                "}\n",
11820            ),
11821            "src/defaults.ts",
11822        );
11823        for expected in [
11824            "static {\n\t\tlet t0, t1;\n",
11825            "if (t0 = 1, t1 = n > 1) {\n\t\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11826            // A parameter default has no body to declare in: the arrow holds
11827            // the whole conditional expression, so its branches still see
11828            // what the test narrowed.
11829            "(() => {\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})()",
11830        ] {
11831            assert!(shaped.contains(expected), "{expected}\n{shaped}");
11832        }
11833    }
11834
11835    #[test]
11836    fn a_top_level_declaration_has_a_line_of_its_own_before_it() {
11837        // See `separate_top_level_declarations`. An overload signature stays
11838        // next to its implementation.
11839        let output = instrument_candidate(
11840            concat!(
11841                "const one = 1;\n",
11842                "function plain() { return one; }\n",
11843                "class Box {}\n",
11844                "function over(value: string): string;\n",
11845                "function over(value: number): number;\n",
11846                "function over(value: any) { return value; }\n",
11847                "export function exported() { return plain(); }\n",
11848            ),
11849            "src/declarations.ts",
11850        )
11851        .unwrap();
11852        for expected in [
11853            "\n;\nfunction plain() {",
11854            "\n;\nclass Box {",
11855            "\n;\nfunction over(value: string): string;\nfunction over(value: number): number;\nfunction over(value: any) {",
11856        ] {
11857            assert!(
11858                output.code.contains(expected),
11859                "{expected}\n{}",
11860                output.code
11861            );
11862        }
11863        assert!(
11864            !output.code.contains(";\nexport function"),
11865            "{}",
11866            output.code
11867        );
11868    }
11869
11870    #[test]
11871    fn exposes_the_complete_probe_v2_instrumenter_contract() {
11872        let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
11873        assert!(output.complete);
11874        assert!(output.limitations.is_empty());
11875        assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
11876        let runtime = output.runtime.expect("candidate runtime binding");
11877        assert!(output.code.contains(&runtime.mcdc_end_v2));
11878        assert!(output.code.contains("_supercovMcdcFrame"));
11879        assert!(output.code.contains("_supercovMcdcValue"));
11880        assert_eq!(output.decisions.len(), 1);
11881        assert!(output.points.iter().any(|point| point.kind == "statement"));
11882        assert!(output.points.iter().any(|point| point.kind == "function"));
11883
11884        let allocator = Allocator::default();
11885        let reparsed = Parser::new(
11886            &allocator,
11887            &output.code,
11888            SourceType::from_path("app/decide.ts").unwrap(),
11889        )
11890        .parse();
11891        assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
11892    }
11893
11894    #[test]
11895    fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
11896        let source = concat!(
11897            "import type { Session } from './types.ts';\n",
11898            "import { type Row } from './rows.ts';\n",
11899            "import './register.ts';\n",
11900            "const value = 1;\n",
11901        );
11902        let output = instrument_candidate(source, "app/imports.ts").unwrap();
11903        let statement_lines = output
11904            .points
11905            .iter()
11906            .filter(|point| point.kind == "statement")
11907            .map(|point| point.line)
11908            .collect::<Vec<_>>();
11909        assert_eq!(statement_lines, vec![2, 3, 4]);
11910    }
11911
11912    #[test]
11913    fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
11914        let source = concat!(
11915            "import { Logger } from './types.js';\n",
11916            "import DefaultType from './default.js';\n",
11917            "import * as Types from './namespace.js';\n",
11918            "import { type Inline } from './inline.js';\n",
11919            "import { run, type Config } from './mixed.js';\n",
11920            "import './register.js';\n",
11921            "import {} from './empty.js';\n",
11922            "import { unused } from './unused.js';\n",
11923            "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
11924        );
11925        let preserved =
11926            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", false)
11927                .unwrap();
11928        let erased =
11929            instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true).unwrap();
11930        assert_eq!(
11931            erased
11932                .excluded_statements
11933                .iter()
11934                .map(|p| p.line)
11935                .collect::<Vec<_>>(),
11936            vec![1, 2, 3, 4]
11937        );
11938        let excluded = erased
11939            .excluded_statements
11940            .iter()
11941            .map(|p| &p.id)
11942            .collect::<HashSet<_>>();
11943        let expected = preserved
11944            .points
11945            .iter()
11946            .filter(|p| !excluded.contains(&p.id))
11947            .collect::<Vec<_>>();
11948        assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
11949        for line in [5, 6, 7, 8] {
11950            assert!(erased.points.iter().any(|p| p.line == line));
11951        }
11952        // JavaScript value imports and type queries with runtime uses remain.
11953        let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
11954        let mixed = instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true)
11955            .unwrap();
11956        assert!(mixed.excluded_statements.is_empty());
11957    }
11958
11959    /// A line's first mapping: `None` for no segment, `Some(None)` for an
11960    /// unmapped one, else the source line and column it names.
11961    type FirstMapping = Option<Option<(u32, u32)>>;
11962
11963    /// The instrumented copy's lines with their first mapping.
11964    fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
11965        let map = oxc_sourcemap::SourceMap::from_json_string(
11966            &output
11967                .map
11968                .as_ref()
11969                .expect("instrumented copy has a map")
11970                .to_string(),
11971        )
11972        .unwrap();
11973        let tokens = map.get_tokens().collect::<Vec<_>>();
11974        output
11975            .code
11976            .split('\n')
11977            .enumerate()
11978            .map(|(line, text)| {
11979                let first = tokens
11980                    .iter()
11981                    .find(|token| token.get_dst_line() == line as u32)
11982                    .map(|token| {
11983                        token
11984                            .get_source_id()
11985                            .map(|_| (token.get_src_line(), token.get_src_col()))
11986                    });
11987                (text.trim().to_string(), first)
11988            })
11989            .collect()
11990    }
11991
11992    #[test]
11993    fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
11994        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";
11995        let output = instrument_candidate(source, "app/f.js").unwrap();
11996        let lines = first_mappings(&output);
11997        let runtime = output.runtime.as_ref().unwrap();
11998        for (text, first) in &lines {
11999            // A probe maps to nothing: a coverage tool reading the copy
12000            // through its map counts no branch of the user's for its `||`.
12001            if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
12002                && let Some(first) = first
12003            {
12004                assert_eq!(*first, None, "{text}");
12005            }
12006        }
12007        let at = |needle: &str| {
12008            lines
12009                .iter()
12010                .find(|(text, _)| text.starts_with(needle))
12011                .unwrap_or_else(|| panic!("{needle} in {}", output.code))
12012                .1
12013        };
12014        // Each statement after a block Supercov added maps at its own start,
12015        // where the block's brace used to take that mapping from it.
12016        assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
12017        assert_eq!(at("continue;"), Some(Some((4, 25))));
12018        assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
12019        // The arrow's body, made a block, returns what the expression it was
12020        // evaluated: the line maps where that expression starts.
12021        assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
12022        // Every line of the program starting with a closing brace maps where
12023        // it starts, so a tool that looks a position up on its own line finds
12024        // a mapping.
12025        let program = lines
12026            .iter()
12027            .position(|(text, _)| text.starts_with("function f("))
12028            .unwrap();
12029        for (text, first) in &lines[program..] {
12030            if text.starts_with('}') {
12031                assert!(first.is_some(), "{text} in {}", output.code);
12032            }
12033        }
12034    }
12035
12036    /// The instrumented code with the calls that record a value tree written
12037    /// `END2(`, `END(` and `HIT(`, and the trees' frames named `s0`, `s1`, …
12038    fn value_shape(source: &str, file: &str) -> String {
12039        let output = instrument_candidate(source, file).unwrap();
12040        let runtime = output.runtime.as_ref().expect("candidate runtime binding");
12041        let call = format!("{}(", runtime.select_end2_v2);
12042        let start = output.code.find(&call).expect("a value tree is recorded") + call.len();
12043        let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
12044        let mut code = output.code.clone();
12045        for (helper, short) in [
12046            (&runtime.select_end2_v2, "END2("),
12047            (&runtime.select_end_v2, "END("),
12048            (&runtime.select_next_v2, "NEXT("),
12049            (&runtime.coverage_hit_v2, "HIT("),
12050            (&runtime.select_right_v2, "RIGHT("),
12051            (&runtime.select_short_v2, "SHORT("),
12052        ] {
12053            code = code.replace(&format!("{helper}({probe_file}, "), short);
12054        }
12055        let mut names: Vec<&str> = Vec::new();
12056        let mut shaped = String::new();
12057        let mut rest = code.as_str();
12058        while let Some(at) = rest.find("_supercovSelected") {
12059            shaped.push_str(&rest[..at]);
12060            let tail = &rest[at..];
12061            let length = tail
12062                .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
12063                .unwrap_or(tail.len());
12064            let name = &tail[..length];
12065            let index = names
12066                .iter()
12067                .position(|known| *known == name)
12068                .unwrap_or_else(|| {
12069                    names.push(name);
12070                    names.len() - 1
12071                });
12072            shaped.push_str(&format!("s{index}"));
12073            rest = &tail[length..];
12074        }
12075        shaped.push_str(rest);
12076        shaped
12077    }
12078
12079    /// Every tree of up to `leaves` leaves over the three operators, with
12080    /// its selections' points 10 apart.
12081    fn selection_shapes(
12082        leaves: usize,
12083        next_leaf: &mut usize,
12084        next_first: &mut usize,
12085    ) -> Vec<(SelectionShape, usize, usize)> {
12086        if leaves == 1 {
12087            let shape = SelectionShape::Leaf(*next_leaf);
12088            return vec![(shape, *next_leaf + 1, *next_first)];
12089        }
12090        let mut shapes = Vec::new();
12091        for left_leaves in 1..leaves {
12092            for kind in 0..3 {
12093                let first = *next_first;
12094                let mut after_first = first + 10;
12095                for (left, leaf_after_left, first_after_left) in
12096                    selection_shapes(left_leaves, next_leaf, &mut after_first)
12097                {
12098                    let mut leaf = leaf_after_left;
12099                    let mut first_right = first_after_left;
12100                    for (right, leaf_after, first_after) in
12101                        selection_shapes(leaves - left_leaves, &mut leaf, &mut first_right)
12102                    {
12103                        shapes.push((
12104                            SelectionShape::Selection {
12105                                first,
12106                                kind,
12107                                left: Box::new(copy_shape(&left)),
12108                                right: Box::new(right),
12109                            },
12110                            leaf_after,
12111                            first_after,
12112                        ));
12113                    }
12114                }
12115            }
12116        }
12117        shapes
12118    }
12119
12120    fn copy_shape(shape: &SelectionShape) -> SelectionShape {
12121        match shape {
12122            SelectionShape::Leaf(leaf) => SelectionShape::Leaf(*leaf),
12123            SelectionShape::Selection {
12124                first,
12125                kind,
12126                left,
12127                right,
12128            } => SelectionShape::Selection {
12129                first: *first,
12130                kind: *kind,
12131                left: Box::new(copy_shape(left)),
12132                right: Box::new(copy_shape(right)),
12133            },
12134        }
12135    }
12136
12137    /// A leaf's steps, as `LogicalValueTransformer::shape` builds them.
12138    fn leaf_steps(shape: &SelectionShape, path: &[usize], steps: &mut Vec<Vec<usize>>) {
12139        match shape {
12140            SelectionShape::Leaf(_) => steps.push(path.to_vec()),
12141            SelectionShape::Selection {
12142                first,
12143                kind,
12144                left,
12145                right,
12146            } => {
12147                let mut left_path = vec![first * 4 + kind];
12148                left_path.extend_from_slice(path);
12149                let mut right_path = vec![first * 4 + 3];
12150                right_path.extend_from_slice(path);
12151                leaf_steps(left, &left_path, steps);
12152                leaf_steps(right, &right_path, steps);
12153            }
12154        }
12155    }
12156
12157    /// Evaluate a tree over values that are truthy (2), falsy (1) or nullish
12158    /// (0): its value, and the leaves evaluated, in order.
12159    fn evaluate(shape: &SelectionShape, values: &[u8], evaluated: &mut Vec<usize>) -> u8 {
12160        match shape {
12161            SelectionShape::Leaf(leaf) => {
12162                evaluated.push(*leaf);
12163                values[*leaf]
12164            }
12165            SelectionShape::Selection {
12166                kind, left, right, ..
12167            } => {
12168                let value = evaluate(left, values, evaluated);
12169                let short = match kind {
12170                    0 => value == 2,
12171                    1 => value != 2,
12172                    _ => value != 0,
12173                };
12174                if short {
12175                    value
12176                } else {
12177                    evaluate(right, values, evaluated)
12178                }
12179            }
12180        }
12181    }
12182
12183    /// What the runtime records for a leaf's steps and its value.
12184    fn record_steps(steps: &[usize], value: u8, recorded: &mut Vec<usize>) {
12185        for step in steps {
12186            let (first, code) = (step / 4, step % 4);
12187            let truthy = usize::from(value == 2);
12188            if code == 3 {
12189                recorded.push(first + 2 + truthy);
12190            } else if match code {
12191                0 => value == 2,
12192                1 => value != 2,
12193                _ => value != 0,
12194            } {
12195                recorded.push(first + truthy);
12196            } else {
12197                break;
12198            }
12199        }
12200    }
12201
12202    #[test]
12203    fn a_value_tree_records_what_a_call_around_each_leaf_recorded() {
12204        // The points a leaf decided are written out where evaluation goes
12205        // on from it, from the tree's shape alone. Every tree of up to five
12206        // leaves, under every combination of truthy, falsy and nullish
12207        // values, records the points `selectPathV2` recorded around each
12208        // leaf, in the same order.
12209        let mut checked = 0;
12210        for leaves in 2..=5 {
12211            for (shape, ..) in selection_shapes(leaves, &mut 0, &mut 0) {
12212                let mut steps = Vec::new();
12213                leaf_steps(&shape, &[], &mut steps);
12214                let mut before = vec![Vec::new(); leaves];
12215                shape.before(&mut before);
12216                for combination in 0..3_usize.pow(leaves as u32) {
12217                    let values = (0..leaves)
12218                        .map(|leaf| (combination / 3_usize.pow(leaf as u32) % 3) as u8)
12219                        .collect::<Vec<_>>();
12220                    let mut evaluated = Vec::new();
12221                    evaluate(&shape, &values, &mut evaluated);
12222                    let mut each_leaf = Vec::new();
12223                    for leaf in &evaluated {
12224                        record_steps(&steps[*leaf], values[*leaf], &mut each_leaf);
12225                    }
12226                    let mut tree = Vec::new();
12227                    for pair in evaluated.windows(2) {
12228                        // As `recorded_before` writes it: the only leaf that
12229                        // can come before, or the one the frame names.
12230                        let deciders = &before[pair[1]];
12231                        let (_, points) = match &deciders[..] {
12232                            [only] => only,
12233                            several => several
12234                                .iter()
12235                                .find(|(leaf, _)| *leaf == pair[0])
12236                                .expect("the leaf before is one the table has"),
12237                        };
12238                        tree.extend(points);
12239                    }
12240                    let last = *evaluated.last().unwrap();
12241                    record_steps(&steps[last], values[last], &mut tree);
12242                    assert_eq!(tree, each_leaf, "{values:?} over {steps:?}");
12243                    assert!(
12244                        evaluated
12245                            .windows(2)
12246                            .all(|pair| before[pair[1]].iter().any(|(leaf, _)| *leaf == pair[0])),
12247                        "{values:?} over {steps:?}: {before:?}"
12248                    );
12249                    checked += 1;
12250                }
12251            }
12252        }
12253        assert!(checked > 100_000, "{checked}");
12254    }
12255
12256    #[test]
12257    fn a_constant_keeps_its_tree_and_a_named_chain_its_name() {
12258        let shaped = value_shape(
12259            concat!(
12260                "export function text(value: string | number) {\n",
12261                "  const isText = typeof value === 'string' && value.length > 0;\n",
12262                "  if (isText) return value;\n",
12263                "  return null;\n",
12264                "}\n",
12265                "export const both = text(1) ?? text('a');\n",
12266                "export class Owner {\n",
12267                "  pet?: { name: string };\n",
12268                "  named(other?: Owner) { return [other?.pet, this?.pet, this.pet?.name]; }\n",
12269                "}\n",
12270            ),
12271            "src/alias.ts",
12272        );
12273        for expected in [
12274            // TypeScript narrows through a constant only when the condition
12275            // is its initializer: the value is recorded by the statement
12276            // after, from the constant.
12277            "const isText = (s1 = 0, typeof value === \"string\") && (s1 = 1, value.length > 0);\n\tEND2(9, isText, s1);\n",
12278            "export const both = (s0 = 0, text(1)) ?? (s0 = 1, text(\"a\"));\nEND2(13, both, s0);\n",
12279            // A name and `this` are read again after the call that records
12280            // them, so the chain narrows them.
12281            ", other)?.pet",
12282            ", this)?.pet",
12283        ] {
12284            assert!(shaped.contains(expected), "{expected}\n{shaped}");
12285        }
12286        // A member may run a getter: it is read once, through the call.
12287        assert!(shaped.contains(", this.pet)?.name"), "{shaped}");
12288        assert!(!shaped.contains(", this.pet), this.pet)"), "{shaped}");
12289    }
12290
12291    #[test]
12292    fn a_value_tree_keeps_its_leaves_the_programs_own_expressions() {
12293        // With a call around each leaf, `user.name` was read from a `user`
12294        // nothing had narrowed: `any`, when a member of its type has no
12295        // `name`, for everything that reads the value. A leaf is the last
12296        // operand of a comma now, and the call goes around the tree.
12297        let shaped = value_shape(
12298            concat!(
12299                "export function name(user: { name: string } | false) {\n",
12300                "  return user && user.name;\n",
12301                "}\n",
12302                "export function ok(total: number, name: string) {\n",
12303                "  return (total > 0 && name !== 'x') || !!name;\n",
12304                "}\n",
12305                "export function decided(total: number, name: string, force: boolean) {\n",
12306                "  return (total > 0 && name !== 'x') || force;\n",
12307                "}\n",
12308                "export function pick(a: number | null, b: number | null, c: number) {\n",
12309                "  return a ?? b ?? c;\n",
12310                "}\n",
12311                "export function held(list: string[] | null, fallback = list || []) {\n",
12312                "  return fallback;\n",
12313                "}\n",
12314            ),
12315            "src/values.ts",
12316        );
12317        for expected in [
12318            "let s0: any;\n",
12319            "return END2(12, (s0 = 0, user) && (s0 = 1, user.name), s0);",
12320            // More than two leaves: the tree's own operators stay between
12321            // them, so a coverage tool the tests run reads one branch, and
12322            // what `total > 0` or `name !== 'x'` decided is recorded where
12323            // evaluation went on to `!!name`, by which of them it left:
12324            // the runtime looks that up, because a choice written out here
12325            // would be a branch of this line to that tool. The compound left
12326            // operand is a decision too: its conditions are recorded in the
12327            // leaves and its outcome after the tree, from their values with
12328            // `&`, so the tree stays one tree and narrows as written.
12329            "(s1 = 0, _supercovMcdcFrame27 = 1, _supercovMcdcValue29 = 0, _supercovMcdcValue28 = total > 0) && (s1 = 1, _supercovMcdcFrame27 += 3, _supercovMcdcValue29 = name !== \"x\") || (NEXT(0, s1, 2), s1 = 2, !!name), s1)",
12330            "!!(+!!_supercovMcdcValue28 & +!!_supercovMcdcValue29)), _supercovMcdcResult30;",
12331            "|| (NEXT(1, s2, 2), s2 = 2, force), s2)",
12332            "kind: \"value\"",
12333            // Only `b` can come before `c`: a first `??` that kept `a` ends
12334            // the second.
12335            "return END(2, (s3 = 0, a) ?? (s3 = 1, b) ?? (HIT(34), s3 = 2, c), s3);",
12336            // A parameter list cannot see a frame its function's body
12337            // declares: the leaves keep their calls there.
12338            "SHORT(40, list, 0) || RIGHT(40, [])",
12339        ] {
12340            assert!(shaped.contains(expected), "{expected}\n{shaped}");
12341        }
12342    }
12343
12344    #[test]
12345    fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
12346        let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
12347        for file in ["app/fill.js", "app/fill.ts"] {
12348            let output = instrument_candidate(source, file).unwrap();
12349            let runtime = output.runtime.expect("candidate runtime binding");
12350            // No frame per evaluation: the target is written by the program's
12351            // own operator, once.
12352            assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
12353            assert!(output.code.contains("||= "), "{}", output.code);
12354            assert!(output.code.contains("??= "), "{}", output.code);
12355            assert!(output.code.contains("&&= "), "{}", output.code);
12356            assert_eq!(
12357                output
12358                    .code
12359                    .matches(&format!("{}(", runtime.select_assign_end_v2))
12360                    .count(),
12361                3
12362            );
12363            assert_eq!(
12364                output
12365                    .code
12366                    .matches(&format!("{}(", runtime.select_named_right_v2))
12367                    .count(),
12368                1
12369            );
12370            let allocator = Allocator::default();
12371            let reparsed = Parser::new(
12372                &allocator,
12373                &output.code,
12374                SourceType::from_path(file).unwrap(),
12375            )
12376            .parse();
12377            assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
12378        }
12379    }
12380
12381    #[test]
12382    fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
12383        let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
12384        let output = instrument_candidate(source, "app/collisions.js").unwrap();
12385        let runtime = output.runtime.expect("candidate runtime binding");
12386        assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
12387        assert!(!output.code.contains("let _supercovMcdcFrame1,"));
12388    }
12389
12390    #[test]
12391    fn instruments_wider_decisions_with_the_exact_v1_fallback() {
12392        let predicate = (0..33)
12393            .map(|index| format!("c{index}"))
12394            .collect::<Vec<_>>()
12395            .join(" && ");
12396        let source = format!("if ({predicate}) work();");
12397        let output = instrument_candidate(&source, "app/wide.js").unwrap();
12398        assert_eq!(output.decisions[0].conditions.len(), 33);
12399        let runtime = output.runtime.expect("candidate runtime binding");
12400        assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
12401        assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
12402        assert!(
12403            output
12404                .code
12405                .contains(&format!("{}(", runtime.mcdc_condition))
12406        );
12407        assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
12408    }
12409
12410    /// A server's request, upgrade and connection callbacks run in the
12411    /// request's phase; a spread or a missing callback is left as written.
12412    #[test]
12413    fn server_callbacks_run_in_the_request_phase_however_they_are_passed() {
12414        let source = "import http from 'node:http';\nconst handlers = [() => {}];\nconst server = http.createServer({}, function (req, res) { res.end('a'); });\nserver.on('request', ...handlers);\nserver.on('request');\nserver.on('connection', (socket) => socket.setNoDelay());\nserver.once('upgrade', onUpgrade);\nserver.on('close', () => {});\nfunction onUpgrade() {}\n";
12415        let output = instrument_candidate(source, "src/server.mjs").unwrap();
12416        let runtime = output.runtime.as_ref().expect("runtime binding");
12417        let phase = &runtime.with_request_phase;
12418        let line = |start: &str| {
12419            output
12420                .code
12421                .lines()
12422                .find(|line| line.starts_with(start))
12423                .unwrap_or_else(|| panic!("no {start} in\n{}", output.code))
12424                .to_owned()
12425        };
12426        assert!(line("const server").contains(&format!("{{}}, {phase}(function(req, res)")));
12427        assert_eq!(
12428            line("server.on(\"request\", ..."),
12429            "server.on(\"request\", ...handlers);"
12430        );
12431        assert_eq!(line("server.on(\"request\")"), "server.on(\"request\");");
12432        assert!(line("server.on(\"connection\"").contains(&format!("{phase}((socket)")));
12433        // A connection callback is told it is one.
12434        assert_eq!(
12435            output.code.matches("\"connection\")").count(),
12436            1,
12437            "{}",
12438            output.code
12439        );
12440        assert_eq!(
12441            line("server.once"),
12442            format!("server.once(\"upgrade\", {phase}(onUpgrade));")
12443        );
12444        assert!(!line("server.on(\"close\"").contains(phase.as_str()));
12445    }
12446
12447    #[test]
12448    fn leaves_look_alike_route_exports_alone() {
12449        for (file, source) in [
12450            // Not under `app/`.
12451            (
12452                "lib/route.ts",
12453                "export function GET(request) { return request.url; }",
12454            ),
12455            // Under `app/`, but not a route module or not a method name.
12456            (
12457                "app/api/items/handler.ts",
12458                "export function GET(request) { return request.url; }",
12459            ),
12460            (
12461                "app/route.ts",
12462                "export function helper(request) { return request.url; }",
12463            ),
12464        ] {
12465            let output = instrument_candidate(source, file).unwrap();
12466            assert!(
12467                !output.code.contains("withRequestPhase"),
12468                "{file}: {}",
12469                output.code
12470            );
12471        }
12472    }
12473
12474    #[test]
12475    fn wraps_framework_request_exports_without_changing_the_public_api() {
12476        for (file, source, export_prefix) in [
12477            (
12478                "app/routes/example.ts",
12479                "export const loader = async ({ request }) => request.url;",
12480                "export const loader = ",
12481            ),
12482            (
12483                "app/routes/example.ts",
12484                "export async function action({ request }) { return request.method; }",
12485                "export const action = ",
12486            ),
12487            (
12488                "app/api/items/route.ts",
12489                "export function GET(request) { return Response.json({ url: request.url }); }",
12490                "export const GET = ",
12491            ),
12492            // The root route handler serves `/`.
12493            (
12494                "app/route.ts",
12495                "export function GET(request) { return Response.json({ url: request.url }); }",
12496                "export const GET = ",
12497            ),
12498            (
12499                "src/app/api/items/route.ts",
12500                "export async function POST(request) { return Response.json(await request.json()); }",
12501                "export const POST = ",
12502            ),
12503            (
12504                "app/routes/example.ts",
12505                "export { generateAction as action } from './generateAction';",
12506                "export const action = ",
12507            ),
12508            (
12509                "app/entry.server.tsx",
12510                "export default async function handleRequest(request) { return request.url; }",
12511                "export default ",
12512            ),
12513        ] {
12514            let output = instrument_candidate(source, file).unwrap();
12515            let runtime = output.runtime.as_ref().expect("runtime binding");
12516            assert!(
12517                output
12518                    .code
12519                    .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
12520                "{file}: {}",
12521                output.code
12522            );
12523            assert!(
12524                output.code.contains(&format!(
12525                    "withRequestPhase as {}",
12526                    runtime.with_request_phase
12527                )),
12528                "{file}: {}",
12529                output.code
12530            );
12531            let allocator = Allocator::default();
12532            let reparsed = Parser::new(
12533                &allocator,
12534                &output.code,
12535                SourceType::from_path(file).unwrap(),
12536            )
12537            .parse();
12538            assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
12539        }
12540    }
12541
12542    #[test]
12543    fn parse_failures_are_explicit_and_never_claim_completeness() {
12544        assert!(matches!(
12545            analyze_candidate("if (", "broken.js"),
12546            Err(CandidateError::Parse(errors)) if !errors.is_empty()
12547        ));
12548        assert!(matches!(
12549            analyze_candidate("let value = 1", "unknown.extension"),
12550            Err(CandidateError::UnknownSourceType(_))
12551        ));
12552    }
12553}