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