diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index 30f8b5f0f3..ff55adc02f 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -224,6 +224,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -257,6 +258,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -290,6 +292,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -323,6 +326,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -356,6 +360,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(flake8_quotes::settings::Quote::Single), multiline_quotes: None, @@ -432,6 +437,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -466,6 +472,7 @@ mod tests { dummy_variable_rgx: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(flake8_quotes::settings::Quote::Single), multiline_quotes: None, diff --git a/resources/test/fixtures/B008_extended.py b/resources/test/fixtures/B008_extended.py new file mode 100644 index 0000000000..b1edf7f9a7 --- /dev/null +++ b/resources/test/fixtures/B008_extended.py @@ -0,0 +1,17 @@ +from typing import List + +import fastapi +from fastapi import Query + + +def this_is_okay_extended(db=fastapi.Depends(get_db)): + ... + + +def this_is_okay_extended_second(data: List[str] = fastapi.Query(None)): + ... + + +# TODO(charlie): Support `import from`. +def this_is_not_okay_relative_import_not_listed(data: List[str] = Query(None)): + ... diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index 6837a34638..2a2e55d03b 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -7,6 +7,9 @@ extend-exclude = [ ] per-file-ignores = { "__init__.py" = ["F401"] } +[tool.ruff.flake8-bugbear] +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] + [tool.ruff.flake8-quotes] inline-quotes = "single" multiline-quotes = "double" diff --git a/src/flake8_bugbear/mod.rs b/src/flake8_bugbear/mod.rs index 8556dc6fed..407ecd7513 100644 --- a/src/flake8_bugbear/mod.rs +++ b/src/flake8_bugbear/mod.rs @@ -1,2 +1,36 @@ mod constants; pub mod plugins; +pub mod settings; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + + use crate::autofix::fixer; + use crate::checks::CheckCode; + use crate::linter::test_path; + use crate::{flake8_bugbear, Settings}; + + #[test] + fn extend_immutable_calls() -> Result<()> { + let snapshot = "extend_immutable_calls".to_string(); + let mut checks = test_path( + Path::new("./resources/test/fixtures/B008_extended.py"), + &Settings { + flake8_bugbear: flake8_bugbear::settings::Settings { + extend_immutable_calls: vec![ + "fastapi.Depends".to_string(), + "fastapi.Query".to_string(), + ], + }, + ..Settings::for_rules(vec![CheckCode::B008]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } +} diff --git a/src/flake8_bugbear/plugins/function_call_argument_default.rs b/src/flake8_bugbear/plugins/function_call_argument_default.rs index c162fcc93a..e1303cc280 100644 --- a/src/flake8_bugbear/plugins/function_call_argument_default.rs +++ b/src/flake8_bugbear/plugins/function_call_argument_default.rs @@ -23,23 +23,27 @@ const IMMUTABLE_FUNCS: [&str; 11] = [ "re.compile", ]; -fn is_immutable_func(expr: &Expr) -> bool { - compose_call_path(expr).map_or_else(|| false, |func| IMMUTABLE_FUNCS.contains(&func.as_str())) +fn is_immutable_func(expr: &Expr, extend_immutable_calls: &[String]) -> bool { + compose_call_path(expr).map_or_else( + || false, + |func| IMMUTABLE_FUNCS.contains(&func.as_str()) || extend_immutable_calls.contains(&func), + ) } -struct ArgumentDefaultVisitor { +struct ArgumentDefaultVisitor<'a> { checks: Vec<(CheckKind, Range)>, + extend_immutable_calls: &'a [String], } -impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor +impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b> where 'b: 'a, { - fn visit_expr(&mut self, expr: &'a Expr) { + fn visit_expr(&mut self, expr: &'b Expr) { match &expr.node { ExprKind::Call { func, args, .. } => { if !is_mutable_func(func) - && !is_immutable_func(func) + && !is_immutable_func(func, &self.extend_immutable_calls) && !is_nan_or_infinity(func, args) { self.checks.push(( @@ -83,7 +87,10 @@ fn is_nan_or_infinity(expr: &Expr, args: &[Expr]) -> bool { /// B008 pub fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) { - let mut visitor = ArgumentDefaultVisitor { checks: vec![] }; + let mut visitor = ArgumentDefaultVisitor { + checks: vec![], + extend_immutable_calls: &checker.settings.flake8_bugbear.extend_immutable_calls, + }; for expr in arguments .defaults .iter() diff --git a/src/flake8_bugbear/settings.rs b/src/flake8_bugbear/settings.rs new file mode 100644 index 0000000000..8bf0422aa5 --- /dev/null +++ b/src/flake8_bugbear/settings.rs @@ -0,0 +1,22 @@ +//! Settings for the `pep8-naming` plugin. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct Options { + pub extend_immutable_calls: Option>, +} + +#[derive(Debug, Hash, Default)] +pub struct Settings { + pub extend_immutable_calls: Vec, +} + +impl Settings { + pub fn from_options(options: Options) -> Self { + Self { + extend_immutable_calls: options.extend_immutable_calls.unwrap_or_default(), + } + } +} diff --git a/src/flake8_bugbear/snapshots/ruff__flake8_bugbear__tests__extend_immutable_calls.snap b/src/flake8_bugbear/snapshots/ruff__flake8_bugbear__tests__extend_immutable_calls.snap new file mode 100644 index 0000000000..02b1147703 --- /dev/null +++ b/src/flake8_bugbear/snapshots/ruff__flake8_bugbear__tests__extend_immutable_calls.snap @@ -0,0 +1,13 @@ +--- +source: src/flake8_bugbear/mod.rs +expression: checks +--- +- kind: FunctionCallArgumentDefault + location: + row: 15 + column: 66 + end_location: + row: 15 + column: 77 + fix: ~ + diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index b87b4553a3..ae75d22c72 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -12,7 +12,7 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::settings::pyproject::load_options; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; -use crate::{flake8_annotations, flake8_quotes, fs, isort, pep8_naming}; +use crate::{flake8_annotations, flake8_bugbear, flake8_quotes, fs, isort, pep8_naming}; #[derive(Debug)] pub struct Configuration { @@ -30,6 +30,7 @@ pub struct Configuration { pub target_version: PythonVersion, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, + pub flake8_bugbear: flake8_bugbear::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub isort: isort::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, @@ -133,6 +134,10 @@ impl Configuration { .flake8_annotations .map(flake8_annotations::settings::Settings::from_options) .unwrap_or_default(), + flake8_bugbear: options + .flake8_bugbear + .map(flake8_bugbear::settings::Settings::from_options) + .unwrap_or_default(), flake8_quotes: options .flake8_quotes .map(flake8_quotes::settings::Settings::from_options) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index eade10b63e..c2d2577681 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -13,7 +13,7 @@ use crate::checks::CheckCode; use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; -use crate::{flake8_annotations, flake8_quotes, isort, pep8_naming}; +use crate::{flake8_annotations, flake8_bugbear, flake8_quotes, isort, pep8_naming}; pub mod configuration; pub mod options; @@ -33,6 +33,7 @@ pub struct Settings { pub target_version: PythonVersion, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, + pub flake8_bugbear: flake8_bugbear::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub isort: isort::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, @@ -51,6 +52,7 @@ impl Settings { exclude: config.exclude, extend_exclude: config.extend_exclude, flake8_annotations: config.flake8_annotations, + flake8_bugbear: config.flake8_bugbear, flake8_quotes: config.flake8_quotes, isort: config.isort, line_length: config.line_length, @@ -72,6 +74,7 @@ impl Settings { src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, flake8_annotations: Default::default(), + flake8_bugbear: Default::default(), flake8_quotes: Default::default(), isort: Default::default(), pep8_naming: Default::default(), @@ -89,6 +92,7 @@ impl Settings { src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, flake8_annotations: Default::default(), + flake8_bugbear: Default::default(), flake8_quotes: Default::default(), isort: Default::default(), pep8_naming: Default::default(), diff --git a/src/settings/options.rs b/src/settings/options.rs index dcf087fefd..47aaa361c9 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::checks_gen::CheckCodePrefix; use crate::settings::types::PythonVersion; -use crate::{flake8_annotations, flake8_quotes, isort, pep8_naming}; +use crate::{flake8_annotations, flake8_bugbear, flake8_quotes, isort, pep8_naming}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -25,6 +25,7 @@ pub struct Options { pub target_version: Option, // Plugins pub flake8_annotations: Option, + pub flake8_bugbear: Option, pub flake8_quotes: Option, pub isort: Option, pub pep8_naming: Option, diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 802c64a9d0..6aa4e6ae2d 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -109,7 +109,7 @@ mod tests { find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; use crate::settings::types::PatternPrefixPair; - use crate::{flake8_quotes, pep8_naming}; + use crate::{flake8_bugbear, flake8_quotes, pep8_naming}; #[test] fn deserialize() -> Result<()> { @@ -146,6 +146,7 @@ mod tests { src: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -177,6 +178,7 @@ line-length = 79 src: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -208,6 +210,7 @@ exclude = ["foo.py"] src: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -239,6 +242,7 @@ select = ["E501"] src: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -271,6 +275,7 @@ ignore = ["E501"] src: None, target_version: None, flake8_annotations: None, + flake8_bugbear: None, flake8_quotes: None, isort: None, pep8_naming: None, @@ -349,6 +354,12 @@ other-attribute = 1 src: None, target_version: None, flake8_annotations: None, + flake8_bugbear: Some(flake8_bugbear::settings::Options { + extend_immutable_calls: Some(vec![ + "fastapi.Depends".to_string(), + "fastapi.Query".to_string(), + ]), + }), flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(Quote::Single), multiline_quotes: Some(Quote::Double),