Compare commits

...

12 Commits

Author SHA1 Message Date
Dhruv Manilawala
9cb191c496 Update rule docs for preview feature 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
ff2bb6867c Move logic behind preview 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
574843049e Add test case 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
9e7821f4a6 Flag B018 for strings and f-strings which aren't docstrings 2024-05-20 12:52:47 +05:30
Dhruv Manilawala
403f0dccd8 Consider soft keywords for E27 rules (#11446)
## Summary

This is a follow-up PR to #11445 update the `E27` rules to consider soft
keywords as well.

## Test Plan

Add test cases consisting of soft keywords and update the snapshot.
2024-05-20 05:38:06 +00:00
Zanie Blue
46fcd19ca6 Fix division by zero error in ecosystem check (#11469)
e.g.
https://github.com/astral-sh/ruff/actions/runs/9144809516/job/25143076896?pr=11468

<img width="1388" alt="Screenshot 2024-05-19 at 12 02 15 AM"
src="https://github.com/astral-sh/ruff/assets/2586601/0df7cbcd-712c-4ea9-96f5-73f871570525">
2024-05-19 09:08:10 -05:00
Charlie Marsh
d9ec3d56b0 Add some new projects to the ecosystem CI (#11468)
Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-05-19 08:08:38 -05:00
Auguste Lalande
cd87b787d9 Fix windows-ci failure (#11470)
<!--
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

The recent issues with the windows CI seem to be caused by
https://github.com/nextest-rs/nextest/issues/1493. With this
https://github.com/nextest-rs/nextest/issues/1493#issuecomment-2106331574
as a fix.

(Let's see if it works)
2024-05-19 07:25:06 -05:00
Charlie Marsh
dd6d411026 Remove comma from ecosystem checks (#11466)
## Summary

Something's up with this repo -- they added a post-checkout hook? So
let's just remove it for now. We should go through and add a new batch
of repositories some time.
2024-05-18 23:37:56 -04:00
Charlie Marsh
cfceb437a8 Treat escaped newline as valid sequence (#11465)
## Summary

We weren't treating the escaped newline as a valid condition to trigger
the safer fix (add an extra backslash before each invalid escape
sequence).

Closes https://github.com/astral-sh/ruff/issues/11461.
2024-05-19 03:32:32 +00:00
Charlie Marsh
48b0660228 Respect operator precedence in FURB110 (#11464)
## Summary

Ensures that we parenthesize expressions (if necessary) to preserve
operator precedence in `FURB110`.

Closes https://github.com/astral-sh/ruff/issues/11398.
2024-05-19 03:17:11 +00:00
Charlie Marsh
24899efe50 Remove example from tab-indentation (#11462)
## Summary

I think the example is more confusing than helpful, since there's no
visual difference between the tab and space here (even if it rendered
properly).

Closes
https://github.com/astral-sh/ruff/issues/11460#issuecomment-2118397278.
2024-05-17 17:49:16 -04:00
28 changed files with 897 additions and 57 deletions

View File

@@ -167,6 +167,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc

View File

@@ -6,8 +6,8 @@ class Foo2:
"""abc"""
a = 2
"str" # Str (no raise)
f"{int}" # JoinedStr (no raise)
"str" # StringLiteral
f"{int}" # FString
1j # Number (complex)
1 # Number (int)
1.0 # Number (float)
@@ -34,8 +34,8 @@ def foo1():
def foo2():
"""my docstring"""
a = 2
"str" # Str (no raise)
f"{int}" # JoinedStr (no raise)
"str" # StringLiteral
f"{int}" # FString
1j # Number (complex)
1 # Number (int)
1.0 # Number (float)

View File

@@ -0,0 +1,93 @@
# These test cases not only check for `B018` but also verifies that the semantic model
# correctly identifies certain strings as attribute docstring. And, by way of not
# raising the `B018` violation, it can be verified.
a: int
"a: docstring"
b = 1
"b: docstring" " continue"
"b: not a docstring"
c: int = 1
"c: docstring"
_a: int
"_a: docstring"
if True:
d = 1
"d: not a docstring"
(e := 1)
"e: not a docstring"
f = 0
f += 1
"f: not a docstring"
g.h = 1
"g.h: not a docstring"
(i) = 1
"i: docstring"
(j): int = 1
"j: docstring"
(k): int
"k: docstring"
l = m = 1
"l m: not a docstring"
n.a = n.b = n.c = 1
"n.*: not a docstring"
(o, p) = (1, 2)
"o p: not a docstring"
[q, r] = [1, 2]
"q r: not a docstring"
*s = 1
"s: not a docstring"
class Foo:
a = 1
"Foo.a: docstring"
b: int
"Foo.b: docstring"
"Foo.b: not a docstring"
c: int = 1
"Foo.c: docstring"
def __init__(self) -> None:
# This is actually a docstring but we currently don't detect it.
self.x = 1
"self.x: not a docstring"
t = 2
"t: not a docstring"
def random(self):
self.y = 2
"self.y: not a docstring"
u = 2
"u: not a docstring"
def add(self, y: int):
self.x += y
def function():
v = 2
"v: not a docstring"
function.a = 1
"function.a: not a docstring"

View File

@@ -63,3 +63,16 @@ if (a and
#: Okay
def f():
return 1
# Soft keywords
#: E271
type Number = int
#: E273
type Number = int
#: E275
match(foo):
case(1):
pass

View File

@@ -46,3 +46,15 @@ regex = '\\\_'
#: W605:1:7
u'foo\ bar'
#: W605:1:13
(
"foo \
bar \. baz"
)
#: W605:1:6
"foo \. bar \t"
#: W605:1:13
"foo \t bar \."

View File

@@ -38,3 +38,12 @@ z = (
else
y
)
# FURB110
z = (
x
if x
else y
if y > 0
else None
)

View File

@@ -13,6 +13,7 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
@@ -62,6 +63,7 @@ mod tests {
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018_attribute_docstring.py"))]
#[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
@@ -73,6 +75,26 @@ mod tests {
Ok(())
}
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018_attribute_docstring.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";

View File

@@ -16,6 +16,9 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// by mistake. Assign a useless expression to a variable, or remove it
/// entirely.
///
/// In [preview mode], this rule will also flag string literals and f-strings that
/// are not used as a docstring or an attribute docstring.
///
/// ## Example
/// ```python
/// 1 + 1
@@ -45,6 +48,8 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// with errors.ExceptionRaisedContext():
/// _ = obj.attribute
/// ```
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[violation]
pub struct UselessExpression {
kind: Kind,
@@ -69,15 +74,16 @@ impl Violation for UselessExpression {
/// B018
pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore comparisons, as they're handled by `useless_comparison`.
if value.is_compare_expr() {
if matches!(value, Expr::Compare(_) | Expr::EllipsisLiteral(_)) {
return;
}
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::FString(_) | Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
) {
if checker.settings.preview.is_enabled() {
if checker.semantic().in_pep_257_docstring() || checker.semantic().in_attribute_docstring()
{
return;
}
} else if matches!(value, Expr::StringLiteral(_) | Expr::FString(_)) {
return;
}

View File

@@ -3,8 +3,8 @@ source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
9 | "str" # Str (no raise)
10 | f"{int}" # JoinedStr (no raise)
9 | "str" # StringLiteral
10 | f"{int}" # FString
11 | 1j # Number (complex)
| ^^ B018
12 | 1 # Number (int)
@@ -13,7 +13,7 @@ B018.py:11:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:12:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
10 | f"{int}" # JoinedStr (no raise)
10 | f"{int}" # FString
11 | 1j # Number (complex)
12 | 1 # Number (int)
| ^ B018
@@ -117,8 +117,8 @@ B018.py:27:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:39:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
37 | "str" # Str (no raise)
38 | f"{int}" # JoinedStr (no raise)
37 | "str" # StringLiteral
38 | f"{int}" # FString
39 | 1j # Number (complex)
| ^^ B018
40 | 1 # Number (int)
@@ -127,7 +127,7 @@ B018.py:39:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:40:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
38 | f"{int}" # JoinedStr (no raise)
38 | f"{int}" # FString
39 | 1j # Number (complex)
40 | 1 # Number (int)
| ^ B018
@@ -254,5 +254,3 @@ B018.py:65:5: B018 Found useless expression. Either assign it to a variable or r
65 | "foo" + "bar" # BinOp (raise)
| ^^^^^^^^^^^^^ B018
|

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018_attribute_docstring.py:22:2: B018 Found useless expression. Either assign it to a variable or remove it.
|
20 | "d: not a docstring"
21 |
22 | (e := 1)
| ^^^^^^ B018
23 | "e: not a docstring"
|

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.ipynb:5:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
3 | x
4 | # Only skip the last expression
5 | x # B018
| ^ B018
6 | x
7 | # Nested expressions isn't relevant
|
B018.ipynb:9:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
7 | # Nested expressions isn't relevant
8 | if True:
9 | x
| ^ B018
10 | # Semicolons shouldn't affect the output
11 | x;
|
B018.ipynb:13:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
11 | x;
12 | # Semicolons with multiple expressions
13 | x; x
| ^ B018
14 | # Comments, newlines and whitespace
15 | x # comment
|

View File

@@ -0,0 +1,295 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.py:10:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
8 | a = 2
9 | "str" # StringLiteral
10 | f"{int}" # FString
| ^^^^^^^^ B018
11 | 1j # Number (complex)
12 | 1 # Number (int)
|
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
9 | "str" # StringLiteral
10 | f"{int}" # FString
11 | 1j # Number (complex)
| ^^ B018
12 | 1 # Number (int)
13 | 1.0 # Number (float)
|
B018.py:12:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
10 | f"{int}" # FString
11 | 1j # Number (complex)
12 | 1 # Number (int)
| ^ B018
13 | 1.0 # Number (float)
14 | b"foo" # Binary
|
B018.py:13:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
11 | 1j # Number (complex)
12 | 1 # Number (int)
13 | 1.0 # Number (float)
| ^^^ B018
14 | b"foo" # Binary
15 | True # NameConstant (True)
|
B018.py:14:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
12 | 1 # Number (int)
13 | 1.0 # Number (float)
14 | b"foo" # Binary
| ^^^^^^ B018
15 | True # NameConstant (True)
16 | False # NameConstant (False)
|
B018.py:15:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
13 | 1.0 # Number (float)
14 | b"foo" # Binary
15 | True # NameConstant (True)
| ^^^^ B018
16 | False # NameConstant (False)
17 | None # NameConstant (None)
|
B018.py:16:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
14 | b"foo" # Binary
15 | True # NameConstant (True)
16 | False # NameConstant (False)
| ^^^^^ B018
17 | None # NameConstant (None)
18 | [1, 2] # list
|
B018.py:17:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
15 | True # NameConstant (True)
16 | False # NameConstant (False)
17 | None # NameConstant (None)
| ^^^^ B018
18 | [1, 2] # list
19 | {1, 2} # set
|
B018.py:18:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
16 | False # NameConstant (False)
17 | None # NameConstant (None)
18 | [1, 2] # list
| ^^^^^^ B018
19 | {1, 2} # set
20 | {"foo": "bar"} # dict
|
B018.py:19:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
17 | None # NameConstant (None)
18 | [1, 2] # list
19 | {1, 2} # set
| ^^^^^^ B018
20 | {"foo": "bar"} # dict
|
B018.py:20:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
18 | [1, 2] # list
19 | {1, 2} # set
20 | {"foo": "bar"} # dict
| ^^^^^^^^^^^^^^ B018
|
B018.py:24:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
23 | class Foo3:
24 | 123
| ^^^ B018
25 | a = 2
26 | "str"
|
B018.py:27:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
25 | a = 2
26 | "str"
27 | 1
| ^ B018
|
B018.py:37:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
35 | """my docstring"""
36 | a = 2
37 | "str" # StringLiteral
| ^^^^^ B018
38 | f"{int}" # FString
39 | 1j # Number (complex)
|
B018.py:38:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
36 | a = 2
37 | "str" # StringLiteral
38 | f"{int}" # FString
| ^^^^^^^^ B018
39 | 1j # Number (complex)
40 | 1 # Number (int)
|
B018.py:39:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
37 | "str" # StringLiteral
38 | f"{int}" # FString
39 | 1j # Number (complex)
| ^^ B018
40 | 1 # Number (int)
41 | 1.0 # Number (float)
|
B018.py:40:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
38 | f"{int}" # FString
39 | 1j # Number (complex)
40 | 1 # Number (int)
| ^ B018
41 | 1.0 # Number (float)
42 | b"foo" # Binary
|
B018.py:41:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
39 | 1j # Number (complex)
40 | 1 # Number (int)
41 | 1.0 # Number (float)
| ^^^ B018
42 | b"foo" # Binary
43 | True # NameConstant (True)
|
B018.py:42:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
40 | 1 # Number (int)
41 | 1.0 # Number (float)
42 | b"foo" # Binary
| ^^^^^^ B018
43 | True # NameConstant (True)
44 | False # NameConstant (False)
|
B018.py:43:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
41 | 1.0 # Number (float)
42 | b"foo" # Binary
43 | True # NameConstant (True)
| ^^^^ B018
44 | False # NameConstant (False)
45 | None # NameConstant (None)
|
B018.py:44:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
42 | b"foo" # Binary
43 | True # NameConstant (True)
44 | False # NameConstant (False)
| ^^^^^ B018
45 | None # NameConstant (None)
46 | [1, 2] # list
|
B018.py:45:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
43 | True # NameConstant (True)
44 | False # NameConstant (False)
45 | None # NameConstant (None)
| ^^^^ B018
46 | [1, 2] # list
47 | {1, 2} # set
|
B018.py:46:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
44 | False # NameConstant (False)
45 | None # NameConstant (None)
46 | [1, 2] # list
| ^^^^^^ B018
47 | {1, 2} # set
48 | {"foo": "bar"} # dict
|
B018.py:47:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
45 | None # NameConstant (None)
46 | [1, 2] # list
47 | {1, 2} # set
| ^^^^^^ B018
48 | {"foo": "bar"} # dict
|
B018.py:48:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
46 | [1, 2] # list
47 | {1, 2} # set
48 | {"foo": "bar"} # dict
| ^^^^^^^^^^^^^^ B018
|
B018.py:52:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
51 | def foo3():
52 | 123
| ^^^ B018
53 | a = 2
54 | "str"
|
B018.py:54:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
52 | 123
53 | a = 2
54 | "str"
| ^^^^^ B018
55 | 3
|
B018.py:55:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
53 | a = 2
54 | "str"
55 | 3
| ^ B018
|
B018.py:63:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
62 | def foo5():
63 | foo.bar # Attribute (raise)
| ^^^^^^^ B018
64 | object().__class__ # Attribute (raise)
65 | "foo" + "bar" # BinOp (raise)
|
B018.py:64:5: B018 Found useless attribute access. Either assign it to a variable or remove it.
|
62 | def foo5():
63 | foo.bar # Attribute (raise)
64 | object().__class__ # Attribute (raise)
| ^^^^^^^^^^^^^^^^^^ B018
65 | "foo" + "bar" # BinOp (raise)
|
B018.py:65:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
63 | foo.bar # Attribute (raise)
64 | object().__class__ # Attribute (raise)
65 | "foo" + "bar" # BinOp (raise)
| ^^^^^^^^^^^^^ B018
|

View File

@@ -0,0 +1,165 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018_attribute_docstring.py:10:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
8 | b = 1
9 | "b: docstring" " continue"
10 | "b: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
11 |
12 | c: int = 1
|
B018_attribute_docstring.py:20:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
18 | if True:
19 | d = 1
20 | "d: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
21 |
22 | (e := 1)
|
B018_attribute_docstring.py:22:2: B018 Found useless expression. Either assign it to a variable or remove it.
|
20 | "d: not a docstring"
21 |
22 | (e := 1)
| ^^^^^^ B018
23 | "e: not a docstring"
|
B018_attribute_docstring.py:23:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
22 | (e := 1)
23 | "e: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
24 |
25 | f = 0
|
B018_attribute_docstring.py:27:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
25 | f = 0
26 | f += 1
27 | "f: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
28 |
29 | g.h = 1
|
B018_attribute_docstring.py:30:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
29 | g.h = 1
30 | "g.h: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
31 |
32 | (i) = 1
|
B018_attribute_docstring.py:42:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
41 | l = m = 1
42 | "l m: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
43 |
44 | n.a = n.b = n.c = 1
|
B018_attribute_docstring.py:45:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
44 | n.a = n.b = n.c = 1
45 | "n.*: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
46 |
47 | (o, p) = (1, 2)
|
B018_attribute_docstring.py:48:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
47 | (o, p) = (1, 2)
48 | "o p: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
49 |
50 | [q, r] = [1, 2]
|
B018_attribute_docstring.py:51:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
50 | [q, r] = [1, 2]
51 | "q r: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
52 |
53 | *s = 1
|
B018_attribute_docstring.py:54:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
53 | *s = 1
54 | "s: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
|
B018_attribute_docstring.py:63:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
61 | b: int
62 | "Foo.b: docstring"
63 | "Foo.b: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^ B018
64 |
65 | c: int = 1
|
B018_attribute_docstring.py:71:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
69 | # This is actually a docstring but we currently don't detect it.
70 | self.x = 1
71 | "self.x: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B018
72 |
73 | t = 2
|
B018_attribute_docstring.py:74:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
73 | t = 2
74 | "t: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
75 |
76 | def random(self):
|
B018_attribute_docstring.py:78:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
76 | def random(self):
77 | self.y = 2
78 | "self.y: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B018
79 |
80 | u = 2
|
B018_attribute_docstring.py:81:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
80 | u = 2
81 | "u: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
82 |
83 | def add(self, y: int):
|
B018_attribute_docstring.py:89:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
87 | def function():
88 | v = 2
89 | "v: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
|
B018_attribute_docstring.py:93:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
92 | function.a = 1
93 | "function.a: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B018
|

View File

@@ -181,6 +181,7 @@ fn check(
// If we're at the end of line, skip.
if matches!(next_char, '\n' | '\r') {
contains_valid_escape_sequence = true;
continue;
}

View File

@@ -52,7 +52,7 @@ pub(crate) fn missing_whitespace_after_keyword(
let tok0_kind = tok0.kind();
let tok1_kind = tok1.kind();
if tok0_kind.is_non_soft_keyword()
if tok0_kind.is_keyword()
&& !(tok0_kind.is_singleton()
|| matches!(tok0_kind, TokenKind::Async | TokenKind::Await)
|| tok0_kind == TokenKind::Except && tok1_kind == TokenKind::Star

View File

@@ -445,7 +445,7 @@ impl LogicalLinesBuilder {
if matches!(kind, TokenKind::Comma | TokenKind::Semi | TokenKind::Colon) {
line.flags.insert(TokenFlags::PUNCTUATION);
} else if kind.is_non_soft_keyword() {
} else if kind.is_keyword() {
line.flags.insert(TokenFlags::KEYWORD);
}

View File

@@ -127,8 +127,8 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic
let mut after_keyword = false;
for token in line.tokens() {
let is_non_soft_keyword = token.kind().is_non_soft_keyword();
if is_non_soft_keyword {
let is_keyword = token.kind().is_keyword();
if is_keyword {
if !after_keyword {
match line.leading_whitespace(token) {
(Whitespace::Tab, offset) => {
@@ -184,6 +184,6 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic
}
}
after_keyword = is_non_soft_keyword;
after_keyword = is_keyword;
}
}

View File

@@ -11,18 +11,6 @@ use ruff_text_size::{TextRange, TextSize};
/// According to [PEP 8], spaces are preferred over tabs (unless used to remain
/// consistent with code that is already indented with tabs).
///
/// ## Example
/// ```python
/// if True:
/// a = 1
/// ```
///
/// Use instead:
/// ```python
/// if True:
/// a = 1
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.

View File

@@ -190,4 +190,22 @@ E27.py:35:14: E271 [*] Multiple spaces after keyword
37 37 | from w import(e, f)
38 38 | #: E275
E27.py:70:5: E271 [*] Multiple spaces after keyword
|
69 | #: E271
70 | type Number = int
| ^^ E271
71 |
72 | #: E273
|
= help: Replace with single space
Safe fix
67 67 | # Soft keywords
68 68 |
69 69 | #: E271
70 |-type Number = int
70 |+type Number = int
71 71 |
72 72 | #: E273
73 73 | type Number = int

View File

@@ -106,4 +106,22 @@ E27.py:30:10: E273 [*] Tab after keyword
32 32 | from u import (a, b)
33 33 | from v import c, d
E27.py:73:5: E273 [*] Tab after keyword
|
72 | #: E273
73 | type Number = int
| ^^^^ E273
74 |
75 | #: E275
|
= help: Replace with single space
Safe fix
70 70 | type Number = int
71 71 |
72 72 | #: E273
73 |-type Number = int
73 |+type Number = int
74 74 |
75 75 | #: E275
76 76 | match(foo):

View File

@@ -106,4 +106,39 @@ E27.py:54:5: E275 [*] Missing whitespace after keyword
56 56 | def f():
57 57 | print((yield))
E27.py:76:1: E275 [*] Missing whitespace after keyword
|
75 | #: E275
76 | match(foo):
| ^^^^^ E275
77 | case(1):
78 | pass
|
= help: Added missing whitespace after keyword
Safe fix
73 73 | type Number = int
74 74 |
75 75 | #: E275
76 |-match(foo):
76 |+match (foo):
77 77 | case(1):
78 78 | pass
E27.py:77:5: E275 [*] Missing whitespace after keyword
|
75 | #: E275
76 | match(foo):
77 | case(1):
| ^^^^ E275
78 | pass
|
= help: Added missing whitespace after keyword
Safe fix
74 74 |
75 75 | #: E275
76 76 | match(foo):
77 |- case(1):
77 |+ case (1):
78 78 | pass

View File

@@ -145,6 +145,8 @@ W605_0.py:48:6: W605 [*] Invalid escape sequence: `\ `
47 | #: W605:1:7
48 | u'foo\ bar'
| ^^ W605
49 |
50 | #: W605:1:13
|
= help: Use a raw string literal
@@ -154,5 +156,61 @@ W605_0.py:48:6: W605 [*] Invalid escape sequence: `\ `
47 47 | #: W605:1:7
48 |-u'foo\ bar'
48 |+r'foo\ bar'
49 49 |
50 50 | #: W605:1:13
51 51 | (
W605_0.py:53:9: W605 [*] Invalid escape sequence: `\.`
|
51 | (
52 | "foo \
53 | bar \. baz"
| ^^ W605
54 | )
|
= help: Add backslash to escape sequence
Safe fix
50 50 | #: W605:1:13
51 51 | (
52 52 | "foo \
53 |- bar \. baz"
53 |+ bar \\. baz"
54 54 | )
55 55 |
56 56 | #: W605:1:6
W605_0.py:57:6: W605 [*] Invalid escape sequence: `\.`
|
56 | #: W605:1:6
57 | "foo \. bar \t"
| ^^ W605
58 |
59 | #: W605:1:13
|
= help: Add backslash to escape sequence
Safe fix
54 54 | )
55 55 |
56 56 | #: W605:1:6
57 |-"foo \. bar \t"
57 |+"foo \\. bar \t"
58 58 |
59 59 | #: W605:1:13
60 60 | "foo \t bar \."
W605_0.py:60:13: W605 [*] Invalid escape sequence: `\.`
|
59 | #: W605:1:13
60 | "foo \t bar \."
| ^^ W605
|
= help: Add backslash to escape sequence
Safe fix
57 57 | "foo \. bar \t"
58 58 |
59 59 | #: W605:1:13
60 |-"foo \t bar \."
60 |+"foo \t bar \\."

View File

@@ -1,9 +1,14 @@
use std::borrow::Cow;
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::Expr;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -64,29 +69,13 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &mut Checker, if_expr: &ast
let mut diagnostic = Diagnostic::new(IfExpInsteadOfOrOperator, *range);
// Grab the range of the `test` and `orelse` expressions.
let left = parenthesized_range(
test.into(),
if_expr.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(test.range());
let right = parenthesized_range(
orelse.into(),
if_expr.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(orelse.range());
// Replace with `{test} or {orelse}`.
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(
format!(
"{} or {}",
checker.locator().slice(left),
checker.locator().slice(right),
parenthesize_test(test, if_expr, checker.indexer(), checker.locator()),
parenthesize_test(orelse, if_expr, checker.indexer(), checker.locator()),
),
if_expr.range(),
),
@@ -99,3 +88,30 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &mut Checker, if_expr: &ast
checker.diagnostics.push(diagnostic);
}
/// Parenthesize an expression for use in an `or` operator (e.g., parenthesize `x` in `x or y`),
/// if it's required to maintain the correct order of operations.
///
/// If the expression is already parenthesized, it will be returned as-is regardless of whether
/// the parentheses are required.
///
/// See: <https://docs.python.org/3/reference/expressions.html#operator-precedence>
fn parenthesize_test<'a>(
expr: &Expr,
if_expr: &ast::ExprIf,
indexer: &Indexer,
locator: &Locator<'a>,
) -> Cow<'a, str> {
if let Some(range) = parenthesized_range(
expr.into(),
if_expr.into(),
indexer.comment_ranges(),
locator.contents(),
) {
Cow::Borrowed(locator.slice(range))
} else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) {
Cow::Owned(format!("({})", locator.slice(expr.range())))
} else {
Cow::Borrowed(locator.slice(expr.range()))
}
}

View File

@@ -177,3 +177,33 @@ FURB110.py:34:5: FURB110 [*] Replace ternary `if` expression with `or` operator
39 |- y
34 |+ x or y
40 35 | )
41 36 |
42 37 | # FURB110
FURB110.py:44:5: FURB110 [*] Replace ternary `if` expression with `or` operator
|
42 | # FURB110
43 | z = (
44 | x
| _____^
45 | | if x
46 | | else y
47 | | if y > 0
48 | | else None
| |_____________^ FURB110
49 | )
|
= help: Replace with `or` operator
Safe fix
41 41 |
42 42 | # FURB110
43 43 | z = (
44 |- x
45 |- if x
46 |- else y
44 |+ x or (y
47 45 | if y > 0
48 |- else None
46 |+ else None)
49 47 | )

View File

@@ -145,7 +145,20 @@ def markdown_check_result(result: Result) -> str:
# Limit the number of items displayed per project to between 10 and 50
# based on the proportion of total changes present in this project
max_display_per_project = max(10, int((project_changes / total_changes) * 50))
max_display_per_project = max(
10,
int(
(
# TODO(zanieb): We take the `max` here to avoid division by zero errors where
# `total_changes` is zero but `total_affected_rules` is non-zero so we did not
# skip display. This shouldn't really happen and indicates a problem in the
# calculation of these values. Instead of skipping entirely when `total_changes`
# is zero, we'll attempt to report the results to help diagnose the problem.
project_changes / max(total_changes, 1)
)
* 50
),
)
# Limit the number of items displayed per rule to between 5 and the max for
# the project based on the number of rules affected (less rules, more per rule)

View File

@@ -30,7 +30,6 @@ DEFAULT_TARGETS = [
repo=Repository(owner="bokeh", name="bokeh", ref="branch-3.3"),
check_options=CheckOptions(select="ALL"),
),
Project(repo=Repository(owner="commaai", name="openpilot", ref="master")),
Project(
repo=Repository(owner="demisto", name="content", ref="master"),
format_options=FormatOptions(
@@ -117,4 +116,11 @@ DEFAULT_TARGETS = [
],
},
),
Project(repo=Repository(owner="agronholm", name="anyio", ref="master")),
Project(repo=Repository(owner="python-trio", name="trio", ref="master")),
Project(repo=Repository(owner="wntrblm", name="nox", ref="main")),
Project(repo=Repository(owner="pytest-dev", name="pytest", ref="main")),
Project(repo=Repository(owner="encode", name="httpx", ref="master")),
Project(repo=Repository(owner="mesonbuild", name="meson-python", ref="main")),
Project(repo=Repository(owner="pdm-project", name="pdm", ref="main")),
]

View File

@@ -74,7 +74,6 @@ KNOWN_FORMATTING_VIOLATIONS = [
"redundant-backslash",
"shebang-leading-whitespace",
"surrounding-whitespace",
"tab-indentation",
"too-few-spaces-before-inline-comment",
"too-many-blank-lines",
"too-many-boolean-expressions",

View File

@@ -124,7 +124,6 @@ REPOSITORIES: list[Repository] = [
Repository("aws", "aws-sam-cli", "develop"),
Repository("bloomberg", "pytest-memray", "main"),
Repository("bokeh", "bokeh", "branch-3.3", select="ALL"),
Repository("commaai", "openpilot", "master"),
Repository("demisto", "content", "master"),
Repository("docker", "docker-py", "main"),
Repository("freedomofpress", "securedrop", "develop"),