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