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