Skip to main content

codemod_core/rule/
builtin.rs

1//! Built-in codemod rule templates.
2//!
3//! This module provides a small library of commonly useful transformation
4//! rules that ship with the engine. Users can apply these out of the box or
5//! use them as starting points for custom rules.
6
7use super::schema::{CodemodRule, RuleConfig, RulePattern};
8
9/// Collection of built-in codemod rules.
10pub struct BuiltinRules;
11
12impl BuiltinRules {
13    /// Returns all built-in rules.
14    pub fn all() -> Vec<CodemodRule> {
15        vec![
16            Self::replace_println_with_log(),
17            Self::replace_unwrap_with_expect(),
18            Self::replace_deprecated_trim(),
19        ]
20    }
21
22    /// Get a built-in rule by name, or `None` if not found.
23    pub fn get(name: &str) -> Option<CodemodRule> {
24        Self::all().into_iter().find(|r| r.name == name)
25    }
26
27    /// List the names of all available built-in rules.
28    pub fn names() -> Vec<&'static str> {
29        vec![
30            "replace-println-with-log",
31            "replace-unwrap-with-expect",
32            "replace-deprecated-trim",
33        ]
34    }
35
36    // -----------------------------------------------------------------
37    // Individual rule definitions
38    // -----------------------------------------------------------------
39
40    /// Replace `println!` calls with `log::info!`.
41    ///
42    /// This is one of the most common Rust codemods — switching from ad-hoc
43    /// `println!` debugging to structured logging.
44    ///
45    /// ```yaml
46    /// before: "println!($args)"
47    /// after:  "log::info!($args)"
48    /// ```
49    pub fn replace_println_with_log() -> CodemodRule {
50        CodemodRule {
51            name: "replace-println-with-log".into(),
52            description: "Replace println!() calls with log::info!() for structured logging".into(),
53            language: "rust".into(),
54            version: "1.0".into(),
55            pattern: RulePattern {
56                before: "println!($args)".into(),
57                after: "log::info!($args)".into(),
58            },
59            config: RuleConfig {
60                include: vec!["**/*.rs".into()],
61                exclude: vec!["tests/**".into(), "examples/**".into()],
62                respect_gitignore: true,
63                max_file_size: Some(1_000_000),
64            },
65        }
66    }
67
68    /// Replace `.unwrap()` calls with `.expect("descriptive message")`.
69    ///
70    /// Bare `.unwrap()` calls produce unhelpful panic messages. This rule
71    /// replaces them with `.expect()` so developers can add context.
72    ///
73    /// ```yaml
74    /// before: "$expr.unwrap()"
75    /// after:  "$expr.expect(\"TODO: add error context\")"
76    /// ```
77    pub fn replace_unwrap_with_expect() -> CodemodRule {
78        CodemodRule {
79            name: "replace-unwrap-with-expect".into(),
80            description:
81                "Replace .unwrap() with .expect(\"...\") to encourage better error messages".into(),
82            language: "rust".into(),
83            version: "1.0".into(),
84            pattern: RulePattern {
85                before: "$expr.unwrap()".into(),
86                after: "$expr.expect(\"TODO: add error context\")".into(),
87            },
88            config: RuleConfig {
89                include: vec!["**/*.rs".into()],
90                exclude: vec!["tests/**".into()],
91                respect_gitignore: true,
92                max_file_size: Some(1_000_000),
93            },
94        }
95    }
96
97    /// Replace the deprecated `trim_left()` / `trim_right()` with
98    /// `trim_start()` / `trim_end()` (Rust 1.30+).
99    ///
100    /// ```yaml
101    /// before: "$s.trim_left()"
102    /// after:  "$s.trim_start()"
103    /// ```
104    pub fn replace_deprecated_trim() -> CodemodRule {
105        CodemodRule {
106            name: "replace-deprecated-trim".into(),
107            description: "Replace deprecated trim_left()/trim_right() with trim_start()/trim_end()"
108                .into(),
109            language: "rust".into(),
110            version: "1.0".into(),
111            pattern: RulePattern {
112                before: "$s.trim_left()".into(),
113                after: "$s.trim_start()".into(),
114            },
115            config: RuleConfig {
116                include: vec!["**/*.rs".into()],
117                exclude: vec![],
118                respect_gitignore: true,
119                max_file_size: None,
120            },
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_all_rules_valid() {
131        for rule in BuiltinRules::all() {
132            rule.validate()
133                .unwrap_or_else(|e| panic!("Built-in rule '{}' failed validation: {e}", rule.name));
134        }
135    }
136
137    #[test]
138    fn test_get_existing_rule() {
139        let rule = BuiltinRules::get("replace-println-with-log");
140        assert!(rule.is_some());
141        assert_eq!(rule.unwrap().language, "rust");
142    }
143
144    #[test]
145    fn test_get_nonexistent_rule() {
146        assert!(BuiltinRules::get("does-not-exist").is_none());
147    }
148
149    #[test]
150    fn test_names_match_rules() {
151        let names = BuiltinRules::names();
152        let rules = BuiltinRules::all();
153        assert_eq!(names.len(), rules.len());
154        for (name, rule) in names.iter().zip(rules.iter()) {
155            assert_eq!(*name, rule.name);
156        }
157    }
158}