Skip to content

Commit

Permalink
Add RDJson support. (#11682)
Browse files Browse the repository at this point in the history
## Summary

Implement support for RDJson output for `ruff check`, as requested in
#8655.

## Test Plan

Tested using a snapshot test. Same approach as for e.g. the JSON output
formatter.

## Additional info

I tried to keep the implementation close to the JSON implementation.

I had to deviate a bit to make the `suggestions` key work: If there are
no suggestions, then setting `suggestions` to `null` is invalid
according to the JSONSchema. Therefore, I opted for a slightly more
complex implementation, that skips the `suggestions` key entirely if
there are no fixes available for the given diagnostic. Maybe it would
have been easier to set `"suggestions": []`, but I ended up doing it
this way.

I didn't consider notebooks, as I _think_ that RDJson doesn't work with
notebooks. This should be confirmed, and if so, there should be some
form of warning or error emitted when trying to output diagnostics for a
notebook.

I also didn't consider `ruff format`, as this comment:
#8655 (comment)
suggests that that wouldn't be compatible.

I'm new to Rust, any feedback is appreciated. 🙂 I
implemented this in order to have a productive rainy saturday afternoon,
I'm not knowledgeable about RDJson beyond the sources linked in the
issue.
  • Loading branch information
tobb10001 authored Jun 2, 2024
1 parent 6d79ddc commit 0ea2519
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 2 deletions.
6 changes: 5 additions & 1 deletion crates/ruff/src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ 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, SarifEmitter, TextEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, RdjsonEmitter, SarifEmitter,
TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::registry::{AsRule, Rule};
Expand Down Expand Up @@ -242,6 +243,9 @@ impl Printer {
SerializationFormat::Json => {
JsonEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Rdjson => {
RdjsonEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::JsonLines => {
JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?;
}
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 @@ -13,6 +13,7 @@ pub use json::JsonEmitter;
pub use json_lines::JsonLinesEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
pub use rdjson::RdjsonEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceLocation};
Expand All @@ -29,6 +30,7 @@ mod json;
mod json_lines;
mod junit;
mod pylint;
mod rdjson;
mod sarif;
mod text;

Expand Down
138 changes: 138 additions & 0 deletions crates/ruff_linter/src/message/rdjson.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::io::Write;

use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::{json, Value};

use ruff_diagnostics::Edit;
use ruff_source_file::SourceCode;
use ruff_text_size::Ranged;

use crate::message::{Emitter, EmitterContext, Message, SourceLocation};
use crate::registry::AsRule;

#[derive(Default)]
pub struct RdjsonEmitter;

impl Emitter for RdjsonEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
messages: &[Message],
_context: &EmitterContext,
) -> anyhow::Result<()> {
serde_json::to_writer_pretty(
writer,
&json!({
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff",
},
"severity": "warning",
"diagnostics": &ExpandedMessages{ messages }
}),
)?;

Ok(())
}
}

struct ExpandedMessages<'a> {
messages: &'a [Message],
}

impl Serialize for ExpandedMessages<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;

for message in self.messages {
let value = message_to_rdjson_value(message);
s.serialize_element(&value)?;
}

s.end()
}
}

fn message_to_rdjson_value(message: &Message) -> Value {
let source_code = message.file.to_source_code();

let start_location = source_code.source_location(message.start());
let end_location = source_code.source_location(message.end());

if let Some(fix) = message.fix.as_ref() {
json!({
"message": message.kind.body,
"location": {
"path": message.filename(),
"range": rdjson_range(&start_location, &end_location),
},
"code": {
"value": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
},
"suggestions": rdjson_suggestions(fix.edits(), &source_code),
})
} else {
json!({
"message": message.kind.body,
"location": {
"path": message.filename(),
"range": rdjson_range(&start_location, &end_location),
},
"code": {
"value": message.kind.rule().noqa_code().to_string(),
"url": message.kind.rule().url(),
},
})
}
}

fn rdjson_suggestions(edits: &[Edit], source_code: &SourceCode) -> Value {
Value::Array(
edits
.iter()
.map(|edit| {
let location = source_code.source_location(edit.start());
let end_location = source_code.source_location(edit.end());

json!({
"range": rdjson_range(&location, &end_location),
"text": edit.content().unwrap_or_default(),
})
})
.collect(),
)
}

fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value {
json!({
"start": {
"line": start.row,
"column": start.column,
},
"end": {
"line": end.row,
"column": end.column,
},
})
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;

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

#[test]
fn output() {
let mut emitter = RdjsonEmitter;
let content = capture_emitter_output(&mut emitter, &create_messages());

assert_snapshot!(content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
source: crates/ruff_linter/src/message/rdjson.rs
expression: content
---
{
"diagnostics": [
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/unused-import",
"value": "F401"
},
"location": {
"path": "fib.py",
"range": {
"end": {
"column": 10,
"line": 1
},
"start": {
"column": 8,
"line": 1
}
}
},
"message": "`os` imported but unused",
"suggestions": [
{
"range": {
"end": {
"column": 1,
"line": 2
},
"start": {
"column": 1,
"line": 1
}
},
"text": ""
}
]
},
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/unused-variable",
"value": "F841"
},
"location": {
"path": "fib.py",
"range": {
"end": {
"column": 6,
"line": 6
},
"start": {
"column": 5,
"line": 6
}
}
},
"message": "Local variable `x` is assigned to but never used",
"suggestions": [
{
"range": {
"end": {
"column": 10,
"line": 6
},
"start": {
"column": 5,
"line": 6
}
},
"text": ""
}
]
},
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/undefined-name",
"value": "F821"
},
"location": {
"path": "undef.py",
"range": {
"end": {
"column": 5,
"line": 1
},
"start": {
"column": 4,
"line": 1
}
}
},
"message": "Undefined name `a`"
}
],
"severity": "warning",
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff"
}
}
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/settings/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ pub enum SerializationFormat {
Github,
Gitlab,
Pylint,
Rdjson,
Azure,
Sarif,
}
Expand All @@ -532,6 +533,7 @@ impl Display for SerializationFormat {
Self::Github => write!(f, "github"),
Self::Gitlab => write!(f, "gitlab"),
Self::Pylint => write!(f, "pylint"),
Self::Rdjson => write!(f, "rdjson"),
Self::Azure => write!(f, "azure"),
Self::Sarif => write!(f, "sarif"),
}
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ Options:
format is "concise". In preview mode, the default serialization
format is "full" [env: RUFF_OUTPUT_FORMAT=] [possible values: text,
concise, full, json, json-lines, junit, grouped, github, gitlab,
pylint, azure, sarif]
pylint, rdjson, azure, sarif]
-o, --output-file <OUTPUT_FILE>
Specify file to write the linter output to (default: stdout) [env:
RUFF_OUTPUT_FILE=]
Expand Down
1 change: 1 addition & 0 deletions ruff.schema.json

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

0 comments on commit 0ea2519

Please sign in to comment.