rude-clean-code 0.1.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.
Files changed (35) hide show
  1. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/PKG-INFO +30 -14
  2. rude_clean_code-0.2.0/README.md +69 -0
  3. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/features/deterministic_rules.feature +11 -4
  4. rude_clean_code-0.2.0/features/module_rules.feature +24 -0
  5. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/pyproject.toml +5 -2
  6. rude_clean_code-0.2.0/src/rude_clean_code/_scope.py +186 -0
  7. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/plugin.py +10 -0
  8. rude_clean_code-0.2.0/src/rude_clean_code/rules/_module_analysis.py +293 -0
  9. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/rules/classes.py +77 -18
  10. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/rules/complexity.py +96 -4
  11. rude_clean_code-0.2.0/src/rude_clean_code/rules/dry.py +110 -0
  12. rude_clean_code-0.2.0/src/rude_clean_code/rules/errors.py +233 -0
  13. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/rules/functions.py +117 -5
  14. rude_clean_code-0.2.0/src/rude_clean_code/rules/heuristics.py +115 -0
  15. rude_clean_code-0.2.0/src/rude_clean_code/rules/modules.py +364 -0
  16. rude_clean_code-0.2.0/src/rude_clean_code/rules/naming.py +350 -0
  17. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_deterministic_rules_bdd.py +119 -31
  18. rude_clean_code-0.2.0/tests/test_heuristic_rules.py +231 -0
  19. rude_clean_code-0.2.0/tests/test_module_rules.py +402 -0
  20. rude_clean_code-0.2.0/tests/test_module_rules_bdd.py +218 -0
  21. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_plugin_discovery.py +51 -16
  22. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_scope_traversal.py +6 -1
  23. rude_clean_code-0.2.0/tests/test_structural_and_error_rules.py +281 -0
  24. rude_clean_code-0.1.0/README.md +0 -53
  25. rude_clean_code-0.1.0/src/rude_clean_code/_scope.py +0 -91
  26. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/.gitignore +0 -0
  27. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/LICENSE +0 -0
  28. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/features/package_version.feature +0 -0
  29. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/__init__.py +0 -0
  30. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/py.typed +0 -0
  31. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/src/rude_clean_code/rules/__init__.py +0 -0
  32. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_class_rules.py +0 -0
  33. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_complexity_rules.py +0 -0
  34. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_function_rules.py +0 -0
  35. {rude_clean_code-0.1.0 → rude_clean_code-0.2.0}/tests/test_package_version_bdd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rude-clean-code
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Clean Code plugin for the Rude Python linter.
5
5
  Project-URL: Homepage, https://github.com/Archont561/rude-clean-code#readme
6
6
  Project-URL: Documentation, https://github.com/Archont561/rude-clean-code/blob/main/docs/README.md
@@ -28,9 +28,10 @@ Description-Content-Type: text/markdown
28
28
  # Rude Clean Code
29
29
 
30
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.
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.
34
35
 
35
36
  ## Installation
36
37
 
@@ -40,13 +41,15 @@ pip install rude-clean-code
40
41
 
41
42
  Rude discovers the plugin automatically via `rude.plugins` once installed.
42
43
 
43
- ## Basic profile
44
+ ## Configuration profiles
44
45
 
45
- Add the **Basic** profile snippet to your `pyproject.toml`:
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:
46
49
 
47
50
  ```toml
48
51
  [tool.rude]
49
- select = ["CC1", "CC2", "CC3"]
52
+ select = ["CC0", "CC1", "CC2", "CC3"]
50
53
 
51
54
  [tool.rude.rules.CC101]
52
55
  max_lines = 30
@@ -55,21 +58,34 @@ max_lines = 30
55
58
  Run Rude from the command line:
56
59
 
57
60
  ```sh
58
- rude check --select CC src/
61
+ rude check --select CC0,CC1,CC2,CC3 src/
59
62
  ```
60
63
 
61
- All deterministic core rules report `WARNING` severity with measured value,
62
- configured limit, and a suggested refactoring action.
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`.
63
67
 
64
68
  ## Implemented rules
65
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__`)
66
74
  - **Functions (`CC1xx`)**: `CC101` (`max_lines = 30`), `CC102`
67
75
  (`max_parameters = 5`), `CC103` (`max_returns = 5`), `CC104`
68
- (`max_locals = 10`)
76
+ (`max_locals = 10`), `CC107` (unused private helper)
69
77
  - **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`)
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`)
73
89
 
74
90
  The package initializer `rude_clean_code` exposes only `VERSION` (read from
75
91
  installed distribution metadata); plugin rules live in `rude_clean_code.plugin`.
@@ -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.
@@ -1,8 +1,8 @@
1
1
  Feature: Deterministic Clean Code core rules
2
- Scenario: Rude discovers deterministic Clean Code rules via entry point
2
+ Scenario: Rude discovers Clean Code rules via entry point
3
3
  Given the rude-clean-code plugin is installed
4
4
  When Rude discovers plugin rules
5
- Then all nine deterministic CC rules are available with WARNING severity
5
+ Then all registered CC rules are available with their documented severities
6
6
  And selecting by CC, family prefix, or exact code filters the matching rules
7
7
 
8
8
  Scenario: Consumer runs Basic profile and configured thresholds on Python code
@@ -18,7 +18,14 @@ Feature: Deterministic Clean Code core rules
18
18
  When Rude checks the temporary project
19
19
  Then Rude reports CC101 with measured 5 and limit 4
20
20
 
21
- Scenario: Dogfooding deterministic CC rules on the package source
21
+ Scenario: Dogfooding Clean Code rules on the package source
22
22
  Given the rude-clean-code plugin is installed
23
- When Rude checks the package source directory with CC selected
23
+ When Rude checks the package source directory with the project Basic profile
24
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rude-clean-code"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Clean Code plugin for the Rude Python linter."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -51,4 +51,7 @@ include = [
51
51
  ]
52
52
 
53
53
  [tool.rude]
54
- select = ["CC1", "CC2", "CC3"]
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)
@@ -6,15 +6,25 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from rude_clean_code.rules.classes import CLASS_RULES
8
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
9
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
10
15
 
11
16
  if TYPE_CHECKING:
12
17
  from rude.core.rule import Rule
13
18
 
14
19
  RULES: list[type[Rule]] = [
20
+ *MODULE_RULES,
15
21
  *FUNCTION_RULES,
16
22
  *CLASS_RULES,
17
23
  *COMPLEXITY_RULES,
24
+ *ERROR_RULES,
25
+ *NAMING_RULES,
26
+ *DRY_RULES,
27
+ *HEURISTIC_RULES,
18
28
  ]
19
29
 
20
30
  __all__ = ["RULES"]