diff --git a/README.md b/README.md index ec5406595b..f9318f0ff5 100644 --- a/README.md +++ b/README.md @@ -526,6 +526,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/ | B017 | NoAssertRaisesException | `assertRaises(Exception)` should be considered evil | | | B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | | | B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | | +| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | | | B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | | | B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | | diff --git a/resources/test/fixtures/B021.py b/resources/test/fixtures/B021.py new file mode 100644 index 0000000000..e34f9a3812 --- /dev/null +++ b/resources/test/fixtures/B021.py @@ -0,0 +1,76 @@ +f""" +Should emit: +B021 - on lines 14, 22, 30, 38, 46, 54, 62, 70, 73 +""" + +VARIABLE = "world" + + +def foo1(): + """hello world!""" + + +def foo2(): + f"""hello {VARIABLE}!""" + + +class bar1: + """hello world!""" + + +class bar2: + f"""hello {VARIABLE}!""" + + +def foo1(): + """hello world!""" + + +def foo2(): + f"""hello {VARIABLE}!""" + + +class bar1: + """hello world!""" + + +class bar2: + f"""hello {VARIABLE}!""" + + +def foo1(): + "hello world!" + + +def foo2(): + f"hello {VARIABLE}!" + + +class bar1: + "hello world!" + + +class bar2: + f"hello {VARIABLE}!" + + +def foo1(): + "hello world!" + + +def foo2(): + f"hello {VARIABLE}!" + + +class bar1: + "hello world!" + + +class bar2: + f"hello {VARIABLE}!" + + +def baz(): + f"""I'm probably a docstring: {VARIABLE}!""" + print(f"""I'm a normal string""") + f"""Don't detect me!""" diff --git a/src/check_ast.rs b/src/check_ast.rs index c90fd049b9..4c1b0543fe 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -875,6 +875,9 @@ where let prev_visible_scope = self.visible_scope.clone(); match &stmt.node { StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => { + if self.settings.enabled.contains(&CheckCode::B021) { + flake8_bugbear::plugins::f_string_docstring(self, body); + } let definition = docstrings::extraction::extract( &self.visible_scope, stmt, @@ -894,6 +897,9 @@ where )); } StmtKind::ClassDef { body, .. } => { + if self.settings.enabled.contains(&CheckCode::B021) { + flake8_bugbear::plugins::f_string_docstring(self, body); + } let definition = docstrings::extraction::extract( &self.visible_scope, stmt, @@ -2245,6 +2251,9 @@ impl<'a> Checker<'a> { where 'b: 'a, { + if self.settings.enabled.contains(&CheckCode::B021) { + flake8_bugbear::plugins::f_string_docstring(self, python_ast); + } let docstring = docstrings::extraction::docstring_from(python_ast); self.definitions.push(( Definition { diff --git a/src/checks.rs b/src/checks.rs index 2061100e96..f6c6359dfb 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -94,6 +94,7 @@ pub enum CheckCode { B017, B018, B019, + B021, B025, B026, // flake8-comprehensions @@ -393,6 +394,7 @@ pub enum CheckKind { NoAssertRaisesException, UselessExpression, CachedInstanceMethod, + FStringDocstring, DuplicateTryBlockException(String), StarArgUnpackingAfterKeywordArg, // flake8-comprehensions @@ -632,6 +634,7 @@ impl CheckCode { CheckCode::B017 => CheckKind::NoAssertRaisesException, CheckCode::B018 => CheckKind::UselessExpression, CheckCode::B019 => CheckKind::CachedInstanceMethod, + CheckCode::B021 => CheckKind::FStringDocstring, CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()), CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg, // flake8-comprehensions @@ -872,6 +875,7 @@ impl CheckCode { CheckCode::B017 => CheckCategory::Flake8Bugbear, CheckCode::B018 => CheckCategory::Flake8Bugbear, CheckCode::B019 => CheckCategory::Flake8Bugbear, + CheckCode::B021 => CheckCategory::Flake8Bugbear, CheckCode::B025 => CheckCategory::Flake8Bugbear, CheckCode::B026 => CheckCategory::Flake8Bugbear, CheckCode::C400 => CheckCategory::Flake8Comprehensions, @@ -1075,6 +1079,7 @@ impl CheckKind { CheckKind::NoAssertRaisesException => &CheckCode::B017, CheckKind::UselessExpression => &CheckCode::B018, CheckKind::CachedInstanceMethod => &CheckCode::B019, + CheckKind::FStringDocstring => &CheckCode::B021, CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025, CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026, // flake8-comprehensions @@ -1442,6 +1447,9 @@ impl CheckKind { CheckKind::CachedInstanceMethod => "Use of `functools.lru_cache` or `functools.cache` \ on methods can lead to memory leaks" .to_string(), + CheckKind::FStringDocstring => "f-string used as docstring. This will be interpreted \ + by python as a joined string rather than a docstring." + .to_string(), CheckKind::DuplicateTryBlockException(name) => { format!("try-except block with duplicate exception `{name}`") } diff --git a/src/checks_gen.rs b/src/checks_gen.rs index ed7631f98a..1acdd2066f 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -55,6 +55,7 @@ pub enum CheckCodePrefix { B018, B019, B02, + B021, B025, B026, C, @@ -380,6 +381,7 @@ impl CheckCodePrefix { CheckCode::B017, CheckCode::B018, CheckCode::B019, + CheckCode::B021, CheckCode::B025, CheckCode::B026, ], @@ -401,6 +403,7 @@ impl CheckCodePrefix { CheckCode::B017, CheckCode::B018, CheckCode::B019, + CheckCode::B021, CheckCode::B025, CheckCode::B026, ], @@ -442,7 +445,8 @@ impl CheckCodePrefix { CheckCodePrefix::B017 => vec![CheckCode::B017], CheckCodePrefix::B018 => vec![CheckCode::B018], CheckCodePrefix::B019 => vec![CheckCode::B019], - CheckCodePrefix::B02 => vec![CheckCode::B025, CheckCode::B026], + CheckCodePrefix::B02 => vec![CheckCode::B021, CheckCode::B025, CheckCode::B026], + CheckCodePrefix::B021 => vec![CheckCode::B021], CheckCodePrefix::B025 => vec![CheckCode::B025], CheckCodePrefix::B026 => vec![CheckCode::B026], CheckCodePrefix::C => vec![ @@ -1184,6 +1188,7 @@ impl CheckCodePrefix { CheckCodePrefix::B018 => PrefixSpecificity::Explicit, CheckCodePrefix::B019 => PrefixSpecificity::Explicit, CheckCodePrefix::B02 => PrefixSpecificity::Tens, + CheckCodePrefix::B021 => PrefixSpecificity::Explicit, CheckCodePrefix::B025 => PrefixSpecificity::Explicit, CheckCodePrefix::B026 => PrefixSpecificity::Explicit, CheckCodePrefix::C => PrefixSpecificity::Category, @@ -1338,15 +1343,6 @@ impl CheckCodePrefix { CheckCodePrefix::I0 => PrefixSpecificity::Hundreds, CheckCodePrefix::I00 => PrefixSpecificity::Tens, CheckCodePrefix::I001 => PrefixSpecificity::Explicit, - CheckCodePrefix::S => PrefixSpecificity::Category, - CheckCodePrefix::S1 => PrefixSpecificity::Hundreds, - CheckCodePrefix::S10 => PrefixSpecificity::Tens, - CheckCodePrefix::S101 => PrefixSpecificity::Explicit, - CheckCodePrefix::S102 => PrefixSpecificity::Explicit, - CheckCodePrefix::S104 => PrefixSpecificity::Explicit, - CheckCodePrefix::S105 => PrefixSpecificity::Explicit, - CheckCodePrefix::S106 => PrefixSpecificity::Explicit, - CheckCodePrefix::S107 => PrefixSpecificity::Explicit, CheckCodePrefix::M => PrefixSpecificity::Category, CheckCodePrefix::M0 => PrefixSpecificity::Hundreds, CheckCodePrefix::M00 => PrefixSpecificity::Tens, @@ -1383,6 +1379,15 @@ impl CheckCodePrefix { CheckCodePrefix::RUF001 => PrefixSpecificity::Explicit, CheckCodePrefix::RUF002 => PrefixSpecificity::Explicit, CheckCodePrefix::RUF003 => PrefixSpecificity::Explicit, + CheckCodePrefix::S => PrefixSpecificity::Category, + CheckCodePrefix::S1 => PrefixSpecificity::Hundreds, + CheckCodePrefix::S10 => PrefixSpecificity::Tens, + CheckCodePrefix::S101 => PrefixSpecificity::Explicit, + CheckCodePrefix::S102 => PrefixSpecificity::Explicit, + CheckCodePrefix::S104 => PrefixSpecificity::Explicit, + CheckCodePrefix::S105 => PrefixSpecificity::Explicit, + CheckCodePrefix::S106 => PrefixSpecificity::Explicit, + CheckCodePrefix::S107 => PrefixSpecificity::Explicit, CheckCodePrefix::T => PrefixSpecificity::Category, CheckCodePrefix::T2 => PrefixSpecificity::Hundreds, CheckCodePrefix::T20 => PrefixSpecificity::Tens, diff --git a/src/flake8_bugbear/plugins/f_string_docstring.rs b/src/flake8_bugbear/plugins/f_string_docstring.rs new file mode 100644 index 0000000000..9b5ce50842 --- /dev/null +++ b/src/flake8_bugbear/plugins/f_string_docstring.rs @@ -0,0 +1,19 @@ +use rustpython_ast::{ExprKind, Stmt, StmtKind}; + +use crate::ast::types::Range; +use crate::check_ast::Checker; +use crate::checks::{Check, CheckKind}; + +/// B021 +pub fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) { + if let Some(stmt) = body.first() { + if let StmtKind::Expr { value } = &stmt.node { + if let ExprKind::JoinedStr { .. } = value.node { + checker.add_check(Check::new( + CheckKind::FStringDocstring, + Range::from_located(stmt), + )); + } + } + } +} diff --git a/src/flake8_bugbear/plugins/mod.rs b/src/flake8_bugbear/plugins/mod.rs index 1860767250..e722690ec9 100644 --- a/src/flake8_bugbear/plugins/mod.rs +++ b/src/flake8_bugbear/plugins/mod.rs @@ -4,6 +4,7 @@ pub use assignment_to_os_environ::assignment_to_os_environ; pub use cached_instance_method::cached_instance_method; pub use cannot_raise_literal::cannot_raise_literal; pub use duplicate_exceptions::{duplicate_exceptions, duplicate_handler_exceptions}; +pub use f_string_docstring::f_string_docstring; pub use function_call_argument_default::function_call_argument_default; pub use getattr_with_constant::getattr_with_constant; pub use mutable_argument_default::mutable_argument_default; @@ -23,6 +24,7 @@ mod assignment_to_os_environ; mod cached_instance_method; mod cannot_raise_literal; mod duplicate_exceptions; +mod f_string_docstring; mod function_call_argument_default; mod getattr_with_constant; mod mutable_argument_default; diff --git a/src/linter.rs b/src/linter.rs index 16e43b03fb..8d990712e9 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -343,6 +343,7 @@ mod tests { #[test_case(CheckCode::B017, Path::new("B017.py"); "B017")] #[test_case(CheckCode::B018, Path::new("B018.py"); "B018")] #[test_case(CheckCode::B019, Path::new("B019.py"); "B019")] + #[test_case(CheckCode::B021, Path::new("B021.py"); "B021")] #[test_case(CheckCode::B025, Path::new("B025.py"); "B025")] #[test_case(CheckCode::B026, Path::new("B026.py"); "B026")] #[test_case(CheckCode::C400, Path::new("C400.py"); "C400")] diff --git a/src/snapshots/ruff__linter__tests__B021_B021.py.snap b/src/snapshots/ruff__linter__tests__B021_B021.py.snap new file mode 100644 index 0000000000..530fa68ef2 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__B021_B021.py.snap @@ -0,0 +1,85 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: FStringDocstring + location: + row: 1 + column: 0 + end_location: + row: 4 + column: 3 + fix: ~ +- kind: FStringDocstring + location: + row: 14 + column: 4 + end_location: + row: 14 + column: 28 + fix: ~ +- kind: FStringDocstring + location: + row: 22 + column: 4 + end_location: + row: 22 + column: 28 + fix: ~ +- kind: FStringDocstring + location: + row: 30 + column: 4 + end_location: + row: 30 + column: 28 + fix: ~ +- kind: FStringDocstring + location: + row: 38 + column: 4 + end_location: + row: 38 + column: 28 + fix: ~ +- kind: FStringDocstring + location: + row: 46 + column: 4 + end_location: + row: 46 + column: 24 + fix: ~ +- kind: FStringDocstring + location: + row: 54 + column: 4 + end_location: + row: 54 + column: 24 + fix: ~ +- kind: FStringDocstring + location: + row: 62 + column: 4 + end_location: + row: 62 + column: 24 + fix: ~ +- kind: FStringDocstring + location: + row: 70 + column: 4 + end_location: + row: 70 + column: 24 + fix: ~ +- kind: FStringDocstring + location: + row: 74 + column: 4 + end_location: + row: 74 + column: 48 + fix: ~ +