rude-clean-code 0.0.0__tar.gz → 0.2.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.2.0/.gitignore +17 -0
- rude_clean_code-0.2.0/LICENSE +21 -0
- rude_clean_code-0.2.0/PKG-INFO +96 -0
- rude_clean_code-0.2.0/README.md +69 -0
- rude_clean_code-0.2.0/features/deterministic_rules.feature +31 -0
- rude_clean_code-0.2.0/features/module_rules.feature +24 -0
- rude_clean_code-0.2.0/features/package_version.feature +8 -0
- rude_clean_code-0.2.0/pyproject.toml +57 -0
- rude_clean_code-0.2.0/src/rude_clean_code/__init__.py +7 -0
- rude_clean_code-0.2.0/src/rude_clean_code/_scope.py +186 -0
- rude_clean_code-0.2.0/src/rude_clean_code/plugin.py +30 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/__init__.py +1 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/_module_analysis.py +293 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/classes.py +175 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/complexity.py +262 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/dry.py +110 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/errors.py +233 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/functions.py +283 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/heuristics.py +115 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/modules.py +364 -0
- rude_clean_code-0.2.0/src/rude_clean_code/rules/naming.py +350 -0
- rude_clean_code-0.2.0/tests/test_class_rules.py +119 -0
- rude_clean_code-0.2.0/tests/test_complexity_rules.py +231 -0
- rude_clean_code-0.2.0/tests/test_deterministic_rules_bdd.py +311 -0
- rude_clean_code-0.2.0/tests/test_function_rules.py +271 -0
- rude_clean_code-0.2.0/tests/test_heuristic_rules.py +231 -0
- rude_clean_code-0.2.0/tests/test_module_rules.py +402 -0
- rude_clean_code-0.2.0/tests/test_module_rules_bdd.py +218 -0
- rude_clean_code-0.2.0/tests/test_package_version_bdd.py +81 -0
- rude_clean_code-0.2.0/tests/test_plugin_discovery.py +147 -0
- rude_clean_code-0.2.0/tests/test_scope_traversal.py +101 -0
- rude_clean_code-0.2.0/tests/test_structural_and_error_rules.py +281 -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.2.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,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rude-clean-code
|
|
3
|
+
Version: 0.2.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 26 Clean Code rules across module structure, functions, classes,
|
|
32
|
+
complexity, error handling, naming/literals, same-file duplication, and
|
|
33
|
+
polymorphism indicators (`CC001`–`CC601`) through Rude's `rude.plugins`
|
|
34
|
+
entry point.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pip install rude-clean-code
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Rude discovers the plugin automatically via `rude.plugins` once installed.
|
|
43
|
+
|
|
44
|
+
## Configuration profiles
|
|
45
|
+
|
|
46
|
+
Add the **Basic** profile snippet (`CC0`–`CC3`) to your `pyproject.toml`, or
|
|
47
|
+
include `"CC4"`, `"CC5"`, `"CC6"` for the **Strict** profile with heuristic
|
|
48
|
+
indicators:
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
[tool.rude]
|
|
52
|
+
select = ["CC0", "CC1", "CC2", "CC3"]
|
|
53
|
+
|
|
54
|
+
[tool.rude.rules.CC101]
|
|
55
|
+
max_lines = 30
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run Rude from the command line:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
rude check --select CC0,CC1,CC2,CC3 src/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
All `CC0xx`–`CC3xx` rules (except `CC002` at `INFO`), plus `CC403` and
|
|
65
|
+
`CC501`, report `WARNING` severity; `CC002` and subjective indicator rules
|
|
66
|
+
(`CC401`, `CC402`, `CC601`) report `INFO`.
|
|
67
|
+
|
|
68
|
+
## Implemented rules
|
|
69
|
+
|
|
70
|
+
- **Modules (`CC0xx`)**: `CC001` (`max_lines = 400`), `CC002` (`__init__.py`
|
|
71
|
+
contains runtime logic), `CC003` (`__init__.py` missing `__all__`), `CC004`
|
|
72
|
+
(deep relative import, `max_relative_depth = 2`), `CC005` (private symbol
|
|
73
|
+
exported in `__all__`)
|
|
74
|
+
- **Functions (`CC1xx`)**: `CC101` (`max_lines = 30`), `CC102`
|
|
75
|
+
(`max_parameters = 5`), `CC103` (`max_returns = 5`), `CC104`
|
|
76
|
+
(`max_locals = 10`), `CC107` (unused private helper)
|
|
77
|
+
- **Classes (`CC2xx`)**: `CC201` (`max_lines = 200`), `CC202`
|
|
78
|
+
(`max_methods = 15`), `CC207` (`max_bases = 3`)
|
|
79
|
+
- **Complexity and error handling (`CC3xx`)**: `CC301` (`max_depth = 4`),
|
|
80
|
+
`CC302` (`max_operands = 3`), `CC303` (`max_branches = 10`), `CC306`
|
|
81
|
+
(nested ternary), `CC308` (`global` statement), `CC309` (broad exception
|
|
82
|
+
swallowed), `CC310` (missing exception chaining), `CC311` (`max_lines = 15`)
|
|
83
|
+
- **In-file heuristics (`CC4xx`–`CC6xx`, Strict profile)**: `CC401`
|
|
84
|
+
(unclear function name), `CC402` (unclear class/variable name), `CC403`
|
|
85
|
+
(repeated magic literal, `min_occurrences = 3`), `CC501` (duplicate function
|
|
86
|
+
body within the **same file**, `min_lines = 5`; cross-file duplication is
|
|
87
|
+
planned as `CC502`), `CC601` (repeated `isinstance`/attribute switch,
|
|
88
|
+
`min_isinstance = 3`)
|
|
89
|
+
|
|
90
|
+
The package initializer `rude_clean_code` exposes only `VERSION` (read from
|
|
91
|
+
installed distribution metadata); plugin rules live in `rude_clean_code.plugin`.
|
|
92
|
+
|
|
93
|
+
See the
|
|
94
|
+
[repository README](https://github.com/Archont561/rude-clean-code#readme) and
|
|
95
|
+
[rule reference](https://github.com/Archont561/rude-clean-code/blob/main/docs/reference/rules.md)
|
|
96
|
+
for full detection contracts, configuration options, and the backlog roadmap.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Rude Clean Code
|
|
2
|
+
|
|
3
|
+
Clean Code plugin for the [Rude](https://pypi.org/project/rude/) Python linter,
|
|
4
|
+
exposing 26 Clean Code rules across module structure, functions, classes,
|
|
5
|
+
complexity, error handling, naming/literals, same-file duplication, and
|
|
6
|
+
polymorphism indicators (`CC001`–`CC601`) through Rude's `rude.plugins`
|
|
7
|
+
entry point.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install rude-clean-code
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Rude discovers the plugin automatically via `rude.plugins` once installed.
|
|
16
|
+
|
|
17
|
+
## Configuration profiles
|
|
18
|
+
|
|
19
|
+
Add the **Basic** profile snippet (`CC0`–`CC3`) to your `pyproject.toml`, or
|
|
20
|
+
include `"CC4"`, `"CC5"`, `"CC6"` for the **Strict** profile with heuristic
|
|
21
|
+
indicators:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[tool.rude]
|
|
25
|
+
select = ["CC0", "CC1", "CC2", "CC3"]
|
|
26
|
+
|
|
27
|
+
[tool.rude.rules.CC101]
|
|
28
|
+
max_lines = 30
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run Rude from the command line:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
rude check --select CC0,CC1,CC2,CC3 src/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
All `CC0xx`–`CC3xx` rules (except `CC002` at `INFO`), plus `CC403` and
|
|
38
|
+
`CC501`, report `WARNING` severity; `CC002` and subjective indicator rules
|
|
39
|
+
(`CC401`, `CC402`, `CC601`) report `INFO`.
|
|
40
|
+
|
|
41
|
+
## Implemented rules
|
|
42
|
+
|
|
43
|
+
- **Modules (`CC0xx`)**: `CC001` (`max_lines = 400`), `CC002` (`__init__.py`
|
|
44
|
+
contains runtime logic), `CC003` (`__init__.py` missing `__all__`), `CC004`
|
|
45
|
+
(deep relative import, `max_relative_depth = 2`), `CC005` (private symbol
|
|
46
|
+
exported in `__all__`)
|
|
47
|
+
- **Functions (`CC1xx`)**: `CC101` (`max_lines = 30`), `CC102`
|
|
48
|
+
(`max_parameters = 5`), `CC103` (`max_returns = 5`), `CC104`
|
|
49
|
+
(`max_locals = 10`), `CC107` (unused private helper)
|
|
50
|
+
- **Classes (`CC2xx`)**: `CC201` (`max_lines = 200`), `CC202`
|
|
51
|
+
(`max_methods = 15`), `CC207` (`max_bases = 3`)
|
|
52
|
+
- **Complexity and error handling (`CC3xx`)**: `CC301` (`max_depth = 4`),
|
|
53
|
+
`CC302` (`max_operands = 3`), `CC303` (`max_branches = 10`), `CC306`
|
|
54
|
+
(nested ternary), `CC308` (`global` statement), `CC309` (broad exception
|
|
55
|
+
swallowed), `CC310` (missing exception chaining), `CC311` (`max_lines = 15`)
|
|
56
|
+
- **In-file heuristics (`CC4xx`–`CC6xx`, Strict profile)**: `CC401`
|
|
57
|
+
(unclear function name), `CC402` (unclear class/variable name), `CC403`
|
|
58
|
+
(repeated magic literal, `min_occurrences = 3`), `CC501` (duplicate function
|
|
59
|
+
body within the **same file**, `min_lines = 5`; cross-file duplication is
|
|
60
|
+
planned as `CC502`), `CC601` (repeated `isinstance`/attribute switch,
|
|
61
|
+
`min_isinstance = 3`)
|
|
62
|
+
|
|
63
|
+
The package initializer `rude_clean_code` exposes only `VERSION` (read from
|
|
64
|
+
installed distribution metadata); plugin rules live in `rude_clean_code.plugin`.
|
|
65
|
+
|
|
66
|
+
See the
|
|
67
|
+
[repository README](https://github.com/Archont561/rude-clean-code#readme) and
|
|
68
|
+
[rule reference](https://github.com/Archont561/rude-clean-code/blob/main/docs/reference/rules.md)
|
|
69
|
+
for full detection contracts, configuration options, and the backlog roadmap.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Feature: Deterministic Clean Code core rules
|
|
2
|
+
Scenario: Rude discovers Clean Code rules via entry point
|
|
3
|
+
Given the rude-clean-code plugin is installed
|
|
4
|
+
When Rude discovers plugin rules
|
|
5
|
+
Then all registered CC rules are available with their documented severities
|
|
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 Clean Code rules on the package source
|
|
22
|
+
Given the rude-clean-code plugin is installed
|
|
23
|
+
When Rude checks the package source directory with the project Basic profile
|
|
24
|
+
Then no CC diagnostics are emitted
|
|
25
|
+
|
|
26
|
+
Scenario: Consumer runs Strict profile including in-file heuristics
|
|
27
|
+
Given a temporary project configured with the Strict profile
|
|
28
|
+
And a Python file violating heuristic CC rules
|
|
29
|
+
When Rude checks the temporary project
|
|
30
|
+
Then Strict heuristic diagnostics carry expected severities and same-file guidance
|
|
31
|
+
And Rude exits successfully without treating warnings as errors
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Feature: Module and package organization Clean Code rules
|
|
2
|
+
Scenario: Oversized module receives a tailored non-autofix split suggestion
|
|
3
|
+
Given a temporary module project with CC001 max_lines set to 12
|
|
4
|
+
And a Python module with two separate symbol clusters exceeding 12 lines
|
|
5
|
+
When Rude runs CC0 checks on the module project
|
|
6
|
+
Then Rude reports CC001 without an autofix and suggests splitting by clusters
|
|
7
|
+
|
|
8
|
+
Scenario: Package initializer permits facade constructs and configured VERSION
|
|
9
|
+
Given a temporary module project checking CC002 with VERSION allowed
|
|
10
|
+
And a package initializer with VERSION, TYPE_CHECKING, __all__, and a helper function
|
|
11
|
+
When Rude runs CC0 checks on the module project
|
|
12
|
+
Then Rude flags only the helper function in __init__.py at INFO severity
|
|
13
|
+
|
|
14
|
+
Scenario: Non-empty initializer requires __all__ while marker files stay silent
|
|
15
|
+
Given a temporary module project checking CC003
|
|
16
|
+
And an empty marker __init__.py alongside a non-empty __init__.py missing __all__
|
|
17
|
+
When Rude runs CC0 checks on the module project
|
|
18
|
+
Then Rude flags only the non-empty __init__.py for missing __all__
|
|
19
|
+
|
|
20
|
+
Scenario: Deep relative imports and private __all__ exports are flagged
|
|
21
|
+
Given a temporary module project checking CC004 and CC005
|
|
22
|
+
And a module with a 3-dot relative import and __all__ containing a private name and a dunder
|
|
23
|
+
When Rude runs CC0 checks on the module project
|
|
24
|
+
Then Rude reports CC004 and CC005 while keeping __version__ exempt
|
|
@@ -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,57 @@
|
|
|
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.2.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 = ["CC0", "CC1", "CC2", "CC3"]
|
|
55
|
+
|
|
56
|
+
[tool.rude.rules.CC002]
|
|
57
|
+
allowed_names = ["VERSION"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Shared own-scope AST traversal, option parsing, and rule base helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from fnmatch import fnmatch
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
8
|
+
|
|
9
|
+
from rude.core.rule import Rule
|
|
10
|
+
from rude.core.types import Diagnostic, Severity
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from rude.core.node import Node
|
|
14
|
+
from rude.core.types import FileContext
|
|
15
|
+
|
|
16
|
+
SCOPE_BOUNDARY_TYPES: frozenset[str] = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"function_definition",
|
|
19
|
+
"class_definition",
|
|
20
|
+
"lambda",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def definition_name(node: Node) -> str:
|
|
26
|
+
"""Return the readable name of a function or class definition node."""
|
|
27
|
+
return node.name or "<anonymous>"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_dunder(name: str) -> bool:
|
|
31
|
+
"""Return whether *name* is a double-underscore Python dunder identifier."""
|
|
32
|
+
return len(name) > 4 and name.startswith("__") and name.endswith("__")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def unwrap_decorated(node: Node) -> Node:
|
|
36
|
+
"""Return the underlying definition when *node* is a ``decorated_definition``."""
|
|
37
|
+
if node.type != "decorated_definition":
|
|
38
|
+
return node
|
|
39
|
+
inner = node.child_by_field("definition")
|
|
40
|
+
if inner is not None:
|
|
41
|
+
return inner
|
|
42
|
+
for child in node.named_children:
|
|
43
|
+
if child.type != "decorator":
|
|
44
|
+
return child
|
|
45
|
+
return node
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _decorator_target_names(decorator: Node) -> Iterator[str]:
|
|
49
|
+
for child in decorator.named_children:
|
|
50
|
+
target = child.child_by_field("function") if child.type == "call" else child
|
|
51
|
+
if target is None:
|
|
52
|
+
continue
|
|
53
|
+
text = target.text
|
|
54
|
+
if text:
|
|
55
|
+
yield text
|
|
56
|
+
if "." in text:
|
|
57
|
+
yield text.rsplit(".", maxsplit=1)[-1]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def node_decorators(node: Node) -> list[str]:
|
|
61
|
+
"""Return normalized decorator names attached to *node*'s ``decorated_definition``."""
|
|
62
|
+
parent = node.parent
|
|
63
|
+
if parent is None or parent.type != "decorated_definition":
|
|
64
|
+
return []
|
|
65
|
+
names: list[str] = []
|
|
66
|
+
for child in parent.named_children:
|
|
67
|
+
if child.type == "decorator":
|
|
68
|
+
names.extend(_decorator_target_names(child))
|
|
69
|
+
return names
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_test_context(context: FileContext) -> bool:
|
|
73
|
+
"""Return whether *context* points to a test module or file inside a test folder."""
|
|
74
|
+
if context.is_test_file() or context.path.stem == "conftest":
|
|
75
|
+
return True
|
|
76
|
+
parent_parts = set(context.path.parts[:-1])
|
|
77
|
+
return "tests" in parent_parts or "test" in parent_parts
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def matches_exclude_paths(
|
|
81
|
+
context: FileContext,
|
|
82
|
+
patterns: tuple[str, ...],
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""Return whether *context* matches any configured path fragment or glob."""
|
|
85
|
+
if not patterns:
|
|
86
|
+
return False
|
|
87
|
+
posix = context.path.as_posix()
|
|
88
|
+
return context.is_in_path(*patterns) or any(
|
|
89
|
+
fnmatch(posix, pattern) for pattern in patterns
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _option_error(
|
|
94
|
+
code: str,
|
|
95
|
+
option_name: str,
|
|
96
|
+
expected: str,
|
|
97
|
+
raw: object,
|
|
98
|
+
) -> ValueError:
|
|
99
|
+
return ValueError(f"{code} option '{option_name}' must be {expected}, got {raw!r}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_nonneg_int(code: str, option_name: str, raw: object) -> int:
|
|
103
|
+
"""Validate a non-negative integer rule option."""
|
|
104
|
+
if isinstance(raw, bool) or not isinstance(raw, int) or raw < 0:
|
|
105
|
+
raise _option_error(code, option_name, "a non-negative integer", raw)
|
|
106
|
+
return raw
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_bool_option(code: str, option_name: str, raw: object) -> bool:
|
|
110
|
+
"""Validate a boolean rule option."""
|
|
111
|
+
if not isinstance(raw, bool):
|
|
112
|
+
raise _option_error(code, option_name, "a boolean", raw)
|
|
113
|
+
return raw
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_str_list_option(
|
|
117
|
+
code: str,
|
|
118
|
+
option_name: str,
|
|
119
|
+
raw: object,
|
|
120
|
+
) -> tuple[str, ...]:
|
|
121
|
+
"""Validate a list/tuple-of-strings rule option and freeze it into a tuple."""
|
|
122
|
+
if not isinstance(raw, (list, tuple)) or any(
|
|
123
|
+
not isinstance(item, str) for item in raw
|
|
124
|
+
):
|
|
125
|
+
raise _option_error(code, option_name, "a list of strings", raw)
|
|
126
|
+
return tuple(raw)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def iter_own_scope(root: Node) -> Iterator[Node]:
|
|
130
|
+
"""Yield own-scope descendants without entering nested functions, classes, or lambdas."""
|
|
131
|
+
stack = list(reversed(root.named_children))
|
|
132
|
+
while stack:
|
|
133
|
+
current = stack.pop()
|
|
134
|
+
if current.type in SCOPE_BOUNDARY_TYPES:
|
|
135
|
+
continue
|
|
136
|
+
yield current
|
|
137
|
+
stack.extend(reversed(current.named_children))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def max_own_scope_depth(root: Node, control_types: frozenset[str]) -> int:
|
|
141
|
+
"""Return maximum nesting depth of *control_types* in *root*'s own scope."""
|
|
142
|
+
max_depth = 0
|
|
143
|
+
stack: list[tuple[Node, int]] = [
|
|
144
|
+
(child, 0) for child in reversed(root.named_children)
|
|
145
|
+
]
|
|
146
|
+
while stack:
|
|
147
|
+
current, depth = stack.pop()
|
|
148
|
+
if current.type in SCOPE_BOUNDARY_TYPES:
|
|
149
|
+
continue
|
|
150
|
+
next_depth = depth + 1 if current.type in control_types else depth
|
|
151
|
+
max_depth = max(max_depth, next_depth)
|
|
152
|
+
stack.extend((child, next_depth) for child in reversed(current.named_children))
|
|
153
|
+
return max_depth
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ThresholdRule(Rule):
|
|
157
|
+
"""Base class for deterministic rules governed by one non-negative integer limit."""
|
|
158
|
+
|
|
159
|
+
severity: ClassVar[Severity | None] = Severity.WARNING
|
|
160
|
+
option_name: ClassVar[str]
|
|
161
|
+
default_limit: ClassVar[int]
|
|
162
|
+
|
|
163
|
+
def __init__(self) -> None:
|
|
164
|
+
setattr(self, self.option_name, self.default_limit)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def limit(self) -> int:
|
|
168
|
+
"""Configured upper bound (inclusive) for this rule."""
|
|
169
|
+
return int(getattr(self, self.option_name))
|
|
170
|
+
|
|
171
|
+
def configure(self, options: dict[str, Any]) -> None:
|
|
172
|
+
"""Validate and apply the configured threshold from ``[tool.rude.rules.<CODE>]``."""
|
|
173
|
+
if self.option_name not in options:
|
|
174
|
+
return
|
|
175
|
+
value = parse_nonneg_int(self.code, self.option_name, options[self.option_name])
|
|
176
|
+
setattr(self, self.option_name, value)
|
|
177
|
+
|
|
178
|
+
def emit_if_exceeds(
|
|
179
|
+
self,
|
|
180
|
+
node: Node,
|
|
181
|
+
measured: int,
|
|
182
|
+
message: str,
|
|
183
|
+
) -> Iterator[Diagnostic]:
|
|
184
|
+
"""Yield a diagnostic on *node* when *measured* exceeds the configured limit."""
|
|
185
|
+
if measured > self.limit:
|
|
186
|
+
yield self.diagnostic(node, message)
|
|
@@ -0,0 +1,30 @@
|
|
|
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.dry import DRY_RULES
|
|
10
|
+
from rude_clean_code.rules.errors import ERROR_RULES
|
|
11
|
+
from rude_clean_code.rules.functions import FUNCTION_RULES
|
|
12
|
+
from rude_clean_code.rules.heuristics import HEURISTIC_RULES
|
|
13
|
+
from rude_clean_code.rules.modules import MODULE_RULES
|
|
14
|
+
from rude_clean_code.rules.naming import NAMING_RULES
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from rude.core.rule import Rule
|
|
18
|
+
|
|
19
|
+
RULES: list[type[Rule]] = [
|
|
20
|
+
*MODULE_RULES,
|
|
21
|
+
*FUNCTION_RULES,
|
|
22
|
+
*CLASS_RULES,
|
|
23
|
+
*COMPLEXITY_RULES,
|
|
24
|
+
*ERROR_RULES,
|
|
25
|
+
*NAMING_RULES,
|
|
26
|
+
*DRY_RULES,
|
|
27
|
+
*HEURISTIC_RULES,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__all__ = ["RULES"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Rule implementations grouped by Clean Code family."""
|