Fix placement of inline parameter comments (#13379)

This commit is contained in:
Micha Reiser 2024-09-18 08:26:06 +02:00 committed by GitHub
parent c7b2e336f0
commit 6ac61d7b89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 419 additions and 63 deletions

View file

@ -458,3 +458,108 @@ def foo(x: S) -> S: ...
@decorator # comment
def foo(x: S) -> S: ...
# Regression tests for https://github.com/astral-sh/ruff/issues/13369
def foo(
arg: ( # comment with non-return annotation
int
# comment with non-return annotation
),
):
pass
def foo(
arg: ( # comment with non-return annotation
int
| range
| memoryview
# comment with non-return annotation
),
):
pass
def foo(arg: (
int
# only after
)):
pass
# Asserts that "incorrectly" placed comments don't *move* by fixing https://github.com/astral-sh/ruff/issues/13369
def foo(
# comment with non-return annotation
# comment with non-return annotation
arg: (int),
):
pass
# Comments between *args and **kwargs
def args_no_type_annotation(*
# comment
args): pass
def args_type_annotation(*
# comment
args: int): pass
def args_trailing_end_of_line_comment(* # comment
args): pass
def args_blank_line_comment(*
# comment
args): pass
def args_with_leading_parameter_comment(
# What comes next are arguments
*
# with an inline comment
args): pass
def kargs_no_type_annotation(**
# comment
kwargs): pass
def kwargs_type_annotation(**
# comment
kwargs: int): pass
def args_many_comments(
# before
*
# between * and name
args # trailing args
# after name
): pass
def args_many_comments_with_type_annotation(
# before
*
# between * and name
args # trailing args
# before colon
: # after colon
# before type
int # trailing type
# after type
): pass
def args_with_type_annotations_no_after_colon_comment(
# before
*
# between * and name
args # trailing args
# before colon
:
# before type
int # trailing type
# after type
): pass

View file

@ -2,10 +2,12 @@ use std::cmp::Ordering;
use ast::helpers::comment_indentation_after;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, AnyNodeRef, Comprehension, Expr, ModModule, Parameters};
use ruff_python_ast::{
self as ast, AnyNodeRef, Comprehension, Expr, ModModule, Parameter, Parameters,
};
use ruff_python_trivia::{
find_only_token_in_range, indentation_at_offset, BackwardsTokenizer, CommentRanges,
SimpleToken, SimpleTokenKind, SimpleTokenizer,
find_only_token_in_range, first_non_trivia_token, indentation_at_offset, BackwardsTokenizer,
CommentRanges, SimpleToken, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange};
@ -202,14 +204,7 @@ fn handle_enclosed_comment<'a>(
}
})
}
AnyNodeRef::Parameter(parameter) => {
// E.g. a comment between the `*` or `**` and the parameter name.
if comment.preceding_node().is_none() || comment.following_node().is_none() {
CommentPlacement::leading(parameter, comment)
} else {
CommentPlacement::Default(comment)
}
}
AnyNodeRef::Parameter(parameter) => handle_parameter_comment(comment, parameter, locator),
AnyNodeRef::Arguments(_) | AnyNodeRef::TypeParams(_) | AnyNodeRef::PatternArguments(_) => {
handle_bracketed_end_of_line_comment(comment, locator)
}
@ -760,6 +755,41 @@ fn handle_parameters_separator_comment<'a>(
CommentPlacement::Default(comment)
}
/// Associate comments that come before the `:` starting the type annotation or before the
/// parameter's name for unannotated parameters as leading parameter-comments.
///
/// The parameter's name isn't a node to which comments can be associated.
/// That's why we pull out all comments that come before the expression name or the type annotation
/// and make them leading parameter comments. For example:
/// * `* # comment\nargs`
/// * `arg # comment\n : int`
///
/// Associate comments with the type annotation when possible.
fn handle_parameter_comment<'a>(
comment: DecoratedComment<'a>,
parameter: &'a Parameter,
locator: &Locator,
) -> CommentPlacement<'a> {
if parameter.annotation.as_deref().is_some() {
let colon = first_non_trivia_token(parameter.name.end(), locator.contents()).expect(
"A annotated parameter should have a colon following its name when it is valid syntax.",
);
assert_eq!(colon.kind(), SimpleTokenKind::Colon);
if comment.start() < colon.start() {
// The comment is before the colon, pull it out and make it a leading comment of the parameter.
CommentPlacement::leading(parameter, comment)
} else {
CommentPlacement::Default(comment)
}
} else if comment.start() < parameter.name.start() {
CommentPlacement::leading(parameter, comment)
} else {
CommentPlacement::Default(comment)
}
}
/// Handles comments between the left side and the operator of a binary expression (trailing comments of the left),
/// and trailing end-of-line comments that are on the same line as the operator.
///

View file

@ -1,7 +1,6 @@
use ruff_formatter::write;
use ruff_python_ast::Parameter;
use crate::expression::parentheses::is_expression_parenthesized;
use crate::prelude::*;
use ruff_python_ast::Parameter;
#[derive(Default)]
pub struct FormatParameter;
@ -16,8 +15,22 @@ impl FormatNodeRule<Parameter> for FormatParameter {
name.format().fmt(f)?;
if let Some(annotation) = annotation {
write!(f, [token(":"), space(), annotation.format()])?;
if let Some(annotation) = annotation.as_deref() {
token(":").fmt(f)?;
if f.context().comments().has_leading(annotation)
&& !is_expression_parenthesized(
annotation.into(),
f.context().comments().ranges(),
f.context().source(),
)
{
hard_line_break().fmt(f)?;
} else {
space().fmt(f)?;
}
annotation.format().fmt(f)?;
}
Ok(())

View file

@ -142,53 +142,29 @@ variable: (
): ...
@@ -143,34 +141,31 @@
@@ -153,16 +151,18 @@
def foo(
- arg: ( # comment with non-return annotation
- int
- # comment with non-return annotation
- ),
+ # comment with non-return annotation
+ # comment with non-return annotation
+ arg: (int),
):
pass
def foo(
- arg: ( # comment with non-return annotation
arg: ( # comment with non-return annotation
- int
- | range
- | memoryview
- # comment with non-return annotation
- ),
+ # comment with non-return annotation
+ # comment with non-return annotation
+ arg: (int | range | memoryview),
+ int | range | memoryview
# comment with non-return annotation
),
):
pass
-def foo(arg: int): # only before
+def foo(
+ # only before
+ arg: (int),
+ arg: ( # only before
+ int
+ ),
+):
pass
def foo(
- arg: (
- int
- # only after
- ),
+ # only after
+ arg: (int),
):
pass
```
## Ruff Output
@ -337,31 +313,36 @@ def foo() -> (
def foo(
# comment with non-return annotation
# comment with non-return annotation
arg: (int),
arg: ( # comment with non-return annotation
int
# comment with non-return annotation
),
):
pass
def foo(
# comment with non-return annotation
# comment with non-return annotation
arg: (int | range | memoryview),
arg: ( # comment with non-return annotation
int | range | memoryview
# comment with non-return annotation
),
):
pass
def foo(
# only before
arg: (int),
arg: ( # only before
int
),
):
pass
def foo(
# only after
arg: (int),
arg: (
int
# only after
),
):
pass

View file

@ -464,6 +464,111 @@ def foo(x: S) -> S: ...
@decorator # comment
def foo(x: S) -> S: ...
# Regression tests for https://github.com/astral-sh/ruff/issues/13369
def foo(
arg: ( # comment with non-return annotation
int
# comment with non-return annotation
),
):
pass
def foo(
arg: ( # comment with non-return annotation
int
| range
| memoryview
# comment with non-return annotation
),
):
pass
def foo(arg: (
int
# only after
)):
pass
# Asserts that "incorrectly" placed comments don't *move* by fixing https://github.com/astral-sh/ruff/issues/13369
def foo(
# comment with non-return annotation
# comment with non-return annotation
arg: (int),
):
pass
# Comments between *args and **kwargs
def args_no_type_annotation(*
# comment
args): pass
def args_type_annotation(*
# comment
args: int): pass
def args_trailing_end_of_line_comment(* # comment
args): pass
def args_blank_line_comment(*
# comment
args): pass
def args_with_leading_parameter_comment(
# What comes next are arguments
*
# with an inline comment
args): pass
def kargs_no_type_annotation(**
# comment
kwargs): pass
def kwargs_type_annotation(**
# comment
kwargs: int): pass
def args_many_comments(
# before
*
# between * and name
args # trailing args
# after name
): pass
def args_many_comments_with_type_annotation(
# before
*
# between * and name
args # trailing args
# before colon
: # after colon
# before type
int # trailing type
# after type
): pass
def args_with_type_annotations_no_after_colon_comment(
# before
*
# between * and name
args # trailing args
# before colon
:
# before type
int # trailing type
# after type
): pass
```
## Output
@ -1089,6 +1194,130 @@ def foo(x: S) -> S: ...
@decorator # comment
def foo(x: S) -> S: ...
# Regression tests for https://github.com/astral-sh/ruff/issues/13369
def foo(
arg: ( # comment with non-return annotation
int
# comment with non-return annotation
),
):
pass
def foo(
arg: ( # comment with non-return annotation
int | range | memoryview
# comment with non-return annotation
),
):
pass
def foo(
arg: (
int
# only after
),
):
pass
# Asserts that "incorrectly" placed comments don't *move* by fixing https://github.com/astral-sh/ruff/issues/13369
def foo(
# comment with non-return annotation
# comment with non-return annotation
arg: (int),
):
pass
# Comments between *args and **kwargs
def args_no_type_annotation(
# comment
*args,
):
pass
def args_type_annotation(
# comment
*args: int,
):
pass
def args_trailing_end_of_line_comment(
# comment
*args,
):
pass
def args_blank_line_comment(
# comment
*args,
):
pass
def args_with_leading_parameter_comment(
# What comes next are arguments
# with an inline comment
*args,
):
pass
def kargs_no_type_annotation(
# comment
**kwargs,
):
pass
def kwargs_type_annotation(
# comment
**kwargs: int,
):
pass
def args_many_comments(
# before
# between * and name
*args, # trailing args
# after name
):
pass
def args_many_comments_with_type_annotation(
# before
# between * and name
# trailing args
# before colon
*args:
# after colon
# before type
int, # trailing type
# after type
):
pass
def args_with_type_annotations_no_after_colon_comment(
# before
# between * and name
# trailing args
# before colon
*args:
# before type
int, # trailing type
# after type
):
pass
```

View file

@ -4,14 +4,12 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::{is_python_whitespace, Cursor};
/// Searches for the first non-trivia character in `range`.
/// Searches for the first non-trivia character after `offset`.
///
/// The search skips over any whitespace and comments.
///
/// Returns `Some` if the range contains any non-trivia character. The first item is the absolute offset
/// of the character, the second item the non-trivia character.
///
/// Returns `None` if the range is empty or only contains trivia (whitespace or comments).
/// Returns `Some` if the source code after `offset` contains any non-trivia character.///
/// Returns `None` if the text after `offset` is empty or only contains trivia (whitespace or comments).
pub fn first_non_trivia_token(offset: TextSize, code: &str) -> Option<SimpleToken> {
SimpleTokenizer::starts_at(offset, code)
.skip_trivia()