mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:09:22 +00:00
Add RDJson support. (#11682)
## 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: https://github.com/astral-sh/ruff/issues/8655#issuecomment-1811446160 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.
This commit is contained in:
parent
6d79ddc0aa
commit
0ea2519e80
7 changed files with 252 additions and 2 deletions
|
@ -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};
|
||||
|
@ -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)?;
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
@ -29,6 +30,7 @@ mod json;
|
|||
mod json_lines;
|
||||
mod junit;
|
||||
mod pylint;
|
||||
mod rdjson;
|
||||
mod sarif;
|
||||
mod text;
|
||||
|
||||
|
|
138
crates/ruff_linter/src/message/rdjson.rs
Normal file
138
crates/ruff_linter/src/message/rdjson.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -515,6 +515,7 @@ pub enum SerializationFormat {
|
|||
Github,
|
||||
Gitlab,
|
||||
Pylint,
|
||||
Rdjson,
|
||||
Azure,
|
||||
Sarif,
|
||||
}
|
||||
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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=]
|
||||
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3947,6 +3947,7 @@
|
|||
"github",
|
||||
"gitlab",
|
||||
"pylint",
|
||||
"rdjson",
|
||||
"azure",
|
||||
"sarif"
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue