Skip to content

Commit 7f6cb9d

Browse files
authored
Format call expressions (without call chaining) (#5341)
## Summary This formats call expressions with magic trailing comma and parentheses behaviour but without call chaining ## Test Plan Lots of new test fixtures, including some that don't work yet
1 parent 50a7769 commit 7f6cb9d

File tree

53 files changed

+1662
-1853
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1662
-1853
lines changed

Cargo.lock

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+7-7
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ toml = { version = "0.7.2" }
5050
# v0.0.1
5151
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
5252

53-
# Please tag the RustPython version everytime you update its revision here.
53+
# Please tag the RustPython version everytime you update its revision here and in fuzz/Cargo.toml
5454
# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork.
55-
# Current tag: v0.0.6
56-
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" }
57-
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["num-bigint"]}
58-
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c", default-features = false, features = ["num-bigint"] }
59-
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c", default-features = false }
60-
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8078663b6c914c1cb86993e427764f7c422fc12c" , default-features = false, features = ["full-lexer", "num-bigint"] }
55+
# Current tag: v0.0.7
56+
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" }
57+
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["num-bigint"]}
58+
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false, features = ["num-bigint"] }
59+
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false }
60+
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] }
6161

6262
[profile.release]
6363
lto = "fat"

crates/ruff/src/test.rs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist}
1515

1616
use crate::autofix::{fix_file, FixResult};
1717
use crate::directives;
18+
#[cfg(not(fuzzing))]
1819
use crate::jupyter::Notebook;
1920
use crate::linter::{check_path, LinterResult};
2021
use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from unittest.mock import MagicMock
2+
3+
4+
def f(*args, **kwargs):
5+
pass
6+
7+
this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1
8+
session = MagicMock()
9+
models = MagicMock()
10+
11+
f()
12+
13+
f(1)
14+
15+
f(x=2)
16+
17+
f(1, x=2)
18+
19+
f(
20+
this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd
21+
)
22+
f(
23+
this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1
24+
)
25+
26+
f(
27+
1,
28+
mixed_very_long_arguments=1,
29+
)
30+
31+
f(
32+
this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd,
33+
these_arguments_have_values_that_need_to_break_because_they_are_too_long1=(100000 - 100000000000),
34+
these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + "asdfasdfa",
35+
these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session,
36+
)
37+
38+
f(
39+
# dangling comment
40+
)
41+
42+
43+
f(
44+
only=1, short=1, arguments=1
45+
)
46+
47+
f(
48+
hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1
49+
)
50+
51+
f(
52+
hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True
53+
)
54+
55+
# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
56+
result = (
57+
session.query(models.Customer.id)
58+
.filter(
59+
models.Customer.account_id == 10000,
60+
models.Customer.email == "[email protected]",
61+
)
62+
.order_by(models.Customer.id.asc())
63+
.all()
64+
)
65+
# TODO(konstin): Black has this special case for comment placement where everything stays in one line
66+
f(
67+
"aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa"
68+
)
69+
70+
f(
71+
session,
72+
b=1,
73+
** # oddly placed end-of-line comment
74+
dict()
75+
)
76+
f(
77+
session,
78+
b=1,
79+
**
80+
# oddly placed own line comment
81+
dict()
82+
)
83+

crates/ruff_python_formatter/src/comments/placement.rs

+29-23
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ fn handle_dict_unpacking_comment<'a>(
989989
match comment.enclosing_node() {
990990
// TODO: can maybe also add AnyNodeRef::Arguments here, but tricky to test due to
991991
// https://github.com/astral-sh/ruff/issues/5176
992-
AnyNodeRef::ExprDict(_) => {}
992+
AnyNodeRef::ExprDict(_) | AnyNodeRef::Keyword(_) => {}
993993
_ => {
994994
return CommentPlacement::Default(comment);
995995
}
@@ -1015,12 +1015,22 @@ fn handle_dict_unpacking_comment<'a>(
10151015
)
10161016
.skip_trivia();
10171017

1018+
// if the remaining tokens from the previous node are exactly `**`,
1019+
// re-assign the comment to the one that follows the stars
1020+
let mut count = 0;
1021+
10181022
// we start from the preceding node but we skip its token
10191023
for token in tokens.by_ref() {
10201024
// Skip closing parentheses that are not part of the node range
10211025
if token.kind == TokenKind::RParen {
10221026
continue;
10231027
}
1028+
// The Keyword case
1029+
if token.kind == TokenKind::Star {
1030+
count += 1;
1031+
break;
1032+
}
1033+
// The dict case
10241034
debug_assert!(
10251035
matches!(
10261036
token,
@@ -1034,9 +1044,6 @@ fn handle_dict_unpacking_comment<'a>(
10341044
break;
10351045
}
10361046

1037-
// if the remaining tokens from the previous node is exactly `**`,
1038-
// re-assign the comment to the one that follows the stars
1039-
let mut count = 0;
10401047
for token in tokens {
10411048
if token.kind != TokenKind::Star {
10421049
return CommentPlacement::Default(comment);
@@ -1050,19 +1057,19 @@ fn handle_dict_unpacking_comment<'a>(
10501057
CommentPlacement::Default(comment)
10511058
}
10521059

1053-
// Own line comments coming after the node are always dangling comments
1054-
// ```python
1055-
// (
1056-
// a
1057-
// # trailing a comment
1058-
// . # dangling comment
1059-
// # or this
1060-
// b
1061-
// )
1062-
// ```
1060+
/// Own line comments coming after the node are always dangling comments
1061+
/// ```python
1062+
/// (
1063+
/// a
1064+
/// # trailing a comment
1065+
/// . # dangling comment
1066+
/// # or this
1067+
/// b
1068+
/// )
1069+
/// ```
10631070
fn handle_attribute_comment<'a>(
10641071
comment: DecoratedComment<'a>,
1065-
locator: &Locator,
1072+
_locator: &Locator,
10661073
) -> CommentPlacement<'a> {
10671074
let Some(attribute) = comment.enclosing_node().expr_attribute() else {
10681075
return CommentPlacement::Default(comment);
@@ -1073,14 +1080,13 @@ fn handle_attribute_comment<'a>(
10731080
return CommentPlacement::Default(comment);
10741081
}
10751082

1076-
let between_value_and_attr = TextRange::new(attribute.value.end(), attribute.attr.start());
1077-
1078-
let dot = SimpleTokenizer::new(locator.contents(), between_value_and_attr)
1079-
.skip_trivia()
1080-
.next()
1081-
.expect("Expected the `.` character after the value");
1082-
1083-
if TextRange::new(dot.end(), attribute.attr.start()).contains(comment.slice().start()) {
1083+
if TextRange::new(attribute.value.end(), attribute.attr.start())
1084+
.contains(comment.slice().start())
1085+
{
1086+
// ```text
1087+
// value . attr
1088+
// ^^^^^^^ the range of dangling comments
1089+
// ```
10841090
if comment.line_position().is_end_of_line() {
10851091
// Attach to node with b
10861092
// ```python

crates/ruff_python_formatter/src/expression/expr_call.rs

+71-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use crate::comments::Comments;
1+
use crate::builders::PyFormatterExtensions;
2+
use crate::comments::{dangling_comments, Comments};
23
use crate::expression::parentheses::{
34
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
45
};
5-
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
6+
use crate::{AsFormat, FormatNodeRule, PyFormatter};
7+
use ruff_formatter::prelude::{format_with, group, soft_block_indent, text};
68
use ruff_formatter::{write, Buffer, FormatResult};
79
use rustpython_parser::ast::ExprCall;
810

@@ -11,19 +13,75 @@ pub struct FormatExprCall;
1113

1214
impl FormatNodeRule<ExprCall> for FormatExprCall {
1315
fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> {
14-
if item.args.is_empty() && item.keywords.is_empty() {
15-
write!(
16-
f,
17-
[not_yet_implemented_custom_text("NOT_IMPLEMENTED_call()")]
18-
)
19-
} else {
20-
write!(
16+
let ExprCall {
17+
range: _,
18+
func,
19+
args,
20+
keywords,
21+
} = item;
22+
23+
// We have a case with `f()` without any argument, which is a special case because we can
24+
// have a comment with no node attachment inside:
25+
// ```python
26+
// f(
27+
// # This function has a dangling comment
28+
// )
29+
// ```
30+
if args.is_empty() && keywords.is_empty() {
31+
let comments = f.context().comments().clone();
32+
let comments = comments.dangling_comments(item);
33+
return write!(
2134
f,
22-
[not_yet_implemented_custom_text(
23-
"NOT_IMPLEMENTED_call(NOT_IMPLEMENTED_arg)"
24-
)]
25-
)
35+
[
36+
func.format(),
37+
text("("),
38+
dangling_comments(comments),
39+
text(")")
40+
]
41+
);
2642
}
43+
44+
let all_args = format_with(|f| {
45+
f.join_comma_separated()
46+
.entries(
47+
// We have the parentheses from the call so the arguments never need any
48+
args.iter()
49+
.map(|arg| (arg, arg.format().with_options(Parenthesize::Never))),
50+
)
51+
.nodes(keywords.iter())
52+
.finish()
53+
});
54+
55+
write!(
56+
f,
57+
[
58+
func.format(),
59+
text("("),
60+
// The outer group is for things like
61+
// ```python
62+
// get_collection(
63+
// hey_this_is_a_very_long_call,
64+
// it_has_funny_attributes_asdf_asdf,
65+
// too_long_for_the_line,
66+
// really=True,
67+
// )
68+
// ```
69+
// The inner group is for things like:
70+
// ```python
71+
// get_collection(
72+
// hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True
73+
// )
74+
// ```
75+
// TODO(konstin): Doesn't work see wrongly formatted test
76+
&group(&soft_block_indent(&group(&all_args))),
77+
text(")")
78+
]
79+
)
80+
}
81+
82+
fn fmt_dangling_comments(&self, _node: &ExprCall, _f: &mut PyFormatter) -> FormatResult<()> {
83+
// Handled in `fmt_fields`
84+
Ok(())
2785
}
2886
}
2987

crates/ruff_python_formatter/src/other/keyword.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ impl FormatNodeRule<Keyword> for FormatKeyword {
1313
arg,
1414
value,
1515
} = item;
16-
if let Some(argument) = arg {
17-
write!(f, [argument.format(), text("=")])?;
16+
if let Some(arg) = arg {
17+
write!(f, [arg.format(), text("="), value.format()])
18+
} else {
19+
// Comments after the stars are reassigned as trailing value comments
20+
write!(f, [text("**"), value.format()])
1821
}
19-
20-
value.format().fmt(f)
2122
}
2223
}

0 commit comments

Comments
 (0)