use ast::helpers::comment_indentation_after;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{
self as ast, AnyNodeRef, Comprehension, Expr, ModModule, Parameter, Parameters, StringLike,
};
use ruff_python_trivia::{
BackwardsTokenizer, CommentRanges, SimpleToken, SimpleTokenKind, SimpleTokenizer,
find_only_token_in_range, first_non_trivia_token, indentation_at_offset,
};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use std::cmp::Ordering;
use crate::comments::visitor::{CommentPlacement, DecoratedComment};
use crate::expression::expr_slice::{ExprSliceCommentSection, assign_comment_in_slice};
use crate::expression::parentheses::is_expression_parenthesized;
use crate::other::parameters::{
assign_argument_separator_comment_placement, find_parameter_separators,
};
use crate::pattern::pattern_match_sequence::SequenceType;
pub(super) fn place_comment<'a>(
comment: DecoratedComment<'a>,
comment_ranges: &CommentRanges,
source: &str,
) -> CommentPlacement<'a> {
handle_parenthesized_comment(comment, source)
.or_else(|comment| handle_end_of_line_comment_around_body(comment, source))
.or_else(|comment| handle_own_line_comment_around_body(comment, source))
.or_else(|comment| handle_enclosed_comment(comment, comment_ranges, source))
}
fn handle_parenthesized_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
if comment.enclosing_node().is_expr_f_string() {
return CommentPlacement::Default(comment);
}
let Some(preceding) = comment.preceding_node() else {
return CommentPlacement::Default(comment);
};
let Some(following) = comment.following_node() else {
return CommentPlacement::Default(comment);
};
let range = TextRange::new(preceding.end(), comment.start());
let tokenizer = SimpleTokenizer::new(source, range);
if tokenizer
.skip_trivia()
.take_while(|token| {
!matches!(
token.kind,
SimpleTokenKind::As | SimpleTokenKind::Def | SimpleTokenKind::Class
)
})
.any(|token| {
debug_assert!(
!matches!(token.kind, SimpleTokenKind::Bogus),
"Unexpected token between nodes: `{:?}`",
&source[range]
);
token.kind() == SimpleTokenKind::LParen
})
{
return CommentPlacement::leading(following, comment);
}
let range = TextRange::new(comment.end(), following.start());
let tokenizer = SimpleTokenizer::new(source, range);
if tokenizer
.skip_trivia()
.take_while(|token| {
!matches!(
token.kind,
SimpleTokenKind::As | SimpleTokenKind::Def | SimpleTokenKind::Class
)
})
.any(|token| {
debug_assert!(
!matches!(token.kind, SimpleTokenKind::Bogus),
"Unexpected token between nodes: `{:?}`",
&source[range]
);
token.kind() == SimpleTokenKind::RParen
})
{
return CommentPlacement::trailing(preceding, comment);
}
CommentPlacement::Default(comment)
}
fn handle_enclosed_comment<'a>(
comment: DecoratedComment<'a>,
comment_ranges: &CommentRanges,
source: &str,
) -> CommentPlacement<'a> {
match comment.enclosing_node() {
AnyNodeRef::Parameters(parameters) => {
handle_parameters_separator_comment(comment, parameters, source).or_else(|comment| {
if are_parameters_parenthesized(parameters, source) {
handle_bracketed_end_of_line_comment(comment, source)
} else {
CommentPlacement::Default(comment)
}
})
}
AnyNodeRef::Parameter(parameter) => handle_parameter_comment(comment, parameter, source),
AnyNodeRef::Arguments(_) | AnyNodeRef::TypeParams(_) | AnyNodeRef::PatternArguments(_) => {
handle_bracketed_end_of_line_comment(comment, source)
}
AnyNodeRef::Comprehension(comprehension) => {
handle_comprehension_comment(comment, comprehension, source)
}
AnyNodeRef::ExprAttribute(attribute) => {
handle_attribute_comment(comment, attribute, source)
}
AnyNodeRef::ExprBinOp(binary_expression) => {
handle_trailing_binary_expression_left_or_operator_comment(
comment,
binary_expression,
source,
)
}
AnyNodeRef::ExprBoolOp(_) | AnyNodeRef::ExprCompare(_) => {
handle_trailing_binary_like_comment(comment, source)
}
AnyNodeRef::Keyword(keyword) => handle_keyword_comment(comment, keyword, source),
AnyNodeRef::PatternKeyword(pattern_keyword) => {
handle_pattern_keyword_comment(comment, pattern_keyword, source)
}
AnyNodeRef::ExprUnaryOp(unary_op) => handle_unary_op_comment(comment, unary_op, source),
AnyNodeRef::ExprNamed(_) => handle_named_expr_comment(comment, source),
AnyNodeRef::ExprLambda(lambda) => handle_lambda_comment(comment, lambda, source),
AnyNodeRef::ExprDict(_) => handle_dict_unpacking_comment(comment, source)
.or_else(|comment| handle_bracketed_end_of_line_comment(comment, source))
.or_else(|comment| handle_key_value_comment(comment, source)),
AnyNodeRef::ExprDictComp(_) => handle_dict_unpacking_comment(comment, source)
.or_else(|comment| handle_key_value_comment(comment, source))
.or_else(|comment| handle_bracketed_end_of_line_comment(comment, source)),
AnyNodeRef::ExprIf(expr_if) => handle_expr_if_comment(comment, expr_if, source),
AnyNodeRef::ExprSlice(expr_slice) => {
handle_slice_comments(comment, expr_slice, comment_ranges, source)
}
AnyNodeRef::ExprStarred(starred) => {
handle_trailing_expression_starred_star_end_of_line_comment(comment, starred, source)
}
AnyNodeRef::ExprSubscript(expr_subscript) => {
if let Expr::Slice(expr_slice) = expr_subscript.slice.as_ref() {
return handle_slice_comments(comment, expr_slice, comment_ranges, source);
}
if comment.line_position().is_end_of_line()
&& expr_subscript.value.end() < comment.start()
{
let mut lexer = SimpleTokenizer::new(
source,
TextRange::new(expr_subscript.value.end(), comment.start()),
)
.skip_trivia();
if !lexer
.by_ref()
.any(|token| token.kind() == SimpleTokenKind::LBracket)
{
return CommentPlacement::Default(comment);
}
if lexer.next().is_none() {
return CommentPlacement::dangling(expr_subscript, comment);
}
}
CommentPlacement::Default(comment)
}
AnyNodeRef::ModModule(module) => {
handle_trailing_module_comment(module, comment).or_else(|comment| {
handle_module_level_own_line_comment_before_class_or_function_comment(
comment, source,
)
})
}
AnyNodeRef::WithItem(_) => handle_with_item_comment(comment, source),
AnyNodeRef::PatternMatchSequence(pattern_match_sequence) => {
if SequenceType::from_pattern(pattern_match_sequence, source).is_parenthesized() {
handle_bracketed_end_of_line_comment(comment, source)
} else {
CommentPlacement::Default(comment)
}
}
AnyNodeRef::PatternMatchClass(class) => handle_pattern_match_class_comment(comment, class),
AnyNodeRef::PatternMatchAs(_) => handle_pattern_match_as_comment(comment, source),
AnyNodeRef::PatternMatchStar(_) => handle_pattern_match_star_comment(comment),
AnyNodeRef::PatternMatchMapping(pattern) => {
handle_bracketed_end_of_line_comment(comment, source)
.or_else(|comment| handle_pattern_match_mapping_comment(comment, pattern, source))
}
AnyNodeRef::StmtFunctionDef(_) => handle_leading_function_with_decorators_comment(comment),
AnyNodeRef::StmtClassDef(class_def) => {
handle_leading_class_with_decorators_comment(comment, class_def)
}
AnyNodeRef::StmtImportFrom(import_from) => {
handle_import_from_comment(comment, import_from, source)
}
AnyNodeRef::Alias(alias) => handle_alias_comment(comment, alias, source),
AnyNodeRef::StmtWith(with_) => handle_with_comment(comment, with_),
AnyNodeRef::ExprCall(_) => handle_call_comment(comment),
AnyNodeRef::ExprStringLiteral(_) => match comment.enclosing_parent() {
Some(AnyNodeRef::FString(fstring)) => CommentPlacement::dangling(fstring, comment),
Some(AnyNodeRef::TString(tstring)) => CommentPlacement::dangling(tstring, comment),
_ => CommentPlacement::Default(comment),
},
AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment),
AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment),
AnyNodeRef::InterpolatedElement(element) => {
if let Some(preceding) = comment.preceding_node() {
if comment.line_position().is_own_line()
&& element.format_spec.is_some()
&& comment.following_node().is_some()
{
return CommentPlacement::trailing(preceding, comment);
}
}
handle_bracketed_end_of_line_comment(comment, source)
}
AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprSet(_)
| AnyNodeRef::ExprListComp(_)
| AnyNodeRef::ExprSetComp(_) => handle_bracketed_end_of_line_comment(comment, source),
AnyNodeRef::ExprTuple(ast::ExprTuple {
parenthesized: true,
..
}) => handle_bracketed_end_of_line_comment(comment, source),
AnyNodeRef::ExprGenerator(generator) if generator.parenthesized => {
handle_bracketed_end_of_line_comment(comment, source)
}
AnyNodeRef::StmtReturn(_) => {
handle_trailing_implicit_concatenated_string_comment(comment, comment_ranges, source)
}
AnyNodeRef::StmtAssign(assignment)
if comment.preceding_node().is_some_and(|preceding| {
preceding.ptr_eq(AnyNodeRef::from(&*assignment.value))
}) =>
{
handle_trailing_implicit_concatenated_string_comment(comment, comment_ranges, source)
}
AnyNodeRef::StmtAnnAssign(assignment)
if comment.preceding_node().is_some_and(|preceding| {
assignment
.value
.as_deref()
.is_some_and(|value| preceding.ptr_eq(value.into()))
}) =>
{
handle_trailing_implicit_concatenated_string_comment(comment, comment_ranges, source)
}
AnyNodeRef::StmtAugAssign(assignment)
if comment.preceding_node().is_some_and(|preceding| {
preceding.ptr_eq(AnyNodeRef::from(&*assignment.value))
}) =>
{
handle_trailing_implicit_concatenated_string_comment(comment, comment_ranges, source)
}
AnyNodeRef::StmtTypeAlias(assignment)
if comment.preceding_node().is_some_and(|preceding| {
preceding.ptr_eq(AnyNodeRef::from(&*assignment.value))
}) =>
{
handle_trailing_implicit_concatenated_string_comment(comment, comment_ranges, source)
}
_ => CommentPlacement::Default(comment),
}
}
fn handle_end_of_line_comment_around_body<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
if comment.line_position().is_own_line() {
return CommentPlacement::Default(comment);
}
if let Some(following) = comment.following_node() {
if following.is_first_statement_in_body(comment.enclosing_node())
&& SimpleTokenizer::new(source, TextRange::new(comment.end(), following.start()))
.skip_trivia()
.next()
.is_none()
{
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}
if let Some(preceding) = comment.preceding_node() {
if let Some(last_child) = preceding.last_child_in_body() {
let innermost_child =
std::iter::successors(Some(last_child), AnyNodeRef::last_child_in_body)
.last()
.unwrap_or(last_child);
return CommentPlacement::trailing(innermost_child, comment);
}
}
CommentPlacement::Default(comment)
}
fn handle_own_line_comment_around_body<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
if comment.line_position().is_end_of_line() {
return CommentPlacement::Default(comment);
}
let Some(preceding) = comment.preceding_node() else {
return CommentPlacement::Default(comment);
};
let maybe_token =
SimpleTokenizer::new(source, TextRange::new(preceding.end(), comment.start()))
.skip_trivia()
.next();
if maybe_token.is_some() {
return CommentPlacement::Default(comment);
}
handle_own_line_comment_between_branches(comment, preceding, source)
.or_else(|comment| {
handle_own_line_comment_after_branch(comment, preceding, source)
})
.or_else(|comment| handle_own_line_comment_between_statements(comment, source))
}
fn handle_own_line_comment_between_statements<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
let Some(preceding) = comment.preceding_node() else {
return CommentPlacement::Default(comment);
};
let Some(following) = comment.following_node() else {
return CommentPlacement::Default(comment);
};
if !preceding.is_statement() || !following.is_statement() {
return CommentPlacement::Default(comment);
}
if comment.line_position().is_end_of_line() {
return CommentPlacement::Default(comment);
}
if max_empty_lines(&source[TextRange::new(comment.end(), following.start())]) == 0 {
CommentPlacement::leading(following, comment)
} else {
CommentPlacement::trailing(preceding, comment)
}
}
fn handle_own_line_comment_between_branches<'a>(
comment: DecoratedComment<'a>,
preceding: AnyNodeRef<'a>,
source: &str,
) -> CommentPlacement<'a> {
let Some(following) = comment.following_node() else {
return CommentPlacement::Default(comment);
};
if !following.is_first_statement_in_alternate_body(comment.enclosing_node()) {
return CommentPlacement::Default(comment);
}
let comment_indentation = comment_indentation_after(preceding, comment.range(), source);
let preceding_indentation = indentation(source, &preceding).map_or_else(
|| {
comment_indentation
+ TextSize::new(1)
},
ruff_text_size::TextLen::text_len,
);
match comment_indentation.cmp(&preceding_indentation) {
Ordering::Greater => {
CommentPlacement::Default(comment)
}
Ordering::Equal => {
if preceding.is_alternative_branch_with_node() {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::trailing(preceding, comment)
}
}
Ordering::Less => {
if following.is_alternative_branch_with_node() {
CommentPlacement::leading(following, comment)
} else {
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
}
}
}
fn handle_own_line_comment_after_branch<'a>(
comment: DecoratedComment<'a>,
preceding: AnyNodeRef<'a>,
source: &str,
) -> CommentPlacement<'a> {
let last_child = match preceding.last_child_in_body() {
Some(last) => last,
None if comment.following_node().is_some_and(|following| {
following.is_first_statement_in_alternate_body(comment.enclosing_node())
}) =>
{
preceding
}
_ => {
return CommentPlacement::Default(comment);
}
};
let comment_indentation = comment_indentation_after(preceding, comment.range(), source);
let preceding_indentation = indentation_at_offset(preceding.start(), source)
.unwrap_or_default()
.text_len();
if comment_indentation == preceding_indentation {
return CommentPlacement::Default(comment);
}
let mut parent = None;
let mut last_child_in_parent = last_child;
loop {
let child_indentation = indentation(source, &last_child_in_parent)
.unwrap_or_default()
.text_len();
match comment_indentation.cmp(&child_indentation) {
Ordering::Less => {
return if let Some(parent_block) = parent {
CommentPlacement::trailing(parent_block, comment)
} else {
CommentPlacement::Default(comment)
};
}
Ordering::Equal => {
return CommentPlacement::trailing(last_child_in_parent, comment);
}
Ordering::Greater => {
if let Some(nested_child) = last_child_in_parent.last_child_in_body() {
parent = Some(last_child_in_parent);
last_child_in_parent = nested_child;
} else {
return CommentPlacement::trailing(last_child_in_parent, comment);
}
}
}
}
}
fn handle_parameters_separator_comment<'a>(
comment: DecoratedComment<'a>,
parameters: &Parameters,
source: &str,
) -> CommentPlacement<'a> {
let (slash, star) = find_parameter_separators(source, parameters);
let placement = assign_argument_separator_comment_placement(
slash.as_ref(),
star.as_ref(),
comment.range(),
comment.line_position(),
);
if placement.is_some() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
CommentPlacement::Default(comment)
}
fn handle_parameter_comment<'a>(
comment: DecoratedComment<'a>,
parameter: &'a Parameter,
source: &str,
) -> CommentPlacement<'a> {
if parameter.annotation().is_some() {
let colon = first_non_trivia_token(parameter.name.end(), source).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() {
CommentPlacement::leading(parameter, comment)
} else {
CommentPlacement::Default(comment)
}
} else if comment.start() < parameter.name.start() {
if let Some(AnyNodeRef::Parameters(parameters)) = comment.enclosing_parent()
&& parameters.start() == parameter.start()
{
CommentPlacement::leading(parameters, comment)
} else {
CommentPlacement::leading(parameter, comment)
}
} else {
CommentPlacement::Default(comment)
}
}
fn handle_trailing_binary_expression_left_or_operator_comment<'a>(
comment: DecoratedComment<'a>,
binary_expression: &'a ast::ExprBinOp,
source: &str,
) -> CommentPlacement<'a> {
if comment.preceding_node().is_none() || comment.following_node().is_none() {
return CommentPlacement::Default(comment);
}
let between_operands_range = TextRange::new(
binary_expression.left.end(),
binary_expression.right.start(),
);
let mut tokens = SimpleTokenizer::new(source, between_operands_range)
.skip_trivia()
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
let operator_offset = tokens
.next()
.expect("Expected a token for the operator")
.start();
if comment.end() < operator_offset {
CommentPlacement::trailing(binary_expression.left.as_ref(), comment)
} else if comment.line_position().is_end_of_line() {
if source.contains_line_break(TextRange::new(
binary_expression.left.end(),
operator_offset,
)) && source.contains_line_break(TextRange::new(
operator_offset,
binary_expression.right.start(),
)) {
CommentPlacement::dangling(binary_expression, comment)
} else {
CommentPlacement::Default(comment)
}
} else {
CommentPlacement::Default(comment)
}
}
fn handle_trailing_binary_like_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(
comment.enclosing_node().is_expr_bool_op() || comment.enclosing_node().is_expr_compare()
);
let (Some(left_operand), Some(right_operand)) =
(comment.preceding_node(), comment.following_node())
else {
return CommentPlacement::Default(comment);
};
let between_operands_range = TextRange::new(left_operand.end(), right_operand.start());
let mut tokens = SimpleTokenizer::new(source, between_operands_range)
.skip_trivia()
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
let operator_offset = tokens
.next()
.expect("Expected a token for the operator")
.start();
if comment.end() < operator_offset {
CommentPlacement::trailing(left_operand, comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_trailing_module_comment<'a>(
module: &'a ModModule,
comment: DecoratedComment<'a>,
) -> CommentPlacement<'a> {
if comment.preceding_node().is_none() && comment.following_node().is_none() {
if let Some(last_statement) = module.body.last() {
CommentPlacement::trailing(last_statement, comment)
} else {
CommentPlacement::leading(comment.enclosing_node(), comment)
}
} else {
CommentPlacement::Default(comment)
}
}
fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(comment.enclosing_node().is_module());
if comment.line_position().is_end_of_line() {
return CommentPlacement::Default(comment);
}
let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node())
else {
return CommentPlacement::Default(comment);
};
if !matches!(
following,
AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::StmtClassDef(_)
) {
return CommentPlacement::Default(comment);
}
if max_empty_lines(&source[TextRange::new(comment.end(), following.start())]) == 0 {
CommentPlacement::leading(following, comment)
} else {
CommentPlacement::trailing(preceding, comment)
}
}
fn handle_slice_comments<'a>(
comment: DecoratedComment<'a>,
expr_slice: &'a ast::ExprSlice,
comment_ranges: &CommentRanges,
source: &str,
) -> CommentPlacement<'a> {
let ast::ExprSlice {
range: _,
node_index: _,
lower,
upper,
step,
} = expr_slice;
let after_lbracket = matches!(
BackwardsTokenizer::up_to(comment.start(), source, comment_ranges)
.skip_trivia()
.next(),
Some(SimpleToken {
kind: SimpleTokenKind::LBracket,
..
})
);
if comment.line_position().is_end_of_line() && after_lbracket {
debug_assert!(
matches!(comment.enclosing_node(), AnyNodeRef::ExprSubscript(_)),
"{:?}",
comment.enclosing_node()
);
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
let assignment = assign_comment_in_slice(comment.range(), source, expr_slice);
let node = match assignment {
ExprSliceCommentSection::Lower => lower,
ExprSliceCommentSection::Upper => upper,
ExprSliceCommentSection::Step => step,
};
if let Some(node) = node {
if comment.start() < node.start() {
CommentPlacement::leading(node.as_ref(), comment)
} else {
CommentPlacement::trailing(node.as_ref(), comment)
}
} else {
CommentPlacement::dangling(expr_slice, comment)
}
}
fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) -> CommentPlacement {
let is_preceding_decorator = comment
.preceding_node()
.is_some_and(|node| node.is_decorator());
let is_following_parameters = comment
.following_node()
.is_some_and(|node| node.is_parameters() || node.is_type_params());
if comment.line_position().is_own_line() && is_preceding_decorator && is_following_parameters {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_leading_class_with_decorators_comment<'a>(
comment: DecoratedComment<'a>,
class_def: &'a ast::StmtClassDef,
) -> CommentPlacement<'a> {
if comment.line_position().is_own_line() && comment.start() < class_def.name.start() {
if let Some(decorator) = class_def.decorator_list.last() {
if decorator.end() < comment.start() {
return CommentPlacement::dangling(class_def, comment);
}
}
}
CommentPlacement::Default(comment)
}
fn handle_keyword_comment<'a>(
comment: DecoratedComment<'a>,
keyword: &'a ast::Keyword,
source: &str,
) -> CommentPlacement<'a> {
let start = keyword.arg.as_ref().map_or(keyword.start(), Ranged::end);
let mut tokenizer = SimpleTokenizer::new(source, TextRange::new(start, comment.start()));
if tokenizer.any(|token| token.kind == SimpleTokenKind::LParen) {
return CommentPlacement::Default(comment);
}
CommentPlacement::leading(comment.enclosing_node(), comment)
}
fn handle_pattern_keyword_comment<'a>(
comment: DecoratedComment<'a>,
pattern_keyword: &'a ast::PatternKeyword,
source: &str,
) -> CommentPlacement<'a> {
let mut tokenizer = SimpleTokenizer::new(
source,
TextRange::new(pattern_keyword.attr.end(), comment.start()),
);
if tokenizer.any(|token| token.kind == SimpleTokenKind::LParen) {
return CommentPlacement::Default(comment);
}
CommentPlacement::leading(comment.enclosing_node(), comment)
}
fn handle_dict_unpacking_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(matches!(
comment.enclosing_node(),
AnyNodeRef::ExprDict(_) | AnyNodeRef::ExprDictComp(_)
));
let Some(following) = comment.following_node() else {
return CommentPlacement::Default(comment);
};
let preceding_end = match comment.preceding_node() {
Some(preceding) => preceding.end(),
None => comment.enclosing_node().start(),
};
let mut tokens = SimpleTokenizer::new(source, TextRange::new(preceding_end, comment.start()))
.skip_trivia()
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
if tokens.any(|token| token.kind == SimpleTokenKind::DoubleStar) {
CommentPlacement::leading(following, comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_key_value_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(matches!(
comment.enclosing_node(),
AnyNodeRef::ExprDict(_) | AnyNodeRef::ExprDictComp(_)
));
let (Some(following), Some(preceding)) = (comment.following_node(), comment.preceding_node())
else {
return CommentPlacement::Default(comment);
};
let tokens = SimpleTokenizer::new(source, TextRange::new(preceding.end(), following.start()));
if tokens
.skip_trivia()
.any(|token| token.kind == SimpleTokenKind::Colon)
{
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_call_comment(comment: DecoratedComment) -> CommentPlacement {
if comment.line_position().is_own_line() {
if comment.preceding_node().is_some_and(|preceding| {
comment.following_node().is_some_and(|following| {
preceding.end() < comment.start() && comment.end() < following.start()
})
}) {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}
CommentPlacement::Default(comment)
}
fn handle_attribute_comment<'a>(
comment: DecoratedComment<'a>,
attribute: &'a ast::ExprAttribute,
source: &str,
) -> CommentPlacement<'a> {
if comment.preceding_node().is_none() {
return CommentPlacement::leading(attribute.value.as_ref(), comment);
}
if let Some(right_paren) = SimpleTokenizer::starts_at(attribute.value.end(), source)
.skip_trivia()
.take_while(|token| token.kind == SimpleTokenKind::RParen)
.last()
{
if comment.start() < right_paren.start() {
return CommentPlacement::trailing(attribute.value.as_ref(), comment);
}
}
if comment.line_position().is_end_of_line() {
let dot_token = find_only_token_in_range(
TextRange::new(attribute.value.end(), attribute.attr.start()),
SimpleTokenKind::Dot,
source,
);
if comment.end() < dot_token.start() {
return CommentPlacement::trailing(attribute.value.as_ref(), comment);
}
}
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
fn handle_expr_if_comment<'a>(
comment: DecoratedComment<'a>,
expr_if: &'a ast::ExprIf,
source: &str,
) -> CommentPlacement<'a> {
let ast::ExprIf {
range: _,
node_index: _,
test,
body,
orelse,
} = expr_if;
if comment.line_position().is_own_line() {
return CommentPlacement::Default(comment);
}
let if_token = find_only_token_in_range(
TextRange::new(body.end(), test.start()),
SimpleTokenKind::If,
source,
);
if if_token.start() < comment.start() && comment.start() < test.start() {
return CommentPlacement::leading(test.as_ref(), comment);
}
let else_token = find_only_token_in_range(
TextRange::new(test.end(), orelse.start()),
SimpleTokenKind::Else,
source,
);
if else_token.start() < comment.start() && comment.start() < orelse.start() {
return CommentPlacement::leading(orelse.as_ref(), comment);
}
CommentPlacement::Default(comment)
}
fn handle_trailing_expression_starred_star_end_of_line_comment<'a>(
comment: DecoratedComment<'a>,
starred: &'a ast::ExprStarred,
source: &str,
) -> CommentPlacement<'a> {
if comment.following_node().is_some() {
let tokenizer =
SimpleTokenizer::new(source, TextRange::new(starred.start(), comment.start()));
if !tokenizer
.skip_trivia()
.any(|token| token.kind() == SimpleTokenKind::LParen)
{
return CommentPlacement::leading(starred, comment);
}
}
CommentPlacement::Default(comment)
}
fn handle_with_item_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(comment.enclosing_node().is_with_item());
let (Some(context_expr), Some(optional_vars)) =
(comment.preceding_node(), comment.following_node())
else {
return CommentPlacement::Default(comment);
};
let as_token = find_only_token_in_range(
TextRange::new(context_expr.end(), optional_vars.start()),
SimpleTokenKind::As,
source,
);
if comment.end() < as_token.start() {
CommentPlacement::trailing(context_expr, comment)
} else if comment.line_position().is_end_of_line() {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::leading(optional_vars, comment)
}
}
fn handle_pattern_match_class_comment<'a>(
comment: DecoratedComment<'a>,
class: &'a ast::PatternMatchClass,
) -> CommentPlacement<'a> {
if class.cls.end() < comment.start() && comment.end() < class.arguments.start() {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_pattern_match_as_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(comment.enclosing_node().is_pattern_match_as());
let Some(pattern) = comment.preceding_node() else {
return CommentPlacement::Default(comment);
};
let mut tokens = SimpleTokenizer::starts_at(pattern.end(), source)
.skip_trivia()
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
let Some(as_token) = tokens
.next()
.filter(|token| token.kind == SimpleTokenKind::As)
else {
return CommentPlacement::Default(comment);
};
if comment.end() < as_token.start() {
CommentPlacement::trailing(pattern, comment)
} else {
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
}
fn handle_pattern_match_star_comment(comment: DecoratedComment) -> CommentPlacement {
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
fn handle_pattern_match_mapping_comment<'a>(
comment: DecoratedComment<'a>,
pattern: &'a ast::PatternMatchMapping,
source: &str,
) -> CommentPlacement<'a> {
if comment.following_node().is_some() {
return CommentPlacement::Default(comment);
}
let Some(rest) = pattern.rest.as_ref() else {
return CommentPlacement::Default(comment);
};
if comment.start() > rest.end() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
let preceding_end = match comment.preceding_node() {
Some(preceding) => preceding.end(),
None => comment.enclosing_node().start(),
};
let mut tokens =
SimpleTokenizer::new(source, TextRange::new(preceding_end, comment.start())).skip_trivia();
if tokens.any(|token| token.kind == SimpleTokenKind::DoubleStar) {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_named_expr_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
debug_assert!(comment.enclosing_node().is_expr_named());
let (Some(target), Some(value)) = (comment.preceding_node(), comment.following_node()) else {
return CommentPlacement::Default(comment);
};
let colon_equal = find_only_token_in_range(
TextRange::new(target.end(), value.start()),
SimpleTokenKind::ColonEqual,
source,
);
if comment.end() < colon_equal.start() {
CommentPlacement::trailing(target, comment)
} else {
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
}
fn handle_lambda_comment<'a>(
comment: DecoratedComment<'a>,
lambda: &'a ast::ExprLambda,
source: &str,
) -> CommentPlacement<'a> {
if let Some(parameters) = lambda.parameters.as_deref() {
if comment.start() < parameters.start() {
return if comment.line_position().is_own_line() {
CommentPlacement::leading(parameters, comment)
} else {
CommentPlacement::dangling(comment.enclosing_node(), comment)
};
}
if parameters.end() < comment.start() && comment.start() < lambda.body.start() {
let tokenizer =
SimpleTokenizer::new(source, TextRange::new(parameters.end(), comment.start()));
if tokenizer
.skip_trivia()
.any(|token| token.kind == SimpleTokenKind::LParen)
{
return CommentPlacement::Default(comment);
}
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
} else {
if comment.start() < lambda.body.start() {
let tokenizer =
SimpleTokenizer::new(source, TextRange::new(lambda.start(), comment.start()));
if tokenizer
.skip_trivia()
.any(|token| token.kind == SimpleTokenKind::LParen)
{
return CommentPlacement::Default(comment);
}
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}
CommentPlacement::Default(comment)
}
fn handle_unary_op_comment<'a>(
comment: DecoratedComment<'a>,
unary_op: &'a ast::ExprUnaryOp,
source: &str,
) -> CommentPlacement<'a> {
let mut tokenizer = SimpleTokenizer::new(
source,
TextRange::new(unary_op.start(), unary_op.operand.start()),
)
.skip_trivia();
let op_token = tokenizer.next();
debug_assert!(op_token.is_some_and(|token| matches!(
token.kind,
SimpleTokenKind::Tilde
| SimpleTokenKind::Not
| SimpleTokenKind::Plus
| SimpleTokenKind::Minus
)));
let up_to = tokenizer
.find(|token| token.kind == SimpleTokenKind::LParen)
.map_or(unary_op.operand.start(), |lparen| lparen.start());
if comment.end() < up_to && comment.line_position().is_end_of_line() {
CommentPlacement::dangling(unary_op, comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_bracketed_end_of_line_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,
) -> CommentPlacement<'a> {
if comment.line_position().is_end_of_line() {
let mut lexer = SimpleTokenizer::new(
source,
TextRange::new(comment.enclosing_node().start(), comment.start()),
)
.skip_trivia();
let Some(paren) = lexer.next() else {
return CommentPlacement::Default(comment);
};
debug_assert!(matches!(
paren.kind(),
SimpleTokenKind::LParen | SimpleTokenKind::LBrace | SimpleTokenKind::LBracket
));
if lexer.next().is_none() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}
CommentPlacement::Default(comment)
}
fn handle_import_from_comment<'a>(
comment: DecoratedComment<'a>,
import_from: &'a ast::StmtImportFrom,
source: &str,
) -> CommentPlacement<'a> {
if comment.line_position().is_end_of_line()
&& import_from.names.first().is_some_and(|first_name| {
import_from.start() < comment.start() && comment.start() < first_name.start()
})
{
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
if let Some(SimpleToken {
kind: SimpleTokenKind::Comma,
..
}) = SimpleTokenizer::starts_at(comment.start(), source)
.skip_trivia()
.next()
{
if let Some(AnyNodeRef::Alias(alias)) = comment.preceding_node() {
CommentPlacement::dangling(alias, comment)
} else {
CommentPlacement::Default(comment)
}
} else {
CommentPlacement::Default(comment)
}
}
}
fn handle_alias_comment<'a>(
comment: DecoratedComment<'a>,
alias: &'a ruff_python_ast::Alias,
source: &str,
) -> CommentPlacement<'a> {
if let Some(asname) = &alias.asname {
if let Some(SimpleToken {
kind: SimpleTokenKind::As,
range: as_range,
}) = SimpleTokenizer::starts_at(alias.name.end(), source)
.skip_trivia()
.next()
{
return if comment.start() < as_range.start() {
CommentPlacement::trailing(&alias.name, comment)
} else {
CommentPlacement::leading(asname, comment)
};
}
}
CommentPlacement::Default(comment)
}
fn handle_with_comment<'a>(
comment: DecoratedComment<'a>,
with_statement: &'a ast::StmtWith,
) -> CommentPlacement<'a> {
if comment.line_position().is_end_of_line()
&& with_statement.items.first().is_some_and(|with_item| {
with_statement.start() < comment.start() && comment.start() < with_item.start()
})
{
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
fn handle_comprehension_comment<'a>(
comment: DecoratedComment<'a>,
comprehension: &'a Comprehension,
source: &str,
) -> CommentPlacement<'a> {
let is_own_line = comment.line_position().is_own_line();
if comment.end() < comprehension.target.start() {
return if is_own_line {
CommentPlacement::Default(comment)
} else {
CommentPlacement::dangling(comprehension, comment)
};
}
let in_token = find_only_token_in_range(
TextRange::new(comprehension.target.end(), comprehension.iter.start()),
SimpleTokenKind::In,
source,
);
if comment.start() < in_token.start() {
return if is_own_line {
CommentPlacement::dangling(comprehension, comment)
} else {
CommentPlacement::Default(comment)
};
}
if comment.start() < comprehension.iter.start() {
return if is_own_line {
CommentPlacement::Default(comment)
} else {
CommentPlacement::dangling(comprehension, comment)
};
}
let mut last_end = comprehension.iter.end();
for if_node in &comprehension.ifs {
let if_token = find_only_token_in_range(
TextRange::new(last_end, if_node.start()),
SimpleTokenKind::If,
source,
);
if is_own_line {
if last_end < comment.start() && comment.start() < if_token.start() {
return CommentPlacement::dangling(comprehension, comment);
}
} else if if_token.start() < comment.start() && comment.start() < if_node.start() {
return CommentPlacement::dangling(comprehension, comment);
}
last_end = if_node.end();
}
CommentPlacement::Default(comment)
}
fn handle_trailing_implicit_concatenated_string_comment<'a>(
comment: DecoratedComment<'a>,
comment_ranges: &CommentRanges,
source: &str,
) -> CommentPlacement<'a> {
if !comment.line_position().is_end_of_line() {
return CommentPlacement::Default(comment);
}
let Some(string_like) = comment
.preceding_node()
.and_then(|preceding| StringLike::try_from(preceding).ok())
else {
return CommentPlacement::Default(comment);
};
let mut parts = string_like.parts();
let (Some(last), Some(second_last)) = (parts.next_back(), parts.next_back()) else {
return CommentPlacement::Default(comment);
};
if source.contains_line_break(TextRange::new(second_last.end(), last.start()))
&& is_expression_parenthesized(string_like.as_expression_ref(), comment_ranges, source)
{
let range = TextRange::new(last.end(), comment.start());
if !SimpleTokenizer::new(source, range)
.skip_trivia()
.any(|token| token.kind() == SimpleTokenKind::RParen)
{
return CommentPlacement::trailing(AnyNodeRef::from(last), comment);
}
}
CommentPlacement::Default(comment)
}
fn are_parameters_parenthesized(parameters: &Parameters, contents: &str) -> bool {
contents[parameters.range()].starts_with('(')
}
fn max_empty_lines(contents: &str) -> u32 {
let mut newlines = 0u32;
let mut max_new_lines = 0;
for token in SimpleTokenizer::new(contents, TextRange::up_to(contents.text_len())) {
match token.kind() {
SimpleTokenKind::Newline => {
newlines += 1;
}
SimpleTokenKind::Whitespace => {}
SimpleTokenKind::Comment => {
max_new_lines = newlines.max(max_new_lines);
newlines = 0;
}
_ => {
max_new_lines = newlines.max(max_new_lines);
break;
}
}
}
max_new_lines = newlines.max(max_new_lines);
max_new_lines.saturating_sub(1)
}
#[cfg(test)]
mod tests {
use crate::comments::placement::max_empty_lines;
#[test]
fn count_empty_lines_in_trivia() {
assert_eq!(max_empty_lines(""), 0);
assert_eq!(max_empty_lines("# trailing comment\n # other comment\n"), 0);
assert_eq!(
max_empty_lines("# trailing comment\n# own line comment\n"),
0
);
assert_eq!(
max_empty_lines("# trailing comment\n\n# own line comment\n"),
1
);
assert_eq!(
max_empty_lines(
"# trailing comment\n\n# own line comment\n\n# an other own line comment"
),
1
);
assert_eq!(
max_empty_lines(
"# trailing comment\n\n# own line comment\n\n# an other own line comment\n# block"
),
1
);
assert_eq!(
max_empty_lines(
"# trailing comment\n\n# own line comment\n\n\n# an other own line comment\n# block"
),
2
);
assert_eq!(
max_empty_lines(
r"# This multiline comments section
# should be split from the statement
# above by two lines.
"
),
0
);
}
}