vtcode_core/code/code_quality/linting/
mod.rs

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