mirror of https://github.com/astral-sh/ruff
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:
parent
5c1f7fd5dd
commit
214eb707a6
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue