|
| 1 | +use std::{collections::HashMap, fs::File, io::Write}; |
| 2 | + |
| 3 | +use emmylua_code_analysis::{DbIndex, FileId, file_path_to_uri}; |
| 4 | +use lsp_types::{Diagnostic, DiagnosticSeverity}; |
| 5 | +use serde_json::{Value, json}; |
| 6 | + |
| 7 | +use crate::cmd_args::OutputDestination; |
| 8 | + |
| 9 | +use super::OutputWriter; |
| 10 | + |
| 11 | +const CRATE_NAME: &str = env!("CARGO_PKG_NAME"); |
| 12 | +const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION"); |
| 13 | + |
| 14 | +#[derive(Debug)] |
| 15 | +pub struct SarifOutputWriter { |
| 16 | + output: Option<File>, |
| 17 | + tools: HashMap<String, Value>, |
| 18 | + current_results: Vec<Value>, |
| 19 | +} |
| 20 | + |
| 21 | +impl SarifOutputWriter { |
| 22 | + pub fn new(output: OutputDestination) -> Self { |
| 23 | + let output = match output { |
| 24 | + OutputDestination::Stdout => None, |
| 25 | + OutputDestination::File(path) => { |
| 26 | + if let Some(parent) = path.parent() { |
| 27 | + if !parent.exists() { |
| 28 | + std::fs::create_dir_all(parent).unwrap(); |
| 29 | + } |
| 30 | + } |
| 31 | + Some(std::fs::File::create(path).unwrap()) |
| 32 | + } |
| 33 | + }; |
| 34 | + |
| 35 | + SarifOutputWriter { |
| 36 | + output, |
| 37 | + tools: HashMap::new(), |
| 38 | + current_results: Vec::new(), |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + fn get_sarif_level(&self, severity: Option<DiagnosticSeverity>) -> &'static str { |
| 43 | + match severity { |
| 44 | + Some(DiagnosticSeverity::ERROR) => "error", |
| 45 | + Some(DiagnosticSeverity::WARNING) => "warning", |
| 46 | + Some(DiagnosticSeverity::INFORMATION) => "note", |
| 47 | + Some(DiagnosticSeverity::HINT) => "note", |
| 48 | + None => "note", |
| 49 | + _ => "note", // Handle other possible values |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + fn ensure_tool(&mut self) -> String { |
| 54 | + let tool_name = "emmylua_check".to_string(); |
| 55 | + if !self.tools.contains_key(&tool_name) { |
| 56 | + let tool = json!({ |
| 57 | + "name": CRATE_NAME, |
| 58 | + "version": CRATE_VERSION, |
| 59 | + "informationUri": "https://github.com/EmmyLuaLs/emmylua-analyzer-rust", |
| 60 | + "organization": "EmmyLuaLs" |
| 61 | + }); |
| 62 | + self.tools.insert(tool_name.clone(), tool); |
| 63 | + } |
| 64 | + tool_name |
| 65 | + } |
| 66 | + |
| 67 | + fn convert_diagnostic_to_sarif_result( |
| 68 | + &mut self, |
| 69 | + file_uri: &str, |
| 70 | + diagnostic: &Diagnostic, |
| 71 | + ) -> Value { |
| 72 | + // Convert LSP Range to SARIF region |
| 73 | + let region = json!({ |
| 74 | + "startLine": diagnostic.range.start.line + 1, // SARIF uses 1-based line numbers |
| 75 | + "startColumn": diagnostic.range.start.character + 1, // SARIF uses 1-based column numbers |
| 76 | + "endLine": diagnostic.range.end.line + 1, |
| 77 | + "endColumn": diagnostic.range.end.character + 1 |
| 78 | + }); |
| 79 | + |
| 80 | + let location = json!({ |
| 81 | + "physicalLocation": { |
| 82 | + "artifactLocation": { |
| 83 | + "uri": file_uri |
| 84 | + }, |
| 85 | + "region": region |
| 86 | + } |
| 87 | + }); |
| 88 | + |
| 89 | + let rule_id = diagnostic |
| 90 | + .code |
| 91 | + .as_ref() |
| 92 | + .map(|code| match code { |
| 93 | + lsp_types::NumberOrString::Number(n) => n.to_string(), |
| 94 | + lsp_types::NumberOrString::String(s) => s.clone(), |
| 95 | + }) |
| 96 | + .unwrap_or_else(|| "unknown".to_string()); |
| 97 | + |
| 98 | + let result = json!({ |
| 99 | + "ruleId": rule_id, |
| 100 | + "level": self.get_sarif_level(diagnostic.severity), |
| 101 | + "message": { |
| 102 | + "text": diagnostic.message |
| 103 | + }, |
| 104 | + "locations": [location] |
| 105 | + }); |
| 106 | + |
| 107 | + result |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +impl OutputWriter for SarifOutputWriter { |
| 112 | + fn write(&mut self, db: &DbIndex, file_id: FileId, diagnostics: Vec<Diagnostic>) { |
| 113 | + if diagnostics.is_empty() { |
| 114 | + return; |
| 115 | + } |
| 116 | + |
| 117 | + let file_path = db.get_vfs().get_file_path(&file_id).unwrap(); |
| 118 | + let file_uri = file_path_to_uri(&file_path).unwrap().as_str().to_string(); |
| 119 | + self.ensure_tool(); |
| 120 | + |
| 121 | + for diagnostic in diagnostics { |
| 122 | + let result = self.convert_diagnostic_to_sarif_result(&file_uri, &diagnostic); |
| 123 | + self.current_results.push(result); |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + fn finish(&mut self) { |
| 128 | + // Create the tool object |
| 129 | + let tool_name = self.ensure_tool(); |
| 130 | + let tool = self.tools.get(&tool_name).unwrap().clone(); |
| 131 | + |
| 132 | + // Create a single run |
| 133 | + let run = json!({ |
| 134 | + "tool": { |
| 135 | + "driver": tool |
| 136 | + }, |
| 137 | + "results": self.current_results |
| 138 | + }); |
| 139 | + |
| 140 | + // Create the complete SARIF document |
| 141 | + let sarif_document = json!({ |
| 142 | + "version": "2.1.0", |
| 143 | + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", |
| 144 | + "runs": [run] |
| 145 | + }); |
| 146 | + |
| 147 | + let pretty_json = serde_json::to_string_pretty(&sarif_document).unwrap(); |
| 148 | + |
| 149 | + if let Some(output) = self.output.as_mut() { |
| 150 | + output.write_all(pretty_json.as_bytes()).unwrap(); |
| 151 | + } else { |
| 152 | + println!("{}", pretty_json); |
| 153 | + } |
| 154 | + } |
| 155 | +} |
0 commit comments