Compare commits

...

4 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
9 changed files with 640 additions and 18 deletions

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

@@ -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
|