Parse PHPCS's JSON output into LSP diagnostics. PHPCS JSON format (with `--report=json`): ```json { "totals": { "errors": 1, "warnings": 1, "fixable": 2 }, "files": { "/path/to/file.php": { "errors": 1, "warnings": 1, "messages": [ { "message": "Line indented incorrectly; expected 4 spaces, found 2", "source": "PSR2.Methods.FunctionCallSignature.Indent", "severity": 5, "fixable": true, "type": "
(json_str: &str, file_path: &Path)
| 252 | /// by the `--stdin-path` value. When there is only one file entry, |
| 253 | /// we use it regardless of the key to avoid path-matching issues. |
| 254 | fn parse_phpcs_json(json_str: &str, file_path: &Path) -> Result<Vec<Diagnostic>, String> { |
| 255 | let output: serde_json::Value = |
| 256 | serde_json::from_str(json_str).map_err(|e| format!("Failed to parse PHPCS JSON: {}", e))?; |
| 257 | |
| 258 | let mut diagnostics = Vec::new(); |
| 259 | |
| 260 | if let Some(files) = output.get("files").and_then(|f| f.as_object()) { |
| 261 | // When using stdin mode with --stdin-path, PHPCS keys the output |
| 262 | // by the stdin-path value. Try matching by the path, and if |
| 263 | // there's only one file entry, use it regardless of key. |
| 264 | let messages = if files.len() == 1 { |
| 265 | files.values().next() |
| 266 | } else { |
| 267 | let file_path_str = file_path.to_string_lossy(); |
| 268 | files |
| 269 | .iter() |
| 270 | .find(|(path, _)| paths_match(path, &file_path_str)) |
| 271 | .map(|(_, v)| v) |
| 272 | }; |
| 273 | |
| 274 | if let Some(msgs) = messages |
| 275 | .and_then(|fd| fd.get("messages")) |
| 276 | .and_then(|m| m.as_array()) |
| 277 | { |
| 278 | for msg in msgs { |
| 279 | if let Some(diag) = parse_phpcs_message(msg) { |
| 280 | diagnostics.push(diag); |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | Ok(diagnostics) |
| 287 | } |
| 288 | |
| 289 | /// Parse a single PHPCS message object into an LSP `Diagnostic`. |
| 290 | fn parse_phpcs_message(msg: &serde_json::Value) -> Option<Diagnostic> { |