Skip to main content

ai_refactor_cli/
rules.rs

1//! Built-in rule definitions.
2//!
3//! Each rule has an id, target file extensions, and a regex used for detection
4//! in v0.1.0. The regex is a placeholder until tree-sitter AST queries land in
5//! v0.2.0 — the goal here is "good enough to dogfood on jimolab repos".
6
7use regex::Regex;
8
9pub struct Rule {
10    pub id: &'static str,
11    /// Human-readable description shown by future `--list-rules` UI (v0.2.0+).
12    #[allow(dead_code)]
13    pub description: &'static str,
14    pub extensions: &'static [&'static str],
15    /// Compile this lazily at call-site; cheap enough for MVP.
16    pub pattern: &'static str,
17}
18
19pub const RULES: &[Rule] = &[
20    Rule {
21        id: "typescript-no-any",
22        description: "Detect explicit `any` usage in TypeScript.",
23        extensions: &["ts", "tsx"],
24        // matches `: any`, `<any>`, `any[]`, `Array<any>`
25        pattern: r"(?P<a>:\s*any\b)|(?P<b><any>)|(?P<c>\bany\[\])|(?P<d>Array<any>)",
26    },
27    Rule {
28        id: "python-missing-typing",
29        description: "Detect Python `def` declarations without type annotations.",
30        extensions: &["py"],
31        // function whose signature has no `:` after a parameter name and no `-> Type`
32        // captures lines like `def foo(x, y):` but not `def foo(x: int) -> int:`
33        pattern: r"^\s*def\s+\w+\(([^):]*)\)\s*:\s*$",
34    },
35    Rule {
36        id: "django-fbv",
37        description: "Detect Django function-based views taking `request` as first arg.",
38        extensions: &["py"],
39        pattern: r"^\s*def\s+\w+\(\s*request\s*[,)]",
40    },
41];
42
43pub fn find_rule(id: &str) -> Option<&'static Rule> {
44    RULES.iter().find(|r| r.id == id)
45}
46
47pub fn compile(rule: &Rule) -> Regex {
48    // Multiline so `^` / `$` work line-by-line for the Python rules.
49    Regex::new(&format!("(?m){}", rule.pattern)).expect("built-in rule regex must compile")
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn typescript_no_any_matches_colon_any() {
58        let re = compile(find_rule("typescript-no-any").unwrap());
59        assert!(re.is_match("const x: any = 1;"));
60        assert!(re.is_match("function f<T>(x: any) {}"));
61        assert!(re.is_match("let y: any[] = [];"));
62        assert!(re.is_match("let z: Array<any> = [];"));
63    }
64
65    #[test]
66    fn typescript_no_any_skips_clean_code() {
67        let re = compile(find_rule("typescript-no-any").unwrap());
68        assert!(!re.is_match("const x: number = 1;"));
69        assert!(!re.is_match("const company = 'jimolab';")); // 'any' as substring of 'company' must not match
70    }
71
72    #[test]
73    fn python_missing_typing_matches_untyped_def() {
74        let re = compile(find_rule("python-missing-typing").unwrap());
75        assert!(re.is_match("def foo(x, y):"));
76        assert!(re.is_match("    def bar():"));
77    }
78
79    #[test]
80    fn python_missing_typing_skips_annotated_def() {
81        let re = compile(find_rule("python-missing-typing").unwrap());
82        assert!(!re.is_match("def foo(x: int) -> int:"));
83        assert!(!re.is_match("def bar() -> None:"));
84    }
85
86    #[test]
87    fn django_fbv_matches_request_arg() {
88        let re = compile(find_rule("django-fbv").unwrap());
89        assert!(re.is_match("def home(request):"));
90        assert!(re.is_match("def detail(request, pk):"));
91    }
92
93    #[test]
94    fn django_fbv_skips_cbv_methods() {
95        let re = compile(find_rule("django-fbv").unwrap());
96        assert!(!re.is_match("def get(self, request, *args, **kwargs):"));
97    }
98
99    #[test]
100    fn find_rule_returns_none_for_unknown_id() {
101        assert!(find_rule("not-a-rule").is_none());
102    }
103}