rude-clean-code 0.0.0__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. rude_clean_code-0.1.0/.gitignore +17 -0
  2. rude_clean_code-0.1.0/LICENSE +21 -0
  3. rude_clean_code-0.1.0/PKG-INFO +80 -0
  4. rude_clean_code-0.1.0/README.md +53 -0
  5. rude_clean_code-0.1.0/features/deterministic_rules.feature +24 -0
  6. rude_clean_code-0.1.0/features/package_version.feature +8 -0
  7. rude_clean_code-0.1.0/pyproject.toml +54 -0
  8. rude_clean_code-0.1.0/src/rude_clean_code/__init__.py +7 -0
  9. rude_clean_code-0.1.0/src/rude_clean_code/_scope.py +91 -0
  10. rude_clean_code-0.1.0/src/rude_clean_code/plugin.py +20 -0
  11. rude_clean_code-0.1.0/src/rude_clean_code/rules/__init__.py +1 -0
  12. rude_clean_code-0.1.0/src/rude_clean_code/rules/classes.py +116 -0
  13. rude_clean_code-0.1.0/src/rude_clean_code/rules/complexity.py +170 -0
  14. rude_clean_code-0.1.0/src/rude_clean_code/rules/functions.py +171 -0
  15. rude_clean_code-0.1.0/tests/test_class_rules.py +119 -0
  16. rude_clean_code-0.1.0/tests/test_complexity_rules.py +231 -0
  17. rude_clean_code-0.1.0/tests/test_deterministic_rules_bdd.py +223 -0
  18. rude_clean_code-0.1.0/tests/test_function_rules.py +271 -0
  19. rude_clean_code-0.1.0/tests/test_package_version_bdd.py +81 -0
  20. rude_clean_code-0.1.0/tests/test_plugin_discovery.py +112 -0
  21. rude_clean_code-0.1.0/tests/test_scope_traversal.py +96 -0
  22. rude_clean_code-0.0.0/PKG-INFO +0 -4
  23. rude_clean_code-0.0.0/pyproject.toml +0 -11
  24. rude_clean_code-0.0.0/setup.cfg +0 -4
  25. rude_clean_code-0.0.0/src/rude_clean_code.egg-info/PKG-INFO +0 -4
  26. rude_clean_code-0.0.0/src/rude_clean_code.egg-info/SOURCES.txt +0 -6
  27. rude_clean_code-0.0.0/src/rude_clean_code.egg-info/dependency_links.txt +0 -1
  28. rude_clean_code-0.0.0/src/rude_clean_code.egg-info/top_level.txt +0 -1
  29. /rude_clean_code-0.0.0/src/rude_clean_code/__init__.py → /rude_clean_code-0.1.0/src/rude_clean_code/py.typed +0 -0
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pixi/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ distributions/
8
+ .coverage
9
+ .coverage.*
10
+ coverage.xml
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .hypothesis/
15
+ node_modules/
16
+ .tox/
17
+ .doit.db*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Archont561
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: rude-clean-code
3
+ Version: 0.1.0
4
+ Summary: Clean Code plugin for the Rude Python linter.
5
+ Project-URL: Homepage, https://github.com/Archont561/rude-clean-code#readme
6
+ Project-URL: Documentation, https://github.com/Archont561/rude-clean-code/blob/main/docs/README.md
7
+ Project-URL: Repository, https://github.com/Archont561/rude-clean-code
8
+ Project-URL: Issues, https://github.com/Archont561/rude-clean-code/issues
9
+ Project-URL: Changelog, https://github.com/Archont561/rude-clean-code/blob/main/CHANGELOG.md
10
+ Author: Archont561
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: clean-code,code-quality,linter,rude,solid,static-analysis
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: rude<0.2,>=0.1a4
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Rude Clean Code
29
+
30
+ Clean Code plugin for the [Rude](https://pypi.org/project/rude/) Python linter,
31
+ exposing deterministic size, parameter, scope, and complexity rules
32
+ (`CC101`–`CC104`, `CC201`–`CC202`, `CC301`–`CC303`) through Rude's
33
+ `rude.plugins` entry point.
34
+
35
+ ## Installation
36
+
37
+ ```sh
38
+ pip install rude-clean-code
39
+ ```
40
+
41
+ Rude discovers the plugin automatically via `rude.plugins` once installed.
42
+
43
+ ## Basic profile
44
+
45
+ Add the **Basic** profile snippet to your `pyproject.toml`:
46
+
47
+ ```toml
48
+ [tool.rude]
49
+ select = ["CC1", "CC2", "CC3"]
50
+
51
+ [tool.rude.rules.CC101]
52
+ max_lines = 30
53
+ ```
54
+
55
+ Run Rude from the command line:
56
+
57
+ ```sh
58
+ rude check --select CC src/
59
+ ```
60
+
61
+ All deterministic core rules report `WARNING` severity with measured value,
62
+ configured limit, and a suggested refactoring action.
63
+
64
+ ## Implemented rules
65
+
66
+ - **Functions (`CC1xx`)**: `CC101` (`max_lines = 30`), `CC102`
67
+ (`max_parameters = 5`), `CC103` (`max_returns = 5`), `CC104`
68
+ (`max_locals = 10`)
69
+ - **Classes (`CC2xx`)**: `CC201` (`max_lines = 200`), `CC202`
70
+ (`max_methods = 15`)
71
+ - **Complexity (`CC3xx`)**: `CC301` (`max_depth = 4`), `CC302`
72
+ (`max_operands = 3`), `CC303` (`max_branches = 10`)
73
+
74
+ The package initializer `rude_clean_code` exposes only `VERSION` (read from
75
+ installed distribution metadata); plugin rules live in `rude_clean_code.plugin`.
76
+
77
+ See the
78
+ [repository README](https://github.com/Archont561/rude-clean-code#readme) and
79
+ [rule reference](https://github.com/Archont561/rude-clean-code/blob/main/docs/reference/rules.md)
80
+ for full detection contracts, configuration options, and the backlog roadmap.
@@ -0,0 +1,53 @@
1
+ # Rude Clean Code
2
+
3
+ Clean Code plugin for the [Rude](https://pypi.org/project/rude/) Python linter,
4
+ exposing deterministic size, parameter, scope, and complexity rules
5
+ (`CC101`–`CC104`, `CC201`–`CC202`, `CC301`–`CC303`) through Rude's
6
+ `rude.plugins` entry point.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ pip install rude-clean-code
12
+ ```
13
+
14
+ Rude discovers the plugin automatically via `rude.plugins` once installed.
15
+
16
+ ## Basic profile
17
+
18
+ Add the **Basic** profile snippet to your `pyproject.toml`:
19
+
20
+ ```toml
21
+ [tool.rude]
22
+ select = ["CC1", "CC2", "CC3"]
23
+
24
+ [tool.rude.rules.CC101]
25
+ max_lines = 30
26
+ ```
27
+
28
+ Run Rude from the command line:
29
+
30
+ ```sh
31
+ rude check --select CC src/
32
+ ```
33
+
34
+ All deterministic core rules report `WARNING` severity with measured value,
35
+ configured limit, and a suggested refactoring action.
36
+
37
+ ## Implemented rules
38
+
39
+ - **Functions (`CC1xx`)**: `CC101` (`max_lines = 30`), `CC102`
40
+ (`max_parameters = 5`), `CC103` (`max_returns = 5`), `CC104`
41
+ (`max_locals = 10`)
42
+ - **Classes (`CC2xx`)**: `CC201` (`max_lines = 200`), `CC202`
43
+ (`max_methods = 15`)
44
+ - **Complexity (`CC3xx`)**: `CC301` (`max_depth = 4`), `CC302`
45
+ (`max_operands = 3`), `CC303` (`max_branches = 10`)
46
+
47
+ The package initializer `rude_clean_code` exposes only `VERSION` (read from
48
+ installed distribution metadata); plugin rules live in `rude_clean_code.plugin`.
49
+
50
+ See the
51
+ [repository README](https://github.com/Archont561/rude-clean-code#readme) and
52
+ [rule reference](https://github.com/Archont561/rude-clean-code/blob/main/docs/reference/rules.md)
53
+ for full detection contracts, configuration options, and the backlog roadmap.
@@ -0,0 +1,24 @@
1
+ Feature: Deterministic Clean Code core rules
2
+ Scenario: Rude discovers deterministic Clean Code rules via entry point
3
+ Given the rude-clean-code plugin is installed
4
+ When Rude discovers plugin rules
5
+ Then all nine deterministic CC rules are available with WARNING severity
6
+ And selecting by CC, family prefix, or exact code filters the matching rules
7
+
8
+ Scenario: Consumer runs Basic profile and configured thresholds on Python code
9
+ Given a temporary project configured with the Basic profile
10
+ And a Python file violating deterministic CC rules and a clean nested function
11
+ When Rude checks the temporary project
12
+ Then diagnostics report measured value, configured limit, and suggested action as warnings
13
+ And Rude exits successfully without treating warnings as errors
14
+
15
+ Scenario: Consumer overrides a deterministic rule threshold in pyproject.toml
16
+ Given a temporary project setting CC101 max_lines to 4
17
+ And a Python function spanning 5 lines
18
+ When Rude checks the temporary project
19
+ Then Rude reports CC101 with measured 5 and limit 4
20
+
21
+ Scenario: Dogfooding deterministic CC rules on the package source
22
+ Given the rude-clean-code plugin is installed
23
+ When Rude checks the package source directory with CC selected
24
+ Then no CC diagnostics are emitted
@@ -0,0 +1,8 @@
1
+ Feature: Rude Clean Code package version discovery
2
+ Scenario: Discover the installed Rude Clean Code package version
3
+ Given the package is installed
4
+ When its root namespace is inspected
5
+ Then VERSION is its only public export
6
+ And VERSION matches pyproject and installed distribution metadata
7
+ And VERSION is read from distribution metadata
8
+ And importing the root namespace does not load rude or plugin submodules
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<1.30"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rude-clean-code"
7
+ version = "0.1.0"
8
+ description = "Clean Code plugin for the Rude Python linter."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Archont561" }]
14
+ keywords = ["rude", "linter", "clean-code", "solid", "static-analysis", "code-quality"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Topic :: Software Development :: Quality Assurance",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "rude>=0.1a4,<0.2",
29
+ ]
30
+
31
+ [project.entry-points."rude.plugins"]
32
+ rude_clean_code = "rude_clean_code.plugin"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/Archont561/rude-clean-code#readme"
36
+ Documentation = "https://github.com/Archont561/rude-clean-code/blob/main/docs/README.md"
37
+ Repository = "https://github.com/Archont561/rude-clean-code"
38
+ Issues = "https://github.com/Archont561/rude-clean-code/issues"
39
+ Changelog = "https://github.com/Archont561/rude-clean-code/blob/main/CHANGELOG.md"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/rude_clean_code"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = [
46
+ "src/**",
47
+ "tests/**",
48
+ "features/**",
49
+ "/README.md",
50
+ "/LICENSE",
51
+ ]
52
+
53
+ [tool.rude]
54
+ select = ["CC1", "CC2", "CC3"]
@@ -0,0 +1,7 @@
1
+ """Rude Clean Code package version."""
2
+
3
+ from importlib.metadata import version as _version
4
+
5
+ VERSION = _version("rude-clean-code")
6
+
7
+ __all__ = ["VERSION"]
@@ -0,0 +1,91 @@
1
+ """Shared own-scope AST traversal and threshold rule base helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ from rude.core.rule import Rule
9
+ from rude.core.types import Diagnostic, Severity
10
+
11
+ if TYPE_CHECKING:
12
+ from rude.core.node import Node
13
+
14
+ SCOPE_BOUNDARY_TYPES: frozenset[str] = frozenset(
15
+ {
16
+ "function_definition",
17
+ "class_definition",
18
+ "lambda",
19
+ }
20
+ )
21
+
22
+
23
+ def definition_name(node: Node) -> str:
24
+ """Return the readable name of a function or class definition node."""
25
+ return node.name or "<anonymous>"
26
+
27
+
28
+ def iter_own_scope(root: Node) -> Iterator[Node]:
29
+ """Yield own-scope descendants without entering nested functions, classes, or lambdas."""
30
+ stack = list(reversed(root.named_children))
31
+ while stack:
32
+ current = stack.pop()
33
+ if current.type in SCOPE_BOUNDARY_TYPES:
34
+ continue
35
+ yield current
36
+ stack.extend(reversed(current.named_children))
37
+
38
+
39
+ def max_own_scope_depth(root: Node, control_types: frozenset[str]) -> int:
40
+ """Return maximum nesting depth of *control_types* in *root*'s own scope."""
41
+ max_depth = 0
42
+ stack: list[tuple[Node, int]] = [
43
+ (child, 0) for child in reversed(root.named_children)
44
+ ]
45
+ while stack:
46
+ current, depth = stack.pop()
47
+ if current.type in SCOPE_BOUNDARY_TYPES:
48
+ continue
49
+ next_depth = depth + 1 if current.type in control_types else depth
50
+ max_depth = max(max_depth, next_depth)
51
+ stack.extend((child, next_depth) for child in reversed(current.named_children))
52
+ return max_depth
53
+
54
+
55
+ class ThresholdRule(Rule):
56
+ """Base class for deterministic rules governed by one non-negative integer limit."""
57
+
58
+ severity: ClassVar[Severity | None] = Severity.WARNING
59
+ option_name: ClassVar[str]
60
+ default_limit: ClassVar[int]
61
+
62
+ def __init__(self) -> None:
63
+ setattr(self, self.option_name, self.default_limit)
64
+
65
+ @property
66
+ def limit(self) -> int:
67
+ """Configured upper bound (inclusive) for this rule."""
68
+ return int(getattr(self, self.option_name))
69
+
70
+ def configure(self, options: dict[str, Any]) -> None:
71
+ """Validate and apply the configured threshold from ``[tool.rude.rules.<CODE>]``."""
72
+ if self.option_name not in options:
73
+ return
74
+ raw = options[self.option_name]
75
+ if isinstance(raw, bool) or not isinstance(raw, int) or raw < 0:
76
+ msg = (
77
+ f"{self.code} option '{self.option_name}' must be a "
78
+ f"non-negative integer, got {raw!r}"
79
+ )
80
+ raise ValueError(msg)
81
+ setattr(self, self.option_name, raw)
82
+
83
+ def emit_if_exceeds(
84
+ self,
85
+ node: Node,
86
+ measured: int,
87
+ message: str,
88
+ ) -> Iterator[Diagnostic]:
89
+ """Yield a diagnostic on *node* when *measured* exceeds the configured limit."""
90
+ if measured > self.limit:
91
+ yield self.diagnostic(node, message)
@@ -0,0 +1,20 @@
1
+ """Rude entry-point module exposing implemented Clean Code rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rude_clean_code.rules.classes import CLASS_RULES
8
+ from rude_clean_code.rules.complexity import COMPLEXITY_RULES
9
+ from rude_clean_code.rules.functions import FUNCTION_RULES
10
+
11
+ if TYPE_CHECKING:
12
+ from rude.core.rule import Rule
13
+
14
+ RULES: list[type[Rule]] = [
15
+ *FUNCTION_RULES,
16
+ *CLASS_RULES,
17
+ *COMPLEXITY_RULES,
18
+ ]
19
+
20
+ __all__ = ["RULES"]
@@ -0,0 +1 @@
1
+ """Rule implementations grouped by Clean Code family."""
@@ -0,0 +1,116 @@
1
+ """Deterministic Clean Code rules for classes (CC201-CC202)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import TYPE_CHECKING, ClassVar
7
+
8
+ from rude.core.node_types import NodeType
9
+
10
+ from rude_clean_code._scope import ThresholdRule, definition_name
11
+
12
+ if TYPE_CHECKING:
13
+ from rude.core.node import Node
14
+ from rude.core.types import Diagnostic
15
+
16
+
17
+ def _is_dunder(name: str) -> bool:
18
+ return len(name) > 4 and name.startswith("__") and name.endswith("__")
19
+
20
+
21
+ def _direct_method_node(child: Node) -> Node | None:
22
+ if child.type == "function_definition":
23
+ return child
24
+ if child.type == "decorated_definition":
25
+ definition = child.child_by_field("definition")
26
+ if definition is not None and definition.type == "function_definition":
27
+ return definition
28
+ return None
29
+
30
+
31
+ def _count_direct_methods(body: Node) -> tuple[int, int]:
32
+ total = 0
33
+ dunders = 0
34
+ for child in body.named_children:
35
+ method = _direct_method_node(child)
36
+ if method is None:
37
+ continue
38
+ total += 1
39
+ if _is_dunder(definition_name(method)):
40
+ dunders += 1
41
+ return total, dunders
42
+
43
+
44
+ class ClassTooLarge(ThresholdRule):
45
+ """CC201: Class spans too many lines.
46
+
47
+ Large classes tend to accumulate unrelated responsibilities. This size check
48
+ serves as an early prompt to inspect cohesion.
49
+
50
+ Configuration::
51
+
52
+ [tool.rude.rules.CC201]
53
+ max_lines = 200
54
+ """
55
+
56
+ code: ClassVar[str] = "CC201"
57
+ message: ClassVar[str] = "Class spans too many lines"
58
+ node_types: ClassVar[set[NodeType] | None] = {NodeType.CLASS_DEFINITION}
59
+ option_name: ClassVar[str] = "max_lines"
60
+ default_limit: ClassVar[int] = 200
61
+
62
+ def check(self, node: Node) -> Iterator[Diagnostic]:
63
+ """Flag class definitions spanning more lines than ``max_lines``."""
64
+ lines = node.end_line - node.line + 1
65
+ name = definition_name(node)
66
+ msg = (
67
+ f"Class '{name}' is too large ({lines} > {self.limit} lines); "
68
+ "extract cohesive helper classes or modules"
69
+ )
70
+ yield from self.emit_if_exceeds(node, lines, msg)
71
+
72
+
73
+ class TooManyMethods(ThresholdRule):
74
+ """CC202: Class defines too many direct methods.
75
+
76
+ Classes exposing a large method surface often violate the Single
77
+ Responsibility Principle. Dunder protocol methods are included in the total
78
+ and counted separately in the diagnostic message for context.
79
+
80
+ Configuration::
81
+
82
+ [tool.rude.rules.CC202]
83
+ max_methods = 15
84
+ """
85
+
86
+ code: ClassVar[str] = "CC202"
87
+ message: ClassVar[str] = "Class defines too many direct methods"
88
+ node_types: ClassVar[set[NodeType] | None] = {NodeType.CLASS_DEFINITION}
89
+ option_name: ClassVar[str] = "max_methods"
90
+ default_limit: ClassVar[int] = 15
91
+
92
+ def check(self, node: Node) -> Iterator[Diagnostic]:
93
+ """Flag classes defining more direct methods than ``max_methods``."""
94
+ body = node.body
95
+ if body is None:
96
+ return
97
+ total, dunders = _count_direct_methods(body)
98
+ name = definition_name(node)
99
+ msg = (
100
+ f"Class '{name}' has too many methods "
101
+ f"({total} > {self.limit}, including {dunders} dunder); "
102
+ "extract cohesive collaborators or helper classes"
103
+ )
104
+ yield from self.emit_if_exceeds(node, total, msg)
105
+
106
+
107
+ CLASS_RULES: list[type[ThresholdRule]] = [
108
+ ClassTooLarge,
109
+ TooManyMethods,
110
+ ]
111
+
112
+ __all__ = [
113
+ "CLASS_RULES",
114
+ "ClassTooLarge",
115
+ "TooManyMethods",
116
+ ]
@@ -0,0 +1,170 @@
1
+ """Deterministic Clean Code rules for control complexity (CC301-CC303)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import TYPE_CHECKING, ClassVar
7
+
8
+ from rude.core.node_types import NodeType
9
+
10
+ from rude_clean_code._scope import (
11
+ ThresholdRule,
12
+ definition_name,
13
+ iter_own_scope,
14
+ max_own_scope_depth,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from rude.core.node import Node
19
+ from rude.core.types import Diagnostic
20
+
21
+ NESTING_NODE_TYPES: frozenset[str] = frozenset(
22
+ {
23
+ "if_statement",
24
+ "for_statement",
25
+ "while_statement",
26
+ "try_statement",
27
+ "with_statement",
28
+ "match_statement",
29
+ }
30
+ )
31
+
32
+ BRANCH_NODE_TYPES: frozenset[str] = frozenset(
33
+ {
34
+ "if_statement",
35
+ "elif_clause",
36
+ "for_statement",
37
+ "while_statement",
38
+ "boolean_operator",
39
+ "conditional_expression",
40
+ "case_clause",
41
+ }
42
+ )
43
+
44
+
45
+ def _count_boolean_operands(node: Node) -> int:
46
+ count = 0
47
+ stack = [node]
48
+ while stack:
49
+ current = stack.pop()
50
+ for field in ("left", "right"):
51
+ operand = current.child_by_field(field)
52
+ if operand is None:
53
+ continue
54
+ if operand.type == "boolean_operator":
55
+ stack.append(operand)
56
+ else:
57
+ count += 1
58
+ return count
59
+
60
+
61
+ class ExcessiveNesting(ThresholdRule):
62
+ """CC301: Function exceeds maximum control-structure nesting depth.
63
+
64
+ Deeply nested ``if``/``for``/``while``/``try``/``with``/``match`` blocks
65
+ increase cognitive load; flattening with guard clauses and helper functions
66
+ keeps intent obvious. Complements Rude's ``C901``.
67
+
68
+ Configuration::
69
+
70
+ [tool.rude.rules.CC301]
71
+ max_depth = 4
72
+ """
73
+
74
+ code: ClassVar[str] = "CC301"
75
+ message: ClassVar[str] = "Function exceeds maximum control nesting depth"
76
+ node_types: ClassVar[set[NodeType] | None] = {NodeType.FUNCTION_DEFINITION}
77
+ option_name: ClassVar[str] = "max_depth"
78
+ default_limit: ClassVar[int] = 4
79
+
80
+ def check(self, node: Node) -> Iterator[Diagnostic]:
81
+ """Flag functions whose deepest own-scope control nesting exceeds ``max_depth``."""
82
+ body = node.body
83
+ if body is None:
84
+ return
85
+ depth = max_own_scope_depth(body, NESTING_NODE_TYPES)
86
+ name = definition_name(node)
87
+ msg = (
88
+ f"Function '{name}' has excessive nesting depth ({depth} > {self.limit}); "
89
+ "use early returns or extract helpers"
90
+ )
91
+ yield from self.emit_if_exceeds(node, depth, msg)
92
+
93
+
94
+ class ExcessiveBooleanOperands(ThresholdRule):
95
+ """CC302: Boolean expression combines too many operands.
96
+
97
+ Long ``and``/``or`` chains encode compound predicates that read far better as
98
+ named booleans or predicate helper functions. Complements Rude's ``C901``.
99
+
100
+ Configuration::
101
+
102
+ [tool.rude.rules.CC302]
103
+ max_operands = 3
104
+ """
105
+
106
+ code: ClassVar[str] = "CC302"
107
+ message: ClassVar[str] = "Boolean expression combines too many operands"
108
+ node_types: ClassVar[set[NodeType] | None] = {NodeType.BOOLEAN_OPERATOR}
109
+ option_name: ClassVar[str] = "max_operands"
110
+ default_limit: ClassVar[int] = 3
111
+
112
+ def check(self, node: Node) -> Iterator[Diagnostic]:
113
+ """Flag outermost boolean chains combining more than ``max_operands`` terms."""
114
+ if node.parent_type == "boolean_operator":
115
+ return
116
+ operands = _count_boolean_operands(node)
117
+ msg = (
118
+ f"Boolean expression has too many operands ({operands} > {self.limit}); "
119
+ "extract named predicate variables or helper functions"
120
+ )
121
+ yield from self.emit_if_exceeds(node, operands, msg)
122
+
123
+
124
+ class ExcessiveBranching(ThresholdRule):
125
+ """CC303: Function contains too many decision points.
126
+
127
+ Counts own-scope ``if``, ``elif``, ``for``, ``while``, ``and``/``or``,
128
+ ternary expressions, and ``match`` cases. Intentionally distinct from and
129
+ complementary to Rude's ``C901``.
130
+
131
+ Configuration::
132
+
133
+ [tool.rude.rules.CC303]
134
+ max_branches = 10
135
+ """
136
+
137
+ code: ClassVar[str] = "CC303"
138
+ message: ClassVar[str] = "Function contains too many decision points"
139
+ node_types: ClassVar[set[NodeType] | None] = {NodeType.FUNCTION_DEFINITION}
140
+ option_name: ClassVar[str] = "max_branches"
141
+ default_limit: ClassVar[int] = 10
142
+
143
+ def check(self, node: Node) -> Iterator[Diagnostic]:
144
+ """Flag functions with more own-scope decision points than ``max_branches``."""
145
+ body = node.body
146
+ if body is None:
147
+ return
148
+ branches = sum(
149
+ 1 for child in iter_own_scope(body) if child.type in BRANCH_NODE_TYPES
150
+ )
151
+ name = definition_name(node)
152
+ msg = (
153
+ f"Function '{name}' has too many branches ({branches} > {self.limit}); "
154
+ "decompose conditional logic or use dispatch"
155
+ )
156
+ yield from self.emit_if_exceeds(node, branches, msg)
157
+
158
+
159
+ COMPLEXITY_RULES: list[type[ThresholdRule]] = [
160
+ ExcessiveNesting,
161
+ ExcessiveBooleanOperands,
162
+ ExcessiveBranching,
163
+ ]
164
+
165
+ __all__ = [
166
+ "COMPLEXITY_RULES",
167
+ "ExcessiveBooleanOperands",
168
+ "ExcessiveBranching",
169
+ "ExcessiveNesting",
170
+ ]