From f44fada446db8f671cbb4082acc2a5c7fd8be512 Mon Sep 17 00:00:00 2001 From: "Edgar R. M" Date: Thu, 17 Nov 2022 16:40:50 -0600 Subject: [PATCH] Implement C901 (mccabe) (#765) --- CONTRIBUTING.md | 7 + README.md | 14 +- flake8_to_ruff/src/converter.rs | 20 ++- flake8_to_ruff/src/plugin.rs | 12 +- resources/test/fixtures/C901.py | 108 +++++++++++ resources/test/fixtures/pyproject.toml | 3 + src/check_ast.rs | 14 +- src/checks.rs | 15 ++ src/checks_gen.rs | 10 ++ src/cli.rs | 3 + src/lib.rs | 1 + src/main.rs | 3 + src/mccabe/checks.rs | 73 ++++++++ src/mccabe/mod.rs | 33 ++++ src/mccabe/settings.rs | 28 +++ ...ruff__mccabe__tests__max_complexity_0.snap | 170 ++++++++++++++++++ ...uff__mccabe__tests__max_complexity_10.snap | 6 + ...ruff__mccabe__tests__max_complexity_3.snap | 16 ++ src/settings/configuration.rs | 8 +- src/settings/mod.rs | 8 +- src/settings/options.rs | 4 +- src/settings/pyproject.rs | 10 +- 22 files changed, 555 insertions(+), 11 deletions(-) create mode 100644 resources/test/fixtures/C901.py create mode 100644 src/mccabe/checks.rs create mode 100644 src/mccabe/mod.rs create mode 100644 src/mccabe/settings.rs create mode 100644 src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_0.snap create mode 100644 src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_10.snap create mode 100644 src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_3.snap diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8af18c9259..349e7e0887 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,6 +98,13 @@ _and_ a `pyproject.toml` parameter to `src/pyproject.rs`. If you want to pattern existing example, grep for `dummy_variable_rgx`, which defines a regular expression to match against acceptable unused variables (e.g., `_`). +If the new plugin's configuration should be cached between runs, you'll need to add it to the +`Hash` implementation for `Settings` in `src/settings/mod.rs`. + +You may also want to add the new configuration option to the `flake8-to-ruff` tool, which is +responsible for converting `flake8` configuration files to Ruff's TOML format. This logic +lives in `flake8_to_ruff/src/converter.rs`. + ## Release process As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub diff --git a/README.md b/README.md index 9236688575..af0181e984 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,8 @@ Options: The minimum Python version that should be supported --line-length Set the line-length for length-associated checks and automatic formatting + --max-complexity + Set the maximum cyclomatic complexity for complexity-associated checks --stdin-filename The name of the file when passing it through stdin -h, --help @@ -357,8 +359,8 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI | ---- | ---- | ------- | --- | | E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | | | E501 | LineTooLong | Line too long (89 > 88 characters) | | -| E711 | NoneComparison | Comparison to `None` should be `cond is None` | 🛠 | -| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | 🛠 | +| E711 | NoneComparison | Comparison to `None` should be `cond is None` | | +| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | | | E713 | NotInTest | Test for membership should be `not in` | | | E714 | NotIsTest | Test for object identity should be `is not` | | | E721 | TypeComparison | Do not compare types, use `isinstance()` | | @@ -612,6 +614,14 @@ For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI | YTT302 | SysVersionCmpStr10 | `sys.version` compared to string (python10), use `sys.version_info` | | | YTT303 | SysVersionSlice1Referenced | `sys.version[:1]` referenced (python10), use `sys.version_info` | | +### mccabe + +For more, see [mccabe](https://pypi.org/project/mccabe/0.7.0/) on PyPI. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| C901 | FunctionIsTooComplex | `...` is too complex (10) | | + ### Ruff-specific rules | Code | Name | Message | Fix | diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index 7aba8fb79b..2068673211 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -6,7 +6,9 @@ use ruff::flake8_quotes::settings::Quote; use ruff::flake8_tidy_imports::settings::Strictness; use ruff::settings::options::Options; use ruff::settings::pyproject::Pyproject; -use ruff::{flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, pep8_naming}; +use ruff::{ + flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, +}; use crate::plugin::Plugin; use crate::{parser, plugin}; @@ -73,6 +75,7 @@ pub fn convert( let mut flake8_bugbear: flake8_bugbear::settings::Options = Default::default(); let mut flake8_quotes: flake8_quotes::settings::Options = Default::default(); let mut flake8_tidy_imports: flake8_tidy_imports::settings::Options = Default::default(); + let mut mccabe: mccabe::settings::Options = Default::default(); let mut pep8_naming: pep8_naming::settings::Options = Default::default(); for (key, value) in flake8 { if let Some(value) = value { @@ -186,6 +189,11 @@ pub fn convert( "docstring-convention" => { // No-op (handled above). } + // mccabe + "max-complexity" | "max_complexity" => match value.clone().parse::() { + Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity), + Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), + }, // Unknown _ => eprintln!("Skipping unsupported property: {key}"), } @@ -207,6 +215,9 @@ pub fn convert( if flake8_tidy_imports != Default::default() { options.flake8_tidy_imports = Some(flake8_tidy_imports); } + if mccabe != Default::default() { + options.mccabe = Some(mccabe); + } if pep8_naming != Default::default() { options.pep8_naming = Some(pep8_naming); } @@ -253,6 +264,7 @@ mod tests { flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); @@ -288,6 +300,7 @@ mod tests { flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); @@ -323,6 +336,7 @@ mod tests { flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); @@ -358,6 +372,7 @@ mod tests { flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); @@ -398,6 +413,7 @@ mod tests { }), flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); @@ -471,6 +487,7 @@ mod tests { flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); @@ -512,6 +529,7 @@ mod tests { }), flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }); assert_eq!(actual, expected); diff --git a/flake8_to_ruff/src/plugin.rs b/flake8_to_ruff/src/plugin.rs index 519a675724..0a7d90033a 100644 --- a/flake8_to_ruff/src/plugin.rs +++ b/flake8_to_ruff/src/plugin.rs @@ -15,6 +15,7 @@ pub enum Plugin { Flake8Print, Flake8Quotes, Flake8Annotations, + McCabe, PEP8Naming, Pyupgrade, } @@ -33,6 +34,7 @@ impl FromStr for Plugin { "flake8-print" => Ok(Plugin::Flake8Print), "flake8-quotes" => Ok(Plugin::Flake8Quotes), "flake8-annotations" => Ok(Plugin::Flake8Annotations), + "mccabe" => Ok(Plugin::McCabe), "pep8-naming" => Ok(Plugin::PEP8Naming), "pyupgrade" => Ok(Plugin::Pyupgrade), _ => Err(anyhow!("Unknown plugin: {}", string)), @@ -46,12 +48,13 @@ impl Plugin { Plugin::Flake8Bandit => CheckCodePrefix::S, Plugin::Flake8Bugbear => CheckCodePrefix::B, Plugin::Flake8Builtins => CheckCodePrefix::A, - Plugin::Flake8Comprehensions => CheckCodePrefix::C, + Plugin::Flake8Comprehensions => CheckCodePrefix::C4, Plugin::Flake8Docstrings => CheckCodePrefix::D, Plugin::Flake8TidyImports => CheckCodePrefix::I25, Plugin::Flake8Print => CheckCodePrefix::T, Plugin::Flake8Quotes => CheckCodePrefix::Q, Plugin::Flake8Annotations => CheckCodePrefix::ANN, + Plugin::McCabe => CheckCodePrefix::C9, Plugin::PEP8Naming => CheckCodePrefix::N, Plugin::Pyupgrade => CheckCodePrefix::U, } @@ -62,7 +65,7 @@ impl Plugin { Plugin::Flake8Bandit => vec![CheckCodePrefix::S], Plugin::Flake8Bugbear => vec![CheckCodePrefix::B], Plugin::Flake8Builtins => vec![CheckCodePrefix::A], - Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C], + Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C4], Plugin::Flake8Docstrings => { // Use the user-provided docstring. for key in ["docstring-convention", "docstring_convention"] { @@ -83,6 +86,7 @@ impl Plugin { Plugin::Flake8Print => vec![CheckCodePrefix::T], Plugin::Flake8Quotes => vec![CheckCodePrefix::Q], Plugin::Flake8Annotations => vec![CheckCodePrefix::ANN], + Plugin::McCabe => vec![CheckCodePrefix::C9], Plugin::PEP8Naming => vec![CheckCodePrefix::N], Plugin::Pyupgrade => vec![CheckCodePrefix::U], } @@ -326,6 +330,10 @@ pub fn infer_plugins_from_options(flake8: &HashMap>) -> V "banned-modules" | "banned_modules" => { plugins.insert(Plugin::Flake8TidyImports); } + // mccabe + "max-complexity" | "max_complexity" => { + plugins.insert(Plugin::McCabe); + } // pep8-naming "ignore-names" | "ignore_names" => { plugins.insert(Plugin::PEP8Naming); diff --git a/resources/test/fixtures/C901.py b/resources/test/fixtures/C901.py new file mode 100644 index 0000000000..4028f300df --- /dev/null +++ b/resources/test/fixtures/C901.py @@ -0,0 +1,108 @@ +# Complexity = 1 +def trivial(): + pass + + +# Complexity = 1 +def expr_as_statement(): + 0xF00D + + +# Complexity = 1 +def sequential(n): + k = n + 4 + s = k + n + return s + + +# Complexity = 3 +def if_elif_else_dead_path(n): + if n > 3: + return "bigger than three" + elif n > 4: + return "is never executed" + else: + return "smaller than or equal to three" + + +# Complexity = 3 +def nested_ifs(): + if n > 3: + if n > 4: + return "bigger than four" + else: + return "bigger than three" + else: + return "smaller than or equal to three" + + +# Complexity = 2 +def for_loop(): + for i in range(10): + print(i) + + +# Complexity = 2 +def for_else(mylist): + for i in mylist: + print(i) + else: + print(None) + + +# Complexity = 2 +def recursive(n): + if n > 4: + return f(n - 1) + else: + return n + + +# Complexity = 3 +def nested_functions(): + def a(): + def b(): + pass + + b() + + a() + + +# Complexity = 4 +def try_else(): + try: + print(1) + except TypeA: + print(2) + except TypeB: + print(3) + else: + print(4) + + +# Complexity = 3 +def nested_try_finally(): + try: + try: + print(1) + finally: + print(2) + finally: + print(3) + + +# Complexity = 3 +async def foobar(a, b, c): + await whatever(a, b, c) + if await b: + pass + async with c: + pass + async for x in a: + pass + + +# Complexity = 1 +def annotated_assign(): + x: Any = None diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index 89ddedff85..1d62e7ba0c 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -16,6 +16,9 @@ multiline-quotes = "double" docstring-quotes = "double" avoid-escape = true +[tool.ruff.mccabe] +max-complexity = 10 + [tool.ruff.pep8-naming] ignore-names = [ "setUp", diff --git a/src/check_ast.rs b/src/check_ast.rs index 2b96911c30..1a93893495 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_bugbear, flake8_builtins, - flake8_comprehensions, flake8_print, flake8_tidy_imports, pep8_naming, pycodestyle, pydocstyle, - pyflakes, pyupgrade, + flake8_comprehensions, flake8_print, flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, + pydocstyle, pyflakes, pyupgrade, }; const GLOBAL_SCOPE_INDEX: usize = 0; @@ -349,6 +349,16 @@ where if self.settings.enabled.contains(&CheckCode::B019) { flake8_bugbear::plugins::cached_instance_method(self, decorator_list); } + if self.settings.enabled.contains(&CheckCode::C901) { + if let Some(check) = mccabe::checks::function_is_too_complex( + stmt, + name, + body, + self.settings.mccabe.max_complexity, + ) { + self.add_check(check); + } + } if self.settings.enabled.contains(&CheckCode::S107) { self.add_checks( diff --git a/src/checks.rs b/src/checks.rs index d62bf318b1..4271cc0c50 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -120,6 +120,8 @@ pub enum CheckCode { C415, C416, C417, + // mccabe + C901, // flake8-tidy-imports I252, // flake8-print @@ -260,6 +262,7 @@ pub enum CheckCategory { Flake8Quotes, Flake8Annotations, Flake82020, + McCabe, Ruff, Meta, } @@ -282,6 +285,7 @@ impl CheckCategory { CheckCategory::Pyupgrade => "pyupgrade", CheckCategory::Pydocstyle => "pydocstyle", CheckCategory::PEP8Naming => "pep8-naming", + CheckCategory::McCabe => "mccabe", CheckCategory::Ruff => "Ruff-specific rules", CheckCategory::Meta => "Meta rules", } @@ -314,6 +318,7 @@ impl CheckCategory { CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"), CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"), CheckCategory::Flake8Bandit => Some("https://pypi.org/project/flake8-bandit/4.1.1/"), + CheckCategory::McCabe => Some("https://pypi.org/project/mccabe/0.7.0/"), CheckCategory::Ruff => None, CheckCategory::Meta => None, } @@ -546,6 +551,8 @@ pub enum CheckKind { HardcodedPasswordString(String), HardcodedPasswordFuncArg(String), HardcodedPasswordDefault(String), + // mccabe + FunctionIsTooComplex(String, usize), // Ruff AmbiguousUnicodeCharacterString(char, char), AmbiguousUnicodeCharacterDocstring(char, char), @@ -826,6 +833,7 @@ impl CheckCode { CheckCode::S105 => CheckKind::HardcodedPasswordString("...".to_string()), CheckCode::S106 => CheckKind::HardcodedPasswordFuncArg("...".to_string()), CheckCode::S107 => CheckKind::HardcodedPasswordDefault("...".to_string()), + CheckCode::C901 => CheckKind::FunctionIsTooComplex("...".to_string(), 10), // Ruff CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'), CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'), @@ -1030,6 +1038,7 @@ impl CheckCode { CheckCode::S105 => CheckCategory::Flake8Bandit, CheckCode::S106 => CheckCategory::Flake8Bandit, CheckCode::S107 => CheckCategory::Flake8Bandit, + CheckCode::C901 => CheckCategory::McCabe, CheckCode::RUF001 => CheckCategory::Ruff, CheckCode::RUF002 => CheckCategory::Ruff, CheckCode::RUF003 => CheckCategory::Ruff, @@ -1250,6 +1259,8 @@ impl CheckKind { CheckKind::HardcodedPasswordString(..) => &CheckCode::S105, CheckKind::HardcodedPasswordFuncArg(..) => &CheckCode::S106, CheckKind::HardcodedPasswordDefault(..) => &CheckCode::S107, + // McCabe + CheckKind::FunctionIsTooComplex(..) => &CheckCode::C901, // Ruff CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001, CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002, @@ -1902,6 +1913,10 @@ impl CheckKind { CheckKind::HardcodedPasswordDefault(string) => { format!("Possible hardcoded password: `\"{string}\"`") } + // McCabe + CheckKind::FunctionIsTooComplex(name, complexity) => { + format!("`{name}` is too complex ({complexity})") + } // Ruff CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => { format!( diff --git a/src/checks_gen.rs b/src/checks_gen.rs index 9fddec318c..135992b939 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -83,6 +83,9 @@ pub enum CheckCodePrefix { C415, C416, C417, + C9, + C90, + C901, D, D1, D10, @@ -497,6 +500,7 @@ impl CheckCodePrefix { CheckCode::C415, CheckCode::C416, CheckCode::C417, + CheckCode::C901, ], CheckCodePrefix::C4 => vec![ CheckCode::C400, @@ -552,6 +556,9 @@ impl CheckCodePrefix { CheckCodePrefix::C415 => vec![CheckCode::C415], CheckCodePrefix::C416 => vec![CheckCode::C416], CheckCodePrefix::C417 => vec![CheckCode::C417], + CheckCodePrefix::C9 => vec![CheckCode::C901], + CheckCodePrefix::C90 => vec![CheckCode::C901], + CheckCodePrefix::C901 => vec![CheckCode::C901], CheckCodePrefix::D => vec![ CheckCode::D100, CheckCode::D101, @@ -1246,6 +1253,9 @@ impl CheckCodePrefix { CheckCodePrefix::C415 => PrefixSpecificity::Explicit, CheckCodePrefix::C416 => PrefixSpecificity::Explicit, CheckCodePrefix::C417 => PrefixSpecificity::Explicit, + CheckCodePrefix::C9 => PrefixSpecificity::Hundreds, + CheckCodePrefix::C90 => PrefixSpecificity::Tens, + CheckCodePrefix::C901 => PrefixSpecificity::Explicit, CheckCodePrefix::D => PrefixSpecificity::Category, CheckCodePrefix::D1 => PrefixSpecificity::Hundreds, CheckCodePrefix::D10 => PrefixSpecificity::Tens, diff --git a/src/cli.rs b/src/cli.rs index c120ebafe6..d112979cd5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -91,6 +91,9 @@ pub struct Cli { /// formatting. #[arg(long)] pub line_length: Option, + /// Max McCabe complexity allowed for a function. + #[arg(long)] + pub max_complexity: Option, /// Round-trip auto-formatting. // TODO(charlie): This should be a sub-command. #[arg(long, hide = true)] diff --git a/src/lib.rs b/src/lib.rs index 0c974c72ca..349319a090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ mod isort; mod lex; pub mod linter; pub mod logging; +pub mod mccabe; pub mod message; mod noqa; pub mod pep8_naming; diff --git a/src/main.rs b/src/main.rs index eef2049dee..bc428e59b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -269,6 +269,9 @@ fn inner_main() -> Result { if let Some(line_length) = cli.line_length { configuration.line_length = line_length; } + if let Some(max_complexity) = cli.max_complexity { + configuration.mccabe.max_complexity = max_complexity; + } if let Some(target_version) = cli.target_version { configuration.target_version = target_version; } diff --git a/src/mccabe/checks.rs b/src/mccabe/checks.rs new file mode 100644 index 0000000000..1c78e5f866 --- /dev/null +++ b/src/mccabe/checks.rs @@ -0,0 +1,73 @@ +use rustpython_ast::{ExcepthandlerKind, ExprKind, Stmt, StmtKind}; + +use crate::ast::types::Range; +use crate::checks::{Check, CheckKind}; + +fn get_complexity_number(stmts: &[Stmt]) -> usize { + let mut complexity = 0; + for stmt in stmts { + match &stmt.node { + StmtKind::If { body, orelse, .. } => { + complexity += 1; + complexity += get_complexity_number(body); + complexity += get_complexity_number(orelse); + } + StmtKind::For { body, orelse, .. } | StmtKind::AsyncFor { body, orelse, .. } => { + complexity += 1; + complexity += get_complexity_number(body); + complexity += get_complexity_number(orelse); + } + StmtKind::While { test, body, orelse } => { + complexity += 1; + complexity += get_complexity_number(body); + complexity += get_complexity_number(orelse); + if let ExprKind::BoolOp { .. } = &test.node { + complexity += 1; + } + } + StmtKind::Try { + body, + handlers, + orelse, + finalbody, + } => { + complexity += 1; + complexity += get_complexity_number(body); + complexity += get_complexity_number(orelse); + complexity += get_complexity_number(finalbody); + for handler in handlers { + complexity += 1; + let ExcepthandlerKind::ExceptHandler { body, .. } = &handler.node; + complexity += get_complexity_number(body); + } + } + StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => { + complexity += 1; + complexity += get_complexity_number(body); + } + StmtKind::ClassDef { body, .. } => { + complexity += 1; + complexity += get_complexity_number(body); + } + _ => {} + } + } + complexity +} + +pub fn function_is_too_complex( + stmt: &Stmt, + name: &str, + body: &[Stmt], + max_complexity: usize, +) -> Option { + let complexity = get_complexity_number(body) + 1; + if complexity > max_complexity { + Some(Check::new( + CheckKind::FunctionIsTooComplex(name.to_string(), complexity), + Range::from_located(stmt), + )) + } else { + None + } +} diff --git a/src/mccabe/mod.rs b/src/mccabe/mod.rs new file mode 100644 index 0000000000..316bbaac9c --- /dev/null +++ b/src/mccabe/mod.rs @@ -0,0 +1,33 @@ +pub mod checks; +pub mod settings; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::autofix::fixer; + use crate::checks::CheckCode; + use crate::linter::test_path; + use crate::{mccabe, Settings}; + + #[test_case(0)] + #[test_case(3)] + #[test_case(10)] + fn max_complexity_zero(max_complexity: usize) -> Result<()> { + let snapshot = format!("max_complexity_{}", max_complexity); + let mut checks = test_path( + Path::new("./resources/test/fixtures/C901.py"), + &Settings { + mccabe: mccabe::settings::Settings { max_complexity }, + ..Settings::for_rules(vec![CheckCode::C901]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } +} diff --git a/src/mccabe/settings.rs b/src/mccabe/settings.rs new file mode 100644 index 0000000000..2f1cde3a50 --- /dev/null +++ b/src/mccabe/settings.rs @@ -0,0 +1,28 @@ +//! Settings for the `mccabe` plugin. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct Options { + pub max_complexity: Option, +} + +#[derive(Debug, Hash)] +pub struct Settings { + pub max_complexity: usize, +} + +impl Settings { + pub fn from_options(options: Options) -> Self { + Self { + max_complexity: options.max_complexity.unwrap_or_default(), + } + } +} + +impl Default for Settings { + fn default() -> Self { + Self { max_complexity: 10 } + } +} diff --git a/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_0.snap b/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_0.snap new file mode 100644 index 0000000000..2f5343cc90 --- /dev/null +++ b/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_0.snap @@ -0,0 +1,170 @@ +--- +source: src/mccabe/mod.rs +expression: checks +--- +- kind: + FunctionIsTooComplex: + - trivial + - 1 + location: + row: 2 + column: 0 + end_location: + row: 7 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - expr_as_statement + - 1 + location: + row: 7 + column: 0 + end_location: + row: 12 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - sequential + - 1 + location: + row: 12 + column: 0 + end_location: + row: 19 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - if_elif_else_dead_path + - 3 + location: + row: 19 + column: 0 + end_location: + row: 29 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - nested_ifs + - 3 + location: + row: 29 + column: 0 + end_location: + row: 40 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - for_loop + - 2 + location: + row: 40 + column: 0 + end_location: + row: 46 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - for_else + - 2 + location: + row: 46 + column: 0 + end_location: + row: 54 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - recursive + - 2 + location: + row: 54 + column: 0 + end_location: + row: 62 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - nested_functions + - 3 + location: + row: 62 + column: 0 + end_location: + row: 73 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - a + - 2 + location: + row: 63 + column: 4 + end_location: + row: 69 + column: 4 + fix: ~ +- kind: + FunctionIsTooComplex: + - b + - 1 + location: + row: 64 + column: 8 + end_location: + row: 67 + column: 8 + fix: ~ +- kind: + FunctionIsTooComplex: + - try_else + - 4 + location: + row: 73 + column: 0 + end_location: + row: 85 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - nested_try_finally + - 3 + location: + row: 85 + column: 0 + end_location: + row: 96 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - foobar + - 3 + location: + row: 96 + column: 0 + end_location: + row: 107 + column: 0 + fix: ~ +- kind: + FunctionIsTooComplex: + - annotated_assign + - 1 + location: + row: 107 + column: 0 + end_location: + row: 109 + column: 0 + fix: ~ + diff --git a/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_10.snap b/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_10.snap new file mode 100644 index 0000000000..8efd1fb269 --- /dev/null +++ b/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_10.snap @@ -0,0 +1,6 @@ +--- +source: src/mccabe/mod.rs +expression: checks +--- +[] + diff --git a/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_3.snap b/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_3.snap new file mode 100644 index 0000000000..8a838f96b4 --- /dev/null +++ b/src/mccabe/snapshots/ruff__mccabe__tests__max_complexity_3.snap @@ -0,0 +1,16 @@ +--- +source: src/mccabe/mod.rs +expression: checks +--- +- kind: + FunctionIsTooComplex: + - try_else + - 4 + location: + row: 73 + column: 0 + end_location: + row: 85 + column: 0 + fix: ~ + diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 906092d6c7..53cb60cf6c 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -13,7 +13,8 @@ use crate::checks_gen::CheckCodePrefix; use crate::settings::pyproject::load_options; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; use crate::{ - flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, pep8_naming, + flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, mccabe, + pep8_naming, }; #[derive(Debug)] @@ -36,6 +37,7 @@ pub struct Configuration { pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_tidy_imports: flake8_tidy_imports::settings::Settings, pub isort: isort::settings::Settings, + pub mccabe: mccabe::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, } @@ -153,6 +155,10 @@ impl Configuration { .isort .map(isort::settings::Settings::from_options) .unwrap_or_default(), + mccabe: options + .mccabe + .map(mccabe::settings::Settings::from_options) + .unwrap_or_default(), pep8_naming: options .pep8_naming .map(pep8_naming::settings::Settings::from_options) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index c16d73d89f..567c3178f9 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -14,7 +14,8 @@ use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; use crate::{ - flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, pep8_naming, + flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, mccabe, + pep8_naming, }; pub mod configuration; @@ -39,6 +40,7 @@ pub struct Settings { pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_tidy_imports: flake8_tidy_imports::settings::Settings, pub isort: isort::settings::Settings, + pub mccabe: mccabe::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, } @@ -59,6 +61,7 @@ impl Settings { flake8_quotes: config.flake8_quotes, flake8_tidy_imports: config.flake8_tidy_imports, isort: config.isort, + mccabe: config.mccabe, line_length: config.line_length, pep8_naming: config.pep8_naming, per_file_ignores: config.per_file_ignores, @@ -82,6 +85,7 @@ impl Settings { flake8_quotes: Default::default(), flake8_tidy_imports: Default::default(), isort: Default::default(), + mccabe: Default::default(), pep8_naming: Default::default(), } } @@ -101,6 +105,7 @@ impl Settings { flake8_quotes: Default::default(), flake8_tidy_imports: Default::default(), isort: Default::default(), + mccabe: Default::default(), pep8_naming: Default::default(), } } @@ -124,6 +129,7 @@ impl Hash for Settings { self.flake8_quotes.hash(state); self.flake8_tidy_imports.hash(state); self.isort.hash(state); + self.mccabe.hash(state); self.pep8_naming.hash(state); } } diff --git a/src/settings/options.rs b/src/settings/options.rs index 574e0d7e6a..0989e52501 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::checks_gen::CheckCodePrefix; use crate::settings::types::PythonVersion; use crate::{ - flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, pep8_naming, + flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, mccabe, + pep8_naming, }; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] @@ -29,6 +30,7 @@ pub struct Options { pub flake8_quotes: Option, pub flake8_tidy_imports: Option, pub isort: Option, + pub mccabe: Option, pub pep8_naming: Option, // Tables are required to go last. pub per_file_ignores: Option>>, diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 7b11f75549..8e789d4189 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -110,7 +110,7 @@ mod tests { find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; use crate::settings::types::PatternPrefixPair; - use crate::{flake8_bugbear, flake8_quotes, flake8_tidy_imports, pep8_naming}; + use crate::{flake8_bugbear, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming}; #[test] fn deserialize() -> Result<()> { @@ -151,6 +151,7 @@ mod tests { flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }) }) @@ -184,6 +185,7 @@ line-length = 79 flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }) }) @@ -217,6 +219,7 @@ exclude = ["foo.py"] flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }) }) @@ -250,6 +253,7 @@ select = ["E501"] flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }) }) @@ -284,6 +288,7 @@ ignore = ["E501"] flake8_quotes: None, flake8_tidy_imports: None, isort: None, + mccabe: None, pep8_naming: None, }) }) @@ -376,6 +381,9 @@ other-attribute = 1 ban_relative_imports: Some(Strictness::Parents) }), isort: None, + mccabe: Some(mccabe::settings::Options { + max_complexity: Some(10), + }), pep8_naming: Some(pep8_naming::settings::Options { ignore_names: Some(vec![ "setUp".to_string(),