Skip to content

Commit

Permalink
feat: Also skip matching method calls
Browse files Browse the repository at this point in the history
  • Loading branch information
sourcefrog committed Nov 18, 2024
1 parent 3be76a8 commit 609d3aa
Showing 1 changed file with 60 additions and 15 deletions.
75 changes: 60 additions & 15 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,26 +324,37 @@ impl DiscoveryVisitor<'_> {

impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> {
fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
let _span = trace_span!("call", line = i.span().start().line, ?i).entered();
let _span = trace_span!("expr_call", line = i.span().start().line, ?i).entered();
if attrs_excluded(&i.attrs) {
return;
}
if !self.options.skip_calls.is_empty() {
if let Expr::Path(ExprPath { path, .. }) = &*i.func {
if let Some(last) = path.segments.last() {
let ident = &last.ident;
trace!(?ident, "call ident");
if self.options.skip_calls.iter().any(|s| ident == s) {
trace!("skip call to {ident}");
return;
}
}
if let Expr::Path(ExprPath { path, .. }) = &*i.func {
debug!(?path, "visit call");
if let Some(hit) = self
.options
.skip_calls
.iter()
.find(|s| path_ends_with(path, s))
{
trace!("skip call to {hit}");
return;
}
// TODO: Also visit_expr_method_call
}
syn::visit::visit_expr_call(self, i);
}

fn visit_expr_method_call(&mut self, i: &'ast syn::ExprMethodCall) {
let _span = trace_span!("expr_method_call", line = i.span().start().line, ?i).entered();
if attrs_excluded(&i.attrs) {
return;
}
if let Some(hit) = self.options.skip_calls.iter().find(|s| i.method == s) {
trace!("skip method call to {hit}");
return;
}
syn::visit::visit_expr_method_call(self, i);
}

/// Visit a source file.
fn visit_file(&mut self, i: &'ast File) {
// No trace here; it's created per file for the whole visitor
Expand Down Expand Up @@ -427,12 +438,12 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> {
}
let type_name = i.self_ty.to_pretty_string();
let name = if let Some((_, trait_path, _)) = &i.trait_ {
let trait_name = &trait_path.segments.last().unwrap().ident;
if trait_name == "Default" {
if path_ends_with(trait_path, "Default") {
// Can't think of how to generate a viable different default.
return;
}
format!("<impl {trait_name} for {type_name}>")
format!("<impl {trait} for {type_name}>", trait = trait_path.segments.last().unwrap().ident)
// TODO: trait = trait_path.to_pretty_string()) and update tests to match
} else {
type_name
};
Expand Down Expand Up @@ -719,6 +730,20 @@ fn path_is(path: &syn::Path, idents: &[&str]) -> bool {
path.segments.iter().map(|ps| &ps.ident).eq(idents.iter())
}

/// True if the path ends with this identifier.
///
/// This is used as a heuristic to match types without being sensitive to which
/// module they are in, or to match functions without being sensitive to which
/// type they might be associated with.
///
/// This does not check type arguments.
fn path_ends_with(path: &syn::Path, ident: &str) -> bool {
path.segments
.last()
.map(|s| s.ident == ident)
.unwrap_or(false)
}

/// True if the attribute contains `mutants::skip`.
///
/// This for example returns true for `#[mutants::skip] or `#[cfg_attr(test, mutants::skip)]`.
Expand Down Expand Up @@ -785,6 +810,26 @@ mod test {

use super::*;

#[test]
fn path_ends_with() {
use super::path_ends_with;
use syn::parse_quote;

let path = parse_quote! { foo::bar::baz };
assert!(path_ends_with(&path, "baz"));
assert!(!path_ends_with(&path, "bar"));
assert!(!path_ends_with(&path, "foo"));

let path = parse_quote! { baz };
assert!(path_ends_with(&path, "baz"));
assert!(!path_ends_with(&path, "bar"));

let path = parse_quote! { BTreeMap<K, V> };
assert!(path_ends_with(&path, "BTreeMap"));
assert!(!path_ends_with(&path, "V"));
assert!(!path_ends_with(&path, "K"));
}

/// We should not generate mutants that produce the same tokens as the
/// source.
#[test]
Expand Down

0 comments on commit 609d3aa

Please sign in to comment.