vtcode_core/code/code_quality/linting/
mod.rs1use crate::code::code_quality::config::{LintConfig, LintSeverity};
2use serde_json::Value;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5mod parser_utils {
9 use crate::code::code_quality::config::LintSeverity;
10 use serde_json::Value;
11
12 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 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 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 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#[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#[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
66pub 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 orchestrator.register(LintConfig::clippy());
85 orchestrator.register(LintConfig::eslint());
86 orchestrator.register(LintConfig::pylint());
87
88 orchestrator
89 }
90
91 pub fn register(&mut self, config: LintConfig) {
93 self.configs.push(config);
94 }
95
96 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 let mut cmd = Command::new(&config.command[0]);
114
115 for arg in &config.args {
117 cmd.arg(arg);
118 }
119
120 cmd.arg(path);
122
123 match cmd.output() {
124 Ok(output) => {
125 if output.status.success() {
126 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 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(), }
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}