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