Repository: ast-grep/ast-grep (rust)

Issue:
[feature] Named fixes and multiple selections
# ⭐ Suggestion

I am working on turbopack where we are performing a code migration. I'd like a lint with two fixes, either do a codemod to update a type, or insert a comment above the item to ignore it. I know we can use ast-grep-ignore for this however we'd like to enforce some things such as an explainer as well as automatically inserting the comment.

General support for yielding a few options to pick between would resolve this case and make the tool much more flexible. The problem of course would be knowing which fix to apply when doing blanket rewrites. I think nominating one of the options as the 'default' is probably good enough in this case.

# 💻 Use Cases

Disable lints with custom comments / handling for our particular use-case, but, more generally, I can see a number of ways that this could be useful.


Pull request description:
feat: multiple fixer support 
fix #1603 

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for rules with multiple fixers, allowing a rule to specify and use more than one fixer.
  - Introduced an optional title field for fixers to provide descriptive labels.

- **Bug Fixes**
  - Improved handling and validation of fixers across configuration, scanning, and rewriting processes.

- **Refactor**
  - Updated internal logic and APIs to treat fixers as collections instead of single values, ensuring consistent handling throughout the application.
  - Enhanced code actions to support multiple fixes with titles for better user clarity.
  - Improved note rendering for rules to support richer formatting in printed outputs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Patch:
diff --git a/crates/cli/src/print/colored_print.rs b/crates/cli/src/print/colored_print.rs
index 478a8712..d8dfe63b 100644
--- a/crates/cli/src/print/colored_print.rs
+++ b/crates/cli/src/print/colored_print.rs
@@ -269,7 +269,8 @@ impl PrintProcessor<Buffer> for ColoredProcessor {
           .print_diff(source, &new_str, writer, context)?;
       }
       if matches!(display_style, DisplayStyle::Medium | DisplayStyle::Rich) {
-        if let Some(note) = &rule.note {
+        let notes = self.markdown.render_note(rule);
+        if let Some(note) = notes {
           writeln!(writer, "{}", self.styles.rule.note.paint("Note:"))?;
           writeln!(writer, "{note}")?;
         }
diff --git a/crates/cli/src/print/interactive_print.rs b/crates/cli/src/print/interactive_print.rs
index f4bd481e..9995f115 100644
--- a/crates/cli/src/print/interactive_print.rs
+++ b/crates/cli/src/print/interactive_print.rs
@@ -402,7 +402,7 @@ rule:
 fix: ($B, lifecycle.update(['$A']))",
     );
     let mut matcher = config.matcher;
-    let fixer = matcher.fixer.take().unwrap();
+    let fixer = matcher.fixer.remove(0);
     let diffs = make_diffs(&root, matcher, &fixer);
     let ret = apply_rewrite(diffs);
     assert_eq!(ret, "let a = () => (c++, lifecycle.update(['c']))");
diff --git a/crates/cli/src/scan.rs b/crates/cli/src/scan.rs
index 63b229ba..a5b2bfbe 100644
--- a/crates/cli/src/scan.rs
+++ b/crates/cli/src/scan.rs
@@ -304,7 +304,7 @@ fn match_rule_diff_on_file<T>(
   let diffs = matches
     .into_iter()
     .filter_map(|(rule, m)| {
-      let fix = rule.matcher.fixer.as_ref()?;
+      let fix = rule.matcher.fixer.first()?;
       let diff = Diff::generate(m, &rule.matcher, fix);
       Some((diff, rule))
     })
@@ -321,7 +321,7 @@ fn match_rule_on_file<T>(
   processor: &impl PrintProcessor<T>,
 ) -> Result<T> {
   let file = SimpleFile::new(path.to_string_lossy(), file_content);
-  let processed = if let Some(fixer) = &rule.matcher.fixer {
+  let processed = if let Some(fixer) = &rule.matcher.fixer.first() {
     let diffs = matches
       .into_iter()
       .map(|m| (Diff::generate(m, &rule.matcher, fixer), rule))
diff --git a/crates/cli/src/verify/snapshot.rs b/crates/cli/src/verify/snapshot.rs
index 003635e6..c7b1a58d 100644
--- a/crates/cli/src/verify/snapshot.rs
+++ b/crates/cli/src/verify/snapshot.rs
@@ -95,7 +95,7 @@ impl TestSnapshot {
       .into_iter()
       .map(LabelSnapshot::from)
       .collect();
-    let Some(fix) = &rule_config.matcher.fixer else {
+    let Some(fix) = rule_config.matcher.fixer.first() else {
       return Ok(Some(Self {
         fixed: None,
         labels,
diff --git a/crates/config/src/check_var.rs b/crates/config/src/check_var.rs
index b7751207..76a7a861 100644
--- a/crates/config/src/check_var.rs
+++ b/crates/config/src/check_var.rs
@@ -1,4 +1,4 @@
-use crate::fixer::Fixer;
+use crate::fixer::{Fixer, FixerError};
 use crate::rule::referent_rule::RuleRegistration;
 use crate::rule::Rule;
 use crate::rule_config::RuleConfigError;
@@ -23,7 +23,7 @@ pub fn check_rule_with_hint<'r>(
   utils: &'r RuleRegistration,
   constraints: &'r HashMap<String, Rule>,
   transform: &'r Option<Transform>,
-  fixer: &Option<Fixer>,
+  fixer: &Vec<Fixer>,
   hint: CheckHint<'r>,
 ) -> RResult<()> {
   match hint {
@@ -37,6 +37,9 @@ pub fn check_rule_with_hint<'r>(
     }
     // upper_vars is needed to check metavar defined in containing vars
     CheckHint::Rewriter(upper_vars) => {
+      if fixer.is_empty() {
+        return Err(RuleCoreError::Fixer(FixerError::InvalidRewriter));
+      }
       check_utils_defined(rule, constraints)?;
       check_vars_in_rewriter(rule, utils, constraints, transform, fixer, upper_vars)?;
     }
@@ -49,7 +52,7 @@ fn check_vars_in_rewriter<'r>(
   utils: &'r RuleRegistration,
   constraints: &'r HashMap<String, Rule>,
   transform: &'r Option<Transform>,
-  fixer: &Option<Fixer>,
+  fixer: &Vec<Fixer>,
   upper_var: &HashSet<&str>,
 ) -> RResult<()> {
   let vars = get_vars_from_rules(rule, utils);
@@ -75,7 +78,7 @@ fn check_vars<'r>(
   utils: &'r RuleRegistration,
   constraints: &'r HashMap<String, Rule>,
   transform: &'r Option<Transform>,
-  fixer: &Option<Fixer>,
+  fixer: &Vec<Fixer>,
 ) -> RResult<()> {
   let vars = get_vars_from_rules(rule, utils);
   let vars = check_var_in_constraints(vars, constraints)?;
@@ -140,13 +143,12 @@ fn check_var_in_transform<'r>(
   Ok(vars)
 }
 
-fn check_var_in_fix(vars: HashSet<&str>, fixer: &Option<Fixer>) -> RResult<()> {
-  let Some(fixer) = fixer else {
-    return Ok(());
-  };
-  for var in fixer.used_vars() {
-    if !vars.contains(&var) {
-      return Err(RuleCoreError::UndefinedMetaVar(var.to_string(), "fix"));
+fn check_var_in_fix(vars: HashSet<&str>, fixers: &Vec<Fixer>) -> RResult<()> {
+  for fixer in fixers {
+    for var in fixer.used_vars() {
+      if !vars.contains(&var) {
+        return Err(RuleCoreError::UndefinedMetaVar(var.to_string(), "fix"));
+      }
     }
   }
   Ok(())
diff --git a/crates/config/src/fixer.rs b/crates/config/src/fixer.rs
index 8199506e..a46f5d00 100644
--- a/crates/config/src/fixer.rs
+++ b/crates/config/src/fixer.rs
@@ -18,6 +18,7 @@ use std::ops::Range;
 pub enum SerializableFixer {
   Str(String),
   Config(Box<SerializableFixConfig>),
+  List(Vec<SerializableFixConfig>),
 }
 
 #[derive(Serialize, Deserialize, Clone, JsonSchema)]
@@ -28,6 +29,8 @@ pub struct SerializableFixConfig {
   expand_end: Maybe<Relation>,
   #[serde(default, skip_serializing_if = "Maybe::is_absent")]
   expand_start: Maybe<Relation>,
+  #[serde(skip_serializing_if = "Option::is_none")]
+  title: Option<String>,
 }
 
 #[derive(Debug, Error)]
@@ -36,6 +39,10 @@ pub enum FixerError {
   InvalidTemplate(#[from] TemplateFixError),
   #[error("Fixer expansion contains invalid rule.")]
   WrongExpansion(#[from] RuleSerializeError),
+  #[error("Rewriter must have exactly one fixer.")]
+  InvalidRewriter,
+  #[error("Fixer in list must have title.")]
+  MissingTitle,
 }
 
 struct Expansion {
@@ -62,6 +69,7 @@ pub struct Fixer {
   template: TemplateFix,
   expand_start: Option<Expansion>,
   expand_end: Option<Expansion>,
+  title: Option<String>,
 }
 
 impl Fixer {
@@ -74,6 +82,7 @@ impl Fixer {
       template: fix,
       expand_end,
       expand_start,
+      title,
     } = serialized;
     let expand_start = Expansion::parse(expand_start, env)?;
     let expand_end = Expansion::parse(expand_end, env)?;
@@ -87,6 +96,7 @@ impl Fixer {
       template,
       expand_start,
       expand_end,
+      title: title.clone(),
     })
   }
 
@@ -94,11 +104,31 @@ impl Fixer {
     fixer: &SerializableFixer,
     env: &DeserializeEnv<L>,
     transform: &Option<HashMap<String, Transformation>>,
-  ) -> Result<Self, FixerError> {
-    match fixer {
+  ) -> Result<Vec<Self>, FixerError> {
+    let ret = match fixer {
       SerializableFixer::Str(fix) => Self::with_transform(fix, env, transform),
       SerializableFixer::Config(cfg) => Self::do_parse(cfg, env, transform),
-    }
+      SerializableFixer::List(list) => {
+        return Self::parse_list(list, env, transform);
+      }
+    };
+    Ok(vec![ret?])
+  }
+
+  fn parse_list<L: Language>(
+    list: &[SerializableFixConfig],
+    env: &DeserializeEnv<L>,
+    transform: &Option<HashMap<String, Transformation>>,
+  ) -> Result<Vec<Self>, FixerError> {
+    list
+      .iter()
+      .map(|cfg| {
+        if cfg.title.is_none() {
+          return Err(FixerError::MissingTitle);
+        }
+        Self::do_parse(cfg, env, transform)
+      })
+      .collect()
   }
 
   pub(crate) fn with_transform<L: Language>(
@@ -116,6 +146,7 @@ impl Fixer {
       template,
       expand_end: None,
       expand_start: None,
+      title: None,
     })
   }
 
@@ -125,9 +156,14 @@ impl Fixer {
       template,
       expand_start: None,
       expand_end: None,
+      title: None,
     })
   }
 
+  pub fn title(&self) -> Option<&str> {
+    self.title.as_deref()
+  }
+
   pub(crate) fn used_vars(&self) -> HashSet<&str> {
     self.template.used_vars()
   }
@@ -203,6 +239,13 @@ mod test {
     assert!(matches!(fixer, SerializableFixer::Str(_)));
   }
 
+  fn parse(config: SerializableFixConfig) -> Result<Fixer, FixerError> {
+    let config = SerializableFixer::Config(Box::new(config));
+    let env = DeserializeEnv::new(TypeScript::Tsx);
+    let fixer = Fixer::parse(&config, &env, &Some(Default::default()))?.remove(0);
+    Ok(fixer)
+  }
+
   #[test]
   fn test_deserialize_object() -> Result<(), serde_yaml::Error> {
     let src = "{template: 'abc', expandEnd: {regex: ',', stopBy: neighbor}}";
@@ -226,10 +269,9 @@ mod test {
       expand_end: Maybe::Present(relation),
       expand_start: Maybe::Absent,
       template: "abcd".to_string(),
+      title: None,
     };
-    let config = SerializableFixer::Config(Box::new(config));
-    let env = DeserializeEnv::new(TypeScript::Tsx);
-    let ret = Fixer::parse(&config, &env, &Some(Default::default()))?;
+    let ret = parse(config)?;
     assert!(ret.expand_start.is_none());
     assert!(ret.expand_end.is_some());
     assert!(matches!(ret.template, TemplateFix::Textual(_)));
@@ -240,7 +282,7 @@ mod test {
   fn test_parse_str() -> Result<(), FixerError> {
     let config = SerializableFixer::Str("abcd".to_string());
     let env = DeserializeEnv::new(TypeScript::Tsx);
-    let ret = Fixer::parse(&config, &env, &Some(Default::default()))?;
+    let ret = Fixer::parse(&config, &env, &None)?.remove(0);
     assert!(ret.expand_end.is_none());
     assert!(ret.expand_start.is_none());
     assert!(matches!(ret.template, TemplateFix::Textual(_)));
@@ -254,10 +296,9 @@ mod test {
       expand_end: Maybe::Present(expand_end),
       expand_start: Maybe::Absent,
       template: "var $A = 456".to_string(),
+      title: None,
     };
-    let config = SerializableFixer::Config(Box::new(config));
-    let env = DeserializeEnv::new(TypeScript::Tsx);
-    let fixer = Fixer::parse(&config, &env, &Some(Default::default()))?;
+    let fixer = parse(config)?;
     let grep = TypeScript::Tsx.ast_grep("let a = 123");
     let node = grep.root().find("let $A = 123").expect("should found");
     let edit = fixer.generate_replacement(&node);
@@ -273,10 +314,9 @@ mod test {
       expand_end: Maybe::Present(expand_end),
       expand_start: Maybe::Absent,
       template: "c: 456".to_string(),
+      title: None,
     };
-    let config = SerializableFixer::Config(Box::new(config));
-    let env = DeserializeEnv::new(TypeScript::Tsx);
-    let fixer = Fixer::parse(&config, &env, &Some(Default::default()))?;
+    let fixer = parse(config)?;
     let grep = TypeScript::Tsx.ast_grep("var a = { b: 123, }");
     let matcher = KindMatcher::new("pair", TypeScript::Tsx);
     let node = grep.root().find(&matcher).expect("should found");
@@ -287,4 +327,27 @@ mod test {
     assert_eq!(edit.deleted_length, 7);
     Ok(())
   }
+
+  #[test]
+  fn test_fixer_list() -> Result<(), FixerError> {
+    let config: SerializableFixer = from_str(
+      r"
+- { template: 'abc', title: 'fixer 1'}
+- { template: 'def', title: 'fixer 2'}",
+    )
+    .expect("should parse");
+    let env = DeserializeEnv::new(TypeScript::Tsx);
+    let fixers = Fixer::parse(&config, &env, &Some(Default::default()))?;
+    assert_eq!(fixers.len(), 2);
+    let config: SerializableFixer = from_str(
+      r"
+- { template: 'abc', title: 'fixer 1'}
+- { template: 'def'}",
+    )
+    .expect("should parse");
+    let env = DeserializeEnv::new(TypeScript::Tsx);
+    let ret = Fixer::parse(&config, &env, &Some(Default::default()));
+    assert!(ret.is_err());
+    Ok(())
+  }
 }
diff --git a/crates/config/src/rule_config.rs b/crates/config/src/rule_config.rs
index 63e6c4c8..1785945f 100644
--- a/crates/config/src/rule_config.rs
+++ b/crates/config/src/rule_config.rs
@@ -223,13 +223,13 @@ impl<L: Language> RuleConfig<L> {
     let bytes = parsed.generate_replacement(node);
     <D::Source as Content>::encode_bytes(&bytes).to_string()
   }
-  pub fn get_fixer(&self) -> Result<Option<Fixer>, RuleConfigError> {
+  pub fn get_fixer(&self) -> Result<Vec<Fixer>, RuleConfigError> {
     if let Some(fix) = &self.fix {
       let env = self.matcher.get_env(self.language.clone());
       let parsed = Fixer::parse(fix, &env, &self.transform).map_err(RuleCoreError::Fixer)?;
-      Ok(Some(parsed))
+      Ok(parsed)
     } else {
-      Ok(None)
+      Ok(vec![])
     }
   }
   pub fn get_labels<'t, D: Doc>(&self, node: &NodeMatch<'t, D>) -> Vec<Label<'_, 't, D>> {
@@ -433,7 +433,7 @@ test-rule:
     let mut config = get_matches_config();
     config.fix = Some(from_str("string!!").unwrap());
     let rule = RuleConfig::try_from(config, &globals).unwrap();
-    let fixer = rule.get_fixer().unwrap().unwrap();
+    let fixer = rule.get_fixer().unwrap().remove(0);
     let grep = TypeScript::Tsx.ast_grep("some(123)");
     let nm = grep.root().find(&rule.matcher).unwrap();
     let replacement = fixer.generate_replacement(&nm);
diff --git a/crates/config/src/rule_core.rs b/crates/config/src/rule_core.rs
index 889d3639..4c85f0e6 100644
--- a/crates/config/src/rule_core.rs
+++ b/crates/config/src/rule_core.rs
@@ -87,12 +87,12 @@ impl SerializableRuleCore {
     Ok(constraints)
   }
 
-  fn get_fixer<L: Language>(&self, env: &DeserializeEnv<L>) -> RResult<Option<Fixer>> {
+  fn get_fixer<L: Language>(&self, env: &DeserializeEnv<L>) -> RResult<Vec<Fixer>> {
     if let Some(fix) = &self.fix {
       let parsed = Fixer::parse(fix, env, &self.transform)?;
-      Ok(Some(parsed))
+      Ok(parsed)
     } else {
-      Ok(None)
+      Ok(vec![])
     }
   }
 
@@ -142,7 +142,7 @@ pub struct RuleCore {
   constraints: HashMap<String, Rule>,
   kinds: Option<BitSet>,
   pub(crate) transform: Option<Transform>,
-  pub fixer: Option<Fixer>,
+  pub fixer: Vec<Fixer>,
   // this is required to hold util rule reference
   registration: RuleRegistration,
 }
@@ -180,7 +180,7 @@ impl RuleCore {
   }
 
   #[inline]
-  pub fn with_fixer(self, fixer: Option<Fixer>) -> Self {
+  pub fn with_fixer(self, fixer: Vec<Fixer>) -> Self {
     Self { fixer, ..self }
   }
 
@@ -258,7 +258,7 @@ impl Default for RuleCore {
       constraints: HashMap::default(),
       kinds: None,
       transform: None,
-      fixer: None,
+      fixer: vec![],
       registration: RuleRegistration::default(),
     }
   }
diff --git a/crates/config/src/transform/rewrite.rs b/crates/config/src/transform/rewrite.rs
index 5d9ed7a4..3c2e83f7 100644
--- a/crates/config/src/transform/rewrite.rs
+++ b/crates/config/src/transform/rewrite.rs
@@ -115,7 +115,7 @@ fn replace_one<'n, D: Doc>(
       // in future, we can use the explict `expose` to control env inheritance
       if let Some(n) = rule.do_match(child.clone(), &mut env, Some(ctx.enclosing_env)) {
         let nm = NodeMatch::new(n, env.into_owned());
-        edits.push(nm.make_edit(rule, rule.fixer.as_ref().expect("rewriter must have fix")));
+        edits.push(nm.make_edit(rule, rule.fixer.first().expect("rewriter must have fix")));
         // stop at first fix, skip duplicate fix
         break;
       }
diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs
index 80652a5a..7a64d978 100644
--- a/crates/lsp/src/lib.rs
+++ b/crates/lsp/src/lib.rs
@@ -421,7 +421,8 @@ impl<L: LSPLang> Backend<L> {
           return None;
         }
         let rewrite_data = RewriteData::from_value(d.data?)?;
-        let edit = TextEdit::new(d.range, rewrite_data.fixed);
+        let fixed = rewrite_data.fixers.first()?.fixed.to_string();
+        let edit = TextEdit::new(d.range, fixed);
         last = d.range.end;
         Some(edit)
       })
@@ -478,6 +479,7 @@ impl<L: LSPLang> Backend<L> {
           .unwrap_or(false)
       })
       .filter_map(|d| diagnostic_to_code_action(&text_doc, d))
+      .flatten()
       .map(CodeActionOrCommand::from)
       .collect();
     Some(response)
diff --git a/crates/lsp/src/utils.rs b/crates/lsp/src/utils.rs
index 076ff4ec..6770bf30 100644
--- a/crates/lsp/src/utils.rs
+++ b/crates/lsp/src/utils.rs
@@ -13,8 +13,14 @@ use std::collections::HashMap;
 use std::str::FromStr;
 
 #[derive(Serialize, Deserialize)]
-pub struct RewriteData {
+pub struct OneFix {
+  pub title: Option<String>,
   pub fixed: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RewriteData {
+  pub fixers: Vec<OneFix>,
   // maybe we should have fixed range
 }
 
@@ -27,37 +33,58 @@ impl RewriteData {
     node_match: &NodeMatch<StrDoc<L>>,
     rule: &RuleConfig<L>,
   ) -> Option<Self> {
-    let fixer = rule.matcher.fixer.as_ref()?;
-    let edit = node_match.replace_by(fixer);
-    let rewrite = String::from_utf8(edit.inserted_text).ok()?;
-    Some(Self { fixed: rewrite })
+    let fixers = rule
+      .matcher
+      .fixer
+      .iter()
+      .filter_map(|fixer| {
+        let edit = node_match.make_edit(&rule.matcher, fixer);
+        let range = node_match.range();
+        // skip if the edit does not match the range
+        // TODO: implement expanding range
+        if edit.position != range.start || edit.deleted_length != range.len() {
+          return None;
+        }
+        let rewrite = String::from_utf8(edit.inserted_text).ok()?;
+        Some(OneFix {
+          title: fixer.title().map(ToString::to_string),
+          fixed: rewrite,
+        })
+      })
+      .collect();
+    Some(Self { fixers })
   }
 }
 
 pub fn diagnostic_to_code_action(
   text_doc: &TextDocumentIdentifier,
   diagnostic: Diagnostic,
-) -> Option<CodeAction> {
+) -> Option<Vec<CodeAction>> {
   let rewrite_data = RewriteData::from_value(diagnostic.data?)?;
-  let mut changes = HashMap::new();
-  let text_edit = TextEdit::new(diagnostic.range, rewrite_data.fixed);
-  changes.insert(text_doc.uri.clone(), vec![text_edit]);
+  let actions = rewrite_data.fixers.into_iter().filter_map(|fixer| {
+    let mut changes = HashMap::new();
+    let text_edit = TextEdit::new(diagnostic.range, fixer.fixed);
+    changes.insert(text_doc.uri.clone(), vec![text_edit]);
 
-  let edit = WorkspaceEdit::new(changes);
-  let NumberOrString::String(id) = diagnostic.code? else {
-    return None;
-  };
-  let action = CodeAction {
-    title: format!("Fix `{id}` with ast-grep"),
-    command: None,
-    diagnostics: None,
-    edit: Some(edit),
-    disabled: None,
-    kind: Some(CodeActionKind::QUICKFIX),
-    is_preferred: Some(true),
-    data: None,
-  };
-  Some(action)
+    let edit = WorkspaceEdit::new(changes);
+    let NumberOrString::String(id) = diagnostic.code.as_ref()? else {
+      return None;
+    };
+    let title = fixer
+      .title
+      .unwrap_or_else(|| format!("Fix `{}` with ast-grep", id));
+    Some(CodeAction {
+      title,
+      command: None,
+      diagnostics: None,
+      edit: Some(edit),
+      disabled: None,
+      kind: Some(CodeActionKind::QUICKFIX),
+      is_preferred: Some(true),
+      data: None,
+    })
+  });
+  Some(actions.collect())
 }
 
 fn convert_nodes_to_range<D: Doc>(start_node: &Node<D>, end_node: &Node<D>) -> Range {


Test patch:
diff --git a/crates/cli/src/print/colored_print/test.rs b/crates/cli/src/print/colored_print/test.rs
index d53aa692..be609065 100644
--- a/crates/cli/src/print/colored_print/test.rs
+++ b/crates/cli/src/print/colored_print/test.rs
@@ -263,7 +263,7 @@ fix: '{rewrite}'"
   .pop()
   .unwrap();
   let matcher = rule.get_matcher(&globals).expect("should parse");
-  let fixer = matcher.fixer.as_ref().expect("should have fixer");
+  let fixer = matcher.fixer.first().expect("should have fixer");
   let root = grep.root();
   let matches = root.find_all(&matcher);
   let diffs = matches.map(|n| (Diff::generate(n, &pattern, fixer), &rule));