ai-refactor-cli 0.2.0

Rule-based legacy code refactoring CLI (TypeScript any / Python typing / Django FBV→CBV). Complement to general AI coding agents.
Documentation
//! Built-in rule definitions.
//!
//! Each rule has an id, target file extensions, and a regex used for detection
//! in v0.1.0. The regex is a placeholder until tree-sitter AST queries land in
//! v0.2.0 — the goal here is "good enough to dogfood on jimolab repos".

use regex::Regex;

pub struct Rule {
    pub id: &'static str,
    /// Human-readable description shown by future `--list-rules` UI (v0.2.0+).
    #[allow(dead_code)]
    pub description: &'static str,
    pub extensions: &'static [&'static str],
    /// Compile this lazily at call-site; cheap enough for MVP.
    pub pattern: &'static str,
}

pub const RULES: &[Rule] = &[
    Rule {
        id: "typescript-no-any",
        description: "Detect explicit `any` usage in TypeScript.",
        extensions: &["ts", "tsx"],
        // matches `: any`, `<any>`, `any[]`, `Array<any>`
        pattern: r"(?P<a>:\s*any\b)|(?P<b><any>)|(?P<c>\bany\[\])|(?P<d>Array<any>)",
    },
    Rule {
        id: "python-missing-typing",
        description: "Detect Python `def` declarations without type annotations.",
        extensions: &["py"],
        // function whose signature has no `:` after a parameter name and no `-> Type`
        // captures lines like `def foo(x, y):` but not `def foo(x: int) -> int:`
        pattern: r"^\s*def\s+\w+\(([^):]*)\)\s*:\s*$",
    },
    Rule {
        id: "django-fbv",
        description: "Detect Django function-based views taking `request` as first arg.",
        extensions: &["py"],
        pattern: r"^\s*def\s+\w+\(\s*request\s*[,)]",
    },
];

pub fn find_rule(id: &str) -> Option<&'static Rule> {
    RULES.iter().find(|r| r.id == id)
}

pub fn compile(rule: &Rule) -> Regex {
    // Multiline so `^` / `$` work line-by-line for the Python rules.
    Regex::new(&format!("(?m){}", rule.pattern)).expect("built-in rule regex must compile")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn typescript_no_any_matches_colon_any() {
        let re = compile(find_rule("typescript-no-any").unwrap());
        assert!(re.is_match("const x: any = 1;"));
        assert!(re.is_match("function f<T>(x: any) {}"));
        assert!(re.is_match("let y: any[] = [];"));
        assert!(re.is_match("let z: Array<any> = [];"));
    }

    #[test]
    fn typescript_no_any_skips_clean_code() {
        let re = compile(find_rule("typescript-no-any").unwrap());
        assert!(!re.is_match("const x: number = 1;"));
        assert!(!re.is_match("const company = 'jimolab';")); // 'any' as substring of 'company' must not match
    }

    #[test]
    fn python_missing_typing_matches_untyped_def() {
        let re = compile(find_rule("python-missing-typing").unwrap());
        assert!(re.is_match("def foo(x, y):"));
        assert!(re.is_match("    def bar():"));
    }

    #[test]
    fn python_missing_typing_skips_annotated_def() {
        let re = compile(find_rule("python-missing-typing").unwrap());
        assert!(!re.is_match("def foo(x: int) -> int:"));
        assert!(!re.is_match("def bar() -> None:"));
    }

    #[test]
    fn django_fbv_matches_request_arg() {
        let re = compile(find_rule("django-fbv").unwrap());
        assert!(re.is_match("def home(request):"));
        assert!(re.is_match("def detail(request, pk):"));
    }

    #[test]
    fn django_fbv_skips_cbv_methods() {
        let re = compile(find_rule("django-fbv").unwrap());
        assert!(!re.is_match("def get(self, request, *args, **kwargs):"));
    }

    #[test]
    fn find_rule_returns_none_for_unknown_id() {
        assert!(find_rule("not-a-rule").is_none());
    }
}