1use std::collections::BTreeSet;
25
26use oxc_ast::ast::{CallExpression, Expression, Program, Statement, VariableDeclarator};
27use oxc_ast_visit::{Visit, walk};
28use oxc_span::GetSpan;
29
30use crate::{
31 Instrumented, MutantId, MutationError, MutationOptions, SourceKind, SpliceMap, SpliceSegment,
32 TextInput, instrument_text, line_column, line_starts,
33};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ScriptRole {
38 VueScript,
40 VueSetup,
42 SvelteModule,
45 SvelteInstance,
48 AstroFrontmatter,
51}
52
53impl ScriptRole {
54 const fn is_module_scope(self) -> bool {
56 matches!(self, Self::VueScript | Self::SvelteModule)
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct ScriptBlock {
63 pub span: (u32, u32),
65 pub kind: SourceKind,
67 pub role: ScriptRole,
69}
70
71pub fn instrument_blocks(
83 source: &str,
84 path: &str,
85 blocks: &[ScriptBlock],
86 options: &MutationOptions,
87) -> Result<Instrumented, MutationError> {
88 if u32::try_from(source.len()).is_err() {
89 return Err(MutationError::TooLarge);
90 }
91 let ordered = valid_blocks(source, blocks);
92 let module_scope = ordered.iter().any(|block| block.role.is_module_scope());
93 let starts = line_starts(source);
94 let mut seen_ids: BTreeSet<MutantId> = BTreeSet::new();
95 let mut output = Instrumented {
96 mutants: Vec::new(),
97 code: String::with_capacity(source.len() * 2),
98 map: SpliceMap::default(),
99 };
100 let mut cursor = 0;
101 for block in ordered {
102 copy_original(source, (cursor, block.span.0), &mut output);
103 let (start, end) = block.span;
104 let input = TextInput {
105 source: &source[start as usize..end as usize],
106 path,
107 kind: block.kind,
108 role: Some(block.role),
109 prelude: !module_scope || block.role.is_module_scope(),
110 };
111 let local_options = MutationOptions {
112 operators: options.operators.clone(),
113 ranges: options
114 .ranges
115 .as_ref()
116 .map(|ranges| local_ranges(ranges, block.span)),
117 };
118 let result = instrument_text(&input, &local_options, &mut seen_ids)?;
119 append_block(result, start, &starts, &mut output)?;
120 cursor = end;
121 }
122 copy_original(source, (cursor, source.len() as u32), &mut output);
123 output.mutants.sort_by_key(|mutant| mutant.span);
124 if u32::try_from(output.code.len()).is_err() {
125 return Err(MutationError::TooLarge);
126 }
127 Ok(output)
128}
129
130fn valid_blocks(source: &str, blocks: &[ScriptBlock]) -> Vec<ScriptBlock> {
134 let mut ordered: Vec<ScriptBlock> = blocks
135 .iter()
136 .copied()
137 .filter(|block| {
138 let (start, end) = (block.span.0 as usize, block.span.1 as usize);
139 start <= end && source.get(start..end).is_some()
140 })
141 .collect();
142 ordered.sort_by_key(|block| block.span);
143 let mut end = 0;
144 ordered.retain(|block| {
145 let keep = block.span.0 >= end;
146 if keep {
147 end = block.span.1;
148 }
149 keep
150 });
151 ordered
152}
153
154fn local_ranges(ranges: &[(u32, u32)], block: (u32, u32)) -> Vec<(u32, u32)> {
156 ranges
157 .iter()
158 .filter_map(|&(from, to)| {
159 let start = from.max(block.0);
160 let end = to.min(block.1);
161 (start < end).then(|| (start - block.0, end - block.0))
162 })
163 .collect()
164}
165
166fn copy_original(source: &str, span: (u32, u32), output: &mut Instrumented) {
167 let (from, to) = span;
168 if to <= from {
169 return;
170 }
171 output.map.segments.push(SpliceSegment {
172 generated_start: output.code.len() as u32,
173 original_start: from,
174 len: to - from,
175 });
176 output.code.push_str(&source[from as usize..to as usize]);
177}
178
179fn append_block(
182 block: Instrumented,
183 start: u32,
184 starts: &[u32],
185 output: &mut Instrumented,
186) -> Result<(), MutationError> {
187 let base = u32::try_from(output.code.len()).map_err(|_| MutationError::TooLarge)?;
188 output
189 .map
190 .segments
191 .extend(block.map.segments.into_iter().map(|segment| SpliceSegment {
192 generated_start: segment.generated_start + base,
193 original_start: segment.original_start + start,
194 len: segment.len,
195 }));
196 output.code.push_str(&block.code);
197 for mut mutant in block.mutants {
198 mutant.span = (mutant.span.0 + start, mutant.span.1 + start);
199 (mutant.line, mutant.column) = line_column(starts, mutant.span.0);
200 if let Some(function) = &mut mutant.enclosing_function {
201 function.span = (function.span.0 + start, function.span.1 + start);
202 }
203 output.mutants.push(mutant);
204 }
205 Ok(())
206}
207
208#[derive(Debug, Default)]
210pub struct Protected {
211 overlap: Vec<(u32, u32)>,
213 contain: Vec<(u32, u32)>,
215}
216
217impl Protected {
218 pub fn excludes(&self, span: (u32, u32)) -> bool {
220 self.overlap
221 .iter()
222 .any(|&(start, end)| start < span.1 && span.0 < end)
223 || self
224 .contain
225 .iter()
226 .any(|&(start, end)| span.0 <= start && end <= span.1)
227 }
228}
229
230const VUE_MACROS: [&str; 7] = [
234 "defineProps",
235 "defineEmits",
236 "defineExpose",
237 "defineModel",
238 "defineOptions",
239 "defineSlots",
240 "withDefaults",
241];
242
243const SVELTE_RUNES: [&str; 7] = [
245 "$state",
246 "$derived",
247 "$effect",
248 "$props",
249 "$bindable",
250 "$inspect",
251 "$host",
252];
253
254pub fn protected_spans(program: &Program<'_>, role: ScriptRole) -> Protected {
256 let mut protected = Protected::default();
257 match role {
258 ScriptRole::VueScript => {}
259 ScriptRole::VueSetup => {
260 let mut finder = CallFinder {
261 names: &VUE_MACROS,
262 declarators: true,
263 spans: Vec::new(),
264 };
265 finder.visit_program(program);
266 protected.overlap = finder.spans;
267 }
268 ScriptRole::SvelteModule | ScriptRole::SvelteInstance => {
269 let mut finder = CallFinder {
270 names: &SVELTE_RUNES,
271 declarators: false,
272 spans: Vec::new(),
273 };
274 finder.visit_program(program);
275 protected.contain = finder.spans;
276 }
277 ScriptRole::AstroFrontmatter => {
278 protected.overlap = program
279 .body
280 .iter()
281 .filter(|statement| {
282 matches!(
283 statement,
284 Statement::ExportDeclaration(_) | Statement::ExportDefaultDeclaration(_)
285 )
286 })
287 .map(|statement| {
288 let span = statement.span();
289 (span.start, span.end)
290 })
291 .collect();
292 }
293 }
294 protected
295}
296
297struct CallFinder {
300 names: &'static [&'static str],
301 declarators: bool,
305 spans: Vec<(u32, u32)>,
306}
307
308impl CallFinder {
309 fn matches(&self, callee: &Expression<'_>) -> bool {
310 let mut current = callee;
311 loop {
312 match current {
313 Expression::Identifier(identifier) => {
314 return self.names.contains(&identifier.name.as_str());
315 }
316 Expression::StaticMemberExpression(member) => current = &member.object,
317 Expression::ParenthesizedExpression(inner) => current = &inner.expression,
318 _ => return false,
319 }
320 }
321 }
322}
323
324impl<'a> Visit<'a> for CallFinder {
325 fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
326 let call = it
327 .init
328 .as_ref()
329 .and_then(|init| match init.without_parentheses() {
330 Expression::CallExpression(call) => Some(call),
331 _ => None,
332 });
333 if self.declarators && call.is_some_and(|call| self.matches(&call.callee)) {
334 self.spans.push((it.span.start, it.span.end));
335 }
336 walk::walk_variable_declarator(self, it);
337 }
338
339 fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
340 if self.matches(&it.callee) {
341 self.spans.push((it.span.start, it.span.end));
342 }
343 walk::walk_call_expression(self, it);
344 }
345}