use std::{
collections::{HashMap, HashSet},
fmt::Write,
path::Path,
sync::Arc,
};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use oxc_allocator::{Allocator, CloneIn, TakeIn};
use oxc_ast::{
AstBuilder, AstKind, NONE,
ast::{
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentExpression,
AssignmentPattern, AssignmentTarget, BindingPattern, CallExpression, CatchClause,
ChainElement, ChainExpression, Class, Comment, ComputedMemberExpression,
ConditionalExpression, Declaration, DoWhileStatement, ExportDefaultDeclarationKind,
Expression, ExpressionStatement, ForInStatement, ForOfStatement, ForStatement,
ForStatementLeft, FormalParameter, FormalParameterKind, FormalParameters, Function,
FunctionBody, IfStatement, ImportDeclarationSpecifier, ImportOrExportKind, JSXExpression,
JSXExpressionContainer, LogicalExpression, NewExpression, ObjectPropertyKind,
PrivateFieldExpression, Program, PropertyKey, PropertyKind, Statement,
StaticMemberExpression, SwitchStatement, TSGlobalDeclaration, TSModuleDeclaration,
TryStatement, VariableDeclaration, VariableDeclarationKind, VariableDeclarator,
WhileStatement, WithStatement,
},
};
use oxc_ast_visit::{Visit, VisitMut, walk, walk_mut};
use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::{GetSpan, GetSpanMut, SourceType, Span};
use oxc_syntax::{
number::NumberBase,
operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator},
scope::ScopeFlags,
symbol::SymbolId,
};
use oxc_traverse::{Ancestor, Traverse, TraverseCtx, traverse_mut};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateDecision {
pub id: String,
pub file: String,
pub line: usize,
pub column: usize,
pub source: String,
pub conditions: Vec<String>,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidatePoint {
pub id: String,
pub kind: String,
pub file: String,
pub line: usize,
pub column: usize,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateBranchAlternative {
pub id: String,
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateBranch {
pub id: String,
pub kind: String,
pub file: String,
pub line: usize,
pub column: usize,
pub source: String,
pub alternatives: Vec<CandidateBranchAlternative>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateOutput {
pub engine: String,
pub complete: bool,
pub supported_surface: String,
pub code: String,
pub map: Option<serde_json::Value>,
pub decisions: Vec<CandidateDecision>,
pub points: Vec<CandidatePoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub excluded_statements: Vec<CandidatePoint>,
pub branches: Vec<CandidateBranch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<CandidateRuntime>,
pub coverage_limitations: Vec<CandidateLimitation>,
pub limitations: Vec<String>,
}
fn restore_comment_text(
program: &Program<'_>,
generated: &str,
map: oxc_sourcemap::SourceMap,
) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
if program.comments.is_empty() {
return Ok((generated.to_string(), map));
}
let allocator = Allocator::default();
let reparsed = Parser::new(&allocator, generated, program.source_type).parse();
if !reparsed.errors.is_empty() {
return Err(CandidateError::Parse(
reparsed
.errors
.into_iter()
.map(|error| error.to_string())
.collect(),
));
}
let mut matched = vec![false; program.comments.len()];
let mut original_index = 0;
let mut edits = Vec::<(usize, usize, String)>::new();
for emitted in &reparsed.program.comments {
let emitted_text = emitted.span.source_text(generated);
let same = |index: usize| {
let original = &program.comments[index];
original.kind == emitted.kind
&& equal_ignoring_whitespace(
original.span.source_text(program.source_text),
emitted_text,
)
};
let index = match (original_index..program.comments.len()).find(|index| same(*index)) {
Some(index) => {
original_index = index + 1;
index
}
None => match (0..original_index).find(|index| !matched[*index] && same(*index)) {
Some(index) => index,
None => {
return Err(CandidateError::CommentPreservation {
expected: program.comments.len(),
actual: reparsed.program.comments.len(),
detail: format!(
"no source comment matches the generated comment {emitted_text:?} (matched {} before it)",
edits.len()
),
});
}
},
};
let original = &program.comments[index];
matched[index] = true;
edits.push((
emitted.span.start as usize,
emitted.span.end as usize,
original.span.source_text(program.source_text).to_string(),
));
}
let source_lines = Utf16LineIndex::new(program.source_text);
let generated_lines = Utf16LineIndex::new(generated);
let mut mappings = map
.get_tokens()
.filter_map(|token| {
token.get_source_id()?;
Some((
source_lines
.byte_offset(token.get_src_line() as usize, token.get_src_col() as usize),
generated_lines
.byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize),
))
})
.collect::<Vec<_>>();
mappings.sort_unstable_by_key(|(source, destination)| (*source, *destination));
let statements = statement_spans(&reparsed.program);
for (index, original) in program.comments.iter().enumerate() {
if matched[index] {
continue;
}
let anchor = if original.attached_to > 0 {
original.attached_to as usize
} else {
original.span.end as usize
};
let mapping_index = mappings.partition_point(|(source, _)| *source < anchor);
let mapped = mappings
.get(mapping_index)
.map_or(generated.len(), |(_, destination)| *destination);
let (destination, text) = place_restored_comment(
generated,
&statements,
mapped,
original,
original.span.source_text(program.source_text),
);
edits.push((destination, destination, text));
}
edits.sort_by_key(|(start, end, _)| (*start, *end));
let restored_len = edits
.iter()
.fold(generated.len(), |length, (start, end, text)| {
length + text.len() - (end - start)
});
let mut restored = String::with_capacity(restored_len);
let mut cursor = 0;
for (start, end, replacement) in &edits {
if *start < cursor {
return Err(CandidateError::CommentPreservation {
expected: program.comments.len(),
actual: reparsed.program.comments.len(),
detail: format!(
"restored comments overlap: {replacement:?} would be placed at byte {start}, behind byte {cursor} already written"
),
});
}
restored.push_str(&generated[cursor..*start]);
restored.push_str(replacement);
cursor = *end;
}
restored.push_str(&generated[cursor..]);
let map = shift_source_map(map, generated, &restored, &edits);
Ok((restored, map))
}
fn statement_spans(program: &Program<'_>) -> Vec<Span> {
struct Spans(Vec<Span>);
impl<'a> Visit<'a> for Spans {
fn visit_statement(&mut self, statement: &Statement<'a>) {
self.0.push(statement.span());
walk::walk_statement(self, statement);
}
}
let mut spans = Spans(Vec::new());
spans.visit_program(program);
spans.0
}
fn place_restored_comment(
generated: &str,
statements: &[Span],
mapped: usize,
comment: &Comment,
text: &str,
) -> (usize, String) {
let line_prefix = |offset: usize| generated[..offset].rsplit('\n').next().unwrap_or("");
let at_line_start = line_prefix(mapped)
.chars()
.all(|character| character == ' ' || character == '\t');
if at_line_start {
let mut placed = String::new();
placed.push(if comment.preceded_by_newline() {
'\n'
} else {
' '
});
placed.push_str(text);
placed.push(if comment.is_line() || comment.followed_by_newline() {
'\n'
} else {
' '
});
return (mapped, placed);
}
if !comment.is_line() && !text.contains('\n') {
let leading = if generated[..mapped].ends_with([' ', '\t']) {
""
} else {
" "
};
return (mapped, format!("{leading}{text} "));
}
let Some(statement) = statements
.iter()
.filter(|span| span.start as usize <= mapped && mapped < span.end as usize)
.max_by_key(|span| span.start)
else {
return (mapped, format!("\n{text}\n"));
};
let start = statement.start as usize;
let indentation: String = line_prefix(start)
.chars()
.take_while(|character| *character == ' ' || *character == '\t')
.collect();
(start, format!("{text}\n{indentation}"))
}
fn equal_ignoring_whitespace(left: &str, right: &str) -> bool {
fn significant(text: &str) -> impl Iterator<Item = char> + '_ {
let mut rest = text.char_indices();
std::iter::from_fn(move || {
loop {
let (index, character) = rest.next()?;
if character == '\\'
&& text[index + 1..].starts_with('/')
&& text[..index].ends_with('<')
{
continue;
}
if !character.is_whitespace() {
return Some(character);
}
}
})
}
significant(left).eq(significant(right))
}
struct Utf16LineIndex<'s> {
source: &'s str,
starts: Vec<usize>,
checkpoints: Vec<Option<Vec<(usize, usize)>>>,
}
const CHECKPOINT: usize = 256;
impl<'s> Utf16LineIndex<'s> {
fn new(source: &'s str) -> Self {
let mut starts = Vec::with_capacity(source.lines().count() + 1);
starts.push(0);
starts.extend(
source
.char_indices()
.filter_map(|(offset, character)| (character == '\n').then_some(offset + 1)),
);
let checkpoints = (0..starts.len())
.map(|line| {
let end = starts.get(line + 1).copied().unwrap_or(source.len());
let text = &source[starts[line]..end];
if text.is_ascii() {
return None;
}
let mut marks = vec![(0, 0)];
let mut column = 0;
for (offset, character) in text.char_indices() {
if column >= marks.last().expect("a first mark").0 + CHECKPOINT {
marks.push((column, offset));
}
column += character.len_utf16();
}
Some(marks)
})
.collect();
Self {
source,
starts,
checkpoints,
}
}
fn line_end(&self, line: usize) -> usize {
self.starts
.get(line + 1)
.copied()
.unwrap_or(self.source.len())
}
fn byte_offset(&self, target_line: usize, target_utf16_col: usize) -> usize {
let Some(&start) = self.starts.get(target_line) else {
return self.source.len();
};
let end = self.line_end(target_line);
match &self.checkpoints[target_line] {
None => start + target_utf16_col.min(end - start),
Some(marks) => {
let at = marks.partition_point(|(column, _)| *column <= target_utf16_col) - 1;
let (column, offset) = marks[at];
start
+ offset
+ utf16_col_to_byte(
&self.source[start + offset..end],
target_utf16_col - column,
)
}
}
}
fn line_col(&self, byte_offset: usize) -> (u32, u32) {
let byte_offset = byte_offset.min(self.source.len());
let line = self.starts.partition_point(|start| *start <= byte_offset) - 1;
let start = self.starts[line];
let column = match &self.checkpoints[line] {
None => byte_offset - start,
Some(marks) => {
let at = marks.partition_point(|(_, offset)| *offset <= byte_offset - start) - 1;
let (column, offset) = marks[at];
column
+ self.source[start + offset..byte_offset]
.chars()
.map(char::len_utf16)
.sum::<usize>()
}
};
(line as u32, column as u32)
}
}
fn utf16_col_to_byte(line: &str, target_utf16_col: usize) -> usize {
let mut column = 0;
for (offset, character) in line.char_indices() {
if column >= target_utf16_col {
return offset;
}
column += character.len_utf16();
}
line.len()
}
fn shift_source_map(
map: oxc_sourcemap::SourceMap,
generated: &str,
restored: &str,
edits: &[(usize, usize, String)],
) -> oxc_sourcemap::SourceMap {
let generated_lines = Utf16LineIndex::new(generated);
let restored_lines = Utf16LineIndex::new(restored);
let mut edit_index = 0;
let mut shift = 0isize;
let tokens = map
.get_tokens()
.map(|token| {
let original_offset = generated_lines
.byte_offset(token.get_dst_line() as usize, token.get_dst_col() as usize);
while let Some((start, end, replacement)) = edits.get(edit_index) {
if *end > original_offset {
break;
}
shift += replacement.len() as isize - (*end - *start) as isize;
edit_index += 1;
}
let shifted_offset = edits
.get(edit_index)
.filter(|(start, end, _)| *start <= original_offset && original_offset < *end)
.map_or_else(
|| original_offset.saturating_add_signed(shift),
|(start, _, _)| start.saturating_add_signed(shift),
);
let (dst_line, dst_col) = restored_lines.line_col(shifted_offset);
oxc_sourcemap::Token::new(
dst_line,
dst_col,
token.get_src_line(),
token.get_src_col(),
token.get_source_id(),
token.get_name_id(),
)
})
.collect::<Vec<_>>();
oxc_sourcemap::SourceMap::new(
map.get_file().cloned(),
map.get_names().cloned().collect::<Vec<Arc<str>>>(),
map.get_source_root().map(str::to_string),
map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
map.get_source_contents()
.map(|content| content.cloned())
.collect::<Vec<Option<Arc<str>>>>(),
tokens.into_boxed_slice(),
None,
)
}
fn generated_statement_span(source: &str) -> Option<Span> {
let last_line = source.rfind('\n').map_or(0, |newline| newline + 1);
let length = u32::try_from(source.len()).ok()?;
(source[last_line..].is_ascii() && length < u32::MAX - 2)
.then(|| Span::new(length + 1, length + 2))
}
struct GeneratedStatementSpans<'a, 's> {
ast: AstBuilder<'a>,
source: &'s str,
span: Span,
depth: usize,
}
impl GeneratedStatementSpans<'_, '_> {
fn borrowed(&self, span: Span) -> bool {
!span.is_empty()
&& span != self.span
&& self.source.as_bytes().get(span.start as usize) != Some(&b'{')
}
}
impl<'a> VisitMut<'a> for GeneratedStatementSpans<'a, '_> {
fn visit_statement(&mut self, statement: &mut Statement<'a>) {
if self.depth > 0 && statement.span().is_empty() {
*statement.span_mut() = self.span;
}
self.depth += 1;
walk_mut::walk_statement(self, statement);
self.depth -= 1;
}
fn visit_block_statement(&mut self, block: &mut oxc_ast::ast::BlockStatement<'a>) {
if self.depth > 0 && block.span.is_empty() {
block.span = self.span;
} else if self.borrowed(block.span)
&& !matches!(
block.body.last(),
Some(
Statement::ReturnStatement(_)
| Statement::ThrowStatement(_)
| Statement::BreakStatement(_)
| Statement::ContinueStatement(_)
)
)
{
block.body.push(self.ast.statement_empty(self.span));
}
walk_mut::walk_block_statement(self, block);
}
fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
if self.borrowed(body.span) && (body.span.end as usize) < self.source.len() {
body.span.end += 1;
}
self.depth += 1;
walk_mut::walk_function_body(self, body);
self.depth -= 1;
}
}
fn separate_top_level_declarations<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>, span: Span) {
let starts_a_range = |statement: &Statement<'a>| match statement {
Statement::FunctionDeclaration(function) => !function.declare,
Statement::ClassDeclaration(class) => !class.declare,
_ => false,
};
let signature = |statement: &Statement<'a>| matches!(statement, Statement::FunctionDeclaration(function) if function.body.is_none());
let mut separated = ast.vec_with_capacity(program.body.len());
let mut after_signature = false;
for statement in program.body.take_in(ast.allocator) {
if starts_a_range(&statement) && !after_signature {
separated.push(ast.statement_empty(span));
}
after_signature = signature(&statement);
separated.push(statement);
}
program.body = separated;
}
fn mark_generated_statements<'a>(ast: AstBuilder<'a>, program: &mut Program<'a>) {
let Some(span) = generated_statement_span(program.source_text) else {
return;
};
separate_top_level_declarations(ast, program, span);
GeneratedStatementSpans {
ast,
source: program.source_text,
span,
depth: 0,
}
.visit_program(program);
}
fn unmap_generated_statements(
map: oxc_sourcemap::SourceMap,
source: &str,
) -> oxc_sourcemap::SourceMap {
let Some(span) = generated_statement_span(source) else {
return map;
};
let line = source.matches('\n').count() as u32;
let column = span.start - source.rfind('\n').map_or(0, |newline| newline as u32 + 1);
let is_generated = |token: &oxc_sourcemap::Token| {
token.get_src_line() == line && token.get_src_col() == column
};
if !map.get_tokens().any(|token| is_generated(&token)) {
return map;
}
let tokens = map
.get_tokens()
.map(|token| {
if is_generated(&token) {
oxc_sourcemap::Token::new(
token.get_dst_line(),
token.get_dst_col(),
0,
0,
None,
None,
)
} else {
token
}
})
.collect::<Vec<_>>();
oxc_sourcemap::SourceMap::new(
map.get_file().cloned(),
map.get_names().cloned().collect::<Vec<Arc<str>>>(),
map.get_source_root().map(str::to_string),
map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
map.get_source_contents()
.map(|content| content.cloned())
.collect::<Vec<Option<Arc<str>>>>(),
tokens.into_boxed_slice(),
None,
)
}
fn map_closing_lines(
map: oxc_sourcemap::SourceMap,
code: &str,
source: &str,
) -> oxc_sourcemap::SourceMap {
let source_lines = source.split('\n').collect::<Vec<_>>();
let tokens = map.get_tokens().collect::<Vec<_>>();
let mut patched = Vec::with_capacity(tokens.len());
let mut next = 0;
let mut previous: Option<oxc_sourcemap::Token> = None;
let mut added = false;
for (line, text) in code.split('\n').enumerate() {
let line = line as u32;
let first = next;
while next < tokens.len() && tokens[next].get_dst_line() == line {
next += 1;
}
if text == ";"
&& next == first + 1
&& tokens[first].get_source_id().is_none()
&& let Some(declaration) = tokens.get(next)
&& declaration.get_dst_col() == 0
&& declaration.get_source_id().is_some()
{
let separator = oxc_sourcemap::Token::new(
line,
0,
declaration.get_src_line(),
declaration.get_src_col(),
declaration.get_source_id(),
None,
);
patched.push(separator);
previous = Some(separator);
added = true;
continue;
}
let column = (text.len() - text.trim_start().len()) as u32;
if text.trim_start().starts_with(['}', ')', ']'])
&& (first == next || tokens[first].get_dst_col() > column)
&& let Some(previous) = &previous
{
patched.push(oxc_sourcemap::Token::new(
line,
column,
previous.get_src_line(),
previous.get_src_col(),
previous.get_source_id(),
None,
));
added = true;
}
patched.extend_from_slice(&tokens[first..next]);
if next > first {
let last = tokens[next - 1];
previous = Some(last);
if let (Some(_), Some(original)) = (
last.get_source_id(),
source_lines.get(last.get_src_line() as usize),
) && text.is_ascii()
&& original.is_ascii()
{
let generated_rest = text
.get(last.get_dst_col() as usize..)
.unwrap_or("")
.trim_end();
let original_rest = original
.get(last.get_src_col() as usize..)
.unwrap_or("")
.trim_end();
if generated_rest.len() > 1
&& generated_rest.ends_with(';')
&& original_rest.starts_with(generated_rest)
{
let offset = (generated_rest.len() - 1) as u32;
let end = oxc_sourcemap::Token::new(
line,
last.get_dst_col() + offset,
last.get_src_line(),
last.get_src_col() + offset,
last.get_source_id(),
None,
);
patched.push(end);
previous = Some(end);
added = true;
}
}
}
}
patched.extend_from_slice(&tokens[next..]);
if !added {
return map;
}
oxc_sourcemap::SourceMap::new(
map.get_file().cloned(),
map.get_names().cloned().collect::<Vec<Arc<str>>>(),
map.get_source_root().map(str::to_string),
map.get_sources().cloned().collect::<Vec<Arc<str>>>(),
map.get_source_contents()
.map(|content| content.cloned())
.collect::<Vec<Option<Arc<str>>>>(),
patched.into_boxed_slice(),
None,
)
}
fn generate_candidate(
program: &Program<'_>,
file: &str,
) -> Result<(String, Option<serde_json::Value>), CandidateError> {
let options = CodegenOptions {
source_map_path: Some(Path::new(file).to_path_buf()),
..CodegenOptions::default()
};
let generated = Codegen::new().with_options(options).build(program);
let map = unmap_generated_statements(
generated.map.expect("source maps are enabled"),
program.source_text,
);
let (code, map) = restore_comment_text(program, &generated.code, map)?;
let map = map_closing_lines(map, &code, program.source_text);
let map = Some({
serde_json::from_str(&map.to_json_string())
.expect("oxc must serialize its own generated source map")
});
Ok((code, map))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateRuntime {
pub coverage_hit: String,
pub mcdc_begin: String,
pub mcdc_condition: String,
pub mcdc_end: String,
pub register_probe_v2: String,
pub mcdc_end_v2: String,
pub coverage_hit_v2: String,
pub probe_file_v2: String,
pub parenthesized_assignment_value: String,
pub with_request_phase: String,
pub optional_select_v2: String,
pub select_short_v2: String,
pub select_right_v2: String,
pub select_named_right_v2: String,
pub select_assign_end_v2: String,
pub select_path_v2: String,
pub select_end2_v2: String,
pub select_end_v2: String,
pub select_next_v2: String,
pub rendered_value_v2: String,
pub optional_call_end_v2: String,
pub default_selected_v2: String,
pub default_entered_v2: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateLimitation {
pub id: String,
pub kind: String,
pub file: String,
pub line: usize,
pub column: usize,
pub source: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CandidateError {
UnknownSourceType(String),
Parse(Vec<String>),
CommentPreservation {
expected: usize,
actual: usize,
detail: String,
},
}
impl std::fmt::Display for CandidateError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownSourceType(file) => {
write!(formatter, "no JavaScript source type for {file}")
}
Self::Parse(messages) => write!(formatter, "{}", messages.join("; ")),
Self::CommentPreservation {
expected,
actual,
detail,
} => write!(
formatter,
"{expected} comments in the source, {actual} after instrumenting: {detail}"
),
}
}
}
fn parse_message(source: &str, message: &str, offset: Option<usize>) -> String {
match offset.filter(|offset| *offset <= source.len()) {
Some(offset) => {
let (line, column) = line_and_utf16_column(source, offset);
format!("{message} at line {line}, column {}", column + 1)
}
None => message.to_owned(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RuntimeBinding {
ModuleImport,
DirectGlobal,
}
const NODE_ASSERT_MODULES: &[&str] = &[
"assert",
"assert/strict",
"node:assert",
"node:assert/strict",
];
const NODE_ASSERT_METHODS: &[&str] = &[
"deepEqual",
"deepStrictEqual",
"doesNotMatch",
"doesNotReject",
"doesNotThrow",
"equal",
"fail",
"ifError",
"match",
"notDeepEqual",
"notDeepStrictEqual",
"notEqual",
"notStrictEqual",
"ok",
"partialDeepStrictEqual",
"rejects",
"strictEqual",
"throws",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeAssertionInstrumentation {
pub code: String,
pub assertions: usize,
pub capability_imports: usize,
}
const CAPABILITY_IMPORT_EXCLUSIONS: &[&str] =
&["@jest/globals", "@playwright/test", "playwright", "vitest"];
fn capability_source_candidate(source: &str) -> bool {
let direct_mapping = (source.contains("hostPath") && source.contains("guestPath"))
|| (source.contains("hostRoot") && source.contains("guestRoot"));
let mount_mapping = (source.contains("mounts") || source.contains("Mounts"))
&& source.contains("source")
&& source.contains("target");
direct_mapping || mount_mapping
}
fn excluded_capability_import(source: &str, wrapper: &str) -> bool {
source == wrapper
|| source.starts_with("node:")
|| source.starts_with("virtual:supercov-")
|| source.contains(".supercov/")
|| CAPABILITY_IMPORT_EXCLUSIONS.contains(&source)
}
fn capability_callee_root(expression: &Expression<'_>) -> Option<String> {
match expression {
Expression::Identifier(identifier) => Some(identifier.name.to_string()),
Expression::StaticMemberExpression(member) => capability_callee_root(&member.object),
Expression::ComputedMemberExpression(member) => capability_callee_root(&member.object),
Expression::CallExpression(call) => capability_callee_root(&call.callee),
Expression::NewExpression(construction) => capability_callee_root(&construction.callee),
Expression::ParenthesizedExpression(parenthesized) => {
capability_callee_root(&parenthesized.expression)
}
Expression::TSAsExpression(expression) => capability_callee_root(&expression.expression),
Expression::TSSatisfiesExpression(expression) => {
capability_callee_root(&expression.expression)
}
Expression::TSNonNullExpression(expression) => {
capability_callee_root(&expression.expression)
}
Expression::TSTypeAssertion(expression) => capability_callee_root(&expression.expression),
_ => None,
}
}
struct CapabilityCallCollector<'s> {
source: &'s str,
mapping_variables: HashSet<String>,
roots: HashSet<String>,
}
impl CapabilityCallCollector<'_> {
fn argument_has_mapping(&self, argument: &Argument<'_>) -> bool {
if let Argument::Identifier(identifier) = argument
&& self.mapping_variables.contains(identifier.name.as_str())
{
return true;
}
capability_source_candidate(source_slice(self.source, argument.span()))
}
}
impl<'a> Visit<'a> for CapabilityCallCollector<'_> {
fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
if let (BindingPattern::BindingIdentifier(identifier), Some(initializer)) =
(&declarator.id, &declarator.init)
&& capability_source_candidate(source_slice(self.source, initializer.span()))
{
self.mapping_variables.insert(identifier.name.to_string());
}
walk::walk_variable_declarator(self, declarator);
}
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
let mounting = matches!(&call.callee, Expression::StaticMemberExpression(member)
if member.property.name.to_ascii_lowercase().contains("mount"));
let mounted = |argument: &Argument<'_>| {
let text = source_slice(self.source, argument.span());
mounting && text.contains("source") && text.contains("target")
};
if call
.arguments
.iter()
.any(|argument| self.argument_has_mapping(argument) || mounted(argument))
&& let Some(root) = capability_callee_root(&call.callee)
{
self.roots.insert(root);
}
walk::walk_call_expression(self, call);
}
}
fn capability_import_roots(program: &Program<'_>, source: &str) -> HashSet<String> {
let mut collector = CapabilityCallCollector {
source,
mapping_variables: HashSet::new(),
roots: HashSet::new(),
};
collector.visit_program(program);
let mapping_variables = collector.mapping_variables.clone();
collector.roots.clear();
collector.mapping_variables = mapping_variables;
collector.visit_program(program);
collector.roots
}
struct TextEdit {
start: usize,
end: usize,
rank: i64,
text: String,
}
impl TextEdit {
fn insert(at: usize, rank: i64, text: String) -> Self {
Self {
start: at,
end: at,
rank,
text,
}
}
fn replace(start: usize, end: usize, text: String) -> Self {
Self {
start,
end,
rank: 0,
text,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TokenClass {
Word,
Mark,
Space,
}
impl TokenClass {
fn of(unit: char) -> Self {
if unit.is_whitespace() {
Self::Space
} else if unit.is_alphanumeric() || unit == '_' || unit == '$' {
Self::Word
} else {
Self::Mark
}
}
}
fn names_something(word: &str) -> bool {
const KEYWORDS: &[&str] = &[
"as",
"async",
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"from",
"function",
"if",
"implements",
"import",
"in",
"instanceof",
"interface",
"let",
"new",
"null",
"of",
"package",
"private",
"protected",
"public",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"type",
"typeof",
"var",
"void",
"while",
"with",
"yield",
];
!word.starts_with(|first: char| first.is_ascii_digit()) && !KEYWORDS.contains(&word)
}
fn apply_text_edits(
source: &str,
mut edits: Vec<TextEdit>,
file: &str,
) -> Result<(String, oxc_sourcemap::SourceMap), CandidateError> {
edits.sort_by_key(|edit| (edit.start, edit.end > edit.start, edit.rank));
let mut builder = oxc_sourcemap::SourceMapBuilder::default();
let source_id = builder.set_source_and_content(file, source);
let mut code = String::with_capacity(source.len() + edits.len() * 64);
let (mut line, mut column) = (0u32, 0u32);
let (mut source_line, mut source_column) = (0u32, 0u32);
let advance = |text: &str, line: &mut u32, column: &mut u32| {
for unit in text.chars() {
if unit == '\n' {
*line += 1;
*column = 0;
} else {
*column += unit.len_utf16() as u32;
}
}
};
let copy = |from: usize,
to: usize,
code: &mut String,
line: &mut u32,
column: &mut u32,
source_line: &mut u32,
source_column: &mut u32,
builder: &mut oxc_sourcemap::SourceMapBuilder| {
for (index, piece) in source[from..to].split('\n').enumerate() {
if index > 0 {
code.push('\n');
*line += 1;
*column = 0;
*source_line += 1;
*source_column = 0;
}
if !piece.is_empty() {
builder.add_token(
*line,
*column,
*source_line,
*source_column,
Some(source_id),
None,
);
let (mut offset, mut before) = (0u32, TokenClass::Space);
for (index, unit) in piece.char_indices() {
let class = TokenClass::of(unit);
if offset > 0
&& class != TokenClass::Space
&& (class != before || class == TokenClass::Mark)
{
let name = (class == TokenClass::Word)
.then(|| {
let rest = &piece[index..];
let end = rest
.find(|next| TokenClass::of(next) != TokenClass::Word)
.unwrap_or(rest.len());
&rest[..end]
})
.filter(|word| names_something(word))
.map(|word| builder.add_name(word));
builder.add_token(
*line,
*column + offset,
*source_line,
*source_column + offset,
Some(source_id),
name,
);
}
before = class;
offset += unit.len_utf16() as u32;
}
code.push_str(piece);
*column += offset;
*source_column += offset;
}
}
};
let mut at = 0;
for edit in edits {
if edit.start < at || edit.end > source.len() || edit.start > edit.end {
return Err(CandidateError::Parse(vec![format!(
"overlapping assertion edits in {file}"
)]));
}
copy(
at,
edit.start,
&mut code,
&mut line,
&mut column,
&mut source_line,
&mut source_column,
&mut builder,
);
let removed = &source[edit.start..edit.end];
if edit.text.matches('\n').count() != removed.matches('\n').count() {
return Err(CandidateError::Parse(vec![format!(
"an assertion edit in {file} would move a line"
)]));
}
code.push_str(&edit.text);
advance(&edit.text, &mut line, &mut column);
advance(removed, &mut source_line, &mut source_column);
at = edit.end;
}
copy(
at,
source.len(),
&mut code,
&mut line,
&mut column,
&mut source_line,
&mut source_column,
&mut builder,
);
Ok((code, builder.into_sourcemap()))
}
const ASSERTION_RUNTIME_TYPE: &str = "{ \
withNodeAssertionPhase<T>(operation: string, source: string, callback: () => T): T; \
openNodeAssertionPhase(operation: string, source: string): { close<T>(value: T): T; close(): void; fail(error: unknown): unknown }; \
bindNodeAssertionPhase<T>(operation: string, source: string, target: T, property: null): T; \
bindNodeAssertionPhase<T, K extends keyof T>(operation: string, source: string, target: T, property: K): T[K]; \
bindNodeAssertionPhase(operation: string, source: string, target: unknown, property: PropertyKey): any; \
}";
const ASSERTION_RUNTIME: &str = "globalThis.__SUPERCOV_DIRECT_RUNTIME__";
struct AssertionEdits<'s> {
source: &'s str,
sites: HashMap<SpanKey, (String, String, bool)>,
edits: Vec<TextEdit>,
in_place: Option<(String, String)>,
}
impl AssertionEdits<'_> {
fn opening(helper: &str, operation: &str, source: &str) -> String {
format!(
"{ASSERTION_RUNTIME}.{helper}({}, {}, ",
serde_json::to_string(operation).expect("a string serializes"),
serde_json::to_string(source).expect("a string serializes")
)
}
fn open(operation: &str, source: &str) -> String {
format!(
"{ASSERTION_RUNTIME}.openNodeAssertionPhase({}, {})",
serde_json::to_string(operation).expect("a string serializes"),
serde_json::to_string(source).expect("a string serializes")
)
}
fn wrap(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
let (start, end) = (call.span.start as usize, call.span.end as usize);
let width = (end - start) as i64;
let (opening, closing) = match &self.in_place {
Some((phase, error)) => (
format!("(({phase}) => {{ try {{ return {phase}.close("),
format!(
"); }} catch ({error}) {{ throw {phase}.fail({error}); }} }})({})",
Self::open(operation, source)
),
),
None => (
format!(
"{}() => (",
Self::opening("withNodeAssertionPhase", operation, source)
),
"))".into(),
),
};
self.edits.push(TextEdit::insert(start, -width, opening));
self.edits.push(TextEdit::insert(end, width, closing));
}
fn wrap_statement(
&mut self,
statement: &ExpressionStatement<'_>,
operation: &str,
source: &str,
) {
let Some((phase, error)) = &self.in_place else {
return;
};
let (start, end) = (statement.span.start as usize, statement.span.end as usize);
let width = (end - start) as i64;
let opening = format!(
"{{ const {phase} = {}; try {{ ",
Self::open(operation, source)
);
let closing = format!(
" }} catch ({error}) {{ throw {phase}.fail({error}); }} finally {{ {phase}.close(); }} }}"
);
self.edits.push(TextEdit::insert(start, -width, opening));
self.edits.push(TextEdit::insert(end, width, closing));
}
fn bind_statement(
&mut self,
statement: &ExpressionStatement<'_>,
call: &CallExpression<'_>,
callee: &str,
operation: &str,
source: &str,
) {
let Some((local, _)) = self.in_place.clone() else {
return;
};
let (start, end) = (statement.span.start as usize, statement.span.end as usize);
let width = (end - start) as i64;
self.edits.push(TextEdit::insert(
start,
-width,
format!("{{ const {local}: typeof {callee} = "),
));
self.bind(call, operation, source);
self.edits.push(TextEdit::insert(
call.callee.span().end as usize,
width,
format!("; {local}"),
));
self.edits.push(TextEdit::insert(end, width, " }".into()));
}
fn bind(&mut self, call: &CallExpression<'_>, operation: &str, source: &str) {
let callee = call.callee.span();
let width = (callee.end - callee.start) as i64;
let opening = Self::opening("bindNodeAssertionPhase", operation, source);
self.edits
.push(TextEdit::insert(callee.start as usize, -width, opening));
match &call.callee {
Expression::StaticMemberExpression(member) => {
let between = &self.source
[member.object.span().end as usize..member.property.span.start as usize];
let dot = between.rfind('.').unwrap_or(0);
let mut text = String::from(",");
text.push_str(&between[..dot]);
text.push(' ');
text.push_str(&between[dot + 1..]);
text.push_str(
&serde_json::to_string(member.property.name.as_str())
.expect("a string serializes"),
);
text.push(')');
self.edits.push(TextEdit::replace(
member.object.span().end as usize,
member.property.span.end as usize,
text,
));
}
Expression::ComputedMemberExpression(member) => {
let object_end = member.object.span().end as usize;
let open = object_end + self.source[object_end..].find('[').unwrap_or(0);
self.edits
.push(TextEdit::replace(open, open + 1, ",".into()));
let close = member.span.end as usize - 1;
self.edits
.push(TextEdit::replace(close, close + 1, ")".into()));
}
_ => {
self.edits.push(TextEdit::insert(
callee.end as usize,
width,
", null)".into(),
));
}
}
}
}
impl<'a> Visit<'a> for AssertionEdits<'_> {
fn visit_expression_statement(&mut self, statement: &ExpressionStatement<'a>) {
if self.in_place.is_some()
&& let Expression::CallExpression(call) = &statement.expression
&& let Some(&(_, _, bound)) = self.sites.get(&span_key(call.span))
{
let entity = entity_name(&call.callee);
if !bound || entity.is_some() {
let (operation, source, _) = self
.sites
.remove(&span_key(call.span))
.expect("the site was just found");
match entity {
Some(callee) if bound => {
self.bind_statement(statement, call, &callee, &operation, &source)
}
_ => self.wrap_statement(statement, &operation, &source),
}
}
}
walk::walk_expression_statement(self, statement);
}
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
if let Some((operation, source, bound)) = self.sites.remove(&span_key(call.span)) {
if bound {
self.bind(call, &operation, &source);
} else {
self.wrap(call, &operation, &source);
}
}
walk::walk_call_expression(self, call);
}
}
fn entity_name(callee: &Expression<'_>) -> Option<String> {
match callee {
Expression::Identifier(identifier) => Some(identifier.name.to_string()),
Expression::StaticMemberExpression(member) if !member.optional => Some(format!(
"{}.{}",
entity_name(&member.object)?,
member.property.name
)),
_ => None,
}
}
fn capability_import_edits(
program: &Program<'_>,
source: &str,
wrapper: &str,
) -> (Vec<TextEdit>, usize) {
if !capability_source_candidate(source) || !program.source_type.is_module() {
return (Vec::new(), 0);
}
let capability_roots = capability_import_roots(program, source);
if capability_roots.is_empty() {
return (Vec::new(), 0);
}
let mut names = CandidateNames::new(source);
let wrapper_local = names.allocate("__supercovImportedCapability");
let mut edits = Vec::new();
let mut wrapped = 0;
for statement in &program.body {
let Statement::ImportDeclaration(declaration) = statement else {
continue;
};
if declaration.import_kind == ImportOrExportKind::Type
|| excluded_capability_import(declaration.source.value.as_str(), wrapper)
{
continue;
}
let mut declarators = Vec::new();
for specifier in declaration.specifiers.iter().flatten() {
let (local, shorthand) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier)
if specifier.import_kind == ImportOrExportKind::Type =>
{
continue;
}
ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
&specifier.local,
specifier.imported.span() == specifier.local.span,
),
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
(&specifier.local, false)
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
(&specifier.local, false)
}
};
let original = local.name.to_string();
if !capability_roots.contains(&original) {
continue;
}
let raw = names.allocate(&format!("__supercovRaw{original}"));
if shorthand {
edits.push(TextEdit::insert(
local.span.end as usize,
0,
format!(" as {raw}"),
));
} else {
edits.push(TextEdit::replace(
local.span.start as usize,
local.span.end as usize,
raw.clone(),
));
}
declarators.push(format!("{original} = {wrapper_local}({raw})"));
wrapped += 1;
}
if !declarators.is_empty() {
edits.push(TextEdit::insert(
declaration.span.end as usize,
i64::MAX,
format!("; const {};", declarators.join(", ")),
));
}
}
if wrapped > 0 {
let first = program
.body
.first()
.map_or(0, |statement| statement.span().start as usize);
edits.push(TextEdit::insert(
first,
i64::MIN,
format!(
"import {{ wrapImportedCapability as {wrapper_local} }} from {}; ",
serde_json::to_string(wrapper).expect("a string serializes")
),
));
}
(edits, wrapped)
}
fn transform_capability_imports<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
wrapper: &str,
) -> usize {
if !capability_source_candidate(source) || !program.source_type.is_module() {
return 0;
}
let capability_roots = capability_import_roots(program, source);
if capability_roots.is_empty() {
return 0;
}
let ast = AstBuilder::new(allocator);
let mut names = CandidateNames::new(source);
let wrapper_local = names.allocate("__supercovImportedCapability");
let mut wrapped = 0;
let mut output = ast.vec();
for statement in program.body.take_in(allocator) {
let Statement::ImportDeclaration(mut declaration) = statement else {
output.push(statement);
continue;
};
let module_name = declaration.source.value.as_str();
if declaration.import_kind == ImportOrExportKind::Type
|| excluded_capability_import(module_name, wrapper)
{
output.push(Statement::ImportDeclaration(declaration));
continue;
}
let mut declarations = ast.vec();
for specifier in declaration.specifiers.iter_mut().flatten() {
let local = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier)
if specifier.import_kind == ImportOrExportKind::Type =>
{
continue;
}
ImportDeclarationSpecifier::ImportSpecifier(specifier) => &mut specifier.local,
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
&mut specifier.local
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
&mut specifier.local
}
};
let original = local.name.to_string();
if !capability_roots.contains(&original) {
continue;
}
let raw = names.allocate(&format!("__supercovRaw{original}"));
local.name = ast.ident(&raw);
let wrapped_value = ast.expression_call(
Span::default(),
ast.expression_identifier(Span::default(), ast.ident(&wrapper_local)),
NONE,
ast.vec1(Argument::from(
ast.expression_identifier(Span::default(), ast.ident(&raw)),
)),
false,
);
declarations.push(ast.variable_declarator(
Span::default(),
VariableDeclarationKind::Const,
ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&original)),
NONE,
Some(wrapped_value),
false,
));
wrapped += 1;
}
output.push(Statement::ImportDeclaration(declaration));
if !declarations.is_empty() {
output.push(Statement::VariableDeclaration(
ast.alloc_variable_declaration(
Span::default(),
VariableDeclarationKind::Const,
declarations,
false,
),
));
}
}
if wrapped > 0 {
output.insert(
0,
Statement::ImportDeclaration(ast.alloc_import_declaration(
Span::default(),
Some(ast.vec1(ast.import_declaration_specifier_import_specifier(
Span::default(),
ast.module_export_name_identifier_name(
Span::default(),
ast.ident("wrapImportedCapability"),
),
ast.binding_identifier(Span::default(), ast.ident(&wrapper_local)),
ImportOrExportKind::Value,
))),
ast.string_literal(Span::default(), ast.str(wrapper), None),
None,
NONE,
ImportOrExportKind::Value,
)),
);
}
program.body = output;
wrapped
}
fn canonical_assert_module(value: &str) -> Option<String> {
NODE_ASSERT_MODULES.contains(&value).then(|| {
if value.starts_with("node:") {
value.into()
} else {
format!("node:{value}")
}
})
}
fn required_assert_module(
expression: &Expression<'_>,
scoping: &oxc_semantic::Scoping,
) -> Option<String> {
if let Expression::CallExpression(call) = expression
&& matches!(&call.callee, Expression::Identifier(identifier) if identifier.name == "require")
&& matches!(&call.callee, Expression::Identifier(identifier) if referenced_symbol(identifier, scoping).is_none())
&& let [Argument::StringLiteral(module)] = call.arguments.as_slice()
{
return canonical_assert_module(module.value.as_str());
}
if let Expression::StaticMemberExpression(member) = expression
&& member.property.name == "strict"
&& let Some(module) = required_assert_module(&member.object, scoping)
{
return Some(if module == "node:assert" {
"node:assert/strict".into()
} else {
module
});
}
None
}
#[derive(Default)]
struct NodeAssertionBindings {
objects: HashMap<SymbolId, String>,
direct: HashMap<SymbolId, String>,
expects: HashSet<SymbolId>,
global_expect: bool,
}
fn bind_object(
bindings: &mut NodeAssertionBindings,
identifier: &oxc_ast::ast::BindingIdentifier<'_>,
module: String,
) {
if let Some(symbol) = identifier.symbol_id.get() {
bindings.objects.insert(symbol, module);
}
}
fn bind_direct(
bindings: &mut NodeAssertionBindings,
identifier: &oxc_ast::ast::BindingIdentifier<'_>,
operation: String,
) {
if let Some(symbol) = identifier.symbol_id.get() {
bindings.direct.insert(symbol, operation);
}
}
fn bind_expect(
bindings: &mut NodeAssertionBindings,
identifier: &oxc_ast::ast::BindingIdentifier<'_>,
) {
if let Some(symbol) = identifier.symbol_id.get() {
bindings.expects.insert(symbol);
}
}
struct NodeAssertionBindingCollector<'s> {
bindings: NodeAssertionBindings,
scoping: &'s oxc_semantic::Scoping,
allow_contextual_expect: bool,
expect_modules: HashSet<String>,
}
impl<'a> Visit<'a> for NodeAssertionBindingCollector<'_> {
fn visit_import_declaration(&mut self, declaration: &oxc_ast::ast::ImportDeclaration<'a>) {
let module_name = declaration.source.value.as_str();
let assert_module = canonical_assert_module(module_name);
let expect_module =
self.expect_modules.contains(module_name) || self.allow_contextual_expect;
for specifier in declaration.specifiers.iter().flatten() {
match specifier {
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
if let Some(module) = &assert_module {
bind_object(&mut self.bindings, &specifier.local, module.clone());
} else if expect_module && specifier.local.name == "expect" {
bind_expect(&mut self.bindings, &specifier.local);
}
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
if let Some(module) = &assert_module {
bind_object(&mut self.bindings, &specifier.local, module.clone());
}
}
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
let imported = specifier.imported.name().to_string();
if let Some(module) = &assert_module {
if imported == "strict" {
bind_object(
&mut self.bindings,
&specifier.local,
"node:assert/strict".into(),
);
} else if NODE_ASSERT_METHODS.contains(&imported.as_str()) {
bind_direct(
&mut self.bindings,
&specifier.local,
format!("{module}.{imported}"),
);
}
} else if expect_module && imported == "expect" {
bind_expect(&mut self.bindings, &specifier.local);
}
}
}
}
walk::walk_import_declaration(self, declaration);
}
fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
if let Some(Expression::CallExpression(call)) = &declarator.init
&& matches!(&call.callee, Expression::Identifier(i) if i.name == "require" && referenced_symbol(i, self.scoping).is_none())
&& let [Argument::StringLiteral(module)] = call.arguments.as_slice()
&& self.expect_modules.contains(module.value.as_str())
&& let BindingPattern::ObjectPattern(pattern) = &declarator.id
{
for property in &pattern.properties {
if property.key.static_name().as_deref() == Some("expect")
&& let BindingPattern::BindingIdentifier(local) = &property.value
{
bind_expect(&mut self.bindings, local);
}
}
}
if let Some(module) = declarator
.init
.as_ref()
.and_then(|expression| required_assert_module(expression, self.scoping))
{
match &declarator.id {
BindingPattern::BindingIdentifier(identifier) => {
bind_object(&mut self.bindings, identifier, module);
}
BindingPattern::ObjectPattern(pattern) => {
for property in &pattern.properties {
let Some(imported) = property.key.static_name() else {
continue;
};
let BindingPattern::BindingIdentifier(local) = &property.value else {
continue;
};
if imported == "strict" {
bind_object(&mut self.bindings, local, "node:assert/strict".into());
} else if NODE_ASSERT_METHODS.contains(&imported.as_ref()) {
bind_direct(&mut self.bindings, local, format!("{module}.{imported}"));
}
}
}
_ => {}
}
}
walk::walk_variable_declarator(self, declarator);
}
}
fn node_assertion_bindings(
program: &Program<'_>,
scoping: &oxc_semantic::Scoping,
extra_expect_modules: &[String],
) -> NodeAssertionBindings {
let allow_contextual_expect = program.source_text.contains("node:test")
&& program
.source_text
.split(|character: char| !character.is_alphanumeric() && character != '_')
.any(|word| word == "expect");
let mut collector = NodeAssertionBindingCollector {
bindings: NodeAssertionBindings::default(),
scoping,
allow_contextual_expect,
expect_modules: ["vitest", "@jest/globals", "expect", "@playwright/test"]
.into_iter()
.map(str::to_owned)
.chain(extra_expect_modules.iter().cloned())
.collect(),
};
collector.visit_program(program);
let unresolved = scoping.root_unresolved_references();
let uses = |name: &str| unresolved.contains_key(name);
collector.bindings.global_expect =
uses("expect") && (uses("test") || uses("it") || uses("describe"));
collector.bindings
}
struct NodeAssertionSiteCollector<'s> {
source: &'s str,
file: &'s str,
bindings: &'s NodeAssertionBindings,
scoping: &'s oxc_semantic::Scoping,
sites: HashMap<SpanKey, (String, String, bool)>,
}
pub fn project_source_type(path: &Path) -> Result<SourceType, String> {
let source_type = SourceType::from_path(path).map_err(|error| error.to_string())?;
Ok(if source_type.is_typescript() {
source_type
} else {
source_type.with_jsx(true)
})
}
fn referenced_symbol(
identifier: &oxc_ast::ast::IdentifierReference<'_>,
scoping: &oxc_semantic::Scoping,
) -> Option<SymbolId> {
identifier
.reference_id
.get()
.and_then(|reference| scoping.get_reference(reference).symbol_id())
}
fn assertion_member_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
match expression {
Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
Expression::ComputedMemberExpression(member) => match &member.expression {
Expression::StringLiteral(literal) => Some(literal.value.as_str()),
_ => None,
},
_ => None,
}
}
fn assertion_member_object<'a>(expression: &'a Expression<'a>) -> Option<&'a Expression<'a>> {
match expression {
Expression::StaticMemberExpression(member) => Some(&member.object),
Expression::ComputedMemberExpression(member) => Some(&member.object),
_ => None,
}
}
fn expect_operation(
callee: &Expression<'_>,
bindings: &NodeAssertionBindings,
scoping: &oxc_semantic::Scoping,
) -> Option<String> {
let mut current = callee;
let mut matchers = Vec::new();
while let Some(name) = assertion_member_name(current) {
matchers.push(name.to_owned());
current = assertion_member_object(current)?;
}
matchers.reverse();
let matcher = matchers.last()?;
if !matcher.starts_with("to") || !matcher.chars().nth(2).is_some_and(char::is_uppercase) {
return None;
}
let Expression::CallExpression(expect_call) = current else {
return None;
};
let mut root = &expect_call.callee;
let mut prefix = Vec::new();
while let Some(name) = assertion_member_name(root) {
prefix.push(name.to_owned());
root = assertion_member_object(root)?;
}
prefix.reverse();
let Expression::Identifier(identifier) = root else {
return None;
};
let recognized = match referenced_symbol(identifier, scoping) {
Some(symbol) => bindings.expects.contains(&symbol),
None => bindings.global_expect && identifier.name == "expect",
};
recognized.then(|| {
let mut parts = prefix;
parts.extend(matchers.iter().cloned());
format!("expect.{}", parts.join("."))
})
}
#[derive(Default)]
struct AwaitYieldScanner {
found: bool,
}
impl<'a> Visit<'a> for AwaitYieldScanner {
fn visit_await_expression(&mut self, _expression: &oxc_ast::ast::AwaitExpression<'a>) {
self.found = true;
}
fn visit_yield_expression(&mut self, _expression: &oxc_ast::ast::YieldExpression<'a>) {
self.found = true;
}
fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}
fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {}
}
impl<'a> Visit<'a> for NodeAssertionSiteCollector<'_> {
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
let operation = match &call.callee {
Expression::Identifier(identifier) => referenced_symbol(identifier, self.scoping)
.and_then(|symbol| {
self.bindings.direct.get(&symbol).cloned().or_else(|| {
self.bindings
.objects
.get(&symbol)
.map(|module| format!("{module}.ok"))
})
}),
callee => {
let member_operation = assertion_member_object(callee).and_then(|object| {
let Expression::Identifier(identifier) = object else {
return None;
};
let symbol = referenced_symbol(identifier, self.scoping)?;
let method = assertion_member_name(callee)?;
self.bindings
.objects
.get(&symbol)
.filter(|_| NODE_ASSERT_METHODS.contains(&method))
.map(|module| format!("{module}.{method}"))
});
member_operation.or_else(|| expect_operation(callee, self.bindings, self.scoping))
}
};
if let Some(operation) = operation {
let mut unsafe_argument = AwaitYieldScanner::default();
unsafe_argument.visit_expression(&call.callee);
for argument in &call.arguments {
unsafe_argument.visit_argument(argument);
}
let optional = call.optional
|| matches!(&call.callee,
Expression::StaticMemberExpression(m) if m.optional)
|| matches!(&call.callee, Expression::ComputedMemberExpression(m) if m.optional);
if optional {
walk::walk_call_expression(self, call);
return;
}
let (line, column) = line_and_utf16_column(self.source, call.span.start as usize);
self.sites.insert(
span_key(call.span),
(
operation,
format!("{}:{line}:{column}", self.file),
unsafe_argument.found,
),
);
}
walk::walk_call_expression(self, call);
}
}
pub fn instrument_node_assertion_phases(
source: &str,
file: &str,
) -> Result<NodeAssertionInstrumentation, CandidateError> {
instrument_node_assertion_phases_with_expect_modules(source, file, &[])
}
pub fn instrument_node_assertion_phases_with_expect_modules(
source: &str,
file: &str,
extra_expect_modules: &[String],
) -> Result<NodeAssertionInstrumentation, CandidateError> {
instrument_node_assertion_phases_with_runtime_hooks(source, file, extra_expect_modules, None)
}
pub fn instrument_node_assertion_phases_with_runtime_hooks(
source: &str,
file: &str,
extra_expect_modules: &[String],
capability_wrapper: Option<&str>,
) -> Result<NodeAssertionInstrumentation, CandidateError> {
instrument_node_assertion_phases_with_runtime_imports(
source,
file,
extra_expect_modules,
capability_wrapper,
None,
)
}
const RUNTIME_LOADER_BODY: &str = r#""const host = globalThis.process; if (!host || !host.getBuiltinModule) return; const frame = /(?:file:\\/\\/)?((?:\\/|[A-Za-z]:[\\\\/])[^\\s()]+?)(?=:\\d+:\\d+)/.exec(String(stack)); if (!frame) return; host.getBuiltinModule(\"node:module\").createRequire(frame[0].startsWith(\"file:\") ? frame[0] : frame[1])(specifier);""#;
pub(crate) const RUNTIME_LOADER_START: &str = "if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {";
pub(crate) fn runtime_loader(bootstrap: &str) -> String {
let specifier = serde_json::to_string(bootstrap).expect("a path string serializes");
format!(
"{RUNTIME_LOADER_START} new Function(\"stack\", \"specifier\", {RUNTIME_LOADER_BODY})(new Error().stack, {specifier}); }} catch {{}}"
)
}
pub fn instrument_node_assertion_phases_with_runtime_imports(
source: &str,
file: &str,
extra_expect_modules: &[String],
capability_wrapper: Option<&str>,
assertion_runtime: Option<&str>,
) -> Result<NodeAssertionInstrumentation, CandidateError> {
let _positions = PositionScope::enter();
let assertion_candidate = source.contains("assert") || source.contains("expect");
let capability_candidate = capability_wrapper.is_some() && capability_source_candidate(source);
if !assertion_candidate && !capability_candidate {
return Ok(NodeAssertionInstrumentation {
code: source.into(),
assertions: 0,
capability_imports: 0,
});
}
let source_type =
project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, source_type).parse();
if !parsed.errors.is_empty() {
return Err(CandidateError::Parse(
parsed
.errors
.into_iter()
.map(|error| error.to_string())
.collect(),
));
}
let mut assertions = 0;
let mut edits = Vec::new();
if assertion_candidate {
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
let bindings =
node_assertion_bindings(&parsed.program, semantic.scoping(), extra_expect_modules);
let mut collector = NodeAssertionSiteCollector {
source,
file,
bindings: &bindings,
scoping: semantic.scoping(),
sites: HashMap::new(),
};
collector.visit_program(&parsed.program);
assertions = collector.sites.len();
if assertions > 0 {
let in_place = source_type.is_typescript().then(|| {
let mut names = CandidateNames::new(source);
(
names.allocate("__supercovAssertionPhase"),
names.allocate("__supercovAssertionError"),
)
});
let mut located = AssertionEdits {
source,
sites: collector.sites,
edits: Vec::new(),
in_place,
};
located.visit_program(&parsed.program);
edits.extend(located.edits);
}
}
let capability_imports = capability_wrapper.map_or(0, |wrapper| {
let (found, wrapped) = capability_import_edits(&parsed.program, source, wrapper);
edits.extend(found);
wrapped
});
if assertions == 0 && capability_imports == 0 {
return Ok(NodeAssertionInstrumentation {
code: source.into(),
assertions: 0,
capability_imports: 0,
});
}
let module = parsed.program.source_type.is_module();
if let Some(bootstrap) = assertion_runtime.filter(|_| assertions > 0) {
let at = if source.starts_with("#!") {
source
.find('\n')
.map_or(source.len(), |newline| newline + 1)
} else {
0
};
edits.push(TextEdit::insert(
at,
i64::MIN,
format!("{} ", runtime_loader(bootstrap)),
));
}
let reference = Path::new(file)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(file);
let (mut code, map) = apply_text_edits(source, edits, reference)?;
if assertions > 0 && source_type.is_typescript() {
if module {
code.push_str(&format!(
"\ndeclare global {{ var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE}; }}\n"
));
} else {
code.push_str(&format!(
"\ndeclare var __SUPERCOV_DIRECT_RUNTIME__: {ASSERTION_RUNTIME_TYPE};\n"
));
}
}
let map = map.to_json_string();
code.push_str("\n//# sourceMappingURL=data:application/json;base64,");
BASE64_STANDARD.encode_string(map, &mut code);
code.push('\n');
Ok(NodeAssertionInstrumentation {
code,
assertions,
capability_imports,
})
}
type SpanKey = (u32, u32);
#[derive(Default)]
struct SafetyAnalysis {
source_sensitive_functions: HashSet<SpanKey>,
semantic_limitations: Vec<CandidateLimitation>,
dynamic_limitations: Vec<CandidateLimitation>,
}
struct SourceReads<'s> {
scoping: &'s oxc_semantic::Scoping,
symbols: HashSet<SymbolId>,
}
impl SourceReads<'_> {
fn read(&mut self, expression: &Expression<'_>) {
match expression {
Expression::Identifier(identifier) => {
self.symbols
.extend(referenced_symbol(identifier, self.scoping));
}
Expression::ParenthesizedExpression(inner) => self.read(&inner.expression),
Expression::TSAsExpression(inner) => self.read(&inner.expression),
Expression::TSSatisfiesExpression(inner) => self.read(&inner.expression),
Expression::TSTypeAssertion(inner) => self.read(&inner.expression),
Expression::TSNonNullExpression(inner) => self.read(&inner.expression),
Expression::ConditionalExpression(inner) => {
self.read(&inner.consequent);
self.read(&inner.alternate);
}
Expression::LogicalExpression(inner) => {
self.read(&inner.left);
self.read(&inner.right);
}
Expression::SequenceExpression(inner) => {
if let Some(last) = inner.expressions.last() {
self.read(last);
}
}
Expression::AssignmentExpression(inner) => self.read(&inner.right),
_ => {}
}
}
}
impl<'a> Visit<'a> for SourceReads<'_> {
fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
if member.property.name == "toString" {
self.read(&member.object);
}
walk::walk_static_member_expression(self, member);
}
fn visit_computed_member_expression(
&mut self,
member: &oxc_ast::ast::ComputedMemberExpression<'a>,
) {
self.read(&member.expression);
walk::walk_computed_member_expression(self, member);
}
fn visit_object_property(&mut self, property: &oxc_ast::ast::ObjectProperty<'a>) {
if property.computed
&& let Some(key) = property.key.as_expression()
{
self.read(key);
}
walk::walk_object_property(self, property);
}
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
if expression_is_identifier(&call.callee, "String")
&& let Some(argument) = call.arguments.first().and_then(Argument::as_expression)
{
self.read(argument);
}
walk::walk_call_expression(self, call);
}
fn visit_template_literal(&mut self, template: &oxc_ast::ast::TemplateLiteral<'a>) {
for expression in &template.expressions {
self.read(expression);
}
walk::walk_template_literal(self, template);
}
fn visit_binary_expression(&mut self, binary: &oxc_ast::ast::BinaryExpression<'a>) {
if matches!(
binary.operator,
BinaryOperator::Addition
| BinaryOperator::LessThan
| BinaryOperator::LessEqualThan
| BinaryOperator::GreaterThan
| BinaryOperator::GreaterEqualThan
) {
self.read(&binary.left);
self.read(&binary.right);
}
walk::walk_binary_expression(self, binary);
}
}
fn bound_symbols<State>(
own: Option<&oxc_ast::ast::BindingIdentifier<'_>>,
scoping: &oxc_semantic::Scoping,
context: &TraverseCtx<'_, State>,
) -> Vec<SymbolId> {
let mut symbols = own
.and_then(|identifier| identifier.symbol_id.get())
.into_iter()
.collect::<Vec<_>>();
match context.ancestors().next() {
Some(Ancestor::VariableDeclaratorInit(parent)) => symbols.extend(
parent
.id()
.get_binding_identifier()
.and_then(|identifier| identifier.symbol_id.get()),
),
Some(Ancestor::AssignmentExpressionRight(parent)) => {
if let AssignmentTarget::AssignmentTargetIdentifier(target) = parent.left() {
symbols.extend(referenced_symbol(target, scoping));
}
}
_ => {}
}
symbols
}
struct SafetyScanner<'s> {
source: &'s str,
file: &'s str,
scoping: &'s oxc_semantic::Scoping,
source_reads: HashSet<SymbolId>,
source_sensitive_functions: HashSet<SpanKey>,
function_limitations: Vec<CandidateLimitation>,
with_limitations: Vec<CandidateLimitation>,
dynamic_limitations: Vec<CandidateLimitation>,
unsafe_function_depth: usize,
with_depth: usize,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PointPass {
Statements,
Functions,
}
struct PointCollector<'s> {
source: &'s str,
file: &'s str,
pass: PointPass,
points: Vec<CandidatePoint>,
statement_targets: HashMap<SpanKey, Vec<String>>,
function_targets: HashMap<SpanKey, String>,
rendered_targets: HashMap<SpanKey, String>,
source_sensitive_functions: &'s HashSet<SpanKey>,
erased_imports: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
ambient_depth: usize,
}
#[derive(Default)]
struct PointAnalysis {
points: Vec<CandidatePoint>,
statement_targets: HashMap<SpanKey, Vec<String>>,
function_targets: HashMap<SpanKey, String>,
rendered_targets: HashMap<SpanKey, String>,
}
#[derive(Clone)]
struct PointTarget {
index: usize,
}
impl PointCollector<'_> {
fn point(&self, span: Span, kind: &str, label: Option<String>) -> CandidatePoint {
let (line, column) = line_and_utf16_column(self.source, span.start as usize);
CandidatePoint {
id: stable_id(self.source, self.file, kind, span, ""),
kind: kind.to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, span).to_string(),
label,
}
}
fn unsafe_context(&self) -> bool {
self.unsafe_function_depth > 0 || self.with_depth > 0 || self.ambient_depth > 0
}
fn exit_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
fn function_label<State>(
own_name: Option<&str>,
context: &TraverseCtx<'_, State>,
) -> Option<String> {
if let Some(name) = own_name {
return Some(name.to_string());
}
match context.ancestors().next()? {
Ancestor::ObjectPropertyValue(parent) => property_label(parent.key()),
Ancestor::MethodDefinitionValue(parent) => property_label(parent.key()),
Ancestor::VariableDeclaratorInit(parent) => parent
.id()
.get_binding_identifier()
.map(|identifier| identifier.name.to_string()),
_ => None,
}
}
fn property_label(key: &PropertyKey<'_>) -> Option<String> {
key.static_name()
.map(|name| name.into_owned())
.or_else(|| match key {
PropertyKey::Identifier(identifier) => Some(identifier.name.to_string()),
_ => None,
})
}
fn function_point_span<State>(span: Span, context: &TraverseCtx<'_, State>) -> Span {
match context.ancestors().next() {
Some(Ancestor::ObjectPropertyValue(parent))
if *parent.method() || *parent.kind() != PropertyKind::Init =>
{
*parent.span()
}
Some(Ancestor::MethodDefinitionValue(parent)) => *parent.span(),
_ => span,
}
}
fn type_only_import(statement: &Statement<'_>) -> bool {
let Statement::ImportDeclaration(declaration) = statement else {
return false;
};
declaration.import_kind == ImportOrExportKind::Type
}
fn erased_imports(program: &Program<'_>, elide_type_imports: bool) -> HashSet<SpanKey> {
let semantic = SemanticBuilder::new().build(program).semantic;
let scoping = semantic.scoping();
program
.body
.iter()
.filter_map(|statement| {
let Statement::ImportDeclaration(declaration) = statement else {
return None;
};
let erased = declaration.import_kind == ImportOrExportKind::Type
|| (elide_type_imports
&& program.source_type.is_typescript()
&& !program.source_type.is_jsx()
&& declaration.specifiers.as_ref().is_some_and(|specifiers| {
!specifiers.is_empty()
&& specifiers.iter().all(|specifier| {
let local = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
if s.import_kind == ImportOrExportKind::Type {
return true;
}
&s.local
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
&s.local
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
&s.local
}
};
local.symbol_id.get().is_some_and(|symbol| {
let mut refs =
scoping.get_resolved_references(symbol).peekable();
refs.peek().is_some()
&& refs.all(|r| r.is_type() && !r.is_value())
})
})
}));
erased.then_some(span_key(declaration.span))
})
.collect()
}
fn executable_statement(statement: &Statement<'_>) -> bool {
!matches!(
statement,
Statement::BlockStatement(_)
| Statement::EmptyStatement(_)
| Statement::FunctionDeclaration(_)
| Statement::ExportNamedDeclaration(_)
| Statement::ExportDefaultDeclaration(_)
) && !statement.is_typescript_syntax()
&& !type_only_import(statement)
}
impl<'a> Traverse<'a, ()> for PointCollector<'_> {
fn enter_statement(&mut self, node: &mut Statement<'a>, context: &mut TraverseCtx<'a, ()>) {
let mut ancestors = context.ancestors();
let parent = ancestors.next();
let expression_arrow_body = matches!(parent, Some(Ancestor::FunctionBodyStatements(_)))
&& matches!(ancestors.next(), Some(Ancestor::ArrowFunctionExpressionBody(arrow)) if *arrow.expression());
if self.pass != PointPass::Statements
|| self.unsafe_context()
|| !executable_statement(node)
|| self.erased_imports.contains(&span_key(node.span()))
|| matches!(parent, Some(Ancestor::LabeledStatementBody(_)))
|| expression_arrow_body
{
return;
}
let point = self.point(node.span(), "statement", None);
self.statement_targets
.entry(span_key(node.span()))
.or_default()
.push(point.id.clone());
self.points.push(point);
}
fn enter_expression(&mut self, node: &mut Expression<'a>, context: &mut TraverseCtx<'a, ()>) {
if self.pass != PointPass::Statements
|| self.unsafe_context()
|| !matches!(
context.ancestors().next(),
Some(Ancestor::JSXExpressionContainerExpression(_))
)
|| !rendered_expression_is_measurable(node)
{
return;
}
let span = node.span();
let point = self.point(span, "statement", Some("jsx-rendered-expression".into()));
self.rendered_targets
.insert(span_key(span), point.id.clone());
self.points.push(point);
}
fn enter_declaration(&mut self, node: &mut Declaration<'a>, context: &mut TraverseCtx<'a, ()>) {
if self.pass != PointPass::Statements
|| self.unsafe_context()
|| node.is_typescript_syntax()
|| matches!(node, Declaration::FunctionDeclaration(_))
|| !matches!(
context.ancestors().next(),
Some(Ancestor::ExportNamedDeclarationDeclaration(_))
| Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
)
{
return;
}
let point = self.point(node.span(), "statement", None);
self.statement_targets
.entry(span_key(node.span()))
.or_default()
.push(point.id.clone());
self.points.push(point);
}
fn enter_class(&mut self, node: &mut Class<'a>, context: &mut TraverseCtx<'a, ()>) {
if self.pass != PointPass::Statements
|| self.unsafe_context()
|| node.declare
|| !matches!(
context.ancestors().next(),
Some(Ancestor::ExportDefaultDeclarationDeclaration(_))
)
{
return;
}
let point = self.point(node.span, "statement", None);
self.statement_targets
.entry(span_key(node.span))
.or_default()
.push(point.id.clone());
self.points.push(point);
}
fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
let point_span = function_point_span(node.span, context);
let label = if point_span == node.span {
function_label(node.id.as_ref().map(|id| id.name.as_str()), context)
} else {
None
};
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
return;
}
if self.pass == PointPass::Functions && !self.unsafe_context() && node.body.is_some() {
let point = self.point(point_span, "function", label);
self.function_targets
.insert(span_key(node.span), point.id.clone());
self.points.push(point);
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
let point_span = function_point_span(node.span, context);
let label = if point_span == node.span {
function_label(None, context)
} else {
None
};
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
return;
}
if self.pass == PointPass::Functions && !self.unsafe_context() {
let point = self.point(point_span, "function", label);
self.function_targets
.insert(span_key(node.span), point.id.clone());
self.points.push(point);
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_ts_global_declaration(
&mut self,
_node: &mut TSGlobalDeclaration<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.ambient_depth += 1;
}
fn exit_ts_global_declaration(
&mut self,
_node: &mut TSGlobalDeclaration<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.ambient_depth -= 1;
}
fn enter_ts_module_declaration(
&mut self,
node: &mut TSModuleDeclaration<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if node.declare {
self.ambient_depth += 1;
}
}
fn exit_ts_module_declaration(
&mut self,
node: &mut TSModuleDeclaration<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if node.declare {
self.ambient_depth -= 1;
}
}
}
fn rendered_expression_is_measurable(expression: &Expression<'_>) -> bool {
match expression {
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
Expression::Identifier(_)
| Expression::ThisExpression(_)
| Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::PrivateFieldExpression(_)
| Expression::JSXElement(_)
| Expression::JSXFragment(_) => false,
Expression::ChainExpression(chain) => {
matches!(chain.expression, ChainElement::CallExpression(_))
}
other => !other.is_literal(),
}
}
fn collect_points<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
erased_imports: &HashSet<SpanKey>,
) -> PointAnalysis {
let mut analysis = PointAnalysis::default();
for pass in [PointPass::Statements, PointPass::Functions] {
let mut collector = PointCollector {
source,
file,
pass,
points: Vec::new(),
statement_targets: HashMap::new(),
function_targets: HashMap::new(),
rendered_targets: HashMap::new(),
source_sensitive_functions,
erased_imports,
unsafe_function_depth: 0,
with_depth: 0,
ambient_depth: 0,
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
analysis.points.extend(collector.points);
analysis
.statement_targets
.extend(collector.statement_targets);
analysis.function_targets.extend(collector.function_targets);
analysis.rendered_targets.extend(collector.rendered_targets);
}
analysis
}
impl<'s> SafetyScanner<'s> {
fn new(source: &'s str, file: &'s str, scoping: &'s oxc_semantic::Scoping) -> Self {
Self {
source,
file,
scoping,
source_reads: HashSet::new(),
source_sensitive_functions: HashSet::new(),
function_limitations: Vec::new(),
with_limitations: Vec::new(),
dynamic_limitations: Vec::new(),
unsafe_function_depth: 0,
with_depth: 0,
}
}
fn limitation(
&self,
span: Span,
kind: &str,
suffix: &str,
reason: &str,
) -> CandidateLimitation {
let (line, column) = line_and_utf16_column(self.source, span.start as usize);
CandidateLimitation {
id: stable_id(self.source, self.file, kind, span, suffix),
kind: kind.to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, span).to_string(),
reason: reason.to_string(),
}
}
fn enter_source_sensitive_function<State>(
&mut self,
span: Span,
bindings: Vec<SymbolId>,
context: &TraverseCtx<'_, State>,
) {
if compile_time_macro_argument(context) {
self.source_sensitive_functions.insert(span_key(span));
self.unsafe_function_depth += 1;
return;
}
let sensitive = observes_function_source(span, context)
|| bindings
.iter()
.any(|symbol| self.source_reads.contains(symbol));
if sensitive {
self.source_sensitive_functions.insert(span_key(span));
let limitation = self.limitation(
span,
"semantic-safety",
"function-source",
"function body is left uninstrumented because this expression observes or coerces Function source text",
);
self.function_limitations.push(limitation);
self.unsafe_function_depth += 1;
}
}
fn exit_source_sensitive_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
fn is_unsafe_context(&self) -> bool {
self.unsafe_function_depth > 0 || self.with_depth > 0
}
}
impl<'a> Traverse<'a, ()> for SafetyScanner<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, context: &mut TraverseCtx<'a, ()>) {
let bindings = bound_symbols(node.id.as_ref(), self.scoping, context);
self.enter_source_sensitive_function(node.span, bindings, context);
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_sensitive_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
let bindings = bound_symbols(None, self.scoping, context);
self.enter_source_sensitive_function(node.span, bindings, context);
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_sensitive_function(node.span);
}
fn enter_with_statement(
&mut self,
node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
let limitation = self.limitation(
node.span,
"semantic-safety",
"with-environment",
"with-statement body is left uninstrumented because its object environment can intercept probe identifiers",
);
self.with_limitations.push(limitation);
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_call_expression(
&mut self,
node: &mut CallExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "eval") {
return;
}
let limitation = self.limitation(
node.span,
"dynamic-code",
"eval",
"eval-generated source has no stable pre-run coverage denominator",
);
self.dynamic_limitations.push(limitation);
}
fn enter_new_expression(
&mut self,
node: &mut NewExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self.is_unsafe_context() || !expression_is_identifier(&node.callee, "Function") {
return;
}
let limitation = self.limitation(
node.span,
"dynamic-code",
"Function",
"Function-generated source has no stable pre-run coverage denominator",
);
self.dynamic_limitations.push(limitation);
}
}
fn analyze_safety<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
) -> SafetyAnalysis {
let scoping = SemanticBuilder::new()
.build(program)
.semantic
.into_scoping();
let mut reads = SourceReads {
scoping: &scoping,
symbols: HashSet::new(),
};
reads.visit_program(program);
let mut scanner = SafetyScanner::new(source, file, &scoping);
scanner.source_reads = reads.symbols;
traverse_mut(&mut scanner, allocator, program, Default::default(), ());
let mut semantic_limitations = scanner.function_limitations;
semantic_limitations.extend(scanner.with_limitations);
SafetyAnalysis {
source_sensitive_functions: scanner.source_sensitive_functions,
semantic_limitations,
dynamic_limitations: scanner.dynamic_limitations,
}
}
fn span_key(span: Span) -> SpanKey {
(span.start, span.end)
}
fn expression_is_identifier(expression: &Expression<'_>, name: &str) -> bool {
matches!(expression, Expression::Identifier(identifier) if identifier.name == name)
}
fn binding_identifier_name(pattern: &BindingPattern<'_>) -> Option<String> {
match pattern {
BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.to_string()),
_ => None,
}
}
fn expression_is_anonymous_definition(expression: &Expression<'_>) -> bool {
match expression {
Expression::ArrowFunctionExpression(_) => true,
Expression::FunctionExpression(function) => function.id.is_none(),
Expression::ClassExpression(class) => class.id.is_none(),
Expression::ParenthesizedExpression(expression) => {
expression_is_anonymous_definition(&expression.expression)
}
Expression::TSAsExpression(expression) => {
expression_is_anonymous_definition(&expression.expression)
}
Expression::TSSatisfiesExpression(expression) => {
expression_is_anonymous_definition(&expression.expression)
}
Expression::TSTypeAssertion(expression) => {
expression_is_anonymous_definition(&expression.expression)
}
Expression::TSNonNullExpression(expression) => {
expression_is_anonymous_definition(&expression.expression)
}
_ => false,
}
}
struct AssertedTargetTransformer<'a> {
ast: AstBuilder<'a>,
}
impl<'a> AssertedTargetTransformer<'a> {
fn target(
expression: Expression<'a>,
) -> Result<oxc_ast::ast::SimpleAssignmentTarget<'a>, Expression<'a>> {
Ok(match expression {
Expression::Identifier(identifier) => {
oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(identifier)
}
Expression::ComputedMemberExpression(member) => {
oxc_ast::ast::SimpleAssignmentTarget::ComputedMemberExpression(member)
}
Expression::StaticMemberExpression(member) => {
oxc_ast::ast::SimpleAssignmentTarget::StaticMemberExpression(member)
}
Expression::PrivateFieldExpression(member) => {
oxc_ast::ast::SimpleAssignmentTarget::PrivateFieldExpression(member)
}
Expression::TSAsExpression(asserted) => {
oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted)
}
Expression::TSSatisfiesExpression(asserted) => {
oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted)
}
Expression::TSNonNullExpression(asserted) => {
oxc_ast::ast::SimpleAssignmentTarget::TSNonNullExpression(asserted)
}
Expression::TSTypeAssertion(asserted) => {
oxc_ast::ast::SimpleAssignmentTarget::TSTypeAssertion(asserted)
}
Expression::ParenthesizedExpression(parenthesized) => {
return Self::target(parenthesized.unbox().expression);
}
other => return Err(other),
})
}
}
impl<'a> VisitMut<'a> for AssertedTargetTransformer<'a> {
fn visit_simple_assignment_target(
&mut self,
target: &mut oxc_ast::ast::SimpleAssignmentTarget<'a>,
) {
loop {
let inner = match target {
oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
&mut asserted.expression
}
oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
&mut asserted.expression
}
_ => break,
};
match Self::target(inner.take_in(self.ast.allocator)) {
Ok(bare) => *target = bare,
Err(kept) => {
match target {
oxc_ast::ast::SimpleAssignmentTarget::TSAsExpression(asserted) => {
asserted.expression = kept;
}
oxc_ast::ast::SimpleAssignmentTarget::TSSatisfiesExpression(asserted) => {
asserted.expression = kept;
}
_ => unreachable!("the target was matched above"),
}
break;
}
}
}
walk_mut::walk_simple_assignment_target(self, target);
}
}
pub const RUNTIME_TYPES_MODULE: &str = "virtual:supercov-helper-types";
fn runtime_helper_type<'a>(
allocator: &'a Allocator,
source_type: SourceType,
helper: &str,
) -> Option<oxc_allocator::Box<'a, oxc_ast::ast::TSTypeAnnotation<'a>>> {
let text = allocator.alloc_str(&format!(
"var helper: typeof import(\"{RUNTIME_TYPES_MODULE}\").{helper};"
));
let mut parsed = Parser::new(allocator, text, source_type).parse();
if !parsed.errors.is_empty() {
return None;
}
match parsed.program.body.first_mut()? {
Statement::VariableDeclaration(declaration) => {
declaration.declarations.first_mut()?.type_annotation.take()
}
_ => None,
}
}
struct AssignmentNameSafetyTransformer<'a> {
ast: AstBuilder<'a>,
parenthesized_assignment_value: String,
}
impl<'a> VisitMut<'a> for AssignmentNameSafetyTransformer<'a> {
fn visit_assignment_expression(&mut self, assignment: &mut AssignmentExpression<'a>) {
walk_mut::walk_assignment_expression(self, assignment);
let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left else {
return;
};
if !(assignment.operator == AssignmentOperator::Assign || assignment.operator.is_logical())
|| assignment.span.start == identifier.span.start
|| !expression_is_anonymous_definition(&assignment.right)
{
return;
}
let right = assignment.right.take_in(self.ast.allocator);
assignment.right = self.ast.expression_call(
Span::default(),
self.ast.expression_identifier(
Span::default(),
self.ast.ident(&self.parenthesized_assignment_value),
),
NONE,
self.ast.vec_from_array([
Argument::from(right),
Argument::from(self.ast.expression_string_literal(
Span::default(),
identifier.name,
None,
)),
]),
false,
);
}
}
const COMPILE_TIME_STYLE_MACROS: &[&str] = &[
"create",
"createTheme",
"defineConsts",
"defineVars",
"firstThatWorks",
"keyframes",
"positionTry",
"viewTransitionClass",
];
fn compile_time_macro_argument<State>(context: &TraverseCtx<'_, State>) -> bool {
context.ancestors().any(|ancestor| {
let Ancestor::CallExpressionArguments(parent) = ancestor else {
return false;
};
let Expression::StaticMemberExpression(member) = parent.callee() else {
return false;
};
let Expression::Identifier(object) = &member.object else {
return false;
};
object.name == "stylex"
&& COMPILE_TIME_STYLE_MACROS.contains(&member.property.name.as_str())
})
}
fn observes_function_source<State>(span: Span, context: &TraverseCtx<'_, State>) -> bool {
let mut child_end = span.end;
for ancestor in context.ancestors() {
match ancestor {
Ancestor::ParenthesizedExpressionExpression(parent) => child_end = parent.span().end,
Ancestor::TSAsExpressionExpression(parent) => child_end = parent.span().end,
Ancestor::TSSatisfiesExpressionExpression(parent) => child_end = parent.span().end,
Ancestor::TSTypeAssertionExpression(parent) => child_end = parent.span().end,
Ancestor::TSNonNullExpressionExpression(parent) => child_end = parent.span().end,
Ancestor::ConditionalExpressionConsequent(parent) => child_end = parent.span().end,
Ancestor::ConditionalExpressionAlternate(parent) => child_end = parent.span().end,
Ancestor::LogicalExpressionLeft(parent) => {
child_end = parent.span().end;
}
Ancestor::LogicalExpressionRight(parent) => {
child_end = parent.span().end;
}
Ancestor::SequenceExpressionExpressions(parent) => {
if child_end != parent.span().end {
return false;
}
child_end = parent.span().end;
}
Ancestor::AssignmentExpressionRight(parent) => child_end = parent.span().end,
Ancestor::ObjectPropertyKey(parent) => return *parent.computed(),
Ancestor::MethodDefinitionKey(parent) => return *parent.computed(),
Ancestor::PropertyDefinitionKey(parent) => return *parent.computed(),
Ancestor::AccessorPropertyKey(parent) => return *parent.computed(),
Ancestor::ComputedMemberExpressionExpression(_) => return true,
Ancestor::StaticMemberExpressionObject(parent) => {
return parent.property().name == "toString";
}
Ancestor::CallExpressionArguments(parent) => {
return expression_is_identifier(parent.callee(), "String");
}
Ancestor::TemplateLiteralExpressions(_) => return true,
Ancestor::BinaryExpressionLeft(parent) => {
return matches!(
parent.operator(),
BinaryOperator::Addition
| BinaryOperator::LessThan
| BinaryOperator::LessEqualThan
| BinaryOperator::GreaterThan
| BinaryOperator::GreaterEqualThan
);
}
Ancestor::BinaryExpressionRight(parent) => {
return matches!(
parent.operator(),
BinaryOperator::Addition
| BinaryOperator::LessThan
| BinaryOperator::LessEqualThan
| BinaryOperator::GreaterThan
| BinaryOperator::GreaterEqualThan
);
}
_ => return false,
}
}
false
}
pub fn analyze_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
let _positions = PositionScope::enter();
let elide_type_imports = false;
let source_type =
project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
let allocator = Allocator::default();
let mut parsed = Parser::new(&allocator, source, source_type).parse();
if !parsed.errors.is_empty() {
return Err(CandidateError::Parse(
parsed
.errors
.iter()
.map(|error| {
parse_message(
source,
&error.message,
error
.labels
.as_ref()
.and_then(|labels| labels.first())
.map(|label| label.offset()),
)
})
.collect(),
));
}
let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
let erased = erased_imports(&parsed.program, elide_type_imports);
let excluded_statements = parsed
.program
.body
.iter()
.filter(|s| erased.contains(&span_key(s.span())))
.map(|s| {
let span = s.span();
let (line, column) = line_and_utf16_column(source, span.start as usize);
CandidatePoint {
id: stable_id(source, file, "statement", span, ""),
kind: "statement".into(),
file: file.into(),
line,
column,
source: source_slice(source, span).into(),
label: Some("typescript-import-erasure".into()),
}
})
.collect();
let point_analysis = collect_points(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
&erased,
);
let mut collector = DecisionCollector {
source,
file,
decisions: Vec::new(),
decision_vector_counts: Vec::new(),
decision_logical_nodes: HashSet::new(),
source_sensitive_functions: &safety.source_sensitive_functions,
};
collector.visit_program(&parsed.program);
let optional_analysis = collect_optional_member_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let call_analysis = collect_optional_call_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let assignment_analysis = collect_logical_assignment_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let default_analysis = collect_default_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let extended_analysis = collect_extended_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let logical_analysis = collect_logical_value_branches(
&allocator,
&mut parsed.program,
source,
file,
&collector.decision_logical_nodes,
&safety.source_sensitive_functions,
);
let switch_analysis = collect_switch_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let mut branches = optional_analysis.branches;
branches.extend(call_analysis.branches);
branches.extend(assignment_analysis.branches);
branches.extend(default_analysis.branches);
branches.extend(extended_analysis.branches);
branches.extend(logical_analysis.branches);
branches.extend(switch_analysis.branches);
AssertedTargetTransformer {
ast: AstBuilder::new(&allocator),
}
.visit_program(&mut parsed.program);
let (generated, map) = generate_candidate(&parsed.program, file)?;
Ok(CandidateOutput {
engine: "rust-oxc".to_string(),
complete: false,
supported_surface: "control-decision-manifest-v1".to_string(),
code: generated,
map,
decisions: collector.decisions,
points: point_analysis.points,
excluded_statements,
branches,
runtime: None,
coverage_limitations: {
let mut limitations = safety.semantic_limitations;
limitations.extend(call_analysis.limitations);
limitations.extend(default_analysis.limitations);
limitations.extend(safety.dynamic_limitations);
limitations
},
limitations: vec![
"candidate emits metadata only; use the private differential transform for probes"
.to_string(),
"coverage points, value branches, and extended branch obligations are not included"
.to_string(),
],
})
}
fn json_expression<'a>(ast: AstBuilder<'a>, value: &serde_json::Value) -> Expression<'a> {
match value {
serde_json::Value::Null => ast.expression_null_literal(Span::default()),
serde_json::Value::Bool(value) => ast.expression_boolean_literal(Span::default(), *value),
serde_json::Value::Number(value) => ast.expression_numeric_literal(
Span::default(),
value
.as_f64()
.expect("coverage registration numbers must fit JavaScript"),
None,
NumberBase::Decimal,
),
serde_json::Value::String(value) => {
ast.expression_string_literal(Span::default(), ast.str(value), None)
}
serde_json::Value::Array(values) => ast.expression_array(
Span::default(),
ast.vec_from_iter(
values
.iter()
.map(|value| ArrayExpressionElement::from(json_expression(ast, value))),
),
),
serde_json::Value::Object(properties) => ast.expression_object(
Span::default(),
ast.vec_from_iter(properties.iter().map(|(key, value)| {
ast.object_property_kind_object_property(
Span::default(),
PropertyKind::Init,
ast.property_key_static_identifier(Span::default(), ast.ident(key)),
json_expression(ast, value),
false,
false,
false,
)
})),
),
}
}
pub fn instrument_candidate(source: &str, file: &str) -> Result<CandidateOutput, CandidateError> {
instrument_candidate_with_binding(source, file, RuntimeBinding::ModuleImport, None, false)
}
pub fn instrument_direct_candidate(
source: &str,
file: &str,
) -> Result<CandidateOutput, CandidateError> {
instrument_candidate_with_binding(source, file, RuntimeBinding::DirectGlobal, None, false)
}
pub fn instrument_with_import_policy(
source: &str,
file: &str,
capability_wrapper: &str,
elide_type_imports: bool,
) -> Result<CandidateOutput, CandidateError> {
instrument_candidate_with_binding(
source,
file,
RuntimeBinding::DirectGlobal,
Some(capability_wrapper),
elide_type_imports,
)
}
fn instrument_candidate_with_binding(
source: &str,
file: &str,
runtime_binding: RuntimeBinding,
capability_wrapper: Option<&str>,
elide_type_imports: bool,
) -> Result<CandidateOutput, CandidateError> {
let _positions = PositionScope::enter();
let source_type =
project_source_type(Path::new(file)).map_err(CandidateError::UnknownSourceType)?;
let allocator = Allocator::default();
let mut parsed = Parser::new(&allocator, source, source_type).parse();
if !parsed.errors.is_empty() {
return Err(CandidateError::Parse(
parsed
.errors
.iter()
.map(|error| {
parse_message(
source,
&error.message,
error
.labels
.as_ref()
.and_then(|labels| labels.first())
.map(|label| label.offset()),
)
})
.collect(),
));
}
let safety = analyze_safety(&allocator, &mut parsed.program, source, file);
let erased = erased_imports(&parsed.program, elide_type_imports);
let excluded_statements = parsed
.program
.body
.iter()
.filter(|s| erased.contains(&span_key(s.span())))
.map(|s| {
let span = s.span();
let (line, column) = line_and_utf16_column(source, span.start as usize);
CandidatePoint {
id: stable_id(source, file, "statement", span, ""),
kind: "statement".into(),
file: file.into(),
line,
column,
source: source_slice(source, span).into(),
label: Some("typescript-import-erasure".into()),
}
})
.collect();
let point_analysis = collect_points(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
&erased,
);
let mut collector = DecisionCollector {
source,
file,
decisions: Vec::new(),
decision_vector_counts: Vec::new(),
decision_logical_nodes: HashSet::new(),
source_sensitive_functions: &safety.source_sensitive_functions,
};
collector.visit_program(&parsed.program);
let optional_analysis = collect_optional_member_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let call_analysis = collect_optional_call_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let assignment_analysis = collect_logical_assignment_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let default_analysis = collect_default_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let extended_analysis = collect_extended_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let logical_analysis = collect_logical_value_branches(
&allocator,
&mut parsed.program,
source,
file,
&collector.decision_logical_nodes,
&safety.source_sensitive_functions,
);
let switch_analysis = collect_switch_branches(
&allocator,
&mut parsed.program,
source,
file,
&safety.source_sensitive_functions,
);
let mut branches = optional_analysis.branches;
branches.extend(call_analysis.branches.clone());
branches.extend(assignment_analysis.branches);
branches.extend(default_analysis.branches.clone());
branches.extend(extended_analysis.branches.clone());
branches.extend(logical_analysis.branches);
branches.extend(switch_analysis.branches.clone());
let (namespace, runtime_declaration_kind) = if runtime_binding == RuntimeBinding::DirectGlobal {
(
format!(
"_{}",
&stable_id("", file, "runtime", Span::default(), "")[..8]
),
VariableDeclarationKind::Var,
)
} else {
(String::new(), VariableDeclarationKind::Const)
};
let mut names = CandidateNames::within(source, &namespace);
let mcdc_begin = names.allocate("__supercovMcdcBegin");
let mcdc_condition = names.allocate("__supercovMcdcCondition");
let mcdc_end = names.allocate("__supercovMcdcEnd");
let coverage_hit = names.allocate("__supercovCoverageHit");
let register_probe_v2 = names.allocate("__supercovRegisterProbeV2");
let mcdc_end_v2 = names.allocate("__supercovMcdcEndV2");
let coverage_hit_v2 = names.allocate("__supercovCoverageHitV2");
let probe_file_v2 = names.allocate("__supercovProbeFileV2");
let _probe_clock_v2 = names.allocate("__supercovProbeClockV2");
let _probe_hits_v2 = names.allocate("__supercovProbeHitsV2");
let _probe_decisions_v2 = names.allocate("__supercovProbeDecisionsV2");
let _probe_complete_v2 = names.allocate("__supercovProbeCompleteV2");
let parenthesized_assignment_value = names.allocate("__supercovParenthesizedAssignmentValue");
let with_request_phase = names.allocate("__supercovWithRequestPhase");
let optional_select_v2 = names.allocate("__supercovOptionalSelectV2");
let select_short_v2 = names.allocate("__supercovSelectShortV2");
let select_right_v2 = names.allocate("__supercovSelectRightV2");
let select_named_right_v2 = names.allocate("__supercovSelectNamedRightV2");
let select_assign_end_v2 = names.allocate("__supercovSelectAssignEndV2");
let select_path_v2 = names.allocate("__supercovSelectPathV2");
let select_end2_v2 = names.allocate("__supercovSelectEnd2V2");
let select_end_v2 = names.allocate("__supercovSelectEndV2");
let select_next_v2 = names.allocate("__supercovSelectNextV2");
let rendered_value_v2 = names.allocate("__supercovRenderedValueV2");
let optional_call_end_v2 = names.allocate("__supercovOptionalCallEndV2");
let default_selected_v2 = names.allocate("__supercovDefaultSelectedV2");
let default_entered_v2 = names.allocate("__supercovDefaultEnteredV2");
let typescript = parsed.program.source_type.is_typescript();
let ast = AstBuilder::new(&allocator);
let mut assignment_name_safety = AssignmentNameSafetyTransformer {
ast,
parenthesized_assignment_value: parenthesized_assignment_value.clone(),
};
assignment_name_safety.visit_program(&mut parsed.program);
let point_indices = point_analysis
.points
.iter()
.enumerate()
.map(|(index, point)| (point.id.clone(), index))
.collect::<HashMap<_, _>>();
let mut default_sites = default_analysis
.parameter_targets
.iter()
.chain(&default_analysis.binding_targets)
.map(|(span, target)| (*span, target.default_id.clone(), target.provided_id.clone()))
.collect::<Vec<_>>();
default_sites.sort();
let mut default_point_ids = Vec::with_capacity(default_sites.len() * 2);
let default_slots = default_sites
.into_iter()
.enumerate()
.map(|(slot, (_, default_id, provided_id))| {
let default_point = point_analysis.points.len() + default_point_ids.len();
default_point_ids.push(default_id.clone());
default_point_ids.push(provided_id);
(
default_id,
DefaultSlot {
slot,
default_point,
provided_point: default_point + 1,
},
)
})
.collect::<HashMap<_, _>>();
let default_count = default_slots.len();
let mut extra = ExtraPoints {
base: point_analysis.points.len() + default_point_ids.len(),
ids: Vec::new(),
};
let selection_first = extra.next();
let selection_targets = extra.allocate(&logical_analysis.logical_targets, |(short, right)| {
vec![short.clone(), short.clone(), right.clone(), right.clone()]
});
let assignment_targets = extra.allocate(&assignment_analysis.targets, |(short, right)| {
vec![short.clone(), short.clone(), right.clone(), right.clone()]
});
let selection_count = selection_targets.len() + assignment_targets.len();
let optional_member_targets = extra
.allocate(&optional_analysis.targets, |(short, continued)| {
vec![short.clone(), continued.clone()]
});
let optional_call_sites = extra.allocate(&call_analysis.sites, |(short, continued)| {
vec![short.clone(), continued.clone()]
});
let loop_targets = extra.allocate(&extended_analysis.loop_targets, |target| {
vec![target.first_id.clone(), target.second_id.clone()]
});
let try_targets = extra.allocate(&extended_analysis.try_targets, |target| {
vec![target.first_id.clone(), target.second_id.clone()]
});
let statement_targets = point_analysis
.statement_targets
.into_iter()
.map(|(span, ids)| {
(
span,
ids.into_iter()
.map(|id| PointTarget {
index: *point_indices
.get(&id)
.expect("statement point must have a global index"),
})
.collect(),
)
})
.collect();
let function_targets = point_analysis
.function_targets
.into_iter()
.map(|(span, id)| {
(
span,
PointTarget {
index: *point_indices
.get(&id)
.expect("function point must have a global index"),
},
)
})
.collect();
let rendered_targets = point_analysis
.rendered_targets
.into_iter()
.map(|(span, id)| {
(
span,
PointTarget {
index: *point_indices
.get(&id)
.expect("rendered point must have a global index"),
},
)
})
.collect();
let mut statement_transformer = StatementProbeTransformer {
ast,
coverage_hit_v2: coverage_hit_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
targets: statement_targets,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
statement_transformer.visit_program(&mut parsed.program);
let mut function_transformer = FunctionProbeTransformer {
ast,
coverage_hit_v2: coverage_hit_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
targets: function_targets,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
function_transformer.visit_program(&mut parsed.program);
let mut rendered_transformer = RenderedExpressionTransformer {
ast,
rendered_value_v2: rendered_value_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
targets: rendered_targets,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
rendered_transformer.visit_program(&mut parsed.program);
let mut call_transformer = OptionalCallTransformer::new(
ast,
source,
optional_call_end_v2.clone(),
probe_file_v2.clone(),
typescript,
optional_call_sites,
call_analysis.roots,
safety.source_sensitive_functions.clone(),
);
let call_frames = optional_analysis
.after_call
.iter()
.filter_map(|(member, call)| {
call_transformer
.sites
.get(call)
.map(|site| (*member, site.frame.clone()))
})
.collect();
let mut optional_transformer = OptionalMemberTransformer {
ast,
optional_select_v2: optional_select_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
targets: optional_member_targets,
links: optional_analysis.links,
call_frames,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
optional_transformer.visit_program(&mut parsed.program);
call_transformer.visit_program(&mut parsed.program);
let mut default_transformer = DefaultTransformer {
ast,
default_selected_v2: default_selected_v2.clone(),
default_entered_v2: default_entered_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
slots: default_slots,
parameter_targets: default_analysis.parameter_targets,
binding_targets: default_analysis.binding_targets,
function_entries: Vec::new(),
declaration_entries: HashMap::new(),
active_declaration: Vec::new(),
parameter_pattern_depth: 0,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
typescript,
};
default_transformer.visit_program(&mut parsed.program);
let mut extended_transformer = ExtendedTransformer {
ast,
coverage_hit_v2: coverage_hit_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
typescript,
try_targets,
loop_targets,
names: CandidateNames::within(source, &namespace),
scope_declarations: Vec::new(),
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
extended_transformer.visit_program(&mut parsed.program);
let mut transformer = ControlProbeV2Transformer {
ast,
decisions: &collector.decisions,
mcdc_begin: mcdc_begin.clone(),
mcdc_condition: mcdc_condition.clone(),
mcdc_end: mcdc_end.clone(),
mcdc_end_v2: mcdc_end_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
names,
scope_declarations: Vec::new(),
decision_index: 0,
parameter_depth: 0,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
after_statement: None,
label_body: false,
inline_names: None,
};
transformer.visit_program(&mut parsed.program);
let mut logical_transformer = LogicalValueTransformer {
ast,
select_short_v2: select_short_v2.clone(),
select_right_v2: select_right_v2.clone(),
select_named_right_v2: select_named_right_v2.clone(),
select_assign_end_v2: select_assign_end_v2.clone(),
select_path_v2: select_path_v2.clone(),
select_end2_v2: select_end2_v2.clone(),
select_end_v2: select_end_v2.clone(),
select_next_v2: select_next_v2.clone(),
coverage_hit_v2: coverage_hit_v2.clone(),
probe_file_v2: probe_file_v2.clone(),
typescript,
names: CandidateNames::within(source, &namespace),
scope_declarations: Vec::new(),
frameless: 0,
trees: Vec::new(),
aliasing: false,
after_declaration: Vec::new(),
logical_targets: selection_targets,
assignment_targets,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
logical_transformer.visit_program(&mut parsed.program);
let mut switch_transformer = SwitchTransformer {
ast,
coverage_hit: coverage_hit.clone(),
targets: switch_analysis.targets,
names: CandidateNames::within(source, &namespace),
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
switch_transformer.visit_program(&mut parsed.program);
let mut route_transformer = RouteRequestPhaseTransformer {
ast,
file,
with_request_phase: with_request_phase.clone(),
used: false,
names: CandidateNames::within(source, &namespace),
};
route_transformer.transform_program(&mut parsed.program);
let mut request_transformer = RequestPhaseTransformer {
ast,
with_request_phase: with_request_phase.clone(),
used: route_transformer.used,
source_sensitive_functions: safety.source_sensitive_functions.clone(),
};
request_transformer.visit_program(&mut parsed.program);
let uses_request_phase = request_transformer.used;
let registration = serde_json::json!({
"decisions": &collector.decisions,
"pointIds": point_analysis.points.iter().map(|point| &point.id).chain(&default_point_ids).chain(&extra.ids).collect::<Vec<_>>(),
"decisionVectorCounts": &collector.decision_vector_counts,
"defaultCount": default_count,
"selectionPoints": [selection_first, selection_count],
"selectionTrees": &logical_transformer.trees,
});
let registration_call = ast.expression_call(
Span::default(),
ast.expression_identifier(Span::default(), ast.ident(®ister_probe_v2)),
NONE,
ast.vec1(Argument::from(json_expression(ast, ®istration))),
false,
);
parsed.program.body.insert(
0,
Statement::VariableDeclaration(ast.alloc_variable_declaration(
Span::default(),
runtime_declaration_kind,
ast.vec1(ast.variable_declarator(
Span::default(),
runtime_declaration_kind,
ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&probe_file_v2)),
NONE,
Some(registration_call),
false,
)),
false,
)),
);
let mut runtime_imports = vec![
("mcdcBegin", &mcdc_begin),
("mcdcCondition", &mcdc_condition),
("mcdcEnd", &mcdc_end),
("coverageHit", &coverage_hit),
("registerProbeV2", ®ister_probe_v2),
("mcdcEndV2", &mcdc_end_v2),
("coverageHitV2", &coverage_hit_v2),
(
"parenthesizedAssignmentValue",
&parenthesized_assignment_value,
),
("optionalSelectV2", &optional_select_v2),
("selectShortV2", &select_short_v2),
("selectRightV2", &select_right_v2),
("selectNamedRightV2", &select_named_right_v2),
("selectAssignEndV2", &select_assign_end_v2),
("selectPathV2", &select_path_v2),
("selectEnd2V2", &select_end2_v2),
("selectEndV2", &select_end_v2),
("selectNextV2", &select_next_v2),
("renderedValueV2", &rendered_value_v2),
("optionalCallEndV2", &optional_call_end_v2),
("defaultSelectedV2", &default_selected_v2),
("defaultEnteredV2", &default_entered_v2),
];
if uses_request_phase {
runtime_imports.insert(10, ("withRequestPhase", &with_request_phase));
}
if runtime_binding == RuntimeBinding::DirectGlobal || parsed.program.source_type.is_script() {
let typed = typescript && runtime_binding == RuntimeBinding::DirectGlobal;
let declarators =
ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
let annotation = typed
.then(|| runtime_helper_type(&allocator, parsed.program.source_type, imported))
.flatten();
let global_runtime =
Expression::StaticMemberExpression(ast.alloc_static_member_expression(
Span::default(),
ast.expression_identifier(Span::default(), ast.ident("globalThis")),
ast.identifier_name(
Span::default(),
ast.ident(if runtime_binding == RuntimeBinding::DirectGlobal {
"__SUPERCOV_DIRECT_RUNTIME__"
} else {
"__supercovRuntime"
}),
),
false,
));
let runtime_helper =
Expression::StaticMemberExpression(ast.alloc_static_member_expression(
Span::default(),
global_runtime,
ast.identifier_name(Span::default(), ast.ident(imported)),
false,
));
ast.variable_declarator(
Span::default(),
runtime_declaration_kind,
ast.binding_pattern_binding_identifier(Span::default(), ast.ident(local)),
annotation,
Some(runtime_helper),
false,
)
}));
parsed.program.body.insert(
0,
Statement::VariableDeclaration(ast.alloc_variable_declaration(
Span::default(),
runtime_declaration_kind,
declarators,
false,
)),
);
} else {
let import_specifiers =
ast.vec_from_iter(runtime_imports.into_iter().map(|(imported, local)| {
ast.import_declaration_specifier_import_specifier(
Span::default(),
ast.module_export_name_identifier_name(Span::default(), ast.ident(imported)),
ast.binding_identifier(Span::default(), ast.ident(local)),
oxc_ast::ast::ImportOrExportKind::Value,
)
}));
parsed.program.body.insert(
0,
Statement::ImportDeclaration(ast.alloc_import_declaration(
Span::default(),
Some(import_specifiers),
ast.string_literal(Span::default(), ast.str("virtual:supercov-runtime"), None),
None,
NONE,
oxc_ast::ast::ImportOrExportKind::Value,
)),
);
}
if let Some(wrapper) = capability_wrapper {
transform_capability_imports(&allocator, &mut parsed.program, source, wrapper);
}
let limitations = Vec::new();
mark_generated_statements(ast, &mut parsed.program);
AssertedTargetTransformer {
ast: AstBuilder::new(&allocator),
}
.visit_program(&mut parsed.program);
let (code, map) = generate_candidate(&parsed.program, file)?;
Ok(CandidateOutput {
engine: "rust-oxc".to_string(),
complete: true,
supported_surface: "complete-js-instrumenter-v1".to_string(),
code,
map,
decisions: collector.decisions,
points: point_analysis.points,
excluded_statements,
branches,
runtime: Some(CandidateRuntime {
coverage_hit,
mcdc_begin,
mcdc_condition,
mcdc_end,
register_probe_v2,
mcdc_end_v2,
coverage_hit_v2,
probe_file_v2,
parenthesized_assignment_value,
with_request_phase,
optional_select_v2,
select_short_v2,
select_right_v2,
select_named_right_v2,
select_assign_end_v2,
select_path_v2,
select_end2_v2,
select_end_v2,
select_next_v2,
rendered_value_v2,
optional_call_end_v2,
default_selected_v2,
default_entered_v2,
}),
coverage_limitations: {
let mut limitations = safety.semantic_limitations;
limitations.extend(call_analysis.limitations);
limitations.extend(default_analysis.limitations);
limitations.extend(safety.dynamic_limitations);
limitations
},
limitations,
})
}
fn probe_v2_hit<'a>(ast: AstBuilder<'a>, hit: &str, file: &str, index: usize) -> Statement<'a> {
ast.statement_expression(
Span::default(),
probe_v2_hit_expression(ast, hit, file, index),
)
}
fn probe_v2_hit_expression<'a>(
ast: AstBuilder<'a>,
hit: &str,
file: &str,
index: usize,
) -> Expression<'a> {
let span = Span::default();
let file_expression = || ast.expression_identifier(span, ast.ident(file));
let member = |object: Expression<'a>, name: &str| -> Expression<'a> {
Expression::from(ast.member_expression_static(
span,
object,
ast.identifier_name(span, ast.ident(name)),
false,
))
};
let index_literal =
|| ast.expression_numeric_literal(span, index as f64, None, NumberBase::Decimal);
let fast = member(member(file_expression(), "clock"), "fast");
let seen = ast.expression_binary(
span,
Expression::from(ast.member_expression_computed(
span,
member(file_expression(), "hitEpochs"),
index_literal(),
false,
)),
BinaryOperator::StrictEquality,
member(member(file_expression(), "clock"), "epoch"),
);
let call = ast.expression_call(
span,
ast.expression_identifier(span, ast.ident(hit)),
NONE,
ast.vec_from_array([
Argument::from(file_expression()),
Argument::from(index_literal()),
]),
false,
);
ast.expression_logical(
span,
ast.expression_logical(span, fast, LogicalOperator::And, seen),
LogicalOperator::Or,
call,
)
}
fn probe_temporaries<'a>(
ast: AstBuilder<'a>,
names: Vec<String>,
typescript: bool,
) -> Statement<'a> {
let declarations = ast.vec_from_iter(names.into_iter().map(|name| {
let annotation = typescript.then(|| {
ast.alloc_ts_type_annotation(Span::default(), ast.ts_type_any_keyword(Span::default()))
});
ast.variable_declarator(
Span::default(),
VariableDeclarationKind::Let,
ast.binding_pattern_binding_identifier(Span::default(), ast.ident(&name)),
annotation,
None,
false,
)
}));
Statement::VariableDeclaration(ast.alloc_variable_declaration(
Span::default(),
VariableDeclarationKind::Let,
declarations,
false,
))
}
fn assign_temporary<'a>(ast: AstBuilder<'a>, name: &str, value: Expression<'a>) -> Expression<'a> {
ast.expression_assignment(
Span::default(),
AssignmentOperator::Assign,
AssignmentTarget::from(ast.simple_assignment_target_assignment_target_identifier(
Span::default(),
ast.ident(name),
)),
value,
)
}
fn numeric<'a>(ast: AstBuilder<'a>, value: usize) -> Expression<'a> {
ast.expression_numeric_literal(Span::default(), value as f64, None, NumberBase::Decimal)
}
#[derive(Default)]
struct ExtraPoints {
base: usize,
ids: Vec<String>,
}
impl ExtraPoints {
fn allocate<T>(
&mut self,
targets: &HashMap<SpanKey, T>,
ids: impl Fn(&T) -> Vec<String>,
) -> HashMap<SpanKey, usize> {
let mut keys = targets.keys().copied().collect::<Vec<_>>();
keys.sort();
keys.into_iter()
.map(|key| {
let first = self.base + self.ids.len();
self.ids.extend(ids(&targets[&key]));
(key, first)
})
.collect()
}
fn next(&self) -> usize {
self.base + self.ids.len()
}
}
struct StatementProbeTransformer<'a> {
ast: AstBuilder<'a>,
coverage_hit_v2: String,
probe_file_v2: String,
targets: HashMap<SpanKey, Vec<PointTarget>>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> StatementProbeTransformer<'a> {
fn probe(&self, target: &PointTarget) -> Statement<'a> {
probe_v2_hit(
self.ast,
&self.coverage_hit_v2,
&self.probe_file_v2,
target.index,
)
}
fn take_statement_ids(&mut self, statement: &Statement<'a>) -> Vec<PointTarget> {
let mut ids = self
.targets
.remove(&span_key(statement.span()))
.unwrap_or_default();
if let Statement::ExportNamedDeclaration(export) = statement
&& let Some(declaration) = &export.declaration
&& let Some(nested) = self.targets.remove(&span_key(declaration.span()))
{
ids.extend(nested);
}
if let Statement::ExportDefaultDeclaration(export) = statement
&& let ExportDefaultDeclarationKind::ClassDeclaration(class) = &export.declaration
&& let Some(nested) = self.targets.remove(&span_key(class.span))
{
ids.extend(nested);
}
ids
}
fn wrap_bare(&mut self, statement: &mut Statement<'a>) {
let ids = self.take_statement_ids(statement);
if ids.is_empty() {
return;
}
let span = statement.span();
let original = statement.take_in(self.ast.allocator);
let mut body = self.ast.vec_with_capacity(ids.len() + 1);
body.extend(ids.iter().map(|target| self.probe(target)));
body.push(original);
*statement = self.ast.statement_block(span, body);
}
}
impl<'a> VisitMut<'a> for StatementProbeTransformer<'a> {
fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
let original = statements.take_in(self.ast.allocator);
let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
for mut statement in original {
let ids = self.take_statement_ids(&statement);
self.visit_statement(&mut statement);
instrumented.extend(ids.iter().map(|target| self.probe(target)));
instrumented.push(statement);
}
*statements = instrumented;
}
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
self.wrap_bare(&mut statement.consequent);
if let Some(alternate) = &mut statement.alternate {
self.wrap_bare(alternate);
}
walk_mut::walk_if_statement(self, statement);
}
fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
self.wrap_bare(&mut statement.body);
walk_mut::walk_while_statement(self, statement);
}
fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
self.wrap_bare(&mut statement.body);
walk_mut::walk_do_while_statement(self, statement);
}
fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
self.wrap_bare(&mut statement.body);
walk_mut::walk_for_statement(self, statement);
}
fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
self.wrap_bare(&mut statement.body);
walk_mut::walk_for_in_statement(self, statement);
}
fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
self.wrap_bare(&mut statement.body);
walk_mut::walk_for_of_statement(self, statement);
}
}
struct RenderedExpressionTransformer<'a> {
ast: AstBuilder<'a>,
rendered_value_v2: String,
probe_file_v2: String,
targets: HashMap<SpanKey, PointTarget>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> RenderedExpressionTransformer<'a> {
fn probe(&self, target: &PointTarget, value: Expression<'a>) -> Expression<'a> {
self.ast.expression_call(
Span::default(),
self.ast
.expression_identifier(Span::default(), self.ast.ident(&self.rendered_value_v2)),
NONE,
self.ast.vec_from_array([
Argument::from(
self.ast.expression_identifier(
Span::default(),
self.ast.ident(&self.probe_file_v2),
),
),
Argument::from(self.ast.expression_numeric_literal(
Span::default(),
target.index as f64,
None,
NumberBase::Decimal,
)),
Argument::from(value),
]),
false,
)
}
}
impl<'a> VisitMut<'a> for RenderedExpressionTransformer<'a> {
fn visit_jsx_expression_container(&mut self, container: &mut JSXExpressionContainer<'a>) {
walk_mut::walk_jsx_expression_container(self, container);
if matches!(container.expression, JSXExpression::EmptyExpression(_)) {
return;
}
let span = container.expression.span();
if self.source_sensitive_functions.contains(&span_key(span)) {
return;
}
let Some(target) = self.targets.remove(&span_key(span)) else {
return;
};
let inner = container
.expression
.take_in(self.ast.allocator)
.into_expression();
container.expression = JSXExpression::from(self.probe(&target, inner));
}
}
struct FunctionProbeTransformer<'a> {
ast: AstBuilder<'a>,
coverage_hit_v2: String,
probe_file_v2: String,
targets: HashMap<SpanKey, PointTarget>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> FunctionProbeTransformer<'a> {
fn probe(&self, target: &PointTarget) -> Statement<'a> {
probe_v2_hit(
self.ast,
&self.coverage_hit_v2,
&self.probe_file_v2,
target.index,
)
}
}
impl<'a> VisitMut<'a> for FunctionProbeTransformer<'a> {
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
if let Some(target) = self.targets.remove(&span_key(function.span))
&& let Some(body) = &mut function.body
{
body.statements.insert(0, self.probe(&target));
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
if let Some(target) = self.targets.remove(&span_key(function.span)) {
let probe = self.probe(&target);
if function.expression {
let original = function
.body
.statements
.pop()
.expect("expression arrow must contain its expression statement");
let Statement::ExpressionStatement(expression) = original else {
panic!("expression arrow body must be represented as an expression statement");
};
function.expression = false;
function.body.statements.push(probe);
function.body.statements.push(
self.ast
.statement_return(Span::default(), Some(expression.unbox().expression)),
);
} else {
function.body.statements.insert(0, probe);
}
}
walk_mut::walk_arrow_function_expression(self, function);
}
}
struct OptionalMemberTransformer<'a> {
ast: AstBuilder<'a>,
optional_select_v2: String,
probe_file_v2: String,
targets: HashMap<SpanKey, usize>,
links: HashMap<SpanKey, u8>,
call_frames: HashMap<SpanKey, String>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> OptionalMemberTransformer<'a> {
fn instrument_operand(
&self,
operand: Expression<'a>,
first: usize,
links: u8,
call_frame: Option<&str>,
) -> Expression<'a> {
let mut arguments = self.ast.vec_from_array([
Argument::from(
self.ast
.expression_identifier(Span::default(), self.ast.ident(&self.probe_file_v2)),
),
Argument::from(numeric(self.ast, first)),
Argument::from(operand),
]);
if links != 0 {
arguments.push(Argument::from(numeric(self.ast, usize::from(links))));
}
if let Some(frame) = call_frame {
arguments.push(Argument::from(
self.ast
.expression_identifier(Span::default(), self.ast.ident(frame)),
));
}
self.ast.expression_call(
Span::default(),
self.ast
.expression_identifier(Span::default(), self.ast.ident(&self.optional_select_v2)),
NONE,
arguments,
false,
)
}
fn instrument_target(&mut self, span: Span, object: &mut Expression<'a>) {
let Some(first) = self.targets.remove(&span_key(span)) else {
return;
};
let mut links = self.links.get(&span_key(span)).copied().unwrap_or(0);
let call_frame = self.call_frames.get(&span_key(span)).cloned();
if call_frame.is_none() {
links &= !LINK_AFTER_CALL;
}
let again = match &*object {
Expression::Identifier(identifier) => Some(
self.ast
.expression_identifier(identifier.span, identifier.name),
),
Expression::ThisExpression(this) => Some(self.ast.expression_this(this.span)),
_ => None,
};
let mut operand = object.take_in(self.ast.allocator);
if again.is_some() {
*operand.span_mut() = Span::default();
}
let recorded = self.instrument_operand(operand, first, links, call_frame.as_deref());
*object = match again {
Some(again) => self
.ast
.expression_sequence(Span::default(), self.ast.vec_from_array([recorded, again])),
None => recorded,
};
}
}
impl<'a> VisitMut<'a> for OptionalMemberTransformer<'a> {
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_computed_member_expression(&mut self, member: &mut ComputedMemberExpression<'a>) {
self.instrument_target(member.span, &mut member.object);
walk_mut::walk_computed_member_expression(self, member);
}
fn visit_static_member_expression(&mut self, member: &mut StaticMemberExpression<'a>) {
self.instrument_target(member.span, &mut member.object);
walk_mut::walk_static_member_expression(self, member);
}
fn visit_private_field_expression(&mut self, member: &mut PrivateFieldExpression<'a>) {
self.instrument_target(member.span, &mut member.object);
walk_mut::walk_private_field_expression(self, member);
}
}
#[derive(Clone)]
struct OptionalCallSiteRuntime {
frame: String,
first: usize,
}
struct OptionalCallTransformer<'a, 's> {
ast: AstBuilder<'a>,
optional_call_end_v2: String,
probe_file_v2: String,
typescript: bool,
scope_declarations: Vec<Vec<String>>,
sites: HashMap<SpanKey, OptionalCallSiteRuntime>,
roots: HashMap<SpanKey, Vec<SpanKey>>,
source_sensitive_functions: HashSet<SpanKey>,
_source: std::marker::PhantomData<&'s str>,
}
impl<'a, 's> OptionalCallTransformer<'a, 's> {
#[allow(clippy::too_many_arguments)]
fn new(
ast: AstBuilder<'a>,
source: &'s str,
optional_call_end_v2: String,
probe_file_v2: String,
typescript: bool,
sites: HashMap<SpanKey, usize>,
roots: HashMap<SpanKey, Vec<SpanKey>>,
source_sensitive_functions: HashSet<SpanKey>,
) -> Self {
let mut names = CandidateNames::new(source);
let mut ordered = sites.into_iter().collect::<Vec<_>>();
ordered.sort();
let sites = ordered
.into_iter()
.map(|(key, first)| {
(
key,
OptionalCallSiteRuntime {
frame: names.allocate("_optionalCall"),
first,
},
)
})
.collect();
Self {
ast,
optional_call_end_v2,
probe_file_v2,
typescript,
scope_declarations: Vec::new(),
sites,
roots,
source_sensitive_functions,
_source: std::marker::PhantomData,
}
}
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
self.ast.expression_call(
Span::default(),
self.identifier(name),
NONE,
arguments,
false,
)
}
fn reached(&self, frame: &str, value: Expression<'a>) -> Expression<'a> {
self.ast.expression_sequence(
Span::default(),
self.ast.vec_from_array([
assign_temporary(self.ast, frame, numeric(self.ast, 1)),
value,
]),
)
}
fn enter_scope(&mut self) {
self.scope_declarations.push(Vec::new());
}
fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
let names = self
.scope_declarations
.pop()
.expect("optional-call scope stack must remain balanced");
if names.is_empty() {
return;
}
statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
}
fn instrument_callee(
&self,
callee: Expression<'a>,
site: &OptionalCallSiteRuntime,
) -> Expression<'a> {
match callee {
Expression::ComputedMemberExpression(mut member) => {
let property = member.expression.take_in(self.ast.allocator);
member.expression = self.reached(&site.frame, property);
Expression::ComputedMemberExpression(member)
}
Expression::StaticMemberExpression(member) => {
let member = member.unbox();
let property = self.ast.expression_string_literal(
Span::default(),
self.ast.str(member.property.name.as_str()),
None,
);
Expression::ComputedMemberExpression(self.ast.alloc_computed_member_expression(
member.span,
member.object,
self.reached(&site.frame, property),
member.optional,
))
}
Expression::PrivateFieldExpression(mut member) => {
let object = member.object.take_in(self.ast.allocator);
member.object = self.reached(&site.frame, object);
Expression::PrivateFieldExpression(member)
}
Expression::ChainExpression(mut chain) => {
match &mut chain.expression {
ChainElement::ComputedMemberExpression(member) => {
let property = member.expression.take_in(self.ast.allocator);
member.expression = self.reached(&site.frame, property);
}
ChainElement::StaticMemberExpression(member) => {
let member = member.take_in(self.ast.allocator);
let property = self.ast.expression_string_literal(
Span::default(),
self.ast.str(member.property.name.as_str()),
None,
);
chain.expression = ChainElement::ComputedMemberExpression(
self.ast.alloc_computed_member_expression(
member.span,
member.object,
self.reached(&site.frame, property),
member.optional,
),
);
}
ChainElement::PrivateFieldExpression(member) => {
let object = member.object.take_in(self.ast.allocator);
member.object = self.reached(&site.frame, object);
}
ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => {
return self.reached(&site.frame, Expression::ChainExpression(chain));
}
}
Expression::ChainExpression(chain)
}
Expression::ParenthesizedExpression(mut parenthesized) => {
let inner = parenthesized.expression.take_in(self.ast.allocator);
parenthesized.expression = self.instrument_callee(inner, site);
Expression::ParenthesizedExpression(parenthesized)
}
Expression::TSAsExpression(mut wrapped) => {
let inner = wrapped.expression.take_in(self.ast.allocator);
wrapped.expression = self.instrument_callee(inner, site);
Expression::TSAsExpression(wrapped)
}
Expression::TSSatisfiesExpression(mut wrapped) => {
let inner = wrapped.expression.take_in(self.ast.allocator);
wrapped.expression = self.instrument_callee(inner, site);
Expression::TSSatisfiesExpression(wrapped)
}
Expression::TSTypeAssertion(mut wrapped) => {
let inner = wrapped.expression.take_in(self.ast.allocator);
wrapped.expression = self.instrument_callee(inner, site);
Expression::TSTypeAssertion(wrapped)
}
Expression::TSNonNullExpression(mut wrapped) => {
let inner = wrapped.expression.take_in(self.ast.allocator);
wrapped.expression = self.instrument_callee(inner, site);
Expression::TSNonNullExpression(wrapped)
}
Expression::CallExpression(mut call) => {
call.arguments.insert(0, self.marker_spread(&site.frame, 1));
Expression::CallExpression(call)
}
other => self.reached(&site.frame, other),
}
}
fn instrument_call(&self, call: &mut CallExpression<'a>, site: &OptionalCallSiteRuntime) {
let callee = call.callee.take_in(self.ast.allocator);
call.callee = self.instrument_callee(callee, site);
call.arguments.insert(0, self.marker_spread(&site.frame, 2));
}
fn marker_spread(&self, frame: &str, value: usize) -> Argument<'a> {
let none = Expression::from(
self.ast.member_expression_static(
Span::default(),
self.identifier(&self.probe_file_v2),
self.ast
.identifier_name(Span::default(), self.ast.ident("none")),
false,
),
);
let marked = self.ast.expression_sequence(
Span::default(),
self.ast.vec_from_array([
assign_temporary(self.ast, frame, numeric(self.ast, value)),
none,
]),
);
Argument::SpreadElement(self.ast.alloc_spread_element(Span::default(), marked))
}
fn wrap_root(&mut self, expression: &mut Expression<'a>, site_keys: &[SpanKey]) {
let sites = site_keys
.iter()
.map(|key| {
self.sites
.get(key)
.expect("optional-call root must reference a known site")
.clone()
})
.collect::<Vec<_>>();
self.scope_declarations
.last_mut()
.expect("optional-call root must be inside a program or function")
.extend(sites.iter().map(|site| site.frame.clone()));
let original = expression.take_in(self.ast.allocator);
let mut measured = original;
for site in sites.iter().rev() {
measured = self.call(
&self.optional_call_end_v2,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, site.first)),
Argument::from(measured),
Argument::from(self.identifier(&site.frame)),
]),
);
}
let mut sequence = self.ast.vec_with_capacity(sites.len() + 1);
for site in &sites {
sequence.push(assign_temporary(
self.ast,
&site.frame,
numeric(self.ast, 0),
));
}
sequence.push(measured);
*expression = self.ast.expression_sequence(Span::default(), sequence);
}
}
impl<'a> VisitMut<'a> for OptionalCallTransformer<'a, '_> {
fn visit_program(&mut self, program: &mut Program<'a>) {
self.enter_scope();
walk_mut::walk_program(self, program);
self.leave_scope(&mut program.body);
}
fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
self.enter_scope();
walk_mut::walk_function_body(self, body);
self.leave_scope(&mut body.statements);
}
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
walk_mut::walk_call_expression(self, call);
if let Some(site) = self.sites.get(&span_key(call.span)).cloned() {
self.instrument_call(call, &site);
}
}
fn visit_expression(&mut self, expression: &mut Expression<'a>) {
let key = span_key(expression.span());
walk_mut::walk_expression(self, expression);
if let Some(site_keys) = self.roots.remove(&key) {
self.wrap_root(expression, &site_keys);
}
}
}
struct DefaultSlot {
slot: usize,
default_point: usize,
provided_point: usize,
}
struct DefaultTransformer<'a> {
ast: AstBuilder<'a>,
default_selected_v2: String,
default_entered_v2: String,
probe_file_v2: String,
slots: HashMap<String, DefaultSlot>,
parameter_targets: HashMap<SpanKey, DefaultTarget>,
binding_targets: HashMap<SpanKey, DefaultTarget>,
function_entries: Vec<Vec<Statement<'a>>>,
declaration_entries: HashMap<SpanKey, Vec<Statement<'a>>>,
active_declaration: Vec<SpanKey>,
parameter_pattern_depth: usize,
source_sensitive_functions: HashSet<SpanKey>,
typescript: bool,
}
impl<'a> DefaultTransformer<'a> {
fn default_type(&self, value: &Expression<'a>) -> Option<oxc_ast::ast::TSType<'a>> {
let span = Span::default();
let mut value = value;
while let Expression::ParenthesizedExpression(parenthesized) = value {
value = &parenthesized.expression;
}
match value {
Expression::TSAsExpression(asserted) => {
(!asserted.type_annotation.is_const_type_reference())
.then(|| asserted.type_annotation.clone_in(self.ast.allocator))
}
Expression::TSTypeAssertion(asserted) => {
(!asserted.type_annotation.is_const_type_reference())
.then(|| asserted.type_annotation.clone_in(self.ast.allocator))
}
Expression::BooleanLiteral(_) => Some(self.ast.ts_type_boolean_keyword(span)),
Expression::NumericLiteral(_) => Some(self.ast.ts_type_number_keyword(span)),
Expression::BigIntLiteral(_) => Some(self.ast.ts_type_big_int_keyword(span)),
Expression::StringLiteral(_) => Some(self.ast.ts_type_string_keyword(span)),
Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
Some(self.ast.ts_type_string_keyword(span))
}
Expression::UnaryExpression(unary)
if unary.operator == UnaryOperator::UnaryNegation
&& matches!(unary.argument, Expression::NumericLiteral(_)) =>
{
Some(self.ast.ts_type_number_keyword(span))
}
_ => None,
}
}
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn string_argument(&self, value: &str) -> Argument<'a> {
Argument::from(self.ast.expression_string_literal(
Span::default(),
self.ast.str(value),
None,
))
}
fn index_argument(&self, index: usize) -> Argument<'a> {
Argument::from(self.ast.expression_numeric_literal(
Span::default(),
index as f64,
None,
NumberBase::Decimal,
))
}
fn slot(&self, target: &DefaultTarget) -> &DefaultSlot {
self.slots
.get(&target.default_id)
.expect("every default target has a V2 slot")
}
fn selected(&self, value: Expression<'a>, target: &DefaultTarget) -> Expression<'a> {
let mut arguments = self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
self.index_argument(self.slot(target).slot),
Argument::from(value),
]);
if let Some(name) = &target.inferred_name {
arguments.push(self.string_argument(name));
}
self.ast.expression_call(
Span::default(),
self.identifier(&self.default_selected_v2),
NONE,
arguments,
false,
)
}
fn entered(&self, target: &DefaultTarget) -> Statement<'a> {
let slot = self.slot(target);
self.ast.statement_expression(
Span::default(),
self.ast.expression_call(
Span::default(),
self.identifier(&self.default_entered_v2),
NONE,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
self.index_argument(slot.slot),
self.index_argument(slot.default_point),
self.index_argument(slot.provided_point),
]),
false,
),
)
}
fn push_entry(&mut self, target: &DefaultTarget) {
let entry = self.entered(target);
if self.parameter_pattern_depth > 0 {
self.function_entries
.last_mut()
.expect("parameter default must belong to a function")
.push(entry);
} else {
let declaration = *self
.active_declaration
.last()
.expect("binding default must belong to a declaration");
self.declaration_entries
.entry(declaration)
.or_default()
.push(entry);
}
}
fn prepend_entries(&self, body: &mut Statement<'a>, entries: Vec<Statement<'a>>) {
if entries.is_empty() {
return;
}
if let Statement::BlockStatement(block) = body {
for (index, entry) in entries.into_iter().enumerate() {
block.body.insert(index, entry);
}
return;
}
let span = body.span();
let original = body.take_in(self.ast.allocator);
let mut statements = self.ast.vec_with_capacity(entries.len() + 1);
statements.extend(entries);
statements.push(original);
*body = self.ast.statement_block(span, statements);
}
fn variable_span(statement: &Statement<'a>) -> Option<SpanKey> {
match statement {
Statement::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
Statement::ExportNamedDeclaration(export) => match &export.declaration {
Some(Declaration::VariableDeclaration(declaration)) => {
Some(span_key(declaration.span))
}
_ => None,
},
_ => None,
}
}
fn loop_declaration_span(left: &ForStatementLeft<'a>) -> Option<SpanKey> {
match left {
ForStatementLeft::VariableDeclaration(declaration) => Some(span_key(declaration.span)),
_ => None,
}
}
}
impl<'a> VisitMut<'a> for DefaultTransformer<'a> {
fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
let original = statements.take_in(self.ast.allocator);
let mut instrumented = self.ast.vec_with_capacity(original.len() * 2);
for mut statement in original {
let declaration = Self::variable_span(&statement);
self.visit_statement(&mut statement);
instrumented.push(statement);
if let Some(declaration) = declaration
&& let Some(entries) = self.declaration_entries.remove(&declaration)
{
instrumented.extend(entries);
}
}
*statements = instrumented;
}
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
self.function_entries.push(Vec::new());
walk_mut::walk_function(self, function, flags);
let entries = self
.function_entries
.pop()
.expect("default function-entry stack must remain balanced");
if let Some(body) = &mut function.body {
for (index, entry) in entries.into_iter().enumerate() {
body.statements.insert(index, entry);
}
}
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
self.function_entries.push(Vec::new());
walk_mut::walk_arrow_function_expression(self, function);
let entries = self
.function_entries
.pop()
.expect("default arrow-entry stack must remain balanced");
for (index, entry) in entries.into_iter().enumerate() {
function.body.statements.insert(index, entry);
}
}
fn visit_formal_parameter(&mut self, parameter: &mut FormalParameter<'a>) {
let outer = self.parameter_targets.remove(&span_key(parameter.span));
if let Some(target) = &outer {
let entry = self.entered(target);
self.function_entries
.last_mut()
.expect("formal parameter must belong to a function")
.push(entry);
}
self.visit_decorators(&mut parameter.decorators);
self.parameter_pattern_depth += 1;
self.visit_binding_pattern(&mut parameter.pattern);
self.parameter_pattern_depth -= 1;
if let Some(annotation) = &mut parameter.type_annotation {
self.visit_ts_type_annotation(annotation);
}
if let Some(initializer) = &mut parameter.initializer {
if self.typescript
&& outer.is_some()
&& parameter.type_annotation.is_none()
&& matches!(parameter.pattern, BindingPattern::BindingIdentifier(_))
{
let named = self.default_type(initializer);
if let Some(named) = named {
parameter.type_annotation =
Some(self.ast.alloc_ts_type_annotation(Span::default(), named));
}
}
self.visit_expression(initializer);
if let Some(target) = outer {
let value = initializer.take_in(self.ast.allocator);
**initializer = self.selected(value, &target);
}
}
}
fn visit_assignment_pattern(&mut self, assignment: &mut AssignmentPattern<'a>) {
let target = if self.parameter_pattern_depth > 0 {
self.parameter_targets.remove(&span_key(assignment.span))
} else {
self.binding_targets.remove(&span_key(assignment.span))
};
if let Some(target) = &target {
self.push_entry(target);
}
walk_mut::walk_assignment_pattern(self, assignment);
if let Some(target) = target {
let value = assignment.right.take_in(self.ast.allocator);
assignment.right = self.selected(value, &target);
}
}
fn visit_variable_declaration(&mut self, declaration: &mut VariableDeclaration<'a>) {
self.active_declaration.push(span_key(declaration.span));
walk_mut::walk_variable_declaration(self, declaration);
self.active_declaration
.pop()
.expect("default declaration stack must remain balanced");
}
fn visit_for_in_statement(&mut self, statement: &mut ForInStatement<'a>) {
let declaration = Self::loop_declaration_span(&statement.left);
walk_mut::walk_for_in_statement(self, statement);
if let Some(declaration) = declaration
&& let Some(entries) = self.declaration_entries.remove(&declaration)
{
self.prepend_entries(&mut statement.body, entries);
}
}
fn visit_for_of_statement(&mut self, statement: &mut ForOfStatement<'a>) {
let declaration = Self::loop_declaration_span(&statement.left);
walk_mut::walk_for_of_statement(self, statement);
if let Some(declaration) = declaration
&& let Some(entries) = self.declaration_entries.remove(&declaration)
{
self.prepend_entries(&mut statement.body, entries);
}
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
}
enum ExtendedKind {
Try,
Loop,
}
struct ExtendedTransformer<'a, 's> {
ast: AstBuilder<'a>,
coverage_hit_v2: String,
probe_file_v2: String,
typescript: bool,
try_targets: HashMap<SpanKey, usize>,
loop_targets: HashMap<SpanKey, usize>,
names: CandidateNames<'s>,
scope_declarations: Vec<Vec<String>>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> ExtendedTransformer<'a, '_> {
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn enter_scope(&mut self) {
self.scope_declarations.push(Vec::new());
}
fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
let names = self
.scope_declarations
.pop()
.expect("extended scope stack must remain balanced");
if names.is_empty() {
return;
}
statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
}
fn scratch(&mut self, base: &str) -> String {
let name = self.names.allocate(base);
self.scope_declarations
.last_mut()
.expect("extended branch must be inside a program or function")
.push(name.clone());
name
}
fn target(statement: &Statement<'a>) -> Option<(ExtendedKind, SpanKey)> {
match statement {
Statement::TryStatement(node) => Some((ExtendedKind::Try, span_key(node.span))),
Statement::ForInStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
Statement::ForOfStatement(node) => Some((ExtendedKind::Loop, span_key(node.span))),
Statement::LabeledStatement(node) => Self::target(&node.body),
_ => None,
}
}
fn inner_try<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut TryStatement<'a>> {
match statement {
Statement::TryStatement(node) => Some(node),
Statement::LabeledStatement(node) => Self::inner_try(&mut node.body),
_ => None,
}
}
fn inner_loop_body<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut Statement<'a>> {
match statement {
Statement::ForInStatement(node) => Some(&mut node.body),
Statement::ForOfStatement(node) => Some(&mut node.body),
Statement::LabeledStatement(node) => Self::inner_loop_body(&mut node.body),
_ => None,
}
}
fn prepend(body: &mut Statement<'a>, entry: Statement<'a>, ast: AstBuilder<'a>) {
if let Statement::BlockStatement(block) = body {
block.body.insert(0, entry);
return;
}
let span = body.span();
let original = body.take_in(ast.allocator);
*body = ast.statement_block(span, ast.vec_from_array([entry, original]));
}
fn hit(&self, index: usize) -> Expression<'a> {
probe_v2_hit_expression(self.ast, &self.coverage_hit_v2, &self.probe_file_v2, index)
}
fn reset(&self, flag: &str) -> Statement<'a> {
self.ast.statement_expression(
Span::default(),
assign_temporary(self.ast, flag, numeric(self.ast, 0)),
)
}
fn first_time(&self, flag: &str, index: usize) -> Statement<'a> {
let record = self.ast.expression_sequence(
Span::default(),
self.ast.vec_from_array([
assign_temporary(self.ast, flag, numeric(self.ast, 1)),
self.hit(index),
]),
);
self.ast.statement_expression(
Span::default(),
self.ast.expression_logical(
Span::default(),
self.identifier(flag),
LogicalOperator::Or,
record,
),
)
}
fn otherwise(&self, flag: &str, index: usize) -> Statement<'a> {
self.ast.statement_expression(
Span::default(),
self.ast.expression_logical(
Span::default(),
self.identifier(flag),
LogicalOperator::Or,
self.hit(index),
),
)
}
fn instrument_try(&mut self, statement: &mut Statement<'a>, first: usize) {
let span = statement.span();
let flag = self.scratch("_supercovTryFrame");
let reset = self.reset(&flag);
let caught = self.first_time(&flag, first + 1);
let node = Self::inner_try(statement).expect("try target must remain a try statement");
node.handler
.as_mut()
.expect("try coverage requires a catch handler")
.body
.body
.insert(0, caught);
let end = self.otherwise(&flag, first);
if let Some(finalizer) = &mut node.finalizer {
finalizer.body.insert(0, end);
} else {
node.finalizer = Some(
self.ast
.alloc_block_statement(Span::new(span.end - 1, span.end), self.ast.vec1(end)),
);
}
let original = statement.take_in(self.ast.allocator);
*statement = self
.ast
.statement_block(span, self.ast.vec_from_array([reset, original]));
}
fn instrument_loop(&mut self, statement: &mut Statement<'a>, first: usize) {
let span = statement.span();
let flag = self.scratch("_supercovLoopFrame");
let reset = self.reset(&flag);
let entered = self.first_time(&flag, first + 1);
Self::prepend(
Self::inner_loop_body(statement).expect("loop target must remain an enumeration loop"),
entered,
self.ast,
);
let original = statement.take_in(self.ast.allocator);
let end = self.otherwise(&flag, first);
let wrapped = self.ast.statement_try(
Span::default(),
self.ast
.block_statement(Span::default(), self.ast.vec1(original)),
None::<oxc_allocator::Box<'a, CatchClause<'a>>>,
Some(
self.ast
.block_statement(Span::default(), self.ast.vec1(end)),
),
);
*statement = self
.ast
.statement_block(span, self.ast.vec_from_array([reset, wrapped]));
}
}
impl<'a> VisitMut<'a> for ExtendedTransformer<'a, '_> {
fn visit_program(&mut self, program: &mut Program<'a>) {
self.enter_scope();
walk_mut::walk_program(self, program);
self.leave_scope(&mut program.body);
}
fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
self.enter_scope();
walk_mut::walk_function_body(self, body);
self.leave_scope(&mut body.statements);
}
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_statement(&mut self, statement: &mut Statement<'a>) {
let Some((kind, key)) = Self::target(statement) else {
walk_mut::walk_statement(self, statement);
return;
};
match kind {
ExtendedKind::Try => {
if let Some(target) = self.try_targets.remove(&key) {
walk_mut::walk_statement(self, statement);
self.instrument_try(statement, target);
} else {
walk_mut::walk_statement(self, statement);
}
}
ExtendedKind::Loop => {
if let Some(target) = self.loop_targets.remove(&key) {
walk_mut::walk_statement(self, statement);
self.instrument_loop(statement, target);
} else {
walk_mut::walk_statement(self, statement);
}
}
}
}
}
enum SelectionShape {
Leaf(usize),
Selection {
first: usize,
kind: usize,
left: Box<SelectionShape>,
right: Box<SelectionShape>,
},
}
impl SelectionShape {
fn leftmost(&self) -> usize {
match self {
Self::Leaf(leaf) => *leaf,
Self::Selection { left, .. } => left.leftmost(),
}
}
fn deciders(&self, truthy: bool, nullish: bool) -> Vec<(usize, Vec<usize>)> {
match self {
Self::Leaf(leaf) => vec![(*leaf, Vec::new())],
Self::Selection {
first,
kind,
left,
right,
} => {
let mut deciders = Vec::new();
let short = match kind {
0 => truthy,
1 => !truthy,
_ => !nullish,
};
if short {
for (leaf, mut hits) in left.deciders(truthy, nullish) {
hits.push(first + usize::from(truthy));
deciders.push((leaf, hits));
}
}
for (leaf, mut hits) in right.deciders(truthy, nullish) {
hits.push(first + 2 + usize::from(truthy));
deciders.push((leaf, hits));
}
deciders
}
}
}
fn before(&self, before: &mut Vec<Vec<(usize, Vec<usize>)>>) {
let Self::Selection {
kind, left, right, ..
} = self
else {
return;
};
before[right.leftmost()] = left.deciders(*kind == 1, *kind == 2);
left.before(before);
right.before(before);
}
}
type SelectionTable = (Vec<Vec<usize>>, Vec<Vec<Vec<usize>>>);
struct SelectionSite {
helper: String,
index: usize,
frame: String,
}
struct LogicalValueTransformer<'a, 's> {
ast: AstBuilder<'a>,
select_short_v2: String,
select_right_v2: String,
select_named_right_v2: String,
select_assign_end_v2: String,
select_path_v2: String,
select_end2_v2: String,
select_end_v2: String,
select_next_v2: String,
coverage_hit_v2: String,
probe_file_v2: String,
typescript: bool,
names: CandidateNames<'s>,
scope_declarations: Vec<Vec<String>>,
frameless: usize,
trees: Vec<SelectionTable>,
aliasing: bool,
after_declaration: Vec<Expression<'a>>,
logical_targets: HashMap<SpanKey, usize>,
assignment_targets: HashMap<SpanKey, usize>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> LogicalValueTransformer<'a, '_> {
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
AssignmentTarget::from(
self.ast
.simple_assignment_target_assignment_target_identifier(
Span::default(),
self.ast.ident(name),
),
)
}
fn call(&self, name: &str, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> {
self.ast.expression_call(
Span::default(),
self.identifier(name),
NONE,
arguments,
false,
)
}
fn string_argument(&self, value: &str) -> Argument<'a> {
Argument::from(self.ast.expression_string_literal(
Span::default(),
self.ast.str(value),
None,
))
}
fn enter_scope(&mut self) {
self.scope_declarations.push(Vec::new());
}
fn leave_scope(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
let names = self
.scope_declarations
.pop()
.expect("logical-value scope stack must remain balanced");
if names.is_empty() {
return;
}
statements.insert(0, probe_temporaries(self.ast, names, self.typescript));
}
fn scratch(&mut self, base: &str) -> String {
let name = self.names.allocate(base);
self.scope_declarations
.last_mut()
.expect("logical expression must be inside a program or function")
.push(name.clone());
name
}
fn helper(&self, name: &str, arguments: [Argument<'a>; 3]) -> Expression<'a> {
let mut all = self
.ast
.vec_from_array([Argument::from(self.identifier(&self.probe_file_v2))]);
all.extend(arguments);
self.call(name, all)
}
fn instrument_tree(&mut self, expression: &mut Expression<'a>, path: &[usize]) {
let key = span_key(expression.span());
let first = self
.logical_targets
.remove(&key)
.expect("a selection tree starts at a selection");
let Expression::LogicalExpression(logical) = expression else {
panic!("logical-value target must remain a logical expression");
};
let kind = match logical.operator {
LogicalOperator::Or => 0,
LogicalOperator::And => 1,
LogicalOperator::Coalesce => 2,
};
let mut left_path = vec![first * 4 + kind];
left_path.extend_from_slice(path);
let mut right_path = vec![first * 4 + 3];
right_path.extend_from_slice(path);
self.instrument_operand(&mut logical.left, &left_path);
self.instrument_operand(&mut logical.right, &right_path);
}
fn instrument_operand(&mut self, operand: &mut Expression<'a>, path: &[usize]) {
let mut inner = &mut *operand;
while let Expression::ParenthesizedExpression(parenthesized) = inner {
inner = &mut parenthesized.expression;
}
if matches!(inner, Expression::LogicalExpression(_))
&& self.logical_targets.contains_key(&span_key(inner.span()))
{
self.instrument_tree(inner, path);
return;
}
self.visit_expression(operand);
let value = operand.take_in(self.ast.allocator);
*operand = match *path {
[step] if step % 4 == 3 => {
let mut arguments = self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, step / 4)),
]);
arguments.push(Argument::from(value));
self.call(&self.select_right_v2, arguments)
}
[step] => self.helper(
&self.select_short_v2,
[
Argument::from(numeric(self.ast, step / 4)),
Argument::from(value),
Argument::from(numeric(self.ast, step % 4)),
],
),
_ => {
let mut arguments = self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(value),
]);
arguments.extend(
path.iter()
.map(|step| Argument::from(numeric(self.ast, *step))),
);
self.call(&self.select_path_v2, arguments)
}
};
}
fn shape(
&mut self,
expression: &Expression<'a>,
path: &[usize],
leaves: &mut Vec<Vec<usize>>,
) -> SelectionShape {
let mut inner = expression;
while let Expression::ParenthesizedExpression(parenthesized) = inner {
inner = &parenthesized.expression;
}
if let Expression::LogicalExpression(logical) = inner
&& let Some(first) = self.logical_targets.remove(&span_key(logical.span))
{
let kind = match logical.operator {
LogicalOperator::Or => 0,
LogicalOperator::And => 1,
LogicalOperator::Coalesce => 2,
};
let mut left_path = vec![first * 4 + kind];
left_path.extend_from_slice(path);
let mut right_path = vec![first * 4 + 3];
right_path.extend_from_slice(path);
let left = Box::new(self.shape(&logical.left, &left_path, leaves));
let right = Box::new(self.shape(&logical.right, &right_path, leaves));
return SelectionShape::Selection {
first,
kind,
left,
right,
};
}
leaves.push(path.to_vec());
SelectionShape::Leaf(leaves.len() - 1)
}
fn hits(&self, points: &[usize]) -> Option<Expression<'a>> {
let mut hits = self.ast.vec_from_iter(points.iter().map(|point| {
self.call(
&self.coverage_hit_v2,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, *point)),
]),
)
}));
match hits.len() {
0 => None,
1 => hits.pop(),
_ => Some(self.ast.expression_sequence(Span::default(), hits)),
}
}
fn recorded_before(
&self,
frame: &str,
tree: Option<usize>,
leaf: usize,
deciders: &[(usize, Vec<usize>)],
) -> Option<Expression<'a>> {
if deciders.iter().all(|(_, points)| points.is_empty()) {
return None;
}
if let [(_, points)] = deciders {
return self.hits(points);
}
let tree = tree.expect("a tree with a choice of leaves has a table");
Some(self.call(
&self.select_next_v2,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, tree)),
Argument::from(self.identifier(frame)),
Argument::from(numeric(self.ast, leaf)),
]),
))
}
fn name_leaves(
&mut self,
expression: &mut Expression<'a>,
shape: &SelectionShape,
frame: &str,
tree: Option<usize>,
before: &[Vec<(usize, Vec<usize>)>],
) {
let leaf = match shape {
SelectionShape::Leaf(leaf) => *leaf,
SelectionShape::Selection { left, right, .. } => {
let mut inner = expression;
while let Expression::ParenthesizedExpression(parenthesized) = inner {
inner = &mut parenthesized.expression;
}
let Expression::LogicalExpression(logical) = inner else {
panic!("a selection must remain a logical expression");
};
self.name_leaves(&mut logical.left, left, frame, tree, before);
self.name_leaves(&mut logical.right, right, frame, tree, before);
return;
}
};
self.visit_expression(expression);
let value = expression.take_in(self.ast.allocator);
let mut steps = self.ast.vec_with_capacity(3);
steps.extend(self.recorded_before(frame, tree, leaf, &before[leaf]));
steps.push(self.ast.expression_assignment(
Span::default(),
AssignmentOperator::Assign,
self.assignment_target(frame),
numeric(self.ast, leaf),
));
steps.push(value);
*expression = self.ast.expression_sequence(Span::default(), steps);
}
fn instrument_value_tree(&mut self, expression: &mut Expression<'a>) {
let site = self.name_tree(expression);
let tree = expression.take_in(self.ast.allocator);
*expression = self.record_tree(&site, tree);
}
fn record_tree(&self, site: &SelectionSite, value: Expression<'a>) -> Expression<'a> {
self.call(
&site.helper,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, site.index)),
Argument::from(value),
Argument::from(self.identifier(&site.frame)),
]),
)
}
fn name_tree(&mut self, expression: &mut Expression<'a>) -> SelectionSite {
let mut leaves = Vec::new();
let shape = self.shape(expression, &[], &mut leaves);
let SelectionShape::Selection { first, .. } = shape else {
panic!("a selection tree starts at a selection");
};
let mut before = vec![Vec::new(); leaves.len()];
shape.before(&mut before);
let frame = self.scratch("_supercovSelected");
let tree = (leaves.len() > 2).then(|| {
let choices = before
.iter()
.map(|deciders| {
let mut by_leaf = Vec::new();
if deciders.len() > 1 {
for (leaf, points) in deciders {
if by_leaf.len() <= *leaf {
by_leaf.resize(*leaf + 1, Vec::new());
}
by_leaf[*leaf] = points.clone();
}
}
by_leaf
})
.collect();
self.trees.push((leaves, choices));
self.trees.len() - 1
});
self.name_leaves(expression, &shape, &frame, tree, &before);
let (helper, index) = match tree {
Some(tree) => (self.select_end_v2.clone(), tree),
None => (self.select_end2_v2.clone(), first),
};
SelectionSite {
helper,
index,
frame,
}
}
fn instrument_assignment(
&mut self,
assignment: oxc_allocator::Box<'a, AssignmentExpression<'a>>,
first: usize,
) -> Expression<'a> {
let assignment = assignment.unbox();
let inferred_name = match &assignment.left {
AssignmentTarget::AssignmentTargetIdentifier(identifier)
if assignment.span.start == identifier.span.start
&& expression_is_anonymous_definition(&assignment.right) =>
{
Some(identifier.name.to_string())
}
_ => None,
};
let right_evaluated = self.scratch("_supercovAssignedRight");
let set = |value| {
self.ast.expression_assignment(
Span::default(),
AssignmentOperator::Assign,
self.assignment_target(&right_evaluated),
numeric(self.ast, value),
)
};
let (reset, mark) = (set(0), set(1));
let mut right_arguments = self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, first)),
Argument::from(assignment.right),
]);
let right_helper = match inferred_name {
Some(name) => {
right_arguments.push(self.string_argument(&name));
&self.select_named_right_v2
}
None => &self.select_right_v2,
};
right_arguments.push(Argument::from(mark));
let right = self.call(right_helper, right_arguments);
let measured_assignment = self.ast.expression_assignment(
Span::default(),
assignment.operator,
assignment.left,
right,
);
let end = self.call(
&self.select_assign_end_v2,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(numeric(self.ast, first)),
Argument::from(measured_assignment),
Argument::from(self.identifier(&right_evaluated)),
]),
);
self.ast
.expression_sequence(Span::default(), self.ast.vec_from_array([reset, end]))
}
}
impl<'a> VisitMut<'a> for LogicalValueTransformer<'a, '_> {
fn visit_program(&mut self, program: &mut Program<'a>) {
self.enter_scope();
walk_mut::walk_program(self, program);
self.leave_scope(&mut program.body);
}
fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
let frameless = std::mem::take(&mut self.frameless);
self.enter_scope();
walk_mut::walk_function_body(self, body);
self.leave_scope(&mut body.statements);
self.frameless = frameless;
}
fn visit_statements(&mut self, statements: &mut oxc_allocator::Vec<'a, Statement<'a>>) {
let mut index = 0;
while index < statements.len() {
let declaration = match &statements[index] {
Statement::VariableDeclaration(declaration) => Some(&**declaration),
Statement::ExportNamedDeclaration(export) => match &export.declaration {
Some(Declaration::VariableDeclaration(declaration)) => Some(&**declaration),
_ => None,
},
_ => None,
};
let aliasing = std::mem::replace(
&mut self.aliasing,
declaration.is_some_and(|declaration| declaration.kind.is_const()),
);
self.visit_statement(&mut statements[index]);
self.aliasing = aliasing;
for recorded in std::mem::take(&mut self.after_declaration) {
index += 1;
statements.insert(
index,
self.ast.statement_expression(Span::default(), recorded),
);
}
index += 1;
}
}
fn visit_variable_declarator(&mut self, declarator: &mut VariableDeclarator<'a>) {
let aliasing = std::mem::take(&mut self.aliasing);
if aliasing
&& self.frameless == 0
&& let BindingPattern::BindingIdentifier(name) = &declarator.id
&& let Some(initializer) = &mut declarator.init
{
let mut tree = &mut *initializer;
while let Expression::ParenthesizedExpression(parenthesized) = tree {
tree = &mut parenthesized.expression;
}
if matches!(tree, Expression::LogicalExpression(_))
&& self.logical_targets.contains_key(&span_key(tree.span()))
{
let site = self.name_tree(tree);
let recorded = self.record_tree(&site, self.identifier(&name.name));
self.after_declaration.push(recorded);
self.aliasing = aliasing;
return;
}
}
walk_mut::walk_variable_declarator(self, declarator);
self.aliasing = aliasing;
}
fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
self.frameless += 1;
walk_mut::walk_formal_parameters(self, parameters);
self.frameless -= 1;
}
fn visit_property_definition(&mut self, property: &mut oxc_ast::ast::PropertyDefinition<'a>) {
self.frameless += 1;
walk_mut::walk_property_definition(self, property);
self.frameless -= 1;
}
fn visit_accessor_property(&mut self, property: &mut oxc_ast::ast::AccessorProperty<'a>) {
self.frameless += 1;
walk_mut::walk_accessor_property(self, property);
self.frameless -= 1;
}
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_expression(&mut self, expression: &mut Expression<'a>) {
let key = span_key(expression.span());
if matches!(expression, Expression::LogicalExpression(_))
&& self.logical_targets.contains_key(&key)
{
if self.frameless > 0 {
self.instrument_tree(expression, &[]);
} else {
self.instrument_value_tree(expression);
}
return;
}
walk_mut::walk_expression(self, expression);
if let Some(first) = self.assignment_targets.remove(&key) {
let original = expression.take_in(self.ast.allocator);
let Expression::AssignmentExpression(assignment) = original else {
panic!("logical-assignment target must remain an assignment expression");
};
*expression = self.instrument_assignment(assignment, first);
}
}
}
struct SwitchTransformer<'a, 's> {
ast: AstBuilder<'a>,
coverage_hit: String,
targets: HashMap<SpanKey, SwitchTarget>,
names: CandidateNames<'s>,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> SwitchTransformer<'a, '_> {
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
AssignmentTarget::from(
self.ast
.simple_assignment_target_assignment_target_identifier(
Span::default(),
self.ast.ident(name),
),
)
}
fn probe(&self, id: &str) -> Statement<'a> {
self.ast.statement_expression(
Span::default(),
self.ast.expression_call(
Span::default(),
self.identifier(&self.coverage_hit),
NONE,
self.ast
.vec1(Argument::from(self.ast.expression_string_literal(
Span::default(),
self.ast.str(id),
None,
))),
false,
),
)
}
fn target(statement: &Statement<'a>) -> Option<SpanKey> {
match statement {
Statement::SwitchStatement(node) => Some(span_key(node.span)),
Statement::LabeledStatement(node) => Self::target(&node.body),
_ => None,
}
}
fn inner_switch<'b>(statement: &'b mut Statement<'a>) -> Option<&'b mut SwitchStatement<'a>> {
match statement {
Statement::SwitchStatement(node) => Some(node),
Statement::LabeledStatement(node) => Self::inner_switch(&mut node.body),
_ => None,
}
}
fn entered_assignment(&self, entered: &str) -> Statement<'a> {
self.ast.statement_expression(
Span::default(),
self.ast.expression_assignment(
Span::default(),
AssignmentOperator::Assign,
self.assignment_target(entered),
self.ast.expression_boolean_literal(Span::default(), true),
),
)
}
fn instrument(&mut self, statement: &mut Statement<'a>, target: &SwitchTarget) {
let entered = self.names.allocate("_supercovSwitchEntered");
let node = Self::inner_switch(statement).expect("switch target must remain a switch");
for (index, case) in node.cases.iter_mut().enumerate() {
let probe = self.probe(
target
.case_ids
.get(index)
.expect("switch case target count must remain stable"),
);
let selected = self.ast.statement_if(
Span::default(),
self.ast.expression_unary(
Span::default(),
UnaryOperator::LogicalNot,
self.identifier(&entered),
),
probe,
None,
);
case.consequent.insert(0, self.entered_assignment(&entered));
case.consequent.insert(0, selected);
}
let declaration =
Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
Span::default(),
VariableDeclarationKind::Let,
self.ast.vec1(self.ast.variable_declarator(
Span::default(),
VariableDeclarationKind::Let,
self.ast.binding_pattern_binding_identifier(
Span::default(),
self.ast.ident(&entered),
),
NONE,
Some(self.ast.expression_boolean_literal(Span::default(), false)),
false,
)),
false,
));
let original = statement.take_in(self.ast.allocator);
let mut body = self.ast.vec_from_array([declaration, original]);
if let Some(no_match_id) = &target.no_match_id {
body.push(self.ast.statement_if(
Span::default(),
self.ast.expression_unary(
Span::default(),
UnaryOperator::LogicalNot,
self.identifier(&entered),
),
self.probe(no_match_id),
None,
));
}
*statement = self.ast.statement_block(Span::default(), body);
}
}
impl<'a> VisitMut<'a> for SwitchTransformer<'a, '_> {
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_statement(&mut self, statement: &mut Statement<'a>) {
let Some(key) = Self::target(statement) else {
walk_mut::walk_statement(self, statement);
return;
};
let Some(target) = self.targets.remove(&key) else {
walk_mut::walk_statement(self, statement);
return;
};
self.instrument(statement, &target);
walk_mut::walk_statement(self, statement);
}
}
struct RouteRequestPhaseTransformer<'a, 's> {
ast: AstBuilder<'a>,
file: &'s str,
with_request_phase: String,
used: bool,
names: CandidateNames<'s>,
}
impl<'a> RouteRequestPhaseTransformer<'a, '_> {
fn is_remix_route(&self) -> bool {
self.file.starts_with("app/routes/")
}
fn is_next_route(&self) -> bool {
let path = Path::new(self.file);
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
let is_route_module = [
"route.js",
"route.jsx",
"route.ts",
"route.tsx",
"route.mjs",
"route.mts",
"route.cjs",
"route.cts",
]
.contains(&name);
if !is_route_module {
return false;
}
let components = path
.components()
.filter_map(|component| component.as_os_str().to_str())
.collect::<Vec<_>>();
components
.windows(2)
.any(|window| window[0] == "app" && window[1] != name)
}
fn is_handler_name(&self, name: &str) -> bool {
(self.is_remix_route() && matches!(name, "loader" | "action"))
|| (self.is_next_route()
&& matches!(
name,
"GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
))
}
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn wrap_expression(&self, expression: Expression<'a>) -> Expression<'a> {
self.ast.expression_call(
Span::default(),
self.identifier(&self.with_request_phase),
NONE,
self.ast.vec1(Argument::from(expression)),
false,
)
}
fn wrapped_named_export(&self, exported_name: &str, original_name: &str) -> Statement<'a> {
Statement::ExportNamedDeclaration(self.ast.alloc_export_named_declaration(
Span::default(),
Some(self.ast.declaration_variable(
Span::default(),
VariableDeclarationKind::Const,
self.ast.vec1(self.ast.variable_declarator(
Span::default(),
VariableDeclarationKind::Const,
self.ast.binding_pattern_binding_identifier(
Span::default(),
self.ast.ident(exported_name),
),
NONE,
Some(self.wrap_expression(self.identifier(original_name))),
false,
)),
false,
)),
self.ast.vec(),
None,
ImportOrExportKind::Value,
NONE,
))
}
fn transform_named_export(
&mut self,
mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportNamedDeclaration<'a>>,
output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
) {
if let Some(Declaration::VariableDeclaration(declaration)) = &mut export.declaration {
for declarator in &mut declaration.declarations {
let Some(name) = binding_identifier_name(&declarator.id) else {
continue;
};
if !self.is_handler_name(&name) {
continue;
}
if let Some(initializer) = &mut declarator.init {
let original = initializer.take_in(self.ast.allocator);
*initializer = self.wrap_expression(original);
self.used = true;
}
}
output.push(Statement::ExportNamedDeclaration(export));
return;
}
if matches!(
export.declaration,
Some(Declaration::FunctionDeclaration(_))
) {
let Some(Declaration::FunctionDeclaration(mut function)) = export.declaration.take()
else {
unreachable!();
};
let Some(exported_name) = function.id.as_ref().map(|id| id.name.to_string()) else {
output.push(Statement::FunctionDeclaration(function));
return;
};
if !self.is_handler_name(&exported_name) {
export.declaration = Some(Declaration::FunctionDeclaration(function));
output.push(Statement::ExportNamedDeclaration(export));
return;
}
let original_name = self
.names
.allocate(&format!("__supercov{exported_name}CoverageOriginal"));
function.id = Some(
self.ast
.binding_identifier(Span::default(), self.ast.ident(&original_name)),
);
output.push(Statement::FunctionDeclaration(function));
output.push(self.wrapped_named_export(&exported_name, &original_name));
self.used = true;
return;
}
if export.source.is_none() {
output.push(Statement::ExportNamedDeclaration(export));
return;
}
let source = export
.source
.as_ref()
.expect("checked above")
.clone_in(self.ast.allocator);
let specifiers = export.specifiers.take_in(self.ast.allocator);
let mut untouched = self.ast.vec();
let mut handlers = Vec::new();
for specifier in specifiers {
let exported_name = specifier
.exported
.identifier_name()
.map(|name| name.to_string());
if exported_name
.as_deref()
.is_some_and(|name| self.is_handler_name(name))
{
handlers.push((exported_name.expect("checked above"), specifier));
} else {
untouched.push(specifier);
}
}
if handlers.is_empty() {
export.specifiers = untouched;
output.push(Statement::ExportNamedDeclaration(export));
return;
}
if !untouched.is_empty() {
output.push(Statement::ExportNamedDeclaration(
self.ast.alloc_export_named_declaration(
export.span,
None,
untouched,
Some(source.clone_in(self.ast.allocator)),
export.export_kind,
export.with_clause.take(),
),
));
}
for (exported_name, specifier) in handlers {
let original_name = self
.names
.allocate(&format!("__supercov{exported_name}CoverageOriginal"));
output.push(Statement::ImportDeclaration(
self.ast.alloc_import_declaration(
Span::default(),
Some(self.ast.vec1(
self.ast.import_declaration_specifier_import_specifier(
Span::default(),
specifier.local,
self.ast.binding_identifier(
Span::default(),
self.ast.ident(&original_name),
),
ImportOrExportKind::Value,
),
)),
source.clone_in(self.ast.allocator),
None,
NONE,
ImportOrExportKind::Value,
),
));
output.push(self.wrapped_named_export(&exported_name, &original_name));
}
self.used = true;
}
fn transform_default_export(
&mut self,
mut export: oxc_allocator::Box<'a, oxc_ast::ast::ExportDefaultDeclaration<'a>>,
output: &mut oxc_allocator::Vec<'a, Statement<'a>>,
) {
if let ExportDefaultDeclarationKind::FunctionDeclaration(mut function) =
export.declaration.take_in(self.ast.allocator)
{
let original_name = self
.names
.allocate("__supercovHandleRequestCoverageOriginal");
function.id = Some(
self.ast
.binding_identifier(Span::default(), self.ast.ident(&original_name)),
);
output.push(Statement::FunctionDeclaration(function));
output.push(Statement::ExportDefaultDeclaration(
self.ast.alloc_export_default_declaration(
Span::default(),
ExportDefaultDeclarationKind::from(
self.wrap_expression(self.identifier(&original_name)),
),
),
));
self.used = true;
return;
}
if export.declaration.is_expression() {
let original = export
.declaration
.take_in(self.ast.allocator)
.into_expression();
export.declaration = ExportDefaultDeclarationKind::from(self.wrap_expression(original));
self.used = true;
}
output.push(Statement::ExportDefaultDeclaration(export));
}
fn transform_program(&mut self, program: &mut Program<'a>) {
let route_module = self.is_remix_route() || self.is_next_route();
let server_entry = self.file.starts_with("app/entry.server.")
&& ["js", "jsx", "ts", "tsx", "mjs", "mts", "cjs", "cts"].contains(
&Path::new(self.file)
.extension()
.and_then(|value| value.to_str())
.unwrap_or(""),
);
if !route_module && !server_entry {
return;
}
let original = program.body.take_in(self.ast.allocator);
let mut output = self.ast.vec_with_capacity(original.len() + 4);
for statement in original {
match statement {
Statement::ExportNamedDeclaration(export) if route_module => {
self.transform_named_export(export, &mut output);
}
Statement::ExportDefaultDeclaration(export) if server_entry => {
self.transform_default_export(export, &mut output);
}
statement => output.push(statement),
}
}
program.body = output;
}
}
struct RequestPhaseTransformer<'a> {
ast: AstBuilder<'a>,
with_request_phase: String,
used: bool,
source_sensitive_functions: HashSet<SpanKey>,
}
impl<'a> RequestPhaseTransformer<'a> {
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn callee_is(callee: &Expression<'a>, name: &str) -> bool {
match callee {
Expression::Identifier(identifier) => identifier.name == name,
Expression::StaticMemberExpression(member) => member.property.name == name,
_ => false,
}
}
fn callback_candidate(argument: &Argument<'a>) -> bool {
matches!(
argument,
Argument::FunctionExpression(_)
| Argument::ArrowFunctionExpression(_)
| Argument::Identifier(_)
| Argument::ComputedMemberExpression(_)
| Argument::StaticMemberExpression(_)
| Argument::PrivateFieldExpression(_)
)
}
fn already_wrapped(&self, argument: &Argument<'a>) -> bool {
matches!(
argument,
Argument::CallExpression(call)
if expression_is_identifier(&call.callee, &self.with_request_phase)
)
}
fn wrap_argument(&mut self, argument: &mut Argument<'a>, event: Option<&str>) {
if self.already_wrapped(argument) || !argument.is_expression() {
return;
}
let expression = argument.to_expression_mut();
let original = expression.take_in(self.ast.allocator);
*expression = self.ast.expression_call(
Span::default(),
self.identifier(&self.with_request_phase),
NONE,
self.ast
.vec_from_iter(std::iter::once(Argument::from(original)).chain(event.map(
|name| {
Argument::from(self.ast.expression_string_literal(
Span::default(),
self.ast.str(name),
None,
))
},
))),
false,
);
self.used = true;
}
}
impl<'a> VisitMut<'a> for RequestPhaseTransformer<'a> {
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk_mut::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_call_expression(&mut self, call: &mut CallExpression<'a>) {
walk_mut::walk_call_expression(self, call);
let property = match &call.callee {
Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
_ => None,
};
let mut callback_index = None;
if matches!(property, Some("on" | "once" | "addListener"))
&& matches!(
call.arguments.first(),
Some(Argument::StringLiteral(event))
if matches!(event.value.as_str(), "request" | "upgrade" | "connection")
)
{
callback_index = Some(1);
} else if Self::callee_is(&call.callee, "createServer") {
callback_index = call.arguments.iter().rposition(Self::callback_candidate);
}
let connection = matches!(call.arguments.first(), Some(Argument::StringLiteral(event)) if event.value == "connection");
if let Some(index) = callback_index
&& let Some(argument) = call.arguments.get_mut(index)
{
self.wrap_argument(argument, connection.then_some("connection"));
}
}
}
struct CandidateNames<'s> {
source: &'s str,
underscored: HashSet<&'s str>,
allocated: HashSet<String>,
suffix: usize,
namespace: String,
}
impl<'s> CandidateNames<'s> {
fn new(source: &'s str) -> Self {
Self::within(source, "")
}
fn within(source: &'s str, namespace: &str) -> Self {
let identifier = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
let underscored = source
.split(|c: char| !identifier(c))
.filter(|word| word.starts_with('_'))
.collect();
Self {
source,
underscored,
allocated: HashSet::new(),
suffix: 0,
namespace: namespace.to_owned(),
}
}
fn taken(&self, candidate: &str) -> bool {
self.allocated.contains(candidate)
|| if candidate.starts_with('_') {
self.underscored.contains(candidate)
} else {
self.source.contains(candidate)
}
}
fn allocate(&mut self, base: &str) -> String {
loop {
let candidate = if self.suffix == 0 {
format!("{base}{}", self.namespace)
} else {
format!("{base}{}{}", self.suffix, self.namespace)
};
self.suffix += 1;
if !self.taken(&candidate) {
self.allocated.insert(candidate.clone());
return candidate;
}
}
}
}
struct ControlProbeV2Transformer<'a, 's> {
ast: AstBuilder<'a>,
decisions: &'s [CandidateDecision],
mcdc_begin: String,
mcdc_condition: String,
mcdc_end: String,
mcdc_end_v2: String,
probe_file_v2: String,
names: CandidateNames<'s>,
scope_declarations: Vec<Vec<String>>,
decision_index: usize,
parameter_depth: usize,
source_sensitive_functions: HashSet<SpanKey>,
after_statement: Option<(Option<Statement<'a>>, Statement<'a>)>,
label_body: bool,
inline_names: Option<Vec<String>>,
}
#[derive(Clone, Copy)]
struct DecisionPlan {
index: usize,
condition_count: usize,
inline_frame: bool,
}
struct AppliedDecision {
index: usize,
frame: String,
values: Vec<String>,
inline: bool,
}
impl<'a> ControlProbeV2Transformer<'a, '_> {
fn enter_declaration_scope(&mut self) {
self.scope_declarations.push(Vec::new());
}
fn leave_declaration_scope(&mut self) -> Vec<String> {
self.scope_declarations
.pop()
.expect("program/function scope stack must remain balanced")
}
fn allocate_scratch(&mut self, base: &str) -> String {
let name = self.names.allocate(base);
self.scope_declarations
.last_mut()
.expect("a control decision must be inside a program or function body")
.push(name.clone());
name
}
fn scratch_for(&mut self, base: &str, inline: bool) -> String {
if inline {
self.names.allocate(base)
} else {
self.allocate_scratch(base)
}
}
fn wrap_inline_frame(&self, expression: Expression<'a>, names: &[String]) -> Expression<'a> {
let declarations = self.ast.vec_from_iter(names.iter().map(|name| {
self.ast.variable_declarator(
Span::default(),
VariableDeclarationKind::Let,
self.ast
.binding_pattern_binding_identifier(Span::default(), self.ast.ident(name)),
NONE,
None,
false,
)
}));
let body = self.ast.alloc_function_body(
Span::default(),
self.ast.vec(),
self.ast.vec_from_array([
Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
Span::default(),
VariableDeclarationKind::Let,
declarations,
false,
)),
self.ast.statement_return(Span::default(), Some(expression)),
]),
);
let params = self.ast.alloc_formal_parameters(
Span::default(),
FormalParameterKind::ArrowFormalParameters,
self.ast.vec(),
NONE,
);
let arrow = self.ast.expression_arrow_function(
Span::default(),
false,
false,
NONE,
params,
NONE,
body,
);
self.ast
.expression_call(Span::default(), arrow, NONE, self.ast.vec(), false)
}
fn prepend_declarations(
&self,
names: Vec<String>,
statements: &mut oxc_allocator::Vec<'a, Statement<'a>>,
) {
if names.is_empty() {
return;
}
let declarators = self.ast.vec_from_iter(names.into_iter().map(|name| {
self.ast.variable_declarator(
Span::default(),
VariableDeclarationKind::Let,
self.ast
.binding_pattern_binding_identifier(Span::default(), self.ast.ident(&name)),
NONE,
None,
false,
)
}));
statements.insert(
0,
Statement::VariableDeclaration(self.ast.alloc_variable_declaration(
Span::default(),
VariableDeclarationKind::Let,
declarators,
false,
)),
);
}
fn identifier(&self, name: &str) -> Expression<'a> {
self.ast
.expression_identifier(Span::default(), self.ast.ident(name))
}
fn assignment_target(&self, name: &str) -> AssignmentTarget<'a> {
let target = self
.ast
.simple_assignment_target_assignment_target_identifier(
Span::default(),
self.ast.ident(name),
);
AssignmentTarget::from(target)
}
fn number(&self, value: u64) -> Expression<'a> {
self.ast.expression_numeric_literal(
Span::default(),
value as f64,
None,
NumberBase::Decimal,
)
}
fn string(&self, value: &str) -> Expression<'a> {
self.ast
.expression_string_literal(Span::default(), self.ast.str(value), None)
}
fn object_property(&self, name: &str, value: Expression<'a>) -> ObjectPropertyKind<'a> {
self.ast.object_property_kind_object_property(
Span::default(),
PropertyKind::Init,
self.ast
.property_key_static_identifier(Span::default(), self.ast.ident(name)),
value,
false,
false,
false,
)
}
fn decision_meta(&self, decision: &CandidateDecision) -> Expression<'a> {
let conditions = self.ast.expression_array(
Span::default(),
self.ast.vec_from_iter(
decision
.conditions
.iter()
.map(|condition| ArrayExpressionElement::from(self.string(condition))),
),
);
let properties = self.ast.vec_from_array([
self.object_property("id", self.string(&decision.id)),
self.object_property("file", self.string(&decision.file)),
self.object_property("line", self.number(decision.line as u64)),
self.object_property("column", self.number(decision.column as u64)),
self.object_property("source", self.string(&decision.source)),
self.object_property("conditions", conditions),
self.object_property("kind", self.string(&decision.kind)),
]);
Expression::ObjectExpression(
self.ast
.alloc_object_expression(Span::default(), properties),
)
}
fn assign(&self, name: &str, value: Expression<'a>) -> Expression<'a> {
self.ast.expression_assignment(
Span::default(),
AssignmentOperator::Assign,
self.assignment_target(name),
value,
)
}
fn instrument_condition(
&self,
expression: Expression<'a>,
applied: &AppliedDecision,
index: usize,
) -> Expression<'a> {
let mut steps = self.ast.vec();
if index == 0 {
steps.push(self.assign(&applied.frame, self.number(1)));
for name in &applied.values[1..] {
steps.push(self.assign(name, self.number(0)));
}
} else {
steps.push(self.ast.expression_assignment(
Span::default(),
AssignmentOperator::Addition,
self.assignment_target(&applied.frame),
self.number(3_u64.pow(index as u32)),
));
}
steps.push(self.assign(&applied.values[index], expression));
self.ast.expression_sequence(Span::default(), steps)
}
fn instrument_conditions(
&self,
expression: &mut Expression<'a>,
applied: &AppliedDecision,
next_index: &mut usize,
) {
match expression {
Expression::ParenthesizedExpression(parenthesized) => {
self.instrument_conditions(&mut parenthesized.expression, applied, next_index)
}
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
self.instrument_conditions(&mut logical.left, applied, next_index);
self.instrument_conditions(&mut logical.right, applied, next_index);
}
Expression::UnaryExpression(unary)
if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
{
self.instrument_conditions(&mut unary.argument, applied, next_index);
}
_ => {
let index = *next_index;
*next_index += 1;
let original = expression.take_in(self.ast.allocator);
*expression = self.instrument_condition(original, applied, index);
}
}
}
fn encoded(&self, applied: &AppliedDecision) -> Expression<'a> {
let mut sum = self.identifier(&applied.frame);
for (index, name) in applied.values.iter().enumerate() {
let truth = self.ast.expression_unary(
Span::default(),
UnaryOperator::UnaryPlus,
self.ast.expression_unary(
Span::default(),
UnaryOperator::LogicalNot,
self.ast.expression_unary(
Span::default(),
UnaryOperator::LogicalNot,
self.identifier(name),
),
),
);
let term = if index == 0 {
truth
} else {
self.ast.expression_binary(
Span::default(),
self.number(3_u64.pow(index as u32)),
BinaryOperator::Multiplication,
truth,
)
};
sum = self
.ast
.expression_binary(Span::default(), sum, BinaryOperator::Addition, term);
}
sum
}
fn outcome(&self, applied: &AppliedDecision, outcome: bool) -> Expression<'a> {
self.ast.expression_call(
Span::default(),
self.identifier(&self.mcdc_end_v2),
NONE,
self.ast.vec_from_array([
Argument::from(self.identifier(&self.probe_file_v2)),
Argument::from(self.number(applied.index as u64)),
Argument::from(self.encoded(applied)),
Argument::from(
self.ast
.expression_boolean_literal(Span::default(), outcome),
),
]),
false,
)
}
fn prepend_statement(&self, body: &mut Statement<'a>, first: Statement<'a>) {
if let Statement::BlockStatement(block) = body {
block.body.insert(0, first);
return;
}
let original = body.take_in(self.ast.allocator);
*body = self
.ast
.statement_block(Span::default(), self.ast.vec_from_array([first, original]));
}
fn settled(&self, applied: &AppliedDecision) -> Expression<'a> {
self.ast.expression_sequence(
Span::default(),
self.ast.vec_from_array([
self.outcome(applied, true),
self.assign(&applied.frame, self.number(0)),
]),
)
}
fn when_unsettled(&self, applied: &AppliedDecision, record: Expression<'a>) -> Statement<'a> {
self.ast.statement_expression(
Span::default(),
self.ast.expression_logical(
Span::default(),
self.identifier(&applied.frame),
LogicalOperator::And,
record,
),
)
}
fn record_false_after(&mut self, applied: &AppliedDecision, before: Option<Statement<'a>>) {
let after = self.when_unsettled(applied, self.outcome(applied, false));
self.after_statement = Some((before, after));
}
fn loop_outcomes(
&mut self,
body: &mut Statement<'a>,
applied: &AppliedDecision,
tested_after: bool,
) {
if !tested_after {
let entered = self
.ast
.statement_expression(Span::default(), self.settled(applied));
self.prepend_statement(body, entered);
self.record_false_after(applied, None);
return;
}
let entered = self.when_unsettled(applied, self.settled(applied));
self.prepend_statement(body, entered);
let before = self
.ast
.statement_expression(Span::default(), self.assign(&applied.frame, self.number(0)));
self.record_false_after(applied, Some(before));
}
fn instrument_condition_v1(
&self,
expression: Expression<'a>,
frame_name: &str,
index: usize,
) -> Expression<'a> {
self.ast.expression_call(
Span::default(),
self.identifier(&self.mcdc_condition),
NONE,
self.ast.vec_from_array([
Argument::from(self.identifier(frame_name)),
Argument::from(self.number(index as u64)),
Argument::from(expression),
]),
false,
)
}
fn instrument_conditions_v1(
&self,
expression: &mut Expression<'a>,
frame_name: &str,
next_index: &mut usize,
) {
match expression {
Expression::ParenthesizedExpression(parenthesized) => {
self.instrument_conditions_v1(&mut parenthesized.expression, frame_name, next_index)
}
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
self.instrument_conditions_v1(&mut logical.left, frame_name, next_index);
self.instrument_conditions_v1(&mut logical.right, frame_name, next_index);
}
Expression::UnaryExpression(unary)
if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
{
self.instrument_conditions_v1(&mut unary.argument, frame_name, next_index);
}
_ => {
let index = *next_index;
*next_index += 1;
let original = expression.take_in(self.ast.allocator);
*expression = self.instrument_condition_v1(original, frame_name, index);
}
}
}
fn reserve_decision(&mut self, test: &Expression<'a>) -> DecisionPlan {
let mut condition_spans = Vec::new();
collect_conditions(test, &mut condition_spans);
let plan = DecisionPlan {
index: self.decision_index,
condition_count: condition_spans.len(),
inline_frame: self.parameter_depth > 0,
};
self.decision_index += 1;
plan
}
fn apply_decision(
&mut self,
test: &mut Expression<'a>,
plan: DecisionPlan,
) -> Option<AppliedDecision> {
if plan.condition_count > 32 {
let frame_name = self.scratch_for("_supercovMcdcFrame", plan.inline_frame);
let mut next_index = 0;
self.instrument_conditions_v1(test, &frame_name, &mut next_index);
debug_assert_eq!(next_index, plan.condition_count);
let decision = &self.decisions[plan.index];
let begin = self.ast.expression_call(
Span::default(),
self.identifier(&self.mcdc_begin),
NONE,
self.ast.vec_from_array([
Argument::from(self.string(&decision.id)),
Argument::from(self.decision_meta(decision)),
]),
false,
);
let assign_frame = self.ast.expression_assignment(
Span::default(),
AssignmentOperator::Assign,
self.assignment_target(&frame_name),
begin,
);
let instrumented = test.take_in(self.ast.allocator);
let end = self.ast.expression_call(
Span::default(),
self.identifier(&self.mcdc_end),
NONE,
self.ast.vec_from_array([
Argument::from(self.identifier(&frame_name)),
Argument::from(instrumented),
]),
false,
);
let observed = self.ast.expression_sequence(
Span::default(),
self.ast.vec_from_array([assign_frame, end]),
);
*test = if plan.inline_frame {
self.wrap_inline_frame(observed, &[frame_name])
} else {
observed
};
return None;
}
let applied = AppliedDecision {
index: plan.index,
frame: self.scratch_for("_supercovMcdcFrame", plan.inline_frame),
values: (0..plan.condition_count)
.map(|_| self.scratch_for("_supercovMcdcValue", plan.inline_frame))
.collect(),
inline: plan.inline_frame,
};
let mut next_index = 0;
self.instrument_conditions(test, &applied, &mut next_index);
debug_assert_eq!(next_index, plan.condition_count);
Some(applied)
}
}
impl<'a> VisitMut<'a> for ControlProbeV2Transformer<'a, '_> {
fn visit_program(&mut self, program: &mut Program<'a>) {
self.enter_declaration_scope();
walk_mut::walk_program(self, program);
let declarations = self.leave_declaration_scope();
self.prepend_declarations(declarations, &mut program.body);
}
fn visit_function_body(&mut self, body: &mut FunctionBody<'a>) {
self.enter_declaration_scope();
walk_mut::walk_function_body(self, body);
let declarations = self.leave_declaration_scope();
self.prepend_declarations(declarations, &mut body.statements);
}
fn visit_function(&mut self, function: &mut Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
let outer_parameter_depth = self.parameter_depth;
self.parameter_depth = 0;
walk_mut::walk_function(self, function, flags);
self.parameter_depth = outer_parameter_depth;
}
fn visit_arrow_function_expression(&mut self, function: &mut ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
let outer_parameter_depth = self.parameter_depth;
self.parameter_depth = 0;
walk_mut::walk_arrow_function_expression(self, function);
self.parameter_depth = outer_parameter_depth;
}
fn visit_formal_parameters(&mut self, parameters: &mut FormalParameters<'a>) {
self.parameter_depth += 1;
walk_mut::walk_formal_parameters(self, parameters);
self.parameter_depth -= 1;
}
fn visit_with_statement(&mut self, _statement: &mut WithStatement<'a>) {}
fn visit_statement(&mut self, statement: &mut Statement<'a>) {
let label_body = self.label_body;
self.label_body = matches!(statement, Statement::LabeledStatement(_));
walk_mut::walk_statement(self, statement);
self.label_body = false;
if label_body {
return;
}
if let Some((before, after)) = self.after_statement.take() {
let decided = statement.take_in(self.ast.allocator);
let mut recorded = self.ast.vec_with_capacity(3);
recorded.extend(before);
recorded.push(decided);
recorded.push(after);
*statement = self.ast.statement_block(Span::default(), recorded);
}
}
fn visit_static_block(&mut self, block: &mut oxc_ast::ast::StaticBlock<'a>) {
let outer_parameter_depth = self.parameter_depth;
self.parameter_depth = 0;
self.enter_declaration_scope();
walk_mut::walk_static_block(self, block);
let declarations = self.leave_declaration_scope();
self.prepend_declarations(declarations, &mut block.body);
self.parameter_depth = outer_parameter_depth;
}
fn visit_expression(&mut self, expression: &mut Expression<'a>) {
walk_mut::walk_expression(self, expression);
if let Some(names) = self.inline_names.take()
&& matches!(expression, Expression::ConditionalExpression(_))
{
let conditional = expression.take_in(self.ast.allocator);
*expression = self.wrap_inline_frame(conditional, &names);
}
}
fn visit_if_statement(&mut self, statement: &mut IfStatement<'a>) {
let plan = decision_outcome_is_variable(&statement.test)
.then(|| self.reserve_decision(&statement.test));
self.visit_expression(&mut statement.test);
let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
self.visit_statement(&mut statement.consequent);
if let Some(alternate) = &mut statement.alternate {
self.visit_statement(alternate);
}
let Some(applied) = applied else {
return;
};
let Some(alternate) = &mut statement.alternate else {
let taken = self
.ast
.statement_expression(Span::default(), self.settled(&applied));
self.prepend_statement(&mut statement.consequent, taken);
self.record_false_after(&applied, None);
return;
};
let skipped = self
.ast
.statement_expression(Span::default(), self.outcome(&applied, false));
self.prepend_statement(alternate, skipped);
let taken = self
.ast
.statement_expression(Span::default(), self.outcome(&applied, true));
self.prepend_statement(&mut statement.consequent, taken);
}
fn visit_conditional_expression(&mut self, expression: &mut ConditionalExpression<'a>) {
let plan = decision_outcome_is_variable(&expression.test)
.then(|| self.reserve_decision(&expression.test));
self.visit_expression(&mut expression.test);
let applied = plan.and_then(|plan| self.apply_decision(&mut expression.test, plan));
self.visit_expression(&mut expression.consequent);
self.visit_expression(&mut expression.alternate);
let Some(applied) = applied else {
return;
};
for (branch, outcome) in [
(&mut expression.consequent, true),
(&mut expression.alternate, false),
] {
let value = branch.take_in(self.ast.allocator);
let start = value.span().start;
let record = self.ast.expression_unary(
Span::new(start, start + 1),
UnaryOperator::Void,
self.outcome(&applied, outcome),
);
*branch = self
.ast
.expression_sequence(Span::default(), self.ast.vec_from_array([record, value]));
}
if applied.inline {
let mut names = vec![applied.frame];
names.extend(applied.values);
self.inline_names = Some(names);
}
}
fn visit_while_statement(&mut self, statement: &mut WhileStatement<'a>) {
let plan = decision_outcome_is_variable(&statement.test)
.then(|| self.reserve_decision(&statement.test));
self.visit_expression(&mut statement.test);
let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
self.visit_statement(&mut statement.body);
if let Some(applied) = applied {
self.loop_outcomes(&mut statement.body, &applied, false);
}
}
fn visit_do_while_statement(&mut self, statement: &mut DoWhileStatement<'a>) {
let plan = decision_outcome_is_variable(&statement.test)
.then(|| self.reserve_decision(&statement.test));
self.visit_statement(&mut statement.body);
self.visit_expression(&mut statement.test);
let applied = plan.and_then(|plan| self.apply_decision(&mut statement.test, plan));
if let Some(applied) = applied {
self.loop_outcomes(&mut statement.body, &applied, true);
}
}
fn visit_for_statement(&mut self, statement: &mut ForStatement<'a>) {
let plan = statement
.test
.as_ref()
.filter(|test| decision_outcome_is_variable(test))
.map(|test| self.reserve_decision(test));
if let Some(init) = &mut statement.init {
self.visit_for_statement_init(init);
}
let mut applied = None;
if let (Some(test), Some(plan)) = (&mut statement.test, plan) {
self.visit_expression(test);
applied = self.apply_decision(test, plan);
}
if let Some(update) = &mut statement.update {
self.visit_expression(update);
}
self.visit_statement(&mut statement.body);
if let Some(applied) = applied {
self.loop_outcomes(&mut statement.body, &applied, false);
}
}
}
struct DecisionCollector<'s> {
source: &'s str,
file: &'s str,
decisions: Vec<CandidateDecision>,
decision_vector_counts: Vec<usize>,
decision_logical_nodes: HashSet<SpanKey>,
source_sensitive_functions: &'s HashSet<SpanKey>,
}
impl DecisionCollector<'_> {
fn record_decision(&mut self, test: &Expression<'_>, kind: &str) {
let mut condition_spans = Vec::new();
collect_conditions(test, &mut condition_spans);
collect_decision_logical_nodes(test, &mut self.decision_logical_nodes);
let span = transparent_expression(test).span();
let (line, column) = line_and_utf16_column(self.source, span.start as usize);
self.decisions.push(CandidateDecision {
id: stable_id(self.source, self.file, "decision", span, kind),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, span).to_string(),
conditions: condition_spans
.iter()
.map(|condition| source_slice(self.source, *condition).to_string())
.collect(),
kind: kind.to_string(),
});
self.decision_vector_counts.push(
if condition_spans.len() <= 6 && decision_conditions_are_transparent(test) {
reachable_vector_count(test, &condition_spans)
} else {
0
},
);
}
}
fn decision_outcome_is_variable(expression: &Expression<'_>) -> bool {
syntactic_boolean_outcome(expression).is_none()
}
fn syntactic_boolean_outcome(expression: &Expression<'_>) -> Option<bool> {
match transparent_expression(expression) {
Expression::BooleanLiteral(literal) => Some(literal.value),
Expression::UnaryExpression(unary) if unary.operator.is_not() => {
syntactic_boolean_outcome(&unary.argument).map(|value| !value)
}
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
let left = syntactic_boolean_outcome(&logical.left);
let right = syntactic_boolean_outcome(&logical.right);
match logical.operator {
LogicalOperator::And => match (left, right) {
(Some(false), _) | (_, Some(false)) => Some(false),
(Some(true), value) | (value, Some(true)) => value,
_ => None,
},
LogicalOperator::Or => match (left, right) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), value) | (value, Some(false)) => value,
_ => None,
},
LogicalOperator::Coalesce => None,
}
}
_ => None,
}
}
#[derive(Default)]
struct CoverageSurfaceScanner {
found: bool,
}
impl<'a> Visit<'a> for CoverageSurfaceScanner {
fn visit_logical_expression(&mut self, _expression: &LogicalExpression<'a>) {
self.found = true;
}
fn visit_conditional_expression(&mut self, _expression: &ConditionalExpression<'a>) {
self.found = true;
}
fn visit_chain_expression(&mut self, _expression: &ChainExpression<'a>) {
self.found = true;
}
fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {
self.found = true;
}
fn visit_arrow_function_expression(&mut self, _function: &ArrowFunctionExpression<'a>) {
self.found = true;
}
fn visit_class(&mut self, _class: &Class<'a>) {
self.found = true;
}
fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'a>) {
if expression.operator.is_logical() {
self.found = true;
} else {
walk::walk_assignment_expression(self, expression);
}
}
}
fn decision_conditions_are_transparent(expression: &Expression<'_>) -> bool {
fn visit_condition(expression: &Expression<'_>) -> bool {
let mut scanner = CoverageSurfaceScanner::default();
scanner.visit_expression(expression);
!scanner.found
}
match transparent_expression(expression) {
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
decision_conditions_are_transparent(&logical.left)
&& decision_conditions_are_transparent(&logical.right)
}
Expression::UnaryExpression(unary)
if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
{
decision_conditions_are_transparent(&unary.argument)
}
condition => visit_condition(condition),
}
}
fn reachable_vector_count(expression: &Expression<'_>, conditions: &[Span]) -> usize {
fn evaluate(
expression: &Expression<'_>,
assignment: usize,
encoded: &mut usize,
indices: &HashMap<SpanKey, usize>,
) -> bool {
match transparent_expression(expression) {
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
let left = evaluate(&logical.left, assignment, encoded, indices);
if logical.operator == LogicalOperator::And {
left && evaluate(&logical.right, assignment, encoded, indices)
} else {
left || evaluate(&logical.right, assignment, encoded, indices)
}
}
Expression::UnaryExpression(unary)
if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
{
!evaluate(&unary.argument, assignment, encoded, indices)
}
condition => {
let index = indices
.get(&span_key(condition.span()))
.expect("decision condition index must remain stable");
let value = assignment & (1 << index) != 0;
*encoded += (if value { 2 } else { 1 }) * 3_usize.pow(*index as u32);
value
}
}
}
let indices = conditions
.iter()
.enumerate()
.map(|(index, span)| (span_key(*span), index))
.collect::<HashMap<_, _>>();
let mut vectors = HashSet::new();
for assignment in 0..(1_usize << conditions.len()) {
let mut encoded = 0;
let outcome = evaluate(expression, assignment, &mut encoded, &indices);
vectors.insert(encoded * 2 + usize::from(outcome));
}
vectors.len()
}
fn collect_decision_logical_nodes(expression: &Expression<'_>, nodes: &mut HashSet<SpanKey>) {
match expression {
Expression::ParenthesizedExpression(parenthesized) => {
collect_decision_logical_nodes(&parenthesized.expression, nodes);
}
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
nodes.insert(span_key(logical.span));
collect_decision_logical_nodes(&logical.left, nodes);
collect_decision_logical_nodes(&logical.right, nodes);
}
Expression::UnaryExpression(unary)
if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
{
collect_decision_logical_nodes(&unary.argument, nodes);
}
_ => {}
}
}
#[derive(Default)]
struct LogicalBranchAnalysis {
branches: Vec<CandidateBranch>,
logical_targets: HashMap<SpanKey, (String, String)>,
}
#[derive(Default)]
struct LogicalAssignmentAnalysis {
branches: Vec<CandidateBranch>,
targets: HashMap<SpanKey, (String, String)>,
}
#[derive(Default)]
struct OptionalMemberAnalysis {
branches: Vec<CandidateBranch>,
targets: HashMap<SpanKey, (String, String)>,
links: HashMap<SpanKey, u8>,
after_call: HashMap<SpanKey, SpanKey>,
}
const LINK_AFTER_ANOTHER: u8 = 1;
const LINK_BEFORE_ANOTHER: u8 = 2;
const LINK_AFTER_CALL: u8 = 4;
enum LinkBelow {
None,
Member,
Call(SpanKey),
}
fn optional_link_below(object: &Expression<'_>) -> LinkBelow {
let mut current = object;
loop {
current = match current {
Expression::StaticMemberExpression(member) if member.optional => {
return LinkBelow::Member;
}
Expression::ComputedMemberExpression(member) if member.optional => {
return LinkBelow::Member;
}
Expression::PrivateFieldExpression(member) if member.optional => {
return LinkBelow::Member;
}
Expression::CallExpression(call) if call.optional => {
return LinkBelow::Call(span_key(call.span));
}
Expression::StaticMemberExpression(member) => &member.object,
Expression::ComputedMemberExpression(member) => &member.object,
Expression::PrivateFieldExpression(member) => &member.object,
Expression::CallExpression(call) => &call.callee,
Expression::TSNonNullExpression(inner) => &inner.expression,
_ => return LinkBelow::None,
};
}
}
fn optional_link_above<State>(context: &TraverseCtx<'_, State>) -> bool {
for ancestor in context.ancestors() {
match ancestor {
Ancestor::StaticMemberExpressionObject(parent) if *parent.optional() => return true,
Ancestor::ComputedMemberExpressionObject(parent) if *parent.optional() => return true,
Ancestor::PrivateFieldExpressionObject(parent) if *parent.optional() => return true,
Ancestor::StaticMemberExpressionObject(_)
| Ancestor::ComputedMemberExpressionObject(_)
| Ancestor::PrivateFieldExpressionObject(_)
| Ancestor::TSNonNullExpressionExpression(_) => {}
Ancestor::CallExpressionCallee(parent) if !*parent.optional() => {}
_ => return false,
}
}
false
}
#[derive(Default)]
struct OptionalCallAnalysis {
branches: Vec<CandidateBranch>,
sites: HashMap<SpanKey, (String, String)>,
roots: HashMap<SpanKey, Vec<SpanKey>>,
limitations: Vec<CandidateLimitation>,
}
#[derive(Clone)]
struct DefaultTarget {
default_id: String,
provided_id: String,
inferred_name: Option<String>,
}
#[derive(Default)]
struct DefaultAnalysis {
branches: Vec<CandidateBranch>,
parameter_targets: HashMap<SpanKey, DefaultTarget>,
binding_targets: HashMap<SpanKey, DefaultTarget>,
limitations: Vec<CandidateLimitation>,
}
#[derive(Clone)]
struct ExtendedTarget {
first_id: String,
second_id: String,
}
#[derive(Default)]
struct ExtendedAnalysis {
branches: Vec<CandidateBranch>,
try_targets: HashMap<SpanKey, ExtendedTarget>,
loop_targets: HashMap<SpanKey, ExtendedTarget>,
}
#[derive(Clone)]
struct SwitchTarget {
case_ids: Vec<String>,
no_match_id: Option<String>,
}
#[derive(Default)]
struct SwitchAnalysis {
branches: Vec<CandidateBranch>,
targets: HashMap<SpanKey, SwitchTarget>,
}
struct SwitchCollector<'s> {
source: &'s str,
file: &'s str,
source_sensitive_functions: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
analysis: SwitchAnalysis,
}
impl SwitchCollector<'_> {
fn unsafe_context(&self) -> bool {
self.unsafe_function_depth > 0 || self.with_depth > 0
}
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for SwitchCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_switch_statement(
&mut self,
node: &mut SwitchStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
let has_default = node.cases.iter().any(|case| case.test.is_none());
if self.unsafe_context() {
return;
}
let id = stable_id(self.source, self.file, "switch", node.span, "");
let mut case_ids = Vec::with_capacity(node.cases.len());
let mut alternatives = Vec::with_capacity(node.cases.len() + usize::from(!has_default));
for (index, case) in node.cases.iter().enumerate() {
let alternative_id = format!("{id}:case:{index}");
let label = case.test.as_ref().map_or_else(
|| "default".to_string(),
|test| format!("case {}", source_slice(self.source, test.span())),
);
case_ids.push(alternative_id.clone());
alternatives.push(CandidateBranchAlternative {
id: alternative_id,
label,
});
}
let no_match_id = (!has_default).then(|| format!("{id}:no-match"));
if let Some(no_match_id) = &no_match_id {
alternatives.push(CandidateBranchAlternative {
id: no_match_id.clone(),
label: "no matching case".to_string(),
});
}
let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
self.analysis.branches.push(CandidateBranch {
id,
kind: "switch".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, node.discriminant.span()).to_string(),
alternatives,
});
self.analysis.targets.insert(
span_key(node.span),
SwitchTarget {
case_ids,
no_match_id,
},
);
}
}
fn collect_switch_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
) -> SwitchAnalysis {
let mut collector = SwitchCollector {
source,
file,
source_sensitive_functions,
unsafe_function_depth: 0,
with_depth: 0,
analysis: SwitchAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
struct ExtendedCollector<'s> {
source: &'s str,
file: &'s str,
source_sensitive_functions: &'s HashSet<SpanKey>,
entered_loops: HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
analysis: ExtendedAnalysis,
}
impl ExtendedCollector<'_> {
fn unsafe_context(&self) -> bool {
self.unsafe_function_depth > 0 || self.with_depth > 0
}
fn enter_try(&mut self, node: &TryStatement<'_>) {
let transformed = !self.unsafe_context() && node.handler.is_some();
if !transformed {
return;
}
let id = stable_id(self.source, self.file, "try-catch", node.span, "");
let success_id = format!("{id}:success");
let catch_id = format!("{id}:catch");
let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
self.analysis.branches.push(CandidateBranch {
id,
kind: "try-catch".to_string(),
file: self.file.to_string(),
line,
column,
source: "try / catch".to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: success_id.clone(),
label: "try completed without catch".to_string(),
},
CandidateBranchAlternative {
id: catch_id.clone(),
label: "catch entered".to_string(),
},
],
});
self.analysis.try_targets.insert(
span_key(node.span),
ExtendedTarget {
first_id: success_id,
second_id: catch_id,
},
);
}
fn enter_loop(&mut self, span: Span, right: &Expression<'_>, kind: &str) {
let transformed = !self.unsafe_context();
if !transformed {
return;
}
if self.entered_loops.contains(&span_key(span)) {
return;
}
let id = stable_id(self.source, self.file, kind, span, "");
let zero_id = format!("{id}:zero");
let entered_id = format!("{id}:entered");
let (line, column) = line_and_utf16_column(self.source, span.start as usize);
self.analysis.branches.push(CandidateBranch {
id,
kind: kind.to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, right.span()).to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: zero_id.clone(),
label: "zero iterations".to_string(),
},
CandidateBranchAlternative {
id: entered_id.clone(),
label: "one or more iterations".to_string(),
},
],
});
self.analysis.loop_targets.insert(
span_key(span),
ExtendedTarget {
first_id: zero_id,
second_id: entered_id,
},
);
}
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for ExtendedCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_try_statement(
&mut self,
node: &mut TryStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.enter_try(node);
}
fn exit_try_statement(
&mut self,
_node: &mut TryStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
}
fn enter_for_in_statement(
&mut self,
node: &mut ForInStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.enter_loop(node.span, &node.right, "for-in");
}
fn exit_for_in_statement(
&mut self,
_node: &mut ForInStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
}
fn enter_for_of_statement(
&mut self,
node: &mut ForOfStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.enter_loop(node.span, &node.right, "for-of");
}
fn exit_for_of_statement(
&mut self,
_node: &mut ForOfStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
}
}
fn provably_entered_loops(program: &Program<'_>) -> HashSet<SpanKey> {
let semantic = SemanticBuilder::new().build(program).semantic;
let mut found = HashSet::new();
for node in semantic.nodes() {
if let AstKind::ForOfStatement(loop_node) = node.kind()
&& never_empty(&loop_node.right, &semantic)
{
found.insert(span_key(loop_node.span));
}
}
found
}
fn never_empty(expression: &Expression<'_>, semantic: &oxc_semantic::Semantic<'_>) -> bool {
match transparent_expression(expression) {
Expression::ArrayExpression(array) => has_definite_element(array),
Expression::Identifier(identifier) => {
let Some(symbol) = referenced_symbol(identifier, semantic.scoping()) else {
return false;
};
if semantic.scoping().get_resolved_references(symbol).count() != 1 {
return false;
}
let declaration = semantic.scoping().symbol_declaration(symbol);
let AstKind::VariableDeclarator(declarator) =
semantic.nodes().get_node(declaration).kind()
else {
return false;
};
declarator.kind == VariableDeclarationKind::Const
&& declarator.init.as_ref().is_some_and(|init| {
matches!(transparent_expression(init), Expression::ArrayExpression(array)
if has_definite_element(array))
})
}
_ => false,
}
}
fn has_definite_element(array: &oxc_ast::ast::ArrayExpression<'_>) -> bool {
array
.elements
.iter()
.any(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
}
fn collect_extended_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
) -> ExtendedAnalysis {
let entered_loops = provably_entered_loops(program);
let mut collector = ExtendedCollector {
source,
file,
source_sensitive_functions,
entered_loops,
unsafe_function_depth: 0,
with_depth: 0,
analysis: ExtendedAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
struct DefaultCollector<'s> {
source: &'s str,
file: &'s str,
source_sensitive_functions: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
analysis: DefaultAnalysis,
}
impl DefaultCollector<'_> {
fn unsafe_context(&self) -> bool {
self.unsafe_function_depth > 0 || self.with_depth > 0
}
fn target(
&mut self,
span: Span,
left: &BindingPattern<'_>,
right: &Expression<'_>,
) -> DefaultTarget {
let id = stable_id(self.source, self.file, "default-value", span, "");
let default_id = format!("{id}:default");
let provided_id = format!("{id}:provided");
let (line, column) = line_and_utf16_column(self.source, span.start as usize);
self.analysis.branches.push(CandidateBranch {
id,
kind: "default-value".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, span).to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: default_id.clone(),
label: "default evaluated".to_string(),
},
CandidateBranchAlternative {
id: provided_id.clone(),
label: "value provided".to_string(),
},
],
});
DefaultTarget {
default_id,
provided_id,
inferred_name: binding_identifier_name(left)
.filter(|_| expression_is_anonymous_definition(right)),
}
}
fn collect_binding_pattern(&mut self, pattern: &BindingPattern<'_>, parameter: bool) {
match pattern {
BindingPattern::AssignmentPattern(assignment) => {
let target = self.target(assignment.span, &assignment.left, &assignment.right);
if parameter {
self.analysis
.parameter_targets
.insert(span_key(assignment.span), target);
} else {
self.analysis
.binding_targets
.insert(span_key(assignment.span), target);
}
self.collect_binding_pattern(&assignment.left, parameter);
}
BindingPattern::ObjectPattern(object) => {
for property in &object.properties {
self.collect_binding_pattern(&property.value, parameter);
}
if let Some(rest) = &object.rest {
self.collect_binding_pattern(&rest.argument, parameter);
}
}
BindingPattern::ArrayPattern(array) => {
for element in array.elements.iter().flatten() {
self.collect_binding_pattern(element, parameter);
}
if let Some(rest) = &array.rest {
self.collect_binding_pattern(&rest.argument, parameter);
}
}
BindingPattern::BindingIdentifier(_) => {}
}
}
fn collect_parameters(&mut self, parameters: &FormalParameters<'_>) {
for parameter in ¶meters.items {
if let Some(initializer) = ¶meter.initializer {
let default_span =
Span::new(parameter.pattern.span().start, initializer.span().end);
let target = self.target(default_span, ¶meter.pattern, initializer);
self.analysis
.parameter_targets
.insert(span_key(parameter.span), target);
}
self.collect_binding_pattern(¶meter.pattern, true);
}
if let Some(rest) = ¶meters.rest {
self.collect_binding_pattern(&rest.rest.argument, true);
}
}
fn has_binding_default(pattern: &BindingPattern<'_>) -> bool {
match pattern {
BindingPattern::AssignmentPattern(_) => true,
BindingPattern::ObjectPattern(object) => {
object
.properties
.iter()
.any(|property| Self::has_binding_default(&property.value))
|| object
.rest
.as_ref()
.is_some_and(|rest| Self::has_binding_default(&rest.argument))
}
BindingPattern::ArrayPattern(array) => {
array
.elements
.iter()
.flatten()
.any(Self::has_binding_default)
|| array
.rest
.as_ref()
.is_some_and(|rest| Self::has_binding_default(&rest.argument))
}
BindingPattern::BindingIdentifier(_) => false,
}
}
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for DefaultCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
return;
}
if !self.unsafe_context() && node.body.is_some() {
self.collect_parameters(&node.params);
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
return;
}
if !self.unsafe_context() {
self.collect_parameters(&node.params);
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_variable_declaration(
&mut self,
node: &mut VariableDeclaration<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
if self.unsafe_context() {
return;
}
let has_default = node
.declarations
.iter()
.any(|declaration| Self::has_binding_default(&declaration.id));
if !has_default {
return;
}
if matches!(
context.ancestors().next(),
Some(Ancestor::ForStatementInit(_))
) {
let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
self.analysis.limitations.push(CandidateLimitation {
id: stable_id(
self.source,
self.file,
"dynamic-code",
node.span,
"for-init-default",
),
kind: "dynamic-code".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, node.span).to_string(),
reason: "destructuring defaults in a classic for initializer cannot yet be finalized without restructuring control flow".to_string(),
});
return;
}
for declaration in &node.declarations {
self.collect_binding_pattern(&declaration.id, false);
}
}
}
fn collect_default_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
) -> DefaultAnalysis {
let mut collector = DefaultCollector {
source,
file,
source_sensitive_functions,
unsafe_function_depth: 0,
with_depth: 0,
analysis: DefaultAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
struct OptionalCallCollector<'s> {
source: &'s str,
file: &'s str,
source_sensitive_functions: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
chain_roots: Vec<SpanKey>,
analysis: OptionalCallAnalysis,
}
impl OptionalCallCollector<'_> {
fn unsafe_context(&self) -> bool {
self.unsafe_function_depth > 0 || self.with_depth > 0
}
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for OptionalCallCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_chain_expression(
&mut self,
node: &mut ChainExpression<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
let root = match context.ancestors().next() {
Some(Ancestor::UnaryExpressionArgument(parent))
if *parent.operator() == UnaryOperator::Delete =>
{
span_key(*parent.span())
}
_ => span_key(node.span),
};
self.chain_roots.push(root);
}
fn exit_chain_expression(
&mut self,
_node: &mut ChainExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.chain_roots.pop();
}
fn enter_call_expression(
&mut self,
node: &mut CallExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if !node.optional || self.unsafe_context() {
return;
}
let root = *self
.chain_roots
.last()
.expect("an optional call must be enclosed by a chain expression");
let id = stable_id(self.source, self.file, "optional-chain", node.span, "call");
let short_id = format!("{id}:short");
let continued_id = format!("{id}:continued");
let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
self.analysis.sites.insert(
span_key(node.span),
(short_id.clone(), continued_id.clone()),
);
self.analysis
.roots
.entry(root)
.or_default()
.push(span_key(node.span));
self.analysis.branches.push(CandidateBranch {
id,
kind: "optional-chain".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, node.span).to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: short_id,
label: "nullish / short-circuited".to_string(),
},
CandidateBranchAlternative {
id: continued_id,
label: "non-nullish / continued".to_string(),
},
],
});
}
}
fn collect_optional_call_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
) -> OptionalCallAnalysis {
let mut collector = OptionalCallCollector {
source,
file,
source_sensitive_functions,
unsafe_function_depth: 0,
with_depth: 0,
chain_roots: Vec::new(),
analysis: OptionalCallAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
struct OptionalMemberCollector<'s> {
source: &'s str,
file: &'s str,
source_sensitive_functions: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
analysis: OptionalMemberAnalysis,
}
impl OptionalMemberCollector<'_> {
fn record<State>(
&mut self,
span: Span,
optional: bool,
object: &Expression<'_>,
context: &TraverseCtx<'_, State>,
) {
if !optional || self.unsafe_function_depth > 0 || self.with_depth > 0 {
return;
}
let below = match optional_link_below(object) {
LinkBelow::Member => LINK_AFTER_ANOTHER,
LinkBelow::Call(call) => {
self.analysis.after_call.insert(span_key(span), call);
LINK_AFTER_CALL
}
LinkBelow::None => 0,
};
let links = below
| if optional_link_above(context) {
LINK_BEFORE_ANOTHER
} else {
0
};
if links != 0 {
self.analysis.links.insert(span_key(span), links);
}
let id = stable_id(self.source, self.file, "optional-chain", span, "");
let short_id = format!("{id}:short");
let continued_id = format!("{id}:continued");
let (line, column) = line_and_utf16_column(self.source, span.start as usize);
self.analysis
.targets
.insert(span_key(span), (short_id.clone(), continued_id.clone()));
self.analysis.branches.push(CandidateBranch {
id,
kind: "optional-chain".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, span).to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: short_id,
label: "nullish / short-circuited".to_string(),
},
CandidateBranchAlternative {
id: continued_id,
label: "non-nullish / continued".to_string(),
},
],
});
}
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for OptionalMemberCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn enter_computed_member_expression(
&mut self,
node: &mut ComputedMemberExpression<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
self.record(node.span, node.optional, &node.object, context);
}
fn enter_static_member_expression(
&mut self,
node: &mut StaticMemberExpression<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
self.record(node.span, node.optional, &node.object, context);
}
fn enter_private_field_expression(
&mut self,
node: &mut PrivateFieldExpression<'a>,
context: &mut TraverseCtx<'a, ()>,
) {
self.record(node.span, node.optional, &node.object, context);
}
}
fn collect_optional_member_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
) -> OptionalMemberAnalysis {
let mut collector = OptionalMemberCollector {
source,
file,
source_sensitive_functions,
unsafe_function_depth: 0,
with_depth: 0,
analysis: OptionalMemberAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
struct LogicalAssignmentCollector<'s> {
source: &'s str,
file: &'s str,
source_sensitive_functions: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
analysis: LogicalAssignmentAnalysis,
}
impl LogicalAssignmentCollector<'_> {
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for LogicalAssignmentCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn exit_assignment_expression(
&mut self,
node: &mut AssignmentExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self.unsafe_function_depth > 0 || self.with_depth > 0 || !node.operator.is_logical() {
return;
}
let operator = match node.operator {
AssignmentOperator::LogicalAnd => "&&=",
AssignmentOperator::LogicalOr => "||=",
AssignmentOperator::LogicalNullish => "??=",
_ => unreachable!("logical assignment filter must be exhaustive"),
};
let id = stable_id(
self.source,
self.file,
"logical-assignment",
node.span,
operator,
);
let short_id = format!("{id}:short");
let right_id = format!("{id}:right");
let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
self.analysis
.targets
.insert(span_key(node.span), (short_id.clone(), right_id.clone()));
self.analysis.branches.push(CandidateBranch {
id,
kind: "logical-assignment".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, node.span).to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: short_id,
label: "assignment skipped".to_string(),
},
CandidateBranchAlternative {
id: right_id,
label: "right evaluated / assigned".to_string(),
},
],
});
}
}
fn collect_logical_assignment_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
source_sensitive_functions: &HashSet<SpanKey>,
) -> LogicalAssignmentAnalysis {
let mut collector = LogicalAssignmentCollector {
source,
file,
source_sensitive_functions,
unsafe_function_depth: 0,
with_depth: 0,
analysis: LogicalAssignmentAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
struct LogicalBranchCollector<'s> {
source: &'s str,
file: &'s str,
decision_logical_nodes: &'s HashSet<SpanKey>,
source_sensitive_functions: &'s HashSet<SpanKey>,
unsafe_function_depth: usize,
with_depth: usize,
analysis: LogicalBranchAnalysis,
}
impl LogicalBranchCollector<'_> {
fn exit_source_function(&mut self, span: Span) {
if self.source_sensitive_functions.contains(&span_key(span)) {
self.unsafe_function_depth -= 1;
}
}
}
impl<'a> Traverse<'a, ()> for LogicalBranchCollector<'_> {
fn enter_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_function(&mut self, node: &mut Function<'a>, _context: &mut TraverseCtx<'a, ()>) {
self.exit_source_function(node.span);
}
fn enter_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self
.source_sensitive_functions
.contains(&span_key(node.span))
{
self.unsafe_function_depth += 1;
}
}
fn exit_arrow_function_expression(
&mut self,
node: &mut ArrowFunctionExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.exit_source_function(node.span);
}
fn enter_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth += 1;
}
fn exit_with_statement(
&mut self,
_node: &mut WithStatement<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
self.with_depth -= 1;
}
fn exit_logical_expression(
&mut self,
node: &mut LogicalExpression<'a>,
_context: &mut TraverseCtx<'a, ()>,
) {
if self.unsafe_function_depth > 0
|| self.with_depth > 0
|| self.decision_logical_nodes.contains(&span_key(node.span))
{
return;
}
let operator = match node.operator {
LogicalOperator::And => "&&",
LogicalOperator::Or => "||",
LogicalOperator::Coalesce => "??",
};
let id = stable_id(self.source, self.file, "logical-value", node.span, operator);
let short_id = format!("{id}:short");
let right_id = format!("{id}:right");
let (line, column) = line_and_utf16_column(self.source, node.span.start as usize);
self.analysis
.logical_targets
.insert(span_key(node.span), (short_id.clone(), right_id.clone()));
self.analysis.branches.push(CandidateBranch {
id,
kind: "logical-value".to_string(),
file: self.file.to_string(),
line,
column,
source: source_slice(self.source, node.span).to_string(),
alternatives: vec![
CandidateBranchAlternative {
id: short_id,
label: "short-circuit / left selected".to_string(),
},
CandidateBranchAlternative {
id: right_id,
label: "right evaluated / selected".to_string(),
},
],
});
}
}
fn collect_logical_value_branches<'a>(
allocator: &'a Allocator,
program: &mut Program<'a>,
source: &str,
file: &str,
decision_logical_nodes: &HashSet<SpanKey>,
source_sensitive_functions: &HashSet<SpanKey>,
) -> LogicalBranchAnalysis {
let mut collector = LogicalBranchCollector {
source,
file,
decision_logical_nodes,
source_sensitive_functions,
unsafe_function_depth: 0,
with_depth: 0,
analysis: LogicalBranchAnalysis::default(),
};
traverse_mut(&mut collector, allocator, program, Default::default(), ());
collector.analysis
}
impl<'a> Visit<'a> for DecisionCollector<'_> {
fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
if self
.source_sensitive_functions
.contains(&span_key(function.span))
{
return;
}
walk::walk_arrow_function_expression(self, function);
}
fn visit_with_statement(&mut self, _statement: &WithStatement<'a>) {}
fn visit_if_statement(&mut self, statement: &IfStatement<'a>) {
if decision_outcome_is_variable(&statement.test) {
self.record_decision(&statement.test, "if");
}
self.visit_expression(&statement.test);
self.visit_statement(&statement.consequent);
if let Some(alternate) = &statement.alternate {
self.visit_statement(alternate);
}
}
fn visit_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
if decision_outcome_is_variable(&expression.test) {
self.record_decision(&expression.test, "ternary");
}
self.visit_expression(&expression.test);
self.visit_expression(&expression.consequent);
self.visit_expression(&expression.alternate);
}
fn visit_while_statement(&mut self, statement: &WhileStatement<'a>) {
if decision_outcome_is_variable(&statement.test) {
self.record_decision(&statement.test, "while");
}
self.visit_expression(&statement.test);
self.visit_statement(&statement.body);
}
fn visit_do_while_statement(&mut self, statement: &DoWhileStatement<'a>) {
if decision_outcome_is_variable(&statement.test) {
self.record_decision(&statement.test, "do-while");
}
self.visit_statement(&statement.body);
self.visit_expression(&statement.test);
}
fn visit_for_statement(&mut self, statement: &ForStatement<'a>) {
if let Some(test) = &statement.test
&& decision_outcome_is_variable(test)
{
self.record_decision(test, "for");
}
if let Some(init) = &statement.init {
self.visit_for_statement_init(init);
}
if let Some(test) = &statement.test {
self.visit_expression(test);
}
if let Some(update) = &statement.update {
self.visit_expression(update);
}
self.visit_statement(&statement.body);
}
}
fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
match expression {
Expression::ParenthesizedExpression(parenthesized) => {
transparent_expression(&parenthesized.expression)
}
_ => expression,
}
}
fn has_compound_boolean_decision(expression: &Expression<'_>) -> bool {
match expression {
Expression::ParenthesizedExpression(parenthesized) => {
has_compound_boolean_decision(&parenthesized.expression)
}
Expression::LogicalExpression(logical) => {
matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or)
}
Expression::UnaryExpression(unary) if unary.operator.is_not() => {
has_compound_boolean_decision(&unary.argument)
}
_ => false,
}
}
fn collect_conditions(expression: &Expression<'_>, conditions: &mut Vec<Span>) {
match expression {
Expression::ParenthesizedExpression(parenthesized) => {
collect_conditions(&parenthesized.expression, conditions);
}
Expression::LogicalExpression(logical)
if matches!(logical.operator, LogicalOperator::And | LogicalOperator::Or) =>
{
collect_conditions(&logical.left, conditions);
collect_conditions(&logical.right, conditions);
}
Expression::UnaryExpression(unary)
if unary.operator.is_not() && has_compound_boolean_decision(&unary.argument) =>
{
collect_conditions(&unary.argument, conditions);
}
_ => conditions.push(expression.span()),
}
}
fn source_slice(source: &str, span: Span) -> &str {
&source[span.start as usize..span.end as usize]
}
struct SourcePositions {
address: usize,
length: usize,
sample: u64,
line_starts: Vec<usize>,
utf16_before_line: Vec<usize>,
checkpoints: Vec<Option<Vec<(usize, usize)>>>,
}
impl SourcePositions {
fn sample(source: &str) -> u64 {
let bytes = source.as_bytes();
let head = &bytes[..bytes.len().min(64)];
let tail = &bytes[bytes.len().saturating_sub(64)..];
head.iter()
.chain(tail)
.fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
})
}
fn new(source: &str) -> Self {
let mut line_starts = vec![0];
let mut utf16_before_line = vec![0];
let mut checkpoints = Vec::new();
let mut marks: Option<Vec<(usize, usize)>> = None;
let mut utf16 = 0;
let mut column = 0;
let mut line_start = 0;
for (index, character) in source.char_indices() {
if !character.is_ascii() && marks.is_none() {
marks = Some(vec![(0, 0)]);
}
if let Some(marks) = &mut marks
&& column >= marks.last().expect("a first mark").1 + CHECKPOINT
{
marks.push((index - line_start, column));
}
utf16 += character.len_utf16();
column += character.len_utf16();
if character == '\n' {
line_start = index + 1;
line_starts.push(line_start);
utf16_before_line.push(utf16);
checkpoints.push(marks.take());
column = 0;
}
}
checkpoints.push(marks);
Self {
address: source.as_ptr() as usize,
length: source.len(),
sample: Self::sample(source),
line_starts,
utf16_before_line,
checkpoints,
}
}
fn describes(&self, source: &str) -> bool {
self.address == source.as_ptr() as usize
&& self.length == source.len()
&& self.sample == Self::sample(source)
}
fn locate(&self, source: &str, offset: usize) -> (usize, usize) {
let line = self.line_starts.partition_point(|start| *start <= offset) - 1;
let start = self.line_starts[line];
let column = match &self.checkpoints[line] {
None => offset - start,
Some(marks) => {
let at = marks.partition_point(|(byte, _)| *byte <= offset - start) - 1;
let (byte, column) = marks[at];
column + source[start + byte..offset].encode_utf16().count()
}
};
(line, column)
}
}
thread_local! {
static POSITIONS: std::cell::RefCell<Option<Vec<std::rc::Rc<SourcePositions>>>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) struct PositionScope {
owner: bool,
}
impl PositionScope {
pub(crate) fn enter() -> Self {
let owner = POSITIONS.with(|positions| {
let mut positions = positions.borrow_mut();
let owner = positions.is_none();
if owner {
*positions = Some(Vec::new());
}
owner
});
Self { owner }
}
}
impl Drop for PositionScope {
fn drop(&mut self) {
if self.owner {
POSITIONS.with(|positions| *positions.borrow_mut() = None);
}
}
}
fn indexed(source: &str) -> Option<std::rc::Rc<SourcePositions>> {
POSITIONS.with(|positions| {
let mut positions = positions.borrow_mut();
let known = positions.as_mut()?;
if let Some(found) = known.iter().find(|index| index.describes(source)) {
return Some(found.clone());
}
let built = std::rc::Rc::new(SourcePositions::new(source));
known.push(built.clone());
Some(built)
})
}
fn utf16_offset(source: &str, offset: usize) -> usize {
match indexed(source) {
Some(index) => {
let (line, column) = index.locate(source, offset);
index.utf16_before_line[line] + column
}
None => source[..offset].encode_utf16().count(),
}
}
pub(crate) fn line_and_utf16_column(source: &str, offset: usize) -> (usize, usize) {
if let Some(index) = indexed(source) {
let (line, column) = index.locate(source, offset);
return (line + 1, column + 1);
}
let prefix = &source[..offset];
let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
let column = source[line_start..offset].encode_utf16().count() + 1;
(line, column)
}
fn stable_id(source: &str, file: &str, kind: &str, span: Span, suffix: &str) -> String {
let start = utf16_offset(source, span.start as usize);
let end = utf16_offset(source, span.end as usize);
let digest = Sha256::digest(format!("{file}:{kind}:{start}:{end}:{suffix}").as_bytes());
let mut id = String::with_capacity(16);
for byte in &digest[..8] {
write!(&mut id, "{byte:02x}").expect("writing to a String cannot fail");
}
id
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_index_lookups_agree_with_walking_the_line() {
let long = "é🚀x".repeat(700);
let source = format!("const a = 1;\nconst s = '{long}';\n\nplain ascii line\n{long}");
let index = Utf16LineIndex::new(&source);
for (line, text) in source.split_inclusive('\n').enumerate() {
let start = index.starts[line];
let width: usize = text.chars().map(char::len_utf16).sum();
for column in 0..=width + 2 {
assert_eq!(
index.byte_offset(line, column),
start + utf16_col_to_byte(text, column),
"line {line} column {column}"
);
}
for (offset, _) in text.char_indices() {
let walked: usize = text[..offset].chars().map(char::len_utf16).sum();
assert_eq!(index.line_col(start + offset), (line as u32, walked as u32));
}
}
}
#[test]
fn indexed_positions_agree_with_counting_from_the_top() {
let source = &format!(
"const s = 'naïve 🚀';\n\nexport function f(a) {{\n return a ? 'é' : s;\n}}\n// 日本\nconst t = '{}';\nx",
"é🚀x".repeat(400)
);
let source = source.as_str();
let counted: Vec<_> = source
.char_indices()
.map(|(offset, _)| {
(
line_and_utf16_column(source, offset),
stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
)
})
.collect();
let _positions = PositionScope::enter();
for ((offset, _), (position, id)) in source.char_indices().zip(counted) {
assert_eq!(
line_and_utf16_column(source, offset),
position,
"offset {offset}"
);
assert_eq!(
stable_id(source, "f.js", "k", Span::new(0, offset as u32), ""),
id
);
}
}
#[test]
fn classic_scripts_share_application_globals_but_not_probe_bindings() {
let a = instrument_direct_candidate(
"var count = 0; function increment() { if (count >= 0) { count += 1; } }",
"public/a.js",
)
.unwrap();
let b = instrument_direct_candidate("increment(); var observed = count;", "public/b.js")
.unwrap();
let runtime = include_str!("../runtime-assets/javascript/runtime.mjs");
let runtime = &runtime[..runtime.rfind("\nexport {").unwrap()];
let script = format!(
"const vm = require('node:vm'); const ctx = vm.createContext({{ setTimeout, clearTimeout, queueMicrotask, console }});\
vm.runInContext({}, ctx); vm.runInContext({}, ctx); vm.runInContext({}, ctx);\
if (ctx.observed !== 1) throw Error('shared globals changed');\
vm.runInContext({}, ctx);\
if (ctx.observed !== 2) throw Error('a repeated script changed');",
serde_json::to_string(runtime).unwrap(),
serde_json::to_string(&a.code).unwrap(),
serde_json::to_string(&b.code).unwrap(),
serde_json::to_string(&b.code).unwrap()
);
let mut node = std::process::Command::new("node")
.arg("-")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("Node.js for the JavaScript regression");
std::io::Write::write_all(&mut node.stdin.take().unwrap(), script.as_bytes()).unwrap();
let result = node.wait_with_output().unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
}
#[test]
fn javascript_with_jsx_is_measured_in_every_javascript_extension() {
for file in [
"src/index.js",
"src/index.jsx",
"src/index.mjs",
"src/index.cjs",
"src/index.tsx",
] {
let source = "export const View = ({ok}) => <div>{ok ? 'yes' : 'no'}</div>;";
let output = instrument_candidate(source, file).unwrap();
assert!(!output.points.is_empty(), "{file}");
assert!(output.code.contains("<div>"), "{file}");
}
assert!(instrument_candidate("const n = <number>value;", "src/index.ts").is_ok());
for file in ["a.js", "a.cjs", "a.mjs"] {
let before = SourceType::from_path(file).unwrap();
let after = project_source_type(Path::new(file)).unwrap();
assert_eq!(after.module_kind(), before.module_kind(), "{file}");
assert!(after.is_jsx(), "{file}");
}
}
const SOURCE: &str =
"export function decide(a,b,c) {\n if ((a && b) || !c) return 1;\n return 0;\n}\n";
#[test]
fn matches_the_frozen_if_decision_manifest_exactly() {
let output = analyze_candidate(SOURCE, "app/decide.ts").unwrap();
assert!(!output.complete);
assert_eq!(
output.decisions,
vec![CandidateDecision {
id: "2f65989a5782c5bd".to_string(),
file: "app/decide.ts".to_string(),
line: 2,
column: 7,
source: "(a && b) || !c".to_string(),
conditions: vec!["a".to_string(), "b".to_string(), "!c".to_string()],
kind: "if".to_string(),
}]
);
}
#[test]
fn a_test_files_unchanged_text_keeps_its_columns() {
let source = "import { expect } from 'vitest';\n expect(value(1)).toBe(2);\n";
let wrapper = "wrap(() => ";
let edits = vec![
TextEdit {
start: source.find("expect(value").unwrap(),
end: source.find("expect(value").unwrap(),
text: wrapper.into(),
rank: 0,
},
TextEdit {
start: source.rfind(';').unwrap(),
end: source.rfind(';').unwrap(),
text: ")".into(),
rank: 0,
},
];
let (code, map) = apply_text_edits(source, edits, "a.test.js").unwrap();
assert_eq!(
code,
"import { expect } from 'vitest';\n wrap(() => expect(value(1)).toBe(2));\n"
);
let original = |line: u32, column: u32| {
map.get_tokens()
.filter(|token| token.get_dst_line() == line && token.get_dst_col() <= column)
.max_by_key(|token| token.get_dst_col())
.map(|token| (token.get_src_line(), token.get_src_col()))
};
let matcher = code.lines().nth(1).unwrap().find("toBe").unwrap() as u32;
assert_eq!(original(1, matcher), Some((1, 19)));
assert_eq!(original(0, 9), Some((0, 9)));
assert_eq!(original(0, 7), Some((0, 7)));
assert_eq!(original(1, 4), Some((1, 0)));
let name_at = |line: u32, column: u32| {
map.get_tokens()
.find(|token| token.get_dst_line() == line && token.get_dst_col() == column)
.and_then(|token| token.get_name_id())
.and_then(|id| map.get_name(id))
.map(|name| name.to_string())
};
assert_eq!(name_at(1, matcher).as_deref(), Some("toBe"));
assert_eq!(name_at(0, 9).as_deref(), Some("expect"));
assert_eq!(name_at(0, 18), None, "`from` is a keyword");
}
#[test]
fn codegen_output_reparses_for_typescript_and_tsx() {
for (file, source) in [
(
"component.tsx",
"export const View = ({ok}: {ok: boolean}) => <div>{ok ? 'yes' : 'no'}</div>;",
),
("module.ts", SOURCE),
] {
let output = analyze_candidate(source, file).unwrap();
let map = output.map.as_ref().expect("candidate source map");
assert_eq!(map["version"], 3);
assert_eq!(map["sources"][0], file);
assert_eq!(map["sourcesContent"][0], source);
assert!(
map["mappings"]
.as_str()
.is_some_and(|value| !value.is_empty())
);
let allocator = Allocator::default();
let source_type = SourceType::from_path(file).unwrap();
let reparsed = Parser::new(&allocator, &output.code, source_type).parse();
assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
}
}
#[test]
fn direct_runtime_mode_has_no_virtual_module_or_legacy_global() {
for file in ["src/module.mjs", "src/script.cjs"] {
let output = instrument_direct_candidate(
"export const selected = value => value ? 1 : 0;",
file,
)
.unwrap();
assert!(
output
.code
.contains("globalThis.__SUPERCOV_DIRECT_RUNTIME__")
);
assert!(!output.code.contains("virtual:supercov-runtime"));
assert!(!output.code.contains("globalThis.__supercovRuntime"));
}
}
#[test]
fn capability_imports_are_rewritten_ahead_of_run_by_rust() {
let source = concat!(
"import { ImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
"await ImageBuilder.build({ mounts: [{ source: process.cwd(), target: '/workspace' }], snapshotKey: 'base' });\n",
"ordinaryHelper();\n",
);
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"tests/runner.mjs",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.assertions, 0);
assert_eq!(output.capability_imports, 1);
assert!(output.code.contains("wrapImportedCapability"));
assert!(output.code.contains("__supercovRawImageBuilder"));
assert!(output.code.contains("const ImageBuilder"));
assert!(!output.code.contains("__supercovRawordinaryHelper"));
assert!(output.code.contains("../.supercov/launchSupervisor.mjs"));
}
#[test]
fn capability_imports_accept_a_computed_guest_mount_path() {
let source = concat!(
"import { OpaqueImageBuilder, ordinaryHelper } from './sdk.mjs';\n",
"const guestRoot = resolve(tmpdir(), 'workspace');\n",
"await OpaqueImageBuilder.build({ mounts: [{ source: process.cwd(), target: guestRoot }], snapshotTag: 'base' });\n",
"ordinaryHelper();\n",
);
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"tests/runner.mjs",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.capability_imports, 1);
assert!(output.code.contains("__supercovRawOpaqueImageBuilder"));
assert!(!output.code.contains("__supercovRawordinaryHelper"));
}
#[test]
fn capability_import_selection_follows_a_mapping_variable() {
let source = concat!(
"import { launch, unrelated } from './sdk.mjs';\n",
"run();\n",
"const options = { hostPath: process.cwd(), guestPath: '/workspace' };\n",
"function run() { launch(options); unrelated(); }\n",
);
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"tests/runner.mjs",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.capability_imports, 1);
assert!(output.code.contains("__supercovRawlaunch"));
assert!(!output.code.contains("__supercovRawunrelated"));
}
#[test]
fn ordinary_imports_remain_byte_identical_without_a_capability_shape() {
let source = "import { format } from './format.mjs';\nconsole.log(format('value'));";
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"src/main.mjs",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.code, source);
assert_eq!(output.capability_imports, 0);
}
#[test]
fn ordinary_mount_language_does_not_enable_capability_proxies() {
let source = concat!(
"import { render } from '@testing-library/react';\n",
"it('mounts into a host element', () => render(<main />));\n",
);
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"tests/component.test.tsx",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.capability_imports, 0);
assert!(!output.code.contains("wrapImportedCapability"));
}
#[test]
fn test_snapshot_apis_are_not_mistaken_for_remote_workspace_capabilities() {
let source = concat!(
"import { test, expect } from '@example/test-admin';\n",
"test('visual snapshot', async ({ page }) => {\n",
" expect(await page.screenshot()).toMatchSnapshot('screen.png');\n",
"});\n",
);
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"tests/visual.spec.ts",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.capability_imports, 0);
assert!(!output.code.contains("wrapImportedCapability"));
}
#[test]
fn capability_rewrite_excludes_runtime_and_type_only_imports() {
let source = concat!(
"import type { Machine } from './types.ts';\n",
"import { test } from 'node:test';\n",
"import { runtime } from '../.supercov/runtime.mjs';\n",
"console.log({ machine: true, test, runtime });\n",
);
let output = instrument_node_assertion_phases_with_runtime_hooks(
source,
"tests/runner.ts",
&[],
Some("../.supercov/launchSupervisor.mjs"),
)
.unwrap();
assert_eq!(output.code, source);
assert_eq!(output.capability_imports, 0);
}
#[test]
fn native_assertion_arguments_execute_inside_the_assertion_phase() {
let source = concat!(
"import assert from 'node:assert/strict';\n",
"assert.equal(value(), 1);\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("withNodeAssertionPhase"));
assert!(output.code.contains("node:assert/strict.equal"));
assert!(output.code.contains("tests/value.test.mjs:2:1"));
assert!(output.code.contains("assert.equal(value(), 1)"));
}
#[test]
fn esm_assertion_transform_carries_its_runtime_across_opaque_launches() {
let source = "import assert from 'node:assert';\nassert.equal(value(), 1);\n";
let output = instrument_node_assertion_phases_with_runtime_imports(
source,
"tests/value.test.mjs",
&[],
None,
Some("../.supercov/node_modules/bootstrap.cjs"),
)
.unwrap();
assert_eq!(output.assertions, 1);
let lines = output.code.lines().collect::<Vec<_>>();
assert!(lines[0].starts_with("if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) try {"));
assert!(lines[0].contains("\"../.supercov/node_modules/bootstrap.cjs\""));
assert!(lines[0].ends_with("import assert from 'node:assert';"));
assert!(lines[1].contains("withNodeAssertionPhase"));
let allocator = Allocator::default();
assert!(
Parser::new(&allocator, &output.code, SourceType::mjs())
.parse()
.errors
.is_empty()
);
}
#[test]
fn native_assertion_phase_never_moves_await_into_a_sync_callback() {
let source = concat!(
"import assert from 'node:assert/strict';\n",
"export async function check() { assert.equal(await value(), 1); }\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("bindNodeAssertionPhase"));
assert!(output.code.contains("await value()"));
let allocator = Allocator::default();
assert!(
Parser::new(&allocator, &output.code, SourceType::mjs())
.parse()
.errors
.is_empty()
);
}
#[test]
fn a_typescript_assertion_with_an_await_calls_a_typed_local() {
let source = concat!(
"import assert from 'node:assert/strict';\n",
"export async function check(user?: string) {\n",
" assert.ok(user, await value());\n",
" assert\n",
" .equal(await value(), 1)\n",
"}\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.ts").unwrap();
assert_eq!(output.assertions, 2);
let code = &output.code;
let lines: Vec<&str> = code.lines().collect();
assert!(
lines[2].starts_with(
" { const __supercovAssertionPhase: typeof assert.ok = globalThis.__SUPERCOV_DIRECT_RUNTIME__.bindNodeAssertionPhase("
),
"{code}"
);
assert!(
lines[2]
.ends_with(", assert, \"ok\"); __supercovAssertionPhase(user, await value()); }"),
"{code}"
);
assert!(lines[3].contains(": typeof assert.equal = "), "{code}");
assert_eq!(
lines[4], " \"equal\"); __supercovAssertionPhase(await value(), 1) }",
"{code}"
);
assert_eq!(lines[5], "}", "{code}");
let allocator = Allocator::default();
assert!(
Parser::new(&allocator, code, SourceType::ts())
.parse()
.errors
.is_empty()
);
}
#[test]
fn matcher_assertion_phase_never_moves_receiver_await_into_a_sync_callback() {
let source = concat!(
"import { expect, test } from '@playwright/test';\n",
"test('value', async () => { expect(await value()).toBe(1); });\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("bindNodeAssertionPhase"));
assert!(output.code.contains("await value()"));
let allocator = Allocator::default();
assert!(
Parser::new(&allocator, &output.code, SourceType::mjs())
.parse()
.errors
.is_empty()
);
}
#[test]
fn native_commonjs_assertion_bindings_are_attributed() {
let source = concat!(
"const assert = require('node:assert/strict');\n",
"const { ok: verify } = require('assert');\n",
"assert.equal(value(), 1);\n",
"verify(other());\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
assert_eq!(output.assertions, 2);
assert!(output.code.contains("node:assert/strict.equal"));
assert!(output.code.contains("node:assert.ok"));
}
#[test]
fn nested_commonjs_bindings_are_supported_but_shadowed_require_is_not() {
let source = concat!(
"function checked() { const assert = require('node:assert'); assert.ok(value()); }\n",
"function unrelated(require) { const assert = require('node:assert'); assert.ok(other()); }\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.cjs").unwrap();
assert_eq!(output.assertions, 1);
assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
}
#[test]
fn assertion_bindings_are_lexical_and_never_wrap_shadowed_values() {
let source = concat!(
"import assert from 'node:assert/strict';\n",
"function unrelated(assert) { assert.equal(effect(), 1); }\n",
"assert.equal(value(), 1);\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert_eq!(output.code.matches("withNodeAssertionPhase").count(), 1);
}
#[test]
fn node_test_expect_matchers_are_attributed_through_lexical_imports() {
let source = concat!(
"import test from 'node:test';\n",
"import { expect as verify } from 'expect-library';\n",
"test('value', () => verify(value()).not.toEqual(1));\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("expect.not.toEqual"));
assert!(output.code.contains("verify(value()).not.toEqual(1)"));
}
#[test]
fn vitest_expect_matchers_are_attributed_by_the_static_frontend() {
let source = concat!(
"import { expect, test } from 'vitest';\n",
"test('value', () => expect(value()).toBe(1));\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.test.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("expect.toBe"));
}
#[test]
fn browser_mode_expect_wrappers_are_attributed_like_a_bare_expect() {
for (call, operation) in [
(
"expect.element(button).toHaveAccessibleName('Copy')",
"expect.element.toHaveAccessibleName",
),
("expect.soft(value()).toBe(1)", "expect.soft.toBe"),
("expect.poll(() => value()).toBe(1)", "expect.poll.toBe"),
] {
let source = format!(
"import {{ expect, test }} from 'vitest';\ntest('value', async () => await {call});\n"
);
let output = instrument_node_assertion_phases(&source, "tests/value.test.mjs").unwrap();
assert_eq!(output.assertions, 1, "{call}");
assert!(output.code.contains(operation), "{call} -> {operation}");
}
}
#[test]
fn a_rendered_jsx_expression_is_measured_apart_from_the_tree_that_holds_it() {
let source = concat!(
"export function Button({ label, copied }) {\n",
" return (\n",
" <button aria-label={format(copied)} title={label} onClick={() => tap()}>\n",
" {format(copied)}\n",
" </button>\n",
" );\n",
"}\n",
);
let output = instrument_candidate(source, "src/button.tsx").unwrap();
let rendered = output
.points
.iter()
.filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
.collect::<Vec<_>>();
assert_eq!(rendered.len(), 2, "{:?}", output.points);
assert!(
rendered
.iter()
.all(|point| point.source == "format(copied)")
);
assert_eq!(rendered[0].line, 3);
assert_eq!(rendered[1].line, 4);
assert!(rendered[0].column != rendered[1].column || rendered[0].line != rendered[1].line);
assert!(!output.code.contains("title={__supercovRenderedValueV2"));
assert!(!output.code.contains("), format(copied)}"));
assert!(output.code.contains(", format(copied))}"));
}
#[test]
fn instrumented_jsx_stays_parseable_through_every_container_shape() {
let source = concat!(
"export function Panel({ items, user, error, tone }) {\n",
" return (\n",
" <>\n",
" <Row {...spread(user)} title={`hi ${name(user)}`} />\n",
" <div className={cx('a', tone && 'b')} data-id={user?.id}>\n",
" {error ? <Alert text={message(error)} /> : null}\n",
" {items.map((item) => (\n",
" <Item key={item.id} label={format(item)} />\n",
" ))}\n",
" {count(items) > 0 && <Footer total={sum(items)} />}\n",
" </div>\n",
" </>\n",
" );\n",
"}\n",
);
let output = instrument_candidate(source, "src/panel.tsx").unwrap();
instrument_candidate(&output.code, "src/panel.tsx").expect("instrumented JSX must reparse");
assert!(output.code.contains("{...spread(user)}"), "{}", output.code);
let rendered = output
.points
.iter()
.filter(|point| point.label.as_deref() == Some("jsx-rendered-expression"))
.map(|point| point.source.as_str())
.collect::<Vec<_>>();
assert!(rendered.contains(&"`hi ${name(user)}`"), "{rendered:?}");
assert!(
rendered.contains(&"cx(\'a\', tone && \'b\')"),
"{rendered:?}"
);
assert!(rendered.contains(&"format(item)"), "{rendered:?}");
assert!(rendered.contains(&"message(error)"), "{rendered:?}");
assert!(!rendered.contains(&"user?.id"), "{rendered:?}");
assert!(!rendered.contains(&"item.id"), "{rendered:?}");
}
#[test]
fn jest_global_expect_is_attributed_next_to_its_test_globals() {
let source = "test('value', () => expect(value()).toBe(1));\n";
let output = instrument_node_assertion_phases(source, "tests/value.test.js").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("expect.toBe"));
let plain = "const ok = expect(value()).toBe(1);\n";
let output = instrument_node_assertion_phases(plain, "src/value.js").unwrap();
assert_eq!(output.assertions, 0);
assert_eq!(output.code, plain);
}
#[test]
fn playwright_expect_matchers_have_the_same_source_identity_as_other_expect_bindings() {
let source = concat!(
"import { expect, test } from '@playwright/test';\n",
"test('value', () => expect(value()).toBe(1));\n",
);
let output = instrument_node_assertion_phases(source, "tests/value.spec.mjs").unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("expect.toBe"));
}
#[test]
fn project_discovered_expect_modules_are_not_hardcoded_in_the_transformer() {
let source = concat!(
"import { browserTest, expect } from '@acme/browser-fixtures';\n",
"browserTest('value', () => expect(value()).toBe(1));\n",
);
let output = instrument_node_assertion_phases_with_expect_modules(
source,
"tests/value.spec.mjs",
&["@acme/browser-fixtures".into()],
)
.unwrap();
assert_eq!(output.assertions, 1);
assert!(output.code.contains("expect.toBe"));
}
#[test]
fn preserves_comment_payloads_byte_for_byte() {
let comment = "/*---\ndescription: >\n nested indentation is data\ninfo: |\n first\n second\n---*/";
let source = format!("{comment}\nfunction run() {{ return 1; }}\n");
let output = instrument_candidate(&source, "app/comments.js").unwrap();
assert!(output.code.contains(comment), "{}", output.code);
}
#[test]
fn source_map_destinations_follow_restored_comments() {
let source = "function run() {\n // first omitted comment\n // second omitted comment\n return 1;\n}\n";
let output = analyze_candidate(source, "app/comment-map.js").unwrap();
let encoded = serde_json::to_string(output.map.as_ref().unwrap()).unwrap();
let map = oxc_sourcemap::SourceMap::from_json_string(&encoded).unwrap();
let return_token = map
.get_tokens()
.find(|token| token.get_src_line() == 3 && token.get_src_col() == 2)
.expect("return token mapping");
let offset = Utf16LineIndex::new(&output.code).byte_offset(
return_token.get_dst_line() as usize,
return_token.get_dst_col() as usize,
);
assert!(
output.code[offset..].starts_with("return"),
"{}",
output.code
);
}
#[test]
fn a_comment_mentioning_a_closing_script_tag_still_instruments() {
for source in [
"// </script>\nexport const x = 1;\n",
"// </script\nexport const x = 1;\n",
"// </SCRIPT>\nexport const x = 1;\n",
"// see the </script> tag here\nexport const x = 1;\n",
"/* block </script> here */\nexport const x = 1;\n",
"/** jsdoc </script> */\nexport const x = 1;\n",
"// one </script>\n// two </script>\n// three\nexport const x = 1;\n",
] {
let output = instrument_candidate(source, "src/probe.ts")
.unwrap_or_else(|error| panic!("{source:?}: {error:?}"));
let original = source.lines().next().expect("a comment");
assert!(
output.code.contains(original),
"comment was not restored verbatim: {original:?} missing from {:?}",
output.code
);
}
let written = "// author wrote <\\/script>\nexport const x = 1;\n";
let output = instrument_candidate(written, "src/probe.ts").expect("instruments");
assert!(
output.code.contains("// author wrote <\\/script>"),
"{:?}",
output.code
);
assert!(!equal_ignoring_whitespace("// a/b", "// a\\/b"));
assert!(!equal_ignoring_whitespace("// </div>", "// </span>"));
assert!(equal_ignoring_whitespace("// </script>", "// <\\/script>"));
}
#[test]
fn a_comment_that_cannot_be_restored_says_which_comment_and_why() {
let error = CandidateError::CommentPreservation {
expected: 116,
actual: 112,
detail:
"no source comment matches the generated comment \"// x\" (matched 97 before it)"
.to_owned(),
};
let rendered = format!("{error:?}");
assert!(rendered.contains("no source comment matches"), "{rendered}");
assert!(rendered.contains("matched 97 before it"), "{rendered}");
}
#[test]
fn a_wrapped_default_keeps_the_type_its_syntax_names() {
let source = concat!(
"export function f(a: number, strict = false, limit = 10, name = `x`, big = 1n, low = -1) { return a; }\n",
"export function g<E extends 'utf8' | false = 'utf8'>(encoding = 'utf8' as E, list = [1], kept: string = 'a') { return encoding; }\n",
"export const h = ({ open = false }, mode = 'x' as const) => open;\n",
);
let output = instrument_candidate(source, "src/defaults.ts").unwrap();
for annotated in [
"strict: boolean = ",
"limit: number = ",
"name: string = ",
"big: bigint = ",
"low: number = ",
"encoding: E = ",
"kept: string = ",
] {
assert!(
output.code.contains(annotated),
"{annotated}\n{}",
output.code
);
}
for bare in ["list = ", "mode = "] {
assert!(output.code.contains(bare), "{bare}\n{}", output.code);
}
assert!(!output.code.contains("list:"), "{}", output.code);
assert!(!output.code.contains("mode:"), "{}", output.code);
let script = instrument_candidate(
"export function f(strict = false) { return strict; }\n",
"src/defaults.js",
)
.unwrap();
assert!(!script.code.contains("strict:"), "{}", script.code);
}
#[test]
fn an_asserted_assignment_target_is_printed_as_a_target() {
let source = concat!(
"export function join(buffer: [unknown], child: string, n: { v?: number }) {\n",
" ;(buffer[0] as string) += child\n",
" ;(buffer[0] as string) = child\n",
" ;(n.v as number)++\n",
" ;(n.v satisfies number | undefined) = 1\n",
" ;[(buffer[0] as string)] = [child]\n",
" for ((buffer[0] as string) of [child]) {}\n",
" return buffer\n",
"}\n",
);
let output = instrument_candidate(source, "src/targets.ts").unwrap();
assert!(!output.code.contains(" as string"), "{}", output.code);
assert!(!output.code.contains("satisfies"), "{}", output.code);
assert!(
output.code.contains("buffer[0] += child"),
"{}",
output.code
);
assert!(output.code.contains("n.v++"), "{}", output.code);
let allocator = Allocator::default();
let reparsed = Parser::new(&allocator, &output.code, SourceType::ts()).parse();
assert!(
reparsed.errors.is_empty(),
"{:?}\n{}",
reparsed.errors,
output.code
);
}
#[test]
fn a_comment_the_printer_moves_is_still_recognised() {
let source = concat!(
"// first\n",
"// @__NO_SIDE_EFFECTS__\n",
"/** documented\n */\n",
"export function f(a: number) {\n",
" return a > 1 ? a : 0;\n",
"}\n",
"\n",
"// second\n",
"export type T = number;\n",
);
let output = instrument_candidate(source, "src/annotated.ts").unwrap();
for comment in [
"// first",
"// @__NO_SIDE_EFFECTS__",
"/** documented\n */",
"// second",
] {
assert_eq!(
output.code.matches(comment).count(),
1,
"{comment}\n{}",
output.code
);
}
}
#[test]
fn restored_comments_never_split_a_keyword_from_its_argument() {
let source = concat!(
"export const A = ({ open }) => {\n",
" return (\n",
" // leading comment before JSX\n",
" <div aria-expanded={open}>a</div>\n",
" )\n",
"}\n",
"export const B = ({ open }) => (\n",
" // comment in an expression body\n",
" <div aria-expanded={open}>b</div>\n",
")\n",
"export function C(x) {\n",
" return ( // inline line comment\n",
" x + 1\n",
" )\n",
"}\n",
"export function D(x) {\n",
" throw (\n",
" /* block\n comment */\n",
" new Error(String(x))\n",
" )\n",
"}\n",
"export function E(x) {\n",
" return ( /* inline block */ x + 2 )\n",
"}\n",
);
let output = instrument_direct_candidate(source, "app/components/List.tsx").unwrap();
for keyword in ["return ", "throw "] {
for (index, _) in output.code.match_indices(keyword) {
let rest = output.code[index + keyword.len()..].trim_start_matches([' ', '\t']);
let block_with_line_break = rest.starts_with("/*")
&& rest[..rest.find("*/").unwrap_or(rest.len())].contains('\n');
assert!(
!rest.starts_with('\n') && !rest.starts_with("//") && !block_with_line_break,
"`{keyword}` is separated from its argument:\n{}",
output.code
);
}
}
for comment in [
"// leading comment before JSX",
"// comment in an expression body",
"// inline line comment",
"/* block\n comment */",
"/* inline block */",
] {
assert!(
output.code.contains(comment),
"{comment} was lost:\n{}",
output.code
);
}
assert!(
output
.code
.contains("return <div aria-expanded={open}>a</div>"),
"{}",
output.code
);
assert!(
output
.code
.contains("return <div aria-expanded={open}>b</div>"),
"{}",
output.code
);
let allocator = Allocator::default();
let reparsed = Parser::new(
&allocator,
&output.code,
SourceType::from_path("app/components/List.tsx").unwrap(),
)
.parse();
assert!(
reparsed.errors.is_empty(),
"{:?}\n{}",
reparsed.errors,
output.code
);
}
#[test]
fn preserves_reference_order_across_every_control_decision_kind() {
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}";
let output = instrument_candidate(source, "app/control.js").unwrap();
assert_eq!(
output
.decisions
.iter()
.map(|decision| decision.kind.as_str())
.collect::<Vec<_>>(),
vec!["ternary", "while", "do-while", "for", "if"]
);
}
#[test]
fn a_loop_that_must_run_has_no_zero_iteration_obligation() {
let entered = |source: &str| {
let output = analyze_candidate(source, "src/loops.ts").unwrap();
output
.branches
.iter()
.filter(|branch| branch.kind == "for-of" || branch.kind == "for-in")
.count()
};
for source in [
"export function f(g) { for (const x of [1, 2]) g(x); }\n",
"export function f(g) { for (const x of [1]) g(x); }\n",
"export function f(g) { const xs = [1, 2]; for (const x of xs) g(x); }\n",
"export function f(g) { for (const x of [,]) g(x); }\n",
"export function f(g, ys) { for (const x of [...ys, 1]) g(x); }\n",
] {
assert_eq!(entered(source), 0, "should be folded: {source}");
}
for source in [
"export function f(g, ys) { for (const x of ys) g(x); }\n",
"export function f(g) { for (const x of []) g(x); }\n",
"export function f(g, ys) { for (const x of [...ys]) g(x); }\n",
"export function f(g) { let xs = [1]; xs = []; for (const x of xs) g(x); }\n",
"export function f(g) { const xs = [1]; xs.pop(); for (const x of xs) g(x); }\n",
"export function f(g) { const xs = [1]; g(xs); for (const x of xs) g(x); }\n",
"export function f(g) { const xs = [1]; xs.length = 0; for (const x of xs) g(x); }\n",
concat!(
"const xs = [1, 2];\n",
"export function f(g, ys) { const xs = ys; for (const x of xs) g(x); }\n",
),
"export function f(g) { for (const k in [1, 2]) g(k); }\n",
] {
assert_eq!(entered(source), 1, "should be kept: {source}");
}
let output = analyze_candidate(
"export function f(g) { for (const x of [1, 2]) g(x); }\n",
"src/loops.ts",
)
.unwrap();
assert!(
output
.points
.iter()
.any(|point| point.source.contains("g(x)")),
"the body must still be an obligation"
);
}
#[test]
fn excludes_syntactically_invariant_control_decisions_from_mcdc() {
let source = concat!(
"export function run(value) {\n",
" while (true) { break; }\n",
" do { value += 1; } while (false);\n",
" if (false) value += 2;\n",
" if (value || true) value += 3;\n",
" if (value && false) value += 4;\n",
" if (value) value += 5;\n",
" return value;\n",
"}\n",
);
let output = analyze_candidate(source, "src/constants.js").unwrap();
assert_eq!(output.decisions.len(), 1);
assert_eq!(output.decisions[0].source, "value");
let instrumented = instrument_direct_candidate(source, "src/constants.js").unwrap();
assert_eq!(instrumented.decisions.len(), 1);
assert_eq!(instrumented.decisions[0].source, "value");
}
#[test]
fn leaves_compile_time_style_macro_arguments_as_source() {
let source = concat!(
"import * as stylex from '@stylexjs/stylex';\n",
"const styles = stylex.create({\n",
" container: { display: 'flex' },\n",
" paddingTop: (spacing: number) => ({ paddingTop: `${spacing}px` }),\n",
" tone: (dark: boolean) => ({ color: dark ? 'white' : 'black' }),\n",
"});\n",
"export function render(spacing: number) {\n",
" return stylex.props(styles.container, styles.paddingTop(spacing));\n",
"}\n",
);
let output = instrument_direct_candidate(source, "src/styles.tsx").unwrap();
assert!(
output.code.contains("=> ({ paddingTop: `${spacing}px` })")
|| output
.code
.contains("=> ({\n\t\tpaddingTop: `${spacing}px`\n\t})"),
"dynamic style arrow must stay an expression body:\n{}",
output.code
);
assert!(
!output
.decisions
.iter()
.any(|decision| decision.source.contains("dark")),
"no decision may be collected inside the macro argument"
);
let function_points = output
.points
.iter()
.filter(|point| point.kind == "function")
.map(|point| point.source.as_str())
.collect::<Vec<_>>();
assert!(
function_points
.iter()
.all(|source| source.contains("render")),
"only the real function may carry a function point: {function_points:?}"
);
assert!(
output.limitations.is_empty(),
"compiled-away code is not a limitation: {:?}",
output.limitations
);
}
#[test]
fn excludes_typescript_ambient_declarations_from_the_executable_denominator() {
let source = concat!(
"declare global {\n",
" var Beacon: undefined | ((action: string) => void);\n",
"}\n",
"declare module 'virtual:feature' {\n",
" export const enabled: boolean;\n",
"}\n",
"declare const buildOnly: string;\n",
"export const live = 1;\n",
);
let output = instrument_direct_candidate(source, "src/ambient.ts").unwrap();
let statements = output
.points
.iter()
.filter(|point| point.kind == "statement")
.map(|point| point.source.as_str())
.collect::<Vec<_>>();
assert_eq!(statements, ["const live = 1;"]);
assert!(output.code.contains("const live = 1"));
}
fn decision_shape(source: &str, file: &str) -> String {
let output = instrument_candidate(source, file).unwrap();
let runtime = output.runtime.as_ref().expect("candidate runtime binding");
let call = format!("{}(", runtime.mcdc_end_v2);
let start = output.code.find(&call).expect("an outcome is recorded") + call.len();
let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
let code = output
.code
.replace(&format!("{call}{probe_file}, "), "END(");
let mut names: Vec<&str> = Vec::new();
let mut shaped = String::new();
let mut rest = code.as_str();
while let Some(at) = rest.find("_supercovMcdc") {
shaped.push_str(&rest[..at]);
let tail = &rest[at..];
if shaped.ends_with('_') {
shaped.push_str("_supercovMcdc");
rest = &tail["_supercovMcdc".len()..];
continue;
}
let length = tail
.find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
.unwrap_or(tail.len());
let name = &tail[..length];
let index = names
.iter()
.position(|known| *known == name)
.unwrap_or_else(|| {
names.push(name);
names.len() - 1
});
shaped.push_str(&format!("t{index}"));
rest = &tail[length..];
}
shaped.push_str(rest);
shaped
}
#[test]
fn a_decision_stays_the_test_it_was() {
let shaped = decision_shape(
concat!(
"export function pick(client: { id: number } | null, strict: boolean) {\n",
" if (client && strict) return client;\n",
" return null;\n",
"}\n",
"export function size(list: string[] | null) {\n",
" if (!list) { return 0; } else { return list.length; }\n",
"}\n",
"export function first(list: string[] | null) {\n",
" return list ? list[0] : undefined;\n",
"}\n",
),
"src/pick.ts",
);
for expected in [
"if ((t0 = 1, t2 = 0, t1 = client) && (t0 += 3, t2 = strict)) {\n\t\t\tEND(0, t0 + +!!t1 + 3 * +!!t2, true), t0 = 0;",
"\t\t}\n\t\tt0 && END(0, t0 + +!!t1 + 3 * +!!t2, false);\n\t}",
"if (t3 = 1, t4 = !list) {\n\t\tEND(1, t3 + +!!t4, true);",
"} else {\n\t\tEND(1, t3 + +!!t4, false);",
"return (t5 = 1, t6 = list) ? (void END(2, t5 + +!!t6, true), list[0]) : (void END(2, t5 + +!!t6, false), undefined);",
] {
assert!(shaped.contains(expected), "{expected}\n{shaped}");
}
}
#[test]
fn a_loop_records_its_last_test_after_itself() {
let shaped = decision_shape(
concat!(
"export function scan(limit: number) {\n",
" let i = 0;\n",
" walk: while (i < limit) { i++; if (i > 5) break walk; }\n",
" do i--; while (i > 0);\n",
" for (let j = 0; j < limit; j++) i += j;\n",
" return i;\n",
"}\n",
),
"src/scan.ts",
);
for expected in [
"{\n\t\twalk: while (t0 = 1, t1 = i < limit) {\n\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
"\t\t}\n\t\tt0 && END(0, t0 + +!!t1, false);\n\t}",
"{\n\t\tt4 = 0;\n\t\tdo {\n\t\t\tt4 && (END(2, t4 + +!!t5, true), t4 = 0);",
"} while (t4 = 1, t5 = i > 0);\n\t\tt4 && END(2, t4 + +!!t5, false);\n\t}",
"for (let j = 0; t6 = 1, t7 = j < limit; j++) {\n\t\t\tEND(3, t6 + +!!t7, true), t6 = 0;",
"\t\t}\n\t\tt6 && END(3, t6 + +!!t7, false);\n\t}",
] {
assert!(shaped.contains(expected), "{expected}\n{shaped}");
}
}
#[test]
fn a_decision_without_a_function_body_around_it_declares_its_own_temporaries() {
let shaped = decision_shape(
concat!(
"export function pick(\n",
" n: number,\n",
" kind = class { static label = 'none'; static { if (n > 1) this.label = 'some'; } },\n",
" tag = n > 5 ? 'high' : 'low',\n",
") {\n",
" return [kind.label, tag];\n",
"}\n",
),
"src/defaults.ts",
);
for expected in [
"static {\n\t\tlet t0, t1;\n",
"if (t0 = 1, t1 = n > 1) {\n\t\t\t\tEND(0, t0 + +!!t1, true), t0 = 0;",
"(() => {\n\tlet t2, t3;\n\treturn (t2 = 1, t3 = n > 5) ? (void END(1, t2 + +!!t3, true), \"high\") : (void END(1, t2 + +!!t3, false), \"low\");\n})()",
] {
assert!(shaped.contains(expected), "{expected}\n{shaped}");
}
}
#[test]
fn a_top_level_declaration_has_a_line_of_its_own_before_it() {
let output = instrument_candidate(
concat!(
"const one = 1;\n",
"function plain() { return one; }\n",
"class Box {}\n",
"function over(value: string): string;\n",
"function over(value: number): number;\n",
"function over(value: any) { return value; }\n",
"export function exported() { return plain(); }\n",
),
"src/declarations.ts",
)
.unwrap();
for expected in [
"\n;\nfunction plain() {",
"\n;\nclass Box {",
"\n;\nfunction over(value: string): string;\nfunction over(value: number): number;\nfunction over(value: any) {",
] {
assert!(
output.code.contains(expected),
"{expected}\n{}",
output.code
);
}
assert!(
!output.code.contains(";\nexport function"),
"{}",
output.code
);
}
#[test]
fn exposes_the_complete_probe_v2_instrumenter_contract() {
let output = instrument_candidate(SOURCE, "app/decide.ts").unwrap();
assert!(output.complete);
assert!(output.limitations.is_empty());
assert_eq!(output.supported_surface, "complete-js-instrumenter-v1");
let runtime = output.runtime.expect("candidate runtime binding");
assert!(output.code.contains(&runtime.mcdc_end_v2));
assert!(output.code.contains("_supercovMcdcFrame"));
assert!(output.code.contains("_supercovMcdcValue"));
assert_eq!(output.decisions.len(), 1);
assert!(output.points.iter().any(|point| point.kind == "statement"));
assert!(output.points.iter().any(|point| point.kind == "function"));
let allocator = Allocator::default();
let reparsed = Parser::new(
&allocator,
&output.code,
SourceType::from_path("app/decide.ts").unwrap(),
)
.parse();
assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
}
#[test]
fn explicit_type_only_imports_are_not_runtime_coverage_obligations() {
let source = concat!(
"import type { Session } from './types.ts';\n",
"import { type Row } from './rows.ts';\n",
"import './register.ts';\n",
"const value = 1;\n",
);
let output = instrument_candidate(source, "app/imports.ts").unwrap();
let statement_lines = output
.points
.iter()
.filter(|point| point.kind == "statement")
.map(|point| point.line)
.collect::<Vec<_>>();
assert_eq!(statement_lines, vec![2, 3, 4]);
}
#[test]
fn configured_import_elision_keeps_values_side_effects_and_statement_ids() {
let source = concat!(
"import { Logger } from './types.js';\n",
"import DefaultType from './default.js';\n",
"import * as Types from './namespace.js';\n",
"import { type Inline } from './inline.js';\n",
"import { run, type Config } from './mixed.js';\n",
"import './register.js';\n",
"import {} from './empty.js';\n",
"import { unused } from './unused.js';\n",
"export function main(logger: Logger, value: DefaultType, t: Types.Row) { return run(logger); }\n",
);
let preserved =
instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", false)
.unwrap();
let erased =
instrument_with_import_policy(source, "src/main.ts", "./capability.mjs", true).unwrap();
assert_eq!(
erased
.excluded_statements
.iter()
.map(|p| p.line)
.collect::<Vec<_>>(),
vec![1, 2, 3, 4]
);
let excluded = erased
.excluded_statements
.iter()
.map(|p| &p.id)
.collect::<HashSet<_>>();
let expected = preserved
.points
.iter()
.filter(|p| !excluded.contains(&p.id))
.collect::<Vec<_>>();
assert_eq!(erased.points.iter().collect::<Vec<_>>(), expected);
for line in [5, 6, 7, 8] {
assert!(erased.points.iter().any(|p| p.line == line));
}
let source = "import { Factory } from './types.js'; type T = typeof Factory; export const f = () => new Factory();";
let mixed = instrument_with_import_policy(source, "src/mixed.ts", "./capability.mjs", true)
.unwrap();
assert!(mixed.excluded_statements.is_empty());
}
type FirstMapping = Option<Option<(u32, u32)>>;
fn first_mappings(output: &CandidateOutput) -> Vec<(String, FirstMapping)> {
let map = oxc_sourcemap::SourceMap::from_json_string(
&output
.map
.as_ref()
.expect("instrumented copy has a map")
.to_string(),
)
.unwrap();
let tokens = map.get_tokens().collect::<Vec<_>>();
output
.code
.split('\n')
.enumerate()
.map(|(line, text)| {
let first = tokens
.iter()
.find(|token| token.get_dst_line() == line as u32)
.map(|token| {
token
.get_source_id()
.map(|_| (token.get_src_line(), token.get_src_col()))
});
(text.trim().to_string(), first)
})
.collect()
}
#[test]
fn probes_map_to_nothing_and_the_statements_around_them_to_their_own_starts() {
let source = "function f(list) {\n let total = 0;\n for (const item of list) {\n if (item > 1) total += item;\n else if (item === 0) continue;\n else total -= 1;\n }\n return total;\n}\nconst twice = (value) => value * 2;\n";
let output = instrument_candidate(source, "app/f.js").unwrap();
let lines = first_mappings(&output);
let runtime = output.runtime.as_ref().unwrap();
for (text, first) in &lines {
if text.contains(&format!("{}.clock.fast", runtime.probe_file_v2))
&& let Some(first) = first
{
assert_eq!(*first, None, "{text}");
}
}
let at = |needle: &str| {
lines
.iter()
.find(|(text, _)| text.starts_with(needle))
.unwrap_or_else(|| panic!("{needle} in {}", output.code))
.1
};
assert_eq!(at("for (const item of list)"), Some(Some((2, 2))));
assert_eq!(at("continue;"), Some(Some((4, 25))));
assert_eq!(at("total -= 1;"), Some(Some((5, 9))));
assert_eq!(at("return value * 2;"), Some(Some((9, 25))));
let program = lines
.iter()
.position(|(text, _)| text.starts_with("function f("))
.unwrap();
for (text, first) in &lines[program..] {
if text.starts_with('}') {
assert!(first.is_some(), "{text} in {}", output.code);
}
}
}
fn value_shape(source: &str, file: &str) -> String {
let output = instrument_candidate(source, file).unwrap();
let runtime = output.runtime.as_ref().expect("candidate runtime binding");
let call = format!("{}(", runtime.select_end2_v2);
let start = output.code.find(&call).expect("a value tree is recorded") + call.len();
let probe_file = &output.code[start..start + output.code[start..].find(',').unwrap()];
let mut code = output.code.clone();
for (helper, short) in [
(&runtime.select_end2_v2, "END2("),
(&runtime.select_end_v2, "END("),
(&runtime.select_next_v2, "NEXT("),
(&runtime.coverage_hit_v2, "HIT("),
(&runtime.select_right_v2, "RIGHT("),
(&runtime.select_short_v2, "SHORT("),
] {
code = code.replace(&format!("{helper}({probe_file}, "), short);
}
let mut names: Vec<&str> = Vec::new();
let mut shaped = String::new();
let mut rest = code.as_str();
while let Some(at) = rest.find("_supercovSelected") {
shaped.push_str(&rest[..at]);
let tail = &rest[at..];
let length = tail
.find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
.unwrap_or(tail.len());
let name = &tail[..length];
let index = names
.iter()
.position(|known| *known == name)
.unwrap_or_else(|| {
names.push(name);
names.len() - 1
});
shaped.push_str(&format!("s{index}"));
rest = &tail[length..];
}
shaped.push_str(rest);
shaped
}
fn selection_shapes(
leaves: usize,
next_leaf: &mut usize,
next_first: &mut usize,
) -> Vec<(SelectionShape, usize, usize)> {
if leaves == 1 {
let shape = SelectionShape::Leaf(*next_leaf);
return vec![(shape, *next_leaf + 1, *next_first)];
}
let mut shapes = Vec::new();
for left_leaves in 1..leaves {
for kind in 0..3 {
let first = *next_first;
let mut after_first = first + 10;
for (left, leaf_after_left, first_after_left) in
selection_shapes(left_leaves, next_leaf, &mut after_first)
{
let mut leaf = leaf_after_left;
let mut first_right = first_after_left;
for (right, leaf_after, first_after) in
selection_shapes(leaves - left_leaves, &mut leaf, &mut first_right)
{
shapes.push((
SelectionShape::Selection {
first,
kind,
left: Box::new(copy_shape(&left)),
right: Box::new(right),
},
leaf_after,
first_after,
));
}
}
}
}
shapes
}
fn copy_shape(shape: &SelectionShape) -> SelectionShape {
match shape {
SelectionShape::Leaf(leaf) => SelectionShape::Leaf(*leaf),
SelectionShape::Selection {
first,
kind,
left,
right,
} => SelectionShape::Selection {
first: *first,
kind: *kind,
left: Box::new(copy_shape(left)),
right: Box::new(copy_shape(right)),
},
}
}
fn leaf_steps(shape: &SelectionShape, path: &[usize], steps: &mut Vec<Vec<usize>>) {
match shape {
SelectionShape::Leaf(_) => steps.push(path.to_vec()),
SelectionShape::Selection {
first,
kind,
left,
right,
} => {
let mut left_path = vec![first * 4 + kind];
left_path.extend_from_slice(path);
let mut right_path = vec![first * 4 + 3];
right_path.extend_from_slice(path);
leaf_steps(left, &left_path, steps);
leaf_steps(right, &right_path, steps);
}
}
}
fn evaluate(shape: &SelectionShape, values: &[u8], evaluated: &mut Vec<usize>) -> u8 {
match shape {
SelectionShape::Leaf(leaf) => {
evaluated.push(*leaf);
values[*leaf]
}
SelectionShape::Selection {
kind, left, right, ..
} => {
let value = evaluate(left, values, evaluated);
let short = match kind {
0 => value == 2,
1 => value != 2,
_ => value != 0,
};
if short {
value
} else {
evaluate(right, values, evaluated)
}
}
}
}
fn record_steps(steps: &[usize], value: u8, recorded: &mut Vec<usize>) {
for step in steps {
let (first, code) = (step / 4, step % 4);
let truthy = usize::from(value == 2);
if code == 3 {
recorded.push(first + 2 + truthy);
} else if match code {
0 => value == 2,
1 => value != 2,
_ => value != 0,
} {
recorded.push(first + truthy);
} else {
break;
}
}
}
#[test]
fn a_value_tree_records_what_a_call_around_each_leaf_recorded() {
let mut checked = 0;
for leaves in 2..=5 {
for (shape, ..) in selection_shapes(leaves, &mut 0, &mut 0) {
let mut steps = Vec::new();
leaf_steps(&shape, &[], &mut steps);
let mut before = vec![Vec::new(); leaves];
shape.before(&mut before);
for combination in 0..3_usize.pow(leaves as u32) {
let values = (0..leaves)
.map(|leaf| (combination / 3_usize.pow(leaf as u32) % 3) as u8)
.collect::<Vec<_>>();
let mut evaluated = Vec::new();
evaluate(&shape, &values, &mut evaluated);
let mut each_leaf = Vec::new();
for leaf in &evaluated {
record_steps(&steps[*leaf], values[*leaf], &mut each_leaf);
}
let mut tree = Vec::new();
for pair in evaluated.windows(2) {
let deciders = &before[pair[1]];
let (_, points) = match &deciders[..] {
[only] => only,
several => several
.iter()
.find(|(leaf, _)| *leaf == pair[0])
.expect("the leaf before is one the table has"),
};
tree.extend(points);
}
let last = *evaluated.last().unwrap();
record_steps(&steps[last], values[last], &mut tree);
assert_eq!(tree, each_leaf, "{values:?} over {steps:?}");
assert!(
evaluated
.windows(2)
.all(|pair| before[pair[1]].iter().any(|(leaf, _)| *leaf == pair[0])),
"{values:?} over {steps:?}: {before:?}"
);
checked += 1;
}
}
}
assert!(checked > 100_000, "{checked}");
}
#[test]
fn a_constant_keeps_its_tree_and_a_named_chain_its_name() {
let shaped = value_shape(
concat!(
"export function text(value: string | number) {\n",
" const isText = typeof value === 'string' && value.length > 0;\n",
" if (isText) return value;\n",
" return null;\n",
"}\n",
"export const both = text(1) ?? text('a');\n",
"export class Owner {\n",
" pet?: { name: string };\n",
" named(other?: Owner) { return [other?.pet, this?.pet, this.pet?.name]; }\n",
"}\n",
),
"src/alias.ts",
);
for expected in [
"const isText = (s1 = 0, typeof value === \"string\") && (s1 = 1, value.length > 0);\n\tEND2(9, isText, s1);\n",
"export const both = (s0 = 0, text(1)) ?? (s0 = 1, text(\"a\"));\nEND2(13, both, s0);\n",
", other)?.pet",
", this)?.pet",
] {
assert!(shaped.contains(expected), "{expected}\n{shaped}");
}
assert!(shaped.contains(", this.pet)?.name"), "{shaped}");
assert!(!shaped.contains(", this.pet), this.pet)"), "{shaped}");
}
#[test]
fn a_value_tree_keeps_its_leaves_the_programs_own_expressions() {
let shaped = value_shape(
concat!(
"export function name(user: { name: string } | false) {\n",
" return user && user.name;\n",
"}\n",
"export function ok(total: number, name: string, force: boolean) {\n",
" return (total > 0 && name !== 'x') || !!force;\n",
"}\n",
"export function pick(a: number | null, b: number | null, c: number) {\n",
" return a ?? b ?? c;\n",
"}\n",
"export function held(list: string[] | null, fallback = list || []) {\n",
" return fallback;\n",
"}\n",
),
"src/values.ts",
);
for expected in [
"let s0: any;\n",
"return END2(10, (s0 = 0, user) && (s0 = 1, user.name), s0);",
"return END(0, (s1 = 0, total > 0) && (s1 = 1, name !== \"x\") || (NEXT(0, s1, 2), s1 = 2, !!force), s1);",
"return END(1, (s2 = 0, a) ?? (s2 = 1, b) ?? (HIT(24), s2 = 2, c), s2);",
"SHORT(30, list, 0) || RIGHT(30, [])",
] {
assert!(shaped.contains(expected), "{expected}\n{shaped}");
}
}
#[test]
fn logical_assignments_keep_their_operator_and_record_through_v2_points() {
let source = "export function fill(o, key) { o.name ||= 'anon'; o[key()] ??= 0; let named; named &&= function () {}; return o; }";
for file in ["app/fill.js", "app/fill.ts"] {
let output = instrument_candidate(source, file).unwrap();
let runtime = output.runtime.expect("candidate runtime binding");
assert!(!output.code.contains("SelectionFrame"), "{}", output.code);
assert!(output.code.contains("||= "), "{}", output.code);
assert!(output.code.contains("??= "), "{}", output.code);
assert!(output.code.contains("&&= "), "{}", output.code);
assert_eq!(
output
.code
.matches(&format!("{}(", runtime.select_assign_end_v2))
.count(),
3
);
assert_eq!(
output
.code
.matches(&format!("{}(", runtime.select_named_right_v2))
.count(),
1
);
let allocator = Allocator::default();
let reparsed = Parser::new(
&allocator,
&output.code,
SourceType::from_path(file).unwrap(),
)
.parse();
assert!(reparsed.errors.is_empty(), "{:?}", reparsed.errors);
}
}
#[test]
fn allocates_runtime_and_scratch_names_away_from_user_bindings() {
let source = "const __supercovMcdcEndV2 = 1, _supercovMcdcFrame1 = 2;\nif (a && b) work();";
let output = instrument_candidate(source, "app/collisions.js").unwrap();
let runtime = output.runtime.expect("candidate runtime binding");
assert_ne!(runtime.mcdc_end_v2, "__supercovMcdcEndV2");
assert!(!output.code.contains("let _supercovMcdcFrame1,"));
}
#[test]
fn instruments_wider_decisions_with_the_exact_v1_fallback() {
let predicate = (0..33)
.map(|index| format!("c{index}"))
.collect::<Vec<_>>()
.join(" && ");
let source = format!("if ({predicate}) work();");
let output = instrument_candidate(&source, "app/wide.js").unwrap();
assert_eq!(output.decisions[0].conditions.len(), 33);
let runtime = output.runtime.expect("candidate runtime binding");
assert!(!output.code.contains(&format!("{}(", runtime.mcdc_end_v2)));
assert!(output.code.contains(&format!("{}(", runtime.mcdc_begin)));
assert!(
output
.code
.contains(&format!("{}(", runtime.mcdc_condition))
);
assert!(output.code.contains(&format!("{}(", runtime.mcdc_end)));
}
#[test]
fn wraps_framework_request_exports_without_changing_the_public_api() {
for (file, source, export_prefix) in [
(
"app/routes/example.ts",
"export const loader = async ({ request }) => request.url;",
"export const loader = ",
),
(
"app/routes/example.ts",
"export async function action({ request }) { return request.method; }",
"export const action = ",
),
(
"app/api/items/route.ts",
"export function GET(request) { return Response.json({ url: request.url }); }",
"export const GET = ",
),
(
"app/routes/example.ts",
"export { generateAction as action } from './generateAction';",
"export const action = ",
),
(
"app/entry.server.tsx",
"export default async function handleRequest(request) { return request.url; }",
"export default ",
),
] {
let output = instrument_candidate(source, file).unwrap();
let runtime = output.runtime.as_ref().expect("runtime binding");
assert!(
output
.code
.contains(&format!("{export_prefix}{}(", runtime.with_request_phase)),
"{file}: {}",
output.code
);
assert!(
output.code.contains(&format!(
"withRequestPhase as {}",
runtime.with_request_phase
)),
"{file}: {}",
output.code
);
let allocator = Allocator::default();
let reparsed = Parser::new(
&allocator,
&output.code,
SourceType::from_path(file).unwrap(),
)
.parse();
assert!(reparsed.errors.is_empty(), "{file}: {:?}", reparsed.errors);
}
}
#[test]
fn parse_failures_are_explicit_and_never_claim_completeness() {
assert!(matches!(
analyze_candidate("if (", "broken.js"),
Err(CandidateError::Parse(errors)) if !errors.is_empty()
));
assert!(matches!(
analyze_candidate("let value = 1", "unknown.extension"),
Err(CandidateError::UnknownSourceType(_))
));
}
}