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.
- rude_clean_code-0.1.0/.gitignore +17 -0
- rude_clean_code-0.1.0/LICENSE +21 -0
- rude_clean_code-0.1.0/PKG-INFO +80 -0
- rude_clean_code-0.1.0/README.md +53 -0
- rude_clean_code-0.1.0/features/deterministic_rules.feature +24 -0
- rude_clean_code-0.1.0/features/package_version.feature +8 -0
- rude_clean_code-0.1.0/pyproject.toml +54 -0
- rude_clean_code-0.1.0/src/rude_clean_code/__init__.py +7 -0
- rude_clean_code-0.1.0/src/rude_clean_code/_scope.py +91 -0
- rude_clean_code-0.1.0/src/rude_clean_code/plugin.py +20 -0
- rude_clean_code-0.1.0/src/rude_clean_code/rules/__init__.py +1 -0
- rude_clean_code-0.1.0/src/rude_clean_code/rules/classes.py +116 -0
- rude_clean_code-0.1.0/src/rude_clean_code/rules/complexity.py +170 -0
- rude_clean_code-0.1.0/src/rude_clean_code/rules/functions.py +171 -0
- rude_clean_code-0.1.0/tests/test_class_rules.py +119 -0
- rude_clean_code-0.1.0/tests/test_complexity_rules.py +231 -0
- rude_clean_code-0.1.0/tests/test_deterministic_rules_bdd.py +223 -0
- rude_clean_code-0.1.0/tests/test_function_rules.py +271 -0
- rude_clean_code-0.1.0/tests/test_package_version_bdd.py +81 -0
- rude_clean_code-0.1.0/tests/test_plugin_discovery.py +112 -0
- rude_clean_code-0.1.0/tests/test_scope_traversal.py +96 -0
- rude_clean_code-0.0.0/PKG-INFO +0 -4
- rude_clean_code-0.0.0/pyproject.toml +0 -11
- rude_clean_code-0.0.0/setup.cfg +0 -4
- rude_clean_code-0.0.0/src/rude_clean_code.egg-info/PKG-INFO +0 -4
- rude_clean_code-0.0.0/src/rude_clean_code.egg-info/SOURCES.txt +0 -6
- rude_clean_code-0.0.0/src/rude_clean_code.egg-info/dependency_links.txt +0 -1
- rude_clean_code-0.0.0/src/rude_clean_code.egg-info/top_level.txt +0 -1
- /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,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,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
|
+
]
|