Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ruff] Recognize more expressions (RUF027) #15247

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF027_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
foo = 0


## Errors

print("{foo()}")
print("{foo(non_existent)}")
print("{foo.baz}")
print("{foo['bar']}")

print("{foo().qux}")
print("{foo[lorem].ipsum()}")
print("{foo.dolor[sit]().amet}")

print("{id(foo)}")
print("{__path__}")


## No errors

print("{foo if consectetur else adipiscing}")
print("{[foo]}")
print("{ {foo} }")

print("{id}")
print("{id.foo}")
print("{id[foo]}")
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ mod tests {
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_0.py"))]
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_1.py"))]
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))]
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_3.py"))]
#[test_case(Rule::InvalidFormatterSuppressionComment, Path::new("RUF028.py"))]
#[test_case(Rule::UnusedAsync, Path::new("RUF029.py"))]
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
Expand Down
37 changes: 26 additions & 11 deletions crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use rustc_hash::FxHashSet;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::is_dunder;
use ruff_python_literal::format::FormatSpec;
use ruff_python_parser::parse_expression;
use ruff_python_semantic::analyze::logging::is_logger_candidate;
Expand Down Expand Up @@ -204,20 +205,23 @@ fn should_be_fstring(
for f_string in value.f_strings() {
let mut has_name = false;
for element in f_string.elements.expressions() {
if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
let expr = element.expression.as_ref();
if let Some(id) = left_most_name(expr) {
if arg_names.contains(id) {
return false;
}
if semantic
// the parsed expression nodes have incorrect ranges
// so we need to use the range of the literal for the
// lookup in order to get reasonable results.
.simulate_runtime_load_at_location_in_scope(
id,
literal.range(),
semantic.scope_id,
)
.map_or(true, |id| semantic.binding(id).kind.is_builtin())
if !matches!(expr, ast::Expr::Call(_))
&& !is_dunder(id)
&& semantic
// the parsed expression nodes have incorrect ranges
// so we need to use the range of the literal for the
// lookup in order to get reasonable results.
.simulate_runtime_load_at_location_in_scope(
id,
literal.range(),
semantic.scope_id,
)
.map_or(true, |id| semantic.binding(id).kind.is_builtin())
{
return false;
}
Expand All @@ -238,6 +242,17 @@ fn should_be_fstring(
true
}

#[inline]
fn left_most_name(expr: &ast::Expr) -> Option<&ast::name::Name> {
match expr {
ast::Expr::Name(ast::ExprName { id, .. }) => Some(id),
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => left_most_name(value),
ast::Expr::Call(ast::ExprCall { func, .. }) => left_most_name(func),
ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => left_most_name(value),
_ => None,
}
}

// fast check to disqualify any string literal without brackets
#[inline]
fn has_brackets(possible_fstring: &str) -> bool {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
---
RUF027_3.py:6:7: RUF027 [*] Possible f-string without an `f` prefix
|
4 | ## Errors
5 |
6 | print("{foo()}")
| ^^^^^^^^^ RUF027
7 | print("{foo(non_existent)}")
8 | print("{foo.baz}")
|
= help: Add `f` prefix

ℹ Unsafe fix
3 3 |
4 4 | ## Errors
5 5 |
6 |-print("{foo()}")
6 |+print(f"{foo()}")
7 7 | print("{foo(non_existent)}")
8 8 | print("{foo.baz}")
9 9 | print("{foo['bar']}")

RUF027_3.py:7:7: RUF027 [*] Possible f-string without an `f` prefix
|
6 | print("{foo()}")
7 | print("{foo(non_existent)}")
| ^^^^^^^^^^^^^^^^^^^^^ RUF027
8 | print("{foo.baz}")
9 | print("{foo['bar']}")
|
= help: Add `f` prefix

ℹ Unsafe fix
4 4 | ## Errors
5 5 |
6 6 | print("{foo()}")
7 |-print("{foo(non_existent)}")
7 |+print(f"{foo(non_existent)}")
8 8 | print("{foo.baz}")
9 9 | print("{foo['bar']}")
10 10 |

RUF027_3.py:8:7: RUF027 [*] Possible f-string without an `f` prefix
|
6 | print("{foo()}")
7 | print("{foo(non_existent)}")
8 | print("{foo.baz}")
| ^^^^^^^^^^^ RUF027
9 | print("{foo['bar']}")
|
= help: Add `f` prefix

ℹ Unsafe fix
5 5 |
6 6 | print("{foo()}")
7 7 | print("{foo(non_existent)}")
8 |-print("{foo.baz}")
8 |+print(f"{foo.baz}")
9 9 | print("{foo['bar']}")
10 10 |
11 11 | print("{foo().qux}")

RUF027_3.py:9:7: RUF027 [*] Possible f-string without an `f` prefix
|
7 | print("{foo(non_existent)}")
8 | print("{foo.baz}")
9 | print("{foo['bar']}")
| ^^^^^^^^^^^^^^ RUF027
10 |
11 | print("{foo().qux}")
|
= help: Add `f` prefix

ℹ Unsafe fix
6 6 | print("{foo()}")
7 7 | print("{foo(non_existent)}")
8 8 | print("{foo.baz}")
9 |-print("{foo['bar']}")
9 |+print(f"{foo['bar']}")
10 10 |
11 11 | print("{foo().qux}")
12 12 | print("{foo[lorem].ipsum()}")

RUF027_3.py:11:7: RUF027 [*] Possible f-string without an `f` prefix
|
9 | print("{foo['bar']}")
10 |
11 | print("{foo().qux}")
| ^^^^^^^^^^^^^ RUF027
12 | print("{foo[lorem].ipsum()}")
13 | print("{foo.dolor[sit]().amet}")
|
= help: Add `f` prefix

ℹ Unsafe fix
8 8 | print("{foo.baz}")
9 9 | print("{foo['bar']}")
10 10 |
11 |-print("{foo().qux}")
11 |+print(f"{foo().qux}")
12 12 | print("{foo[lorem].ipsum()}")
13 13 | print("{foo.dolor[sit]().amet}")
14 14 |

RUF027_3.py:12:7: RUF027 [*] Possible f-string without an `f` prefix
|
11 | print("{foo().qux}")
12 | print("{foo[lorem].ipsum()}")
| ^^^^^^^^^^^^^^^^^^^^^^ RUF027
13 | print("{foo.dolor[sit]().amet}")
|
= help: Add `f` prefix

ℹ Unsafe fix
9 9 | print("{foo['bar']}")
10 10 |
11 11 | print("{foo().qux}")
12 |-print("{foo[lorem].ipsum()}")
12 |+print(f"{foo[lorem].ipsum()}")
13 13 | print("{foo.dolor[sit]().amet}")
14 14 |
15 15 | print("{id(foo)}")

RUF027_3.py:13:7: RUF027 [*] Possible f-string without an `f` prefix
|
11 | print("{foo().qux}")
12 | print("{foo[lorem].ipsum()}")
13 | print("{foo.dolor[sit]().amet}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF027
14 |
15 | print("{id(foo)}")
|
= help: Add `f` prefix

ℹ Unsafe fix
10 10 |
11 11 | print("{foo().qux}")
12 12 | print("{foo[lorem].ipsum()}")
13 |-print("{foo.dolor[sit]().amet}")
13 |+print(f"{foo.dolor[sit]().amet}")
14 14 |
15 15 | print("{id(foo)}")
16 16 | print("{__path__}")

RUF027_3.py:15:7: RUF027 [*] Possible f-string without an `f` prefix
|
13 | print("{foo.dolor[sit]().amet}")
14 |
15 | print("{id(foo)}")
| ^^^^^^^^^^^ RUF027
16 | print("{__path__}")
|
= help: Add `f` prefix

ℹ Unsafe fix
12 12 | print("{foo[lorem].ipsum()}")
13 13 | print("{foo.dolor[sit]().amet}")
14 14 |
15 |-print("{id(foo)}")
15 |+print(f"{id(foo)}")
16 16 | print("{__path__}")
17 17 |
18 18 |

RUF027_3.py:16:7: RUF027 [*] Possible f-string without an `f` prefix
|
15 | print("{id(foo)}")
16 | print("{__path__}")
| ^^^^^^^^^^^^ RUF027
|
= help: Add `f` prefix

ℹ Unsafe fix
13 13 | print("{foo.dolor[sit]().amet}")
14 14 |
15 15 | print("{id(foo)}")
16 |-print("{__path__}")
16 |+print(f"{__path__}")
17 17 |
18 18 |
19 19 | ## No errors
Loading