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