use ruff_formatter::{FormatContext, write};
use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, StringFlags, StringLike};
use ruff_python_trivia::{PythonWhitespace, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::expression::expr_generator::GeneratorExpParentheses;
use crate::expression::is_expression_huggable;
use crate::expression::parentheses::{Parentheses, empty_parenthesized, parenthesized};
use crate::other::commas;
use crate::prelude::*;
use crate::string::StringLikeExtensions;
#[derive(Default)]
pub struct FormatArguments;
impl FormatNodeRule<Arguments> for FormatArguments {
fn fmt_fields(&self, item: &Arguments, f: &mut PyFormatter) -> FormatResult<()> {
let Arguments {
range,
args,
keywords,
node_index: _,
} = item;
if args.is_empty() && keywords.is_empty() {
let comments = f.context().comments().clone();
let dangling = comments.dangling(item);
return write!(f, [empty_parenthesized("(", dangling, ")")]);
}
let all_arguments = format_with(|f: &mut PyFormatter| {
let source = f.context().source();
let mut joiner = f.join_comma_separated(range.end());
match args.as_ref() {
[arg] if keywords.is_empty() => {
match arg {
Expr::Generator(generator_exp) => joiner.entry(
generator_exp,
&generator_exp
.format()
.with_options(GeneratorExpParentheses::Preserve),
),
other => {
let parentheses =
if is_single_argument_parenthesized(arg, range.end(), source) {
Parentheses::Always
} else {
Parentheses::Never
};
joiner.entry(other, &other.format().with_options(parentheses))
}
};
}
_ => {
for arg_or_keyword in item.iter_source_order() {
match arg_or_keyword {
ArgOrKeyword::Arg(arg) => {
joiner.entry(arg, &arg.format());
}
ArgOrKeyword::Keyword(keyword) => {
joiner.entry(keyword, &keyword.format());
}
}
}
}
}
joiner.finish()
});
let comments = f.context().comments().clone();
let dangling_comments = comments.dangling(item);
write!(
f,
[
parenthesized("(", &group(&all_arguments), ")")
.with_hugging(is_arguments_huggable(item, f.context()))
.with_dangling_comments(dangling_comments)
]
)
}
}
fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: &str) -> bool {
let mut has_seen_r_paren = false;
for token in
SimpleTokenizer::new(source, TextRange::new(argument.end(), call_end)).skip_trivia()
{
match token.kind() {
SimpleTokenKind::RParen => {
if has_seen_r_paren {
return true;
}
has_seen_r_paren = true;
}
SimpleTokenKind::Comma => continue,
_ => {
break;
}
}
}
false
}
fn is_arguments_huggable(arguments: &Arguments, context: &PyFormatContext) -> bool {
let arg = match (arguments.args.as_ref(), arguments.keywords.as_ref()) {
([arg], []) => arg,
([], [keyword]) if keyword.arg.is_none() && !context.comments().has(keyword) => {
&keyword.value
}
_ => return false,
};
if !(is_expression_huggable(arg, context)
|| StringLike::try_from(arg)
.is_ok_and(|string| is_huggable_string_argument(string, arguments, context)))
{
return false;
}
let comments = context.comments().leading_dangling_trailing(arg);
if comments.has_leading() || comments.has_trailing() {
return false;
}
let options = context.options();
if options.magic_trailing_comma().is_respect()
&& commas::has_magic_trailing_comma(TextRange::new(arg.end(), arguments.end()), context)
{
return false;
}
true
}
fn is_huggable_string_argument(
string: StringLike,
arguments: &Arguments,
context: &PyFormatContext,
) -> bool {
if string.is_implicit_concatenated()
|| !string.is_multiline(context)
|| !string
.parts()
.next()
.is_some_and(|part| part.flags().is_triple_quoted())
{
return false;
}
let between_parens_range = TextRange::new(arguments.start() + '('.text_len(), string.start());
let between_parens = &context.source()[between_parens_range];
!between_parens.trim_whitespace_end().ends_with(['\n', '\r'])
}