diff --git a/README.md b/README.md index 00aa827487..a6585ef928 100644 --- a/README.md +++ b/README.md @@ -512,6 +512,16 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens | C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | 🛠 | | C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | | +### flake8-boolean-trap + +For more, see [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/0.1.0/) on PyPI. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| FBT001 | BooleanPositionalArgInFunctionDefinition | Boolean positional arg in function definition | | +| FBT002 | BooleanDefaultValueInFunctionDefinition | Boolean default value in function definition | | +| FBT003 | BooleanPositionalValueInFunctionCall | Boolean positional value in function call | | + ### flake8-bugbear For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/) on PyPI. diff --git a/resources/test/fixtures/FBT.py b/resources/test/fixtures/FBT.py new file mode 100644 index 0000000000..a996fd1855 --- /dev/null +++ b/resources/test/fixtures/FBT.py @@ -0,0 +1,42 @@ +def function( + posonly_nohint, + posonly_nonboolhint: int, + posonly_boolhint: bool, + posonly_boolstrhint: "bool", + /, + offset, + posorkw_nonvalued_nohint, + posorkw_nonvalued_nonboolhint: int, + posorkw_nonvalued_boolhint: bool, + posorkw_nonvalued_boolstrhint: "bool", + posorkw_boolvalued_nohint=True, + posorkw_boolvalued_nonboolhint: int = True, + posorkw_boolvalued_boolhint: bool = True, + posorkw_boolvalued_boolstrhint: "bool" = True, + posorkw_nonboolvalued_nohint=1, + posorkw_nonboolvalued_nonboolhint: int = 2, + posorkw_nonboolvalued_boolhint: bool = 3, + posorkw_nonboolvalued_boolstrhint: "bool" = 4, + *, + kwonly_nonvalued_nohint, + kwonly_nonvalued_nonboolhint: int, + kwonly_nonvalued_boolhint: bool, + kwonly_nonvalued_boolstrhint: "bool", + kwonly_boolvalued_nohint=True, + kwonly_boolvalued_nonboolhint: int = False, + kwonly_boolvalued_boolhint: bool = True, + kwonly_boolvalued_boolstrhint: "bool" = True, + kwonly_nonboolvalued_nohint=5, + kwonly_nonboolvalued_nonboolhint: int = 1, + kwonly_nonboolvalued_boolhint: bool = 1, + kwonly_nonboolvalued_boolstrhint: "bool" = 1, + **kw, +): + ... + + +def used(do): + return do + +used("a", True) +used(do=True) diff --git a/src/check_ast.rs b/src/check_ast.rs index d76f6a4818..53e0021560 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -36,8 +36,8 @@ use crate::source_code_locator::SourceCodeLocator; use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope}; use crate::{ docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, - flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print, flake8_tidy_imports, - mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade, + flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print, + flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade, }; const GLOBAL_SCOPE_INDEX: usize = 0; @@ -1462,6 +1462,12 @@ where pyupgrade::plugins::type_of_primitive(self, expr, func, args); } + // flake8-boolean-trap + if self.settings.enabled.contains(&CheckCode::FBT003) { + flake8_boolean_trap::plugins::check_boolean_positional_value_in_function_call( + self, args, + ); + } if let ExprKind::Name { id, ctx } = &func.node { if id == "locals" && matches!(ctx, ExprContext::Load) { let scope = &mut self.scopes @@ -1955,6 +1961,16 @@ where flake8_bugbear::plugins::function_call_argument_default(self, arguments) } + // flake8-boolean-trap + if self.settings.enabled.contains(&CheckCode::FBT001) { + flake8_boolean_trap::plugins::check_positional_boolean_in_def(self, arguments); + } + if self.settings.enabled.contains(&CheckCode::FBT002) { + flake8_boolean_trap::plugins::check_boolean_default_value_in_function_definition( + self, arguments, + ); + } + // Bind, but intentionally avoid walking default expressions, as we handle them // upstream. for arg in &arguments.posonlyargs { diff --git a/src/checks.rs b/src/checks.rs index 2f51399221..4ee881ace1 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -240,6 +240,10 @@ pub enum CheckCode { S105, S106, S107, + // flake8-boolean-trap + FBT001, + FBT002, + FBT003, // Ruff RUF001, RUF002, @@ -258,6 +262,7 @@ pub enum CheckCategory { PEP8Naming, Flake8Bandit, Flake8Comprehensions, + Flake8BooleanTrap, Flake8Bugbear, Flake8Builtins, Flake8TidyImports, @@ -278,6 +283,7 @@ impl CheckCategory { CheckCategory::Pyflakes => "Pyflakes", CheckCategory::Isort => "isort", CheckCategory::Flake8Bandit => "flake8-bandit", + CheckCategory::Flake8BooleanTrap => "flake8-boolean-trap", CheckCategory::Flake8Builtins => "flake8-builtins", CheckCategory::Flake8Bugbear => "flake8-bugbear", CheckCategory::Flake8Comprehensions => "flake8-comprehensions", @@ -327,6 +333,9 @@ impl CheckCategory { Some("https://pypi.org/project/flake8-blind-except/0.2.1/") } CheckCategory::McCabe => Some("https://pypi.org/project/mccabe/0.7.0/"), + CheckCategory::Flake8BooleanTrap => { + Some("https://pypi.org/project/flake8-boolean-trap/0.1.0/") + } CheckCategory::Ruff => None, CheckCategory::Meta => None, } @@ -564,6 +573,10 @@ pub enum CheckKind { HardcodedPasswordDefault(String), // mccabe FunctionIsTooComplex(String, usize), + // flake8-boolean-trap + BooleanPositionalArgInFunctionDefinition, + BooleanDefaultValueInFunctionDefinition, + BooleanPositionalValueInFunctionCall, // Ruff AmbiguousUnicodeCharacterString(char, char), AmbiguousUnicodeCharacterDocstring(char, char), @@ -848,6 +861,10 @@ impl CheckCode { CheckCode::S106 => CheckKind::HardcodedPasswordFuncArg("...".to_string()), CheckCode::S107 => CheckKind::HardcodedPasswordDefault("...".to_string()), CheckCode::C901 => CheckKind::FunctionIsTooComplex("...".to_string(), 10), + // flake8-boolean-trap + CheckCode::FBT001 => CheckKind::BooleanPositionalArgInFunctionDefinition, + CheckCode::FBT002 => CheckKind::BooleanDefaultValueInFunctionDefinition, + CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall, // Ruff CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'), CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'), @@ -1055,6 +1072,9 @@ impl CheckCode { CheckCode::S106 => CheckCategory::Flake8Bandit, CheckCode::S107 => CheckCategory::Flake8Bandit, CheckCode::C901 => CheckCategory::McCabe, + CheckCode::FBT001 => CheckCategory::Flake8BooleanTrap, + CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap, + CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap, CheckCode::RUF001 => CheckCategory::Ruff, CheckCode::RUF002 => CheckCategory::Ruff, CheckCode::RUF003 => CheckCategory::Ruff, @@ -1280,6 +1300,10 @@ impl CheckKind { CheckKind::HardcodedPasswordDefault(..) => &CheckCode::S107, // McCabe CheckKind::FunctionIsTooComplex(..) => &CheckCode::C901, + // flake8-boolean-trap + CheckKind::BooleanPositionalArgInFunctionDefinition => &CheckCode::FBT001, + CheckKind::BooleanDefaultValueInFunctionDefinition => &CheckCode::FBT002, + CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003, // Ruff CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001, CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002, @@ -1941,6 +1965,16 @@ impl CheckKind { CheckKind::FunctionIsTooComplex(name, complexity) => { format!("`{name}` is too complex ({complexity})") } + // flake8-boolean-trap + CheckKind::BooleanPositionalArgInFunctionDefinition => { + "Boolean positional arg in function definition".to_string() + } + CheckKind::BooleanDefaultValueInFunctionDefinition => { + "Boolean default value in function definition".to_string() + } + CheckKind::BooleanPositionalValueInFunctionCall => { + "Boolean positional value in function call".to_string() + } // Ruff CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => { format!( diff --git a/src/checks_gen.rs b/src/checks_gen.rs index e69f855293..74a6cdf3a7 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -217,6 +217,12 @@ pub enum CheckCodePrefix { F9, F90, F901, + FBT, + FBT0, + FBT00, + FBT001, + FBT002, + FBT003, I, I0, I00, @@ -943,6 +949,12 @@ impl CheckCodePrefix { CheckCodePrefix::F9 => vec![CheckCode::F901], CheckCodePrefix::F90 => vec![CheckCode::F901], CheckCodePrefix::F901 => vec![CheckCode::F901], + CheckCodePrefix::FBT => vec![CheckCode::FBT001, CheckCode::FBT002, CheckCode::FBT003], + CheckCodePrefix::FBT0 => vec![CheckCode::FBT001, CheckCode::FBT002, CheckCode::FBT003], + CheckCodePrefix::FBT00 => vec![CheckCode::FBT001, CheckCode::FBT002, CheckCode::FBT003], + CheckCodePrefix::FBT001 => vec![CheckCode::FBT001], + CheckCodePrefix::FBT002 => vec![CheckCode::FBT002], + CheckCodePrefix::FBT003 => vec![CheckCode::FBT003], CheckCodePrefix::I => vec![CheckCode::I252, CheckCode::I001], CheckCodePrefix::I0 => vec![CheckCode::I001], CheckCodePrefix::I00 => vec![CheckCode::I001], @@ -1403,6 +1415,12 @@ impl CheckCodePrefix { CheckCodePrefix::F9 => PrefixSpecificity::Hundreds, CheckCodePrefix::F90 => PrefixSpecificity::Tens, CheckCodePrefix::F901 => PrefixSpecificity::Explicit, + CheckCodePrefix::FBT => PrefixSpecificity::Category, + CheckCodePrefix::FBT0 => PrefixSpecificity::Hundreds, + CheckCodePrefix::FBT00 => PrefixSpecificity::Tens, + CheckCodePrefix::FBT001 => PrefixSpecificity::Explicit, + CheckCodePrefix::FBT002 => PrefixSpecificity::Explicit, + CheckCodePrefix::FBT003 => PrefixSpecificity::Explicit, CheckCodePrefix::I => PrefixSpecificity::Category, CheckCodePrefix::I0 => PrefixSpecificity::Hundreds, CheckCodePrefix::I00 => PrefixSpecificity::Tens, diff --git a/src/flake8_boolean_trap/mod.rs b/src/flake8_boolean_trap/mod.rs new file mode 100644 index 0000000000..25a06164e5 --- /dev/null +++ b/src/flake8_boolean_trap/mod.rs @@ -0,0 +1 @@ +pub mod plugins; diff --git a/src/flake8_boolean_trap/plugins.rs b/src/flake8_boolean_trap/plugins.rs new file mode 100644 index 0000000000..85d7424cb9 --- /dev/null +++ b/src/flake8_boolean_trap/plugins.rs @@ -0,0 +1,71 @@ +use rustpython_ast::{Arguments, ExprKind}; +use rustpython_parser::ast::{Constant, Expr}; + +use crate::ast::types::Range; +use crate::check_ast::Checker; +use crate::checks::{Check, CheckKind}; + +fn is_boolean_arg(arg: &Expr) -> bool { + matches!( + &arg.node, + ExprKind::Constant { + value: Constant::Bool(_), + .. + } + ) +} + +fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: CheckKind) { + if is_boolean_arg(arg) { + checker.add_check(Check::new(kind, Range::from_located(arg))); + } +} + +pub fn check_positional_boolean_in_def(checker: &mut Checker, arguments: &Arguments) { + for arg in arguments.posonlyargs.iter().chain(arguments.args.iter()) { + if arg.node.annotation.is_none() { + continue; + } + + if let Some(expr) = &arg.node.annotation { + // check for both bool (python class) and 'bool' (string annotation) + let hint = match &expr.node { + ExprKind::Name { id, .. } => id == "bool", + ExprKind::Constant { + value: Constant::Str(value), + .. + } => value == "bool", + _ => false, + }; + if hint { + checker.add_check(Check::new( + CheckKind::BooleanPositionalArgInFunctionDefinition, + Range::from_located(arg), + )); + } + } + } +} + +pub fn check_boolean_default_value_in_function_definition( + checker: &mut Checker, + arguments: &Arguments, +) { + for arg in arguments.defaults.iter() { + add_if_boolean( + checker, + arg, + CheckKind::BooleanDefaultValueInFunctionDefinition, + ); + } +} + +pub fn check_boolean_positional_value_in_function_call(checker: &mut Checker, args: &[Expr]) { + for arg in args { + add_if_boolean( + checker, + arg, + CheckKind::BooleanPositionalValueInFunctionCall, + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index bdc7f510b1..76de99f8f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ mod flake8_2020; pub mod flake8_annotations; pub mod flake8_bandit; mod flake8_blind_except; +pub mod flake8_boolean_trap; pub mod flake8_bugbear; mod flake8_builtins; mod flake8_comprehensions; diff --git a/src/linter.rs b/src/linter.rs index a649834e05..fbca489738 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -521,6 +521,9 @@ mod tests { #[test_case(CheckCode::YTT301, Path::new("YTT301.py"); "YTT301")] #[test_case(CheckCode::YTT302, Path::new("YTT302.py"); "YTT302")] #[test_case(CheckCode::YTT303, Path::new("YTT303.py"); "YTT303")] + #[test_case(CheckCode::FBT001, Path::new("FBT.py"); "FBT001")] + #[test_case(CheckCode::FBT002, Path::new("FBT.py"); "FBT002")] + #[test_case(CheckCode::FBT003, Path::new("FBT.py"); "FBT003")] fn checks(check_code: CheckCode, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy()); let mut checks = test_path( diff --git a/src/snapshots/ruff__linter__tests__FBT001_FBT.py.snap b/src/snapshots/ruff__linter__tests__FBT001_FBT.py.snap new file mode 100644 index 0000000000..b91be5183b --- /dev/null +++ b/src/snapshots/ruff__linter__tests__FBT001_FBT.py.snap @@ -0,0 +1,69 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 4 + column: 4 + end_location: + row: 4 + column: 26 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 5 + column: 4 + end_location: + row: 5 + column: 31 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 10 + column: 4 + end_location: + row: 10 + column: 36 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 11 + column: 4 + end_location: + row: 11 + column: 41 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 14 + column: 4 + end_location: + row: 14 + column: 37 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 15 + column: 4 + end_location: + row: 15 + column: 42 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 18 + column: 4 + end_location: + row: 18 + column: 40 + fix: ~ +- kind: BooleanPositionalArgInFunctionDefinition + location: + row: 19 + column: 4 + end_location: + row: 19 + column: 45 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__FBT002_FBT.py.snap b/src/snapshots/ruff__linter__tests__FBT002_FBT.py.snap new file mode 100644 index 0000000000..32c5ee948a --- /dev/null +++ b/src/snapshots/ruff__linter__tests__FBT002_FBT.py.snap @@ -0,0 +1,37 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: BooleanDefaultValueInFunctionDefinition + location: + row: 12 + column: 30 + end_location: + row: 12 + column: 34 + fix: ~ +- kind: BooleanDefaultValueInFunctionDefinition + location: + row: 13 + column: 42 + end_location: + row: 13 + column: 46 + fix: ~ +- kind: BooleanDefaultValueInFunctionDefinition + location: + row: 14 + column: 40 + end_location: + row: 14 + column: 44 + fix: ~ +- kind: BooleanDefaultValueInFunctionDefinition + location: + row: 15 + column: 45 + end_location: + row: 15 + column: 49 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__FBT003_FBT.py.snap b/src/snapshots/ruff__linter__tests__FBT003_FBT.py.snap new file mode 100644 index 0000000000..58f30d34af --- /dev/null +++ b/src/snapshots/ruff__linter__tests__FBT003_FBT.py.snap @@ -0,0 +1,13 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: BooleanPositionalValueInFunctionCall + location: + row: 41 + column: 10 + end_location: + row: 41 + column: 14 + fix: ~ +