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