Merge branch 'main' into dcreager/breakest-the-cycle

This commit is contained in:
Alex Waygood 2025-12-11 18:14:27 +00:00
commit 3651068fce
150 changed files with 3080 additions and 3598 deletions

View File

@ -298,7 +298,7 @@ jobs:
# sync, not just public items. Eventually we should do this for all # sync, not just public items. Eventually we should do this for all
# crates; for now add crates here as they are warning-clean to prevent # crates; for now add crates here as they are warning-clean to prevent
# regression. # regression.
- run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db -p ruff_python_formatter --document-private-items
env: env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings"

1
Cargo.lock generated
View File

@ -3349,6 +3349,7 @@ dependencies = [
"compact_str", "compact_str",
"get-size2", "get-size2",
"insta", "insta",
"itertools 0.14.0",
"memchr", "memchr",
"ruff_annotate_snippets", "ruff_annotate_snippets",
"ruff_python_ast", "ruff_python_ast",

View File

@ -437,6 +437,15 @@ impl<'a> Checker<'a> {
} }
} }
/// Returns the [`Tokens`] for the parsed source file.
///
///
/// Unlike [`Self::tokens`], this method always returns
/// the tokens for the current file, even when within a parsed type annotation.
pub(crate) fn source_tokens(&self) -> &'a Tokens {
self.parsed.tokens()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte /// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets. /// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> { pub(crate) const fn locator(&self) -> &'a Locator<'a> {

View File

@ -3,14 +3,13 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ruff_python_ast::AnyNodeRef; use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{self, Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt}; use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_trivia::textwrap::dedent_to; use ruff_python_trivia::textwrap::dedent_to;
use ruff_python_trivia::{ use ruff_python_trivia::{
CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace,
is_python_whitespace,
}; };
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines}; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -209,7 +208,7 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments, arguments: &Arguments,
parentheses: Parentheses, parentheses: Parentheses,
source: &str, source: &str,
comment_ranges: &CommentRanges, tokens: &Tokens,
) -> Result<Edit> { ) -> Result<Edit> {
// Partition into arguments before and after the argument to remove. // Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments let (before, after): (Vec<_>, Vec<_>) = arguments
@ -224,7 +223,7 @@ pub(crate) fn remove_argument<T: Ranged>(
.context("Unable to find argument")?; .context("Unable to find argument")?;
let parenthesized_range = let parenthesized_range =
parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source) token::parenthesized_range(arg.value().into(), arguments.into(), tokens)
.unwrap_or(arg.range()); .unwrap_or(arg.range());
if !after.is_empty() { if !after.is_empty() {
@ -270,25 +269,14 @@ pub(crate) fn remove_argument<T: Ranged>(
/// ///
/// The new argument will be inserted before the first existing keyword argument in `arguments`, if /// The new argument will be inserted before the first existing keyword argument in `arguments`, if
/// there are any present. Otherwise, the new argument is added to the end of the argument list. /// there are any present. Otherwise, the new argument is added to the end of the argument list.
pub(crate) fn add_argument( pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Tokens) -> Edit {
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() { if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() {
let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source) let keyword = parenthesized_range(value.into(), arguments.into(), tokens).unwrap_or(*range);
.unwrap_or(*range);
Edit::insertion(format!("{argument}, "), keyword.start()) Edit::insertion(format!("{argument}, "), keyword.start())
} else if let Some(last) = arguments.arguments_source_order().last() { } else if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument. // Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range( let last = parenthesized_range(last.value().into(), arguments.into(), tokens)
last.value().into(), .unwrap_or(last.range());
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end()) Edit::insertion(format!(", {argument}"), last.end())
} else { } else {
// Case 2: no arguments. Add argument, without any trailing comma. // Case 2: no arguments. Add argument, without any trailing comma.

View File

@ -91,8 +91,8 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def:
response_model_arg, response_model_arg,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::unsafe_edit) .map(Fix::unsafe_edit)
}); });

View File

@ -74,12 +74,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
checker checker
.report_diagnostic(MapWithoutExplicitStrict, call.range()) .report_diagnostic(MapWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit( .set_fix(Fix::applicable_edit(
add_argument( add_argument("strict=False", &call.arguments, checker.tokens()),
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
Applicability::Unsafe, Applicability::Unsafe,
)); ));
} }

View File

@ -3,7 +3,7 @@ use std::fmt::Write;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault}; use ruff_python_ast::{self as ast, Expr, ParameterWithDefault};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::function_type::is_stub; use ruff_python_semantic::analyze::function_type::is_stub;
@ -166,12 +166,7 @@ fn move_initialization(
return None; return None;
} }
let range = match parenthesized_range( let range = match parenthesized_range(default.into(), parameter.into(), checker.tokens()) {
default.into(),
parameter.into(),
checker.comment_ranges(),
checker.source(),
) {
Some(range) => range, Some(range) => range,
None => default.range(), None => default.range(),
}; };
@ -194,13 +189,8 @@ fn move_initialization(
"{} = {}", "{} = {}",
parameter.parameter.name(), parameter.parameter.name(),
locator.slice( locator.slice(
parenthesized_range( parenthesized_range(default.into(), parameter.into(), checker.tokens())
default.into(), .unwrap_or(default.range())
parameter.into(),
checker.comment_ranges(),
checker.source()
)
.unwrap_or(default.range())
) )
); );
} else { } else {

View File

@ -92,12 +92,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
} }
let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range()); let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range());
let edit = add_argument( let edit = add_argument("stacklevel=2", &call.arguments, checker.tokens());
"stacklevel=2",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
diagnostic.set_fix(Fix::unsafe_edit(edit)); diagnostic.set_fix(Fix::unsafe_edit(edit));
} }

View File

@ -70,12 +70,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
checker checker
.report_diagnostic(ZipWithoutExplicitStrict, call.range()) .report_diagnostic(ZipWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit( .set_fix(Fix::applicable_edit(
add_argument( add_argument("strict=False", &call.arguments, checker.tokens()),
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
Applicability::Unsafe, Applicability::Unsafe,
)); ));
} }

View File

@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::ExprGenerator; use ruff_python_ast::ExprGenerator;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -142,13 +142,9 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall
if *parenthesized { if *parenthesized {
// The generator's range will include the innermost parentheses, but it could be // The generator's range will include the innermost parentheses, but it could be
// surrounded by additional parentheses. // surrounded by additional parentheses.
let range = parenthesized_range( let range =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
(&call.arguments).into(), .unwrap_or(argument.range());
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(argument.range());
// The generator always parenthesizes the expression; trim the parentheses. // The generator always parenthesizes the expression; trim the parentheses.
let generator = checker.generator().expr(argument); let generator = checker.generator().expr(argument);

View File

@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::ExprGenerator; use ruff_python_ast::ExprGenerator;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -147,13 +147,9 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall)
if *parenthesized { if *parenthesized {
// The generator's range will include the innermost parentheses, but it could be // The generator's range will include the innermost parentheses, but it could be
// surrounded by additional parentheses. // surrounded by additional parentheses.
let range = parenthesized_range( let range =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
(&call.arguments).into(), .unwrap_or(argument.range());
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(argument.range());
// The generator always parenthesizes the expression; trim the parentheses. // The generator always parenthesizes the expression; trim the parentheses.
let generator = checker.generator().expr(argument); let generator = checker.generator().expr(argument);

View File

@ -1,7 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -89,13 +89,9 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast::
// If the list comprehension is parenthesized, remove the parentheses in addition to // If the list comprehension is parenthesized, remove the parentheses in addition to
// removing the brackets. // removing the brackets.
let replacement_range = parenthesized_range( let replacement_range =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
(&call.arguments).into(), .unwrap_or_else(|| argument.range());
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or_else(|| argument.range());
let span = argument.range().add_start(one).sub_end(one); let span = argument.range().add_start(one).sub_end(one);
let replacement = let replacement =

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_trivia::is_python_whitespace; use ruff_python_trivia::is_python_whitespace;
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
@ -88,13 +88,7 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
checker.report_diagnostic(ExplicitStringConcatenation, expr.range()); checker.report_diagnostic(ExplicitStringConcatenation, expr.range());
let is_parenthesized = |expr: &Expr| { let is_parenthesized = |expr: &Expr| {
parenthesized_range( parenthesized_range(expr.into(), bin_op.into(), checker.tokens()).is_some()
expr.into(),
bin_op.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
}; };
// If either `left` or `right` is parenthesized, generating // If either `left` or `right` is parenthesized, generating
// a fix would be too involved. Just report the diagnostic. // a fix would be too involved. Just report the diagnostic.

View File

@ -111,7 +111,6 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall
} }
let arguments = &call.arguments; let arguments = &call.arguments;
let source = checker.source();
let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range); let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range);
@ -120,8 +119,8 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall
exc_info, exc_info,
arguments, arguments,
Parentheses::Preserve, Parentheses::Preserve,
source, checker.source(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
Ok(Fix::unsafe_edit(edit)) Ok(Fix::unsafe_edit(edit))
}); });

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -129,8 +129,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
keyword, keyword,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -158,8 +158,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
parenthesized_range( parenthesized_range(
value.into(), value.into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens()
checker.locator().contents(),
) )
.unwrap_or(value.range()) .unwrap_or(value.range())
) )

View File

@ -73,11 +73,11 @@ pub(crate) fn unnecessary_range_start(checker: &Checker, call: &ast::ExprCall) {
let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range()); let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range());
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
remove_argument( remove_argument(
&start, start,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });

View File

@ -160,20 +160,16 @@ fn generate_fix(
) -> anyhow::Result<Fix> { ) -> anyhow::Result<Fix> {
let locator = checker.locator(); let locator = checker.locator();
let source = locator.contents(); let source = locator.contents();
let tokens = checker.tokens();
let deletion = remove_argument( let deletion = remove_argument(
generic_base, generic_base,
arguments, arguments,
Parentheses::Preserve, Parentheses::Preserve,
source, source,
checker.comment_ranges(), tokens,
)?; )?;
let insertion = add_argument( let insertion = add_argument(locator.slice(generic_base), arguments, tokens);
locator.slice(generic_base),
arguments,
checker.comment_ranges(),
source,
);
Ok(Fix::unsafe_edits(deletion, [insertion])) Ok(Fix::unsafe_edits(deletion, [insertion]))
} }

View File

@ -5,7 +5,7 @@ use ruff_python_ast::{
helpers::{pep_604_union, typing_optional}, helpers::{pep_604_union, typing_optional},
name::Name, name::Name,
operator_precedence::OperatorPrecedence, operator_precedence::OperatorPrecedence,
parenthesize::parenthesized_range, token::{Tokens, parenthesized_range},
}; };
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union}; use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -243,16 +243,12 @@ fn create_fix(
let union_expr = pep_604_union(&[new_literal_expr, none_expr]); let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
// Check if we need parentheses to preserve operator precedence // Check if we need parentheses to preserve operator precedence
let content = if needs_parentheses_for_precedence( let content =
semantic, if needs_parentheses_for_precedence(semantic, literal_expr, checker.tokens()) {
literal_expr, format!("({})", checker.generator().expr(&union_expr))
checker.comment_ranges(), } else {
checker.source(), checker.generator().expr(&union_expr)
) { };
format!("({})", checker.generator().expr(&union_expr))
} else {
checker.generator().expr(&union_expr)
};
let union_edit = Edit::range_replacement(content, literal_expr.range()); let union_edit = Edit::range_replacement(content, literal_expr.range());
Fix::applicable_edit(union_edit, applicability) Fix::applicable_edit(union_edit, applicability)
@ -278,8 +274,7 @@ enum UnionKind {
fn needs_parentheses_for_precedence( fn needs_parentheses_for_precedence(
semantic: &ruff_python_semantic::SemanticModel, semantic: &ruff_python_semantic::SemanticModel,
literal_expr: &Expr, literal_expr: &Expr,
comment_ranges: &ruff_python_trivia::CommentRanges, tokens: &Tokens,
source: &str,
) -> bool { ) -> bool {
// Get the parent expression to check if we're in a context that needs parentheses // Get the parent expression to check if we're in a context that needs parentheses
let Some(parent_expr) = semantic.current_expression_parent() else { let Some(parent_expr) = semantic.current_expression_parent() else {
@ -287,14 +282,7 @@ fn needs_parentheses_for_precedence(
}; };
// Check if the literal expression is already parenthesized // Check if the literal expression is already parenthesized
if parenthesized_range( if parenthesized_range(literal_expr.into(), parent_expr.into(), tokens).is_some() {
literal_expr.into(),
parent_expr.into(),
comment_ranges,
source,
)
.is_some()
{
return false; // Already parenthesized, don't add more return false; // Already parenthesized, don't add more
} }

View File

@ -10,7 +10,7 @@ use libcst_native::{
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp, self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp,
@ -303,8 +303,7 @@ pub(crate) fn unittest_assertion(
parenthesized_range( parenthesized_range(
expr.into(), expr.into(),
checker.semantic().current_statement().into(), checker.semantic().current_statement().into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or(expr.range()), .unwrap_or(expr.range()),
))); )));

View File

@ -768,8 +768,8 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor
keyword, keyword,
arguments, arguments,
edits::Parentheses::Preserve, edits::Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::unsafe_edit) .map(Fix::unsafe_edit)
}); });

View File

@ -2,10 +2,9 @@ use rustc_hash::{FxBuildHasher, FxHashMap};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags}; use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags};
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
@ -322,18 +321,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -
/// ``` /// ```
/// ///
/// This method assumes that the first argument is a string. /// This method assumes that the first argument is a string.
fn get_parametrize_name_range( fn get_parametrize_name_range(call: &ExprCall, expr: &Expr, tokens: &Tokens) -> Option<TextRange> {
call: &ExprCall, parenthesized_range(expr.into(), (&call.arguments).into(), tokens)
expr: &Expr,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<TextRange> {
parenthesized_range(
expr.into(),
(&call.arguments).into(),
comment_ranges,
source,
)
} }
/// PT006 /// PT006
@ -349,13 +338,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
if names.len() > 1 { if names.len() > 1 {
match names_type { match names_type {
types::ParametrizeNameType::Tuple => { types::ParametrizeNameType::Tuple => {
let name_range = get_parametrize_name_range( let name_range = get_parametrize_name_range(call, expr, checker.tokens())
call, .unwrap_or(expr.range());
expr,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range());
let mut diagnostic = checker.report_diagnostic( let mut diagnostic = checker.report_diagnostic(
PytestParametrizeNamesWrongType { PytestParametrizeNamesWrongType {
single_argument: false, single_argument: false,
@ -386,13 +370,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
))); )));
} }
types::ParametrizeNameType::List => { types::ParametrizeNameType::List => {
let name_range = get_parametrize_name_range( let name_range = get_parametrize_name_range(call, expr, checker.tokens())
call, .unwrap_or(expr.range());
expr,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range());
let mut diagnostic = checker.report_diagnostic( let mut diagnostic = checker.report_diagnostic(
PytestParametrizeNamesWrongType { PytestParametrizeNamesWrongType {
single_argument: false, single_argument: false,

View File

@ -10,7 +10,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::{Truthiness, contains_effect}; use ruff_python_ast::helpers::{Truthiness, contains_effect};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
@ -800,14 +800,9 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit( edit = Some(get_short_circuit_edit(
value, value,
TextRange::new( TextRange::new(
parenthesized_range( parenthesized_range(furthest.into(), expr.into(), checker.tokens())
furthest.into(), .unwrap_or(furthest.range())
expr.into(), .start(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range())
.start(),
expr.end(), expr.end(),
), ),
short_circuit_truthiness, short_circuit_truthiness,
@ -828,14 +823,9 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit( edit = Some(get_short_circuit_edit(
next_value, next_value,
TextRange::new( TextRange::new(
parenthesized_range( parenthesized_range(furthest.into(), expr.into(), checker.tokens())
furthest.into(), .unwrap_or(furthest.range())
expr.into(), .start(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range())
.start(),
expr.end(), expr.end(),
), ),
short_circuit_truthiness, short_circuit_truthiness,

View File

@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{is_const_false, is_const_true}; use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
@ -171,13 +171,8 @@ pub(crate) fn if_expr_with_true_false(
checker checker
.locator() .locator()
.slice( .slice(
parenthesized_range( parenthesized_range(test.into(), expr.into(), checker.tokens())
test.into(), .unwrap_or(test.range()),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(test.range()),
) )
.to_string(), .to_string(),
expr.range(), expr.range(),

View File

@ -4,10 +4,10 @@ use anyhow::Result;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableStmt; use ruff_python_ast::comparable::ComparableStmt;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches}; use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches};
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -99,7 +99,7 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) {
&current_branch, &current_branch,
following_branch, following_branch,
checker.locator(), checker.locator(),
checker.comment_ranges(), checker.tokens(),
) )
}); });
} }
@ -111,7 +111,7 @@ fn merge_branches(
current_branch: &IfElifBranch, current_branch: &IfElifBranch,
following_branch: &IfElifBranch, following_branch: &IfElifBranch,
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, tokens: &ruff_python_ast::token::Tokens,
) -> Result<Fix> { ) -> Result<Fix> {
// Identify the colon (`:`) at the end of the current branch's test. // Identify the colon (`:`) at the end of the current branch's test.
let Some(current_branch_colon) = let Some(current_branch_colon) =
@ -127,12 +127,9 @@ fn merge_branches(
); );
// If the following test isn't parenthesized, consider parenthesizing it. // If the following test isn't parenthesized, consider parenthesizing it.
let following_branch_test = if let Some(range) = parenthesized_range( let following_branch_test = if let Some(range) =
following_branch.test.into(), parenthesized_range(following_branch.test.into(), stmt_if.into(), tokens)
stmt_if.into(), {
comment_ranges,
locator.contents(),
) {
Cow::Borrowed(locator.slice(range)) Cow::Borrowed(locator.slice(range))
} else if matches!( } else if matches!(
following_branch.test, following_branch.test,
@ -153,24 +150,19 @@ fn merge_branches(
// //
// For example, if the current test is `x if x else y`, we should parenthesize it to // For example, if the current test is `x if x else y`, we should parenthesize it to
// `(x if x else y) or ...`. // `(x if x else y) or ...`.
let parenthesize_edit = if matches!( let parenthesize_edit =
current_branch.test, if matches!(
Expr::Lambda(_) | Expr::Named(_) | Expr::If(_) current_branch.test,
) && parenthesized_range( Expr::Lambda(_) | Expr::Named(_) | Expr::If(_)
current_branch.test.into(), ) && parenthesized_range(current_branch.test.into(), stmt_if.into(), tokens).is_none()
stmt_if.into(), {
comment_ranges, Some(Edit::range_replacement(
locator.contents(), format!("({})", locator.slice(current_branch.test)),
) current_branch.test.range(),
.is_none() ))
{ } else {
Some(Edit::range_replacement( None
format!("({})", locator.slice(current_branch.test)), };
current_branch.test.range(),
))
} else {
None
};
Ok(Fix::safe_edits( Ok(Fix::safe_edits(
deletion_edit, deletion_edit,

View File

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::AnyNodeRef; use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr}; use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr};
use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
@ -90,20 +90,10 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa
} }
// Extract the exact range of the left and right expressions. // Extract the exact range of the left and right expressions.
let left_range = parenthesized_range( let left_range =
left.into(), parenthesized_range(left.into(), parent, checker.tokens()).unwrap_or(left.range());
parent, let right_range =
checker.comment_ranges(), parenthesized_range(right.into(), parent, checker.tokens()).unwrap_or(right.range());
checker.locator().contents(),
)
.unwrap_or(left.range());
let right_range = parenthesized_range(
right.into(),
parent,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(right.range());
let mut diagnostic = checker.report_diagnostic( let mut diagnostic = checker.report_diagnostic(
InDictKeys { InDictKeys {

View File

@ -11,7 +11,7 @@ use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression; use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
/// ## What it does /// ## What it does
/// Checks if [PEP 613] explicit type aliases contain references to /// Checks if [PEP 613] explicit type aliases contain references to
@ -295,21 +295,20 @@ pub(crate) fn quoted_type_alias(
let range = annotation_expr.range(); let range = annotation_expr.range();
let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range); let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range);
let fix_string = annotation_expr.value.to_string(); let fix_string = annotation_expr.value.to_string();
let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r')) let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r'))
&& parenthesized_range( && parenthesized_range(
// Check for parenthesis outside string ("""...""") // Check for parentheses outside the string ("""...""")
annotation_expr.into(), annotation_expr.into(),
checker.semantic().current_statement().into(), checker.semantic().current_statement().into(),
checker.comment_ranges(), checker.source_tokens(),
checker.locator().contents(),
) )
.is_none() .is_none()
&& parenthesized_range( && parenthesized_range(
// Check for parenthesis inside string """(...)""" // Check for parentheses inside the string """(...)"""
expr.into(), expr.into(),
annotation_expr.into(), annotation_expr.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.is_none() .is_none()
{ {

View File

@ -1,10 +1,9 @@
use std::ops::Range; use std::ops::Range;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator}; use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -89,11 +88,7 @@ pub(crate) fn path_constructor_current_directory(
let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range()); let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range());
match parent_and_next_path_fragment_range( match parent_and_next_path_fragment_range(checker.semantic(), checker.tokens()) {
checker.semantic(),
checker.comment_ranges(),
checker.source(),
) {
Some((parent_range, next_fragment_range)) => { Some((parent_range, next_fragment_range)) => {
let next_fragment_expr = checker.locator().slice(next_fragment_range); let next_fragment_expr = checker.locator().slice(next_fragment_range);
let call_expr = checker.locator().slice(call.range()); let call_expr = checker.locator().slice(call.range());
@ -116,7 +111,7 @@ pub(crate) fn path_constructor_current_directory(
arguments, arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.source(), checker.source(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
Ok(Fix::applicable_edit(edit, applicability(call.range()))) Ok(Fix::applicable_edit(edit, applicability(call.range())))
}), }),
@ -125,8 +120,7 @@ pub(crate) fn path_constructor_current_directory(
fn parent_and_next_path_fragment_range( fn parent_and_next_path_fragment_range(
semantic: &SemanticModel, semantic: &SemanticModel,
comment_ranges: &CommentRanges, tokens: &ruff_python_ast::token::Tokens,
source: &str,
) -> Option<(TextRange, TextRange)> { ) -> Option<(TextRange, TextRange)> {
let parent = semantic.current_expression_parent()?; let parent = semantic.current_expression_parent()?;
@ -142,6 +136,6 @@ fn parent_and_next_path_fragment_range(
Some(( Some((
parent.range(), parent.range(),
parenthesized_range(right.into(), parent.into(), comment_ranges, source).unwrap_or(range), parenthesized_range(right.into(), parent.into(), tokens).unwrap_or(range),
)) ))
} }

View File

@ -1,8 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Keyword, Stmt}; use ruff_python_ast::{self as ast, Keyword, Stmt};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Locator; use crate::Locator;
@ -91,7 +90,7 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) {
call, call,
keyword, keyword,
statement, statement,
checker.comment_ranges(), checker.tokens(),
checker.locator(), checker.locator(),
) { ) {
diagnostic.set_fix(fix); diagnostic.set_fix(fix);
@ -111,21 +110,16 @@ fn convert_inplace_argument_to_assignment(
call: &ast::ExprCall, call: &ast::ExprCall,
keyword: &Keyword, keyword: &Keyword,
statement: &Stmt, statement: &Stmt,
comment_ranges: &CommentRanges, tokens: &Tokens,
locator: &Locator, locator: &Locator,
) -> Option<Fix> { ) -> Option<Fix> {
// Add the assignment. // Add the assignment.
let attr = call.func.as_attribute_expr()?; let attr = call.func.as_attribute_expr()?;
let insert_assignment = Edit::insertion( let insert_assignment = Edit::insertion(
format!("{name} = ", name = locator.slice(attr.value.range())), format!("{name} = ", name = locator.slice(attr.value.range())),
parenthesized_range( parenthesized_range(call.into(), statement.into(), tokens)
call.into(), .unwrap_or(call.range())
statement.into(), .start(),
comment_ranges,
locator.contents(),
)
.unwrap_or(call.range())
.start(),
); );
// Remove the `inplace` argument. // Remove the `inplace` argument.
@ -134,7 +128,7 @@ fn convert_inplace_argument_to_assignment(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
locator.contents(), locator.contents(),
comment_ranges, tokens,
) )
.ok()?; .ok()?;

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter, self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter,
ParameterWithDefault, Parameters, Stmt, ParameterWithDefault, Parameters, Stmt,
@ -265,29 +265,19 @@ fn replace_trailing_ellipsis_with_original_expr(
stmt: &Stmt, stmt: &Stmt,
checker: &Checker, checker: &Checker,
) -> String { ) -> String {
let original_expr_range = parenthesized_range( let original_expr_range =
(&lambda.body).into(), parenthesized_range((&lambda.body).into(), lambda.into(), checker.tokens())
lambda.into(), .unwrap_or(lambda.body.range());
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(lambda.body.range());
// This prevents the autofix of introducing a syntax error if the lambda's body is an // This prevents the autofix of introducing a syntax error if the lambda's body is an
// expression spanned across multiple lines. To avoid the syntax error we preserve // expression spanned across multiple lines. To avoid the syntax error we preserve
// the parenthesis around the body. // the parenthesis around the body.
let original_expr_in_source = if parenthesized_range( let original_expr_in_source =
lambda.into(), if parenthesized_range(lambda.into(), stmt.into(), checker.tokens()).is_some() {
stmt.into(), format!("({})", checker.locator().slice(original_expr_range))
checker.comment_ranges(), } else {
checker.source(), checker.locator().slice(original_expr_range).to_string()
) };
.is_some()
{
format!("({})", checker.locator().slice(original_expr_range))
} else {
checker.locator().slice(original_expr_range).to_string()
};
let placeholder_ellipsis_start = generated.rfind("...").unwrap(); let placeholder_ellipsis_start = generated.rfind("...").unwrap();
let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len(); let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len();

View File

@ -1,4 +1,4 @@
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
@ -179,15 +179,14 @@ fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option<bool>
fn generate_redundant_comparison( fn generate_redundant_comparison(
compare: &ast::ExprCompare, compare: &ast::ExprCompare,
comment_ranges: &ruff_python_trivia::CommentRanges, tokens: &Tokens,
source: &str, source: &str,
comparator: &Expr, comparator: &Expr,
kind: bool, kind: bool,
needs_wrap: bool, needs_wrap: bool,
) -> String { ) -> String {
let comparator_range = let comparator_range = parenthesized_range(comparator.into(), compare.into(), tokens)
parenthesized_range(comparator.into(), compare.into(), comment_ranges, source) .unwrap_or(comparator.range());
.unwrap_or(comparator.range());
let comparator_str = &source[comparator_range]; let comparator_str = &source[comparator_range];
@ -379,7 +378,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
.copied() .copied()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let comment_ranges = checker.comment_ranges(); let tokens = checker.tokens();
let source = checker.source(); let source = checker.source();
let content = match (&*compare.ops, &*compare.comparators) { let content = match (&*compare.ops, &*compare.comparators) {
@ -387,18 +386,13 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) { if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) {
let needs_wrap = compare.left.range().start() != compare.range().start(); let needs_wrap = compare.left.range().start() != compare.range().start();
generate_redundant_comparison( generate_redundant_comparison(
compare, compare, tokens, source, comparator, kind, needs_wrap,
comment_ranges,
source,
comparator,
kind,
needs_wrap,
) )
} else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) { } else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) {
let needs_wrap = comparator.range().end() != compare.range().end(); let needs_wrap = comparator.range().end() != compare.range().end();
generate_redundant_comparison( generate_redundant_comparison(
compare, compare,
comment_ranges, tokens,
source, source,
&compare.left, &compare.left,
kind, kind,
@ -410,7 +404,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
&ops, &ops,
&compare.comparators, &compare.comparators,
compare.into(), compare.into(),
comment_ranges, tokens,
source, source,
) )
} }
@ -420,7 +414,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
&ops, &ops,
&compare.comparators, &compare.comparators,
compare.into(), compare.into(),
comment_ranges, tokens,
source, source,
), ),
}; };

View File

@ -107,7 +107,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) {
&[CmpOp::NotIn], &[CmpOp::NotIn],
comparators, comparators,
unary_op.into(), unary_op.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(), checker.source(),
), ),
unary_op.range(), unary_op.range(),
@ -127,7 +127,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) {
&[CmpOp::IsNot], &[CmpOp::IsNot],
comparators, comparators,
unary_op.into(), unary_op.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(), checker.source(),
), ),
unary_op.range(), unary_op.range(),

View File

@ -3,7 +3,7 @@ use std::collections::hash_map::Entry;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::{ComparableExpr, HashableExpr}; use ruff_python_ast::comparable::{ComparableExpr, HashableExpr};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -193,16 +193,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
parenthesized_range( parenthesized_range(
dict.value(i - 1).into(), dict.value(i - 1).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i - 1).range()) .unwrap_or_else(|| dict.value(i - 1).range())
.end(), .end(),
parenthesized_range( parenthesized_range(
dict.value(i).into(), dict.value(i).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i).range()) .unwrap_or_else(|| dict.value(i).range())
.end(), .end(),
@ -224,16 +222,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
parenthesized_range( parenthesized_range(
dict.value(i - 1).into(), dict.value(i - 1).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i - 1).range()) .unwrap_or_else(|| dict.value(i - 1).range())
.end(), .end(),
parenthesized_range( parenthesized_range(
dict.value(i).into(), dict.value(i).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i).range()) .unwrap_or_else(|| dict.value(i).range())
.end(), .end(),

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::{self as ast, Stmt}; use ruff_python_ast::{self as ast, Stmt};
use ruff_python_semantic::Binding; use ruff_python_semantic::Binding;
@ -172,14 +172,10 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
{ {
// If the expression is complex (`x = foo()`), remove the assignment, // If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side. // but preserve the right-hand side.
let start = parenthesized_range( let start =
target.into(), parenthesized_range(target.into(), statement.into(), checker.tokens())
statement.into(), .unwrap_or(target.range())
checker.comment_ranges(), .start();
checker.locator().contents(),
)
.unwrap_or(target.range())
.start();
let end = match_token_after(checker.tokens(), target.end(), |token| { let end = match_token_after(checker.tokens(), target.end(), |token| {
token == TokenKind::Equal token == TokenKind::Equal
})? })?

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ use ruff_python_ast::{
BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare,
parenthesize::{parentheses_iterator, parenthesized_range}, token::{parentheses_iterator, parenthesized_range},
}; };
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -62,7 +62,7 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
} }
let locator = checker.locator(); let locator = checker.locator();
let comment_ranges = checker.comment_ranges(); let tokens = checker.tokens();
// retrieve all compare expressions from boolean expression // retrieve all compare expressions from boolean expression
let compare_expressions = expr_bool_op let compare_expressions = expr_bool_op
@ -89,40 +89,22 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
continue; continue;
} }
let left_paren_count = parentheses_iterator( let left_paren_count =
left_compare.into(), parentheses_iterator(left_compare.into(), Some(expr_bool_op.into()), tokens).count();
Some(expr_bool_op.into()),
comment_ranges,
locator.contents(),
)
.count();
let right_paren_count = parentheses_iterator( let right_paren_count =
right_compare.into(), parentheses_iterator(right_compare.into(), Some(expr_bool_op.into()), tokens).count();
Some(expr_bool_op.into()),
comment_ranges,
locator.contents(),
)
.count();
// Create the edit that removes the comparison operator // Create the edit that removes the comparison operator
// In `a<(b) and ((b))<c`, we need to handle the // In `a<(b) and ((b))<c`, we need to handle the
// parentheses when specifying the fix range. // parentheses when specifying the fix range.
let left_compare_right_range = parenthesized_range( let left_compare_right_range =
left_compare_right.into(), parenthesized_range(left_compare_right.into(), left_compare.into(), tokens)
left_compare.into(), .unwrap_or(left_compare_right.range());
comment_ranges, let right_compare_left_range =
locator.contents(), parenthesized_range(right_compare_left.into(), right_compare.into(), tokens)
) .unwrap_or(right_compare_left.range());
.unwrap_or(left_compare_right.range());
let right_compare_left_range = parenthesized_range(
right_compare_left.into(),
right_compare.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(right_compare_left.range());
let edit = Edit::range_replacement( let edit = Edit::range_replacement(
locator.slice(left_compare_right_range).to_string(), locator.slice(left_compare_right_range).to_string(),
TextRange::new( TextRange::new(

View File

@ -99,7 +99,7 @@ pub(crate) fn duplicate_bases(checker: &Checker, name: &str, arguments: Option<&
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(|edit| { .map(|edit| {
Fix::applicable_edit( Fix::applicable_edit(

View File

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, CmpOp, Stmt}; use ruff_python_ast::{self as ast, CmpOp, Stmt};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -166,13 +166,8 @@ pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) {
let replacement = format!( let replacement = format!(
"{} = {min_max}({}, {})", "{} = {min_max}({}, {})",
checker.locator().slice( checker.locator().slice(
parenthesized_range( parenthesized_range(body_target.into(), body.into(), checker.tokens())
body_target.into(), .unwrap_or(body_target.range())
body.into(),
checker.comment_ranges(),
checker.locator().contents()
)
.unwrap_or(body_target.range())
), ),
checker.locator().slice(arg1), checker.locator().slice(arg1),
checker.locator().slice(arg2), checker.locator().slice(arg2),

View File

@ -174,12 +174,8 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
SliceBoundary::Last => "rsplit", SliceBoundary::Last => "rsplit",
}; };
let maxsplit_argument_edit = fix::edits::add_argument( let maxsplit_argument_edit =
"maxsplit=1", fix::edits::add_argument("maxsplit=1", arguments, checker.tokens());
arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
// Only change `actual_split_type` if it doesn't match `suggested_split_type` // Only change `actual_split_type` if it doesn't match `suggested_split_type`
let split_type_edit: Option<Edit> = if actual_split_type == suggested_split_type { let split_type_edit: Option<Edit> = if actual_split_type == suggested_split_type {

View File

@ -2,7 +2,7 @@ use ast::Expr;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ExprBinOp, ExprRef, Operator}; use ruff_python_ast::{ExprBinOp, ExprRef, Operator};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -150,12 +150,10 @@ fn augmented_assignment(
let right_operand_ref = ExprRef::from(right_operand); let right_operand_ref = ExprRef::from(right_operand);
let parent = original_expr.into(); let parent = original_expr.into();
let comment_ranges = checker.comment_ranges(); let tokens = checker.tokens();
let source = checker.source();
let right_operand_range = let right_operand_range =
parenthesized_range(right_operand_ref, parent, comment_ranges, source) parenthesized_range(right_operand_ref, parent, tokens).unwrap_or(right_operand.range());
.unwrap_or(right_operand.range());
let right_operand_expr = locator.slice(right_operand_range); let right_operand_expr = locator.slice(right_operand_range);
let target_expr = locator.slice(target); let target_expr = locator.slice(target);

View File

@ -75,12 +75,7 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa
let mut diagnostic = let mut diagnostic =
checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range()); checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range());
diagnostic.set_fix(Fix::applicable_edit( diagnostic.set_fix(Fix::applicable_edit(
add_argument( add_argument("check=False", &call.arguments, checker.tokens()),
"check=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
// If the function call contains `**kwargs`, mark the fix as unsafe. // If the function call contains `**kwargs`, mark the fix as unsafe.
if call if call
.arguments .arguments

View File

@ -1,8 +1,7 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, name::QualifiedName};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -193,8 +192,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
})) }))
), ),
&call.arguments, &call.arguments,
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
)) ))
} }

View File

@ -204,7 +204,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.source(), checker.source(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
Ok(Fix::unsafe_edits( Ok(Fix::unsafe_edits(
Edit::insertion(type_params.to_string(), name.end()), Edit::insertion(type_params.to_string(), name.end()),

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef}; use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -261,11 +261,11 @@ fn create_diagnostic(
type_alias_kind: TypeAliasKind, type_alias_kind: TypeAliasKind,
) { ) {
let source = checker.source(); let source = checker.source();
let tokens = checker.tokens();
let comment_ranges = checker.comment_ranges(); let comment_ranges = checker.comment_ranges();
let range_with_parentheses = let range_with_parentheses =
parenthesized_range(value.into(), stmt.into(), comment_ranges, source) parenthesized_range(value.into(), stmt.into(), tokens).unwrap_or(value.range());
.unwrap_or(value.range());
let content = format!( let content = format!(
"type {name}{type_params} = {value}", "type {name}{type_params} = {value}",

View File

@ -1,9 +1,8 @@
use anyhow::Result; use anyhow::Result;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Keyword}; use ruff_python_ast::{self as ast, Keyword, token::Tokens};
use ruff_python_semantic::Modules; use ruff_python_semantic::Modules;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -104,7 +103,7 @@ pub(crate) fn replace_stdout_stderr(checker: &Checker, call: &ast::ExprCall) {
stderr, stderr,
call, call,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
}); });
} }
@ -117,7 +116,7 @@ fn generate_fix(
stderr: &Keyword, stderr: &Keyword,
call: &ast::ExprCall, call: &ast::ExprCall,
source: &str, source: &str,
comment_ranges: &CommentRanges, tokens: &Tokens,
) -> Result<Fix> { ) -> Result<Fix> {
let (first, second) = if stdout.start() < stderr.start() { let (first, second) = if stdout.start() < stderr.start() {
(stdout, stderr) (stdout, stderr)
@ -132,7 +131,7 @@ fn generate_fix(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
source, source,
comment_ranges, tokens,
)?], )?],
)) ))
} }

View File

@ -78,7 +78,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });

View File

@ -188,7 +188,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -206,7 +206,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -231,7 +231,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -249,7 +249,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });

View File

@ -70,7 +70,7 @@ pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtCl
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
let range = edit.range(); let range = edit.range();

View File

@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance(checker: &Checker, class_def: &ast::Stm
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
let range = edit.range(); let range = edit.range();

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -139,13 +139,8 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) {
let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range()); let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range());
let contents = checker.locator().slice( let contents = checker.locator().slice(
parenthesized_range( parenthesized_range(iter.as_ref().into(), stmt_for.into(), checker.tokens())
iter.as_ref().into(), .unwrap_or(iter.range()),
stmt_for.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iter.range()),
); );
let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) { let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) {
format!("yield from ({contents})") format!("yield from ({contents})")

View File

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range}; use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range};
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -330,12 +330,8 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>(
let locator = checker.locator(); let locator = checker.locator();
let iter = for_stmt.iter.as_ref(); let iter = for_stmt.iter.as_ref();
let original_parenthesized_range = parenthesized_range( let original_parenthesized_range =
iter.into(), parenthesized_range(iter.into(), for_stmt.into(), checker.tokens());
for_stmt.into(),
checker.comment_ranges(),
checker.source(),
);
if let Some(range) = original_parenthesized_range { if let Some(range) = original_parenthesized_range {
return Cow::Borrowed(locator.slice(range)); return Cow::Borrowed(locator.slice(range));

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ use ruff_python_ast::{
Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp, Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp,
Number, Operator, PythonVersion, UnaryOp, Number, Operator, PythonVersion, UnaryOp,
@ -112,8 +112,7 @@ pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) {
let value_full_range = parenthesized_range( let value_full_range = parenthesized_range(
replace_time_zone.date.into(), replace_time_zone.date.into(),
replace_time_zone.parent.into(), replace_time_zone.parent.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(),
) )
.unwrap_or(replace_time_zone.date.range()); .unwrap_or(replace_time_zone.date.range());

View File

@ -5,8 +5,7 @@ use ruff_python_ast as ast;
use ruff_python_ast::Expr; use ruff_python_ast::Expr;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Locator; use crate::Locator;
@ -76,8 +75,8 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex
Edit::range_replacement( Edit::range_replacement(
format!( format!(
"{} or {}", "{} or {}",
parenthesize_test(test, if_expr, checker.comment_ranges(), checker.locator()), parenthesize_test(test, if_expr, checker.tokens(), checker.locator()),
parenthesize_test(orelse, if_expr, checker.comment_ranges(), checker.locator()), parenthesize_test(orelse, if_expr, checker.tokens(), checker.locator()),
), ),
if_expr.range(), if_expr.range(),
), ),
@ -99,15 +98,10 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex
fn parenthesize_test<'a>( fn parenthesize_test<'a>(
expr: &Expr, expr: &Expr,
if_expr: &ast::ExprIf, if_expr: &ast::ExprIf,
comment_ranges: &CommentRanges, tokens: &Tokens,
locator: &Locator<'a>, locator: &Locator<'a>,
) -> Cow<'a, str> { ) -> Cow<'a, str> {
if let Some(range) = parenthesized_range( if let Some(range) = parenthesized_range(expr.into(), if_expr.into(), tokens) {
expr.into(),
if_expr.into(),
comment_ranges,
locator.contents(),
) {
Cow::Borrowed(locator.slice(range)) Cow::Borrowed(locator.slice(range))
} else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) { } else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) {
Cow::Owned(format!("({})", locator.slice(expr.range()))) Cow::Owned(format!("({})", locator.slice(expr.range())))

View File

@ -1,6 +1,6 @@
use ruff_diagnostics::Applicability; use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Comprehension, Expr, StmtFor}; use ruff_python_ast::{Comprehension, Expr, StmtFor};
use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::analyze::typing::is_io_base_expr; use ruff_python_semantic::analyze::typing::is_io_base_expr;
@ -104,8 +104,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
let deletion_range = if let Some(parenthesized_range) = parenthesized_range( let deletion_range = if let Some(parenthesized_range) = parenthesized_range(
expr_attr.value.as_ref().into(), expr_attr.value.as_ref().into(),
expr_attr.into(), expr_attr.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(),
) { ) {
expr_call.range().add_start(parenthesized_range.len()) expr_call.range().add_start(parenthesized_range.len())
} else { } else {

View File

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use ruff_diagnostics::Applicability; use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Number}; use ruff_python_ast::{self as ast, Expr, Number};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -152,13 +152,8 @@ fn generate_fix(checker: &Checker, call: &ast::ExprCall, base: Base, arg: &Expr)
checker.semantic(), checker.semantic(),
)?; )?;
let arg_range = parenthesized_range( let arg_range =
arg.into(), parenthesized_range(arg.into(), call.into(), checker.tokens()).unwrap_or(arg.range());
call.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(arg.range());
let arg_str = checker.locator().slice(arg_range); let arg_str = checker.locator().slice(arg_range);
Ok(Fix::applicable_edits( Ok(Fix::applicable_edits(

View File

@ -95,7 +95,7 @@ pub(crate) fn single_item_membership_test(
&[membership_test.replacement_op()], &[membership_test.replacement_op()],
std::slice::from_ref(item), std::slice::from_ref(item),
expr.into(), expr.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(), checker.source(),
), ),
expr.range(), expr.range(),

View File

@ -163,7 +163,7 @@ fn convert_type_vars(
class_arguments, class_arguments,
Parentheses::Remove, Parentheses::Remove,
source, source,
checker.comment_ranges(), checker.tokens(),
)?; )?;
let replace_type_params = let replace_type_params =
Edit::range_replacement(new_type_params.to_string(), type_params.range); Edit::range_replacement(new_type_params.to_string(), type_params.range);

View File

@ -3,8 +3,8 @@ use anyhow::Result;
use ast::Keyword; use ast::Keyword;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_constant; use ruff_python_ast::helpers::is_constant;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Locator; use crate::Locator;
@ -108,9 +108,8 @@ pub(crate) fn default_factory_kwarg(checker: &Checker, call: &ast::ExprCall) {
}, },
call.range(), call.range(),
); );
diagnostic.try_set_fix(|| { diagnostic
convert_to_positional(call, keyword, checker.locator(), checker.comment_ranges()) .try_set_fix(|| convert_to_positional(call, keyword, checker.locator(), checker.tokens()));
});
} }
/// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`). /// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`).
@ -136,7 +135,7 @@ fn convert_to_positional(
call: &ast::ExprCall, call: &ast::ExprCall,
default_factory: &Keyword, default_factory: &Keyword,
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, tokens: &Tokens,
) -> Result<Fix> { ) -> Result<Fix> {
if call.arguments.len() == 1 { if call.arguments.len() == 1 {
// Ex) `defaultdict(default_factory=list)` // Ex) `defaultdict(default_factory=list)`
@ -153,7 +152,7 @@ fn convert_to_positional(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
locator.contents(), locator.contents(),
comment_ranges, tokens,
)?; )?;
// Second, insert the value as the first positional argument. // Second, insert the value as the first positional argument.

View File

@ -128,7 +128,7 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(|edit| Fix::applicable_edit(edit, applicability)) .map(|edit| Fix::applicable_edit(edit, applicability))
}); });

View File

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -77,14 +77,7 @@ pub(crate) fn parenthesize_chained_logical_operators(checker: &Checker, expr: &a
) => { ) => {
let locator = checker.locator(); let locator = checker.locator();
let source_range = bool_op.range(); let source_range = bool_op.range();
if parenthesized_range( if parenthesized_range(bool_op.into(), expr.into(), checker.tokens()).is_none() {
bool_op.into(),
expr.into(),
checker.comment_ranges(),
locator.contents(),
)
.is_none()
{
let new_source = format!("({})", locator.slice(source_range)); let new_source = format!("({})", locator.slice(source_range));
let edit = Edit::range_replacement(new_source, source_range); let edit = Edit::range_replacement(new_source, source_range);
checker checker

View File

@ -2,7 +2,7 @@ use anyhow::Context;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_semantic::{Scope, ScopeKind}; use ruff_python_semantic::{Scope, ScopeKind};
use ruff_python_trivia::{indentation_at_offset, textwrap}; use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
@ -159,8 +159,7 @@ fn use_initvar(
let default_loc = parenthesized_range( let default_loc = parenthesized_range(
default.into(), default.into(),
parameter_with_default.into(), parameter_with_default.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(),
) )
.unwrap_or(default.range()); .unwrap_or(default.range());

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -116,13 +116,8 @@ fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) -
)?; )?;
let iterable = checker.locator().slice( let iterable = checker.locator().slice(
parenthesized_range( parenthesized_range(iterable.into(), (&call.arguments).into(), checker.tokens())
iterable.into(), .unwrap_or(iterable.range()),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iterable.range()),
); );
Ok(Fix::unsafe_edits( Ok(Fix::unsafe_edits(

View File

@ -1,7 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::{Expr, ExprCall, parenthesize::parenthesized_range}; use ruff_python_ast::{Expr, ExprCall, token::parenthesized_range};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -124,13 +124,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op
let mut remove_zip = vec![]; let mut remove_zip = vec![];
let full_zip_range = parenthesized_range( let full_zip_range =
zip.into(), parenthesized_range(zip.into(), starmap.into(), checker.tokens()).unwrap_or(zip.range());
starmap.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(zip.range());
// Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple. // Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple.
remove_zip.push(Edit::range_deletion(TextRange::new( remove_zip.push(Edit::range_deletion(TextRange::new(
@ -138,13 +133,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op
zip.start(), zip.start(),
))); )));
let full_zip_func_range = parenthesized_range( let full_zip_func_range = parenthesized_range((&zip.func).into(), zip.into(), checker.tokens())
(&zip.func).into(), .unwrap_or(zip.func.range());
zip.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(zip.func.range());
// Delete the `zip` callee // Delete the `zip` callee
remove_zip.push(Edit::range_deletion(full_zip_func_range)); remove_zip.push(Edit::range_deletion(full_zip_func_range));

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{Arguments, Expr, ExprCall}; use ruff_python_ast::{Arguments, Expr, ExprCall};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
@ -86,6 +86,7 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) {
applicability, applicability,
checker.semantic(), checker.semantic(),
checker.locator(), checker.locator(),
checker.tokens(),
checker.comment_ranges(), checker.comment_ranges(),
checker.source(), checker.source(),
); );
@ -95,27 +96,26 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) {
} }
/// Creates a fix that replaces `int(expression)` with `expression`. /// Creates a fix that replaces `int(expression)` with `expression`.
#[allow(clippy::too_many_arguments)]
fn unwrap_int_expression( fn unwrap_int_expression(
call: &ExprCall, call: &ExprCall,
argument: &Expr, argument: &Expr,
applicability: Applicability, applicability: Applicability,
semantic: &SemanticModel, semantic: &SemanticModel,
locator: &Locator, locator: &Locator,
tokens: &Tokens,
comment_ranges: &CommentRanges, comment_ranges: &CommentRanges,
source: &str, source: &str,
) -> Fix { ) -> Fix {
let content = if let Some(range) = parenthesized_range( let content = if let Some(range) =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), tokens)
(&call.arguments).into(), {
comment_ranges,
source,
) {
locator.slice(range).to_string() locator.slice(range).to_string()
} else { } else {
let parenthesize = semantic.current_expression_parent().is_some() let parenthesize = semantic.current_expression_parent().is_some()
|| argument.is_named_expr() || argument.is_named_expr()
|| locator.count_lines(argument.range()) > 0; || locator.count_lines(argument.range()) > 0;
if parenthesize && !has_own_parentheses(argument, comment_ranges, source) { if parenthesize && !has_own_parentheses(argument, tokens, source) {
format!("({})", locator.slice(argument.range())) format!("({})", locator.slice(argument.range()))
} else { } else {
locator.slice(argument.range()).to_string() locator.slice(argument.range()).to_string()
@ -255,7 +255,7 @@ fn round_applicability(arguments: &Arguments, semantic: &SemanticModel) -> Optio
} }
/// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`). /// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`).
fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str) -> bool { fn has_own_parentheses(expr: &Expr, tokens: &Tokens, source: &str) -> bool {
match expr { match expr {
Expr::ListComp(_) Expr::ListComp(_)
| Expr::SetComp(_) | Expr::SetComp(_)
@ -276,14 +276,10 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str
// f // f
// (10) // (10)
// ``` // ```
let func_end = parenthesized_range( let func_end =
call_expr.func.as_ref().into(), parenthesized_range(call_expr.func.as_ref().into(), call_expr.into(), tokens)
call_expr.into(), .unwrap_or(call_expr.func.range())
comment_ranges, .end();
source,
)
.unwrap_or(call_expr.func.range())
.end();
lines_after_ignoring_trivia(func_end, source) == 0 lines_after_ignoring_trivia(func_end, source) == 0
} }
Expr::Subscript(subscript_expr) => { Expr::Subscript(subscript_expr) => {
@ -291,8 +287,7 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str
let subscript_end = parenthesized_range( let subscript_end = parenthesized_range(
subscript_expr.value.as_ref().into(), subscript_expr.value.as_ref().into(),
subscript_expr.into(), subscript_expr.into(),
comment_ranges, tokens,
source,
) )
.unwrap_or(subscript_expr.value.range()) .unwrap_or(subscript_expr.value.range())
.end(); .end();

View File

@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -108,22 +108,12 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) {
format!( format!(
"{}.get({})", "{}.get({})",
checker.locator().slice( checker.locator().slice(
parenthesized_range( parenthesized_range(obj_right.into(), right.into(), checker.tokens(),)
obj_right.into(), .unwrap_or(obj_right.range())
right.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(obj_right.range())
), ),
checker.locator().slice( checker.locator().slice(
parenthesized_range( parenthesized_range(key_right.into(), right.into(), checker.tokens(),)
key_right.into(), .unwrap_or(key_right.range())
right.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(key_right.range())
), ),
), ),
expr.range(), expr.range(),

View File

@ -2,7 +2,7 @@ use ruff_diagnostics::{Applicability, Edit};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_empty_f_string; use ruff_python_ast::helpers::is_empty_f_string;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -140,31 +140,19 @@ fn fix_unnecessary_literal_in_deque(
// call. otherwise, we only delete the `iterable` argument and leave the others untouched. // call. otherwise, we only delete the `iterable` argument and leave the others untouched.
let edit = if let Some(maxlen) = maxlen { let edit = if let Some(maxlen) = maxlen {
let deque_name = checker.locator().slice( let deque_name = checker.locator().slice(
parenthesized_range( parenthesized_range(deque.func.as_ref().into(), deque.into(), checker.tokens())
deque.func.as_ref().into(), .unwrap_or(deque.func.range()),
deque.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(deque.func.range()),
); );
let len_str = checker.locator().slice(maxlen); let len_str = checker.locator().slice(maxlen);
let deque_str = format!("{deque_name}(maxlen={len_str})"); let deque_str = format!("{deque_name}(maxlen={len_str})");
Edit::range_replacement(deque_str, deque.range) Edit::range_replacement(deque_str, deque.range)
} else { } else {
let range = parenthesized_range(
iterable.value().into(),
(&deque.arguments).into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(iterable.range());
remove_argument( remove_argument(
&range, &iterable,
&deque.arguments, &deque.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.source(), checker.source(),
checker.comment_ranges(), checker.tokens(),
)? )?
}; };
let has_comments = checker.comment_ranges().intersects(edit.range()); let has_comments = checker.comment_ranges().intersects(edit.range());

View File

@ -3,13 +3,14 @@ use std::path::Path;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer, indentation_at_offset}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, indentation_at_offset};
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::name::{Name, QualifiedName, QualifiedNameBuilder}; use crate::name::{Name, QualifiedName, QualifiedNameBuilder};
use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor; use crate::statement_visitor::StatementVisitor;
use crate::token::Tokens;
use crate::token::parenthesized_range;
use crate::visitor::Visitor; use crate::visitor::Visitor;
use crate::{ use crate::{
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral, self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral,
@ -1474,7 +1475,7 @@ pub fn generate_comparison(
ops: &[CmpOp], ops: &[CmpOp],
comparators: &[Expr], comparators: &[Expr],
parent: AnyNodeRef, parent: AnyNodeRef,
comment_ranges: &CommentRanges, tokens: &Tokens,
source: &str, source: &str,
) -> String { ) -> String {
let start = left.start(); let start = left.start();
@ -1483,8 +1484,7 @@ pub fn generate_comparison(
// Add the left side of the comparison. // Add the left side of the comparison.
contents.push_str( contents.push_str(
&source[parenthesized_range(left.into(), parent, comment_ranges, source) &source[parenthesized_range(left.into(), parent, tokens).unwrap_or(left.range())],
.unwrap_or(left.range())],
); );
for (op, comparator) in ops.iter().zip(comparators) { for (op, comparator) in ops.iter().zip(comparators) {
@ -1504,7 +1504,7 @@ pub fn generate_comparison(
// Add the right side of the comparison. // Add the right side of the comparison.
contents.push_str( contents.push_str(
&source[parenthesized_range(comparator.into(), parent, comment_ranges, source) &source[parenthesized_range(comparator.into(), parent, tokens)
.unwrap_or(comparator.range())], .unwrap_or(comparator.range())],
); );
} }

View File

@ -36,7 +36,7 @@ impl Debug for DebugComment<'_> {
} }
} }
/// Pretty-printed debug representation of [`Comments`]. /// Pretty-printed debug representation of [`Comments`](super::Comments).
pub(crate) struct DebugComments<'a> { pub(crate) struct DebugComments<'a> {
comments: &'a CommentsMap<'a>, comments: &'a CommentsMap<'a>,
source_code: SourceCode<'a>, source_code: SourceCode<'a>,

View File

@ -504,7 +504,7 @@ impl InOrderEntry {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct OutOfOrderEntry { struct OutOfOrderEntry {
/// Index into the [`MultiMap::out_of_order`] vector at which offset the leading vec is stored. /// Index into the [`MultiMap::out_of_order_parts`] vector at which offset the leading vec is stored.
leading_index: usize, leading_index: usize,
_count: Count<OutOfOrderEntry>, _count: Count<OutOfOrderEntry>,
} }

View File

@ -2,7 +2,8 @@ use ruff_python_ast::AnyNodeRef;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
/// Used as key into the [`MultiMap`] storing the comments per node by [`Comments`]. /// Used as key into the [`MultiMap`](super::MultiMap) storing the comments per node by
/// [`Comments`](super::Comments).
/// ///
/// Implements equality and hashing based on the address of the [`AnyNodeRef`] to get fast and cheap /// Implements equality and hashing based on the address of the [`AnyNodeRef`] to get fast and cheap
/// hashing/equality comparison. /// hashing/equality comparison.

View File

@ -1974,8 +1974,8 @@ fn handle_unary_op_comment<'a>(
/// ) /// )
/// ``` /// ```
/// ///
/// The comment will be attached to the [`Arguments`] node as a dangling comment, to ensure /// The comment will be attached to the [`Arguments`](ast::Arguments) node as a dangling comment, to
/// that it remains on the same line as open parenthesis. /// ensure that it remains on the same line as open parenthesis.
/// ///
/// Similarly, given: /// Similarly, given:
/// ```python /// ```python
@ -1984,8 +1984,8 @@ fn handle_unary_op_comment<'a>(
/// ] = ... /// ] = ...
/// ``` /// ```
/// ///
/// The comment will be attached to the [`TypeParams`] node as a dangling comment, to ensure /// The comment will be attached to the [`TypeParams`](ast::TypeParams) node as a dangling comment,
/// that it remains on the same line as open bracket. /// to ensure that it remains on the same line as open bracket.
fn handle_bracketed_end_of_line_comment<'a>( fn handle_bracketed_end_of_line_comment<'a>(
comment: DecoratedComment<'a>, comment: DecoratedComment<'a>,
source: &str, source: &str,

View File

@ -174,7 +174,8 @@ impl<'ast> SourceOrderVisitor<'ast> for CommentsVisitor<'ast, '_> {
/// A comment decorated with additional information about its surrounding context in the source document. /// A comment decorated with additional information about its surrounding context in the source document.
/// ///
/// Used by [`CommentStyle::place_comment`] to determine if this should become a [leading](self#leading-comments), [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment. /// Used by [`place_comment`] to determine if this should become a [leading](self#leading-comments),
/// [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct DecoratedComment<'a> { pub(crate) struct DecoratedComment<'a> {
enclosing: AnyNodeRef<'a>, enclosing: AnyNodeRef<'a>,
@ -465,7 +466,7 @@ pub(super) enum CommentPlacement<'a> {
/// ///
/// [`preceding_node`]: DecoratedComment::preceding_node /// [`preceding_node`]: DecoratedComment::preceding_node
/// [`following_node`]: DecoratedComment::following_node /// [`following_node`]: DecoratedComment::following_node
/// [`enclosing_node`]: DecoratedComment::enclosing_node_id /// [`enclosing_node`]: DecoratedComment::enclosing_node
/// [trailing comment]: self#trailing-comments /// [trailing comment]: self#trailing-comments
/// [leading comment]: self#leading-comments /// [leading comment]: self#leading-comments
/// [dangling comment]: self#dangling-comments /// [dangling comment]: self#dangling-comments

View File

@ -166,7 +166,7 @@ impl InterpolatedStringState {
} }
} }
/// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`]. /// Returns `true` if the interpolated string state is [`Self::NestedInterpolatedElement`].
pub(crate) fn is_nested(self) -> bool { pub(crate) fn is_nested(self) -> bool {
matches!(self, Self::NestedInterpolatedElement(..)) matches!(self, Self::NestedInterpolatedElement(..))
} }

View File

@ -1095,9 +1095,9 @@ impl OperandIndex {
} }
} }
/// Returns the index of the operand's right operator. The method always returns an index /// Returns the index of the operand's right operator. The method always returns an index even
/// even if the operand has no right operator. Use [`BinaryCallChain::get_operator`] to test if /// if the operand has no right operator. Use [`FlatBinaryExpressionSlice::get_operator`] to
/// the operand has a right operator. /// test if the operand has a right operator.
fn right_operator(self) -> OperatorIndex { fn right_operator(self) -> OperatorIndex {
OperatorIndex::new(self.0 + 1) OperatorIndex::new(self.0 + 1)
} }

View File

@ -56,18 +56,20 @@ pub(crate) enum Parenthesize {
/// Adding parentheses is desired to prevent the comments from wandering. /// Adding parentheses is desired to prevent the comments from wandering.
IfRequired, IfRequired,
/// Same as [`Self::IfBreaks`] except that it uses [`parenthesize_if_expands`] for expressions /// Same as [`Self::IfBreaks`] except that it uses
/// with the layout [`NeedsParentheses::BestFit`] which is used by non-splittable /// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for expressions with
/// expressions like literals, name, and strings. /// the layout [`OptionalParentheses::BestFit`] which is used by non-splittable expressions like
/// literals, name, and strings.
/// ///
/// Use this layout over `IfBreaks` when there's a sequence of `maybe_parenthesize_expression` /// Use this layout over `IfBreaks` when there's a sequence of `maybe_parenthesize_expression`
/// in a single logical-line and you want to break from right-to-left. Use `IfBreaks` for the /// in a single logical-line and you want to break from right-to-left. Use `IfBreaks` for the
/// first expression and `IfBreaksParenthesized` for the rest. /// first expression and `IfBreaksParenthesized` for the rest.
IfBreaksParenthesized, IfBreaksParenthesized,
/// Same as [`Self::IfBreaksParenthesized`] but uses [`parenthesize_if_expands`] for nested /// Same as [`Self::IfBreaksParenthesized`] but uses
/// [`maybe_parenthesized_expression`] calls unlike other layouts that always omit parentheses /// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for nested
/// when outer parentheses are present. /// [`maybe_parenthesized_expression`](crate::expression::maybe_parenthesize_expression) calls
/// unlike other layouts that always omit parentheses when outer parentheses are present.
IfBreaksParenthesizedNested, IfBreaksParenthesizedNested,
} }

View File

@ -214,8 +214,9 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizePattern<'_> {
} }
} }
/// This function is very similar to [`can_omit_optional_parentheses`] with the only difference that it is for patterns /// This function is very similar to
/// and not expressions. /// [`can_omit_optional_parentheses`](crate::expression::can_omit_optional_parentheses)
/// with the only difference that it is for patterns and not expressions.
/// ///
/// The base idea of the omit optional parentheses layout is to prefer using parentheses of sub-patterns /// The base idea of the omit optional parentheses layout is to prefer using parentheses of sub-patterns
/// when splitting the pattern over introducing new patterns. For example, prefer splitting the sequence pattern in /// when splitting the pattern over introducing new patterns. For example, prefer splitting the sequence pattern in

View File

@ -72,8 +72,9 @@ impl FormatNodeRule<PatternArguments> for FormatPatternArguments {
} }
} }
/// Returns `true` if the pattern (which is the only argument to a [`PatternMatchClass`]) is /// Returns `true` if the pattern (which is the only argument to a
/// parenthesized. Used to avoid falsely assuming that `x` is parenthesized in cases like: /// [`PatternMatchClass`](ruff_python_ast::PatternMatchClass)) is parenthesized.
/// Used to avoid falsely assuming that `x` is parenthesized in cases like:
/// ```python /// ```python
/// case Point2D(x): ... /// case Point2D(x): ...
/// ``` /// ```

View File

@ -23,7 +23,8 @@ use crate::{FormatModuleError, PyFormatOptions, format_module_source};
/// ///
/// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger. /// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger.
/// Some cases in which the returned range is larger than `range` are: /// Some cases in which the returned range is larger than `range` are:
/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] and [`IndentWidth`]. /// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`]
/// and [`IndentWidth`](ruff_formatter::IndentWidth).
/// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line. /// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line.
/// * `range` falls on a single line body. /// * `range` falls on a single line body.
/// ///
@ -129,16 +130,19 @@ pub fn format_range(
/// b) formatting a sub-expression has fewer split points than formatting the entire expressions. /// b) formatting a sub-expression has fewer split points than formatting the entire expressions.
/// ///
/// ### Possible docstrings /// ### Possible docstrings
/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing suite instead /// Strings that are suspected to be docstrings are excluded from the search to format the enclosing
/// so that the formatter's docstring detection in [`FormatSuite`] correctly detects and formats the docstrings. /// suite instead so that the formatter's docstring detection in
/// [`FormatSuite`](crate::statement::suite::FormatSuite) correctly detects and formats the
/// docstrings.
/// ///
/// ### Compound statements with a simple statement body /// ### Compound statements with a simple statement body
/// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter /// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter
/// must run [`FormatClauseBody`] to determine if the body should be collapsed or not. /// must run `FormatClauseBody` to determine if the body should be collapsed or not.
/// ///
/// ### Incorrectly indented code /// ### Incorrectly indented code
/// Code that uses indentations that don't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search, /// Code that uses indentations that don't match the configured [`IndentStyle`] and
/// because formatting such nodes on their own can lead to indentation mismatch with its sibling nodes. /// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search, because formatting
/// such nodes on their own can lead to indentation mismatch with its sibling nodes.
/// ///
/// ## Suppression comments /// ## Suppression comments
/// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the /// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the
@ -279,13 +283,15 @@ enum EnclosingNode<'a> {
/// ///
/// ## Compound statements with simple statement bodies /// ## Compound statements with simple statement bodies
/// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header /// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header
/// with the body. This ensures that the formatter runs [`FormatClauseBody`] that determines if the body should be indented.s /// with the body. This ensures that the formatter runs `FormatClauseBody` that determines if the body should be indented.
/// ///
/// ## Non-standard indentation /// ## Non-standard indentation
/// Node's that use an indentation that doesn't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search. /// Nodes that use an indentation that doesn't match the configured [`IndentStyle`] and
/// This is because the formatter always uses the configured [`IndentStyle`] and [`IndentWidth`], resulting in the /// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search. This is because the
/// formatted nodes using a different indentation than the unformatted sibling nodes. This would be tolerable /// formatter always uses the configured [`IndentStyle`] and
/// in non whitespace sensitive languages like JavaScript but results in lexical errors in Python. /// [`IndentWidth`](ruff_formatter::IndentWidth), resulting in the formatted nodes using a different
/// indentation than the unformatted sibling nodes. This would be tolerable in non whitespace
/// sensitive languages like JavaScript but results in lexical errors in Python.
/// ///
/// ## Implementation /// ## Implementation
/// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because /// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because
@ -713,9 +719,11 @@ impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
} }
} }
/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] and [`IndentWidth`]. /// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`]
/// and [`IndentWidth`](ruff_formatter::IndentWidth).
/// ///
/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and [`IndentWidth`]. /// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and
/// [`IndentWidth`](ruff_formatter::IndentWidth).
/// ///
/// # Panics /// # Panics
/// If `offset` is outside of `source`. /// If `offset` is outside of `source`.

View File

@ -184,7 +184,7 @@ impl Format<PyFormatContext<'_>> for FormatTargetWithEqualOperator<'_> {
/// No parentheses are added for `short` because it fits into the configured line length, regardless of whether /// No parentheses are added for `short` because it fits into the configured line length, regardless of whether
/// the comment exceeds the line width or not. /// the comment exceeds the line width or not.
/// ///
/// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because /// This logic isn't implemented in `place_comment` by associating trailing statement comments to the expression because
/// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement. /// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement.
#[derive(Debug)] #[derive(Debug)]
pub(super) enum FormatStatementsLastExpression<'a> { pub(super) enum FormatStatementsLastExpression<'a> {
@ -202,8 +202,8 @@ pub(super) enum FormatStatementsLastExpression<'a> {
/// ] = some_long_value /// ] = some_long_value
/// ``` /// ```
/// ///
/// This layout is preferred over [`RightToLeft`] if the left is unsplittable (single keyword like `return` or a Name) /// This layout is preferred over [`Self::RightToLeft`] if the left is unsplittable (single
/// because it has better performance characteristics. /// keyword like `return` or a Name) because it has better performance characteristics.
LeftToRight { LeftToRight {
/// The right side of an assignment or the value returned in a return statement. /// The right side of an assignment or the value returned in a return statement.
value: &'a Expr, value: &'a Expr,
@ -1083,11 +1083,10 @@ impl Format<PyFormatContext<'_>> for InterpolatedString<'_> {
/// For legibility, we discuss only the case of f-strings below, but the /// For legibility, we discuss only the case of f-strings below, but the
/// same comments apply to t-strings. /// same comments apply to t-strings.
/// ///
/// This is just a wrapper around [`FormatFString`] while considering a special /// This is just a wrapper around [`FormatFString`](crate::other::f_string::FormatFString) while
/// case when the f-string is at an assignment statement's value position. /// considering a special case when the f-string is at an assignment statement's value position.
/// This is necessary to prevent an instability where an f-string contains a /// This is necessary to prevent an instability where an f-string contains a multiline expression
/// multiline expression and the f-string fits on the line, but only when it's /// and the f-string fits on the line, but only when it's surrounded by parentheses.
/// surrounded by parentheses.
/// ///
/// ```python /// ```python
/// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ /// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{

View File

@ -177,8 +177,10 @@ enum WithItemsLayout<'a> {
/// ... /// ...
/// ``` /// ```
/// ///
/// In this case, use [`maybe_parenthesize_expression`] to format the context expression /// In this case, use
/// to get the exact same formatting as when formatting an expression in any other clause header. /// [`maybe_parenthesize_expression`](crate::expression::maybe_parenthesize_expression) to
/// format the context expression to get the exact same formatting as when formatting an
/// expression in any other clause header.
/// ///
/// Only used for Python 3.9+ /// Only used for Python 3.9+
/// ///

View File

@ -783,7 +783,7 @@ enum CodeExampleKind<'src> {
/// ///
/// Documentation describing doctests and how they're recognized can be /// Documentation describing doctests and how they're recognized can be
/// found as part of the Python standard library: /// found as part of the Python standard library:
/// https://docs.python.org/3/library/doctest.html. /// <https://docs.python.org/3/library/doctest.html>.
/// ///
/// (You'll likely need to read the [regex matching] used internally by the /// (You'll likely need to read the [regex matching] used internally by the
/// doctest module to determine more precisely how it works.) /// doctest module to determine more precisely how it works.)

View File

@ -38,8 +38,9 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
/// it can't because the string contains the preferred quotes OR /// it can't because the string contains the preferred quotes OR
/// it leads to more escaping. /// it leads to more escaping.
/// ///
/// Note: If you add more cases here where we return `QuoteStyle::Preserve`, /// Note: If you add more cases here where we return `QuoteStyle::Preserve`, make sure to also
/// make sure to also add them to [`FormatImplicitConcatenatedStringFlat::new`]. /// add them to
/// [`FormatImplicitConcatenatedStringFlat::new`](crate::string::implicit::FormatImplicitConcatenatedStringFlat::new).
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle { pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
let preferred_quote_style = self let preferred_quote_style = self
.preferred_quote_style .preferred_quote_style

View File

@ -9,7 +9,7 @@ use crate::prelude::*;
#[derive(Default)] #[derive(Default)]
pub struct FormatTypeParams; pub struct FormatTypeParams;
/// Formats a sequence of [`TypeParam`] nodes. /// Formats a sequence of [`TypeParam`](ruff_python_ast::TypeParam) nodes.
impl FormatNodeRule<TypeParams> for FormatTypeParams { impl FormatNodeRule<TypeParams> for FormatTypeParams {
fn fmt_fields(&self, item: &TypeParams, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_fields(&self, item: &TypeParams, f: &mut PyFormatter) -> FormatResult<()> {
// A dangling comment indicates a comment on the same line as the opening bracket, e.g.: // A dangling comment indicates a comment on the same line as the opening bracket, e.g.:

View File

@ -679,8 +679,9 @@ impl Indentation {
/// Returns `true` for a space or tab character. /// Returns `true` for a space or tab character.
/// ///
/// This is different than [`is_python_whitespace`] in that it returns `false` for a form feed character. /// This is different than [`is_python_whitespace`](ruff_python_trivia::is_python_whitespace) in
/// Form feed characters are excluded because they should be preserved in the suppressed output. /// that it returns `false` for a form feed character. Form feed characters are excluded because
/// they should be preserved in the suppressed output.
const fn is_indent_whitespace(c: char) -> bool { const fn is_indent_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t') matches!(c, ' ' | '\t')
} }

View File

@ -35,6 +35,7 @@ ruff_source_file = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
insta = { workspace = true, features = ["glob"] } insta = { workspace = true, features = ["glob"] }
itertools = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }

View File

@ -0,0 +1,5 @@
# Regression test for https://github.com/astral-sh/ty/issues/1828
(c: int = 1,f"""{d=[
def a(
class A:
pass

View File

@ -67,26 +67,59 @@ impl<'src> TokenSource<'src> {
/// ///
/// [`re_lex_logical_token`]: Lexer::re_lex_logical_token /// [`re_lex_logical_token`]: Lexer::re_lex_logical_token
pub(crate) fn re_lex_logical_token(&mut self) { pub(crate) fn re_lex_logical_token(&mut self) {
let mut non_logical_newline_start = None; let mut non_logical_newline = None;
for token in self.tokens.iter().rev() {
#[cfg(debug_assertions)]
let last_non_trivia_end_before = {
self.tokens
.iter()
.rev()
.find(|tok| !tok.kind().is_trivia())
.map(ruff_text_size::Ranged::end)
};
for (index, token) in self.tokens.iter().enumerate().rev() {
match token.kind() { match token.kind() {
TokenKind::NonLogicalNewline => { TokenKind::NonLogicalNewline => {
non_logical_newline_start = Some(token.start()); non_logical_newline = Some((index, token.start()));
} }
TokenKind::Comment => continue, TokenKind::Comment => continue,
_ => break, _ => break,
} }
} }
if self.lexer.re_lex_logical_token(non_logical_newline_start) { if !self
let current_start = self.current_range().start(); .lexer
while self .re_lex_logical_token(non_logical_newline.map(|(_, start)| start))
.tokens {
.last() return;
.is_some_and(|last| last.start() >= current_start) }
{
self.tokens.pop(); let non_logical_line_index = non_logical_newline
} .expect(
"`re_lex_logical_token` should only return `true` if `non_logical_line` is `Some`",
)
.0;
// Trim the already bumped logical line token (and comments coming after it) as it might now have become a logical line token
self.tokens.truncate(non_logical_line_index);
#[cfg(debug_assertions)]
{
let last_non_trivia_end_now = {
self.tokens
.iter()
.rev()
.find(|tok| !tok.kind().is_trivia())
.map(ruff_text_size::Ranged::end)
};
assert_eq!(last_non_trivia_end_before, last_non_trivia_end_now);
}
// Ensure `current` is positioned at a non-trivia token.
if self.current_kind().is_trivia() {
self.bump(self.current_kind());
} }
} }

View File

@ -4,15 +4,16 @@ use std::fmt::{Formatter, Write};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use itertools::Itertools;
use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_annotate_snippets::{Level, Renderer, Snippet};
use ruff_python_ast::token::Token; use ruff_python_ast::token::{Token, Tokens};
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module};
use ruff_python_ast::{self as ast, AnyNodeRef, Mod, PythonVersion}; use ruff_python_ast::{self as ast, AnyNodeRef, Mod, PythonVersion};
use ruff_python_parser::semantic_errors::{ use ruff_python_parser::semantic_errors::{
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError,
}; };
use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, parse_unchecked}; use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, Parsed, parse_unchecked};
use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -81,7 +82,7 @@ fn test_valid_syntax(input_path: &Path) {
} }
validate_tokens(parsed.tokens(), source.text_len(), input_path); validate_tokens(parsed.tokens(), source.text_len(), input_path);
validate_ast(parsed.syntax(), source.text_len(), input_path); validate_ast(&parsed, source.text_len(), input_path);
let mut output = String::new(); let mut output = String::new();
writeln!(&mut output, "## AST").unwrap(); writeln!(&mut output, "## AST").unwrap();
@ -139,7 +140,7 @@ fn test_invalid_syntax(input_path: &Path) {
let parsed = parse_unchecked(&source, options.clone()); let parsed = parse_unchecked(&source, options.clone());
validate_tokens(parsed.tokens(), source.text_len(), input_path); validate_tokens(parsed.tokens(), source.text_len(), input_path);
validate_ast(parsed.syntax(), source.text_len(), input_path); validate_ast(&parsed, source.text_len(), input_path);
let mut output = String::new(); let mut output = String::new();
writeln!(&mut output, "## AST").unwrap(); writeln!(&mut output, "## AST").unwrap();
@ -402,12 +403,16 @@ Tokens: {tokens:#?}
/// * the range of the parent node fully encloses all its child nodes /// * the range of the parent node fully encloses all its child nodes
/// * the ranges are strictly increasing when traversing the nodes in pre-order. /// * the ranges are strictly increasing when traversing the nodes in pre-order.
/// * all ranges are within the length of the source code. /// * all ranges are within the length of the source code.
fn validate_ast(root: &Mod, source_len: TextSize, test_path: &Path) { fn validate_ast(parsed: &Parsed<Mod>, source_len: TextSize, test_path: &Path) {
walk_module(&mut ValidateAstVisitor::new(source_len, test_path), root); walk_module(
&mut ValidateAstVisitor::new(parsed.tokens(), source_len, test_path),
parsed.syntax(),
);
} }
#[derive(Debug)] #[derive(Debug)]
struct ValidateAstVisitor<'a> { struct ValidateAstVisitor<'a> {
tokens: std::iter::Peekable<std::slice::Iter<'a, Token>>,
parents: Vec<AnyNodeRef<'a>>, parents: Vec<AnyNodeRef<'a>>,
previous: Option<AnyNodeRef<'a>>, previous: Option<AnyNodeRef<'a>>,
source_length: TextSize, source_length: TextSize,
@ -415,8 +420,9 @@ struct ValidateAstVisitor<'a> {
} }
impl<'a> ValidateAstVisitor<'a> { impl<'a> ValidateAstVisitor<'a> {
fn new(source_length: TextSize, test_path: &'a Path) -> Self { fn new(tokens: &'a Tokens, source_length: TextSize, test_path: &'a Path) -> Self {
Self { Self {
tokens: tokens.iter().peekable(),
parents: Vec::new(), parents: Vec::new(),
previous: None, previous: None,
source_length, source_length,
@ -425,6 +431,47 @@ impl<'a> ValidateAstVisitor<'a> {
} }
} }
impl ValidateAstVisitor<'_> {
/// Check that the node's start doesn't fall within a token.
/// Called in `enter_node` before visiting children.
fn assert_start_boundary(&mut self, node: AnyNodeRef<'_>) {
// Skip tokens that end at or before the node starts.
self.tokens
.peeking_take_while(|t| t.end() <= node.start())
.last();
if let Some(next) = self.tokens.peek() {
// At this point, next_token.end() > node.start()
assert!(
next.start() >= node.start(),
"{path}: The start of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}",
path = self.test_path.display(),
root = self.parents.first()
);
}
}
/// Check that the node's end doesn't fall within a token.
/// Called in `leave_node` after visiting children, so all tokens
/// within the node have been consumed.
fn assert_end_boundary(&mut self, node: AnyNodeRef<'_>) {
// Skip tokens that end at or before the node ends.
self.tokens
.peeking_take_while(|t| t.end() <= node.end())
.last();
if let Some(next) = self.tokens.peek() {
// At this point, `next_token.end() > node.end()`
assert!(
next.start() >= node.end(),
"{path}: The end of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}",
path = self.test_path.display(),
root = self.parents.first()
);
}
}
}
impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal { fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal {
assert!( assert!(
@ -452,12 +499,16 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
); );
} }
self.assert_start_boundary(node);
self.parents.push(node); self.parents.push(node);
TraversalSignal::Traverse TraversalSignal::Traverse
} }
fn leave_node(&mut self, node: AnyNodeRef<'ast>) { fn leave_node(&mut self, node: AnyNodeRef<'ast>) {
self.assert_end_boundary(node);
self.parents.pop().expect("Expected tree to be balanced"); self.parents.pop().expect("Expected tree to be balanced");
self.previous = Some(node); self.previous = Some(node);

View File

@ -296,7 +296,7 @@ Module(
test: Call( test: Call(
ExprCall { ExprCall {
node_index: NodeIndex(None), node_index: NodeIndex(None),
range: 456..472, range: 456..471,
func: Name( func: Name(
ExprName { ExprName {
node_index: NodeIndex(None), node_index: NodeIndex(None),
@ -306,7 +306,7 @@ Module(
}, },
), ),
arguments: Arguments { arguments: Arguments {
range: 460..472, range: 460..471,
node_index: NodeIndex(None), node_index: NodeIndex(None),
args: [ args: [
Name( Name(
@ -581,7 +581,7 @@ Module(
test: Call( test: Call(
ExprCall { ExprCall {
node_index: NodeIndex(None), node_index: NodeIndex(None),
range: 890..906, range: 890..905,
func: Name( func: Name(
ExprName { ExprName {
node_index: NodeIndex(None), node_index: NodeIndex(None),
@ -591,7 +591,7 @@ Module(
}, },
), ),
arguments: Arguments { arguments: Arguments {
range: 894..906, range: 894..905,
node_index: NodeIndex(None), node_index: NodeIndex(None),
args: [ args: [
FString( FString(
@ -832,7 +832,16 @@ Module(
| |
28 | # The lexer is nested with multiple levels of parentheses 28 | # The lexer is nested with multiple levels of parentheses
29 | if call(foo, [a, b 29 | if call(foo, [a, b
| ^ Syntax Error: Expected `]`, found NonLogicalNewline 30 | def bar():
| ^^^ Syntax Error: Expected `]`, found `def`
31 | pass
|
|
28 | # The lexer is nested with multiple levels of parentheses
29 | if call(foo, [a, b
| ^ Syntax Error: Expected `)`, found newline
30 | def bar(): 30 | def bar():
31 | pass 31 | pass
| |
@ -857,11 +866,10 @@ Module(
| |
41 | # test is to make sure it emits a `NonLogicalNewline` token after `b`.
42 | if call(foo, [a, 42 | if call(foo, [a,
43 | b 43 | b
| ^ Syntax Error: Expected `]`, found NonLogicalNewline
44 | ) 44 | )
| ^ Syntax Error: Expected `]`, found `)`
45 | def bar(): 45 | def bar():
46 | pass 46 | pass
| |
@ -898,7 +906,7 @@ Module(
| |
49 | # F-strings uses normal list parsing, so test those as well 49 | # F-strings uses normal list parsing, so test those as well
50 | if call(f"hello {x 50 | if call(f"hello {x
| ^ Syntax Error: Expected FStringEnd, found NonLogicalNewline | ^ Syntax Error: Expected `)`, found newline
51 | def bar(): 51 | def bar():
52 | pass 52 | pass
| |

View File

@ -17,7 +17,7 @@ Module(
test: Call( test: Call(
ExprCall { ExprCall {
node_index: NodeIndex(None), node_index: NodeIndex(None),
range: 3..19, range: 3..18,
func: Name( func: Name(
ExprName { ExprName {
node_index: NodeIndex(None), node_index: NodeIndex(None),
@ -27,7 +27,7 @@ Module(
}, },
), ),
arguments: Arguments { arguments: Arguments {
range: 7..19, range: 7..18,
node_index: NodeIndex(None), node_index: NodeIndex(None),
args: [ args: [
Name( Name(
@ -113,5 +113,11 @@ Module(
| |
1 | if call(foo, [a, b def bar(): pass 1 | if call(foo, [a, b def bar(): pass
| ^ Syntax Error: Expected `]`, found NonLogicalNewline | ^^^ Syntax Error: Expected `]`, found `def`
|
|
1 | if call(foo, [a, b def bar(): pass
| ^ Syntax Error: Expected `)`, found newline
| |

View File

@ -17,7 +17,7 @@ Module(
test: Call( test: Call(
ExprCall { ExprCall {
node_index: NodeIndex(None), node_index: NodeIndex(None),
range: 3..20, range: 3..18,
func: Name( func: Name(
ExprName { ExprName {
node_index: NodeIndex(None), node_index: NodeIndex(None),
@ -27,7 +27,7 @@ Module(
}, },
), ),
arguments: Arguments { arguments: Arguments {
range: 7..20, range: 7..18,
node_index: NodeIndex(None), node_index: NodeIndex(None),
args: [ args: [
Name( Name(
@ -113,7 +113,15 @@ Module(
| |
1 | if call(foo, [a, b 1 | if call(foo, [a, b
| ^ Syntax Error: Expected `]`, found NonLogicalNewline 2 | def bar():
| ^^^ Syntax Error: Expected `]`, found `def`
3 | pass
|
|
1 | if call(foo, [a, b
| ^ Syntax Error: Expected `)`, found newline
2 | def bar(): 2 | def bar():
3 | pass 3 | pass
| |

View File

@ -0,0 +1,227 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..112,
body: [
AnnAssign(
StmtAnnAssign {
node_index: NodeIndex(None),
range: 66..93,
target: Name(
ExprName {
node_index: NodeIndex(None),
range: 67..68,
id: Name("c"),
ctx: Store,
},
),
annotation: Name(
ExprName {
node_index: NodeIndex(None),
range: 70..73,
id: Name("int"),
ctx: Load,
},
),
value: Some(
Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 76..93,
elts: [
NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 76..77,
value: Int(
1,
),
},
),
Subscript(
ExprSubscript {
node_index: NodeIndex(None),
range: 78..90,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 78..85,
value: FStringValue {
inner: Single(
FString(
FString {
range: 78..85,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 82..85,
node_index: NodeIndex(None),
expression: Name(
ExprName {
node_index: NodeIndex(None),
range: 83..84,
id: Name("d"),
ctx: Load,
},
),
debug_text: Some(
DebugText {
leading: "",
trailing: "=",
},
),
conversion: None,
format_spec: None,
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: true,
unclosed: true,
},
},
),
),
},
},
),
slice: Slice(
ExprSlice {
node_index: NodeIndex(None),
range: 87..90,
lower: None,
upper: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 87..90,
id: Name("def"),
ctx: Load,
},
),
),
step: None,
},
),
ctx: Load,
},
),
Call(
ExprCall {
node_index: NodeIndex(None),
range: 91..93,
func: Name(
ExprName {
node_index: NodeIndex(None),
range: 91..92,
id: Name("a"),
ctx: Load,
},
),
arguments: Arguments {
range: 92..93,
node_index: NodeIndex(None),
args: [],
keywords: [],
},
},
),
],
ctx: Load,
parenthesized: false,
},
),
),
simple: false,
},
),
],
},
)
```
## Errors
|
1 | # Regression test for https://github.com/astral-sh/ty/issues/1828
2 | (c: int = 1,f"""{d=[
| ^ Syntax Error: Expected `)`, found `:`
3 | def a(
4 | class A:
|
|
1 | # Regression test for https://github.com/astral-sh/ty/issues/1828
2 | (c: int = 1,f"""{d=[
| ^ Syntax Error: f-string: expecting `}`
3 | def a(
4 | class A:
|
|
1 | # Regression test for https://github.com/astral-sh/ty/issues/1828
2 | (c: int = 1,f"""{d=[
3 | def a(
| ^^^ Syntax Error: Expected `:`, found `def`
4 | class A:
5 | pass
|
|
1 | # Regression test for https://github.com/astral-sh/ty/issues/1828
2 | (c: int = 1,f"""{d=[
3 | def a(
| ^ Syntax Error: Expected `]`, found name
4 | class A:
5 | pass
|
|
1 | # Regression test for https://github.com/astral-sh/ty/issues/1828
2 | (c: int = 1,f"""{d=[
3 | def a(
| _______^
4 | | class A:
5 | | pass
| |_________^ Syntax Error: f-string: unterminated triple-quoted string
|
|
2 | (c: int = 1,f"""{d=[
3 | def a(
4 | class A:
| ^^^^^ Syntax Error: Expected `)`, found `class`
5 | pass
|
|
1 | # Regression test for https://github.com/astral-sh/ty/issues/1828
2 | (c: int = 1,f"""{d=[
3 | def a(
| _______^
4 | | class A:
5 | | pass
| |_________^ Syntax Error: Expected a statement
|
|
4 | class A:
5 | pass
| ^ Syntax Error: unexpected EOF while parsing
|

View File

@ -6433,6 +6433,155 @@ collabc<CURSOR>
assert_snapshot!(snapshot, @"collections.abc"); assert_snapshot!(snapshot, @"collections.abc");
} }
#[test]
fn local_function_variable_with_return() {
let builder = completion_test_builder(
"\
variable_global = 1
def foo():
variable_local = 1
variable_<CURSOR>
return
",
);
assert_snapshot!(
builder.skip_auto_import().build().snapshot(),
@r"
variable_global
variable_local
",
);
}
#[test]
fn nested_scopes_with_return() {
let builder = completion_test_builder(
"\
variable_1 = 1
def fun1():
variable_2 = 1
def fun2():
variable_3 = 1
def fun3():
variable_4 = 1
variable_<CURSOR>
return
return
return
",
);
assert_snapshot!(
builder.skip_auto_import().build().snapshot(),
@r"
variable_1
variable_2
variable_3
variable_4
",
);
}
#[test]
fn multiple_declarations_global_scope1() {
let builder = completion_test_builder(
"\
zqzqzq: int = 1
zqzqzq: str = 'foo'
zqzq<CURSOR>
",
);
// The type for `zqzqzq` *should* be `str`, but we consider all
// reachable declarations and bindings, which means we get a
// union of `int` and `str` here even though the `int` binding
// isn't live at the cursor position.
assert_snapshot!(
builder.skip_auto_import().type_signatures().build().snapshot(),
@"zqzqzq :: int | str",
);
}
#[test]
fn multiple_declarations_global_scope2() {
let builder = completion_test_builder(
"\
zqzqzq: int = 1
zqzq<CURSOR>
zqzqzq: str = 'foo'
",
);
// The type for `zqzqzq` *should* be `int`, but we consider all
// reachable declarations and bindings, which means we get a
// union of `int` and `str` here even though the `str` binding
// doesn't exist at the cursor position.
assert_snapshot!(
builder.skip_auto_import().type_signatures().build().snapshot(),
@"zqzqzq :: int | str",
);
}
#[test]
fn multiple_declarations_function_scope1() {
let builder = completion_test_builder(
"\
def foo():
zqzqzq: int = 1
zqzqzq: str = 'foo'
zqzq<CURSOR>
return
",
);
// The type for `zqzqzq` *should* be `str`, but we consider all
// reachable declarations and bindings, which means we get a
// union of `int` and `str` here even though the `int` binding
// isn't live at the cursor position.
assert_snapshot!(
builder.skip_auto_import().type_signatures().build().snapshot(),
@"zqzqzq :: int | str",
);
}
#[test]
fn multiple_declarations_function_scope2() {
let builder = completion_test_builder(
"\
def foo():
zqzqzq: int = 1
zqzq<CURSOR>
zqzqzq: str = 'foo'
return
",
);
// The type for `zqzqzq` *should* be `int`, but we consider all
// reachable declarations and bindings, which means we get a
// union of `int` and `str` here even though the `str` binding
// doesn't exist at the cursor position.
assert_snapshot!(
builder.skip_auto_import().type_signatures().build().snapshot(),
@"zqzqzq :: int | str",
);
}
#[test]
fn multiple_declarations_function_parameter() {
let builder = completion_test_builder(
"\
from pathlib import Path
def f(zqzqzq: str):
zqzqzq: Path = Path(zqzqzq)
zqzq<CURSOR>
return
",
);
// The type for `zqzqzq` *should* be `Path`, but we consider all
// reachable declarations and bindings, which means we get a
// union of `str` and `Path` here even though the `str` binding
// isn't live at the cursor position.
assert_snapshot!(
builder.skip_auto_import().type_signatures().build().snapshot(),
@"zqzqzq :: str | Path",
);
}
/// A way to create a simple single-file (named `main.py`) completion test /// A way to create a simple single-file (named `main.py`) completion test
/// builder. /// builder.
/// ///

File diff suppressed because it is too large Load Diff

View File

@ -295,7 +295,7 @@ impl<'db> Definitions<'db> {
impl GotoTarget<'_> { impl GotoTarget<'_> {
pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option<Type<'db>> { pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
let ty = match self { match self {
GotoTarget::Expression(expression) => expression.inferred_type(model), GotoTarget::Expression(expression) => expression.inferred_type(model),
GotoTarget::FunctionDef(function) => function.inferred_type(model), GotoTarget::FunctionDef(function) => function.inferred_type(model),
GotoTarget::ClassDef(class) => class.inferred_type(model), GotoTarget::ClassDef(class) => class.inferred_type(model),
@ -317,7 +317,7 @@ impl GotoTarget<'_> {
} => { } => {
// We don't currently support hovering the bare `.` so there is always a name // We don't currently support hovering the bare `.` so there is always a name
let module = import_name(module_name, *component_index); let module = import_name(module_name, *component_index);
model.resolve_module_type(Some(module), *level)? model.resolve_module_type(Some(module), *level)
} }
GotoTarget::StringAnnotationSubexpr { GotoTarget::StringAnnotationSubexpr {
string_expr, string_expr,
@ -334,16 +334,16 @@ impl GotoTarget<'_> {
} else { } else {
// TODO: force the typechecker to tell us its secrets // TODO: force the typechecker to tell us its secrets
// (it computes but then immediately discards these types) // (it computes but then immediately discards these types)
return None; None
} }
} }
GotoTarget::BinOp { expression, .. } => { GotoTarget::BinOp { expression, .. } => {
let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?; let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?;
ty Some(ty)
} }
GotoTarget::UnaryOp { expression, .. } => { GotoTarget::UnaryOp { expression, .. } => {
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?; let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
ty Some(ty)
} }
// TODO: Support identifier targets // TODO: Support identifier targets
GotoTarget::PatternMatchRest(_) GotoTarget::PatternMatchRest(_)
@ -353,10 +353,8 @@ impl GotoTarget<'_> {
| GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. } | GotoTarget::NonLocal { .. }
| GotoTarget::Globals { .. } => return None, | GotoTarget::Globals { .. } => None,
}; }
Some(ty)
} }
/// Try to get a simplified display of this callable type by resolving overloads /// Try to get a simplified display of this callable type by resolving overloads

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3610,6 +3610,20 @@ def function():
"); ");
} }
#[test]
fn hover_tuple_assignment_target() {
let test = CursorTest::builder()
.source(
"test.py",
r#"
(x, y)<CURSOR> = "test", 10
"#,
)
.build();
assert_snapshot!(test.hover(), @"Hover provided no content");
}
impl CursorTest { impl CursorTest {
fn hover(&self) -> String { fn hover(&self) -> String {
use std::fmt::Write; use std::fmt::Write;

View File

@ -362,8 +362,9 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
Expr::Name(name) => { Expr::Name(name) => {
if let Some(rhs) = self.assignment_rhs { if let Some(rhs) = self.assignment_rhs {
if name.ctx.is_store() { if name.ctx.is_store() {
let ty = expr.inferred_type(&self.model); if let Some(ty) = expr.inferred_type(&self.model) {
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
}
} }
} }
source_order::walk_expr(self, expr); source_order::walk_expr(self, expr);
@ -371,8 +372,9 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
Expr::Attribute(attribute) => { Expr::Attribute(attribute) => {
if let Some(rhs) = self.assignment_rhs { if let Some(rhs) = self.assignment_rhs {
if attribute.ctx.is_store() { if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model); if let Some(ty) = expr.inferred_type(&self.model) {
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
}
} }
} }
source_order::walk_expr(self, expr); source_order::walk_expr(self, expr);

View File

@ -230,6 +230,11 @@ impl NavigationTargets {
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_empty()
} }
#[cfg(test)]
fn len(&self) -> usize {
self.0.len()
}
} }
impl IntoIterator for NavigationTargets { impl IntoIterator for NavigationTargets {

View File

@ -98,7 +98,9 @@ mod tests {
impl CursorTest { impl CursorTest {
fn prepare_rename(&self) -> String { fn prepare_rename(&self) -> String {
let Some(range) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else { let Some(range) = salsa::attach(&self.db, || {
can_rename(&self.db, self.cursor.file, self.cursor.offset)
}) else {
return "Cannot rename".to_string(); return "Cannot rename".to_string();
}; };
@ -106,13 +108,13 @@ mod tests {
} }
fn rename(&self, new_name: &str) -> String { fn rename(&self, new_name: &str) -> String {
let Some(_) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else { let rename_results = salsa::attach(&self.db, || {
return "Cannot rename".to_string(); can_rename(&self.db, self.cursor.file, self.cursor.offset)?;
};
let Some(rename_results) =
rename(&self.db, self.cursor.file, self.cursor.offset, new_name) rename(&self.db, self.cursor.file, self.cursor.offset, new_name)
else { });
let Some(rename_results) = rename_results else {
return "Cannot rename".to_string(); return "Cannot rename".to_string();
}; };

Some files were not shown because too many files have changed in this diff Show More