use crate::comments::SourceComment; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{lines_after, lines_before}; use ruff_formatter::{format_args, write, FormatError, SourceCode}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::prelude::AstNode; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::Ranged; /// Formats the leading comments of a node. pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments where T: AstNode, { FormatLeadingComments::Node(node.as_any_node_ref()) } /// Formats the passed comments as leading comments pub(crate) const fn leading_comments(comments: &[SourceComment]) -> FormatLeadingComments { FormatLeadingComments::Comments(comments) } #[derive(Copy, Clone, Debug)] pub(crate) enum FormatLeadingComments<'a> { Node(AnyNodeRef<'a>), Comments(&'a [SourceComment]), } impl Format> for FormatLeadingComments<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let comments = f.context().comments().clone(); let leading_comments = match self { FormatLeadingComments::Node(node) => comments.leading_comments(*node), FormatLeadingComments::Comments(comments) => comments, }; for comment in leading_comments { let slice = comment.slice(); let lines_after_comment = lines_after(f.context().contents(), slice.end()); write!( f, [format_comment(comment), empty_lines(lines_after_comment)] )?; comment.mark_formatted(); } Ok(()) } } /// Formats the leading `comments` of an alternate branch and ensures that it preserves the right /// number of empty lines before. The `last_node` is the last node of the preceding body. /// /// For example, `last_node` is the last statement in the if body when formatting the leading /// comments of the `else` branch. pub(crate) fn leading_alternate_branch_comments<'a, T>( comments: &'a [SourceComment], last_node: Option, ) -> FormatLeadingAlternateBranchComments<'a> where T: Into>, { FormatLeadingAlternateBranchComments { comments, last_node: last_node.map(std::convert::Into::into), } } pub(crate) struct FormatLeadingAlternateBranchComments<'a> { comments: &'a [SourceComment], last_node: Option>, } impl Format> for FormatLeadingAlternateBranchComments<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { if let Some(first_leading) = self.comments.first() { // Leading comments only preserves the lines after the comment but not before. // Insert the necessary lines. if lines_before(f.context().contents(), first_leading.slice().start()) > 1 { write!(f, [empty_line()])?; } write!(f, [leading_comments(self.comments)])?; } else if let Some(last_preceding) = self.last_node { // The leading comments formatting ensures that it preserves the right amount of lines after // We need to take care of this ourselves, if there's no leading `else` comment. if lines_after(f.context().contents(), last_preceding.end()) > 1 { write!(f, [empty_line()])?; } } Ok(()) } } /// Formats the trailing comments of `node` pub(crate) fn trailing_node_comments(node: &T) -> FormatTrailingComments where T: AstNode, { FormatTrailingComments::Node(node.as_any_node_ref()) } /// Formats the passed comments as trailing comments pub(crate) fn trailing_comments(comments: &[SourceComment]) -> FormatTrailingComments { FormatTrailingComments::Comments(comments) } pub(crate) enum FormatTrailingComments<'a> { Node(AnyNodeRef<'a>), Comments(&'a [SourceComment]), } impl Format> for FormatTrailingComments<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let comments = f.context().comments().clone(); let trailing_comments = match self { FormatTrailingComments::Node(node) => comments.trailing_comments(*node), FormatTrailingComments::Comments(comments) => comments, }; let mut has_trailing_own_line_comment = false; for trailing in trailing_comments { let slice = trailing.slice(); has_trailing_own_line_comment |= trailing.position().is_own_line(); if has_trailing_own_line_comment { let lines_before_comment = lines_before(f.context().contents(), slice.start()); // A trailing comment at the end of a body or list // ```python // def test(): // pass // // # Some comment // ``` write!( f, [ line_suffix(&format_with(|f| { write!( f, [empty_lines(lines_before_comment), format_comment(trailing)] ) })), expand_parent() ] )?; } else { write!( f, [ line_suffix(&format_args![space(), space(), format_comment(trailing)]), expand_parent() ] )?; } trailing.mark_formatted(); } Ok(()) } } /// Formats the dangling comments of `node`. pub(crate) fn dangling_node_comments(node: &T) -> FormatDanglingComments where T: AstNode, { FormatDanglingComments { node: node.as_any_node_ref(), } } pub(crate) struct FormatDanglingComments<'a> { node: AnyNodeRef<'a>, } impl Format> for FormatDanglingComments<'_> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let comments = f.context().comments().clone(); let dangling_comments = comments.dangling_comments(self.node); let mut first = true; for comment in dangling_comments { if first && comment.position().is_end_of_line() { write!(f, [space(), space()])?; } write!( f, [ format_comment(comment), empty_lines(lines_after(f.context().contents(), comment.slice().end())) ] )?; comment.mark_formatted(); first = false; } Ok(()) } } /// Formats the content of the passed comment. /// /// * Adds a whitespace between `#` and the comment text except if the first character is a `#`, `:`, `'`, or `!` /// * Replaces non breaking whitespaces with regular whitespaces except if in front of a `types:` comment const fn format_comment(comment: &SourceComment) -> FormatComment { FormatComment { comment } } struct FormatComment<'a> { comment: &'a SourceComment, } impl Format> for FormatComment<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let slice = self.comment.slice(); let comment_text = slice.text(SourceCode::new(f.context().contents())); let trimmed = comment_text.trim_end(); let trailing_whitespace_len = comment_text.text_len() - trimmed.text_len(); let Some(content) = trimmed.strip_prefix('#') else { return Err(FormatError::SyntaxError); }; // Fast path for correctly formatted comments: // * Start with a `#` and are followed by a space // * Have no trailing whitespace. if trailing_whitespace_len == TextSize::new(0) && content.starts_with(' ') { return source_text_slice(slice.range(), ContainsNewlines::No).fmt(f); } write!(f, [source_position(slice.start()), text("#")])?; // Starts with a non breaking space let start_offset = if content.starts_with('\u{A0}') && !content.trim_start().starts_with("type:") { // Replace non-breaking space with a space (if not followed by a normal space) "#\u{A0}".text_len() } else { '#'.text_len() }; // Add a space between the `#` and the text if the source contains none. if !content.is_empty() && !content.starts_with([' ', '!', ':', '#', '\'']) { write!(f, [space()])?; } let start = slice.start() + start_offset; let end = slice.range().end() - trailing_whitespace_len; write!( f, [ source_text_slice(TextRange::new(start, end), ContainsNewlines::No), source_position(slice.end()) ] ) } } // Helper that inserts the appropriate number of empty lines before a comment, depending on the node level. // Top level: Up to two empty lines // parenthesized: A single empty line // other: Up to a single empty line const fn empty_lines(lines: u32) -> FormatEmptyLines { FormatEmptyLines { lines } } #[derive(Copy, Clone, Debug)] struct FormatEmptyLines { lines: u32, } impl Format> for FormatEmptyLines { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { match f.context().node_level() { NodeLevel::TopLevel => match self.lines { 0 | 1 => write!(f, [hard_line_break()]), 2 => write!(f, [empty_line()]), _ => write!(f, [empty_line(), empty_line()]), }, NodeLevel::CompoundStatement => match self.lines { 0 | 1 => write!(f, [hard_line_break()]), _ => write!(f, [empty_line()]), }, // Remove all whitespace in parenthesized expressions NodeLevel::Parenthesized => write!(f, [hard_line_break()]), } } }