#![allow(dead_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeReviewRequest {
pub code: String,
pub language: String,
pub context: Option<String>,
pub file_path: Option<String>,
pub review_type: ReviewType,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReviewType {
PreCommit,
PullRequest,
SelfReview,
Security,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeIssue {
pub severity: IssueSeverity,
pub category: IssueCategory,
pub message: String,
pub line: Option<usize>,
pub suggestion: Option<String>,
pub pattern_match: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueSeverity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum IssueCategory {
Style,
Performance,
Security,
BestPractice,
Documentation,
Testing,
Architecture,
Naming,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeReviewResult {
pub issues: Vec<CodeIssue>,
pub summary: String,
pub score: f64,
pub patterns_detected: Vec<String>,
pub suggestions: Vec<String>,
pub positive_observations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeveloperPatterns {
pub naming_conventions: NamingPatterns,
pub style_preferences: StylePreferences,
pub common_patterns: Vec<String>,
pub test_approaches: Vec<String>,
pub documentation_style: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NamingPatterns {
pub variables: ConventionType,
pub functions: ConventionType,
pub constants: ConventionType,
pub classes: ConventionType,
pub files: ConventionType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConventionType {
SnakeCase,
CamelCase,
PascalCase,
KebabCase,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StylePreferences {
pub indent_style: IndentStyle,
pub indent_size: u8,
pub quote_style: QuoteStyle,
pub semicolon_usage: bool,
pub brace_style: BraceStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IndentStyle {
Spaces,
Tabs,
Mixed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum QuoteStyle {
Single,
Double,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BraceStyle {
Allman,
#[serde(rename = "K&R")]
KR,
None,
}
pub struct CodeReviewer {
patterns: Option<DeveloperPatterns>,
rules: ReviewRules,
}
#[derive(Debug, Clone)]
pub struct ReviewRules {
pub check_naming: bool,
pub check_security: bool,
pub check_performance: bool,
pub check_style: bool,
pub check_documentation: bool,
pub check_testing: bool,
}
impl Default for ReviewRules {
fn default() -> Self {
Self {
check_naming: true,
check_security: true,
check_performance: true,
check_style: true,
check_documentation: true,
check_testing: true,
}
}
}
impl CodeReviewer {
pub fn new() -> Self {
Self {
patterns: None,
rules: ReviewRules::default(),
}
}
pub fn with_patterns(patterns: DeveloperPatterns) -> Self {
Self {
patterns: Some(patterns),
rules: ReviewRules::default(),
}
}
pub fn review(&self, request: &CodeReviewRequest) -> CodeReviewResult {
let mut issues = Vec::new();
let mut patterns_detected = Vec::new();
let mut positive = Vec::new();
let lines: Vec<&str> = request.code.lines().collect();
if self.rules.check_security {
issues.extend(self.check_security(&lines, request.language.as_str()));
}
if self.rules.check_style {
issues.extend(self.check_style(&lines, request.language.as_str()));
}
if self.rules.check_performance {
issues.extend(self.check_performance(&lines, request.language.as_str()));
}
if self.rules.check_naming {
if let Some(ref patterns) = self.patterns {
issues.extend(self.check_against_patterns(&lines, patterns, request.language.as_str()));
}
}
patterns_detected.extend(self.detect_patterns(&lines, request.language.as_str()));
if let Some(ref patterns) = self.patterns {
positive.extend(self.find_positive_patterns(&lines, patterns, request.language.as_str()));
}
let suggestions = self.generate_suggestions(&issues, request.language.as_str());
let score = self.calculate_score(&issues);
let summary = format!(
"Found {} issue(s): {} critical, {} error(s), {} warning(s), {} info",
issues.len(),
issues.iter().filter(|i| i.severity == IssueSeverity::Critical).count(),
issues.iter().filter(|i| i.severity == IssueSeverity::Error).count(),
issues.iter().filter(|i| i.severity == IssueSeverity::Warning).count(),
issues.iter().filter(|i| i.severity == IssueSeverity::Info).count(),
);
CodeReviewResult {
issues,
summary,
score,
patterns_detected,
suggestions,
positive_observations: positive,
}
}
fn check_security(&self, lines: &[&str], _language: &str) -> Vec<CodeIssue> {
let mut issues = Vec::new();
for (idx, line) in lines.iter().enumerate() {
let line_lower = line.to_lowercase();
if line_lower.contains("eval(") || line_lower.contains("exec(") {
issues.push(CodeIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Security,
message: "Avoid using eval() or exec() - potential code injection risk".to_string(),
line: Some(idx + 1),
suggestion: Some("Use safer alternatives like JSON.parse() or ast.literal_eval()".to_string()),
pattern_match: Some("eval/exec usage".to_string()),
});
}
if line_lower.contains("password") || line_lower.contains("secret") || line_lower.contains("api_key") {
if !line_lower.contains("env") && !line_lower.contains("getenv") && !line_lower.contains("os.environ") {
if line.contains("=") && (line.contains("\"") || line.contains("'")) {
issues.push(CodeIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Security,
message: "Potential hardcoded secret detected".to_string(),
line: Some(idx + 1),
suggestion: Some("Use environment variables or a secrets manager".to_string()),
pattern_match: Some("hardcoded secret".to_string()),
});
}
}
}
if line.contains("TODO") || line.contains("FIXME") || line.contains("HACK") {
if line_lower.contains("security") || line_lower.contains("bug") || line_lower.contains("vuln") {
issues.push(CodeIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Security,
message: "Security-related TODO comment found".to_string(),
line: Some(idx + 1),
suggestion: Some("Address this security concern before merging".to_string()),
pattern_match: Some("security TODO".to_string()),
});
}
}
}
issues
}
fn check_style(&self, lines: &[&str], language: &str) -> Vec<CodeIssue> {
let mut issues = Vec::new();
let has_trailing_whitespace = lines.iter().any(|l| l.ends_with(' ') || l.ends_with('\t'));
if has_trailing_whitespace {
issues.push(CodeIssue {
severity: IssueSeverity::Info,
category: IssueCategory::Style,
message: "Trailing whitespace detected".to_string(),
line: None,
suggestion: Some("Remove trailing whitespace".to_string()),
pattern_match: None,
});
}
let mut line_lengths_ok = true;
for (idx, line) in lines.iter().enumerate() {
if line.len() > 120 {
line_lengths_ok = false;
issues.push(CodeIssue {
severity: IssueSeverity::Info,
category: IssueCategory::Style,
message: format!("Line exceeds 120 characters ({} chars)", line.len()),
line: Some(idx + 1),
suggestion: Some("Consider breaking long lines".to_string()),
pattern_match: None,
});
}
}
if line_lengths_ok && lines.len() > 10 {
issues.push(CodeIssue {
severity: IssueSeverity::Info,
category: IssueCategory::Style,
message: "Good: Lines are within recommended length".to_string(),
line: None,
suggestion: None,
pattern_match: None,
});
}
if language == "rust" || language == "python" {
let mut has_main = false;
for line in lines {
if line.contains("fn main()") || line.contains("def main():") {
has_main = true;
break;
}
}
if !has_main && lines.len() > 50 {
issues.push(CodeIssue {
severity: IssueSeverity::Info,
category: IssueCategory::Architecture,
message: "Consider adding a main function for entry point".to_string(),
line: None,
suggestion: Some("Add a main function to clearly define program entry".to_string()),
pattern_match: None,
});
}
}
issues
}
fn check_performance(&self, lines: &[&str], language: &str) -> Vec<CodeIssue> {
let mut issues = Vec::new();
for (idx, line) in lines.iter().enumerate() {
if language == "python" {
if line.contains("for ") && line.contains(" in ") && line.contains(".keys()") {
issues.push(CodeIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Performance,
message: "Unnecessary .keys() call in iteration".to_string(),
line: Some(idx + 1),
suggestion: Some("Iterate directly over the dictionary".to_string()),
pattern_match: Some("dict.keys() iteration".to_string()),
});
}
if line.contains("+=") && line.contains("str(") {
issues.push(CodeIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Performance,
message: "String concatenation in loop detected".to_string(),
line: Some(idx + 1),
suggestion: Some("Use list append and join() instead".to_string()),
pattern_match: Some("string concat in loop".to_string()),
});
}
}
if language == "javascript" || language == "typescript" {
if line.contains("==") || line.contains("!=") {
issues.push(CodeIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::BestPractice,
message: "Use === instead of == for strict equality".to_string(),
line: Some(idx + 1),
suggestion: Some("Use === for type-safe comparison".to_string()),
pattern_match: Some("loose equality".to_string()),
});
}
}
}
issues
}
fn check_against_patterns(&self, lines: &[&str], patterns: &DeveloperPatterns, _language: &str) -> Vec<CodeIssue> {
let mut issues = Vec::new();
for (idx, line) in lines.iter().enumerate() {
if !line.contains("fn ") && !line.contains("def ") && !line.contains("function ") {
continue;
}
let func_name = self.extract_function_name(line);
if let Some(name) = func_name {
let expected = match patterns.naming_conventions.functions {
ConventionType::SnakeCase => !name.contains('-') && !name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false),
ConventionType::CamelCase => !name.contains('_') && name.chars().next().map(|c| c.is_lowercase()).unwrap_or(false),
ConventionType::PascalCase => !name.contains('_') && name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false),
_ => true,
};
if !expected {
issues.push(CodeIssue {
severity: IssueSeverity::Info,
category: IssueCategory::Naming,
message: format!("Function name '{}' doesn't match your convention", name),
line: Some(idx + 1),
suggestion: Some("Consider using your typical naming style".to_string()),
pattern_match: Some("naming convention".to_string()),
});
}
}
}
issues
}
fn extract_function_name(&self, line: &str) -> Option<String> {
if let Some(start) = line.find("fn ") {
let rest = &line[start + 3..];
return rest.split_whitespace().next().map(|s| s.trim_matches('(').to_string());
}
if let Some(start) = line.find("def ") {
let rest = &line[start + 4..];
return rest.split_whitespace().next().map(|s| s.trim_matches('(').to_string());
}
if let Some(start) = line.find("function ") {
let rest = &line[start + 9..];
return rest.split_whitespace().next().map(|s| s.trim_matches('(').to_string());
}
None
}
fn detect_patterns(&self, lines: &[&str], language: &str) -> Vec<String> {
let mut patterns = Vec::new();
let code = lines.join("\n").to_lowercase();
if code.contains("async") && code.contains("await") {
patterns.push("Async/await pattern".to_string());
}
if code.contains("match ") || code.contains("case ") {
patterns.push("Pattern matching".to_string());
}
if code.contains("trait ") || code.contains("interface ") {
patterns.push("Interface/trait usage".to_string());
}
if language == "rust" && code.contains("unwrap()") {
patterns.push("Error handling with unwrap".to_string());
}
if code.contains("try!") || code.contains("?") {
patterns.push("Error propagation".to_string());
}
if code.contains("#[derive(") {
patterns.push("Derive macros".to_string());
}
if code.contains("struct ") && code.contains("impl ") {
patterns.push("Traditional OOP with impl".to_string());
}
patterns
}
fn find_positive_patterns(&self, lines: &[&str], _patterns: &DeveloperPatterns, _language: &str) -> Vec<String> {
let mut positive = Vec::new();
let code = lines.join("\n");
if code.contains("#[cfg(test)]") || code.contains("#[test]") || code.contains("def test_") || code.contains("describe(") {
positive.push("Good test coverage detected".to_string());
}
if code.contains("///") || code.contains("\"\"\"") || code.contains("/**") {
positive.push("Good documentation comments".to_string());
}
if code.contains("pub ") && code.contains("struct ") {
positive.push("Proper public API design".to_string());
}
if lines.len() < 50 {
positive.push("Concise and focused code".to_string());
}
positive
}
fn generate_suggestions(&self, issues: &[CodeIssue], _language: &str) -> Vec<String> {
let mut suggestions = Vec::new();
let critical = issues.iter().filter(|i| i.severity == IssueSeverity::Critical).count();
if critical > 0 {
suggestions.push(format!("Address {} critical security issue(s) before merging", critical));
}
let errors = issues.iter().filter(|i| i.severity == IssueSeverity::Error).count();
if errors > 0 {
suggestions.push(format!("Fix {} error-level issue(s)", errors));
}
if issues.is_empty() {
suggestions.push("Code looks good! Consider adding more tests.".to_string());
}
suggestions
}
fn calculate_score(&self, issues: &[CodeIssue]) -> f64 {
if issues.is_empty() {
return 100.0;
}
let mut score: f64 = 100.0;
for issue in issues {
let deduction = match issue.severity {
IssueSeverity::Critical => 15.0,
IssueSeverity::Error => 10.0,
IssueSeverity::Warning => 5.0,
IssueSeverity::Info => 1.0,
};
score -= deduction;
}
if score < 0.0 { 0.0 } else { score }
}
}
impl Default for CodeReviewer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_code_review() {
let reviewer = CodeReviewer::new();
let request = CodeReviewRequest {
code: String::new(),
language: "rust".to_string(),
context: None,
file_path: None,
review_type: ReviewType::SelfReview,
};
let result = reviewer.review(&request);
assert_eq!(result.issues.len(), 0);
assert_eq!(result.score, 100.0);
}
#[test]
fn test_security_issue_detection() {
let reviewer = CodeReviewer::new();
let request = CodeReviewRequest {
code: "let password = \"secret\";\nlet token = format!(\"Bearer {}\", password);"
.to_string(),
language: "rust".to_string(),
context: None,
file_path: None,
review_type: ReviewType::Security,
};
let result = reviewer.review(&request);
assert!(result.issues.iter().any(|i| i.category == IssueCategory::Security));
}
#[test]
fn test_hardcoded_secret_detection() {
let reviewer = CodeReviewer::new();
let request = CodeReviewRequest {
code: "const API_KEY = \"sk-1234567890abcdef\";"
.to_string(),
language: "rust".to_string(),
context: None,
file_path: None,
review_type: ReviewType::Security,
};
let result = reviewer.review(&request);
assert!(result.issues.iter().any(|i|
i.severity == IssueSeverity::Critical &&
i.category == IssueCategory::Security
));
}
#[test]
fn test_eval_usage_detection() {
let reviewer = CodeReviewer::new();
let request = CodeReviewRequest {
code: "eval(user_input)".to_string(),
language: "javascript".to_string(),
context: None,
file_path: None,
review_type: ReviewType::Security,
};
let result = reviewer.review(&request);
assert!(result.issues.iter().any(|i|
i.category == IssueCategory::Security &&
i.message.to_lowercase().contains("eval")
));
}
#[test]
fn test_score_calculation() {
let reviewer = CodeReviewer::new();
let issues = vec![
CodeIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Security,
message: "Test".to_string(),
line: None,
suggestion: None,
pattern_match: None,
},
CodeIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Style,
message: "Test".to_string(),
line: None,
suggestion: None,
pattern_match: None,
},
];
let score = reviewer.calculate_score(&issues);
assert_eq!(score, 80.0); }
#[test]
fn test_score_with_no_issues() {
let reviewer = CodeReviewer::new();
let score = reviewer.calculate_score(&[]);
assert_eq!(score, 100.0);
}
#[test]
fn test_score_minimum_zero() {
let reviewer = CodeReviewer::new();
let issues: Vec<CodeIssue> = (0..20).map(|i| CodeIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Security,
message: format!("Issue {}", i),
line: None,
suggestion: None,
pattern_match: None,
}).collect();
let score = reviewer.calculate_score(&issues);
assert!(score >= 0.0);
}
#[test]
fn test_generate_suggestions_empty() {
let reviewer = CodeReviewer::new();
let suggestions = reviewer.generate_suggestions(&[], "rust");
assert!(!suggestions.is_empty());
}
#[test]
fn test_generate_suggestions_critical() {
let reviewer = CodeReviewer::new();
let issues = vec![
CodeIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Security,
message: "Hardcoded secret".to_string(),
line: Some(1),
suggestion: None,
pattern_match: None,
},
];
let suggestions = reviewer.generate_suggestions(&issues, "rust");
assert!(suggestions.iter().any(|s| s.to_lowercase().contains("critical")));
}
#[test]
fn test_review_type_variants() {
assert_eq!(ReviewType::PreCommit, ReviewType::PreCommit);
assert_eq!(ReviewType::PullRequest, ReviewType::PullRequest);
assert_eq!(ReviewType::SelfReview, ReviewType::SelfReview);
assert_eq!(ReviewType::Security, ReviewType::Security);
}
#[test]
fn test_issue_severity_ordering() {
assert!(IssueSeverity::Info < IssueSeverity::Warning);
assert!(IssueSeverity::Warning < IssueSeverity::Error);
assert!(IssueSeverity::Error < IssueSeverity::Critical);
}
}