ballpython 2.0.0__py3-none-any.whl → 2.0.2__py3-none-any.whl

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.
@@ -0,0 +1,323 @@
1
+ """
2
+ pycleaner.explanations
3
+ ======================
4
+
5
+ Comprehensive diagnostic explanations, remediation guides, and architectural
6
+ rationale for PyCleaner rules, security findings, and verification tiers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RuleExplanation:
16
+ code: str
17
+ title: str
18
+ category: str
19
+ severity: str
20
+ description: str
21
+ vulnerable_example: str
22
+ remediated_example: str
23
+ remediation_details: str
24
+
25
+
26
+ _RULES_REGISTRY: dict[str, RuleExplanation] = {
27
+ "DC001": RuleExplanation(
28
+ code="DC001",
29
+ title="Unused Local Variable",
30
+ category="Dead Code Elimination",
31
+ severity="Low",
32
+ description=(
33
+ "A variable is assigned or bound in a local function scope but is never referenced, "
34
+ "read, or returned anywhere within that scope. This clutters code, consumes memory, "
35
+ "and often signals incomplete logic or leftover debugging artifacts."
36
+ ),
37
+ vulnerable_example=(
38
+ "def calculate_total(items: list[float]) -> float:\n"
39
+ " tax_rate = 0.08 # DC001: assigned but never used\n"
40
+ " return sum(items)\n"
41
+ ),
42
+ remediated_example=(
43
+ "def calculate_total(items: list[float]) -> float:\n return sum(items)\n"
44
+ ),
45
+ remediation_details=(
46
+ "PyCleaner prunes the unused assignment statement if it is side-effect free. "
47
+ "If the expression on the right-hand side has potential side effects (such as a function call), "
48
+ "PyCleaner preserves the call and removes only the unused variable binding."
49
+ ),
50
+ ),
51
+ "DC002": RuleExplanation(
52
+ code="DC002",
53
+ title="Unused Import",
54
+ category="Dead Code Elimination",
55
+ severity="Low",
56
+ description=(
57
+ "A module or symbol is imported at the module level or within a function, but is never "
58
+ "referenced across the AST. Unused imports slow down process startup time, introduce "
59
+ "unnecessary coupling, and complicate dependency auditing."
60
+ ),
61
+ vulnerable_example=(
62
+ "import os\n"
63
+ "import sys # DC002: sys is never referenced\n"
64
+ "from pathlib import Path\n\n"
65
+ "print(os.getcwd())\n"
66
+ ),
67
+ remediated_example=(
68
+ "import os\nfrom pathlib import Path\n\nprint(os.getcwd())\n"
69
+ ),
70
+ remediation_details=(
71
+ "PyCleaner cleanly removes the unused alias or entire import line without altering "
72
+ "unrelated imports or docstrings."
73
+ ),
74
+ ),
75
+ "DC003": RuleExplanation(
76
+ code="DC003",
77
+ title="Unused Private Function or Method",
78
+ category="Dead Code Elimination",
79
+ severity="Medium",
80
+ description=(
81
+ "A private helper function or method (prefixed with an underscore '_') is defined within "
82
+ "a module or class, but has zero internal call references throughout the workspace. "
83
+ "Because private functions cannot be part of the public API contract, unreferenced private "
84
+ "functions represent dead code."
85
+ ),
86
+ vulnerable_example=(
87
+ "def _internal_helper(x: int) -> int: # DC003: never called\n"
88
+ " return x * 2\n\n"
89
+ "def public_api(val: int) -> int:\n"
90
+ " return val + 1\n"
91
+ ),
92
+ remediated_example=("def public_api(val: int) -> int:\n return val + 1\n"),
93
+ remediation_details=(
94
+ "PyCleaner identifies dead private callables across the project graph and prunes them, "
95
+ "while respecting framework entry points (e.g. pytest hooks, FastAPI routes, and dataclass fields)."
96
+ ),
97
+ ),
98
+ "SEC001": RuleExplanation(
99
+ code="SEC001",
100
+ title="SQL Injection Vulnerability",
101
+ category="Security & Taint Analysis",
102
+ severity="Critical",
103
+ description=(
104
+ "Dynamic string interpolation or concatenation is used to construct a SQL query using "
105
+ "untrusted parameters. This allows attackers to manipulate query structure, bypass "
106
+ "authentication, or extract unauthorized database records."
107
+ ),
108
+ vulnerable_example=(
109
+ "def get_user(db_cursor, username: str):\n"
110
+ " query = f\"SELECT * FROM users WHERE username = '{username}'\" # SEC001\n"
111
+ " db_cursor.execute(query)\n"
112
+ ),
113
+ remediated_example=(
114
+ "def get_user(db_cursor, username: str):\n"
115
+ ' query = "SELECT * FROM users WHERE username = %s"\n'
116
+ " db_cursor.execute(query, (username,))\n"
117
+ ),
118
+ remediation_details=(
119
+ "Always use parameterized queries or ORM abstractions (SQLAlchemy, Django ORM). "
120
+ "Never construct SQL statements via f-strings, %, or str.format."
121
+ ),
122
+ ),
123
+ "SEC002": RuleExplanation(
124
+ code="SEC002",
125
+ title="Command Injection via Subprocess",
126
+ category="Security & Taint Analysis",
127
+ severity="Critical",
128
+ description=(
129
+ "Executing shell commands using `shell=True` or string interpolation with `subprocess.Popen`, "
130
+ "`subprocess.run`, or `os.system`. Attackers can append shell metacharacters (;, &, |) to execute "
131
+ "arbitrary OS binaries with the privileges of the running process."
132
+ ),
133
+ vulnerable_example=(
134
+ "import subprocess\n\n"
135
+ "def ping_host(host: str):\n"
136
+ ' subprocess.run(f"ping -c 1 {host}", shell=True) # SEC002\n'
137
+ ),
138
+ remediated_example=(
139
+ "import subprocess\n\n"
140
+ "def ping_host(host: str):\n"
141
+ ' subprocess.run(["ping", "-c", "1", host], shell=False, check=True)\n'
142
+ ),
143
+ remediation_details=(
144
+ "Pass command arguments strictly as a list of strings and set `shell=False`. "
145
+ "This bypasses the shell interpreter entirely, immunizing against metacharacter injection."
146
+ ),
147
+ ),
148
+ "SEC003": RuleExplanation(
149
+ code="SEC003",
150
+ title="Path Traversal Vulnerability",
151
+ category="Security & Taint Analysis",
152
+ severity="High",
153
+ description=(
154
+ "Filesystem paths constructed using unsanitized user inputs allow directory traversal sequences "
155
+ "('../') to read, overwrite, or delete arbitrary files outside the intended root folder."
156
+ ),
157
+ vulnerable_example=(
158
+ "from pathlib import Path\n\n"
159
+ "def read_file(root_dir: Path, user_filename: str) -> str:\n"
160
+ " target = root_dir / user_filename # SEC003: e.g. '../../etc/passwd'\n"
161
+ " return target.read_text()\n"
162
+ ),
163
+ remediated_example=(
164
+ "from pathlib import Path\n\n"
165
+ "def read_file(root_dir: Path, user_filename: str) -> str:\n"
166
+ " target = (root_dir / user_filename).resolve()\n"
167
+ " if not target.is_relative_to(root_dir.resolve()):\n"
168
+ " raise PermissionError('Path traversal attempt detected')\n"
169
+ " return target.read_text()\n"
170
+ ),
171
+ remediation_details=(
172
+ "Canonicalize paths with `.resolve()` and verify that the target path is strictly relative "
173
+ "to the root boundary using `path.is_relative_to(base_dir)`."
174
+ ),
175
+ ),
176
+ "CMP001": RuleExplanation(
177
+ code="CMP001",
178
+ title="Excessive Cyclomatic Complexity",
179
+ category="Cognitive & Complexity Architecture",
180
+ severity="Medium",
181
+ description=(
182
+ "The cyclomatic complexity of a function exceeds the configured threshold (default: 10). "
183
+ "Functions with too many independent linear execution paths are difficult to reason about, "
184
+ "prone to regression bugs, and hard to test exhaustively."
185
+ ),
186
+ vulnerable_example=(
187
+ "def dispatch_event(event_type: str, data: dict):\n"
188
+ " if event_type == 'A':\n"
189
+ " ...\n"
190
+ " elif event_type == 'B':\n"
191
+ " ...\n"
192
+ " # 15 additional elif branches...\n"
193
+ ),
194
+ remediated_example=(
195
+ "HANDLERS = {'A': handle_a, 'B': handle_b}\n\n"
196
+ "def dispatch_event(event_type: str, data: dict):\n"
197
+ " handler = HANDLERS.get(event_type, default_handler)\n"
198
+ " return handler(data)\n"
199
+ ),
200
+ remediation_details=(
201
+ "Refactor complex nested branching using lookup tables, dictionary dispatch, polymorphism, "
202
+ "or by decomposing into smaller cohesive helper functions."
203
+ ),
204
+ ),
205
+ "SYN001": RuleExplanation(
206
+ code="SYN001",
207
+ title="Unclosed Delimiter / Syntax Error Healing",
208
+ category="Syntax Healing",
209
+ severity="High",
210
+ description=(
211
+ "The source file contains an unclosed bracket, brace, parenthesis, or incomplete block "
212
+ "that prevents Python from compiling the file (`SyntaxError`)."
213
+ ),
214
+ vulnerable_example=(
215
+ "items = [\n {'id': 1, 'name': 'test'\nprint('hello')\n"
216
+ ),
217
+ remediated_example=(
218
+ "items = [\n {'id': 1, 'name': 'test'}\n]\nprint('hello')\n"
219
+ ),
220
+ remediation_details=(
221
+ "PyCleaner's Syntax Healer uses token-stream stack analysis to detect unclosed delimiters "
222
+ "and automatically synthesizes minimal, correct closure tokens at the appropriate line boundaries."
223
+ ),
224
+ ),
225
+ "TYPE001": RuleExplanation(
226
+ code="TYPE001",
227
+ title="Incompatible Type Assignment",
228
+ category="Static Type Checking",
229
+ severity="High",
230
+ description=(
231
+ "An expression of one type is assigned to a variable annotated with an incompatible type "
232
+ "(e.g. assigning a `str` to an `int`), violating static type contracts."
233
+ ),
234
+ vulnerable_example=(
235
+ "count: int = 0\ncount = 'none' # TYPE001: incompatible assignment\n"
236
+ ),
237
+ remediated_example=("count: int | None = 0\ncount = None\n"),
238
+ remediation_details=(
239
+ "Update variable type annotations to represent the full union of allowable states, or "
240
+ "convert the assigned value to the expected type."
241
+ ),
242
+ ),
243
+ "PROVE001": RuleExplanation(
244
+ code="PROVE001",
245
+ title="Verification Tier A: Proven Invariant-Preserving",
246
+ category="Differential Equivalence Proof",
247
+ severity="Info",
248
+ description=(
249
+ "The transformation has been verified against a suite of deterministically synthesized input "
250
+ "permutations in an isolated process sandbox. The original and transformed callables produced "
251
+ "100% identical outputs and exception behaviors across all fuzz iterations."
252
+ ),
253
+ vulnerable_example="# Baseline transformation before proof",
254
+ remediated_example="# Verified transformation guaranteed safe for production application",
255
+ remediation_details=(
256
+ "Transformations achieving Tier A are marked safe for automatic disk application (`--apply`). "
257
+ "A deterministic proof receipt is recorded with seed, execution duration, and iterations."
258
+ ),
259
+ ),
260
+ "PROVE002": RuleExplanation(
261
+ code="PROVE002",
262
+ title="Verification Tier B: Suggested Transformation",
263
+ category="Differential Equivalence Proof",
264
+ severity="Info",
265
+ description=(
266
+ "The transformation passed static syntax and lint validation, but cannot be dynamically fuzzed "
267
+ "in an isolated sandbox (e.g. the callable depends on database connections, hardware sockets, "
268
+ "external modules, or cannot be dynamically isolated)."
269
+ ),
270
+ vulnerable_example="# File with module-level side effects or unresolvable imports",
271
+ remediated_example="# Suggested change for manual or test-suite verification",
272
+ remediation_details=(
273
+ "Tier B changes are suggested to the user. They can be applied with `pycleaner fix` or reviewed "
274
+ "via `pycleaner prove --diff`."
275
+ ),
276
+ ),
277
+ "PROVE003": RuleExplanation(
278
+ code="PROVE003",
279
+ title="Verification Tier C: Refused & Rolled Back",
280
+ category="Differential Equivalence Proof",
281
+ severity="Critical",
282
+ description=(
283
+ "A candidate transformation was falsified by the differential execution fuzzer. On at least one "
284
+ "synthesized input vector, the transformed function produced a different return value or error "
285
+ "than the original function."
286
+ ),
287
+ vulnerable_example=(
288
+ "# Original:\n"
289
+ "def is_positive(x: int) -> bool:\n"
290
+ " if x >= 0:\n"
291
+ " return True\n"
292
+ " return False\n\n"
293
+ "# Flawed candidate:\n"
294
+ "def is_positive(x: int) -> bool:\n"
295
+ " if x > 0:\n"
296
+ " return True\n"
297
+ " return False\n"
298
+ ),
299
+ remediated_example=(
300
+ "# PyCleaner automatically rejects the candidate and rolls back the file:\n"
301
+ "def is_positive(x: int) -> bool:\n"
302
+ " if x >= 0:\n"
303
+ " return True\n"
304
+ " return False\n"
305
+ ),
306
+ remediation_details=(
307
+ "Tier C guarantees zero silent regressions. The file is immediately restored to its exact original "
308
+ "state, and the falsifying counterexample (input arguments, original output, transformed output, "
309
+ "and seed) is logged to stdout and `.pycleaner/verification-report.json`."
310
+ ),
311
+ ),
312
+ }
313
+
314
+
315
+ def get_explanation(code_or_topic: str) -> RuleExplanation | None:
316
+ """Retrieves the explanation for a given rule code or alias."""
317
+ key = code_or_topic.strip().upper()
318
+ return _RULES_REGISTRY.get(key)
319
+
320
+
321
+ def list_rules() -> list[RuleExplanation]:
322
+ """Returns all available rule explanations sorted by code."""
323
+ return sorted(_RULES_REGISTRY.values(), key=lambda r: r.code)
@@ -0,0 +1,115 @@
1
+ """
2
+ pycleaner.frameworks
3
+ ====================
4
+
5
+ Framework Semantic Index and Extensible Plugin Protocol.
6
+
7
+ Provides AST-level awareness of framework contracts (Pydantic, Pytest, FastAPI,
8
+ SQLAlchemy, Dataclasses) to eliminate false positives in dead-code detection,
9
+ type inference, and automated transformations.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ from pathlib import Path
16
+ from typing import ClassVar, Protocol, runtime_checkable
17
+
18
+
19
+ @runtime_checkable
20
+ class FrameworkPlugin(Protocol):
21
+ """Protocol that every framework awareness plugin must implement."""
22
+
23
+ name: str
24
+
25
+ def is_applicable(self, tree: ast.AST, filepath: Path | str) -> bool:
26
+ """Return True if this framework is utilized within the AST or file."""
27
+ ...
28
+
29
+ def get_protected_names(self, tree: ast.AST) -> set[str]:
30
+ """Return special method or attribute names dynamically called by the framework."""
31
+ ...
32
+
33
+ def get_protected_decorators(self) -> set[str]:
34
+ """Return decorator prefixes that register symbols dynamically."""
35
+ ...
36
+
37
+ def is_protected_field(self, node: ast.AST, class_node: ast.ClassDef) -> bool:
38
+ """Return True if node is a framework field declaration that must not be pruned."""
39
+ ...
40
+
41
+ def should_ignore_definition(
42
+ self,
43
+ name: str,
44
+ kind: str,
45
+ node: ast.AST,
46
+ context: str,
47
+ tree: ast.AST,
48
+ ) -> bool:
49
+ """Return True if the symbol definition is implicitly loaded by the framework."""
50
+ ...
51
+
52
+
53
+ class FrameworkRegistry:
54
+ """Central registry of framework awareness plugins."""
55
+
56
+ def __init__(self) -> None:
57
+ self._plugins: list[FrameworkPlugin] = []
58
+
59
+ def register(self, plugin: FrameworkPlugin) -> None:
60
+ self._plugins.append(plugin)
61
+
62
+ @property
63
+ def plugins(self) -> list[FrameworkPlugin]:
64
+ return list(self._plugins)
65
+
66
+ def get_applicable_plugins(
67
+ self, tree: ast.AST, filepath: Path | str
68
+ ) -> list[FrameworkPlugin]:
69
+ return [p for p in self._plugins if p.is_applicable(tree, filepath)]
70
+
71
+ def is_protected(
72
+ self,
73
+ name: str,
74
+ kind: str,
75
+ node: ast.AST,
76
+ context: str,
77
+ tree: ast.AST,
78
+ filepath: Path | str,
79
+ ) -> bool:
80
+ """Check if any applicable plugin protects this symbol from being marked dead."""
81
+ for plugin in self.get_applicable_plugins(tree, filepath):
82
+ if plugin.should_ignore_definition(name, kind, node, context, tree):
83
+ return True
84
+ return False
85
+
86
+ def is_field_protected(
87
+ self,
88
+ node: ast.AST,
89
+ class_node: ast.ClassDef,
90
+ tree: ast.AST,
91
+ filepath: Path | str,
92
+ ) -> bool:
93
+ for plugin in self.get_applicable_plugins(tree, filepath):
94
+ if plugin.is_protected_field(node, class_node):
95
+ return True
96
+ return False
97
+
98
+
99
+ def get_default_registry() -> FrameworkRegistry:
100
+ """Construct and populate the default registry with built-in plugins."""
101
+ from pycleaner.frameworks.plugins import (
102
+ DataclassPlugin,
103
+ FastAPIPlugin,
104
+ PydanticPlugin,
105
+ PytestPlugin,
106
+ SQLAlchemyPlugin,
107
+ )
108
+
109
+ reg = FrameworkRegistry()
110
+ reg.register(PydanticPlugin())
111
+ reg.register(PytestPlugin())
112
+ reg.register(FastAPIPlugin())
113
+ reg.register(SQLAlchemyPlugin())
114
+ reg.register(DataclassPlugin())
115
+ return reg