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