use core::ops::Range;
use proc_macro2::Span;
use syn::spanned::Spanned as _;
use syn::visit::{self, Visit};
use syn::{Attribute, Expr, ForeignItem, ImplItem, Item, Stmt, TraitItem};
use crate::HashMap;
use crate::parse::SourceFile;
#[derive(Debug, Default)]
pub(super) struct Scopes {
spans: Vec<Range<usize>>,
pub(super) attributes: Vec<(Attribute, Range<usize>)>,
lines: HashMap<usize, Range<usize>>,
}
impl Scopes {
pub(super) fn of(file: &SourceFile) -> Self {
let mut collector = ScopeCollector {
scopes: Self::default(),
text_len: file.text().len(),
};
collector.visit_file(file.ast());
let mut scopes = collector.scopes;
scopes.sort_spans();
for span in &scopes.spans {
let line = file.line_of(span.start);
let entry = scopes.lines.entry(line).or_insert_with(|| span.clone());
if span.end > entry.end {
*entry = span.clone();
}
}
scopes
}
fn sort_spans(&mut self) {
self.spans
.sort_by(|left, right| left.start.cmp(&right.start).then_with(|| right.end.cmp(&left.end)));
}
pub(super) fn following(&self, offset: usize) -> Option<Range<usize>> {
let index = self.spans.partition_point(|span| span.start < offset);
let mut candidates = self.spans.iter().skip(index);
let first = candidates.next()?;
let start = first.start;
let mut best = first.clone();
for span in candidates {
if span.start != start {
break;
}
if span.end > best.end {
best = span.clone();
}
}
Some(best)
}
#[cfg(test)]
pub(super) fn from_spans(spans: Vec<Range<usize>>) -> Self {
let mut scopes = Self { spans, ..Self::default() };
scopes.sort_spans();
scopes
}
pub(super) fn enclosing_on_line(&self, line: usize) -> Option<Range<usize>> {
self.lines.get(&line).cloned()
}
}
fn admissible(range: &Range<usize>, text_len: usize) -> bool {
!range.is_empty() && range.end <= text_len
}
struct ScopeCollector {
scopes: Scopes,
text_len: usize,
}
impl ScopeCollector {
fn record(&mut self, span: Span) -> Option<Range<usize>> {
let range = span.byte_range();
if !admissible(&range, self.text_len) {
return None;
}
self.scopes.spans.push(range.clone());
Some(range)
}
fn record_attributes(&mut self, attributes: &[Attribute], span: Span) {
let Some(range) = self.record(span) else {
return;
};
for attribute in attributes {
self.scopes.attributes.push((attribute.clone(), range.clone()));
}
}
fn record_attributed_function_head(&mut self, attributes: &[Attribute], function: Span, item: Span) {
if attributes.is_empty() {
return;
}
let start = function.byte_range().start;
let end = item.byte_range().end;
let range = start..end;
if admissible(&range, self.text_len) {
self.scopes.spans.push(range);
}
}
}
#[expect(
clippy::renamed_function_params,
reason = "syn names every visitor parameter `i`, which says nothing about what it is"
)]
impl<'ast> Visit<'ast> for ScopeCollector {
fn visit_item(&mut self, node: &'ast Item) {
self.record_attributes(item_attributes(node), node.span());
if let Item::Fn(function) = node {
self.record_attributed_function_head(&function.attrs, function.sig.fn_token.span, node.span());
}
visit::visit_item(self, node);
}
fn visit_impl_item(&mut self, node: &'ast ImplItem) {
self.record_attributes(impl_item_attributes(node), node.span());
if let ImplItem::Fn(function) = node {
self.record_attributed_function_head(&function.attrs, function.sig.fn_token.span, node.span());
}
visit::visit_impl_item(self, node);
}
fn visit_trait_item(&mut self, node: &'ast TraitItem) {
self.record_attributes(trait_item_attributes(node), node.span());
if let TraitItem::Fn(function) = node {
self.record_attributed_function_head(&function.attrs, function.sig.fn_token.span, node.span());
}
visit::visit_trait_item(self, node);
}
fn visit_foreign_item(&mut self, node: &'ast ForeignItem) {
self.record_attributes(foreign_item_attributes(node), node.span());
if let ForeignItem::Fn(function) = node {
self.record_attributed_function_head(&function.attrs, function.sig.fn_token.span, node.span());
}
visit::visit_foreign_item(self, node);
}
fn visit_stmt(&mut self, node: &'ast Stmt) {
let _ = self.record(node.span());
visit::visit_stmt(self, node);
}
fn visit_expr(&mut self, node: &'ast Expr) {
let _ = self.record(node.span());
visit::visit_expr(self, node);
}
}
fn item_attributes(item: &Item) -> &[Attribute] {
match item {
Item::Const(node) => &node.attrs,
Item::Enum(node) => &node.attrs,
Item::ExternCrate(node) => &node.attrs,
Item::Fn(node) => &node.attrs,
Item::ForeignMod(node) => &node.attrs,
Item::Impl(node) => &node.attrs,
Item::Macro(node) => &node.attrs,
Item::Mod(node) => &node.attrs,
Item::Static(node) => &node.attrs,
Item::Struct(node) => &node.attrs,
Item::Trait(node) => &node.attrs,
Item::TraitAlias(node) => &node.attrs,
Item::Type(node) => &node.attrs,
Item::Union(node) => &node.attrs,
Item::Use(node) => &node.attrs,
_ => &[],
}
}
fn impl_item_attributes(item: &ImplItem) -> &[Attribute] {
match item {
ImplItem::Const(node) => &node.attrs,
ImplItem::Fn(node) => &node.attrs,
ImplItem::Macro(node) => &node.attrs,
ImplItem::Type(node) => &node.attrs,
_ => &[],
}
}
fn trait_item_attributes(item: &TraitItem) -> &[Attribute] {
match item {
TraitItem::Const(node) => &node.attrs,
TraitItem::Fn(node) => &node.attrs,
TraitItem::Macro(node) => &node.attrs,
TraitItem::Type(node) => &node.attrs,
_ => &[],
}
}
fn foreign_item_attributes(item: &ForeignItem) -> &[Attribute] {
match item {
ForeignItem::Fn(node) => &node.attrs,
ForeignItem::Macro(node) => &node.attrs,
ForeignItem::Static(node) => &node.attrs,
ForeignItem::Type(node) => &node.attrs,
_ => &[],
}
}
#[cfg(test)]
mod tests {
use syn::ItemFn;
use super::super::tests::{file, mutants_of};
use super::super::{directives, suppress};
use super::*;
fn suppressed(source: &str, mutators: &str) -> (usize, usize) {
let (parsed, mut mutants) = mutants_of(source, mutators);
let found = directives(&parsed).unwrap();
let count = suppress(&mut mutants, &found);
(count, mutants.len())
}
#[test]
fn a_comment_directive_suppresses_the_following_statement() {
let source = "fn f(a: i32, b: i32) {\n // #[gamma::skip(arith)]\n let x = a + b;\n let y = a - b;\n}";
let (count, total) = suppressed(source, "arith");
assert_eq!(total, 4);
assert_eq!(count, 2);
}
#[test]
fn a_comment_directive_above_a_function_covers_the_whole_function() {
let source = "// #[gamma::skip(arith)]\nfn f(a: i32, b: i32) -> i32 {\n let x = a + b;\n x - b\n}";
let (count, total) = suppressed(source, "arith");
assert_eq!(count, total);
assert!(total > 0);
}
#[test]
fn a_comment_between_attributes_and_an_impl_method_suppresses_its_whole_body_mutant() {
let source = "struct S;
impl S {
#[cfg_attr(test, mutants::skip)]
// #[gamma::skip(fn_value.none)]
pub fn next(&self) -> Option<i32> {
Some(1)
}
}";
let (count, total) = suppressed(source, "fn_value.none");
assert_eq!(total, 1);
assert_eq!(count, total);
}
#[test]
fn an_attribute_directive_covers_the_whole_function() {
let source = "#[gamma::skip(arith)]\nfn f(a: i32, b: i32) -> i32 { a + b }";
let (count, total) = suppressed(source, "arith");
assert_eq!(count, total);
assert!(total > 0);
}
#[test]
fn a_trailing_directive_governs_its_own_line() {
let source = "fn f(a: i32, b: i32) {\n let x = a + b; // #[gamma::skip(arith)]\n let y = a - b;\n}";
let (count, total) = suppressed(source, "arith");
assert_eq!(total, 4);
assert_eq!(count, 2);
}
#[test]
fn one_line_function_trailing_directive_prefers_the_widest_scope() {
let source = "fn f(a: i32, b: i32) -> i32 { a + b } // #[gamma::skip(arith)]";
let (count, total) = suppressed(source, "arith");
assert_eq!(count, total);
assert!(total > 0);
}
#[test]
fn following_prefers_the_widest_scope_at_the_earliest_start() {
let parsed = file("// directive\nfn f(a: i32, b: i32) -> i32 { a + b }\n");
let scopes = Scopes::of(&parsed);
let scope = scopes.following(0).unwrap();
assert_eq!(parsed.slice(&scope), "fn f(a: i32, b: i32) -> i32 { a + b }");
}
#[test]
fn directives_attach_to_impl_trait_and_module_scopes() {
let source = "trait T {
// #[gamma::skip(arith)]
fn f(&self) -> i32 { 1 + 1 }
}
struct S;
impl S {
// #[gamma::skip(arith)]
fn g(&self) -> i32 { 2 + 2 }
}
// #[gamma::skip(arith)]
impl T for S { fn f(&self) -> i32 { 3 + 3 } }
// #[gamma::skip(arith)]
mod m { pub fn h() -> i32 { 4 + 4 } }";
let (count, total) = suppressed(source, "arith");
assert_eq!(count, total);
assert!(total >= 8, "{total}");
}
#[test]
fn a_directive_above_a_struct_does_not_reach_the_function_below_it() {
let source = "// #[gamma::skip(arith)]\nstruct S(u32);\nfn f(a: i32, b: i32) -> i32 { a + b }";
let (count, total) = suppressed(source, "arith");
assert!(total > 0, "the fixture must offer something to suppress");
assert_eq!(count, 0, "the struct's directive must not reach the function");
}
#[test]
fn an_attribute_directive_on_a_trait_covers_its_default_methods() {
let source = "#[gamma::skip(arith)]\ntrait T {\n fn f(&self) -> i32 { 1 + 1 }\n fn g(&self) -> i32 { 2 + 2 }\n}";
let (count, total) = suppressed(source, "arith");
assert!(total > 0);
assert_eq!(count, total);
}
#[test]
fn a_comment_directive_above_a_trait_governs_the_whole_trait() {
let source = "// #[gamma::skip(arith)]\ntrait T {\n fn f(&self) -> i32 { 1 + 1 }\n fn g(&self) -> i32 { 2 + 2 }\n}";
let (count, total) = suppressed(source, "arith");
assert!(total > 0);
assert_eq!(count, total);
}
#[test]
fn the_widest_span_starting_on_a_line_wins() {
let source = "fn a() { let _ = 1 + 1; } fn bbbbb() { let _ = 2 + 2; let _ = 3 + 3; }\n";
let parsed = file(source);
let scopes = Scopes::of(&parsed);
let governing = scopes.enclosing_on_line(1).expect("a span on the only line");
assert_eq!(governing.end, source.trim_end().len());
}
#[test]
fn the_outermost_span_at_a_shared_start_wins_in_either_order() {
let inner_first = Scopes::from_spans(vec![0..10, 0..40, 0..25]);
let outer_first = Scopes::from_spans(vec![0..40, 0..25, 0..10]);
assert_eq!(inner_first.following(0), Some(0..40));
assert_eq!(outer_first.following(0), Some(0..40));
}
#[test]
fn the_nearest_span_after_the_offset_wins() {
let scopes = Scopes::from_spans(vec![50..60, 20..30, 5..8]);
assert_eq!(scopes.following(10), Some(20..30));
assert_eq!(scopes.following(61), None);
}
#[test]
fn among_spans_sharing_a_start_the_widest_is_chosen() {
let scopes = Scopes::from_spans(vec![5..8, 5..20, 5..12]);
assert_eq!(scopes.following(0), Some(5..20));
}
#[test]
fn an_offset_on_a_span_start_selects_that_span() {
let scopes = Scopes::from_spans(vec![10..20, 30..40]);
assert_eq!(scopes.following(10), Some(10..20));
}
#[test]
fn an_offset_past_the_last_span_start_governs_nothing() {
let scopes = Scopes::from_spans(vec![10..20, 30..40]);
assert_eq!(scopes.following(41), None);
}
#[test]
fn an_empty_scope_set_governs_nothing() {
let scopes = Scopes::from_spans(vec![]);
assert_eq!(scopes.following(0), None);
}
#[test]
fn nested_spans_sharing_a_start_resolve_to_the_outermost() {
let scopes = Scopes::from_spans(vec![0..5, 0..100, 0..50, 10..20]);
assert_eq!(scopes.following(0), Some(0..100));
}
#[test]
fn many_spans_resolve_the_widest_at_the_earliest_start_at_every_offset() {
let mut spans = Vec::new();
for group in 0..1_000_usize {
let start = group * 10;
spans.push(start..start + 5);
spans.push(start..start + 8);
spans.push(start..start + 2);
}
let scopes = Scopes::from_spans(spans);
assert_eq!(scopes.following(0), Some(0..8));
assert_eq!(scopes.following(1), Some(10..18));
assert_eq!(scopes.following(5_001), Some(5_010..5_018));
assert_eq!(scopes.following(9_990), Some(9_990..9_998));
assert_eq!(scopes.following(9_991), None);
}
#[test]
fn an_empty_or_overrunning_span_is_not_a_scope() {
assert!(!admissible(&(5..5), 100));
assert!(!admissible(&(90..120), 100));
assert!(admissible(&(90..100), 100));
}
#[test]
fn the_outermost_span_from_a_real_parse_is_the_governing_one() {
let source = "fn f() {\n let _ = 1;\n}\n";
let parsed = file(source);
let scopes = Scopes::of(&parsed);
let governing = scopes.following(0).expect("a span after the start of the file");
assert_eq!(governing, 0..source.trim_end().len());
}
#[test]
fn a_span_reaching_past_the_recorded_file_end_is_dropped_by_the_collector() {
let source = "fn f() { let _ = 1; }";
let item: ItemFn = syn::parse_str(source).expect("the fixture parses");
let mut collector = ScopeCollector {
scopes: Scopes::default(),
text_len: 3,
};
assert!(collector.record(item.span()).is_none());
assert!(collector.scopes.spans.is_empty(), "an inadmissible span must not be recorded");
}
#[test]
fn attributes_on_a_span_reaching_past_the_recorded_file_end_are_dropped_too() {
let source = "#[gamma::skip(arith)]\nfn f() { let _ = 1; }";
let item: ItemFn = syn::parse_str(source).expect("the fixture parses");
let mut collector = ScopeCollector {
scopes: Scopes::default(),
text_len: 3,
};
collector.record_attributes(&item.attrs, item.span());
assert!(
collector.scopes.attributes.is_empty(),
"an inadmissible span's attributes must not survive"
);
}
}