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, ForInStatement, ForOfStatement, ForStatement, ForStatementLeft,
25 FormalParameter, FormalParameterKind, FormalParameters, Function, FunctionBody,
26 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, 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> {
349 source: &'s str,
350 starts: Vec<usize>,
351}
352
353impl<'s> Utf16LineIndex<'s> {
354 fn new(source: &'s str) -> Self {
355 let mut starts = Vec::with_capacity(source.lines().count() + 1);
356 starts.push(0);
357 starts.extend(
358 source
359 .char_indices()
360 .filter_map(|(offset, character)| (character == '\n').then_some(offset + 1)),
361 );
362 Self { source, starts }
363 }
364
365 fn byte_offset(&self, target_line: usize, target_utf16_col: usize) -> usize {
366 let Some(&start) = self.starts.get(target_line) else {
367 return self.source.len();
368 };
369 let end = self
370 .starts
371 .get(target_line + 1)
372 .copied()
373 .unwrap_or(self.source.len());
374 start + utf16_col_to_byte(&self.source[start..end], target_utf16_col)
375 }
376
377 fn line_col(&self, byte_offset: usize) -> (u32, u32) {
378 let byte_offset = byte_offset.min(self.source.len());
379 let line = self.starts.partition_point(|start| *start <= byte_offset) - 1;
380 let column = self.source[self.starts[line]..byte_offset]
381 .chars()
382 .map(char::len_utf16)
383 .sum::<usize>();
384 (line as u32, column as u32)
385 }
386}
387
388fn utf16_col_to_byte(line: &str, target_utf16_col: usize) -> usize {
389 let mut column = 0;
390 for (offset, character) in line.char_indices() {
391 if column >= target_utf16_col {
392 return offset;
393 }
394 column += character.len_utf16();
395 }
396 line.len()
397}
398
399fn shift_source_map(
400 map: oxc_sourcemap::SourceMap,
401 generated: &str,
402 restored: &str,
403 edits: &[(usize, usize, String)],
404) -> oxc_sourcemap::SourceMap {
405 let generated_lines = Utf16LineIndex::new(generated);
406 let restored_lines = Utf16LineIndex::new(restored);
407 let mut edit_index = 0;
408 let mut shift = 0isize;
409 let tokens = map
410 .get_tokens()
411 .map(|token| {
412 let original_offset = generated_lines
413 .byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
414 while let Some((start, end, replacement)) = edits.get(edit_index) {
415 if *end > original_offset {
416 break;
417 }
418 shift += replacement.len() as isize - (*end - *start) as isize;
419 edit_index += 1;
420 }
421 let shifted_offset = edits
422 .get(edit_index)
423 .filter(|(start, end, _)| *start <= original_offset && original_offset < *end)
424 .map_or_else(
425 || original_offset.saturating_add_signed(shift),
426 |(start, _, _)| start.saturating_add_signed(shift),
427 );
428 let (dst_line, dst_col) = restored_lines.line_col(shifted_offset);
429 oxc_sourcemap::Token::new(
430 dst_line,
431 dst_col,
432 token.get_src_line(),
433 token.get_src_col(),
434 token.get_source_id(),
435 token.get_name_id(),
436 )
437 })
438 .collect::<Vec<_>>();
439 let mut shifted = oxc_sourcemap::SourceMap::new(
440 map.get_file().cloned(),
441 map.get_names().cloned().collect::<Vec<Arc<str>>>(),
442 map.get_source_root().map(str::to_string),
443 map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
444 map.get_source_contents()
445 .map(|content| content.cloned())
446 .collect::<Vec<Option<Arc<str>>>>(),
447 tokens.into_boxed_slice(),
448 None,
449 );
450 if let Some(ignore_list) = map.get_x_google_ignore_list() {
451 shifted.set_x_google_ignore_list(ignore_list.to_vec());
452 }
453 if let Some(debug_id) = map.get_debug_id() {
454 shifted.set_debug_id(debug_id);
455 }
456 shifted
457}
458
459fn generate_candidate(
460 program: &Program<'_>,
461 file: &str,
462) -> Result<(String, Option<serde_json::Value>), CandidateError> {
463 let options = CodegenOptions {
464 source_map_path: Some(Path::new(file).to_path_buf()),
465 ..CodegenOptions::default()
466 };
467 let generated = Codegen::new().with_options(options).build(program);
468 let (code, map) = restore_comment_text(
469 program,
470 &generated.code,
471 generated.map.expect("source maps are enabled"),
472 )?;
473 let map = Some({
474 serde_json::from_str(&map.to_json_string())
475 .expect("oxc must serialize its own generated source map")
476 });
477 Ok((code, map))
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
481#[serde(rename_all = "camelCase")]
482pub struct CandidateRuntime {
483 pub coverage_hit: String,
484 pub mcdc_begin: String,
485 pub mcdc_condition: String,
486 pub mcdc_end: String,
487 pub register_probe_v2: String,
488 pub mcdc_end_v2: String,
489 pub coverage_hit_v2: String,
490 pub probe_file_v2: String,
491 pub selection_begin: String,
492 pub selection_right: String,
493 pub selection_end: String,
494 pub parenthesized_assignment_value: String,
495 pub with_request_phase: String,
496 pub optional_select: String,
497 pub rendered_value_v2: String,
498 pub optional_call_begin: String,
499 pub optional_call_reached: String,
500 pub optional_call_continued: String,
501 pub optional_call_end: String,
502 pub default_selected: String,
503 pub default_entered: String,
504 pub try_begin: String,
505 pub try_catch: String,
506 pub try_end: String,
507 pub loop_begin: String,
508 pub loop_entered: String,
509 pub loop_end: String,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513#[serde(rename_all = "camelCase")]
514pub struct CandidateLimitation {
515 pub id: String,
516 pub kind: String,
517 pub file: String,
518 pub line: usize,
519 pub column: usize,
520 pub source: String,
521 pub reason: String,
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
525pub enum CandidateError {
526 UnknownSourceType(String),
527 Parse(Vec<String>),
528 CommentPreservation {
529 expected: usize,
530 actual: usize,
531 detail: String,
532 },
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
536enum RuntimeBinding {
537 ModuleImport,
538 DirectGlobal,
539}
540
541const NODE_ASSERT_MODULES: &[&str] = &[
542 "assert",
543 "assert/strict",
544 "node:assert",
545 "node:assert/strict",
546];
547const NODE_ASSERT_METHODS: &[&str] = &[
548 "deepEqual",
549 "deepStrictEqual",
550 "doesNotMatch",
551 "doesNotReject",
552 "doesNotThrow",
553 "equal",
554 "fail",
555 "ifError",
556 "match",
557 "notDeepEqual",
558 "notDeepStrictEqual",
559 "notEqual",
560 "notStrictEqual",
561 "ok",
562 "partialDeepStrictEqual",
563 "rejects",
564 "strictEqual",
565 "throws",
566];
567
568#[derive(Debug, Clone, PartialEq, Eq)]
569pub struct NodeAssertionInstrumentation {
570 pub code: String,
571 pub assertions: usize,
572 pub capability_imports: usize,
573}
574
575const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
576 &["@jest/globals", "@playwright/test", "playwright", "vitest"];
577
578fn capability_source_candidate(source: &str) -> bool {
579 let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
585 || (source.contains("hostRoot") && source.contains("guestRoot"));
586 let mount_mapping =
592 source.contains("mounts") && source.contains("source") && source.contains("target");
593 direct_mapping || mount_mapping
594}
595
596fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
597 source == wrapper
598 || source.starts_with("node:")
599 || source.starts_with("virtual:supercov-")
600 || source.contains(".supercov/")
601 || CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
602}
603
604fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
605 match expression {
606 Expression::Identifier(identifier) => Some(identifier.name.to_string()),
607 Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
608 Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
609 Expression::CallExpression(call) => capability_callee_root(&call.callee),
610 Expression::ParenthesizedExpression(parenthesized) => {
611 capability_callee_root(&parenthesized.expression)
612 }
613 Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
614 Expression::TSSatisfiesExpression(expression) => {
615 capability_callee_root(&expression.expression)
616 }
617 Expression::TSNonNullExpression(expression) => {
618 capability_callee_root(&expression.expression)
619 }
620 Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
621 _ => None,
622 }
623}
624
625struct CapabilityCallCollector<'s> {
626 source: &'s str,
627 mapping_variables: HashSet<String>,
628 roots: HashSet<String>,
629}
630
631impl CapabilityCallCollector<'_> {
632 fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
633 if let Argument::Identifier(identifier) = argument
634 && self.mapping_variables.contains(identifier.name.as_str())
635 {
636 return true;
637 }
638 capability_source_candidate(source_slice(self.source, argument.span()))
639 }
640}
641
642impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
643 fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
644 if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
645 (&declarator.id, &declarator.init)
646 && capability_source_candidate(source_slice(self.source, initializer.span()))
647 {
648 self.mapping_variables.insert(identifier.name.to_string());
649 }
650 walk::walk_variable_declarator(self, declarator);
651 }
652
653 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
654 if call
655 .arguments
656 .iter()
657 .any(|argument| self.argument_has_mapping(argument))
658 && let Some(root) = capability_callee_root(&call.callee)
659 {
660 self.roots.insert(root);
661 }
662 walk::walk_call_expression(self, call);
663 }
664}
665
666fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
667 let mut collector = CapabilityCallCollector {
668 source,
669 mapping_variables: HashSet::new(),
670 roots: HashSet::new(),
671 };
672 collector.visit_program(program);
675 let mapping_variables = collector.mapping_variables.clone();
676 collector.roots.clear();
677 collector.mapping_variables = mapping_variables;
678 collector.visit_program(program);
679 collector.roots
680}
681
682fn transform_capability_imports<'a>(
686 allocator: &'a Allocator,
687 program: &mut Program<'a>,
688 source: &str,
689 wrapper: &str,
690) -> usize {
691 if !capability_source_candidate(source) || !program.source_type.is_module() {
692 return 0;
693 }
694 let capability_roots = capability_import_roots(program, source);
695 if capability_roots.is_empty() {
696 return 0;
697 }
698 let ast = AstBuilder::new(allocator);
699 let mut names = CandidateNames::new(source);
700 let wrapper_local = names.allocate("__supercovImportedCapability");
701 let mut wrapped = 0;
702 let mut output = ast.vec();
703 for statement in program.body.take_in(allocator) {
704 let Statement::ImportDeclaration(mut declaration) = statement else {
705 output.push(statement);
706 continue;
707 };
708 let module_name = declaration.source.value.as_str();
709 if declaration.import_kind == ImportOrExportKind::Type
710 || excluded_capability_import(module_name, wrapper)
711 {
712 output.push(Statement::ImportDeclaration(declaration));
713 continue;
714 }
715 let mut declarations = ast.vec();
716 for specifier in declaration.specifiers.iter_mut().flatten() {
717 let local = match specifier {
718 ImportDeclarationSpecifier::ImportSpecifier(specifier)
719 if specifier.import_kind == ImportOrExportKind::Type =>
720 {
721 continue;
722 }
723 ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
724 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
725 &mut specifier.local
726 }
727 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
728 &mut specifier.local
729 }
730 };
731 let original = local.name.to_string();
732 if !capability_roots.contains(&original) {
733 continue;
734 }
735 let raw = names.allocate(&format!("__supercovRaw{original}"));
736 local.name = ast.ident(&raw);
737 let wrapped_value = ast.expression_call(
738 Span::default(),
739 ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
740 NONE,
741 ast.vec1(Argument::from(
742 ast.expression_identifier(Span::default(), ast.ident(&raw)),
743 )),
744 false,
745 );
746 declarations.push(ast.variable_declarator(
747 Span::default(),
748 VariableDeclarationKind::Const,
749 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
750 NONE,
751 Some(wrapped_value),
752 false,
753 ));
754 wrapped += 1;
755 }
756 output.push(Statement::ImportDeclaration(declaration));
757 if !declarations.is_empty() {
758 output.push(Statement::VariableDeclaration(
759 ast.alloc_variable_declaration(
760 Span::default(),
761 VariableDeclarationKind::Const,
762 declarations,
763 false,
764 ),
765 ));
766 }
767 }
768 if wrapped > 0 {
769 output.insert(
770 0,
771 Statement::ImportDeclaration(ast.alloc_import_declaration(
772 Span::default(),
773 Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
774 Span::default(),
775 ast.module_export_name_identifier_name(
776 Span::default(),
777 ast.ident("wrapImportedCapability"),
778 ),
779 ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
780 ImportOrExportKind::Value,
781 ))),
782 ast.string_literal(Span::default(), ast.str(wrapper), None),
783 None,
784 NONE,
785 ImportOrExportKind::Value,
786 )),
787 );
788 }
789 program.body = output;
790 wrapped
791}
792
793fn canonical_assert_module(value: &str) -> Option<String> {
794 NODE_ASSERT_MODULES.contains(&value).then(|| {
795 if value.starts_with("node:") {
796 value.into()
797 } else {
798 format!("node:{value}")
799 }
800 })
801}
802
803fn required_assert_module(
804 expression: &Expression<'_>,
805 scoping: &oxc_semantic::Scoping,
806) -> Option<String> {
807 if let Expression::CallExpression(call) = expression
808 && matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
809 && matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
810 && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
811 {
812 return canonical_assert_module(module.value.as_str());
813 }
814 if let Expression::StaticMemberExpression(member) = expression
815 && member.property.name == "strict"
816 && let Some(module) = required_assert_module(&member.object, scoping)
817 {
818 return Some(if module == "node:assert" {
819 "node:assert/strict".into()
820 } else {
821 module
822 });
823 }
824 None
825}
826
827#[derive(Default)]
828struct NodeAssertionBindings {
829 objects: HashMap<SymbolId, String>,
830 direct: HashMap<SymbolId, String>,
831 expects: HashSet<SymbolId>,
832 global_expect: bool,
836}
837
838fn bind_object(
839 bindings: &mut NodeAssertionBindings,
840 identifier: &oxc_ast::ast::BindingIdentifier<'_>,
841 module: String,
842) {
843 if let Some(symbol) = identifier.symbol_id.get() {
844 bindings.objects.insert(symbol, module);
845 }
846}
847
848fn bind_direct(
849 bindings: &mut NodeAssertionBindings,
850 identifier: &oxc_ast::ast::BindingIdentifier<'_>,
851 operation: String,
852) {
853 if let Some(symbol) = identifier.symbol_id.get() {
854 bindings.direct.insert(symbol, operation);
855 }
856}
857
858fn bind_expect(
859 bindings: &mut NodeAssertionBindings,
860 identifier: &oxc_ast::ast::BindingIdentifier<'_>,
861) {
862 if let Some(symbol) = identifier.symbol_id.get() {
863 bindings.expects.insert(symbol);
864 }
865}
866
867struct NodeAssertionBindingCollector<'s> {
868 bindings: NodeAssertionBindings,
869 scoping: &'s oxc_semantic::Scoping,
870 allow_contextual_expect: bool,
871 expect_modules: HashSet<String>,
872}
873
874impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
875 fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
876 let module_name = declaration.source.value.as_str();
877 let assert_module = canonical_assert_module(module_name);
878 let expect_module =
879 self.expect_modules.contains(module_name) || self.allow_contextual_expect;
880 for specifier in declaration.specifiers.iter().flatten() {
881 match specifier {
882 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
883 if let Some(module) = &assert_module {
884 bind_object(&mut self.bindings, &specifier.local, module.clone());
885 } else if expect_module && specifier.local.name == "expect" {
886 bind_expect(&mut self.bindings, &specifier.local);
887 }
888 }
889 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
890 if let Some(module) = &assert_module {
891 bind_object(&mut self.bindings, &specifier.local, module.clone());
892 }
893 }
894 ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
895 let imported = specifier.imported.name().to_string();
896 if let Some(module) = &assert_module {
897 if imported == "strict" {
898 bind_object(
899 &mut self.bindings,
900 &specifier.local,
901 "node:assert/strict".into(),
902 );
903 } else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
904 bind_direct(
905 &mut self.bindings,
906 &specifier.local,
907 format!("{module}.{imported}"),
908 );
909 }
910 } else if expect_module && imported == "expect" {
911 bind_expect(&mut self.bindings, &specifier.local);
912 }
913 }
914 }
915 }
916 walk::walk_import_declaration(self, declaration);
917 }
918
919 fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
920 if let Some(Expression::CallExpression(call)) = &declarator.init
923 && matches!(&call.callee, Expression::Identifier(i) if i.name == "require" && referenced_symbol(i, self.scoping).is_none())
924 && let [Argument::StringLiteral(module)] = call.arguments.as_slice()
925 && self.expect_modules.contains(module.value.as_str())
926 && let BindingPattern::ObjectPattern(pattern) = &declarator.id
927 {
928 for property in &pattern.properties {
929 if property.key.static_name().as_deref() == Some("expect")
930 && let BindingPattern::BindingIdentifier(local) = &property.value
931 {
932 bind_expect(&mut self.bindings, local);
933 }
934 }
935 }
936 if let Some(module) = declarator
937 .init
938 .as_ref()
939 .and_then(|expression| required_assert_module(expression, self.scoping))
940 {
941 match &declarator.id {
942 BindingPattern::BindingIdentifier(identifier) => {
943 bind_object(&mut self.bindings, identifier, module);
944 }
945 BindingPattern::ObjectPattern(pattern) => {
946 for property in &pattern.properties {
947 let Some(imported) = property.key.static_name() else {
948 continue;
949 };
950 let BindingPattern::BindingIdentifier(local) = &property.value else {
951 continue;
952 };
953 if imported == "strict" {
954 bind_object(&mut self.bindings, local, "node:assert/strict".into());
955 } else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
956 bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
957 }
958 }
959 }
960 _ => {}
961 }
962 }
963 walk::walk_variable_declarator(self, declarator);
964 }
965}
966
967fn node_assertion_bindings(
968 program: &Program<'_>,
969 scoping: &oxc_semantic::Scoping,
970 extra_expect_modules: &[String],
971) -> NodeAssertionBindings {
972 let allow_contextual_expect = program.source_text.contains("node:test")
973 && program
974 .source_text
975 .split(|character: char| !character.is_alphanumeric() && character != '_')
976 .any(|word| word == "expect");
977 let mut collector = NodeAssertionBindingCollector {
978 bindings: NodeAssertionBindings::default(),
979 scoping,
980 allow_contextual_expect,
981 expect_modules: ["vitest", "@jest/globals", "expect", "@playwright/test"]
982 .into_iter()
983 .map(str::to_owned)
984 .chain(extra_expect_modules.iter().cloned())
985 .collect(),
986 };
987 collector.visit_program(program);
988 let unresolved = scoping.root_unresolved_references();
989 let uses = |name: &str| unresolved.contains_key(name);
990 collector.bindings.global_expect =
991 uses("expect") && (uses("test") || uses("it") || uses("describe"));
992 collector.bindings
993}
994
995struct NodeAssertionSiteCollector<'s> {
996 source: &'s str,
997 file: &'s str,
998 bindings: &'s NodeAssertionBindings,
999 scoping: &'s oxc_semantic::Scoping,
1000 sites: HashMap<SpanKey, (String, String, bool)>,
1001 inventory: bool,
1002}
1003
1004pub fn assertion_ranges(file: &str, source: &str) -> Result<Vec<(usize, usize, String)>, String> {
1007 assertion_ranges_with_expect_modules(file, source, &[])
1008}
1009
1010pub fn assertion_ranges_with_expect_modules(
1011 file: &str,
1012 source: &str,
1013 modules: &[String],
1014) -> Result<Vec<(usize, usize, String)>, String> {
1015 let source_type = SourceType::from_path(Path::new(file)).map_err(|e| e.to_string())?;
1016 let allocator = Allocator::default();
1017 let parsed = Parser::new(&allocator, source, source_type).parse();
1018 if !parsed.errors.is_empty() {
1019 return Err(format!("{} parse errors", parsed.errors.len()));
1020 }
1021 let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
1022 let bindings = node_assertion_bindings(&parsed.program, semantic.scoping(), modules);
1023 let mut collector = NodeAssertionSiteCollector {
1024 source,
1025 file,
1026 bindings: &bindings,
1027 scoping: semantic.scoping(),
1028 sites: HashMap::new(),
1029 inventory: true,
1030 };
1031 collector.visit_program(&parsed.program);
1032 let mut sites = collector
1033 .sites
1034 .into_iter()
1035 .map(|((start, end), (op, _, _))| (start as usize, end as usize, op))
1036 .collect::<Vec<_>>();
1037 sites.sort();
1038 Ok(sites)
1039}
1040
1041fn referenced_symbol(
1042 identifier: &oxc_ast::ast::IdentifierReference<'_>,
1043 scoping: &oxc_semantic::Scoping,
1044) -> Option<SymbolId> {
1045 identifier
1046 .reference_id
1047 .get()
1048 .and_then(|reference| scoping.get_reference(reference).symbol_id())
1049}
1050
1051fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1052 match expression {
1053 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1054 Expression::ComputedMemberExpression(member) => match &member.expression {
1055 Expression::StringLiteral(literal) => Some(literal.value.as_str()),
1056 _ => None,
1057 },
1058 _ => None,
1059 }
1060}
1061
1062fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
1063 match expression {
1064 Expression::StaticMemberExpression(member) => Some(&member.object),
1065 Expression::ComputedMemberExpression(member) => Some(&member.object),
1066 _ => None,
1067 }
1068}
1069
1070fn expect_operation(
1071 callee: &Expression<'_>,
1072 bindings: &NodeAssertionBindings,
1073 scoping: &oxc_semantic::Scoping,
1074) -> Option<String> {
1075 let mut current = callee;
1076 let mut matchers = Vec::new();
1077 while let Some(name) = assertion_member_name(current) {
1078 matchers.push(name.to_owned());
1079 current = assertion_member_object(current)?;
1080 }
1081 matchers.reverse();
1082 let matcher = matchers.last()?;
1083 if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
1084 return None;
1085 }
1086 let Expression::CallExpression(expect_call) = current else {
1087 return None;
1088 };
1089 let mut root = &expect_call.callee;
1096 let mut prefix = Vec::new();
1097 while let Some(name) = assertion_member_name(root) {
1098 prefix.push(name.to_owned());
1099 root = assertion_member_object(root)?;
1100 }
1101 prefix.reverse();
1102 let Expression::Identifier(identifier) = root else {
1103 return None;
1104 };
1105 let recognized = match referenced_symbol(identifier, scoping) {
1106 Some(symbol) => bindings.expects.contains(&symbol),
1107 None => bindings.global_expect && identifier.name == "expect",
1108 };
1109 recognized.then(|| {
1110 let mut parts = prefix;
1111 parts.extend(matchers.iter().cloned());
1112 format!("expect.{}", parts.join("."))
1113 })
1114}
1115
1116#[derive(Default)]
1117struct AwaitYieldScanner {
1118 found: bool,
1119}
1120
1121impl<'a> Visit<'a> for AwaitYieldScanner {
1122 fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
1123 self.found = true;
1124 }
1125
1126 fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
1127 self.found = true;
1128 }
1129
1130 fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
1131
1132 fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
1133}
1134
1135impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
1136 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
1137 let operation = match &call.callee {
1138 Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
1139 .and_then(|symbol| {
1140 self.bindings.direct.get(&symbol).cloned().or_else(|| {
1141 self.bindings
1142 .objects
1143 .get(&symbol)
1144 .map(|module| format!("{module}.ok"))
1145 })
1146 }),
1147 callee => {
1148 let member_operation = assertion_member_object(callee).and_then(|object| {
1149 let Expression::Identifier(identifier) = object else {
1150 return None;
1151 };
1152 let symbol = referenced_symbol(identifier, self.scoping)?;
1153 let method = assertion_member_name(callee)?;
1154 self.bindings
1155 .objects
1156 .get(&symbol)
1157 .filter(|_| NODE_ASSERT_METHODS.contains(&method))
1158 .map(|module| format!("{module}.{method}"))
1159 });
1160 member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
1161 }
1162 };
1163 if let Some(operation) = operation {
1164 let mut unsafe_argument = AwaitYieldScanner::default();
1165 unsafe_argument.visit_expression(&call.callee);
1166 for argument in &call.arguments {
1167 unsafe_argument.visit_argument(argument);
1168 }
1169 let optional = call.optional
1170 || matches!(&call.callee,
1171 Expression::StaticMemberExpression(m) if m.optional)
1172 || matches!(&call.callee, Expression::ComputedMemberExpression(m) if m.optional);
1173 if optional && !self.inventory {
1176 walk::walk_call_expression(self, call);
1177 return;
1178 }
1179 let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
1180 self.sites.insert(
1181 span_key(call.span),
1182 (
1183 operation,
1184 format!("{}:{line}:{column}", self.file),
1185 unsafe_argument.found,
1186 ),
1187 );
1188 }
1189 walk::walk_call_expression(self, call);
1190 }
1191}
1192
1193struct NodeAssertionTransformer<'a> {
1194 ast: AstBuilder<'a>,
1195 sites: HashMap<SpanKey, (String, String, bool)>,
1196}
1197
1198impl<'a> NodeAssertionTransformer<'a> {
1199 fn helper(&self, name: &str) -> Expression<'a> {
1200 let runtime = Expression::StaticMemberExpression(
1201 self.ast.alloc_static_member_expression(
1202 Span::default(),
1203 self.ast
1204 .expression_identifier(Span::default(), self.ast.ident("globalThis")),
1205 self.ast.identifier_name(
1206 Span::default(),
1207 self.ast.ident("__SUPERCOV_DIRECT_RUNTIME__"),
1208 ),
1209 false,
1210 ),
1211 );
1212 Expression::StaticMemberExpression(
1213 self.ast.alloc_static_member_expression(
1214 Span::default(),
1215 runtime,
1216 self.ast
1217 .identifier_name(Span::default(), self.ast.ident(name)),
1218 false,
1219 ),
1220 )
1221 }
1222 fn wrap(&self, original: Expression<'a>, operation: &str, source: &str) -> Expression<'a> {
1223 let helper = self.helper("withNodeAssertionPhase");
1224 let parameters = self.ast.alloc_formal_parameters(
1225 Span::default(),
1226 FormalParameterKind::ArrowFormalParameters,
1227 self.ast.vec(),
1228 NONE,
1229 );
1230 let body = self.ast.alloc_function_body(
1231 Span::default(),
1232 self.ast.vec(),
1233 self.ast
1234 .vec1(self.ast.statement_return(Span::default(), Some(original))),
1235 );
1236 let callback = self.ast.expression_arrow_function(
1237 Span::default(),
1238 false,
1239 false,
1240 NONE,
1241 parameters,
1242 NONE,
1243 body,
1244 );
1245 self.ast.expression_call(
1246 Span::default(),
1247 helper,
1248 NONE,
1249 self.ast.vec_from_array([
1250 Argument::from(self.ast.expression_string_literal(
1251 Span::default(),
1252 self.ast.str(operation),
1253 None,
1254 )),
1255 Argument::from(self.ast.expression_string_literal(
1256 Span::default(),
1257 self.ast.str(source),
1258 None,
1259 )),
1260 Argument::from(callback),
1261 ]),
1262 false,
1263 )
1264 }
1265}
1266
1267impl<'a> NodeAssertionTransformer<'a> {
1268 fn bind_call(&self, original: Expression<'a>, operation: &str, source: &str) -> Expression<'a> {
1271 let Expression::CallExpression(mut call) = original else {
1272 unreachable!("assertion call")
1273 };
1274 let (target, property) = match &mut call.callee {
1275 Expression::StaticMemberExpression(m) => (
1276 m.object.take_in(self.ast.allocator),
1277 self.ast.expression_string_literal(
1278 Span::default(),
1279 self.ast.str(m.property.name.as_str()),
1280 None,
1281 ),
1282 ),
1283 Expression::ComputedMemberExpression(m) => (
1284 m.object.take_in(self.ast.allocator),
1285 m.expression.take_in(self.ast.allocator),
1286 ),
1287 _ => (
1288 call.callee.take_in(self.ast.allocator),
1289 self.ast.expression_null_literal(Span::default()),
1290 ),
1291 };
1292 call.callee = self.ast.expression_call(
1293 Span::default(),
1294 self.helper("bindNodeAssertionPhase"),
1295 NONE,
1296 self.ast.vec_from_array([
1297 Argument::from(self.ast.expression_string_literal(
1298 Span::default(),
1299 self.ast.str(operation),
1300 None,
1301 )),
1302 Argument::from(self.ast.expression_string_literal(
1303 Span::default(),
1304 self.ast.str(source),
1305 None,
1306 )),
1307 Argument::from(target),
1308 Argument::from(property),
1309 ]),
1310 false,
1311 );
1312 Expression::CallExpression(call)
1313 }
1314}
1315
1316impl<'a> VisitMut<'a> for NodeAssertionTransformer<'a> {
1317 fn visit_expression(&mut self, expression: &mut Expression<'a>) {
1318 let key = span_key(expression.span());
1319 walk_mut::walk_expression(self, expression);
1320 let Some((operation, source, bound)) = self.sites.remove(&key) else {
1321 return;
1322 };
1323 let original = expression.take_in(self.ast.allocator);
1324 *expression = if bound {
1325 self.bind_call(original, &operation, &source)
1326 } else {
1327 self.wrap(original, &operation, &source)
1328 };
1329 }
1330}
1331
1332pub fn instrument_node_assertion_phases(
1337 source: &str,
1338 file: &str,
1339) -> Result<NodeAssertionInstrumentation, CandidateError> {
1340 instrument_node_assertion_phases_with_expect_modules(source, file, &[])
1341}
1342
1343pub fn instrument_node_assertion_phases_with_expect_modules(
1344 source: &str,
1345 file: &str,
1346 extra_expect_modules: &[String],
1347) -> Result<NodeAssertionInstrumentation, CandidateError> {
1348 instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
1349}
1350
1351pub fn instrument_node_assertion_phases_with_runtime_hooks(
1352 source: &str,
1353 file: &str,
1354 extra_expect_modules: &[String],
1355 capability_wrapper: Option<&str>,
1356) -> Result<NodeAssertionInstrumentation, CandidateError> {
1357 instrument_node_assertion_phases_with_runtime_imports(
1358 source,
1359 file,
1360 extra_expect_modules,
1361 capability_wrapper,
1362 None,
1363 )
1364}
1365
1366pub fn instrument_node_assertion_phases_with_runtime_imports(
1373 source: &str,
1374 file: &str,
1375 extra_expect_modules: &[String],
1376 capability_wrapper: Option<&str>,
1377 assertion_runtime: Option<&str>,
1378) -> Result<NodeAssertionInstrumentation, CandidateError> {
1379 let assertion_candidate = source.contains("assert") || source.contains("expect");
1380 let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
1381 if !assertion_candidate && !capability_candidate {
1382 return Ok(NodeAssertionInstrumentation {
1383 code: source.into(),
1384 assertions: 0,
1385 capability_imports: 0,
1386 });
1387 }
1388 let source_type = SourceType::from_path(Path::new(file))
1389 .map_err(|error| CandidateError::UnknownSourceType(error.to_string()))?;
1390 let allocator = Allocator::default();
1391 let mut parsed = Parser::new(&allocator, source, source_type).parse();
1392 if !parsed.errors.is_empty() {
1393 return Err(CandidateError::Parse(
1394 parsed
1395 .errors
1396 .into_iter()
1397 .map(|error| error.to_string())
1398 .collect(),
1399 ));
1400 }
1401 let mut assertions = 0;
1402
1403 if assertion_candidate {
1404 let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
1405 let bindings =
1406 node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
1407 let mut collector = NodeAssertionSiteCollector {
1408 source,
1409 file,
1410 bindings: &bindings,
1411 scoping: semantic.scoping(),
1412 sites: HashMap::new(),
1413 inventory: false,
1414 };
1415 collector.visit_program(&parsed.program);
1416 assertions = collector.sites.len();
1417 if assertions > 0 {
1418 NodeAssertionTransformer {
1419 ast: AstBuilder::new(&allocator),
1420 sites: collector.sites,
1421 }
1422 .visit_program(&mut parsed.program);
1423 }
1424 }
1425 let capability_imports = capability_wrapper.map_or(0, |wrapper| {
1426 transform_capability_imports(&allocator, &mut parsed.program, source, wrapper)
1427 });
1428 if assertions == 0 && capability_imports == 0 {
1429 return Ok(NodeAssertionInstrumentation {
1430 code: source.into(),
1431 assertions: 0,
1432 capability_imports: 0,
1433 });
1434 }
1435 let module_assertion_runtime = (assertions > 0 && parsed.program.source_type.is_module())
1436 .then_some(assertion_runtime)
1437 .flatten();
1438 let (mut code, map) = generate_candidate(&parsed.program, file)?;
1439 if let Some(runtime) = module_assertion_runtime {
1443 code.push_str("\nimport ");
1444 code.push_str(
1445 &serde_json::to_string(runtime)
1446 .expect("a JavaScript module specifier always serializes as a string"),
1447 );
1448 code.push_str(";\n");
1449 }
1450 let mut map = map.expect("assertion source maps are enabled");
1451 map["sources"] = serde_json::json!([Path::new(file)
1457 .file_name()
1458 .and_then(|name| name.to_str())
1459 .unwrap_or(file)]);
1460 let map = serde_json::to_vec(&map).expect("source-map values always serialize");
1461 code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
1462 BASE64_STANDARD.encode_string(map, &mut code);
1463 code.push('\n');
1464 Ok(NodeAssertionInstrumentation {
1465 code,
1466 assertions,
1467 capability_imports,
1468 })
1469}
1470
1471type SpanKey = (u32, u32);
1472
1473#[derive(Default)]
1474struct SafetyAnalysis {
1475 source_sensitive_functions: HashSet<SpanKey>,
1476 with_statements: HashSet<SpanKey>,
1477 semantic_limitations: Vec<CandidateLimitation>,
1478 dynamic_limitations: Vec<CandidateLimitation>,
1479}
1480
1481struct SafetyScanner<'s> {
1482 source: &'s str,
1483 file: &'s str,
1484 source_sensitive_functions: HashSet<SpanKey>,
1485 with_statements: HashSet<SpanKey>,
1486 function_limitations: Vec<CandidateLimitation>,
1487 with_limitations: Vec<CandidateLimitation>,
1488 dynamic_limitations: Vec<CandidateLimitation>,
1489 unsafe_function_depth: usize,
1490 with_depth: usize,
1491}
1492
1493#[derive(Clone, Copy, PartialEq, Eq)]
1494enum PointPass {
1495 Statements,
1496 Functions,
1497}
1498
1499struct PointCollector<'s> {
1500 source: &'s str,
1501 file: &'s str,
1502 pass: PointPass,
1503 points: Vec<CandidatePoint>,
1504 statement_targets: HashMap<SpanKey, Vec<String>>,
1505 function_targets: HashMap<SpanKey, String>,
1506 rendered_targets: HashMap<SpanKey, String>,
1507 source_sensitive_functions: &'s HashSet<SpanKey>,
1508 erased_imports: &'s HashSet<SpanKey>,
1509 unsafe_function_depth: usize,
1510 with_depth: usize,
1511 ambient_depth: usize,
1512}
1513
1514#[derive(Default)]
1515struct PointAnalysis {
1516 points: Vec<CandidatePoint>,
1517 statement_targets: HashMap<SpanKey, Vec<String>>,
1518 function_targets: HashMap<SpanKey, String>,
1519 rendered_targets: HashMap<SpanKey, String>,
1520}
1521
1522#[derive(Clone)]
1523struct PointTarget {
1524 index: usize,
1525}
1526
1527impl PointCollector<'_> {
1528 fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
1529 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
1530 CandidatePoint {
1531 id: stable_id(self.source, self.file, kind, span, ""),
1532 kind: kind.to_string(),
1533 file: self.file.to_string(),
1534 line,
1535 column,
1536 source: source_slice(self.source, span).to_string(),
1537 label,
1538 }
1539 }
1540
1541 fn unsafe_context(&self) -> bool {
1542 self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
1543 }
1544
1545 fn exit_function(&mut self, span: Span) {
1546 if self.source_sensitive_functions.contains(&span_key(span)) {
1547 self.unsafe_function_depth -= 1;
1548 }
1549 }
1550}
1551
1552fn function_label<State>(
1553 own_name: Option<&str>,
1554 context: &TraverseCtx<'_, State>,
1555) -> Option<String> {
1556 if let Some(name) = own_name {
1557 return Some(name.to_string());
1558 }
1559 match context.ancestors().next()? {
1560 Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
1561 Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
1562 Ancestor::VariableDeclaratorInit(parent) => parent
1563 .id()
1564 .get_binding_identifier()
1565 .map(|identifier| identifier.name.to_string()),
1566 _ => None,
1567 }
1568}
1569
1570fn property_label(key: &PropertyKey<'_>) -> Option<String> {
1571 key.static_name()
1572 .map(|name| name.into_owned())
1573 .or_else(|| match key {
1574 PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
1575 _ => None,
1576 })
1577}
1578
1579fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
1580 match context.ancestors().next() {
1581 Some(Ancestor::ObjectPropertyValue(parent))
1582 if *parent.method() || *parent.kind() != PropertyKind::Init =>
1583 {
1584 *parent.span()
1585 }
1586 Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
1587 _ => span,
1588 }
1589}
1590
1591fn type_only_import(statement: &Statement<'_>) -> bool {
1592 let Statement::ImportDeclaration(declaration) = statement else {
1593 return false;
1594 };
1595 declaration.import_kind == ImportOrExportKind::Type
1598}
1599
1600fn erased_imports(program: &Program<'_>, elide_type_imports: bool) -> HashSet<SpanKey> {
1603 let semantic = SemanticBuilder::new().build(program).semantic;
1604 let scoping = semantic.scoping();
1605 program
1606 .body
1607 .iter()
1608 .filter_map(|statement| {
1609 let Statement::ImportDeclaration(declaration) = statement else {
1610 return None;
1611 };
1612 let erased = declaration.import_kind == ImportOrExportKind::Type
1613 || (elide_type_imports
1614 && program.source_type.is_typescript()
1615 && !program.source_type.is_jsx()
1616 && declaration.specifiers.as_ref().is_some_and(|specifiers| {
1617 !specifiers.is_empty()
1618 && specifiers.iter().all(|specifier| {
1619 let local = match specifier {
1620 ImportDeclarationSpecifier::ImportSpecifier(s) => {
1621 if s.import_kind == ImportOrExportKind::Type {
1622 return true;
1623 }
1624 &s.local
1625 }
1626 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
1627 &s.local
1628 }
1629 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
1630 &s.local
1631 }
1632 };
1633 local.symbol_id.get().is_some_and(|symbol| {
1634 let mut refs =
1635 scoping.get_resolved_references(symbol).peekable();
1636 refs.peek().is_some()
1639 && refs.all(|r| r.is_type() && !r.is_value())
1640 })
1641 })
1642 }));
1643 erased.then_some(span_key(declaration.span))
1644 })
1645 .collect()
1646}
1647
1648fn executable_statement(statement: &Statement<'_>) -> bool {
1649 !matches!(
1650 statement,
1651 Statement::BlockStatement(_)
1652 | Statement::EmptyStatement(_)
1653 | Statement::FunctionDeclaration(_)
1654 | Statement::ExportNamedDeclaration(_)
1655 | Statement::ExportDefaultDeclaration(_)
1656 ) && !statement.is_typescript_syntax()
1657 && !type_only_import(statement)
1658}
1659
1660impl<'a> Traverse<'a, ()> for PointCollector<'_> {
1661 fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
1662 let mut ancestors = context.ancestors();
1663 let parent = ancestors.next();
1664 let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
1665 && matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
1666 if self.pass != PointPass::Statements
1667 || self.unsafe_context()
1668 || !executable_statement(node)
1669 || self.erased_imports.contains(&span_key(node.span()))
1670 || matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
1671 || expression_arrow_body
1672 {
1673 return;
1674 }
1675 let point = self.point(node.span(), "statement", None);
1676 self.statement_targets
1677 .entry(span_key(node.span()))
1678 .or_default()
1679 .push(point.id.clone());
1680 self.points.push(point);
1681 }
1682
1683 fn enter_expression(&mut self, node: &mut Expression<'a>, context: &mut TraverseCtx<'a, ()>) {
1696 if self.pass != PointPass::Statements
1697 || self.unsafe_context()
1698 || !matches!(
1699 context.ancestors().next(),
1700 Some(Ancestor::JSXExpressionContainerExpression(_))
1701 )
1702 || !rendered_expression_is_measurable(node)
1703 {
1704 return;
1705 }
1706 let span = node.span();
1707 let point = self.point(span, "statement", Some("jsx-rendered-expression".into()));
1708 self.rendered_targets
1709 .insert(span_key(span), point.id.clone());
1710 self.points.push(point);
1711 }
1712
1713 fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
1714 if self.pass != PointPass::Statements
1715 || self.unsafe_context()
1716 || node.is_typescript_syntax()
1717 || matches!(node, Declaration::FunctionDeclaration(_))
1718 || !matches!(
1719 context.ancestors().next(),
1720 Some(Ancestor::ExportNamedDeclarationDeclaration(_))
1721 | Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
1722 )
1723 {
1724 return;
1725 }
1726 let point = self.point(node.span(), "statement", None);
1727 self.statement_targets
1728 .entry(span_key(node.span()))
1729 .or_default()
1730 .push(point.id.clone());
1731 self.points.push(point);
1732 }
1733
1734 fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
1735 if self.pass != PointPass::Statements
1741 || self.unsafe_context()
1742 || node.declare
1743 || !matches!(
1744 context.ancestors().next(),
1745 Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
1746 )
1747 {
1748 return;
1749 }
1750 let point = self.point(node.span, "statement", None);
1751 self.statement_targets
1752 .entry(span_key(node.span))
1753 .or_default()
1754 .push(point.id.clone());
1755 self.points.push(point);
1756 }
1757
1758 fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
1759 let point_span = function_point_span(node.span, context);
1760 let label = if point_span == node.span {
1761 function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
1762 } else {
1763 None
1764 };
1765 if self
1766 .source_sensitive_functions
1767 .contains(&span_key(node.span))
1768 {
1769 self.unsafe_function_depth += 1;
1770 return;
1771 }
1772 if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
1773 let point = self.point(point_span, "function", label);
1774 self.function_targets
1775 .insert(span_key(node.span), point.id.clone());
1776 self.points.push(point);
1777 }
1778 }
1779
1780 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
1781 self.exit_function(node.span);
1782 }
1783
1784 fn enter_arrow_function_expression(
1785 &mut self,
1786 node: &mut ArrowFunctionExpression<'a>,
1787 context: &mut TraverseCtx<'a, ()>,
1788 ) {
1789 let point_span = function_point_span(node.span, context);
1790 let label = if point_span == node.span {
1791 function_label(None, context)
1792 } else {
1793 None
1794 };
1795 if self
1796 .source_sensitive_functions
1797 .contains(&span_key(node.span))
1798 {
1799 self.unsafe_function_depth += 1;
1800 return;
1801 }
1802 if self.pass == PointPass::Functions && !self.unsafe_context() {
1803 let point = self.point(point_span, "function", label);
1804 self.function_targets
1805 .insert(span_key(node.span), point.id.clone());
1806 self.points.push(point);
1807 }
1808 }
1809
1810 fn exit_arrow_function_expression(
1811 &mut self,
1812 node: &mut ArrowFunctionExpression<'a>,
1813 _context: &mut TraverseCtx<'a, ()>,
1814 ) {
1815 self.exit_function(node.span);
1816 }
1817
1818 fn enter_with_statement(
1819 &mut self,
1820 _node: &mut WithStatement<'a>,
1821 _context: &mut TraverseCtx<'a, ()>,
1822 ) {
1823 self.with_depth += 1;
1824 }
1825
1826 fn exit_with_statement(
1827 &mut self,
1828 _node: &mut WithStatement<'a>,
1829 _context: &mut TraverseCtx<'a, ()>,
1830 ) {
1831 self.with_depth -= 1;
1832 }
1833
1834 fn enter_ts_global_declaration(
1835 &mut self,
1836 _node: &mut TSGlobalDeclaration<'a>,
1837 _context: &mut TraverseCtx<'a, ()>,
1838 ) {
1839 self.ambient_depth += 1;
1840 }
1841
1842 fn exit_ts_global_declaration(
1843 &mut self,
1844 _node: &mut TSGlobalDeclaration<'a>,
1845 _context: &mut TraverseCtx<'a, ()>,
1846 ) {
1847 self.ambient_depth -= 1;
1848 }
1849
1850 fn enter_ts_module_declaration(
1851 &mut self,
1852 node: &mut TSModuleDeclaration<'a>,
1853 _context: &mut TraverseCtx<'a, ()>,
1854 ) {
1855 if node.declare {
1856 self.ambient_depth += 1;
1857 }
1858 }
1859
1860 fn exit_ts_module_declaration(
1861 &mut self,
1862 node: &mut TSModuleDeclaration<'a>,
1863 _context: &mut TraverseCtx<'a, ()>,
1864 ) {
1865 if node.declare {
1866 self.ambient_depth -= 1;
1867 }
1868 }
1869}
1870
1871fn rendered_expression_is_measurable(expression: &Expression<'_>) -> bool {
1874 match expression {
1875 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
1877 Expression::Identifier(_)
1879 | Expression::ThisExpression(_)
1880 | Expression::StaticMemberExpression(_)
1881 | Expression::ComputedMemberExpression(_)
1882 | Expression::PrivateFieldExpression(_)
1883 | Expression::JSXElement(_)
1884 | Expression::JSXFragment(_) => false,
1885 Expression::ChainExpression(chain) => {
1888 matches!(chain.expression, ChainElement::CallExpression(_))
1889 }
1890 other => !other.is_literal(),
1891 }
1892}
1893
1894fn collect_points<'a>(
1895 allocator: &'a Allocator,
1896 program: &mut Program<'a>,
1897 source: &str,
1898 file: &str,
1899 source_sensitive_functions: &HashSet<SpanKey>,
1900 erased_imports: &HashSet<SpanKey>,
1901) -> PointAnalysis {
1902 let mut analysis = PointAnalysis::default();
1903 for pass in [PointPass::Statements, PointPass::Functions] {
1904 let mut collector = PointCollector {
1905 source,
1906 file,
1907 pass,
1908 points: Vec::new(),
1909 statement_targets: HashMap::new(),
1910 function_targets: HashMap::new(),
1911 rendered_targets: HashMap::new(),
1912 source_sensitive_functions,
1913 erased_imports,
1914 unsafe_function_depth: 0,
1915 with_depth: 0,
1916 ambient_depth: 0,
1917 };
1918 traverse_mut(&mut collector, allocator, program, Default::default(), ());
1919 analysis.points.extend(collector.points);
1920 analysis
1921 .statement_targets
1922 .extend(collector.statement_targets);
1923 analysis.function_targets.extend(collector.function_targets);
1924 analysis.rendered_targets.extend(collector.rendered_targets);
1925 }
1926 analysis
1927}
1928
1929impl<'s> SafetyScanner<'s> {
1930 fn new(source: &'s str, file: &'s str) -> Self {
1931 Self {
1932 source,
1933 file,
1934 source_sensitive_functions: HashSet::new(),
1935 with_statements: HashSet::new(),
1936 function_limitations: Vec::new(),
1937 with_limitations: Vec::new(),
1938 dynamic_limitations: Vec::new(),
1939 unsafe_function_depth: 0,
1940 with_depth: 0,
1941 }
1942 }
1943
1944 fn limitation(
1945 &self,
1946 span: Span,
1947 kind: &str,
1948 suffix: &str,
1949 reason: &str,
1950 ) -> CandidateLimitation {
1951 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
1952 CandidateLimitation {
1953 id: stable_id(self.source, self.file, kind, span, suffix),
1954 kind: kind.to_string(),
1955 file: self.file.to_string(),
1956 line,
1957 column,
1958 source: source_slice(self.source, span).to_string(),
1959 reason: reason.to_string(),
1960 }
1961 }
1962
1963 fn enter_source_sensitive_function<State>(
1964 &mut self,
1965 span: Span,
1966 context: &TraverseCtx<'_, State>,
1967 ) {
1968 if compile_time_macro_argument(context) {
1974 self.source_sensitive_functions.insert(span_key(span));
1975 self.unsafe_function_depth += 1;
1976 return;
1977 }
1978 let sensitive = observes_function_source(span, context);
1979 if sensitive {
1980 self.source_sensitive_functions.insert(span_key(span));
1981 let limitation = self.limitation(
1982 span,
1983 "semantic-safety",
1984 "function-source",
1985 "function body is left uninstrumented because this expression observes or coerces Function source text",
1986 );
1987 self.function_limitations.push(limitation);
1988 self.unsafe_function_depth += 1;
1989 }
1990 }
1991
1992 fn exit_source_sensitive_function(&mut self, span: Span) {
1993 if self.source_sensitive_functions.contains(&span_key(span)) {
1994 self.unsafe_function_depth -= 1;
1995 }
1996 }
1997
1998 fn is_unsafe_context(&self) -> bool {
1999 self.unsafe_function_depth > 0 || self.with_depth > 0
2000 }
2001}
2002
2003impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
2004 fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
2005 self.enter_source_sensitive_function(node.span, context);
2006 }
2007
2008 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
2009 self.exit_source_sensitive_function(node.span);
2010 }
2011
2012 fn enter_arrow_function_expression(
2013 &mut self,
2014 node: &mut ArrowFunctionExpression<'a>,
2015 context: &mut TraverseCtx<'a, ()>,
2016 ) {
2017 self.enter_source_sensitive_function(node.span, context);
2018 }
2019
2020 fn exit_arrow_function_expression(
2021 &mut self,
2022 node: &mut ArrowFunctionExpression<'a>,
2023 _context: &mut TraverseCtx<'a, ()>,
2024 ) {
2025 self.exit_source_sensitive_function(node.span);
2026 }
2027
2028 fn enter_with_statement(
2029 &mut self,
2030 node: &mut WithStatement<'a>,
2031 _context: &mut TraverseCtx<'a, ()>,
2032 ) {
2033 self.with_statements.insert(span_key(node.span));
2034 let limitation = self.limitation(
2035 node.span,
2036 "semantic-safety",
2037 "with-environment",
2038 "with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
2039 );
2040 self.with_limitations.push(limitation);
2041 self.with_depth += 1;
2042 }
2043
2044 fn exit_with_statement(
2045 &mut self,
2046 _node: &mut WithStatement<'a>,
2047 _context: &mut TraverseCtx<'a, ()>,
2048 ) {
2049 self.with_depth -= 1;
2050 }
2051
2052 fn enter_call_expression(
2053 &mut self,
2054 node: &mut CallExpression<'a>,
2055 _context: &mut TraverseCtx<'a, ()>,
2056 ) {
2057 if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
2058 return;
2059 }
2060 let limitation = self.limitation(
2061 node.span,
2062 "dynamic-code",
2063 "eval",
2064 "eval-generated source has no stable pre-run coverage denominator",
2065 );
2066 self.dynamic_limitations.push(limitation);
2067 }
2068
2069 fn enter_new_expression(
2070 &mut self,
2071 node: &mut NewExpression<'a>,
2072 _context: &mut TraverseCtx<'a, ()>,
2073 ) {
2074 if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
2075 return;
2076 }
2077 let limitation = self.limitation(
2078 node.span,
2079 "dynamic-code",
2080 "Function",
2081 "Function-generated source has no stable pre-run coverage denominator",
2082 );
2083 self.dynamic_limitations.push(limitation);
2084 }
2085}
2086
2087fn analyze_safety<'a>(
2088 allocator: &'a Allocator,
2089 program: &mut Program<'a>,
2090 source: &str,
2091 file: &str,
2092) -> SafetyAnalysis {
2093 SemanticBuilder::new().build(program);
2096 let mut scanner = SafetyScanner::new(source, file);
2097 traverse_mut(&mut scanner, allocator, program, Default::default(), ());
2098 let mut semantic_limitations = scanner.function_limitations;
2099 semantic_limitations.extend(scanner.with_limitations);
2100 SafetyAnalysis {
2101 source_sensitive_functions: scanner.source_sensitive_functions,
2102 with_statements: scanner.with_statements,
2103 semantic_limitations,
2104 dynamic_limitations: scanner.dynamic_limitations,
2105 }
2106}
2107
2108fn span_key(span: Span) -> SpanKey {
2109 (span.start, span.end)
2110}
2111
2112fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
2113 matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
2114}
2115
2116fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
2117 match pattern {
2118 BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
2119 _ => None,
2120 }
2121}
2122
2123fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
2124 match expression {
2125 Expression::ArrowFunctionExpression(_) => true,
2126 Expression::FunctionExpression(function) => function.id.is_none(),
2127 Expression::ClassExpression(class) => class.id.is_none(),
2128 Expression::ParenthesizedExpression(expression) => {
2129 expression_is_anonymous_definition(&expression.expression)
2130 }
2131 Expression::TSAsExpression(expression) => {
2132 expression_is_anonymous_definition(&expression.expression)
2133 }
2134 Expression::TSSatisfiesExpression(expression) => {
2135 expression_is_anonymous_definition(&expression.expression)
2136 }
2137 Expression::TSTypeAssertion(expression) => {
2138 expression_is_anonymous_definition(&expression.expression)
2139 }
2140 Expression::TSNonNullExpression(expression) => {
2141 expression_is_anonymous_definition(&expression.expression)
2142 }
2143 _ => false,
2144 }
2145}
2146
2147struct AssignmentNameSafetyTransformer<'a> {
2148 ast: AstBuilder<'a>,
2149 parenthesized_assignment_value: String,
2150}
2151
2152impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
2153 fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
2154 walk_mut::walk_assignment_expression(self, assignment);
2155 let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
2156 return;
2157 };
2158 if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
2159 || assignment.span.start == identifier.span.start
2160 || !expression_is_anonymous_definition(&assignment.right)
2161 {
2162 return;
2163 }
2164 let right = assignment.right.take_in(self.ast.allocator);
2165 assignment.right = self.ast.expression_call(
2166 Span::default(),
2167 self.ast.expression_identifier(
2168 Span::default(),
2169 self.ast.ident(&self.parenthesized_assignment_value),
2170 ),
2171 NONE,
2172 self.ast.vec_from_array([
2173 Argument::from(right),
2174 Argument::from(self.ast.expression_string_literal(
2175 Span::default(),
2176 identifier.name,
2177 None,
2178 )),
2179 ]),
2180 false,
2181 );
2182 }
2183}
2184
2185const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
2190 "create",
2191 "createTheme",
2192 "defineConsts",
2193 "defineVars",
2194 "firstThatWorks",
2195 "keyframes",
2196 "positionTry",
2197 "viewTransitionClass",
2198];
2199
2200fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
2201 context.ancestors().any(|ancestor| {
2202 let Ancestor::CallExpressionArguments(parent) = ancestor else {
2203 return false;
2204 };
2205 let Expression::StaticMemberExpression(member) = parent.callee() else {
2206 return false;
2207 };
2208 let Expression::Identifier(object) = &member.object else {
2209 return false;
2210 };
2211 object.name == "stylex"
2212 && COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
2213 })
2214}
2215
2216fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
2217 let mut child_end = span.end;
2218 for ancestor in context.ancestors() {
2219 match ancestor {
2220 Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
2221 Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
2222 Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
2223 Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
2224 Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
2225 Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
2226 Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
2227 Ancestor::LogicalExpressionLeft(parent) => {
2228 child_end = parent.span().end;
2229 }
2230 Ancestor::LogicalExpressionRight(parent) => {
2231 child_end = parent.span().end;
2232 }
2233 Ancestor::SequenceExpressionExpressions(parent) => {
2234 if child_end != parent.span().end {
2235 return false;
2236 }
2237 child_end = parent.span().end;
2238 }
2239 Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
2240 Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
2241 Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
2242 Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
2243 Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
2244 Ancestor::ComputedMemberExpressionExpression(_) => return true,
2245 Ancestor::StaticMemberExpressionObject(parent) => {
2246 return parent.property().name == "toString";
2247 }
2248 Ancestor::CallExpressionArguments(parent) => {
2249 return expression_is_identifier(parent.callee(), "String");
2250 }
2251 Ancestor::BinaryExpressionLeft(parent) => {
2252 return matches!(
2253 parent.operator(),
2254 BinaryOperator::Addition
2255 | BinaryOperator::LessThan
2256 | BinaryOperator::LessEqualThan
2257 | BinaryOperator::GreaterThan
2258 | BinaryOperator::GreaterEqualThan
2259 );
2260 }
2261 Ancestor::BinaryExpressionRight(parent) => {
2262 return matches!(
2263 parent.operator(),
2264 BinaryOperator::Addition
2265 | BinaryOperator::LessThan
2266 | BinaryOperator::LessEqualThan
2267 | BinaryOperator::GreaterThan
2268 | BinaryOperator::GreaterEqualThan
2269 );
2270 }
2271 _ => return false,
2272 }
2273 }
2274 false
2275}
2276
2277pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
2278 let elide_type_imports = false;
2279 let source_type = SourceType::from_path(Path::new(file))
2280 .map_err(|error| CandidateError::UnknownSourceType(error.to_string()))?;
2281 let allocator = Allocator::default();
2282 let mut parsed = Parser::new(&allocator, source, source_type).parse();
2283 if !parsed.errors.is_empty() {
2284 return Err(CandidateError::Parse(
2285 parsed
2286 .errors
2287 .into_iter()
2288 .map(|error| format!("{error:?}"))
2289 .collect(),
2290 ));
2291 }
2292
2293 let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
2294 let erased = erased_imports(&parsed.program, elide_type_imports);
2295 let excluded_statements = parsed
2296 .program
2297 .body
2298 .iter()
2299 .filter(|s| erased.contains(&span_key(s.span())))
2300 .map(|s| {
2301 let span = s.span();
2302 let (line, column) = line_and_utf16_column(source, span.start as usize);
2303 CandidatePoint {
2304 id: stable_id(source, file, "statement", span, ""),
2305 kind: "statement".into(),
2306 file: file.into(),
2307 line,
2308 column,
2309 source: source_slice(source, span).into(),
2310 label: Some("typescript-import-erasure".into()),
2311 }
2312 })
2313 .collect();
2314 let point_analysis = collect_points(
2315 &allocator,
2316 &mut parsed.program,
2317 source,
2318 file,
2319 &safety.source_sensitive_functions,
2320 &erased,
2321 );
2322 let mut collector = DecisionCollector {
2323 source,
2324 file,
2325 decisions: Vec::new(),
2326 decision_vector_counts: Vec::new(),
2327 decision_logical_nodes: HashSet::new(),
2328 source_sensitive_functions: &safety.source_sensitive_functions,
2329 with_statements: &safety.with_statements,
2330 };
2331 collector.visit_program(&parsed.program);
2332 let optional_analysis = collect_optional_member_branches(
2333 &allocator,
2334 &mut parsed.program,
2335 source,
2336 file,
2337 &safety.source_sensitive_functions,
2338 );
2339 let call_analysis = collect_optional_call_branches(
2340 &allocator,
2341 &mut parsed.program,
2342 source,
2343 file,
2344 &safety.source_sensitive_functions,
2345 );
2346 let assignment_analysis = collect_logical_assignment_branches(
2347 &allocator,
2348 &mut parsed.program,
2349 source,
2350 file,
2351 &safety.source_sensitive_functions,
2352 );
2353 let default_analysis = collect_default_branches(
2354 &allocator,
2355 &mut parsed.program,
2356 source,
2357 file,
2358 &safety.source_sensitive_functions,
2359 );
2360 let extended_analysis = collect_extended_branches(
2361 &allocator,
2362 &mut parsed.program,
2363 source,
2364 file,
2365 &safety.source_sensitive_functions,
2366 );
2367 let logical_analysis = collect_logical_value_branches(
2368 &allocator,
2369 &mut parsed.program,
2370 source,
2371 file,
2372 &collector.decision_logical_nodes,
2373 &safety.source_sensitive_functions,
2374 );
2375 let switch_analysis = collect_switch_branches(
2376 &allocator,
2377 &mut parsed.program,
2378 source,
2379 file,
2380 &safety.source_sensitive_functions,
2381 );
2382 let mut branches = optional_analysis.branches;
2383 branches.extend(call_analysis.branches);
2384 branches.extend(assignment_analysis.branches);
2385 branches.extend(default_analysis.branches);
2386 branches.extend(extended_analysis.branches);
2387 branches.extend(logical_analysis.branches);
2388 branches.extend(switch_analysis.branches);
2389 let (generated, map) = generate_candidate(&parsed.program, file)?;
2390 Ok(CandidateOutput {
2391 engine: "rust-oxc".to_string(),
2392 complete: false,
2393 supported_surface: "control-decision-manifest-v1".to_string(),
2394 code: generated,
2395 map,
2396 decisions: collector.decisions,
2397 points: point_analysis.points,
2398 excluded_statements,
2399 branches,
2400 runtime: None,
2401 coverage_limitations: {
2402 let mut limitations = safety.semantic_limitations;
2403 limitations.extend(call_analysis.limitations);
2404 limitations.extend(default_analysis.limitations);
2405 limitations.extend(safety.dynamic_limitations);
2406 limitations
2407 },
2408 limitations: vec![
2409 "candidate emits metadata only; use the private differential transform for probes"
2410 .to_string(),
2411 "coverage points, value branches, and extended branch obligations are not included"
2412 .to_string(),
2413 ],
2414 })
2415}
2416
2417fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
2418 match value {
2419 serde_json::Value::Null => ast.expression_null_literal(Span::default()),
2420 serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
2421 serde_json::Value::Number(value) => ast.expression_numeric_literal(
2422 Span::default(),
2423 value
2424 .as_f64()
2425 .expect("coverage registration numbers must fit JavaScript"),
2426 None,
2427 NumberBase::Decimal,
2428 ),
2429 serde_json::Value::String(value) => {
2430 ast.expression_string_literal(Span::default(), ast.str(value), None)
2431 }
2432 serde_json::Value::Array(values) => ast.expression_array(
2433 Span::default(),
2434 ast.vec_from_iter(
2435 values
2436 .iter()
2437 .map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
2438 ),
2439 ),
2440 serde_json::Value::Object(properties) => ast.expression_object(
2441 Span::default(),
2442 ast.vec_from_iter(properties.iter().map(|(key, value)| {
2443 ast.object_property_kind_object_property(
2444 Span::default(),
2445 PropertyKind::Init,
2446 ast.property_key_static_identifier(Span::default(), ast.ident(key)),
2447 json_expression(ast, value),
2448 false,
2449 false,
2450 false,
2451 )
2452 })),
2453 ),
2454 }
2455}
2456
2457pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
2462 instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None, false)
2463}
2464
2465pub fn instrument_candidate_with_runtime_hooks(
2466 source: &str,
2467 file: &str,
2468 capability_wrapper: &str,
2469) -> Result<CandidateOutput, CandidateError> {
2470 instrument_candidate_with_binding(
2471 source,
2472 file,
2473 RuntimeBinding::ModuleImport,
2474 Some(capability_wrapper),
2475 false,
2476 )
2477}
2478
2479pub fn instrument_direct_candidate(
2484 source: &str,
2485 file: &str,
2486) -> Result<CandidateOutput, CandidateError> {
2487 instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None, false)
2488}
2489
2490pub fn instrument_direct_candidate_with_runtime_hooks(
2491 source: &str,
2492 file: &str,
2493 capability_wrapper: &str,
2494) -> Result<CandidateOutput, CandidateError> {
2495 instrument_candidate_with_binding(
2496 source,
2497 file,
2498 RuntimeBinding::DirectGlobal,
2499 Some(capability_wrapper),
2500 false,
2501 )
2502}
2503
2504pub fn instrument_with_import_policy(
2505 source: &str,
2506 file: &str,
2507 capability_wrapper: &str,
2508 direct: bool,
2509 elide_type_imports: bool,
2510) -> Result<CandidateOutput, CandidateError> {
2511 instrument_candidate_with_binding(
2512 source,
2513 file,
2514 if direct {
2515 RuntimeBinding::DirectGlobal
2516 } else {
2517 RuntimeBinding::ModuleImport
2518 },
2519 Some(capability_wrapper),
2520 elide_type_imports,
2521 )
2522}
2523
2524fn instrument_candidate_with_binding(
2525 source: &str,
2526 file: &str,
2527 runtime_binding: RuntimeBinding,
2528 capability_wrapper: Option<&str>,
2529 elide_type_imports: bool,
2530) -> Result<CandidateOutput, CandidateError> {
2531 let source_type = SourceType::from_path(Path::new(file))
2532 .map_err(|error| CandidateError::UnknownSourceType(error.to_string()))?;
2533 let allocator = Allocator::default();
2534 let mut parsed = Parser::new(&allocator, source, source_type).parse();
2535 if !parsed.errors.is_empty() {
2536 return Err(CandidateError::Parse(
2537 parsed
2538 .errors
2539 .into_iter()
2540 .map(|error| format!("{error:?}"))
2541 .collect(),
2542 ));
2543 }
2544
2545 let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
2546 let erased = erased_imports(&parsed.program, elide_type_imports);
2547 let excluded_statements = parsed
2548 .program
2549 .body
2550 .iter()
2551 .filter(|s| erased.contains(&span_key(s.span())))
2552 .map(|s| {
2553 let span = s.span();
2554 let (line, column) = line_and_utf16_column(source, span.start as usize);
2555 CandidatePoint {
2556 id: stable_id(source, file, "statement", span, ""),
2557 kind: "statement".into(),
2558 file: file.into(),
2559 line,
2560 column,
2561 source: source_slice(source, span).into(),
2562 label: Some("typescript-import-erasure".into()),
2563 }
2564 })
2565 .collect();
2566 let point_analysis = collect_points(
2567 &allocator,
2568 &mut parsed.program,
2569 source,
2570 file,
2571 &safety.source_sensitive_functions,
2572 &erased,
2573 );
2574 let mut collector = DecisionCollector {
2575 source,
2576 file,
2577 decisions: Vec::new(),
2578 decision_vector_counts: Vec::new(),
2579 decision_logical_nodes: HashSet::new(),
2580 source_sensitive_functions: &safety.source_sensitive_functions,
2581 with_statements: &safety.with_statements,
2582 };
2583 collector.visit_program(&parsed.program);
2584 let optional_analysis = collect_optional_member_branches(
2585 &allocator,
2586 &mut parsed.program,
2587 source,
2588 file,
2589 &safety.source_sensitive_functions,
2590 );
2591 let call_analysis = collect_optional_call_branches(
2592 &allocator,
2593 &mut parsed.program,
2594 source,
2595 file,
2596 &safety.source_sensitive_functions,
2597 );
2598 let assignment_analysis = collect_logical_assignment_branches(
2599 &allocator,
2600 &mut parsed.program,
2601 source,
2602 file,
2603 &safety.source_sensitive_functions,
2604 );
2605 let default_analysis = collect_default_branches(
2606 &allocator,
2607 &mut parsed.program,
2608 source,
2609 file,
2610 &safety.source_sensitive_functions,
2611 );
2612 let extended_analysis = collect_extended_branches(
2613 &allocator,
2614 &mut parsed.program,
2615 source,
2616 file,
2617 &safety.source_sensitive_functions,
2618 );
2619 let logical_analysis = collect_logical_value_branches(
2620 &allocator,
2621 &mut parsed.program,
2622 source,
2623 file,
2624 &collector.decision_logical_nodes,
2625 &safety.source_sensitive_functions,
2626 );
2627 let switch_analysis = collect_switch_branches(
2628 &allocator,
2629 &mut parsed.program,
2630 source,
2631 file,
2632 &safety.source_sensitive_functions,
2633 );
2634 let mut branches = optional_analysis.branches;
2635 branches.extend(call_analysis.branches.clone());
2636 branches.extend(assignment_analysis.branches);
2637 branches.extend(default_analysis.branches.clone());
2638 branches.extend(extended_analysis.branches.clone());
2639 branches.extend(logical_analysis.branches);
2640 branches.extend(switch_analysis.branches.clone());
2641
2642 let mut names = CandidateNames::new(source);
2643 let mcdc_begin = names.allocate("__supercovMcdcBegin");
2644 let mcdc_condition = names.allocate("__supercovMcdcCondition");
2645 let mcdc_end = names.allocate("__supercovMcdcEnd");
2646 let coverage_hit = names.allocate("__supercovCoverageHit");
2647 let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
2648 let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
2649 let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
2650 let probe_file_v2 = names.allocate("__supercovProbeFileV2");
2651 let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
2652 let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
2653 let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
2654 let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
2655 let selection_begin = names.allocate("__supercovSelectionBegin");
2656 let selection_right = names.allocate("__supercovSelectionRight");
2657 let selection_end = names.allocate("__supercovSelectionEnd");
2658 let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
2659 let with_request_phase = names.allocate("__supercovWithRequestPhase");
2660 let optional_select = names.allocate("__supercovOptionalSelect");
2661 let rendered_value_v2 = names.allocate("__supercovRenderedValueV2");
2662 let optional_call_begin = names.allocate("__supercovOptionalCallBegin");
2663 let optional_call_reached = names.allocate("__supercovOptionalCallReached");
2664 let optional_call_continued = names.allocate("__supercovOptionalCallContinued");
2665 let optional_call_end = names.allocate("__supercovOptionalCallEnd");
2666 let default_selected = names.allocate("__supercovDefaultSelected");
2667 let default_entered = names.allocate("__supercovDefaultEntered");
2668 let try_begin = names.allocate("__supercovTryBegin");
2669 let try_catch = names.allocate("__supercovTryCatch");
2670 let try_end = names.allocate("__supercovTryEnd");
2671 let loop_begin = names.allocate("__supercovLoopBegin");
2672 let loop_entered = names.allocate("__supercovLoopEntered");
2673 let loop_end = names.allocate("__supercovLoopEnd");
2674 let ast = AstBuilder::new(&allocator);
2675 let mut assignment_name_safety = AssignmentNameSafetyTransformer {
2676 ast,
2677 parenthesized_assignment_value: parenthesized_assignment_value.clone(),
2678 };
2679 assignment_name_safety.visit_program(&mut parsed.program);
2680 let point_indices = point_analysis
2681 .points
2682 .iter()
2683 .enumerate()
2684 .map(|(index, point)| (point.id.clone(), index))
2685 .collect::<HashMap<_, _>>();
2686 let statement_targets = point_analysis
2687 .statement_targets
2688 .into_iter()
2689 .map(|(span, ids)| {
2690 (
2691 span,
2692 ids.into_iter()
2693 .map(|id| PointTarget {
2694 index: *point_indices
2695 .get(&id)
2696 .expect("statement point must have a global index"),
2697 })
2698 .collect(),
2699 )
2700 })
2701 .collect();
2702 let function_targets = point_analysis
2703 .function_targets
2704 .into_iter()
2705 .map(|(span, id)| {
2706 (
2707 span,
2708 PointTarget {
2709 index: *point_indices
2710 .get(&id)
2711 .expect("function point must have a global index"),
2712 },
2713 )
2714 })
2715 .collect();
2716 let rendered_targets = point_analysis
2717 .rendered_targets
2718 .into_iter()
2719 .map(|(span, id)| {
2720 (
2721 span,
2722 PointTarget {
2723 index: *point_indices
2724 .get(&id)
2725 .expect("rendered point must have a global index"),
2726 },
2727 )
2728 })
2729 .collect();
2730 let mut statement_transformer = StatementProbeTransformer {
2731 ast,
2732 coverage_hit_v2: coverage_hit_v2.clone(),
2733 probe_file_v2: probe_file_v2.clone(),
2734 targets: statement_targets,
2735 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2736 with_statements: safety.with_statements.clone(),
2737 };
2738 statement_transformer.visit_program(&mut parsed.program);
2739 let mut function_transformer = FunctionProbeTransformer {
2740 ast,
2741 coverage_hit_v2: coverage_hit_v2.clone(),
2742 probe_file_v2: probe_file_v2.clone(),
2743 targets: function_targets,
2744 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2745 };
2746 function_transformer.visit_program(&mut parsed.program);
2747 let mut rendered_transformer = RenderedExpressionTransformer {
2748 ast,
2749 rendered_value_v2: rendered_value_v2.clone(),
2750 probe_file_v2: probe_file_v2.clone(),
2751 targets: rendered_targets,
2752 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2753 };
2754 rendered_transformer.visit_program(&mut parsed.program);
2755 let mut optional_transformer = OptionalMemberTransformer {
2756 ast,
2757 optional_select: optional_select.clone(),
2758 targets: optional_analysis.targets,
2759 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2760 with_statements: safety.with_statements.clone(),
2761 };
2762 optional_transformer.visit_program(&mut parsed.program);
2763 let mut call_transformer = OptionalCallTransformer::new(
2764 ast,
2765 source,
2766 optional_call_begin.clone(),
2767 optional_call_reached.clone(),
2768 optional_call_continued.clone(),
2769 optional_call_end.clone(),
2770 call_analysis.sites,
2771 call_analysis.roots,
2772 safety.source_sensitive_functions.clone(),
2773 safety.with_statements.clone(),
2774 );
2775 call_transformer.visit_program(&mut parsed.program);
2776 let mut default_transformer = DefaultTransformer {
2777 ast,
2778 default_selected: default_selected.clone(),
2779 default_entered: default_entered.clone(),
2780 parameter_targets: default_analysis.parameter_targets,
2781 binding_targets: default_analysis.binding_targets,
2782 function_entries: Vec::new(),
2783 declaration_entries: HashMap::new(),
2784 active_declaration: Vec::new(),
2785 parameter_pattern_depth: 0,
2786 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2787 with_statements: safety.with_statements.clone(),
2788 };
2789 default_transformer.visit_program(&mut parsed.program);
2790 let mut extended_transformer = ExtendedTransformer {
2791 ast,
2792 try_begin: try_begin.clone(),
2793 try_catch: try_catch.clone(),
2794 try_end: try_end.clone(),
2795 loop_begin: loop_begin.clone(),
2796 loop_entered: loop_entered.clone(),
2797 loop_end: loop_end.clone(),
2798 try_targets: extended_analysis.try_targets,
2799 loop_targets: extended_analysis.loop_targets,
2800 names: CandidateNames::new(source),
2801 scope_declarations: Vec::new(),
2802 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2803 with_statements: safety.with_statements.clone(),
2804 };
2805 extended_transformer.visit_program(&mut parsed.program);
2806 let mut transformer = ControlProbeV2Transformer {
2807 ast,
2808 decisions: &collector.decisions,
2809 mcdc_begin: mcdc_begin.clone(),
2810 mcdc_condition: mcdc_condition.clone(),
2811 mcdc_end: mcdc_end.clone(),
2812 mcdc_end_v2: mcdc_end_v2.clone(),
2813 probe_file_v2: probe_file_v2.clone(),
2814 names,
2815 scope_declarations: Vec::new(),
2816 decision_index: 0,
2817 parameter_depth: 0,
2818 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2819 with_statements: safety.with_statements.clone(),
2820 };
2821 transformer.visit_program(&mut parsed.program);
2822 let mut logical_transformer = LogicalValueTransformer {
2823 ast,
2824 selection_begin: selection_begin.clone(),
2825 selection_right: selection_right.clone(),
2826 selection_end: selection_end.clone(),
2827 names: CandidateNames::new(source),
2828 scope_declarations: Vec::new(),
2829 logical_targets: logical_analysis.logical_targets,
2830 assignment_targets: assignment_analysis.targets,
2831 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2832 with_statements: safety.with_statements.clone(),
2833 };
2834 logical_transformer.visit_program(&mut parsed.program);
2835 let mut switch_transformer = SwitchTransformer {
2836 ast,
2837 coverage_hit: coverage_hit.clone(),
2838 targets: switch_analysis.targets,
2839 names: CandidateNames::new(source),
2840 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2841 with_statements: safety.with_statements.clone(),
2842 };
2843 switch_transformer.visit_program(&mut parsed.program);
2844 let mut route_transformer = RouteRequestPhaseTransformer {
2845 ast,
2846 file,
2847 with_request_phase: with_request_phase.clone(),
2848 used: false,
2849 names: CandidateNames::new(source),
2850 };
2851 route_transformer.transform_program(&mut parsed.program);
2852 let mut request_transformer = RequestPhaseTransformer {
2853 ast,
2854 with_request_phase: with_request_phase.clone(),
2855 used: route_transformer.used,
2856 source_sensitive_functions: safety.source_sensitive_functions.clone(),
2857 with_statements: safety.with_statements.clone(),
2858 };
2859 request_transformer.visit_program(&mut parsed.program);
2860 let uses_request_phase = request_transformer.used;
2861
2862 let registration = serde_json::json!({
2863 "decisions": &collector.decisions,
2864 "pointIds": point_analysis.points.iter().map(|point| &point.id).collect::<Vec<_>>(),
2865 "decisionVectorCounts": &collector.decision_vector_counts,
2866 });
2867 let registration_call = ast.expression_call(
2868 Span::default(),
2869 ast.expression_identifier(Span::default(), ast.ident(®ister_probe_v2)),
2870 NONE,
2871 ast.vec1(Argument::from(json_expression(ast, ®istration))),
2872 false,
2873 );
2874 parsed.program.body.insert(
2875 0,
2876 Statement::VariableDeclaration(ast.alloc_variable_declaration(
2877 Span::default(),
2878 VariableDeclarationKind::Const,
2879 ast.vec1(ast.variable_declarator(
2880 Span::default(),
2881 VariableDeclarationKind::Const,
2882 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
2883 NONE,
2884 Some(registration_call),
2885 false,
2886 )),
2887 false,
2888 )),
2889 );
2890 let mut runtime_imports = vec![
2891 ("mcdcBegin", &mcdc_begin),
2892 ("mcdcCondition", &mcdc_condition),
2893 ("mcdcEnd", &mcdc_end),
2894 ("coverageHit", &coverage_hit),
2895 ("registerProbeV2", ®ister_probe_v2),
2896 ("mcdcEndV2", &mcdc_end_v2),
2897 ("coverageHitV2", &coverage_hit_v2),
2898 ("selectionBegin", &selection_begin),
2899 ("selectionRight", &selection_right),
2900 ("selectionEnd", &selection_end),
2901 (
2902 "parenthesizedAssignmentValue",
2903 &parenthesized_assignment_value,
2904 ),
2905 ("optionalSelect", &optional_select),
2906 ("renderedValueV2", &rendered_value_v2),
2907 ("optionalCallBegin", &optional_call_begin),
2908 ("optionalCallReached", &optional_call_reached),
2909 ("optionalCallContinued", &optional_call_continued),
2910 ("optionalCallEnd", &optional_call_end),
2911 ("defaultSelected", &default_selected),
2912 ("defaultEntered", &default_entered),
2913 ("tryBegin", &try_begin),
2914 ("tryCatch", &try_catch),
2915 ("tryEnd", &try_end),
2916 ("loopBegin", &loop_begin),
2917 ("loopEntered", &loop_entered),
2918 ("loopEnd", &loop_end),
2919 ];
2920 if uses_request_phase {
2921 runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
2922 }
2923 if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
2924 let declarators =
2925 ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
2926 let global_runtime =
2927 Expression::StaticMemberExpression(ast.alloc_static_member_expression(
2928 Span::default(),
2929 ast.expression_identifier(Span::default(), ast.ident("globalThis")),
2930 ast.identifier_name(
2931 Span::default(),
2932 ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
2933 "__SUPERCOV_DIRECT_RUNTIME__"
2934 } else {
2935 "__supercovRuntime"
2936 }),
2937 ),
2938 false,
2939 ));
2940 let runtime_helper =
2941 Expression::StaticMemberExpression(ast.alloc_static_member_expression(
2942 Span::default(),
2943 global_runtime,
2944 ast.identifier_name(Span::default(), ast.ident(imported)),
2945 false,
2946 ));
2947 ast.variable_declarator(
2948 Span::default(),
2949 VariableDeclarationKind::Const,
2950 ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
2951 NONE,
2952 Some(runtime_helper),
2953 false,
2954 )
2955 }));
2956 parsed.program.body.insert(
2957 0,
2958 Statement::VariableDeclaration(ast.alloc_variable_declaration(
2959 Span::default(),
2960 VariableDeclarationKind::Const,
2961 declarators,
2962 false,
2963 )),
2964 );
2965 } else {
2966 let import_specifiers =
2967 ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
2968 ast.import_declaration_specifier_import_specifier(
2969 Span::default(),
2970 ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
2971 ast.binding_identifier(Span::default(), ast.ident(local)),
2972 oxc_ast::ast::ImportOrExportKind::Value,
2973 )
2974 }));
2975 parsed.program.body.insert(
2976 0,
2977 Statement::ImportDeclaration(ast.alloc_import_declaration(
2978 Span::default(),
2979 Some(import_specifiers),
2980 ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
2981 None,
2982 NONE,
2983 oxc_ast::ast::ImportOrExportKind::Value,
2984 )),
2985 );
2986 }
2987
2988 if let Some(wrapper) = capability_wrapper {
2989 transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
2990 }
2991 let limitations = Vec::new();
2992 let (code, map) = generate_candidate(&parsed.program, file)?;
2993 Ok(CandidateOutput {
2994 engine: "rust-oxc".to_string(),
2995 complete: true,
2996 supported_surface: "complete-js-instrumenter-v1".to_string(),
2997 code,
2998 map,
2999 decisions: collector.decisions,
3000 points: point_analysis.points,
3001 excluded_statements,
3002 branches,
3003 runtime: Some(CandidateRuntime {
3004 coverage_hit,
3005 mcdc_begin,
3006 mcdc_condition,
3007 mcdc_end,
3008 register_probe_v2,
3009 mcdc_end_v2,
3010 coverage_hit_v2,
3011 probe_file_v2,
3012 selection_begin,
3013 selection_right,
3014 selection_end,
3015 parenthesized_assignment_value,
3016 with_request_phase,
3017 optional_select,
3018 rendered_value_v2,
3019 optional_call_begin,
3020 optional_call_reached,
3021 optional_call_continued,
3022 optional_call_end,
3023 default_selected,
3024 default_entered,
3025 try_begin,
3026 try_catch,
3027 try_end,
3028 loop_begin,
3029 loop_entered,
3030 loop_end,
3031 }),
3032 coverage_limitations: {
3033 let mut limitations = safety.semantic_limitations;
3034 limitations.extend(call_analysis.limitations);
3035 limitations.extend(default_analysis.limitations);
3036 limitations.extend(safety.dynamic_limitations);
3037 limitations
3038 },
3039 limitations,
3040 })
3041}
3042
3043struct StatementProbeTransformer<'a> {
3044 ast: AstBuilder<'a>,
3045 coverage_hit_v2: String,
3046 probe_file_v2: String,
3047 targets: HashMap<SpanKey, Vec<PointTarget>>,
3048 source_sensitive_functions: HashSet<SpanKey>,
3049 with_statements: HashSet<SpanKey>,
3050}
3051
3052impl<'a> StatementProbeTransformer<'a> {
3053 fn probe(&self, target: &PointTarget) -> Statement<'a> {
3054 self.ast.statement_expression(
3055 Span::default(),
3056 self.ast.expression_call(
3057 Span::default(),
3058 self.ast
3059 .expression_identifier(Span::default(), self.ast.ident(&self.coverage_hit_v2)),
3060 NONE,
3061 self.ast.vec_from_array([
3062 Argument::from(self.ast.expression_identifier(
3063 Span::default(),
3064 self.ast.ident(&self.probe_file_v2),
3065 )),
3066 Argument::from(self.ast.expression_numeric_literal(
3067 Span::default(),
3068 target.index as f64,
3069 None,
3070 NumberBase::Decimal,
3071 )),
3072 ]),
3073 false,
3074 ),
3075 )
3076 }
3077
3078 fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
3079 let mut ids = self
3080 .targets
3081 .remove(&span_key(statement.span()))
3082 .unwrap_or_default();
3083 if let Statement::ExportNamedDeclaration(export) = statement
3084 && let Some(declaration) = &export.declaration
3085 && let Some(nested) = self.targets.remove(&span_key(declaration.span()))
3086 {
3087 ids.extend(nested);
3088 }
3089 if let Statement::ExportDefaultDeclaration(export) = statement
3090 && let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
3091 && let Some(nested) = self.targets.remove(&span_key(class.span))
3092 {
3093 ids.extend(nested);
3094 }
3095 ids
3096 }
3097
3098 fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
3099 let ids = self.take_statement_ids(statement);
3100 if ids.is_empty() {
3101 return;
3102 }
3103 let original = statement.take_in(self.ast.allocator);
3104 let mut body = self.ast.vec_with_capacity(ids.len() + 1);
3105 body.extend(ids.iter().map(|target| self.probe(target)));
3106 body.push(original);
3107 *statement = self.ast.statement_block(Span::default(), body);
3108 }
3109}
3110
3111impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
3112 fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
3113 let original = statements.take_in(self.ast.allocator);
3114 let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
3115 for mut statement in original {
3116 let ids = self.take_statement_ids(&statement);
3117 self.visit_statement(&mut statement);
3118 instrumented.extend(ids.iter().map(|target| self.probe(target)));
3119 instrumented.push(statement);
3120 }
3121 *statements = instrumented;
3122 }
3123
3124 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3125 if self
3126 .source_sensitive_functions
3127 .contains(&span_key(function.span))
3128 {
3129 return;
3130 }
3131 walk_mut::walk_function(self, function, flags);
3132 }
3133
3134 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3135 if self
3136 .source_sensitive_functions
3137 .contains(&span_key(function.span))
3138 {
3139 return;
3140 }
3141 walk_mut::walk_arrow_function_expression(self, function);
3142 }
3143
3144 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
3145 if self.with_statements.contains(&span_key(statement.span)) {
3146 return;
3147 }
3148 walk_mut::walk_with_statement(self, statement);
3149 }
3150
3151 fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
3152 self.wrap_bare(&mut statement.consequent);
3153 if let Some(alternate) = &mut statement.alternate {
3154 self.wrap_bare(alternate);
3155 }
3156 walk_mut::walk_if_statement(self, statement);
3157 }
3158
3159 fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
3160 self.wrap_bare(&mut statement.body);
3161 walk_mut::walk_while_statement(self, statement);
3162 }
3163
3164 fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
3165 self.wrap_bare(&mut statement.body);
3166 walk_mut::walk_do_while_statement(self, statement);
3167 }
3168
3169 fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
3170 self.wrap_bare(&mut statement.body);
3171 walk_mut::walk_for_statement(self, statement);
3172 }
3173
3174 fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
3175 self.wrap_bare(&mut statement.body);
3176 walk_mut::walk_for_in_statement(self, statement);
3177 }
3178
3179 fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
3180 self.wrap_bare(&mut statement.body);
3181 walk_mut::walk_for_of_statement(self, statement);
3182 }
3183}
3184
3185struct RenderedExpressionTransformer<'a> {
3194 ast: AstBuilder<'a>,
3195 rendered_value_v2: String,
3196 probe_file_v2: String,
3197 targets: HashMap<SpanKey, PointTarget>,
3198 source_sensitive_functions: HashSet<SpanKey>,
3199}
3200
3201impl<'a> RenderedExpressionTransformer<'a> {
3202 fn probe(&self, target: &PointTarget, value: Expression<'a>) -> Expression<'a> {
3203 self.ast.expression_call(
3204 Span::default(),
3205 self.ast
3206 .expression_identifier(Span::default(), self.ast.ident(&self.rendered_value_v2)),
3207 NONE,
3208 self.ast.vec_from_array([
3209 Argument::from(
3210 self.ast.expression_identifier(
3211 Span::default(),
3212 self.ast.ident(&self.probe_file_v2),
3213 ),
3214 ),
3215 Argument::from(self.ast.expression_numeric_literal(
3216 Span::default(),
3217 target.index as f64,
3218 None,
3219 NumberBase::Decimal,
3220 )),
3221 Argument::from(value),
3222 ]),
3223 false,
3224 )
3225 }
3226}
3227
3228impl<'a> VisitMut<'a> for RenderedExpressionTransformer<'a> {
3229 fn visit_jsx_expression_container(&mut self, container: &mut JSXExpressionContainer<'a>) {
3230 walk_mut::walk_jsx_expression_container(self, container);
3231 if matches!(container.expression, JSXExpression::EmptyExpression(_)) {
3232 return;
3233 }
3234 let span = container.expression.span();
3235 if self.source_sensitive_functions.contains(&span_key(span)) {
3236 return;
3237 }
3238 let Some(target) = self.targets.remove(&span_key(span)) else {
3239 return;
3240 };
3241 let inner = container
3242 .expression
3243 .take_in(self.ast.allocator)
3244 .into_expression();
3245 container.expression = JSXExpression::from(self.probe(&target, inner));
3246 }
3247}
3248
3249struct FunctionProbeTransformer<'a> {
3250 ast: AstBuilder<'a>,
3251 coverage_hit_v2: String,
3252 probe_file_v2: String,
3253 targets: HashMap<SpanKey, PointTarget>,
3254 source_sensitive_functions: HashSet<SpanKey>,
3255}
3256
3257impl<'a> FunctionProbeTransformer<'a> {
3258 fn probe(&self, target: &PointTarget) -> Statement<'a> {
3259 self.ast.statement_expression(
3260 Span::default(),
3261 self.ast.expression_call(
3262 Span::default(),
3263 self.ast
3264 .expression_identifier(Span::default(), self.ast.ident(&self.coverage_hit_v2)),
3265 NONE,
3266 self.ast.vec_from_array([
3267 Argument::from(self.ast.expression_identifier(
3268 Span::default(),
3269 self.ast.ident(&self.probe_file_v2),
3270 )),
3271 Argument::from(self.ast.expression_numeric_literal(
3272 Span::default(),
3273 target.index as f64,
3274 None,
3275 NumberBase::Decimal,
3276 )),
3277 ]),
3278 false,
3279 ),
3280 )
3281 }
3282}
3283
3284impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
3285 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3286 if self
3287 .source_sensitive_functions
3288 .contains(&span_key(function.span))
3289 {
3290 return;
3291 }
3292 if let Some(target) = self.targets.remove(&span_key(function.span))
3293 && let Some(body) = &mut function.body
3294 {
3295 body.statements.insert(0, self.probe(&target));
3296 }
3297 walk_mut::walk_function(self, function, flags);
3298 }
3299
3300 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3301 if self
3302 .source_sensitive_functions
3303 .contains(&span_key(function.span))
3304 {
3305 return;
3306 }
3307 if let Some(target) = self.targets.remove(&span_key(function.span)) {
3308 let probe = self.probe(&target);
3309 if function.expression {
3310 let original = function
3311 .body
3312 .statements
3313 .pop()
3314 .expect("expression arrow must contain its expression statement");
3315 let Statement::ExpressionStatement(expression) = original else {
3316 panic!("expression arrow body must be represented as an expression statement");
3317 };
3318 function.expression = false;
3319 function.body.statements.push(probe);
3320 function.body.statements.push(
3321 self.ast
3322 .statement_return(Span::default(), Some(expression.unbox().expression)),
3323 );
3324 } else {
3325 function.body.statements.insert(0, probe);
3326 }
3327 }
3328 walk_mut::walk_arrow_function_expression(self, function);
3329 }
3330}
3331
3332struct OptionalMemberTransformer<'a> {
3333 ast: AstBuilder<'a>,
3334 optional_select: String,
3335 targets: HashMap<SpanKey, (String, String)>,
3336 source_sensitive_functions: HashSet<SpanKey>,
3337 with_statements: HashSet<SpanKey>,
3338}
3339
3340impl<'a> OptionalMemberTransformer<'a> {
3341 fn instrument_operand(
3342 &self,
3343 operand: Expression<'a>,
3344 short_id: &str,
3345 continued_id: &str,
3346 ) -> Expression<'a> {
3347 self.ast.expression_call(
3348 Span::default(),
3349 self.ast
3350 .expression_identifier(Span::default(), self.ast.ident(&self.optional_select)),
3351 NONE,
3352 self.ast.vec_from_array([
3353 Argument::from(self.ast.expression_string_literal(
3354 Span::default(),
3355 self.ast.str(short_id),
3356 None,
3357 )),
3358 Argument::from(self.ast.expression_string_literal(
3359 Span::default(),
3360 self.ast.str(continued_id),
3361 None,
3362 )),
3363 Argument::from(operand),
3364 ]),
3365 false,
3366 )
3367 }
3368
3369 fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
3370 let Some((short_id, continued_id)) = self.targets.remove(&span_key(span)) else {
3371 return;
3372 };
3373 let operand = object.take_in(self.ast.allocator);
3374 *object = self.instrument_operand(operand, &short_id, &continued_id);
3375 }
3376}
3377
3378impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
3379 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3380 if self
3381 .source_sensitive_functions
3382 .contains(&span_key(function.span))
3383 {
3384 return;
3385 }
3386 walk_mut::walk_function(self, function, flags);
3387 }
3388
3389 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3390 if self
3391 .source_sensitive_functions
3392 .contains(&span_key(function.span))
3393 {
3394 return;
3395 }
3396 walk_mut::walk_arrow_function_expression(self, function);
3397 }
3398
3399 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
3400 if self.with_statements.contains(&span_key(statement.span)) {
3401 return;
3402 }
3403 walk_mut::walk_with_statement(self, statement);
3404 }
3405
3406 fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
3407 self.instrument_target(member.span, &mut member.object);
3408 walk_mut::walk_computed_member_expression(self, member);
3409 }
3410
3411 fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
3412 self.instrument_target(member.span, &mut member.object);
3413 walk_mut::walk_static_member_expression(self, member);
3414 }
3415
3416 fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
3417 self.instrument_target(member.span, &mut member.object);
3418 walk_mut::walk_private_field_expression(self, member);
3419 }
3420}
3421
3422#[derive(Clone)]
3423struct OptionalCallSiteRuntime {
3424 frame: String,
3425 short_id: String,
3426 continued_id: String,
3427}
3428
3429struct OptionalCallTransformer<'a, 's> {
3430 ast: AstBuilder<'a>,
3431 optional_call_begin: String,
3432 optional_call_reached: String,
3433 optional_call_continued: String,
3434 optional_call_end: String,
3435 scope_declarations: Vec<Vec<String>>,
3436 sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
3437 roots: HashMap<SpanKey, Vec<SpanKey>>,
3438 source_sensitive_functions: HashSet<SpanKey>,
3439 with_statements: HashSet<SpanKey>,
3440 _source: std::marker::PhantomData<&'s str>,
3441}
3442
3443impl<'a, 's> OptionalCallTransformer<'a, 's> {
3444 #[allow(clippy::too_many_arguments)]
3445 fn new(
3446 ast: AstBuilder<'a>,
3447 source: &'s str,
3448 optional_call_begin: String,
3449 optional_call_reached: String,
3450 optional_call_continued: String,
3451 optional_call_end: String,
3452 sites: HashMap<SpanKey, (String, String)>,
3453 roots: HashMap<SpanKey, Vec<SpanKey>>,
3454 source_sensitive_functions: HashSet<SpanKey>,
3455 with_statements: HashSet<SpanKey>,
3456 ) -> Self {
3457 let mut names = CandidateNames::new(source);
3458 let sites = sites
3459 .into_iter()
3460 .map(|(key, (short_id, continued_id))| {
3461 (
3462 key,
3463 OptionalCallSiteRuntime {
3464 frame: names.allocate("_optionalCall"),
3465 short_id,
3466 continued_id,
3467 },
3468 )
3469 })
3470 .collect();
3471 Self {
3472 ast,
3473 optional_call_begin,
3474 optional_call_reached,
3475 optional_call_continued,
3476 optional_call_end,
3477 scope_declarations: Vec::new(),
3478 sites,
3479 roots,
3480 source_sensitive_functions,
3481 with_statements,
3482 _source: std::marker::PhantomData,
3483 }
3484 }
3485
3486 fn identifier(&self, name: &str) -> Expression<'a> {
3487 self.ast
3488 .expression_identifier(Span::default(), self.ast.ident(name))
3489 }
3490
3491 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
3492 AssignmentTarget::from(
3493 self.ast
3494 .simple_assignment_target_assignment_target_identifier(
3495 Span::default(),
3496 self.ast.ident(name),
3497 ),
3498 )
3499 }
3500
3501 fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
3502 self.ast.expression_call(
3503 Span::default(),
3504 self.identifier(name),
3505 NONE,
3506 arguments,
3507 false,
3508 )
3509 }
3510
3511 fn string_argument(&self, value: &str) -> Argument<'a> {
3512 Argument::from(self.ast.expression_string_literal(
3513 Span::default(),
3514 self.ast.str(value),
3515 None,
3516 ))
3517 }
3518
3519 fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
3520 self.call(
3521 &self.optional_call_reached,
3522 self.ast.vec_from_array([
3523 Argument::from(self.identifier(frame)),
3524 Argument::from(value),
3525 ]),
3526 )
3527 }
3528
3529 fn enter_scope(&mut self) {
3530 self.scope_declarations.push(Vec::new());
3531 }
3532
3533 fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
3534 let names = self
3535 .scope_declarations
3536 .pop()
3537 .expect("optional-call scope stack must remain balanced");
3538 if names.is_empty() {
3539 return;
3540 }
3541 let declarations = self.ast.vec_from_iter(names.into_iter().map(|name| {
3542 self.ast.variable_declarator(
3543 Span::default(),
3544 VariableDeclarationKind::Let,
3545 self.ast
3546 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
3547 NONE,
3548 None,
3549 false,
3550 )
3551 }));
3552 statements.insert(
3553 0,
3554 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
3555 Span::default(),
3556 VariableDeclarationKind::Let,
3557 declarations,
3558 false,
3559 )),
3560 );
3561 }
3562
3563 fn instrument_callee(
3564 &self,
3565 callee: Expression<'a>,
3566 site: &OptionalCallSiteRuntime,
3567 ) -> Expression<'a> {
3568 match callee {
3569 Expression::ComputedMemberExpression(mut member) => {
3570 let property = member.expression.take_in(self.ast.allocator);
3571 member.expression = self.reached(&site.frame, property);
3572 Expression::ComputedMemberExpression(member)
3573 }
3574 Expression::StaticMemberExpression(member) => {
3575 let member = member.unbox();
3576 let property = self.ast.expression_string_literal(
3577 Span::default(),
3578 self.ast.str(member.property.name.as_str()),
3579 None,
3580 );
3581 Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
3582 member.span,
3583 member.object,
3584 self.reached(&site.frame, property),
3585 member.optional,
3586 ))
3587 }
3588 Expression::PrivateFieldExpression(mut member) => {
3589 let object = member.object.take_in(self.ast.allocator);
3590 member.object = self.reached(&site.frame, object);
3591 Expression::PrivateFieldExpression(member)
3592 }
3593 Expression::ChainExpression(mut chain) => {
3594 match &mut chain.expression {
3595 ChainElement::ComputedMemberExpression(member) => {
3596 let property = member.expression.take_in(self.ast.allocator);
3597 member.expression = self.reached(&site.frame, property);
3598 }
3599 ChainElement::StaticMemberExpression(member) => {
3600 let member = member.take_in(self.ast.allocator);
3601 let property = self.ast.expression_string_literal(
3602 Span::default(),
3603 self.ast.str(member.property.name.as_str()),
3604 None,
3605 );
3606 chain.expression = ChainElement::ComputedMemberExpression(
3607 self.ast.alloc_computed_member_expression(
3608 member.span,
3609 member.object,
3610 self.reached(&site.frame, property),
3611 member.optional,
3612 ),
3613 );
3614 }
3615 ChainElement::PrivateFieldExpression(member) => {
3616 let object = member.object.take_in(self.ast.allocator);
3617 member.object = self.reached(&site.frame, object);
3618 }
3619 ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
3620 return self.reached(&site.frame, Expression::ChainExpression(chain));
3621 }
3622 }
3623 Expression::ChainExpression(chain)
3624 }
3625 Expression::ParenthesizedExpression(mut parenthesized) => {
3626 let inner = parenthesized.expression.take_in(self.ast.allocator);
3627 parenthesized.expression = self.instrument_callee(inner, site);
3628 Expression::ParenthesizedExpression(parenthesized)
3629 }
3630 Expression::TSAsExpression(mut wrapped) => {
3631 let inner = wrapped.expression.take_in(self.ast.allocator);
3632 wrapped.expression = self.instrument_callee(inner, site);
3633 Expression::TSAsExpression(wrapped)
3634 }
3635 Expression::TSSatisfiesExpression(mut wrapped) => {
3636 let inner = wrapped.expression.take_in(self.ast.allocator);
3637 wrapped.expression = self.instrument_callee(inner, site);
3638 Expression::TSSatisfiesExpression(wrapped)
3639 }
3640 Expression::TSTypeAssertion(mut wrapped) => {
3641 let inner = wrapped.expression.take_in(self.ast.allocator);
3642 wrapped.expression = self.instrument_callee(inner, site);
3643 Expression::TSTypeAssertion(wrapped)
3644 }
3645 Expression::TSNonNullExpression(mut wrapped) => {
3646 let inner = wrapped.expression.take_in(self.ast.allocator);
3647 wrapped.expression = self.instrument_callee(inner, site);
3648 Expression::TSNonNullExpression(wrapped)
3649 }
3650 other => self.reached(&site.frame, other),
3651 }
3652 }
3653
3654 fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
3655 let callee = call.callee.take_in(self.ast.allocator);
3656 call.callee = self.instrument_callee(callee, site);
3657 let continued = self.call(
3658 &self.optional_call_continued,
3659 self.ast.vec1(Argument::from(self.identifier(&site.frame))),
3660 );
3661 call.arguments.insert(
3662 0,
3663 Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), continued)),
3664 );
3665 }
3666
3667 fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
3668 let sites = site_keys
3669 .iter()
3670 .map(|key| {
3671 self.sites
3672 .get(key)
3673 .expect("optional-call root must reference a known site")
3674 .clone()
3675 })
3676 .collect::<Vec<_>>();
3677 self.scope_declarations
3678 .last_mut()
3679 .expect("optional-call root must be inside a program or function")
3680 .extend(sites.iter().map(|site| site.frame.clone()));
3681
3682 let original = expression.take_in(self.ast.allocator);
3683 let mut measured = original;
3684 for site in sites.iter().rev() {
3685 measured = self.call(
3686 &self.optional_call_end,
3687 self.ast.vec_from_array([
3688 Argument::from(self.identifier(&site.frame)),
3689 Argument::from(measured),
3690 ]),
3691 );
3692 }
3693 let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
3694 for site in &sites {
3695 let begin = self.call(
3696 &self.optional_call_begin,
3697 self.ast.vec_from_array([
3698 self.string_argument(&site.short_id),
3699 self.string_argument(&site.continued_id),
3700 ]),
3701 );
3702 sequence.push(self.ast.expression_assignment(
3703 Span::default(),
3704 AssignmentOperator::Assign,
3705 self.assignment_target(&site.frame),
3706 begin,
3707 ));
3708 }
3709 sequence.push(measured);
3710 *expression = self.ast.expression_sequence(Span::default(), sequence);
3711 }
3712}
3713
3714impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
3715 fn visit_program(&mut self, program: &mut Program<'a>) {
3716 self.enter_scope();
3717 walk_mut::walk_program(self, program);
3718 self.leave_scope(&mut program.body);
3719 }
3720
3721 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
3722 self.enter_scope();
3723 walk_mut::walk_function_body(self, body);
3724 self.leave_scope(&mut body.statements);
3725 }
3726
3727 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3728 if self
3729 .source_sensitive_functions
3730 .contains(&span_key(function.span))
3731 {
3732 return;
3733 }
3734 walk_mut::walk_function(self, function, flags);
3735 }
3736
3737 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3738 if self
3739 .source_sensitive_functions
3740 .contains(&span_key(function.span))
3741 {
3742 return;
3743 }
3744 walk_mut::walk_arrow_function_expression(self, function);
3745 }
3746
3747 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
3748 if self.with_statements.contains(&span_key(statement.span)) {
3749 return;
3750 }
3751 walk_mut::walk_with_statement(self, statement);
3752 }
3753
3754 fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
3755 walk_mut::walk_call_expression(self, call);
3756 if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
3757 self.instrument_call(call, &site);
3758 }
3759 }
3760
3761 fn visit_expression(&mut self, expression: &mut Expression<'a>) {
3762 let key = span_key(expression.span());
3763 walk_mut::walk_expression(self, expression);
3764 if let Some(site_keys) = self.roots.remove(&key) {
3765 self.wrap_root(expression, &site_keys);
3766 }
3767 }
3768}
3769
3770struct DefaultTransformer<'a> {
3771 ast: AstBuilder<'a>,
3772 default_selected: String,
3773 default_entered: String,
3774 parameter_targets: HashMap<SpanKey, DefaultTarget>,
3775 binding_targets: HashMap<SpanKey, DefaultTarget>,
3776 function_entries: Vec<Vec<Statement<'a>>>,
3777 declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
3778 active_declaration: Vec<SpanKey>,
3779 parameter_pattern_depth: usize,
3780 source_sensitive_functions: HashSet<SpanKey>,
3781 with_statements: HashSet<SpanKey>,
3782}
3783
3784impl<'a> DefaultTransformer<'a> {
3785 fn identifier(&self, name: &str) -> Expression<'a> {
3786 self.ast
3787 .expression_identifier(Span::default(), self.ast.ident(name))
3788 }
3789
3790 fn string_argument(&self, value: &str) -> Argument<'a> {
3791 Argument::from(self.ast.expression_string_literal(
3792 Span::default(),
3793 self.ast.str(value),
3794 None,
3795 ))
3796 }
3797
3798 fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
3799 let mut arguments = self.ast.vec_from_array([
3800 self.string_argument(&target.default_id),
3801 Argument::from(value),
3802 ]);
3803 if let Some(name) = &target.inferred_name {
3804 arguments.push(self.string_argument(name));
3805 }
3806 self.ast.expression_call(
3807 Span::default(),
3808 self.identifier(&self.default_selected),
3809 NONE,
3810 arguments,
3811 false,
3812 )
3813 }
3814
3815 fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
3816 self.ast.statement_expression(
3817 Span::default(),
3818 self.ast.expression_call(
3819 Span::default(),
3820 self.identifier(&self.default_entered),
3821 NONE,
3822 self.ast.vec_from_array([
3823 self.string_argument(&target.default_id),
3824 self.string_argument(&target.provided_id),
3825 ]),
3826 false,
3827 ),
3828 )
3829 }
3830
3831 fn push_entry(&mut self, target: &DefaultTarget) {
3832 let entry = self.entered(target);
3833 if self.parameter_pattern_depth > 0 {
3834 self.function_entries
3835 .last_mut()
3836 .expect("parameter default must belong to a function")
3837 .push(entry);
3838 } else {
3839 let declaration = *self
3840 .active_declaration
3841 .last()
3842 .expect("binding default must belong to a declaration");
3843 self.declaration_entries
3844 .entry(declaration)
3845 .or_default()
3846 .push(entry);
3847 }
3848 }
3849
3850 fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
3851 if entries.is_empty() {
3852 return;
3853 }
3854 if let Statement::BlockStatement(block) = body {
3855 for (index, entry) in entries.into_iter().enumerate() {
3856 block.body.insert(index, entry);
3857 }
3858 return;
3859 }
3860 let original = body.take_in(self.ast.allocator);
3861 let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
3862 statements.extend(entries);
3863 statements.push(original);
3864 *body = self.ast.statement_block(Span::default(), statements);
3865 }
3866
3867 fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
3868 match statement {
3869 Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
3870 Statement::ExportNamedDeclaration(export) => match &export.declaration {
3871 Some(Declaration::VariableDeclaration(declaration)) => {
3872 Some(span_key(declaration.span))
3873 }
3874 _ => None,
3875 },
3876 _ => None,
3877 }
3878 }
3879
3880 fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
3881 match left {
3882 ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
3883 _ => None,
3884 }
3885 }
3886}
3887
3888impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
3889 fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
3890 let original = statements.take_in(self.ast.allocator);
3891 let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
3892 for mut statement in original {
3893 let declaration = Self::variable_span(&statement);
3894 self.visit_statement(&mut statement);
3895 instrumented.push(statement);
3896 if let Some(declaration) = declaration
3897 && let Some(entries) = self.declaration_entries.remove(&declaration)
3898 {
3899 instrumented.extend(entries);
3900 }
3901 }
3902 *statements = instrumented;
3903 }
3904
3905 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
3906 if self
3907 .source_sensitive_functions
3908 .contains(&span_key(function.span))
3909 {
3910 return;
3911 }
3912 self.function_entries.push(Vec::new());
3913 walk_mut::walk_function(self, function, flags);
3914 let entries = self
3915 .function_entries
3916 .pop()
3917 .expect("default function-entry stack must remain balanced");
3918 if let Some(body) = &mut function.body {
3919 for (index, entry) in entries.into_iter().enumerate() {
3920 body.statements.insert(index, entry);
3921 }
3922 }
3923 }
3924
3925 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
3926 if self
3927 .source_sensitive_functions
3928 .contains(&span_key(function.span))
3929 {
3930 return;
3931 }
3932 self.function_entries.push(Vec::new());
3933 walk_mut::walk_arrow_function_expression(self, function);
3934 let entries = self
3935 .function_entries
3936 .pop()
3937 .expect("default arrow-entry stack must remain balanced");
3938 for (index, entry) in entries.into_iter().enumerate() {
3939 function.body.statements.insert(index, entry);
3940 }
3941 }
3942
3943 fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
3944 let outer = self.parameter_targets.remove(&span_key(parameter.span));
3945 if let Some(target) = &outer {
3946 let entry = self.entered(target);
3947 self.function_entries
3948 .last_mut()
3949 .expect("formal parameter must belong to a function")
3950 .push(entry);
3951 }
3952 self.visit_decorators(&mut parameter.decorators);
3953 self.parameter_pattern_depth += 1;
3954 self.visit_binding_pattern(&mut parameter.pattern);
3955 self.parameter_pattern_depth -= 1;
3956 if let Some(annotation) = &mut parameter.type_annotation {
3957 self.visit_ts_type_annotation(annotation);
3958 }
3959 if let Some(initializer) = &mut parameter.initializer {
3960 self.visit_expression(initializer);
3961 if let Some(target) = outer {
3962 let value = initializer.take_in(self.ast.allocator);
3963 **initializer = self.selected(value, &target);
3964 }
3965 }
3966 }
3967
3968 fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
3969 let target = if self.parameter_pattern_depth > 0 {
3970 self.parameter_targets.remove(&span_key(assignment.span))
3971 } else {
3972 self.binding_targets.remove(&span_key(assignment.span))
3973 };
3974 if let Some(target) = &target {
3975 self.push_entry(target);
3976 }
3977 walk_mut::walk_assignment_pattern(self, assignment);
3978 if let Some(target) = target {
3979 let value = assignment.right.take_in(self.ast.allocator);
3980 assignment.right = self.selected(value, &target);
3981 }
3982 }
3983
3984 fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
3985 self.active_declaration.push(span_key(declaration.span));
3986 walk_mut::walk_variable_declaration(self, declaration);
3987 self.active_declaration
3988 .pop()
3989 .expect("default declaration stack must remain balanced");
3990 }
3991
3992 fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
3993 let declaration = Self::loop_declaration_span(&statement.left);
3994 walk_mut::walk_for_in_statement(self, statement);
3995 if let Some(declaration) = declaration
3996 && let Some(entries) = self.declaration_entries.remove(&declaration)
3997 {
3998 self.prepend_entries(&mut statement.body, entries);
3999 }
4000 }
4001
4002 fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
4003 let declaration = Self::loop_declaration_span(&statement.left);
4004 walk_mut::walk_for_of_statement(self, statement);
4005 if let Some(declaration) = declaration
4006 && let Some(entries) = self.declaration_entries.remove(&declaration)
4007 {
4008 self.prepend_entries(&mut statement.body, entries);
4009 }
4010 }
4011
4012 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4013 if self.with_statements.contains(&span_key(statement.span)) {
4014 return;
4015 }
4016 walk_mut::walk_with_statement(self, statement);
4017 }
4018}
4019
4020enum ExtendedKind {
4021 Try,
4022 Loop,
4023}
4024
4025struct ExtendedTransformer<'a, 's> {
4026 ast: AstBuilder<'a>,
4027 try_begin: String,
4028 try_catch: String,
4029 try_end: String,
4030 loop_begin: String,
4031 loop_entered: String,
4032 loop_end: String,
4033 try_targets: HashMap<SpanKey, ExtendedTarget>,
4034 loop_targets: HashMap<SpanKey, ExtendedTarget>,
4035 names: CandidateNames<'s>,
4036 scope_declarations: Vec<Vec<String>>,
4037 source_sensitive_functions: HashSet<SpanKey>,
4038 with_statements: HashSet<SpanKey>,
4039}
4040
4041impl<'a> ExtendedTransformer<'a, '_> {
4042 fn identifier(&self, name: &str) -> Expression<'a> {
4043 self.ast
4044 .expression_identifier(Span::default(), self.ast.ident(name))
4045 }
4046
4047 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
4048 AssignmentTarget::from(
4049 self.ast
4050 .simple_assignment_target_assignment_target_identifier(
4051 Span::default(),
4052 self.ast.ident(name),
4053 ),
4054 )
4055 }
4056
4057 fn string_argument(&self, value: &str) -> Argument<'a> {
4058 Argument::from(self.ast.expression_string_literal(
4059 Span::default(),
4060 self.ast.str(value),
4061 None,
4062 ))
4063 }
4064
4065 fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
4066 self.ast.expression_call(
4067 Span::default(),
4068 self.identifier(name),
4069 NONE,
4070 arguments,
4071 false,
4072 )
4073 }
4074
4075 fn call_statement(
4076 &self,
4077 name: &str,
4078 arguments: oxc_allocator::Vec<'a, Argument<'a>>,
4079 ) -> Statement<'a> {
4080 self.ast
4081 .statement_expression(Span::default(), self.call(name, arguments))
4082 }
4083
4084 fn enter_scope(&mut self) {
4085 self.scope_declarations.push(Vec::new());
4086 }
4087
4088 fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4089 let names = self
4090 .scope_declarations
4091 .pop()
4092 .expect("extended scope stack must remain balanced");
4093 if names.is_empty() {
4094 return;
4095 }
4096 let declarations = self.ast.vec_from_iter(names.into_iter().map(|name| {
4097 self.ast.variable_declarator(
4098 Span::default(),
4099 VariableDeclarationKind::Let,
4100 self.ast
4101 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
4102 NONE,
4103 None,
4104 false,
4105 )
4106 }));
4107 statements.insert(
4108 0,
4109 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
4110 Span::default(),
4111 VariableDeclarationKind::Let,
4112 declarations,
4113 false,
4114 )),
4115 );
4116 }
4117
4118 fn scratch(&mut self, base: &str) -> String {
4119 let name = self.names.allocate(base);
4120 self.scope_declarations
4121 .last_mut()
4122 .expect("extended branch must be inside a program or function")
4123 .push(name.clone());
4124 name
4125 }
4126
4127 fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
4128 match statement {
4129 Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
4130 Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
4131 Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
4132 Statement::LabeledStatement(node) => Self::target(&node.body),
4133 _ => None,
4134 }
4135 }
4136
4137 fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
4138 match statement {
4139 Statement::TryStatement(node) => Some(node),
4140 Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
4141 _ => None,
4142 }
4143 }
4144
4145 fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
4146 match statement {
4147 Statement::ForInStatement(node) => Some(&mut node.body),
4148 Statement::ForOfStatement(node) => Some(&mut node.body),
4149 Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
4150 _ => None,
4151 }
4152 }
4153
4154 fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
4155 if let Statement::BlockStatement(block) = body {
4156 block.body.insert(0, entry);
4157 return;
4158 }
4159 let original = body.take_in(ast.allocator);
4160 *body = ast.statement_block(Span::default(), ast.vec_from_array([entry, original]));
4161 }
4162
4163 fn begin_assignment(&self, frame: &str, begin: &str, target: &ExtendedTarget) -> Statement<'a> {
4164 let call = self.call(
4165 begin,
4166 self.ast.vec_from_array([
4167 self.string_argument(&target.first_id),
4168 self.string_argument(&target.second_id),
4169 ]),
4170 );
4171 self.ast.statement_expression(
4172 Span::default(),
4173 self.ast.expression_assignment(
4174 Span::default(),
4175 AssignmentOperator::Assign,
4176 self.assignment_target(frame),
4177 call,
4178 ),
4179 )
4180 }
4181
4182 fn instrument_try(&mut self, statement: &mut Statement<'a>, target: ExtendedTarget) {
4183 let frame = self.scratch("_supercovTryFrame");
4184 let assignment = self.begin_assignment(&frame, &self.try_begin, &target);
4185 let node = Self::inner_try(statement).expect("try target must remain a try statement");
4186 node.handler
4187 .as_mut()
4188 .expect("try coverage requires a catch handler")
4189 .body
4190 .body
4191 .insert(
4192 0,
4193 self.call_statement(
4194 &self.try_catch,
4195 self.ast.vec_from_array([
4196 Argument::from(self.identifier(&frame)),
4197 Argument::from(self.identifier("undefined")),
4198 ]),
4199 ),
4200 );
4201 let end = self.call_statement(
4202 &self.try_end,
4203 self.ast.vec1(Argument::from(self.identifier(&frame))),
4204 );
4205 if let Some(finalizer) = &mut node.finalizer {
4206 finalizer.body.insert(0, end);
4207 } else {
4208 node.finalizer = Some(
4209 self.ast
4210 .alloc_block_statement(Span::default(), self.ast.vec1(end)),
4211 );
4212 }
4213 let original = statement.take_in(self.ast.allocator);
4214 *statement = self.ast.statement_block(
4215 Span::default(),
4216 self.ast.vec_from_array([assignment, original]),
4217 );
4218 }
4219
4220 fn instrument_loop(&mut self, statement: &mut Statement<'a>, target: ExtendedTarget) {
4221 let frame = self.scratch("_supercovLoopFrame");
4222 let assignment = self.begin_assignment(&frame, &self.loop_begin, &target);
4223 let entered = self.call_statement(
4224 &self.loop_entered,
4225 self.ast.vec1(Argument::from(self.identifier(&frame))),
4226 );
4227 Self::prepend(
4228 Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
4229 entered,
4230 self.ast,
4231 );
4232 let original = statement.take_in(self.ast.allocator);
4233 let end = self.call_statement(
4234 &self.loop_end,
4235 self.ast.vec1(Argument::from(self.identifier(&frame))),
4236 );
4237 let wrapped = self.ast.statement_try(
4238 Span::default(),
4239 self.ast
4240 .block_statement(Span::default(), self.ast.vec1(original)),
4241 None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
4242 Some(
4243 self.ast
4244 .block_statement(Span::default(), self.ast.vec1(end)),
4245 ),
4246 );
4247 *statement = self.ast.statement_block(
4248 Span::default(),
4249 self.ast.vec_from_array([assignment, wrapped]),
4250 );
4251 }
4252}
4253
4254impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
4255 fn visit_program(&mut self, program: &mut Program<'a>) {
4256 self.enter_scope();
4257 walk_mut::walk_program(self, program);
4258 self.leave_scope(&mut program.body);
4259 }
4260
4261 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
4262 self.enter_scope();
4263 walk_mut::walk_function_body(self, body);
4264 self.leave_scope(&mut body.statements);
4265 }
4266
4267 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4268 if self
4269 .source_sensitive_functions
4270 .contains(&span_key(function.span))
4271 {
4272 return;
4273 }
4274 walk_mut::walk_function(self, function, flags);
4275 }
4276
4277 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4278 if self
4279 .source_sensitive_functions
4280 .contains(&span_key(function.span))
4281 {
4282 return;
4283 }
4284 walk_mut::walk_arrow_function_expression(self, function);
4285 }
4286
4287 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4288 if self.with_statements.contains(&span_key(statement.span)) {
4289 return;
4290 }
4291 walk_mut::walk_with_statement(self, statement);
4292 }
4293
4294 fn visit_statement(&mut self, statement: &mut Statement<'a>) {
4295 let Some((kind, key)) = Self::target(statement) else {
4296 walk_mut::walk_statement(self, statement);
4297 return;
4298 };
4299 match kind {
4300 ExtendedKind::Try => {
4301 if let Some(target) = self.try_targets.remove(&key) {
4302 walk_mut::walk_statement(self, statement);
4303 self.instrument_try(statement, target);
4304 } else {
4305 walk_mut::walk_statement(self, statement);
4306 }
4307 }
4308 ExtendedKind::Loop => {
4309 if let Some(target) = self.loop_targets.remove(&key) {
4310 walk_mut::walk_statement(self, statement);
4311 self.instrument_loop(statement, target);
4312 } else {
4313 walk_mut::walk_statement(self, statement);
4314 }
4315 }
4316 }
4317 }
4318}
4319
4320struct LogicalValueTransformer<'a, 's> {
4321 ast: AstBuilder<'a>,
4322 selection_begin: String,
4323 selection_right: String,
4324 selection_end: String,
4325 names: CandidateNames<'s>,
4326 scope_declarations: Vec<Vec<String>>,
4327 logical_targets: HashMap<SpanKey, (String, String)>,
4328 assignment_targets: HashMap<SpanKey, (String, String)>,
4329 source_sensitive_functions: HashSet<SpanKey>,
4330 with_statements: HashSet<SpanKey>,
4331}
4332
4333impl<'a> LogicalValueTransformer<'a, '_> {
4334 fn identifier(&self, name: &str) -> Expression<'a> {
4335 self.ast
4336 .expression_identifier(Span::default(), self.ast.ident(name))
4337 }
4338
4339 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
4340 AssignmentTarget::from(
4341 self.ast
4342 .simple_assignment_target_assignment_target_identifier(
4343 Span::default(),
4344 self.ast.ident(name),
4345 ),
4346 )
4347 }
4348
4349 fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
4350 self.ast.expression_call(
4351 Span::default(),
4352 self.identifier(name),
4353 NONE,
4354 arguments,
4355 false,
4356 )
4357 }
4358
4359 fn string_argument(&self, value: &str) -> Argument<'a> {
4360 Argument::from(self.ast.expression_string_literal(
4361 Span::default(),
4362 self.ast.str(value),
4363 None,
4364 ))
4365 }
4366
4367 fn enter_scope(&mut self) {
4368 self.scope_declarations.push(Vec::new());
4369 }
4370
4371 fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
4372 let names = self
4373 .scope_declarations
4374 .pop()
4375 .expect("logical-value scope stack must remain balanced");
4376 if names.is_empty() {
4377 return;
4378 }
4379 let declarations = self.ast.vec_from_iter(names.into_iter().map(|name| {
4380 self.ast.variable_declarator(
4381 Span::default(),
4382 VariableDeclarationKind::Let,
4383 self.ast
4384 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
4385 NONE,
4386 None,
4387 false,
4388 )
4389 }));
4390 statements.insert(
4391 0,
4392 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
4393 Span::default(),
4394 VariableDeclarationKind::Let,
4395 declarations,
4396 false,
4397 )),
4398 );
4399 }
4400
4401 fn scratch(&mut self) -> String {
4402 let name = self.names.allocate("_supercovSelectionFrame");
4403 self.scope_declarations
4404 .last_mut()
4405 .expect("logical expression must be inside a program or function")
4406 .push(name.clone());
4407 name
4408 }
4409
4410 fn instrument(
4411 &mut self,
4412 logical: oxc_allocator::Box<'a, LogicalExpression<'a>>,
4413 short_id: &str,
4414 right_id: &str,
4415 ) -> Expression<'a> {
4416 let logical = logical.unbox();
4417 let frame = self.scratch();
4418 let begin = self.call(
4419 &self.selection_begin,
4420 self.ast.vec_from_array([
4421 self.string_argument(short_id),
4422 self.string_argument(right_id),
4423 ]),
4424 );
4425 let assign = self.ast.expression_assignment(
4426 Span::default(),
4427 AssignmentOperator::Assign,
4428 self.assignment_target(&frame),
4429 begin,
4430 );
4431 let right = self.call(
4432 &self.selection_right,
4433 self.ast.vec_from_array([
4434 Argument::from(self.identifier(&frame)),
4435 Argument::from(logical.right),
4436 ]),
4437 );
4438 let selection =
4439 self.ast
4440 .expression_logical(Span::default(), logical.left, logical.operator, right);
4441 let end = self.call(
4442 &self.selection_end,
4443 self.ast.vec_from_array([
4444 Argument::from(self.identifier(&frame)),
4445 Argument::from(selection),
4446 ]),
4447 );
4448 self.ast
4449 .expression_sequence(Span::default(), self.ast.vec_from_array([assign, end]))
4450 }
4451
4452 fn instrument_assignment(
4453 &mut self,
4454 assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
4455 short_id: &str,
4456 right_id: &str,
4457 ) -> Expression<'a> {
4458 let assignment = assignment.unbox();
4459 let inferred_name = match &assignment.left {
4460 AssignmentTarget::AssignmentTargetIdentifier(identifier)
4461 if assignment.span.start == identifier.span.start
4462 && expression_is_anonymous_definition(&assignment.right) =>
4463 {
4464 Some(identifier.name.to_string())
4465 }
4466 _ => None,
4467 };
4468 let frame = self.scratch();
4469 let begin = self.call(
4470 &self.selection_begin,
4471 self.ast.vec_from_array([
4472 self.string_argument(short_id),
4473 self.string_argument(right_id),
4474 ]),
4475 );
4476 let assign_frame = self.ast.expression_assignment(
4477 Span::default(),
4478 AssignmentOperator::Assign,
4479 self.assignment_target(&frame),
4480 begin,
4481 );
4482 let mut right_arguments = self.ast.vec_from_array([
4483 Argument::from(self.identifier(&frame)),
4484 Argument::from(assignment.right),
4485 ]);
4486 if let Some(name) = inferred_name {
4487 right_arguments.push(self.string_argument(&name));
4488 }
4489 let right = self.call(&self.selection_right, right_arguments);
4490 let measured_assignment = self.ast.expression_assignment(
4491 Span::default(),
4492 assignment.operator,
4493 assignment.left,
4494 right,
4495 );
4496 let end = self.call(
4497 &self.selection_end,
4498 self.ast.vec_from_array([
4499 Argument::from(self.identifier(&frame)),
4500 Argument::from(measured_assignment),
4501 ]),
4502 );
4503 self.ast.expression_sequence(
4504 Span::default(),
4505 self.ast.vec_from_array([assign_frame, end]),
4506 )
4507 }
4508}
4509
4510impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
4511 fn visit_program(&mut self, program: &mut Program<'a>) {
4512 self.enter_scope();
4513 walk_mut::walk_program(self, program);
4514 self.leave_scope(&mut program.body);
4515 }
4516
4517 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
4518 self.enter_scope();
4519 walk_mut::walk_function_body(self, body);
4520 self.leave_scope(&mut body.statements);
4521 }
4522
4523 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4524 if self
4525 .source_sensitive_functions
4526 .contains(&span_key(function.span))
4527 {
4528 return;
4529 }
4530 walk_mut::walk_function(self, function, flags);
4531 }
4532
4533 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4534 if self
4535 .source_sensitive_functions
4536 .contains(&span_key(function.span))
4537 {
4538 return;
4539 }
4540 walk_mut::walk_arrow_function_expression(self, function);
4541 }
4542
4543 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4544 if self.with_statements.contains(&span_key(statement.span)) {
4545 return;
4546 }
4547 walk_mut::walk_with_statement(self, statement);
4548 }
4549
4550 fn visit_expression(&mut self, expression: &mut Expression<'a>) {
4551 let key = span_key(expression.span());
4552 walk_mut::walk_expression(self, expression);
4553 if let Some((short_id, right_id)) = self.assignment_targets.remove(&key) {
4554 let original = expression.take_in(self.ast.allocator);
4555 let Expression::AssignmentExpression(assignment) = original else {
4556 panic!("logical-assignment target must remain an assignment expression");
4557 };
4558 *expression = self.instrument_assignment(assignment, &short_id, &right_id);
4559 return;
4560 }
4561 let Some((short_id, right_id)) = self.logical_targets.remove(&key) else {
4562 return;
4563 };
4564 let original = expression.take_in(self.ast.allocator);
4565 let Expression::LogicalExpression(logical) = original else {
4566 panic!("logical-value target must remain a logical expression");
4567 };
4568 *expression = self.instrument(logical, &short_id, &right_id);
4569 }
4570}
4571
4572struct SwitchTransformer<'a, 's> {
4573 ast: AstBuilder<'a>,
4574 coverage_hit: String,
4575 targets: HashMap<SpanKey, SwitchTarget>,
4576 names: CandidateNames<'s>,
4577 source_sensitive_functions: HashSet<SpanKey>,
4578 with_statements: HashSet<SpanKey>,
4579}
4580
4581impl<'a> SwitchTransformer<'a, '_> {
4582 fn identifier(&self, name: &str) -> Expression<'a> {
4583 self.ast
4584 .expression_identifier(Span::default(), self.ast.ident(name))
4585 }
4586
4587 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
4588 AssignmentTarget::from(
4589 self.ast
4590 .simple_assignment_target_assignment_target_identifier(
4591 Span::default(),
4592 self.ast.ident(name),
4593 ),
4594 )
4595 }
4596
4597 fn probe(&self, id: &str) -> Statement<'a> {
4598 self.ast.statement_expression(
4599 Span::default(),
4600 self.ast.expression_call(
4601 Span::default(),
4602 self.identifier(&self.coverage_hit),
4603 NONE,
4604 self.ast
4605 .vec1(Argument::from(self.ast.expression_string_literal(
4606 Span::default(),
4607 self.ast.str(id),
4608 None,
4609 ))),
4610 false,
4611 ),
4612 )
4613 }
4614
4615 fn target(statement: &Statement<'a>) -> Option<SpanKey> {
4616 match statement {
4617 Statement::SwitchStatement(node) => Some(span_key(node.span)),
4618 Statement::LabeledStatement(node) => Self::target(&node.body),
4619 _ => None,
4620 }
4621 }
4622
4623 fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
4624 match statement {
4625 Statement::SwitchStatement(node) => Some(node),
4626 Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
4627 _ => None,
4628 }
4629 }
4630
4631 fn entered_assignment(&self, entered: &str) -> Statement<'a> {
4632 self.ast.statement_expression(
4633 Span::default(),
4634 self.ast.expression_assignment(
4635 Span::default(),
4636 AssignmentOperator::Assign,
4637 self.assignment_target(entered),
4638 self.ast.expression_boolean_literal(Span::default(), true),
4639 ),
4640 )
4641 }
4642
4643 fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
4644 let entered = target
4645 .no_match_id
4646 .as_ref()
4647 .map(|_| self.names.allocate("_supercovSwitchEntered"));
4648 let node = Self::inner_switch(statement).expect("switch target must remain a switch");
4649 for (index, case) in node.cases.iter_mut().enumerate() {
4650 let probe = self.probe(
4651 target
4652 .case_ids
4653 .get(index)
4654 .expect("switch case target count must remain stable"),
4655 );
4656 case.consequent.insert(0, probe);
4657 if let Some(entered) = &entered {
4658 case.consequent.insert(0, self.entered_assignment(entered));
4659 }
4660 }
4661 let (Some(entered), Some(no_match_id)) = (entered, &target.no_match_id) else {
4662 return;
4663 };
4664 let declaration =
4665 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
4666 Span::default(),
4667 VariableDeclarationKind::Let,
4668 self.ast.vec1(self.ast.variable_declarator(
4669 Span::default(),
4670 VariableDeclarationKind::Let,
4671 self.ast.binding_pattern_binding_identifier(
4672 Span::default(),
4673 self.ast.ident(&entered),
4674 ),
4675 NONE,
4676 Some(self.ast.expression_boolean_literal(Span::default(), false)),
4677 false,
4678 )),
4679 false,
4680 ));
4681 let original = statement.take_in(self.ast.allocator);
4682 let no_match = self.ast.statement_if(
4683 Span::default(),
4684 self.ast.expression_unary(
4685 Span::default(),
4686 UnaryOperator::LogicalNot,
4687 self.identifier(&entered),
4688 ),
4689 self.probe(no_match_id),
4690 None,
4691 );
4692 *statement = self.ast.statement_block(
4693 Span::default(),
4694 self.ast.vec_from_array([declaration, original, no_match]),
4695 );
4696 }
4697}
4698
4699impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
4700 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
4701 if self
4702 .source_sensitive_functions
4703 .contains(&span_key(function.span))
4704 {
4705 return;
4706 }
4707 walk_mut::walk_function(self, function, flags);
4708 }
4709
4710 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
4711 if self
4712 .source_sensitive_functions
4713 .contains(&span_key(function.span))
4714 {
4715 return;
4716 }
4717 walk_mut::walk_arrow_function_expression(self, function);
4718 }
4719
4720 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
4721 if self.with_statements.contains(&span_key(statement.span)) {
4722 return;
4723 }
4724 walk_mut::walk_with_statement(self, statement);
4725 }
4726
4727 fn visit_statement(&mut self, statement: &mut Statement<'a>) {
4728 let Some(key) = Self::target(statement) else {
4729 walk_mut::walk_statement(self, statement);
4730 return;
4731 };
4732 let Some(target) = self.targets.remove(&key) else {
4733 walk_mut::walk_statement(self, statement);
4734 return;
4735 };
4736 let has_no_match = target.no_match_id.is_some();
4737 self.instrument(statement, &target);
4738 if !has_no_match {
4739 walk_mut::walk_statement(self, statement);
4740 }
4741 }
4742}
4743
4744struct RouteRequestPhaseTransformer<'a, 's> {
4745 ast: AstBuilder<'a>,
4746 file: &'s str,
4747 with_request_phase: String,
4748 used: bool,
4749 names: CandidateNames<'s>,
4750}
4751
4752impl<'a> RouteRequestPhaseTransformer<'a, '_> {
4753 fn is_remix_route(&self) -> bool {
4754 self.file.starts_with("app/routes/")
4755 }
4756
4757 fn is_next_route(&self) -> bool {
4758 let path = Path::new(self.file);
4759 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
4760 return false;
4761 };
4762 let is_route_module = [
4763 "route.js",
4764 "route.jsx",
4765 "route.ts",
4766 "route.tsx",
4767 "route.mjs",
4768 "route.mts",
4769 "route.cjs",
4770 "route.cts",
4771 ]
4772 .contains(&name);
4773 if !is_route_module {
4774 return false;
4775 }
4776 let components = path
4777 .components()
4778 .filter_map(|component| component.as_os_str().to_str())
4779 .collect::<Vec<_>>();
4780 components
4781 .windows(2)
4782 .any(|window| window[0] == "app" && window[1] != name)
4783 }
4784
4785 fn is_handler_name(&self, name: &str) -> bool {
4786 (self.is_remix_route() && matches!(name, "loader" | "action"))
4787 || (self.is_next_route()
4788 && matches!(
4789 name,
4790 "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
4791 ))
4792 }
4793
4794 fn identifier(&self, name: &str) -> Expression<'a> {
4795 self.ast
4796 .expression_identifier(Span::default(), self.ast.ident(name))
4797 }
4798
4799 fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
4800 self.ast.expression_call(
4801 Span::default(),
4802 self.identifier(&self.with_request_phase),
4803 NONE,
4804 self.ast.vec1(Argument::from(expression)),
4805 false,
4806 )
4807 }
4808
4809 fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
4810 Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
4811 Span::default(),
4812 Some(self.ast.declaration_variable(
4813 Span::default(),
4814 VariableDeclarationKind::Const,
4815 self.ast.vec1(self.ast.variable_declarator(
4816 Span::default(),
4817 VariableDeclarationKind::Const,
4818 self.ast.binding_pattern_binding_identifier(
4819 Span::default(),
4820 self.ast.ident(exported_name),
4821 ),
4822 NONE,
4823 Some(self.wrap_expression(self.identifier(original_name))),
4824 false,
4825 )),
4826 false,
4827 )),
4828 self.ast.vec(),
4829 None,
4830 ImportOrExportKind::Value,
4831 NONE,
4832 ))
4833 }
4834
4835 fn transform_named_export(
4836 &mut self,
4837 mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
4838 output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
4839 ) {
4840 if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
4841 for declarator in &mut declaration.declarations {
4842 let Some(name) = binding_identifier_name(&declarator.id) else {
4843 continue;
4844 };
4845 if !self.is_handler_name(&name) {
4846 continue;
4847 }
4848 if let Some(initializer) = &mut declarator.init {
4849 let original = initializer.take_in(self.ast.allocator);
4850 *initializer = self.wrap_expression(original);
4851 self.used = true;
4852 }
4853 }
4854 output.push(Statement::ExportNamedDeclaration(export));
4855 return;
4856 }
4857
4858 if matches!(
4859 export.declaration,
4860 Some(Declaration::FunctionDeclaration(_))
4861 ) {
4862 let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
4863 else {
4864 unreachable!();
4865 };
4866 let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
4867 output.push(Statement::FunctionDeclaration(function));
4868 return;
4869 };
4870 if !self.is_handler_name(&exported_name) {
4871 export.declaration = Some(Declaration::FunctionDeclaration(function));
4872 output.push(Statement::ExportNamedDeclaration(export));
4873 return;
4874 }
4875 let original_name = self
4876 .names
4877 .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
4878 function.id = Some(
4879 self.ast
4880 .binding_identifier(Span::default(), self.ast.ident(&original_name)),
4881 );
4882 output.push(Statement::FunctionDeclaration(function));
4883 output.push(self.wrapped_named_export(&exported_name, &original_name));
4884 self.used = true;
4885 return;
4886 }
4887
4888 if export.source.is_none() {
4889 output.push(Statement::ExportNamedDeclaration(export));
4890 return;
4891 }
4892 let source = export
4893 .source
4894 .as_ref()
4895 .expect("checked above")
4896 .clone_in(self.ast.allocator);
4897 let specifiers = export.specifiers.take_in(self.ast.allocator);
4898 let mut untouched = self.ast.vec();
4899 let mut handlers = Vec::new();
4900 for specifier in specifiers {
4901 let exported_name = specifier
4902 .exported
4903 .identifier_name()
4904 .map(|name| name.to_string());
4905 if exported_name
4906 .as_deref()
4907 .is_some_and(|name| self.is_handler_name(name))
4908 {
4909 handlers.push((exported_name.expect("checked above"), specifier));
4910 } else {
4911 untouched.push(specifier);
4912 }
4913 }
4914 if handlers.is_empty() {
4915 export.specifiers = untouched;
4916 output.push(Statement::ExportNamedDeclaration(export));
4917 return;
4918 }
4919 if !untouched.is_empty() {
4920 output.push(Statement::ExportNamedDeclaration(
4921 self.ast.alloc_export_named_declaration(
4922 export.span,
4923 None,
4924 untouched,
4925 Some(source.clone_in(self.ast.allocator)),
4926 export.export_kind,
4927 export.with_clause.take(),
4928 ),
4929 ));
4930 }
4931 for (exported_name, specifier) in handlers {
4932 let original_name = self
4933 .names
4934 .allocate(&format!("__supercov{exported_name}CoverageOriginal"));
4935 output.push(Statement::ImportDeclaration(
4936 self.ast.alloc_import_declaration(
4937 Span::default(),
4938 Some(self.ast.vec1(
4939 self.ast.import_declaration_specifier_import_specifier(
4940 Span::default(),
4941 specifier.local,
4942 self.ast.binding_identifier(
4943 Span::default(),
4944 self.ast.ident(&original_name),
4945 ),
4946 ImportOrExportKind::Value,
4947 ),
4948 )),
4949 source.clone_in(self.ast.allocator),
4950 None,
4951 NONE,
4952 ImportOrExportKind::Value,
4953 ),
4954 ));
4955 output.push(self.wrapped_named_export(&exported_name, &original_name));
4956 }
4957 self.used = true;
4958 }
4959
4960 fn transform_default_export(
4961 &mut self,
4962 mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
4963 output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
4964 ) {
4965 if let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
4966 export.declaration.take_in(self.ast.allocator)
4967 {
4968 let original_name = self
4969 .names
4970 .allocate("__supercovHandleRequestCoverageOriginal");
4971 function.id = Some(
4972 self.ast
4973 .binding_identifier(Span::default(), self.ast.ident(&original_name)),
4974 );
4975 output.push(Statement::FunctionDeclaration(function));
4976 output.push(Statement::ExportDefaultDeclaration(
4977 self.ast.alloc_export_default_declaration(
4978 Span::default(),
4979 ExportDefaultDeclarationKind::from(
4980 self.wrap_expression(self.identifier(&original_name)),
4981 ),
4982 ),
4983 ));
4984 self.used = true;
4985 return;
4986 }
4987 if export.declaration.is_expression() {
4988 let original = export
4989 .declaration
4990 .take_in(self.ast.allocator)
4991 .into_expression();
4992 export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
4993 self.used = true;
4994 }
4995 output.push(Statement::ExportDefaultDeclaration(export));
4996 }
4997
4998 fn transform_program(&mut self, program: &mut Program<'a>) {
4999 let route_module = self.is_remix_route() || self.is_next_route();
5000 let server_entry = self.file.starts_with("app/entry.server.")
5001 && ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
5002 &Path::new(self.file)
5003 .extension()
5004 .and_then(|value| value.to_str())
5005 .unwrap_or(""),
5006 );
5007 if !route_module && !server_entry {
5008 return;
5009 }
5010 let original = program.body.take_in(self.ast.allocator);
5011 let mut output = self.ast.vec_with_capacity(original.len() + 4);
5012 for statement in original {
5013 match statement {
5014 Statement::ExportNamedDeclaration(export) if route_module => {
5015 self.transform_named_export(export, &mut output);
5016 }
5017 Statement::ExportDefaultDeclaration(export) if server_entry => {
5018 self.transform_default_export(export, &mut output);
5019 }
5020 statement => output.push(statement),
5021 }
5022 }
5023 program.body = output;
5024 }
5025}
5026
5027struct RequestPhaseTransformer<'a> {
5028 ast: AstBuilder<'a>,
5029 with_request_phase: String,
5030 used: bool,
5031 source_sensitive_functions: HashSet<SpanKey>,
5032 with_statements: HashSet<SpanKey>,
5033}
5034
5035impl<'a> RequestPhaseTransformer<'a> {
5036 fn identifier(&self, name: &str) -> Expression<'a> {
5037 self.ast
5038 .expression_identifier(Span::default(), self.ast.ident(name))
5039 }
5040
5041 fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
5042 match callee {
5043 Expression::Identifier(identifier) => identifier.name == name,
5044 Expression::StaticMemberExpression(member) => member.property.name == name,
5045 _ => false,
5046 }
5047 }
5048
5049 fn callback_candidate(argument: &Argument<'a>) -> bool {
5050 matches!(
5051 argument,
5052 Argument::FunctionExpression(_)
5053 | Argument::ArrowFunctionExpression(_)
5054 | Argument::Identifier(_)
5055 | Argument::ComputedMemberExpression(_)
5056 | Argument::StaticMemberExpression(_)
5057 | Argument::PrivateFieldExpression(_)
5058 )
5059 }
5060
5061 fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
5062 matches!(
5063 argument,
5064 Argument::CallExpression(call)
5065 if expression_is_identifier(&call.callee, &self.with_request_phase)
5066 )
5067 }
5068
5069 fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
5070 if self.already_wrapped(argument) || !argument.is_expression() {
5071 return;
5072 }
5073 let expression = argument.to_expression_mut();
5074 let original = expression.take_in(self.ast.allocator);
5075 *expression = self.ast.expression_call(
5076 Span::default(),
5077 self.identifier(&self.with_request_phase),
5078 NONE,
5079 self.ast
5080 .vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
5081 |name| {
5082 Argument::from(self.ast.expression_string_literal(
5083 Span::default(),
5084 self.ast.str(name),
5085 None,
5086 ))
5087 },
5088 ))),
5089 false,
5090 );
5091 self.used = true;
5092 }
5093}
5094
5095impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
5096 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5097 if self
5098 .source_sensitive_functions
5099 .contains(&span_key(function.span))
5100 {
5101 return;
5102 }
5103 walk_mut::walk_function(self, function, flags);
5104 }
5105
5106 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5107 if self
5108 .source_sensitive_functions
5109 .contains(&span_key(function.span))
5110 {
5111 return;
5112 }
5113 walk_mut::walk_arrow_function_expression(self, function);
5114 }
5115
5116 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
5117 if self.with_statements.contains(&span_key(statement.span)) {
5118 return;
5119 }
5120 walk_mut::walk_with_statement(self, statement);
5121 }
5122
5123 fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
5124 walk_mut::walk_call_expression(self, call);
5125 let property = match &call.callee {
5126 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
5127 _ => None,
5128 };
5129 let mut callback_index = None;
5130 if matches!(property, Some("on" | "once" | "addListener"))
5131 && matches!(
5132 call.arguments.first(),
5133 Some(Argument::StringLiteral(event))
5134 if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
5135 )
5136 {
5137 callback_index = Some(1);
5138 } else if Self::callee_is(&call.callee, "createServer") {
5139 callback_index = call.arguments.iter().rposition(Self::callback_candidate);
5140 }
5141 let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
5142 if let Some(index) = callback_index
5143 && let Some(argument) = call.arguments.get_mut(index)
5144 {
5145 self.wrap_argument(argument, connection.then_some("connection"));
5146 }
5147 }
5148}
5149
5150struct CandidateNames<'s> {
5151 source: &'s str,
5152 allocated: Vec<String>,
5153 suffix: usize,
5154}
5155
5156impl<'s> CandidateNames<'s> {
5157 fn new(source: &'s str) -> Self {
5158 Self {
5159 source,
5160 allocated: Vec::new(),
5161 suffix: 0,
5162 }
5163 }
5164
5165 fn allocate(&mut self, base: &str) -> String {
5166 loop {
5167 let candidate = if self.suffix == 0 {
5168 base.to_string()
5169 } else {
5170 format!("{base}{}", self.suffix)
5171 };
5172 self.suffix += 1;
5173 if !self.source.contains(&candidate) && !self.allocated.contains(&candidate) {
5174 self.allocated.push(candidate.clone());
5175 return candidate;
5176 }
5177 }
5178 }
5179}
5180
5181struct ControlProbeV2Transformer<'a, 's> {
5182 ast: AstBuilder<'a>,
5183 decisions: &'s [CandidateDecision],
5184 mcdc_begin: String,
5185 mcdc_condition: String,
5186 mcdc_end: String,
5187 mcdc_end_v2: String,
5188 probe_file_v2: String,
5189 names: CandidateNames<'s>,
5190 scope_declarations: Vec<Vec<String>>,
5191 decision_index: usize,
5192 parameter_depth: usize,
5193 source_sensitive_functions: HashSet<SpanKey>,
5194 with_statements: HashSet<SpanKey>,
5195}
5196
5197#[derive(Clone, Copy)]
5198struct DecisionPlan {
5199 index: usize,
5200 condition_count: usize,
5201 inline_frame: bool,
5202}
5203
5204impl<'a> ControlProbeV2Transformer<'a, '_> {
5205 fn enter_declaration_scope(&mut self) {
5206 self.scope_declarations.push(Vec::new());
5207 }
5208
5209 fn leave_declaration_scope(&mut self) -> Vec<String> {
5210 self.scope_declarations
5211 .pop()
5212 .expect("program/function scope stack must remain balanced")
5213 }
5214
5215 fn allocate_scratch(&mut self, base: &str) -> String {
5216 let name = self.names.allocate(base);
5217 self.scope_declarations
5218 .last_mut()
5219 .expect("a control decision must be inside a program or function body")
5220 .push(name.clone());
5221 name
5222 }
5223
5224 fn scratch_for(&mut self, base: &str, inline: bool) -> String {
5225 if inline {
5226 self.names.allocate(base)
5227 } else {
5228 self.allocate_scratch(base)
5229 }
5230 }
5231
5232 fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
5233 let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
5234 self.ast.variable_declarator(
5235 Span::default(),
5236 VariableDeclarationKind::Let,
5237 self.ast
5238 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
5239 NONE,
5240 None,
5241 false,
5242 )
5243 }));
5244 let body = self.ast.alloc_function_body(
5245 Span::default(),
5246 self.ast.vec(),
5247 self.ast.vec_from_array([
5248 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
5249 Span::default(),
5250 VariableDeclarationKind::Let,
5251 declarations,
5252 false,
5253 )),
5254 self.ast.statement_return(Span::default(), Some(expression)),
5255 ]),
5256 );
5257 let params = self.ast.alloc_formal_parameters(
5258 Span::default(),
5259 FormalParameterKind::ArrowFormalParameters,
5260 self.ast.vec(),
5261 NONE,
5262 );
5263 let arrow = self.ast.expression_arrow_function(
5264 Span::default(),
5265 false,
5266 false,
5267 NONE,
5268 params,
5269 NONE,
5270 body,
5271 );
5272 self.ast
5273 .expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
5274 }
5275
5276 fn prepend_declarations(
5277 &self,
5278 names: Vec<String>,
5279 statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
5280 ) {
5281 if names.is_empty() {
5282 return;
5283 }
5284 let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
5285 self.ast.variable_declarator(
5286 Span::default(),
5287 VariableDeclarationKind::Let,
5288 self.ast
5289 .binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
5290 NONE,
5291 None,
5292 false,
5293 )
5294 }));
5295 statements.insert(
5296 0,
5297 Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
5298 Span::default(),
5299 VariableDeclarationKind::Let,
5300 declarators,
5301 false,
5302 )),
5303 );
5304 }
5305
5306 fn identifier(&self, name: &str) -> Expression<'a> {
5307 self.ast
5308 .expression_identifier(Span::default(), self.ast.ident(name))
5309 }
5310
5311 fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
5312 let target = self
5313 .ast
5314 .simple_assignment_target_assignment_target_identifier(
5315 Span::default(),
5316 self.ast.ident(name),
5317 );
5318 AssignmentTarget::from(target)
5319 }
5320
5321 fn number(&self, value: u64) -> Expression<'a> {
5322 self.ast.expression_numeric_literal(
5323 Span::default(),
5324 value as f64,
5325 None,
5326 NumberBase::Decimal,
5327 )
5328 }
5329
5330 fn string(&self, value: &str) -> Expression<'a> {
5331 self.ast
5332 .expression_string_literal(Span::default(), self.ast.str(value), None)
5333 }
5334
5335 fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
5336 self.ast.object_property_kind_object_property(
5337 Span::default(),
5338 PropertyKind::Init,
5339 self.ast
5340 .property_key_static_identifier(Span::default(), self.ast.ident(name)),
5341 value,
5342 false,
5343 false,
5344 false,
5345 )
5346 }
5347
5348 fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
5349 let conditions = self.ast.expression_array(
5350 Span::default(),
5351 self.ast.vec_from_iter(
5352 decision
5353 .conditions
5354 .iter()
5355 .map(|condition| ArrayExpressionElement::from(self.string(condition))),
5356 ),
5357 );
5358 let properties = self.ast.vec_from_array([
5359 self.object_property("id", self.string(&decision.id)),
5360 self.object_property("file", self.string(&decision.file)),
5361 self.object_property("line", self.number(decision.line as u64)),
5362 self.object_property("column", self.number(decision.column as u64)),
5363 self.object_property("source", self.string(&decision.source)),
5364 self.object_property("conditions", conditions),
5365 self.object_property("kind", self.string(&decision.kind)),
5366 ]);
5367 Expression::ObjectExpression(
5368 self.ast
5369 .alloc_object_expression(Span::default(), properties),
5370 )
5371 }
5372
5373 fn instrument_condition(
5374 &self,
5375 expression: Expression<'a>,
5376 frame_name: &str,
5377 temporary_name: &str,
5378 index: usize,
5379 ) -> Expression<'a> {
5380 let assign_value = self.ast.expression_assignment(
5381 Span::default(),
5382 AssignmentOperator::Assign,
5383 self.assignment_target(temporary_name),
5384 expression,
5385 );
5386 let weight = 3_u64.pow(index as u32);
5387 let digit = self.ast.expression_conditional(
5388 Span::default(),
5389 self.identifier(temporary_name),
5390 self.number(weight * 2),
5391 self.number(weight),
5392 );
5393 let add_digit = self.ast.expression_assignment(
5394 Span::default(),
5395 AssignmentOperator::Addition,
5396 self.assignment_target(frame_name),
5397 digit,
5398 );
5399 self.ast.expression_sequence(
5400 Span::default(),
5401 self.ast
5402 .vec_from_array([assign_value, add_digit, self.identifier(temporary_name)]),
5403 )
5404 }
5405
5406 fn instrument_conditions(
5407 &self,
5408 expression: &mut Expression<'a>,
5409 frame_name: &str,
5410 temporary_names: &[String],
5411 next_index: &mut usize,
5412 ) {
5413 match expression {
5414 Expression::ParenthesizedExpression(parenthesized) => self.instrument_conditions(
5415 &mut parenthesized.expression,
5416 frame_name,
5417 temporary_names,
5418 next_index,
5419 ),
5420 Expression::LogicalExpression(logical)
5421 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5422 {
5423 self.instrument_conditions(
5424 &mut logical.left,
5425 frame_name,
5426 temporary_names,
5427 next_index,
5428 );
5429 self.instrument_conditions(
5430 &mut logical.right,
5431 frame_name,
5432 temporary_names,
5433 next_index,
5434 );
5435 }
5436 Expression::UnaryExpression(unary)
5437 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5438 {
5439 self.instrument_conditions(
5440 &mut unary.argument,
5441 frame_name,
5442 temporary_names,
5443 next_index,
5444 );
5445 }
5446 _ => {
5447 let index = *next_index;
5448 *next_index += 1;
5449 let original = expression.take_in(self.ast.allocator);
5450 *expression =
5451 self.instrument_condition(original, frame_name, &temporary_names[index], index);
5452 }
5453 }
5454 }
5455
5456 fn instrument_condition_v1(
5457 &self,
5458 expression: Expression<'a>,
5459 frame_name: &str,
5460 index: usize,
5461 ) -> Expression<'a> {
5462 self.ast.expression_call(
5463 Span::default(),
5464 self.identifier(&self.mcdc_condition),
5465 NONE,
5466 self.ast.vec_from_array([
5467 Argument::from(self.identifier(frame_name)),
5468 Argument::from(self.number(index as u64)),
5469 Argument::from(expression),
5470 ]),
5471 false,
5472 )
5473 }
5474
5475 fn instrument_conditions_v1(
5476 &self,
5477 expression: &mut Expression<'a>,
5478 frame_name: &str,
5479 next_index: &mut usize,
5480 ) {
5481 match expression {
5482 Expression::ParenthesizedExpression(parenthesized) => {
5483 self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
5484 }
5485 Expression::LogicalExpression(logical)
5486 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5487 {
5488 self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
5489 self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
5490 }
5491 Expression::UnaryExpression(unary)
5492 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5493 {
5494 self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
5495 }
5496 _ => {
5497 let index = *next_index;
5498 *next_index += 1;
5499 let original = expression.take_in(self.ast.allocator);
5500 *expression = self.instrument_condition_v1(original, frame_name, index);
5501 }
5502 }
5503 }
5504
5505 fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
5506 let mut condition_spans = Vec::new();
5507 collect_conditions(test, &mut condition_spans);
5508 let plan = DecisionPlan {
5509 index: self.decision_index,
5510 condition_count: condition_spans.len(),
5511 inline_frame: self.parameter_depth > 0,
5512 };
5513 self.decision_index += 1;
5514 plan
5515 }
5516
5517 fn apply_decision(&mut self, test: &mut Expression<'a>, plan: DecisionPlan) {
5518 if plan.condition_count > 32 {
5519 let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
5520 let mut next_index = 0;
5521 self.instrument_conditions_v1(test, &frame_name, &mut next_index);
5522 debug_assert_eq!(next_index, plan.condition_count);
5523
5524 let decision = &self.decisions[plan.index];
5525 let begin = self.ast.expression_call(
5526 Span::default(),
5527 self.identifier(&self.mcdc_begin),
5528 NONE,
5529 self.ast.vec_from_array([
5530 Argument::from(self.string(&decision.id)),
5531 Argument::from(self.decision_meta(decision)),
5532 ]),
5533 false,
5534 );
5535 let assign_frame = self.ast.expression_assignment(
5536 Span::default(),
5537 AssignmentOperator::Assign,
5538 self.assignment_target(&frame_name),
5539 begin,
5540 );
5541 let instrumented = test.take_in(self.ast.allocator);
5542 let end = self.ast.expression_call(
5543 Span::default(),
5544 self.identifier(&self.mcdc_end),
5545 NONE,
5546 self.ast.vec_from_array([
5547 Argument::from(self.identifier(&frame_name)),
5548 Argument::from(instrumented),
5549 ]),
5550 false,
5551 );
5552 let observed = self.ast.expression_sequence(
5553 Span::default(),
5554 self.ast.vec_from_array([assign_frame, end]),
5555 );
5556 *test = if plan.inline_frame {
5557 self.wrap_inline_frame(observed, &[frame_name])
5558 } else {
5559 observed
5560 };
5561 return;
5562 }
5563
5564 let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
5565 let result_name = self.scratch_for("_supercovMcdcResult", plan.inline_frame);
5566 let temporary_names = (0..plan.condition_count)
5567 .map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
5568 .collect::<Vec<_>>();
5569 let mut next_index = 0;
5570 self.instrument_conditions(test, &frame_name, &temporary_names, &mut next_index);
5571 debug_assert_eq!(next_index, plan.condition_count);
5572
5573 let original = test.take_in(self.ast.allocator);
5574 let assign_frame = self.ast.expression_assignment(
5575 Span::default(),
5576 AssignmentOperator::Assign,
5577 self.assignment_target(&frame_name),
5578 self.number(0),
5579 );
5580 let assign_result = self.ast.expression_assignment(
5581 Span::default(),
5582 AssignmentOperator::Assign,
5583 self.assignment_target(&result_name),
5584 original,
5585 );
5586 let arguments = self.ast.vec_from_array([
5587 Argument::from(self.identifier(&self.probe_file_v2)),
5588 Argument::from(self.number(plan.index as u64)),
5589 Argument::from(self.identifier(&frame_name)),
5590 Argument::from(self.identifier(&result_name)),
5591 ]);
5592 let record = self.ast.expression_call(
5593 Span::default(),
5594 self.identifier(&self.mcdc_end_v2),
5595 NONE,
5596 arguments,
5597 false,
5598 );
5599 let observed = self.ast.expression_sequence(
5600 Span::default(),
5601 self.ast.vec_from_array([
5602 assign_frame,
5603 assign_result,
5604 record,
5605 self.identifier(&result_name),
5606 ]),
5607 );
5608 *test = if plan.inline_frame {
5609 let mut names = vec![frame_name, result_name];
5610 names.extend(temporary_names);
5611 self.wrap_inline_frame(observed, &names)
5612 } else {
5613 observed
5614 };
5615 }
5616}
5617
5618impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
5619 fn visit_program(&mut self, program: &mut Program<'a>) {
5620 self.enter_declaration_scope();
5621 walk_mut::walk_program(self, program);
5622 let declarations = self.leave_declaration_scope();
5623 self.prepend_declarations(declarations, &mut program.body);
5624 }
5625
5626 fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
5627 self.enter_declaration_scope();
5628 walk_mut::walk_function_body(self, body);
5629 let declarations = self.leave_declaration_scope();
5630 self.prepend_declarations(declarations, &mut body.statements);
5631 }
5632
5633 fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
5634 if self
5635 .source_sensitive_functions
5636 .contains(&span_key(function.span))
5637 {
5638 return;
5639 }
5640 let outer_parameter_depth = self.parameter_depth;
5641 self.parameter_depth = 0;
5642 walk_mut::walk_function(self, function, flags);
5643 self.parameter_depth = outer_parameter_depth;
5644 }
5645
5646 fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
5647 if self
5648 .source_sensitive_functions
5649 .contains(&span_key(function.span))
5650 {
5651 return;
5652 }
5653 let outer_parameter_depth = self.parameter_depth;
5654 self.parameter_depth = 0;
5655 walk_mut::walk_arrow_function_expression(self, function);
5656 self.parameter_depth = outer_parameter_depth;
5657 }
5658
5659 fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
5660 self.parameter_depth += 1;
5661 walk_mut::walk_formal_parameters(self, parameters);
5662 self.parameter_depth -= 1;
5663 }
5664
5665 fn visit_with_statement(&mut self, statement: &mut WithStatement<'a>) {
5666 if self.with_statements.contains(&span_key(statement.span)) {
5667 return;
5668 }
5669 walk_mut::walk_with_statement(self, statement);
5670 }
5671
5672 fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
5673 let plan = decision_outcome_is_variable(&statement.test)
5674 .then(|| self.reserve_decision(&statement.test));
5675 self.visit_expression(&mut statement.test);
5676 if let Some(plan) = plan {
5677 self.apply_decision(&mut statement.test, plan);
5678 }
5679 self.visit_statement(&mut statement.consequent);
5680 if let Some(alternate) = &mut statement.alternate {
5681 self.visit_statement(alternate);
5682 }
5683 }
5684
5685 fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
5686 let plan = decision_outcome_is_variable(&expression.test)
5687 .then(|| self.reserve_decision(&expression.test));
5688 self.visit_expression(&mut expression.test);
5689 if let Some(plan) = plan {
5690 self.apply_decision(&mut expression.test, plan);
5691 }
5692 self.visit_expression(&mut expression.consequent);
5693 self.visit_expression(&mut expression.alternate);
5694 }
5695
5696 fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
5697 let plan = decision_outcome_is_variable(&statement.test)
5698 .then(|| self.reserve_decision(&statement.test));
5699 self.visit_expression(&mut statement.test);
5700 if let Some(plan) = plan {
5701 self.apply_decision(&mut statement.test, plan);
5702 }
5703 self.visit_statement(&mut statement.body);
5704 }
5705
5706 fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
5707 let plan = decision_outcome_is_variable(&statement.test)
5708 .then(|| self.reserve_decision(&statement.test));
5709 self.visit_statement(&mut statement.body);
5710 self.visit_expression(&mut statement.test);
5711 if let Some(plan) = plan {
5712 self.apply_decision(&mut statement.test, plan);
5713 }
5714 }
5715
5716 fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
5717 let plan = statement
5718 .test
5719 .as_ref()
5720 .filter(|test| decision_outcome_is_variable(test))
5721 .map(|test| self.reserve_decision(test));
5722 if let Some(init) = &mut statement.init {
5723 self.visit_for_statement_init(init);
5724 }
5725 if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
5726 self.visit_expression(test);
5727 self.apply_decision(test, plan);
5728 }
5729 if let Some(update) = &mut statement.update {
5730 self.visit_expression(update);
5731 }
5732 self.visit_statement(&mut statement.body);
5733 }
5734}
5735
5736struct DecisionCollector<'s> {
5737 source: &'s str,
5738 file: &'s str,
5739 decisions: Vec<CandidateDecision>,
5740 decision_vector_counts: Vec<usize>,
5741 decision_logical_nodes: HashSet<SpanKey>,
5742 source_sensitive_functions: &'s HashSet<SpanKey>,
5743 with_statements: &'s HashSet<SpanKey>,
5744}
5745
5746impl DecisionCollector<'_> {
5747 fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
5748 let mut condition_spans = Vec::new();
5749 collect_conditions(test, &mut condition_spans);
5750 collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
5751 let span = transparent_expression(test).span();
5755 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
5756 self.decisions.push(CandidateDecision {
5757 id: stable_id(self.source, self.file, "decision", span, kind),
5758 file: self.file.to_string(),
5759 line,
5760 column,
5761 source: source_slice(self.source, span).to_string(),
5762 conditions: condition_spans
5763 .iter()
5764 .map(|condition| source_slice(self.source, *condition).to_string())
5765 .collect(),
5766 kind: kind.to_string(),
5767 });
5768 self.decision_vector_counts.push(
5769 if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
5770 reachable_vector_count(test, &condition_spans)
5771 } else {
5772 0
5773 },
5774 );
5775 }
5776}
5777
5778fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
5782 syntactic_boolean_outcome(expression).is_none()
5783}
5784
5785fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
5786 match transparent_expression(expression) {
5787 Expression::BooleanLiteral(literal) => Some(literal.value),
5788 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
5789 syntactic_boolean_outcome(&unary.argument).map(|value| !value)
5790 }
5791 Expression::LogicalExpression(logical)
5792 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5793 {
5794 let left = syntactic_boolean_outcome(&logical.left);
5795 let right = syntactic_boolean_outcome(&logical.right);
5796 match logical.operator {
5797 LogicalOperator::And => match (left, right) {
5798 (Some(false), _) | (_, Some(false)) => Some(false),
5799 (Some(true), value) | (value, Some(true)) => value,
5800 _ => None,
5801 },
5802 LogicalOperator::Or => match (left, right) {
5803 (Some(true), _) | (_, Some(true)) => Some(true),
5804 (Some(false), value) | (value, Some(false)) => value,
5805 _ => None,
5806 },
5807 LogicalOperator::Coalesce => None,
5808 }
5809 }
5810 _ => None,
5811 }
5812}
5813
5814#[derive(Default)]
5815struct CoverageSurfaceScanner {
5816 found: bool,
5817}
5818
5819impl<'a> Visit<'a> for CoverageSurfaceScanner {
5820 fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
5821 self.found = true;
5822 }
5823
5824 fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
5825 self.found = true;
5826 }
5827
5828 fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
5829 self.found = true;
5830 }
5831
5832 fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
5833 self.found = true;
5834 }
5835
5836 fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
5837 self.found = true;
5838 }
5839
5840 fn visit_class(&mut self, _class: &Class<'a>) {
5841 self.found = true;
5842 }
5843
5844 fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
5845 if expression.operator.is_logical() {
5846 self.found = true;
5847 } else {
5848 walk::walk_assignment_expression(self, expression);
5849 }
5850 }
5851}
5852
5853fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
5854 fn visit_condition(expression: &Expression<'_>) -> bool {
5855 let mut scanner = CoverageSurfaceScanner::default();
5856 scanner.visit_expression(expression);
5857 !scanner.found
5858 }
5859 match transparent_expression(expression) {
5860 Expression::LogicalExpression(logical)
5861 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5862 {
5863 decision_conditions_are_transparent(&logical.left)
5864 && decision_conditions_are_transparent(&logical.right)
5865 }
5866 Expression::UnaryExpression(unary)
5867 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5868 {
5869 decision_conditions_are_transparent(&unary.argument)
5870 }
5871 condition => visit_condition(condition),
5872 }
5873}
5874
5875fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
5876 fn evaluate(
5877 expression: &Expression<'_>,
5878 assignment: usize,
5879 encoded: &mut usize,
5880 indices: &HashMap<SpanKey, usize>,
5881 ) -> bool {
5882 match transparent_expression(expression) {
5883 Expression::LogicalExpression(logical)
5884 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5885 {
5886 let left = evaluate(&logical.left, assignment, encoded, indices);
5887 if logical.operator == LogicalOperator::And {
5888 left && evaluate(&logical.right, assignment, encoded, indices)
5889 } else {
5890 left || evaluate(&logical.right, assignment, encoded, indices)
5891 }
5892 }
5893 Expression::UnaryExpression(unary)
5894 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5895 {
5896 !evaluate(&unary.argument, assignment, encoded, indices)
5897 }
5898 condition => {
5899 let index = indices
5900 .get(&span_key(condition.span()))
5901 .expect("decision condition index must remain stable");
5902 let value = assignment & (1 << index) != 0;
5903 *encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
5904 value
5905 }
5906 }
5907 }
5908
5909 let indices = conditions
5910 .iter()
5911 .enumerate()
5912 .map(|(index, span)| (span_key(*span), index))
5913 .collect::<HashMap<_, _>>();
5914 let mut vectors = HashSet::new();
5915 for assignment in 0..(1_usize << conditions.len()) {
5916 let mut encoded = 0;
5917 let outcome = evaluate(expression, assignment, &mut encoded, &indices);
5918 vectors.insert(encoded * 2 + usize::from(outcome));
5919 }
5920 vectors.len()
5921}
5922
5923fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
5924 match expression {
5925 Expression::ParenthesizedExpression(parenthesized) => {
5926 collect_decision_logical_nodes(&parenthesized.expression, nodes);
5927 }
5928 Expression::LogicalExpression(logical)
5929 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
5930 {
5931 nodes.insert(span_key(logical.span));
5932 collect_decision_logical_nodes(&logical.left, nodes);
5933 collect_decision_logical_nodes(&logical.right, nodes);
5934 }
5935 Expression::UnaryExpression(unary)
5936 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
5937 {
5938 collect_decision_logical_nodes(&unary.argument, nodes);
5939 }
5940 _ => {}
5941 }
5942}
5943
5944#[derive(Default)]
5945struct LogicalBranchAnalysis {
5946 branches: Vec<CandidateBranch>,
5947 logical_targets: HashMap<SpanKey, (String, String)>,
5948}
5949
5950#[derive(Default)]
5951struct LogicalAssignmentAnalysis {
5952 branches: Vec<CandidateBranch>,
5953 targets: HashMap<SpanKey, (String, String)>,
5954}
5955
5956#[derive(Default)]
5957struct OptionalMemberAnalysis {
5958 branches: Vec<CandidateBranch>,
5959 targets: HashMap<SpanKey, (String, String)>,
5960}
5961
5962#[derive(Default)]
5963struct OptionalCallAnalysis {
5964 branches: Vec<CandidateBranch>,
5965 sites: HashMap<SpanKey, (String, String)>,
5966 roots: HashMap<SpanKey, Vec<SpanKey>>,
5967 limitations: Vec<CandidateLimitation>,
5968}
5969
5970#[derive(Clone)]
5971struct DefaultTarget {
5972 default_id: String,
5973 provided_id: String,
5974 inferred_name: Option<String>,
5975}
5976
5977#[derive(Default)]
5978struct DefaultAnalysis {
5979 branches: Vec<CandidateBranch>,
5980 parameter_targets: HashMap<SpanKey, DefaultTarget>,
5981 binding_targets: HashMap<SpanKey, DefaultTarget>,
5982 limitations: Vec<CandidateLimitation>,
5983}
5984
5985#[derive(Clone)]
5986struct ExtendedTarget {
5987 first_id: String,
5988 second_id: String,
5989}
5990
5991#[derive(Default)]
5992struct ExtendedAnalysis {
5993 branches: Vec<CandidateBranch>,
5994 try_targets: HashMap<SpanKey, ExtendedTarget>,
5995 loop_targets: HashMap<SpanKey, ExtendedTarget>,
5996}
5997
5998#[derive(Clone)]
5999struct SwitchTarget {
6000 case_ids: Vec<String>,
6001 no_match_id: Option<String>,
6002}
6003
6004#[derive(Default)]
6005struct SwitchAnalysis {
6006 branches: Vec<CandidateBranch>,
6007 targets: HashMap<SpanKey, SwitchTarget>,
6008}
6009
6010struct SwitchCollector<'s> {
6011 source: &'s str,
6012 file: &'s str,
6013 source_sensitive_functions: &'s HashSet<SpanKey>,
6014 unsafe_function_depth: usize,
6015 with_depth: usize,
6016 suppressed_depth: usize,
6017 suppressed_nodes: Vec<bool>,
6018 analysis: SwitchAnalysis,
6019}
6020
6021impl SwitchCollector<'_> {
6022 fn unsafe_context(&self) -> bool {
6023 self.unsafe_function_depth > 0 || self.with_depth > 0
6024 }
6025
6026 fn exit_source_function(&mut self, span: Span) {
6027 if self.source_sensitive_functions.contains(&span_key(span)) {
6028 self.unsafe_function_depth -= 1;
6029 }
6030 }
6031}
6032
6033impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
6034 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6035 if self
6036 .source_sensitive_functions
6037 .contains(&span_key(node.span))
6038 {
6039 self.unsafe_function_depth += 1;
6040 }
6041 }
6042
6043 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6044 self.exit_source_function(node.span);
6045 }
6046
6047 fn enter_arrow_function_expression(
6048 &mut self,
6049 node: &mut ArrowFunctionExpression<'a>,
6050 _context: &mut TraverseCtx<'a, ()>,
6051 ) {
6052 if self
6053 .source_sensitive_functions
6054 .contains(&span_key(node.span))
6055 {
6056 self.unsafe_function_depth += 1;
6057 }
6058 }
6059
6060 fn exit_arrow_function_expression(
6061 &mut self,
6062 node: &mut ArrowFunctionExpression<'a>,
6063 _context: &mut TraverseCtx<'a, ()>,
6064 ) {
6065 self.exit_source_function(node.span);
6066 }
6067
6068 fn enter_with_statement(
6069 &mut self,
6070 _node: &mut WithStatement<'a>,
6071 _context: &mut TraverseCtx<'a, ()>,
6072 ) {
6073 self.with_depth += 1;
6074 }
6075
6076 fn exit_with_statement(
6077 &mut self,
6078 _node: &mut WithStatement<'a>,
6079 _context: &mut TraverseCtx<'a, ()>,
6080 ) {
6081 self.with_depth -= 1;
6082 }
6083
6084 fn enter_switch_statement(
6085 &mut self,
6086 node: &mut SwitchStatement<'a>,
6087 _context: &mut TraverseCtx<'a, ()>,
6088 ) {
6089 let has_default = node.cases.iter().any(|case| case.test.is_none());
6090 let transformed = self.suppressed_depth == 0 && !self.unsafe_context();
6091 let suppresses = transformed && !has_default;
6092 self.suppressed_nodes.push(suppresses);
6093 if suppresses {
6094 self.suppressed_depth += 1;
6095 }
6096 if !transformed {
6097 return;
6098 }
6099 let id = stable_id(self.source, self.file, "switch", node.span, "");
6100 let mut case_ids = Vec::with_capacity(node.cases.len());
6101 let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
6102 for (index, case) in node.cases.iter().enumerate() {
6103 let alternative_id = format!("{id}:case:{index}");
6104 let label = case.test.as_ref().map_or_else(
6105 || "default".to_string(),
6106 |test| format!("case {}", source_slice(self.source, test.span())),
6107 );
6108 case_ids.push(alternative_id.clone());
6109 alternatives.push(CandidateBranchAlternative {
6110 id: alternative_id,
6111 label,
6112 });
6113 }
6114 let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
6115 if let Some(no_match_id) = &no_match_id {
6116 alternatives.push(CandidateBranchAlternative {
6117 id: no_match_id.clone(),
6118 label: "no matching case".to_string(),
6119 });
6120 }
6121 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6122 self.analysis.branches.push(CandidateBranch {
6123 id,
6124 kind: "switch".to_string(),
6125 file: self.file.to_string(),
6126 line,
6127 column,
6128 source: source_slice(self.source, node.discriminant.span()).to_string(),
6129 alternatives,
6130 });
6131 self.analysis.targets.insert(
6132 span_key(node.span),
6133 SwitchTarget {
6134 case_ids,
6135 no_match_id,
6136 },
6137 );
6138 }
6139
6140 fn exit_switch_statement(
6141 &mut self,
6142 _node: &mut SwitchStatement<'a>,
6143 _context: &mut TraverseCtx<'a, ()>,
6144 ) {
6145 if self
6146 .suppressed_nodes
6147 .pop()
6148 .expect("switch collector stack must remain balanced")
6149 {
6150 self.suppressed_depth -= 1;
6151 }
6152 }
6153}
6154
6155fn collect_switch_branches<'a>(
6156 allocator: &'a Allocator,
6157 program: &mut Program<'a>,
6158 source: &str,
6159 file: &str,
6160 source_sensitive_functions: &HashSet<SpanKey>,
6161) -> SwitchAnalysis {
6162 let mut collector = SwitchCollector {
6163 source,
6164 file,
6165 source_sensitive_functions,
6166 unsafe_function_depth: 0,
6167 with_depth: 0,
6168 suppressed_depth: 0,
6169 suppressed_nodes: Vec::new(),
6170 analysis: SwitchAnalysis::default(),
6171 };
6172 traverse_mut(&mut collector, allocator, program, Default::default(), ());
6173 collector.analysis
6174}
6175
6176struct ExtendedCollector<'s> {
6177 source: &'s str,
6178 file: &'s str,
6179 source_sensitive_functions: &'s HashSet<SpanKey>,
6180 entered_loops: HashSet<SpanKey>,
6181 unsafe_function_depth: usize,
6182 with_depth: usize,
6183 analysis: ExtendedAnalysis,
6184}
6185
6186impl ExtendedCollector<'_> {
6187 fn unsafe_context(&self) -> bool {
6188 self.unsafe_function_depth > 0 || self.with_depth > 0
6189 }
6190
6191 fn enter_try(&mut self, node: &TryStatement<'_>) {
6192 let transformed = !self.unsafe_context() && node.handler.is_some();
6193 if !transformed {
6194 return;
6195 }
6196 let id = stable_id(self.source, self.file, "try-catch", node.span, "");
6197 let success_id = format!("{id}:success");
6198 let catch_id = format!("{id}:catch");
6199 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6200 self.analysis.branches.push(CandidateBranch {
6201 id,
6202 kind: "try-catch".to_string(),
6203 file: self.file.to_string(),
6204 line,
6205 column,
6206 source: "try / catch".to_string(),
6207 alternatives: vec![
6208 CandidateBranchAlternative {
6209 id: success_id.clone(),
6210 label: "try completed without catch".to_string(),
6211 },
6212 CandidateBranchAlternative {
6213 id: catch_id.clone(),
6214 label: "catch entered".to_string(),
6215 },
6216 ],
6217 });
6218 self.analysis.try_targets.insert(
6219 span_key(node.span),
6220 ExtendedTarget {
6221 first_id: success_id,
6222 second_id: catch_id,
6223 },
6224 );
6225 }
6226
6227 fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
6228 let transformed = !self.unsafe_context();
6229 if !transformed {
6230 return;
6231 }
6232 if self.entered_loops.contains(&span_key(span)) {
6236 return;
6237 }
6238 let id = stable_id(self.source, self.file, kind, span, "");
6239 let zero_id = format!("{id}:zero");
6240 let entered_id = format!("{id}:entered");
6241 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
6242 self.analysis.branches.push(CandidateBranch {
6243 id,
6244 kind: kind.to_string(),
6245 file: self.file.to_string(),
6246 line,
6247 column,
6248 source: source_slice(self.source, right.span()).to_string(),
6249 alternatives: vec![
6250 CandidateBranchAlternative {
6251 id: zero_id.clone(),
6252 label: "zero iterations".to_string(),
6253 },
6254 CandidateBranchAlternative {
6255 id: entered_id.clone(),
6256 label: "one or more iterations".to_string(),
6257 },
6258 ],
6259 });
6260 self.analysis.loop_targets.insert(
6261 span_key(span),
6262 ExtendedTarget {
6263 first_id: zero_id,
6264 second_id: entered_id,
6265 },
6266 );
6267 }
6268
6269 fn exit_source_function(&mut self, span: Span) {
6270 if self.source_sensitive_functions.contains(&span_key(span)) {
6271 self.unsafe_function_depth -= 1;
6272 }
6273 }
6274}
6275
6276impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
6277 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6278 if self
6279 .source_sensitive_functions
6280 .contains(&span_key(node.span))
6281 {
6282 self.unsafe_function_depth += 1;
6283 }
6284 }
6285
6286 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6287 self.exit_source_function(node.span);
6288 }
6289
6290 fn enter_arrow_function_expression(
6291 &mut self,
6292 node: &mut ArrowFunctionExpression<'a>,
6293 _context: &mut TraverseCtx<'a, ()>,
6294 ) {
6295 if self
6296 .source_sensitive_functions
6297 .contains(&span_key(node.span))
6298 {
6299 self.unsafe_function_depth += 1;
6300 }
6301 }
6302
6303 fn exit_arrow_function_expression(
6304 &mut self,
6305 node: &mut ArrowFunctionExpression<'a>,
6306 _context: &mut TraverseCtx<'a, ()>,
6307 ) {
6308 self.exit_source_function(node.span);
6309 }
6310
6311 fn enter_with_statement(
6312 &mut self,
6313 _node: &mut WithStatement<'a>,
6314 _context: &mut TraverseCtx<'a, ()>,
6315 ) {
6316 self.with_depth += 1;
6317 }
6318
6319 fn exit_with_statement(
6320 &mut self,
6321 _node: &mut WithStatement<'a>,
6322 _context: &mut TraverseCtx<'a, ()>,
6323 ) {
6324 self.with_depth -= 1;
6325 }
6326
6327 fn enter_try_statement(
6328 &mut self,
6329 node: &mut TryStatement<'a>,
6330 _context: &mut TraverseCtx<'a, ()>,
6331 ) {
6332 self.enter_try(node);
6333 }
6334
6335 fn exit_try_statement(
6336 &mut self,
6337 _node: &mut TryStatement<'a>,
6338 _context: &mut TraverseCtx<'a, ()>,
6339 ) {
6340 }
6341
6342 fn enter_for_in_statement(
6343 &mut self,
6344 node: &mut ForInStatement<'a>,
6345 _context: &mut TraverseCtx<'a, ()>,
6346 ) {
6347 self.enter_loop(node.span, &node.right, "for-in");
6348 }
6349
6350 fn exit_for_in_statement(
6351 &mut self,
6352 _node: &mut ForInStatement<'a>,
6353 _context: &mut TraverseCtx<'a, ()>,
6354 ) {
6355 }
6356
6357 fn enter_for_of_statement(
6358 &mut self,
6359 node: &mut ForOfStatement<'a>,
6360 _context: &mut TraverseCtx<'a, ()>,
6361 ) {
6362 self.enter_loop(node.span, &node.right, "for-of");
6363 }
6364
6365 fn exit_for_of_statement(
6366 &mut self,
6367 _node: &mut ForOfStatement<'a>,
6368 _context: &mut TraverseCtx<'a, ()>,
6369 ) {
6370 }
6371}
6372
6373fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
6386 let semantic = SemanticBuilder::new().build(program).semantic;
6387 let mut found = HashSet::new();
6388 for node in semantic.nodes() {
6389 if let AstKind::ForOfStatement(loop_node) = node.kind()
6390 && never_empty(&loop_node.right, &semantic)
6391 {
6392 found.insert(span_key(loop_node.span));
6393 }
6394 }
6395 found
6396}
6397
6398fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
6401 match transparent_expression(expression) {
6402 Expression::ArrayExpression(array) => has_definite_element(array),
6403 Expression::Identifier(identifier) => {
6404 let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
6405 return false;
6406 };
6407 if semantic.scoping().get_resolved_references(symbol).count() != 1 {
6412 return false;
6413 }
6414 let declaration = semantic.scoping().symbol_declaration(symbol);
6415 let AstKind::VariableDeclarator(declarator) =
6416 semantic.nodes().get_node(declaration).kind()
6417 else {
6418 return false;
6419 };
6420 declarator.kind == VariableDeclarationKind::Const
6421 && declarator.init.as_ref().is_some_and(|init| {
6422 matches!(transparent_expression(init), Expression::ArrayExpression(array)
6423 if has_definite_element(array))
6424 })
6425 }
6426 _ => false,
6427 }
6428}
6429
6430fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
6434 array
6435 .elements
6436 .iter()
6437 .any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
6438}
6439
6440fn collect_extended_branches<'a>(
6441 allocator: &'a Allocator,
6442 program: &mut Program<'a>,
6443 source: &str,
6444 file: &str,
6445 source_sensitive_functions: &HashSet<SpanKey>,
6446) -> ExtendedAnalysis {
6447 let entered_loops = provably_entered_loops(program);
6448 let mut collector = ExtendedCollector {
6449 source,
6450 file,
6451 source_sensitive_functions,
6452 entered_loops,
6453 unsafe_function_depth: 0,
6454 with_depth: 0,
6455 analysis: ExtendedAnalysis::default(),
6456 };
6457 traverse_mut(&mut collector, allocator, program, Default::default(), ());
6458 collector.analysis
6459}
6460
6461struct DefaultCollector<'s> {
6462 source: &'s str,
6463 file: &'s str,
6464 source_sensitive_functions: &'s HashSet<SpanKey>,
6465 unsafe_function_depth: usize,
6466 with_depth: usize,
6467 analysis: DefaultAnalysis,
6468}
6469
6470impl DefaultCollector<'_> {
6471 fn unsafe_context(&self) -> bool {
6472 self.unsafe_function_depth > 0 || self.with_depth > 0
6473 }
6474
6475 fn target(
6476 &mut self,
6477 span: Span,
6478 left: &BindingPattern<'_>,
6479 right: &Expression<'_>,
6480 ) -> DefaultTarget {
6481 let id = stable_id(self.source, self.file, "default-value", span, "");
6482 let default_id = format!("{id}:default");
6483 let provided_id = format!("{id}:provided");
6484 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
6485 self.analysis.branches.push(CandidateBranch {
6486 id,
6487 kind: "default-value".to_string(),
6488 file: self.file.to_string(),
6489 line,
6490 column,
6491 source: source_slice(self.source, span).to_string(),
6492 alternatives: vec![
6493 CandidateBranchAlternative {
6494 id: default_id.clone(),
6495 label: "default evaluated".to_string(),
6496 },
6497 CandidateBranchAlternative {
6498 id: provided_id.clone(),
6499 label: "value provided".to_string(),
6500 },
6501 ],
6502 });
6503 DefaultTarget {
6504 default_id,
6505 provided_id,
6506 inferred_name: binding_identifier_name(left)
6507 .filter(|_| expression_is_anonymous_definition(right)),
6508 }
6509 }
6510
6511 fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
6512 match pattern {
6513 BindingPattern::AssignmentPattern(assignment) => {
6514 let target = self.target(assignment.span, &assignment.left, &assignment.right);
6515 if parameter {
6516 self.analysis
6517 .parameter_targets
6518 .insert(span_key(assignment.span), target);
6519 } else {
6520 self.analysis
6521 .binding_targets
6522 .insert(span_key(assignment.span), target);
6523 }
6524 self.collect_binding_pattern(&assignment.left, parameter);
6525 }
6526 BindingPattern::ObjectPattern(object) => {
6527 for property in &object.properties {
6528 self.collect_binding_pattern(&property.value, parameter);
6529 }
6530 if let Some(rest) = &object.rest {
6531 self.collect_binding_pattern(&rest.argument, parameter);
6532 }
6533 }
6534 BindingPattern::ArrayPattern(array) => {
6535 for element in array.elements.iter().flatten() {
6536 self.collect_binding_pattern(element, parameter);
6537 }
6538 if let Some(rest) = &array.rest {
6539 self.collect_binding_pattern(&rest.argument, parameter);
6540 }
6541 }
6542 BindingPattern::BindingIdentifier(_) => {}
6543 }
6544 }
6545
6546 fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
6547 for parameter in ¶meters.items {
6548 if let Some(initializer) = ¶meter.initializer {
6549 let default_span =
6556 Span::new(parameter.pattern.span().start, initializer.span().end);
6557 let target = self.target(default_span, ¶meter.pattern, initializer);
6558 self.analysis
6559 .parameter_targets
6560 .insert(span_key(parameter.span), target);
6561 }
6562 self.collect_binding_pattern(¶meter.pattern, true);
6563 }
6564 if let Some(rest) = ¶meters.rest {
6565 self.collect_binding_pattern(&rest.rest.argument, true);
6566 }
6567 }
6568
6569 fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
6570 match pattern {
6571 BindingPattern::AssignmentPattern(_) => true,
6572 BindingPattern::ObjectPattern(object) => {
6573 object
6574 .properties
6575 .iter()
6576 .any(|property| Self::has_binding_default(&property.value))
6577 || object
6578 .rest
6579 .as_ref()
6580 .is_some_and(|rest| Self::has_binding_default(&rest.argument))
6581 }
6582 BindingPattern::ArrayPattern(array) => {
6583 array
6584 .elements
6585 .iter()
6586 .flatten()
6587 .any(Self::has_binding_default)
6588 || array
6589 .rest
6590 .as_ref()
6591 .is_some_and(|rest| Self::has_binding_default(&rest.argument))
6592 }
6593 BindingPattern::BindingIdentifier(_) => false,
6594 }
6595 }
6596
6597 fn exit_source_function(&mut self, span: Span) {
6598 if self.source_sensitive_functions.contains(&span_key(span)) {
6599 self.unsafe_function_depth -= 1;
6600 }
6601 }
6602}
6603
6604impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
6605 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6606 if self
6607 .source_sensitive_functions
6608 .contains(&span_key(node.span))
6609 {
6610 self.unsafe_function_depth += 1;
6611 return;
6612 }
6613 if !self.unsafe_context() && node.body.is_some() {
6614 self.collect_parameters(&node.params);
6615 }
6616 }
6617
6618 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6619 self.exit_source_function(node.span);
6620 }
6621
6622 fn enter_arrow_function_expression(
6623 &mut self,
6624 node: &mut ArrowFunctionExpression<'a>,
6625 _context: &mut TraverseCtx<'a, ()>,
6626 ) {
6627 if self
6628 .source_sensitive_functions
6629 .contains(&span_key(node.span))
6630 {
6631 self.unsafe_function_depth += 1;
6632 return;
6633 }
6634 if !self.unsafe_context() {
6635 self.collect_parameters(&node.params);
6636 }
6637 }
6638
6639 fn exit_arrow_function_expression(
6640 &mut self,
6641 node: &mut ArrowFunctionExpression<'a>,
6642 _context: &mut TraverseCtx<'a, ()>,
6643 ) {
6644 self.exit_source_function(node.span);
6645 }
6646
6647 fn enter_with_statement(
6648 &mut self,
6649 _node: &mut WithStatement<'a>,
6650 _context: &mut TraverseCtx<'a, ()>,
6651 ) {
6652 self.with_depth += 1;
6653 }
6654
6655 fn exit_with_statement(
6656 &mut self,
6657 _node: &mut WithStatement<'a>,
6658 _context: &mut TraverseCtx<'a, ()>,
6659 ) {
6660 self.with_depth -= 1;
6661 }
6662
6663 fn enter_variable_declaration(
6664 &mut self,
6665 node: &mut VariableDeclaration<'a>,
6666 context: &mut TraverseCtx<'a, ()>,
6667 ) {
6668 if self.unsafe_context() {
6669 return;
6670 }
6671 let has_default = node
6672 .declarations
6673 .iter()
6674 .any(|declaration| Self::has_binding_default(&declaration.id));
6675 if !has_default {
6676 return;
6677 }
6678 if matches!(
6679 context.ancestors().next(),
6680 Some(Ancestor::ForStatementInit(_))
6681 ) {
6682 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6683 self.analysis.limitations.push(CandidateLimitation {
6684 id: stable_id(
6685 self.source,
6686 self.file,
6687 "dynamic-code",
6688 node.span,
6689 "for-init-default",
6690 ),
6691 kind: "dynamic-code".to_string(),
6692 file: self.file.to_string(),
6693 line,
6694 column,
6695 source: source_slice(self.source, node.span).to_string(),
6696 reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
6697 });
6698 return;
6699 }
6700 for declaration in &node.declarations {
6701 self.collect_binding_pattern(&declaration.id, false);
6702 }
6703 }
6704}
6705
6706fn collect_default_branches<'a>(
6707 allocator: &'a Allocator,
6708 program: &mut Program<'a>,
6709 source: &str,
6710 file: &str,
6711 source_sensitive_functions: &HashSet<SpanKey>,
6712) -> DefaultAnalysis {
6713 let mut collector = DefaultCollector {
6714 source,
6715 file,
6716 source_sensitive_functions,
6717 unsafe_function_depth: 0,
6718 with_depth: 0,
6719 analysis: DefaultAnalysis::default(),
6720 };
6721 traverse_mut(&mut collector, allocator, program, Default::default(), ());
6722 collector.analysis
6723}
6724
6725struct OptionalCallCollector<'s> {
6726 source: &'s str,
6727 file: &'s str,
6728 source_sensitive_functions: &'s HashSet<SpanKey>,
6729 unsafe_function_depth: usize,
6730 with_depth: usize,
6731 chain_roots: Vec<SpanKey>,
6732 analysis: OptionalCallAnalysis,
6733}
6734
6735impl OptionalCallCollector<'_> {
6736 fn unsafe_context(&self) -> bool {
6737 self.unsafe_function_depth > 0 || self.with_depth > 0
6738 }
6739
6740 fn exit_source_function(&mut self, span: Span) {
6741 if self.source_sensitive_functions.contains(&span_key(span)) {
6742 self.unsafe_function_depth -= 1;
6743 }
6744 }
6745}
6746
6747impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
6748 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6749 if self
6750 .source_sensitive_functions
6751 .contains(&span_key(node.span))
6752 {
6753 self.unsafe_function_depth += 1;
6754 }
6755 }
6756
6757 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6758 self.exit_source_function(node.span);
6759 }
6760
6761 fn enter_arrow_function_expression(
6762 &mut self,
6763 node: &mut ArrowFunctionExpression<'a>,
6764 _context: &mut TraverseCtx<'a, ()>,
6765 ) {
6766 if self
6767 .source_sensitive_functions
6768 .contains(&span_key(node.span))
6769 {
6770 self.unsafe_function_depth += 1;
6771 }
6772 }
6773
6774 fn exit_arrow_function_expression(
6775 &mut self,
6776 node: &mut ArrowFunctionExpression<'a>,
6777 _context: &mut TraverseCtx<'a, ()>,
6778 ) {
6779 self.exit_source_function(node.span);
6780 }
6781
6782 fn enter_with_statement(
6783 &mut self,
6784 _node: &mut WithStatement<'a>,
6785 _context: &mut TraverseCtx<'a, ()>,
6786 ) {
6787 self.with_depth += 1;
6788 }
6789
6790 fn exit_with_statement(
6791 &mut self,
6792 _node: &mut WithStatement<'a>,
6793 _context: &mut TraverseCtx<'a, ()>,
6794 ) {
6795 self.with_depth -= 1;
6796 }
6797
6798 fn enter_chain_expression(
6799 &mut self,
6800 node: &mut ChainExpression<'a>,
6801 context: &mut TraverseCtx<'a, ()>,
6802 ) {
6803 let root = match context.ancestors().next() {
6804 Some(Ancestor::UnaryExpressionArgument(parent))
6805 if *parent.operator() == UnaryOperator::Delete =>
6806 {
6807 span_key(*parent.span())
6808 }
6809 _ => span_key(node.span),
6810 };
6811 self.chain_roots.push(root);
6812 }
6813
6814 fn exit_chain_expression(
6815 &mut self,
6816 _node: &mut ChainExpression<'a>,
6817 _context: &mut TraverseCtx<'a, ()>,
6818 ) {
6819 self.chain_roots.pop();
6820 }
6821
6822 fn enter_call_expression(
6823 &mut self,
6824 node: &mut CallExpression<'a>,
6825 _context: &mut TraverseCtx<'a, ()>,
6826 ) {
6827 if !node.optional || self.unsafe_context() {
6828 return;
6829 }
6830 let root = *self
6831 .chain_roots
6832 .last()
6833 .expect("an optional call must be enclosed by a chain expression");
6834 let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
6835 let short_id = format!("{id}:short");
6836 let continued_id = format!("{id}:continued");
6837 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
6838 self.analysis.sites.insert(
6839 span_key(node.span),
6840 (short_id.clone(), continued_id.clone()),
6841 );
6842 self.analysis
6843 .roots
6844 .entry(root)
6845 .or_default()
6846 .push(span_key(node.span));
6847 self.analysis.branches.push(CandidateBranch {
6848 id,
6849 kind: "optional-chain".to_string(),
6850 file: self.file.to_string(),
6851 line,
6852 column,
6853 source: source_slice(self.source, node.span).to_string(),
6854 alternatives: vec![
6855 CandidateBranchAlternative {
6856 id: short_id,
6857 label: "nullish / short-circuited".to_string(),
6858 },
6859 CandidateBranchAlternative {
6860 id: continued_id,
6861 label: "non-nullish / continued".to_string(),
6862 },
6863 ],
6864 });
6865 }
6866}
6867
6868fn collect_optional_call_branches<'a>(
6869 allocator: &'a Allocator,
6870 program: &mut Program<'a>,
6871 source: &str,
6872 file: &str,
6873 source_sensitive_functions: &HashSet<SpanKey>,
6874) -> OptionalCallAnalysis {
6875 let mut collector = OptionalCallCollector {
6876 source,
6877 file,
6878 source_sensitive_functions,
6879 unsafe_function_depth: 0,
6880 with_depth: 0,
6881 chain_roots: Vec::new(),
6882 analysis: OptionalCallAnalysis::default(),
6883 };
6884 traverse_mut(&mut collector, allocator, program, Default::default(), ());
6885 collector.analysis
6886}
6887
6888struct OptionalMemberCollector<'s> {
6889 source: &'s str,
6890 file: &'s str,
6891 source_sensitive_functions: &'s HashSet<SpanKey>,
6892 unsafe_function_depth: usize,
6893 with_depth: usize,
6894 analysis: OptionalMemberAnalysis,
6895}
6896
6897impl OptionalMemberCollector<'_> {
6898 fn record(&mut self, span: Span, optional: bool) {
6899 if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
6900 return;
6901 }
6902 let id = stable_id(self.source, self.file, "optional-chain", span, "");
6903 let short_id = format!("{id}:short");
6904 let continued_id = format!("{id}:continued");
6905 let (line, column) = line_and_utf16_column(self.source, span.start as usize);
6906 self.analysis
6907 .targets
6908 .insert(span_key(span), (short_id.clone(), continued_id.clone()));
6909 self.analysis.branches.push(CandidateBranch {
6910 id,
6911 kind: "optional-chain".to_string(),
6912 file: self.file.to_string(),
6913 line,
6914 column,
6915 source: source_slice(self.source, span).to_string(),
6916 alternatives: vec![
6917 CandidateBranchAlternative {
6918 id: short_id,
6919 label: "nullish / short-circuited".to_string(),
6920 },
6921 CandidateBranchAlternative {
6922 id: continued_id,
6923 label: "non-nullish / continued".to_string(),
6924 },
6925 ],
6926 });
6927 }
6928
6929 fn exit_source_function(&mut self, span: Span) {
6930 if self.source_sensitive_functions.contains(&span_key(span)) {
6931 self.unsafe_function_depth -= 1;
6932 }
6933 }
6934}
6935
6936impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
6937 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6938 if self
6939 .source_sensitive_functions
6940 .contains(&span_key(node.span))
6941 {
6942 self.unsafe_function_depth += 1;
6943 }
6944 }
6945
6946 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
6947 self.exit_source_function(node.span);
6948 }
6949
6950 fn enter_arrow_function_expression(
6951 &mut self,
6952 node: &mut ArrowFunctionExpression<'a>,
6953 _context: &mut TraverseCtx<'a, ()>,
6954 ) {
6955 if self
6956 .source_sensitive_functions
6957 .contains(&span_key(node.span))
6958 {
6959 self.unsafe_function_depth += 1;
6960 }
6961 }
6962
6963 fn exit_arrow_function_expression(
6964 &mut self,
6965 node: &mut ArrowFunctionExpression<'a>,
6966 _context: &mut TraverseCtx<'a, ()>,
6967 ) {
6968 self.exit_source_function(node.span);
6969 }
6970
6971 fn enter_with_statement(
6972 &mut self,
6973 _node: &mut WithStatement<'a>,
6974 _context: &mut TraverseCtx<'a, ()>,
6975 ) {
6976 self.with_depth += 1;
6977 }
6978
6979 fn exit_with_statement(
6980 &mut self,
6981 _node: &mut WithStatement<'a>,
6982 _context: &mut TraverseCtx<'a, ()>,
6983 ) {
6984 self.with_depth -= 1;
6985 }
6986
6987 fn enter_computed_member_expression(
6988 &mut self,
6989 node: &mut ComputedMemberExpression<'a>,
6990 _context: &mut TraverseCtx<'a, ()>,
6991 ) {
6992 self.record(node.span, node.optional);
6993 }
6994
6995 fn enter_static_member_expression(
6996 &mut self,
6997 node: &mut StaticMemberExpression<'a>,
6998 _context: &mut TraverseCtx<'a, ()>,
6999 ) {
7000 self.record(node.span, node.optional);
7001 }
7002
7003 fn enter_private_field_expression(
7004 &mut self,
7005 node: &mut PrivateFieldExpression<'a>,
7006 _context: &mut TraverseCtx<'a, ()>,
7007 ) {
7008 self.record(node.span, node.optional);
7009 }
7010}
7011
7012fn collect_optional_member_branches<'a>(
7013 allocator: &'a Allocator,
7014 program: &mut Program<'a>,
7015 source: &str,
7016 file: &str,
7017 source_sensitive_functions: &HashSet<SpanKey>,
7018) -> OptionalMemberAnalysis {
7019 let mut collector = OptionalMemberCollector {
7020 source,
7021 file,
7022 source_sensitive_functions,
7023 unsafe_function_depth: 0,
7024 with_depth: 0,
7025 analysis: OptionalMemberAnalysis::default(),
7026 };
7027 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7028 collector.analysis
7029}
7030
7031struct LogicalAssignmentCollector<'s> {
7032 source: &'s str,
7033 file: &'s str,
7034 source_sensitive_functions: &'s HashSet<SpanKey>,
7035 unsafe_function_depth: usize,
7036 with_depth: usize,
7037 analysis: LogicalAssignmentAnalysis,
7038}
7039
7040impl LogicalAssignmentCollector<'_> {
7041 fn exit_source_function(&mut self, span: Span) {
7042 if self.source_sensitive_functions.contains(&span_key(span)) {
7043 self.unsafe_function_depth -= 1;
7044 }
7045 }
7046}
7047
7048impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
7049 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7050 if self
7051 .source_sensitive_functions
7052 .contains(&span_key(node.span))
7053 {
7054 self.unsafe_function_depth += 1;
7055 }
7056 }
7057
7058 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7059 self.exit_source_function(node.span);
7060 }
7061
7062 fn enter_arrow_function_expression(
7063 &mut self,
7064 node: &mut ArrowFunctionExpression<'a>,
7065 _context: &mut TraverseCtx<'a, ()>,
7066 ) {
7067 if self
7068 .source_sensitive_functions
7069 .contains(&span_key(node.span))
7070 {
7071 self.unsafe_function_depth += 1;
7072 }
7073 }
7074
7075 fn exit_arrow_function_expression(
7076 &mut self,
7077 node: &mut ArrowFunctionExpression<'a>,
7078 _context: &mut TraverseCtx<'a, ()>,
7079 ) {
7080 self.exit_source_function(node.span);
7081 }
7082
7083 fn enter_with_statement(
7084 &mut self,
7085 _node: &mut WithStatement<'a>,
7086 _context: &mut TraverseCtx<'a, ()>,
7087 ) {
7088 self.with_depth += 1;
7089 }
7090
7091 fn exit_with_statement(
7092 &mut self,
7093 _node: &mut WithStatement<'a>,
7094 _context: &mut TraverseCtx<'a, ()>,
7095 ) {
7096 self.with_depth -= 1;
7097 }
7098
7099 fn exit_assignment_expression(
7100 &mut self,
7101 node: &mut AssignmentExpression<'a>,
7102 _context: &mut TraverseCtx<'a, ()>,
7103 ) {
7104 if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
7105 return;
7106 }
7107 let operator = match node.operator {
7108 AssignmentOperator::LogicalAnd => "&&=",
7109 AssignmentOperator::LogicalOr => "||=",
7110 AssignmentOperator::LogicalNullish => "??=",
7111 _ => unreachable!("logical assignment filter must be exhaustive"),
7112 };
7113 let id = stable_id(
7114 self.source,
7115 self.file,
7116 "logical-assignment",
7117 node.span,
7118 operator,
7119 );
7120 let short_id = format!("{id}:short");
7121 let right_id = format!("{id}:right");
7122 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
7123 self.analysis
7124 .targets
7125 .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
7126 self.analysis.branches.push(CandidateBranch {
7127 id,
7128 kind: "logical-assignment".to_string(),
7129 file: self.file.to_string(),
7130 line,
7131 column,
7132 source: source_slice(self.source, node.span).to_string(),
7133 alternatives: vec![
7134 CandidateBranchAlternative {
7135 id: short_id,
7136 label: "assignment skipped".to_string(),
7137 },
7138 CandidateBranchAlternative {
7139 id: right_id,
7140 label: "right evaluated / assigned".to_string(),
7141 },
7142 ],
7143 });
7144 }
7145}
7146
7147fn collect_logical_assignment_branches<'a>(
7148 allocator: &'a Allocator,
7149 program: &mut Program<'a>,
7150 source: &str,
7151 file: &str,
7152 source_sensitive_functions: &HashSet<SpanKey>,
7153) -> LogicalAssignmentAnalysis {
7154 let mut collector = LogicalAssignmentCollector {
7155 source,
7156 file,
7157 source_sensitive_functions,
7158 unsafe_function_depth: 0,
7159 with_depth: 0,
7160 analysis: LogicalAssignmentAnalysis::default(),
7161 };
7162 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7163 collector.analysis
7164}
7165
7166struct LogicalBranchCollector<'s> {
7167 source: &'s str,
7168 file: &'s str,
7169 decision_logical_nodes: &'s HashSet<SpanKey>,
7170 source_sensitive_functions: &'s HashSet<SpanKey>,
7171 unsafe_function_depth: usize,
7172 with_depth: usize,
7173 analysis: LogicalBranchAnalysis,
7174}
7175
7176impl LogicalBranchCollector<'_> {
7177 fn exit_source_function(&mut self, span: Span) {
7178 if self.source_sensitive_functions.contains(&span_key(span)) {
7179 self.unsafe_function_depth -= 1;
7180 }
7181 }
7182}
7183
7184impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
7185 fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7186 if self
7187 .source_sensitive_functions
7188 .contains(&span_key(node.span))
7189 {
7190 self.unsafe_function_depth += 1;
7191 }
7192 }
7193
7194 fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
7195 self.exit_source_function(node.span);
7196 }
7197
7198 fn enter_arrow_function_expression(
7199 &mut self,
7200 node: &mut ArrowFunctionExpression<'a>,
7201 _context: &mut TraverseCtx<'a, ()>,
7202 ) {
7203 if self
7204 .source_sensitive_functions
7205 .contains(&span_key(node.span))
7206 {
7207 self.unsafe_function_depth += 1;
7208 }
7209 }
7210
7211 fn exit_arrow_function_expression(
7212 &mut self,
7213 node: &mut ArrowFunctionExpression<'a>,
7214 _context: &mut TraverseCtx<'a, ()>,
7215 ) {
7216 self.exit_source_function(node.span);
7217 }
7218
7219 fn enter_with_statement(
7220 &mut self,
7221 _node: &mut WithStatement<'a>,
7222 _context: &mut TraverseCtx<'a, ()>,
7223 ) {
7224 self.with_depth += 1;
7225 }
7226
7227 fn exit_with_statement(
7228 &mut self,
7229 _node: &mut WithStatement<'a>,
7230 _context: &mut TraverseCtx<'a, ()>,
7231 ) {
7232 self.with_depth -= 1;
7233 }
7234
7235 fn exit_logical_expression(
7236 &mut self,
7237 node: &mut LogicalExpression<'a>,
7238 _context: &mut TraverseCtx<'a, ()>,
7239 ) {
7240 if self.unsafe_function_depth > 0
7241 || self.with_depth > 0
7242 || self.decision_logical_nodes.contains(&span_key(node.span))
7243 {
7244 return;
7245 }
7246 let operator = match node.operator {
7247 LogicalOperator::And => "&&",
7248 LogicalOperator::Or => "||",
7249 LogicalOperator::Coalesce => "??",
7250 };
7251 let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
7252 let short_id = format!("{id}:short");
7253 let right_id = format!("{id}:right");
7254 let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
7255 self.analysis
7256 .logical_targets
7257 .insert(span_key(node.span), (short_id.clone(), right_id.clone()));
7258 self.analysis.branches.push(CandidateBranch {
7259 id,
7260 kind: "logical-value".to_string(),
7261 file: self.file.to_string(),
7262 line,
7263 column,
7264 source: source_slice(self.source, node.span).to_string(),
7265 alternatives: vec![
7266 CandidateBranchAlternative {
7267 id: short_id,
7268 label: "short-circuit / left selected".to_string(),
7269 },
7270 CandidateBranchAlternative {
7271 id: right_id,
7272 label: "right evaluated / selected".to_string(),
7273 },
7274 ],
7275 });
7276 }
7277}
7278
7279fn collect_logical_value_branches<'a>(
7280 allocator: &'a Allocator,
7281 program: &mut Program<'a>,
7282 source: &str,
7283 file: &str,
7284 decision_logical_nodes: &HashSet<SpanKey>,
7285 source_sensitive_functions: &HashSet<SpanKey>,
7286) -> LogicalBranchAnalysis {
7287 let mut collector = LogicalBranchCollector {
7288 source,
7289 file,
7290 decision_logical_nodes,
7291 source_sensitive_functions,
7292 unsafe_function_depth: 0,
7293 with_depth: 0,
7294 analysis: LogicalBranchAnalysis::default(),
7295 };
7296 traverse_mut(&mut collector, allocator, program, Default::default(), ());
7297 collector.analysis
7298}
7299
7300impl<'a> Visit<'a> for DecisionCollector<'_> {
7301 fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
7302 if self
7303 .source_sensitive_functions
7304 .contains(&span_key(function.span))
7305 {
7306 return;
7307 }
7308 walk::walk_function(self, function, flags);
7309 }
7310
7311 fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
7312 if self
7313 .source_sensitive_functions
7314 .contains(&span_key(function.span))
7315 {
7316 return;
7317 }
7318 walk::walk_arrow_function_expression(self, function);
7319 }
7320
7321 fn visit_with_statement(&mut self, statement: &WithStatement<'a>) {
7322 if self.with_statements.contains(&span_key(statement.span)) {
7323 return;
7324 }
7325 walk::walk_with_statement(self, statement);
7326 }
7327
7328 fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
7329 if decision_outcome_is_variable(&statement.test) {
7330 self.record_decision(&statement.test, "if");
7331 }
7332 self.visit_expression(&statement.test);
7333 self.visit_statement(&statement.consequent);
7334 if let Some(alternate) = &statement.alternate {
7335 self.visit_statement(alternate);
7336 }
7337 }
7338
7339 fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
7340 if decision_outcome_is_variable(&expression.test) {
7341 self.record_decision(&expression.test, "ternary");
7342 }
7343 self.visit_expression(&expression.test);
7344 self.visit_expression(&expression.consequent);
7345 self.visit_expression(&expression.alternate);
7346 }
7347
7348 fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
7349 if decision_outcome_is_variable(&statement.test) {
7350 self.record_decision(&statement.test, "while");
7351 }
7352 self.visit_expression(&statement.test);
7353 self.visit_statement(&statement.body);
7354 }
7355
7356 fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
7357 if decision_outcome_is_variable(&statement.test) {
7358 self.record_decision(&statement.test, "do-while");
7359 }
7360 self.visit_statement(&statement.body);
7361 self.visit_expression(&statement.test);
7362 }
7363
7364 fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
7365 if let Some(test) = &statement.test
7366 && decision_outcome_is_variable(test)
7367 {
7368 self.record_decision(test, "for");
7369 }
7370 if let Some(init) = &statement.init {
7371 self.visit_for_statement_init(init);
7372 }
7373 if let Some(test) = &statement.test {
7374 self.visit_expression(test);
7375 }
7376 if let Some(update) = &statement.update {
7377 self.visit_expression(update);
7378 }
7379 self.visit_statement(&statement.body);
7380 }
7381}
7382
7383fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
7384 match expression {
7385 Expression::ParenthesizedExpression(parenthesized) => {
7386 transparent_expression(&parenthesized.expression)
7387 }
7388 _ => expression,
7389 }
7390}
7391
7392fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
7393 match expression {
7394 Expression::ParenthesizedExpression(parenthesized) => {
7395 has_compound_boolean_decision(&parenthesized.expression)
7396 }
7397 Expression::LogicalExpression(logical) => {
7398 matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
7399 }
7400 Expression::UnaryExpression(unary) if unary.operator.is_not() => {
7401 has_compound_boolean_decision(&unary.argument)
7402 }
7403 _ => false,
7404 }
7405}
7406
7407fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
7408 match expression {
7409 Expression::ParenthesizedExpression(parenthesized) => {
7410 collect_conditions(&parenthesized.expression, conditions);
7411 }
7412 Expression::LogicalExpression(logical)
7413 if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
7414 {
7415 collect_conditions(&logical.left, conditions);
7416 collect_conditions(&logical.right, conditions);
7417 }
7418 Expression::UnaryExpression(unary)
7419 if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
7420 {
7421 collect_conditions(&unary.argument, conditions);
7422 }
7423 _ => conditions.push(expression.span()),
7424 }
7425}
7426
7427fn source_slice(source: &str, span: Span) -> &str {
7428 &source[span.start as usize..span.end as usize]
7429}
7430
7431pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
7432 let prefix = &source[..offset];
7433 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
7434 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
7435 let column = source[line_start..offset].encode_utf16().count() + 1;
7436 (line, column)
7437}
7438
7439fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
7440 let start = source[..span.start as usize].encode_utf16().count();
7441 let end = source[..span.end as usize].encode_utf16().count();
7442 let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
7443 let mut id = String::with_capacity(16);
7444 for byte in &digest[..8] {
7445 write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
7446 }
7447 id
7448}
7449
7450#[cfg(test)]
7451mod tests {
7452 use super::*;
7453
7454 const SOURCE: &str =
7455 "export function decide(a,b,c) {\n if ((a && b) || !c) return 1;\n return 0;\n}\n";
7456
7457 #[test]
7458 fn matches_the_frozen_if_decision_manifest_exactly() {
7459 let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
7460 assert!(!output.complete);
7461 assert_eq!(
7462 output.decisions,
7463 vec![CandidateDecision {
7464 id: "2f65989a5782c5bd".to_string(),
7465 file: "app/decide.ts".to_string(),
7466 line: 2,
7467 column: 7,
7468 source: "(a && b) || !c".to_string(),
7469 conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
7470 kind: "if".to_string(),
7471 }]
7472 );
7473 }
7474
7475 #[test]
7476 fn codegen_output_reparses_for_typescript_and_tsx() {
7477 for (file, source) in [
7478 (
7479 "component.tsx",
7480 "export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
7481 ),
7482 ("module.ts", SOURCE),
7483 ] {
7484 let output = analyze_candidate(source, file).unwrap();
7485 let map = output.map.as_ref().expect("candidate source map");
7486 assert_eq!(map["version"], 3);
7487 assert_eq!(map["sources"][0], file);
7488 assert_eq!(map["sourcesContent"][0], source);
7489 assert!(
7490 map["mappings"]
7491 .as_str()
7492 .is_some_and(|value| !value.is_empty())
7493 );
7494 let allocator = Allocator::default();
7495 let source_type = SourceType::from_path(file).unwrap();
7496 let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
7497 assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
7498 }
7499 }
7500
7501 #[test]
7502 fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
7503 for file in ["src/module.mjs", "src/script.cjs"] {
7504 let output = instrument_direct_candidate(
7505 "export const selected = value => value ? 1 : 0;",
7506 file,
7507 )
7508 .unwrap();
7509 assert!(
7510 output
7511 .code
7512 .contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
7513 );
7514 assert!(!output.code.contains("virtual:supercov-runtime"));
7515 assert!(!output.code.contains("globalThis.__supercovRuntime"));
7516 }
7517 }
7518
7519 #[test]
7520 fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
7521 let source = concat!(
7522 "import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
7523 "await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
7524 "ordinaryHelper();\n",
7525 );
7526 let output = instrument_node_assertion_phases_with_runtime_hooks(
7527 source,
7528 "tests/runner.mjs",
7529 &[],
7530 Some("../.supercov/launchSupervisor.mjs"),
7531 )
7532 .unwrap();
7533 assert_eq!(output.assertions, 0);
7534 assert_eq!(output.capability_imports, 1);
7535 assert!(output.code.contains("wrapImportedCapability"));
7536 assert!(output.code.contains("__supercovRawImageBuilder"));
7537 assert!(output.code.contains("const ImageBuilder"));
7538 assert!(!output.code.contains("__supercovRawordinaryHelper"));
7539 assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
7540 }
7541
7542 #[test]
7543 fn capability_imports_accept_a_computed_guest_mount_path() {
7544 let source = concat!(
7545 "import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
7546 "const guestRoot = resolve(tmpdir(), 'workspace');\n",
7547 "await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
7548 "ordinaryHelper();\n",
7549 );
7550 let output = instrument_node_assertion_phases_with_runtime_hooks(
7551 source,
7552 "tests/runner.mjs",
7553 &[],
7554 Some("../.supercov/launchSupervisor.mjs"),
7555 )
7556 .unwrap();
7557 assert_eq!(output.capability_imports, 1);
7558 assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
7559 assert!(!output.code.contains("__supercovRawordinaryHelper"));
7560 }
7561
7562 #[test]
7563 fn capability_import_selection_follows_a_mapping_variable() {
7564 let source = concat!(
7565 "import { launch, unrelated } from './sdk.mjs';\n",
7566 "run();\n",
7567 "const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
7568 "function run() { launch(options); unrelated(); }\n",
7569 );
7570 let output = instrument_node_assertion_phases_with_runtime_hooks(
7571 source,
7572 "tests/runner.mjs",
7573 &[],
7574 Some("../.supercov/launchSupervisor.mjs"),
7575 )
7576 .unwrap();
7577 assert_eq!(output.capability_imports, 1);
7578 assert!(output.code.contains("__supercovRawlaunch"));
7579 assert!(!output.code.contains("__supercovRawunrelated"));
7580 }
7581
7582 #[test]
7583 fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
7584 let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
7585 let output = instrument_node_assertion_phases_with_runtime_hooks(
7586 source,
7587 "src/main.mjs",
7588 &[],
7589 Some("../.supercov/launchSupervisor.mjs"),
7590 )
7591 .unwrap();
7592 assert_eq!(output.code, source);
7593 assert_eq!(output.capability_imports, 0);
7594 }
7595
7596 #[test]
7597 fn ordinary_mount_language_does_not_enable_capability_proxies() {
7598 let source = concat!(
7599 "import { render } from '@testing-library/react';\n",
7600 "it('mounts into a host element', () => render(<main />));\n",
7601 );
7602 let output = instrument_node_assertion_phases_with_runtime_hooks(
7603 source,
7604 "tests/component.test.tsx",
7605 &[],
7606 Some("../.supercov/launchSupervisor.mjs"),
7607 )
7608 .unwrap();
7609 assert_eq!(output.capability_imports, 0);
7610 assert!(!output.code.contains("wrapImportedCapability"));
7611 }
7612
7613 #[test]
7614 fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
7615 let source = concat!(
7616 "import { test, expect } from '@example/test-admin';\n",
7617 "test('visual snapshot', async ({ page }) => {\n",
7618 " expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
7619 "});\n",
7620 );
7621 let output = instrument_node_assertion_phases_with_runtime_hooks(
7622 source,
7623 "tests/visual.spec.ts",
7624 &[],
7625 Some("../.supercov/launchSupervisor.mjs"),
7626 )
7627 .unwrap();
7628 assert_eq!(output.capability_imports, 0);
7629 assert!(!output.code.contains("wrapImportedCapability"));
7630 }
7631
7632 #[test]
7633 fn capability_rewrite_excludes_runtime_and_type_only_imports() {
7634 let source = concat!(
7635 "import type { Machine } from './types.ts';\n",
7636 "import { test } from 'node:test';\n",
7637 "import { runtime } from '../.supercov/runtime.mjs';\n",
7638 "console.log({ machine: true, test, runtime });\n",
7639 );
7640 let output = instrument_node_assertion_phases_with_runtime_hooks(
7641 source,
7642 "tests/runner.ts",
7643 &[],
7644 Some("../.supercov/launchSupervisor.mjs"),
7645 )
7646 .unwrap();
7647 assert_eq!(output.code, source);
7648 assert_eq!(output.capability_imports, 0);
7649 }
7650
7651 #[test]
7652 fn native_assertion_arguments_execute_inside_the_assertion_phase() {
7653 let source = concat!(
7654 "import assert from 'node:assert/strict';\n",
7655 "assert.equal(value(), 1);\n",
7656 );
7657 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7658 assert_eq!(output.assertions, 1);
7659 assert!(output.code.contains("withNodeAssertionPhase"));
7660 assert!(output.code.contains("node:assert/strict.equal"));
7661 assert!(output.code.contains("tests/value.test.mjs:2:1"));
7662 assert!(output.code.contains("assert.equal(value(), 1)"));
7663 }
7664
7665 #[test]
7666 fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
7667 let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
7668 let output = instrument_node_assertion_phases_with_runtime_imports(
7669 source,
7670 "tests/value.test.mjs",
7671 &[],
7672 None,
7673 Some("../.supercov/runtime.mjs"),
7674 )
7675 .unwrap();
7676 assert_eq!(output.assertions, 1);
7677 assert!(output.code.contains("withNodeAssertionPhase"));
7678 assert!(output.code.contains("import \"../.supercov/runtime.mjs\";"));
7679 }
7680
7681 #[test]
7682 fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
7683 let source = concat!(
7684 "import assert from 'node:assert/strict';\n",
7685 "export async function check() { assert.equal(await value(), 1); }\n",
7686 );
7687 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7688 assert_eq!(output.assertions, 1);
7689 assert!(output.code.contains("bindNodeAssertionPhase"));
7690 assert!(output.code.contains("await value()"));
7691 let allocator = Allocator::default();
7692 assert!(
7693 Parser::new(&allocator, &output.code, SourceType::mjs())
7694 .parse()
7695 .errors
7696 .is_empty()
7697 );
7698 }
7699
7700 #[test]
7701 fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
7702 let source = concat!(
7703 "import { expect, test } from '@playwright/test';\n",
7704 "test('value', async () => { expect(await value()).toBe(1); });\n",
7705 );
7706 let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
7707 assert_eq!(output.assertions, 1);
7708 assert!(output.code.contains("bindNodeAssertionPhase"));
7709 assert!(output.code.contains("await value()"));
7710 let allocator = Allocator::default();
7711 assert!(
7712 Parser::new(&allocator, &output.code, SourceType::mjs())
7713 .parse()
7714 .errors
7715 .is_empty()
7716 );
7717 }
7718
7719 #[test]
7720 fn native_commonjs_assertion_bindings_are_attributed() {
7721 let source = concat!(
7722 "const assert = require('node:assert/strict');\n",
7723 "const { ok: verify } = require('assert');\n",
7724 "assert.equal(value(), 1);\n",
7725 "verify(other());\n",
7726 );
7727 let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
7728 assert_eq!(output.assertions, 2);
7729 assert!(output.code.contains("node:assert/strict.equal"));
7730 assert!(output.code.contains("node:assert.ok"));
7731 }
7732
7733 #[test]
7734 fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
7735 let source = concat!(
7736 "function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
7737 "function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
7738 );
7739 let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
7740 assert_eq!(output.assertions, 1);
7741 assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
7742 }
7743
7744 #[test]
7745 fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
7746 let source = concat!(
7747 "import assert from 'node:assert/strict';\n",
7748 "function unrelated(assert) { assert.equal(effect(), 1); }\n",
7749 "assert.equal(value(), 1);\n",
7750 );
7751 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7752 assert_eq!(output.assertions, 1);
7753 assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
7754 }
7755
7756 #[test]
7757 fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
7758 let source = concat!(
7759 "import test from 'node:test';\n",
7760 "import { expect as verify } from 'expect-library';\n",
7761 "test('value', () => verify(value()).not.toEqual(1));\n",
7762 );
7763 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7764 assert_eq!(output.assertions, 1);
7765 assert!(output.code.contains("expect.not.toEqual"));
7766 assert!(output.code.contains("verify(value()).not.toEqual(1)"));
7767 }
7768
7769 #[test]
7770 fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
7771 let source = concat!(
7772 "import { expect, test } from 'vitest';\n",
7773 "test('value', () => expect(value()).toBe(1));\n",
7774 );
7775 let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
7776 assert_eq!(output.assertions, 1);
7777 assert!(output.code.contains("expect.toBe"));
7778 }
7779
7780 #[test]
7781 fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
7782 for (call, operation) in [
7787 (
7788 "expect.element(button).toHaveAccessibleName('Copy')",
7789 "expect.element.toHaveAccessibleName",
7790 ),
7791 ("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
7792 ("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
7793 ] {
7794 let source = format!(
7795 "import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
7796 );
7797 let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
7798 assert_eq!(output.assertions, 1, "{call}");
7799 assert!(output.code.contains(operation), "{call} -> {operation}");
7800 }
7801 }
7802
7803 #[test]
7804 fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
7805 let source = concat!(
7810 "export function Button({ label, copied }) {\n",
7811 " return (\n",
7812 " <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
7813 " {format(copied)}\n",
7814 " </button>\n",
7815 " );\n",
7816 "}\n",
7817 );
7818 let output = instrument_candidate(source, "src/button.tsx").unwrap();
7819 let rendered = output
7820 .points
7821 .iter()
7822 .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
7823 .collect::<Vec<_>>();
7824 assert_eq!(rendered.len(), 2, "{:?}", output.points);
7827 assert!(
7828 rendered
7829 .iter()
7830 .all(|point| point.source == "format(copied)")
7831 );
7832 assert_eq!(rendered[0].line, 3);
7833 assert_eq!(rendered[1].line, 4);
7834 assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
7835 assert!(!output.code.contains("title={__supercovRenderedValueV2"));
7838 assert!(!output.code.contains("), format(copied)}"));
7841 assert!(output.code.contains(", format(copied))}"));
7842 }
7843
7844 #[test]
7845 fn instrumented_jsx_stays_parseable_through_every_container_shape() {
7846 let source = concat!(
7850 "export function Panel({ items, user, error, tone }) {\n",
7851 " return (\n",
7852 " <>\n",
7853 " <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
7854 " <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
7855 " {error ? <Alert text={message(error)} /> : null}\n",
7856 " {items.map((item) => (\n",
7857 " <Item key={item.id} label={format(item)} />\n",
7858 " ))}\n",
7859 " {count(items) > 0 && <Footer total={sum(items)} />}\n",
7860 " </div>\n",
7861 " </>\n",
7862 " );\n",
7863 "}\n",
7864 );
7865 let output = instrument_candidate(source, "src/panel.tsx").unwrap();
7866 instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
7868 assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
7870 let rendered = output
7871 .points
7872 .iter()
7873 .filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
7874 .map(|point| point.source.as_str())
7875 .collect::<Vec<_>>();
7876 assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
7877 assert!(
7878 rendered.contains(&"cx(\'a\', tone && \'b\')"),
7879 "{rendered:?}"
7880 );
7881 assert!(rendered.contains(&"format(item)"), "{rendered:?}");
7882 assert!(rendered.contains(&"message(error)"), "{rendered:?}");
7883 assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
7886 assert!(!rendered.contains(&"item.id"), "{rendered:?}");
7887 }
7888
7889 #[test]
7890 fn jest_global_expect_is_attributed_next_to_its_test_globals() {
7891 let source = "test('value', () => expect(value()).toBe(1));\n";
7895 let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
7896 assert_eq!(output.assertions, 1);
7897 assert!(output.code.contains("expect.toBe"));
7898 let plain = "const ok = expect(value()).toBe(1);\n";
7900 let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
7901 assert_eq!(output.assertions, 0);
7902 assert_eq!(output.code, plain);
7903 }
7904
7905 #[test]
7906 fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
7907 let source = concat!(
7908 "import { expect, test } from '@playwright/test';\n",
7909 "test('value', () => expect(value()).toBe(1));\n",
7910 );
7911 let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
7912 assert_eq!(output.assertions, 1);
7913 assert!(output.code.contains("expect.toBe"));
7914 }
7915
7916 #[test]
7917 fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
7918 let source = concat!(
7919 "import { browserTest, expect } from '@acme/browser-fixtures';\n",
7920 "browserTest('value', () => expect(value()).toBe(1));\n",
7921 );
7922 let output = instrument_node_assertion_phases_with_expect_modules(
7923 source,
7924 "tests/value.spec.mjs",
7925 &["@acme/browser-fixtures".into()],
7926 )
7927 .unwrap();
7928 assert_eq!(output.assertions, 1);
7929 assert!(output.code.contains("expect.toBe"));
7930 }
7931
7932 #[test]
7933 fn preserves_comment_payloads_byte_for_byte() {
7934 let comment = "/*---\ndescription: >\n nested indentation is data\ninfo: |\n first\n second\n---*/";
7935 let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
7936 let output = instrument_candidate(&source, "app/comments.js").unwrap();
7937 assert!(output.code.contains(comment), "{}", output.code);
7938 }
7939
7940 #[test]
7941 fn source_map_destinations_follow_restored_comments() {
7942 let source = "function run() {\n // first omitted comment\n // second omitted comment\n return 1;\n}\n";
7943 let output = analyze_candidate(source, "app/comment-map.js").unwrap();
7944 let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
7945 let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
7946 let return_token = map
7947 .get_tokens()
7948 .find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
7949 .expect("return token mapping");
7950 let offset = Utf16LineIndex::new(&output.code).byte_offset(
7951 return_token.get_dst_line() as usize,
7952 return_token.get_dst_col() as usize,
7953 );
7954 assert!(
7955 output.code[offset..].starts_with("return"),
7956 "{}",
7957 output.code
7958 );
7959 }
7960
7961 #[test]
7962 fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
7963 for source in [
7974 "// </script>\nexport const x = 1;\n",
7975 "// </script\nexport const x = 1;\n",
7976 "// </SCRIPT>\nexport const x = 1;\n",
7977 "// see the </script> tag here\nexport const x = 1;\n",
7978 "/* block </script> here */\nexport const x = 1;\n",
7979 "/** jsdoc </script> */\nexport const x = 1;\n",
7980 "// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
7982 ] {
7983 let output = instrument_candidate(source, "src/probe.ts")
7984 .unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
7985 let original = source.lines().next().expect("a comment");
7986 assert!(
7987 output.code.contains(original),
7988 "comment was not restored verbatim: {original:?} missing from {:?}",
7989 output.code
7990 );
7991 }
7992 let written = "// author wrote <\\/script>\nexport const x = 1;\n";
7995 let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
7996 assert!(
7997 output.code.contains("// author wrote <\\/script>"),
7998 "{:?}",
7999 output.code
8000 );
8001 assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
8004 assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
8005 assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
8006 }
8007
8008 #[test]
8009 fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
8010 let error = CandidateError::CommentPreservation {
8015 expected: 116,
8016 actual: 112,
8017 detail:
8018 "no source comment matches the generated comment \"// x\" (matched 97 before it)"
8019 .to_owned(),
8020 };
8021 let rendered = format!("{error:?}");
8022 assert!(rendered.contains("no source comment matches"), "{rendered}");
8023 assert!(rendered.contains("matched 97 before it"), "{rendered}");
8024 }
8025
8026 #[test]
8027 fn restored_comments_never_split_a_keyword_from_its_argument() {
8028 let source = concat!(
8036 "export const A = ({ open }) => {\n",
8037 " return (\n",
8038 " // leading comment before JSX\n",
8039 " <div aria-expanded={open}>a</div>\n",
8040 " )\n",
8041 "}\n",
8042 "export const B = ({ open }) => (\n",
8043 " // comment in an expression body\n",
8044 " <div aria-expanded={open}>b</div>\n",
8045 ")\n",
8046 "export function C(x) {\n",
8047 " return ( // inline line comment\n",
8048 " x + 1\n",
8049 " )\n",
8050 "}\n",
8051 "export function D(x) {\n",
8052 " throw (\n",
8053 " /* block\n comment */\n",
8054 " new Error(String(x))\n",
8055 " )\n",
8056 "}\n",
8057 "export function E(x) {\n",
8058 " return ( /* inline block */ x + 2 )\n",
8059 "}\n",
8060 );
8061 let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
8062 for keyword in ["return ", "throw "] {
8063 for (index, _) in output.code.match_indices(keyword) {
8064 let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
8065 let block_with_line_break = rest.starts_with("/*")
8066 && rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
8067 assert!(
8068 !rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
8069 "`{keyword}` is separated from its argument:\n{}",
8070 output.code
8071 );
8072 }
8073 }
8074 for comment in [
8075 "// leading comment before JSX",
8076 "// comment in an expression body",
8077 "// inline line comment",
8078 "/* block\n comment */",
8079 "/* inline block */",
8080 ] {
8081 assert!(
8082 output.code.contains(comment),
8083 "{comment} was lost:\n{}",
8084 output.code
8085 );
8086 }
8087 assert!(
8088 output
8089 .code
8090 .contains("return <div aria-expanded={open}>a</div>"),
8091 "{}",
8092 output.code
8093 );
8094 assert!(
8095 output
8096 .code
8097 .contains("return <div aria-expanded={open}>b</div>"),
8098 "{}",
8099 output.code
8100 );
8101 let allocator = Allocator::default();
8103 let reparsed = Parser::new(
8104 &allocator,
8105 &output.code,
8106 SourceType::from_path("app/components/List.tsx").unwrap(),
8107 )
8108 .parse();
8109 assert!(
8110 reparsed.errors.is_empty(),
8111 "{:?}\n{}",
8112 reparsed.errors,
8113 output.code
8114 );
8115 }
8116
8117 #[test]
8118 fn preserves_reference_order_across_every_control_decision_kind() {
8119 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}";
8120 let output = instrument_candidate(source, "app/control.js").unwrap();
8121 assert_eq!(
8122 output
8123 .decisions
8124 .iter()
8125 .map(|decision| decision.kind.as_str())
8126 .collect::<Vec<_>>(),
8127 vec!["ternary", "while", "do-while", "for", "if"]
8128 );
8129 }
8130
8131 #[test]
8132 fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
8133 let entered = |source: &str| {
8137 let output = analyze_candidate(source, "src/loops.ts").unwrap();
8138 output
8139 .branches
8140 .iter()
8141 .filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
8142 .count()
8143 };
8144 for source in [
8146 "export function f(g) { for (const x of [1, 2]) g(x); }\n",
8147 "export function f(g) { for (const x of [1]) g(x); }\n",
8148 "export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
8149 "export function f(g) { for (const x of [,]) g(x); }\n",
8151 "export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
8153 ] {
8154 assert_eq!(entered(source), 0, "should be folded: {source}");
8155 }
8156 for source in [
8158 "export function f(g, ys) { for (const x of ys) g(x); }\n",
8159 "export function f(g) { for (const x of []) g(x); }\n",
8160 "export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
8161 "export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
8162 "export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
8164 "export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
8165 "export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
8166 concat!(
8168 "const xs = [1, 2];\n",
8169 "export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
8170 ),
8171 "export function f(g) { for (const k in [1, 2]) g(k); }\n",
8173 ] {
8174 assert_eq!(entered(source), 1, "should be kept: {source}");
8175 }
8176 let output = analyze_candidate(
8178 "export function f(g) { for (const x of [1, 2]) g(x); }\n",
8179 "src/loops.ts",
8180 )
8181 .unwrap();
8182 assert!(
8183 output
8184 .points
8185 .iter()
8186 .any(|point| point.source.contains("g(x)")),
8187 "the body must still be an obligation"
8188 );
8189 }
8190
8191 #[test]
8192 fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
8193 let source = concat!(
8194 "export function run(value) {\n",
8195 " while (true) { break; }\n",
8196 " do { value += 1; } while (false);\n",
8197 " if (false) value += 2;\n",
8198 " if (value || true) value += 3;\n",
8199 " if (value && false) value += 4;\n",
8200 " if (value) value += 5;\n",
8201 " return value;\n",
8202 "}\n",
8203 );
8204 let output = analyze_candidate(source, "src/constants.js").unwrap();
8205 assert_eq!(output.decisions.len(), 1);
8206 assert_eq!(output.decisions[0].source, "value");
8207
8208 let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
8209 assert_eq!(instrumented.decisions.len(), 1);
8210 assert_eq!(instrumented.decisions[0].source, "value");
8211 }
8212
8213 #[test]
8214 fn leaves_compile_time_style_macro_arguments_as_source() {
8215 let source = concat!(
8218 "import * as stylex from '@stylexjs/stylex';\n",
8219 "const styles = stylex.create({\n",
8220 " container: { display: 'flex' },\n",
8221 " paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
8222 " tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
8223 "});\n",
8224 "export function render(spacing: number) {\n",
8225 " return stylex.props(styles.container, styles.paddingTop(spacing));\n",
8226 "}\n",
8227 );
8228 let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
8229 assert!(
8230 output.code.contains("=> ({ paddingTop: `${spacing}px` })")
8231 || output
8232 .code
8233 .contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
8234 "dynamic style arrow must stay an expression body:\n{}",
8235 output.code
8236 );
8237 assert!(
8238 !output
8239 .decisions
8240 .iter()
8241 .any(|decision| decision.source.contains("dark")),
8242 "no decision may be collected inside the macro argument"
8243 );
8244 let function_points = output
8245 .points
8246 .iter()
8247 .filter(|point| point.kind == "function")
8248 .map(|point| point.source.as_str())
8249 .collect::<Vec<_>>();
8250 assert!(
8251 function_points
8252 .iter()
8253 .all(|source| source.contains("render")),
8254 "only the real function may carry a function point: {function_points:?}"
8255 );
8256 assert!(
8257 output.limitations.is_empty(),
8258 "compiled-away code is not a limitation: {:?}",
8259 output.limitations
8260 );
8261 }
8262
8263 #[test]
8264 fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
8265 let source = concat!(
8266 "declare global {\n",
8267 " var Beacon: undefined | ((action: string) => void);\n",
8268 "}\n",
8269 "declare module 'virtual:feature' {\n",
8270 " export const enabled: boolean;\n",
8271 "}\n",
8272 "declare const buildOnly: string;\n",
8273 "export const live = 1;\n",
8274 );
8275 let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
8276 let statements = output
8277 .points
8278 .iter()
8279 .filter(|point| point.kind == "statement")
8280 .map(|point| point.source.as_str())
8281 .collect::<Vec<_>>();
8282 assert_eq!(statements, ["const live = 1;"]);
8283 assert!(output.code.contains("const live = 1"));
8284 }
8285
8286 #[test]
8287 fn exposes_the_complete_probe_v2_instrumenter_contract() {
8288 let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
8289 assert!(output.complete);
8290 assert!(output.limitations.is_empty());
8291 assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
8292 let runtime = output.runtime.expect("candidate runtime binding");
8293 assert!(output.code.contains(&runtime.mcdc_end_v2));
8294 assert!(output.code.contains("_supercovMcdcFrame"));
8295 assert!(output.code.contains("_supercovMcdcResult"));
8296 assert!(output.code.contains("+= _supercovMcdcValue"));
8297 assert_eq!(output.decisions.len(), 1);
8298 assert!(output.points.iter().any(|point| point.kind == "statement"));
8299 assert!(output.points.iter().any(|point| point.kind == "function"));
8300
8301 let allocator = Allocator::default();
8302 let reparsed = Parser::new(
8303 &allocator,
8304 &output.code,
8305 SourceType::from_path("app/decide.ts").unwrap(),
8306 )
8307 .parse();
8308 assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
8309 }
8310
8311 #[test]
8312 fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
8313 let source = concat!(
8314 "import type { Session } from './types.ts';\n",
8315 "import { type Row } from './rows.ts';\n",
8316 "import './register.ts';\n",
8317 "const value = 1;\n",
8318 );
8319 let output = instrument_candidate(source, "app/imports.ts").unwrap();
8320 let statement_lines = output
8321 .points
8322 .iter()
8323 .filter(|point| point.kind == "statement")
8324 .map(|point| point.line)
8325 .collect::<Vec<_>>();
8326 assert_eq!(statement_lines, vec![2, 3, 4]);
8327 }
8328
8329 #[test]
8330 fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
8331 let source = concat!(
8332 "import { Logger } from './types.js';\n",
8333 "import DefaultType from './default.js';\n",
8334 "import * as Types from './namespace.js';\n",
8335 "import { type Inline } from './inline.js';\n",
8336 "import { run, type Config } from './mixed.js';\n",
8337 "import './register.js';\n",
8338 "import {} from './empty.js';\n",
8339 "import { unused } from './unused.js';\n",
8340 "export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
8341 );
8342 let preserved =
8343 instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true, false)
8344 .unwrap();
8345 let erased =
8346 instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true, true)
8347 .unwrap();
8348 assert_eq!(
8349 erased
8350 .excluded_statements
8351 .iter()
8352 .map(|p| p.line)
8353 .collect::<Vec<_>>(),
8354 vec![1, 2, 3, 4]
8355 );
8356 let excluded = erased
8357 .excluded_statements
8358 .iter()
8359 .map(|p| &p.id)
8360 .collect::<HashSet<_>>();
8361 let expected = preserved
8362 .points
8363 .iter()
8364 .filter(|p| !excluded.contains(&p.id))
8365 .collect::<Vec<_>>();
8366 assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
8367 for line in [5, 6, 7, 8] {
8368 assert!(erased.points.iter().any(|p| p.line == line));
8369 }
8370 let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
8372 let mixed =
8373 instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true, true)
8374 .unwrap();
8375 assert!(mixed.excluded_statements.is_empty());
8376 }
8377
8378 #[test]
8379 fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
8380 let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
8381 let output = instrument_candidate(source, "app/collisions.js").unwrap();
8382 let runtime = output.runtime.expect("candidate runtime binding");
8383 assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
8384 assert!(!output.code.contains("let _supercovMcdcFrame1,"));
8385 }
8386
8387 #[test]
8388 fn instruments_wider_decisions_with_the_exact_v1_fallback() {
8389 let predicate = (0..33)
8390 .map(|index| format!("c{index}"))
8391 .collect::<Vec<_>>()
8392 .join(" && ");
8393 let source = format!("if ({predicate}) work();");
8394 let output = instrument_candidate(&source, "app/wide.js").unwrap();
8395 assert_eq!(output.decisions[0].conditions.len(), 33);
8396 let runtime = output.runtime.expect("candidate runtime binding");
8397 assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
8398 assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
8399 assert!(
8400 output
8401 .code
8402 .contains(&format!("{}(", runtime.mcdc_condition))
8403 );
8404 assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
8405 }
8406
8407 #[test]
8408 fn wraps_framework_request_exports_without_changing_the_public_api() {
8409 for (file, source, export_prefix) in [
8410 (
8411 "app/routes/example.ts",
8412 "export const loader = async ({ request }) => request.url;",
8413 "export const loader = ",
8414 ),
8415 (
8416 "app/routes/example.ts",
8417 "export async function action({ request }) { return request.method; }",
8418 "export const action = ",
8419 ),
8420 (
8421 "app/api/items/route.ts",
8422 "export function GET(request) { return Response.json({ url: request.url }); }",
8423 "export const GET = ",
8424 ),
8425 (
8426 "app/routes/example.ts",
8427 "export { generateAction as action } from './generateAction';",
8428 "export const action = ",
8429 ),
8430 (
8431 "app/entry.server.tsx",
8432 "export default async function handleRequest(request) { return request.url; }",
8433 "export default ",
8434 ),
8435 ] {
8436 let output = instrument_candidate(source, file).unwrap();
8437 let runtime = output.runtime.as_ref().expect("runtime binding");
8438 assert!(
8439 output
8440 .code
8441 .contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
8442 "{file}: {}",
8443 output.code
8444 );
8445 assert!(
8446 output.code.contains(&format!(
8447 "withRequestPhase as {}",
8448 runtime.with_request_phase
8449 )),
8450 "{file}: {}",
8451 output.code
8452 );
8453 let allocator = Allocator::default();
8454 let reparsed = Parser::new(
8455 &allocator,
8456 &output.code,
8457 SourceType::from_path(file).unwrap(),
8458 )
8459 .parse();
8460 assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
8461 }
8462 }
8463
8464 #[test]
8465 fn parse_failures_are_explicit_and_never_claim_completeness() {
8466 assert!(matches!(
8467 analyze_candidate("if (", "broken.js"),
8468 Err(CandidateError::Parse(errors)) if !errors.is_empty()
8469 ));
8470 assert!(matches!(
8471 analyze_candidate("let value = 1", "unknown.extension"),
8472 Err(CandidateError::UnknownSourceType(_))
8473 ));
8474 }
8475}