use regex::Regex;
use std::fs;
use std::path::Path;
use super::config::CustomRule;
use crate::utils::types::{LintIssue, Severity};
use crate::Result;
#[derive(Debug)]
struct CompiledRule {
rule: CustomRule,
regex: Regex,
}
#[derive(Debug)]
pub struct CustomRulesChecker {
rules: Vec<CompiledRule>,
}
impl CustomRulesChecker {
pub fn new(rules: &[CustomRule]) -> Result<Self> {
let compiled_rules: Result<Vec<CompiledRule>> = rules
.iter()
.filter(|r| r.enabled)
.map(|rule| {
let regex = Regex::new(&rule.pattern).map_err(|e| {
crate::LintisError::Config(format!(
"Invalid regex pattern in rule '{}': {}",
rule.code, e
))
})?;
Ok(CompiledRule {
rule: rule.clone(),
regex,
})
})
.collect();
Ok(Self {
rules: compiled_rules?,
})
}
pub fn has_rules(&self) -> bool {
!self.rules.is_empty()
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn check(&self, path: &Path, language: Option<&str>) -> Result<Vec<LintIssue>> {
if self.rules.is_empty() {
return Ok(Vec::new());
}
let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
if e.kind() == std::io::ErrorKind::InvalidData {
return Ok(Vec::new());
}
return Err(crate::LintisError::Io(e));
}
};
let mut issues = Vec::new();
for compiled in &self.rules {
if !compiled.rule.applies_to_extension(extension) {
continue;
}
if let Some(lang) = language {
if !compiled.rule.applies_to_language(lang) {
continue;
}
}
for (line_num, line) in content.lines().enumerate() {
for mat in compiled.regex.find_iter(line) {
let mut issue = LintIssue::new(
path.to_path_buf(),
line_num + 1, compiled.rule.message.clone(),
compiled.rule.severity,
);
issue.code = Some(compiled.rule.code.clone());
issue.column = Some(mat.start() + 1); issue.source = Some("custom".to_string());
issue.suggestion = compiled.rule.suggestion.clone();
issue.code_line = Some(line.to_string());
issues.push(issue);
}
}
}
Ok(issues)
}
pub fn rule_info(&self) -> Vec<RuleInfo> {
self.rules
.iter()
.map(|r| RuleInfo {
code: r.rule.code.clone(),
pattern: r.rule.pattern.clone(),
severity: r.rule.severity,
has_suggestion: r.rule.suggestion.is_some(),
extensions: r.rule.extensions.clone(),
languages: r.rule.languages.clone(),
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct RuleInfo {
pub code: String,
pub pattern: String,
pub severity: Severity,
pub has_suggestion: bool,
pub extensions: Vec<String>,
pub languages: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_test_file(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::with_suffix(".rs").unwrap();
file.write_all(content.as_bytes()).unwrap();
file.flush().unwrap();
file
}
#[test]
fn test_custom_checker_empty() {
let checker = CustomRulesChecker::new(&[]).unwrap();
assert!(!checker.has_rules());
assert_eq!(checker.rule_count(), 0);
}
#[test]
fn test_custom_checker_simple_pattern() {
let rules = vec![CustomRule::new(
"custom/no-todo",
"TODO",
"Found TODO comment",
)];
let checker = CustomRulesChecker::new(&rules).unwrap();
assert!(checker.has_rules());
assert_eq!(checker.rule_count(), 1);
let file = create_test_file("// TODO: fix this\nlet x = 1;\n// Another TODO here");
let issues = checker.check(file.path(), None).unwrap();
assert_eq!(issues.len(), 2);
assert_eq!(issues[0].line, 1);
assert_eq!(issues[0].code.as_deref(), Some("custom/no-todo"));
assert_eq!(issues[1].line, 3);
}
#[test]
fn test_custom_checker_with_severity() {
let rules = vec![
CustomRule::new("custom/no-fixme", "FIXME|XXX", "Found FIXME/XXX")
.with_severity(Severity::Error),
];
let checker = CustomRulesChecker::new(&rules).unwrap();
let file = create_test_file("// FIXME: urgent\n// XXX: hack");
let issues = checker.check(file.path(), None).unwrap();
assert_eq!(issues.len(), 2);
assert_eq!(issues[0].severity, Severity::Error);
assert_eq!(issues[1].severity, Severity::Error);
}
#[test]
fn test_custom_checker_extension_filter() {
let rules = vec![
CustomRule::new("custom/py-print", r"print\s*\(", "No print statements")
.with_extensions(vec!["py".to_string()]),
];
let checker = CustomRulesChecker::new(&rules).unwrap();
let rs_file = create_test_file("print(\"hello\")");
let issues = checker.check(rs_file.path(), None).unwrap();
assert_eq!(issues.len(), 0);
let mut py_file = NamedTempFile::with_suffix(".py").unwrap();
py_file.write_all(b"print(\"hello\")").unwrap();
py_file.flush().unwrap();
let issues = checker.check(py_file.path(), None).unwrap();
assert_eq!(issues.len(), 1);
}
#[test]
fn test_custom_checker_language_filter() {
let rules = vec![
CustomRule::new("custom/rust-unwrap", r"\.unwrap\(\)", "Avoid unwrap()")
.with_languages(vec!["rust".to_string()]),
];
let checker = CustomRulesChecker::new(&rules).unwrap();
let file = create_test_file("let x = foo.unwrap();");
let issues = checker.check(file.path(), None).unwrap();
assert_eq!(issues.len(), 1);
let issues = checker.check(file.path(), Some("rust")).unwrap();
assert_eq!(issues.len(), 1);
let issues = checker.check(file.path(), Some("python")).unwrap();
assert_eq!(issues.len(), 0);
}
#[test]
fn test_custom_checker_disabled_rule() {
let mut rule = CustomRule::new("custom/disabled", "test", "Test");
rule.enabled = false;
let checker = CustomRulesChecker::new(&[rule]).unwrap();
assert!(!checker.has_rules());
assert_eq!(checker.rule_count(), 0);
}
#[test]
fn test_custom_checker_invalid_regex() {
let rules = vec![CustomRule::new(
"custom/bad-regex",
"[invalid(regex",
"Bad pattern",
)];
let result = CustomRulesChecker::new(&rules);
assert!(result.is_err());
}
#[test]
fn test_custom_checker_with_suggestion() {
let rules =
vec![
CustomRule::new("custom/no-print", r"println!\(", "Direct println! found")
.with_suggestion("Use log::info! or log::debug! instead"),
];
let checker = CustomRulesChecker::new(&rules).unwrap();
let file = create_test_file("fn main() { println!(\"hello\"); }");
let issues = checker.check(file.path(), None).unwrap();
assert_eq!(issues.len(), 1);
assert!(issues[0].suggestion.is_some());
assert!(issues[0].suggestion.as_ref().unwrap().contains("log::info"));
}
#[test]
fn test_custom_checker_column_tracking() {
let rules = vec![CustomRule::new("custom/test", "TODO", "Found TODO")];
let checker = CustomRulesChecker::new(&rules).unwrap();
let file = create_test_file(" // TODO: test");
let issues = checker.check(file.path(), None).unwrap();
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].column, Some(7)); }
#[test]
fn test_rule_info() {
let rules = vec![
CustomRule::new("custom/rule1", "pattern1", "Message 1")
.with_suggestion("Fix 1")
.with_extensions(vec!["rs".to_string()]),
CustomRule::new("custom/rule2", "pattern2", "Message 2").with_severity(Severity::Error),
];
let checker = CustomRulesChecker::new(&rules).unwrap();
let info = checker.rule_info();
assert_eq!(info.len(), 2);
assert_eq!(info[0].code, "custom/rule1");
assert!(info[0].has_suggestion);
assert_eq!(info[0].extensions, vec!["rs"]);
assert_eq!(info[1].severity, Severity::Error);
}
}