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