Skip to main content

vtcode_core/code/code_quality/linting/
mod.rs

1use crate::code::code_quality::config::{LintConfig, LintSeverity};
2use serde_json::Value;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5// use anyhow::Result;
6
7/// Shared utilities for lint output parsing
8mod parser_utils {
9    use crate::code::code_quality::config::LintSeverity;
10    use serde_json::Value;
11
12    /// Extract string value from JSON, return as owned String or default
13    pub fn get_str(value: &Value, key: &str, default: &str) -> String {
14        value
15            .get(key)
16            .and_then(Value::as_str)
17            .unwrap_or(default)
18            .to_string()
19    }
20
21    /// Extract u64 and convert to usize
22    pub fn get_u64(value: &Value, key: &str) -> usize {
23        value.get(key).and_then(Value::as_u64).unwrap_or(0) as usize
24    }
25
26    /// Parse severity level from string
27    pub fn parse_severity_error(level: &str) -> LintSeverity {
28        match level {
29            "error" | "fatal" => LintSeverity::Error,
30            "warning" => LintSeverity::Warning,
31            _ => LintSeverity::Info,
32        }
33    }
34
35    /// Parse severity level from numeric code (1=warning, 2=error)
36    pub fn parse_severity_numeric(code: u64) -> LintSeverity {
37        match code {
38            2 => LintSeverity::Error,
39            1 => LintSeverity::Warning,
40            _ => LintSeverity::Info,
41        }
42    }
43}
44
45/// Individual lint finding
46#[derive(Debug, Clone)]
47pub struct LintFinding {
48    pub file_path: PathBuf,
49    pub line: usize,
50    pub column: usize,
51    pub severity: LintSeverity,
52    pub rule: String,
53    pub message: String,
54    pub suggestion: Option<String>,
55}
56
57/// Result of linting operation
58#[derive(Debug, Clone)]
59pub struct LintResult {
60    pub success: bool,
61    pub findings: Vec<LintFinding>,
62    pub error_message: Option<String>,
63    pub tool_used: String,
64}
65
66/// Linting orchestrator that manages multiple linters
67pub struct LintingOrchestrator {
68    configs: Vec<LintConfig>,
69}
70
71impl Default for LintingOrchestrator {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl LintingOrchestrator {
78    pub fn new() -> Self {
79        let mut orchestrator = Self {
80            configs: Vec::new(),
81        };
82
83        // Register default linters
84        orchestrator.register(LintConfig::clippy());
85        orchestrator.register(LintConfig::eslint());
86        orchestrator.register(LintConfig::pylint());
87
88        orchestrator
89    }
90
91    /// Register a linting configuration
92    pub fn register(&mut self, config: LintConfig) {
93        self.configs.push(config);
94    }
95
96    /// Lint a file or directory
97    pub async fn lint_path(&self, path: &Path) -> Vec<LintResult> {
98        let mut results = Vec::new();
99
100        for config in &self.configs {
101            if config.enabled
102                && let Some(result) = self.run_linter(config, path).await
103            {
104                results.push(result);
105            }
106        }
107
108        results
109    }
110
111    async fn run_linter(&self, config: &LintConfig, path: &Path) -> Option<LintResult> {
112        // Execute the actual linting tool
113        let mut cmd = Command::new(&config.command[0]);
114
115        // Add arguments
116        for arg in &config.args {
117            cmd.arg(arg);
118        }
119
120        // Add the path as the last argument
121        cmd.arg(path);
122
123        match cmd.output() {
124            Ok(output) => {
125                if output.status.success() {
126                    // Parse the lint output based on the tool
127                    let findings = self.parse_lint_output(config, &output.stdout, path);
128
129                    Some(LintResult {
130                        success: true,
131                        findings,
132                        error_message: None,
133                        tool_used: config.tool_name.clone(),
134                    })
135                } else {
136                    let error_msg = String::from_utf8_lossy(&output.stderr).to_string();
137                    Some(LintResult {
138                        success: false,
139                        findings: Vec::new(),
140                        error_message: Some(error_msg),
141                        tool_used: config.tool_name.clone(),
142                    })
143                }
144            }
145            Err(e) => Some(LintResult {
146                success: false,
147                findings: Vec::new(),
148                error_message: Some(format!("Failed to execute {}: {}", config.tool_name, e)),
149                tool_used: config.tool_name.clone(),
150            }),
151        }
152    }
153
154    fn parse_lint_output(
155        &self,
156        config: &LintConfig,
157        output: &[u8],
158        base_path: &Path,
159    ) -> Vec<LintFinding> {
160        let output_str = String::from_utf8_lossy(output);
161
162        // Parse based on the tool used
163        match config.tool_name.as_str() {
164            "clippy" => self.parse_clippy_output(&output_str, base_path),
165            "eslint" => self.parse_eslint_output(&output_str, base_path),
166            "pylint" => self.parse_pylint_output(&output_str, base_path),
167            _ => Vec::new(), // Unknown tool, return empty findings
168        }
169    }
170
171    fn parse_clippy_output(&self, output: &str, base_path: &Path) -> Vec<LintFinding> {
172        let mut findings = Vec::new();
173        for line in output.lines() {
174            if let Ok(json) = serde_json::from_str::<Value>(line)
175                && json.get("reason").and_then(Value::as_str) == Some("compiler-message")
176                && let Some(message) = json.get("message")
177                && let Some(spans) = message.get("spans").and_then(Value::as_array)
178            {
179                for span in spans {
180                    if span.get("is_primary").and_then(Value::as_bool) == Some(true) {
181                        let file = span.get("file_name").and_then(Value::as_str).unwrap_or("");
182                        let line_num =
183                            span.get("line_start").and_then(Value::as_u64).unwrap_or(0) as usize;
184                        let column = span
185                            .get("column_start")
186                            .and_then(Value::as_u64)
187                            .unwrap_or(0) as usize;
188                        let rule = parser_utils::get_str(
189                            message
190                                .get("code")
191                                .and_then(|c| c.get("code"))
192                                .unwrap_or(&Value::Null),
193                            "code",
194                            "",
195                        );
196                        let severity = parser_utils::parse_severity_error(&parser_utils::get_str(
197                            message, "level", "",
198                        ));
199                        let msg = parser_utils::get_str(message, "message", "");
200                        findings.push(LintFinding {
201                            file_path: base_path.join(file),
202                            line: line_num,
203                            column,
204                            severity,
205                            rule,
206                            message: msg,
207                            suggestion: None,
208                        });
209                    }
210                }
211            }
212        }
213        findings
214    }
215
216    fn parse_eslint_output(&self, output: &str, base_path: &Path) -> Vec<LintFinding> {
217        let mut findings = Vec::new();
218        if let Ok(json) = serde_json::from_str::<Value>(output)
219            && let Some(arr) = json.as_array()
220        {
221            for file in arr {
222                let path = file.get("filePath").and_then(Value::as_str).unwrap_or("");
223                if let Some(messages) = file.get("messages").and_then(Value::as_array) {
224                    for m in messages {
225                        let line = parser_utils::get_u64(m, "line");
226                        let column = parser_utils::get_u64(m, "column");
227                        let rule = parser_utils::get_str(m, "ruleId", "");
228                        let severity = parser_utils::parse_severity_numeric(
229                            m.get("severity").and_then(Value::as_u64).unwrap_or(0),
230                        );
231                        let msg = parser_utils::get_str(m, "message", "");
232                        findings.push(LintFinding {
233                            file_path: base_path.join(path),
234                            line,
235                            column,
236                            severity,
237                            rule,
238                            message: msg,
239                            suggestion: m.get("fix").map(|_| "fix available".to_owned()),
240                        });
241                    }
242                }
243            }
244        }
245        findings
246    }
247
248    fn parse_pylint_output(&self, output: &str, base_path: &Path) -> Vec<LintFinding> {
249        let mut findings = Vec::new();
250        if let Ok(json) = serde_json::from_str::<Value>(output)
251            && let Some(arr) = json.as_array()
252        {
253            for item in arr {
254                let path = parser_utils::get_str(item, "path", "");
255                let line = parser_utils::get_u64(item, "line");
256                let column = parser_utils::get_u64(item, "column");
257                let rule = parser_utils::get_str(item, "symbol", "");
258                let msg = parser_utils::get_str(item, "message", "");
259                let severity =
260                    parser_utils::parse_severity_error(&parser_utils::get_str(item, "type", ""));
261                findings.push(LintFinding {
262                    file_path: base_path.join(path),
263                    line,
264                    column,
265                    severity,
266                    rule,
267                    message: msg,
268                    suggestion: None,
269                });
270            }
271        }
272        findings
273    }
274}