use std::fmt::{Debug, Formatter, Write}; use itertools::Itertools; use ruff_python_ast::Ranged; use ruff_formatter::SourceCode; use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::{CommentsMap, SourceComment}; /// Prints a debug representation of [`SourceComment`] that includes the comment's text pub(crate) struct DebugComment<'a> { comment: &'a SourceComment, source_code: SourceCode<'a>, } impl<'a> DebugComment<'a> { pub(super) fn new(comment: &'a SourceComment, source_code: SourceCode<'a>) -> Self { Self { comment, source_code, } } } impl Debug for DebugComment<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut strut = f.debug_struct("SourceComment"); strut .field("text", &self.comment.slice.text(self.source_code)) .field("position", &self.comment.line_position) .field("formatted", &self.comment.formatted.get()); strut.finish() } } /// Pretty-printed debug representation of [`Comments`]. pub(crate) struct DebugComments<'a> { comments: &'a CommentsMap<'a>, source_code: SourceCode<'a>, } impl<'a> DebugComments<'a> { pub(super) fn new(comments: &'a CommentsMap, source_code: SourceCode<'a>) -> Self { Self { comments, source_code, } } } impl Debug for DebugComments<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut map = f.debug_map(); for node in self.comments.keys().sorted_by_key(|key| key.node().start()) { map.entry( &NodeKindWithSource { key: *node, source: self.source_code, }, &DebugNodeComments { comments: self.comments, source_code: self.source_code, key: *node, }, ); } map.finish() } } /// Prints the source code up to the first new line character. Truncates the text if it exceeds 40 characters. struct NodeKindWithSource<'a> { key: NodeRefEqualityKey<'a>, source: SourceCode<'a>, } impl Debug for NodeKindWithSource<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { struct TruncatedSource<'a>(&'a str); impl Debug for TruncatedSource<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_char('`')?; let first_line = if let Some(line_end_pos) = self.0.find(['\n', '\r']) { &self.0[..line_end_pos] } else { self.0 }; if first_line.len() > 40 { let (head, rest) = first_line.split_at(27); f.write_str(head)?; f.write_str("...")?; // Take the last 10 characters let tail = &rest[rest.len().saturating_sub(10)..]; f.write_str(tail)?; } else { f.write_str(first_line)?; } if first_line.len() < self.0.len() { f.write_str("\u{23ce}")?; } f.write_char('`') } } let kind = self.key.node().kind(); let source = self.source.slice(self.key.node().range()).text(self.source); f.debug_struct("Node") .field("kind", &kind) .field("range", &self.key.node().range()) .field("source", &TruncatedSource(source)) .finish() } } struct DebugNodeComments<'a> { comments: &'a CommentsMap<'a>, source_code: SourceCode<'a>, key: NodeRefEqualityKey<'a>, } impl Debug for DebugNodeComments<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_map() .entry( &"leading", &DebugNodeCommentSlice { node_comments: self.comments.leading(&self.key), source_code: self.source_code, }, ) .entry( &"dangling", &DebugNodeCommentSlice { node_comments: self.comments.dangling(&self.key), source_code: self.source_code, }, ) .entry( &"trailing", &DebugNodeCommentSlice { node_comments: self.comments.trailing(&self.key), source_code: self.source_code, }, ) .finish() } } struct DebugNodeCommentSlice<'a> { node_comments: &'a [SourceComment], source_code: SourceCode<'a>, } impl Debug for DebugNodeCommentSlice<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut list = f.debug_list(); for comment in self.node_comments { list.entry(&comment.debug(self.source_code)); } list.finish() } } #[cfg(test)] mod tests { use insta::assert_debug_snapshot; use ruff_python_ast::{StmtBreak, StmtContinue}; use ruff_text_size::{TextRange, TextSize}; use ruff_formatter::SourceCode; use ruff_python_ast::node::AnyNode; use crate::comments::map::MultiMap; use crate::comments::{CommentLinePosition, Comments, CommentsMap, SourceComment}; #[test] fn debug() { let continue_statement = AnyNode::from(StmtContinue { range: TextRange::default(), }); let break_statement = AnyNode::from(StmtBreak { range: TextRange::default(), }); let source = r#"# leading comment continue; # trailing # break leading break; "#; let source_code = SourceCode::new(source); let mut comments_map: CommentsMap = MultiMap::new(); comments_map.push_leading( continue_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(0), TextSize::new(17))), CommentLinePosition::OwnLine, ), ); comments_map.push_trailing( continue_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(28), TextSize::new(10))), CommentLinePosition::EndOfLine, ), ); comments_map.push_leading( break_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(39), TextSize::new(15))), CommentLinePosition::OwnLine, ), ); let comments = Comments::new(comments_map); assert_debug_snapshot!(comments.debug(source_code)); } }