diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py new file mode 100644 index 0000000000..0a8118079f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py @@ -0,0 +1,23 @@ +pass + +# fmt: off + # A comment that falls into the verbatim range +a + b # a trailing comment + +# in between comments + +# function comment +def test(): + pass + + # trailing comment that falls into the verbatim range + + # fmt: on + +a + b + +def test(): + pass + # fmt: off + # a trailing comment + diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/empty_file.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/empty_file.py new file mode 100644 index 0000000000..c61bb37fe0 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/empty_file.py @@ -0,0 +1,5 @@ +# fmt: off + + # this does not work because there are no statements + +# fmt: on diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.py new file mode 100644 index 0000000000..c387829010 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.py @@ -0,0 +1,19 @@ +def test(): + # fmt: off + """ This docstring does not + get formatted + """ + + # fmt: on + + but + this + does + +def test(): + # fmt: off + # just for fun + # fmt: on + # leading comment + """ This docstring gets formatted + """ # trailing comment + + and_this + gets + formatted + too diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/form_feed.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/form_feed.py new file mode 100644 index 0000000000..e5efe750eb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/form_feed.py @@ -0,0 +1,7 @@ +# fmt: off +# DB layer (form feed at the start of the next line) + +# fmt: on + +def test(): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/last_statement.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/last_statement.py new file mode 100644 index 0000000000..1fa2cee40a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/last_statement.py @@ -0,0 +1,10 @@ +def test(): + # fmt: off + + a + b + + + + # suppressed comments + +a + b # formatted diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/no_fmt_on.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/no_fmt_on.py new file mode 100644 index 0000000000..d2955856e3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/no_fmt_on.py @@ -0,0 +1,9 @@ +def test(): + # fmt: off + not formatted + + if unformatted + a: + pass + +# Get's formatted again +a + b diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/off_on_off_on.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/off_on_off_on.py new file mode 100644 index 0000000000..7bc6ca8b46 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/off_on_off_on.py @@ -0,0 +1,71 @@ +# Tricky sequences of fmt off and on + +# Formatted +a + b + +# fmt: off + # not formatted 1 +# fmt: on +a + b + # formatted + + +# fmt: off + # not formatted 1 +# fmt: on + # not formatted 2 +# fmt: off +a + b +# fmt: on + + +# fmt: off + # not formatted 1 +# fmt: on + # formatted 1 +# fmt: off + # not formatted 2 +a + b +# fmt: on + # formatted +b + c + + +# fmt: off +a + b + + # not formatted +# fmt: on + # formatted +a + b + + +# fmt: off +a + b + + # not formatted 1 +# fmt: on + # formatted +# fmt: off + # not formatted 2 +a + b + + +# fmt: off +a + b + + # not formatted 1 +# fmt: on + # formatted + +# leading +a + b +# fmt: off + + # leading unformatted +def test (): + pass + + # fmt: on + +a + b diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/simple.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/simple.py new file mode 100644 index 0000000000..dfe64b1c2e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/simple.py @@ -0,0 +1,9 @@ +# Get's formatted +a + b + +# fmt: off +a + [1, 2, 3, 4, 5 ] +# fmt: on + +# Get's formatted again +a + b diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/trailing_comments.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/trailing_comments.py new file mode 100644 index 0000000000..4ddb4ad8a6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/trailing_comments.py @@ -0,0 +1,40 @@ +a = 10 +# fmt: off + +# more format + +def test(): ... + + +# fmt: on + +b = 20 +# Sequence of trailing comments that toggle between format on and off. The sequence ends with a `fmt: on`, so that the function gets formatted. +# formatted 1 +# fmt: off + # not formatted +# fmt: on + # formatted comment +# fmt: off + # not formatted 2 +# fmt: on + + # formatted +def test2 (): + ... + +a = 10 + +# Sequence of trailing comments that toggles between format on and off. The sequence ends with a `fmt: off`, so that the function is not formatted. + # formatted 1 +# fmt: off + # not formatted +# fmt: on + # formattd +# fmt: off + + # not formatted +def test3 (): + ... + +# fmt: on diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/yapf.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/yapf.py new file mode 100644 index 0000000000..741ca7213a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/yapf.py @@ -0,0 +1,17 @@ +# Get's formatted +a + b + +# yapf: disable +a + [1, 2, 3, 4, 5 ] +# yapf: enable + +# Get's formatted again +a + b + + +# yapf: disable +a + [1, 2, 3, 4, 5 ] +# fmt: on + +# Get's formatted again +a + b diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 316f4b5ae2..fdcf4c5d81 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -281,11 +281,11 @@ impl Format> for FormatDanglingOpenParenthesisComments<'_> { /// /// * 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 { +pub(crate) const fn format_comment(comment: &SourceComment) -> FormatComment { FormatComment { comment } } -struct FormatComment<'a> { +pub(crate) struct FormatComment<'a> { comment: &'a SourceComment, } @@ -343,12 +343,12 @@ impl Format> for FormatComment<'_> { // 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 { +pub(crate) const fn empty_lines(lines: u32) -> FormatEmptyLines { FormatEmptyLines { lines } } #[derive(Copy, Clone, Debug)] -struct FormatEmptyLines { +pub(crate) struct FormatEmptyLines { lines: u32, } diff --git a/crates/ruff_python_formatter/src/comments/map.rs b/crates/ruff_python_formatter/src/comments/map.rs index 9bef831903..4b303caa05 100644 --- a/crates/ruff_python_formatter/src/comments/map.rs +++ b/crates/ruff_python_formatter/src/comments/map.rs @@ -244,6 +244,7 @@ impl MultiMap { } /// Returns `true` if `key` has any *leading*, *dangling*, or *trailing* parts. + #[allow(unused)] pub(super) fn has(&self, key: &K) -> bool { self.index.get(key).is_some() } diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 238665f9b7..ee9ba687d0 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -103,6 +103,7 @@ use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::visitor::preorder::{PreorderVisitor, TraversalSignal}; use ruff_python_index::CommentRanges; +use ruff_python_trivia::PythonWhitespace; use crate::comments::debug::{DebugComment, DebugComments}; use crate::comments::map::MultiMap; @@ -110,7 +111,7 @@ use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::visitor::CommentsVisitor; mod debug; -mod format; +pub(crate) mod format; mod map; mod node_key; mod placement; @@ -150,6 +151,11 @@ impl SourceComment { self.formatted.set(true); } + /// Marks the comment as not-formatted + pub(crate) fn mark_unformatted(&self) { + self.formatted.set(false); + } + /// If the comment has already been formatted pub(crate) fn is_formatted(&self) -> bool { self.formatted.get() @@ -163,6 +169,50 @@ impl SourceComment { pub(crate) fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { DebugComment::new(self, source_code) } + + pub(crate) fn suppression_kind(&self, source: &str) -> Option { + let text = self.slice.text(SourceCode::new(source)); + let trimmed = text.strip_prefix('#').unwrap_or(text).trim_whitespace(); + + if let Some(command) = trimmed.strip_prefix("fmt:") { + match command.trim_whitespace_start() { + "off" => Some(SuppressionKind::Off), + "on" => Some(SuppressionKind::On), + "skip" => Some(SuppressionKind::Skip), + _ => None, + } + } else if let Some(command) = trimmed.strip_prefix("yapf:") { + match command.trim_whitespace_start() { + "disable" => Some(SuppressionKind::Off), + "enable" => Some(SuppressionKind::On), + _ => None, + } + } else { + None + } + } + + /// Returns true if this comment is a `fmt: off` or `yapf: disable` own line suppression comment. + pub(crate) fn is_suppression_off_comment(&self, source: &str) -> bool { + self.line_position.is_own_line() + && matches!(self.suppression_kind(source), Some(SuppressionKind::Off)) + } + + /// Returns true if this comment is a `fmt: on` or `yapf: enable` own line suppression comment. + pub(crate) fn is_suppression_on_comment(&self, source: &str) -> bool { + self.line_position.is_own_line() + && matches!(self.suppression_kind(source), Some(SuppressionKind::On)) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub(crate) enum SuppressionKind { + /// A `fmt: off` or `yapf: disable` comment + Off, + /// A `fmt: on` or `yapf: enable` comment + On, + /// A `fmt: skip` comment + Skip, } impl Ranged for SourceComment { @@ -246,8 +296,6 @@ pub(crate) struct Comments<'a> { data: Rc>, } -#[allow(unused)] -// TODO(micha): Remove after using the new comments infrastructure in the formatter. impl<'a> Comments<'a> { fn new(comments: CommentsMap<'a>) -> Self { Self { @@ -270,16 +318,6 @@ impl<'a> Comments<'a> { Self::new(map) } - #[inline] - pub(crate) fn has_comments(&self, node: T) -> bool - where - T: Into>, - { - self.data - .comments - .has(&NodeRefEqualityKey::from_ref(node.into())) - } - /// Returns `true` if the given `node` has any [leading comments](self#leading-comments). #[inline] pub(crate) fn has_leading_comments(&self, node: T) -> bool diff --git a/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs b/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs index ab087df685..00afaee756 100644 --- a/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs +++ b/crates/ruff_python_formatter/src/expression/expr_ipy_escape_command.rs @@ -1,12 +1,11 @@ -use crate::{verbatim_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use ruff_python_ast::ExprIpyEscapeCommand; +use crate::prelude::*; +use ruff_python_ast::{ExprIpyEscapeCommand, Ranged}; #[derive(Default)] pub struct FormatExprIpyEscapeCommand; impl FormatNodeRule for FormatExprIpyEscapeCommand { fn fmt_fields(&self, item: &ExprIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [verbatim_text(item)]) + source_text_slice(item.range(), ContainsNewlines::No).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 5e3213ae95..2a704ae242 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -1,26 +1,24 @@ +use thiserror::Error; + +use ruff_formatter::format_element::tag; +use ruff_formatter::prelude::{source_position, text, Formatter, Tag}; +use ruff_formatter::{ + format, write, Buffer, Format, FormatElement, FormatError, FormatResult, PrintError, +}; +use ruff_formatter::{Formatted, Printed, SourceCode}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; +use ruff_python_ast::Mod; +use ruff_python_index::{CommentRanges, CommentRangesBuilder}; +use ruff_python_parser::lexer::{lex, LexicalError}; +use ruff_python_parser::{parse_tokens, Mode, ParseError}; +use ruff_source_file::Locator; +use ruff_text_size::TextLen; + use crate::comments::{ dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, }; use crate::context::PyFormatContext; pub use crate::options::{MagicTrailingComma, PyFormatOptions, QuoteStyle}; -use ruff_formatter::format_element::tag; -use ruff_formatter::prelude::{ - dynamic_text, source_position, source_text_slice, text, ContainsNewlines, Formatter, Tag, -}; -use ruff_formatter::{ - format, normalize_newlines, write, Buffer, Format, FormatElement, FormatError, FormatResult, - PrintError, -}; -use ruff_formatter::{Formatted, Printed, SourceCode}; -use ruff_python_ast::node::{AnyNodeRef, AstNode}; -use ruff_python_ast::{Mod, Ranged}; -use ruff_python_index::{CommentRanges, CommentRangesBuilder}; -use ruff_python_parser::lexer::{lex, LexicalError}; -use ruff_python_parser::{parse_tokens, Mode, ParseError}; -use ruff_source_file::Locator; -use ruff_text_size::{TextLen, TextRange}; -use std::borrow::Cow; -use thiserror::Error; pub(crate) mod builders; pub mod cli; @@ -35,6 +33,7 @@ pub(crate) mod pattern; mod prelude; pub(crate) mod statement; pub(crate) mod type_param; +mod verbatim; include!("../../ruff_formatter/shared_traits.rs"); @@ -47,10 +46,10 @@ where N: AstNode, { fn fmt(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - self.fmt_leading_comments(node, f)?; + leading_node_comments(node).fmt(f)?; self.fmt_node(node, f)?; self.fmt_dangling_comments(node, f)?; - self.fmt_trailing_comments(node, f) + trailing_node_comments(node).fmt(f) } /// Formats the node without comments. Ignores any suppression comments. @@ -63,14 +62,6 @@ where /// Formats the node's fields. fn fmt_fields(&self, item: &N, f: &mut PyFormatter) -> FormatResult<()>; - /// Formats the [leading comments](comments#leading-comments) of the node. - /// - /// You may want to override this method if you want to manually handle the formatting of comments - /// inside of the `fmt_fields` method or customize the formatting of the leading comments. - fn fmt_leading_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - leading_node_comments(node).fmt(f) - } - /// Formats the [dangling comments](comments#dangling-comments) of the node. /// /// You should override this method if the node handled by this rule can have dangling comments because the @@ -81,14 +72,6 @@ where fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { dangling_node_comments(node).fmt(f) } - - /// Formats the [trailing comments](comments#trailing-comments) of the node. - /// - /// You may want to override this method if you want to manually handle the formatting of comments - /// inside of the `fmt_fields` method or customize the formatting of the trailing comments. - fn fmt_trailing_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - trailing_node_comments(node).fmt(f) - } } #[derive(Error, Debug)] @@ -234,53 +217,18 @@ impl Format> for NotYetImplementedCustomText<'_> { } } -pub(crate) struct VerbatimText(TextRange); - -#[allow(unused)] -pub(crate) fn verbatim_text(item: &T) -> VerbatimText -where - T: Ranged, -{ - VerbatimText(item.range()) -} - -impl Format> for VerbatimText { - fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - f.write_element(FormatElement::Tag(Tag::StartVerbatim( - tag::VerbatimKind::Verbatim { - length: self.0.len(), - }, - )))?; - - match normalize_newlines(f.context().locator().slice(self.0), ['\r']) { - Cow::Borrowed(_) => { - write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; - } - Cow::Owned(cleaned) => { - write!( - f, - [ - dynamic_text(&cleaned, Some(self.0.start())), - source_position(self.0.end()) - ] - )?; - } - } - - f.write_element(FormatElement::Tag(Tag::EndVerbatim))?; - Ok(()) - } -} - #[cfg(test)] mod tests { - use crate::{format_module, format_node, PyFormatOptions}; + use std::path::Path; + use anyhow::Result; use insta::assert_snapshot; + use ruff_python_index::CommentRangesBuilder; use ruff_python_parser::lexer::lex; use ruff_python_parser::{parse_tokens, Mode}; - use std::path::Path; + + use crate::{format_module, format_node, PyFormatOptions}; /// Very basic test intentionally kept very similar to the CLI #[test] diff --git a/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs b/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs index 008c22b793..539ab3a8d8 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ipy_escape_command.rs @@ -1,12 +1,11 @@ -use crate::{verbatim_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use ruff_python_ast::StmtIpyEscapeCommand; +use crate::prelude::*; +use ruff_python_ast::{Ranged, StmtIpyEscapeCommand}; #[derive(Default)] pub struct FormatStmtIpyEscapeCommand; impl FormatNodeRule for FormatStmtIpyEscapeCommand { fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [verbatim_text(item)]) + source_text_slice(item.range(), ContainsNewlines::No).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 75143cf84d..a18760117c 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -1,14 +1,19 @@ +use crate::comments::{leading_comments, trailing_comments}; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; use ruff_python_ast::helpers::is_compound_statement; -use ruff_python_ast::{self as ast, Ranged, Stmt, Suite}; -use ruff_python_ast::{Constant, ExprConstant}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::{self as ast, Expr, ExprConstant, Ranged, Stmt, Suite}; use ruff_python_trivia::{lines_after_ignoring_trivia, lines_before}; +use ruff_text_size::TextRange; -use crate::comments::{leading_comments, trailing_comments}; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::expr_constant::ExprConstantLayout; use crate::expression::string::StringLayout; use crate::prelude::*; +use crate::verbatim::{ + write_suppressed_statements_starting_with_leading_comment, + write_suppressed_statements_starting_with_trailing_comment, +}; /// Level at which the [`Suite`] appears in the source code. #[derive(Copy, Clone, Debug)] @@ -51,196 +56,231 @@ impl FormatRule> for FormatSuite { let comments = f.context().comments().clone(); let source = f.context().source(); - let mut iter = statements.iter(); - let Some(first) = iter.next() else { - return Ok(()); - }; - let mut f = WithNodeLevel::new(node_level, f); + write!( + f, + [format_with(|f| { + let mut iter = statements.iter(); + let Some(first) = iter.next() else { + return Ok(()); + }; - // Format the first statement in the body, which often has special formatting rules. - let mut last = first; - match self.kind { - SuiteKind::Other => { - if is_class_or_function_definition(first) && !comments.has_leading_comments(first) { - // Add an empty line for any nested functions or classes defined within - // non-function or class compound statements, e.g., this is stable formatting: - // ```python - // if True: - // - // def test(): - // ... - // ``` - write!(f, [empty_line()])?; - } - write!(f, [first.format()])?; - } - SuiteKind::Function => { - if let Some(constant) = get_docstring(first) { - write!( - f, - [ - // We format the expression, but the statement carries the comments - leading_comments(comments.leading_comments(first)), - constant - .format() - .with_options(ExprConstantLayout::String(StringLayout::DocString)), - trailing_comments(comments.trailing_comments(first)), - ] - )?; + // Format the first statement in the body, which often has special formatting rules. + let first = match self.kind { + SuiteKind::Other => { + if is_class_or_function_definition(first) + && !comments.has_leading_comments(first) + { + // Add an empty line for any nested functions or classes defined within + // non-function or class compound statements, e.g., this is stable formatting: + // ```python + // if True: + // + // def test(): + // ... + // ``` + empty_line().fmt(f)?; + } + + SuiteChildStatement::Other(first) + } + + SuiteKind::Function => { + if let Some(docstring) = DocstringStmt::try_from_statement(first) { + SuiteChildStatement::Docstring(docstring) + } else { + SuiteChildStatement::Other(first) + } + } + + SuiteKind::Class => { + if let Some(docstring) = DocstringStmt::try_from_statement(first) { + if !comments.has_leading_comments(first) + && lines_before(first.start(), source) > 1 + { + // Allow up to one empty line before a class docstring, e.g., this is + // stable formatting: + // ```python + // class Test: + // + // """Docstring""" + // ``` + empty_line().fmt(f)?; + } + + SuiteChildStatement::Docstring(docstring) + } else { + SuiteChildStatement::Other(first) + } + } + SuiteKind::TopLevel => SuiteChildStatement::Other(first), + }; + + let (mut preceding, mut after_class_docstring) = if comments + .leading_comments(first) + .iter() + .any(|comment| comment.is_suppression_off_comment(source)) + { + ( + write_suppressed_statements_starting_with_leading_comment( + first, &mut iter, f, + )?, + false, + ) + } else if comments + .trailing_comments(first) + .iter() + .any(|comment| comment.is_suppression_off_comment(source)) + { + ( + write_suppressed_statements_starting_with_trailing_comment( + first, &mut iter, f, + )?, + false, + ) } else { - write!(f, [first.format()])?; - } - } - SuiteKind::Class => { - if let Some(constant) = get_docstring(first) { - if !comments.has_leading_comments(first) - && lines_before(first.start(), source) > 1 + first.fmt(f)?; + ( + first.statement(), + matches!(first, SuiteChildStatement::Docstring(_)) + && matches!(self.kind, SuiteKind::Class), + ) + }; + + while let Some(following) = iter.next() { + if is_class_or_function_definition(preceding) + || is_class_or_function_definition(following) { - // Allow up to one empty line before a class docstring + match self.kind { + SuiteKind::TopLevel => { + write!(f, [empty_line(), empty_line()])?; + } + SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => { + empty_line().fmt(f)?; + } + } + } else if is_import_definition(preceding) && !is_import_definition(following) { + empty_line().fmt(f)?; + } else if is_compound_statement(preceding) { + // Handles the case where a body has trailing comments. The issue is that RustPython does not include + // the comments in the range of the suite. This means, the body ends right after the last statement in the body. // ```python + // def test(): + // ... + // # The body of `test` ends right after `...` and before this comment + // + // # leading comment + // + // + // a = 10 + // ``` + // Using `lines_after` for the node doesn't work because it would count the lines after the `...` + // which is 0 instead of 1, the number of lines between the trailing comment and + // the leading comment. This is why the suite handling counts the lines before the + // start of the next statement or before the first leading comments for compound statements. + let start = if let Some(first_leading) = + comments.leading_comments(following).first() + { + first_leading.slice().start() + } else { + following.start() + }; + + match lines_before(start, source) { + 0 | 1 => hard_line_break().fmt(f)?, + 2 => empty_line().fmt(f)?, + 3.. => match self.kind { + SuiteKind::TopLevel => write!(f, [empty_line(), empty_line()])?, + SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => { + empty_line().fmt(f)?; + } + }, + } + } else if after_class_docstring { + // Enforce an empty line after a class docstring, e.g., these are both stable + // formatting: + // ```python + // class Test: + // """Docstring""" + // + // ... + // + // // class Test: // // """Docstring""" + // + // ... // ``` - write!(f, [empty_line()])?; - } - write!( - f, - [ - // We format the expression, but the statement carries the comments - leading_comments(comments.leading_comments(first)), - constant - .format() - .with_options(ExprConstantLayout::String(StringLayout::DocString)), - trailing_comments(comments.trailing_comments(first)), - ] - )?; - - // Enforce an empty line after a class docstring - // ```python - // class Test: - // """Docstring""" - // - // ... - // - // - // class Test: - // - // """Docstring""" - // - // ... - // ``` - // Unlike black, we add the newline also after single quoted docstrings - if let Some(second) = iter.next() { - // Format the subsequent statement immediately. This rule takes precedence - // over the rules in the loop below (and most of them won't apply anyway, - // e.g., we know the first statement isn't an import). - write!(f, [empty_line(), second.format()])?; - last = second; - } - } else { - // No docstring, use normal formatting - write!(f, [first.format()])?; - } - } - SuiteKind::TopLevel => { - write!(f, [first.format()])?; - } - } - - for statement in iter { - if is_class_or_function_definition(last) || is_class_or_function_definition(statement) { - match self.kind { - SuiteKind::TopLevel => { - write!(f, [empty_line(), empty_line(), statement.format()])?; - } - SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => { - write!(f, [empty_line(), statement.format()])?; - } - } - } else if is_import_definition(last) && !is_import_definition(statement) { - write!(f, [empty_line(), statement.format()])?; - } else if is_compound_statement(last) { - // Handles the case where a body has trailing comments. The issue is that RustPython does not include - // the comments in the range of the suite. This means, the body ends right after the last statement in the body. - // ```python - // def test(): - // ... - // # The body of `test` ends right after `...` and before this comment - // - // # leading comment - // - // - // a = 10 - // ``` - // Using `lines_after` for the node doesn't work because it would count the lines after the `...` - // which is 0 instead of 1, the number of lines between the trailing comment and - // the leading comment. This is why the suite handling counts the lines before the - // start of the next statement or before the first leading comments for compound statements. - let start = - if let Some(first_leading) = comments.leading_comments(statement).first() { - first_leading.slice().start() + empty_line().fmt(f)?; + after_class_docstring = false; } else { - statement.start() - }; + // Insert the appropriate number of empty lines based on the node level, e.g.: + // * [`NodeLevel::Module`]: Up to two empty lines + // * [`NodeLevel::CompoundStatement`]: Up to one empty line + // * [`NodeLevel::Expression`]: No empty lines - match lines_before(start, source) { - 0 | 1 => write!(f, [hard_line_break()])?, - 2 => write!(f, [empty_line()])?, - 3.. => match self.kind { - SuiteKind::TopLevel => write!(f, [empty_line(), empty_line()])?, - SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => { - write!(f, [empty_line()])?; + let count_lines = |offset| { + // It's necessary to skip any trailing line comment because RustPython doesn't include trailing comments + // in the node's range + // ```python + // a # The range of `a` ends right before this comment + // + // b + // ``` + // + // Simply using `lines_after` doesn't work if a statement has a trailing comment because + // it then counts the lines between the statement and the trailing comment, which is + // always 0. This is why it skips any trailing trivia (trivia that's on the same line) + // and counts the lines after. + lines_after_ignoring_trivia(offset, source) + }; + + match node_level { + NodeLevel::TopLevel => match count_lines(preceding.end()) { + 0 | 1 => hard_line_break().fmt(f)?, + 2 => empty_line().fmt(f)?, + _ => write!(f, [empty_line(), empty_line()])?, + }, + NodeLevel::CompoundStatement => match count_lines(preceding.end()) { + 0 | 1 => hard_line_break().fmt(f)?, + _ => empty_line().fmt(f)?, + }, + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => { + hard_line_break().fmt(f)?; + } } - }, - } + } - write!(f, [statement.format()])?; - } else { - // Insert the appropriate number of empty lines based on the node level, e.g.: - // * [`NodeLevel::Module`]: Up to two empty lines - // * [`NodeLevel::CompoundStatement`]: Up to one empty line - // * [`NodeLevel::Expression`]: No empty lines - - let count_lines = |offset| { - // It's necessary to skip any trailing line comment because RustPython doesn't include trailing comments - // in the node's range - // ```python - // a # The range of `a` ends right before this comment - // - // b - // ``` - // - // Simply using `lines_after` doesn't work if a statement has a trailing comment because - // it then counts the lines between the statement and the trailing comment, which is - // always 0. This is why it skips any trailing trivia (trivia that's on the same line) - // and counts the lines after. - lines_after_ignoring_trivia(offset, source) - }; - - match node_level { - NodeLevel::TopLevel => match count_lines(last.end()) { - 0 | 1 => write!(f, [hard_line_break()])?, - 2 => write!(f, [empty_line()])?, - _ => write!(f, [empty_line(), empty_line()])?, - }, - NodeLevel::CompoundStatement => match count_lines(last.end()) { - 0 | 1 => write!(f, [hard_line_break()])?, - _ => write!(f, [empty_line()])?, - }, - NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => { - write!(f, [hard_line_break()])?; + if comments + .leading_comments(following) + .iter() + .any(|comment| comment.is_suppression_off_comment(source)) + { + preceding = write_suppressed_statements_starting_with_leading_comment( + SuiteChildStatement::Other(following), + &mut iter, + f, + )?; + } else if comments + .trailing_comments(following) + .iter() + .any(|comment| comment.is_suppression_off_comment(source)) + { + preceding = write_suppressed_statements_starting_with_trailing_comment( + SuiteChildStatement::Other(following), + &mut iter, + f, + )?; + } else { + following.format().fmt(f)?; + preceding = following; } } - write!(f, [statement.format()])?; - } - - last = statement; - } - - Ok(()) + Ok(()) + })] + ) } } @@ -254,23 +294,6 @@ const fn is_import_definition(stmt: &Stmt) -> bool { matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) } -/// Checks if the statement is a simple string that can be formatted as a docstring -fn get_docstring(stmt: &Stmt) -> Option<&ExprConstant> { - let stmt_expr = stmt.as_expr_stmt()?; - let expr_constant = stmt_expr.value.as_constant_expr()?; - if matches!( - expr_constant.value, - Constant::Str(ast::StringConstant { - implicit_concatenated: false, - .. - }) - ) { - Some(expr_constant) - } else { - None - } -} - impl FormatRuleWithOptions> for FormatSuite { type Options = SuiteKind; @@ -296,6 +319,93 @@ impl<'ast> IntoFormat> for Suite { } } +/// A statement representing a docstring. +#[derive(Copy, Clone)] +pub(crate) struct DocstringStmt<'a>(&'a Stmt); + +impl<'a> DocstringStmt<'a> { + /// Checks if the statement is a simple string that can be formatted as a docstring + fn try_from_statement(stmt: &'a Stmt) -> Option> { + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return None; + }; + + if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() { + if !value.is_implicit_concatenated() { + return Some(DocstringStmt(stmt)); + } + } + + None + } +} + +impl Format> for DocstringStmt<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + // SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ConstantExpr`. + let constant = self + .0 + .as_expr_stmt() + .unwrap() + .value + .as_constant_expr() + .unwrap(); + let comments = f.context().comments().clone(); + + // We format the expression, but the statement carries the comments + write!( + f, + [ + leading_comments(comments.leading_comments(self.0)), + constant + .format() + .with_options(ExprConstantLayout::String(StringLayout::DocString)), + trailing_comments(comments.trailing_comments(self.0)), + ] + ) + } +} + +/// A Child of a suite. +#[derive(Copy, Clone)] +pub(crate) enum SuiteChildStatement<'a> { + /// A docstring documenting a class or function definition. + Docstring(DocstringStmt<'a>), + + /// Any other statement. + Other(&'a Stmt), +} + +impl<'a> SuiteChildStatement<'a> { + pub(crate) const fn statement(self) -> &'a Stmt { + match self { + SuiteChildStatement::Docstring(docstring) => docstring.0, + SuiteChildStatement::Other(statement) => statement, + } + } +} + +impl Ranged for SuiteChildStatement<'_> { + fn range(&self) -> TextRange { + self.statement().range() + } +} + +impl<'a> From> for AnyNodeRef<'a> { + fn from(value: SuiteChildStatement<'a>) -> Self { + value.statement().into() + } +} + +impl Format> for SuiteChildStatement<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + match self { + SuiteChildStatement::Docstring(docstring) => docstring.fmt(f), + SuiteChildStatement::Other(statement) => statement.format().fmt(f), + } + } +} + #[cfg(test)] mod tests { use ruff_formatter::format; diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs new file mode 100644 index 0000000000..c96a35c59c --- /dev/null +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -0,0 +1,610 @@ +use std::borrow::Cow; +use std::iter::FusedIterator; + +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::{Ranged, Stmt}; +use ruff_python_trivia::lines_before; +use ruff_text_size::TextRange; + +use crate::comments::format::{empty_lines, format_comment}; +use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::prelude::*; +use crate::statement::suite::SuiteChildStatement; + +/// Disables formatting for all statements between the `first_suppressed` that has a leading `fmt: off` comment +/// and the first trailing or leading `fmt: on` comment. The statements are formatted as they appear in the source code. +/// +/// Returns the last formatted statement. +/// +/// ## Panics +/// If `first_suppressed` has no leading suppression comment. +#[cold] +pub(crate) fn write_suppressed_statements_starting_with_leading_comment<'a>( + // The first suppressed statement + first_suppressed: SuiteChildStatement<'a>, + statements: &mut std::slice::Iter<'a, Stmt>, + f: &mut PyFormatter, +) -> FormatResult<&'a Stmt> { + let comments = f.context().comments().clone(); + let source = f.context().source(); + + let mut leading_comment_ranges = + CommentRangeIter::outside_suppression(comments.leading_comments(first_suppressed), source); + + let before_format_off = leading_comment_ranges + .next() + .expect("Suppressed node to have leading comments"); + + let (formatted_comments, format_off_comment) = before_format_off.unwrap_suppression_starts(); + + // Format the leading comments before the fmt off + // ```python + // # leading comment that gets formatted + // # fmt: off + // statement + // ``` + write!( + f, + [ + leading_comments(formatted_comments), + // Format the off comment without adding any trailing new lines + format_comment(format_off_comment) + ] + )?; + + format_off_comment.mark_formatted(); + + // Now inside a suppressed range + write_suppressed_statements( + format_off_comment, + first_suppressed, + leading_comment_ranges.as_slice(), + statements, + f, + ) +} + +/// Disables formatting for all statements between the `last_formatted` and the first trailing or leading `fmt: on` comment. +/// The statements are formatted as they appear in the source code. +/// +/// Returns the last formatted statement. +/// +/// ## Panics +/// If `last_formatted` has no trailing suppression comment. +#[cold] +pub(crate) fn write_suppressed_statements_starting_with_trailing_comment<'a>( + last_formatted: SuiteChildStatement<'a>, + statements: &mut std::slice::Iter<'a, Stmt>, + f: &mut PyFormatter, +) -> FormatResult<&'a Stmt> { + let comments = f.context().comments().clone(); + let source = f.context().source(); + + let trailing_node_comments = comments.trailing_comments(last_formatted); + let mut trailing_comment_ranges = + CommentRangeIter::outside_suppression(trailing_node_comments, source); + + // Formatted comments gets formatted as part of the statement. + let (_, mut format_off_comment) = trailing_comment_ranges + .next() + .expect("Suppressed statement to have trailing comments") + .unwrap_suppression_starts(); + + let maybe_suppressed = trailing_comment_ranges.as_slice(); + + // Mark them as formatted so that calling the node's formatting doesn't format the comments. + for comment in maybe_suppressed { + comment.mark_formatted(); + } + format_off_comment.mark_formatted(); + + // Format the leading comments, the node, and the trailing comments up to the `fmt: off` comment. + last_formatted.fmt(f)?; + + format_off_comment.mark_unformatted(); + TrailingFormatOffComment(format_off_comment).fmt(f)?; + + for range in trailing_comment_ranges { + match range { + // A `fmt: off`..`fmt: on` sequence. Disable formatting for the in-between comments. + // ```python + // def test(): + // pass + // # fmt: off + // # haha + // # fmt: on + // # fmt: off (maybe) + // ``` + SuppressionComments::SuppressionEnds { + suppressed_comments: _, + format_on_comment, + formatted_comments, + format_off_comment: new_format_off_comment, + } => { + format_on_comment.mark_unformatted(); + + for comment in formatted_comments { + comment.mark_unformatted(); + } + + write!( + f, + [ + verbatim_text(TextRange::new( + format_off_comment.end(), + format_on_comment.start(), + )), + trailing_comments(std::slice::from_ref(format_on_comment)), + trailing_comments(formatted_comments), + ] + )?; + + // `fmt: off`..`fmt:on`..`fmt:off` sequence + // ```python + // def test(): + // pass + // # fmt: off + // # haha + // # fmt: on + // # fmt: off + // ``` + if let Some(new_format_off_comment) = new_format_off_comment { + new_format_off_comment.mark_unformatted(); + + TrailingFormatOffComment(new_format_off_comment).fmt(f)?; + + format_off_comment = new_format_off_comment; + } else { + // `fmt: off`..`fmt:on` sequence. The suppression ends here. Start formatting the nodes again. + return Ok(last_formatted.statement()); + } + } + + // All comments in this range are suppressed + SuppressionComments::Suppressed { comments: _ } => {} + // SAFETY: Unreachable because the function returns as soon as we reach the end of the suppressed range + SuppressionComments::SuppressionStarts { .. } + | SuppressionComments::Formatted { .. } => unreachable!(), + } + } + + // The statement with the suppression comment isn't the last statement in the suite. + // Format the statements up to the first `fmt: on` comment (or end of the suite) as verbatim/suppressed. + // ```python + // a + b + // # fmt: off + // + // def a(): + // pass + // ``` + if let Some(first_suppressed) = statements.next() { + write_suppressed_statements( + format_off_comment, + SuiteChildStatement::Other(first_suppressed), + comments.leading_comments(first_suppressed), + statements, + f, + ) + } + // The suppression comment is the block's last node. Format any trailing comments as suppressed + // ```python + // def test(): + // pass + // # fmt: off + // # a trailing comment + // ``` + else if let Some(last_comment) = trailing_node_comments.last() { + verbatim_text(TextRange::new(format_off_comment.end(), last_comment.end())).fmt(f)?; + Ok(last_formatted.statement()) + } + // The suppression comment is the very last code in the block. There's nothing more to format. + // ```python + // def test(): + // pass + // # fmt: off + // ``` + else { + Ok(last_formatted.statement()) + } +} + +/// Formats the statements from `first_suppressed` until the suppression ends (by a `fmt: on` comment) +/// as they appear in the source code. +fn write_suppressed_statements<'a>( + // The `fmt: off` comment that starts the suppressed range. Can be a leading comment of `first_suppressed` or + // a trailing comment of the previous node. + format_off_comment: &SourceComment, + // The first suppressed statement + first_suppressed: SuiteChildStatement<'a>, + // The leading comments of `first_suppressed` that come after the `format_off_comment` + first_suppressed_leading_comments: &[SourceComment], + // The remaining statements + statements: &mut std::slice::Iter<'a, Stmt>, + f: &mut PyFormatter, +) -> FormatResult<&'a Stmt> { + let comments = f.context().comments().clone(); + let source = f.context().source(); + + // TODO(micha) Fixup indent + let mut statement = first_suppressed; + let mut leading_node_comments = first_suppressed_leading_comments; + let mut format_off_comment = format_off_comment; + + loop { + for range in CommentRangeIter::in_suppression(leading_node_comments, source) { + match range { + // All leading comments are suppressed + // ```python + // # suppressed comment + // statement + // ``` + SuppressionComments::Suppressed { comments } => { + for comment in comments { + comment.mark_formatted(); + } + } + + // Node has a leading `fmt: on` comment and maybe another `fmt: off` comment + // ```python + // # suppressed comment (optional) + // # fmt: on + // # formatted comment (optional) + // # fmt: off (optional) + // statement + // ``` + SuppressionComments::SuppressionEnds { + suppressed_comments, + format_on_comment, + formatted_comments, + format_off_comment: new_format_off_comment, + } => { + for comment in suppressed_comments { + comment.mark_formatted(); + } + + write!( + f, + [ + verbatim_text(TextRange::new( + format_off_comment.end(), + format_on_comment.start(), + )), + leading_comments(std::slice::from_ref(format_on_comment)), + leading_comments(formatted_comments), + ] + )?; + + if let Some(new_format_off_comment) = new_format_off_comment { + format_off_comment = new_format_off_comment; + format_comment(format_off_comment).fmt(f)?; + format_off_comment.mark_formatted(); + } else { + // Suppression ends here. Test if the node has a trailing suppression comment and, if so, + // recurse and format the trailing comments and the following statements as suppressed. + return if comments + .trailing_comments(statement) + .iter() + .any(|comment| comment.is_suppression_off_comment(source)) + { + // Node has a trailing suppression comment, hell yeah, start all over again. + write_suppressed_statements_starting_with_trailing_comment( + statement, statements, f, + ) + } else { + // Formats the trailing comments + statement.fmt(f)?; + Ok(statement.statement()) + }; + } + } + + // Unreachable because the function exits as soon as it reaches the end of the suppression + // and it already starts in a suppressed range. + SuppressionComments::SuppressionStarts { .. } => unreachable!(), + SuppressionComments::Formatted { .. } => unreachable!(), + } + } + + comments.mark_verbatim_node_comments_formatted(AnyNodeRef::from(statement)); + + for range in CommentRangeIter::in_suppression(comments.trailing_comments(statement), source) + { + match range { + // All leading comments are suppressed + // ```python + // statement + // # suppressed + // ``` + SuppressionComments::Suppressed { comments } => { + for comment in comments { + comment.mark_formatted(); + } + } + + // Node has a trailing `fmt: on` comment and maybe another `fmt: off` comment + // ```python + // statement + // # suppressed comment (optional) + // # fmt: on + // # formatted comment (optional) + // # fmt: off (optional) + // ``` + SuppressionComments::SuppressionEnds { + suppressed_comments, + format_on_comment, + formatted_comments, + format_off_comment: new_format_off_comment, + } => { + for comment in suppressed_comments { + comment.mark_formatted(); + } + + write!( + f, + [ + verbatim_text(TextRange::new( + format_off_comment.end(), + format_on_comment.start() + )), + format_comment(format_on_comment), + hard_line_break(), + trailing_comments(formatted_comments), + ] + )?; + + format_on_comment.mark_formatted(); + + if let Some(new_format_off_comment) = new_format_off_comment { + format_off_comment = new_format_off_comment; + format_comment(format_off_comment).fmt(f)?; + format_off_comment.mark_formatted(); + } else { + return Ok(statement.statement()); + } + } + + // Unreachable because the function exits as soon as it reaches the end of the suppression + // and it already starts in a suppressed range. + SuppressionComments::SuppressionStarts { .. } => unreachable!(), + SuppressionComments::Formatted { .. } => unreachable!(), + } + } + + if let Some(next_statement) = statements.next() { + statement = SuiteChildStatement::Other(next_statement); + leading_node_comments = comments.leading_comments(next_statement); + } else { + let end = comments + .trailing_comments(statement) + .last() + .map_or(statement.end(), Ranged::end); + + verbatim_text(TextRange::new(format_off_comment.end(), end)).fmt(f)?; + + return Ok(statement.statement()); + } + } +} + +#[derive(Copy, Clone, Debug)] +enum InSuppression { + No, + Yes, +} + +#[derive(Debug)] +enum SuppressionComments<'a> { + /// The first `fmt: off` comment. + SuppressionStarts { + /// The comments appearing before the `fmt: off` comment + formatted_comments: &'a [SourceComment], + format_off_comment: &'a SourceComment, + }, + + /// A `fmt: on` comment inside a suppressed range. + SuppressionEnds { + /// The comments before the `fmt: on` comment that should *not* be formatted. + suppressed_comments: &'a [SourceComment], + format_on_comment: &'a SourceComment, + + /// The comments after the `fmt: on` comment (if any), that should be formatted. + formatted_comments: &'a [SourceComment], + + /// Any following `fmt: off` comment if any. + /// * `None`: The suppression ends here (for good) + /// * `Some`: A `fmt: off`..`fmt: on` .. `fmt: off` sequence. The suppression continues after + /// the `fmt: off` comment. + format_off_comment: Option<&'a SourceComment>, + }, + + /// Comments that all fall into the suppressed range. + Suppressed { comments: &'a [SourceComment] }, + + /// Comments that all fall into the formatted range. + Formatted { + #[allow(unused)] + comments: &'a [SourceComment], + }, +} + +impl<'a> SuppressionComments<'a> { + fn unwrap_suppression_starts(&self) -> (&'a [SourceComment], &'a SourceComment) { + if let SuppressionComments::SuppressionStarts { + formatted_comments, + format_off_comment, + } = self + { + (formatted_comments, *format_off_comment) + } else { + panic!("Expected SuppressionStarts") + } + } +} + +struct CommentRangeIter<'a> { + comments: &'a [SourceComment], + source: &'a str, + in_suppression: InSuppression, +} + +impl<'a> CommentRangeIter<'a> { + fn in_suppression(comments: &'a [SourceComment], source: &'a str) -> Self { + Self { + comments, + in_suppression: InSuppression::Yes, + source, + } + } + + fn outside_suppression(comments: &'a [SourceComment], source: &'a str) -> Self { + Self { + comments, + in_suppression: InSuppression::No, + source, + } + } + + /// Returns a slice containing the remaining comments. + fn as_slice(&self) -> &'a [SourceComment] { + self.comments + } +} + +impl<'a> Iterator for CommentRangeIter<'a> { + type Item = SuppressionComments<'a>; + + fn next(&mut self) -> Option { + if self.comments.is_empty() { + return None; + } + + Some(match self.in_suppression { + // Inside of a suppressed range + InSuppression::Yes => { + if let Some(format_on_position) = self + .comments + .iter() + .position(|comment| comment.is_suppression_on_comment(self.source)) + { + let (suppressed_comments, formatted) = + self.comments.split_at(format_on_position); + let (format_on_comment, rest) = formatted.split_first().unwrap(); + + let (formatted_comments, format_off_comment) = + if let Some(format_off_position) = rest + .iter() + .position(|comment| comment.is_suppression_off_comment(self.source)) + { + let (formatted_comments, suppressed_comments) = + rest.split_at(format_off_position); + let (format_off_comment, rest) = + suppressed_comments.split_first().unwrap(); + + self.comments = rest; + + (formatted_comments, Some(format_off_comment)) + } else { + self.in_suppression = InSuppression::No; + + self.comments = &[]; + (rest, None) + }; + + SuppressionComments::SuppressionEnds { + suppressed_comments, + format_on_comment, + formatted_comments, + format_off_comment, + } + } else { + SuppressionComments::Suppressed { + comments: std::mem::take(&mut self.comments), + } + } + } + + // Outside of a suppression + InSuppression::No => { + if let Some(format_off_position) = self + .comments + .iter() + .position(|comment| comment.is_suppression_off_comment(self.source)) + { + self.in_suppression = InSuppression::Yes; + + let (formatted_comments, suppressed) = + self.comments.split_at(format_off_position); + let format_off_comment = &suppressed[0]; + + self.comments = &suppressed[1..]; + + SuppressionComments::SuppressionStarts { + formatted_comments, + format_off_comment, + } + } else { + SuppressionComments::Formatted { + comments: std::mem::take(&mut self.comments), + } + } + } + }) + } +} + +impl FusedIterator for CommentRangeIter<'_> {} + +struct TrailingFormatOffComment<'a>(&'a SourceComment); + +impl Format> for TrailingFormatOffComment<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + debug_assert!(self.0.is_unformatted()); + let lines_before_comment = lines_before(self.0.start(), f.context().source()); + + write!( + f, + [empty_lines(lines_before_comment), format_comment(self.0)] + )?; + + self.0.mark_formatted(); + + Ok(()) + } +} + +struct VerbatimText(TextRange); + +fn verbatim_text(item: T) -> VerbatimText +where + T: Ranged, +{ + VerbatimText(item.range()) +} + +impl Format> for VerbatimText { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + f.write_element(FormatElement::Tag(Tag::StartVerbatim( + tag::VerbatimKind::Verbatim { + length: self.0.len(), + }, + )))?; + + match normalize_newlines(f.context().locator().slice(self.0), ['\r']) { + Cow::Borrowed(_) => { + write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; + } + Cow::Owned(cleaned) => { + write!( + f, + [ + dynamic_text(&cleaned, Some(self.0.start())), + source_position(self.0.end()) + ] + )?; + } + } + + f.write_element(FormatElement::Tag(Tag::EndVerbatim)) + } +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index 16f03bc425..614cee9816 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -198,77 +198,7 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -6,8 +6,8 @@ - - from library import some_connection, some_decorator - # fmt: off --from third_party import (X, -- Y, Z) -+from third_party import X, Y, Z -+ - # fmt: on - f"trigger 3.6 mode" - # Comment 1 -@@ -17,26 +17,40 @@ - - # fmt: off - def func_no_args(): -- a; b; c -- if True: raise RuntimeError -- if False: ... -- for i in range(10): -- print(i) -- continue -- exec('new-style exec', {}, {}) -- return None -+ a -+ b -+ c -+ if True: -+ raise RuntimeError -+ if False: -+ ... -+ for i in range(10): -+ print(i) -+ continue -+ exec("new-style exec", {}, {}) -+ return None -+ -+ - async def coroutine(arg, exec=False): -- 'Single-line docstring. Multiline is harder to reformat.' -- async with some_connection() as conn: -- await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) -- await asyncio.sleep(1) -+ "Single-line docstring. Multiline is harder to reformat." -+ async with some_connection() as conn: -+ await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) -+ await asyncio.sleep(1) -+ -+ - @asyncio.coroutine --@some_decorator( --with_args=True, --many_args=[1,2,3] --) --def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: -- return text[number:-1] -+@some_decorator(with_args=True, many_args=[1, 2, 3]) -+def function_signature_stress_test( -+ number: int, -+ no_annotation=None, -+ text: str = "default", -+ *, -+ debug: bool = False, -+ **kwargs, -+) -> str: -+ return text[number:-1] -+ -+ - # fmt: on - def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) -@@ -63,15 +77,15 @@ +@@ -63,15 +63,15 @@ something = { # fmt: off @@ -287,88 +217,28 @@ d={'a':1, # fmt: on goes + here, andhere, -@@ -80,38 +94,42 @@ - - def import_as_names(): +@@ -122,8 +122,10 @@ + """ # fmt: off -- from hello import a, b -- 'unformatted' -+ from hello import a, b + +- # hey, that won't work + ++ #hey, that won't work ++ + -+ "unformatted" # fmt: on + pass - - def testlist_star_expr(): - # fmt: off -- a , b = *hello -- 'unformatted' -+ a, b = *hello -+ "unformatted" - # fmt: on - - - def yield_expr(): - # fmt: off - yield hello -- 'unformatted' -+ "unformatted" - # fmt: on - "formatted" - # fmt: off -- ( yield hello ) -- 'unformatted' -+ (yield hello) -+ "unformatted" - # fmt: on - - - def example(session): - # fmt: off -- result = session\ -- .query(models.Customer.id)\ -- .filter(models.Customer.account_id == account_id, -- models.Customer.email == email_address)\ -- .order_by(models.Customer.id.asc())\ -+ result = ( -+ session.query(models.Customer.id) -+ .filter( -+ models.Customer.account_id == account_id, -+ models.Customer.email == email_address, -+ ) -+ .order_by(models.Customer.id.asc()) - .all() -+ ) - # fmt: on - - -@@ -132,10 +150,10 @@ - """Another known limitation.""" +@@ -138,7 +140,7 @@ + now . considers . multiple . fmt . directives . within . one . prefix # fmt: on # fmt: off -- this=should.not_be.formatted() -- and_=indeed . it is not formatted -- because . the . handling . inside . generate_ignored_nodes() -- now . considers . multiple . fmt . directives . within . one . prefix -+ this = should.not_be.formatted() -+ and_ = indeed.it is not formatted -+ because.the.handling.inside.generate_ignored_nodes() -+ now.considers.multiple.fmt.directives.within.one.prefix +- # ...but comments still get reformatted even though they should not be ++ # ...but comments still get reformatted even though they should not be # fmt: on - # fmt: off - # ...but comments still get reformatted even though they should not be -@@ -153,9 +171,7 @@ - ) - ) - # fmt: off -- a = ( -- unnecessary_bracket() -- ) -+ a = unnecessary_bracket() - # fmt: on - _type_comment_re = re.compile( - r""" -@@ -178,7 +194,7 @@ + + +@@ -178,7 +180,7 @@ $ """, # fmt: off @@ -377,18 +247,6 @@ d={'a':1, # fmt: on ) -@@ -216,8 +232,7 @@ - xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, - ) - # fmt: off --yield 'hello' -+yield "hello" - # No formatting to the end of the file --l=[1,2,3] --d={'a':1, -- 'b':2} -+l = [1, 2, 3] -+d = {"a": 1, "b": 2} ``` ## Ruff Output @@ -402,8 +260,8 @@ from third_party import X, Y, Z from library import some_connection, some_decorator # fmt: off -from third_party import X, Y, Z - +from third_party import (X, + Y, Z) # fmt: on f"trigger 3.6 mode" # Comment 1 @@ -413,40 +271,26 @@ f"trigger 3.6 mode" # fmt: off def func_no_args(): - a - b - c - if True: - raise RuntimeError - if False: - ... - for i in range(10): - print(i) - continue - exec("new-style exec", {}, {}) - return None - - + a; b; c + if True: raise RuntimeError + if False: ... + for i in range(10): + print(i) + continue + exec('new-style exec', {}, {}) + return None async def coroutine(arg, exec=False): - "Single-line docstring. Multiline is harder to reformat." - async with some_connection() as conn: - await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) - await asyncio.sleep(1) - - + 'Single-line docstring. Multiline is harder to reformat.' + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) @asyncio.coroutine -@some_decorator(with_args=True, many_args=[1, 2, 3]) -def function_signature_stress_test( - number: int, - no_annotation=None, - text: str = "default", - *, - debug: bool = False, - **kwargs, -) -> str: - return text[number:-1] - - +@some_decorator( +with_args=True, +many_args=[1,2,3] +) +def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: + return text[number:-1] # fmt: on def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) @@ -490,42 +334,38 @@ def subscriptlist(): def import_as_names(): # fmt: off - from hello import a, b - - "unformatted" + from hello import a, b + 'unformatted' # fmt: on def testlist_star_expr(): # fmt: off - a, b = *hello - "unformatted" + a , b = *hello + 'unformatted' # fmt: on def yield_expr(): # fmt: off yield hello - "unformatted" + 'unformatted' # fmt: on "formatted" # fmt: off - (yield hello) - "unformatted" + ( yield hello ) + 'unformatted' # fmt: on def example(session): # fmt: off - result = ( - session.query(models.Customer.id) - .filter( - models.Customer.account_id == account_id, - models.Customer.email == email_address, - ) - .order_by(models.Customer.id.asc()) + result = session\ + .query(models.Customer.id)\ + .filter(models.Customer.account_id == account_id, + models.Customer.email == email_address)\ + .order_by(models.Customer.id.asc())\ .all() - ) # fmt: on @@ -536,7 +376,9 @@ def off_and_on_without_data(): """ # fmt: off - # hey, that won't work + + #hey, that won't work + # fmt: on pass @@ -546,13 +388,13 @@ def on_and_off_broken(): """Another known limitation.""" # fmt: on # fmt: off - this = should.not_be.formatted() - and_ = indeed.it is not formatted - because.the.handling.inside.generate_ignored_nodes() - now.considers.multiple.fmt.directives.within.one.prefix + this=should.not_be.formatted() + and_=indeed . it is not formatted + because . the . handling . inside . generate_ignored_nodes() + now . considers . multiple . fmt . directives . within . one . prefix # fmt: on # fmt: off - # ...but comments still get reformatted even though they should not be + # ...but comments still get reformatted even though they should not be # fmt: on @@ -567,7 +409,9 @@ def long_lines(): ) ) # fmt: off - a = unnecessary_bracket() + a = ( + unnecessary_bracket() + ) # fmt: on _type_comment_re = re.compile( r""" @@ -628,10 +472,11 @@ cfg.rule( xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off -yield "hello" +yield 'hello' # No formatting to the end of the file -l = [1, 2, 3] -d = {"a": 1, "b": 2} +l=[1,2,3] +d={'a':1, + 'b':2} ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap deleted file mode 100644 index 012dd71335..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap +++ /dev/null @@ -1,201 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff2.py ---- -## Input - -```py -import pytest - -TmSt = 1 -TmEx = 2 - -# fmt: off - -# Test data: -# Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] - -@pytest.mark.parametrize('test', [ - - # Test don't manage the volume - [ - ('stuff', 'in') - ], -]) -def test_fader(test): - pass - -def check_fader(test): - - pass - -def verify_fader(test): - # misaligned comment - pass - -def verify_fader(test): - """Hey, ho.""" - assert test.passed() - -def test_calculate_fades(): - calcs = [ - # one is zero/none - (0, 4, 0, 0, 10, 0, 0, 6, 10), - (None, 4, 0, 0, 10, 0, 0, 6, 10), - ] - -# fmt: on -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -5,36 +5,40 @@ - - # fmt: off - -+ - # Test data: - # Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] - --@pytest.mark.parametrize('test', [ -- -- # Test don't manage the volume -+@pytest.mark.parametrize( -+ "test", - [ -- ('stuff', 'in') -+ # Test don't manage the volume -+ [("stuff", "in")], - ], --]) -+) - def test_fader(test): - pass - -+ - def check_fader(test): -- - pass - -+ - def verify_fader(test): -- # misaligned comment -+ # misaligned comment - pass - -+ - def verify_fader(test): - """Hey, ho.""" - assert test.passed() - -+ - def test_calculate_fades(): - calcs = [ - # one is zero/none -- (0, 4, 0, 0, 10, 0, 0, 6, 10), -- (None, 4, 0, 0, 10, 0, 0, 6, 10), -+ (0, 4, 0, 0, 10, 0, 0, 6, 10), -+ (None, 4, 0, 0, 10, 0, 0, 6, 10), - ] - - # fmt: on -``` - -## Ruff Output - -```py -import pytest - -TmSt = 1 -TmEx = 2 - -# fmt: off - - -# Test data: -# Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] - -@pytest.mark.parametrize( - "test", - [ - # Test don't manage the volume - [("stuff", "in")], - ], -) -def test_fader(test): - pass - - -def check_fader(test): - pass - - -def verify_fader(test): - # misaligned comment - pass - - -def verify_fader(test): - """Hey, ho.""" - assert test.passed() - - -def test_calculate_fades(): - calcs = [ - # one is zero/none - (0, 4, 0, 0, 10, 0, 0, 6, 10), - (None, 4, 0, 0, 10, 0, 0, 6, 10), - ] - -# fmt: on -``` - -## Black Output - -```py -import pytest - -TmSt = 1 -TmEx = 2 - -# fmt: off - -# Test data: -# Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] - -@pytest.mark.parametrize('test', [ - - # Test don't manage the volume - [ - ('stuff', 'in') - ], -]) -def test_fader(test): - pass - -def check_fader(test): - - pass - -def verify_fader(test): - # misaligned comment - pass - -def verify_fader(test): - """Hey, ho.""" - assert test.passed() - -def test_calculate_fades(): - calcs = [ - # one is zero/none - (0, 4, 0, 0, 10, 0, 0, 6, 10), - (None, 4, 0, 0, 10, 0, 0, 6, 10), - ] - -# fmt: on -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap deleted file mode 100644 index 42ebc85486..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap +++ /dev/null @@ -1,101 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff3.py ---- -## Input - -```py -# fmt: off -x = [ - 1, 2, - 3, 4, -] -# fmt: on - -# fmt: off -x = [ - 1, 2, - 3, 4, -] -# fmt: on - -x = [ - 1, 2, 3, 4 -] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,14 +1,18 @@ - # fmt: off - x = [ -- 1, 2, -- 3, 4, -+ 1, -+ 2, -+ 3, -+ 4, - ] - # fmt: on - - # fmt: off - x = [ -- 1, 2, -- 3, 4, -+ 1, -+ 2, -+ 3, -+ 4, - ] - # fmt: on - -``` - -## Ruff Output - -```py -# fmt: off -x = [ - 1, - 2, - 3, - 4, -] -# fmt: on - -# fmt: off -x = [ - 1, - 2, - 3, - 4, -] -# fmt: on - -x = [1, 2, 3, 4] -``` - -## Black Output - -```py -# fmt: off -x = [ - 1, 2, - 3, 4, -] -# fmt: on - -# fmt: off -x = [ - 1, 2, - 3, 4, -] -# fmt: on - -x = [1, 2, 3, 4] -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap index a8dc2ef620..5962f31f5f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap @@ -25,52 +25,48 @@ def f(): pass ```diff --- Black +++ Ruff -@@ -1,8 +1,12 @@ - # fmt: off --@test([ -- 1, 2, -- 3, 4, --]) -+@test( -+ [ -+ 1, -+ 2, -+ 3, -+ 4, -+ ] -+) +@@ -4,17 +4,10 @@ + 3, 4, + ]) # fmt: on - def f(): - pass +-def f(): +- pass +- ++def f(): pass + +-@test( +- [ +- 1, +- 2, +- 3, +- 4, +- ] +-) +-def f(): +- pass ++@test([ ++ 1, 2, ++ 3, 4, ++]) ++def f(): pass ``` ## Ruff Output ```py # fmt: off -@test( - [ - 1, - 2, - 3, - 4, - ] -) +@test([ + 1, 2, + 3, 4, +]) # fmt: on -def f(): - pass +def f(): pass - -@test( - [ - 1, - 2, - 3, - 4, - ] -) -def f(): - pass +@test([ + 1, 2, + 3, 4, +]) +def f(): pass ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap index 99e98844ff..b28415f3ac 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap @@ -110,57 +110,7 @@ elif unformatted: }, ) -@@ -27,7 +26,7 @@ - # Regression test for https://github.com/psf/black/issues/3026. - def test_func(): - # yapf: disable -- if unformatted( args ): -+ if unformatted(args): - return True - # yapf: enable - elif b: -@@ -39,10 +38,10 @@ - # Regression test for https://github.com/psf/black/issues/2567. - if True: - # fmt: off -- for _ in range( 1 ): -- # fmt: on -- print ( "This won't be formatted" ) -- print ( "This won't be formatted either" ) -+ for _ in range(1): -+ # fmt: on -+ print("This won't be formatted") -+ print("This won't be formatted either") - else: - print("This will be formatted") - -@@ -52,14 +51,12 @@ - async def call(param): - if param: - # fmt: off -- if param[0:4] in ( -- "ABCD", "EFGH" -- ) : -+ if param[0:4] in ("ABCD", "EFGH"): - # fmt: on -- print ( "This won't be formatted" ) -+ print("This won't be formatted") - - elif param[0:4] in ("ZZZZ",): -- print ( "This won't be formatted either" ) -+ print("This won't be formatted either") - - print("This will be formatted") - -@@ -68,13 +65,13 @@ - class Named(t.Protocol): - # fmt: off - @property -- def this_wont_be_formatted ( self ) -> str: ... -+ def this_wont_be_formatted(self) -> str: -+ ... - - +@@ -74,7 +73,6 @@ class Factory(t.Protocol): def this_will_be_formatted(self, **kwargs) -> Named: ... @@ -168,7 +118,7 @@ elif unformatted: # fmt: on -@@ -82,6 +79,6 @@ +@@ -82,6 +80,6 @@ if x: return x # fmt: off @@ -209,7 +159,7 @@ run( # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if unformatted(args): + if unformatted( args ): return True # yapf: enable elif b: @@ -221,10 +171,10 @@ def test_func(): # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off - for _ in range(1): - # fmt: on - print("This won't be formatted") - print("This won't be formatted either") + for _ in range( 1 ): + # fmt: on + print ( "This won't be formatted" ) + print ( "This won't be formatted either" ) else: print("This will be formatted") @@ -234,12 +184,14 @@ class A: async def call(param): if param: # fmt: off - if param[0:4] in ("ABCD", "EFGH"): + if param[0:4] in ( + "ABCD", "EFGH" + ) : # fmt: on - print("This won't be formatted") + print ( "This won't be formatted" ) elif param[0:4] in ("ZZZZ",): - print("This won't be formatted either") + print ( "This won't be formatted either" ) print("This will be formatted") @@ -248,8 +200,7 @@ class A: class Named(t.Protocol): # fmt: off @property - def this_wont_be_formatted(self) -> str: - ... + def this_wont_be_formatted ( self ) -> str: ... class Factory(t.Protocol): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap deleted file mode 100644 index 002454e8da..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap +++ /dev/null @@ -1,64 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip3.py ---- -## Input - -```py -a = 3 -# fmt: off -b, c = 1, 2 -d = 6 # fmt: skip -e = 5 -# fmt: on -f = ["This is a very long line that should be formatted into a clearer line ", "by rearranging."] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,7 +1,7 @@ - a = 3 - # fmt: off --b, c = 1, 2 --d = 6 # fmt: skip -+b, c = 1, 2 -+d = 6 # fmt: skip - e = 5 - # fmt: on - f = [ -``` - -## Ruff Output - -```py -a = 3 -# fmt: off -b, c = 1, 2 -d = 6 # fmt: skip -e = 5 -# fmt: on -f = [ - "This is a very long line that should be formatted into a clearer line ", - "by rearranging.", -] -``` - -## Black Output - -```py -a = 3 -# fmt: off -b, c = 1, 2 -d = 6 # fmt: skip -e = 5 -# fmt: on -f = [ - "This is a very long line that should be formatted into a clearer line ", - "by rearranging.", -] -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap new file mode 100644 index 0000000000..f961fc36e8 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__comments.py.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/comments.py +--- +## Input +```py +pass + +# fmt: off + # A comment that falls into the verbatim range +a + b # a trailing comment + +# in between comments + +# function comment +def test(): + pass + + # trailing comment that falls into the verbatim range + + # fmt: on + +a + b + +def test(): + pass + # fmt: off + # a trailing comment + +``` + +## Output +```py +pass + +# fmt: off + # A comment that falls into the verbatim range +a + b # a trailing comment + +# in between comments + +# function comment +def test(): + pass + + # trailing comment that falls into the verbatim range + + # fmt: on + +a + b + + +def test(): + pass + # fmt: off + # a trailing comment +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__empty_file.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__empty_file.py.snap new file mode 100644 index 0000000000..7dbe39d5d1 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__empty_file.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/empty_file.py +--- +## Input +```py +# fmt: off + + # this does not work because there are no statements + +# fmt: on +``` + +## Output +```py +# fmt: off + +# this does not work because there are no statements + +# fmt: on +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap new file mode 100644 index 0000000000..396dd028e3 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.py +--- +## Input +```py +def test(): + # fmt: off + """ This docstring does not + get formatted + """ + + # fmt: on + + but + this + does + +def test(): + # fmt: off + # just for fun + # fmt: on + # leading comment + """ This docstring gets formatted + """ # trailing comment + + and_this + gets + formatted + too +``` + +## Output +```py +def test(): + # fmt: off + """ This docstring does not + get formatted + """ + + # fmt: on + + but + this + does + + +def test(): + # fmt: off + # just for fun + # fmt: on + # leading comment + """This docstring gets formatted""" # trailing comment + + and_this + gets + formatted + too +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap new file mode 100644 index 0000000000..0c009fa424 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/form_feed.py +--- +## Input +```py +# fmt: off +# DB layer (form feed at the start of the next line) + +# fmt: on + +def test(): + pass +``` + +## Output +```py +# fmt: off +# DB layer (form feed at the start of the next line) + +# fmt: on + +def test(): + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__last_statement.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__last_statement.py.snap new file mode 100644 index 0000000000..07d0eeaf0c --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__last_statement.py.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/last_statement.py +--- +## Input +```py +def test(): + # fmt: off + + a + b + + + + # suppressed comments + +a + b # formatted +``` + +## Output +```py +def test(): + # fmt: off + + a + b + + + + # suppressed comments + + +a + b # formatted +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__no_fmt_on.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__no_fmt_on.py.snap new file mode 100644 index 0000000000..15ded04ff6 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__no_fmt_on.py.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/no_fmt_on.py +--- +## Input +```py +def test(): + # fmt: off + not formatted + + if unformatted + a: + pass + +# Get's formatted again +a + b +``` + +## Output +```py +def test(): + # fmt: off + not formatted + + if unformatted + a: + pass + + +# Get's formatted again +a + b +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__off_on_off_on.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__off_on_off_on.py.snap new file mode 100644 index 0000000000..c33f5faa9b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__off_on_off_on.py.snap @@ -0,0 +1,156 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/off_on_off_on.py +--- +## Input +```py +# Tricky sequences of fmt off and on + +# Formatted +a + b + +# fmt: off + # not formatted 1 +# fmt: on +a + b + # formatted + + +# fmt: off + # not formatted 1 +# fmt: on + # not formatted 2 +# fmt: off +a + b +# fmt: on + + +# fmt: off + # not formatted 1 +# fmt: on + # formatted 1 +# fmt: off + # not formatted 2 +a + b +# fmt: on + # formatted +b + c + + +# fmt: off +a + b + + # not formatted +# fmt: on + # formatted +a + b + + +# fmt: off +a + b + + # not formatted 1 +# fmt: on + # formatted +# fmt: off + # not formatted 2 +a + b + + +# fmt: off +a + b + + # not formatted 1 +# fmt: on + # formatted + +# leading +a + b +# fmt: off + + # leading unformatted +def test (): + pass + + # fmt: on + +a + b +``` + +## Output +```py +# Tricky sequences of fmt off and on + +# Formatted +a + b + +# fmt: off + # not formatted 1 +# fmt: on +a + b +# formatted + + +# fmt: off + # not formatted 1 +# fmt: on +# not formatted 2 +# fmt: off +a + b +# fmt: on + + +# fmt: off + # not formatted 1 +# fmt: on +# formatted 1 +# fmt: off + # not formatted 2 +a + b +# fmt: on +# formatted +b + c + + +# fmt: off +a + b + + # not formatted +# fmt: on +# formatted +a + b + + +# fmt: off +a + b + + # not formatted 1 +# fmt: on +# formatted +# fmt: off + # not formatted 2 +a + b + + +# fmt: off +a + b + + # not formatted 1 +# fmt: on +# formatted + +# leading +a + b +# fmt: off + + # leading unformatted +def test (): + pass + + # fmt: on + +a + b +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__simple.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__simple.py.snap new file mode 100644 index 0000000000..e9f968a425 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__simple.py.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/simple.py +--- +## Input +```py +# Get's formatted +a + b + +# fmt: off +a + [1, 2, 3, 4, 5 ] +# fmt: on + +# Get's formatted again +a + b +``` + +## Output +```py +# Get's formatted +a + b + +# fmt: off +a + [1, 2, 3, 4, 5 ] +# fmt: on + +# Get's formatted again +a + b +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap new file mode 100644 index 0000000000..e80951bd3a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap @@ -0,0 +1,96 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/trailing_comments.py +--- +## Input +```py +a = 10 +# fmt: off + +# more format + +def test(): ... + + +# fmt: on + +b = 20 +# Sequence of trailing comments that toggle between format on and off. The sequence ends with a `fmt: on`, so that the function gets formatted. +# formatted 1 +# fmt: off + # not formatted +# fmt: on + # formatted comment +# fmt: off + # not formatted 2 +# fmt: on + + # formatted +def test2 (): + ... + +a = 10 + +# Sequence of trailing comments that toggles between format on and off. The sequence ends with a `fmt: off`, so that the function is not formatted. + # formatted 1 +# fmt: off + # not formatted +# fmt: on + # formattd +# fmt: off + + # not formatted +def test3 (): + ... + +# fmt: on +``` + +## Output +```py +a = 10 +# fmt: off + +# more format + +def test(): ... + + +# fmt: on + +b = 20 +# Sequence of trailing comments that toggle between format on and off. The sequence ends with a `fmt: on`, so that the function gets formatted. +# formatted 1 +# fmt: off + # not formatted +# fmt: on +# formatted comment +# fmt: off + # not formatted 2 +# fmt: on + + +# formatted +def test2(): + ... + + +a = 10 + +# Sequence of trailing comments that toggles between format on and off. The sequence ends with a `fmt: off`, so that the function is not formatted. +# formatted 1 +# fmt: off + # not formatted +# fmt: on +# formattd +# fmt: off + + # not formatted +def test3 (): + ... + +# fmt: on +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__yapf.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__yapf.py.snap new file mode 100644 index 0000000000..45f0ad07d0 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__yapf.py.snap @@ -0,0 +1,48 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/yapf.py +--- +## Input +```py +# Get's formatted +a + b + +# yapf: disable +a + [1, 2, 3, 4, 5 ] +# yapf: enable + +# Get's formatted again +a + b + + +# yapf: disable +a + [1, 2, 3, 4, 5 ] +# fmt: on + +# Get's formatted again +a + b +``` + +## Output +```py +# Get's formatted +a + b + +# yapf: disable +a + [1, 2, 3, 4, 5 ] +# yapf: enable + +# Get's formatted again +a + b + + +# yapf: disable +a + [1, 2, 3, 4, 5 ] +# fmt: on + +# Get's formatted again +a + b +``` + + +