Skip to content

Commit f441854

Browse files
committed
support sarif output format
1 parent b1f4eb5 commit f441854

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed

crates/emmylua_check/src/cmd_args.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub struct CmdArgs {
5454
pub enum OutputFormat {
5555
Json,
5656
Text,
57+
Sarif,
5758
}
5859

5960
#[allow(unused)]

crates/emmylua_check/src/output/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod json_output_writer;
2+
mod sarif_output_writer;
23
mod text_output_writer;
34

45
use std::path::PathBuf;
@@ -25,6 +26,7 @@ pub async fn output_result(
2526
OutputFormat::Text => {
2627
Box::new(text_output_writer::TextOutputWriter::new(workspace.clone()))
2728
}
29+
OutputFormat::Sarif => Box::new(sarif_output_writer::SarifOutputWriter::new(output)),
2830
};
2931

3032
let terminal_display = TerminalDisplay::new(workspace);
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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

Comments
 (0)