mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 06:41:23 +00:00
Simplify logic for RUF027
(#12907)
## Summary This PR is a pure refactor to simplify some of the logic for `RUF027`. This will make it easier to file some followup PRs to help reduce the false positives from this rule. I'm separating the refactor out into a separate PR so it's easier to review, and so I can double-check from the ecosystem report that this doesn't have any user-facing impact. ## Test Plan `cargo test -p ruff_linter --lib`
This commit is contained in:
parent
bd4a947b29
commit
d8debb7a36
1 changed files with 55 additions and 61 deletions
|
@ -1,9 +1,9 @@
|
||||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::{self as ast};
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_literal::format::FormatSpec;
|
use ruff_python_literal::format::FormatSpec;
|
||||||
use ruff_python_parser::parse_expression;
|
use ruff_python_parser::parse_expression;
|
||||||
use ruff_python_semantic::analyze::logging;
|
use ruff_python_semantic::analyze::logging::is_logger_candidate;
|
||||||
use ruff_python_semantic::SemanticModel;
|
use ruff_python_semantic::SemanticModel;
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
@ -33,6 +33,8 @@ use crate::checkers::ast::Checker;
|
||||||
/// 4. The string has no `{...}` expression sections, or uses invalid f-string syntax.
|
/// 4. The string has no `{...}` expression sections, or uses invalid f-string syntax.
|
||||||
/// 5. The string references variables that are not in scope, or it doesn't capture variables at all.
|
/// 5. The string references variables that are not in scope, or it doesn't capture variables at all.
|
||||||
/// 6. Any format specifiers in the potential f-string are invalid.
|
/// 6. Any format specifiers in the potential f-string are invalid.
|
||||||
|
/// 7. The string is part of a function call that is known to expect a template string rather than an
|
||||||
|
/// evaluated f-string: for example, a `logging` call or a [`gettext`] call
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
|
@ -48,6 +50,9 @@ use crate::checkers::ast::Checker;
|
||||||
/// day_of_week = "Tuesday"
|
/// day_of_week = "Tuesday"
|
||||||
/// print(f"Hello {name}! It is {day_of_week} today!")
|
/// print(f"Hello {name}! It is {day_of_week} today!")
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`logging`]: https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application
|
||||||
|
/// [`gettext`]: https://docs.python.org/3/library/gettext.html
|
||||||
#[violation]
|
#[violation]
|
||||||
pub struct MissingFStringSyntax;
|
pub struct MissingFStringSyntax;
|
||||||
|
|
||||||
|
@ -75,11 +80,22 @@ pub(crate) fn missing_fstring_syntax(checker: &mut Checker, literal: &ast::Strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We also want to avoid expressions that are intended to be translated.
|
let logger_objects = &checker.settings.logger_objects;
|
||||||
if semantic.current_expressions().any(|expr| {
|
|
||||||
is_gettext(expr, semantic)
|
// We also want to avoid:
|
||||||
|| is_logger_call(expr, semantic, &checker.settings.logger_objects)
|
// - Expressions inside `gettext()` calls
|
||||||
}) {
|
// - Expressions passed to logging calls (since the `logging` module evaluates them lazily:
|
||||||
|
// https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application)
|
||||||
|
// - Expressions where a method is immediately called on the string literal
|
||||||
|
if semantic
|
||||||
|
.current_expressions()
|
||||||
|
.filter_map(ast::Expr::as_call_expr)
|
||||||
|
.any(|call_expr| {
|
||||||
|
is_method_call_on_literal(call_expr, literal)
|
||||||
|
|| is_gettext(call_expr, semantic)
|
||||||
|
|| is_logger_candidate(&call_expr.func, semantic, logger_objects)
|
||||||
|
})
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,13 +106,6 @@ pub(crate) fn missing_fstring_syntax(checker: &mut Checker, literal: &ast::Strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_logger_call(expr: &ast::Expr, semantic: &SemanticModel, logger_objects: &[String]) -> bool {
|
|
||||||
let ast::Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
logging::is_logger_candidate(func, semantic, logger_objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if an expression appears to be a `gettext` call.
|
/// Returns `true` if an expression appears to be a `gettext` call.
|
||||||
///
|
///
|
||||||
/// We want to avoid statement expressions and assignments related to aliases
|
/// We want to avoid statement expressions and assignments related to aliases
|
||||||
|
@ -107,12 +116,9 @@ fn is_logger_call(expr: &ast::Expr, semantic: &SemanticModel, logger_objects: &[
|
||||||
/// and replace the original string with its translated counterpart. If the
|
/// and replace the original string with its translated counterpart. If the
|
||||||
/// string contains variable placeholders or formatting, it can complicate the
|
/// string contains variable placeholders or formatting, it can complicate the
|
||||||
/// translation process, lead to errors or incorrect translations.
|
/// translation process, lead to errors or incorrect translations.
|
||||||
fn is_gettext(expr: &ast::Expr, semantic: &SemanticModel) -> bool {
|
fn is_gettext(call_expr: &ast::ExprCall, semantic: &SemanticModel) -> bool {
|
||||||
let ast::Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
let func = &*call_expr.func;
|
||||||
return false;
|
let short_circuit = match func {
|
||||||
};
|
|
||||||
|
|
||||||
let short_circuit = match func.as_ref() {
|
|
||||||
ast::Expr::Name(ast::ExprName { id, .. }) => {
|
ast::Expr::Name(ast::ExprName { id, .. }) => {
|
||||||
matches!(id.as_str(), "gettext" | "ngettext" | "_")
|
matches!(id.as_str(), "gettext" | "ngettext" | "_")
|
||||||
}
|
}
|
||||||
|
@ -136,6 +142,21 @@ fn is_gettext(expr: &ast::Expr, semantic: &SemanticModel) -> bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if `call_expr` is a method call on an [`ast::ExprStringLiteral`]
|
||||||
|
/// in which `literal` is one of the [`ast::StringLiteral`] parts.
|
||||||
|
///
|
||||||
|
/// For example: `expr` is a node representing the expression `"{foo}".format(foo="bar")`,
|
||||||
|
/// and `literal` is the node representing the string literal `"{foo}"`.
|
||||||
|
fn is_method_call_on_literal(call_expr: &ast::ExprCall, literal: &ast::StringLiteral) -> bool {
|
||||||
|
let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = &*call_expr.func else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &**value else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
value.as_slice().contains(literal)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if `literal` is likely an f-string with a missing `f` prefix.
|
/// Returns `true` if `literal` is likely an f-string with a missing `f` prefix.
|
||||||
/// See [`MissingFStringSyntax`] for the validation criteria.
|
/// See [`MissingFStringSyntax`] for the validation criteria.
|
||||||
fn should_be_fstring(
|
fn should_be_fstring(
|
||||||
|
@ -158,55 +179,28 @@ fn should_be_fstring(
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut arg_names = FxHashSet::default();
|
let mut arg_names = FxHashSet::default();
|
||||||
let mut last_expr: Option<&ast::Expr> = None;
|
for expr in semantic
|
||||||
for expr in semantic.current_expressions() {
|
.current_expressions()
|
||||||
match expr {
|
.filter_map(ast::Expr::as_call_expr)
|
||||||
ast::Expr::Call(ast::ExprCall {
|
|
||||||
arguments: ast::Arguments { keywords, args, .. },
|
|
||||||
func,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
if let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() {
|
|
||||||
match value.as_ref() {
|
|
||||||
// if the first part of the attribute is the string literal,
|
|
||||||
// we want to ignore this literal from the lint.
|
|
||||||
// for example: `"{x}".some_method(...)`
|
|
||||||
ast::Expr::StringLiteral(expr_literal)
|
|
||||||
if expr_literal.value.as_slice().contains(literal) =>
|
|
||||||
{
|
{
|
||||||
return false;
|
let ast::Arguments { keywords, args, .. } = &expr.arguments;
|
||||||
}
|
|
||||||
// if the first part of the attribute was the expression we
|
|
||||||
// just went over in the last iteration, then we also want to pass
|
|
||||||
// this over in the lint.
|
|
||||||
// for example: `some_func("{x}").some_method(...)`
|
|
||||||
value if last_expr == Some(value) => {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for keyword in &**keywords {
|
for keyword in &**keywords {
|
||||||
if let Some(ident) = keyword.arg.as_ref() {
|
if let Some(ident) = keyword.arg.as_ref() {
|
||||||
arg_names.insert(ident.as_str());
|
arg_names.insert(&ident.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for arg in &**args {
|
for arg in &**args {
|
||||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = arg {
|
if let ast::Expr::Name(ast::ExprName { id, .. }) = arg {
|
||||||
arg_names.insert(id.as_str());
|
arg_names.insert(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
last_expr.replace(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for f_string in value.f_strings() {
|
for f_string in value.f_strings() {
|
||||||
let mut has_name = false;
|
let mut has_name = false;
|
||||||
for element in f_string.elements.expressions() {
|
for element in f_string.elements.expressions() {
|
||||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
|
if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
|
||||||
if arg_names.contains(id.as_str()) {
|
if arg_names.contains(id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if semantic
|
if semantic
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue