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