use crate::models::proxy::{
ProxyMode, ProxyOperation, ProxyRequest, ProxyResponse, ProxyStatus, QualityConfig,
QualityMetrics, QualityReport, QualityViolation, ViolationSeverity, ViolationType,
};
use crate::services::ast_rust::analyze_rust_file_with_complexity;
use crate::services::complexity::aggregate_results_with_thresholds;
use crate::services::satd_detector::SATDDetector;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::Path;
use tracing::{debug, info, warn};
pub struct QualityProxyService {
satd_detector: SATDDetector,
}
impl QualityProxyService {
#[must_use]
pub fn new() -> Self {
Self {
satd_detector: SATDDetector::new(),
}
}
pub async fn proxy_operation(&self, request: ProxyRequest) -> Result<ProxyResponse> {
info!(
"Proxying {} operation for {}",
match request.operation {
ProxyOperation::Write => "write",
ProxyOperation::Edit => "edit",
ProxyOperation::Append => "append",
},
request.file_path
);
let content = self.get_operation_content(&request)?;
let file_extension = Path::new(&request.file_path)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("rs");
let ((quality_metrics, passed), violations) = self
.analyze_content(
&content,
&request.file_path,
file_extension,
&request.quality_config,
)
.await?;
let (status, final_content, refactoring_applied, refactoring_plan) = match request.mode {
ProxyMode::Strict => {
if passed {
(ProxyStatus::Accepted, content, false, None)
} else {
(ProxyStatus::Rejected, String::new(), false, None)
}
}
ProxyMode::Advisory => (ProxyStatus::Accepted, content, false, None),
ProxyMode::AutoFix => {
if passed {
(ProxyStatus::Accepted, content, false, None)
} else {
match self
.auto_fix_content(
&content,
&request.file_path,
file_extension,
&request.quality_config,
)
.await
{
Ok((fixed_content, plan)) => {
let ((_, fixed_passed), _) = self
.analyze_content(
&fixed_content,
&request.file_path,
file_extension,
&request.quality_config,
)
.await?;
if fixed_passed {
(ProxyStatus::Modified, fixed_content, true, Some(plan))
} else {
warn!("Auto-fix failed to meet quality standards");
(ProxyStatus::Rejected, String::new(), false, None)
}
}
Err(e) => {
warn!("Auto-fix failed: {}", e);
(ProxyStatus::Rejected, String::new(), false, None)
}
}
}
}
};
Ok(ProxyResponse {
status,
quality_report: QualityReport {
passed,
metrics: quality_metrics,
violations,
},
final_content,
refactoring_applied,
refactoring_plan,
})
}
fn get_operation_content(&self, request: &ProxyRequest) -> Result<String> {
match request.operation {
ProxyOperation::Write => request
.content
.clone()
.context("Write operation requires content"),
ProxyOperation::Edit => {
let old = request
.old_content
.as_ref()
.context("Edit operation requires old_content")?;
let new = request
.new_content
.as_ref()
.context("Edit operation requires new_content")?;
if let Some(existing_content) = &request.content {
Ok(existing_content.replace(old, new))
} else {
Ok(new.clone())
}
}
ProxyOperation::Append => {
let append_content = request
.content
.as_ref()
.context("Append operation requires content")?;
if let Some(existing) = &request.old_content {
Ok(format!("{existing}\n{append_content}"))
} else {
Ok(append_content.clone())
}
}
}
}
async fn analyze_content(
&self,
content: &str,
file_path: &str,
extension: &str,
config: &QualityConfig,
) -> Result<((QualityMetrics, bool), Vec<QualityViolation>)> {
let mut violations = Vec::new();
if extension != "rs" {
debug!("Skipping Rust-specific analysis for non-Rust file");
return Ok((
(
QualityMetrics {
max_complexity: 0,
satd_count: 0,
lint_violations: 0,
coverage_percentage: None,
},
true,
),
violations,
));
}
let temp_file = self.create_temp_file(content, extension)?;
let temp_path = temp_file.path();
let max_complexity = match analyze_rust_file_with_complexity(temp_path).await {
Ok(file_metrics) => {
let report = aggregate_results_with_thresholds(
vec![file_metrics],
Some(config.max_complexity as u16),
Some(config.max_complexity as u16 + 5),
);
let max_comp = u32::from(report
.hotspots
.iter()
.map(|h| h.complexity)
.max()
.unwrap_or(0));
if max_comp > config.max_complexity {
if let Some(hotspot) = report.hotspots.first() {
violations.push(QualityViolation {
violation_type: ViolationType::Complexity,
severity: ViolationSeverity::Error,
location: format!("{}:{}", file_path, hotspot.line),
message: format!(
"Function '{}' complexity {} exceeds maximum {}",
hotspot.function.as_ref().unwrap_or(&"unknown".to_string()),
hotspot.complexity,
config.max_complexity
),
suggestion: Some(
"Consider splitting this function into smaller functions"
.to_string(),
),
});
}
}
max_comp
}
Err(e) => {
warn!("Failed to analyze complexity: {}", e);
0
}
};
let satd_instances = self
.satd_detector
.extract_from_content(content, Path::new(file_path))?;
let satd_count = satd_instances.len();
if !config.allow_satd && satd_count > 0 {
for instance in &satd_instances {
violations.push(QualityViolation {
violation_type: ViolationType::Satd,
severity: ViolationSeverity::Error,
location: format!("{}:{}", file_path, instance.line),
message: format!("SATD detected: {}", instance.text),
suggestion: Some(
"Remove TODO/FIXME comments and implement the functionality".to_string(),
),
});
}
}
let lint_violations = match self.run_lint_checks(content).await {
Ok(violations_found) => {
for (line, message) in &violations_found {
violations.push(QualityViolation {
violation_type: ViolationType::Lint,
severity: ViolationSeverity::Warning,
location: format!("{file_path}:{line}"),
message: message.clone(),
suggestion: Some("Fix lint issue".to_string()),
});
}
violations_found.len()
}
Err(e) => {
warn!("Failed to run lint checks: {}", e);
0
}
};
if config.require_docs {
let doc_violations = self.check_documentation(content, file_path);
violations.extend(doc_violations);
}
let passed = violations
.iter()
.all(|v| matches!(v.severity, ViolationSeverity::Warning));
Ok((
(
QualityMetrics {
max_complexity,
satd_count,
lint_violations,
coverage_percentage: None,
},
passed,
),
violations,
))
}
async fn auto_fix_content(
&self,
content: &str,
file_path: &str,
extension: &str,
config: &QualityConfig,
) -> Result<(String, Vec<HashMap<String, serde_json::Value>>)> {
if extension != "rs" {
return Ok((content.to_string(), Vec::new()));
}
info!("Applying auto-fix refactoring to {}", file_path);
let mut fixed_content = content.to_string();
let mut plan = Vec::new();
if !config.allow_satd {
let satd_patterns = vec![
r"//\s*TODO:.*\n",
r"//\s*FIXME:.*\n",
r"//\s*HACK:.*\n",
r"//\s*BUG:.*\n",
];
for pattern in satd_patterns {
let re = regex::Regex::new(pattern)?;
if re.is_match(&fixed_content) {
fixed_content = re.replace_all(&fixed_content, "").to_string();
let mut step = HashMap::new();
step.insert("action".to_string(), serde_json::json!("remove_satd"));
step.insert("pattern".to_string(), serde_json::json!(pattern));
plan.push(step);
}
}
}
if config.require_docs {
let lines: Vec<&str> = fixed_content.lines().collect();
let mut new_lines = Vec::new();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("pub fn")
|| trimmed.starts_with("pub struct")
|| trimmed.starts_with("pub enum")
{
let prev_has_doc = i > 0 && lines[i - 1].trim().starts_with("///");
if !prev_has_doc {
let item_name = trimmed
.split_whitespace()
.nth(2)
.unwrap_or("item")
.split('(')
.next()
.unwrap_or("item");
new_lines.push(format!("/// {item_name}"));
let mut step = HashMap::new();
step.insert("action".to_string(), serde_json::json!("add_documentation"));
step.insert("item".to_string(), serde_json::json!(item_name));
plan.push(step);
}
}
new_lines.push((*line).to_string());
}
if !plan.is_empty() {
fixed_content = new_lines.join("\n");
}
}
if config.auto_format {
if let Ok(formatted) = self.format_rust_code(&fixed_content).await {
if formatted != fixed_content {
fixed_content = formatted;
let mut step = HashMap::new();
step.insert("action".to_string(), serde_json::json!("format_code"));
plan.push(step);
}
}
}
Ok((fixed_content, plan))
}
fn check_documentation(&self, content: &str, file_path: &str) -> Vec<QualityViolation> {
let mut violations = Vec::new();
let lines: Vec<&str> = content.lines().collect();
for (line_num, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("pub fn")
|| trimmed.starts_with("pub struct")
|| trimmed.starts_with("pub enum")
{
let has_doc = if line_num > 0 {
let start = line_num.saturating_sub(5);
lines[start..line_num]
.iter()
.any(|l| l.trim().starts_with("///"))
} else {
false
};
if !has_doc {
violations.push(QualityViolation {
violation_type: ViolationType::Docs,
severity: ViolationSeverity::Warning,
location: format!("{}:{}", file_path, line_num + 1),
message: "Public item missing documentation".to_string(),
suggestion: Some("Add /// documentation comment".to_string()),
});
}
}
}
violations
}
fn create_temp_file(&self, content: &str, extension: &str) -> Result<tempfile::NamedTempFile> {
use std::io::Write;
let mut temp_file = tempfile::Builder::new()
.suffix(&format!(".{extension}"))
.tempfile()?;
temp_file.write_all(content.as_bytes())?;
temp_file.flush()?;
Ok(temp_file)
}
async fn run_lint_checks(&self, content: &str) -> Result<Vec<(usize, String)>> {
use std::fs;
use std::io::Write;
use std::process::Command;
let temp_dir = tempfile::TempDir::new()?;
let src_dir = temp_dir.path().join("src");
fs::create_dir(&src_dir)?;
let lib_path = src_dir.join("lib.rs");
let mut lib_file = fs::File::create(&lib_path)?;
lib_file.write_all(content.as_bytes())?;
lib_file.flush()?;
let cargo_toml = r#"[package]
name = "temp_quality_check"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let cargo_path = temp_dir.path().join("Cargo.toml");
let mut cargo_file = fs::File::create(&cargo_path)?;
cargo_file.write_all(cargo_toml.as_bytes())?;
cargo_file.flush()?;
let output = Command::new("cargo")
.arg("clippy")
.arg("--")
.arg("-D")
.arg("warnings")
.current_dir(temp_dir.path())
.output()?;
let mut violations = Vec::new();
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
for line in stderr.lines() {
if line.contains("warning:") || line.contains("error:") {
let line_num = 1; let message = line.to_string();
violations.push((line_num, message));
}
}
}
Ok(violations)
}
async fn format_rust_code(&self, content: &str) -> Result<String> {
use std::process::Command;
let temp_file = self.create_temp_file(content, "rs")?;
let output = Command::new("rustfmt")
.arg("--edition")
.arg("2021")
.arg(temp_file.path())
.output()?;
if output.status.success() {
std::fs::read_to_string(temp_file.path()).context("Failed to read formatted file")
} else {
Err(anyhow::anyhow!(
"rustfmt failed: {}",
String::from_utf8_lossy(&output.stderr)
))
}
}
}
impl Default for QualityProxyService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_proxy_high_quality_code() {
let service = QualityProxyService::new();
let request = ProxyRequest {
operation: ProxyOperation::Write,
file_path: "test.rs".to_string(),
content: Some(
r#"/// A simple greeting function
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}"#
.to_string(),
),
old_content: None,
new_content: None,
mode: ProxyMode::Strict,
quality_config: QualityConfig::default(),
};
let response = service.proxy_operation(request).await.unwrap();
assert!(matches!(response.status, ProxyStatus::Accepted));
assert!(response.quality_report.passed);
}
#[tokio::test]
async fn test_proxy_reject_satd() {
let service = QualityProxyService::new();
let request = ProxyRequest {
operation: ProxyOperation::Write,
file_path: "test.rs".to_string(),
content: Some(
r#"fn process() {
// Test comment for SATD detection (example removed)
unimplemented!()
}"#
.to_string(),
),
old_content: None,
new_content: None,
mode: ProxyMode::Strict,
quality_config: QualityConfig::default(),
};
let response = service.proxy_operation(request).await.unwrap();
assert!(matches!(response.status, ProxyStatus::Rejected));
assert!(!response.quality_report.passed);
assert!(response.quality_report.metrics.satd_count > 0);
}
#[tokio::test]
async fn test_proxy_advisory_mode() {
let service = QualityProxyService::new();
let request = ProxyRequest {
operation: ProxyOperation::Write,
file_path: "test.rs".to_string(),
content: Some(
r#"pub fn undocumented() {
println!("No docs");
}"#
.to_string(),
),
old_content: None,
new_content: None,
mode: ProxyMode::Advisory,
quality_config: QualityConfig::default(),
};
let response = service.proxy_operation(request).await.unwrap();
assert!(matches!(response.status, ProxyStatus::Accepted));
assert!(!response.quality_report.violations.is_empty());
}
#[test]
fn test_get_operation_content() {
let service = QualityProxyService::new();
let write_request = ProxyRequest {
operation: ProxyOperation::Write,
file_path: "test.rs".to_string(),
content: Some("write content".to_string()),
old_content: None,
new_content: None,
mode: ProxyMode::Strict,
quality_config: QualityConfig::default(),
};
let content = service.get_operation_content(&write_request).unwrap();
assert_eq!(content, "write content");
let edit_request = ProxyRequest {
operation: ProxyOperation::Edit,
file_path: "test.rs".to_string(),
content: Some("original content here".to_string()),
old_content: Some("original".to_string()),
new_content: Some("modified".to_string()),
mode: ProxyMode::Strict,
quality_config: QualityConfig::default(),
};
let content = service.get_operation_content(&edit_request).unwrap();
assert_eq!(content, "modified content here");
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}