From 4cd4b37e7473733e3a90fc2d4ad3c6219acb8eeb Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 2 Jun 2023 13:22:34 +0200 Subject: [PATCH] Format the comment content (#4786) --- .../src/comments/format.rs | 79 ++++++++++++++++--- ...tter__tests__black_test__comments2_py.snap | 11 +-- ...tter__tests__black_test__comments8_py.snap | 53 ------------- ..._test__comments_non_breaking_space_py.snap | 24 +++--- ...atter__tests__black_test__fmtskip7_py.snap | 15 ++-- 5 files changed, 88 insertions(+), 94 deletions(-) delete mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments8_py.snap diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index f38f7224b9..1eb2f44184 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -1,9 +1,11 @@ +use crate::comments::SourceComment; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{lines_after, lines_before}; -use ruff_formatter::{format_args, write}; +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}; /// Formats the leading comments of a node. pub(crate) fn leading_comments(node: &T) -> FormatLeadingComments @@ -30,10 +32,7 @@ impl Format> for FormatLeadingComments<'_> { let lines_after_comment = lines_after(f.context().contents(), slice.end()); write!( f, - [ - source_text_slice(slice.range(), ContainsNewlines::No), - empty_lines(lines_after_comment) - ] + [format_comment(comment), empty_lines(lines_after_comment)] )?; comment.mark_formatted(); @@ -64,7 +63,6 @@ impl Format> for FormatTrailingComments<'_> { for trailing in comments.trailing_comments(self.node) { let slice = trailing.slice(); - let content = source_text_slice(slice.range(), ContainsNewlines::No); let lines_before_comment = lines_before(f.context().contents(), slice.start()); has_empty_lines_before |= lines_before_comment > 0; @@ -81,7 +79,10 @@ impl Format> for FormatTrailingComments<'_> { f, [ line_suffix(&format_with(|f| { - write!(f, [empty_lines(lines_before_comment), content]) + write!( + f, + [empty_lines(lines_before_comment), format_comment(trailing)] + ) })), expand_parent() ] @@ -90,7 +91,7 @@ impl Format> for FormatTrailingComments<'_> { write!( f, [ - line_suffix(&format_args![space(), space(), content]), + line_suffix(&format_args![space(), space(), format_comment(trailing)]), expand_parent() ] )?; @@ -132,7 +133,7 @@ impl Format> for FormatDanglingComments<'_> { write!( f, [ - source_text_slice(comment.slice().range(), ContainsNewlines::No), + format_comment(comment), empty_lines(lines_after(f.context().contents(), comment.slice().end())) ] )?; @@ -146,6 +147,66 @@ impl Format> for FormatDanglingComments<'_> { } } +/// 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 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index caab87f072..808c55756f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -349,7 +349,7 @@ instruction()#comment with bad spacing # yup for element in collection.select_elements() # right -@@ -140,34 +132,26 @@ +@@ -140,28 +132,20 @@ # and round and round we go # and round and round we go @@ -386,13 +386,6 @@ instruction()#comment with bad spacing ####################### ### SECTION COMMENT ### ####################### - - --instruction() # comment with bad spacing -+instruction() #comment with bad spacing - - # END COMMENTS - # MORE END COMMENTS ``` ## Ruff Output @@ -551,7 +544,7 @@ class Test: ####################### -instruction() #comment with bad spacing +instruction() # comment with bad spacing # END COMMENTS # MORE END COMMENTS diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments8_py.snap deleted file mode 100644 index 67df15b111..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments8_py.snap +++ /dev/null @@ -1,53 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments8.py ---- -## Input - -```py -# The percent-percent comments are Spyder IDE cells. -# Both `#%%`` and `# %%` are accepted, so `black` standardises -# to the latter. - -#%% -# %% -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -2,5 +2,5 @@ - # Both `#%%`` and `# %%` are accepted, so `black` standardises - # to the latter. - --# %% -+#%% - # %% -``` - -## Ruff Output - -```py -# The percent-percent comments are Spyder IDE cells. -# Both `#%%`` and `# %%` are accepted, so `black` standardises -# to the latter. - -#%% -# %% -``` - -## Black Output - -```py -# The percent-percent comments are Spyder IDE cells. -# Both `#%%`` and `# %%` are accepted, so `black` standardises -# to the latter. - -# %% -# %% -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap index 6d7e575a83..306cbb3e56 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -42,23 +42,19 @@ def function(a:int=42): + # DEFAULT_TYPE_ATTRIBUTES, ) - --result = 1 # A simple comment + result = 1 # A simple comment -result = (1,) # Another one - --result = 1 #  type: ignore --result = 1 # This comment is talking about type: ignore --square = Square(4) #  type: Optional[Square] ++result = ( 1, ) # Another one + result = 1 #  type: ignore + result = 1 # This comment is talking about type: ignore + square = Square(4) #  type: Optional[Square] - - -def function(a: int = 42): - """This docstring is already formatted - a - b -+result = 1 # A simple comment -+result = ( 1, ) # Another one -+result = 1 # type: ignore -+result = 1 # This comment is talking about type: ignore -+square = Square(4) # type: Optional[Square] +def function(a:int=42): + """ This docstring is already formatted + a @@ -76,11 +72,11 @@ def function(a:int=42): from .config import ( ConfigTypeAttributes, Int, Path, # String, # DEFAULT_TYPE_ATTRIBUTES, ) -result = 1 # A simple comment -result = ( 1, ) # Another one -result = 1 # type: ignore -result = 1 # This comment is talking about type: ignore -square = Square(4) # type: Optional[Square] +result = 1 # A simple comment +result = ( 1, ) # Another one +result = 1 #  type: ignore +result = 1 # This comment is talking about type: ignore +square = Square(4) #  type: Optional[Square] def function(a:int=42): """ This docstring is already formatted a diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap index e9d2293265..57059c0919 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap @@ -19,22 +19,19 @@ d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasu +++ Ruff @@ -1,4 +1,4 @@ -a = "this is some code" --b = 5 # fmt:skip --c = 9 # fmt: skip --d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip +a = "this is some code" -+b = 5 #fmt:skip -+c = 9 #fmt: skip -+d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" #fmt:skip + b = 5 # fmt:skip + c = 9 # fmt: skip + d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip ``` ## Ruff Output ```py a = "this is some code" -b = 5 #fmt:skip -c = 9 #fmt: skip -d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" #fmt:skip +b = 5 # fmt:skip +c = 9 # fmt: skip +d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip ``` ## Black Output