vtcode_core/code/code_quality/linting/
mod.rs1pub 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#[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#[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
32pub 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 orchestrator.register(LintConfig::clippy());
45 orchestrator.register(LintConfig::eslint());
46 orchestrator.register(LintConfig::pylint());
47
48 orchestrator
49 }
50
51 pub fn register(&mut self, config: LintConfig) {
53 self.configs.push(config);
54 }
55
56 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 let mut cmd = Command::new(&config.command[0]);
74
75 for arg in &config.args {
77 cmd.arg(arg);
78 }
79
80 cmd.arg(path);
82
83 match cmd.output() {
84 Ok(output) => {
85 if output.status.success() {
86 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 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(), }
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}