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[..components.len().saturating_sub(1)].contains(&"app")
6839 }
6840
6841 fn is_handler_name(&self, name: &str) -> bool {
6842 (self.is_remix_route() && matches!(name, "loader" | "action"))
6843 || (self.is_next_route()
6844 && matches!(
6845 name,
6846 "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
6847 ))
6848 }
6849
6850 fn identifier(&self, name: &str) -> Expression<'a> {
6851 self.ast
6852 .expression_identifier(Span::default(), self.ast.ident(name))
6853 }
6854
6855 fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
6856 self.ast.expression_call(
6857 Span::default(),
6858 self.identifier(&self.with_request_phase),
6859 NONE,
6860 self.ast.vec1(Argument::from(expression)),
6861 false,
6862 )
6863 }
6864
6865 fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
6866 Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
6867 Span::default(),
6868 Some(self.ast.declaration_variable(
6869 Span::default(),
6870 VariableDeclarationKind::Const,
6871 self.ast.vec1(self.ast.variable_declarator(
6872 Span::default(),
6873 VariableDeclarationKind::Const,
6874 self.ast.binding_pattern_binding_identifier(
6875 Span::default(),
6876 self.ast.ident(exported_name),
6877 ),
6878 NONE,
6879 Some(self.wrap_expression(self.identifier(original_name))),
6880 false,
6881 )),
6882 false,
6883 )),
6884 self.ast.vec(),
6885 None,
6886 ImportOrExportKind::Value,
6887 NONE,
6888 ))
6889 }
6890
6891 fn transform_named_export(
6892 &mut self,
6893 mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
6894 output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6895 ) {
6896 if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
6897 for declarator in &mut declaration.declarations {
6898 let Some(name) = binding_identifier_name(&declarator.id) else {
6899 continue;
6900 };
6901 if !self.is_handler_name(&name) {
6902 continue;
6903 }
6904 if let Some(initializer) = &mut declarator.init {
6905 let original = initializer.take_in(self.ast.allocator);
6906 *initializer = self.wrap_expression(original);
6907 self.used = true;
6908 }
6909 }
6910 output.push(Statement::ExportNamedDeclaration(export));
6911 return;
6912 }
6913
6914 if matches!(
6915 export.declaration,
6916 Some(Declaration::FunctionDeclaration(_))
6917 ) {
6918 let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
6919 else {
6920 unreachable!();
6921 };
6922 let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
6923 output.push(Statement::FunctionDeclaration(function));
6924 return;
6925 };
6926 if !self.is_handler_name(&exported_name) {
6927 export.declaration = Some(Declaration::FunctionDeclaration(function));
6928 output.push(Statement::ExportNamedDeclaration(export));
6929 return;
6930 }
6931 let original_name = self
6932 .names
6933 .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6934 function.id = Some(
6935 self.ast
6936 .binding_identifier(Span::default(), self.ast.ident(&original_name)),
6937 );
6938 output.push(Statement::FunctionDeclaration(function));
6939 output.push(self.wrapped_named_export(&exported_name, &original_name));
6940 self.used = true;
6941 return;
6942 }
6943
6944 if export.source.is_none() {
6945 output.push(Statement::ExportNamedDeclaration(export));
6946 return;
6947 }
6948 let source = export
6949 .source
6950 .as_ref()
6951 .expect("checked above")
6952 .clone_in(self.ast.allocator);
6953 let specifiers = export.specifiers.take_in(self.ast.allocator);
6954 let mut untouched = self.ast.vec();
6955 let mut handlers = Vec::new();
6956 for specifier in specifiers {
6957 let exported_name = specifier
6958 .exported
6959 .identifier_name()
6960 .map(|name| name.to_string());
6961 if exported_name
6962 .as_deref()
6963 .is_some_and(|name| self.is_handler_name(name))
6964 {
6965 handlers.push((exported_name.expect("checked above"), specifier));
6966 } else {
6967 untouched.push(specifier);
6968 }
6969 }
6970 if handlers.is_empty() {
6971 export.specifiers = untouched;
6972 output.push(Statement::ExportNamedDeclaration(export));
6973 return;
6974 }
6975 if !untouched.is_empty() {
6976 output.push(Statement::ExportNamedDeclaration(
6977 self.ast.alloc_export_named_declaration(
6978 export.span,
6979 None,
6980 untouched,
6981 Some(source.clone_in(self.ast.allocator)),
6982 export.export_kind,
6983 export.with_clause.take(),
6984 ),
6985 ));
6986 }
6987 for (exported_name, specifier) in handlers {
6988 let original_name = self
6989 .names
6990 .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
6991 output.push(Statement::ImportDeclaration(
6992 self.ast.alloc_import_declaration(
6993 Span::default(),
6994 Some(self.ast.vec1(
6995 self.ast.import_declaration_specifier_import_specifier(
6996 Span::default(),
6997 specifier.local,
6998 self.ast.binding_identifier(
6999 Span::default(),
7000 self.ast.ident(&original_name),
7001 ),
7002 ImportOrExportKind::Value,
7003 ),
7004 )),
7005 source.clone_in(self.ast.allocator),
7006 None,
7007 NONE,
7008 ImportOrExportKind::Value,
7009 ),
7010 ));
7011 output.push(self.wrapped_named_export(&exported_name, &original_name));
7012 }
7013 self.used = true;
7014 }
7015
7016 fn transform_default_export(
7017 &mut self,
7018 mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
7019 output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7020 ) {
7021 if matches!(
7025 export.declaration,
7026 ExportDefaultDeclarationKind::FunctionDeclaration(_)
7027 ) {
7028 let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
7029 export.declaration.take_in(self.ast.allocator)
7030 else {
7031 unreachable!();
7032 };
7033 let original_name = self
7034 .names
7035 .allocate("__supercovHandleRequestCoverageOriginal");
7036 function.id = Some(
7037 self.ast
7038 .binding_identifier(Span::default(), self.ast.ident(&original_name)),
7039 );
7040 output.push(Statement::FunctionDeclaration(function));
7041 output.push(Statement::ExportDefaultDeclaration(
7042 self.ast.alloc_export_default_declaration(
7043 Span::default(),
7044 ExportDefaultDeclarationKind::from(
7045 self.wrap_expression(self.identifier(&original_name)),
7046 ),
7047 ),
7048 ));
7049 self.used = true;
7050 return;
7051 }
7052 if export.declaration.is_expression() {
7053 let original = export
7054 .declaration
7055 .take_in(self.ast.allocator)
7056 .into_expression();
7057 export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
7058 self.used = true;
7059 }
7060 output.push(Statement::ExportDefaultDeclaration(export));
7061 }
7062
7063 fn transform_program(&mut self, program: &mut Program<'a>) {
7064 let route_module = self.is_remix_route() || self.is_next_route();
7065 let server_entry = self.file.starts_with("app/entry.server.")
7066 && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
7067 &Path::new(self.file)
7068 .extension()
7069 .and_then(|value| value.to_str())
7070 .unwrap_or(""),
7071 );
7072 if !route_module && !server_entry {
7073 return;
7074 }
7075 let original = program.body.take_in(self.ast.allocator);
7076 let mut output = self.ast.vec_with_capacity(original.len() + 4);
7077 for statement in original {
7078 match statement {
7079 Statement::ExportNamedDeclaration(export) if route_module => {
7080 self.transform_named_export(export, &mut output);
7081 }
7082 Statement::ExportDefaultDeclaration(export) if server_entry => {
7083 self.transform_default_export(export, &mut output);
7084 }
7085 statement => output.push(statement),
7086 }
7087 }
7088 program.body = output;
7089 }
7090}
7091
7092struct RequestPhaseTransformer<'a> {
7093 ast: AstBuilder<'a>,
7094 with_request_phase: String,
7095 used: bool,
7096 source_sensitive_functions: HashSet<SpanKey>,
7097}
7098
7099impl<'a> RequestPhaseTransformer<'a> {
7100 fn identifier(&self, name: &str) -> Expression<'a> {
7101 self.ast
7102 .expression_identifier(Span::default(), self.ast.ident(name))
7103 }
7104
7105 fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
7106 match callee {
7107 Expression::Identifier(identifier) => identifier.name == name,
7108 Expression::StaticMemberExpression(member) => member.property.name == name,
7109 _ => false,
7110 }
7111 }
7112
7113 fn callback_candidate(argument: &Argument<'a>) -> bool {
7114 matches!(
7115 argument,
7116 Argument::FunctionExpression(_)
7117 | Argument::ArrowFunctionExpression(_)
7118 | Argument::Identifier(_)
7119 | Argument::ComputedMemberExpression(_)
7120 | Argument::StaticMemberExpression(_)
7121 | Argument::PrivateFieldExpression(_)
7122 )
7123 }
7124
7125 fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
7126 matches!(
7127 argument,
7128 Argument::CallExpression(call)
7129 if expression_is_identifier(&call.callee, &self.with_request_phase)
7130 )
7131 }
7132
7133 fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
7134 if self.already_wrapped(argument) || !argument.is_expression() {
7135 return;
7136 }
7137 let expression = argument.to_expression_mut();
7138 let original = expression.take_in(self.ast.allocator);
7139 *expression = self.ast.expression_call(
7140 Span::default(),
7141 self.identifier(&self.with_request_phase),
7142 NONE,
7143 self.ast
7144 .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
7145 |name| {
7146 Argument::from(self.ast.expression_string_literal(
7147 Span::default(),
7148 self.ast.str(name),
7149 None,
7150 ))
7151 },
7152 ))),
7153 false,
7154 );
7155 self.used = true;
7156 }
7157}
7158
7159impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
7160 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7161 if self
7162 .source_sensitive_functions
7163 .contains(&span_key(function.span))
7164 {
7165 return;
7166 }
7167 walk_mut::walk_function(self, function, flags);
7168 }
7169
7170 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7171 if self
7172 .source_sensitive_functions
7173 .contains(&span_key(function.span))
7174 {
7175 return;
7176 }
7177 walk_mut::walk_arrow_function_expression(self, function);
7178 }
7179
7180 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7183
7184 fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
7185 walk_mut::walk_call_expression(self, call);
7186 let property = match &call.callee {
7187 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
7188 _ => None,
7189 };
7190 let mut callback_index = None;
7191 if matches!(property, Some("on" | "once" | "addListener"))
7192 && matches!(
7193 call.arguments.first(),
7194 Some(Argument::StringLiteral(event))
7195 if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
7196 )
7197 {
7198 callback_index = Some(1);
7199 } else if Self::callee_is(&call.callee, "createServer") {
7200 callback_index = call.arguments.iter().rposition(Self::callback_candidate);
7201 }
7202 let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
7203 if let Some(index) = callback_index
7204 && let Some(argument) = call.arguments.get_mut(index)
7205 {
7206 self.wrap_argument(argument, connection.then_some("connection"));
7207 }
7208 }
7209}
7210
7211struct CandidateNames<'s> {
7212 source: &'s str,
7213 underscored: HashSet<&'s str>,
7218 allocated: HashSet<String>,
7219 suffix: usize,
7220 namespace: String,
7223}
7224
7225impl<'s> CandidateNames<'s> {
7226 fn new(source: &'s str) -> Self {
7227 Self::within(source, "")
7228 }
7229
7230 fn within(source: &'s str, namespace: &str) -> Self {
7231 let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
7232 let underscored = source
7233 .split(|c: char| !identifier(c))
7234 .filter(|word| word.starts_with('_'))
7235 .collect();
7236 Self {
7237 source,
7238 underscored,
7239 allocated: HashSet::new(),
7240 suffix: 0,
7241 namespace: namespace.to_owned(),
7242 }
7243 }
7244
7245 fn taken(&self, candidate: &str) -> bool {
7246 self.allocated.contains(candidate)
7247 || if candidate.starts_with('_') {
7248 self.underscored.contains(candidate)
7249 } else {
7250 self.source.contains(candidate)
7251 }
7252 }
7253
7254 fn allocate(&mut self, base: &str) -> String {
7255 loop {
7256 let candidate = if self.suffix == 0 {
7257 format!("{base}{}", self.namespace)
7258 } else {
7259 format!("{base}{}{}", self.suffix, self.namespace)
7260 };
7261 self.suffix += 1;
7262 if !self.taken(&candidate) {
7263 self.allocated.insert(candidate.clone());
7264 return candidate;
7265 }
7266 }
7267 }
7268}
7269
7270struct ControlProbeV2Transformer<'a, 's> {
7271 ast: AstBuilder<'a>,
7272 decisions: &'s [CandidateDecision],
7273 value_decisions: &'s HashMap<SpanKey, SpanKey>,
7276 pending_values: HashMap<SpanKey, Vec<(AppliedDecision, ValueShape)>>,
7279 mcdc_begin: String,
7280 mcdc_condition: String,
7281 mcdc_end: String,
7282 mcdc_end_v2: String,
7283 probe_file_v2: String,
7284 names: CandidateNames<'s>,
7285 scope_declarations: Vec<Vec<String>>,
7286 decision_index: usize,
7287 parameter_depth: usize,
7288 source_sensitive_functions: HashSet<SpanKey>,
7289 after_statement: Option<(Option<Statement<'a>>, Statement<'a>)>,
7293 label_body: bool,
7296 inline_names: Option<Vec<String>>,
7299}
7300
7301#[derive(Clone, Copy)]
7302struct DecisionPlan {
7303 index: usize,
7304 condition_count: usize,
7305 inline_frame: bool,
7306}
7307
7308struct AppliedDecision {
7311 index: usize,
7312 frame: String,
7313 values: Vec<String>,
7314 inline: bool,
7315}
7316
7317impl<'a> ControlProbeV2Transformer<'a, '_> {
7318 fn enter_declaration_scope(&mut self) {
7319 self.scope_declarations.push(Vec::new());
7320 }
7321
7322 fn leave_declaration_scope(&mut self) -> Vec<String> {
7323 self.scope_declarations
7324 .pop()
7325 .expect("program/function scope stack must remain balanced")
7326 }
7327
7328 fn allocate_scratch(&mut self, base: &str) -> String {
7329 let name = self.names.allocate(base);
7330 self.scope_declarations
7331 .last_mut()
7332 .expect("a control decision must be inside a program or function body")
7333 .push(name.clone());
7334 name
7335 }
7336
7337 fn scratch_for(&mut self, base: &str, inline: bool) -> String {
7338 if inline {
7339 self.names.allocate(base)
7340 } else {
7341 self.allocate_scratch(base)
7342 }
7343 }
7344
7345 fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
7346 let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
7347 self.ast.variable_declarator(
7348 Span::default(),
7349 VariableDeclarationKind::Let,
7350 self.ast
7351 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
7352 NONE,
7353 None,
7354 false,
7355 )
7356 }));
7357 let body = self.ast.alloc_function_body(
7358 Span::default(),
7359 self.ast.vec(),
7360 self.ast.vec_from_array([
7361 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7362 Span::default(),
7363 VariableDeclarationKind::Let,
7364 declarations,
7365 false,
7366 )),
7367 self.ast.statement_return(Span::default(), Some(expression)),
7368 ]),
7369 );
7370 let params = self.ast.alloc_formal_parameters(
7371 Span::default(),
7372 FormalParameterKind::ArrowFormalParameters,
7373 self.ast.vec(),
7374 NONE,
7375 );
7376 let arrow = self.ast.expression_arrow_function(
7377 Span::default(),
7378 false,
7379 false,
7380 NONE,
7381 params,
7382 NONE,
7383 body,
7384 );
7385 self.ast
7386 .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
7387 }
7388
7389 fn prepend_declarations(
7390 &self,
7391 names: Vec<String>,
7392 statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
7393 ) {
7394 if names.is_empty() {
7395 return;
7396 }
7397 let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
7398 self.ast.variable_declarator(
7399 Span::default(),
7400 VariableDeclarationKind::Let,
7401 self.ast
7402 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
7403 NONE,
7404 None,
7405 false,
7406 )
7407 }));
7408 statements.insert(
7409 0,
7410 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
7411 Span::default(),
7412 VariableDeclarationKind::Let,
7413 declarators,
7414 false,
7415 )),
7416 );
7417 }
7418
7419 fn identifier(&self, name: &str) -> Expression<'a> {
7420 self.ast
7421 .expression_identifier(Span::default(), self.ast.ident(name))
7422 }
7423
7424 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
7425 let target = self
7426 .ast
7427 .simple_assignment_target_assignment_target_identifier(
7428 Span::default(),
7429 self.ast.ident(name),
7430 );
7431 AssignmentTarget::from(target)
7432 }
7433
7434 fn number(&self, value: u64) -> Expression<'a> {
7435 self.ast.expression_numeric_literal(
7436 Span::default(),
7437 value as f64,
7438 None,
7439 NumberBase::Decimal,
7440 )
7441 }
7442
7443 fn string(&self, value: &str) -> Expression<'a> {
7444 self.ast
7445 .expression_string_literal(Span::default(), self.ast.str(value), None)
7446 }
7447
7448 fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
7449 self.ast.object_property_kind_object_property(
7450 Span::default(),
7451 PropertyKind::Init,
7452 self.ast
7453 .property_key_static_identifier(Span::default(), self.ast.ident(name)),
7454 value,
7455 false,
7456 false,
7457 false,
7458 )
7459 }
7460
7461 fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
7462 let conditions = self.ast.expression_array(
7463 Span::default(),
7464 self.ast.vec_from_iter(
7465 decision
7466 .conditions
7467 .iter()
7468 .map(|condition| ArrayExpressionElement::from(self.string(condition))),
7469 ),
7470 );
7471 let properties = self.ast.vec_from_array([
7472 self.object_property("id", self.string(&decision.id)),
7473 self.object_property("file", self.string(&decision.file)),
7474 self.object_property("line", self.number(decision.line as u64)),
7475 self.object_property("column", self.number(decision.column as u64)),
7476 self.object_property("source", self.string(&decision.source)),
7477 self.object_property("conditions", conditions),
7478 self.object_property("kind", self.string(&decision.kind)),
7479 ]);
7480 Expression::ObjectExpression(
7481 self.ast
7482 .alloc_object_expression(Span::default(), properties),
7483 )
7484 }
7485
7486 fn assign(&self, name: &str, value: Expression<'a>) -> Expression<'a> {
7487 self.ast.expression_assignment(
7488 Span::default(),
7489 AssignmentOperator::Assign,
7490 self.assignment_target(name),
7491 value,
7492 )
7493 }
7494
7495 fn instrument_condition(
7510 &self,
7511 expression: Expression<'a>,
7512 applied: &AppliedDecision,
7513 index: usize,
7514 ) -> Expression<'a> {
7515 let mut steps = self.ast.vec();
7516 if index == 0 {
7517 steps.push(self.assign(&applied.frame, self.number(1)));
7518 for name in &applied.values[1..] {
7519 steps.push(self.assign(name, self.number(0)));
7520 }
7521 } else {
7522 steps.push(self.ast.expression_assignment(
7523 Span::default(),
7524 AssignmentOperator::Addition,
7525 self.assignment_target(&applied.frame),
7526 self.number(3_u64.pow(index as u32)),
7527 ));
7528 }
7529 steps.push(self.assign(&applied.values[index], expression));
7530 self.ast.expression_sequence(Span::default(), steps)
7531 }
7532
7533 fn instrument_conditions(
7534 &self,
7535 expression: &mut Expression<'a>,
7536 applied: &AppliedDecision,
7537 next_index: &mut usize,
7538 ) {
7539 match expression {
7540 Expression::ParenthesizedExpression(parenthesized) => {
7541 self.instrument_conditions(&mut parenthesized.expression, applied, next_index)
7542 }
7543 Expression::LogicalExpression(logical)
7544 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7545 {
7546 self.instrument_conditions(&mut logical.left, applied, next_index);
7547 self.instrument_conditions(&mut logical.right, applied, next_index);
7548 }
7549 Expression::UnaryExpression(unary)
7550 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7551 {
7552 self.instrument_conditions(&mut unary.argument, applied, next_index);
7553 }
7554 _ => {
7555 let index = *next_index;
7556 *next_index += 1;
7557 let original = expression.take_in(self.ast.allocator);
7558 *expression = self.instrument_condition(original, applied, index);
7559 }
7560 }
7561 }
7562
7563 fn encoded(&self, applied: &AppliedDecision) -> Expression<'a> {
7568 let mut sum = self.identifier(&applied.frame);
7569 for (index, name) in applied.values.iter().enumerate() {
7570 let truth = self.ast.expression_unary(
7571 Span::default(),
7572 UnaryOperator::UnaryPlus,
7573 self.ast.expression_unary(
7574 Span::default(),
7575 UnaryOperator::LogicalNot,
7576 self.ast.expression_unary(
7577 Span::default(),
7578 UnaryOperator::LogicalNot,
7579 self.identifier(name),
7580 ),
7581 ),
7582 );
7583 let term = if index == 0 {
7584 truth
7585 } else {
7586 self.ast.expression_binary(
7587 Span::default(),
7588 self.number(3_u64.pow(index as u32)),
7589 BinaryOperator::Multiplication,
7590 truth,
7591 )
7592 };
7593 sum = self
7594 .ast
7595 .expression_binary(Span::default(), sum, BinaryOperator::Addition, term);
7596 }
7597 sum
7598 }
7599
7600 fn record_value_outcomes(
7611 &mut self,
7612 expression: &mut Expression<'a>,
7613 pending: Vec<(AppliedDecision, ValueShape)>,
7614 ) {
7615 let inline = pending.iter().any(|(applied, _)| applied.inline);
7616 let result = self.scratch_for("_supercovMcdcResult", inline);
7617 let mut steps = self.ast.vec();
7618 for (applied, _) in &pending {
7619 steps.push(self.assign(&applied.frame, self.number(0)));
7620 for name in &applied.values {
7621 steps.push(self.assign(name, self.number(0)));
7622 }
7623 }
7624 let value = expression.take_in(self.ast.allocator);
7625 steps.push(self.assign(&result, value));
7626 let mut names = Vec::new();
7627 for (applied, shape) in &pending {
7628 let truth = self.ast.expression_unary(
7629 Span::default(),
7630 UnaryOperator::LogicalNot,
7631 self.ast.expression_unary(
7632 Span::default(),
7633 UnaryOperator::LogicalNot,
7634 self.shape_bits(shape, applied),
7635 ),
7636 );
7637 steps.push(self.ast.expression_call(
7638 Span::default(),
7639 self.identifier(&self.mcdc_end_v2),
7640 NONE,
7641 self.ast.vec_from_array([
7642 Argument::from(self.identifier(&self.probe_file_v2)),
7643 Argument::from(self.number(applied.index as u64)),
7644 Argument::from(self.encoded(applied)),
7645 Argument::from(truth),
7646 ]),
7647 false,
7648 ));
7649 if applied.inline {
7650 names.push(applied.frame.clone());
7651 names.extend(applied.values.iter().cloned());
7652 }
7653 }
7654 steps.push(self.identifier(&result));
7655 let observed = self.ast.expression_parenthesized(
7656 Span::default(),
7657 self.ast.expression_sequence(Span::default(), steps),
7658 );
7659 *expression = if inline {
7660 names.push(result);
7661 self.wrap_inline_frame(observed, &names)
7662 } else {
7663 observed
7664 };
7665 }
7666
7667 fn shape_bits(&self, shape: &ValueShape, applied: &AppliedDecision) -> Expression<'a> {
7670 let binary = |left, operator, right| {
7671 self.ast.expression_parenthesized(
7672 Span::default(),
7673 self.ast
7674 .expression_binary(Span::default(), left, operator, right),
7675 )
7676 };
7677 match shape {
7678 ValueShape::Condition(index) => self.ast.expression_unary(
7679 Span::default(),
7680 UnaryOperator::UnaryPlus,
7681 self.ast.expression_unary(
7682 Span::default(),
7683 UnaryOperator::LogicalNot,
7684 self.ast.expression_unary(
7685 Span::default(),
7686 UnaryOperator::LogicalNot,
7687 self.identifier(&applied.values[*index]),
7688 ),
7689 ),
7690 ),
7691 ValueShape::And(left, right) => binary(
7692 self.shape_bits(left, applied),
7693 BinaryOperator::BitwiseAnd,
7694 self.shape_bits(right, applied),
7695 ),
7696 ValueShape::Or(left, right) => binary(
7697 self.shape_bits(left, applied),
7698 BinaryOperator::BitwiseOR,
7699 self.shape_bits(right, applied),
7700 ),
7701 ValueShape::Not(inner) => binary(
7702 self.number(1),
7703 BinaryOperator::BitwiseXOR,
7704 self.shape_bits(inner, applied),
7705 ),
7706 }
7707 }
7708
7709 fn outcome(&self, applied: &AppliedDecision, outcome: bool) -> Expression<'a> {
7710 self.ast.expression_call(
7711 Span::default(),
7712 self.identifier(&self.mcdc_end_v2),
7713 NONE,
7714 self.ast.vec_from_array([
7715 Argument::from(self.identifier(&self.probe_file_v2)),
7716 Argument::from(self.number(applied.index as u64)),
7717 Argument::from(self.encoded(applied)),
7718 Argument::from(
7719 self.ast
7720 .expression_boolean_literal(Span::default(), outcome),
7721 ),
7722 ]),
7723 false,
7724 )
7725 }
7726
7727 fn prepend_statement(&self, body: &mut Statement<'a>, first: Statement<'a>) {
7730 if let Statement::BlockStatement(block) = body {
7731 block.body.insert(0, first);
7732 return;
7733 }
7734 let original = body.take_in(self.ast.allocator);
7735 *body = self
7736 .ast
7737 .statement_block(Span::default(), self.ast.vec_from_array([first, original]));
7738 }
7739
7740 fn settled(&self, applied: &AppliedDecision) -> Expression<'a> {
7744 self.ast.expression_sequence(
7745 Span::default(),
7746 self.ast.vec_from_array([
7747 self.outcome(applied, true),
7748 self.assign(&applied.frame, self.number(0)),
7749 ]),
7750 )
7751 }
7752
7753 fn when_unsettled(&self, applied: &AppliedDecision, record: Expression<'a>) -> Statement<'a> {
7754 self.ast.statement_expression(
7755 Span::default(),
7756 self.ast.expression_logical(
7757 Span::default(),
7758 self.identifier(&applied.frame),
7759 LogicalOperator::And,
7760 record,
7761 ),
7762 )
7763 }
7764
7765 fn record_false_after(&mut self, applied: &AppliedDecision, before: Option<Statement<'a>>) {
7773 let after = self.when_unsettled(applied, self.outcome(applied, false));
7774 self.after_statement = Some((before, after));
7775 }
7776
7777 fn loop_outcomes(
7779 &mut self,
7780 body: &mut Statement<'a>,
7781 applied: &AppliedDecision,
7782 tested_after: bool,
7783 ) {
7784 if !tested_after {
7785 let entered = self
7786 .ast
7787 .statement_expression(Span::default(), self.settled(applied));
7788 self.prepend_statement(body, entered);
7789 self.record_false_after(applied, None);
7790 return;
7791 }
7792 let entered = self.when_unsettled(applied, self.settled(applied));
7796 self.prepend_statement(body, entered);
7797 let before = self
7798 .ast
7799 .statement_expression(Span::default(), self.assign(&applied.frame, self.number(0)));
7800 self.record_false_after(applied, Some(before));
7801 }
7802
7803 fn instrument_condition_v1(
7804 &self,
7805 expression: Expression<'a>,
7806 frame_name: &str,
7807 index: usize,
7808 ) -> Expression<'a> {
7809 self.ast.expression_call(
7810 Span::default(),
7811 self.identifier(&self.mcdc_condition),
7812 NONE,
7813 self.ast.vec_from_array([
7814 Argument::from(self.identifier(frame_name)),
7815 Argument::from(self.number(index as u64)),
7816 Argument::from(expression),
7817 ]),
7818 false,
7819 )
7820 }
7821
7822 fn instrument_conditions_v1(
7823 &self,
7824 expression: &mut Expression<'a>,
7825 frame_name: &str,
7826 next_index: &mut usize,
7827 ) {
7828 match expression {
7829 Expression::ParenthesizedExpression(parenthesized) => {
7830 self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
7831 }
7832 Expression::LogicalExpression(logical)
7833 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7834 {
7835 self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
7836 self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
7837 }
7838 Expression::UnaryExpression(unary)
7839 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7840 {
7841 self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
7842 }
7843 _ => {
7844 let index = *next_index;
7845 *next_index += 1;
7846 let original = expression.take_in(self.ast.allocator);
7847 *expression = self.instrument_condition_v1(original, frame_name, index);
7848 }
7849 }
7850 }
7851
7852 fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
7853 let mut condition_spans = Vec::new();
7854 collect_conditions(test, &mut condition_spans);
7855 let plan = DecisionPlan {
7856 index: self.decision_index,
7857 condition_count: condition_spans.len(),
7858 inline_frame: self.parameter_depth > 0,
7859 };
7860 self.decision_index += 1;
7861 plan
7862 }
7863
7864 fn apply_decision(
7868 &mut self,
7869 test: &mut Expression<'a>,
7870 plan: DecisionPlan,
7871 ) -> Option<AppliedDecision> {
7872 if plan.condition_count > 32 {
7873 let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
7874 let mut next_index = 0;
7875 self.instrument_conditions_v1(test, &frame_name, &mut next_index);
7876 debug_assert_eq!(next_index, plan.condition_count);
7877
7878 let decision = &self.decisions[plan.index];
7879 let begin = self.ast.expression_call(
7880 Span::default(),
7881 self.identifier(&self.mcdc_begin),
7882 NONE,
7883 self.ast.vec_from_array([
7884 Argument::from(self.string(&decision.id)),
7885 Argument::from(self.decision_meta(decision)),
7886 ]),
7887 false,
7888 );
7889 let assign_frame = self.ast.expression_assignment(
7890 Span::default(),
7891 AssignmentOperator::Assign,
7892 self.assignment_target(&frame_name),
7893 begin,
7894 );
7895 let instrumented = test.take_in(self.ast.allocator);
7896 let end = self.ast.expression_call(
7897 Span::default(),
7898 self.identifier(&self.mcdc_end),
7899 NONE,
7900 self.ast.vec_from_array([
7901 Argument::from(self.identifier(&frame_name)),
7902 Argument::from(instrumented),
7903 ]),
7904 false,
7905 );
7906 let observed = self.ast.expression_sequence(
7907 Span::default(),
7908 self.ast.vec_from_array([assign_frame, end]),
7909 );
7910 *test = if plan.inline_frame {
7911 self.wrap_inline_frame(observed, &[frame_name])
7912 } else {
7913 observed
7914 };
7915 return None;
7916 }
7917
7918 let applied = AppliedDecision {
7919 index: plan.index,
7920 frame: self.scratch_for("_supercovMcdcFrame", plan.inline_frame),
7921 values: (0..plan.condition_count)
7922 .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
7923 .collect(),
7924 inline: plan.inline_frame,
7925 };
7926 let mut next_index = 0;
7927 self.instrument_conditions(test, &applied, &mut next_index);
7928 debug_assert_eq!(
7929 next_index,
7930 plan.condition_count,
7931 "decision at {:?}",
7932 test.span()
7933 );
7934 Some(applied)
7935 }
7936}
7937
7938impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
7939 fn visit_program(&mut self, program: &mut Program<'a>) {
7940 self.enter_declaration_scope();
7941 walk_mut::walk_program(self, program);
7942 let declarations = self.leave_declaration_scope();
7943 self.prepend_declarations(declarations, &mut program.body);
7944 }
7945
7946 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
7947 self.enter_declaration_scope();
7948 walk_mut::walk_function_body(self, body);
7949 let declarations = self.leave_declaration_scope();
7950 self.prepend_declarations(declarations, &mut body.statements);
7951 }
7952
7953 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
7954 if self
7955 .source_sensitive_functions
7956 .contains(&span_key(function.span))
7957 {
7958 return;
7959 }
7960 let outer_parameter_depth = self.parameter_depth;
7961 self.parameter_depth = 0;
7962 walk_mut::walk_function(self, function, flags);
7963 self.parameter_depth = outer_parameter_depth;
7964 }
7965
7966 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
7967 if self
7968 .source_sensitive_functions
7969 .contains(&span_key(function.span))
7970 {
7971 return;
7972 }
7973 let outer_parameter_depth = self.parameter_depth;
7974 self.parameter_depth = 0;
7975 walk_mut::walk_arrow_function_expression(self, function);
7976 self.parameter_depth = outer_parameter_depth;
7977 }
7978
7979 fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
7980 self.parameter_depth += 1;
7981 walk_mut::walk_formal_parameters(self, parameters);
7982 self.parameter_depth -= 1;
7983 }
7984
7985 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
7988
7989 fn visit_statement(&mut self, statement: &mut Statement<'a>) {
7994 let label_body = self.label_body;
7995 self.label_body = matches!(statement, Statement::LabeledStatement(_));
7996 walk_mut::walk_statement(self, statement);
7997 self.label_body = false;
7998 if label_body {
7999 return;
8000 }
8001 if let Some((before, after)) = self.after_statement.take() {
8002 let decided = statement.take_in(self.ast.allocator);
8003 let mut recorded = self.ast.vec_with_capacity(3);
8004 recorded.extend(before);
8005 recorded.push(decided);
8006 recorded.push(after);
8007 *statement = self.ast.statement_block(Span::default(), recorded);
8008 }
8009 }
8010
8011 fn visit_static_block(&mut self, block: &mut oxc_ast::ast::StaticBlock<'a>) {
8015 let outer_parameter_depth = self.parameter_depth;
8016 self.parameter_depth = 0;
8017 self.enter_declaration_scope();
8018 walk_mut::walk_static_block(self, block);
8019 let declarations = self.leave_declaration_scope();
8020 self.prepend_declarations(declarations, &mut block.body);
8021 self.parameter_depth = outer_parameter_depth;
8022 }
8023
8024 fn visit_expression(&mut self, expression: &mut Expression<'a>) {
8025 let key = span_key(expression.span());
8026 let value = self.value_decisions.get(&key).copied().map(|root| {
8027 let mut next = 0;
8028 (
8029 root,
8030 self.reserve_decision(expression),
8031 value_shape(expression, &mut next),
8032 )
8033 });
8034 let is_root = self.value_decisions.values().any(|root| *root == key);
8035 walk_mut::walk_expression(self, expression);
8036 if let Some(names) = self.inline_names.take()
8037 && matches!(expression, Expression::ConditionalExpression(_))
8038 {
8039 let conditional = expression.take_in(self.ast.allocator);
8040 *expression = self.wrap_inline_frame(conditional, &names);
8041 }
8042 if let Some((root, plan, shape)) = value
8043 && let Some(applied) = self.apply_decision(expression, plan)
8044 {
8045 self.pending_values
8046 .entry(root)
8047 .or_default()
8048 .push((applied, shape));
8049 }
8050 if is_root && let Some(pending) = self.pending_values.remove(&key) {
8051 self.record_value_outcomes(expression, pending);
8052 }
8053 }
8054
8055 fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
8056 let plan = decision_outcome_is_variable(&statement.test)
8057 .then(|| self.reserve_decision(&statement.test));
8058 self.visit_expression(&mut statement.test);
8059 let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8060 self.visit_statement(&mut statement.consequent);
8061 if let Some(alternate) = &mut statement.alternate {
8062 self.visit_statement(alternate);
8063 }
8064 let Some(applied) = applied else {
8065 return;
8066 };
8067 let Some(alternate) = &mut statement.alternate else {
8068 let taken = self
8069 .ast
8070 .statement_expression(Span::default(), self.settled(&applied));
8071 self.prepend_statement(&mut statement.consequent, taken);
8072 self.record_false_after(&applied, None);
8073 return;
8074 };
8075 let skipped = self
8076 .ast
8077 .statement_expression(Span::default(), self.outcome(&applied, false));
8078 self.prepend_statement(alternate, skipped);
8079 let taken = self
8080 .ast
8081 .statement_expression(Span::default(), self.outcome(&applied, true));
8082 self.prepend_statement(&mut statement.consequent, taken);
8083 }
8084
8085 fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
8086 let plan = decision_outcome_is_variable(&expression.test)
8087 .then(|| self.reserve_decision(&expression.test));
8088 self.visit_expression(&mut expression.test);
8089 let applied = plan.and_then(|plan| self.apply_decision(&mut expression.test, plan));
8090 self.visit_expression(&mut expression.consequent);
8091 self.visit_expression(&mut expression.alternate);
8092 let Some(applied) = applied else {
8093 return;
8094 };
8095 for (branch, outcome) in [
8100 (&mut expression.consequent, true),
8101 (&mut expression.alternate, false),
8102 ] {
8103 let value = branch.take_in(self.ast.allocator);
8104 let start = value.span().start;
8105 let record = self.ast.expression_unary(
8106 Span::new(start, start + 1),
8107 UnaryOperator::Void,
8108 self.outcome(&applied, outcome),
8109 );
8110 *branch = self
8111 .ast
8112 .expression_sequence(Span::default(), self.ast.vec_from_array([record, value]));
8113 }
8114 if applied.inline {
8115 let mut names = vec![applied.frame];
8116 names.extend(applied.values);
8117 self.inline_names = Some(names);
8118 }
8119 }
8120
8121 fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
8122 let plan = decision_outcome_is_variable(&statement.test)
8123 .then(|| self.reserve_decision(&statement.test));
8124 self.visit_expression(&mut statement.test);
8125 let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8126 self.visit_statement(&mut statement.body);
8127 if let Some(applied) = applied {
8128 self.loop_outcomes(&mut statement.body, &applied, false);
8129 }
8130 }
8131
8132 fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
8133 let plan = decision_outcome_is_variable(&statement.test)
8134 .then(|| self.reserve_decision(&statement.test));
8135 self.visit_statement(&mut statement.body);
8136 self.visit_expression(&mut statement.test);
8137 let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
8138 if let Some(applied) = applied {
8139 self.loop_outcomes(&mut statement.body, &applied, true);
8140 }
8141 }
8142
8143 fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
8144 let plan = statement
8145 .test
8146 .as_ref()
8147 .filter(|test| decision_outcome_is_variable(test))
8148 .map(|test| self.reserve_decision(test));
8149 if let Some(init) = &mut statement.init {
8150 self.visit_for_statement_init(init);
8151 }
8152 let mut applied = None;
8153 if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
8154 self.visit_expression(test);
8155 applied = self.apply_decision(test, plan);
8156 }
8157 if let Some(update) = &mut statement.update {
8158 self.visit_expression(update);
8159 }
8160 self.visit_statement(&mut statement.body);
8161 if let Some(applied) = applied {
8162 self.loop_outcomes(&mut statement.body, &applied, false);
8163 }
8164 }
8165}
8166
8167struct DecisionCollector<'s> {
8168 source: &'s str,
8169 file: &'s str,
8170 decisions: Vec<CandidateDecision>,
8171 decision_vector_counts: Vec<usize>,
8172 decision_logical_nodes: HashSet<SpanKey>,
8173 source_sensitive_functions: &'s HashSet<SpanKey>,
8174 value_decisions: HashMap<SpanKey, SpanKey>,
8179 logical_root: Option<SpanKey>,
8181 value_logical_nodes: HashSet<SpanKey>,
8185}
8186
8187impl DecisionCollector<'_> {
8188 fn in_a_decision(&self, span: Span) -> bool {
8189 let key = span_key(span);
8190 self.decision_logical_nodes.contains(&key) || self.value_logical_nodes.contains(&key)
8191 }
8192
8193 fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
8194 let mut condition_spans = Vec::new();
8195 collect_conditions(test, &mut condition_spans);
8196 if kind == "value" {
8197 collect_decision_logical_nodes(test, &mut self.value_logical_nodes);
8198 } else {
8199 collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
8200 }
8201 let span = transparent_expression(test).span();
8205 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8206 self.decisions.push(CandidateDecision {
8207 id: stable_id(self.source, self.file, "decision", span, kind),
8208 file: self.file.to_string(),
8209 line,
8210 column,
8211 source: source_slice(self.source, span).to_string(),
8212 conditions: condition_spans
8213 .iter()
8214 .map(|condition| source_slice(self.source, *condition).to_string())
8215 .collect(),
8216 kind: kind.to_string(),
8217 });
8218 self.decision_vector_counts.push(
8219 if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
8220 reachable_vector_count(test, &condition_spans)
8221 } else {
8222 0
8223 },
8224 );
8225 }
8226}
8227
8228fn compound_root<'e, 'a>(expression: &'e Expression<'a>) -> Option<&'e LogicalExpression<'a>> {
8231 match expression {
8232 Expression::ParenthesizedExpression(parenthesized) => {
8233 compound_root(&parenthesized.expression)
8234 }
8235 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8236 compound_root(&unary.argument)
8237 }
8238 Expression::LogicalExpression(logical)
8239 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8240 {
8241 Some(logical)
8242 }
8243 _ => None,
8244 }
8245}
8246
8247fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
8251 syntactic_boolean_outcome(expression).is_none()
8252}
8253
8254fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
8255 match transparent_expression(expression) {
8256 Expression::BooleanLiteral(literal) => Some(literal.value),
8257 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8258 syntactic_boolean_outcome(&unary.argument).map(|value| !value)
8259 }
8260 Expression::LogicalExpression(logical)
8261 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8262 {
8263 let left = syntactic_boolean_outcome(&logical.left);
8264 let right = syntactic_boolean_outcome(&logical.right);
8265 match logical.operator {
8266 LogicalOperator::And => match (left, right) {
8267 (Some(false), _) | (_, Some(false)) => Some(false),
8268 (Some(true), value) | (value, Some(true)) => value,
8269 _ => None,
8270 },
8271 LogicalOperator::Or => match (left, right) {
8272 (Some(true), _) | (_, Some(true)) => Some(true),
8273 (Some(false), value) | (value, Some(false)) => value,
8274 _ => None,
8275 },
8276 LogicalOperator::Coalesce => None,
8277 }
8278 }
8279 _ => None,
8280 }
8281}
8282
8283#[derive(Default)]
8284struct CoverageSurfaceScanner {
8285 found: bool,
8286}
8287
8288impl<'a> Visit<'a> for CoverageSurfaceScanner {
8289 fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
8290 self.found = true;
8291 }
8292
8293 fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
8294 self.found = true;
8295 }
8296
8297 fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
8298 self.found = true;
8299 }
8300
8301 fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
8302 self.found = true;
8303 }
8304
8305 fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
8306 self.found = true;
8307 }
8308
8309 fn visit_class(&mut self, _class: &Class<'a>) {
8310 self.found = true;
8311 }
8312
8313 fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
8314 if expression.operator.is_logical() {
8315 self.found = true;
8316 } else {
8317 walk::walk_assignment_expression(self, expression);
8318 }
8319 }
8320}
8321
8322fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
8323 fn visit_condition(expression: &Expression<'_>) -> bool {
8324 let mut scanner = CoverageSurfaceScanner::default();
8325 scanner.visit_expression(expression);
8326 !scanner.found
8327 }
8328 match transparent_expression(expression) {
8329 Expression::LogicalExpression(logical)
8330 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8331 {
8332 decision_conditions_are_transparent(&logical.left)
8333 && decision_conditions_are_transparent(&logical.right)
8334 }
8335 Expression::UnaryExpression(unary)
8336 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8337 {
8338 decision_conditions_are_transparent(&unary.argument)
8339 }
8340 condition => visit_condition(condition),
8341 }
8342}
8343
8344fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
8345 fn evaluate(
8346 expression: &Expression<'_>,
8347 assignment: usize,
8348 encoded: &mut usize,
8349 indices: &HashMap<SpanKey, usize>,
8350 ) -> bool {
8351 match transparent_expression(expression) {
8352 Expression::LogicalExpression(logical)
8353 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8354 {
8355 let left = evaluate(&logical.left, assignment, encoded, indices);
8356 if logical.operator == LogicalOperator::And {
8357 left && evaluate(&logical.right, assignment, encoded, indices)
8358 } else {
8359 left || evaluate(&logical.right, assignment, encoded, indices)
8360 }
8361 }
8362 Expression::UnaryExpression(unary)
8363 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8364 {
8365 !evaluate(&unary.argument, assignment, encoded, indices)
8366 }
8367 condition => {
8368 let index = indices
8369 .get(&span_key(condition.span()))
8370 .expect("decision condition index must remain stable");
8371 let value = assignment & (1 << index) != 0;
8372 *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
8373 value
8374 }
8375 }
8376 }
8377
8378 let indices = conditions
8379 .iter()
8380 .enumerate()
8381 .map(|(index, span)| (span_key(*span), index))
8382 .collect::<HashMap<_, _>>();
8383 let mut vectors = HashSet::new();
8384 for assignment in 0..(1_usize << conditions.len()) {
8385 let mut encoded = 0;
8386 let outcome = evaluate(expression, assignment, &mut encoded, &indices);
8387 vectors.insert(encoded * 2 + usize::from(outcome));
8388 }
8389 vectors.len()
8390}
8391
8392fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
8393 match expression {
8394 Expression::ParenthesizedExpression(parenthesized) => {
8395 collect_decision_logical_nodes(&parenthesized.expression, nodes);
8396 }
8397 Expression::LogicalExpression(logical)
8398 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8399 {
8400 nodes.insert(span_key(logical.span));
8401 collect_decision_logical_nodes(&logical.left, nodes);
8402 collect_decision_logical_nodes(&logical.right, nodes);
8403 }
8404 Expression::UnaryExpression(unary)
8405 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8406 {
8407 collect_decision_logical_nodes(&unary.argument, nodes);
8408 }
8409 _ => {}
8410 }
8411}
8412
8413#[derive(Default)]
8414struct LogicalBranchAnalysis {
8415 branches: Vec<CandidateBranch>,
8416 logical_targets: HashMap<SpanKey, (String, String)>,
8417}
8418
8419#[derive(Default)]
8420struct LogicalAssignmentAnalysis {
8421 branches: Vec<CandidateBranch>,
8422 targets: HashMap<SpanKey, (String, String)>,
8423}
8424
8425#[derive(Default)]
8426struct OptionalMemberAnalysis {
8427 branches: Vec<CandidateBranch>,
8428 targets: HashMap<SpanKey, (String, String)>,
8429 links: HashMap<SpanKey, u8>,
8431 after_call: HashMap<SpanKey, SpanKey>,
8434}
8435
8436const LINK_AFTER_ANOTHER: u8 = 1;
8438const LINK_BEFORE_ANOTHER: u8 = 2;
8440const LINK_AFTER_CALL: u8 = 4;
8442
8443enum LinkBelow {
8445 None,
8446 Member,
8447 Call(SpanKey),
8448}
8449
8450fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
8455 let mut current = object;
8456 loop {
8457 current = match current {
8458 Expression::StaticMemberExpression(member) if member.optional => {
8459 return LinkBelow::Member;
8460 }
8461 Expression::ComputedMemberExpression(member) if member.optional => {
8462 return LinkBelow::Member;
8463 }
8464 Expression::PrivateFieldExpression(member) if member.optional => {
8465 return LinkBelow::Member;
8466 }
8467 Expression::CallExpression(call) if call.optional => {
8468 return LinkBelow::Call(span_key(call.span));
8469 }
8470 Expression::StaticMemberExpression(member) => &member.object,
8471 Expression::ComputedMemberExpression(member) => &member.object,
8472 Expression::PrivateFieldExpression(member) => &member.object,
8473 Expression::CallExpression(call) => &call.callee,
8474 Expression::TSNonNullExpression(inner) => &inner.expression,
8475 _ => return LinkBelow::None,
8476 };
8477 }
8478}
8479
8480fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
8483 for ancestor in context.ancestors() {
8484 match ancestor {
8485 Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
8486 Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
8487 Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
8488 Ancestor::StaticMemberExpressionObject(_)
8489 | Ancestor::ComputedMemberExpressionObject(_)
8490 | Ancestor::PrivateFieldExpressionObject(_)
8491 | Ancestor::TSNonNullExpressionExpression(_) => {}
8492 Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
8493 _ => return false,
8494 }
8495 }
8496 false
8497}
8498
8499#[derive(Default)]
8500struct OptionalCallAnalysis {
8501 branches: Vec<CandidateBranch>,
8502 sites: HashMap<SpanKey, (String, String)>,
8503 roots: HashMap<SpanKey, Vec<SpanKey>>,
8504 limitations: Vec<CandidateLimitation>,
8505}
8506
8507#[derive(Clone)]
8508struct DefaultTarget {
8509 default_id: String,
8510 provided_id: String,
8511 inferred_name: Option<String>,
8512}
8513
8514#[derive(Default)]
8515struct DefaultAnalysis {
8516 branches: Vec<CandidateBranch>,
8517 parameter_targets: HashMap<SpanKey, DefaultTarget>,
8518 binding_targets: HashMap<SpanKey, DefaultTarget>,
8519 limitations: Vec<CandidateLimitation>,
8520}
8521
8522#[derive(Clone)]
8523struct ExtendedTarget {
8524 first_id: String,
8525 second_id: String,
8526}
8527
8528#[derive(Default)]
8529struct ExtendedAnalysis {
8530 branches: Vec<CandidateBranch>,
8531 try_targets: HashMap<SpanKey, ExtendedTarget>,
8532 loop_targets: HashMap<SpanKey, ExtendedTarget>,
8533}
8534
8535#[derive(Clone)]
8536struct SwitchTarget {
8537 case_ids: Vec<String>,
8538 no_match_id: Option<String>,
8539}
8540
8541#[derive(Default)]
8542struct SwitchAnalysis {
8543 branches: Vec<CandidateBranch>,
8544 targets: HashMap<SpanKey, SwitchTarget>,
8545}
8546
8547struct SwitchCollector<'s> {
8548 source: &'s str,
8549 file: &'s str,
8550 source_sensitive_functions: &'s HashSet<SpanKey>,
8551 unsafe_function_depth: usize,
8552 with_depth: usize,
8553 analysis: SwitchAnalysis,
8554}
8555
8556impl SwitchCollector<'_> {
8557 fn unsafe_context(&self) -> bool {
8558 self.unsafe_function_depth > 0 || self.with_depth > 0
8559 }
8560
8561 fn exit_source_function(&mut self, span: Span) {
8562 if self.source_sensitive_functions.contains(&span_key(span)) {
8563 self.unsafe_function_depth -= 1;
8564 }
8565 }
8566}
8567
8568impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
8569 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8570 if self
8571 .source_sensitive_functions
8572 .contains(&span_key(node.span))
8573 {
8574 self.unsafe_function_depth += 1;
8575 }
8576 }
8577
8578 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8579 self.exit_source_function(node.span);
8580 }
8581
8582 fn enter_arrow_function_expression(
8583 &mut self,
8584 node: &mut ArrowFunctionExpression<'a>,
8585 _context: &mut TraverseCtx<'a, ()>,
8586 ) {
8587 if self
8588 .source_sensitive_functions
8589 .contains(&span_key(node.span))
8590 {
8591 self.unsafe_function_depth += 1;
8592 }
8593 }
8594
8595 fn exit_arrow_function_expression(
8596 &mut self,
8597 node: &mut ArrowFunctionExpression<'a>,
8598 _context: &mut TraverseCtx<'a, ()>,
8599 ) {
8600 self.exit_source_function(node.span);
8601 }
8602
8603 fn enter_with_statement(
8604 &mut self,
8605 _node: &mut WithStatement<'a>,
8606 _context: &mut TraverseCtx<'a, ()>,
8607 ) {
8608 self.with_depth += 1;
8609 }
8610
8611 fn exit_with_statement(
8612 &mut self,
8613 _node: &mut WithStatement<'a>,
8614 _context: &mut TraverseCtx<'a, ()>,
8615 ) {
8616 self.with_depth -= 1;
8617 }
8618
8619 fn enter_switch_statement(
8620 &mut self,
8621 node: &mut SwitchStatement<'a>,
8622 _context: &mut TraverseCtx<'a, ()>,
8623 ) {
8624 let has_default = node.cases.iter().any(|case| case.test.is_none());
8625 if self.unsafe_context() {
8626 return;
8627 }
8628 let id = stable_id(self.source, self.file, "switch", node.span, "");
8629 let mut case_ids = Vec::with_capacity(node.cases.len());
8630 let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
8631 for (index, case) in node.cases.iter().enumerate() {
8632 let alternative_id = format!("{id}:case:{index}");
8633 let label = case.test.as_ref().map_or_else(
8634 || "default".to_string(),
8635 |test| format!("case {}", source_slice(self.source, test.span())),
8636 );
8637 case_ids.push(alternative_id.clone());
8638 alternatives.push(CandidateBranchAlternative {
8639 id: alternative_id,
8640 label,
8641 });
8642 }
8643 let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
8644 if let Some(no_match_id) = &no_match_id {
8645 alternatives.push(CandidateBranchAlternative {
8646 id: no_match_id.clone(),
8647 label: "no matching case".to_string(),
8648 });
8649 }
8650 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8651 self.analysis.branches.push(CandidateBranch {
8652 id,
8653 kind: "switch".to_string(),
8654 file: self.file.to_string(),
8655 line,
8656 column,
8657 source: source_slice(self.source, node.discriminant.span()).to_string(),
8658 alternatives,
8659 });
8660 self.analysis.targets.insert(
8661 span_key(node.span),
8662 SwitchTarget {
8663 case_ids,
8664 no_match_id,
8665 },
8666 );
8667 }
8668}
8669
8670fn collect_switch_branches<'a>(
8671 allocator: &'a Allocator,
8672 program: &mut Program<'a>,
8673 source: &str,
8674 file: &str,
8675 source_sensitive_functions: &HashSet<SpanKey>,
8676) -> SwitchAnalysis {
8677 let mut collector = SwitchCollector {
8678 source,
8679 file,
8680 source_sensitive_functions,
8681 unsafe_function_depth: 0,
8682 with_depth: 0,
8683 analysis: SwitchAnalysis::default(),
8684 };
8685 traverse_mut(&mut collector, allocator, program, Default::default(), ());
8686 collector.analysis
8687}
8688
8689struct ExtendedCollector<'s> {
8690 source: &'s str,
8691 file: &'s str,
8692 source_sensitive_functions: &'s HashSet<SpanKey>,
8693 entered_loops: HashSet<SpanKey>,
8694 unsafe_function_depth: usize,
8695 with_depth: usize,
8696 analysis: ExtendedAnalysis,
8697}
8698
8699impl ExtendedCollector<'_> {
8700 fn unsafe_context(&self) -> bool {
8701 self.unsafe_function_depth > 0 || self.with_depth > 0
8702 }
8703
8704 fn enter_try(&mut self, node: &TryStatement<'_>) {
8705 let transformed = !self.unsafe_context() && node.handler.is_some();
8706 if !transformed {
8707 return;
8708 }
8709 let id = stable_id(self.source, self.file, "try-catch", node.span, "");
8710 let success_id = format!("{id}:success");
8711 let catch_id = format!("{id}:catch");
8712 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8713 self.analysis.branches.push(CandidateBranch {
8714 id,
8715 kind: "try-catch".to_string(),
8716 file: self.file.to_string(),
8717 line,
8718 column,
8719 source: "try / catch".to_string(),
8720 alternatives: vec![
8721 CandidateBranchAlternative {
8722 id: success_id.clone(),
8723 label: "try completed without catch".to_string(),
8724 },
8725 CandidateBranchAlternative {
8726 id: catch_id.clone(),
8727 label: "catch entered".to_string(),
8728 },
8729 ],
8730 });
8731 self.analysis.try_targets.insert(
8732 span_key(node.span),
8733 ExtendedTarget {
8734 first_id: success_id,
8735 second_id: catch_id,
8736 },
8737 );
8738 }
8739
8740 fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
8741 let transformed = !self.unsafe_context();
8742 if !transformed {
8743 return;
8744 }
8745 if self.entered_loops.contains(&span_key(span)) {
8749 return;
8750 }
8751 let id = stable_id(self.source, self.file, kind, span, "");
8752 let zero_id = format!("{id}:zero");
8753 let entered_id = format!("{id}:entered");
8754 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8755 self.analysis.branches.push(CandidateBranch {
8756 id,
8757 kind: kind.to_string(),
8758 file: self.file.to_string(),
8759 line,
8760 column,
8761 source: source_slice(self.source, right.span()).to_string(),
8762 alternatives: vec![
8763 CandidateBranchAlternative {
8764 id: zero_id.clone(),
8765 label: "zero iterations".to_string(),
8766 },
8767 CandidateBranchAlternative {
8768 id: entered_id.clone(),
8769 label: "one or more iterations".to_string(),
8770 },
8771 ],
8772 });
8773 self.analysis.loop_targets.insert(
8774 span_key(span),
8775 ExtendedTarget {
8776 first_id: zero_id,
8777 second_id: entered_id,
8778 },
8779 );
8780 }
8781
8782 fn exit_source_function(&mut self, span: Span) {
8783 if self.source_sensitive_functions.contains(&span_key(span)) {
8784 self.unsafe_function_depth -= 1;
8785 }
8786 }
8787}
8788
8789impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
8790 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8791 if self
8792 .source_sensitive_functions
8793 .contains(&span_key(node.span))
8794 {
8795 self.unsafe_function_depth += 1;
8796 }
8797 }
8798
8799 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8800 self.exit_source_function(node.span);
8801 }
8802
8803 fn enter_arrow_function_expression(
8804 &mut self,
8805 node: &mut ArrowFunctionExpression<'a>,
8806 _context: &mut TraverseCtx<'a, ()>,
8807 ) {
8808 if self
8809 .source_sensitive_functions
8810 .contains(&span_key(node.span))
8811 {
8812 self.unsafe_function_depth += 1;
8813 }
8814 }
8815
8816 fn exit_arrow_function_expression(
8817 &mut self,
8818 node: &mut ArrowFunctionExpression<'a>,
8819 _context: &mut TraverseCtx<'a, ()>,
8820 ) {
8821 self.exit_source_function(node.span);
8822 }
8823
8824 fn enter_with_statement(
8825 &mut self,
8826 _node: &mut WithStatement<'a>,
8827 _context: &mut TraverseCtx<'a, ()>,
8828 ) {
8829 self.with_depth += 1;
8830 }
8831
8832 fn exit_with_statement(
8833 &mut self,
8834 _node: &mut WithStatement<'a>,
8835 _context: &mut TraverseCtx<'a, ()>,
8836 ) {
8837 self.with_depth -= 1;
8838 }
8839
8840 fn enter_try_statement(
8841 &mut self,
8842 node: &mut TryStatement<'a>,
8843 _context: &mut TraverseCtx<'a, ()>,
8844 ) {
8845 self.enter_try(node);
8846 }
8847
8848 fn exit_try_statement(
8849 &mut self,
8850 _node: &mut TryStatement<'a>,
8851 _context: &mut TraverseCtx<'a, ()>,
8852 ) {
8853 }
8854
8855 fn enter_for_in_statement(
8856 &mut self,
8857 node: &mut ForInStatement<'a>,
8858 _context: &mut TraverseCtx<'a, ()>,
8859 ) {
8860 self.enter_loop(node.span, &node.right, "for-in");
8861 }
8862
8863 fn exit_for_in_statement(
8864 &mut self,
8865 _node: &mut ForInStatement<'a>,
8866 _context: &mut TraverseCtx<'a, ()>,
8867 ) {
8868 }
8869
8870 fn enter_for_of_statement(
8871 &mut self,
8872 node: &mut ForOfStatement<'a>,
8873 _context: &mut TraverseCtx<'a, ()>,
8874 ) {
8875 self.enter_loop(node.span, &node.right, "for-of");
8876 }
8877
8878 fn exit_for_of_statement(
8879 &mut self,
8880 _node: &mut ForOfStatement<'a>,
8881 _context: &mut TraverseCtx<'a, ()>,
8882 ) {
8883 }
8884}
8885
8886fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
8899 let semantic = SemanticBuilder::new().build(program).semantic;
8900 let mut found = HashSet::new();
8901 for node in semantic.nodes() {
8902 if let AstKind::ForOfStatement(loop_node) = node.kind()
8903 && never_empty(&loop_node.right, &semantic)
8904 {
8905 found.insert(span_key(loop_node.span));
8906 }
8907 }
8908 found
8909}
8910
8911fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
8914 match transparent_expression(expression) {
8915 Expression::ArrayExpression(array) => has_definite_element(array),
8916 Expression::Identifier(identifier) => {
8917 let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
8918 return false;
8919 };
8920 if semantic.scoping().get_resolved_references(symbol).count() != 1 {
8925 return false;
8926 }
8927 let declaration = semantic.scoping().symbol_declaration(symbol);
8928 let AstKind::VariableDeclarator(declarator) =
8929 semantic.nodes().get_node(declaration).kind()
8930 else {
8931 return false;
8932 };
8933 declarator.kind == VariableDeclarationKind::Const
8934 && declarator.init.as_ref().is_some_and(|init| {
8935 matches!(transparent_expression(init), Expression::ArrayExpression(array)
8936 if has_definite_element(array))
8937 })
8938 }
8939 _ => false,
8940 }
8941}
8942
8943fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
8947 array
8948 .elements
8949 .iter()
8950 .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
8951}
8952
8953fn collect_extended_branches<'a>(
8954 allocator: &'a Allocator,
8955 program: &mut Program<'a>,
8956 source: &str,
8957 file: &str,
8958 source_sensitive_functions: &HashSet<SpanKey>,
8959) -> ExtendedAnalysis {
8960 let entered_loops = provably_entered_loops(program);
8961 let mut collector = ExtendedCollector {
8962 source,
8963 file,
8964 source_sensitive_functions,
8965 entered_loops,
8966 unsafe_function_depth: 0,
8967 with_depth: 0,
8968 analysis: ExtendedAnalysis::default(),
8969 };
8970 traverse_mut(&mut collector, allocator, program, Default::default(), ());
8971 collector.analysis
8972}
8973
8974struct DefaultCollector<'s> {
8975 source: &'s str,
8976 file: &'s str,
8977 source_sensitive_functions: &'s HashSet<SpanKey>,
8978 unsafe_function_depth: usize,
8979 with_depth: usize,
8980 analysis: DefaultAnalysis,
8981}
8982
8983impl DefaultCollector<'_> {
8984 fn unsafe_context(&self) -> bool {
8985 self.unsafe_function_depth > 0 || self.with_depth > 0
8986 }
8987
8988 fn target(
8989 &mut self,
8990 span: Span,
8991 left: &BindingPattern<'_>,
8992 right: &Expression<'_>,
8993 ) -> DefaultTarget {
8994 let id = stable_id(self.source, self.file, "default-value", span, "");
8995 let default_id = format!("{id}:default");
8996 let provided_id = format!("{id}:provided");
8997 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8998 self.analysis.branches.push(CandidateBranch {
8999 id,
9000 kind: "default-value".to_string(),
9001 file: self.file.to_string(),
9002 line,
9003 column,
9004 source: source_slice(self.source, span).to_string(),
9005 alternatives: vec![
9006 CandidateBranchAlternative {
9007 id: default_id.clone(),
9008 label: "default evaluated".to_string(),
9009 },
9010 CandidateBranchAlternative {
9011 id: provided_id.clone(),
9012 label: "value provided".to_string(),
9013 },
9014 ],
9015 });
9016 DefaultTarget {
9017 default_id,
9018 provided_id,
9019 inferred_name: binding_identifier_name(left)
9020 .filter(|_| expression_is_anonymous_definition(right)),
9021 }
9022 }
9023
9024 fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
9025 match pattern {
9026 BindingPattern::AssignmentPattern(assignment) => {
9027 let target = self.target(assignment.span, &assignment.left, &assignment.right);
9028 if parameter {
9029 self.analysis
9030 .parameter_targets
9031 .insert(span_key(assignment.span), target);
9032 } else {
9033 self.analysis
9034 .binding_targets
9035 .insert(span_key(assignment.span), target);
9036 }
9037 self.collect_binding_pattern(&assignment.left, parameter);
9038 }
9039 BindingPattern::ObjectPattern(object) => {
9040 for property in &object.properties {
9041 self.collect_binding_pattern(&property.value, parameter);
9042 }
9043 if let Some(rest) = &object.rest {
9044 self.collect_binding_pattern(&rest.argument, parameter);
9045 }
9046 }
9047 BindingPattern::ArrayPattern(array) => {
9048 for element in array.elements.iter().flatten() {
9049 self.collect_binding_pattern(element, parameter);
9050 }
9051 if let Some(rest) = &array.rest {
9052 self.collect_binding_pattern(&rest.argument, parameter);
9053 }
9054 }
9055 BindingPattern::BindingIdentifier(_) => {}
9056 }
9057 }
9058
9059 fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
9060 for parameter in ¶meters.items {
9061 if let Some(initializer) = ¶meter.initializer {
9062 let default_span =
9069 Span::new(parameter.pattern.span().start, initializer.span().end);
9070 let target = self.target(default_span, ¶meter.pattern, initializer);
9071 self.analysis
9072 .parameter_targets
9073 .insert(span_key(parameter.span), target);
9074 }
9075 self.collect_binding_pattern(¶meter.pattern, true);
9076 }
9077 if let Some(rest) = ¶meters.rest {
9078 self.collect_binding_pattern(&rest.rest.argument, true);
9079 }
9080 }
9081
9082 fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
9083 match pattern {
9084 BindingPattern::AssignmentPattern(_) => true,
9085 BindingPattern::ObjectPattern(object) => {
9086 object
9087 .properties
9088 .iter()
9089 .any(|property| Self::has_binding_default(&property.value))
9090 || object
9091 .rest
9092 .as_ref()
9093 .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9094 }
9095 BindingPattern::ArrayPattern(array) => {
9096 array
9097 .elements
9098 .iter()
9099 .flatten()
9100 .any(Self::has_binding_default)
9101 || array
9102 .rest
9103 .as_ref()
9104 .is_some_and(|rest| Self::has_binding_default(&rest.argument))
9105 }
9106 BindingPattern::BindingIdentifier(_) => false,
9107 }
9108 }
9109
9110 fn exit_source_function(&mut self, span: Span) {
9111 if self.source_sensitive_functions.contains(&span_key(span)) {
9112 self.unsafe_function_depth -= 1;
9113 }
9114 }
9115}
9116
9117impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
9118 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9119 if self
9120 .source_sensitive_functions
9121 .contains(&span_key(node.span))
9122 {
9123 self.unsafe_function_depth += 1;
9124 return;
9125 }
9126 if !self.unsafe_context() && node.body.is_some() {
9127 self.collect_parameters(&node.params);
9128 }
9129 }
9130
9131 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9132 self.exit_source_function(node.span);
9133 }
9134
9135 fn enter_arrow_function_expression(
9136 &mut self,
9137 node: &mut ArrowFunctionExpression<'a>,
9138 _context: &mut TraverseCtx<'a, ()>,
9139 ) {
9140 if self
9141 .source_sensitive_functions
9142 .contains(&span_key(node.span))
9143 {
9144 self.unsafe_function_depth += 1;
9145 return;
9146 }
9147 if !self.unsafe_context() {
9148 self.collect_parameters(&node.params);
9149 }
9150 }
9151
9152 fn exit_arrow_function_expression(
9153 &mut self,
9154 node: &mut ArrowFunctionExpression<'a>,
9155 _context: &mut TraverseCtx<'a, ()>,
9156 ) {
9157 self.exit_source_function(node.span);
9158 }
9159
9160 fn enter_with_statement(
9161 &mut self,
9162 _node: &mut WithStatement<'a>,
9163 _context: &mut TraverseCtx<'a, ()>,
9164 ) {
9165 self.with_depth += 1;
9166 }
9167
9168 fn exit_with_statement(
9169 &mut self,
9170 _node: &mut WithStatement<'a>,
9171 _context: &mut TraverseCtx<'a, ()>,
9172 ) {
9173 self.with_depth -= 1;
9174 }
9175
9176 fn enter_variable_declaration(
9177 &mut self,
9178 node: &mut VariableDeclaration<'a>,
9179 context: &mut TraverseCtx<'a, ()>,
9180 ) {
9181 if self.unsafe_context() {
9182 return;
9183 }
9184 let has_default = node
9185 .declarations
9186 .iter()
9187 .any(|declaration| Self::has_binding_default(&declaration.id));
9188 if !has_default {
9189 return;
9190 }
9191 if matches!(
9192 context.ancestors().next(),
9193 Some(Ancestor::ForStatementInit(_))
9194 ) {
9195 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9196 self.analysis.limitations.push(CandidateLimitation {
9197 id: stable_id(
9198 self.source,
9199 self.file,
9200 "dynamic-code",
9201 node.span,
9202 "for-init-default",
9203 ),
9204 kind: "dynamic-code".to_string(),
9205 file: self.file.to_string(),
9206 line,
9207 column,
9208 source: source_slice(self.source, node.span).to_string(),
9209 reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
9210 });
9211 return;
9212 }
9213 for declaration in &node.declarations {
9214 self.collect_binding_pattern(&declaration.id, false);
9215 }
9216 }
9217}
9218
9219fn collect_default_branches<'a>(
9220 allocator: &'a Allocator,
9221 program: &mut Program<'a>,
9222 source: &str,
9223 file: &str,
9224 source_sensitive_functions: &HashSet<SpanKey>,
9225) -> DefaultAnalysis {
9226 let mut collector = DefaultCollector {
9227 source,
9228 file,
9229 source_sensitive_functions,
9230 unsafe_function_depth: 0,
9231 with_depth: 0,
9232 analysis: DefaultAnalysis::default(),
9233 };
9234 traverse_mut(&mut collector, allocator, program, Default::default(), ());
9235 collector.analysis
9236}
9237
9238struct OptionalCallCollector<'s> {
9239 source: &'s str,
9240 file: &'s str,
9241 source_sensitive_functions: &'s HashSet<SpanKey>,
9242 unsafe_function_depth: usize,
9243 with_depth: usize,
9244 chain_roots: Vec<SpanKey>,
9245 analysis: OptionalCallAnalysis,
9246}
9247
9248impl OptionalCallCollector<'_> {
9249 fn unsafe_context(&self) -> bool {
9250 self.unsafe_function_depth > 0 || self.with_depth > 0
9251 }
9252
9253 fn exit_source_function(&mut self, span: Span) {
9254 if self.source_sensitive_functions.contains(&span_key(span)) {
9255 self.unsafe_function_depth -= 1;
9256 }
9257 }
9258}
9259
9260impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
9261 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9262 if self
9263 .source_sensitive_functions
9264 .contains(&span_key(node.span))
9265 {
9266 self.unsafe_function_depth += 1;
9267 }
9268 }
9269
9270 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9271 self.exit_source_function(node.span);
9272 }
9273
9274 fn enter_arrow_function_expression(
9275 &mut self,
9276 node: &mut ArrowFunctionExpression<'a>,
9277 _context: &mut TraverseCtx<'a, ()>,
9278 ) {
9279 if self
9280 .source_sensitive_functions
9281 .contains(&span_key(node.span))
9282 {
9283 self.unsafe_function_depth += 1;
9284 }
9285 }
9286
9287 fn exit_arrow_function_expression(
9288 &mut self,
9289 node: &mut ArrowFunctionExpression<'a>,
9290 _context: &mut TraverseCtx<'a, ()>,
9291 ) {
9292 self.exit_source_function(node.span);
9293 }
9294
9295 fn enter_with_statement(
9296 &mut self,
9297 _node: &mut WithStatement<'a>,
9298 _context: &mut TraverseCtx<'a, ()>,
9299 ) {
9300 self.with_depth += 1;
9301 }
9302
9303 fn exit_with_statement(
9304 &mut self,
9305 _node: &mut WithStatement<'a>,
9306 _context: &mut TraverseCtx<'a, ()>,
9307 ) {
9308 self.with_depth -= 1;
9309 }
9310
9311 fn enter_chain_expression(
9312 &mut self,
9313 node: &mut ChainExpression<'a>,
9314 context: &mut TraverseCtx<'a, ()>,
9315 ) {
9316 let root = match context.ancestors().next() {
9317 Some(Ancestor::UnaryExpressionArgument(parent))
9318 if *parent.operator() == UnaryOperator::Delete =>
9319 {
9320 span_key(*parent.span())
9321 }
9322 _ => span_key(node.span),
9323 };
9324 self.chain_roots.push(root);
9325 }
9326
9327 fn exit_chain_expression(
9328 &mut self,
9329 _node: &mut ChainExpression<'a>,
9330 _context: &mut TraverseCtx<'a, ()>,
9331 ) {
9332 self.chain_roots.pop();
9333 }
9334
9335 fn enter_call_expression(
9336 &mut self,
9337 node: &mut CallExpression<'a>,
9338 _context: &mut TraverseCtx<'a, ()>,
9339 ) {
9340 if !node.optional || self.unsafe_context() {
9341 return;
9342 }
9343 let root = *self
9344 .chain_roots
9345 .last()
9346 .expect("an optional call must be enclosed by a chain expression");
9347 let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
9348 let short_id = format!("{id}:short");
9349 let continued_id = format!("{id}:continued");
9350 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9351 self.analysis.sites.insert(
9352 span_key(node.span),
9353 (short_id.clone(), continued_id.clone()),
9354 );
9355 self.analysis
9356 .roots
9357 .entry(root)
9358 .or_default()
9359 .push(span_key(node.span));
9360 self.analysis.branches.push(CandidateBranch {
9361 id,
9362 kind: "optional-chain".to_string(),
9363 file: self.file.to_string(),
9364 line,
9365 column,
9366 source: source_slice(self.source, node.span).to_string(),
9367 alternatives: vec![
9368 CandidateBranchAlternative {
9369 id: short_id,
9370 label: "nullish / short-circuited".to_string(),
9371 },
9372 CandidateBranchAlternative {
9373 id: continued_id,
9374 label: "non-nullish / continued".to_string(),
9375 },
9376 ],
9377 });
9378 }
9379}
9380
9381fn collect_optional_call_branches<'a>(
9382 allocator: &'a Allocator,
9383 program: &mut Program<'a>,
9384 source: &str,
9385 file: &str,
9386 source_sensitive_functions: &HashSet<SpanKey>,
9387) -> OptionalCallAnalysis {
9388 let mut collector = OptionalCallCollector {
9389 source,
9390 file,
9391 source_sensitive_functions,
9392 unsafe_function_depth: 0,
9393 with_depth: 0,
9394 chain_roots: Vec::new(),
9395 analysis: OptionalCallAnalysis::default(),
9396 };
9397 traverse_mut(&mut collector, allocator, program, Default::default(), ());
9398 collector.analysis
9399}
9400
9401struct OptionalMemberCollector<'s> {
9402 source: &'s str,
9403 file: &'s str,
9404 source_sensitive_functions: &'s HashSet<SpanKey>,
9405 unsafe_function_depth: usize,
9406 with_depth: usize,
9407 analysis: OptionalMemberAnalysis,
9408}
9409
9410impl OptionalMemberCollector<'_> {
9411 fn record<State>(
9412 &mut self,
9413 span: Span,
9414 optional: bool,
9415 object: &Expression<'_>,
9416 context: &TraverseCtx<'_, State>,
9417 ) {
9418 if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
9419 return;
9420 }
9421 let below = match optional_link_below(object) {
9422 LinkBelow::Member => LINK_AFTER_ANOTHER,
9423 LinkBelow::Call(call) => {
9424 self.analysis.after_call.insert(span_key(span), call);
9425 LINK_AFTER_CALL
9426 }
9427 LinkBelow::None => 0,
9428 };
9429 let links = below
9430 | if optional_link_above(context) {
9431 LINK_BEFORE_ANOTHER
9432 } else {
9433 0
9434 };
9435 if links != 0 {
9436 self.analysis.links.insert(span_key(span), links);
9437 }
9438 let id = stable_id(self.source, self.file, "optional-chain", span, "");
9439 let short_id = format!("{id}:short");
9440 let continued_id = format!("{id}:continued");
9441 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
9442 self.analysis
9443 .targets
9444 .insert(span_key(span), (short_id.clone(), continued_id.clone()));
9445 self.analysis.branches.push(CandidateBranch {
9446 id,
9447 kind: "optional-chain".to_string(),
9448 file: self.file.to_string(),
9449 line,
9450 column,
9451 source: source_slice(self.source, span).to_string(),
9452 alternatives: vec![
9453 CandidateBranchAlternative {
9454 id: short_id,
9455 label: "nullish / short-circuited".to_string(),
9456 },
9457 CandidateBranchAlternative {
9458 id: continued_id,
9459 label: "non-nullish / continued".to_string(),
9460 },
9461 ],
9462 });
9463 }
9464
9465 fn exit_source_function(&mut self, span: Span) {
9466 if self.source_sensitive_functions.contains(&span_key(span)) {
9467 self.unsafe_function_depth -= 1;
9468 }
9469 }
9470}
9471
9472impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
9473 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9474 if self
9475 .source_sensitive_functions
9476 .contains(&span_key(node.span))
9477 {
9478 self.unsafe_function_depth += 1;
9479 }
9480 }
9481
9482 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9483 self.exit_source_function(node.span);
9484 }
9485
9486 fn enter_arrow_function_expression(
9487 &mut self,
9488 node: &mut ArrowFunctionExpression<'a>,
9489 _context: &mut TraverseCtx<'a, ()>,
9490 ) {
9491 if self
9492 .source_sensitive_functions
9493 .contains(&span_key(node.span))
9494 {
9495 self.unsafe_function_depth += 1;
9496 }
9497 }
9498
9499 fn exit_arrow_function_expression(
9500 &mut self,
9501 node: &mut ArrowFunctionExpression<'a>,
9502 _context: &mut TraverseCtx<'a, ()>,
9503 ) {
9504 self.exit_source_function(node.span);
9505 }
9506
9507 fn enter_with_statement(
9508 &mut self,
9509 _node: &mut WithStatement<'a>,
9510 _context: &mut TraverseCtx<'a, ()>,
9511 ) {
9512 self.with_depth += 1;
9513 }
9514
9515 fn exit_with_statement(
9516 &mut self,
9517 _node: &mut WithStatement<'a>,
9518 _context: &mut TraverseCtx<'a, ()>,
9519 ) {
9520 self.with_depth -= 1;
9521 }
9522
9523 fn enter_computed_member_expression(
9524 &mut self,
9525 node: &mut ComputedMemberExpression<'a>,
9526 context: &mut TraverseCtx<'a, ()>,
9527 ) {
9528 self.record(node.span, node.optional, &node.object, context);
9529 }
9530
9531 fn enter_static_member_expression(
9532 &mut self,
9533 node: &mut StaticMemberExpression<'a>,
9534 context: &mut TraverseCtx<'a, ()>,
9535 ) {
9536 self.record(node.span, node.optional, &node.object, context);
9537 }
9538
9539 fn enter_private_field_expression(
9540 &mut self,
9541 node: &mut PrivateFieldExpression<'a>,
9542 context: &mut TraverseCtx<'a, ()>,
9543 ) {
9544 self.record(node.span, node.optional, &node.object, context);
9545 }
9546}
9547
9548fn collect_optional_member_branches<'a>(
9549 allocator: &'a Allocator,
9550 program: &mut Program<'a>,
9551 source: &str,
9552 file: &str,
9553 source_sensitive_functions: &HashSet<SpanKey>,
9554) -> OptionalMemberAnalysis {
9555 let mut collector = OptionalMemberCollector {
9556 source,
9557 file,
9558 source_sensitive_functions,
9559 unsafe_function_depth: 0,
9560 with_depth: 0,
9561 analysis: OptionalMemberAnalysis::default(),
9562 };
9563 traverse_mut(&mut collector, allocator, program, Default::default(), ());
9564 collector.analysis
9565}
9566
9567struct LogicalAssignmentCollector<'s> {
9568 source: &'s str,
9569 file: &'s str,
9570 source_sensitive_functions: &'s HashSet<SpanKey>,
9571 unsafe_function_depth: usize,
9572 with_depth: usize,
9573 analysis: LogicalAssignmentAnalysis,
9574}
9575
9576impl LogicalAssignmentCollector<'_> {
9577 fn exit_source_function(&mut self, span: Span) {
9578 if self.source_sensitive_functions.contains(&span_key(span)) {
9579 self.unsafe_function_depth -= 1;
9580 }
9581 }
9582}
9583
9584impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
9585 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9586 if self
9587 .source_sensitive_functions
9588 .contains(&span_key(node.span))
9589 {
9590 self.unsafe_function_depth += 1;
9591 }
9592 }
9593
9594 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9595 self.exit_source_function(node.span);
9596 }
9597
9598 fn enter_arrow_function_expression(
9599 &mut self,
9600 node: &mut ArrowFunctionExpression<'a>,
9601 _context: &mut TraverseCtx<'a, ()>,
9602 ) {
9603 if self
9604 .source_sensitive_functions
9605 .contains(&span_key(node.span))
9606 {
9607 self.unsafe_function_depth += 1;
9608 }
9609 }
9610
9611 fn exit_arrow_function_expression(
9612 &mut self,
9613 node: &mut ArrowFunctionExpression<'a>,
9614 _context: &mut TraverseCtx<'a, ()>,
9615 ) {
9616 self.exit_source_function(node.span);
9617 }
9618
9619 fn enter_with_statement(
9620 &mut self,
9621 _node: &mut WithStatement<'a>,
9622 _context: &mut TraverseCtx<'a, ()>,
9623 ) {
9624 self.with_depth += 1;
9625 }
9626
9627 fn exit_with_statement(
9628 &mut self,
9629 _node: &mut WithStatement<'a>,
9630 _context: &mut TraverseCtx<'a, ()>,
9631 ) {
9632 self.with_depth -= 1;
9633 }
9634
9635 fn exit_assignment_expression(
9636 &mut self,
9637 node: &mut AssignmentExpression<'a>,
9638 _context: &mut TraverseCtx<'a, ()>,
9639 ) {
9640 if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
9641 return;
9642 }
9643 let operator = match node.operator {
9644 AssignmentOperator::LogicalAnd => "&&=",
9645 AssignmentOperator::LogicalOr => "||=",
9646 AssignmentOperator::LogicalNullish => "??=",
9647 _ => unreachable!("logical assignment filter must be exhaustive"),
9648 };
9649 let id = stable_id(
9650 self.source,
9651 self.file,
9652 "logical-assignment",
9653 node.span,
9654 operator,
9655 );
9656 let short_id = format!("{id}:short");
9657 let right_id = format!("{id}:right");
9658 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9659 self.analysis
9660 .targets
9661 .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9662 self.analysis.branches.push(CandidateBranch {
9663 id,
9664 kind: "logical-assignment".to_string(),
9665 file: self.file.to_string(),
9666 line,
9667 column,
9668 source: source_slice(self.source, node.span).to_string(),
9669 alternatives: vec![
9670 CandidateBranchAlternative {
9671 id: short_id,
9672 label: "assignment skipped".to_string(),
9673 },
9674 CandidateBranchAlternative {
9675 id: right_id,
9676 label: "right evaluated / assigned".to_string(),
9677 },
9678 ],
9679 });
9680 }
9681}
9682
9683fn collect_logical_assignment_branches<'a>(
9684 allocator: &'a Allocator,
9685 program: &mut Program<'a>,
9686 source: &str,
9687 file: &str,
9688 source_sensitive_functions: &HashSet<SpanKey>,
9689) -> LogicalAssignmentAnalysis {
9690 let mut collector = LogicalAssignmentCollector {
9691 source,
9692 file,
9693 source_sensitive_functions,
9694 unsafe_function_depth: 0,
9695 with_depth: 0,
9696 analysis: LogicalAssignmentAnalysis::default(),
9697 };
9698 traverse_mut(&mut collector, allocator, program, Default::default(), ());
9699 collector.analysis
9700}
9701
9702struct LogicalBranchCollector<'s> {
9703 source: &'s str,
9704 file: &'s str,
9705 decision_logical_nodes: &'s HashSet<SpanKey>,
9706 source_sensitive_functions: &'s HashSet<SpanKey>,
9707 unsafe_function_depth: usize,
9708 with_depth: usize,
9709 analysis: LogicalBranchAnalysis,
9710}
9711
9712impl LogicalBranchCollector<'_> {
9713 fn exit_source_function(&mut self, span: Span) {
9714 if self.source_sensitive_functions.contains(&span_key(span)) {
9715 self.unsafe_function_depth -= 1;
9716 }
9717 }
9718}
9719
9720impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
9721 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9722 if self
9723 .source_sensitive_functions
9724 .contains(&span_key(node.span))
9725 {
9726 self.unsafe_function_depth += 1;
9727 }
9728 }
9729
9730 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
9731 self.exit_source_function(node.span);
9732 }
9733
9734 fn enter_arrow_function_expression(
9735 &mut self,
9736 node: &mut ArrowFunctionExpression<'a>,
9737 _context: &mut TraverseCtx<'a, ()>,
9738 ) {
9739 if self
9740 .source_sensitive_functions
9741 .contains(&span_key(node.span))
9742 {
9743 self.unsafe_function_depth += 1;
9744 }
9745 }
9746
9747 fn exit_arrow_function_expression(
9748 &mut self,
9749 node: &mut ArrowFunctionExpression<'a>,
9750 _context: &mut TraverseCtx<'a, ()>,
9751 ) {
9752 self.exit_source_function(node.span);
9753 }
9754
9755 fn enter_with_statement(
9756 &mut self,
9757 _node: &mut WithStatement<'a>,
9758 _context: &mut TraverseCtx<'a, ()>,
9759 ) {
9760 self.with_depth += 1;
9761 }
9762
9763 fn exit_with_statement(
9764 &mut self,
9765 _node: &mut WithStatement<'a>,
9766 _context: &mut TraverseCtx<'a, ()>,
9767 ) {
9768 self.with_depth -= 1;
9769 }
9770
9771 fn exit_logical_expression(
9772 &mut self,
9773 node: &mut LogicalExpression<'a>,
9774 _context: &mut TraverseCtx<'a, ()>,
9775 ) {
9776 if self.unsafe_function_depth > 0
9777 || self.with_depth > 0
9778 || self.decision_logical_nodes.contains(&span_key(node.span))
9779 {
9780 return;
9781 }
9782 let operator = match node.operator {
9783 LogicalOperator::And => "&&",
9784 LogicalOperator::Or => "||",
9785 LogicalOperator::Coalesce => "??",
9786 };
9787 let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
9788 let short_id = format!("{id}:short");
9789 let right_id = format!("{id}:right");
9790 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
9791 self.analysis
9792 .logical_targets
9793 .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
9794 self.analysis.branches.push(CandidateBranch {
9795 id,
9796 kind: "logical-value".to_string(),
9797 file: self.file.to_string(),
9798 line,
9799 column,
9800 source: source_slice(self.source, node.span).to_string(),
9801 alternatives: vec![
9802 CandidateBranchAlternative {
9803 id: short_id,
9804 label: "short-circuit / left selected".to_string(),
9805 },
9806 CandidateBranchAlternative {
9807 id: right_id,
9808 label: "right evaluated / selected".to_string(),
9809 },
9810 ],
9811 });
9812 }
9813}
9814
9815fn collect_logical_value_branches<'a>(
9816 allocator: &'a Allocator,
9817 program: &mut Program<'a>,
9818 source: &str,
9819 file: &str,
9820 decision_logical_nodes: &HashSet<SpanKey>,
9821 source_sensitive_functions: &HashSet<SpanKey>,
9822) -> LogicalBranchAnalysis {
9823 let mut collector = LogicalBranchCollector {
9824 source,
9825 file,
9826 decision_logical_nodes,
9827 source_sensitive_functions,
9828 unsafe_function_depth: 0,
9829 with_depth: 0,
9830 analysis: LogicalBranchAnalysis::default(),
9831 };
9832 traverse_mut(&mut collector, allocator, program, Default::default(), ());
9833 collector.analysis
9834}
9835
9836impl<'a> Visit<'a> for DecisionCollector<'_> {
9837 fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
9838 if self
9839 .source_sensitive_functions
9840 .contains(&span_key(function.span))
9841 {
9842 return;
9843 }
9844 walk::walk_function(self, function, flags);
9845 }
9846
9847 fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
9848 if self
9849 .source_sensitive_functions
9850 .contains(&span_key(function.span))
9851 {
9852 return;
9853 }
9854 walk::walk_arrow_function_expression(self, function);
9855 }
9856
9857 fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
9860
9861 fn visit_expression(&mut self, expression: &Expression<'a>) {
9868 let outer_root = self.logical_root;
9869 self.logical_root = match expression {
9873 Expression::LogicalExpression(logical) => {
9874 Some(outer_root.unwrap_or(span_key(logical.span)))
9875 }
9876 Expression::ParenthesizedExpression(_) => outer_root,
9877 _ => None,
9878 };
9879 let candidate = match expression {
9880 Expression::LogicalExpression(logical)
9881 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9882 && !self.in_a_decision(logical.span) =>
9883 {
9884 Some(&logical.left)
9885 }
9886 Expression::UnaryExpression(unary)
9887 if unary.operator.is_not()
9888 && has_compound_boolean_decision(&unary.argument)
9889 && !compound_root(&unary.argument)
9890 .is_some_and(|root| self.in_a_decision(root.span)) =>
9891 {
9892 Some(expression)
9893 }
9894 _ => None,
9895 };
9896 if let Some(decision) = candidate
9897 && has_compound_boolean_decision(decision)
9898 && decision_outcome_is_variable(decision)
9899 {
9900 let mut conditions = Vec::new();
9901 collect_conditions(decision, &mut conditions);
9902 if conditions.len() <= 32 {
9903 let root = self
9905 .logical_root
9906 .or(outer_root)
9907 .unwrap_or(span_key(decision.span()));
9908 self.value_decisions.insert(span_key(decision.span()), root);
9909 self.record_decision(decision, "value");
9910 }
9911 }
9912 walk::walk_expression(self, expression);
9913 self.logical_root = outer_root;
9914 }
9915
9916 fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
9917 if decision_outcome_is_variable(&statement.test) {
9918 self.record_decision(&statement.test, "if");
9919 }
9920 self.visit_expression(&statement.test);
9921 self.visit_statement(&statement.consequent);
9922 if let Some(alternate) = &statement.alternate {
9923 self.visit_statement(alternate);
9924 }
9925 }
9926
9927 fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
9928 if decision_outcome_is_variable(&expression.test) {
9929 self.record_decision(&expression.test, "ternary");
9930 }
9931 self.visit_expression(&expression.test);
9932 self.visit_expression(&expression.consequent);
9933 self.visit_expression(&expression.alternate);
9934 }
9935
9936 fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
9937 if decision_outcome_is_variable(&statement.test) {
9938 self.record_decision(&statement.test, "while");
9939 }
9940 self.visit_expression(&statement.test);
9941 self.visit_statement(&statement.body);
9942 }
9943
9944 fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
9945 if decision_outcome_is_variable(&statement.test) {
9946 self.record_decision(&statement.test, "do-while");
9947 }
9948 self.visit_statement(&statement.body);
9949 self.visit_expression(&statement.test);
9950 }
9951
9952 fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
9953 if let Some(test) = &statement.test
9954 && decision_outcome_is_variable(test)
9955 {
9956 self.record_decision(test, "for");
9957 }
9958 if let Some(init) = &statement.init {
9959 self.visit_for_statement_init(init);
9960 }
9961 if let Some(test) = &statement.test {
9962 self.visit_expression(test);
9963 }
9964 if let Some(update) = &statement.update {
9965 self.visit_expression(update);
9966 }
9967 self.visit_statement(&statement.body);
9968 }
9969}
9970
9971fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
9972 match expression {
9973 Expression::ParenthesizedExpression(parenthesized) => {
9974 transparent_expression(&parenthesized.expression)
9975 }
9976 _ => expression,
9977 }
9978}
9979
9980fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
9981 match expression {
9982 Expression::ParenthesizedExpression(parenthesized) => {
9983 has_compound_boolean_decision(&parenthesized.expression)
9984 }
9985 Expression::LogicalExpression(logical) => {
9986 matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
9987 }
9988 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
9989 has_compound_boolean_decision(&unary.argument)
9990 }
9991 _ => false,
9992 }
9993}
9994
9995enum ValueShape {
9998 Condition(usize),
9999 And(Box<ValueShape>, Box<ValueShape>),
10000 Or(Box<ValueShape>, Box<ValueShape>),
10001 Not(Box<ValueShape>),
10002}
10003
10004fn value_shape(expression: &Expression<'_>, next: &mut usize) -> ValueShape {
10005 match expression {
10006 Expression::ParenthesizedExpression(parenthesized) => {
10007 value_shape(&parenthesized.expression, next)
10008 }
10009 Expression::LogicalExpression(logical)
10010 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10011 {
10012 let left = Box::new(value_shape(&logical.left, next));
10013 let right = Box::new(value_shape(&logical.right, next));
10014 if logical.operator == LogicalOperator::And {
10015 ValueShape::And(left, right)
10016 } else {
10017 ValueShape::Or(left, right)
10018 }
10019 }
10020 Expression::UnaryExpression(unary)
10021 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10022 {
10023 ValueShape::Not(Box::new(value_shape(&unary.argument, next)))
10024 }
10025 _ => {
10026 let index = *next;
10027 *next += 1;
10028 ValueShape::Condition(index)
10029 }
10030 }
10031}
10032
10033fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
10034 match expression {
10035 Expression::ParenthesizedExpression(parenthesized) => {
10036 collect_conditions(&parenthesized.expression, conditions);
10037 }
10038 Expression::LogicalExpression(logical)
10039 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
10040 {
10041 collect_conditions(&logical.left, conditions);
10042 collect_conditions(&logical.right, conditions);
10043 }
10044 Expression::UnaryExpression(unary)
10045 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
10046 {
10047 collect_conditions(&unary.argument, conditions);
10048 }
10049 _ => conditions.push(expression.span()),
10050 }
10051}
10052
10053fn source_slice(source: &str, span: Span) -> &str {
10054 &source[span.start as usize..span.end as usize]
10055}
10056
10057struct SourcePositions {
10066 address: usize,
10067 length: usize,
10068 sample: u64,
10069 line_starts: Vec<usize>,
10070 utf16_before_line: Vec<usize>,
10071 checkpoints: Vec<Option<Vec<(usize, usize)>>>,
10075}
10076
10077impl SourcePositions {
10078 fn sample(source: &str) -> u64 {
10079 let bytes = source.as_bytes();
10080 let head = &bytes[..bytes.len().min(64)];
10081 let tail = &bytes[bytes.len().saturating_sub(64)..];
10082 head.iter()
10083 .chain(tail)
10084 .fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
10085 (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
10086 })
10087 }
10088
10089 fn new(source: &str) -> Self {
10090 let mut line_starts = vec![0];
10091 let mut utf16_before_line = vec![0];
10092 let mut checkpoints = Vec::new();
10093 let mut marks: Option<Vec<(usize, usize)>> = None;
10094 let mut utf16 = 0;
10095 let mut column = 0;
10096 let mut line_start = 0;
10097 for (index, character) in source.char_indices() {
10098 if !character.is_ascii() && marks.is_none() {
10099 marks = Some(vec![(0, 0)]);
10100 }
10101 if let Some(marks) = &mut marks
10102 && column >= marks.last().expect("a first mark").1 + CHECKPOINT
10103 {
10104 marks.push((index - line_start, column));
10105 }
10106 utf16 += character.len_utf16();
10107 column += character.len_utf16();
10108 if character == '\n' {
10109 line_start = index + 1;
10110 line_starts.push(line_start);
10111 utf16_before_line.push(utf16);
10112 checkpoints.push(marks.take());
10113 column = 0;
10114 }
10115 }
10116 checkpoints.push(marks);
10117 Self {
10118 address: source.as_ptr() as usize,
10119 length: source.len(),
10120 sample: Self::sample(source),
10121 line_starts,
10122 utf16_before_line,
10123 checkpoints,
10124 }
10125 }
10126
10127 fn describes(&self, source: &str) -> bool {
10128 self.address == source.as_ptr() as usize
10129 && self.length == source.len()
10130 && self.sample == Self::sample(source)
10131 }
10132
10133 fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
10135 let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
10136 let start = self.line_starts[line];
10137 let column = match &self.checkpoints[line] {
10138 None => offset - start,
10139 Some(marks) => {
10140 let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
10141 let (byte, column) = marks[at];
10142 column + source[start + byte..offset].encode_utf16().count()
10143 }
10144 };
10145 (line, column)
10146 }
10147}
10148
10149thread_local! {
10150 static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
10151 const { std::cell::RefCell::new(None) };
10152}
10153
10154pub(crate) struct PositionScope {
10159 owner: bool,
10160}
10161
10162impl PositionScope {
10163 pub(crate) fn enter() -> Self {
10164 let owner = POSITIONS.with(|positions| {
10165 let mut positions = positions.borrow_mut();
10166 let owner = positions.is_none();
10167 if owner {
10168 *positions = Some(Vec::new());
10169 }
10170 owner
10171 });
10172 Self { owner }
10173 }
10174}
10175
10176impl Drop for PositionScope {
10177 fn drop(&mut self) {
10178 if self.owner {
10179 POSITIONS.with(|positions| *positions.borrow_mut() = None);
10180 }
10181 }
10182}
10183
10184fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
10185 POSITIONS.with(|positions| {
10186 let mut positions = positions.borrow_mut();
10187 let known = positions.as_mut()?;
10188 if let Some(found) = known.iter().find(|index| index.describes(source)) {
10189 return Some(found.clone());
10190 }
10191 let built = std::rc::Rc::new(SourcePositions::new(source));
10192 known.push(built.clone());
10193 Some(built)
10194 })
10195}
10196
10197fn utf16_offset(source: &str, offset: usize) -> usize {
10198 match indexed(source) {
10199 Some(index) => {
10200 let (line, column) = index.locate(source, offset);
10201 index.utf16_before_line[line] + column
10202 }
10203 None => source[..offset].encode_utf16().count(),
10204 }
10205}
10206
10207pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
10208 if let Some(index) = indexed(source) {
10209 let (line, column) = index.locate(source, offset);
10210 return (line + 1, column + 1);
10211 }
10212 let prefix = &source[..offset];
10213 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
10214 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
10215 let column = source[line_start..offset].encode_utf16().count() + 1;
10216 (line, column)
10217}
10218
10219fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
10220 let start = utf16_offset(source, span.start as usize);
10221 let end = utf16_offset(source, span.end as usize);
10222 let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
10223 let mut id = String::with_capacity(16);
10224 for byte in &digest[..8] {
10225 write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
10226 }
10227 id
10228}
10229
10230#[cfg(test)]
10231mod tests {
10232 use super::*;
10233
10234 #[test]
10235 fn line_index_lookups_agree_with_walking_the_line() {
10236 let long = "é🚀x".repeat(700);
10237 let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
10238 let index = Utf16LineIndex::new(&source);
10239 for (line, text) in source.split_inclusive('\n').enumerate() {
10240 let start = index.starts[line];
10241 let width: usize = text.chars().map(char::len_utf16).sum();
10242 for column in 0..=width + 2 {
10243 assert_eq!(
10244 index.byte_offset(line, column),
10245 start + utf16_col_to_byte(text, column),
10246 "line {line} column {column}"
10247 );
10248 }
10249 for (offset, _) in text.char_indices() {
10250 let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
10251 assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
10252 }
10253 }
10254 }
10255
10256 #[test]
10257 fn indexed_positions_agree_with_counting_from_the_top() {
10258 let source = &format!(
10260 "const s = 'naïve 🚀';\n\nexport function f(a) {{\n return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
10261 "é🚀x".repeat(400)
10262 );
10263 let source = source.as_str();
10264 let counted: Vec<_> = source
10265 .char_indices()
10266 .map(|(offset, _)| {
10267 (
10268 line_and_utf16_column(source, offset),
10269 stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10270 )
10271 })
10272 .collect();
10273 let _positions = PositionScope::enter();
10274 for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
10275 assert_eq!(
10276 line_and_utf16_column(source, offset),
10277 position,
10278 "offset {offset}"
10279 );
10280 assert_eq!(
10281 stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
10282 id
10283 );
10284 }
10285 }
10286
10287 #[test]
10288 fn classic_scripts_share_application_globals_but_not_probe_bindings() {
10289 let a = instrument_direct_candidate(
10293 "var count = 0; function increment() { if (count >= 0) { count += 1; } }",
10294 "public/a.js",
10295 )
10296 .unwrap();
10297 let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
10298 .unwrap();
10299 let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
10300 let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
10301 let script = format!(
10302 "const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
10303 vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
10304 if (ctx.observed !== 1) throw Error('shared globals changed');\
10305 vm.runInContext({}, ctx);\
10306 if (ctx.observed !== 2) throw Error('a repeated script changed');",
10307 serde_json::to_string(runtime).unwrap(),
10308 serde_json::to_string(&a.code).unwrap(),
10309 serde_json::to_string(&b.code).unwrap(),
10310 serde_json::to_string(&b.code).unwrap()
10311 );
10312 let mut node = std::process::Command::new("node")
10315 .arg("-")
10316 .stdin(std::process::Stdio::piped())
10317 .stdout(std::process::Stdio::piped())
10318 .stderr(std::process::Stdio::piped())
10319 .spawn()
10320 .expect("Node.js for the JavaScript regression");
10321 std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
10322 let result = node.wait_with_output().unwrap();
10323 assert!(
10324 result.status.success(),
10325 "{}",
10326 String::from_utf8_lossy(&result.stderr)
10327 );
10328 }
10329
10330 #[test]
10331 fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
10332 for file in [
10335 "src/index.js",
10336 "src/index.jsx",
10337 "src/index.mjs",
10338 "src/index.cjs",
10339 "src/index.tsx",
10340 ] {
10341 let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
10342 let output = instrument_candidate(source, file).unwrap();
10343 assert!(!output.points.is_empty(), "{file}");
10344 assert!(output.code.contains("<div>"), "{file}");
10345 }
10346 assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
10348 for file in ["a.js", "a.cjs", "a.mjs"] {
10350 let before = SourceType::from_path(file).unwrap();
10351 let after = project_source_type(Path::new(file)).unwrap();
10352 assert_eq!(after.module_kind(), before.module_kind(), "{file}");
10353 assert!(after.is_jsx(), "{file}");
10354 }
10355 }
10356
10357 const SOURCE: &str =
10358 "export function decide(a,b,c) {\n if ((a && b) || !c) return 1;\n return 0;\n}\n";
10359
10360 #[test]
10361 fn matches_the_frozen_if_decision_manifest_exactly() {
10362 let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
10363 assert!(!output.complete);
10364 assert_eq!(
10365 output.decisions,
10366 vec![CandidateDecision {
10367 id: "2f65989a5782c5bd".to_string(),
10368 file: "app/decide.ts".to_string(),
10369 line: 2,
10370 column: 7,
10371 source: "(a && b) || !c".to_string(),
10372 conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
10373 kind: "if".to_string(),
10374 }]
10375 );
10376 }
10377
10378 #[test]
10379 fn a_test_files_unchanged_text_keeps_its_columns() {
10380 let source = "import { expect } from 'vitest';\n expect(value(1)).toBe(2);\n";
10384 let wrapper = "wrap(() => ";
10385 let edits = vec![
10386 TextEdit {
10387 start: source.find("expect(value").unwrap(),
10388 end: source.find("expect(value").unwrap(),
10389 text: wrapper.into(),
10390 rank: 0,
10391 },
10392 TextEdit {
10393 start: source.rfind(';').unwrap(),
10394 end: source.rfind(';').unwrap(),
10395 text: ")".into(),
10396 rank: 0,
10397 },
10398 ];
10399 let (code, map) = apply_text_edits(source, edits, "a.test.js").unwrap();
10400 assert_eq!(
10401 code,
10402 "import { expect } from 'vitest';\n wrap(() => expect(value(1)).toBe(2));\n"
10403 );
10404 let original = |line: u32, column: u32| {
10405 map.get_tokens()
10406 .filter(|token| token.get_dst_line() == line && token.get_dst_col() <= column)
10407 .max_by_key(|token| token.get_dst_col())
10408 .map(|token| (token.get_src_line(), token.get_src_col()))
10409 };
10410 let matcher = code.lines().nth(1).unwrap().find("toBe").unwrap() as u32;
10412 assert_eq!(original(1, matcher), Some((1, 19)));
10413 assert_eq!(original(0, 9), Some((0, 9)));
10415 assert_eq!(original(0, 7), Some((0, 7)));
10416 assert_eq!(original(1, 4), Some((1, 0)));
10418 let name_at = |line: u32, column: u32| {
10421 map.get_tokens()
10422 .find(|token| token.get_dst_line() == line && token.get_dst_col() == column)
10423 .and_then(|token| token.get_name_id())
10424 .and_then(|id| map.get_name(id))
10425 .map(|name| name.to_string())
10426 };
10427 assert_eq!(name_at(1, matcher).as_deref(), Some("toBe"));
10428 assert_eq!(name_at(0, 9).as_deref(), Some("expect"));
10429 assert_eq!(name_at(0, 18), None, "`from` is a keyword");
10430 }
10431
10432 #[test]
10433 fn a_server_entry_keeps_what_it_exports_by_default() {
10434 for (source, export) in [
10438 (
10439 "function handleRequest(r) { return r; }\nexport default handleRequest;\n",
10440 "export default __supercovWithRequestPhase13(handleRequest);",
10441 ),
10442 (
10443 "export default class Entry {}\n",
10444 "export default class Entry {}",
10445 ),
10446 ] {
10447 let output = instrument_candidate_with_binding(
10448 source,
10449 "app/entry.server.ts",
10450 RuntimeBinding::ModuleImport,
10451 None,
10452 false,
10453 )
10454 .unwrap();
10455 assert!(
10456 output.code.lines().any(|line| line == export),
10457 "{export}\n{}",
10458 output.code
10459 );
10460 }
10461 }
10462
10463 #[test]
10464 fn a_method_read_through_a_property_keeps_its_text() {
10465 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";
10468 let output = instrument_direct_candidate(source, "src/methods.js").unwrap();
10469 for text in [
10470 "method(v) { return v * 2; }",
10471 "area(w) { return w * w; }",
10472 "() => new Shape()",
10473 ] {
10474 assert!(output.code.contains(text), "{text}\n{}", output.code);
10475 }
10476 assert!(
10477 !output.code.contains("untouched(v) { return v + 1; }"),
10478 "{}",
10479 output.code
10480 );
10481 }
10482
10483 #[test]
10484 fn a_function_whose_source_is_read_keeps_its_text_exactly() {
10485 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";
10488 let output = instrument_direct_candidate(source, "src/text.js").unwrap();
10489 for text in [
10490 "function* g() { yield 1; }",
10491 "(a, b) => a + b",
10492 "async function later(x) {\n // kept\n if (x) {\n return x;\n }\n return 0;\n}",
10493 ] {
10494 assert!(output.code.contains(text), "{text}\n{}", output.code);
10495 }
10496 assert!(
10498 !output
10499 .code
10500 .contains("function counted(n) { return n > 1 ? 'many' : 'one'; }"),
10501 "{}",
10502 output.code
10503 );
10504 let typed = "export function twice(value: number): number { return value * 2; }\nexport const text = twice.toString();\n";
10506 let output = instrument_direct_candidate(typed, "src/typed.ts").unwrap();
10507 assert!(
10508 output
10509 .code
10510 .contains("function twice(value: number): number { return value * 2; }"),
10511 "{}",
10512 output.code
10513 );
10514 }
10515
10516 #[test]
10517 fn codegen_output_reparses_for_typescript_and_tsx() {
10518 for (file, source) in [
10519 (
10520 "component.tsx",
10521 "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
10522 ),
10523 ("module.ts", SOURCE),
10524 ] {
10525 let output = analyze_candidate(source, file).unwrap();
10526 let map = output.map.as_ref().expect("candidate source map");
10527 assert_eq!(map["version"], 3);
10528 assert_eq!(map["sources"][0], file);
10529 assert_eq!(map["sourcesContent"][0], source);
10530 assert!(
10531 map["mappings"]
10532 .as_str()
10533 .is_some_and(|value| !value.is_empty())
10534 );
10535 let allocator = Allocator::default();
10536 let source_type = SourceType::from_path(file).unwrap();
10537 let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
10538 assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
10539 }
10540 }
10541
10542 #[test]
10543 fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
10544 for file in ["src/module.mjs", "src/script.cjs"] {
10545 let output = instrument_direct_candidate(
10546 "export const selected = value => value ? 1 : 0;",
10547 file,
10548 )
10549 .unwrap();
10550 assert!(
10551 output
10552 .code
10553 .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
10554 );
10555 assert!(!output.code.contains("virtual:supercov-runtime"));
10556 assert!(!output.code.contains("globalThis.__supercovRuntime"));
10557 }
10558 }
10559
10560 #[test]
10561 fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
10562 let source = concat!(
10563 "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10564 "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
10565 "ordinaryHelper();\n",
10566 );
10567 let output = instrument_node_assertion_phases_with_runtime_hooks(
10568 source,
10569 "tests/runner.mjs",
10570 &[],
10571 Some("../.supercov/launchSupervisor.mjs"),
10572 )
10573 .unwrap();
10574 assert_eq!(output.assertions, 0);
10575 assert_eq!(output.capability_imports, 1);
10576 assert!(output.code.contains("wrapImportedCapability"));
10577 assert!(output.code.contains("__supercovRawImageBuilder"));
10578 assert!(output.code.contains("const ImageBuilder"));
10579 assert!(!output.code.contains("__supercovRawordinaryHelper"));
10580 assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
10581 }
10582
10583 #[test]
10584 fn capability_imports_accept_a_computed_guest_mount_path() {
10585 let source = concat!(
10586 "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
10587 "const guestRoot = resolve(tmpdir(), 'workspace');\n",
10588 "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
10589 "ordinaryHelper();\n",
10590 );
10591 let output = instrument_node_assertion_phases_with_runtime_hooks(
10592 source,
10593 "tests/runner.mjs",
10594 &[],
10595 Some("../.supercov/launchSupervisor.mjs"),
10596 )
10597 .unwrap();
10598 assert_eq!(output.capability_imports, 1);
10599 assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
10600 assert!(!output.code.contains("__supercovRawordinaryHelper"));
10601 }
10602
10603 #[test]
10604 fn capability_import_selection_follows_a_mapping_variable() {
10605 let source = concat!(
10606 "import { launch, unrelated } from './sdk.mjs';\n",
10607 "run();\n",
10608 "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
10609 "function run() { launch(options); unrelated(); }\n",
10610 );
10611 let output = instrument_node_assertion_phases_with_runtime_hooks(
10612 source,
10613 "tests/runner.mjs",
10614 &[],
10615 Some("../.supercov/launchSupervisor.mjs"),
10616 )
10617 .unwrap();
10618 assert_eq!(output.capability_imports, 1);
10619 assert!(output.code.contains("__supercovRawlaunch"));
10620 assert!(!output.code.contains("__supercovRawunrelated"));
10621 }
10622
10623 #[test]
10624 fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
10625 let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
10626 let output = instrument_node_assertion_phases_with_runtime_hooks(
10627 source,
10628 "src/main.mjs",
10629 &[],
10630 Some("../.supercov/launchSupervisor.mjs"),
10631 )
10632 .unwrap();
10633 assert_eq!(output.code, source);
10634 assert_eq!(output.capability_imports, 0);
10635 }
10636
10637 #[test]
10638 fn ordinary_mount_language_does_not_enable_capability_proxies() {
10639 let source = concat!(
10640 "import { render } from '@testing-library/react';\n",
10641 "it('mounts into a host element', () => render(<main />));\n",
10642 );
10643 let output = instrument_node_assertion_phases_with_runtime_hooks(
10644 source,
10645 "tests/component.test.tsx",
10646 &[],
10647 Some("../.supercov/launchSupervisor.mjs"),
10648 )
10649 .unwrap();
10650 assert_eq!(output.capability_imports, 0);
10651 assert!(!output.code.contains("wrapImportedCapability"));
10652 }
10653
10654 #[test]
10655 fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
10656 let source = concat!(
10657 "import { test, expect } from '@example/test-admin';\n",
10658 "test('visual snapshot', async ({ page }) => {\n",
10659 " expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
10660 "});\n",
10661 );
10662 let output = instrument_node_assertion_phases_with_runtime_hooks(
10663 source,
10664 "tests/visual.spec.ts",
10665 &[],
10666 Some("../.supercov/launchSupervisor.mjs"),
10667 )
10668 .unwrap();
10669 assert_eq!(output.capability_imports, 0);
10670 assert!(!output.code.contains("wrapImportedCapability"));
10671 }
10672
10673 #[test]
10674 fn capability_rewrite_excludes_runtime_and_type_only_imports() {
10675 let source = concat!(
10676 "import type { Machine } from './types.ts';\n",
10677 "import { test } from 'node:test';\n",
10678 "import { runtime } from '../.supercov/runtime.mjs';\n",
10679 "console.log({ machine: true, test, runtime });\n",
10680 );
10681 let output = instrument_node_assertion_phases_with_runtime_hooks(
10682 source,
10683 "tests/runner.ts",
10684 &[],
10685 Some("../.supercov/launchSupervisor.mjs"),
10686 )
10687 .unwrap();
10688 assert_eq!(output.code, source);
10689 assert_eq!(output.capability_imports, 0);
10690 }
10691
10692 #[test]
10693 fn native_assertion_arguments_execute_inside_the_assertion_phase() {
10694 let source = concat!(
10695 "import assert from 'node:assert/strict';\n",
10696 "assert.equal(value(), 1);\n",
10697 );
10698 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10699 assert_eq!(output.assertions, 1);
10700 assert!(output.code.contains("withNodeAssertionPhase"));
10701 assert!(output.code.contains("node:assert/strict.equal"));
10702 assert!(output.code.contains("tests/value.test.mjs:2:1"));
10703 assert!(output.code.contains("assert.equal(value(), 1)"));
10704 }
10705
10706 #[test]
10707 fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
10708 let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
10709 let output = instrument_node_assertion_phases_with_runtime_imports(
10710 source,
10711 "tests/value.test.mjs",
10712 &[],
10713 None,
10714 Some("../.supercov/node_modules/bootstrap.cjs"),
10715 )
10716 .unwrap();
10717 assert_eq!(output.assertions, 1);
10718 let lines = output.code.lines().collect::<Vec<_>>();
10720 assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
10721 assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
10722 assert!(lines[0].ends_with("import assert from 'node:assert';"));
10723 assert!(lines[1].contains("withNodeAssertionPhase"));
10724 let allocator = Allocator::default();
10725 assert!(
10726 Parser::new(&allocator, &output.code, SourceType::mjs())
10727 .parse()
10728 .errors
10729 .is_empty()
10730 );
10731 }
10732
10733 #[test]
10734 fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
10735 let source = concat!(
10736 "import assert from 'node:assert/strict';\n",
10737 "export async function check() { assert.equal(await value(), 1); }\n",
10738 );
10739 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10740 assert_eq!(output.assertions, 1);
10741 assert!(output.code.contains("bindNodeAssertionPhase"));
10742 assert!(output.code.contains("await value()"));
10743 let allocator = Allocator::default();
10744 assert!(
10745 Parser::new(&allocator, &output.code, SourceType::mjs())
10746 .parse()
10747 .errors
10748 .is_empty()
10749 );
10750 }
10751
10752 #[test]
10753 fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
10754 let source = concat!(
10755 "import assert from 'node:assert/strict';\n",
10756 "export async function check(user?: string) {\n",
10757 " assert.ok(user, await value());\n",
10758 " assert\n",
10759 " .equal(await value(), 1)\n",
10760 "}\n",
10761 );
10762 let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
10763 assert_eq!(output.assertions, 2);
10764 let code = &output.code;
10765 let lines: Vec<&str> = code.lines().collect();
10766 assert!(
10767 lines[2].starts_with(
10768 " { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
10769 ),
10770 "{code}"
10771 );
10772 assert!(
10773 lines[2]
10774 .ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
10775 "{code}"
10776 );
10777 assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
10778 assert_eq!(
10779 lines[4], " \"equal\"); __supercovAssertionPhase(await value(), 1) }",
10780 "{code}"
10781 );
10782 assert_eq!(lines[5], "}", "{code}");
10783 let allocator = Allocator::default();
10784 assert!(
10785 Parser::new(&allocator, code, SourceType::ts())
10786 .parse()
10787 .errors
10788 .is_empty()
10789 );
10790 }
10791
10792 #[test]
10793 fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
10794 let source = concat!(
10795 "import { expect, test } from '@playwright/test';\n",
10796 "test('value', async () => { expect(await value()).toBe(1); });\n",
10797 );
10798 let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
10799 assert_eq!(output.assertions, 1);
10800 assert!(output.code.contains("bindNodeAssertionPhase"));
10801 assert!(output.code.contains("await value()"));
10802 let allocator = Allocator::default();
10803 assert!(
10804 Parser::new(&allocator, &output.code, SourceType::mjs())
10805 .parse()
10806 .errors
10807 .is_empty()
10808 );
10809 }
10810
10811 #[test]
10812 fn native_commonjs_assertion_bindings_are_attributed() {
10813 let source = concat!(
10814 "const assert = require('node:assert/strict');\n",
10815 "const { ok: verify } = require('assert');\n",
10816 "assert.equal(value(), 1);\n",
10817 "verify(other());\n",
10818 );
10819 let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10820 assert_eq!(output.assertions, 2);
10821 assert!(output.code.contains("node:assert/strict.equal"));
10822 assert!(output.code.contains("node:assert.ok"));
10823 }
10824
10825 #[test]
10826 fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
10827 let source = concat!(
10828 "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
10829 "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
10830 );
10831 let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
10832 assert_eq!(output.assertions, 1);
10833 assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10834 }
10835
10836 #[test]
10837 fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
10838 let source = concat!(
10839 "import assert from 'node:assert/strict';\n",
10840 "function unrelated(assert) { assert.equal(effect(), 1); }\n",
10841 "assert.equal(value(), 1);\n",
10842 );
10843 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10844 assert_eq!(output.assertions, 1);
10845 assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
10846 }
10847
10848 #[test]
10849 fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
10850 let source = concat!(
10851 "import test from 'node:test';\n",
10852 "import { expect as verify } from 'expect-library';\n",
10853 "test('value', () => verify(value()).not.toEqual(1));\n",
10854 );
10855 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10856 assert_eq!(output.assertions, 1);
10857 assert!(output.code.contains("expect.not.toEqual"));
10858 assert!(output.code.contains("verify(value()).not.toEqual(1)"));
10859 }
10860
10861 #[test]
10862 fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
10863 let source = concat!(
10864 "import { expect, test } from 'vitest';\n",
10865 "test('value', () => expect(value()).toBe(1));\n",
10866 );
10867 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
10868 assert_eq!(output.assertions, 1);
10869 assert!(output.code.contains("expect.toBe"));
10870 }
10871
10872 #[test]
10873 fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
10874 for (call, operation) in [
10879 (
10880 "expect.element(button).toHaveAccessibleName('Copy')",
10881 "expect.element.toHaveAccessibleName",
10882 ),
10883 ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
10884 ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
10885 ] {
10886 let source = format!(
10887 "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
10888 );
10889 let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
10890 assert_eq!(output.assertions, 1, "{call}");
10891 assert!(output.code.contains(operation), "{call} -> {operation}");
10892 }
10893 }
10894
10895 #[test]
10896 fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
10897 let source = concat!(
10902 "export function Button({ label, copied }) {\n",
10903 " return (\n",
10904 " <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
10905 " {format(copied)}\n",
10906 " </button>\n",
10907 " );\n",
10908 "}\n",
10909 );
10910 let output = instrument_candidate(source, "src/button.tsx").unwrap();
10911 let rendered = output
10912 .points
10913 .iter()
10914 .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10915 .collect::<Vec<_>>();
10916 assert_eq!(rendered.len(), 2, "{:?}", output.points);
10919 assert!(
10920 rendered
10921 .iter()
10922 .all(|point| point.source == "format(copied)")
10923 );
10924 assert_eq!(rendered[0].line, 3);
10925 assert_eq!(rendered[1].line, 4);
10926 assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
10927 assert!(!output.code.contains("title={__supercovRenderedValueV2"));
10930 assert!(!output.code.contains("), format(copied)}"));
10933 assert!(output.code.contains(", format(copied))}"));
10934 }
10935
10936 #[test]
10937 fn instrumented_jsx_stays_parseable_through_every_container_shape() {
10938 let source = concat!(
10942 "export function Panel({ items, user, error, tone }) {\n",
10943 " return (\n",
10944 " <>\n",
10945 " <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
10946 " <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
10947 " {error ? <Alert text={message(error)} /> : null}\n",
10948 " {items.map((item) => (\n",
10949 " <Item key={item.id} label={format(item)} />\n",
10950 " ))}\n",
10951 " {count(items) > 0 && <Footer total={sum(items)} />}\n",
10952 " </div>\n",
10953 " </>\n",
10954 " );\n",
10955 "}\n",
10956 );
10957 let output = instrument_candidate(source, "src/panel.tsx").unwrap();
10958 instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
10960 assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
10962 let rendered = output
10963 .points
10964 .iter()
10965 .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
10966 .map(|point| point.source.as_str())
10967 .collect::<Vec<_>>();
10968 assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
10969 assert!(
10970 rendered.contains(&"cx(\'a\', tone && \'b\')"),
10971 "{rendered:?}"
10972 );
10973 assert!(rendered.contains(&"format(item)"), "{rendered:?}");
10974 assert!(rendered.contains(&"message(error)"), "{rendered:?}");
10975 assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
10978 assert!(!rendered.contains(&"item.id"), "{rendered:?}");
10979 }
10980
10981 #[test]
10982 fn jest_global_expect_is_attributed_next_to_its_test_globals() {
10983 let source = "test('value', () => expect(value()).toBe(1));\n";
10987 let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
10988 assert_eq!(output.assertions, 1);
10989 assert!(output.code.contains("expect.toBe"));
10990 let plain = "const ok = expect(value()).toBe(1);\n";
10992 let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
10993 assert_eq!(output.assertions, 0);
10994 assert_eq!(output.code, plain);
10995 }
10996
10997 #[test]
10998 fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
10999 let source = concat!(
11000 "import { expect, test } from '@playwright/test';\n",
11001 "test('value', () => expect(value()).toBe(1));\n",
11002 );
11003 let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
11004 assert_eq!(output.assertions, 1);
11005 assert!(output.code.contains("expect.toBe"));
11006 }
11007
11008 #[test]
11013 fn an_assertion_is_recognised_however_it_was_reached_and_only_then() {
11014 for (file, source, assertions) in [
11015 (
11016 "t/a.test.js",
11017 "describe('x', () => { it('y', () => expect(a()).toBe(1)); });\n",
11018 1,
11019 ),
11020 ("t/b.test.js", "it('y', () => expect(a()).toBe(1));\n", 1),
11021 (
11022 "t/p.test.js",
11023 "describe('x', function () { expect(a()).toBe(1); });\n",
11024 1,
11025 ),
11026 ("t/q.test.js", "expect(a()).toBe(1);\n", 0),
11028 (
11029 "t/c.test.cjs",
11030 "const { expect } = require('vitest');\nexpect(a()).toBe(1);\n",
11031 1,
11032 ),
11033 (
11034 "t/d.test.cjs",
11035 "const { expect: check } = require('@jest/globals');\ncheck(a()).toBe(1);\n",
11036 1,
11037 ),
11038 (
11039 "t/i.test.mjs",
11040 "import { expect as e } from 'vitest';\ne(a()).toBe(1);\n",
11041 1,
11042 ),
11043 (
11044 "t/n.test.cjs",
11045 "const { strict } = require('node:assert');\nstrict.equal(a(), 1);\n",
11046 1,
11047 ),
11048 (
11049 "t/s.test.cjs",
11050 "const assert = require('node:assert').strict;\nassert.equal(a(), 1);\n",
11051 1,
11052 ),
11053 (
11055 "t/h.test.mjs",
11056 "import { ok, AssertionError } from 'node:assert';\nok(a());\nnew AssertionError({});\n",
11057 1,
11058 ),
11059 (
11060 "t/j.test.cjs",
11061 "const { equal, AssertionError } = require('assert');\nequal(a(), 1);\nnew AssertionError({});\n",
11062 1,
11063 ),
11064 (
11065 "t/e.test.cjs",
11066 "const { expect } = require('chai');\nexpect(a()).to.equal(1);\n",
11067 0,
11068 ),
11069 (
11070 "t/f.test.cjs",
11071 "const name = 'node:assert';\nconst assert = require(name);\nassert.ok(a());\n",
11072 0,
11073 ),
11074 (
11075 "t/o.test.cjs",
11076 "const [first] = require('node:assert');\nfirst(a());\n",
11077 0,
11078 ),
11079 (
11080 "t/m.test.mjs",
11081 "import { test } from 'node:test';\nconst expect = (v) => ({ toBe() {} });\ntest('x', () => expect(a()).toBe(1));\n",
11082 0,
11083 ),
11084 (
11085 "t/k.test.mjs",
11086 "import assert from 'node:assert';\nassert?.ok(a());\nassert.ok?.(b());\n",
11087 0,
11088 ),
11089 ] {
11090 let output = instrument_node_assertion_phases(source, file).unwrap();
11091 assert_eq!(output.assertions, assertions, "{file}");
11092 assert_eq!(
11093 output.code.matches("withNodeAssertionPhase").count(),
11094 assertions,
11095 "{file}"
11096 );
11097 }
11098 }
11099
11100 #[test]
11104 fn only_a_host_to_guest_mapping_makes_a_capability_candidate() {
11105 assert!(capability_source_candidate(
11106 "run({ hostPath: a, guestPath: b })"
11107 ));
11108 assert!(capability_source_candidate(
11109 "run({ hostRoot: a, guestRoot: b })"
11110 ));
11111 assert!(capability_source_candidate(
11112 "start({ Mounts: [{ source: a, target: b }] })"
11113 ));
11114 assert!(capability_source_candidate(
11115 "start({ mounts: [{ source: a, target: b }] })"
11116 ));
11117 for unrelated in [
11118 "run({ hostPath: a })",
11119 "run({ hostRoot: a, guestPath: b })",
11120 "start({ mounts: [{ source: a }] })",
11121 "start({ source: a, target: b })",
11122 "snapshot(machine.spawn())",
11123 ] {
11124 assert!(!capability_source_candidate(unrelated), "{unrelated}");
11125 }
11126 let wrapper = "./.supercov/capability.mjs";
11127 for excluded in [
11128 wrapper,
11129 "node:child_process",
11130 "virtual:supercov-runtime",
11131 "../.supercov/node_modules/x.mjs",
11132 "vitest",
11133 "@playwright/test",
11134 ] {
11135 assert!(excluded_capability_import(excluded, wrapper), "{excluded}");
11136 }
11137 assert!(!excluded_capability_import("e2b", wrapper));
11138 }
11139
11140 #[test]
11141 fn an_arrow_that_returns_an_assertion_still_returns_it() {
11142 let source = concat!(
11145 "import { expect, test } from '@playwright/test';\n",
11146 "const loginError = (page: Page, error: string) => expect(page).toHaveURL(error);\n",
11147 "test('value', () => expect(value()).toBe(1));\n",
11148 );
11149 for file in ["tests/login.spec.ts", "tests/login.spec.js"] {
11150 let source = if file.ends_with(".js") {
11151 source.replace(": Page", "").replace(": string", "")
11152 } else {
11153 source.into()
11154 };
11155 let output = instrument_node_assertion_phases(&source, file).unwrap();
11156 assert_eq!(output.assertions, 2, "{file}");
11157 let code = output.code.split("\n//# sourceMappingURL").next().unwrap();
11158 assert!(!code.contains("=> { const"), "{file}:\n{code}");
11159 assert!(!code.contains("finally"), "{file}:\n{code}");
11160 assert!(
11161 code.contains("expect(page).toHaveURL(error)"),
11162 "{file}:\n{code}"
11163 );
11164 }
11165 }
11166
11167 #[test]
11168 fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
11169 let source = concat!(
11170 "import { browserTest, expect } from '@acme/browser-fixtures';\n",
11171 "browserTest('value', () => expect(value()).toBe(1));\n",
11172 );
11173 let output = instrument_node_assertion_phases_with_expect_modules(
11174 source,
11175 "tests/value.spec.mjs",
11176 &["@acme/browser-fixtures".into()],
11177 )
11178 .unwrap();
11179 assert_eq!(output.assertions, 1);
11180 assert!(output.code.contains("expect.toBe"));
11181 }
11182
11183 #[test]
11184 fn preserves_comment_payloads_byte_for_byte() {
11185 let comment = "/*---\ndescription: >\n nested indentation is data\ninfo: |\n first\n second\n---*/";
11186 let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
11187 let output = instrument_candidate(&source, "app/comments.js").unwrap();
11188 assert!(output.code.contains(comment), "{}", output.code);
11189 }
11190
11191 #[test]
11192 fn source_map_destinations_follow_restored_comments() {
11193 let source = "function run() {\n // first omitted comment\n // second omitted comment\n return 1;\n}\n";
11194 let output = analyze_candidate(source, "app/comment-map.js").unwrap();
11195 let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
11196 let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
11197 let return_token = map
11198 .get_tokens()
11199 .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
11200 .expect("return token mapping");
11201 let offset = Utf16LineIndex::new(&output.code).byte_offset(
11202 return_token.get_dst_line() as usize,
11203 return_token.get_dst_col() as usize,
11204 );
11205 assert!(
11206 output.code[offset..].starts_with("return"),
11207 "{}",
11208 output.code
11209 );
11210 }
11211
11212 #[test]
11213 fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
11214 for source in [
11225 "// </script>\nexport const x = 1;\n",
11226 "// </script\nexport const x = 1;\n",
11227 "// </SCRIPT>\nexport const x = 1;\n",
11228 "// see the </script> tag here\nexport const x = 1;\n",
11229 "/* block </script> here */\nexport const x = 1;\n",
11230 "/** jsdoc </script> */\nexport const x = 1;\n",
11231 "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
11233 ] {
11234 let output = instrument_candidate(source, "src/probe.ts")
11235 .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
11236 let original = source.lines().next().expect("a comment");
11237 assert!(
11238 output.code.contains(original),
11239 "comment was not restored verbatim: {original:?} missing from {:?}",
11240 output.code
11241 );
11242 }
11243 let written = "// author wrote <\\/script>\nexport const x = 1;\n";
11246 let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
11247 assert!(
11248 output.code.contains("// author wrote <\\/script>"),
11249 "{:?}",
11250 output.code
11251 );
11252 assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
11255 assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
11256 assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
11257 }
11258
11259 #[test]
11260 fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
11261 let error = CandidateError::CommentPreservation {
11266 expected: 116,
11267 actual: 112,
11268 detail:
11269 "no source comment matches the generated comment \"// x\" (matched 97 before it)"
11270 .to_owned(),
11271 };
11272 let rendered = format!("{error:?}");
11273 assert!(rendered.contains("no source comment matches"), "{rendered}");
11274 assert!(rendered.contains("matched 97 before it"), "{rendered}");
11275 }
11276
11277 #[test]
11278 fn a_wrapped_default_keeps_the_type_its_syntax_names() {
11279 let source = concat!(
11283 "export function f(a: number, strict = false, limit = 10, name = `x`, big = 1n, low = -1) { return a; }\n",
11284 "export function g<E extends 'utf8' | false = 'utf8'>(encoding = 'utf8' as E, list = [1], kept: string = 'a') { return encoding; }\n",
11285 "export const h = ({ open = false }, mode = 'x' as const) => open;\n",
11286 );
11287 let output = instrument_candidate(source, "src/defaults.ts").unwrap();
11288 for annotated in [
11289 "strict: boolean = ",
11290 "limit: number = ",
11291 "name: string = ",
11292 "big: bigint = ",
11293 "low: number = ",
11294 "encoding: E = ",
11295 "kept: string = ",
11296 ] {
11297 assert!(
11298 output.code.contains(annotated),
11299 "{annotated}\n{}",
11300 output.code
11301 );
11302 }
11303 for bare in ["list = ", "mode = "] {
11306 assert!(output.code.contains(bare), "{bare}\n{}", output.code);
11307 }
11308 assert!(!output.code.contains("list:"), "{}", output.code);
11309 assert!(!output.code.contains("mode:"), "{}", output.code);
11310 let script = instrument_candidate(
11312 "export function f(strict = false) { return strict; }\n",
11313 "src/defaults.js",
11314 )
11315 .unwrap();
11316 assert!(!script.code.contains("strict:"), "{}", script.code);
11317 }
11318
11319 #[test]
11320 fn an_asserted_assignment_target_is_printed_as_a_target() {
11321 let source = concat!(
11325 "export function join(buffer: [unknown], child: string, n: { v?: number }) {\n",
11326 " ;(buffer[0] as string) += child\n",
11327 " ;(buffer[0] as string) = child\n",
11328 " ;(n.v as number)++\n",
11329 " ;(n.v satisfies number | undefined) = 1\n",
11330 " ;[(buffer[0] as string)] = [child]\n",
11331 " for ((buffer[0] as string) of [child]) {}\n",
11332 " return buffer\n",
11333 "}\n",
11334 );
11335 let output = instrument_candidate(source, "src/targets.ts").unwrap();
11336 assert!(!output.code.contains(" as string"), "{}", output.code);
11337 assert!(!output.code.contains("satisfies"), "{}", output.code);
11338 assert!(
11339 output.code.contains("buffer[0] += child"),
11340 "{}",
11341 output.code
11342 );
11343 assert!(output.code.contains("n.v++"), "{}", output.code);
11344 let allocator = Allocator::default();
11345 let reparsed = Parser::new(&allocator, &output.code, SourceType::ts()).parse();
11346 assert!(
11347 reparsed.errors.is_empty(),
11348 "{:?}\n{}",
11349 reparsed.errors,
11350 output.code
11351 );
11352 }
11353
11354 #[test]
11355 fn a_comment_the_printer_moves_is_still_recognised() {
11356 let source = concat!(
11361 "// first\n",
11362 "// @__NO_SIDE_EFFECTS__\n",
11363 "/** documented\n */\n",
11364 "export function f(a: number) {\n",
11365 " return a > 1 ? a : 0;\n",
11366 "}\n",
11367 "\n",
11368 "// second\n",
11369 "export type T = number;\n",
11370 );
11371 let output = instrument_candidate(source, "src/annotated.ts").unwrap();
11372 for comment in [
11373 "// first",
11374 "// @__NO_SIDE_EFFECTS__",
11375 "/** documented\n */",
11376 "// second",
11377 ] {
11378 assert_eq!(
11379 output.code.matches(comment).count(),
11380 1,
11381 "{comment}\n{}",
11382 output.code
11383 );
11384 }
11385 }
11386
11387 #[test]
11388 fn restored_comments_never_split_a_keyword_from_its_argument() {
11389 let source = concat!(
11397 "export const A = ({ open }) => {\n",
11398 " return (\n",
11399 " // leading comment before JSX\n",
11400 " <div aria-expanded={open}>a</div>\n",
11401 " )\n",
11402 "}\n",
11403 "export const B = ({ open }) => (\n",
11404 " // comment in an expression body\n",
11405 " <div aria-expanded={open}>b</div>\n",
11406 ")\n",
11407 "export function C(x) {\n",
11408 " return ( // inline line comment\n",
11409 " x + 1\n",
11410 " )\n",
11411 "}\n",
11412 "export function D(x) {\n",
11413 " throw (\n",
11414 " /* block\n comment */\n",
11415 " new Error(String(x))\n",
11416 " )\n",
11417 "}\n",
11418 "export function E(x) {\n",
11419 " return ( /* inline block */ x + 2 )\n",
11420 "}\n",
11421 );
11422 let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
11423 for keyword in ["return ", "throw "] {
11424 for (index, _) in output.code.match_indices(keyword) {
11425 let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
11426 let block_with_line_break = rest.starts_with("/*")
11427 && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
11428 assert!(
11429 !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
11430 "`{keyword}` is separated from its argument:\n{}",
11431 output.code
11432 );
11433 }
11434 }
11435 for comment in [
11436 "// leading comment before JSX",
11437 "// comment in an expression body",
11438 "// inline line comment",
11439 "/* block\n comment */",
11440 "/* inline block */",
11441 ] {
11442 assert!(
11443 output.code.contains(comment),
11444 "{comment} was lost:\n{}",
11445 output.code
11446 );
11447 }
11448 assert!(
11449 output
11450 .code
11451 .contains("return <div aria-expanded={open}>a</div>"),
11452 "{}",
11453 output.code
11454 );
11455 assert!(
11456 output
11457 .code
11458 .contains("return <div aria-expanded={open}>b</div>"),
11459 "{}",
11460 output.code
11461 );
11462 let allocator = Allocator::default();
11464 let reparsed = Parser::new(
11465 &allocator,
11466 &output.code,
11467 SourceType::from_path("app/components/List.tsx").unwrap(),
11468 )
11469 .parse();
11470 assert!(
11471 reparsed.errors.is_empty(),
11472 "{:?}\n{}",
11473 reparsed.errors,
11474 output.code
11475 );
11476 }
11477
11478 #[test]
11479 fn preserves_reference_order_across_every_control_decision_kind() {
11480 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}";
11481 let output = instrument_candidate(source, "app/control.js").unwrap();
11482 assert_eq!(
11483 output
11484 .decisions
11485 .iter()
11486 .map(|decision| decision.kind.as_str())
11487 .collect::<Vec<_>>(),
11488 vec!["ternary", "while", "do-while", "for", "if"]
11489 );
11490 }
11491
11492 #[test]
11493 fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
11494 let entered = |source: &str| {
11498 let output = analyze_candidate(source, "src/loops.ts").unwrap();
11499 output
11500 .branches
11501 .iter()
11502 .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
11503 .count()
11504 };
11505 for source in [
11507 "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11508 "export function f(g) { for (const x of [1]) g(x); }\n",
11509 "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
11510 "export function f(g) { for (const x of [,]) g(x); }\n",
11512 "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
11514 ] {
11515 assert_eq!(entered(source), 0, "should be folded: {source}");
11516 }
11517 for source in [
11519 "export function f(g, ys) { for (const x of ys) g(x); }\n",
11520 "export function f(g) { for (const x of []) g(x); }\n",
11521 "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
11522 "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
11523 "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
11525 "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
11526 "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
11527 concat!(
11529 "const xs = [1, 2];\n",
11530 "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
11531 ),
11532 "export function f(g) { for (const k in [1, 2]) g(k); }\n",
11534 ] {
11535 assert_eq!(entered(source), 1, "should be kept: {source}");
11536 }
11537 let output = analyze_candidate(
11539 "export function f(g) { for (const x of [1, 2]) g(x); }\n",
11540 "src/loops.ts",
11541 )
11542 .unwrap();
11543 assert!(
11544 output
11545 .points
11546 .iter()
11547 .any(|point| point.source.contains("g(x)")),
11548 "the body must still be an obligation"
11549 );
11550 }
11551
11552 #[test]
11553 fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
11554 let source = concat!(
11555 "export function run(value) {\n",
11556 " while (true) { break; }\n",
11557 " do { value += 1; } while (false);\n",
11558 " if (false) value += 2;\n",
11559 " if (value || true) value += 3;\n",
11560 " if (value && false) value += 4;\n",
11561 " if (value) value += 5;\n",
11562 " return value;\n",
11563 "}\n",
11564 );
11565 let output = analyze_candidate(source, "src/constants.js").unwrap();
11566 assert_eq!(output.decisions.len(), 1);
11567 assert_eq!(output.decisions[0].source, "value");
11568
11569 let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
11570 assert_eq!(instrumented.decisions.len(), 1);
11571 assert_eq!(instrumented.decisions[0].source, "value");
11572 }
11573
11574 #[test]
11575 fn leaves_compile_time_style_macro_arguments_as_source() {
11576 let source = concat!(
11579 "import * as stylex from '@stylexjs/stylex';\n",
11580 "const styles = stylex.create({\n",
11581 " container: { display: 'flex' },\n",
11582 " paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
11583 " tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
11584 "});\n",
11585 "export function render(spacing: number) {\n",
11586 " return stylex.props(styles.container, styles.paddingTop(spacing));\n",
11587 "}\n",
11588 );
11589 let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
11590 assert!(
11591 output.code.contains("=> ({ paddingTop: `${spacing}px` })")
11592 || output
11593 .code
11594 .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
11595 "dynamic style arrow must stay an expression body:\n{}",
11596 output.code
11597 );
11598 assert!(
11599 !output
11600 .decisions
11601 .iter()
11602 .any(|decision| decision.source.contains("dark")),
11603 "no decision may be collected inside the macro argument"
11604 );
11605 let function_points = output
11606 .points
11607 .iter()
11608 .filter(|point| point.kind == "function")
11609 .map(|point| point.source.as_str())
11610 .collect::<Vec<_>>();
11611 assert!(
11612 function_points
11613 .iter()
11614 .all(|source| source.contains("render")),
11615 "only the real function may carry a function point: {function_points:?}"
11616 );
11617 assert!(
11618 output.limitations.is_empty(),
11619 "compiled-away code is not a limitation: {:?}",
11620 output.limitations
11621 );
11622 }
11623
11624 #[test]
11625 fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
11626 let source = concat!(
11627 "declare global {\n",
11628 " var Beacon: undefined | ((action: string) => void);\n",
11629 "}\n",
11630 "declare module 'virtual:feature' {\n",
11631 " export const enabled: boolean;\n",
11632 "}\n",
11633 "declare const buildOnly: string;\n",
11634 "export const live = 1;\n",
11635 );
11636 let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
11637 let statements = output
11638 .points
11639 .iter()
11640 .filter(|point| point.kind == "statement")
11641 .map(|point| point.source.as_str())
11642 .collect::<Vec<_>>();
11643 assert_eq!(statements, ["const live = 1;"]);
11644 assert!(output.code.contains("const live = 1"));
11645 }
11646
11647 fn decision_shape(source: &str, file: &str) -> String {
11651 let output = instrument_candidate(source, file).unwrap();
11652 let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11653 let call = format!("{}(", runtime.mcdc_end_v2);
11654 let start = output.code.find(&call).expect("an outcome is recorded") + call.len();
11655 let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11656 let code = output
11657 .code
11658 .replace(&format!("{call}{probe_file}, "), "END(");
11659 let mut names: Vec<&str> = Vec::new();
11660 let mut shaped = String::new();
11661 let mut rest = code.as_str();
11662 while let Some(at) = rest.find("_supercovMcdc") {
11663 shaped.push_str(&rest[..at]);
11664 let tail = &rest[at..];
11665 if shaped.ends_with('_') {
11667 shaped.push_str("_supercovMcdc");
11668 rest = &tail["_supercovMcdc".len()..];
11669 continue;
11670 }
11671 let length = tail
11672 .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
11673 .unwrap_or(tail.len());
11674 let name = &tail[..length];
11675 let index = names
11676 .iter()
11677 .position(|known| *known == name)
11678 .unwrap_or_else(|| {
11679 names.push(name);
11680 names.len() - 1
11681 });
11682 shaped.push_str(&format!("t{index}"));
11683 rest = &tail[length..];
11684 }
11685 shaped.push_str(rest);
11686 shaped
11687 }
11688
11689 #[test]
11690 fn a_decision_stays_the_test_it_was() {
11691 let shaped = decision_shape(
11698 concat!(
11699 "export function pick(client: { id: number } | null, strict: boolean) {\n",
11700 " if (client && strict) return client;\n",
11701 " return null;\n",
11702 "}\n",
11703 "export function size(list: string[] | null) {\n",
11704 " if (!list) { return 0; } else { return list.length; }\n",
11705 "}\n",
11706 "export function first(list: string[] | null) {\n",
11707 " return list ? list[0] : undefined;\n",
11708 "}\n",
11709 ),
11710 "src/pick.ts",
11711 );
11712 for expected in [
11713 "if ((t0 = 1, t2 = 0, t1 = client) && (t0 += 3, t2 = strict)) {\n\t\t\tEND(0, t0 + +!!t1 + 3 * +!!t2, true), t0 = 0;",
11716 "\t\t}\n\t\tt0 && END(0, t0 + +!!t1 + 3 * +!!t2, false);\n\t}",
11721 "if (t3 = 1, t4 = !list) {\n\t\tEND(1, t3 + +!!t4, true);",
11722 "} else {\n\t\tEND(1, t3 + +!!t4, false);",
11723 "return (t5 = 1, t6 = list) ? (void END(2, t5 + +!!t6, true), list[0]) : (void END(2, t5 + +!!t6, false), undefined);",
11724 ] {
11725 assert!(shaped.contains(expected), "{expected}\n{shaped}");
11726 }
11727 }
11728
11729 #[test]
11730 fn a_loop_records_its_last_test_after_itself() {
11731 let shaped = decision_shape(
11735 concat!(
11736 "export function scan(limit: number) {\n",
11737 " let i = 0;\n",
11738 " walk: while (i < limit) { i++; if (i > 5) break walk; }\n",
11739 " do i--; while (i > 0);\n",
11740 " for (let j = 0; j < limit; j++) i += j;\n",
11741 " return i;\n",
11742 "}\n",
11743 ),
11744 "src/scan.ts",
11745 );
11746 for expected in [
11747 "{\n\t\twalk: while (t0 = 1, t1 = i < limit) {\n\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11749 "\t\t}\n\t\tt0 && END(0, t0 + +!!t1, false);\n\t}",
11750 "{\n\t\tt4 = 0;\n\t\tdo {\n\t\t\tt4 && (END(2, t4 + +!!t5, true), t4 = 0);",
11753 "} while (t4 = 1, t5 = i > 0);\n\t\tt4 && END(2, t4 + +!!t5, false);\n\t}",
11754 "for (let j = 0; t6 = 1, t7 = j < limit; j++) {\n\t\t\tEND(3, t6 + +!!t7, true), t6 = 0;",
11755 "\t\t}\n\t\tt6 && END(3, t6 + +!!t7, false);\n\t}",
11756 ] {
11757 assert!(shaped.contains(expected), "{expected}\n{shaped}");
11758 }
11759 }
11760
11761 #[test]
11762 fn a_decision_without_a_function_body_around_it_declares_its_own_temporaries() {
11763 let shaped = decision_shape(
11764 concat!(
11765 "export function pick(\n",
11766 " n: number,\n",
11767 " kind = class { static label = 'none'; static { if (n > 1) this.label = 'some'; } },\n",
11768 " tag = n > 5 ? 'high' : 'low',\n",
11769 ") {\n",
11770 " return [kind.label, tag];\n",
11771 "}\n",
11772 ),
11773 "src/defaults.ts",
11774 );
11775 for expected in [
11776 "static {\n\t\tlet t0, t1;\n",
11777 "if (t0 = 1, t1 = n > 1) {\n\t\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
11778 "(() => {\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})()",
11782 ] {
11783 assert!(shaped.contains(expected), "{expected}\n{shaped}");
11784 }
11785 }
11786
11787 #[test]
11788 fn a_top_level_declaration_has_a_line_of_its_own_before_it() {
11789 let output = instrument_candidate(
11792 concat!(
11793 "const one = 1;\n",
11794 "function plain() { return one; }\n",
11795 "class Box {}\n",
11796 "function over(value: string): string;\n",
11797 "function over(value: number): number;\n",
11798 "function over(value: any) { return value; }\n",
11799 "export function exported() { return plain(); }\n",
11800 ),
11801 "src/declarations.ts",
11802 )
11803 .unwrap();
11804 for expected in [
11805 "\n;\nfunction plain() {",
11806 "\n;\nclass Box {",
11807 "\n;\nfunction over(value: string): string;\nfunction over(value: number): number;\nfunction over(value: any) {",
11808 ] {
11809 assert!(
11810 output.code.contains(expected),
11811 "{expected}\n{}",
11812 output.code
11813 );
11814 }
11815 assert!(
11816 !output.code.contains(";\nexport function"),
11817 "{}",
11818 output.code
11819 );
11820 }
11821
11822 #[test]
11823 fn exposes_the_complete_probe_v2_instrumenter_contract() {
11824 let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
11825 assert!(output.complete);
11826 assert!(output.limitations.is_empty());
11827 assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
11828 let runtime = output.runtime.expect("candidate runtime binding");
11829 assert!(output.code.contains(&runtime.mcdc_end_v2));
11830 assert!(output.code.contains("_supercovMcdcFrame"));
11831 assert!(output.code.contains("_supercovMcdcValue"));
11832 assert_eq!(output.decisions.len(), 1);
11833 assert!(output.points.iter().any(|point| point.kind == "statement"));
11834 assert!(output.points.iter().any(|point| point.kind == "function"));
11835
11836 let allocator = Allocator::default();
11837 let reparsed = Parser::new(
11838 &allocator,
11839 &output.code,
11840 SourceType::from_path("app/decide.ts").unwrap(),
11841 )
11842 .parse();
11843 assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
11844 }
11845
11846 #[test]
11847 fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
11848 let source = concat!(
11849 "import type { Session } from './types.ts';\n",
11850 "import { type Row } from './rows.ts';\n",
11851 "import './register.ts';\n",
11852 "const value = 1;\n",
11853 );
11854 let output = instrument_candidate(source, "app/imports.ts").unwrap();
11855 let statement_lines = output
11856 .points
11857 .iter()
11858 .filter(|point| point.kind == "statement")
11859 .map(|point| point.line)
11860 .collect::<Vec<_>>();
11861 assert_eq!(statement_lines, vec![2, 3, 4]);
11862 }
11863
11864 #[test]
11865 fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
11866 let source = concat!(
11867 "import { Logger } from './types.js';\n",
11868 "import DefaultType from './default.js';\n",
11869 "import * as Types from './namespace.js';\n",
11870 "import { type Inline } from './inline.js';\n",
11871 "import { run, type Config } from './mixed.js';\n",
11872 "import './register.js';\n",
11873 "import {} from './empty.js';\n",
11874 "import { unused } from './unused.js';\n",
11875 "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
11876 );
11877 let preserved =
11878 instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", false)
11879 .unwrap();
11880 let erased =
11881 instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true).unwrap();
11882 assert_eq!(
11883 erased
11884 .excluded_statements
11885 .iter()
11886 .map(|p| p.line)
11887 .collect::<Vec<_>>(),
11888 vec![1, 2, 3, 4]
11889 );
11890 let excluded = erased
11891 .excluded_statements
11892 .iter()
11893 .map(|p| &p.id)
11894 .collect::<HashSet<_>>();
11895 let expected = preserved
11896 .points
11897 .iter()
11898 .filter(|p| !excluded.contains(&p.id))
11899 .collect::<Vec<_>>();
11900 assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
11901 for line in [5, 6, 7, 8] {
11902 assert!(erased.points.iter().any(|p| p.line == line));
11903 }
11904 let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
11906 let mixed = instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true)
11907 .unwrap();
11908 assert!(mixed.excluded_statements.is_empty());
11909 }
11910
11911 type FirstMapping = Option<Option<(u32, u32)>>;
11914
11915 fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
11917 let map = oxc_sourcemap::SourceMap::from_json_string(
11918 &output
11919 .map
11920 .as_ref()
11921 .expect("instrumented copy has a map")
11922 .to_string(),
11923 )
11924 .unwrap();
11925 let tokens = map.get_tokens().collect::<Vec<_>>();
11926 output
11927 .code
11928 .split('\n')
11929 .enumerate()
11930 .map(|(line, text)| {
11931 let first = tokens
11932 .iter()
11933 .find(|token| token.get_dst_line() == line as u32)
11934 .map(|token| {
11935 token
11936 .get_source_id()
11937 .map(|_| (token.get_src_line(), token.get_src_col()))
11938 });
11939 (text.trim().to_string(), first)
11940 })
11941 .collect()
11942 }
11943
11944 #[test]
11945 fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
11946 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";
11947 let output = instrument_candidate(source, "app/f.js").unwrap();
11948 let lines = first_mappings(&output);
11949 let runtime = output.runtime.as_ref().unwrap();
11950 for (text, first) in &lines {
11951 if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
11954 && let Some(first) = first
11955 {
11956 assert_eq!(*first, None, "{text}");
11957 }
11958 }
11959 let at = |needle: &str| {
11960 lines
11961 .iter()
11962 .find(|(text, _)| text.starts_with(needle))
11963 .unwrap_or_else(|| panic!("{needle} in {}", output.code))
11964 .1
11965 };
11966 assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
11969 assert_eq!(at("continue;"), Some(Some((4, 25))));
11970 assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
11971 assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
11974 let program = lines
11978 .iter()
11979 .position(|(text, _)| text.starts_with("function f("))
11980 .unwrap();
11981 for (text, first) in &lines[program..] {
11982 if text.starts_with('}') {
11983 assert!(first.is_some(), "{text} in {}", output.code);
11984 }
11985 }
11986 }
11987
11988 fn value_shape(source: &str, file: &str) -> String {
11991 let output = instrument_candidate(source, file).unwrap();
11992 let runtime = output.runtime.as_ref().expect("candidate runtime binding");
11993 let call = format!("{}(", runtime.select_end2_v2);
11994 let start = output.code.find(&call).expect("a value tree is recorded") + call.len();
11995 let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
11996 let mut code = output.code.clone();
11997 for (helper, short) in [
11998 (&runtime.select_end2_v2, "END2("),
11999 (&runtime.select_end_v2, "END("),
12000 (&runtime.select_next_v2, "NEXT("),
12001 (&runtime.coverage_hit_v2, "HIT("),
12002 (&runtime.select_right_v2, "RIGHT("),
12003 (&runtime.select_short_v2, "SHORT("),
12004 ] {
12005 code = code.replace(&format!("{helper}({probe_file}, "), short);
12006 }
12007 let mut names: Vec<&str> = Vec::new();
12008 let mut shaped = String::new();
12009 let mut rest = code.as_str();
12010 while let Some(at) = rest.find("_supercovSelected") {
12011 shaped.push_str(&rest[..at]);
12012 let tail = &rest[at..];
12013 let length = tail
12014 .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
12015 .unwrap_or(tail.len());
12016 let name = &tail[..length];
12017 let index = names
12018 .iter()
12019 .position(|known| *known == name)
12020 .unwrap_or_else(|| {
12021 names.push(name);
12022 names.len() - 1
12023 });
12024 shaped.push_str(&format!("s{index}"));
12025 rest = &tail[length..];
12026 }
12027 shaped.push_str(rest);
12028 shaped
12029 }
12030
12031 fn selection_shapes(
12034 leaves: usize,
12035 next_leaf: &mut usize,
12036 next_first: &mut usize,
12037 ) -> Vec<(SelectionShape, usize, usize)> {
12038 if leaves == 1 {
12039 let shape = SelectionShape::Leaf(*next_leaf);
12040 return vec![(shape, *next_leaf + 1, *next_first)];
12041 }
12042 let mut shapes = Vec::new();
12043 for left_leaves in 1..leaves {
12044 for kind in 0..3 {
12045 let first = *next_first;
12046 let mut after_first = first + 10;
12047 for (left, leaf_after_left, first_after_left) in
12048 selection_shapes(left_leaves, next_leaf, &mut after_first)
12049 {
12050 let mut leaf = leaf_after_left;
12051 let mut first_right = first_after_left;
12052 for (right, leaf_after, first_after) in
12053 selection_shapes(leaves - left_leaves, &mut leaf, &mut first_right)
12054 {
12055 shapes.push((
12056 SelectionShape::Selection {
12057 first,
12058 kind,
12059 left: Box::new(copy_shape(&left)),
12060 right: Box::new(right),
12061 },
12062 leaf_after,
12063 first_after,
12064 ));
12065 }
12066 }
12067 }
12068 }
12069 shapes
12070 }
12071
12072 fn copy_shape(shape: &SelectionShape) -> SelectionShape {
12073 match shape {
12074 SelectionShape::Leaf(leaf) => SelectionShape::Leaf(*leaf),
12075 SelectionShape::Selection {
12076 first,
12077 kind,
12078 left,
12079 right,
12080 } => SelectionShape::Selection {
12081 first: *first,
12082 kind: *kind,
12083 left: Box::new(copy_shape(left)),
12084 right: Box::new(copy_shape(right)),
12085 },
12086 }
12087 }
12088
12089 fn leaf_steps(shape: &SelectionShape, path: &[usize], steps: &mut Vec<Vec<usize>>) {
12091 match shape {
12092 SelectionShape::Leaf(_) => steps.push(path.to_vec()),
12093 SelectionShape::Selection {
12094 first,
12095 kind,
12096 left,
12097 right,
12098 } => {
12099 let mut left_path = vec![first * 4 + kind];
12100 left_path.extend_from_slice(path);
12101 let mut right_path = vec![first * 4 + 3];
12102 right_path.extend_from_slice(path);
12103 leaf_steps(left, &left_path, steps);
12104 leaf_steps(right, &right_path, steps);
12105 }
12106 }
12107 }
12108
12109 fn evaluate(shape: &SelectionShape, values: &[u8], evaluated: &mut Vec<usize>) -> u8 {
12112 match shape {
12113 SelectionShape::Leaf(leaf) => {
12114 evaluated.push(*leaf);
12115 values[*leaf]
12116 }
12117 SelectionShape::Selection {
12118 kind, left, right, ..
12119 } => {
12120 let value = evaluate(left, values, evaluated);
12121 let short = match kind {
12122 0 => value == 2,
12123 1 => value != 2,
12124 _ => value != 0,
12125 };
12126 if short {
12127 value
12128 } else {
12129 evaluate(right, values, evaluated)
12130 }
12131 }
12132 }
12133 }
12134
12135 fn record_steps(steps: &[usize], value: u8, recorded: &mut Vec<usize>) {
12137 for step in steps {
12138 let (first, code) = (step / 4, step % 4);
12139 let truthy = usize::from(value == 2);
12140 if code == 3 {
12141 recorded.push(first + 2 + truthy);
12142 } else if match code {
12143 0 => value == 2,
12144 1 => value != 2,
12145 _ => value != 0,
12146 } {
12147 recorded.push(first + truthy);
12148 } else {
12149 break;
12150 }
12151 }
12152 }
12153
12154 #[test]
12155 fn a_value_tree_records_what_a_call_around_each_leaf_recorded() {
12156 let mut checked = 0;
12162 for leaves in 2..=5 {
12163 for (shape, ..) in selection_shapes(leaves, &mut 0, &mut 0) {
12164 let mut steps = Vec::new();
12165 leaf_steps(&shape, &[], &mut steps);
12166 let mut before = vec![Vec::new(); leaves];
12167 shape.before(&mut before);
12168 for combination in 0..3_usize.pow(leaves as u32) {
12169 let values = (0..leaves)
12170 .map(|leaf| (combination / 3_usize.pow(leaf as u32) % 3) as u8)
12171 .collect::<Vec<_>>();
12172 let mut evaluated = Vec::new();
12173 evaluate(&shape, &values, &mut evaluated);
12174 let mut each_leaf = Vec::new();
12175 for leaf in &evaluated {
12176 record_steps(&steps[*leaf], values[*leaf], &mut each_leaf);
12177 }
12178 let mut tree = Vec::new();
12179 for pair in evaluated.windows(2) {
12180 let deciders = &before[pair[1]];
12183 let (_, points) = match &deciders[..] {
12184 [only] => only,
12185 several => several
12186 .iter()
12187 .find(|(leaf, _)| *leaf == pair[0])
12188 .expect("the leaf before is one the table has"),
12189 };
12190 tree.extend(points);
12191 }
12192 let last = *evaluated.last().unwrap();
12193 record_steps(&steps[last], values[last], &mut tree);
12194 assert_eq!(tree, each_leaf, "{values:?} over {steps:?}");
12195 assert!(
12196 evaluated
12197 .windows(2)
12198 .all(|pair| before[pair[1]].iter().any(|(leaf, _)| *leaf == pair[0])),
12199 "{values:?} over {steps:?}: {before:?}"
12200 );
12201 checked += 1;
12202 }
12203 }
12204 }
12205 assert!(checked > 100_000, "{checked}");
12206 }
12207
12208 #[test]
12209 fn a_constant_keeps_its_tree_and_a_named_chain_its_name() {
12210 let shaped = value_shape(
12211 concat!(
12212 "export function text(value: string | number) {\n",
12213 " const isText = typeof value === 'string' && value.length > 0;\n",
12214 " if (isText) return value;\n",
12215 " return null;\n",
12216 "}\n",
12217 "export const both = text(1) ?? text('a');\n",
12218 "export class Owner {\n",
12219 " pet?: { name: string };\n",
12220 " named(other?: Owner) { return [other?.pet, this?.pet, this.pet?.name]; }\n",
12221 "}\n",
12222 ),
12223 "src/alias.ts",
12224 );
12225 for expected in [
12226 "const isText = (s1 = 0, typeof value === \"string\") && (s1 = 1, value.length > 0);\n\tEND2(9, isText, s1);\n",
12230 "export const both = (s0 = 0, text(1)) ?? (s0 = 1, text(\"a\"));\nEND2(13, both, s0);\n",
12231 ", other)?.pet",
12234 ", this)?.pet",
12235 ] {
12236 assert!(shaped.contains(expected), "{expected}\n{shaped}");
12237 }
12238 assert!(shaped.contains(", this.pet)?.name"), "{shaped}");
12240 assert!(!shaped.contains(", this.pet), this.pet)"), "{shaped}");
12241 }
12242
12243 #[test]
12244 fn a_value_tree_keeps_its_leaves_the_programs_own_expressions() {
12245 let shaped = value_shape(
12250 concat!(
12251 "export function name(user: { name: string } | false) {\n",
12252 " return user && user.name;\n",
12253 "}\n",
12254 "export function ok(total: number, name: string) {\n",
12255 " return (total > 0 && name !== 'x') || !!name;\n",
12256 "}\n",
12257 "export function decided(total: number, name: string, force: boolean) {\n",
12258 " return (total > 0 && name !== 'x') || force;\n",
12259 "}\n",
12260 "export function pick(a: number | null, b: number | null, c: number) {\n",
12261 " return a ?? b ?? c;\n",
12262 "}\n",
12263 "export function held(list: string[] | null, fallback = list || []) {\n",
12264 " return fallback;\n",
12265 "}\n",
12266 ),
12267 "src/values.ts",
12268 );
12269 for expected in [
12270 "let s0: any;\n",
12271 "return END2(12, (s0 = 0, user) && (s0 = 1, user.name), s0);",
12272 "(s1 = 0, _supercovMcdcFrame27 = 1, _supercovMcdcValue29 = 0, _supercovMcdcValue28 = total > 0) && (s1 = 1, _supercovMcdcFrame27 += 3, _supercovMcdcValue29 = name !== \"x\") || (NEXT(0, s1, 2), s1 = 2, !!name), s1)",
12282 "!!(+!!_supercovMcdcValue28 & +!!_supercovMcdcValue29)), _supercovMcdcResult30;",
12283 "|| (NEXT(1, s2, 2), s2 = 2, force), s2)",
12284 "kind: \"value\"",
12285 "return END(2, (s3 = 0, a) ?? (s3 = 1, b) ?? (HIT(34), s3 = 2, c), s3);",
12288 "SHORT(40, list, 0) || RIGHT(40, [])",
12291 ] {
12292 assert!(shaped.contains(expected), "{expected}\n{shaped}");
12293 }
12294 }
12295
12296 #[test]
12297 fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
12298 let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
12299 for file in ["app/fill.js", "app/fill.ts"] {
12300 let output = instrument_candidate(source, file).unwrap();
12301 let runtime = output.runtime.expect("candidate runtime binding");
12302 assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
12305 assert!(output.code.contains("||= "), "{}", output.code);
12306 assert!(output.code.contains("??= "), "{}", output.code);
12307 assert!(output.code.contains("&&= "), "{}", output.code);
12308 assert_eq!(
12309 output
12310 .code
12311 .matches(&format!("{}(", runtime.select_assign_end_v2))
12312 .count(),
12313 3
12314 );
12315 assert_eq!(
12316 output
12317 .code
12318 .matches(&format!("{}(", runtime.select_named_right_v2))
12319 .count(),
12320 1
12321 );
12322 let allocator = Allocator::default();
12323 let reparsed = Parser::new(
12324 &allocator,
12325 &output.code,
12326 SourceType::from_path(file).unwrap(),
12327 )
12328 .parse();
12329 assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
12330 }
12331 }
12332
12333 #[test]
12334 fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
12335 let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
12336 let output = instrument_candidate(source, "app/collisions.js").unwrap();
12337 let runtime = output.runtime.expect("candidate runtime binding");
12338 assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
12339 assert!(!output.code.contains("let _supercovMcdcFrame1,"));
12340 }
12341
12342 #[test]
12343 fn instruments_wider_decisions_with_the_exact_v1_fallback() {
12344 let predicate = (0..33)
12345 .map(|index| format!("c{index}"))
12346 .collect::<Vec<_>>()
12347 .join(" && ");
12348 let source = format!("if ({predicate}) work();");
12349 let output = instrument_candidate(&source, "app/wide.js").unwrap();
12350 assert_eq!(output.decisions[0].conditions.len(), 33);
12351 let runtime = output.runtime.expect("candidate runtime binding");
12352 assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
12353 assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
12354 assert!(
12355 output
12356 .code
12357 .contains(&format!("{}(", runtime.mcdc_condition))
12358 );
12359 assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
12360 }
12361
12362 #[test]
12365 fn server_callbacks_run_in_the_request_phase_however_they_are_passed() {
12366 let source = "import http from 'node:http';\nconst handlers = [() => {}];\nconst server = http.createServer({}, function (req, res) { res.end('a'); });\nserver.on('request', ...handlers);\nserver.on('request');\nserver.on('connection', (socket) => socket.setNoDelay());\nserver.once('upgrade', onUpgrade);\nserver.on('close', () => {});\nfunction onUpgrade() {}\n";
12367 let output = instrument_candidate(source, "src/server.mjs").unwrap();
12368 let runtime = output.runtime.as_ref().expect("runtime binding");
12369 let phase = &runtime.with_request_phase;
12370 let line = |start: &str| {
12371 output
12372 .code
12373 .lines()
12374 .find(|line| line.starts_with(start))
12375 .unwrap_or_else(|| panic!("no {start} in\n{}", output.code))
12376 .to_owned()
12377 };
12378 assert!(line("const server").contains(&format!("{{}}, {phase}(function(req, res)")));
12379 assert_eq!(
12380 line("server.on(\"request\", ..."),
12381 "server.on(\"request\", ...handlers);"
12382 );
12383 assert_eq!(line("server.on(\"request\")"), "server.on(\"request\");");
12384 assert!(line("server.on(\"connection\"").contains(&format!("{phase}((socket)")));
12385 assert_eq!(
12387 output.code.matches("\"connection\")").count(),
12388 1,
12389 "{}",
12390 output.code
12391 );
12392 assert_eq!(
12393 line("server.once"),
12394 format!("server.once(\"upgrade\", {phase}(onUpgrade));")
12395 );
12396 assert!(!line("server.on(\"close\"").contains(phase.as_str()));
12397 }
12398
12399 #[test]
12400 fn leaves_look_alike_route_exports_alone() {
12401 for (file, source) in [
12402 (
12404 "lib/route.ts",
12405 "export function GET(request) { return request.url; }",
12406 ),
12407 (
12409 "app/api/items/handler.ts",
12410 "export function GET(request) { return request.url; }",
12411 ),
12412 (
12413 "app/route.ts",
12414 "export function helper(request) { return request.url; }",
12415 ),
12416 ] {
12417 let output = instrument_candidate(source, file).unwrap();
12418 assert!(
12419 !output.code.contains("withRequestPhase"),
12420 "{file}: {}",
12421 output.code
12422 );
12423 }
12424 }
12425
12426 #[test]
12427 fn wraps_framework_request_exports_without_changing_the_public_api() {
12428 for (file, source, export_prefix) in [
12429 (
12430 "app/routes/example.ts",
12431 "export const loader = async ({ request }) => request.url;",
12432 "export const loader = ",
12433 ),
12434 (
12435 "app/routes/example.ts",
12436 "export async function action({ request }) { return request.method; }",
12437 "export const action = ",
12438 ),
12439 (
12440 "app/api/items/route.ts",
12441 "export function GET(request) { return Response.json({ url: request.url }); }",
12442 "export const GET = ",
12443 ),
12444 (
12446 "app/route.ts",
12447 "export function GET(request) { return Response.json({ url: request.url }); }",
12448 "export const GET = ",
12449 ),
12450 (
12451 "src/app/api/items/route.ts",
12452 "export async function POST(request) { return Response.json(await request.json()); }",
12453 "export const POST = ",
12454 ),
12455 (
12456 "app/routes/example.ts",
12457 "export { generateAction as action } from './generateAction';",
12458 "export const action = ",
12459 ),
12460 (
12461 "app/entry.server.tsx",
12462 "export default async function handleRequest(request) { return request.url; }",
12463 "export default ",
12464 ),
12465 ] {
12466 let output = instrument_candidate(source, file).unwrap();
12467 let runtime = output.runtime.as_ref().expect("runtime binding");
12468 assert!(
12469 output
12470 .code
12471 .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
12472 "{file}: {}",
12473 output.code
12474 );
12475 assert!(
12476 output.code.contains(&format!(
12477 "withRequestPhase as {}",
12478 runtime.with_request_phase
12479 )),
12480 "{file}: {}",
12481 output.code
12482 );
12483 let allocator = Allocator::default();
12484 let reparsed = Parser::new(
12485 &allocator,
12486 &output.code,
12487 SourceType::from_path(file).unwrap(),
12488 )
12489 .parse();
12490 assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
12491 }
12492 }
12493
12494 #[test]
12495 fn parse_failures_are_explicit_and_never_claim_completeness() {
12496 assert!(matches!(
12497 analyze_candidate("if (", "broken.js"),
12498 Err(CandidateError::Parse(errors)) if !errors.is_empty()
12499 ));
12500 assert!(matches!(
12501 analyze_candidate("let value = 1", "unknown.extension"),
12502 Err(CandidateError::UnknownSourceType(_))
12503 ));
12504 }
12505}