Parenthesize expressions prior to LibCST parsing (#6742)

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR adds a utility for transforming expressions via LibCST that
automatically wraps the expression in parentheses, applies a
user-provided transformation, then strips the parentheses from the
generated code. LibCST can't parse arbitrary expression ranges, since
some expressions may require parenthesization in order to be parsed
properly. For example:

```python
option = (
    '{name}={value}'
    .format(nam=name, value=value)
)
```

In this case, the expression range is:

```python
'{name}={value}'
    .format(nam=name, value=value)
```

Which isn't valid on its own. So, instead, we add "fake" parentheses
around the expression.

We were already doing this in a few places, so this is mostly
formalizing and DRYing up that pattern.

Closes https://github.com/astral-sh/ruff/issues/6720.
This commit is contained in:
Charlie Marsh 2023-08-22 13:45:05 -04:00 committed by GitHub
parent 5c1f7fd5dd
commit 214eb707a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 304 additions and 266 deletions

View File

@ -2,6 +2,5 @@
"{bar}{}".format(1, bar=2, spam=3) # F522 "{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues "{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
('' (''
.format(x=2)) .format(x=2)) # F522

View File

@ -28,6 +28,6 @@
"{1}{3}".format(1, 2, 3, 4) # F523, # F524 "{1}{3}".format(1, 2, 3, 4) # F523, # F524
"{1} {8}".format(0, 1) # F523, # F524 "{1} {8}".format(0, 1) # F523, # F524
# Not fixable # Multiline
('' (''
.format(2)) .format(2))

View File

@ -381,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Ok(summary) => { Ok(summary) => {
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) { if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
pyflakes::rules::string_dot_format_extra_named_arguments( pyflakes::rules::string_dot_format_extra_named_arguments(
checker, &summary, keywords, location, checker, call, &summary, keywords,
); );
} }
if checker if checker
.enabled(Rule::StringDotFormatExtraPositionalArguments) .enabled(Rule::StringDotFormatExtraPositionalArguments)
{ {
pyflakes::rules::string_dot_format_extra_positional_arguments( pyflakes::rules::string_dot_format_extra_positional_arguments(
checker, &summary, args, location, checker, call, &summary, args,
); );
} }
if checker.enabled(Rule::StringDotFormatMissingArguments) { if checker.enabled(Rule::StringDotFormatMissingArguments) {
pyflakes::rules::string_dot_format_missing_argument( pyflakes::rules::string_dot_format_missing_argument(
checker, &summary, args, keywords, location, checker, call, &summary, args, keywords,
); );
} }
if checker.enabled(Rule::StringDotFormatMixingAutomatic) { if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
pyflakes::rules::string_dot_format_mixing_automatic( pyflakes::rules::string_dot_format_mixing_automatic(
checker, &summary, location, checker, call, &summary,
); );
} }
if checker.enabled(Rule::FormatLiterals) { if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, call); pyupgrade::rules::format_literals(checker, call, &summary);
} }
if checker.enabled(Rule::FString) { if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings( pyupgrade::rules::f_strings(
checker, checker,
call,
&summary, &summary,
expr,
value, value,
checker.settings.line_length, checker.settings.line_length,
); );

View File

@ -1,9 +1,11 @@
use crate::autofix::codemods::CodegenStylist;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use libcst_native::{ use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef, Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With, ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
}; };
use ruff_python_codegen::Stylist;
pub(crate) fn match_module(module_text: &str) -> Result<Module> { pub(crate) fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) { match libcst_native::parse_module(module_text, None) {
@ -12,13 +14,6 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
} }
} }
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> { pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) { match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement), Ok(statement) => Ok(statement),
@ -205,3 +200,59 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
bail!("Expected Statement::Compound") bail!("Expected Statement::Compound")
} }
} }
/// Given the source code for an expression, return the parsed [`Expression`].
///
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
/// Run a transformation function over an expression.
///
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
/// source). This method instead wraps the expression in "fake" parentheses, runs the
/// transformation, then removes the "fake" parentheses.
pub(crate) fn transform_expression(
source_code: &str,
stylist: &Stylist,
func: impl FnOnce(Expression) -> Result<Expression>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
let expression = match_expression(&source_code)?;
// Run the function on the expression.
let expression = func(expression)?;
// Codegen the expression.
let mut source_code = expression.codegen_stylist(stylist);
// Drop the outer parentheses.
source_code.drain(0..1);
source_code.drain(source_code.len() - 1..source_code.len());
Ok(source_code)
}
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
pub(crate) fn transform_expression_text(
source_code: &str,
func: impl FnOnce(String) -> Result<String>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
// Run the function on the expression.
let mut transformed = func(source_code)?;
// Drop the outer parentheses.
transformed.drain(0..1);
transformed.drain(transformed.len() - 1..transformed.len());
Ok(transformed)
}

View File

@ -8,10 +8,9 @@ use ruff_python_codegen::Stylist;
use ruff_python_stdlib::str::{self}; use ruff_python_stdlib::str::{self};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::snippet::SourceCodeSnippet; use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_comparison, match_expression}; use crate::cst::matchers::{match_comparison, transform_expression};
use crate::registry::AsRule; use crate::registry::AsRule;
/// ## What it does /// ## What it does
@ -96,68 +95,69 @@ fn is_constant_like(expr: &Expr) -> bool {
/// Generate a fix to reverse a comparison. /// Generate a fix to reverse a comparison.
fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Result<String> { fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Result<String> {
let range = expr.range(); let range = expr.range();
let contents = locator.slice(range); let source_code = locator.slice(range);
let mut expression = match_expression(contents)?; transform_expression(source_code, stylist, |mut expression| {
let comparison = match_comparison(&mut expression)?; let comparison = match_comparison(&mut expression)?;
let left = (*comparison.left).clone(); let left = (*comparison.left).clone();
// Copy the right side to the left side. // Copy the right side to the left side.
comparison.left = Box::new(comparison.comparisons[0].comparator.clone()); comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
// Copy the left side to the right side. // Copy the left side to the right side.
comparison.comparisons[0].comparator = left; comparison.comparisons[0].comparator = left;
// Reverse the operator. // Reverse the operator.
let op = comparison.comparisons[0].operator.clone(); let op = comparison.comparisons[0].operator.clone();
comparison.comparisons[0].operator = match op { comparison.comparisons[0].operator = match op {
CompOp::LessThan { CompOp::LessThan {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
} => CompOp::GreaterThan { } => CompOp::GreaterThan {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
}, },
CompOp::GreaterThan { CompOp::GreaterThan {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
} => CompOp::LessThan { } => CompOp::LessThan {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
}, },
CompOp::LessThanEqual { CompOp::LessThanEqual {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
} => CompOp::GreaterThanEqual { } => CompOp::GreaterThanEqual {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
}, },
CompOp::GreaterThanEqual { CompOp::GreaterThanEqual {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
} => CompOp::LessThanEqual { } => CompOp::LessThanEqual {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
}, },
CompOp::Equal { CompOp::Equal {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
} => CompOp::Equal { } => CompOp::Equal {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
}, },
CompOp::NotEqual { CompOp::NotEqual {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
} => CompOp::NotEqual { } => CompOp::NotEqual {
whitespace_before, whitespace_before,
whitespace_after, whitespace_after,
}, },
_ => panic!("Expected comparison operator"), _ => panic!("Expected comparison operator"),
}; };
Ok(expression.codegen_stylist(stylist)) Ok(expression)
})
} }
/// SIM300 /// SIM300

View File

@ -1,93 +1,88 @@
use anyhow::{Context, Ok, Result}; use anyhow::{Context, Ok, Result};
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::Edit; use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Ranged};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_semantic::Binding; use ruff_python_semantic::Binding;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist; use crate::cst::matchers::{match_call_mut, match_dict, transform_expression};
use crate::cst::matchers::{match_call_mut, match_dict, match_expression};
/// Generate a [`Edit`] to remove unused keys from format dict. /// Generate a [`Edit`] to remove unused keys from format dict.
pub(super) fn remove_unused_format_arguments_from_dict( pub(super) fn remove_unused_format_arguments_from_dict(
unused_arguments: &[usize], unused_arguments: &[usize],
stmt: &Expr, dict: &ast::ExprDict,
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
) -> Result<Edit> { ) -> Result<Edit> {
let module_text = locator.slice(stmt.range()); let source_code = locator.slice(dict.range());
let mut tree = match_expression(module_text)?; transform_expression(source_code, stylist, |mut expression| {
let dict = match_dict(&mut tree)?; let dict = match_dict(&mut expression)?;
// Remove the elements at the given indexes. // Remove the elements at the given indexes.
let mut index = 0; let mut index = 0;
dict.elements.retain(|_| { dict.elements.retain(|_| {
let is_unused = unused_arguments.contains(&index); let is_unused = unused_arguments.contains(&index);
index += 1; index += 1;
!is_unused !is_unused
}); });
Ok(Edit::range_replacement( Ok(expression)
tree.codegen_stylist(stylist), })
stmt.range(), .map(|output| Edit::range_replacement(output, dict.range()))
))
} }
/// Generate a [`Edit`] to remove unused keyword arguments from a `format` call. /// Generate a [`Edit`] to remove unused keyword arguments from a `format` call.
pub(super) fn remove_unused_keyword_arguments_from_format_call( pub(super) fn remove_unused_keyword_arguments_from_format_call(
unused_arguments: &[usize], unused_arguments: &[usize],
location: TextRange, call: &ast::ExprCall,
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
) -> Result<Edit> { ) -> Result<Edit> {
let module_text = locator.slice(location); let source_code = locator.slice(call.range());
let mut tree = match_expression(module_text)?; transform_expression(source_code, stylist, |mut expression| {
let call = match_call_mut(&mut tree)?; let call = match_call_mut(&mut expression)?;
// Remove the keyword arguments at the given indexes. // Remove the keyword arguments at the given indexes.
let mut index = 0; let mut index = 0;
call.args.retain(|arg| { call.args.retain(|arg| {
if arg.keyword.is_none() { if arg.keyword.is_none() {
return true; return true;
} }
let is_unused = unused_arguments.contains(&index); let is_unused = unused_arguments.contains(&index);
index += 1; index += 1;
!is_unused !is_unused
}); });
Ok(Edit::range_replacement( Ok(expression)
tree.codegen_stylist(stylist), })
location, .map(|output| Edit::range_replacement(output, call.range()))
))
} }
/// Generate a [`Edit`] to remove unused positional arguments from a `format` call. /// Generate a [`Edit`] to remove unused positional arguments from a `format` call.
pub(crate) fn remove_unused_positional_arguments_from_format_call( pub(crate) fn remove_unused_positional_arguments_from_format_call(
unused_arguments: &[usize], unused_arguments: &[usize],
location: TextRange, call: &ast::ExprCall,
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
) -> Result<Edit> { ) -> Result<Edit> {
let module_text = locator.slice(location); let source_code = locator.slice(call.range());
let mut tree = match_expression(module_text)?; transform_expression(source_code, stylist, |mut expression| {
let call = match_call_mut(&mut tree)?; let call = match_call_mut(&mut expression)?;
// Remove any unused arguments. // Remove any unused arguments.
let mut index = 0; let mut index = 0;
call.args.retain(|_| { call.args.retain(|_| {
let is_unused = unused_arguments.contains(&index); let is_unused = unused_arguments.contains(&index);
index += 1; index += 1;
!is_unused !is_unused
}); });
Ok(Edit::range_replacement( Ok(expression)
tree.codegen_stylist(stylist), })
location, .map(|output| Edit::range_replacement(output, call.range()))
))
} }
/// Generate a [`Edit`] to remove the binding from an exception handler. /// Generate a [`Edit`] to remove the binding from an exception handler.

View File

@ -1,6 +1,6 @@
use std::string::ToString; use std::string::ToString;
use ruff_python_ast::{self as ast, Constant, Expr, Identifier, Keyword}; use ruff_python_ast::{self as ast, Constant, Expr, Identifier, Keyword, Ranged};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -570,15 +570,16 @@ pub(crate) fn percent_format_extra_named_arguments(
if summary.num_positional > 0 { if summary.num_positional > 0 {
return; return;
} }
let Expr::Dict(ast::ExprDict { keys, .. }) = &right else { let Expr::Dict(dict) = &right else {
return; return;
}; };
// If any of the keys are spread, abort. // If any of the keys are spread, abort.
if keys.iter().any(Option::is_none) { if dict.keys.iter().any(Option::is_none) {
return; return;
} }
let missing: Vec<(usize, &str)> = keys let missing: Vec<(usize, &str)> = dict
.keys
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, key)| match key { .filter_map(|(index, key)| match key {
@ -613,7 +614,7 @@ pub(crate) fn percent_format_extra_named_arguments(
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
let edit = remove_unused_format_arguments_from_dict( let edit = remove_unused_format_arguments_from_dict(
&indexes, &indexes,
right, dict,
checker.locator(), checker.locator(),
checker.stylist(), checker.stylist(),
)?; )?;
@ -739,9 +740,9 @@ pub(crate) fn percent_format_star_requires_sequence(
/// F522 /// F522
pub(crate) fn string_dot_format_extra_named_arguments( pub(crate) fn string_dot_format_extra_named_arguments(
checker: &mut Checker, checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary, summary: &FormatSummary,
keywords: &[Keyword], keywords: &[Keyword],
location: TextRange,
) { ) {
// If there are any **kwargs, abort. // If there are any **kwargs, abort.
if has_star_star_kwargs(keywords) { if has_star_star_kwargs(keywords) {
@ -773,14 +774,14 @@ pub(crate) fn string_dot_format_extra_named_arguments(
.collect(); .collect();
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
StringDotFormatExtraNamedArguments { missing: names }, StringDotFormatExtraNamedArguments { missing: names },
location, call.range(),
); );
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect(); let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
let edit = remove_unused_keyword_arguments_from_format_call( let edit = remove_unused_keyword_arguments_from_format_call(
&indexes, &indexes,
location, call,
checker.locator(), checker.locator(),
checker.stylist(), checker.stylist(),
)?; )?;
@ -793,9 +794,9 @@ pub(crate) fn string_dot_format_extra_named_arguments(
/// F523 /// F523
pub(crate) fn string_dot_format_extra_positional_arguments( pub(crate) fn string_dot_format_extra_positional_arguments(
checker: &mut Checker, checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary, summary: &FormatSummary,
args: &[Expr], args: &[Expr],
location: TextRange,
) { ) {
let missing: Vec<usize> = args let missing: Vec<usize> = args
.iter() .iter()
@ -817,7 +818,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
.map(ToString::to_string) .map(ToString::to_string)
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
}, },
location, call.range(),
); );
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
// We can only fix if the positional arguments we're removing don't require re-indexing // We can only fix if the positional arguments we're removing don't require re-indexing
@ -849,7 +850,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
let edit = remove_unused_positional_arguments_from_format_call( let edit = remove_unused_positional_arguments_from_format_call(
&missing, &missing,
location, call,
checker.locator(), checker.locator(),
checker.stylist(), checker.stylist(),
)?; )?;
@ -863,10 +864,10 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
/// F524 /// F524
pub(crate) fn string_dot_format_missing_argument( pub(crate) fn string_dot_format_missing_argument(
checker: &mut Checker, checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary, summary: &FormatSummary,
args: &[Expr], args: &[Expr],
keywords: &[Keyword], keywords: &[Keyword],
location: TextRange,
) { ) {
if has_star_args(args) || has_star_star_kwargs(keywords) { if has_star_args(args) || has_star_star_kwargs(keywords) {
return; return;
@ -898,7 +899,7 @@ pub(crate) fn string_dot_format_missing_argument(
if !missing.is_empty() { if !missing.is_empty() {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
StringDotFormatMissingArguments { missing }, StringDotFormatMissingArguments { missing },
location, call.range(),
)); ));
} }
} }
@ -906,12 +907,13 @@ pub(crate) fn string_dot_format_missing_argument(
/// F525 /// F525
pub(crate) fn string_dot_format_mixing_automatic( pub(crate) fn string_dot_format_mixing_automatic(
checker: &mut Checker, checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary, summary: &FormatSummary,
location: TextRange,
) { ) {
if !(summary.autos.is_empty() || summary.indices.is_empty()) { if !(summary.autos.is_empty() || summary.indices.is_empty()) {
checker checker.diagnostics.push(Diagnostic::new(
.diagnostics StringDotFormatMixingAutomatic,
.push(Diagnostic::new(StringDotFormatMixingAutomatic, location)); call.range(),
));
} }
} }

View File

@ -33,7 +33,7 @@ F522.py:2:1: F522 [*] `.format` call has unused named argument(s): spam
2 |+"{bar}{}".format(1, bar=2, ) # F522 2 |+"{bar}{}".format(1, bar=2, ) # F522
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues 3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 5 | # Not fixable 5 5 | (''
F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
| |
@ -41,8 +41,8 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522
5 | # Not fixable 5 | (''
6 | ('' 6 | .format(x=2)) # F522
| |
= help: Remove extra named arguments: eggs, ham = help: Remove extra named arguments: eggs, ham
@ -52,19 +52,25 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues 3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 |-"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 4 |-"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522 4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522
5 5 | # Not fixable 5 5 | (''
6 6 | ('' 6 6 | .format(x=2)) # F522
7 7 | .format(x=2))
F522.py:6:2: F522 `.format` call has unused named argument(s): x F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x
| |
3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 | # Not fixable 5 | (''
6 | (''
| __^ | __^
7 | | .format(x=2)) 6 | | .format(x=2)) # F522
| |_____________^ F522 | |_____________^ F522
| |
= help: Remove extra named arguments: x = help: Remove extra named arguments: x
Fix
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 5 | (''
6 |- .format(x=2)) # F522
6 |+ .format()) # F522

View File

@ -243,13 +243,13 @@ F523.py:29:1: F523 `.format` call has unused arguments at position(s): 0
29 | "{1} {8}".format(0, 1) # F523, # F524 29 | "{1} {8}".format(0, 1) # F523, # F524
| ^^^^^^^^^^^^^^^^^^^^^^ F523 | ^^^^^^^^^^^^^^^^^^^^^^ F523
30 | 30 |
31 | # Not fixable 31 | # Multiline
| |
= help: Remove extra positional arguments at position(s): 0 = help: Remove extra positional arguments at position(s): 0
F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0 F523.py:32:2: F523 [*] `.format` call has unused arguments at position(s): 0
| |
31 | # Not fixable 31 | # Multiline
32 | ('' 32 | (''
| __^ | __^
33 | | .format(2)) 33 | | .format(2))
@ -257,4 +257,11 @@ F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0
| |
= help: Remove extra positional arguments at position(s): 0 = help: Remove extra positional arguments at position(s): 0
Fix
30 30 |
31 31 | # Multiline
32 32 | (''
33 |-.format(2))
33 |+.format())

View File

@ -1,18 +1,18 @@
use std::borrow::Cow; use std::borrow::Cow;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Ranged};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote}; use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::{self as ast, Constant, Expr, Keyword, Ranged};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::line_width::LineLength; use crate::line_width::LineLength;
@ -67,39 +67,34 @@ struct FormatSummaryValues<'a> {
} }
impl<'a> FormatSummaryValues<'a> { impl<'a> FormatSummaryValues<'a> {
fn try_from_expr(expr: &'a Expr, locator: &'a Locator) -> Option<Self> { fn try_from_call(call: &'a ast::ExprCall, locator: &'a Locator) -> Option<Self> {
let mut extracted_args: Vec<&Expr> = Vec::new(); let mut extracted_args: Vec<&Expr> = Vec::new();
let mut extracted_kwargs: FxHashMap<&str, &Expr> = FxHashMap::default(); let mut extracted_kwargs: FxHashMap<&str, &Expr> = FxHashMap::default();
if let Expr::Call(ast::ExprCall {
arguments: Arguments { args, keywords, .. }, for arg in &call.arguments.args {
.. if matches!(arg, Expr::Starred(..))
}) = expr || contains_quotes(locator.slice(arg.range()))
{ || locator.contains_line_break(arg.range())
for arg in args { {
if matches!(arg, Expr::Starred(..)) return None;
|| contains_quotes(locator.slice(arg.range()))
|| locator.contains_line_break(arg.range())
{
return None;
}
extracted_args.push(arg);
} }
for keyword in keywords { extracted_args.push(arg);
let Keyword { }
arg, for keyword in &call.arguments.keywords {
value, let Keyword {
range: _, arg,
} = keyword; value,
let Some(key) = arg else { range: _,
return None; } = keyword;
}; let Some(key) = arg else {
if contains_quotes(locator.slice(value.range())) return None;
|| locator.contains_line_break(value.range()) };
{ if contains_quotes(locator.slice(value.range()))
return None; || locator.contains_line_break(value.range())
} {
extracted_kwargs.insert(key, value); return None;
} }
extracted_kwargs.insert(key, value);
} }
if extracted_args.is_empty() && extracted_kwargs.is_empty() { if extracted_args.is_empty() && extracted_kwargs.is_empty() {
@ -309,8 +304,8 @@ fn try_convert_to_f_string(
/// UP032 /// UP032
pub(crate) fn f_strings( pub(crate) fn f_strings(
checker: &mut Checker, checker: &mut Checker,
call: &ast::ExprCall,
summary: &FormatSummary, summary: &FormatSummary,
expr: &Expr,
template: &Expr, template: &Expr,
line_length: LineLength, line_length: LineLength,
) { ) {
@ -318,14 +313,7 @@ pub(crate) fn f_strings(
return; return;
} }
let Expr::Call(ast::ExprCall { let Expr::Attribute(ast::ExprAttribute { value, .. }) = call.func.as_ref() else {
func, arguments, ..
}) = expr
else {
return;
};
let Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() else {
return; return;
}; };
@ -339,14 +327,14 @@ pub(crate) fn f_strings(
return; return;
}; };
let Some(mut summary) = FormatSummaryValues::try_from_expr(expr, checker.locator()) else { let Some(mut summary) = FormatSummaryValues::try_from_call(call, checker.locator()) else {
return; return;
}; };
let mut patches: Vec<(TextRange, String)> = vec![]; let mut patches: Vec<(TextRange, String)> = vec![];
let mut lex = lexer::lex_starts_at( let mut lex = lexer::lex_starts_at(
checker.locator().slice(func.range()), checker.locator().slice(call.func.range()),
Mode::Expression, Mode::Expression,
expr.start(), call.start(),
) )
.flatten(); .flatten();
let end = loop { let end = loop {
@ -384,8 +372,8 @@ pub(crate) fn f_strings(
return; return;
} }
let mut contents = String::with_capacity(checker.locator().slice(expr.range()).len()); let mut contents = String::with_capacity(checker.locator().slice(call.range()).len());
let mut prev_end = expr.start(); let mut prev_end = call.start();
for (range, fstring) in patches { for (range, fstring) in patches {
contents.push_str( contents.push_str(
checker checker
@ -415,7 +403,7 @@ pub(crate) fn f_strings(
// If necessary, add a space between any leading keyword (`return`, `yield`, `assert`, etc.) // If necessary, add a space between any leading keyword (`return`, `yield`, `assert`, etc.)
// and the string. For example, `return"foo"` is valid, but `returnf"foo"` is not. // and the string. For example, `return"foo"` is valid, but `returnf"foo"` is not.
let existing = checker.locator().slice(TextRange::up_to(expr.start())); let existing = checker.locator().slice(TextRange::up_to(call.start()));
if existing if existing
.chars() .chars()
.last() .last()
@ -424,7 +412,7 @@ pub(crate) fn f_strings(
contents.insert(0, ' '); contents.insert(0, ' ');
} }
let mut diagnostic = Diagnostic::new(FString, expr.range()); let mut diagnostic = Diagnostic::new(FString, call.range());
// Avoid autofix if there are comments within the call: // Avoid autofix if there are comments within the call:
// ``` // ```
@ -436,11 +424,11 @@ pub(crate) fn f_strings(
&& !checker && !checker
.indexer() .indexer()
.comment_ranges() .comment_ranges()
.intersects(arguments.range()) .intersects(call.arguments.range())
{ {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement( diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents, contents,
expr.range(), call.range(),
))); )));
}; };
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);

View File

@ -2,16 +2,18 @@ use anyhow::{anyhow, Result};
use libcst_native::{Arg, Expression}; use libcst_native::{Arg, Expression};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist; use crate::autofix::codemods::CodegenStylist;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_attribute, match_call_mut, match_expression}; use crate::cst::matchers::{
match_attribute, match_call_mut, match_expression, transform_expression_text,
};
use crate::registry::AsRule; use crate::registry::AsRule;
use crate::rules::pyflakes::format::FormatSummary; use crate::rules::pyflakes::format::FormatSummary;
@ -58,8 +60,8 @@ impl Violation for FormatLiterals {
/// UP030 /// UP030
pub(crate) fn format_literals( pub(crate) fn format_literals(
checker: &mut Checker, checker: &mut Checker,
summary: &FormatSummary,
call: &ast::ExprCall, call: &ast::ExprCall,
summary: &FormatSummary,
) { ) {
// The format we expect is, e.g.: `"{0} {1}".format(...)` // The format we expect is, e.g.: `"{0} {1}".format(...)`
if summary.has_nested_parts { if summary.has_nested_parts {
@ -112,10 +114,8 @@ pub(crate) fn format_literals(
let mut diagnostic = Diagnostic::new(FormatLiterals, call.range()); let mut diagnostic = Diagnostic::new(FormatLiterals, call.range());
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
Ok(Fix::suggested(Edit::range_replacement( generate_call(call, arguments, checker.locator(), checker.stylist())
generate_call(call, arguments, checker.locator(), checker.stylist())?, .map(|suggestion| Fix::suggested(Edit::range_replacement(suggestion, call.range())))
call.range(),
)))
}); });
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
@ -165,7 +165,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
} }
/// Return the corrected argument vector. /// Return the corrected argument vector.
fn generate_arguments<'a>(arguments: &[Arg<'a>], order: &'a [usize]) -> Result<Vec<Arg<'a>>> { fn generate_arguments<'a>(arguments: &[Arg<'a>], order: &[usize]) -> Result<Vec<Arg<'a>>> {
let mut new_arguments: Vec<Arg> = Vec::with_capacity(arguments.len()); let mut new_arguments: Vec<Arg> = Vec::with_capacity(arguments.len());
for (idx, given) in order.iter().enumerate() { for (idx, given) in order.iter().enumerate() {
// We need to keep the formatting in the same order but move the values. // We need to keep the formatting in the same order but move the values.
@ -205,28 +205,27 @@ fn generate_call(
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
) -> Result<String> { ) -> Result<String> {
let content = locator.slice(call.range()); let source_code = locator.slice(call.range());
let parenthesized_content = format!("({content})");
let mut expression = match_expression(&parenthesized_content)?;
// Fix the call arguments. let output = transform_expression_text(source_code, |source_code| {
let call = match_call_mut(&mut expression)?; let mut expression = match_expression(&source_code)?;
if let Arguments::Reorder(order) = arguments {
call.args = generate_arguments(&call.args, order)?;
}
// Fix the string itself. // Fix the call arguments.
let item = match_attribute(&mut call.func)?; let call = match_call_mut(&mut expression)?;
let arena = typed_arena::Arena::new(); if let Arguments::Reorder(order) = arguments {
remove_specifiers(&mut item.value, &arena); call.args = generate_arguments(&call.args, order)?;
}
// Remove the parentheses (first and last characters). // Fix the string itself.
let mut output = expression.codegen_stylist(stylist); let item = match_attribute(&mut call.func)?;
output.remove(0); let arena = typed_arena::Arena::new();
output.pop(); remove_specifiers(&mut item.value, &arena);
Ok(expression.codegen_stylist(stylist))
})?;
// Ex) `'{' '0}'.format(1)` // Ex) `'{' '0}'.format(1)`
if output == content { if output == source_code {
return Err(anyhow!("Unable to identify format literals")); return Err(anyhow!("Unable to identify format literals"));
} }

View File

@ -2,16 +2,15 @@ use anyhow::{bail, Result};
use libcst_native::{ use libcst_native::{
ConcatenatedString, Expression, FormattedStringContent, FormattedStringExpression, ConcatenatedString, Expression, FormattedStringContent, FormattedStringExpression,
}; };
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_call_mut, match_expression, match_name}; use crate::cst::matchers::{match_call_mut, match_name, transform_expression};
use crate::registry::AsRule; use crate::registry::AsRule;
/// ## What it does /// ## What it does
@ -139,36 +138,28 @@ fn convert_call_to_conversion_flag(
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
) -> Result<Fix> { ) -> Result<Fix> {
// Parenthesize the expression, to support implicit concatenation. let source_code = locator.slice(expr.range());
let range = expr.range(); transform_expression(source_code, stylist, |mut expression| {
let content = locator.slice(range); // Replace the formatted call expression at `index` with a conversion flag.
let parenthesized_content = format!("({content})"); let formatted_string_expression = match_part(index, &mut expression)?;
let mut expression = match_expression(&parenthesized_content)?; let call = match_call_mut(&mut formatted_string_expression.expression)?;
let name = match_name(&call.func)?;
// Replace the formatted call expression at `index` with a conversion flag. match name.value {
let formatted_string_expression = match_part(index, &mut expression)?; "str" => {
let call = match_call_mut(&mut formatted_string_expression.expression)?; formatted_string_expression.conversion = Some("s");
let name = match_name(&call.func)?; }
match name.value { "repr" => {
"str" => { formatted_string_expression.conversion = Some("r");
formatted_string_expression.conversion = Some("s"); }
"ascii" => {
formatted_string_expression.conversion = Some("a");
}
_ => bail!("Unexpected function call: `{:?}`", name.value),
} }
"repr" => { formatted_string_expression.expression = call.args[0].value.clone();
formatted_string_expression.conversion = Some("r"); Ok(expression)
} })
"ascii" => { .map(|output| Fix::automatic(Edit::range_replacement(output, expr.range())))
formatted_string_expression.conversion = Some("a");
}
_ => bail!("Unexpected function call: `{:?}`", name.value),
}
formatted_string_expression.expression = call.args[0].value.clone();
// Remove the parentheses (first and last characters).
let mut content = expression.codegen_stylist(stylist);
content.remove(0);
content.pop();
Ok(Fix::automatic(Edit::range_replacement(content, range)))
} }
/// Return the [`FormattedStringContent`] at the given index in an f-string or implicit /// Return the [`FormattedStringContent`] at the given index in an f-string or implicit