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