Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add SARIF output support #9078

Merged
merged 15 commits into from
Dec 13, 2023
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion crates/ruff_cli/src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, TextEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, SarifEmitter, TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::registry::{AsRule, Rule};
Expand Down Expand Up @@ -291,6 +291,9 @@ impl Printer {
SerializationFormat::Azure => {
AzureEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Sarif => {
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
}
}

writer.flush()?;
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode_names2 = { workspace = true }
url = { version = "2.2.2" }
wsl = { version = "0.1.0" }

[dev-dependencies]
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter;
pub use text::TextEmitter;

mod azure;
Expand All @@ -28,6 +29,7 @@ mod json;
mod json_lines;
mod junit;
mod pylint;
mod sarif;
mod text;

#[derive(Debug, PartialEq, Eq)]
Expand Down
212 changes: 212 additions & 0 deletions crates/ruff_linter/src/message/sarif.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use std::io::Write;

use anyhow::Result;
use serde::{Serialize, Serializer};
use serde_json::json;

use ruff_source_file::OneIndexed;

use crate::codes::Rule;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::{AsRule, Linter, RuleNamespace};
use crate::VERSION;

use strum::IntoEnumIterator;

pub struct SarifEmitter;

impl Emitter for SarifEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
messages: &[Message],
_context: &EmitterContext,
) -> Result<()> {
let results = messages
.iter()
.map(SarifResult::from_message)
.collect::<Result<Vec<_>>>()?;

let output = json!({
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "ruff",
"informationUri": "https://github.com/astral-sh/ruff",
"rules": Rule::iter().map(SarifRule::from).collect::<Vec<_>>(),
"version": VERSION.to_string(),
}
},
"results": results,
}],
});
serde_json::to_writer_pretty(writer, &output)?;
Ok(())
}
}

#[derive(Debug, Clone)]
struct SarifRule<'a> {
name: &'a str,
code: String,
linter: &'a str,
summary: &'a str,
explanation: Option<&'a str>,
url: Option<String>,
}

impl From<Rule> for SarifRule<'_> {
fn from(rule: Rule) -> Self {
let code = rule.noqa_code().to_string();
let (linter, _) = Linter::parse_code(&code).unwrap();
Self {
name: rule.into(),
code,
linter: linter.name(),
summary: rule.message_formats()[0],
explanation: rule.explanation(),
url: rule.url(),
}
}
}

impl Serialize for SarifRule<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
json!({
"id": self.code,
"shortDescription": {
"text": self.summary,
},
"fullDescription": {
"text": self.explanation,
},
"help": {
"text": self.summary,
},
"helpUri": self.url,
"properties": {
"id": self.code,
"kind": self.linter,
"name": self.name,
"problem.severity": "error".to_string(),
},
})
.serialize(serializer)
}
}

#[derive(Debug)]
struct SarifResult {
charliermarsh marked this conversation as resolved.
Show resolved Hide resolved
rule: Rule,
level: String,
message: String,
uri: String,
start_line: OneIndexed,
start_column: OneIndexed,
end_line: OneIndexed,
end_column: OneIndexed,
}

impl SarifResult {
#[cfg(not(target_arch = "wasm32"))]
fn from_message(message: &Message) -> Result<Self> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
Ok(Self {
rule: message.kind.rule(),
level: "error".to_string(),
message: message.kind.name.clone(),
uri: url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
.to_string(),
start_line: start_location.row,
start_column: start_location.column,
end_line: end_location.row,
end_column: end_location.column,
})
}

#[cfg(target_arch = "wasm32")]
#[allow(clippy::unnecessary_wraps)]
fn from_message(message: &Message) -> Result<Self> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let path = normalize_path(message.filename());
Ok(Self {
rule: message.kind.rule(),
level: "error".to_string(),
message: message.kind.name.clone(),
uri: path.display().to_string(),
start_line: start_location.row,
start_column: start_location.column,
end_line: end_location.row,
end_column: end_location.column,
})
}
}

impl Serialize for SarifResult {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
json!({
"level": self.level,
"message": {
"text": self.message,
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": self.uri,
},
"region": {
"startLine": self.start_line,
"startColumn": self.start_column,
"endLine": self.end_line,
"endColumn": self.end_column,
}
}
}],
"ruleId": self.rule.noqa_code().to_string(),
})
.serialize(serializer)
}
}

#[cfg(test)]
mod tests {

use crate::message::tests::{capture_emitter_output, create_messages};
use crate::message::SarifEmitter;

fn get_output() -> String {
let mut emitter = SarifEmitter {};
capture_emitter_output(&mut emitter, &create_messages())
}

#[test]
fn valid_json() {
let content = get_output();
serde_json::from_str::<serde_json::Value>(&content).unwrap();
}

#[test]
fn test_results() {
let content = get_output();
let sarif = serde_json::from_str::<serde_json::Value>(content.as_str()).unwrap();
let rules = sarif["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
assert!(rules.len() > 3);
}
}
1 change: 1 addition & 0 deletions crates/ruff_linter/src/settings/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ pub enum SerializationFormat {
Gitlab,
Pylint,
Azure,
Sarif,
}

impl Default for SerializationFormat {
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ Options:
--ignore-noqa
Ignore any `# noqa` comments
--output-format <OUTPUT_FORMAT>
Output serialization format for violations [env: RUFF_OUTPUT_FORMAT=] [possible values: text, json, json-lines, junit, grouped, github, gitlab, pylint, azure]
Output serialization format for violations [env: RUFF_OUTPUT_FORMAT=] [possible values: text, json, json-lines, junit, grouped, github, gitlab, pylint, azure, sarif]
-o, --output-file <OUTPUT_FILE>
Specify file to write the linter output to (default: stdout)
--target-version <TARGET_VERSION>
Expand Down
3 changes: 2 additions & 1 deletion ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading