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 (index, original) = loop {
138 let Some(original) = program.comments.get(original_index) else {
139 return Err(CandidateError::CommentPreservation {
143 expected: program.comments.len(),
144 actual: reparsed.program.comments.len(),
145 detail: format!(
146 "no source comment matches the generated comment {emitted_text:?} (matched {} before it)",
147 edits.len()
148 ),
149 });
150 };
151 let index = original_index;
152 original_index += 1;
153 if original.kind == emitted.kind
154 && equal_ignoring_whitespace(
155 original.span.source_text(program.source_text),
156 emitted_text,
157 )
158 {
159 break (index, original);
160 }
161 };
162 matched[index] = true;
163 edits.push((
164 emitted.span.start as usize,
165 emitted.span.end as usize,
166 original.span.source_text(program.source_text).to_string(),
167 ));
168 }
169
170 let source_lines = Utf16LineIndex::new(program.source_text);
171 let generated_lines = Utf16LineIndex::new(generated);
172 let mut mappings = map
173 .get_tokens()
174 .filter_map(|token| {
175 token.get_source_id()?;
176 Some((
177 source_lines
178 .byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
179 generated_lines
180 .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
181 ))
182 })
183 .collect::<Vec<_>>();
184 mappings.sort_unstable_by_key(|(source, destination)| (*source, *destination));
185 let statements = statement_spans(&reparsed.program);
186 for (index, original) in program.comments.iter().enumerate() {
187 if matched[index] {
188 continue;
189 }
190 let anchor = if original.attached_to > 0 {
191 original.attached_to as usize
192 } else {
193 original.span.end as usize
194 };
195 let mapping_index = mappings.partition_point(|(source, _)| *source < anchor);
196 let mapped = mappings
197 .get(mapping_index)
198 .map_or(generated.len(), |(_, destination)| *destination);
199 let (destination, text) = place_restored_comment(
200 generated,
201 &statements,
202 mapped,
203 original,
204 original.span.source_text(program.source_text),
205 );
206 edits.push((destination, destination, text));
207 }
208 edits.sort_by_key(|(start, end, _)| (*start, *end));
209 let restored_len = edits
210 .iter()
211 .fold(generated.len(), |length, (start, end, text)| {
212 length + text.len() - (end - start)
213 });
214 let mut restored = String::with_capacity(restored_len);
215 let mut cursor = 0;
216 for (start, end, replacement) in &edits {
217 if *start < cursor {
218 return Err(CandidateError::CommentPreservation {
219 expected: program.comments.len(),
220 actual: reparsed.program.comments.len(),
221 detail: format!(
222 "restored comments overlap: {replacement:?} would be placed at byte {start}, behind byte {cursor} already written"
223 ),
224 });
225 }
226 restored.push_str(&generated[cursor..*start]);
227 restored.push_str(replacement);
228 cursor = *end;
229 }
230 restored.push_str(&generated[cursor..]);
231 let map = shift_source_map(map, generated, &restored, &edits);
232 Ok((restored, map))
233}
234
235fn statement_spans(program: &Program<'_>) -> Vec<Span> {
238 struct Spans(Vec<Span>);
239 impl<'a> Visit<'a> for Spans {
240 fn visit_statement(&mut self, statement: &Statement<'a>) {
241 self.0.push(statement.span());
242 walk::walk_statement(self, statement);
243 }
244 }
245 let mut spans = Spans(Vec::new());
246 spans.visit_program(program);
247 spans.0
248}
249
250fn place_restored_comment(
265 generated: &str,
266 statements: &[Span],
267 mapped: usize,
268 comment: &Comment,
269 text: &str,
270) -> (usize, String) {
271 let line_prefix = |offset: usize| generated[..offset].rsplit('\n').next().unwrap_or("");
272 let at_line_start = line_prefix(mapped)
273 .chars()
274 .all(|character| character == ' ' || character == '\t');
275 if at_line_start {
276 let mut placed = String::new();
277 placed.push(if comment.preceded_by_newline() {
278 '\n'
279 } else {
280 ' '
281 });
282 placed.push_str(text);
283 placed.push(if comment.is_line() || comment.followed_by_newline() {
284 '\n'
285 } else {
286 ' '
287 });
288 return (mapped, placed);
289 }
290 if !comment.is_line() && !text.contains('\n') {
291 let leading = if generated[..mapped].ends_with([' ', '\t']) {
292 ""
293 } else {
294 " "
295 };
296 return (mapped, format!("{leading}{text} "));
297 }
298 let Some(statement) = statements
299 .iter()
300 .filter(|span| span.start as usize <= mapped && mapped < span.end as usize)
301 .max_by_key(|span| span.start)
302 else {
303 return (mapped, format!("\n{text}\n"));
304 };
305 let start = statement.start as usize;
306 let indentation: String = line_prefix(start)
307 .chars()
308 .take_while(|character| *character == ' ' || *character == '\t')
309 .collect();
310 (start, format!("{text}\n{indentation}"))
311}
312
313fn equal_ignoring_whitespace(left: &str, right: &str) -> bool {
326 fn significant(text: &str) -> impl Iterator<Item = char> + '_ {
327 let mut rest = text.char_indices();
328 std::iter::from_fn(move || {
329 loop {
330 let (index, character) = rest.next()?;
331 if character == '\\'
334 && text[index + 1..].starts_with('/')
335 && text[..index].ends_with('<')
336 {
337 continue;
338 }
339 if !character.is_whitespace() {
340 return Some(character);
341 }
342 }
343 })
344 }
345 significant(left).eq(significant(right))
346}
347
348struct Utf16LineIndex<'s> {
357 source: &'s str,
358 starts: Vec<usize>,
359 checkpoints: Vec<Option<Vec<(usize, usize)>>>,
362}
363
364const CHECKPOINT: usize = 256;
365
366impl<'s> Utf16LineIndex<'s> {
367 fn new(source: &'s str) -> Self {
368 let mut starts = Vec::with_capacity(source.lines().count() + 1);
369 starts.push(0);
370 starts.extend(
371 source
372 .char_indices()
373 .filter_map(|(offset, character)| (character == '\n').then_some(offset + 1)),
374 );
375 let checkpoints = (0..starts.len())
376 .map(|line| {
377 let end = starts.get(line + 1).copied().unwrap_or(source.len());
378 let text = &source[starts[line]..end];
379 if text.is_ascii() {
380 return None;
381 }
382 let mut marks = vec![(0, 0)];
383 let mut column = 0;
384 for (offset, character) in text.char_indices() {
385 if column >= marks.last().expect("a first mark").0 + CHECKPOINT {
386 marks.push((column, offset));
387 }
388 column += character.len_utf16();
389 }
390 Some(marks)
391 })
392 .collect();
393 Self {
394 source,
395 starts,
396 checkpoints,
397 }
398 }
399
400 fn line_end(&self, line: usize) -> usize {
401 self.starts
402 .get(line + 1)
403 .copied()
404 .unwrap_or(self.source.len())
405 }
406
407 fn byte_offset(&self, target_line: usize, target_utf16_col: usize) -> usize {
408 let Some(&start) = self.starts.get(target_line) else {
409 return self.source.len();
410 };
411 let end = self.line_end(target_line);
412 match &self.checkpoints[target_line] {
413 None => start + target_utf16_col.min(end - start),
414 Some(marks) => {
415 let at = marks.partition_point(|(column, _)| *column <= target_utf16_col) - 1;
416 let (column, offset) = marks[at];
417 start
418 + offset
419 + utf16_col_to_byte(
420 &self.source[start + offset..end],
421 target_utf16_col - column,
422 )
423 }
424 }
425 }
426
427 fn line_col(&self, byte_offset: usize) -> (u32, u32) {
428 let byte_offset = byte_offset.min(self.source.len());
429 let line = self.starts.partition_point(|start| *start <= byte_offset) - 1;
430 let start = self.starts[line];
431 let column = match &self.checkpoints[line] {
432 None => byte_offset - start,
433 Some(marks) => {
434 let at = marks.partition_point(|(_, offset)| *offset <= byte_offset - start) - 1;
435 let (column, offset) = marks[at];
436 column
437 + self.source[start + offset..byte_offset]
438 .chars()
439 .map(char::len_utf16)
440 .sum::<usize>()
441 }
442 };
443 (line as u32, column as u32)
444 }
445}
446
447fn utf16_col_to_byte(line: &str, target_utf16_col: usize) -> usize {
448 let mut column = 0;
449 for (offset, character) in line.char_indices() {
450 if column >= target_utf16_col {
451 return offset;
452 }
453 column += character.len_utf16();
454 }
455 line.len()
456}
457
458fn shift_source_map(
459 map: oxc_sourcemap::SourceMap,
460 generated: &str,
461 restored: &str,
462 edits: &[(usize, usize, String)],
463) -> oxc_sourcemap::SourceMap {
464 let generated_lines = Utf16LineIndex::new(generated);
465 let restored_lines = Utf16LineIndex::new(restored);
466 let mut edit_index = 0;
467 let mut shift = 0isize;
468 let tokens = map
469 .get_tokens()
470 .map(|token| {
471 let original_offset = generated_lines
472 .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
473 while let Some((start, end, replacement)) = edits.get(edit_index) {
474 if *end > original_offset {
475 break;
476 }
477 shift += replacement.len() as isize - (*end - *start) as isize;
478 edit_index += 1;
479 }
480 let shifted_offset = edits
481 .get(edit_index)
482 .filter(|(start, end, _)| *start <= original_offset && original_offset < *end)
483 .map_or_else(
484 || original_offset.saturating_add_signed(shift),
485 |(start, _, _)| start.saturating_add_signed(shift),
486 );
487 let (dst_line, dst_col) = restored_lines.line_col(shifted_offset);
488 oxc_sourcemap::Token::new(
489 dst_line,
490 dst_col,
491 token.get_src_line(),
492 token.get_src_col(),
493 token.get_source_id(),
494 token.get_name_id(),
495 )
496 })
497 .collect::<Vec<_>>();
498 oxc_sourcemap::SourceMap::new(
499 map.get_file().cloned(),
500 map.get_names().cloned().collect::<Vec<Arc<str>>>(),
501 map.get_source_root().map(str::to_string),
502 map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
503 map.get_source_contents()
504 .map(|content| content.cloned())
505 .collect::<Vec<Option<Arc<str>>>>(),
506 tokens.into_boxed_slice(),
507 None,
508 )
509}
510
511fn generated_statement_span(source: &str) -> Option<Span> {
519 let last_line = source.rfind('\n').map_or(0, |newline| newline + 1);
520 let length = u32::try_from(source.len()).ok()?;
521 (source[last_line..].is_ascii() && length < u32::MAX - 2)
522 .then(|| Span::new(length + 1, length + 2))
523}
524
525struct GeneratedStatementSpans<'a, 's> {
542 ast: AstBuilder<'a>,
543 source: &'s str,
544 span: Span,
545 depth: usize,
546}
547
548impl GeneratedStatementSpans<'_, '_> {
549 fn borrowed(&self, span: Span) -> bool {
552 !span.is_empty()
553 && span != self.span
554 && self.source.as_bytes().get(span.start as usize) != Some(&b'{')
555 }
556}
557
558impl<'a> VisitMut<'a> for GeneratedStatementSpans<'a, '_> {
559 fn visit_statement(&mut self, statement: &mut Statement<'a>) {
560 if self.depth > 0 && statement.span().is_empty() {
564 *statement.span_mut() = self.span;
565 }
566 self.depth += 1;
567 walk_mut::walk_statement(self, statement);
568 self.depth -= 1;
569 }
570
571 fn visit_block_statement(&mut self, block: &mut oxc_ast::ast::BlockStatement<'a>) {
572 if self.depth > 0 && block.span.is_empty() {
573 block.span = self.span;
574 } else if self.borrowed(block.span)
575 && !matches!(
578 block.body.last(),
579 Some(
580 Statement::ReturnStatement(_)
581 | Statement::ThrowStatement(_)
582 | Statement::BreakStatement(_)
583 | Statement::ContinueStatement(_)
584 )
585 )
586 {
587 block.body.push(self.ast.statement_empty(self.span));
588 }
589 walk_mut::walk_block_statement(self, block);
590 }
591
592 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
593 if self.borrowed(body.span) && (body.span.end as usize) < self.source.len() {
596 body.span.end += 1;
597 }
598 self.depth += 1;
599 walk_mut::walk_function_body(self, body);
600 self.depth -= 1;
601 }
602}
603
604fn mark_generated_statements<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>) {
605 let Some(span) = generated_statement_span(program.source_text) else {
606 return;
607 };
608 GeneratedStatementSpans {
609 ast,
610 source: program.source_text,
611 span,
612 depth: 0,
613 }
614 .visit_program(program);
615}
616
617fn unmap_generated_statements(
618 map: oxc_sourcemap::SourceMap,
619 source: &str,
620) -> oxc_sourcemap::SourceMap {
621 let Some(span) = generated_statement_span(source) else {
622 return map;
623 };
624 let line = source.matches('\n').count() as u32;
625 let column = span.start - source.rfind('\n').map_or(0, |newline| newline as u32 + 1);
626 let is_generated = |token: &oxc_sourcemap::Token| {
627 token.get_src_line() == line && token.get_src_col() == column
628 };
629 if !map.get_tokens().any(|token| is_generated(&token)) {
630 return map;
631 }
632 let tokens = map
633 .get_tokens()
634 .map(|token| {
635 if is_generated(&token) {
636 oxc_sourcemap::Token::new(
637 token.get_dst_line(),
638 token.get_dst_col(),
639 0,
640 0,
641 None,
642 None,
643 )
644 } else {
645 token
646 }
647 })
648 .collect::<Vec<_>>();
649 oxc_sourcemap::SourceMap::new(
650 map.get_file().cloned(),
651 map.get_names().cloned().collect::<Vec<Arc<str>>>(),
652 map.get_source_root().map(str::to_string),
653 map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
654 map.get_source_contents()
655 .map(|content| content.cloned())
656 .collect::<Vec<Option<Arc<str>>>>(),
657 tokens.into_boxed_slice(),
658 None,
659 )
660}
661
662fn map_closing_lines(
676 map: oxc_sourcemap::SourceMap,
677 code: &str,
678 source: &str,
679) -> oxc_sourcemap::SourceMap {
680 let source_lines = source.split('\n').collect::<Vec<_>>();
681 let tokens = map.get_tokens().collect::<Vec<_>>();
682 let mut patched = Vec::with_capacity(tokens.len());
683 let mut next = 0;
684 let mut previous: Option<oxc_sourcemap::Token> = None;
685 let mut added = false;
686 for (line, text) in code.split('\n').enumerate() {
687 let line = line as u32;
688 let first = next;
689 while next < tokens.len() && tokens[next].get_dst_line() == line {
690 next += 1;
691 }
692 let column = (text.len() - text.trim_start().len()) as u32;
693 if text.trim_start().starts_with(['}', ')', ']'])
694 && (first == next || tokens[first].get_dst_col() > column)
695 && let Some(previous) = &previous
696 {
697 patched.push(oxc_sourcemap::Token::new(
698 line,
699 column,
700 previous.get_src_line(),
701 previous.get_src_col(),
702 previous.get_source_id(),
703 None,
704 ));
705 added = true;
706 }
707 patched.extend_from_slice(&tokens[first..next]);
708 if next > first {
709 let last = tokens[next - 1];
710 previous = Some(last);
711 if let (Some(_), Some(original)) = (
712 last.get_source_id(),
713 source_lines.get(last.get_src_line() as usize),
714 ) && text.is_ascii()
715 && original.is_ascii()
716 {
717 let generated_rest = text
718 .get(last.get_dst_col() as usize..)
719 .unwrap_or("")
720 .trim_end();
721 let original_rest = original
722 .get(last.get_src_col() as usize..)
723 .unwrap_or("")
724 .trim_end();
725 if generated_rest.len() > 1
726 && generated_rest.ends_with(';')
727 && original_rest.starts_with(generated_rest)
728 {
729 let offset = (generated_rest.len() - 1) as u32;
730 let end = oxc_sourcemap::Token::new(
731 line,
732 last.get_dst_col() + offset,
733 last.get_src_line(),
734 last.get_src_col() + offset,
735 last.get_source_id(),
736 None,
737 );
738 patched.push(end);
739 previous = Some(end);
740 added = true;
741 }
742 }
743 }
744 }
745 patched.extend_from_slice(&tokens[next..]);
746 if !added {
747 return map;
748 }
749 oxc_sourcemap::SourceMap::new(
750 map.get_file().cloned(),
751 map.get_names().cloned().collect::<Vec<Arc<str>>>(),
752 map.get_source_root().map(str::to_string),
753 map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
754 map.get_source_contents()
755 .map(|content| content.cloned())
756 .collect::<Vec<Option<Arc<str>>>>(),
757 patched.into_boxed_slice(),
758 None,
759 )
760}
761
762fn generate_candidate(
763 program: &Program<'_>,
764 file: &str,
765) -> Result<(String, Option<serde_json::Value>), CandidateError> {
766 let options = CodegenOptions {
767 source_map_path: Some(Path::new(file).to_path_buf()),
768 ..CodegenOptions::default()
769 };
770 let generated = Codegen::new().with_options(options).build(program);
771 let map = unmap_generated_statements(
775 generated.map.expect("source maps are enabled"),
776 program.source_text,
777 );
778 let (code, map) = restore_comment_text(program, &generated.code, map)?;
779 let map = map_closing_lines(map, &code, program.source_text);
780 let map = Some({
781 serde_json::from_str(&map.to_json_string())
782 .expect("oxc must serialize its own generated source map")
783 });
784 Ok((code, map))
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(rename_all = "camelCase")]
789pub struct CandidateRuntime {
790 pub coverage_hit: String,
791 pub mcdc_begin: String,
792 pub mcdc_condition: String,
793 pub mcdc_end: String,
794 pub register_probe_v2: String,
795 pub mcdc_end_v2: String,
796 pub coverage_hit_v2: String,
797 pub probe_file_v2: String,
798 pub parenthesized_assignment_value: String,
799 pub with_request_phase: String,
800 pub optional_select_v2: String,
801 pub select_short_v2: String,
802 pub select_right_v2: String,
803 pub select_named_right_v2: String,
804 pub select_assign_end_v2: String,
805 pub select_path_v2: String,
806 pub rendered_value_v2: String,
807 pub optional_call_end_v2: String,
808 pub default_selected_v2: String,
809 pub default_entered_v2: String,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
813#[serde(rename_all = "camelCase")]
814pub struct CandidateLimitation {
815 pub id: String,
816 pub kind: String,
817 pub file: String,
818 pub line: usize,
819 pub column: usize,
820 pub source: String,
821 pub reason: String,
822}
823
824#[derive(Debug, Clone, PartialEq, Eq)]
825pub enum CandidateError {
826 UnknownSourceType(String),
827 Parse(Vec<String>),
828 CommentPreservation {
829 expected: usize,
830 actual: usize,
831 detail: String,
832 },
833}
834
835impl std::fmt::Display for CandidateError {
836 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
837 match self {
838 Self::UnknownSourceType(file) => {
839 write!(formatter, "no JavaScript source type for {file}")
840 }
841 Self::Parse(messages) => write!(formatter, "{}", messages.join("; ")),
842 Self::CommentPreservation {
843 expected,
844 actual,
845 detail,
846 } => write!(
847 formatter,
848 "{expected} comments in the source, {actual} after instrumenting: {detail}"
849 ),
850 }
851 }
852}
853
854fn parse_message(source: &str, message: &str, offset: Option<usize>) -> String {
856 match offset.filter(|offset| *offset <= source.len()) {
857 Some(offset) => {
858 let (line, column) = line_and_utf16_column(source, offset);
859 format!("{message} at line {line}, column {}", column + 1)
860 }
861 None => message.to_owned(),
862 }
863}
864
865#[derive(Debug, Clone, Copy, PartialEq, Eq)]
866enum RuntimeBinding {
867 ModuleImport,
868 DirectGlobal,
869}
870
871const NODE_ASSERT_MODULES: &[&str] = &[
872 "assert",
873 "assert/strict",
874 "node:assert",
875 "node:assert/strict",
876];
877const NODE_ASSERT_METHODS: &[&str] = &[
878 "deepEqual",
879 "deepStrictEqual",
880 "doesNotMatch",
881 "doesNotReject",
882 "doesNotThrow",
883 "equal",
884 "fail",
885 "ifError",
886 "match",
887 "notDeepEqual",
888 "notDeepStrictEqual",
889 "notEqual",
890 "notStrictEqual",
891 "ok",
892 "partialDeepStrictEqual",
893 "rejects",
894 "strictEqual",
895 "throws",
896];
897
898#[derive(Debug, Clone, PartialEq, Eq)]
899pub struct NodeAssertionInstrumentation {
900 pub code: String,
901 pub assertions: usize,
902 pub capability_imports: usize,
903}
904
905const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
906 &["@jest/globals", "@playwright/test", "playwright", "vitest"];
907
908fn capability_source_candidate(source: &str) -> bool {
909 let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
915 || (source.contains("hostRoot") && source.contains("guestRoot"));
916 let mount_mapping = (source.contains("mounts") || source.contains("Mounts"))
922 && source.contains("source")
923 && source.contains("target");
924 direct_mapping || mount_mapping
925}
926
927fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
928 source == wrapper
929 || source.starts_with("node:")
930 || source.starts_with("virtual:supercov-")
931 || source.contains(".supercov/")
932 || CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
933}
934
935fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
936 match expression {
937 Expression::Identifier(identifier) => Some(identifier.name.to_string()),
938 Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
939 Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
940 Expression::CallExpression(call) => capability_callee_root(&call.callee),
941 Expression::NewExpression(construction) => capability_callee_root(&construction.callee),
943 Expression::ParenthesizedExpression(parenthesized) => {
944 capability_callee_root(&parenthesized.expression)
945 }
946 Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
947 Expression::TSSatisfiesExpression(expression) => {
948 capability_callee_root(&expression.expression)
949 }
950 Expression::TSNonNullExpression(expression) => {
951 capability_callee_root(&expression.expression)
952 }
953 Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
954 _ => None,
955 }
956}
957
958struct CapabilityCallCollector<'s> {
959 source: &'s str,
960 mapping_variables: HashSet<String>,
961 roots: HashSet<String>,
962}
963
964impl CapabilityCallCollector<'_> {
965 fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
966 if let Argument::Identifier(identifier) = argument
967 && self.mapping_variables.contains(identifier.name.as_str())
968 {
969 return true;
970 }
971 capability_source_candidate(source_slice(self.source, argument.span()))
972 }
973}
974
975impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
976 fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
977 if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
978 (&declarator.id, &declarator.init)
979 && capability_source_candidate(source_slice(self.source, initializer.span()))
980 {
981 self.mapping_variables.insert(identifier.name.to_string());
982 }
983 walk::walk_variable_declarator(self, declarator);
984 }
985
986 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
987 let mounting = matches!(&call.callee, Expression::StaticMemberExpression(member)
990 if member.property.name.to_ascii_lowercase().contains("mount"));
991 let mounted = |argument: &Argument<'_>| {
992 let text = source_slice(self.source, argument.span());
993 mounting && text.contains("source") && text.contains("target")
994 };
995 if call
996 .arguments
997 .iter()
998 .any(|argument| self.argument_has_mapping(argument) || mounted(argument))
999 && let Some(root) = capability_callee_root(&call.callee)
1000 {
1001 self.roots.insert(root);
1002 }
1003 walk::walk_call_expression(self, call);
1004 }
1005}
1006
1007fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
1008 let mut collector = CapabilityCallCollector {
1009 source,
1010 mapping_variables: HashSet::new(),
1011 roots: HashSet::new(),
1012 };
1013 collector.visit_program(program);
1016 let mapping_variables = collector.mapping_variables.clone();
1017 collector.roots.clear();
1018 collector.mapping_variables = mapping_variables;
1019 collector.visit_program(program);
1020 collector.roots
1021}
1022
1023struct TextEdit {
1027 start: usize,
1028 end: usize,
1029 rank: i64,
1030 text: String,
1031}
1032
1033impl TextEdit {
1034 fn insert(at: usize, rank: i64, text: String) -> Self {
1035 Self {
1036 start: at,
1037 end: at,
1038 rank,
1039 text,
1040 }
1041 }
1042
1043 fn replace(start: usize, end: usize, text: String) -> Self {
1044 Self {
1045 start,
1046 end,
1047 rank: 0,
1048 text,
1049 }
1050 }
1051}
1052
1053fn apply_text_edits(
1063 source: &str,
1064 mut edits: Vec<TextEdit>,
1065 file: &str,
1066) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
1067 edits.sort_by_key(|edit| (edit.start, edit.end > edit.start, edit.rank));
1068 let mut builder = oxc_sourcemap::SourceMapBuilder::default();
1069 let source_id = builder.set_source_and_content(file, source);
1070 let mut code = String::with_capacity(source.len() + edits.len() * 64);
1071 let (mut line, mut column) = (0u32, 0u32);
1072 let (mut source_line, mut source_column) = (0u32, 0u32);
1073 let advance = |text: &str, line: &mut u32, column: &mut u32| {
1074 for unit in text.chars() {
1075 if unit == '\n' {
1076 *line += 1;
1077 *column = 0;
1078 } else {
1079 *column += unit.len_utf16() as u32;
1080 }
1081 }
1082 };
1083 let copy = |from: usize,
1084 to: usize,
1085 code: &mut String,
1086 line: &mut u32,
1087 column: &mut u32,
1088 source_line: &mut u32,
1089 source_column: &mut u32,
1090 builder: &mut oxc_sourcemap::SourceMapBuilder| {
1091 for (index, piece) in source[from..to].split('\n').enumerate() {
1092 if index > 0 {
1093 code.push('\n');
1094 *line += 1;
1095 *column = 0;
1096 *source_line += 1;
1097 *source_column = 0;
1098 }
1099 if !piece.is_empty() {
1100 builder.add_token(
1101 *line,
1102 *column,
1103 *source_line,
1104 *source_column,
1105 Some(source_id),
1106 None,
1107 );
1108 code.push_str(piece);
1109 let width = piece.encode_utf16().count() as u32;
1110 *column += width;
1111 *source_column += width;
1112 }
1113 }
1114 };
1115 let mut at = 0;
1116 for edit in edits {
1117 if edit.start < at || edit.end > source.len() || edit.start > edit.end {
1118 return Err(CandidateError::Parse(vec![format!(
1119 "overlapping assertion edits in {file}"
1120 )]));
1121 }
1122 copy(
1123 at,
1124 edit.start,
1125 &mut code,
1126 &mut line,
1127 &mut column,
1128 &mut source_line,
1129 &mut source_column,
1130 &mut builder,
1131 );
1132 let removed = &source[edit.start..edit.end];
1133 if edit.text.matches('\n').count() != removed.matches('\n').count() {
1134 return Err(CandidateError::Parse(vec![format!(
1135 "an assertion edit in {file} would move a line"
1136 )]));
1137 }
1138 code.push_str(&edit.text);
1139 advance(&edit.text, &mut line, &mut column);
1140 advance(removed, &mut source_line, &mut source_column);
1141 at = edit.end;
1142 }
1143 copy(
1144 at,
1145 source.len(),
1146 &mut code,
1147 &mut line,
1148 &mut column,
1149 &mut source_line,
1150 &mut source_column,
1151 &mut builder,
1152 );
1153 Ok((code, builder.into_sourcemap()))
1154}
1155
1156const ASSERTION_RUNTIME_TYPE: &str = "{ \
1168 withNodeAssertionPhase<T>(operation: string, source: string, callback: () => T): T; \
1169 openNodeAssertionPhase(operation: string, source: string): { close<T>(value: T): T; close(): void; fail(error: unknown): unknown }; \
1170 bindNodeAssertionPhase<T>(operation: string, source: string, target: T, property: null): T; \
1171 bindNodeAssertionPhase<T, K extends keyof T>(operation: string, source: string, target: T, property: K): T[K]; \
1172 bindNodeAssertionPhase(operation: string, source: string, target: unknown, property: PropertyKey): any; \
1173 }";
1174
1175const ASSERTION_RUNTIME: &str = "globalThis.__SUPERCOV_DIRECT_RUNTIME__";
1176
1177struct AssertionEdits<'s> {
1179 source: &'s str,
1180 sites: HashMap<SpanKey, (String, String, bool)>,
1181 edits: Vec<TextEdit>,
1182 in_place: Option<(String, String)>,
1185}
1186
1187impl AssertionEdits<'_> {
1188 fn opening(helper: &str, operation: &str, source: &str) -> String {
1189 format!(
1190 "{ASSERTION_RUNTIME}.{helper}({}, {}, ",
1191 serde_json::to_string(operation).expect("a string serializes"),
1192 serde_json::to_string(source).expect("a string serializes")
1193 )
1194 }
1195
1196 fn open(operation: &str, source: &str) -> String {
1197 format!(
1198 "{ASSERTION_RUNTIME}.openNodeAssertionPhase({}, {})",
1199 serde_json::to_string(operation).expect("a string serializes"),
1200 serde_json::to_string(source).expect("a string serializes")
1201 )
1202 }
1203
1204 fn wrap(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1209 let (start, end) = (call.span.start as usize, call.span.end as usize);
1210 let width = (end - start) as i64;
1211 let (opening, closing) = match &self.in_place {
1212 Some((phase, error)) => (
1213 format!("(({phase}) => {{ try {{ return {phase}.close("),
1214 format!(
1215 "); }} catch ({error}) {{ throw {phase}.fail({error}); }} }})({})",
1216 Self::open(operation, source)
1217 ),
1218 ),
1219 None => (
1220 format!(
1221 "{}() => (",
1222 Self::opening("withNodeAssertionPhase", operation, source)
1223 ),
1224 "))".into(),
1225 ),
1226 };
1227 self.edits.push(TextEdit::insert(start, -width, opening));
1228 self.edits.push(TextEdit::insert(end, width, closing));
1229 }
1230
1231 fn wrap_statement(
1236 &mut self,
1237 statement: &ExpressionStatement<'_>,
1238 operation: &str,
1239 source: &str,
1240 ) {
1241 let Some((phase, error)) = &self.in_place else {
1242 return;
1243 };
1244 let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1245 let width = (end - start) as i64;
1246 let opening = format!(
1247 "{{ const {phase} = {}; try {{ ",
1248 Self::open(operation, source)
1249 );
1250 let closing = format!(
1251 " }} catch ({error}) {{ throw {phase}.fail({error}); }} finally {{ {phase}.close(); }} }}"
1252 );
1253 self.edits.push(TextEdit::insert(start, -width, opening));
1254 self.edits.push(TextEdit::insert(end, width, closing));
1255 }
1256
1257 fn bind_statement(
1264 &mut self,
1265 statement: &ExpressionStatement<'_>,
1266 call: &CallExpression<'_>,
1267 callee: &str,
1268 operation: &str,
1269 source: &str,
1270 ) {
1271 let Some((local, _)) = self.in_place.clone() else {
1272 return;
1273 };
1274 let (start, end) = (statement.span.start as usize, statement.span.end as usize);
1275 let width = (end - start) as i64;
1276 self.edits.push(TextEdit::insert(
1277 start,
1278 -width,
1279 format!("{{ const {local}: typeof {callee} = "),
1280 ));
1281 self.bind(call, operation, source);
1282 self.edits.push(TextEdit::insert(
1283 call.callee.span().end as usize,
1284 width,
1285 format!("; {local}"),
1286 ));
1287 self.edits.push(TextEdit::insert(end, width, " }".into()));
1288 }
1289
1290 fn bind(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
1293 let callee = call.callee.span();
1294 let width = (callee.end - callee.start) as i64;
1295 let opening = Self::opening("bindNodeAssertionPhase", operation, source);
1296 self.edits
1297 .push(TextEdit::insert(callee.start as usize, -width, opening));
1298 match &call.callee {
1299 Expression::StaticMemberExpression(member) => {
1300 let between = &self.source
1303 [member.object.span().end as usize..member.property.span.start as usize];
1304 let dot = between.rfind('.').unwrap_or(0);
1305 let mut text = String::from(",");
1306 text.push_str(&between[..dot]);
1307 text.push(' ');
1308 text.push_str(&between[dot + 1..]);
1309 text.push_str(
1310 &serde_json::to_string(member.property.name.as_str())
1311 .expect("a string serializes"),
1312 );
1313 text.push(')');
1314 self.edits.push(TextEdit::replace(
1315 member.object.span().end as usize,
1316 member.property.span.end as usize,
1317 text,
1318 ));
1319 }
1320 Expression::ComputedMemberExpression(member) => {
1321 let object_end = member.object.span().end as usize;
1322 let open = object_end + self.source[object_end..].find('[').unwrap_or(0);
1323 self.edits
1324 .push(TextEdit::replace(open, open + 1, ",".into()));
1325 let close = member.span.end as usize - 1;
1326 self.edits
1327 .push(TextEdit::replace(close, close + 1, ")".into()));
1328 }
1329 _ => {
1330 self.edits.push(TextEdit::insert(
1331 callee.end as usize,
1332 width,
1333 ", null)".into(),
1334 ));
1335 }
1336 }
1337 }
1338}
1339
1340impl<'a> Visit<'a> for AssertionEdits<'_> {
1341 fn visit_expression_statement(&mut self, statement: &ExpressionStatement<'a>) {
1342 if self.in_place.is_some()
1343 && let Expression::CallExpression(call) = &statement.expression
1344 && let Some(&(_, _, bound)) = self.sites.get(&span_key(call.span))
1345 {
1346 let entity = entity_name(&call.callee);
1347 if !bound || entity.is_some() {
1348 let (operation, source, _) = self
1349 .sites
1350 .remove(&span_key(call.span))
1351 .expect("the site was just found");
1352 match entity {
1353 Some(callee) if bound => {
1354 self.bind_statement(statement, call, &callee, &operation, &source)
1355 }
1356 _ => self.wrap_statement(statement, &operation, &source),
1357 }
1358 }
1359 }
1360 walk::walk_expression_statement(self, statement);
1361 }
1362
1363 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1364 if let Some((operation, source, bound)) = self.sites.remove(&span_key(call.span)) {
1365 if bound {
1366 self.bind(call, &operation, &source);
1367 } else {
1368 self.wrap(call, &operation, &source);
1369 }
1370 }
1371 walk::walk_call_expression(self, call);
1372 }
1373}
1374
1375fn entity_name(callee: &Expression<'_>) -> Option<String> {
1378 match callee {
1379 Expression::Identifier(identifier) => Some(identifier.name.to_string()),
1380 Expression::StaticMemberExpression(member) if !member.optional => Some(format!(
1381 "{}.{}",
1382 entity_name(&member.object)?,
1383 member.property.name
1384 )),
1385 _ => None,
1386 }
1387}
1388
1389fn capability_import_edits(
1391 program: &Program<'_>,
1392 source: &str,
1393 wrapper: &str,
1394) -> (Vec<TextEdit>, usize) {
1395 if !capability_source_candidate(source) || !program.source_type.is_module() {
1396 return (Vec::new(), 0);
1397 }
1398 let capability_roots = capability_import_roots(program, source);
1399 if capability_roots.is_empty() {
1400 return (Vec::new(), 0);
1401 }
1402 let mut names = CandidateNames::new(source);
1403 let wrapper_local = names.allocate("__supercovImportedCapability");
1404 let mut edits = Vec::new();
1405 let mut wrapped = 0;
1406 for statement in &program.body {
1407 let Statement::ImportDeclaration(declaration) = statement else {
1408 continue;
1409 };
1410 if declaration.import_kind == ImportOrExportKind::Type
1411 || excluded_capability_import(declaration.source.value.as_str(), wrapper)
1412 {
1413 continue;
1414 }
1415 let mut declarators = Vec::new();
1416 for specifier in declaration.specifiers.iter().flatten() {
1417 let (local, shorthand) = match specifier {
1418 ImportDeclarationSpecifier::ImportSpecifier(specifier)
1419 if specifier.import_kind == ImportOrExportKind::Type =>
1420 {
1421 continue;
1422 }
1423 ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
1424 &specifier.local,
1425 specifier.imported.span() == specifier.local.span,
1426 ),
1427 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1428 (&specifier.local, false)
1429 }
1430 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1431 (&specifier.local, false)
1432 }
1433 };
1434 let original = local.name.to_string();
1435 if !capability_roots.contains(&original) {
1436 continue;
1437 }
1438 let raw = names.allocate(&format!("__supercovRaw{original}"));
1439 if shorthand {
1440 edits.push(TextEdit::insert(
1441 local.span.end as usize,
1442 0,
1443 format!(" as {raw}"),
1444 ));
1445 } else {
1446 edits.push(TextEdit::replace(
1447 local.span.start as usize,
1448 local.span.end as usize,
1449 raw.clone(),
1450 ));
1451 }
1452 declarators.push(format!("{original} = {wrapper_local}({raw})"));
1453 wrapped += 1;
1454 }
1455 if !declarators.is_empty() {
1456 edits.push(TextEdit::insert(
1457 declaration.span.end as usize,
1458 i64::MAX,
1459 format!("; const {};", declarators.join(", ")),
1460 ));
1461 }
1462 }
1463 if wrapped > 0 {
1464 let first = program
1467 .body
1468 .first()
1469 .map_or(0, |statement| statement.span().start as usize);
1470 edits.push(TextEdit::insert(
1471 first,
1472 i64::MIN,
1473 format!(
1474 "import {{ wrapImportedCapability as {wrapper_local} }} from {}; ",
1475 serde_json::to_string(wrapper).expect("a string serializes")
1476 ),
1477 ));
1478 }
1479 (edits, wrapped)
1480}
1481
1482fn transform_capability_imports<'a>(
1486 allocator: &'a Allocator,
1487 program: &mut Program<'a>,
1488 source: &str,
1489 wrapper: &str,
1490) -> usize {
1491 if !capability_source_candidate(source) || !program.source_type.is_module() {
1492 return 0;
1493 }
1494 let capability_roots = capability_import_roots(program, source);
1495 if capability_roots.is_empty() {
1496 return 0;
1497 }
1498 let ast = AstBuilder::new(allocator);
1499 let mut names = CandidateNames::new(source);
1500 let wrapper_local = names.allocate("__supercovImportedCapability");
1501 let mut wrapped = 0;
1502 let mut output = ast.vec();
1503 for statement in program.body.take_in(allocator) {
1504 let Statement::ImportDeclaration(mut declaration) = statement else {
1505 output.push(statement);
1506 continue;
1507 };
1508 let module_name = declaration.source.value.as_str();
1509 if declaration.import_kind == ImportOrExportKind::Type
1510 || excluded_capability_import(module_name, wrapper)
1511 {
1512 output.push(Statement::ImportDeclaration(declaration));
1513 continue;
1514 }
1515 let mut declarations = ast.vec();
1516 for specifier in declaration.specifiers.iter_mut().flatten() {
1517 let local = match specifier {
1518 ImportDeclarationSpecifier::ImportSpecifier(specifier)
1519 if specifier.import_kind == ImportOrExportKind::Type =>
1520 {
1521 continue;
1522 }
1523 ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
1524 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1525 &mut specifier.local
1526 }
1527 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1528 &mut specifier.local
1529 }
1530 };
1531 let original = local.name.to_string();
1532 if !capability_roots.contains(&original) {
1533 continue;
1534 }
1535 let raw = names.allocate(&format!("__supercovRaw{original}"));
1536 local.name = ast.ident(&raw);
1537 let wrapped_value = ast.expression_call(
1538 Span::default(),
1539 ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
1540 NONE,
1541 ast.vec1(Argument::from(
1542 ast.expression_identifier(Span::default(), ast.ident(&raw)),
1543 )),
1544 false,
1545 );
1546 declarations.push(ast.variable_declarator(
1547 Span::default(),
1548 VariableDeclarationKind::Const,
1549 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
1550 NONE,
1551 Some(wrapped_value),
1552 false,
1553 ));
1554 wrapped += 1;
1555 }
1556 output.push(Statement::ImportDeclaration(declaration));
1557 if !declarations.is_empty() {
1558 output.push(Statement::VariableDeclaration(
1559 ast.alloc_variable_declaration(
1560 Span::default(),
1561 VariableDeclarationKind::Const,
1562 declarations,
1563 false,
1564 ),
1565 ));
1566 }
1567 }
1568 if wrapped > 0 {
1569 output.insert(
1570 0,
1571 Statement::ImportDeclaration(ast.alloc_import_declaration(
1572 Span::default(),
1573 Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
1574 Span::default(),
1575 ast.module_export_name_identifier_name(
1576 Span::default(),
1577 ast.ident("wrapImportedCapability"),
1578 ),
1579 ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
1580 ImportOrExportKind::Value,
1581 ))),
1582 ast.string_literal(Span::default(), ast.str(wrapper), None),
1583 None,
1584 NONE,
1585 ImportOrExportKind::Value,
1586 )),
1587 );
1588 }
1589 program.body = output;
1590 wrapped
1591}
1592
1593fn canonical_assert_module(value: &str) -> Option<String> {
1594 NODE_ASSERT_MODULES.contains(&value).then(|| {
1595 if value.starts_with("node:") {
1596 value.into()
1597 } else {
1598 format!("node:{value}")
1599 }
1600 })
1601}
1602
1603fn required_assert_module(
1604 expression: &Expression<'_>,
1605 scoping: &oxc_semantic::Scoping,
1606) -> Option<String> {
1607 if let Expression::CallExpression(call) = expression
1608 && matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
1609 && matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
1610 && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
1611 {
1612 return canonical_assert_module(module.value.as_str());
1613 }
1614 if let Expression::StaticMemberExpression(member) = expression
1615 && member.property.name == "strict"
1616 && let Some(module) = required_assert_module(&member.object, scoping)
1617 {
1618 return Some(if module == "node:assert" {
1619 "node:assert/strict".into()
1620 } else {
1621 module
1622 });
1623 }
1624 None
1625}
1626
1627#[derive(Default)]
1628struct NodeAssertionBindings {
1629 objects: HashMap<SymbolId, String>,
1630 direct: HashMap<SymbolId, String>,
1631 expects: HashSet<SymbolId>,
1632 global_expect: bool,
1636}
1637
1638fn bind_object(
1639 bindings: &mut NodeAssertionBindings,
1640 identifier: &oxc_ast::ast::BindingIdentifier<'_>,
1641 module: String,
1642) {
1643 if let Some(symbol) = identifier.symbol_id.get() {
1644 bindings.objects.insert(symbol, module);
1645 }
1646}
1647
1648fn bind_direct(
1649 bindings: &mut NodeAssertionBindings,
1650 identifier: &oxc_ast::ast::BindingIdentifier<'_>,
1651 operation: String,
1652) {
1653 if let Some(symbol) = identifier.symbol_id.get() {
1654 bindings.direct.insert(symbol, operation);
1655 }
1656}
1657
1658fn bind_expect(
1659 bindings: &mut NodeAssertionBindings,
1660 identifier: &oxc_ast::ast::BindingIdentifier<'_>,
1661) {
1662 if let Some(symbol) = identifier.symbol_id.get() {
1663 bindings.expects.insert(symbol);
1664 }
1665}
1666
1667struct NodeAssertionBindingCollector<'s> {
1668 bindings: NodeAssertionBindings,
1669 scoping: &'s oxc_semantic::Scoping,
1670 allow_contextual_expect: bool,
1671 expect_modules: HashSet<String>,
1672}
1673
1674impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
1675 fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
1676 let module_name = declaration.source.value.as_str();
1677 let assert_module = canonical_assert_module(module_name);
1678 let expect_module =
1679 self.expect_modules.contains(module_name) || self.allow_contextual_expect;
1680 for specifier in declaration.specifiers.iter().flatten() {
1681 match specifier {
1682 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1683 if let Some(module) = &assert_module {
1684 bind_object(&mut self.bindings, &specifier.local, module.clone());
1685 } else if expect_module && specifier.local.name == "expect" {
1686 bind_expect(&mut self.bindings, &specifier.local);
1687 }
1688 }
1689 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1690 if let Some(module) = &assert_module {
1691 bind_object(&mut self.bindings, &specifier.local, module.clone());
1692 }
1693 }
1694 ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
1695 let imported = specifier.imported.name().to_string();
1696 if let Some(module) = &assert_module {
1697 if imported == "strict" {
1698 bind_object(
1699 &mut self.bindings,
1700 &specifier.local,
1701 "node:assert/strict".into(),
1702 );
1703 } else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
1704 bind_direct(
1705 &mut self.bindings,
1706 &specifier.local,
1707 format!("{module}.{imported}"),
1708 );
1709 }
1710 } else if expect_module && imported == "expect" {
1711 bind_expect(&mut self.bindings, &specifier.local);
1712 }
1713 }
1714 }
1715 }
1716 walk::walk_import_declaration(self, declaration);
1717 }
1718
1719 fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
1720 if let Some(Expression::CallExpression(call)) = &declarator.init
1723 && matches!(&call.callee, Expression::Identifier(i) if i.name == "require" && referenced_symbol(i, self.scoping).is_none())
1724 && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
1725 && self.expect_modules.contains(module.value.as_str())
1726 && let BindingPattern::ObjectPattern(pattern) = &declarator.id
1727 {
1728 for property in &pattern.properties {
1729 if property.key.static_name().as_deref() == Some("expect")
1730 && let BindingPattern::BindingIdentifier(local) = &property.value
1731 {
1732 bind_expect(&mut self.bindings, local);
1733 }
1734 }
1735 }
1736 if let Some(module) = declarator
1737 .init
1738 .as_ref()
1739 .and_then(|expression| required_assert_module(expression, self.scoping))
1740 {
1741 match &declarator.id {
1742 BindingPattern::BindingIdentifier(identifier) => {
1743 bind_object(&mut self.bindings, identifier, module);
1744 }
1745 BindingPattern::ObjectPattern(pattern) => {
1746 for property in &pattern.properties {
1747 let Some(imported) = property.key.static_name() else {
1748 continue;
1749 };
1750 let BindingPattern::BindingIdentifier(local) = &property.value else {
1751 continue;
1752 };
1753 if imported == "strict" {
1754 bind_object(&mut self.bindings, local, "node:assert/strict".into());
1755 } else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
1756 bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
1757 }
1758 }
1759 }
1760 _ => {}
1761 }
1762 }
1763 walk::walk_variable_declarator(self, declarator);
1764 }
1765}
1766
1767fn node_assertion_bindings(
1768 program: &Program<'_>,
1769 scoping: &oxc_semantic::Scoping,
1770 extra_expect_modules: &[String],
1771) -> NodeAssertionBindings {
1772 let allow_contextual_expect = program.source_text.contains("node:test")
1773 && program
1774 .source_text
1775 .split(|character: char| !character.is_alphanumeric() && character != '_')
1776 .any(|word| word == "expect");
1777 let mut collector = NodeAssertionBindingCollector {
1778 bindings: NodeAssertionBindings::default(),
1779 scoping,
1780 allow_contextual_expect,
1781 expect_modules: ["vitest", "@jest/globals", "expect", "@playwright/test"]
1782 .into_iter()
1783 .map(str::to_owned)
1784 .chain(extra_expect_modules.iter().cloned())
1785 .collect(),
1786 };
1787 collector.visit_program(program);
1788 let unresolved = scoping.root_unresolved_references();
1789 let uses = |name: &str| unresolved.contains_key(name);
1790 collector.bindings.global_expect =
1791 uses("expect") && (uses("test") || uses("it") || uses("describe"));
1792 collector.bindings
1793}
1794
1795struct NodeAssertionSiteCollector<'s> {
1796 source: &'s str,
1797 file: &'s str,
1798 bindings: &'s NodeAssertionBindings,
1799 scoping: &'s oxc_semantic::Scoping,
1800 sites: HashMap<SpanKey, (String, String, bool)>,
1801}
1802
1803pub fn project_source_type(path: &Path) -> Result<SourceType, String> {
1811 let source_type = SourceType::from_path(path).map_err(|error| error.to_string())?;
1812 Ok(if source_type.is_typescript() {
1813 source_type
1814 } else {
1815 source_type.with_jsx(true)
1816 })
1817}
1818
1819fn referenced_symbol(
1820 identifier: &oxc_ast::ast::IdentifierReference<'_>,
1821 scoping: &oxc_semantic::Scoping,
1822) -> Option<SymbolId> {
1823 identifier
1824 .reference_id
1825 .get()
1826 .and_then(|reference| scoping.get_reference(reference).symbol_id())
1827}
1828
1829fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1830 match expression {
1831 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1832 Expression::ComputedMemberExpression(member) => match &member.expression {
1833 Expression::StringLiteral(literal) => Some(literal.value.as_str()),
1834 _ => None,
1835 },
1836 _ => None,
1837 }
1838}
1839
1840fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
1841 match expression {
1842 Expression::StaticMemberExpression(member) => Some(&member.object),
1843 Expression::ComputedMemberExpression(member) => Some(&member.object),
1844 _ => None,
1845 }
1846}
1847
1848fn expect_operation(
1849 callee: &Expression<'_>,
1850 bindings: &NodeAssertionBindings,
1851 scoping: &oxc_semantic::Scoping,
1852) -> Option<String> {
1853 let mut current = callee;
1854 let mut matchers = Vec::new();
1855 while let Some(name) = assertion_member_name(current) {
1856 matchers.push(name.to_owned());
1857 current = assertion_member_object(current)?;
1858 }
1859 matchers.reverse();
1860 let matcher = matchers.last()?;
1861 if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
1862 return None;
1863 }
1864 let Expression::CallExpression(expect_call) = current else {
1865 return None;
1866 };
1867 let mut root = &expect_call.callee;
1874 let mut prefix = Vec::new();
1875 while let Some(name) = assertion_member_name(root) {
1876 prefix.push(name.to_owned());
1877 root = assertion_member_object(root)?;
1878 }
1879 prefix.reverse();
1880 let Expression::Identifier(identifier) = root else {
1881 return None;
1882 };
1883 let recognized = match referenced_symbol(identifier, scoping) {
1884 Some(symbol) => bindings.expects.contains(&symbol),
1885 None => bindings.global_expect && identifier.name == "expect",
1886 };
1887 recognized.then(|| {
1888 let mut parts = prefix;
1889 parts.extend(matchers.iter().cloned());
1890 format!("expect.{}", parts.join("."))
1891 })
1892}
1893
1894#[derive(Default)]
1895struct AwaitYieldScanner {
1896 found: bool,
1897}
1898
1899impl<'a> Visit<'a> for AwaitYieldScanner {
1900 fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
1901 self.found = true;
1902 }
1903
1904 fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
1905 self.found = true;
1906 }
1907
1908 fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
1909
1910 fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
1911}
1912
1913impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
1914 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1915 let operation = match &call.callee {
1916 Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
1917 .and_then(|symbol| {
1918 self.bindings.direct.get(&symbol).cloned().or_else(|| {
1919 self.bindings
1920 .objects
1921 .get(&symbol)
1922 .map(|module| format!("{module}.ok"))
1923 })
1924 }),
1925 callee => {
1926 let member_operation = assertion_member_object(callee).and_then(|object| {
1927 let Expression::Identifier(identifier) = object else {
1928 return None;
1929 };
1930 let symbol = referenced_symbol(identifier, self.scoping)?;
1931 let method = assertion_member_name(callee)?;
1932 self.bindings
1933 .objects
1934 .get(&symbol)
1935 .filter(|_| NODE_ASSERT_METHODS.contains(&method))
1936 .map(|module| format!("{module}.{method}"))
1937 });
1938 member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
1939 }
1940 };
1941 if let Some(operation) = operation {
1942 let mut unsafe_argument = AwaitYieldScanner::default();
1943 unsafe_argument.visit_expression(&call.callee);
1944 for argument in &call.arguments {
1945 unsafe_argument.visit_argument(argument);
1946 }
1947 let optional = call.optional
1948 || matches!(&call.callee,
1949 Expression::StaticMemberExpression(m) if m.optional)
1950 || matches!(&call.callee, Expression::ComputedMemberExpression(m) if m.optional);
1951 if optional {
1954 walk::walk_call_expression(self, call);
1955 return;
1956 }
1957 let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
1958 self.sites.insert(
1959 span_key(call.span),
1960 (
1961 operation,
1962 format!("{}:{line}:{column}", self.file),
1963 unsafe_argument.found,
1964 ),
1965 );
1966 }
1967 walk::walk_call_expression(self, call);
1968 }
1969}
1970
1971pub fn instrument_node_assertion_phases(
1976 source: &str,
1977 file: &str,
1978) -> Result<NodeAssertionInstrumentation, CandidateError> {
1979 instrument_node_assertion_phases_with_expect_modules(source, file, &[])
1980}
1981
1982pub fn instrument_node_assertion_phases_with_expect_modules(
1983 source: &str,
1984 file: &str,
1985 extra_expect_modules: &[String],
1986) -> Result<NodeAssertionInstrumentation, CandidateError> {
1987 instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
1988}
1989
1990pub fn instrument_node_assertion_phases_with_runtime_hooks(
1991 source: &str,
1992 file: &str,
1993 extra_expect_modules: &[String],
1994 capability_wrapper: Option<&str>,
1995) -> Result<NodeAssertionInstrumentation, CandidateError> {
1996 instrument_node_assertion_phases_with_runtime_imports(
1997 source,
1998 file,
1999 extra_expect_modules,
2000 capability_wrapper,
2001 None,
2002 )
2003}
2004
2005const 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);""#;
2014
2015pub(crate) fn runtime_loader(bootstrap: &str) -> String {
2023 let specifier = serde_json::to_string(bootstrap).expect("a path string serializes");
2024 format!(
2025 "if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {{ new Function(\"stack\", \"specifier\", {RUNTIME_LOADER_BODY})(new Error().stack, {specifier}); }} catch {{}}"
2026 )
2027}
2028
2029pub fn instrument_node_assertion_phases_with_runtime_imports(
2030 source: &str,
2031 file: &str,
2032 extra_expect_modules: &[String],
2033 capability_wrapper: Option<&str>,
2034 assertion_runtime: Option<&str>,
2035) -> Result<NodeAssertionInstrumentation, CandidateError> {
2036 let _positions = PositionScope::enter();
2037 let assertion_candidate = source.contains("assert") || source.contains("expect");
2038 let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
2039 if !assertion_candidate && !capability_candidate {
2040 return Ok(NodeAssertionInstrumentation {
2041 code: source.into(),
2042 assertions: 0,
2043 capability_imports: 0,
2044 });
2045 }
2046 let source_type =
2047 project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
2048 let allocator = Allocator::default();
2049 let parsed = Parser::new(&allocator, source, source_type).parse();
2050 if !parsed.errors.is_empty() {
2051 return Err(CandidateError::Parse(
2052 parsed
2053 .errors
2054 .into_iter()
2055 .map(|error| error.to_string())
2056 .collect(),
2057 ));
2058 }
2059 let mut assertions = 0;
2060 let mut edits = Vec::new();
2061
2062 if assertion_candidate {
2063 let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
2064 let bindings =
2065 node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
2066 let mut collector = NodeAssertionSiteCollector {
2067 source,
2068 file,
2069 bindings: &bindings,
2070 scoping: semantic.scoping(),
2071 sites: HashMap::new(),
2072 };
2073 collector.visit_program(&parsed.program);
2074 assertions = collector.sites.len();
2075 if assertions > 0 {
2076 let in_place = source_type.is_typescript().then(|| {
2077 let mut names = CandidateNames::new(source);
2078 (
2079 names.allocate("__supercovAssertionPhase"),
2080 names.allocate("__supercovAssertionError"),
2081 )
2082 });
2083 let mut located = AssertionEdits {
2084 source,
2085 sites: collector.sites,
2086 edits: Vec::new(),
2087 in_place,
2088 };
2089 located.visit_program(&parsed.program);
2090 edits.extend(located.edits);
2091 }
2092 }
2093 let capability_imports = capability_wrapper.map_or(0, |wrapper| {
2094 let (found, wrapped) = capability_import_edits(&parsed.program, source, wrapper);
2095 edits.extend(found);
2096 wrapped
2097 });
2098 if assertions == 0 && capability_imports == 0 {
2099 return Ok(NodeAssertionInstrumentation {
2100 code: source.into(),
2101 assertions: 0,
2102 capability_imports: 0,
2103 });
2104 }
2105 let module = parsed.program.source_type.is_module();
2106 if let Some(bootstrap) = assertion_runtime.filter(|_| assertions > 0) {
2113 let at = if source.starts_with("#!") {
2114 source
2115 .find('\n')
2116 .map_or(source.len(), |newline| newline + 1)
2117 } else {
2118 0
2119 };
2120 edits.push(TextEdit::insert(
2121 at,
2122 i64::MIN,
2123 format!("{} ", runtime_loader(bootstrap)),
2124 ));
2125 }
2126 let reference = Path::new(file)
2130 .file_name()
2131 .and_then(|name| name.to_str())
2132 .unwrap_or(file);
2133 let (mut code, map) = apply_text_edits(source, edits, reference)?;
2134 if assertions > 0 && source_type.is_typescript() {
2138 if module {
2139 code.push_str(&format!(
2140 "\ndeclare global {{ var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE}; }}\n"
2141 ));
2142 } else {
2143 code.push_str(&format!(
2144 "\ndeclare var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE};\n"
2145 ));
2146 }
2147 }
2148 let map = map.to_json_string();
2149 code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
2150 BASE64_STANDARD.encode_string(map, &mut code);
2151 code.push('\n');
2152 Ok(NodeAssertionInstrumentation {
2153 code,
2154 assertions,
2155 capability_imports,
2156 })
2157}
2158
2159type SpanKey = (u32, u32);
2160
2161#[derive(Default)]
2162struct SafetyAnalysis {
2163 source_sensitive_functions: HashSet<SpanKey>,
2164 semantic_limitations: Vec<CandidateLimitation>,
2165 dynamic_limitations: Vec<CandidateLimitation>,
2166}
2167
2168struct SourceReads<'s> {
2178 scoping: &'s oxc_semantic::Scoping,
2179 symbols: HashSet<SymbolId>,
2180}
2181
2182impl SourceReads<'_> {
2183 fn read(&mut self, expression: &Expression<'_>) {
2184 match expression {
2185 Expression::Identifier(identifier) => {
2186 self.symbols
2187 .extend(referenced_symbol(identifier, self.scoping));
2188 }
2189 Expression::ParenthesizedExpression(inner) => self.read(&inner.expression),
2190 Expression::TSAsExpression(inner) => self.read(&inner.expression),
2191 Expression::TSSatisfiesExpression(inner) => self.read(&inner.expression),
2192 Expression::TSTypeAssertion(inner) => self.read(&inner.expression),
2193 Expression::TSNonNullExpression(inner) => self.read(&inner.expression),
2194 Expression::ConditionalExpression(inner) => {
2195 self.read(&inner.consequent);
2196 self.read(&inner.alternate);
2197 }
2198 Expression::LogicalExpression(inner) => {
2199 self.read(&inner.left);
2200 self.read(&inner.right);
2201 }
2202 Expression::SequenceExpression(inner) => {
2203 if let Some(last) = inner.expressions.last() {
2204 self.read(last);
2205 }
2206 }
2207 Expression::AssignmentExpression(inner) => self.read(&inner.right),
2208 _ => {}
2209 }
2210 }
2211}
2212
2213impl<'a> Visit<'a> for SourceReads<'_> {
2214 fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
2215 if member.property.name == "toString" {
2216 self.read(&member.object);
2217 }
2218 walk::walk_static_member_expression(self, member);
2219 }
2220
2221 fn visit_computed_member_expression(
2222 &mut self,
2223 member: &oxc_ast::ast::ComputedMemberExpression<'a>,
2224 ) {
2225 self.read(&member.expression);
2226 walk::walk_computed_member_expression(self, member);
2227 }
2228
2229 fn visit_object_property(&mut self, property: &oxc_ast::ast::ObjectProperty<'a>) {
2230 if property.computed
2231 && let Some(key) = property.key.as_expression()
2232 {
2233 self.read(key);
2234 }
2235 walk::walk_object_property(self, property);
2236 }
2237
2238 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
2239 if expression_is_identifier(&call.callee, "String")
2240 && let Some(argument) = call.arguments.first().and_then(Argument::as_expression)
2241 {
2242 self.read(argument);
2243 }
2244 walk::walk_call_expression(self, call);
2245 }
2246
2247 fn visit_template_literal(&mut self, template: &oxc_ast::ast::TemplateLiteral<'a>) {
2248 for expression in &template.expressions {
2249 self.read(expression);
2250 }
2251 walk::walk_template_literal(self, template);
2252 }
2253
2254 fn visit_binary_expression(&mut self, binary: &oxc_ast::ast::BinaryExpression<'a>) {
2255 if matches!(
2256 binary.operator,
2257 BinaryOperator::Addition
2258 | BinaryOperator::LessThan
2259 | BinaryOperator::LessEqualThan
2260 | BinaryOperator::GreaterThan
2261 | BinaryOperator::GreaterEqualThan
2262 ) {
2263 self.read(&binary.left);
2264 self.read(&binary.right);
2265 }
2266 walk::walk_binary_expression(self, binary);
2267 }
2268}
2269
2270fn bound_symbols<State>(
2273 own: Option<&oxc_ast::ast::BindingIdentifier<'_>>,
2274 scoping: &oxc_semantic::Scoping,
2275 context: &TraverseCtx<'_, State>,
2276) -> Vec<SymbolId> {
2277 let mut symbols = own
2278 .and_then(|identifier| identifier.symbol_id.get())
2279 .into_iter()
2280 .collect::<Vec<_>>();
2281 match context.ancestors().next() {
2282 Some(Ancestor::VariableDeclaratorInit(parent)) => symbols.extend(
2283 parent
2284 .id()
2285 .get_binding_identifier()
2286 .and_then(|identifier| identifier.symbol_id.get()),
2287 ),
2288 Some(Ancestor::AssignmentExpressionRight(parent)) => {
2289 if let AssignmentTarget::AssignmentTargetIdentifier(target) = parent.left() {
2290 symbols.extend(referenced_symbol(target, scoping));
2291 }
2292 }
2293 _ => {}
2294 }
2295 symbols
2296}
2297
2298struct SafetyScanner<'s> {
2299 source: &'s str,
2300 file: &'s str,
2301 scoping: &'s oxc_semantic::Scoping,
2302 source_reads: HashSet<SymbolId>,
2303 source_sensitive_functions: HashSet<SpanKey>,
2304 function_limitations: Vec<CandidateLimitation>,
2305 with_limitations: Vec<CandidateLimitation>,
2306 dynamic_limitations: Vec<CandidateLimitation>,
2307 unsafe_function_depth: usize,
2308 with_depth: usize,
2309}
2310
2311#[derive(Clone, Copy, PartialEq, Eq)]
2312enum PointPass {
2313 Statements,
2314 Functions,
2315}
2316
2317struct PointCollector<'s> {
2318 source: &'s str,
2319 file: &'s str,
2320 pass: PointPass,
2321 points: Vec<CandidatePoint>,
2322 statement_targets: HashMap<SpanKey, Vec<String>>,
2323 function_targets: HashMap<SpanKey, String>,
2324 rendered_targets: HashMap<SpanKey, String>,
2325 source_sensitive_functions: &'s HashSet<SpanKey>,
2326 erased_imports: &'s HashSet<SpanKey>,
2327 unsafe_function_depth: usize,
2328 with_depth: usize,
2329 ambient_depth: usize,
2330}
2331
2332#[derive(Default)]
2333struct PointAnalysis {
2334 points: Vec<CandidatePoint>,
2335 statement_targets: HashMap<SpanKey, Vec<String>>,
2336 function_targets: HashMap<SpanKey, String>,
2337 rendered_targets: HashMap<SpanKey, String>,
2338}
2339
2340#[derive(Clone)]
2341struct PointTarget {
2342 index: usize,
2343}
2344
2345impl PointCollector<'_> {
2346 fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
2347 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
2348 CandidatePoint {
2349 id: stable_id(self.source, self.file, kind, span, ""),
2350 kind: kind.to_string(),
2351 file: self.file.to_string(),
2352 line,
2353 column,
2354 source: source_slice(self.source, span).to_string(),
2355 label,
2356 }
2357 }
2358
2359 fn unsafe_context(&self) -> bool {
2360 self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
2361 }
2362
2363 fn exit_function(&mut self, span: Span) {
2364 if self.source_sensitive_functions.contains(&span_key(span)) {
2365 self.unsafe_function_depth -= 1;
2366 }
2367 }
2368}
2369
2370fn function_label<State>(
2371 own_name: Option<&str>,
2372 context: &TraverseCtx<'_, State>,
2373) -> Option<String> {
2374 if let Some(name) = own_name {
2375 return Some(name.to_string());
2376 }
2377 match context.ancestors().next()? {
2378 Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
2379 Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
2380 Ancestor::VariableDeclaratorInit(parent) => parent
2381 .id()
2382 .get_binding_identifier()
2383 .map(|identifier| identifier.name.to_string()),
2384 _ => None,
2385 }
2386}
2387
2388fn property_label(key: &PropertyKey<'_>) -> Option<String> {
2389 key.static_name()
2390 .map(|name| name.into_owned())
2391 .or_else(|| match key {
2392 PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
2393 _ => None,
2394 })
2395}
2396
2397fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
2398 match context.ancestors().next() {
2399 Some(Ancestor::ObjectPropertyValue(parent))
2400 if *parent.method() || *parent.kind() != PropertyKind::Init =>
2401 {
2402 *parent.span()
2403 }
2404 Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
2405 _ => span,
2406 }
2407}
2408
2409fn type_only_import(statement: &Statement<'_>) -> bool {
2410 let Statement::ImportDeclaration(declaration) = statement else {
2411 return false;
2412 };
2413 declaration.import_kind == ImportOrExportKind::Type
2416}
2417
2418fn erased_imports(program: &Program<'_>, elide_type_imports: bool) -> HashSet<SpanKey> {
2421 let semantic = SemanticBuilder::new().build(program).semantic;
2422 let scoping = semantic.scoping();
2423 program
2424 .body
2425 .iter()
2426 .filter_map(|statement| {
2427 let Statement::ImportDeclaration(declaration) = statement else {
2428 return None;
2429 };
2430 let erased = declaration.import_kind == ImportOrExportKind::Type
2431 || (elide_type_imports
2432 && program.source_type.is_typescript()
2433 && !program.source_type.is_jsx()
2434 && declaration.specifiers.as_ref().is_some_and(|specifiers| {
2435 !specifiers.is_empty()
2436 && specifiers.iter().all(|specifier| {
2437 let local = match specifier {
2438 ImportDeclarationSpecifier::ImportSpecifier(s) => {
2439 if s.import_kind == ImportOrExportKind::Type {
2440 return true;
2441 }
2442 &s.local
2443 }
2444 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
2445 &s.local
2446 }
2447 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
2448 &s.local
2449 }
2450 };
2451 local.symbol_id.get().is_some_and(|symbol| {
2452 let mut refs =
2453 scoping.get_resolved_references(symbol).peekable();
2454 refs.peek().is_some()
2457 && refs.all(|r| r.is_type() && !r.is_value())
2458 })
2459 })
2460 }));
2461 erased.then_some(span_key(declaration.span))
2462 })
2463 .collect()
2464}
2465
2466fn executable_statement(statement: &Statement<'_>) -> bool {
2467 !matches!(
2468 statement,
2469 Statement::BlockStatement(_)
2470 | Statement::EmptyStatement(_)
2471 | Statement::FunctionDeclaration(_)
2472 | Statement::ExportNamedDeclaration(_)
2473 | Statement::ExportDefaultDeclaration(_)
2474 ) && !statement.is_typescript_syntax()
2475 && !type_only_import(statement)
2476}
2477
2478impl<'a> Traverse<'a, ()> for PointCollector<'_> {
2479 fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
2480 let mut ancestors = context.ancestors();
2481 let parent = ancestors.next();
2482 let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
2483 && matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
2484 if self.pass != PointPass::Statements
2485 || self.unsafe_context()
2486 || !executable_statement(node)
2487 || self.erased_imports.contains(&span_key(node.span()))
2488 || matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
2489 || expression_arrow_body
2490 {
2491 return;
2492 }
2493 let point = self.point(node.span(), "statement", None);
2494 self.statement_targets
2495 .entry(span_key(node.span()))
2496 .or_default()
2497 .push(point.id.clone());
2498 self.points.push(point);
2499 }
2500
2501 fn enter_expression(&mut self, node: &mut Expression<'a>, context: &mut TraverseCtx<'a, ()>) {
2514 if self.pass != PointPass::Statements
2515 || self.unsafe_context()
2516 || !matches!(
2517 context.ancestors().next(),
2518 Some(Ancestor::JSXExpressionContainerExpression(_))
2519 )
2520 || !rendered_expression_is_measurable(node)
2521 {
2522 return;
2523 }
2524 let span = node.span();
2525 let point = self.point(span, "statement", Some("jsx-rendered-expression".into()));
2526 self.rendered_targets
2527 .insert(span_key(span), point.id.clone());
2528 self.points.push(point);
2529 }
2530
2531 fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
2532 if self.pass != PointPass::Statements
2533 || self.unsafe_context()
2534 || node.is_typescript_syntax()
2535 || matches!(node, Declaration::FunctionDeclaration(_))
2536 || !matches!(
2537 context.ancestors().next(),
2538 Some(Ancestor::ExportNamedDeclarationDeclaration(_))
2539 | Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2540 )
2541 {
2542 return;
2543 }
2544 let point = self.point(node.span(), "statement", None);
2545 self.statement_targets
2546 .entry(span_key(node.span()))
2547 .or_default()
2548 .push(point.id.clone());
2549 self.points.push(point);
2550 }
2551
2552 fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
2553 if self.pass != PointPass::Statements
2559 || self.unsafe_context()
2560 || node.declare
2561 || !matches!(
2562 context.ancestors().next(),
2563 Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
2564 )
2565 {
2566 return;
2567 }
2568 let point = self.point(node.span, "statement", None);
2569 self.statement_targets
2570 .entry(span_key(node.span))
2571 .or_default()
2572 .push(point.id.clone());
2573 self.points.push(point);
2574 }
2575
2576 fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
2577 let point_span = function_point_span(node.span, context);
2578 let label = if point_span == node.span {
2579 function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
2580 } else {
2581 None
2582 };
2583 if self
2584 .source_sensitive_functions
2585 .contains(&span_key(node.span))
2586 {
2587 self.unsafe_function_depth += 1;
2588 return;
2589 }
2590 if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
2591 let point = self.point(point_span, "function", label);
2592 self.function_targets
2593 .insert(span_key(node.span), point.id.clone());
2594 self.points.push(point);
2595 }
2596 }
2597
2598 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
2599 self.exit_function(node.span);
2600 }
2601
2602 fn enter_arrow_function_expression(
2603 &mut self,
2604 node: &mut ArrowFunctionExpression<'a>,
2605 context: &mut TraverseCtx<'a, ()>,
2606 ) {
2607 let point_span = function_point_span(node.span, context);
2608 let label = if point_span == node.span {
2609 function_label(None, context)
2610 } else {
2611 None
2612 };
2613 if self
2614 .source_sensitive_functions
2615 .contains(&span_key(node.span))
2616 {
2617 self.unsafe_function_depth += 1;
2618 return;
2619 }
2620 if self.pass == PointPass::Functions && !self.unsafe_context() {
2621 let point = self.point(point_span, "function", label);
2622 self.function_targets
2623 .insert(span_key(node.span), point.id.clone());
2624 self.points.push(point);
2625 }
2626 }
2627
2628 fn exit_arrow_function_expression(
2629 &mut self,
2630 node: &mut ArrowFunctionExpression<'a>,
2631 _context: &mut TraverseCtx<'a, ()>,
2632 ) {
2633 self.exit_function(node.span);
2634 }
2635
2636 fn enter_with_statement(
2637 &mut self,
2638 _node: &mut WithStatement<'a>,
2639 _context: &mut TraverseCtx<'a, ()>,
2640 ) {
2641 self.with_depth += 1;
2642 }
2643
2644 fn exit_with_statement(
2645 &mut self,
2646 _node: &mut WithStatement<'a>,
2647 _context: &mut TraverseCtx<'a, ()>,
2648 ) {
2649 self.with_depth -= 1;
2650 }
2651
2652 fn enter_ts_global_declaration(
2653 &mut self,
2654 _node: &mut TSGlobalDeclaration<'a>,
2655 _context: &mut TraverseCtx<'a, ()>,
2656 ) {
2657 self.ambient_depth += 1;
2658 }
2659
2660 fn exit_ts_global_declaration(
2661 &mut self,
2662 _node: &mut TSGlobalDeclaration<'a>,
2663 _context: &mut TraverseCtx<'a, ()>,
2664 ) {
2665 self.ambient_depth -= 1;
2666 }
2667
2668 fn enter_ts_module_declaration(
2669 &mut self,
2670 node: &mut TSModuleDeclaration<'a>,
2671 _context: &mut TraverseCtx<'a, ()>,
2672 ) {
2673 if node.declare {
2674 self.ambient_depth += 1;
2675 }
2676 }
2677
2678 fn exit_ts_module_declaration(
2679 &mut self,
2680 node: &mut TSModuleDeclaration<'a>,
2681 _context: &mut TraverseCtx<'a, ()>,
2682 ) {
2683 if node.declare {
2684 self.ambient_depth -= 1;
2685 }
2686 }
2687}
2688
2689fn rendered_expression_is_measurable(expression: &Expression<'_>) -> bool {
2692 match expression {
2693 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
2695 Expression::Identifier(_)
2697 | Expression::ThisExpression(_)
2698 | Expression::StaticMemberExpression(_)
2699 | Expression::ComputedMemberExpression(_)
2700 | Expression::PrivateFieldExpression(_)
2701 | Expression::JSXElement(_)
2702 | Expression::JSXFragment(_) => false,
2703 Expression::ChainExpression(chain) => {
2706 matches!(chain.expression, ChainElement::CallExpression(_))
2707 }
2708 other => !other.is_literal(),
2709 }
2710}
2711
2712fn collect_points<'a>(
2713 allocator: &'a Allocator,
2714 program: &mut Program<'a>,
2715 source: &str,
2716 file: &str,
2717 source_sensitive_functions: &HashSet<SpanKey>,
2718 erased_imports: &HashSet<SpanKey>,
2719) -> PointAnalysis {
2720 let mut analysis = PointAnalysis::default();
2721 for pass in [PointPass::Statements, PointPass::Functions] {
2722 let mut collector = PointCollector {
2723 source,
2724 file,
2725 pass,
2726 points: Vec::new(),
2727 statement_targets: HashMap::new(),
2728 function_targets: HashMap::new(),
2729 rendered_targets: HashMap::new(),
2730 source_sensitive_functions,
2731 erased_imports,
2732 unsafe_function_depth: 0,
2733 with_depth: 0,
2734 ambient_depth: 0,
2735 };
2736 traverse_mut(&mut collector, allocator, program, Default::default(), ());
2737 analysis.points.extend(collector.points);
2738 analysis
2739 .statement_targets
2740 .extend(collector.statement_targets);
2741 analysis.function_targets.extend(collector.function_targets);
2742 analysis.rendered_targets.extend(collector.rendered_targets);
2743 }
2744 analysis
2745}
2746
2747impl<'s> SafetyScanner<'s> {
2748 fn new(source: &'s str, file: &'s str, scoping: &'s oxc_semantic::Scoping) -> Self {
2749 Self {
2750 source,
2751 file,
2752 scoping,
2753 source_reads: HashSet::new(),
2754 source_sensitive_functions: HashSet::new(),
2755 function_limitations: Vec::new(),
2756 with_limitations: Vec::new(),
2757 dynamic_limitations: Vec::new(),
2758 unsafe_function_depth: 0,
2759 with_depth: 0,
2760 }
2761 }
2762
2763 fn limitation(
2764 &self,
2765 span: Span,
2766 kind: &str,
2767 suffix: &str,
2768 reason: &str,
2769 ) -> CandidateLimitation {
2770 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
2771 CandidateLimitation {
2772 id: stable_id(self.source, self.file, kind, span, suffix),
2773 kind: kind.to_string(),
2774 file: self.file.to_string(),
2775 line,
2776 column,
2777 source: source_slice(self.source, span).to_string(),
2778 reason: reason.to_string(),
2779 }
2780 }
2781
2782 fn enter_source_sensitive_function<State>(
2783 &mut self,
2784 span: Span,
2785 bindings: Vec<SymbolId>,
2786 context: &TraverseCtx<'_, State>,
2787 ) {
2788 if compile_time_macro_argument(context) {
2794 self.source_sensitive_functions.insert(span_key(span));
2795 self.unsafe_function_depth += 1;
2796 return;
2797 }
2798 let sensitive = observes_function_source(span, context)
2799 || bindings
2800 .iter()
2801 .any(|symbol| self.source_reads.contains(symbol));
2802 if sensitive {
2803 self.source_sensitive_functions.insert(span_key(span));
2804 let limitation = self.limitation(
2805 span,
2806 "semantic-safety",
2807 "function-source",
2808 "function body is left uninstrumented because this expression observes or coerces Function source text",
2809 );
2810 self.function_limitations.push(limitation);
2811 self.unsafe_function_depth += 1;
2812 }
2813 }
2814
2815 fn exit_source_sensitive_function(&mut self, span: Span) {
2816 if self.source_sensitive_functions.contains(&span_key(span)) {
2817 self.unsafe_function_depth -= 1;
2818 }
2819 }
2820
2821 fn is_unsafe_context(&self) -> bool {
2822 self.unsafe_function_depth > 0 || self.with_depth > 0
2823 }
2824}
2825
2826impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
2827 fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
2828 let bindings = bound_symbols(node.id.as_ref(), self.scoping, context);
2829 self.enter_source_sensitive_function(node.span, bindings, context);
2830 }
2831
2832 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
2833 self.exit_source_sensitive_function(node.span);
2834 }
2835
2836 fn enter_arrow_function_expression(
2837 &mut self,
2838 node: &mut ArrowFunctionExpression<'a>,
2839 context: &mut TraverseCtx<'a, ()>,
2840 ) {
2841 let bindings = bound_symbols(None, self.scoping, context);
2842 self.enter_source_sensitive_function(node.span, bindings, context);
2843 }
2844
2845 fn exit_arrow_function_expression(
2846 &mut self,
2847 node: &mut ArrowFunctionExpression<'a>,
2848 _context: &mut TraverseCtx<'a, ()>,
2849 ) {
2850 self.exit_source_sensitive_function(node.span);
2851 }
2852
2853 fn enter_with_statement(
2854 &mut self,
2855 node: &mut WithStatement<'a>,
2856 _context: &mut TraverseCtx<'a, ()>,
2857 ) {
2858 let limitation = self.limitation(
2859 node.span,
2860 "semantic-safety",
2861 "with-environment",
2862 "with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
2863 );
2864 self.with_limitations.push(limitation);
2865 self.with_depth += 1;
2866 }
2867
2868 fn exit_with_statement(
2869 &mut self,
2870 _node: &mut WithStatement<'a>,
2871 _context: &mut TraverseCtx<'a, ()>,
2872 ) {
2873 self.with_depth -= 1;
2874 }
2875
2876 fn enter_call_expression(
2877 &mut self,
2878 node: &mut CallExpression<'a>,
2879 _context: &mut TraverseCtx<'a, ()>,
2880 ) {
2881 if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
2882 return;
2883 }
2884 let limitation = self.limitation(
2885 node.span,
2886 "dynamic-code",
2887 "eval",
2888 "eval-generated source has no stable pre-run coverage denominator",
2889 );
2890 self.dynamic_limitations.push(limitation);
2891 }
2892
2893 fn enter_new_expression(
2894 &mut self,
2895 node: &mut NewExpression<'a>,
2896 _context: &mut TraverseCtx<'a, ()>,
2897 ) {
2898 if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
2899 return;
2900 }
2901 let limitation = self.limitation(
2902 node.span,
2903 "dynamic-code",
2904 "Function",
2905 "Function-generated source has no stable pre-run coverage denominator",
2906 );
2907 self.dynamic_limitations.push(limitation);
2908 }
2909}
2910
2911fn analyze_safety<'a>(
2912 allocator: &'a Allocator,
2913 program: &mut Program<'a>,
2914 source: &str,
2915 file: &str,
2916) -> SafetyAnalysis {
2917 let scoping = SemanticBuilder::new()
2920 .build(program)
2921 .semantic
2922 .into_scoping();
2923 let mut reads = SourceReads {
2924 scoping: &scoping,
2925 symbols: HashSet::new(),
2926 };
2927 reads.visit_program(program);
2928 let mut scanner = SafetyScanner::new(source, file, &scoping);
2929 scanner.source_reads = reads.symbols;
2930 traverse_mut(&mut scanner, allocator, program, Default::default(), ());
2931 let mut semantic_limitations = scanner.function_limitations;
2932 semantic_limitations.extend(scanner.with_limitations);
2933 SafetyAnalysis {
2934 source_sensitive_functions: scanner.source_sensitive_functions,
2935 semantic_limitations,
2936 dynamic_limitations: scanner.dynamic_limitations,
2937 }
2938}
2939
2940fn span_key(span: Span) -> SpanKey {
2941 (span.start, span.end)
2942}
2943
2944fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
2945 matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
2946}
2947
2948fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
2949 match pattern {
2950 BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
2951 _ => None,
2952 }
2953}
2954
2955fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
2956 match expression {
2957 Expression::ArrowFunctionExpression(_) => true,
2958 Expression::FunctionExpression(function) => function.id.is_none(),
2959 Expression::ClassExpression(class) => class.id.is_none(),
2960 Expression::ParenthesizedExpression(expression) => {
2961 expression_is_anonymous_definition(&expression.expression)
2962 }
2963 Expression::TSAsExpression(expression) => {
2964 expression_is_anonymous_definition(&expression.expression)
2965 }
2966 Expression::TSSatisfiesExpression(expression) => {
2967 expression_is_anonymous_definition(&expression.expression)
2968 }
2969 Expression::TSTypeAssertion(expression) => {
2970 expression_is_anonymous_definition(&expression.expression)
2971 }
2972 Expression::TSNonNullExpression(expression) => {
2973 expression_is_anonymous_definition(&expression.expression)
2974 }
2975 _ => false,
2976 }
2977}
2978
2979struct AssignmentNameSafetyTransformer<'a> {
2980 ast: AstBuilder<'a>,
2981 parenthesized_assignment_value: String,
2982}
2983
2984impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
2985 fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
2986 walk_mut::walk_assignment_expression(self, assignment);
2987 let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
2988 return;
2989 };
2990 if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
2991 || assignment.span.start == identifier.span.start
2992 || !expression_is_anonymous_definition(&assignment.right)
2993 {
2994 return;
2995 }
2996 let right = assignment.right.take_in(self.ast.allocator);
2997 assignment.right = self.ast.expression_call(
2998 Span::default(),
2999 self.ast.expression_identifier(
3000 Span::default(),
3001 self.ast.ident(&self.parenthesized_assignment_value),
3002 ),
3003 NONE,
3004 self.ast.vec_from_array([
3005 Argument::from(right),
3006 Argument::from(self.ast.expression_string_literal(
3007 Span::default(),
3008 identifier.name,
3009 None,
3010 )),
3011 ]),
3012 false,
3013 );
3014 }
3015}
3016
3017const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
3022 "create",
3023 "createTheme",
3024 "defineConsts",
3025 "defineVars",
3026 "firstThatWorks",
3027 "keyframes",
3028 "positionTry",
3029 "viewTransitionClass",
3030];
3031
3032fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
3033 context.ancestors().any(|ancestor| {
3034 let Ancestor::CallExpressionArguments(parent) = ancestor else {
3035 return false;
3036 };
3037 let Expression::StaticMemberExpression(member) = parent.callee() else {
3038 return false;
3039 };
3040 let Expression::Identifier(object) = &member.object else {
3041 return false;
3042 };
3043 object.name == "stylex"
3044 && COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
3045 })
3046}
3047
3048fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
3049 let mut child_end = span.end;
3050 for ancestor in context.ancestors() {
3051 match ancestor {
3052 Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
3053 Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
3054 Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
3055 Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
3056 Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
3057 Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
3058 Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
3059 Ancestor::LogicalExpressionLeft(parent) => {
3060 child_end = parent.span().end;
3061 }
3062 Ancestor::LogicalExpressionRight(parent) => {
3063 child_end = parent.span().end;
3064 }
3065 Ancestor::SequenceExpressionExpressions(parent) => {
3066 if child_end != parent.span().end {
3067 return false;
3068 }
3069 child_end = parent.span().end;
3070 }
3071 Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
3072 Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
3073 Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
3074 Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
3075 Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
3076 Ancestor::ComputedMemberExpressionExpression(_) => return true,
3077 Ancestor::StaticMemberExpressionObject(parent) => {
3078 return parent.property().name == "toString";
3079 }
3080 Ancestor::CallExpressionArguments(parent) => {
3081 return expression_is_identifier(parent.callee(), "String");
3082 }
3083 Ancestor::TemplateLiteralExpressions(_) => return true,
3084 Ancestor::BinaryExpressionLeft(parent) => {
3085 return matches!(
3086 parent.operator(),
3087 BinaryOperator::Addition
3088 | BinaryOperator::LessThan
3089 | BinaryOperator::LessEqualThan
3090 | BinaryOperator::GreaterThan
3091 | BinaryOperator::GreaterEqualThan
3092 );
3093 }
3094 Ancestor::BinaryExpressionRight(parent) => {
3095 return matches!(
3096 parent.operator(),
3097 BinaryOperator::Addition
3098 | BinaryOperator::LessThan
3099 | BinaryOperator::LessEqualThan
3100 | BinaryOperator::GreaterThan
3101 | BinaryOperator::GreaterEqualThan
3102 );
3103 }
3104 _ => return false,
3105 }
3106 }
3107 false
3108}
3109
3110pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3111 let _positions = PositionScope::enter();
3112 let elide_type_imports = false;
3113 let source_type =
3114 project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3115 let allocator = Allocator::default();
3116 let mut parsed = Parser::new(&allocator, source, source_type).parse();
3117 if !parsed.errors.is_empty() {
3118 return Err(CandidateError::Parse(
3119 parsed
3120 .errors
3121 .iter()
3122 .map(|error| {
3123 parse_message(
3124 source,
3125 &error.message,
3126 error
3127 .labels
3128 .as_ref()
3129 .and_then(|labels| labels.first())
3130 .map(|label| label.offset()),
3131 )
3132 })
3133 .collect(),
3134 ));
3135 }
3136
3137 let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3138 let erased = erased_imports(&parsed.program, elide_type_imports);
3139 let excluded_statements = parsed
3140 .program
3141 .body
3142 .iter()
3143 .filter(|s| erased.contains(&span_key(s.span())))
3144 .map(|s| {
3145 let span = s.span();
3146 let (line, column) = line_and_utf16_column(source, span.start as usize);
3147 CandidatePoint {
3148 id: stable_id(source, file, "statement", span, ""),
3149 kind: "statement".into(),
3150 file: file.into(),
3151 line,
3152 column,
3153 source: source_slice(source, span).into(),
3154 label: Some("typescript-import-erasure".into()),
3155 }
3156 })
3157 .collect();
3158 let point_analysis = collect_points(
3159 &allocator,
3160 &mut parsed.program,
3161 source,
3162 file,
3163 &safety.source_sensitive_functions,
3164 &erased,
3165 );
3166 let mut collector = DecisionCollector {
3167 source,
3168 file,
3169 decisions: Vec::new(),
3170 decision_vector_counts: Vec::new(),
3171 decision_logical_nodes: HashSet::new(),
3172 source_sensitive_functions: &safety.source_sensitive_functions,
3173 };
3174 collector.visit_program(&parsed.program);
3175 let optional_analysis = collect_optional_member_branches(
3176 &allocator,
3177 &mut parsed.program,
3178 source,
3179 file,
3180 &safety.source_sensitive_functions,
3181 );
3182 let call_analysis = collect_optional_call_branches(
3183 &allocator,
3184 &mut parsed.program,
3185 source,
3186 file,
3187 &safety.source_sensitive_functions,
3188 );
3189 let assignment_analysis = collect_logical_assignment_branches(
3190 &allocator,
3191 &mut parsed.program,
3192 source,
3193 file,
3194 &safety.source_sensitive_functions,
3195 );
3196 let default_analysis = collect_default_branches(
3197 &allocator,
3198 &mut parsed.program,
3199 source,
3200 file,
3201 &safety.source_sensitive_functions,
3202 );
3203 let extended_analysis = collect_extended_branches(
3204 &allocator,
3205 &mut parsed.program,
3206 source,
3207 file,
3208 &safety.source_sensitive_functions,
3209 );
3210 let logical_analysis = collect_logical_value_branches(
3211 &allocator,
3212 &mut parsed.program,
3213 source,
3214 file,
3215 &collector.decision_logical_nodes,
3216 &safety.source_sensitive_functions,
3217 );
3218 let switch_analysis = collect_switch_branches(
3219 &allocator,
3220 &mut parsed.program,
3221 source,
3222 file,
3223 &safety.source_sensitive_functions,
3224 );
3225 let mut branches = optional_analysis.branches;
3226 branches.extend(call_analysis.branches);
3227 branches.extend(assignment_analysis.branches);
3228 branches.extend(default_analysis.branches);
3229 branches.extend(extended_analysis.branches);
3230 branches.extend(logical_analysis.branches);
3231 branches.extend(switch_analysis.branches);
3232 let (generated, map) = generate_candidate(&parsed.program, file)?;
3233 Ok(CandidateOutput {
3234 engine: "rust-oxc".to_string(),
3235 complete: false,
3236 supported_surface: "control-decision-manifest-v1".to_string(),
3237 code: generated,
3238 map,
3239 decisions: collector.decisions,
3240 points: point_analysis.points,
3241 excluded_statements,
3242 branches,
3243 runtime: None,
3244 coverage_limitations: {
3245 let mut limitations = safety.semantic_limitations;
3246 limitations.extend(call_analysis.limitations);
3247 limitations.extend(default_analysis.limitations);
3248 limitations.extend(safety.dynamic_limitations);
3249 limitations
3250 },
3251 limitations: vec![
3252 "candidate emits metadata only; use the private differential transform for probes"
3253 .to_string(),
3254 "coverage points, value branches, and extended branch obligations are not included"
3255 .to_string(),
3256 ],
3257 })
3258}
3259
3260fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
3261 match value {
3262 serde_json::Value::Null => ast.expression_null_literal(Span::default()),
3263 serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
3264 serde_json::Value::Number(value) => ast.expression_numeric_literal(
3265 Span::default(),
3266 value
3267 .as_f64()
3268 .expect("coverage registration numbers must fit JavaScript"),
3269 None,
3270 NumberBase::Decimal,
3271 ),
3272 serde_json::Value::String(value) => {
3273 ast.expression_string_literal(Span::default(), ast.str(value), None)
3274 }
3275 serde_json::Value::Array(values) => ast.expression_array(
3276 Span::default(),
3277 ast.vec_from_iter(
3278 values
3279 .iter()
3280 .map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
3281 ),
3282 ),
3283 serde_json::Value::Object(properties) => ast.expression_object(
3284 Span::default(),
3285 ast.vec_from_iter(properties.iter().map(|(key, value)| {
3286 ast.object_property_kind_object_property(
3287 Span::default(),
3288 PropertyKind::Init,
3289 ast.property_key_static_identifier(Span::default(), ast.ident(key)),
3290 json_expression(ast, value),
3291 false,
3292 false,
3293 false,
3294 )
3295 })),
3296 ),
3297 }
3298}
3299
3300pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
3305 instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None, false)
3306}
3307
3308pub fn instrument_direct_candidate(
3313 source: &str,
3314 file: &str,
3315) -> Result<CandidateOutput, CandidateError> {
3316 instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None, false)
3317}
3318
3319pub fn instrument_with_import_policy(
3320 source: &str,
3321 file: &str,
3322 capability_wrapper: &str,
3323 direct: bool,
3324 elide_type_imports: bool,
3325) -> Result<CandidateOutput, CandidateError> {
3326 instrument_candidate_with_binding(
3327 source,
3328 file,
3329 if direct {
3330 RuntimeBinding::DirectGlobal
3331 } else {
3332 RuntimeBinding::ModuleImport
3333 },
3334 Some(capability_wrapper),
3335 elide_type_imports,
3336 )
3337}
3338
3339fn instrument_candidate_with_binding(
3340 source: &str,
3341 file: &str,
3342 runtime_binding: RuntimeBinding,
3343 capability_wrapper: Option<&str>,
3344 elide_type_imports: bool,
3345) -> Result<CandidateOutput, CandidateError> {
3346 let _positions = PositionScope::enter();
3347 let source_type =
3348 project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
3349 let allocator = Allocator::default();
3350 let mut parsed = Parser::new(&allocator, source, source_type).parse();
3351 if !parsed.errors.is_empty() {
3352 return Err(CandidateError::Parse(
3353 parsed
3354 .errors
3355 .iter()
3356 .map(|error| {
3357 parse_message(
3358 source,
3359 &error.message,
3360 error
3361 .labels
3362 .as_ref()
3363 .and_then(|labels| labels.first())
3364 .map(|label| label.offset()),
3365 )
3366 })
3367 .collect(),
3368 ));
3369 }
3370
3371 let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
3372 let erased = erased_imports(&parsed.program, elide_type_imports);
3373 let excluded_statements = parsed
3374 .program
3375 .body
3376 .iter()
3377 .filter(|s| erased.contains(&span_key(s.span())))
3378 .map(|s| {
3379 let span = s.span();
3380 let (line, column) = line_and_utf16_column(source, span.start as usize);
3381 CandidatePoint {
3382 id: stable_id(source, file, "statement", span, ""),
3383 kind: "statement".into(),
3384 file: file.into(),
3385 line,
3386 column,
3387 source: source_slice(source, span).into(),
3388 label: Some("typescript-import-erasure".into()),
3389 }
3390 })
3391 .collect();
3392 let point_analysis = collect_points(
3393 &allocator,
3394 &mut parsed.program,
3395 source,
3396 file,
3397 &safety.source_sensitive_functions,
3398 &erased,
3399 );
3400 let mut collector = DecisionCollector {
3401 source,
3402 file,
3403 decisions: Vec::new(),
3404 decision_vector_counts: Vec::new(),
3405 decision_logical_nodes: HashSet::new(),
3406 source_sensitive_functions: &safety.source_sensitive_functions,
3407 };
3408 collector.visit_program(&parsed.program);
3409 let optional_analysis = collect_optional_member_branches(
3410 &allocator,
3411 &mut parsed.program,
3412 source,
3413 file,
3414 &safety.source_sensitive_functions,
3415 );
3416 let call_analysis = collect_optional_call_branches(
3417 &allocator,
3418 &mut parsed.program,
3419 source,
3420 file,
3421 &safety.source_sensitive_functions,
3422 );
3423 let assignment_analysis = collect_logical_assignment_branches(
3424 &allocator,
3425 &mut parsed.program,
3426 source,
3427 file,
3428 &safety.source_sensitive_functions,
3429 );
3430 let default_analysis = collect_default_branches(
3431 &allocator,
3432 &mut parsed.program,
3433 source,
3434 file,
3435 &safety.source_sensitive_functions,
3436 );
3437 let extended_analysis = collect_extended_branches(
3438 &allocator,
3439 &mut parsed.program,
3440 source,
3441 file,
3442 &safety.source_sensitive_functions,
3443 );
3444 let logical_analysis = collect_logical_value_branches(
3445 &allocator,
3446 &mut parsed.program,
3447 source,
3448 file,
3449 &collector.decision_logical_nodes,
3450 &safety.source_sensitive_functions,
3451 );
3452 let switch_analysis = collect_switch_branches(
3453 &allocator,
3454 &mut parsed.program,
3455 source,
3456 file,
3457 &safety.source_sensitive_functions,
3458 );
3459 let mut branches = optional_analysis.branches;
3460 branches.extend(call_analysis.branches.clone());
3461 branches.extend(assignment_analysis.branches);
3462 branches.extend(default_analysis.branches.clone());
3463 branches.extend(extended_analysis.branches.clone());
3464 branches.extend(logical_analysis.branches);
3465 branches.extend(switch_analysis.branches.clone());
3466
3467 let (namespace, runtime_declaration_kind) = if runtime_binding == RuntimeBinding::DirectGlobal {
3474 (
3475 format!(
3476 "_{}",
3477 &stable_id("", file, "runtime", Span::default(), "")[..8]
3478 ),
3479 VariableDeclarationKind::Var,
3480 )
3481 } else {
3482 (String::new(), VariableDeclarationKind::Const)
3483 };
3484 let mut names = CandidateNames::within(source, &namespace);
3485 let mcdc_begin = names.allocate("__supercovMcdcBegin");
3486 let mcdc_condition = names.allocate("__supercovMcdcCondition");
3487 let mcdc_end = names.allocate("__supercovMcdcEnd");
3488 let coverage_hit = names.allocate("__supercovCoverageHit");
3489 let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
3490 let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
3491 let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
3492 let probe_file_v2 = names.allocate("__supercovProbeFileV2");
3493 let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
3494 let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
3495 let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
3496 let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
3497 let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
3498 let with_request_phase = names.allocate("__supercovWithRequestPhase");
3499 let optional_select_v2 = names.allocate("__supercovOptionalSelectV2");
3500 let select_short_v2 = names.allocate("__supercovSelectShortV2");
3501 let select_right_v2 = names.allocate("__supercovSelectRightV2");
3502 let select_named_right_v2 = names.allocate("__supercovSelectNamedRightV2");
3503 let select_assign_end_v2 = names.allocate("__supercovSelectAssignEndV2");
3504 let select_path_v2 = names.allocate("__supercovSelectPathV2");
3505 let rendered_value_v2 = names.allocate("__supercovRenderedValueV2");
3506 let optional_call_end_v2 = names.allocate("__supercovOptionalCallEndV2");
3507 let default_selected_v2 = names.allocate("__supercovDefaultSelectedV2");
3508 let default_entered_v2 = names.allocate("__supercovDefaultEnteredV2");
3509 let typescript = parsed.program.source_type.is_typescript();
3510 let ast = AstBuilder::new(&allocator);
3511 let mut assignment_name_safety = AssignmentNameSafetyTransformer {
3512 ast,
3513 parenthesized_assignment_value: parenthesized_assignment_value.clone(),
3514 };
3515 assignment_name_safety.visit_program(&mut parsed.program);
3516 let point_indices = point_analysis
3517 .points
3518 .iter()
3519 .enumerate()
3520 .map(|(index, point)| (point.id.clone(), index))
3521 .collect::<HashMap<_, _>>();
3522 let mut default_sites = default_analysis
3527 .parameter_targets
3528 .iter()
3529 .chain(&default_analysis.binding_targets)
3530 .map(|(span, target)| (*span, target.default_id.clone(), target.provided_id.clone()))
3531 .collect::<Vec<_>>();
3532 default_sites.sort();
3533 let mut default_point_ids = Vec::with_capacity(default_sites.len() * 2);
3534 let default_slots = default_sites
3535 .into_iter()
3536 .enumerate()
3537 .map(|(slot, (_, default_id, provided_id))| {
3538 let default_point = point_analysis.points.len() + default_point_ids.len();
3539 default_point_ids.push(default_id.clone());
3540 default_point_ids.push(provided_id);
3541 (
3542 default_id,
3543 DefaultSlot {
3544 slot,
3545 default_point,
3546 provided_point: default_point + 1,
3547 },
3548 )
3549 })
3550 .collect::<HashMap<_, _>>();
3551 let default_count = default_slots.len();
3552 let mut extra = ExtraPoints {
3553 base: point_analysis.points.len() + default_point_ids.len(),
3554 ids: Vec::new(),
3555 };
3556 let selection_first = extra.next();
3557 let selection_targets = extra.allocate(&logical_analysis.logical_targets, |(short, right)| {
3558 vec![short.clone(), short.clone(), right.clone(), right.clone()]
3559 });
3560 let assignment_targets = extra.allocate(&assignment_analysis.targets, |(short, right)| {
3563 vec![short.clone(), short.clone(), right.clone(), right.clone()]
3564 });
3565 let selection_count = selection_targets.len() + assignment_targets.len();
3566 let optional_member_targets = extra
3567 .allocate(&optional_analysis.targets, |(short, continued)| {
3568 vec![short.clone(), continued.clone()]
3569 });
3570 let optional_call_sites = extra.allocate(&call_analysis.sites, |(short, continued)| {
3571 vec![short.clone(), continued.clone()]
3572 });
3573 let loop_targets = extra.allocate(&extended_analysis.loop_targets, |target| {
3574 vec![target.first_id.clone(), target.second_id.clone()]
3575 });
3576 let try_targets = extra.allocate(&extended_analysis.try_targets, |target| {
3577 vec![target.first_id.clone(), target.second_id.clone()]
3578 });
3579 let statement_targets = point_analysis
3580 .statement_targets
3581 .into_iter()
3582 .map(|(span, ids)| {
3583 (
3584 span,
3585 ids.into_iter()
3586 .map(|id| PointTarget {
3587 index: *point_indices
3588 .get(&id)
3589 .expect("statement point must have a global index"),
3590 })
3591 .collect(),
3592 )
3593 })
3594 .collect();
3595 let function_targets = point_analysis
3596 .function_targets
3597 .into_iter()
3598 .map(|(span, id)| {
3599 (
3600 span,
3601 PointTarget {
3602 index: *point_indices
3603 .get(&id)
3604 .expect("function point must have a global index"),
3605 },
3606 )
3607 })
3608 .collect();
3609 let rendered_targets = point_analysis
3610 .rendered_targets
3611 .into_iter()
3612 .map(|(span, id)| {
3613 (
3614 span,
3615 PointTarget {
3616 index: *point_indices
3617 .get(&id)
3618 .expect("rendered point must have a global index"),
3619 },
3620 )
3621 })
3622 .collect();
3623 let mut statement_transformer = StatementProbeTransformer {
3624 ast,
3625 coverage_hit_v2: coverage_hit_v2.clone(),
3626 probe_file_v2: probe_file_v2.clone(),
3627 targets: statement_targets,
3628 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3629 };
3630 statement_transformer.visit_program(&mut parsed.program);
3631 let mut function_transformer = FunctionProbeTransformer {
3632 ast,
3633 coverage_hit_v2: coverage_hit_v2.clone(),
3634 probe_file_v2: probe_file_v2.clone(),
3635 targets: function_targets,
3636 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3637 };
3638 function_transformer.visit_program(&mut parsed.program);
3639 let mut rendered_transformer = RenderedExpressionTransformer {
3640 ast,
3641 rendered_value_v2: rendered_value_v2.clone(),
3642 probe_file_v2: probe_file_v2.clone(),
3643 targets: rendered_targets,
3644 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3645 };
3646 rendered_transformer.visit_program(&mut parsed.program);
3647 let mut call_transformer = OptionalCallTransformer::new(
3650 ast,
3651 source,
3652 optional_call_end_v2.clone(),
3653 probe_file_v2.clone(),
3654 typescript,
3655 optional_call_sites,
3656 call_analysis.roots,
3657 safety.source_sensitive_functions.clone(),
3658 );
3659 let call_frames = optional_analysis
3660 .after_call
3661 .iter()
3662 .filter_map(|(member, call)| {
3663 call_transformer
3664 .sites
3665 .get(call)
3666 .map(|site| (*member, site.frame.clone()))
3667 })
3668 .collect();
3669 let mut optional_transformer = OptionalMemberTransformer {
3670 ast,
3671 optional_select_v2: optional_select_v2.clone(),
3672 probe_file_v2: probe_file_v2.clone(),
3673 targets: optional_member_targets,
3674 links: optional_analysis.links,
3675 call_frames,
3676 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3677 };
3678 optional_transformer.visit_program(&mut parsed.program);
3679 call_transformer.visit_program(&mut parsed.program);
3680 let mut default_transformer = DefaultTransformer {
3681 ast,
3682 default_selected_v2: default_selected_v2.clone(),
3683 default_entered_v2: default_entered_v2.clone(),
3684 probe_file_v2: probe_file_v2.clone(),
3685 slots: default_slots,
3686 parameter_targets: default_analysis.parameter_targets,
3687 binding_targets: default_analysis.binding_targets,
3688 function_entries: Vec::new(),
3689 declaration_entries: HashMap::new(),
3690 active_declaration: Vec::new(),
3691 parameter_pattern_depth: 0,
3692 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3693 };
3694 default_transformer.visit_program(&mut parsed.program);
3695 let mut extended_transformer = ExtendedTransformer {
3696 ast,
3697 coverage_hit_v2: coverage_hit_v2.clone(),
3698 probe_file_v2: probe_file_v2.clone(),
3699 typescript,
3700 try_targets,
3701 loop_targets,
3702 names: CandidateNames::within(source, &namespace),
3703 scope_declarations: Vec::new(),
3704 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3705 };
3706 extended_transformer.visit_program(&mut parsed.program);
3707 let mut transformer = ControlProbeV2Transformer {
3708 ast,
3709 decisions: &collector.decisions,
3710 mcdc_begin: mcdc_begin.clone(),
3711 mcdc_condition: mcdc_condition.clone(),
3712 mcdc_end: mcdc_end.clone(),
3713 mcdc_end_v2: mcdc_end_v2.clone(),
3714 probe_file_v2: probe_file_v2.clone(),
3715 names,
3716 scope_declarations: Vec::new(),
3717 decision_index: 0,
3718 parameter_depth: 0,
3719 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3720 };
3721 transformer.visit_program(&mut parsed.program);
3722 let mut logical_transformer = LogicalValueTransformer {
3723 ast,
3724 select_short_v2: select_short_v2.clone(),
3725 select_right_v2: select_right_v2.clone(),
3726 select_named_right_v2: select_named_right_v2.clone(),
3727 select_assign_end_v2: select_assign_end_v2.clone(),
3728 select_path_v2: select_path_v2.clone(),
3729 probe_file_v2: probe_file_v2.clone(),
3730 typescript,
3731 names: CandidateNames::within(source, &namespace),
3732 scope_declarations: Vec::new(),
3733 logical_targets: selection_targets,
3734 assignment_targets,
3735 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3736 };
3737 logical_transformer.visit_program(&mut parsed.program);
3738 let mut switch_transformer = SwitchTransformer {
3739 ast,
3740 coverage_hit: coverage_hit.clone(),
3741 targets: switch_analysis.targets,
3742 names: CandidateNames::within(source, &namespace),
3743 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3744 };
3745 switch_transformer.visit_program(&mut parsed.program);
3746 let mut route_transformer = RouteRequestPhaseTransformer {
3747 ast,
3748 file,
3749 with_request_phase: with_request_phase.clone(),
3750 used: false,
3751 names: CandidateNames::within(source, &namespace),
3752 };
3753 route_transformer.transform_program(&mut parsed.program);
3754 let mut request_transformer = RequestPhaseTransformer {
3755 ast,
3756 with_request_phase: with_request_phase.clone(),
3757 used: route_transformer.used,
3758 source_sensitive_functions: safety.source_sensitive_functions.clone(),
3759 };
3760 request_transformer.visit_program(&mut parsed.program);
3761 let uses_request_phase = request_transformer.used;
3762
3763 let registration = serde_json::json!({
3764 "decisions": &collector.decisions,
3765 "pointIds": point_analysis.points.iter().map(|point| &point.id).chain(&default_point_ids).chain(&extra.ids).collect::<Vec<_>>(),
3766 "decisionVectorCounts": &collector.decision_vector_counts,
3767 "defaultCount": default_count,
3768 "selectionPoints": [selection_first, selection_count],
3769 });
3770 let registration_call = ast.expression_call(
3771 Span::default(),
3772 ast.expression_identifier(Span::default(), ast.ident(®ister_probe_v2)),
3773 NONE,
3774 ast.vec1(Argument::from(json_expression(ast, ®istration))),
3775 false,
3776 );
3777 parsed.program.body.insert(
3778 0,
3779 Statement::VariableDeclaration(ast.alloc_variable_declaration(
3780 Span::default(),
3781 runtime_declaration_kind,
3782 ast.vec1(ast.variable_declarator(
3783 Span::default(),
3784 runtime_declaration_kind,
3785 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
3786 NONE,
3787 Some(registration_call),
3788 false,
3789 )),
3790 false,
3791 )),
3792 );
3793 let mut runtime_imports = vec![
3794 ("mcdcBegin", &mcdc_begin),
3795 ("mcdcCondition", &mcdc_condition),
3796 ("mcdcEnd", &mcdc_end),
3797 ("coverageHit", &coverage_hit),
3798 ("registerProbeV2", ®ister_probe_v2),
3799 ("mcdcEndV2", &mcdc_end_v2),
3800 ("coverageHitV2", &coverage_hit_v2),
3801 (
3802 "parenthesizedAssignmentValue",
3803 &parenthesized_assignment_value,
3804 ),
3805 ("optionalSelectV2", &optional_select_v2),
3806 ("selectShortV2", &select_short_v2),
3807 ("selectRightV2", &select_right_v2),
3808 ("selectNamedRightV2", &select_named_right_v2),
3809 ("selectAssignEndV2", &select_assign_end_v2),
3810 ("selectPathV2", &select_path_v2),
3811 ("renderedValueV2", &rendered_value_v2),
3812 ("optionalCallEndV2", &optional_call_end_v2),
3813 ("defaultSelectedV2", &default_selected_v2),
3814 ("defaultEnteredV2", &default_entered_v2),
3815 ];
3816 if uses_request_phase {
3817 runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
3818 }
3819 if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
3820 let declarators =
3821 ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
3822 let global_runtime =
3823 Expression::StaticMemberExpression(ast.alloc_static_member_expression(
3824 Span::default(),
3825 ast.expression_identifier(Span::default(), ast.ident("globalThis")),
3826 ast.identifier_name(
3827 Span::default(),
3828 ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
3829 "__SUPERCOV_DIRECT_RUNTIME__"
3830 } else {
3831 "__supercovRuntime"
3832 }),
3833 ),
3834 false,
3835 ));
3836 let runtime_helper =
3837 Expression::StaticMemberExpression(ast.alloc_static_member_expression(
3838 Span::default(),
3839 global_runtime,
3840 ast.identifier_name(Span::default(), ast.ident(imported)),
3841 false,
3842 ));
3843 ast.variable_declarator(
3844 Span::default(),
3845 runtime_declaration_kind,
3846 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
3847 NONE,
3848 Some(runtime_helper),
3849 false,
3850 )
3851 }));
3852 parsed.program.body.insert(
3853 0,
3854 Statement::VariableDeclaration(ast.alloc_variable_declaration(
3855 Span::default(),
3856 runtime_declaration_kind,
3857 declarators,
3858 false,
3859 )),
3860 );
3861 } else {
3862 let import_specifiers =
3863 ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
3864 ast.import_declaration_specifier_import_specifier(
3865 Span::default(),
3866 ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
3867 ast.binding_identifier(Span::default(), ast.ident(local)),
3868 oxc_ast::ast::ImportOrExportKind::Value,
3869 )
3870 }));
3871 parsed.program.body.insert(
3872 0,
3873 Statement::ImportDeclaration(ast.alloc_import_declaration(
3874 Span::default(),
3875 Some(import_specifiers),
3876 ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
3877 None,
3878 NONE,
3879 oxc_ast::ast::ImportOrExportKind::Value,
3880 )),
3881 );
3882 }
3883
3884 if let Some(wrapper) = capability_wrapper {
3885 transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
3886 }
3887 let limitations = Vec::new();
3888 mark_generated_statements(ast, &mut parsed.program);
3889 let (code, map) = generate_candidate(&parsed.program, file)?;
3890 Ok(CandidateOutput {
3891 engine: "rust-oxc".to_string(),
3892 complete: true,
3893 supported_surface: "complete-js-instrumenter-v1".to_string(),
3894 code,
3895 map,
3896 decisions: collector.decisions,
3897 points: point_analysis.points,
3898 excluded_statements,
3899 branches,
3900 runtime: Some(CandidateRuntime {
3901 coverage_hit,
3902 mcdc_begin,
3903 mcdc_condition,
3904 mcdc_end,
3905 register_probe_v2,
3906 mcdc_end_v2,
3907 coverage_hit_v2,
3908 probe_file_v2,
3909 parenthesized_assignment_value,
3910 with_request_phase,
3911 optional_select_v2,
3912 select_short_v2,
3913 select_right_v2,
3914 select_named_right_v2,
3915 select_assign_end_v2,
3916 select_path_v2,
3917 rendered_value_v2,
3918 optional_call_end_v2,
3919 default_selected_v2,
3920 default_entered_v2,
3921 }),
3922 coverage_limitations: {
3923 let mut limitations = safety.semantic_limitations;
3924 limitations.extend(call_analysis.limitations);
3925 limitations.extend(default_analysis.limitations);
3926 limitations.extend(safety.dynamic_limitations);
3927 limitations
3928 },
3929 limitations,
3930 })
3931}
3932
3933fn probe_v2_hit<'a>(ast: AstBuilder<'a>, hit: &str, file: &str, index: usize) -> Statement<'a> {
3941 ast.statement_expression(
3942 Span::default(),
3943 probe_v2_hit_expression(ast, hit, file, index),
3944 )
3945}
3946
3947fn probe_v2_hit_expression<'a>(
3950 ast: AstBuilder<'a>,
3951 hit: &str,
3952 file: &str,
3953 index: usize,
3954) -> Expression<'a> {
3955 let span = Span::default();
3956 let file_expression = || ast.expression_identifier(span, ast.ident(file));
3957 let member = |object: Expression<'a>, name: &str| -> Expression<'a> {
3958 Expression::from(ast.member_expression_static(
3959 span,
3960 object,
3961 ast.identifier_name(span, ast.ident(name)),
3962 false,
3963 ))
3964 };
3965 let index_literal =
3966 || ast.expression_numeric_literal(span, index as f64, None, NumberBase::Decimal);
3967 let fast = member(member(file_expression(), "clock"), "fast");
3968 let seen = ast.expression_binary(
3969 span,
3970 Expression::from(ast.member_expression_computed(
3971 span,
3972 member(file_expression(), "hitEpochs"),
3973 index_literal(),
3974 false,
3975 )),
3976 BinaryOperator::StrictEquality,
3977 member(member(file_expression(), "clock"), "epoch"),
3978 );
3979 let call = ast.expression_call(
3980 span,
3981 ast.expression_identifier(span, ast.ident(hit)),
3982 NONE,
3983 ast.vec_from_array([
3984 Argument::from(file_expression()),
3985 Argument::from(index_literal()),
3986 ]),
3987 false,
3988 );
3989 ast.expression_logical(
3990 span,
3991 ast.expression_logical(span, fast, LogicalOperator::And, seen),
3992 LogicalOperator::Or,
3993 call,
3994 )
3995}
3996
3997fn probe_temporaries<'a>(
4002 ast: AstBuilder<'a>,
4003 names: Vec<String>,
4004 typescript: bool,
4005) -> Statement<'a> {
4006 let declarations = ast.vec_from_iter(names.into_iter().map(|name| {
4007 let annotation = typescript.then(|| {
4008 ast.alloc_ts_type_annotation(Span::default(), ast.ts_type_any_keyword(Span::default()))
4009 });
4010 ast.variable_declarator(
4011 Span::default(),
4012 VariableDeclarationKind::Let,
4013 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&name)),
4014 annotation,
4015 None,
4016 false,
4017 )
4018 }));
4019 Statement::VariableDeclaration(ast.alloc_variable_declaration(
4020 Span::default(),
4021 VariableDeclarationKind::Let,
4022 declarations,
4023 false,
4024 ))
4025}
4026
4027fn assign_temporary<'a>(ast: AstBuilder<'a>, name: &str, value: Expression<'a>) -> Expression<'a> {
4029 ast.expression_assignment(
4030 Span::default(),
4031 AssignmentOperator::Assign,
4032 AssignmentTarget::from(ast.simple_assignment_target_assignment_target_identifier(
4033 Span::default(),
4034 ast.ident(name),
4035 )),
4036 value,
4037 )
4038}
4039
4040fn numeric<'a>(ast: AstBuilder<'a>, value: usize) -> Expression<'a> {
4041 ast.expression_numeric_literal(Span::default(), value as f64, None, NumberBase::Decimal)
4042}
4043
4044#[derive(Default)]
4048struct ExtraPoints {
4049 base: usize,
4050 ids: Vec<String>,
4051}
4052
4053impl ExtraPoints {
4054 fn allocate<T>(
4055 &mut self,
4056 targets: &HashMap<SpanKey, T>,
4057 ids: impl Fn(&T) -> Vec<String>,
4058 ) -> HashMap<SpanKey, usize> {
4059 let mut keys = targets.keys().copied().collect::<Vec<_>>();
4060 keys.sort();
4061 keys.into_iter()
4062 .map(|key| {
4063 let first = self.base + self.ids.len();
4064 self.ids.extend(ids(&targets[&key]));
4065 (key, first)
4066 })
4067 .collect()
4068 }
4069
4070 fn next(&self) -> usize {
4071 self.base + self.ids.len()
4072 }
4073}
4074
4075struct StatementProbeTransformer<'a> {
4076 ast: AstBuilder<'a>,
4077 coverage_hit_v2: String,
4078 probe_file_v2: String,
4079 targets: HashMap<SpanKey, Vec<PointTarget>>,
4080 source_sensitive_functions: HashSet<SpanKey>,
4081}
4082
4083impl<'a> StatementProbeTransformer<'a> {
4084 fn probe(&self, target: &PointTarget) -> Statement<'a> {
4085 probe_v2_hit(
4086 self.ast,
4087 &self.coverage_hit_v2,
4088 &self.probe_file_v2,
4089 target.index,
4090 )
4091 }
4092
4093 fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
4094 let mut ids = self
4095 .targets
4096 .remove(&span_key(statement.span()))
4097 .unwrap_or_default();
4098 if let Statement::ExportNamedDeclaration(export) = statement
4099 && let Some(declaration) = &export.declaration
4100 && let Some(nested) = self.targets.remove(&span_key(declaration.span()))
4101 {
4102 ids.extend(nested);
4103 }
4104 if let Statement::ExportDefaultDeclaration(export) = statement
4105 && let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
4106 && let Some(nested) = self.targets.remove(&span_key(class.span))
4107 {
4108 ids.extend(nested);
4109 }
4110 ids
4111 }
4112
4113 fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
4114 let ids = self.take_statement_ids(statement);
4115 if ids.is_empty() {
4116 return;
4117 }
4118 let span = statement.span();
4122 let original = statement.take_in(self.ast.allocator);
4123 let mut body = self.ast.vec_with_capacity(ids.len() + 1);
4124 body.extend(ids.iter().map(|target| self.probe(target)));
4125 body.push(original);
4126 *statement = self.ast.statement_block(span, body);
4127 }
4128}
4129
4130impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
4131 fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4132 let original = statements.take_in(self.ast.allocator);
4133 let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
4134 for mut statement in original {
4135 let ids = self.take_statement_ids(&statement);
4136 self.visit_statement(&mut statement);
4137 instrumented.extend(ids.iter().map(|target| self.probe(target)));
4138 instrumented.push(statement);
4139 }
4140 *statements = instrumented;
4141 }
4142
4143 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4144 if self
4145 .source_sensitive_functions
4146 .contains(&span_key(function.span))
4147 {
4148 return;
4149 }
4150 walk_mut::walk_function(self, function, flags);
4151 }
4152
4153 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4154 if self
4155 .source_sensitive_functions
4156 .contains(&span_key(function.span))
4157 {
4158 return;
4159 }
4160 walk_mut::walk_arrow_function_expression(self, function);
4161 }
4162
4163 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4166
4167 fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
4168 self.wrap_bare(&mut statement.consequent);
4169 if let Some(alternate) = &mut statement.alternate {
4170 self.wrap_bare(alternate);
4171 }
4172 walk_mut::walk_if_statement(self, statement);
4173 }
4174
4175 fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
4176 self.wrap_bare(&mut statement.body);
4177 walk_mut::walk_while_statement(self, statement);
4178 }
4179
4180 fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
4181 self.wrap_bare(&mut statement.body);
4182 walk_mut::walk_do_while_statement(self, statement);
4183 }
4184
4185 fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
4186 self.wrap_bare(&mut statement.body);
4187 walk_mut::walk_for_statement(self, statement);
4188 }
4189
4190 fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
4191 self.wrap_bare(&mut statement.body);
4192 walk_mut::walk_for_in_statement(self, statement);
4193 }
4194
4195 fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
4196 self.wrap_bare(&mut statement.body);
4197 walk_mut::walk_for_of_statement(self, statement);
4198 }
4199}
4200
4201struct RenderedExpressionTransformer<'a> {
4210 ast: AstBuilder<'a>,
4211 rendered_value_v2: String,
4212 probe_file_v2: String,
4213 targets: HashMap<SpanKey, PointTarget>,
4214 source_sensitive_functions: HashSet<SpanKey>,
4215}
4216
4217impl<'a> RenderedExpressionTransformer<'a> {
4218 fn probe(&self, target: &PointTarget, value: Expression<'a>) -> Expression<'a> {
4219 self.ast.expression_call(
4220 Span::default(),
4221 self.ast
4222 .expression_identifier(Span::default(), self.ast.ident(&self.rendered_value_v2)),
4223 NONE,
4224 self.ast.vec_from_array([
4225 Argument::from(
4226 self.ast.expression_identifier(
4227 Span::default(),
4228 self.ast.ident(&self.probe_file_v2),
4229 ),
4230 ),
4231 Argument::from(self.ast.expression_numeric_literal(
4232 Span::default(),
4233 target.index as f64,
4234 None,
4235 NumberBase::Decimal,
4236 )),
4237 Argument::from(value),
4238 ]),
4239 false,
4240 )
4241 }
4242}
4243
4244impl<'a> VisitMut<'a> for RenderedExpressionTransformer<'a> {
4245 fn visit_jsx_expression_container(&mut self, container: &mut JSXExpressionContainer<'a>) {
4246 walk_mut::walk_jsx_expression_container(self, container);
4247 if matches!(container.expression, JSXExpression::EmptyExpression(_)) {
4248 return;
4249 }
4250 let span = container.expression.span();
4251 if self.source_sensitive_functions.contains(&span_key(span)) {
4252 return;
4253 }
4254 let Some(target) = self.targets.remove(&span_key(span)) else {
4255 return;
4256 };
4257 let inner = container
4258 .expression
4259 .take_in(self.ast.allocator)
4260 .into_expression();
4261 container.expression = JSXExpression::from(self.probe(&target, inner));
4262 }
4263}
4264
4265struct FunctionProbeTransformer<'a> {
4266 ast: AstBuilder<'a>,
4267 coverage_hit_v2: String,
4268 probe_file_v2: String,
4269 targets: HashMap<SpanKey, PointTarget>,
4270 source_sensitive_functions: HashSet<SpanKey>,
4271}
4272
4273impl<'a> FunctionProbeTransformer<'a> {
4274 fn probe(&self, target: &PointTarget) -> Statement<'a> {
4275 probe_v2_hit(
4276 self.ast,
4277 &self.coverage_hit_v2,
4278 &self.probe_file_v2,
4279 target.index,
4280 )
4281 }
4282}
4283
4284impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
4285 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4286 if self
4287 .source_sensitive_functions
4288 .contains(&span_key(function.span))
4289 {
4290 return;
4291 }
4292 if let Some(target) = self.targets.remove(&span_key(function.span))
4293 && let Some(body) = &mut function.body
4294 {
4295 body.statements.insert(0, self.probe(&target));
4296 }
4297 walk_mut::walk_function(self, function, flags);
4298 }
4299
4300 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4301 if self
4302 .source_sensitive_functions
4303 .contains(&span_key(function.span))
4304 {
4305 return;
4306 }
4307 if let Some(target) = self.targets.remove(&span_key(function.span)) {
4308 let probe = self.probe(&target);
4309 if function.expression {
4310 let original = function
4311 .body
4312 .statements
4313 .pop()
4314 .expect("expression arrow must contain its expression statement");
4315 let Statement::ExpressionStatement(expression) = original else {
4316 panic!("expression arrow body must be represented as an expression statement");
4317 };
4318 function.expression = false;
4319 function.body.statements.push(probe);
4320 function.body.statements.push(
4321 self.ast
4322 .statement_return(Span::default(), Some(expression.unbox().expression)),
4323 );
4324 } else {
4325 function.body.statements.insert(0, probe);
4326 }
4327 }
4328 walk_mut::walk_arrow_function_expression(self, function);
4329 }
4330}
4331
4332struct OptionalMemberTransformer<'a> {
4333 ast: AstBuilder<'a>,
4334 optional_select_v2: String,
4335 probe_file_v2: String,
4336 targets: HashMap<SpanKey, usize>,
4338 links: HashMap<SpanKey, u8>,
4339 call_frames: HashMap<SpanKey, String>,
4341 source_sensitive_functions: HashSet<SpanKey>,
4342}
4343
4344impl<'a> OptionalMemberTransformer<'a> {
4345 fn instrument_operand(
4351 &self,
4352 operand: Expression<'a>,
4353 first: usize,
4354 links: u8,
4355 call_frame: Option<&str>,
4356 ) -> Expression<'a> {
4357 let mut arguments = self.ast.vec_from_array([
4358 Argument::from(
4359 self.ast
4360 .expression_identifier(Span::default(), self.ast.ident(&self.probe_file_v2)),
4361 ),
4362 Argument::from(numeric(self.ast, first)),
4363 Argument::from(operand),
4364 ]);
4365 if links != 0 {
4366 arguments.push(Argument::from(numeric(self.ast, usize::from(links))));
4367 }
4368 if let Some(frame) = call_frame {
4369 arguments.push(Argument::from(
4370 self.ast
4371 .expression_identifier(Span::default(), self.ast.ident(frame)),
4372 ));
4373 }
4374 self.ast.expression_call(
4375 Span::default(),
4376 self.ast
4377 .expression_identifier(Span::default(), self.ast.ident(&self.optional_select_v2)),
4378 NONE,
4379 arguments,
4380 false,
4381 )
4382 }
4383
4384 fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
4385 let Some(first) = self.targets.remove(&span_key(span)) else {
4386 return;
4387 };
4388 let mut links = self.links.get(&span_key(span)).copied().unwrap_or(0);
4389 let call_frame = self.call_frames.get(&span_key(span)).cloned();
4390 if call_frame.is_none() {
4391 links &= !LINK_AFTER_CALL;
4393 }
4394 let operand = object.take_in(self.ast.allocator);
4395 *object = self.instrument_operand(operand, first, links, call_frame.as_deref());
4396 }
4397}
4398
4399impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
4400 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4401 if self
4402 .source_sensitive_functions
4403 .contains(&span_key(function.span))
4404 {
4405 return;
4406 }
4407 walk_mut::walk_function(self, function, flags);
4408 }
4409
4410 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4411 if self
4412 .source_sensitive_functions
4413 .contains(&span_key(function.span))
4414 {
4415 return;
4416 }
4417 walk_mut::walk_arrow_function_expression(self, function);
4418 }
4419
4420 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
4423
4424 fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
4425 self.instrument_target(member.span, &mut member.object);
4426 walk_mut::walk_computed_member_expression(self, member);
4427 }
4428
4429 fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
4430 self.instrument_target(member.span, &mut member.object);
4431 walk_mut::walk_static_member_expression(self, member);
4432 }
4433
4434 fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
4435 self.instrument_target(member.span, &mut member.object);
4436 walk_mut::walk_private_field_expression(self, member);
4437 }
4438}
4439
4440#[derive(Clone)]
4444struct OptionalCallSiteRuntime {
4445 frame: String,
4446 first: usize,
4447}
4448
4449struct OptionalCallTransformer<'a, 's> {
4450 ast: AstBuilder<'a>,
4451 optional_call_end_v2: String,
4452 probe_file_v2: String,
4453 typescript: bool,
4454 scope_declarations: Vec<Vec<String>>,
4455 sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
4456 roots: HashMap<SpanKey, Vec<SpanKey>>,
4457 source_sensitive_functions: HashSet<SpanKey>,
4458 _source: std::marker::PhantomData<&'s str>,
4459}
4460
4461impl<'a, 's> OptionalCallTransformer<'a, 's> {
4462 #[allow(clippy::too_many_arguments)]
4463 fn new(
4464 ast: AstBuilder<'a>,
4465 source: &'s str,
4466 optional_call_end_v2: String,
4467 probe_file_v2: String,
4468 typescript: bool,
4469 sites: HashMap<SpanKey, usize>,
4470 roots: HashMap<SpanKey, Vec<SpanKey>>,
4471 source_sensitive_functions: HashSet<SpanKey>,
4472 ) -> Self {
4473 let mut names = CandidateNames::new(source);
4474 let mut ordered = sites.into_iter().collect::<Vec<_>>();
4475 ordered.sort();
4476 let sites = ordered
4477 .into_iter()
4478 .map(|(key, first)| {
4479 (
4480 key,
4481 OptionalCallSiteRuntime {
4482 frame: names.allocate("_optionalCall"),
4483 first,
4484 },
4485 )
4486 })
4487 .collect();
4488 Self {
4489 ast,
4490 optional_call_end_v2,
4491 probe_file_v2,
4492 typescript,
4493 scope_declarations: Vec::new(),
4494 sites,
4495 roots,
4496 source_sensitive_functions,
4497 _source: std::marker::PhantomData,
4498 }
4499 }
4500
4501 fn identifier(&self, name: &str) -> Expression<'a> {
4502 self.ast
4503 .expression_identifier(Span::default(), self.ast.ident(name))
4504 }
4505
4506 fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
4507 self.ast.expression_call(
4508 Span::default(),
4509 self.identifier(name),
4510 NONE,
4511 arguments,
4512 false,
4513 )
4514 }
4515
4516 fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
4518 self.ast.expression_sequence(
4519 Span::default(),
4520 self.ast.vec_from_array([
4521 assign_temporary(self.ast, frame, numeric(self.ast, 1)),
4522 value,
4523 ]),
4524 )
4525 }
4526
4527 fn enter_scope(&mut self) {
4528 self.scope_declarations.push(Vec::new());
4529 }
4530
4531 fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4532 let names = self
4533 .scope_declarations
4534 .pop()
4535 .expect("optional-call scope stack must remain balanced");
4536 if names.is_empty() {
4537 return;
4538 }
4539 statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
4540 }
4541
4542 fn instrument_callee(
4543 &self,
4544 callee: Expression<'a>,
4545 site: &OptionalCallSiteRuntime,
4546 ) -> Expression<'a> {
4547 match callee {
4548 Expression::ComputedMemberExpression(mut member) => {
4549 let property = member.expression.take_in(self.ast.allocator);
4550 member.expression = self.reached(&site.frame, property);
4551 Expression::ComputedMemberExpression(member)
4552 }
4553 Expression::StaticMemberExpression(member) => {
4554 let member = member.unbox();
4555 let property = self.ast.expression_string_literal(
4556 Span::default(),
4557 self.ast.str(member.property.name.as_str()),
4558 None,
4559 );
4560 Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
4561 member.span,
4562 member.object,
4563 self.reached(&site.frame, property),
4564 member.optional,
4565 ))
4566 }
4567 Expression::PrivateFieldExpression(mut member) => {
4568 let object = member.object.take_in(self.ast.allocator);
4569 member.object = self.reached(&site.frame, object);
4570 Expression::PrivateFieldExpression(member)
4571 }
4572 Expression::ChainExpression(mut chain) => {
4573 match &mut chain.expression {
4574 ChainElement::ComputedMemberExpression(member) => {
4575 let property = member.expression.take_in(self.ast.allocator);
4576 member.expression = self.reached(&site.frame, property);
4577 }
4578 ChainElement::StaticMemberExpression(member) => {
4579 let member = member.take_in(self.ast.allocator);
4580 let property = self.ast.expression_string_literal(
4581 Span::default(),
4582 self.ast.str(member.property.name.as_str()),
4583 None,
4584 );
4585 chain.expression = ChainElement::ComputedMemberExpression(
4586 self.ast.alloc_computed_member_expression(
4587 member.span,
4588 member.object,
4589 self.reached(&site.frame, property),
4590 member.optional,
4591 ),
4592 );
4593 }
4594 ChainElement::PrivateFieldExpression(member) => {
4595 let object = member.object.take_in(self.ast.allocator);
4596 member.object = self.reached(&site.frame, object);
4597 }
4598 ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
4599 return self.reached(&site.frame, Expression::ChainExpression(chain));
4600 }
4601 }
4602 Expression::ChainExpression(chain)
4603 }
4604 Expression::ParenthesizedExpression(mut parenthesized) => {
4605 let inner = parenthesized.expression.take_in(self.ast.allocator);
4606 parenthesized.expression = self.instrument_callee(inner, site);
4607 Expression::ParenthesizedExpression(parenthesized)
4608 }
4609 Expression::TSAsExpression(mut wrapped) => {
4610 let inner = wrapped.expression.take_in(self.ast.allocator);
4611 wrapped.expression = self.instrument_callee(inner, site);
4612 Expression::TSAsExpression(wrapped)
4613 }
4614 Expression::TSSatisfiesExpression(mut wrapped) => {
4615 let inner = wrapped.expression.take_in(self.ast.allocator);
4616 wrapped.expression = self.instrument_callee(inner, site);
4617 Expression::TSSatisfiesExpression(wrapped)
4618 }
4619 Expression::TSTypeAssertion(mut wrapped) => {
4620 let inner = wrapped.expression.take_in(self.ast.allocator);
4621 wrapped.expression = self.instrument_callee(inner, site);
4622 Expression::TSTypeAssertion(wrapped)
4623 }
4624 Expression::TSNonNullExpression(mut wrapped) => {
4625 let inner = wrapped.expression.take_in(self.ast.allocator);
4626 wrapped.expression = self.instrument_callee(inner, site);
4627 Expression::TSNonNullExpression(wrapped)
4628 }
4629 Expression::CallExpression(mut call) => {
4634 call.arguments.insert(0, self.marker_spread(&site.frame, 1));
4635 Expression::CallExpression(call)
4636 }
4637 other => self.reached(&site.frame, other),
4638 }
4639 }
4640
4641 fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
4642 let callee = call.callee.take_in(self.ast.allocator);
4643 call.callee = self.instrument_callee(callee, site);
4644 call.arguments.insert(0, self.marker_spread(&site.frame, 2));
4645 }
4646
4647 fn marker_spread(&self, frame: &str, value: usize) -> Argument<'a> {
4650 let none = Expression::from(
4651 self.ast.member_expression_static(
4652 Span::default(),
4653 self.identifier(&self.probe_file_v2),
4654 self.ast
4655 .identifier_name(Span::default(), self.ast.ident("none")),
4656 false,
4657 ),
4658 );
4659 let marked = self.ast.expression_sequence(
4660 Span::default(),
4661 self.ast.vec_from_array([
4662 assign_temporary(self.ast, frame, numeric(self.ast, value)),
4663 none,
4664 ]),
4665 );
4666 Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), marked))
4667 }
4668
4669 fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
4670 let sites = site_keys
4671 .iter()
4672 .map(|key| {
4673 self.sites
4674 .get(key)
4675 .expect("optional-call root must reference a known site")
4676 .clone()
4677 })
4678 .collect::<Vec<_>>();
4679 self.scope_declarations
4680 .last_mut()
4681 .expect("optional-call root must be inside a program or function")
4682 .extend(sites.iter().map(|site| site.frame.clone()));
4683
4684 let original = expression.take_in(self.ast.allocator);
4687 let mut measured = original;
4688 for site in sites.iter().rev() {
4689 measured = self.call(
4690 &self.optional_call_end_v2,
4691 self.ast.vec_from_array([
4692 Argument::from(self.identifier(&self.probe_file_v2)),
4693 Argument::from(numeric(self.ast, site.first)),
4694 Argument::from(measured),
4695 Argument::from(self.identifier(&site.frame)),
4696 ]),
4697 );
4698 }
4699 let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
4700 for site in &sites {
4701 sequence.push(assign_temporary(
4702 self.ast,
4703 &site.frame,
4704 numeric(self.ast, 0),
4705 ));
4706 }
4707 sequence.push(measured);
4708 *expression = self.ast.expression_sequence(Span::default(), sequence);
4709 }
4710}
4711
4712impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
4713 fn visit_program(&mut self, program: &mut Program<'a>) {
4714 self.enter_scope();
4715 walk_mut::walk_program(self, program);
4716 self.leave_scope(&mut program.body);
4717 }
4718
4719 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
4720 self.enter_scope();
4721 walk_mut::walk_function_body(self, body);
4722 self.leave_scope(&mut body.statements);
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_call_expression(&mut self, call: &mut CallExpression<'a>) {
4750 walk_mut::walk_call_expression(self, call);
4751 if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
4752 self.instrument_call(call, &site);
4753 }
4754 }
4755
4756 fn visit_expression(&mut self, expression: &mut Expression<'a>) {
4757 let key = span_key(expression.span());
4758 walk_mut::walk_expression(self, expression);
4759 if let Some(site_keys) = self.roots.remove(&key) {
4760 self.wrap_root(expression, &site_keys);
4761 }
4762 }
4763}
4764
4765struct DefaultSlot {
4768 slot: usize,
4769 default_point: usize,
4770 provided_point: usize,
4771}
4772
4773struct DefaultTransformer<'a> {
4774 ast: AstBuilder<'a>,
4775 default_selected_v2: String,
4776 default_entered_v2: String,
4777 probe_file_v2: String,
4778 slots: HashMap<String, DefaultSlot>,
4779 parameter_targets: HashMap<SpanKey, DefaultTarget>,
4780 binding_targets: HashMap<SpanKey, DefaultTarget>,
4781 function_entries: Vec<Vec<Statement<'a>>>,
4782 declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
4783 active_declaration: Vec<SpanKey>,
4784 parameter_pattern_depth: usize,
4785 source_sensitive_functions: HashSet<SpanKey>,
4786}
4787
4788impl<'a> DefaultTransformer<'a> {
4789 fn identifier(&self, name: &str) -> Expression<'a> {
4790 self.ast
4791 .expression_identifier(Span::default(), self.ast.ident(name))
4792 }
4793
4794 fn string_argument(&self, value: &str) -> Argument<'a> {
4795 Argument::from(self.ast.expression_string_literal(
4796 Span::default(),
4797 self.ast.str(value),
4798 None,
4799 ))
4800 }
4801
4802 fn index_argument(&self, index: usize) -> Argument<'a> {
4803 Argument::from(self.ast.expression_numeric_literal(
4804 Span::default(),
4805 index as f64,
4806 None,
4807 NumberBase::Decimal,
4808 ))
4809 }
4810
4811 fn slot(&self, target: &DefaultTarget) -> &DefaultSlot {
4812 self.slots
4813 .get(&target.default_id)
4814 .expect("every default target has a V2 slot")
4815 }
4816
4817 fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
4821 let mut arguments = self.ast.vec_from_array([
4822 Argument::from(self.identifier(&self.probe_file_v2)),
4823 self.index_argument(self.slot(target).slot),
4824 Argument::from(value),
4825 ]);
4826 if let Some(name) = &target.inferred_name {
4827 arguments.push(self.string_argument(name));
4828 }
4829 self.ast.expression_call(
4830 Span::default(),
4831 self.identifier(&self.default_selected_v2),
4832 NONE,
4833 arguments,
4834 false,
4835 )
4836 }
4837
4838 fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
4841 let slot = self.slot(target);
4842 self.ast.statement_expression(
4843 Span::default(),
4844 self.ast.expression_call(
4845 Span::default(),
4846 self.identifier(&self.default_entered_v2),
4847 NONE,
4848 self.ast.vec_from_array([
4849 Argument::from(self.identifier(&self.probe_file_v2)),
4850 self.index_argument(slot.slot),
4851 self.index_argument(slot.default_point),
4852 self.index_argument(slot.provided_point),
4853 ]),
4854 false,
4855 ),
4856 )
4857 }
4858
4859 fn push_entry(&mut self, target: &DefaultTarget) {
4860 let entry = self.entered(target);
4861 if self.parameter_pattern_depth > 0 {
4862 self.function_entries
4863 .last_mut()
4864 .expect("parameter default must belong to a function")
4865 .push(entry);
4866 } else {
4867 let declaration = *self
4868 .active_declaration
4869 .last()
4870 .expect("binding default must belong to a declaration");
4871 self.declaration_entries
4872 .entry(declaration)
4873 .or_default()
4874 .push(entry);
4875 }
4876 }
4877
4878 fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
4879 if entries.is_empty() {
4880 return;
4881 }
4882 if let Statement::BlockStatement(block) = body {
4883 for (index, entry) in entries.into_iter().enumerate() {
4884 block.body.insert(index, entry);
4885 }
4886 return;
4887 }
4888 let span = body.span();
4889 let original = body.take_in(self.ast.allocator);
4890 let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
4891 statements.extend(entries);
4892 statements.push(original);
4893 *body = self.ast.statement_block(span, statements);
4894 }
4895
4896 fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
4897 match statement {
4898 Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
4899 Statement::ExportNamedDeclaration(export) => match &export.declaration {
4900 Some(Declaration::VariableDeclaration(declaration)) => {
4901 Some(span_key(declaration.span))
4902 }
4903 _ => None,
4904 },
4905 _ => None,
4906 }
4907 }
4908
4909 fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
4910 match left {
4911 ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
4912 _ => None,
4913 }
4914 }
4915}
4916
4917impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
4918 fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4919 let original = statements.take_in(self.ast.allocator);
4920 let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
4921 for mut statement in original {
4922 let declaration = Self::variable_span(&statement);
4923 self.visit_statement(&mut statement);
4924 instrumented.push(statement);
4925 if let Some(declaration) = declaration
4926 && let Some(entries) = self.declaration_entries.remove(&declaration)
4927 {
4928 instrumented.extend(entries);
4929 }
4930 }
4931 *statements = instrumented;
4932 }
4933
4934 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4935 if self
4936 .source_sensitive_functions
4937 .contains(&span_key(function.span))
4938 {
4939 return;
4940 }
4941 self.function_entries.push(Vec::new());
4942 walk_mut::walk_function(self, function, flags);
4943 let entries = self
4944 .function_entries
4945 .pop()
4946 .expect("default function-entry stack must remain balanced");
4947 if let Some(body) = &mut function.body {
4948 for (index, entry) in entries.into_iter().enumerate() {
4949 body.statements.insert(index, entry);
4950 }
4951 }
4952 }
4953
4954 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4955 if self
4956 .source_sensitive_functions
4957 .contains(&span_key(function.span))
4958 {
4959 return;
4960 }
4961 self.function_entries.push(Vec::new());
4962 walk_mut::walk_arrow_function_expression(self, function);
4963 let entries = self
4964 .function_entries
4965 .pop()
4966 .expect("default arrow-entry stack must remain balanced");
4967 for (index, entry) in entries.into_iter().enumerate() {
4968 function.body.statements.insert(index, entry);
4969 }
4970 }
4971
4972 fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
4973 let outer = self.parameter_targets.remove(&span_key(parameter.span));
4974 if let Some(target) = &outer {
4975 let entry = self.entered(target);
4976 self.function_entries
4977 .last_mut()
4978 .expect("formal parameter must belong to a function")
4979 .push(entry);
4980 }
4981 self.visit_decorators(&mut parameter.decorators);
4982 self.parameter_pattern_depth += 1;
4983 self.visit_binding_pattern(&mut parameter.pattern);
4984 self.parameter_pattern_depth -= 1;
4985 if let Some(annotation) = &mut parameter.type_annotation {
4986 self.visit_ts_type_annotation(annotation);
4987 }
4988 if let Some(initializer) = &mut parameter.initializer {
4989 self.visit_expression(initializer);
4990 if let Some(target) = outer {
4991 let value = initializer.take_in(self.ast.allocator);
4992 **initializer = self.selected(value, &target);
4993 }
4994 }
4995 }
4996
4997 fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
4998 let target = if self.parameter_pattern_depth > 0 {
4999 self.parameter_targets.remove(&span_key(assignment.span))
5000 } else {
5001 self.binding_targets.remove(&span_key(assignment.span))
5002 };
5003 if let Some(target) = &target {
5004 self.push_entry(target);
5005 }
5006 walk_mut::walk_assignment_pattern(self, assignment);
5007 if let Some(target) = target {
5008 let value = assignment.right.take_in(self.ast.allocator);
5009 assignment.right = self.selected(value, &target);
5010 }
5011 }
5012
5013 fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
5014 self.active_declaration.push(span_key(declaration.span));
5015 walk_mut::walk_variable_declaration(self, declaration);
5016 self.active_declaration
5017 .pop()
5018 .expect("default declaration stack must remain balanced");
5019 }
5020
5021 fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
5022 let declaration = Self::loop_declaration_span(&statement.left);
5023 walk_mut::walk_for_in_statement(self, statement);
5024 if let Some(declaration) = declaration
5025 && let Some(entries) = self.declaration_entries.remove(&declaration)
5026 {
5027 self.prepend_entries(&mut statement.body, entries);
5028 }
5029 }
5030
5031 fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
5032 let declaration = Self::loop_declaration_span(&statement.left);
5033 walk_mut::walk_for_of_statement(self, statement);
5034 if let Some(declaration) = declaration
5035 && let Some(entries) = self.declaration_entries.remove(&declaration)
5036 {
5037 self.prepend_entries(&mut statement.body, entries);
5038 }
5039 }
5040
5041 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5044}
5045
5046enum ExtendedKind {
5047 Try,
5048 Loop,
5049}
5050
5051struct ExtendedTransformer<'a, 's> {
5056 ast: AstBuilder<'a>,
5057 coverage_hit_v2: String,
5058 probe_file_v2: String,
5059 typescript: bool,
5060 try_targets: HashMap<SpanKey, usize>,
5061 loop_targets: HashMap<SpanKey, usize>,
5062 names: CandidateNames<'s>,
5063 scope_declarations: Vec<Vec<String>>,
5064 source_sensitive_functions: HashSet<SpanKey>,
5065}
5066
5067impl<'a> ExtendedTransformer<'a, '_> {
5068 fn identifier(&self, name: &str) -> Expression<'a> {
5069 self.ast
5070 .expression_identifier(Span::default(), self.ast.ident(name))
5071 }
5072
5073 fn enter_scope(&mut self) {
5074 self.scope_declarations.push(Vec::new());
5075 }
5076
5077 fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5078 let names = self
5079 .scope_declarations
5080 .pop()
5081 .expect("extended scope stack must remain balanced");
5082 if names.is_empty() {
5083 return;
5084 }
5085 statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5086 }
5087
5088 fn scratch(&mut self, base: &str) -> String {
5089 let name = self.names.allocate(base);
5090 self.scope_declarations
5091 .last_mut()
5092 .expect("extended branch must be inside a program or function")
5093 .push(name.clone());
5094 name
5095 }
5096
5097 fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
5098 match statement {
5099 Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
5100 Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5101 Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
5102 Statement::LabeledStatement(node) => Self::target(&node.body),
5103 _ => None,
5104 }
5105 }
5106
5107 fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
5108 match statement {
5109 Statement::TryStatement(node) => Some(node),
5110 Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
5111 _ => None,
5112 }
5113 }
5114
5115 fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
5116 match statement {
5117 Statement::ForInStatement(node) => Some(&mut node.body),
5118 Statement::ForOfStatement(node) => Some(&mut node.body),
5119 Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
5120 _ => None,
5121 }
5122 }
5123
5124 fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
5125 if let Statement::BlockStatement(block) = body {
5126 block.body.insert(0, entry);
5127 return;
5128 }
5129 let span = body.span();
5130 let original = body.take_in(ast.allocator);
5131 *body = ast.statement_block(span, ast.vec_from_array([entry, original]));
5132 }
5133
5134 fn hit(&self, index: usize) -> Expression<'a> {
5135 probe_v2_hit_expression(self.ast, &self.coverage_hit_v2, &self.probe_file_v2, index)
5136 }
5137
5138 fn reset(&self, flag: &str) -> Statement<'a> {
5140 self.ast.statement_expression(
5141 Span::default(),
5142 assign_temporary(self.ast, flag, numeric(self.ast, 0)),
5143 )
5144 }
5145
5146 fn first_time(&self, flag: &str, index: usize) -> Statement<'a> {
5148 let record = self.ast.expression_sequence(
5149 Span::default(),
5150 self.ast.vec_from_array([
5151 assign_temporary(self.ast, flag, numeric(self.ast, 1)),
5152 self.hit(index),
5153 ]),
5154 );
5155 self.ast.statement_expression(
5156 Span::default(),
5157 self.ast.expression_logical(
5158 Span::default(),
5159 self.identifier(flag),
5160 LogicalOperator::Or,
5161 record,
5162 ),
5163 )
5164 }
5165
5166 fn otherwise(&self, flag: &str, index: usize) -> Statement<'a> {
5168 self.ast.statement_expression(
5169 Span::default(),
5170 self.ast.expression_logical(
5171 Span::default(),
5172 self.identifier(flag),
5173 LogicalOperator::Or,
5174 self.hit(index),
5175 ),
5176 )
5177 }
5178
5179 fn instrument_try(&mut self, statement: &mut Statement<'a>, first: usize) {
5180 let span = statement.span();
5183 let flag = self.scratch("_supercovTryFrame");
5184 let reset = self.reset(&flag);
5185 let caught = self.first_time(&flag, first + 1);
5188 let node = Self::inner_try(statement).expect("try target must remain a try statement");
5189 node.handler
5190 .as_mut()
5191 .expect("try coverage requires a catch handler")
5192 .body
5193 .body
5194 .insert(0, caught);
5195 let end = self.otherwise(&flag, first);
5196 if let Some(finalizer) = &mut node.finalizer {
5197 finalizer.body.insert(0, end);
5198 } else {
5199 node.finalizer = Some(
5203 self.ast
5204 .alloc_block_statement(Span::new(span.end - 1, span.end), self.ast.vec1(end)),
5205 );
5206 }
5207 let original = statement.take_in(self.ast.allocator);
5208 *statement = self
5209 .ast
5210 .statement_block(span, self.ast.vec_from_array([reset, original]));
5211 }
5212
5213 fn instrument_loop(&mut self, statement: &mut Statement<'a>, first: usize) {
5214 let span = statement.span();
5215 let flag = self.scratch("_supercovLoopFrame");
5216 let reset = self.reset(&flag);
5217 let entered = self.first_time(&flag, first + 1);
5218 Self::prepend(
5219 Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
5220 entered,
5221 self.ast,
5222 );
5223 let original = statement.take_in(self.ast.allocator);
5224 let end = self.otherwise(&flag, first);
5225 let wrapped = self.ast.statement_try(
5228 Span::default(),
5229 self.ast
5230 .block_statement(Span::default(), self.ast.vec1(original)),
5231 None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
5232 Some(
5233 self.ast
5234 .block_statement(Span::default(), self.ast.vec1(end)),
5235 ),
5236 );
5237 *statement = self
5238 .ast
5239 .statement_block(span, self.ast.vec_from_array([reset, wrapped]));
5240 }
5241}
5242
5243impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
5244 fn visit_program(&mut self, program: &mut Program<'a>) {
5245 self.enter_scope();
5246 walk_mut::walk_program(self, program);
5247 self.leave_scope(&mut program.body);
5248 }
5249
5250 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5251 self.enter_scope();
5252 walk_mut::walk_function_body(self, body);
5253 self.leave_scope(&mut body.statements);
5254 }
5255
5256 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5257 if self
5258 .source_sensitive_functions
5259 .contains(&span_key(function.span))
5260 {
5261 return;
5262 }
5263 walk_mut::walk_function(self, function, flags);
5264 }
5265
5266 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5267 if self
5268 .source_sensitive_functions
5269 .contains(&span_key(function.span))
5270 {
5271 return;
5272 }
5273 walk_mut::walk_arrow_function_expression(self, function);
5274 }
5275
5276 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5279
5280 fn visit_statement(&mut self, statement: &mut Statement<'a>) {
5281 let Some((kind, key)) = Self::target(statement) else {
5282 walk_mut::walk_statement(self, statement);
5283 return;
5284 };
5285 match kind {
5286 ExtendedKind::Try => {
5287 if let Some(target) = self.try_targets.remove(&key) {
5288 walk_mut::walk_statement(self, statement);
5289 self.instrument_try(statement, target);
5290 } else {
5291 walk_mut::walk_statement(self, statement);
5292 }
5293 }
5294 ExtendedKind::Loop => {
5295 if let Some(target) = self.loop_targets.remove(&key) {
5296 walk_mut::walk_statement(self, statement);
5297 self.instrument_loop(statement, target);
5298 } else {
5299 walk_mut::walk_statement(self, statement);
5300 }
5301 }
5302 }
5303 }
5304}
5305
5306struct LogicalValueTransformer<'a, 's> {
5307 ast: AstBuilder<'a>,
5308 select_short_v2: String,
5309 select_right_v2: String,
5310 select_named_right_v2: String,
5311 select_assign_end_v2: String,
5312 select_path_v2: String,
5313 probe_file_v2: String,
5314 typescript: bool,
5315 names: CandidateNames<'s>,
5316 scope_declarations: Vec<Vec<String>>,
5317 logical_targets: HashMap<SpanKey, usize>,
5320 assignment_targets: HashMap<SpanKey, usize>,
5322 source_sensitive_functions: HashSet<SpanKey>,
5323}
5324
5325impl<'a> LogicalValueTransformer<'a, '_> {
5326 fn identifier(&self, name: &str) -> Expression<'a> {
5327 self.ast
5328 .expression_identifier(Span::default(), self.ast.ident(name))
5329 }
5330
5331 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
5332 AssignmentTarget::from(
5333 self.ast
5334 .simple_assignment_target_assignment_target_identifier(
5335 Span::default(),
5336 self.ast.ident(name),
5337 ),
5338 )
5339 }
5340
5341 fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
5342 self.ast.expression_call(
5343 Span::default(),
5344 self.identifier(name),
5345 NONE,
5346 arguments,
5347 false,
5348 )
5349 }
5350
5351 fn string_argument(&self, value: &str) -> Argument<'a> {
5352 Argument::from(self.ast.expression_string_literal(
5353 Span::default(),
5354 self.ast.str(value),
5355 None,
5356 ))
5357 }
5358
5359 fn enter_scope(&mut self) {
5360 self.scope_declarations.push(Vec::new());
5361 }
5362
5363 fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
5364 let names = self
5365 .scope_declarations
5366 .pop()
5367 .expect("logical-value scope stack must remain balanced");
5368 if names.is_empty() {
5369 return;
5370 }
5371 statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
5372 }
5373
5374 fn scratch(&mut self) -> String {
5375 let name = self.names.allocate("_supercovAssignedRight");
5376 self.scope_declarations
5377 .last_mut()
5378 .expect("logical expression must be inside a program or function")
5379 .push(name.clone());
5380 name
5381 }
5382
5383 fn helper(&self, name: &str, arguments: [Argument<'a>; 3]) -> Expression<'a> {
5384 let mut all = self
5385 .ast
5386 .vec_from_array([Argument::from(self.identifier(&self.probe_file_v2))]);
5387 all.extend(arguments);
5388 self.call(name, all)
5389 }
5390
5391 fn instrument_tree(&mut self, expression: &mut Expression<'a>, path: &[usize]) {
5406 let key = span_key(expression.span());
5407 let first = self
5408 .logical_targets
5409 .remove(&key)
5410 .expect("a selection tree starts at a selection");
5411 let Expression::LogicalExpression(logical) = expression else {
5412 panic!("logical-value target must remain a logical expression");
5413 };
5414 let kind = match logical.operator {
5415 LogicalOperator::Or => 0,
5416 LogicalOperator::And => 1,
5417 LogicalOperator::Coalesce => 2,
5418 };
5419 let mut left_path = vec![first * 4 + kind];
5420 left_path.extend_from_slice(path);
5421 let mut right_path = vec![first * 4 + 3];
5422 right_path.extend_from_slice(path);
5423 self.instrument_operand(&mut logical.left, &left_path);
5424 self.instrument_operand(&mut logical.right, &right_path);
5425 }
5426
5427 fn instrument_operand(&mut self, operand: &mut Expression<'a>, path: &[usize]) {
5428 let mut inner = &mut *operand;
5429 while let Expression::ParenthesizedExpression(parenthesized) = inner {
5430 inner = &mut parenthesized.expression;
5431 }
5432 if matches!(inner, Expression::LogicalExpression(_))
5433 && self.logical_targets.contains_key(&span_key(inner.span()))
5434 {
5435 self.instrument_tree(inner, path);
5436 return;
5437 }
5438 self.visit_expression(operand);
5439 let value = operand.take_in(self.ast.allocator);
5440 *operand = match *path {
5441 [step] if step % 4 == 3 => {
5442 let mut arguments = self.ast.vec_from_array([
5443 Argument::from(self.identifier(&self.probe_file_v2)),
5444 Argument::from(numeric(self.ast, step / 4)),
5445 ]);
5446 arguments.push(Argument::from(value));
5447 self.call(&self.select_right_v2, arguments)
5448 }
5449 [step] => self.helper(
5450 &self.select_short_v2,
5451 [
5452 Argument::from(numeric(self.ast, step / 4)),
5453 Argument::from(value),
5454 Argument::from(numeric(self.ast, step % 4)),
5455 ],
5456 ),
5457 _ => {
5458 let mut arguments = self.ast.vec_from_array([
5459 Argument::from(self.identifier(&self.probe_file_v2)),
5460 Argument::from(value),
5461 ]);
5462 arguments.extend(
5463 path.iter()
5464 .map(|step| Argument::from(numeric(self.ast, *step))),
5465 );
5466 self.call(&self.select_path_v2, arguments)
5467 }
5468 };
5469 }
5470
5471 fn instrument_assignment(
5478 &mut self,
5479 assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
5480 first: usize,
5481 ) -> Expression<'a> {
5482 let assignment = assignment.unbox();
5483 let inferred_name = match &assignment.left {
5484 AssignmentTarget::AssignmentTargetIdentifier(identifier)
5485 if assignment.span.start == identifier.span.start
5486 && expression_is_anonymous_definition(&assignment.right) =>
5487 {
5488 Some(identifier.name.to_string())
5489 }
5490 _ => None,
5491 };
5492 let right_evaluated = self.scratch();
5493 let set = |value| {
5494 self.ast.expression_assignment(
5495 Span::default(),
5496 AssignmentOperator::Assign,
5497 self.assignment_target(&right_evaluated),
5498 numeric(self.ast, value),
5499 )
5500 };
5501 let (reset, mark) = (set(0), set(1));
5502 let mut right_arguments = self.ast.vec_from_array([
5503 Argument::from(self.identifier(&self.probe_file_v2)),
5504 Argument::from(numeric(self.ast, first)),
5505 Argument::from(assignment.right),
5506 ]);
5507 let right_helper = match inferred_name {
5508 Some(name) => {
5509 right_arguments.push(self.string_argument(&name));
5510 &self.select_named_right_v2
5511 }
5512 None => &self.select_right_v2,
5513 };
5514 right_arguments.push(Argument::from(mark));
5515 let right = self.call(right_helper, right_arguments);
5516 let measured_assignment = self.ast.expression_assignment(
5517 Span::default(),
5518 assignment.operator,
5519 assignment.left,
5520 right,
5521 );
5522 let end = self.call(
5523 &self.select_assign_end_v2,
5524 self.ast.vec_from_array([
5525 Argument::from(self.identifier(&self.probe_file_v2)),
5526 Argument::from(numeric(self.ast, first)),
5527 Argument::from(measured_assignment),
5528 Argument::from(self.identifier(&right_evaluated)),
5529 ]),
5530 );
5531 self.ast
5532 .expression_sequence(Span::default(), self.ast.vec_from_array([reset, end]))
5533 }
5534}
5535
5536impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
5537 fn visit_program(&mut self, program: &mut Program<'a>) {
5538 self.enter_scope();
5539 walk_mut::walk_program(self, program);
5540 self.leave_scope(&mut program.body);
5541 }
5542
5543 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5544 self.enter_scope();
5545 walk_mut::walk_function_body(self, body);
5546 self.leave_scope(&mut body.statements);
5547 }
5548
5549 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5550 if self
5551 .source_sensitive_functions
5552 .contains(&span_key(function.span))
5553 {
5554 return;
5555 }
5556 walk_mut::walk_function(self, function, flags);
5557 }
5558
5559 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5560 if self
5561 .source_sensitive_functions
5562 .contains(&span_key(function.span))
5563 {
5564 return;
5565 }
5566 walk_mut::walk_arrow_function_expression(self, function);
5567 }
5568
5569 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5572
5573 fn visit_expression(&mut self, expression: &mut Expression<'a>) {
5574 let key = span_key(expression.span());
5575 if matches!(expression, Expression::LogicalExpression(_))
5576 && self.logical_targets.contains_key(&key)
5577 {
5578 self.instrument_tree(expression, &[]);
5579 return;
5580 }
5581 walk_mut::walk_expression(self, expression);
5582 if let Some(first) = self.assignment_targets.remove(&key) {
5583 let original = expression.take_in(self.ast.allocator);
5584 let Expression::AssignmentExpression(assignment) = original else {
5585 panic!("logical-assignment target must remain an assignment expression");
5586 };
5587 *expression = self.instrument_assignment(assignment, first);
5588 }
5589 }
5590}
5591
5592struct SwitchTransformer<'a, 's> {
5593 ast: AstBuilder<'a>,
5594 coverage_hit: String,
5595 targets: HashMap<SpanKey, SwitchTarget>,
5596 names: CandidateNames<'s>,
5597 source_sensitive_functions: HashSet<SpanKey>,
5598}
5599
5600impl<'a> SwitchTransformer<'a, '_> {
5601 fn identifier(&self, name: &str) -> Expression<'a> {
5602 self.ast
5603 .expression_identifier(Span::default(), self.ast.ident(name))
5604 }
5605
5606 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
5607 AssignmentTarget::from(
5608 self.ast
5609 .simple_assignment_target_assignment_target_identifier(
5610 Span::default(),
5611 self.ast.ident(name),
5612 ),
5613 )
5614 }
5615
5616 fn probe(&self, id: &str) -> Statement<'a> {
5617 self.ast.statement_expression(
5618 Span::default(),
5619 self.ast.expression_call(
5620 Span::default(),
5621 self.identifier(&self.coverage_hit),
5622 NONE,
5623 self.ast
5624 .vec1(Argument::from(self.ast.expression_string_literal(
5625 Span::default(),
5626 self.ast.str(id),
5627 None,
5628 ))),
5629 false,
5630 ),
5631 )
5632 }
5633
5634 fn target(statement: &Statement<'a>) -> Option<SpanKey> {
5635 match statement {
5636 Statement::SwitchStatement(node) => Some(span_key(node.span)),
5637 Statement::LabeledStatement(node) => Self::target(&node.body),
5638 _ => None,
5639 }
5640 }
5641
5642 fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
5643 match statement {
5644 Statement::SwitchStatement(node) => Some(node),
5645 Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
5646 _ => None,
5647 }
5648 }
5649
5650 fn entered_assignment(&self, entered: &str) -> Statement<'a> {
5651 self.ast.statement_expression(
5652 Span::default(),
5653 self.ast.expression_assignment(
5654 Span::default(),
5655 AssignmentOperator::Assign,
5656 self.assignment_target(entered),
5657 self.ast.expression_boolean_literal(Span::default(), true),
5658 ),
5659 )
5660 }
5661
5662 fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
5668 let entered = self.names.allocate("_supercovSwitchEntered");
5669 let node = Self::inner_switch(statement).expect("switch target must remain a switch");
5670 for (index, case) in node.cases.iter_mut().enumerate() {
5671 let probe = self.probe(
5672 target
5673 .case_ids
5674 .get(index)
5675 .expect("switch case target count must remain stable"),
5676 );
5677 let selected = self.ast.statement_if(
5678 Span::default(),
5679 self.ast.expression_unary(
5680 Span::default(),
5681 UnaryOperator::LogicalNot,
5682 self.identifier(&entered),
5683 ),
5684 probe,
5685 None,
5686 );
5687 case.consequent.insert(0, self.entered_assignment(&entered));
5688 case.consequent.insert(0, selected);
5689 }
5690 let declaration =
5691 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
5692 Span::default(),
5693 VariableDeclarationKind::Let,
5694 self.ast.vec1(self.ast.variable_declarator(
5695 Span::default(),
5696 VariableDeclarationKind::Let,
5697 self.ast.binding_pattern_binding_identifier(
5698 Span::default(),
5699 self.ast.ident(&entered),
5700 ),
5701 NONE,
5702 Some(self.ast.expression_boolean_literal(Span::default(), false)),
5703 false,
5704 )),
5705 false,
5706 ));
5707 let original = statement.take_in(self.ast.allocator);
5708 let mut body = self.ast.vec_from_array([declaration, original]);
5709 if let Some(no_match_id) = &target.no_match_id {
5710 body.push(self.ast.statement_if(
5711 Span::default(),
5712 self.ast.expression_unary(
5713 Span::default(),
5714 UnaryOperator::LogicalNot,
5715 self.identifier(&entered),
5716 ),
5717 self.probe(no_match_id),
5718 None,
5719 ));
5720 }
5721 *statement = self.ast.statement_block(Span::default(), body);
5722 }
5723}
5724
5725impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
5726 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5727 if self
5728 .source_sensitive_functions
5729 .contains(&span_key(function.span))
5730 {
5731 return;
5732 }
5733 walk_mut::walk_function(self, function, flags);
5734 }
5735
5736 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5737 if self
5738 .source_sensitive_functions
5739 .contains(&span_key(function.span))
5740 {
5741 return;
5742 }
5743 walk_mut::walk_arrow_function_expression(self, function);
5744 }
5745
5746 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
5749
5750 fn visit_statement(&mut self, statement: &mut Statement<'a>) {
5751 let Some(key) = Self::target(statement) else {
5752 walk_mut::walk_statement(self, statement);
5753 return;
5754 };
5755 let Some(target) = self.targets.remove(&key) else {
5756 walk_mut::walk_statement(self, statement);
5757 return;
5758 };
5759 self.instrument(statement, &target);
5760 walk_mut::walk_statement(self, statement);
5763 }
5764}
5765
5766struct RouteRequestPhaseTransformer<'a, 's> {
5767 ast: AstBuilder<'a>,
5768 file: &'s str,
5769 with_request_phase: String,
5770 used: bool,
5771 names: CandidateNames<'s>,
5772}
5773
5774impl<'a> RouteRequestPhaseTransformer<'a, '_> {
5775 fn is_remix_route(&self) -> bool {
5776 self.file.starts_with("app/routes/")
5777 }
5778
5779 fn is_next_route(&self) -> bool {
5780 let path = Path::new(self.file);
5781 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
5782 return false;
5783 };
5784 let is_route_module = [
5785 "route.js",
5786 "route.jsx",
5787 "route.ts",
5788 "route.tsx",
5789 "route.mjs",
5790 "route.mts",
5791 "route.cjs",
5792 "route.cts",
5793 ]
5794 .contains(&name);
5795 if !is_route_module {
5796 return false;
5797 }
5798 let components = path
5799 .components()
5800 .filter_map(|component| component.as_os_str().to_str())
5801 .collect::<Vec<_>>();
5802 components
5803 .windows(2)
5804 .any(|window| window[0] == "app" && window[1] != name)
5805 }
5806
5807 fn is_handler_name(&self, name: &str) -> bool {
5808 (self.is_remix_route() && matches!(name, "loader" | "action"))
5809 || (self.is_next_route()
5810 && matches!(
5811 name,
5812 "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
5813 ))
5814 }
5815
5816 fn identifier(&self, name: &str) -> Expression<'a> {
5817 self.ast
5818 .expression_identifier(Span::default(), self.ast.ident(name))
5819 }
5820
5821 fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
5822 self.ast.expression_call(
5823 Span::default(),
5824 self.identifier(&self.with_request_phase),
5825 NONE,
5826 self.ast.vec1(Argument::from(expression)),
5827 false,
5828 )
5829 }
5830
5831 fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
5832 Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
5833 Span::default(),
5834 Some(self.ast.declaration_variable(
5835 Span::default(),
5836 VariableDeclarationKind::Const,
5837 self.ast.vec1(self.ast.variable_declarator(
5838 Span::default(),
5839 VariableDeclarationKind::Const,
5840 self.ast.binding_pattern_binding_identifier(
5841 Span::default(),
5842 self.ast.ident(exported_name),
5843 ),
5844 NONE,
5845 Some(self.wrap_expression(self.identifier(original_name))),
5846 false,
5847 )),
5848 false,
5849 )),
5850 self.ast.vec(),
5851 None,
5852 ImportOrExportKind::Value,
5853 NONE,
5854 ))
5855 }
5856
5857 fn transform_named_export(
5858 &mut self,
5859 mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
5860 output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
5861 ) {
5862 if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
5863 for declarator in &mut declaration.declarations {
5864 let Some(name) = binding_identifier_name(&declarator.id) else {
5865 continue;
5866 };
5867 if !self.is_handler_name(&name) {
5868 continue;
5869 }
5870 if let Some(initializer) = &mut declarator.init {
5871 let original = initializer.take_in(self.ast.allocator);
5872 *initializer = self.wrap_expression(original);
5873 self.used = true;
5874 }
5875 }
5876 output.push(Statement::ExportNamedDeclaration(export));
5877 return;
5878 }
5879
5880 if matches!(
5881 export.declaration,
5882 Some(Declaration::FunctionDeclaration(_))
5883 ) {
5884 let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
5885 else {
5886 unreachable!();
5887 };
5888 let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
5889 output.push(Statement::FunctionDeclaration(function));
5890 return;
5891 };
5892 if !self.is_handler_name(&exported_name) {
5893 export.declaration = Some(Declaration::FunctionDeclaration(function));
5894 output.push(Statement::ExportNamedDeclaration(export));
5895 return;
5896 }
5897 let original_name = self
5898 .names
5899 .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
5900 function.id = Some(
5901 self.ast
5902 .binding_identifier(Span::default(), self.ast.ident(&original_name)),
5903 );
5904 output.push(Statement::FunctionDeclaration(function));
5905 output.push(self.wrapped_named_export(&exported_name, &original_name));
5906 self.used = true;
5907 return;
5908 }
5909
5910 if export.source.is_none() {
5911 output.push(Statement::ExportNamedDeclaration(export));
5912 return;
5913 }
5914 let source = export
5915 .source
5916 .as_ref()
5917 .expect("checked above")
5918 .clone_in(self.ast.allocator);
5919 let specifiers = export.specifiers.take_in(self.ast.allocator);
5920 let mut untouched = self.ast.vec();
5921 let mut handlers = Vec::new();
5922 for specifier in specifiers {
5923 let exported_name = specifier
5924 .exported
5925 .identifier_name()
5926 .map(|name| name.to_string());
5927 if exported_name
5928 .as_deref()
5929 .is_some_and(|name| self.is_handler_name(name))
5930 {
5931 handlers.push((exported_name.expect("checked above"), specifier));
5932 } else {
5933 untouched.push(specifier);
5934 }
5935 }
5936 if handlers.is_empty() {
5937 export.specifiers = untouched;
5938 output.push(Statement::ExportNamedDeclaration(export));
5939 return;
5940 }
5941 if !untouched.is_empty() {
5942 output.push(Statement::ExportNamedDeclaration(
5943 self.ast.alloc_export_named_declaration(
5944 export.span,
5945 None,
5946 untouched,
5947 Some(source.clone_in(self.ast.allocator)),
5948 export.export_kind,
5949 export.with_clause.take(),
5950 ),
5951 ));
5952 }
5953 for (exported_name, specifier) in handlers {
5954 let original_name = self
5955 .names
5956 .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
5957 output.push(Statement::ImportDeclaration(
5958 self.ast.alloc_import_declaration(
5959 Span::default(),
5960 Some(self.ast.vec1(
5961 self.ast.import_declaration_specifier_import_specifier(
5962 Span::default(),
5963 specifier.local,
5964 self.ast.binding_identifier(
5965 Span::default(),
5966 self.ast.ident(&original_name),
5967 ),
5968 ImportOrExportKind::Value,
5969 ),
5970 )),
5971 source.clone_in(self.ast.allocator),
5972 None,
5973 NONE,
5974 ImportOrExportKind::Value,
5975 ),
5976 ));
5977 output.push(self.wrapped_named_export(&exported_name, &original_name));
5978 }
5979 self.used = true;
5980 }
5981
5982 fn transform_default_export(
5983 &mut self,
5984 mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
5985 output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
5986 ) {
5987 if let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
5988 export.declaration.take_in(self.ast.allocator)
5989 {
5990 let original_name = self
5991 .names
5992 .allocate("__supercovHandleRequestCoverageOriginal");
5993 function.id = Some(
5994 self.ast
5995 .binding_identifier(Span::default(), self.ast.ident(&original_name)),
5996 );
5997 output.push(Statement::FunctionDeclaration(function));
5998 output.push(Statement::ExportDefaultDeclaration(
5999 self.ast.alloc_export_default_declaration(
6000 Span::default(),
6001 ExportDefaultDeclarationKind::from(
6002 self.wrap_expression(self.identifier(&original_name)),
6003 ),
6004 ),
6005 ));
6006 self.used = true;
6007 return;
6008 }
6009 if export.declaration.is_expression() {
6010 let original = export
6011 .declaration
6012 .take_in(self.ast.allocator)
6013 .into_expression();
6014 export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
6015 self.used = true;
6016 }
6017 output.push(Statement::ExportDefaultDeclaration(export));
6018 }
6019
6020 fn transform_program(&mut self, program: &mut Program<'a>) {
6021 let route_module = self.is_remix_route() || self.is_next_route();
6022 let server_entry = self.file.starts_with("app/entry.server.")
6023 && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
6024 &Path::new(self.file)
6025 .extension()
6026 .and_then(|value| value.to_str())
6027 .unwrap_or(""),
6028 );
6029 if !route_module && !server_entry {
6030 return;
6031 }
6032 let original = program.body.take_in(self.ast.allocator);
6033 let mut output = self.ast.vec_with_capacity(original.len() + 4);
6034 for statement in original {
6035 match statement {
6036 Statement::ExportNamedDeclaration(export) if route_module => {
6037 self.transform_named_export(export, &mut output);
6038 }
6039 Statement::ExportDefaultDeclaration(export) if server_entry => {
6040 self.transform_default_export(export, &mut output);
6041 }
6042 statement => output.push(statement),
6043 }
6044 }
6045 program.body = output;
6046 }
6047}
6048
6049struct RequestPhaseTransformer<'a> {
6050 ast: AstBuilder<'a>,
6051 with_request_phase: String,
6052 used: bool,
6053 source_sensitive_functions: HashSet<SpanKey>,
6054}
6055
6056impl<'a> RequestPhaseTransformer<'a> {
6057 fn identifier(&self, name: &str) -> Expression<'a> {
6058 self.ast
6059 .expression_identifier(Span::default(), self.ast.ident(name))
6060 }
6061
6062 fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
6063 match callee {
6064 Expression::Identifier(identifier) => identifier.name == name,
6065 Expression::StaticMemberExpression(member) => member.property.name == name,
6066 _ => false,
6067 }
6068 }
6069
6070 fn callback_candidate(argument: &Argument<'a>) -> bool {
6071 matches!(
6072 argument,
6073 Argument::FunctionExpression(_)
6074 | Argument::ArrowFunctionExpression(_)
6075 | Argument::Identifier(_)
6076 | Argument::ComputedMemberExpression(_)
6077 | Argument::StaticMemberExpression(_)
6078 | Argument::PrivateFieldExpression(_)
6079 )
6080 }
6081
6082 fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
6083 matches!(
6084 argument,
6085 Argument::CallExpression(call)
6086 if expression_is_identifier(&call.callee, &self.with_request_phase)
6087 )
6088 }
6089
6090 fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
6091 if self.already_wrapped(argument) || !argument.is_expression() {
6092 return;
6093 }
6094 let expression = argument.to_expression_mut();
6095 let original = expression.take_in(self.ast.allocator);
6096 *expression = self.ast.expression_call(
6097 Span::default(),
6098 self.identifier(&self.with_request_phase),
6099 NONE,
6100 self.ast
6101 .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
6102 |name| {
6103 Argument::from(self.ast.expression_string_literal(
6104 Span::default(),
6105 self.ast.str(name),
6106 None,
6107 ))
6108 },
6109 ))),
6110 false,
6111 );
6112 self.used = true;
6113 }
6114}
6115
6116impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
6117 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6118 if self
6119 .source_sensitive_functions
6120 .contains(&span_key(function.span))
6121 {
6122 return;
6123 }
6124 walk_mut::walk_function(self, function, flags);
6125 }
6126
6127 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6128 if self
6129 .source_sensitive_functions
6130 .contains(&span_key(function.span))
6131 {
6132 return;
6133 }
6134 walk_mut::walk_arrow_function_expression(self, function);
6135 }
6136
6137 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6140
6141 fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
6142 walk_mut::walk_call_expression(self, call);
6143 let property = match &call.callee {
6144 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
6145 _ => None,
6146 };
6147 let mut callback_index = None;
6148 if matches!(property, Some("on" | "once" | "addListener"))
6149 && matches!(
6150 call.arguments.first(),
6151 Some(Argument::StringLiteral(event))
6152 if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
6153 )
6154 {
6155 callback_index = Some(1);
6156 } else if Self::callee_is(&call.callee, "createServer") {
6157 callback_index = call.arguments.iter().rposition(Self::callback_candidate);
6158 }
6159 let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
6160 if let Some(index) = callback_index
6161 && let Some(argument) = call.arguments.get_mut(index)
6162 {
6163 self.wrap_argument(argument, connection.then_some("connection"));
6164 }
6165 }
6166}
6167
6168struct CandidateNames<'s> {
6169 source: &'s str,
6170 underscored: HashSet<&'s str>,
6175 allocated: HashSet<String>,
6176 suffix: usize,
6177 namespace: String,
6180}
6181
6182impl<'s> CandidateNames<'s> {
6183 fn new(source: &'s str) -> Self {
6184 Self::within(source, "")
6185 }
6186
6187 fn within(source: &'s str, namespace: &str) -> Self {
6188 let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
6189 let underscored = source
6190 .split(|c: char| !identifier(c))
6191 .filter(|word| word.starts_with('_'))
6192 .collect();
6193 Self {
6194 source,
6195 underscored,
6196 allocated: HashSet::new(),
6197 suffix: 0,
6198 namespace: namespace.to_owned(),
6199 }
6200 }
6201
6202 fn taken(&self, candidate: &str) -> bool {
6203 self.allocated.contains(candidate)
6204 || if candidate.starts_with('_') {
6205 self.underscored.contains(candidate)
6206 } else {
6207 self.source.contains(candidate)
6208 }
6209 }
6210
6211 fn allocate(&mut self, base: &str) -> String {
6212 loop {
6213 let candidate = if self.suffix == 0 {
6214 format!("{base}{}", self.namespace)
6215 } else {
6216 format!("{base}{}{}", self.suffix, self.namespace)
6217 };
6218 self.suffix += 1;
6219 if !self.taken(&candidate) {
6220 self.allocated.insert(candidate.clone());
6221 return candidate;
6222 }
6223 }
6224 }
6225}
6226
6227struct ControlProbeV2Transformer<'a, 's> {
6228 ast: AstBuilder<'a>,
6229 decisions: &'s [CandidateDecision],
6230 mcdc_begin: String,
6231 mcdc_condition: String,
6232 mcdc_end: String,
6233 mcdc_end_v2: String,
6234 probe_file_v2: String,
6235 names: CandidateNames<'s>,
6236 scope_declarations: Vec<Vec<String>>,
6237 decision_index: usize,
6238 parameter_depth: usize,
6239 source_sensitive_functions: HashSet<SpanKey>,
6240}
6241
6242#[derive(Clone, Copy)]
6243struct DecisionPlan {
6244 index: usize,
6245 condition_count: usize,
6246 inline_frame: bool,
6247}
6248
6249impl<'a> ControlProbeV2Transformer<'a, '_> {
6250 fn enter_declaration_scope(&mut self) {
6251 self.scope_declarations.push(Vec::new());
6252 }
6253
6254 fn leave_declaration_scope(&mut self) -> Vec<String> {
6255 self.scope_declarations
6256 .pop()
6257 .expect("program/function scope stack must remain balanced")
6258 }
6259
6260 fn allocate_scratch(&mut self, base: &str) -> String {
6261 let name = self.names.allocate(base);
6262 self.scope_declarations
6263 .last_mut()
6264 .expect("a control decision must be inside a program or function body")
6265 .push(name.clone());
6266 name
6267 }
6268
6269 fn scratch_for(&mut self, base: &str, inline: bool) -> String {
6270 if inline {
6271 self.names.allocate(base)
6272 } else {
6273 self.allocate_scratch(base)
6274 }
6275 }
6276
6277 fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
6278 let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
6279 self.ast.variable_declarator(
6280 Span::default(),
6281 VariableDeclarationKind::Let,
6282 self.ast
6283 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
6284 NONE,
6285 None,
6286 false,
6287 )
6288 }));
6289 let body = self.ast.alloc_function_body(
6290 Span::default(),
6291 self.ast.vec(),
6292 self.ast.vec_from_array([
6293 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
6294 Span::default(),
6295 VariableDeclarationKind::Let,
6296 declarations,
6297 false,
6298 )),
6299 self.ast.statement_return(Span::default(), Some(expression)),
6300 ]),
6301 );
6302 let params = self.ast.alloc_formal_parameters(
6303 Span::default(),
6304 FormalParameterKind::ArrowFormalParameters,
6305 self.ast.vec(),
6306 NONE,
6307 );
6308 let arrow = self.ast.expression_arrow_function(
6309 Span::default(),
6310 false,
6311 false,
6312 NONE,
6313 params,
6314 NONE,
6315 body,
6316 );
6317 self.ast
6318 .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
6319 }
6320
6321 fn prepend_declarations(
6322 &self,
6323 names: Vec<String>,
6324 statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
6325 ) {
6326 if names.is_empty() {
6327 return;
6328 }
6329 let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
6330 self.ast.variable_declarator(
6331 Span::default(),
6332 VariableDeclarationKind::Let,
6333 self.ast
6334 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
6335 NONE,
6336 None,
6337 false,
6338 )
6339 }));
6340 statements.insert(
6341 0,
6342 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
6343 Span::default(),
6344 VariableDeclarationKind::Let,
6345 declarators,
6346 false,
6347 )),
6348 );
6349 }
6350
6351 fn identifier(&self, name: &str) -> Expression<'a> {
6352 self.ast
6353 .expression_identifier(Span::default(), self.ast.ident(name))
6354 }
6355
6356 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
6357 let target = self
6358 .ast
6359 .simple_assignment_target_assignment_target_identifier(
6360 Span::default(),
6361 self.ast.ident(name),
6362 );
6363 AssignmentTarget::from(target)
6364 }
6365
6366 fn number(&self, value: u64) -> Expression<'a> {
6367 self.ast.expression_numeric_literal(
6368 Span::default(),
6369 value as f64,
6370 None,
6371 NumberBase::Decimal,
6372 )
6373 }
6374
6375 fn string(&self, value: &str) -> Expression<'a> {
6376 self.ast
6377 .expression_string_literal(Span::default(), self.ast.str(value), None)
6378 }
6379
6380 fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
6381 self.ast.object_property_kind_object_property(
6382 Span::default(),
6383 PropertyKind::Init,
6384 self.ast
6385 .property_key_static_identifier(Span::default(), self.ast.ident(name)),
6386 value,
6387 false,
6388 false,
6389 false,
6390 )
6391 }
6392
6393 fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
6394 let conditions = self.ast.expression_array(
6395 Span::default(),
6396 self.ast.vec_from_iter(
6397 decision
6398 .conditions
6399 .iter()
6400 .map(|condition| ArrayExpressionElement::from(self.string(condition))),
6401 ),
6402 );
6403 let properties = self.ast.vec_from_array([
6404 self.object_property("id", self.string(&decision.id)),
6405 self.object_property("file", self.string(&decision.file)),
6406 self.object_property("line", self.number(decision.line as u64)),
6407 self.object_property("column", self.number(decision.column as u64)),
6408 self.object_property("source", self.string(&decision.source)),
6409 self.object_property("conditions", conditions),
6410 self.object_property("kind", self.string(&decision.kind)),
6411 ]);
6412 Expression::ObjectExpression(
6413 self.ast
6414 .alloc_object_expression(Span::default(), properties),
6415 )
6416 }
6417
6418 fn instrument_condition(
6419 &self,
6420 expression: Expression<'a>,
6421 frame_name: &str,
6422 temporary_name: &str,
6423 index: usize,
6424 ) -> Expression<'a> {
6425 let assign_value = self.ast.expression_assignment(
6426 Span::default(),
6427 AssignmentOperator::Assign,
6428 self.assignment_target(temporary_name),
6429 expression,
6430 );
6431 let weight = 3_u64.pow(index as u32);
6436 let truth = self.ast.expression_unary(
6437 Span::default(),
6438 UnaryOperator::UnaryPlus,
6439 self.ast.expression_unary(
6440 Span::default(),
6441 UnaryOperator::LogicalNot,
6442 self.ast.expression_unary(
6443 Span::default(),
6444 UnaryOperator::LogicalNot,
6445 self.identifier(temporary_name),
6446 ),
6447 ),
6448 );
6449 let one_or_two = self.ast.expression_binary(
6450 Span::default(),
6451 self.number(1),
6452 BinaryOperator::Addition,
6453 truth,
6454 );
6455 let digit = if weight == 1 {
6456 one_or_two
6457 } else {
6458 self.ast.expression_binary(
6459 Span::default(),
6460 self.number(weight),
6461 BinaryOperator::Multiplication,
6462 self.ast
6463 .expression_parenthesized(Span::default(), one_or_two),
6464 )
6465 };
6466 let add_digit = self.ast.expression_assignment(
6467 Span::default(),
6468 AssignmentOperator::Addition,
6469 self.assignment_target(frame_name),
6470 digit,
6471 );
6472 self.ast.expression_sequence(
6473 Span::default(),
6474 self.ast
6475 .vec_from_array([assign_value, add_digit, self.identifier(temporary_name)]),
6476 )
6477 }
6478
6479 fn instrument_conditions(
6480 &self,
6481 expression: &mut Expression<'a>,
6482 frame_name: &str,
6483 temporary_names: &[String],
6484 next_index: &mut usize,
6485 ) {
6486 match expression {
6487 Expression::ParenthesizedExpression(parenthesized) => self.instrument_conditions(
6488 &mut parenthesized.expression,
6489 frame_name,
6490 temporary_names,
6491 next_index,
6492 ),
6493 Expression::LogicalExpression(logical)
6494 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
6495 {
6496 self.instrument_conditions(
6497 &mut logical.left,
6498 frame_name,
6499 temporary_names,
6500 next_index,
6501 );
6502 self.instrument_conditions(
6503 &mut logical.right,
6504 frame_name,
6505 temporary_names,
6506 next_index,
6507 );
6508 }
6509 Expression::UnaryExpression(unary)
6510 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
6511 {
6512 self.instrument_conditions(
6513 &mut unary.argument,
6514 frame_name,
6515 temporary_names,
6516 next_index,
6517 );
6518 }
6519 _ => {
6520 let index = *next_index;
6521 *next_index += 1;
6522 let original = expression.take_in(self.ast.allocator);
6523 *expression =
6524 self.instrument_condition(original, frame_name, &temporary_names[index], index);
6525 }
6526 }
6527 }
6528
6529 fn instrument_condition_v1(
6530 &self,
6531 expression: Expression<'a>,
6532 frame_name: &str,
6533 index: usize,
6534 ) -> Expression<'a> {
6535 self.ast.expression_call(
6536 Span::default(),
6537 self.identifier(&self.mcdc_condition),
6538 NONE,
6539 self.ast.vec_from_array([
6540 Argument::from(self.identifier(frame_name)),
6541 Argument::from(self.number(index as u64)),
6542 Argument::from(expression),
6543 ]),
6544 false,
6545 )
6546 }
6547
6548 fn instrument_conditions_v1(
6549 &self,
6550 expression: &mut Expression<'a>,
6551 frame_name: &str,
6552 next_index: &mut usize,
6553 ) {
6554 match expression {
6555 Expression::ParenthesizedExpression(parenthesized) => {
6556 self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
6557 }
6558 Expression::LogicalExpression(logical)
6559 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
6560 {
6561 self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
6562 self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
6563 }
6564 Expression::UnaryExpression(unary)
6565 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
6566 {
6567 self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
6568 }
6569 _ => {
6570 let index = *next_index;
6571 *next_index += 1;
6572 let original = expression.take_in(self.ast.allocator);
6573 *expression = self.instrument_condition_v1(original, frame_name, index);
6574 }
6575 }
6576 }
6577
6578 fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
6579 let mut condition_spans = Vec::new();
6580 collect_conditions(test, &mut condition_spans);
6581 let plan = DecisionPlan {
6582 index: self.decision_index,
6583 condition_count: condition_spans.len(),
6584 inline_frame: self.parameter_depth > 0,
6585 };
6586 self.decision_index += 1;
6587 plan
6588 }
6589
6590 fn apply_decision(&mut self, test: &mut Expression<'a>, plan: DecisionPlan) {
6591 if plan.condition_count > 32 {
6592 let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
6593 let mut next_index = 0;
6594 self.instrument_conditions_v1(test, &frame_name, &mut next_index);
6595 debug_assert_eq!(next_index, plan.condition_count);
6596
6597 let decision = &self.decisions[plan.index];
6598 let begin = self.ast.expression_call(
6599 Span::default(),
6600 self.identifier(&self.mcdc_begin),
6601 NONE,
6602 self.ast.vec_from_array([
6603 Argument::from(self.string(&decision.id)),
6604 Argument::from(self.decision_meta(decision)),
6605 ]),
6606 false,
6607 );
6608 let assign_frame = self.ast.expression_assignment(
6609 Span::default(),
6610 AssignmentOperator::Assign,
6611 self.assignment_target(&frame_name),
6612 begin,
6613 );
6614 let instrumented = test.take_in(self.ast.allocator);
6615 let end = self.ast.expression_call(
6616 Span::default(),
6617 self.identifier(&self.mcdc_end),
6618 NONE,
6619 self.ast.vec_from_array([
6620 Argument::from(self.identifier(&frame_name)),
6621 Argument::from(instrumented),
6622 ]),
6623 false,
6624 );
6625 let observed = self.ast.expression_sequence(
6626 Span::default(),
6627 self.ast.vec_from_array([assign_frame, end]),
6628 );
6629 *test = if plan.inline_frame {
6630 self.wrap_inline_frame(observed, &[frame_name])
6631 } else {
6632 observed
6633 };
6634 return;
6635 }
6636
6637 let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
6638 let result_name = self.scratch_for("_supercovMcdcResult", plan.inline_frame);
6639 let temporary_names = (0..plan.condition_count)
6640 .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
6641 .collect::<Vec<_>>();
6642 let mut next_index = 0;
6643 self.instrument_conditions(test, &frame_name, &temporary_names, &mut next_index);
6644 debug_assert_eq!(next_index, plan.condition_count);
6645
6646 let original = test.take_in(self.ast.allocator);
6647 let assign_frame = self.ast.expression_assignment(
6648 Span::default(),
6649 AssignmentOperator::Assign,
6650 self.assignment_target(&frame_name),
6651 self.number(0),
6652 );
6653 let assign_result = self.ast.expression_assignment(
6654 Span::default(),
6655 AssignmentOperator::Assign,
6656 self.assignment_target(&result_name),
6657 original,
6658 );
6659 let arguments = self.ast.vec_from_array([
6660 Argument::from(self.identifier(&self.probe_file_v2)),
6661 Argument::from(self.number(plan.index as u64)),
6662 Argument::from(self.identifier(&frame_name)),
6663 Argument::from(self.identifier(&result_name)),
6664 ]);
6665 let record = self.ast.expression_call(
6666 Span::default(),
6667 self.identifier(&self.mcdc_end_v2),
6668 NONE,
6669 arguments,
6670 false,
6671 );
6672 let observed = self.ast.expression_sequence(
6673 Span::default(),
6674 self.ast.vec_from_array([
6675 assign_frame,
6676 assign_result,
6677 record,
6678 self.identifier(&result_name),
6679 ]),
6680 );
6681 *test = if plan.inline_frame {
6682 let mut names = vec![frame_name, result_name];
6683 names.extend(temporary_names);
6684 self.wrap_inline_frame(observed, &names)
6685 } else {
6686 observed
6687 };
6688 }
6689}
6690
6691impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
6692 fn visit_program(&mut self, program: &mut Program<'a>) {
6693 self.enter_declaration_scope();
6694 walk_mut::walk_program(self, program);
6695 let declarations = self.leave_declaration_scope();
6696 self.prepend_declarations(declarations, &mut program.body);
6697 }
6698
6699 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
6700 self.enter_declaration_scope();
6701 walk_mut::walk_function_body(self, body);
6702 let declarations = self.leave_declaration_scope();
6703 self.prepend_declarations(declarations, &mut body.statements);
6704 }
6705
6706 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
6707 if self
6708 .source_sensitive_functions
6709 .contains(&span_key(function.span))
6710 {
6711 return;
6712 }
6713 let outer_parameter_depth = self.parameter_depth;
6714 self.parameter_depth = 0;
6715 walk_mut::walk_function(self, function, flags);
6716 self.parameter_depth = outer_parameter_depth;
6717 }
6718
6719 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
6720 if self
6721 .source_sensitive_functions
6722 .contains(&span_key(function.span))
6723 {
6724 return;
6725 }
6726 let outer_parameter_depth = self.parameter_depth;
6727 self.parameter_depth = 0;
6728 walk_mut::walk_arrow_function_expression(self, function);
6729 self.parameter_depth = outer_parameter_depth;
6730 }
6731
6732 fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
6733 self.parameter_depth += 1;
6734 walk_mut::walk_formal_parameters(self, parameters);
6735 self.parameter_depth -= 1;
6736 }
6737
6738 fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
6741
6742 fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
6743 let plan = decision_outcome_is_variable(&statement.test)
6744 .then(|| self.reserve_decision(&statement.test));
6745 self.visit_expression(&mut statement.test);
6746 if let Some(plan) = plan {
6747 self.apply_decision(&mut statement.test, plan);
6748 }
6749 self.visit_statement(&mut statement.consequent);
6750 if let Some(alternate) = &mut statement.alternate {
6751 self.visit_statement(alternate);
6752 }
6753 }
6754
6755 fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
6756 let plan = decision_outcome_is_variable(&expression.test)
6757 .then(|| self.reserve_decision(&expression.test));
6758 self.visit_expression(&mut expression.test);
6759 if let Some(plan) = plan {
6760 self.apply_decision(&mut expression.test, plan);
6761 }
6762 self.visit_expression(&mut expression.consequent);
6763 self.visit_expression(&mut expression.alternate);
6764 }
6765
6766 fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
6767 let plan = decision_outcome_is_variable(&statement.test)
6768 .then(|| self.reserve_decision(&statement.test));
6769 self.visit_expression(&mut statement.test);
6770 if let Some(plan) = plan {
6771 self.apply_decision(&mut statement.test, plan);
6772 }
6773 self.visit_statement(&mut statement.body);
6774 }
6775
6776 fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
6777 let plan = decision_outcome_is_variable(&statement.test)
6778 .then(|| self.reserve_decision(&statement.test));
6779 self.visit_statement(&mut statement.body);
6780 self.visit_expression(&mut statement.test);
6781 if let Some(plan) = plan {
6782 self.apply_decision(&mut statement.test, plan);
6783 }
6784 }
6785
6786 fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
6787 let plan = statement
6788 .test
6789 .as_ref()
6790 .filter(|test| decision_outcome_is_variable(test))
6791 .map(|test| self.reserve_decision(test));
6792 if let Some(init) = &mut statement.init {
6793 self.visit_for_statement_init(init);
6794 }
6795 if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
6796 self.visit_expression(test);
6797 self.apply_decision(test, plan);
6798 }
6799 if let Some(update) = &mut statement.update {
6800 self.visit_expression(update);
6801 }
6802 self.visit_statement(&mut statement.body);
6803 }
6804}
6805
6806struct DecisionCollector<'s> {
6807 source: &'s str,
6808 file: &'s str,
6809 decisions: Vec<CandidateDecision>,
6810 decision_vector_counts: Vec<usize>,
6811 decision_logical_nodes: HashSet<SpanKey>,
6812 source_sensitive_functions: &'s HashSet<SpanKey>,
6813}
6814
6815impl DecisionCollector<'_> {
6816 fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
6817 let mut condition_spans = Vec::new();
6818 collect_conditions(test, &mut condition_spans);
6819 collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
6820 let span = transparent_expression(test).span();
6824 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
6825 self.decisions.push(CandidateDecision {
6826 id: stable_id(self.source, self.file, "decision", span, kind),
6827 file: self.file.to_string(),
6828 line,
6829 column,
6830 source: source_slice(self.source, span).to_string(),
6831 conditions: condition_spans
6832 .iter()
6833 .map(|condition| source_slice(self.source, *condition).to_string())
6834 .collect(),
6835 kind: kind.to_string(),
6836 });
6837 self.decision_vector_counts.push(
6838 if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
6839 reachable_vector_count(test, &condition_spans)
6840 } else {
6841 0
6842 },
6843 );
6844 }
6845}
6846
6847fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
6851 syntactic_boolean_outcome(expression).is_none()
6852}
6853
6854fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
6855 match transparent_expression(expression) {
6856 Expression::BooleanLiteral(literal) => Some(literal.value),
6857 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
6858 syntactic_boolean_outcome(&unary.argument).map(|value| !value)
6859 }
6860 Expression::LogicalExpression(logical)
6861 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
6862 {
6863 let left = syntactic_boolean_outcome(&logical.left);
6864 let right = syntactic_boolean_outcome(&logical.right);
6865 match logical.operator {
6866 LogicalOperator::And => match (left, right) {
6867 (Some(false), _) | (_, Some(false)) => Some(false),
6868 (Some(true), value) | (value, Some(true)) => value,
6869 _ => None,
6870 },
6871 LogicalOperator::Or => match (left, right) {
6872 (Some(true), _) | (_, Some(true)) => Some(true),
6873 (Some(false), value) | (value, Some(false)) => value,
6874 _ => None,
6875 },
6876 LogicalOperator::Coalesce => None,
6877 }
6878 }
6879 _ => None,
6880 }
6881}
6882
6883#[derive(Default)]
6884struct CoverageSurfaceScanner {
6885 found: bool,
6886}
6887
6888impl<'a> Visit<'a> for CoverageSurfaceScanner {
6889 fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
6890 self.found = true;
6891 }
6892
6893 fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
6894 self.found = true;
6895 }
6896
6897 fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
6898 self.found = true;
6899 }
6900
6901 fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
6902 self.found = true;
6903 }
6904
6905 fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
6906 self.found = true;
6907 }
6908
6909 fn visit_class(&mut self, _class: &Class<'a>) {
6910 self.found = true;
6911 }
6912
6913 fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
6914 if expression.operator.is_logical() {
6915 self.found = true;
6916 } else {
6917 walk::walk_assignment_expression(self, expression);
6918 }
6919 }
6920}
6921
6922fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
6923 fn visit_condition(expression: &Expression<'_>) -> bool {
6924 let mut scanner = CoverageSurfaceScanner::default();
6925 scanner.visit_expression(expression);
6926 !scanner.found
6927 }
6928 match transparent_expression(expression) {
6929 Expression::LogicalExpression(logical)
6930 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
6931 {
6932 decision_conditions_are_transparent(&logical.left)
6933 && decision_conditions_are_transparent(&logical.right)
6934 }
6935 Expression::UnaryExpression(unary)
6936 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
6937 {
6938 decision_conditions_are_transparent(&unary.argument)
6939 }
6940 condition => visit_condition(condition),
6941 }
6942}
6943
6944fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
6945 fn evaluate(
6946 expression: &Expression<'_>,
6947 assignment: usize,
6948 encoded: &mut usize,
6949 indices: &HashMap<SpanKey, usize>,
6950 ) -> bool {
6951 match transparent_expression(expression) {
6952 Expression::LogicalExpression(logical)
6953 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
6954 {
6955 let left = evaluate(&logical.left, assignment, encoded, indices);
6956 if logical.operator == LogicalOperator::And {
6957 left && evaluate(&logical.right, assignment, encoded, indices)
6958 } else {
6959 left || evaluate(&logical.right, assignment, encoded, indices)
6960 }
6961 }
6962 Expression::UnaryExpression(unary)
6963 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
6964 {
6965 !evaluate(&unary.argument, assignment, encoded, indices)
6966 }
6967 condition => {
6968 let index = indices
6969 .get(&span_key(condition.span()))
6970 .expect("decision condition index must remain stable");
6971 let value = assignment & (1 << index) != 0;
6972 *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
6973 value
6974 }
6975 }
6976 }
6977
6978 let indices = conditions
6979 .iter()
6980 .enumerate()
6981 .map(|(index, span)| (span_key(*span), index))
6982 .collect::<HashMap<_, _>>();
6983 let mut vectors = HashSet::new();
6984 for assignment in 0..(1_usize << conditions.len()) {
6985 let mut encoded = 0;
6986 let outcome = evaluate(expression, assignment, &mut encoded, &indices);
6987 vectors.insert(encoded * 2 + usize::from(outcome));
6988 }
6989 vectors.len()
6990}
6991
6992fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
6993 match expression {
6994 Expression::ParenthesizedExpression(parenthesized) => {
6995 collect_decision_logical_nodes(&parenthesized.expression, nodes);
6996 }
6997 Expression::LogicalExpression(logical)
6998 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
6999 {
7000 nodes.insert(span_key(logical.span));
7001 collect_decision_logical_nodes(&logical.left, nodes);
7002 collect_decision_logical_nodes(&logical.right, nodes);
7003 }
7004 Expression::UnaryExpression(unary)
7005 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7006 {
7007 collect_decision_logical_nodes(&unary.argument, nodes);
7008 }
7009 _ => {}
7010 }
7011}
7012
7013#[derive(Default)]
7014struct LogicalBranchAnalysis {
7015 branches: Vec<CandidateBranch>,
7016 logical_targets: HashMap<SpanKey, (String, String)>,
7017}
7018
7019#[derive(Default)]
7020struct LogicalAssignmentAnalysis {
7021 branches: Vec<CandidateBranch>,
7022 targets: HashMap<SpanKey, (String, String)>,
7023}
7024
7025#[derive(Default)]
7026struct OptionalMemberAnalysis {
7027 branches: Vec<CandidateBranch>,
7028 targets: HashMap<SpanKey, (String, String)>,
7029 links: HashMap<SpanKey, u8>,
7031 after_call: HashMap<SpanKey, SpanKey>,
7034}
7035
7036const LINK_AFTER_ANOTHER: u8 = 1;
7038const LINK_BEFORE_ANOTHER: u8 = 2;
7040const LINK_AFTER_CALL: u8 = 4;
7042
7043enum LinkBelow {
7045 None,
7046 Member,
7047 Call(SpanKey),
7048}
7049
7050fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
7055 let mut current = object;
7056 loop {
7057 current = match current {
7058 Expression::StaticMemberExpression(member) if member.optional => {
7059 return LinkBelow::Member;
7060 }
7061 Expression::ComputedMemberExpression(member) if member.optional => {
7062 return LinkBelow::Member;
7063 }
7064 Expression::PrivateFieldExpression(member) if member.optional => {
7065 return LinkBelow::Member;
7066 }
7067 Expression::CallExpression(call) if call.optional => {
7068 return LinkBelow::Call(span_key(call.span));
7069 }
7070 Expression::StaticMemberExpression(member) => &member.object,
7071 Expression::ComputedMemberExpression(member) => &member.object,
7072 Expression::PrivateFieldExpression(member) => &member.object,
7073 Expression::CallExpression(call) => &call.callee,
7074 Expression::TSNonNullExpression(inner) => &inner.expression,
7075 _ => return LinkBelow::None,
7076 };
7077 }
7078}
7079
7080fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
7083 for ancestor in context.ancestors() {
7084 match ancestor {
7085 Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
7086 Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
7087 Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
7088 Ancestor::StaticMemberExpressionObject(_)
7089 | Ancestor::ComputedMemberExpressionObject(_)
7090 | Ancestor::PrivateFieldExpressionObject(_)
7091 | Ancestor::TSNonNullExpressionExpression(_) => {}
7092 Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
7093 _ => return false,
7094 }
7095 }
7096 false
7097}
7098
7099#[derive(Default)]
7100struct OptionalCallAnalysis {
7101 branches: Vec<CandidateBranch>,
7102 sites: HashMap<SpanKey, (String, String)>,
7103 roots: HashMap<SpanKey, Vec<SpanKey>>,
7104 limitations: Vec<CandidateLimitation>,
7105}
7106
7107#[derive(Clone)]
7108struct DefaultTarget {
7109 default_id: String,
7110 provided_id: String,
7111 inferred_name: Option<String>,
7112}
7113
7114#[derive(Default)]
7115struct DefaultAnalysis {
7116 branches: Vec<CandidateBranch>,
7117 parameter_targets: HashMap<SpanKey, DefaultTarget>,
7118 binding_targets: HashMap<SpanKey, DefaultTarget>,
7119 limitations: Vec<CandidateLimitation>,
7120}
7121
7122#[derive(Clone)]
7123struct ExtendedTarget {
7124 first_id: String,
7125 second_id: String,
7126}
7127
7128#[derive(Default)]
7129struct ExtendedAnalysis {
7130 branches: Vec<CandidateBranch>,
7131 try_targets: HashMap<SpanKey, ExtendedTarget>,
7132 loop_targets: HashMap<SpanKey, ExtendedTarget>,
7133}
7134
7135#[derive(Clone)]
7136struct SwitchTarget {
7137 case_ids: Vec<String>,
7138 no_match_id: Option<String>,
7139}
7140
7141#[derive(Default)]
7142struct SwitchAnalysis {
7143 branches: Vec<CandidateBranch>,
7144 targets: HashMap<SpanKey, SwitchTarget>,
7145}
7146
7147struct SwitchCollector<'s> {
7148 source: &'s str,
7149 file: &'s str,
7150 source_sensitive_functions: &'s HashSet<SpanKey>,
7151 unsafe_function_depth: usize,
7152 with_depth: usize,
7153 analysis: SwitchAnalysis,
7154}
7155
7156impl SwitchCollector<'_> {
7157 fn unsafe_context(&self) -> bool {
7158 self.unsafe_function_depth > 0 || self.with_depth > 0
7159 }
7160
7161 fn exit_source_function(&mut self, span: Span) {
7162 if self.source_sensitive_functions.contains(&span_key(span)) {
7163 self.unsafe_function_depth -= 1;
7164 }
7165 }
7166}
7167
7168impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
7169 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7170 if self
7171 .source_sensitive_functions
7172 .contains(&span_key(node.span))
7173 {
7174 self.unsafe_function_depth += 1;
7175 }
7176 }
7177
7178 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7179 self.exit_source_function(node.span);
7180 }
7181
7182 fn enter_arrow_function_expression(
7183 &mut self,
7184 node: &mut ArrowFunctionExpression<'a>,
7185 _context: &mut TraverseCtx<'a, ()>,
7186 ) {
7187 if self
7188 .source_sensitive_functions
7189 .contains(&span_key(node.span))
7190 {
7191 self.unsafe_function_depth += 1;
7192 }
7193 }
7194
7195 fn exit_arrow_function_expression(
7196 &mut self,
7197 node: &mut ArrowFunctionExpression<'a>,
7198 _context: &mut TraverseCtx<'a, ()>,
7199 ) {
7200 self.exit_source_function(node.span);
7201 }
7202
7203 fn enter_with_statement(
7204 &mut self,
7205 _node: &mut WithStatement<'a>,
7206 _context: &mut TraverseCtx<'a, ()>,
7207 ) {
7208 self.with_depth += 1;
7209 }
7210
7211 fn exit_with_statement(
7212 &mut self,
7213 _node: &mut WithStatement<'a>,
7214 _context: &mut TraverseCtx<'a, ()>,
7215 ) {
7216 self.with_depth -= 1;
7217 }
7218
7219 fn enter_switch_statement(
7220 &mut self,
7221 node: &mut SwitchStatement<'a>,
7222 _context: &mut TraverseCtx<'a, ()>,
7223 ) {
7224 let has_default = node.cases.iter().any(|case| case.test.is_none());
7225 if self.unsafe_context() {
7226 return;
7227 }
7228 let id = stable_id(self.source, self.file, "switch", node.span, "");
7229 let mut case_ids = Vec::with_capacity(node.cases.len());
7230 let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
7231 for (index, case) in node.cases.iter().enumerate() {
7232 let alternative_id = format!("{id}:case:{index}");
7233 let label = case.test.as_ref().map_or_else(
7234 || "default".to_string(),
7235 |test| format!("case {}", source_slice(self.source, test.span())),
7236 );
7237 case_ids.push(alternative_id.clone());
7238 alternatives.push(CandidateBranchAlternative {
7239 id: alternative_id,
7240 label,
7241 });
7242 }
7243 let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
7244 if let Some(no_match_id) = &no_match_id {
7245 alternatives.push(CandidateBranchAlternative {
7246 id: no_match_id.clone(),
7247 label: "no matching case".to_string(),
7248 });
7249 }
7250 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
7251 self.analysis.branches.push(CandidateBranch {
7252 id,
7253 kind: "switch".to_string(),
7254 file: self.file.to_string(),
7255 line,
7256 column,
7257 source: source_slice(self.source, node.discriminant.span()).to_string(),
7258 alternatives,
7259 });
7260 self.analysis.targets.insert(
7261 span_key(node.span),
7262 SwitchTarget {
7263 case_ids,
7264 no_match_id,
7265 },
7266 );
7267 }
7268}
7269
7270fn collect_switch_branches<'a>(
7271 allocator: &'a Allocator,
7272 program: &mut Program<'a>,
7273 source: &str,
7274 file: &str,
7275 source_sensitive_functions: &HashSet<SpanKey>,
7276) -> SwitchAnalysis {
7277 let mut collector = SwitchCollector {
7278 source,
7279 file,
7280 source_sensitive_functions,
7281 unsafe_function_depth: 0,
7282 with_depth: 0,
7283 analysis: SwitchAnalysis::default(),
7284 };
7285 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7286 collector.analysis
7287}
7288
7289struct ExtendedCollector<'s> {
7290 source: &'s str,
7291 file: &'s str,
7292 source_sensitive_functions: &'s HashSet<SpanKey>,
7293 entered_loops: HashSet<SpanKey>,
7294 unsafe_function_depth: usize,
7295 with_depth: usize,
7296 analysis: ExtendedAnalysis,
7297}
7298
7299impl ExtendedCollector<'_> {
7300 fn unsafe_context(&self) -> bool {
7301 self.unsafe_function_depth > 0 || self.with_depth > 0
7302 }
7303
7304 fn enter_try(&mut self, node: &TryStatement<'_>) {
7305 let transformed = !self.unsafe_context() && node.handler.is_some();
7306 if !transformed {
7307 return;
7308 }
7309 let id = stable_id(self.source, self.file, "try-catch", node.span, "");
7310 let success_id = format!("{id}:success");
7311 let catch_id = format!("{id}:catch");
7312 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
7313 self.analysis.branches.push(CandidateBranch {
7314 id,
7315 kind: "try-catch".to_string(),
7316 file: self.file.to_string(),
7317 line,
7318 column,
7319 source: "try / catch".to_string(),
7320 alternatives: vec![
7321 CandidateBranchAlternative {
7322 id: success_id.clone(),
7323 label: "try completed without catch".to_string(),
7324 },
7325 CandidateBranchAlternative {
7326 id: catch_id.clone(),
7327 label: "catch entered".to_string(),
7328 },
7329 ],
7330 });
7331 self.analysis.try_targets.insert(
7332 span_key(node.span),
7333 ExtendedTarget {
7334 first_id: success_id,
7335 second_id: catch_id,
7336 },
7337 );
7338 }
7339
7340 fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
7341 let transformed = !self.unsafe_context();
7342 if !transformed {
7343 return;
7344 }
7345 if self.entered_loops.contains(&span_key(span)) {
7349 return;
7350 }
7351 let id = stable_id(self.source, self.file, kind, span, "");
7352 let zero_id = format!("{id}:zero");
7353 let entered_id = format!("{id}:entered");
7354 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
7355 self.analysis.branches.push(CandidateBranch {
7356 id,
7357 kind: kind.to_string(),
7358 file: self.file.to_string(),
7359 line,
7360 column,
7361 source: source_slice(self.source, right.span()).to_string(),
7362 alternatives: vec![
7363 CandidateBranchAlternative {
7364 id: zero_id.clone(),
7365 label: "zero iterations".to_string(),
7366 },
7367 CandidateBranchAlternative {
7368 id: entered_id.clone(),
7369 label: "one or more iterations".to_string(),
7370 },
7371 ],
7372 });
7373 self.analysis.loop_targets.insert(
7374 span_key(span),
7375 ExtendedTarget {
7376 first_id: zero_id,
7377 second_id: entered_id,
7378 },
7379 );
7380 }
7381
7382 fn exit_source_function(&mut self, span: Span) {
7383 if self.source_sensitive_functions.contains(&span_key(span)) {
7384 self.unsafe_function_depth -= 1;
7385 }
7386 }
7387}
7388
7389impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
7390 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7391 if self
7392 .source_sensitive_functions
7393 .contains(&span_key(node.span))
7394 {
7395 self.unsafe_function_depth += 1;
7396 }
7397 }
7398
7399 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7400 self.exit_source_function(node.span);
7401 }
7402
7403 fn enter_arrow_function_expression(
7404 &mut self,
7405 node: &mut ArrowFunctionExpression<'a>,
7406 _context: &mut TraverseCtx<'a, ()>,
7407 ) {
7408 if self
7409 .source_sensitive_functions
7410 .contains(&span_key(node.span))
7411 {
7412 self.unsafe_function_depth += 1;
7413 }
7414 }
7415
7416 fn exit_arrow_function_expression(
7417 &mut self,
7418 node: &mut ArrowFunctionExpression<'a>,
7419 _context: &mut TraverseCtx<'a, ()>,
7420 ) {
7421 self.exit_source_function(node.span);
7422 }
7423
7424 fn enter_with_statement(
7425 &mut self,
7426 _node: &mut WithStatement<'a>,
7427 _context: &mut TraverseCtx<'a, ()>,
7428 ) {
7429 self.with_depth += 1;
7430 }
7431
7432 fn exit_with_statement(
7433 &mut self,
7434 _node: &mut WithStatement<'a>,
7435 _context: &mut TraverseCtx<'a, ()>,
7436 ) {
7437 self.with_depth -= 1;
7438 }
7439
7440 fn enter_try_statement(
7441 &mut self,
7442 node: &mut TryStatement<'a>,
7443 _context: &mut TraverseCtx<'a, ()>,
7444 ) {
7445 self.enter_try(node);
7446 }
7447
7448 fn exit_try_statement(
7449 &mut self,
7450 _node: &mut TryStatement<'a>,
7451 _context: &mut TraverseCtx<'a, ()>,
7452 ) {
7453 }
7454
7455 fn enter_for_in_statement(
7456 &mut self,
7457 node: &mut ForInStatement<'a>,
7458 _context: &mut TraverseCtx<'a, ()>,
7459 ) {
7460 self.enter_loop(node.span, &node.right, "for-in");
7461 }
7462
7463 fn exit_for_in_statement(
7464 &mut self,
7465 _node: &mut ForInStatement<'a>,
7466 _context: &mut TraverseCtx<'a, ()>,
7467 ) {
7468 }
7469
7470 fn enter_for_of_statement(
7471 &mut self,
7472 node: &mut ForOfStatement<'a>,
7473 _context: &mut TraverseCtx<'a, ()>,
7474 ) {
7475 self.enter_loop(node.span, &node.right, "for-of");
7476 }
7477
7478 fn exit_for_of_statement(
7479 &mut self,
7480 _node: &mut ForOfStatement<'a>,
7481 _context: &mut TraverseCtx<'a, ()>,
7482 ) {
7483 }
7484}
7485
7486fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
7499 let semantic = SemanticBuilder::new().build(program).semantic;
7500 let mut found = HashSet::new();
7501 for node in semantic.nodes() {
7502 if let AstKind::ForOfStatement(loop_node) = node.kind()
7503 && never_empty(&loop_node.right, &semantic)
7504 {
7505 found.insert(span_key(loop_node.span));
7506 }
7507 }
7508 found
7509}
7510
7511fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
7514 match transparent_expression(expression) {
7515 Expression::ArrayExpression(array) => has_definite_element(array),
7516 Expression::Identifier(identifier) => {
7517 let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
7518 return false;
7519 };
7520 if semantic.scoping().get_resolved_references(symbol).count() != 1 {
7525 return false;
7526 }
7527 let declaration = semantic.scoping().symbol_declaration(symbol);
7528 let AstKind::VariableDeclarator(declarator) =
7529 semantic.nodes().get_node(declaration).kind()
7530 else {
7531 return false;
7532 };
7533 declarator.kind == VariableDeclarationKind::Const
7534 && declarator.init.as_ref().is_some_and(|init| {
7535 matches!(transparent_expression(init), Expression::ArrayExpression(array)
7536 if has_definite_element(array))
7537 })
7538 }
7539 _ => false,
7540 }
7541}
7542
7543fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
7547 array
7548 .elements
7549 .iter()
7550 .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
7551}
7552
7553fn collect_extended_branches<'a>(
7554 allocator: &'a Allocator,
7555 program: &mut Program<'a>,
7556 source: &str,
7557 file: &str,
7558 source_sensitive_functions: &HashSet<SpanKey>,
7559) -> ExtendedAnalysis {
7560 let entered_loops = provably_entered_loops(program);
7561 let mut collector = ExtendedCollector {
7562 source,
7563 file,
7564 source_sensitive_functions,
7565 entered_loops,
7566 unsafe_function_depth: 0,
7567 with_depth: 0,
7568 analysis: ExtendedAnalysis::default(),
7569 };
7570 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7571 collector.analysis
7572}
7573
7574struct DefaultCollector<'s> {
7575 source: &'s str,
7576 file: &'s str,
7577 source_sensitive_functions: &'s HashSet<SpanKey>,
7578 unsafe_function_depth: usize,
7579 with_depth: usize,
7580 analysis: DefaultAnalysis,
7581}
7582
7583impl DefaultCollector<'_> {
7584 fn unsafe_context(&self) -> bool {
7585 self.unsafe_function_depth > 0 || self.with_depth > 0
7586 }
7587
7588 fn target(
7589 &mut self,
7590 span: Span,
7591 left: &BindingPattern<'_>,
7592 right: &Expression<'_>,
7593 ) -> DefaultTarget {
7594 let id = stable_id(self.source, self.file, "default-value", span, "");
7595 let default_id = format!("{id}:default");
7596 let provided_id = format!("{id}:provided");
7597 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
7598 self.analysis.branches.push(CandidateBranch {
7599 id,
7600 kind: "default-value".to_string(),
7601 file: self.file.to_string(),
7602 line,
7603 column,
7604 source: source_slice(self.source, span).to_string(),
7605 alternatives: vec![
7606 CandidateBranchAlternative {
7607 id: default_id.clone(),
7608 label: "default evaluated".to_string(),
7609 },
7610 CandidateBranchAlternative {
7611 id: provided_id.clone(),
7612 label: "value provided".to_string(),
7613 },
7614 ],
7615 });
7616 DefaultTarget {
7617 default_id,
7618 provided_id,
7619 inferred_name: binding_identifier_name(left)
7620 .filter(|_| expression_is_anonymous_definition(right)),
7621 }
7622 }
7623
7624 fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
7625 match pattern {
7626 BindingPattern::AssignmentPattern(assignment) => {
7627 let target = self.target(assignment.span, &assignment.left, &assignment.right);
7628 if parameter {
7629 self.analysis
7630 .parameter_targets
7631 .insert(span_key(assignment.span), target);
7632 } else {
7633 self.analysis
7634 .binding_targets
7635 .insert(span_key(assignment.span), target);
7636 }
7637 self.collect_binding_pattern(&assignment.left, parameter);
7638 }
7639 BindingPattern::ObjectPattern(object) => {
7640 for property in &object.properties {
7641 self.collect_binding_pattern(&property.value, parameter);
7642 }
7643 if let Some(rest) = &object.rest {
7644 self.collect_binding_pattern(&rest.argument, parameter);
7645 }
7646 }
7647 BindingPattern::ArrayPattern(array) => {
7648 for element in array.elements.iter().flatten() {
7649 self.collect_binding_pattern(element, parameter);
7650 }
7651 if let Some(rest) = &array.rest {
7652 self.collect_binding_pattern(&rest.argument, parameter);
7653 }
7654 }
7655 BindingPattern::BindingIdentifier(_) => {}
7656 }
7657 }
7658
7659 fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
7660 for parameter in ¶meters.items {
7661 if let Some(initializer) = ¶meter.initializer {
7662 let default_span =
7669 Span::new(parameter.pattern.span().start, initializer.span().end);
7670 let target = self.target(default_span, ¶meter.pattern, initializer);
7671 self.analysis
7672 .parameter_targets
7673 .insert(span_key(parameter.span), target);
7674 }
7675 self.collect_binding_pattern(¶meter.pattern, true);
7676 }
7677 if let Some(rest) = ¶meters.rest {
7678 self.collect_binding_pattern(&rest.rest.argument, true);
7679 }
7680 }
7681
7682 fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
7683 match pattern {
7684 BindingPattern::AssignmentPattern(_) => true,
7685 BindingPattern::ObjectPattern(object) => {
7686 object
7687 .properties
7688 .iter()
7689 .any(|property| Self::has_binding_default(&property.value))
7690 || object
7691 .rest
7692 .as_ref()
7693 .is_some_and(|rest| Self::has_binding_default(&rest.argument))
7694 }
7695 BindingPattern::ArrayPattern(array) => {
7696 array
7697 .elements
7698 .iter()
7699 .flatten()
7700 .any(Self::has_binding_default)
7701 || array
7702 .rest
7703 .as_ref()
7704 .is_some_and(|rest| Self::has_binding_default(&rest.argument))
7705 }
7706 BindingPattern::BindingIdentifier(_) => false,
7707 }
7708 }
7709
7710 fn exit_source_function(&mut self, span: Span) {
7711 if self.source_sensitive_functions.contains(&span_key(span)) {
7712 self.unsafe_function_depth -= 1;
7713 }
7714 }
7715}
7716
7717impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
7718 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7719 if self
7720 .source_sensitive_functions
7721 .contains(&span_key(node.span))
7722 {
7723 self.unsafe_function_depth += 1;
7724 return;
7725 }
7726 if !self.unsafe_context() && node.body.is_some() {
7727 self.collect_parameters(&node.params);
7728 }
7729 }
7730
7731 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7732 self.exit_source_function(node.span);
7733 }
7734
7735 fn enter_arrow_function_expression(
7736 &mut self,
7737 node: &mut ArrowFunctionExpression<'a>,
7738 _context: &mut TraverseCtx<'a, ()>,
7739 ) {
7740 if self
7741 .source_sensitive_functions
7742 .contains(&span_key(node.span))
7743 {
7744 self.unsafe_function_depth += 1;
7745 return;
7746 }
7747 if !self.unsafe_context() {
7748 self.collect_parameters(&node.params);
7749 }
7750 }
7751
7752 fn exit_arrow_function_expression(
7753 &mut self,
7754 node: &mut ArrowFunctionExpression<'a>,
7755 _context: &mut TraverseCtx<'a, ()>,
7756 ) {
7757 self.exit_source_function(node.span);
7758 }
7759
7760 fn enter_with_statement(
7761 &mut self,
7762 _node: &mut WithStatement<'a>,
7763 _context: &mut TraverseCtx<'a, ()>,
7764 ) {
7765 self.with_depth += 1;
7766 }
7767
7768 fn exit_with_statement(
7769 &mut self,
7770 _node: &mut WithStatement<'a>,
7771 _context: &mut TraverseCtx<'a, ()>,
7772 ) {
7773 self.with_depth -= 1;
7774 }
7775
7776 fn enter_variable_declaration(
7777 &mut self,
7778 node: &mut VariableDeclaration<'a>,
7779 context: &mut TraverseCtx<'a, ()>,
7780 ) {
7781 if self.unsafe_context() {
7782 return;
7783 }
7784 let has_default = node
7785 .declarations
7786 .iter()
7787 .any(|declaration| Self::has_binding_default(&declaration.id));
7788 if !has_default {
7789 return;
7790 }
7791 if matches!(
7792 context.ancestors().next(),
7793 Some(Ancestor::ForStatementInit(_))
7794 ) {
7795 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
7796 self.analysis.limitations.push(CandidateLimitation {
7797 id: stable_id(
7798 self.source,
7799 self.file,
7800 "dynamic-code",
7801 node.span,
7802 "for-init-default",
7803 ),
7804 kind: "dynamic-code".to_string(),
7805 file: self.file.to_string(),
7806 line,
7807 column,
7808 source: source_slice(self.source, node.span).to_string(),
7809 reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
7810 });
7811 return;
7812 }
7813 for declaration in &node.declarations {
7814 self.collect_binding_pattern(&declaration.id, false);
7815 }
7816 }
7817}
7818
7819fn collect_default_branches<'a>(
7820 allocator: &'a Allocator,
7821 program: &mut Program<'a>,
7822 source: &str,
7823 file: &str,
7824 source_sensitive_functions: &HashSet<SpanKey>,
7825) -> DefaultAnalysis {
7826 let mut collector = DefaultCollector {
7827 source,
7828 file,
7829 source_sensitive_functions,
7830 unsafe_function_depth: 0,
7831 with_depth: 0,
7832 analysis: DefaultAnalysis::default(),
7833 };
7834 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7835 collector.analysis
7836}
7837
7838struct OptionalCallCollector<'s> {
7839 source: &'s str,
7840 file: &'s str,
7841 source_sensitive_functions: &'s HashSet<SpanKey>,
7842 unsafe_function_depth: usize,
7843 with_depth: usize,
7844 chain_roots: Vec<SpanKey>,
7845 analysis: OptionalCallAnalysis,
7846}
7847
7848impl OptionalCallCollector<'_> {
7849 fn unsafe_context(&self) -> bool {
7850 self.unsafe_function_depth > 0 || self.with_depth > 0
7851 }
7852
7853 fn exit_source_function(&mut self, span: Span) {
7854 if self.source_sensitive_functions.contains(&span_key(span)) {
7855 self.unsafe_function_depth -= 1;
7856 }
7857 }
7858}
7859
7860impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
7861 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7862 if self
7863 .source_sensitive_functions
7864 .contains(&span_key(node.span))
7865 {
7866 self.unsafe_function_depth += 1;
7867 }
7868 }
7869
7870 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7871 self.exit_source_function(node.span);
7872 }
7873
7874 fn enter_arrow_function_expression(
7875 &mut self,
7876 node: &mut ArrowFunctionExpression<'a>,
7877 _context: &mut TraverseCtx<'a, ()>,
7878 ) {
7879 if self
7880 .source_sensitive_functions
7881 .contains(&span_key(node.span))
7882 {
7883 self.unsafe_function_depth += 1;
7884 }
7885 }
7886
7887 fn exit_arrow_function_expression(
7888 &mut self,
7889 node: &mut ArrowFunctionExpression<'a>,
7890 _context: &mut TraverseCtx<'a, ()>,
7891 ) {
7892 self.exit_source_function(node.span);
7893 }
7894
7895 fn enter_with_statement(
7896 &mut self,
7897 _node: &mut WithStatement<'a>,
7898 _context: &mut TraverseCtx<'a, ()>,
7899 ) {
7900 self.with_depth += 1;
7901 }
7902
7903 fn exit_with_statement(
7904 &mut self,
7905 _node: &mut WithStatement<'a>,
7906 _context: &mut TraverseCtx<'a, ()>,
7907 ) {
7908 self.with_depth -= 1;
7909 }
7910
7911 fn enter_chain_expression(
7912 &mut self,
7913 node: &mut ChainExpression<'a>,
7914 context: &mut TraverseCtx<'a, ()>,
7915 ) {
7916 let root = match context.ancestors().next() {
7917 Some(Ancestor::UnaryExpressionArgument(parent))
7918 if *parent.operator() == UnaryOperator::Delete =>
7919 {
7920 span_key(*parent.span())
7921 }
7922 _ => span_key(node.span),
7923 };
7924 self.chain_roots.push(root);
7925 }
7926
7927 fn exit_chain_expression(
7928 &mut self,
7929 _node: &mut ChainExpression<'a>,
7930 _context: &mut TraverseCtx<'a, ()>,
7931 ) {
7932 self.chain_roots.pop();
7933 }
7934
7935 fn enter_call_expression(
7936 &mut self,
7937 node: &mut CallExpression<'a>,
7938 _context: &mut TraverseCtx<'a, ()>,
7939 ) {
7940 if !node.optional || self.unsafe_context() {
7941 return;
7942 }
7943 let root = *self
7944 .chain_roots
7945 .last()
7946 .expect("an optional call must be enclosed by a chain expression");
7947 let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
7948 let short_id = format!("{id}:short");
7949 let continued_id = format!("{id}:continued");
7950 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
7951 self.analysis.sites.insert(
7952 span_key(node.span),
7953 (short_id.clone(), continued_id.clone()),
7954 );
7955 self.analysis
7956 .roots
7957 .entry(root)
7958 .or_default()
7959 .push(span_key(node.span));
7960 self.analysis.branches.push(CandidateBranch {
7961 id,
7962 kind: "optional-chain".to_string(),
7963 file: self.file.to_string(),
7964 line,
7965 column,
7966 source: source_slice(self.source, node.span).to_string(),
7967 alternatives: vec![
7968 CandidateBranchAlternative {
7969 id: short_id,
7970 label: "nullish / short-circuited".to_string(),
7971 },
7972 CandidateBranchAlternative {
7973 id: continued_id,
7974 label: "non-nullish / continued".to_string(),
7975 },
7976 ],
7977 });
7978 }
7979}
7980
7981fn collect_optional_call_branches<'a>(
7982 allocator: &'a Allocator,
7983 program: &mut Program<'a>,
7984 source: &str,
7985 file: &str,
7986 source_sensitive_functions: &HashSet<SpanKey>,
7987) -> OptionalCallAnalysis {
7988 let mut collector = OptionalCallCollector {
7989 source,
7990 file,
7991 source_sensitive_functions,
7992 unsafe_function_depth: 0,
7993 with_depth: 0,
7994 chain_roots: Vec::new(),
7995 analysis: OptionalCallAnalysis::default(),
7996 };
7997 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7998 collector.analysis
7999}
8000
8001struct OptionalMemberCollector<'s> {
8002 source: &'s str,
8003 file: &'s str,
8004 source_sensitive_functions: &'s HashSet<SpanKey>,
8005 unsafe_function_depth: usize,
8006 with_depth: usize,
8007 analysis: OptionalMemberAnalysis,
8008}
8009
8010impl OptionalMemberCollector<'_> {
8011 fn record<State>(
8012 &mut self,
8013 span: Span,
8014 optional: bool,
8015 object: &Expression<'_>,
8016 context: &TraverseCtx<'_, State>,
8017 ) {
8018 if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
8019 return;
8020 }
8021 let below = match optional_link_below(object) {
8022 LinkBelow::Member => LINK_AFTER_ANOTHER,
8023 LinkBelow::Call(call) => {
8024 self.analysis.after_call.insert(span_key(span), call);
8025 LINK_AFTER_CALL
8026 }
8027 LinkBelow::None => 0,
8028 };
8029 let links = below
8030 | if optional_link_above(context) {
8031 LINK_BEFORE_ANOTHER
8032 } else {
8033 0
8034 };
8035 if links != 0 {
8036 self.analysis.links.insert(span_key(span), links);
8037 }
8038 let id = stable_id(self.source, self.file, "optional-chain", span, "");
8039 let short_id = format!("{id}:short");
8040 let continued_id = format!("{id}:continued");
8041 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
8042 self.analysis
8043 .targets
8044 .insert(span_key(span), (short_id.clone(), continued_id.clone()));
8045 self.analysis.branches.push(CandidateBranch {
8046 id,
8047 kind: "optional-chain".to_string(),
8048 file: self.file.to_string(),
8049 line,
8050 column,
8051 source: source_slice(self.source, span).to_string(),
8052 alternatives: vec![
8053 CandidateBranchAlternative {
8054 id: short_id,
8055 label: "nullish / short-circuited".to_string(),
8056 },
8057 CandidateBranchAlternative {
8058 id: continued_id,
8059 label: "non-nullish / continued".to_string(),
8060 },
8061 ],
8062 });
8063 }
8064
8065 fn exit_source_function(&mut self, span: Span) {
8066 if self.source_sensitive_functions.contains(&span_key(span)) {
8067 self.unsafe_function_depth -= 1;
8068 }
8069 }
8070}
8071
8072impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
8073 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8074 if self
8075 .source_sensitive_functions
8076 .contains(&span_key(node.span))
8077 {
8078 self.unsafe_function_depth += 1;
8079 }
8080 }
8081
8082 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8083 self.exit_source_function(node.span);
8084 }
8085
8086 fn enter_arrow_function_expression(
8087 &mut self,
8088 node: &mut ArrowFunctionExpression<'a>,
8089 _context: &mut TraverseCtx<'a, ()>,
8090 ) {
8091 if self
8092 .source_sensitive_functions
8093 .contains(&span_key(node.span))
8094 {
8095 self.unsafe_function_depth += 1;
8096 }
8097 }
8098
8099 fn exit_arrow_function_expression(
8100 &mut self,
8101 node: &mut ArrowFunctionExpression<'a>,
8102 _context: &mut TraverseCtx<'a, ()>,
8103 ) {
8104 self.exit_source_function(node.span);
8105 }
8106
8107 fn enter_with_statement(
8108 &mut self,
8109 _node: &mut WithStatement<'a>,
8110 _context: &mut TraverseCtx<'a, ()>,
8111 ) {
8112 self.with_depth += 1;
8113 }
8114
8115 fn exit_with_statement(
8116 &mut self,
8117 _node: &mut WithStatement<'a>,
8118 _context: &mut TraverseCtx<'a, ()>,
8119 ) {
8120 self.with_depth -= 1;
8121 }
8122
8123 fn enter_computed_member_expression(
8124 &mut self,
8125 node: &mut ComputedMemberExpression<'a>,
8126 context: &mut TraverseCtx<'a, ()>,
8127 ) {
8128 self.record(node.span, node.optional, &node.object, context);
8129 }
8130
8131 fn enter_static_member_expression(
8132 &mut self,
8133 node: &mut StaticMemberExpression<'a>,
8134 context: &mut TraverseCtx<'a, ()>,
8135 ) {
8136 self.record(node.span, node.optional, &node.object, context);
8137 }
8138
8139 fn enter_private_field_expression(
8140 &mut self,
8141 node: &mut PrivateFieldExpression<'a>,
8142 context: &mut TraverseCtx<'a, ()>,
8143 ) {
8144 self.record(node.span, node.optional, &node.object, context);
8145 }
8146}
8147
8148fn collect_optional_member_branches<'a>(
8149 allocator: &'a Allocator,
8150 program: &mut Program<'a>,
8151 source: &str,
8152 file: &str,
8153 source_sensitive_functions: &HashSet<SpanKey>,
8154) -> OptionalMemberAnalysis {
8155 let mut collector = OptionalMemberCollector {
8156 source,
8157 file,
8158 source_sensitive_functions,
8159 unsafe_function_depth: 0,
8160 with_depth: 0,
8161 analysis: OptionalMemberAnalysis::default(),
8162 };
8163 traverse_mut(&mut collector, allocator, program, Default::default(), ());
8164 collector.analysis
8165}
8166
8167struct LogicalAssignmentCollector<'s> {
8168 source: &'s str,
8169 file: &'s str,
8170 source_sensitive_functions: &'s HashSet<SpanKey>,
8171 unsafe_function_depth: usize,
8172 with_depth: usize,
8173 analysis: LogicalAssignmentAnalysis,
8174}
8175
8176impl LogicalAssignmentCollector<'_> {
8177 fn exit_source_function(&mut self, span: Span) {
8178 if self.source_sensitive_functions.contains(&span_key(span)) {
8179 self.unsafe_function_depth -= 1;
8180 }
8181 }
8182}
8183
8184impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
8185 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8186 if self
8187 .source_sensitive_functions
8188 .contains(&span_key(node.span))
8189 {
8190 self.unsafe_function_depth += 1;
8191 }
8192 }
8193
8194 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8195 self.exit_source_function(node.span);
8196 }
8197
8198 fn enter_arrow_function_expression(
8199 &mut self,
8200 node: &mut ArrowFunctionExpression<'a>,
8201 _context: &mut TraverseCtx<'a, ()>,
8202 ) {
8203 if self
8204 .source_sensitive_functions
8205 .contains(&span_key(node.span))
8206 {
8207 self.unsafe_function_depth += 1;
8208 }
8209 }
8210
8211 fn exit_arrow_function_expression(
8212 &mut self,
8213 node: &mut ArrowFunctionExpression<'a>,
8214 _context: &mut TraverseCtx<'a, ()>,
8215 ) {
8216 self.exit_source_function(node.span);
8217 }
8218
8219 fn enter_with_statement(
8220 &mut self,
8221 _node: &mut WithStatement<'a>,
8222 _context: &mut TraverseCtx<'a, ()>,
8223 ) {
8224 self.with_depth += 1;
8225 }
8226
8227 fn exit_with_statement(
8228 &mut self,
8229 _node: &mut WithStatement<'a>,
8230 _context: &mut TraverseCtx<'a, ()>,
8231 ) {
8232 self.with_depth -= 1;
8233 }
8234
8235 fn exit_assignment_expression(
8236 &mut self,
8237 node: &mut AssignmentExpression<'a>,
8238 _context: &mut TraverseCtx<'a, ()>,
8239 ) {
8240 if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
8241 return;
8242 }
8243 let operator = match node.operator {
8244 AssignmentOperator::LogicalAnd => "&&=",
8245 AssignmentOperator::LogicalOr => "||=",
8246 AssignmentOperator::LogicalNullish => "??=",
8247 _ => unreachable!("logical assignment filter must be exhaustive"),
8248 };
8249 let id = stable_id(
8250 self.source,
8251 self.file,
8252 "logical-assignment",
8253 node.span,
8254 operator,
8255 );
8256 let short_id = format!("{id}:short");
8257 let right_id = format!("{id}:right");
8258 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8259 self.analysis
8260 .targets
8261 .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
8262 self.analysis.branches.push(CandidateBranch {
8263 id,
8264 kind: "logical-assignment".to_string(),
8265 file: self.file.to_string(),
8266 line,
8267 column,
8268 source: source_slice(self.source, node.span).to_string(),
8269 alternatives: vec![
8270 CandidateBranchAlternative {
8271 id: short_id,
8272 label: "assignment skipped".to_string(),
8273 },
8274 CandidateBranchAlternative {
8275 id: right_id,
8276 label: "right evaluated / assigned".to_string(),
8277 },
8278 ],
8279 });
8280 }
8281}
8282
8283fn collect_logical_assignment_branches<'a>(
8284 allocator: &'a Allocator,
8285 program: &mut Program<'a>,
8286 source: &str,
8287 file: &str,
8288 source_sensitive_functions: &HashSet<SpanKey>,
8289) -> LogicalAssignmentAnalysis {
8290 let mut collector = LogicalAssignmentCollector {
8291 source,
8292 file,
8293 source_sensitive_functions,
8294 unsafe_function_depth: 0,
8295 with_depth: 0,
8296 analysis: LogicalAssignmentAnalysis::default(),
8297 };
8298 traverse_mut(&mut collector, allocator, program, Default::default(), ());
8299 collector.analysis
8300}
8301
8302struct LogicalBranchCollector<'s> {
8303 source: &'s str,
8304 file: &'s str,
8305 decision_logical_nodes: &'s HashSet<SpanKey>,
8306 source_sensitive_functions: &'s HashSet<SpanKey>,
8307 unsafe_function_depth: usize,
8308 with_depth: usize,
8309 analysis: LogicalBranchAnalysis,
8310}
8311
8312impl LogicalBranchCollector<'_> {
8313 fn exit_source_function(&mut self, span: Span) {
8314 if self.source_sensitive_functions.contains(&span_key(span)) {
8315 self.unsafe_function_depth -= 1;
8316 }
8317 }
8318}
8319
8320impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
8321 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8322 if self
8323 .source_sensitive_functions
8324 .contains(&span_key(node.span))
8325 {
8326 self.unsafe_function_depth += 1;
8327 }
8328 }
8329
8330 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
8331 self.exit_source_function(node.span);
8332 }
8333
8334 fn enter_arrow_function_expression(
8335 &mut self,
8336 node: &mut ArrowFunctionExpression<'a>,
8337 _context: &mut TraverseCtx<'a, ()>,
8338 ) {
8339 if self
8340 .source_sensitive_functions
8341 .contains(&span_key(node.span))
8342 {
8343 self.unsafe_function_depth += 1;
8344 }
8345 }
8346
8347 fn exit_arrow_function_expression(
8348 &mut self,
8349 node: &mut ArrowFunctionExpression<'a>,
8350 _context: &mut TraverseCtx<'a, ()>,
8351 ) {
8352 self.exit_source_function(node.span);
8353 }
8354
8355 fn enter_with_statement(
8356 &mut self,
8357 _node: &mut WithStatement<'a>,
8358 _context: &mut TraverseCtx<'a, ()>,
8359 ) {
8360 self.with_depth += 1;
8361 }
8362
8363 fn exit_with_statement(
8364 &mut self,
8365 _node: &mut WithStatement<'a>,
8366 _context: &mut TraverseCtx<'a, ()>,
8367 ) {
8368 self.with_depth -= 1;
8369 }
8370
8371 fn exit_logical_expression(
8372 &mut self,
8373 node: &mut LogicalExpression<'a>,
8374 _context: &mut TraverseCtx<'a, ()>,
8375 ) {
8376 if self.unsafe_function_depth > 0
8377 || self.with_depth > 0
8378 || self.decision_logical_nodes.contains(&span_key(node.span))
8379 {
8380 return;
8381 }
8382 let operator = match node.operator {
8383 LogicalOperator::And => "&&",
8384 LogicalOperator::Or => "||",
8385 LogicalOperator::Coalesce => "??",
8386 };
8387 let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
8388 let short_id = format!("{id}:short");
8389 let right_id = format!("{id}:right");
8390 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
8391 self.analysis
8392 .logical_targets
8393 .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
8394 self.analysis.branches.push(CandidateBranch {
8395 id,
8396 kind: "logical-value".to_string(),
8397 file: self.file.to_string(),
8398 line,
8399 column,
8400 source: source_slice(self.source, node.span).to_string(),
8401 alternatives: vec![
8402 CandidateBranchAlternative {
8403 id: short_id,
8404 label: "short-circuit / left selected".to_string(),
8405 },
8406 CandidateBranchAlternative {
8407 id: right_id,
8408 label: "right evaluated / selected".to_string(),
8409 },
8410 ],
8411 });
8412 }
8413}
8414
8415fn collect_logical_value_branches<'a>(
8416 allocator: &'a Allocator,
8417 program: &mut Program<'a>,
8418 source: &str,
8419 file: &str,
8420 decision_logical_nodes: &HashSet<SpanKey>,
8421 source_sensitive_functions: &HashSet<SpanKey>,
8422) -> LogicalBranchAnalysis {
8423 let mut collector = LogicalBranchCollector {
8424 source,
8425 file,
8426 decision_logical_nodes,
8427 source_sensitive_functions,
8428 unsafe_function_depth: 0,
8429 with_depth: 0,
8430 analysis: LogicalBranchAnalysis::default(),
8431 };
8432 traverse_mut(&mut collector, allocator, program, Default::default(), ());
8433 collector.analysis
8434}
8435
8436impl<'a> Visit<'a> for DecisionCollector<'_> {
8437 fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
8438 if self
8439 .source_sensitive_functions
8440 .contains(&span_key(function.span))
8441 {
8442 return;
8443 }
8444 walk::walk_function(self, function, flags);
8445 }
8446
8447 fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
8448 if self
8449 .source_sensitive_functions
8450 .contains(&span_key(function.span))
8451 {
8452 return;
8453 }
8454 walk::walk_arrow_function_expression(self, function);
8455 }
8456
8457 fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
8460
8461 fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
8462 if decision_outcome_is_variable(&statement.test) {
8463 self.record_decision(&statement.test, "if");
8464 }
8465 self.visit_expression(&statement.test);
8466 self.visit_statement(&statement.consequent);
8467 if let Some(alternate) = &statement.alternate {
8468 self.visit_statement(alternate);
8469 }
8470 }
8471
8472 fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
8473 if decision_outcome_is_variable(&expression.test) {
8474 self.record_decision(&expression.test, "ternary");
8475 }
8476 self.visit_expression(&expression.test);
8477 self.visit_expression(&expression.consequent);
8478 self.visit_expression(&expression.alternate);
8479 }
8480
8481 fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
8482 if decision_outcome_is_variable(&statement.test) {
8483 self.record_decision(&statement.test, "while");
8484 }
8485 self.visit_expression(&statement.test);
8486 self.visit_statement(&statement.body);
8487 }
8488
8489 fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
8490 if decision_outcome_is_variable(&statement.test) {
8491 self.record_decision(&statement.test, "do-while");
8492 }
8493 self.visit_statement(&statement.body);
8494 self.visit_expression(&statement.test);
8495 }
8496
8497 fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
8498 if let Some(test) = &statement.test
8499 && decision_outcome_is_variable(test)
8500 {
8501 self.record_decision(test, "for");
8502 }
8503 if let Some(init) = &statement.init {
8504 self.visit_for_statement_init(init);
8505 }
8506 if let Some(test) = &statement.test {
8507 self.visit_expression(test);
8508 }
8509 if let Some(update) = &statement.update {
8510 self.visit_expression(update);
8511 }
8512 self.visit_statement(&statement.body);
8513 }
8514}
8515
8516fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
8517 match expression {
8518 Expression::ParenthesizedExpression(parenthesized) => {
8519 transparent_expression(&parenthesized.expression)
8520 }
8521 _ => expression,
8522 }
8523}
8524
8525fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
8526 match expression {
8527 Expression::ParenthesizedExpression(parenthesized) => {
8528 has_compound_boolean_decision(&parenthesized.expression)
8529 }
8530 Expression::LogicalExpression(logical) => {
8531 matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
8532 }
8533 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
8534 has_compound_boolean_decision(&unary.argument)
8535 }
8536 _ => false,
8537 }
8538}
8539
8540fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
8541 match expression {
8542 Expression::ParenthesizedExpression(parenthesized) => {
8543 collect_conditions(&parenthesized.expression, conditions);
8544 }
8545 Expression::LogicalExpression(logical)
8546 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
8547 {
8548 collect_conditions(&logical.left, conditions);
8549 collect_conditions(&logical.right, conditions);
8550 }
8551 Expression::UnaryExpression(unary)
8552 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
8553 {
8554 collect_conditions(&unary.argument, conditions);
8555 }
8556 _ => conditions.push(expression.span()),
8557 }
8558}
8559
8560fn source_slice(source: &str, span: Span) -> &str {
8561 &source[span.start as usize..span.end as usize]
8562}
8563
8564struct SourcePositions {
8573 address: usize,
8574 length: usize,
8575 sample: u64,
8576 line_starts: Vec<usize>,
8577 utf16_before_line: Vec<usize>,
8578 checkpoints: Vec<Option<Vec<(usize, usize)>>>,
8582}
8583
8584impl SourcePositions {
8585 fn sample(source: &str) -> u64 {
8586 let bytes = source.as_bytes();
8587 let head = &bytes[..bytes.len().min(64)];
8588 let tail = &bytes[bytes.len().saturating_sub(64)..];
8589 head.iter()
8590 .chain(tail)
8591 .fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
8592 (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
8593 })
8594 }
8595
8596 fn new(source: &str) -> Self {
8597 let mut line_starts = vec![0];
8598 let mut utf16_before_line = vec![0];
8599 let mut checkpoints = Vec::new();
8600 let mut marks: Option<Vec<(usize, usize)>> = None;
8601 let mut utf16 = 0;
8602 let mut column = 0;
8603 let mut line_start = 0;
8604 for (index, character) in source.char_indices() {
8605 if !character.is_ascii() && marks.is_none() {
8606 marks = Some(vec![(0, 0)]);
8607 }
8608 if let Some(marks) = &mut marks
8609 && column >= marks.last().expect("a first mark").1 + CHECKPOINT
8610 {
8611 marks.push((index - line_start, column));
8612 }
8613 utf16 += character.len_utf16();
8614 column += character.len_utf16();
8615 if character == '\n' {
8616 line_start = index + 1;
8617 line_starts.push(line_start);
8618 utf16_before_line.push(utf16);
8619 checkpoints.push(marks.take());
8620 column = 0;
8621 }
8622 }
8623 checkpoints.push(marks);
8624 Self {
8625 address: source.as_ptr() as usize,
8626 length: source.len(),
8627 sample: Self::sample(source),
8628 line_starts,
8629 utf16_before_line,
8630 checkpoints,
8631 }
8632 }
8633
8634 fn describes(&self, source: &str) -> bool {
8635 self.address == source.as_ptr() as usize
8636 && self.length == source.len()
8637 && self.sample == Self::sample(source)
8638 }
8639
8640 fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
8642 let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
8643 let start = self.line_starts[line];
8644 let column = match &self.checkpoints[line] {
8645 None => offset - start,
8646 Some(marks) => {
8647 let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
8648 let (byte, column) = marks[at];
8649 column + source[start + byte..offset].encode_utf16().count()
8650 }
8651 };
8652 (line, column)
8653 }
8654}
8655
8656thread_local! {
8657 static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
8658 const { std::cell::RefCell::new(None) };
8659}
8660
8661pub(crate) struct PositionScope {
8666 owner: bool,
8667}
8668
8669impl PositionScope {
8670 pub(crate) fn enter() -> Self {
8671 let owner = POSITIONS.with(|positions| {
8672 let mut positions = positions.borrow_mut();
8673 let owner = positions.is_none();
8674 if owner {
8675 *positions = Some(Vec::new());
8676 }
8677 owner
8678 });
8679 Self { owner }
8680 }
8681}
8682
8683impl Drop for PositionScope {
8684 fn drop(&mut self) {
8685 if self.owner {
8686 POSITIONS.with(|positions| *positions.borrow_mut() = None);
8687 }
8688 }
8689}
8690
8691fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
8692 POSITIONS.with(|positions| {
8693 let mut positions = positions.borrow_mut();
8694 let known = positions.as_mut()?;
8695 if let Some(found) = known.iter().find(|index| index.describes(source)) {
8696 return Some(found.clone());
8697 }
8698 let built = std::rc::Rc::new(SourcePositions::new(source));
8699 known.push(built.clone());
8700 Some(built)
8701 })
8702}
8703
8704fn utf16_offset(source: &str, offset: usize) -> usize {
8705 match indexed(source) {
8706 Some(index) => {
8707 let (line, column) = index.locate(source, offset);
8708 index.utf16_before_line[line] + column
8709 }
8710 None => source[..offset].encode_utf16().count(),
8711 }
8712}
8713
8714pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
8715 if let Some(index) = indexed(source) {
8716 let (line, column) = index.locate(source, offset);
8717 return (line + 1, column + 1);
8718 }
8719 let prefix = &source[..offset];
8720 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
8721 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
8722 let column = source[line_start..offset].encode_utf16().count() + 1;
8723 (line, column)
8724}
8725
8726fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
8727 let start = utf16_offset(source, span.start as usize);
8728 let end = utf16_offset(source, span.end as usize);
8729 let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
8730 let mut id = String::with_capacity(16);
8731 for byte in &digest[..8] {
8732 write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
8733 }
8734 id
8735}
8736
8737#[cfg(test)]
8738mod tests {
8739 use super::*;
8740
8741 #[test]
8742 fn line_index_lookups_agree_with_walking_the_line() {
8743 let long = "é🚀x".repeat(700);
8744 let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
8745 let index = Utf16LineIndex::new(&source);
8746 for (line, text) in source.split_inclusive('\n').enumerate() {
8747 let start = index.starts[line];
8748 let width: usize = text.chars().map(char::len_utf16).sum();
8749 for column in 0..=width + 2 {
8750 assert_eq!(
8751 index.byte_offset(line, column),
8752 start + utf16_col_to_byte(text, column),
8753 "line {line} column {column}"
8754 );
8755 }
8756 for (offset, _) in text.char_indices() {
8757 let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
8758 assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
8759 }
8760 }
8761 }
8762
8763 #[test]
8764 fn indexed_positions_agree_with_counting_from_the_top() {
8765 let source = &format!(
8767 "const s = 'naïve 🚀';\n\nexport function f(a) {{\n return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
8768 "é🚀x".repeat(400)
8769 );
8770 let source = source.as_str();
8771 let counted: Vec<_> = source
8772 .char_indices()
8773 .map(|(offset, _)| {
8774 (
8775 line_and_utf16_column(source, offset),
8776 stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
8777 )
8778 })
8779 .collect();
8780 let _positions = PositionScope::enter();
8781 for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
8782 assert_eq!(
8783 line_and_utf16_column(source, offset),
8784 position,
8785 "offset {offset}"
8786 );
8787 assert_eq!(
8788 stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
8789 id
8790 );
8791 }
8792 }
8793
8794 #[test]
8795 fn classic_scripts_share_application_globals_but_not_probe_bindings() {
8796 let a = instrument_direct_candidate(
8800 "var count = 0; function increment() { if (count >= 0) { count += 1; } }",
8801 "public/a.js",
8802 )
8803 .unwrap();
8804 let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
8805 .unwrap();
8806 let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
8807 let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
8808 let script = format!(
8809 "const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
8810 vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
8811 if (ctx.observed !== 1) throw Error('shared globals changed');\
8812 vm.runInContext({}, ctx);\
8813 if (ctx.observed !== 2) throw Error('a repeated script changed');",
8814 serde_json::to_string(runtime).unwrap(),
8815 serde_json::to_string(&a.code).unwrap(),
8816 serde_json::to_string(&b.code).unwrap(),
8817 serde_json::to_string(&b.code).unwrap()
8818 );
8819 let mut node = std::process::Command::new("node")
8822 .arg("-")
8823 .stdin(std::process::Stdio::piped())
8824 .stdout(std::process::Stdio::piped())
8825 .stderr(std::process::Stdio::piped())
8826 .spawn()
8827 .expect("Node.js for the JavaScript regression");
8828 std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
8829 let result = node.wait_with_output().unwrap();
8830 assert!(
8831 result.status.success(),
8832 "{}",
8833 String::from_utf8_lossy(&result.stderr)
8834 );
8835 }
8836
8837 #[test]
8838 fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
8839 for file in [
8842 "src/index.js",
8843 "src/index.jsx",
8844 "src/index.mjs",
8845 "src/index.cjs",
8846 "src/index.tsx",
8847 ] {
8848 let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
8849 let output = instrument_candidate(source, file).unwrap();
8850 assert!(!output.points.is_empty(), "{file}");
8851 assert!(output.code.contains("<div>"), "{file}");
8852 }
8853 assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
8855 for file in ["a.js", "a.cjs", "a.mjs"] {
8857 let before = SourceType::from_path(file).unwrap();
8858 let after = project_source_type(Path::new(file)).unwrap();
8859 assert_eq!(after.module_kind(), before.module_kind(), "{file}");
8860 assert!(after.is_jsx(), "{file}");
8861 }
8862 }
8863
8864 const SOURCE: &str =
8865 "export function decide(a,b,c) {\n if ((a && b) || !c) return 1;\n return 0;\n}\n";
8866
8867 #[test]
8868 fn matches_the_frozen_if_decision_manifest_exactly() {
8869 let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
8870 assert!(!output.complete);
8871 assert_eq!(
8872 output.decisions,
8873 vec![CandidateDecision {
8874 id: "2f65989a5782c5bd".to_string(),
8875 file: "app/decide.ts".to_string(),
8876 line: 2,
8877 column: 7,
8878 source: "(a && b) || !c".to_string(),
8879 conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
8880 kind: "if".to_string(),
8881 }]
8882 );
8883 }
8884
8885 #[test]
8886 fn codegen_output_reparses_for_typescript_and_tsx() {
8887 for (file, source) in [
8888 (
8889 "component.tsx",
8890 "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
8891 ),
8892 ("module.ts", SOURCE),
8893 ] {
8894 let output = analyze_candidate(source, file).unwrap();
8895 let map = output.map.as_ref().expect("candidate source map");
8896 assert_eq!(map["version"], 3);
8897 assert_eq!(map["sources"][0], file);
8898 assert_eq!(map["sourcesContent"][0], source);
8899 assert!(
8900 map["mappings"]
8901 .as_str()
8902 .is_some_and(|value| !value.is_empty())
8903 );
8904 let allocator = Allocator::default();
8905 let source_type = SourceType::from_path(file).unwrap();
8906 let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
8907 assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
8908 }
8909 }
8910
8911 #[test]
8912 fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
8913 for file in ["src/module.mjs", "src/script.cjs"] {
8914 let output = instrument_direct_candidate(
8915 "export const selected = value => value ? 1 : 0;",
8916 file,
8917 )
8918 .unwrap();
8919 assert!(
8920 output
8921 .code
8922 .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
8923 );
8924 assert!(!output.code.contains("virtual:supercov-runtime"));
8925 assert!(!output.code.contains("globalThis.__supercovRuntime"));
8926 }
8927 }
8928
8929 #[test]
8930 fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
8931 let source = concat!(
8932 "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
8933 "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
8934 "ordinaryHelper();\n",
8935 );
8936 let output = instrument_node_assertion_phases_with_runtime_hooks(
8937 source,
8938 "tests/runner.mjs",
8939 &[],
8940 Some("../.supercov/launchSupervisor.mjs"),
8941 )
8942 .unwrap();
8943 assert_eq!(output.assertions, 0);
8944 assert_eq!(output.capability_imports, 1);
8945 assert!(output.code.contains("wrapImportedCapability"));
8946 assert!(output.code.contains("__supercovRawImageBuilder"));
8947 assert!(output.code.contains("const ImageBuilder"));
8948 assert!(!output.code.contains("__supercovRawordinaryHelper"));
8949 assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
8950 }
8951
8952 #[test]
8953 fn capability_imports_accept_a_computed_guest_mount_path() {
8954 let source = concat!(
8955 "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
8956 "const guestRoot = resolve(tmpdir(), 'workspace');\n",
8957 "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
8958 "ordinaryHelper();\n",
8959 );
8960 let output = instrument_node_assertion_phases_with_runtime_hooks(
8961 source,
8962 "tests/runner.mjs",
8963 &[],
8964 Some("../.supercov/launchSupervisor.mjs"),
8965 )
8966 .unwrap();
8967 assert_eq!(output.capability_imports, 1);
8968 assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
8969 assert!(!output.code.contains("__supercovRawordinaryHelper"));
8970 }
8971
8972 #[test]
8973 fn capability_import_selection_follows_a_mapping_variable() {
8974 let source = concat!(
8975 "import { launch, unrelated } from './sdk.mjs';\n",
8976 "run();\n",
8977 "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
8978 "function run() { launch(options); unrelated(); }\n",
8979 );
8980 let output = instrument_node_assertion_phases_with_runtime_hooks(
8981 source,
8982 "tests/runner.mjs",
8983 &[],
8984 Some("../.supercov/launchSupervisor.mjs"),
8985 )
8986 .unwrap();
8987 assert_eq!(output.capability_imports, 1);
8988 assert!(output.code.contains("__supercovRawlaunch"));
8989 assert!(!output.code.contains("__supercovRawunrelated"));
8990 }
8991
8992 #[test]
8993 fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
8994 let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
8995 let output = instrument_node_assertion_phases_with_runtime_hooks(
8996 source,
8997 "src/main.mjs",
8998 &[],
8999 Some("../.supercov/launchSupervisor.mjs"),
9000 )
9001 .unwrap();
9002 assert_eq!(output.code, source);
9003 assert_eq!(output.capability_imports, 0);
9004 }
9005
9006 #[test]
9007 fn ordinary_mount_language_does_not_enable_capability_proxies() {
9008 let source = concat!(
9009 "import { render } from '@testing-library/react';\n",
9010 "it('mounts into a host element', () => render(<main />));\n",
9011 );
9012 let output = instrument_node_assertion_phases_with_runtime_hooks(
9013 source,
9014 "tests/component.test.tsx",
9015 &[],
9016 Some("../.supercov/launchSupervisor.mjs"),
9017 )
9018 .unwrap();
9019 assert_eq!(output.capability_imports, 0);
9020 assert!(!output.code.contains("wrapImportedCapability"));
9021 }
9022
9023 #[test]
9024 fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
9025 let source = concat!(
9026 "import { test, expect } from '@example/test-admin';\n",
9027 "test('visual snapshot', async ({ page }) => {\n",
9028 " expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
9029 "});\n",
9030 );
9031 let output = instrument_node_assertion_phases_with_runtime_hooks(
9032 source,
9033 "tests/visual.spec.ts",
9034 &[],
9035 Some("../.supercov/launchSupervisor.mjs"),
9036 )
9037 .unwrap();
9038 assert_eq!(output.capability_imports, 0);
9039 assert!(!output.code.contains("wrapImportedCapability"));
9040 }
9041
9042 #[test]
9043 fn capability_rewrite_excludes_runtime_and_type_only_imports() {
9044 let source = concat!(
9045 "import type { Machine } from './types.ts';\n",
9046 "import { test } from 'node:test';\n",
9047 "import { runtime } from '../.supercov/runtime.mjs';\n",
9048 "console.log({ machine: true, test, runtime });\n",
9049 );
9050 let output = instrument_node_assertion_phases_with_runtime_hooks(
9051 source,
9052 "tests/runner.ts",
9053 &[],
9054 Some("../.supercov/launchSupervisor.mjs"),
9055 )
9056 .unwrap();
9057 assert_eq!(output.code, source);
9058 assert_eq!(output.capability_imports, 0);
9059 }
9060
9061 #[test]
9062 fn native_assertion_arguments_execute_inside_the_assertion_phase() {
9063 let source = concat!(
9064 "import assert from 'node:assert/strict';\n",
9065 "assert.equal(value(), 1);\n",
9066 );
9067 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9068 assert_eq!(output.assertions, 1);
9069 assert!(output.code.contains("withNodeAssertionPhase"));
9070 assert!(output.code.contains("node:assert/strict.equal"));
9071 assert!(output.code.contains("tests/value.test.mjs:2:1"));
9072 assert!(output.code.contains("assert.equal(value(), 1)"));
9073 }
9074
9075 #[test]
9076 fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
9077 let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
9078 let output = instrument_node_assertion_phases_with_runtime_imports(
9079 source,
9080 "tests/value.test.mjs",
9081 &[],
9082 None,
9083 Some("../.supercov/node_modules/bootstrap.cjs"),
9084 )
9085 .unwrap();
9086 assert_eq!(output.assertions, 1);
9087 let lines = output.code.lines().collect::<Vec<_>>();
9089 assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
9090 assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
9091 assert!(lines[0].ends_with("import assert from 'node:assert';"));
9092 assert!(lines[1].contains("withNodeAssertionPhase"));
9093 let allocator = Allocator::default();
9094 assert!(
9095 Parser::new(&allocator, &output.code, SourceType::mjs())
9096 .parse()
9097 .errors
9098 .is_empty()
9099 );
9100 }
9101
9102 #[test]
9103 fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
9104 let source = concat!(
9105 "import assert from 'node:assert/strict';\n",
9106 "export async function check() { assert.equal(await value(), 1); }\n",
9107 );
9108 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9109 assert_eq!(output.assertions, 1);
9110 assert!(output.code.contains("bindNodeAssertionPhase"));
9111 assert!(output.code.contains("await value()"));
9112 let allocator = Allocator::default();
9113 assert!(
9114 Parser::new(&allocator, &output.code, SourceType::mjs())
9115 .parse()
9116 .errors
9117 .is_empty()
9118 );
9119 }
9120
9121 #[test]
9122 fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
9123 let source = concat!(
9124 "import assert from 'node:assert/strict';\n",
9125 "export async function check(user?: string) {\n",
9126 " assert.ok(user, await value());\n",
9127 " assert\n",
9128 " .equal(await value(), 1)\n",
9129 "}\n",
9130 );
9131 let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
9132 assert_eq!(output.assertions, 2);
9133 let code = &output.code;
9134 let lines: Vec<&str> = code.lines().collect();
9135 assert!(
9136 lines[2].starts_with(
9137 " { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
9138 ),
9139 "{code}"
9140 );
9141 assert!(
9142 lines[2]
9143 .ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
9144 "{code}"
9145 );
9146 assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
9147 assert_eq!(
9148 lines[4], " \"equal\"); __supercovAssertionPhase(await value(), 1) }",
9149 "{code}"
9150 );
9151 assert_eq!(lines[5], "}", "{code}");
9152 let allocator = Allocator::default();
9153 assert!(
9154 Parser::new(&allocator, code, SourceType::ts())
9155 .parse()
9156 .errors
9157 .is_empty()
9158 );
9159 }
9160
9161 #[test]
9162 fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
9163 let source = concat!(
9164 "import { expect, test } from '@playwright/test';\n",
9165 "test('value', async () => { expect(await value()).toBe(1); });\n",
9166 );
9167 let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
9168 assert_eq!(output.assertions, 1);
9169 assert!(output.code.contains("bindNodeAssertionPhase"));
9170 assert!(output.code.contains("await value()"));
9171 let allocator = Allocator::default();
9172 assert!(
9173 Parser::new(&allocator, &output.code, SourceType::mjs())
9174 .parse()
9175 .errors
9176 .is_empty()
9177 );
9178 }
9179
9180 #[test]
9181 fn native_commonjs_assertion_bindings_are_attributed() {
9182 let source = concat!(
9183 "const assert = require('node:assert/strict');\n",
9184 "const { ok: verify } = require('assert');\n",
9185 "assert.equal(value(), 1);\n",
9186 "verify(other());\n",
9187 );
9188 let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
9189 assert_eq!(output.assertions, 2);
9190 assert!(output.code.contains("node:assert/strict.equal"));
9191 assert!(output.code.contains("node:assert.ok"));
9192 }
9193
9194 #[test]
9195 fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
9196 let source = concat!(
9197 "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
9198 "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
9199 );
9200 let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
9201 assert_eq!(output.assertions, 1);
9202 assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
9203 }
9204
9205 #[test]
9206 fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
9207 let source = concat!(
9208 "import assert from 'node:assert/strict';\n",
9209 "function unrelated(assert) { assert.equal(effect(), 1); }\n",
9210 "assert.equal(value(), 1);\n",
9211 );
9212 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9213 assert_eq!(output.assertions, 1);
9214 assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
9215 }
9216
9217 #[test]
9218 fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
9219 let source = concat!(
9220 "import test from 'node:test';\n",
9221 "import { expect as verify } from 'expect-library';\n",
9222 "test('value', () => verify(value()).not.toEqual(1));\n",
9223 );
9224 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9225 assert_eq!(output.assertions, 1);
9226 assert!(output.code.contains("expect.not.toEqual"));
9227 assert!(output.code.contains("verify(value()).not.toEqual(1)"));
9228 }
9229
9230 #[test]
9231 fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
9232 let source = concat!(
9233 "import { expect, test } from 'vitest';\n",
9234 "test('value', () => expect(value()).toBe(1));\n",
9235 );
9236 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
9237 assert_eq!(output.assertions, 1);
9238 assert!(output.code.contains("expect.toBe"));
9239 }
9240
9241 #[test]
9242 fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
9243 for (call, operation) in [
9248 (
9249 "expect.element(button).toHaveAccessibleName('Copy')",
9250 "expect.element.toHaveAccessibleName",
9251 ),
9252 ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
9253 ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
9254 ] {
9255 let source = format!(
9256 "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
9257 );
9258 let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
9259 assert_eq!(output.assertions, 1, "{call}");
9260 assert!(output.code.contains(operation), "{call} -> {operation}");
9261 }
9262 }
9263
9264 #[test]
9265 fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
9266 let source = concat!(
9271 "export function Button({ label, copied }) {\n",
9272 " return (\n",
9273 " <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
9274 " {format(copied)}\n",
9275 " </button>\n",
9276 " );\n",
9277 "}\n",
9278 );
9279 let output = instrument_candidate(source, "src/button.tsx").unwrap();
9280 let rendered = output
9281 .points
9282 .iter()
9283 .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
9284 .collect::<Vec<_>>();
9285 assert_eq!(rendered.len(), 2, "{:?}", output.points);
9288 assert!(
9289 rendered
9290 .iter()
9291 .all(|point| point.source == "format(copied)")
9292 );
9293 assert_eq!(rendered[0].line, 3);
9294 assert_eq!(rendered[1].line, 4);
9295 assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
9296 assert!(!output.code.contains("title={__supercovRenderedValueV2"));
9299 assert!(!output.code.contains("), format(copied)}"));
9302 assert!(output.code.contains(", format(copied))}"));
9303 }
9304
9305 #[test]
9306 fn instrumented_jsx_stays_parseable_through_every_container_shape() {
9307 let source = concat!(
9311 "export function Panel({ items, user, error, tone }) {\n",
9312 " return (\n",
9313 " <>\n",
9314 " <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
9315 " <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
9316 " {error ? <Alert text={message(error)} /> : null}\n",
9317 " {items.map((item) => (\n",
9318 " <Item key={item.id} label={format(item)} />\n",
9319 " ))}\n",
9320 " {count(items) > 0 && <Footer total={sum(items)} />}\n",
9321 " </div>\n",
9322 " </>\n",
9323 " );\n",
9324 "}\n",
9325 );
9326 let output = instrument_candidate(source, "src/panel.tsx").unwrap();
9327 instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
9329 assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
9331 let rendered = output
9332 .points
9333 .iter()
9334 .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
9335 .map(|point| point.source.as_str())
9336 .collect::<Vec<_>>();
9337 assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
9338 assert!(
9339 rendered.contains(&"cx(\'a\', tone && \'b\')"),
9340 "{rendered:?}"
9341 );
9342 assert!(rendered.contains(&"format(item)"), "{rendered:?}");
9343 assert!(rendered.contains(&"message(error)"), "{rendered:?}");
9344 assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
9347 assert!(!rendered.contains(&"item.id"), "{rendered:?}");
9348 }
9349
9350 #[test]
9351 fn jest_global_expect_is_attributed_next_to_its_test_globals() {
9352 let source = "test('value', () => expect(value()).toBe(1));\n";
9356 let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
9357 assert_eq!(output.assertions, 1);
9358 assert!(output.code.contains("expect.toBe"));
9359 let plain = "const ok = expect(value()).toBe(1);\n";
9361 let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
9362 assert_eq!(output.assertions, 0);
9363 assert_eq!(output.code, plain);
9364 }
9365
9366 #[test]
9367 fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
9368 let source = concat!(
9369 "import { expect, test } from '@playwright/test';\n",
9370 "test('value', () => expect(value()).toBe(1));\n",
9371 );
9372 let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
9373 assert_eq!(output.assertions, 1);
9374 assert!(output.code.contains("expect.toBe"));
9375 }
9376
9377 #[test]
9378 fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
9379 let source = concat!(
9380 "import { browserTest, expect } from '@acme/browser-fixtures';\n",
9381 "browserTest('value', () => expect(value()).toBe(1));\n",
9382 );
9383 let output = instrument_node_assertion_phases_with_expect_modules(
9384 source,
9385 "tests/value.spec.mjs",
9386 &["@acme/browser-fixtures".into()],
9387 )
9388 .unwrap();
9389 assert_eq!(output.assertions, 1);
9390 assert!(output.code.contains("expect.toBe"));
9391 }
9392
9393 #[test]
9394 fn preserves_comment_payloads_byte_for_byte() {
9395 let comment = "/*---\ndescription: >\n nested indentation is data\ninfo: |\n first\n second\n---*/";
9396 let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
9397 let output = instrument_candidate(&source, "app/comments.js").unwrap();
9398 assert!(output.code.contains(comment), "{}", output.code);
9399 }
9400
9401 #[test]
9402 fn source_map_destinations_follow_restored_comments() {
9403 let source = "function run() {\n // first omitted comment\n // second omitted comment\n return 1;\n}\n";
9404 let output = analyze_candidate(source, "app/comment-map.js").unwrap();
9405 let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
9406 let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
9407 let return_token = map
9408 .get_tokens()
9409 .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
9410 .expect("return token mapping");
9411 let offset = Utf16LineIndex::new(&output.code).byte_offset(
9412 return_token.get_dst_line() as usize,
9413 return_token.get_dst_col() as usize,
9414 );
9415 assert!(
9416 output.code[offset..].starts_with("return"),
9417 "{}",
9418 output.code
9419 );
9420 }
9421
9422 #[test]
9423 fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
9424 for source in [
9435 "// </script>\nexport const x = 1;\n",
9436 "// </script\nexport const x = 1;\n",
9437 "// </SCRIPT>\nexport const x = 1;\n",
9438 "// see the </script> tag here\nexport const x = 1;\n",
9439 "/* block </script> here */\nexport const x = 1;\n",
9440 "/** jsdoc </script> */\nexport const x = 1;\n",
9441 "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
9443 ] {
9444 let output = instrument_candidate(source, "src/probe.ts")
9445 .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
9446 let original = source.lines().next().expect("a comment");
9447 assert!(
9448 output.code.contains(original),
9449 "comment was not restored verbatim: {original:?} missing from {:?}",
9450 output.code
9451 );
9452 }
9453 let written = "// author wrote <\\/script>\nexport const x = 1;\n";
9456 let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
9457 assert!(
9458 output.code.contains("// author wrote <\\/script>"),
9459 "{:?}",
9460 output.code
9461 );
9462 assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
9465 assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
9466 assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
9467 }
9468
9469 #[test]
9470 fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
9471 let error = CandidateError::CommentPreservation {
9476 expected: 116,
9477 actual: 112,
9478 detail:
9479 "no source comment matches the generated comment \"// x\" (matched 97 before it)"
9480 .to_owned(),
9481 };
9482 let rendered = format!("{error:?}");
9483 assert!(rendered.contains("no source comment matches"), "{rendered}");
9484 assert!(rendered.contains("matched 97 before it"), "{rendered}");
9485 }
9486
9487 #[test]
9488 fn restored_comments_never_split_a_keyword_from_its_argument() {
9489 let source = concat!(
9497 "export const A = ({ open }) => {\n",
9498 " return (\n",
9499 " // leading comment before JSX\n",
9500 " <div aria-expanded={open}>a</div>\n",
9501 " )\n",
9502 "}\n",
9503 "export const B = ({ open }) => (\n",
9504 " // comment in an expression body\n",
9505 " <div aria-expanded={open}>b</div>\n",
9506 ")\n",
9507 "export function C(x) {\n",
9508 " return ( // inline line comment\n",
9509 " x + 1\n",
9510 " )\n",
9511 "}\n",
9512 "export function D(x) {\n",
9513 " throw (\n",
9514 " /* block\n comment */\n",
9515 " new Error(String(x))\n",
9516 " )\n",
9517 "}\n",
9518 "export function E(x) {\n",
9519 " return ( /* inline block */ x + 2 )\n",
9520 "}\n",
9521 );
9522 let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
9523 for keyword in ["return ", "throw "] {
9524 for (index, _) in output.code.match_indices(keyword) {
9525 let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
9526 let block_with_line_break = rest.starts_with("/*")
9527 && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
9528 assert!(
9529 !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
9530 "`{keyword}` is separated from its argument:\n{}",
9531 output.code
9532 );
9533 }
9534 }
9535 for comment in [
9536 "// leading comment before JSX",
9537 "// comment in an expression body",
9538 "// inline line comment",
9539 "/* block\n comment */",
9540 "/* inline block */",
9541 ] {
9542 assert!(
9543 output.code.contains(comment),
9544 "{comment} was lost:\n{}",
9545 output.code
9546 );
9547 }
9548 assert!(
9549 output
9550 .code
9551 .contains("return <div aria-expanded={open}>a</div>"),
9552 "{}",
9553 output.code
9554 );
9555 assert!(
9556 output
9557 .code
9558 .contains("return <div aria-expanded={open}>b</div>"),
9559 "{}",
9560 output.code
9561 );
9562 let allocator = Allocator::default();
9564 let reparsed = Parser::new(
9565 &allocator,
9566 &output.code,
9567 SourceType::from_path("app/components/List.tsx").unwrap(),
9568 )
9569 .parse();
9570 assert!(
9571 reparsed.errors.is_empty(),
9572 "{:?}\n{}",
9573 reparsed.errors,
9574 output.code
9575 );
9576 }
9577
9578 #[test]
9579 fn preserves_reference_order_across_every_control_decision_kind() {
9580 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}";
9581 let output = instrument_candidate(source, "app/control.js").unwrap();
9582 assert_eq!(
9583 output
9584 .decisions
9585 .iter()
9586 .map(|decision| decision.kind.as_str())
9587 .collect::<Vec<_>>(),
9588 vec!["ternary", "while", "do-while", "for", "if"]
9589 );
9590 }
9591
9592 #[test]
9593 fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
9594 let entered = |source: &str| {
9598 let output = analyze_candidate(source, "src/loops.ts").unwrap();
9599 output
9600 .branches
9601 .iter()
9602 .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
9603 .count()
9604 };
9605 for source in [
9607 "export function f(g) { for (const x of [1, 2]) g(x); }\n",
9608 "export function f(g) { for (const x of [1]) g(x); }\n",
9609 "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
9610 "export function f(g) { for (const x of [,]) g(x); }\n",
9612 "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
9614 ] {
9615 assert_eq!(entered(source), 0, "should be folded: {source}");
9616 }
9617 for source in [
9619 "export function f(g, ys) { for (const x of ys) g(x); }\n",
9620 "export function f(g) { for (const x of []) g(x); }\n",
9621 "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
9622 "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
9623 "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
9625 "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
9626 "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
9627 concat!(
9629 "const xs = [1, 2];\n",
9630 "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
9631 ),
9632 "export function f(g) { for (const k in [1, 2]) g(k); }\n",
9634 ] {
9635 assert_eq!(entered(source), 1, "should be kept: {source}");
9636 }
9637 let output = analyze_candidate(
9639 "export function f(g) { for (const x of [1, 2]) g(x); }\n",
9640 "src/loops.ts",
9641 )
9642 .unwrap();
9643 assert!(
9644 output
9645 .points
9646 .iter()
9647 .any(|point| point.source.contains("g(x)")),
9648 "the body must still be an obligation"
9649 );
9650 }
9651
9652 #[test]
9653 fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
9654 let source = concat!(
9655 "export function run(value) {\n",
9656 " while (true) { break; }\n",
9657 " do { value += 1; } while (false);\n",
9658 " if (false) value += 2;\n",
9659 " if (value || true) value += 3;\n",
9660 " if (value && false) value += 4;\n",
9661 " if (value) value += 5;\n",
9662 " return value;\n",
9663 "}\n",
9664 );
9665 let output = analyze_candidate(source, "src/constants.js").unwrap();
9666 assert_eq!(output.decisions.len(), 1);
9667 assert_eq!(output.decisions[0].source, "value");
9668
9669 let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
9670 assert_eq!(instrumented.decisions.len(), 1);
9671 assert_eq!(instrumented.decisions[0].source, "value");
9672 }
9673
9674 #[test]
9675 fn leaves_compile_time_style_macro_arguments_as_source() {
9676 let source = concat!(
9679 "import * as stylex from '@stylexjs/stylex';\n",
9680 "const styles = stylex.create({\n",
9681 " container: { display: 'flex' },\n",
9682 " paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
9683 " tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
9684 "});\n",
9685 "export function render(spacing: number) {\n",
9686 " return stylex.props(styles.container, styles.paddingTop(spacing));\n",
9687 "}\n",
9688 );
9689 let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
9690 assert!(
9691 output.code.contains("=> ({ paddingTop: `${spacing}px` })")
9692 || output
9693 .code
9694 .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
9695 "dynamic style arrow must stay an expression body:\n{}",
9696 output.code
9697 );
9698 assert!(
9699 !output
9700 .decisions
9701 .iter()
9702 .any(|decision| decision.source.contains("dark")),
9703 "no decision may be collected inside the macro argument"
9704 );
9705 let function_points = output
9706 .points
9707 .iter()
9708 .filter(|point| point.kind == "function")
9709 .map(|point| point.source.as_str())
9710 .collect::<Vec<_>>();
9711 assert!(
9712 function_points
9713 .iter()
9714 .all(|source| source.contains("render")),
9715 "only the real function may carry a function point: {function_points:?}"
9716 );
9717 assert!(
9718 output.limitations.is_empty(),
9719 "compiled-away code is not a limitation: {:?}",
9720 output.limitations
9721 );
9722 }
9723
9724 #[test]
9725 fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
9726 let source = concat!(
9727 "declare global {\n",
9728 " var Beacon: undefined | ((action: string) => void);\n",
9729 "}\n",
9730 "declare module 'virtual:feature' {\n",
9731 " export const enabled: boolean;\n",
9732 "}\n",
9733 "declare const buildOnly: string;\n",
9734 "export const live = 1;\n",
9735 );
9736 let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
9737 let statements = output
9738 .points
9739 .iter()
9740 .filter(|point| point.kind == "statement")
9741 .map(|point| point.source.as_str())
9742 .collect::<Vec<_>>();
9743 assert_eq!(statements, ["const live = 1;"]);
9744 assert!(output.code.contains("const live = 1"));
9745 }
9746
9747 #[test]
9748 fn exposes_the_complete_probe_v2_instrumenter_contract() {
9749 let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
9750 assert!(output.complete);
9751 assert!(output.limitations.is_empty());
9752 assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
9753 let runtime = output.runtime.expect("candidate runtime binding");
9754 assert!(output.code.contains(&runtime.mcdc_end_v2));
9755 assert!(output.code.contains("_supercovMcdcFrame"));
9756 assert!(output.code.contains("_supercovMcdcResult"));
9757 assert!(output.code.contains("+= 1 + +!!_supercovMcdcValue"));
9758 assert_eq!(output.decisions.len(), 1);
9759 assert!(output.points.iter().any(|point| point.kind == "statement"));
9760 assert!(output.points.iter().any(|point| point.kind == "function"));
9761
9762 let allocator = Allocator::default();
9763 let reparsed = Parser::new(
9764 &allocator,
9765 &output.code,
9766 SourceType::from_path("app/decide.ts").unwrap(),
9767 )
9768 .parse();
9769 assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
9770 }
9771
9772 #[test]
9773 fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
9774 let source = concat!(
9775 "import type { Session } from './types.ts';\n",
9776 "import { type Row } from './rows.ts';\n",
9777 "import './register.ts';\n",
9778 "const value = 1;\n",
9779 );
9780 let output = instrument_candidate(source, "app/imports.ts").unwrap();
9781 let statement_lines = output
9782 .points
9783 .iter()
9784 .filter(|point| point.kind == "statement")
9785 .map(|point| point.line)
9786 .collect::<Vec<_>>();
9787 assert_eq!(statement_lines, vec![2, 3, 4]);
9788 }
9789
9790 #[test]
9791 fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
9792 let source = concat!(
9793 "import { Logger } from './types.js';\n",
9794 "import DefaultType from './default.js';\n",
9795 "import * as Types from './namespace.js';\n",
9796 "import { type Inline } from './inline.js';\n",
9797 "import { run, type Config } from './mixed.js';\n",
9798 "import './register.js';\n",
9799 "import {} from './empty.js';\n",
9800 "import { unused } from './unused.js';\n",
9801 "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
9802 );
9803 let preserved =
9804 instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true, false)
9805 .unwrap();
9806 let erased =
9807 instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true, true)
9808 .unwrap();
9809 assert_eq!(
9810 erased
9811 .excluded_statements
9812 .iter()
9813 .map(|p| p.line)
9814 .collect::<Vec<_>>(),
9815 vec![1, 2, 3, 4]
9816 );
9817 let excluded = erased
9818 .excluded_statements
9819 .iter()
9820 .map(|p| &p.id)
9821 .collect::<HashSet<_>>();
9822 let expected = preserved
9823 .points
9824 .iter()
9825 .filter(|p| !excluded.contains(&p.id))
9826 .collect::<Vec<_>>();
9827 assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
9828 for line in [5, 6, 7, 8] {
9829 assert!(erased.points.iter().any(|p| p.line == line));
9830 }
9831 let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
9833 let mixed =
9834 instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true, true)
9835 .unwrap();
9836 assert!(mixed.excluded_statements.is_empty());
9837 }
9838
9839 type FirstMapping = Option<Option<(u32, u32)>>;
9842
9843 fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
9845 let map = oxc_sourcemap::SourceMap::from_json_string(
9846 &output
9847 .map
9848 .as_ref()
9849 .expect("instrumented copy has a map")
9850 .to_string(),
9851 )
9852 .unwrap();
9853 let tokens = map.get_tokens().collect::<Vec<_>>();
9854 output
9855 .code
9856 .split('\n')
9857 .enumerate()
9858 .map(|(line, text)| {
9859 let first = tokens
9860 .iter()
9861 .find(|token| token.get_dst_line() == line as u32)
9862 .map(|token| {
9863 token
9864 .get_source_id()
9865 .map(|_| (token.get_src_line(), token.get_src_col()))
9866 });
9867 (text.trim().to_string(), first)
9868 })
9869 .collect()
9870 }
9871
9872 #[test]
9873 fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
9874 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";
9875 let output = instrument_candidate(source, "app/f.js").unwrap();
9876 let lines = first_mappings(&output);
9877 let runtime = output.runtime.as_ref().unwrap();
9878 for (text, first) in &lines {
9879 if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
9882 && let Some(first) = first
9883 {
9884 assert_eq!(*first, None, "{text}");
9885 }
9886 }
9887 let at = |needle: &str| {
9888 lines
9889 .iter()
9890 .find(|(text, _)| text.starts_with(needle))
9891 .unwrap_or_else(|| panic!("{needle} in {}", output.code))
9892 .1
9893 };
9894 assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
9897 assert_eq!(at("continue;"), Some(Some((4, 25))));
9898 assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
9899 assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
9902 let program = lines
9906 .iter()
9907 .position(|(text, _)| text.starts_with("function f("))
9908 .unwrap();
9909 for (text, first) in &lines[program..] {
9910 if text.starts_with('}') {
9911 assert!(first.is_some(), "{text} in {}", output.code);
9912 }
9913 }
9914 }
9915
9916 #[test]
9917 fn nested_selections_stay_one_tree_of_the_programs_operators() {
9918 let source =
9919 "export const ok = (total, name, force) => (total > 0 && name !== 'x') || !!force;\n";
9920 let output = instrument_candidate(source, "app/ok.js").unwrap();
9921 let runtime = output.runtime.expect("candidate runtime binding");
9922 assert_eq!(
9925 output
9926 .code
9927 .matches(&format!("{}(", runtime.select_path_v2))
9928 .count(),
9929 2
9930 );
9931 assert_eq!(
9932 output
9933 .code
9934 .matches(&format!("{}(", runtime.select_right_v2))
9935 .count(),
9936 1
9937 );
9938 assert!(
9939 !output
9940 .code
9941 .contains(&format!("{}(", runtime.select_short_v2)),
9942 "{}",
9943 output.code
9944 );
9945 let allocator = Allocator::default();
9946 let reparsed = Parser::new(&allocator, &output.code, SourceType::mjs()).parse();
9947 assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
9948 let text = &output.code;
9949 let or = text
9950 .find(") || ")
9951 .expect("the `||` stays between the two sides");
9952 assert!(text[..or].contains(" && "), "{text}");
9953 }
9954
9955 #[test]
9956 fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
9957 let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
9958 for file in ["app/fill.js", "app/fill.ts"] {
9959 let output = instrument_candidate(source, file).unwrap();
9960 let runtime = output.runtime.expect("candidate runtime binding");
9961 assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
9964 assert!(output.code.contains("||= "), "{}", output.code);
9965 assert!(output.code.contains("??= "), "{}", output.code);
9966 assert!(output.code.contains("&&= "), "{}", output.code);
9967 assert_eq!(
9968 output
9969 .code
9970 .matches(&format!("{}(", runtime.select_assign_end_v2))
9971 .count(),
9972 3
9973 );
9974 assert_eq!(
9975 output
9976 .code
9977 .matches(&format!("{}(", runtime.select_named_right_v2))
9978 .count(),
9979 1
9980 );
9981 let allocator = Allocator::default();
9982 let reparsed = Parser::new(
9983 &allocator,
9984 &output.code,
9985 SourceType::from_path(file).unwrap(),
9986 )
9987 .parse();
9988 assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
9989 }
9990 }
9991
9992 #[test]
9993 fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
9994 let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
9995 let output = instrument_candidate(source, "app/collisions.js").unwrap();
9996 let runtime = output.runtime.expect("candidate runtime binding");
9997 assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
9998 assert!(!output.code.contains("let _supercovMcdcFrame1,"));
9999 }
10000
10001 #[test]
10002 fn instruments_wider_decisions_with_the_exact_v1_fallback() {
10003 let predicate = (0..33)
10004 .map(|index| format!("c{index}"))
10005 .collect::<Vec<_>>()
10006 .join(" && ");
10007 let source = format!("if ({predicate}) work();");
10008 let output = instrument_candidate(&source, "app/wide.js").unwrap();
10009 assert_eq!(output.decisions[0].conditions.len(), 33);
10010 let runtime = output.runtime.expect("candidate runtime binding");
10011 assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
10012 assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
10013 assert!(
10014 output
10015 .code
10016 .contains(&format!("{}(", runtime.mcdc_condition))
10017 );
10018 assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
10019 }
10020
10021 #[test]
10022 fn wraps_framework_request_exports_without_changing_the_public_api() {
10023 for (file, source, export_prefix) in [
10024 (
10025 "app/routes/example.ts",
10026 "export const loader = async ({ request }) => request.url;",
10027 "export const loader = ",
10028 ),
10029 (
10030 "app/routes/example.ts",
10031 "export async function action({ request }) { return request.method; }",
10032 "export const action = ",
10033 ),
10034 (
10035 "app/api/items/route.ts",
10036 "export function GET(request) { return Response.json({ url: request.url }); }",
10037 "export const GET = ",
10038 ),
10039 (
10040 "app/routes/example.ts",
10041 "export { generateAction as action } from './generateAction';",
10042 "export const action = ",
10043 ),
10044 (
10045 "app/entry.server.tsx",
10046 "export default async function handleRequest(request) { return request.url; }",
10047 "export default ",
10048 ),
10049 ] {
10050 let output = instrument_candidate(source, file).unwrap();
10051 let runtime = output.runtime.as_ref().expect("runtime binding");
10052 assert!(
10053 output
10054 .code
10055 .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
10056 "{file}: {}",
10057 output.code
10058 );
10059 assert!(
10060 output.code.contains(&format!(
10061 "withRequestPhase as {}",
10062 runtime.with_request_phase
10063 )),
10064 "{file}: {}",
10065 output.code
10066 );
10067 let allocator = Allocator::default();
10068 let reparsed = Parser::new(
10069 &allocator,
10070 &output.code,
10071 SourceType::from_path(file).unwrap(),
10072 )
10073 .parse();
10074 assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
10075 }
10076 }
10077
10078 #[test]
10079 fn parse_failures_are_explicit_and_never_claim_completeness() {
10080 assert!(matches!(
10081 analyze_candidate("if (", "broken.js"),
10082 Err(CandidateError::Parse(errors)) if !errors.is_empty()
10083 ));
10084 assert!(matches!(
10085 analyze_candidate("let value = 1", "unknown.extension"),
10086 Err(CandidateError::UnknownSourceType(_))
10087 ));
10088 }
10089}