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