mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:10 +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::logging::LogLevel;
|
||||||
use ruff_linter::message::{
|
use ruff_linter::message::{
|
||||||
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
|
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::notify_user;
|
||||||
use ruff_linter::registry::{AsRule, Rule};
|
use ruff_linter::registry::{AsRule, Rule};
|
||||||
|
@ -242,6 +243,9 @@ impl Printer {
|
||||||
SerializationFormat::Json => {
|
SerializationFormat::Json => {
|
||||||
JsonEmitter.emit(writer, &diagnostics.messages, &context)?;
|
JsonEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||||
}
|
}
|
||||||
|
SerializationFormat::Rdjson => {
|
||||||
|
RdjsonEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||||
|
}
|
||||||
SerializationFormat::JsonLines => {
|
SerializationFormat::JsonLines => {
|
||||||
JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?;
|
JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub use json::JsonEmitter;
|
||||||
pub use json_lines::JsonLinesEmitter;
|
pub use json_lines::JsonLinesEmitter;
|
||||||
pub use junit::JunitEmitter;
|
pub use junit::JunitEmitter;
|
||||||
pub use pylint::PylintEmitter;
|
pub use pylint::PylintEmitter;
|
||||||
|
pub use rdjson::RdjsonEmitter;
|
||||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||||
use ruff_notebook::NotebookIndex;
|
use ruff_notebook::NotebookIndex;
|
||||||
use ruff_source_file::{SourceFile, SourceLocation};
|
use ruff_source_file::{SourceFile, SourceLocation};
|
||||||
|
@ -29,6 +30,7 @@ mod json;
|
||||||
mod json_lines;
|
mod json_lines;
|
||||||
mod junit;
|
mod junit;
|
||||||
mod pylint;
|
mod pylint;
|
||||||
|
mod rdjson;
|
||||||
mod sarif;
|
mod sarif;
|
||||||
mod text;
|
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,
|
Github,
|
||||||
Gitlab,
|
Gitlab,
|
||||||
Pylint,
|
Pylint,
|
||||||
|
Rdjson,
|
||||||
Azure,
|
Azure,
|
||||||
Sarif,
|
Sarif,
|
||||||
}
|
}
|
||||||
|
@ -532,6 +533,7 @@ impl Display for SerializationFormat {
|
||||||
Self::Github => write!(f, "github"),
|
Self::Github => write!(f, "github"),
|
||||||
Self::Gitlab => write!(f, "gitlab"),
|
Self::Gitlab => write!(f, "gitlab"),
|
||||||
Self::Pylint => write!(f, "pylint"),
|
Self::Pylint => write!(f, "pylint"),
|
||||||
|
Self::Rdjson => write!(f, "rdjson"),
|
||||||
Self::Azure => write!(f, "azure"),
|
Self::Azure => write!(f, "azure"),
|
||||||
Self::Sarif => write!(f, "sarif"),
|
Self::Sarif => write!(f, "sarif"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -599,7 +599,7 @@ Options:
|
||||||
format is "concise". In preview mode, the default serialization
|
format is "concise". In preview mode, the default serialization
|
||||||
format is "full" [env: RUFF_OUTPUT_FORMAT=] [possible values: text,
|
format is "full" [env: RUFF_OUTPUT_FORMAT=] [possible values: text,
|
||||||
concise, full, json, json-lines, junit, grouped, github, gitlab,
|
concise, full, json, json-lines, junit, grouped, github, gitlab,
|
||||||
pylint, azure, sarif]
|
pylint, rdjson, azure, sarif]
|
||||||
-o, --output-file <OUTPUT_FILE>
|
-o, --output-file <OUTPUT_FILE>
|
||||||
Specify file to write the linter output to (default: stdout) [env:
|
Specify file to write the linter output to (default: stdout) [env:
|
||||||
RUFF_OUTPUT_FILE=]
|
RUFF_OUTPUT_FILE=]
|
||||||
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3947,6 +3947,7 @@
|
||||||
"github",
|
"github",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
"pylint",
|
"pylint",
|
||||||
|
"rdjson",
|
||||||
"azure",
|
"azure",
|
||||||
"sarif"
|
"sarif"
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue