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