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