diff --git a/crates/ruff_linter/src/message/json.rs b/crates/ruff_linter/src/message/json.rs index e1a2e3e36df98..db7825e244e9b 100644 --- a/crates/ruff_linter/src/message/json.rs +++ b/crates/ruff_linter/src/message/json.rs @@ -5,6 +5,7 @@ use serde::{Serialize, Serializer}; use serde_json::{json, Value}; use ruff_diagnostics::Edit; +use ruff_notebook::NotebookIndex; use ruff_source_file::SourceCode; use ruff_text_size::Ranged; @@ -19,9 +20,9 @@ impl Emitter for JsonEmitter { &mut self, writer: &mut dyn Write, messages: &[Message], - _context: &EmitterContext, + context: &EmitterContext, ) -> anyhow::Result<()> { - serde_json::to_writer_pretty(writer, &ExpandedMessages { messages })?; + serde_json::to_writer_pretty(writer, &ExpandedMessages { messages, context })?; Ok(()) } @@ -29,6 +30,7 @@ impl Emitter for JsonEmitter { struct ExpandedMessages<'a> { messages: &'a [Message], + context: &'a EmitterContext<'a>, } impl Serialize for ExpandedMessages<'_> { @@ -39,7 +41,7 @@ impl Serialize for ExpandedMessages<'_> { let mut s = serializer.serialize_seq(Some(self.messages.len()))?; for message in self.messages { - let value = message_to_json_value(message); + let value = message_to_json_value(message, self.context); s.serialize_element(&value)?; } @@ -47,26 +49,36 @@ impl Serialize for ExpandedMessages<'_> { } } -pub(crate) fn message_to_json_value(message: &Message) -> Value { +pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value { let source_code = message.file.to_source_code(); + let notebook_index = context.notebook_index(message.filename()); let fix = message.fix.as_ref().map(|fix| { json!({ "applicability": fix.applicability(), "message": message.kind.suggestion.as_deref(), - "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code }, + "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index }, }) }); - let start_location = source_code.source_location(message.start()); - let end_location = source_code.source_location(message.end()); - let noqa_location = source_code.source_location(message.noqa_offset); + let mut start_location = source_code.source_location(message.start()); + let mut end_location = source_code.source_location(message.end()); + let mut noqa_location = source_code.source_location(message.noqa_offset); + let mut notebook_cell_index = None; + + if let Some(notebook_index) = notebook_index { + notebook_cell_index = Some(notebook_index.cell(start_location.row.get()).unwrap_or(1)); + start_location = notebook_index.translated_location(&start_location); + end_location = notebook_index.translated_location(&end_location); + noqa_location = notebook_index.translated_location(&noqa_location); + } json!({ "code": message.kind.rule().noqa_code().to_string(), "url": message.kind.rule().url(), "message": message.kind.body, "fix": fix, + "cell": notebook_cell_index, "location": start_location, "end_location": end_location, "filename": message.filename(), @@ -77,6 +89,7 @@ pub(crate) fn message_to_json_value(message: &Message) -> Value { struct ExpandedEdits<'a> { edits: &'a [Edit], source_code: &'a SourceCode<'a, 'a>, + notebook_index: Option<&'a NotebookIndex>, } impl Serialize for ExpandedEdits<'_> { @@ -87,10 +100,18 @@ impl Serialize for ExpandedEdits<'_> { let mut s = serializer.serialize_seq(Some(self.edits.len()))?; for edit in self.edits { + let mut location = self.source_code.source_location(edit.start()); + let mut end_location = self.source_code.source_location(edit.end()); + + if let Some(notebook_index) = self.notebook_index { + location = notebook_index.translated_location(&location); + end_location = notebook_index.translated_location(&end_location); + } + let value = json!({ "content": edit.content().unwrap_or_default(), - "location": self.source_code.source_location(edit.start()), - "end_location": self.source_code.source_location(edit.end()) + "location": location, + "end_location": end_location }); s.serialize_element(&value)?; diff --git a/crates/ruff_linter/src/message/json_lines.rs b/crates/ruff_linter/src/message/json_lines.rs index 360e7ec6a71a7..048cb07fd7f86 100644 --- a/crates/ruff_linter/src/message/json_lines.rs +++ b/crates/ruff_linter/src/message/json_lines.rs @@ -11,11 +11,11 @@ impl Emitter for JsonLinesEmitter { &mut self, writer: &mut dyn Write, messages: &[Message], - _context: &EmitterContext, + context: &EmitterContext, ) -> anyhow::Result<()> { let mut w = writer; for message in messages { - serde_json::to_writer(&mut w, &message_to_json_value(message))?; + serde_json::to_writer(&mut w, &message_to_json_value(message, context))?; w.write_all(b"\n")?; } Ok(()) diff --git a/crates/ruff_notebook/src/index.rs b/crates/ruff_notebook/src/index.rs index 23259468ee56f..25890bc25c607 100644 --- a/crates/ruff_notebook/src/index.rs +++ b/crates/ruff_notebook/src/index.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use ruff_source_file::{OneIndexed, SourceLocation}; + /// Jupyter Notebook indexing table /// /// When we lint a jupyter notebook, we have to translate the row/column based on @@ -23,4 +25,16 @@ impl NotebookIndex { pub fn cell_row(&self, row: usize) -> Option { self.row_to_row_in_cell.get(row).copied() } + + /// Translates the given source location based on the indexing table. + /// + /// This will translate the row/column in the concatenated source code + /// to the row/column in the Jupyter Notebook. + pub fn translated_location(&self, source_location: &SourceLocation) -> SourceLocation { + SourceLocation { + row: OneIndexed::new(self.cell_row(source_location.row.get()).unwrap_or(1) as usize) + .unwrap(), + column: source_location.column, + } + } }