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