mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-29 06:44:16 +00:00
Handle positional-only-arguments separator comments (#4748)
This commit is contained in:
parent
be31d71849
commit
b7294b48e7
7 changed files with 442 additions and 0 deletions
|
@ -716,4 +716,90 @@ def test(
|
||||||
|
|
||||||
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positional_argument_only_comment() {
|
||||||
|
let source = r#"
|
||||||
|
def test(
|
||||||
|
a, # trailing positional comment
|
||||||
|
# Positional arguments only after here
|
||||||
|
/, # trailing positional argument comment.
|
||||||
|
# leading b comment
|
||||||
|
b,
|
||||||
|
): pass
|
||||||
|
"#;
|
||||||
|
let test_case = CommentsTestCase::from_code(source);
|
||||||
|
|
||||||
|
let comments = test_case.to_comments();
|
||||||
|
|
||||||
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positional_argument_only_leading_comma_comment() {
|
||||||
|
let source = r#"
|
||||||
|
def test(
|
||||||
|
a # trailing positional comment
|
||||||
|
# Positional arguments only after here
|
||||||
|
,/, # trailing positional argument comment.
|
||||||
|
# leading b comment
|
||||||
|
b,
|
||||||
|
): pass
|
||||||
|
"#;
|
||||||
|
let test_case = CommentsTestCase::from_code(source);
|
||||||
|
|
||||||
|
let comments = test_case.to_comments();
|
||||||
|
|
||||||
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positional_argument_only_comment_without_following_node() {
|
||||||
|
let source = r#"
|
||||||
|
def test(
|
||||||
|
a, # trailing positional comment
|
||||||
|
# Positional arguments only after here
|
||||||
|
/, # trailing positional argument comment.
|
||||||
|
# Trailing on new line
|
||||||
|
): pass
|
||||||
|
"#;
|
||||||
|
let test_case = CommentsTestCase::from_code(source);
|
||||||
|
|
||||||
|
let comments = test_case.to_comments();
|
||||||
|
|
||||||
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_positional_arguments_with_defaults() {
|
||||||
|
let source = r#"
|
||||||
|
def test(
|
||||||
|
a=10 # trailing positional comment
|
||||||
|
# Positional arguments only after here
|
||||||
|
,/, # trailing positional argument comment.
|
||||||
|
# leading comment for b
|
||||||
|
b=20
|
||||||
|
): pass
|
||||||
|
"#;
|
||||||
|
let test_case = CommentsTestCase::from_code(source);
|
||||||
|
|
||||||
|
let comments = test_case.to_comments();
|
||||||
|
|
||||||
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_positional_arguments_slash_on_same_line() {
|
||||||
|
let source = r#"
|
||||||
|
def test(a=10,/, # trailing positional argument comment.
|
||||||
|
# leading comment for b
|
||||||
|
b=20
|
||||||
|
): pass
|
||||||
|
"#;
|
||||||
|
let test_case = CommentsTestCase::from_code(source);
|
||||||
|
|
||||||
|
let comments = test_case.to_comments();
|
||||||
|
|
||||||
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use crate::comments::visitor::{CommentPlacement, DecoratedComment};
|
use crate::comments::visitor::{CommentPlacement, DecoratedComment};
|
||||||
|
|
||||||
|
use crate::comments::CommentTextPosition;
|
||||||
use ruff_python_ast::node::AnyNodeRef;
|
use ruff_python_ast::node::AnyNodeRef;
|
||||||
use ruff_python_ast::source_code::Locator;
|
use ruff_python_ast::source_code::Locator;
|
||||||
use ruff_python_ast::whitespace;
|
use ruff_python_ast::whitespace;
|
||||||
|
use ruff_text_size::{TextRange, TextSize};
|
||||||
|
use rustpython_parser::ast::Ranged;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
/// Implements the custom comment placement logic.
|
/// Implements the custom comment placement logic.
|
||||||
|
@ -14,6 +17,7 @@ pub(super) fn place_comment<'a>(
|
||||||
.or_else(|comment| handle_match_comment(comment, locator))
|
.or_else(|comment| handle_match_comment(comment, locator))
|
||||||
.or_else(|comment| handle_in_between_bodies_comment(comment, locator))
|
.or_else(|comment| handle_in_between_bodies_comment(comment, locator))
|
||||||
.or_else(|comment| handle_trailing_body_comment(comment, locator))
|
.or_else(|comment| handle_trailing_body_comment(comment, locator))
|
||||||
|
.or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles leading comments in front of a match case or a trailing comment of the `match` statement.
|
/// Handles leading comments in front of a match case or a trailing comment of the `match` statement.
|
||||||
|
@ -393,6 +397,97 @@ fn handle_trailing_body_comment<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attaches comments for the positional-only arguments separator `/` as trailing comments to the
|
||||||
|
/// enclosing [`Arguments`] node.
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// def test(
|
||||||
|
/// a,
|
||||||
|
/// # Positional arguments only after here
|
||||||
|
/// /, # trailing positional argument comment.
|
||||||
|
/// b,
|
||||||
|
/// ): pass
|
||||||
|
/// ```
|
||||||
|
fn handle_positional_only_arguments_separator_comment<'a>(
|
||||||
|
comment: DecoratedComment<'a>,
|
||||||
|
locator: &Locator,
|
||||||
|
) -> CommentPlacement<'a> {
|
||||||
|
let AnyNodeRef::Arguments(arguments) = comment.enclosing_node() else {
|
||||||
|
return CommentPlacement::Default(comment);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Using the `/` without any leading arguments is a syntax error.
|
||||||
|
let Some(last_argument_or_default) = comment.preceding_node() else {
|
||||||
|
return CommentPlacement::Default(comment);
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_last_positional_argument = are_same_optional(last_argument_or_default, arguments.posonlyargs.last())
|
||||||
|
// If the preceding node is the default for the last positional argument
|
||||||
|
// ```python
|
||||||
|
// def test(a=10, /, b): pass
|
||||||
|
// ```
|
||||||
|
|| arguments
|
||||||
|
.defaults
|
||||||
|
.iter()
|
||||||
|
.position(|default| AnyNodeRef::from(default).ptr_eq(last_argument_or_default))
|
||||||
|
== Some(arguments.posonlyargs.len().saturating_sub(1));
|
||||||
|
|
||||||
|
if !is_last_positional_argument {
|
||||||
|
return CommentPlacement::Default(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
let trivia_end = comment
|
||||||
|
.following_node()
|
||||||
|
.map_or(arguments.end(), |following| following.start());
|
||||||
|
let trivia_range = TextRange::new(last_argument_or_default.end(), trivia_end);
|
||||||
|
|
||||||
|
if let Some(slash_offset) = find_pos_only_slash_offset(trivia_range, locator) {
|
||||||
|
let comment_start = comment.slice().range().start();
|
||||||
|
let is_slash_comment = match comment.text_position() {
|
||||||
|
CommentTextPosition::EndOfLine => {
|
||||||
|
let preceding_end_line = locator.line_end(last_argument_or_default.end());
|
||||||
|
let slash_comments_start = preceding_end_line.min(slash_offset);
|
||||||
|
|
||||||
|
comment_start >= slash_comments_start
|
||||||
|
&& locator.line_end(slash_offset) > comment_start
|
||||||
|
}
|
||||||
|
CommentTextPosition::OwnLine => comment_start < slash_offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_slash_comment {
|
||||||
|
CommentPlacement::dangling(comment.enclosing_node(), comment)
|
||||||
|
} else {
|
||||||
|
CommentPlacement::Default(comment)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Should not happen, but let's go with it
|
||||||
|
CommentPlacement::Default(comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_pos_only_slash_offset(trivia_range: TextRange, locator: &Locator) -> Option<TextSize> {
|
||||||
|
let mut in_comment = false;
|
||||||
|
|
||||||
|
for (offset, c) in locator.slice(trivia_range).char_indices() {
|
||||||
|
match c {
|
||||||
|
'\n' | '\r' => {
|
||||||
|
in_comment = false;
|
||||||
|
}
|
||||||
|
'/' if !in_comment => {
|
||||||
|
return Some(trivia_range.start() + TextSize::try_from(offset).unwrap());
|
||||||
|
}
|
||||||
|
'#' => {
|
||||||
|
// SAFE because we know there's only trivia. So all content is either whitespace,
|
||||||
|
// or comments but never strings.
|
||||||
|
in_comment = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option<T>) -> bool
|
fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option<T>) -> bool
|
||||||
where
|
where
|
||||||
T: Into<AnyNodeRef<'a>>,
|
T: Into<AnyNodeRef<'a>>,
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||||
|
expression: comments.debug(test_case.source_code)
|
||||||
|
---
|
||||||
|
{
|
||||||
|
Node {
|
||||||
|
kind: Arguments,
|
||||||
|
range: 10..94,
|
||||||
|
source: `a=10,/, # trailing position...t comment.⏎`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional argument comment.",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 90..91,
|
||||||
|
source: `b`,
|
||||||
|
}: {
|
||||||
|
"leading": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# leading comment for b",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||||
|
expression: comments.debug(test_case.source_code)
|
||||||
|
---
|
||||||
|
{
|
||||||
|
Node {
|
||||||
|
kind: Arguments,
|
||||||
|
range: 15..177,
|
||||||
|
source: `a=10 # trailing positional comment⏎`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# Positional arguments only after here",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional argument comment.",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: ExprConstant,
|
||||||
|
range: 17..19,
|
||||||
|
source: `10`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional comment",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 173..174,
|
||||||
|
source: `b`,
|
||||||
|
}: {
|
||||||
|
"leading": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# leading comment for b",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||||
|
expression: comments.debug(test_case.source_code)
|
||||||
|
---
|
||||||
|
{
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 15..16,
|
||||||
|
source: `a`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional comment",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arguments,
|
||||||
|
range: 15..168,
|
||||||
|
source: `a, # trailing positional comment⏎`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# Positional arguments only after here",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional argument comment.",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 166..167,
|
||||||
|
source: `b`,
|
||||||
|
}: {
|
||||||
|
"leading": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# leading b comment",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||||
|
expression: comments.debug(test_case.source_code)
|
||||||
|
---
|
||||||
|
{
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 15..16,
|
||||||
|
source: `a`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional comment",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arguments,
|
||||||
|
range: 15..97,
|
||||||
|
source: `a, # trailing positional comment⏎`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# Positional arguments only after here",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"trailing": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional argument comment.",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: StmtPass,
|
||||||
|
range: 168..172,
|
||||||
|
source: `pass`,
|
||||||
|
}: {
|
||||||
|
"leading": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# Trailing on new line",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||||
|
expression: comments.debug(test_case.source_code)
|
||||||
|
---
|
||||||
|
{
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 15..16,
|
||||||
|
source: `a`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional comment",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arguments,
|
||||||
|
range: 15..168,
|
||||||
|
source: `a # trailing positional comment⏎`,
|
||||||
|
}: {
|
||||||
|
"leading": [],
|
||||||
|
"dangling": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# Positional arguments only after here",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
SourceComment {
|
||||||
|
text: "# trailing positional argument comment.",
|
||||||
|
position: EndOfLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
kind: Arg,
|
||||||
|
range: 166..167,
|
||||||
|
source: `b`,
|
||||||
|
}: {
|
||||||
|
"leading": [
|
||||||
|
SourceComment {
|
||||||
|
text: "# leading b comment",
|
||||||
|
position: OwnLine,
|
||||||
|
formatted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"dangling": [],
|
||||||
|
"trailing": [],
|
||||||
|
},
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue