Repository: sourcefrog/cargo-mutants (rust)

Issue:
Support for proc-macro crates
I tried running `cargo-mutants` on a `proc-macro` crate and was surprised to see it found no mutants. When I turned log level to tracing I saw this in the logs:

```
DEBUG package{name=despatma-dependency-container}: skipping target "despatma_dependency_container" of kinds ["proc-macro"]
```

Is there a reason `proc-macro`s are skipped that I might be missing?

Pull request:
Mutate proc macro functions
- [x] Docs explaining that proc macro implementations can be mutated, but this only mutates the definitions and not the usage of macros
- [x] News
- [x] Maybe test that the testdata crate is fully covered?

Fixes #406 

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index a8e712e..a132871 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -123,6 +123,7 @@ exclude = [
     "testdata/override_dependency",
     "testdata/package-fails/",
     "testdata/patch_dependency",
+    "testdata/proc_macro",
     "testdata/relative_dependency",
     "testdata/replace_dependency",
     "testdata/small_well_tested",
diff --git a/NEWS.md b/NEWS.md
index fb8f620..e4122ef 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,9 @@
 # cargo-mutants changelog
 
+## Unreleased
+
+- New: Mutate `proc_macro` targets and functions.
+
 ## 24.9.0
 
 - Fixed: Avoid generating empty string elements in `ENCODED_RUSTFLAGS` when `--cap-lints` is set. In some situations these could cause a compiler error complaining about the empty argument.
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 2dd284f..70adbab 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -24,6 +24,7 @@
   - [Strict lints](lints.md)
 - [Generating mutants](mutants.md)
   - [Error values](error-values.md)
+  - [Macros](macros.md)
 - [Improving performance](performance.md)
   - [Parallelism](parallelism.md)
   - [Jobserver](jobserver.md)
diff --git a/book/src/macros.md b/book/src/macros.md
new file mode 100644
index 0000000..48b328b
--- /dev/null
+++ b/book/src/macros.md
@@ -0,0 +1,5 @@
+# Mutating code using macros
+
+cargo-mutants will mutate the contents of `#[proc_macro]` functions defined in the current crate, and run tests to see if those mutations are caught.
+
+cargo-mutants does not currently mutate calls to macros, or the expansion of a macro, or the definition of declarative `macro_rules` macros. As a result on code that is mostly produced by macro expansion it may not find many mutation opportunities.
diff --git a/src/workspace.rs b/src/workspace.rs
index f1f69a9..b5e6ee1 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -265,7 +265,12 @@ fn direct_package_sources(
 }
 
 fn should_mutate_target(target: &cargo_metadata::Target) -> bool {
-    target.kind.iter().any(|k| k.ends_with("lib") || k == "bin")
+    for kind in target.kind.iter() {
+        if kind == "bin" || kind == "proc-macro" || kind.ends_with("lib") {
+            return true;
+        }
+    }
+    false
 }
 
 /// Return the path of the workspace or package directory enclosing a given directory.


Test patch:
diff --git a/testdata/proc_macro/Cargo.toml b/testdata/proc_macro/Cargo.toml
new file mode 100644
index 0000000..11175ff
--- /dev/null
+++ b/testdata/proc_macro/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "cargo-mutants-testdata-proc-macro"
+version = "0.0.0"
+edition = "2021"
+license = "MIT"
+publish = false
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0"
diff --git a/testdata/proc_macro/README.md b/testdata/proc_macro/README.md
new file mode 100644
index 0000000..9bdc0e2
--- /dev/null
+++ b/testdata/proc_macro/README.md
@@ -0,0 +1,3 @@
+# `testdata/proc_macro`
+
+An example of mutating a proc macro, and catching those mutations from tests.
diff --git a/testdata/proc_macro/src/lib.rs b/testdata/proc_macro/src/lib.rs
new file mode 100644
index 0000000..598dac4
--- /dev/null
+++ b/testdata/proc_macro/src/lib.rs
@@ -0,0 +1,13 @@
+use std::iter::once;
+
+use proc_macro::{Literal, TokenStream, TokenTree};
+
+/// Count the number of items in a static array.
+#[proc_macro]
+pub fn static_len(item: TokenStream) -> TokenStream {
+    let count = item
+        .into_iter()
+        .filter(|tt| !matches!(tt, TokenTree::Punct(p) if p.as_char() == ','))
+        .count();
+    once(TokenTree::Literal(Literal::usize_unsuffixed(count))).collect()
+}
diff --git a/testdata/proc_macro/tests/macro.rs b/testdata/proc_macro/tests/macro.rs
new file mode 100644
index 0000000..5deeadb
--- /dev/null
+++ b/testdata/proc_macro/tests/macro.rs
@@ -0,0 +1,11 @@
+use cargo_mutants_testdata_proc_macro::static_len;
+
+#[test]
+fn static_len() {
+    assert_eq!(static_len!(2, 3, 4, 5), 4);
+}
+
+#[test]
+fn static_len_empty() {
+    assert_eq!(static_len!(), 0);
+}
diff --git a/tests/main.rs b/tests/main.rs
index c73ce78..0d881b4 100644
--- a/tests/main.rs
+++ b/tests/main.rs
@@ -185,6 +185,20 @@ fn cdylib_tree_is_well_tested() {
         }));
 }
 
+#[test]
+fn proc_macro_tree_is_well_tested() {
+    let tmp_src_dir = copy_of_testdata("proc_macro");
+    run()
+        .arg("mutants")
+        .args(["--no-times", "--no-shuffle", "-v", "-V"])
+        .current_dir(tmp_src_dir.path())
+        .assert()
+        .success()
+        .stdout(predicate::str::contains(
+            "2 mutants tested: 1 caught, 1 unviable",
+        ));
+}
+
 #[test]
 fn well_tested_tree_finds_no_problems() {
     let tmp_src_dir = copy_of_testdata("well_tested");
diff --git a/tests/snapshots/list__list_mutants_in_all_trees_as_json.snap b/tests/snapshots/list__list_mutants_in_all_trees_as_json.snap
index bac0a09..ebe6acb 100644
--- a/tests/snapshots/list__list_mutants_in_all_trees_as_json.snap
+++ b/tests/snapshots/list__list_mutants_in_all_trees_as_json.snap
@@ -4768,6 +4768,73 @@ expression: buf
 ]
 ```
 
+## testdata/proc_macro
+
+```json
+[
+  {
+    "file": "src/lib.rs",
+    "function": {
+      "function_name": "static_len",
+      "return_type": "-> TokenStream",
+      "span": {
+        "end": {
+          "column": 2,
+          "line": 13
+        },
+        "start": {
+          "column": 1,
+          "line": 5
+        }
+      }
+    },
+    "genre": "FnValue",
+    "package": "cargo-mutants-testdata-proc-macro",
+    "replacement": "Default::default()",
+    "span": {
+      "end": {
+        "column": 73,
+        "line": 12
+      },
+      "start": {
+        "column": 5,
+        "line": 8
+      }
+    }
+  },
+  {
+    "file": "src/lib.rs",
+    "function": {
+      "function_name": "static_len",
+      "return_type": "-> TokenStream",
+      "span": {
+        "end": {
+          "column": 2,
+          "line": 13
+        },
+        "start": {
+          "column": 1,
+          "line": 5
+        }
+      }
+    },
+    "genre": "UnaryOperator",
+    "package": "cargo-mutants-testdata-proc-macro",
+    "replacement": "",
+    "span": {
+      "end": {
+        "column": 23,
+        "line": 10
+      },
+      "start": {
+        "column": 22,
+        "line": 10
+      }
+    }
+  }
+]
+```
+
 ## testdata/relative_dependency
 
 ```json
@@ -9611,5 +9678,3 @@ expression: buf
   }
 ]
 ```
-
-
diff --git a/tests/snapshots/list__list_mutants_in_all_trees_as_text.snap b/tests/snapshots/list__list_mutants_in_all_trees_as_text.snap
index 2e8c992..59ed8b8 100644
--- a/tests/snapshots/list__list_mutants_in_all_trees_as_text.snap
+++ b/tests/snapshots/list__list_mutants_in_all_trees_as_text.snap
@@ -304,6 +304,13 @@ src/lib.rs:7:7: replace % with / in is_even
 src/lib.rs:7:7: replace % with + in is_even
 ```
 
+## testdata/proc_macro
+
+```
+src/lib.rs:8:5: replace static_len -> TokenStream with Default::default()
+src/lib.rs:10:22: delete ! in static_len
+```
+
 ## testdata/relative_dependency
 
 ```
@@ -523,5 +530,3 @@ main2/src/main.rs:10:5: replace triple_3 -> i32 with 0
 main2/src/main.rs:10:5: replace triple_3 -> i32 with 1
 main2/src/main.rs:10:5: replace triple_3 -> i32 with -1
 ```
-
-