crackerjack 0.18.2__py3-none-any.whl → 0.45.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.
Files changed (533) hide show
  1. crackerjack/README.md +19 -0
  2. crackerjack/__init__.py +96 -2
  3. crackerjack/__main__.py +637 -138
  4. crackerjack/adapters/README.md +18 -0
  5. crackerjack/adapters/__init__.py +39 -0
  6. crackerjack/adapters/_output_paths.py +167 -0
  7. crackerjack/adapters/_qa_adapter_base.py +309 -0
  8. crackerjack/adapters/_tool_adapter_base.py +706 -0
  9. crackerjack/adapters/ai/README.md +65 -0
  10. crackerjack/adapters/ai/__init__.py +5 -0
  11. crackerjack/adapters/ai/claude.py +853 -0
  12. crackerjack/adapters/complexity/README.md +53 -0
  13. crackerjack/adapters/complexity/__init__.py +10 -0
  14. crackerjack/adapters/complexity/complexipy.py +641 -0
  15. crackerjack/adapters/dependency/__init__.py +22 -0
  16. crackerjack/adapters/dependency/pip_audit.py +418 -0
  17. crackerjack/adapters/format/README.md +72 -0
  18. crackerjack/adapters/format/__init__.py +11 -0
  19. crackerjack/adapters/format/mdformat.py +313 -0
  20. crackerjack/adapters/format/ruff.py +516 -0
  21. crackerjack/adapters/lint/README.md +47 -0
  22. crackerjack/adapters/lint/__init__.py +11 -0
  23. crackerjack/adapters/lint/codespell.py +273 -0
  24. crackerjack/adapters/lsp/README.md +49 -0
  25. crackerjack/adapters/lsp/__init__.py +27 -0
  26. crackerjack/adapters/lsp/_base.py +194 -0
  27. crackerjack/adapters/lsp/_client.py +358 -0
  28. crackerjack/adapters/lsp/_manager.py +193 -0
  29. crackerjack/adapters/lsp/skylos.py +283 -0
  30. crackerjack/adapters/lsp/zuban.py +557 -0
  31. crackerjack/adapters/refactor/README.md +59 -0
  32. crackerjack/adapters/refactor/__init__.py +12 -0
  33. crackerjack/adapters/refactor/creosote.py +318 -0
  34. crackerjack/adapters/refactor/refurb.py +406 -0
  35. crackerjack/adapters/refactor/skylos.py +494 -0
  36. crackerjack/adapters/sast/README.md +132 -0
  37. crackerjack/adapters/sast/__init__.py +32 -0
  38. crackerjack/adapters/sast/_base.py +201 -0
  39. crackerjack/adapters/sast/bandit.py +423 -0
  40. crackerjack/adapters/sast/pyscn.py +405 -0
  41. crackerjack/adapters/sast/semgrep.py +241 -0
  42. crackerjack/adapters/security/README.md +111 -0
  43. crackerjack/adapters/security/__init__.py +17 -0
  44. crackerjack/adapters/security/gitleaks.py +339 -0
  45. crackerjack/adapters/type/README.md +52 -0
  46. crackerjack/adapters/type/__init__.py +12 -0
  47. crackerjack/adapters/type/pyrefly.py +402 -0
  48. crackerjack/adapters/type/ty.py +402 -0
  49. crackerjack/adapters/type/zuban.py +522 -0
  50. crackerjack/adapters/utility/README.md +51 -0
  51. crackerjack/adapters/utility/__init__.py +10 -0
  52. crackerjack/adapters/utility/checks.py +884 -0
  53. crackerjack/agents/README.md +264 -0
  54. crackerjack/agents/__init__.py +66 -0
  55. crackerjack/agents/architect_agent.py +238 -0
  56. crackerjack/agents/base.py +167 -0
  57. crackerjack/agents/claude_code_bridge.py +641 -0
  58. crackerjack/agents/coordinator.py +600 -0
  59. crackerjack/agents/documentation_agent.py +520 -0
  60. crackerjack/agents/dry_agent.py +585 -0
  61. crackerjack/agents/enhanced_coordinator.py +279 -0
  62. crackerjack/agents/enhanced_proactive_agent.py +185 -0
  63. crackerjack/agents/error_middleware.py +53 -0
  64. crackerjack/agents/formatting_agent.py +230 -0
  65. crackerjack/agents/helpers/__init__.py +9 -0
  66. crackerjack/agents/helpers/performance/__init__.py +22 -0
  67. crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
  68. crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
  69. crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
  70. crackerjack/agents/helpers/refactoring/__init__.py +22 -0
  71. crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
  72. crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
  73. crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
  74. crackerjack/agents/helpers/test_creation/__init__.py +19 -0
  75. crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
  76. crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
  77. crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
  78. crackerjack/agents/import_optimization_agent.py +1181 -0
  79. crackerjack/agents/performance_agent.py +325 -0
  80. crackerjack/agents/performance_helpers.py +205 -0
  81. crackerjack/agents/proactive_agent.py +55 -0
  82. crackerjack/agents/refactoring_agent.py +511 -0
  83. crackerjack/agents/refactoring_helpers.py +247 -0
  84. crackerjack/agents/security_agent.py +793 -0
  85. crackerjack/agents/semantic_agent.py +479 -0
  86. crackerjack/agents/semantic_helpers.py +356 -0
  87. crackerjack/agents/test_creation_agent.py +570 -0
  88. crackerjack/agents/test_specialist_agent.py +526 -0
  89. crackerjack/agents/tracker.py +110 -0
  90. crackerjack/api.py +647 -0
  91. crackerjack/cli/README.md +394 -0
  92. crackerjack/cli/__init__.py +24 -0
  93. crackerjack/cli/cache_handlers.py +209 -0
  94. crackerjack/cli/cache_handlers_enhanced.py +680 -0
  95. crackerjack/cli/facade.py +162 -0
  96. crackerjack/cli/formatting.py +13 -0
  97. crackerjack/cli/handlers/__init__.py +85 -0
  98. crackerjack/cli/handlers/advanced.py +103 -0
  99. crackerjack/cli/handlers/ai_features.py +62 -0
  100. crackerjack/cli/handlers/analytics.py +479 -0
  101. crackerjack/cli/handlers/changelog.py +271 -0
  102. crackerjack/cli/handlers/config_handlers.py +16 -0
  103. crackerjack/cli/handlers/coverage.py +84 -0
  104. crackerjack/cli/handlers/documentation.py +280 -0
  105. crackerjack/cli/handlers/main_handlers.py +497 -0
  106. crackerjack/cli/handlers/monitoring.py +371 -0
  107. crackerjack/cli/handlers.py +700 -0
  108. crackerjack/cli/interactive.py +488 -0
  109. crackerjack/cli/options.py +1216 -0
  110. crackerjack/cli/semantic_handlers.py +292 -0
  111. crackerjack/cli/utils.py +19 -0
  112. crackerjack/cli/version.py +19 -0
  113. crackerjack/code_cleaner.py +1307 -0
  114. crackerjack/config/README.md +472 -0
  115. crackerjack/config/__init__.py +275 -0
  116. crackerjack/config/global_lock_config.py +207 -0
  117. crackerjack/config/hooks.py +390 -0
  118. crackerjack/config/loader.py +239 -0
  119. crackerjack/config/settings.py +141 -0
  120. crackerjack/config/tool_commands.py +331 -0
  121. crackerjack/core/README.md +393 -0
  122. crackerjack/core/__init__.py +0 -0
  123. crackerjack/core/async_workflow_orchestrator.py +738 -0
  124. crackerjack/core/autofix_coordinator.py +282 -0
  125. crackerjack/core/container.py +105 -0
  126. crackerjack/core/enhanced_container.py +583 -0
  127. crackerjack/core/file_lifecycle.py +472 -0
  128. crackerjack/core/performance.py +244 -0
  129. crackerjack/core/performance_monitor.py +357 -0
  130. crackerjack/core/phase_coordinator.py +1227 -0
  131. crackerjack/core/proactive_workflow.py +267 -0
  132. crackerjack/core/resource_manager.py +425 -0
  133. crackerjack/core/retry.py +275 -0
  134. crackerjack/core/service_watchdog.py +601 -0
  135. crackerjack/core/session_coordinator.py +239 -0
  136. crackerjack/core/timeout_manager.py +563 -0
  137. crackerjack/core/websocket_lifecycle.py +410 -0
  138. crackerjack/core/workflow/__init__.py +21 -0
  139. crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
  140. crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
  141. crackerjack/core/workflow/workflow_issue_parser.py +714 -0
  142. crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
  143. crackerjack/core/workflow/workflow_security_gates.py +400 -0
  144. crackerjack/core/workflow_orchestrator.py +2243 -0
  145. crackerjack/data/README.md +11 -0
  146. crackerjack/data/__init__.py +8 -0
  147. crackerjack/data/models.py +79 -0
  148. crackerjack/data/repository.py +210 -0
  149. crackerjack/decorators/README.md +180 -0
  150. crackerjack/decorators/__init__.py +35 -0
  151. crackerjack/decorators/error_handling.py +649 -0
  152. crackerjack/decorators/error_handling_decorators.py +334 -0
  153. crackerjack/decorators/helpers.py +58 -0
  154. crackerjack/decorators/patterns.py +281 -0
  155. crackerjack/decorators/utils.py +58 -0
  156. crackerjack/docs/INDEX.md +11 -0
  157. crackerjack/docs/README.md +11 -0
  158. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  159. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  160. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  161. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  162. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  163. crackerjack/documentation/README.md +11 -0
  164. crackerjack/documentation/__init__.py +31 -0
  165. crackerjack/documentation/ai_templates.py +756 -0
  166. crackerjack/documentation/dual_output_generator.py +767 -0
  167. crackerjack/documentation/mkdocs_integration.py +518 -0
  168. crackerjack/documentation/reference_generator.py +1065 -0
  169. crackerjack/dynamic_config.py +678 -0
  170. crackerjack/errors.py +378 -0
  171. crackerjack/events/README.md +11 -0
  172. crackerjack/events/__init__.py +16 -0
  173. crackerjack/events/telemetry.py +175 -0
  174. crackerjack/events/workflow_bus.py +346 -0
  175. crackerjack/exceptions/README.md +301 -0
  176. crackerjack/exceptions/__init__.py +5 -0
  177. crackerjack/exceptions/config.py +4 -0
  178. crackerjack/exceptions/tool_execution_error.py +245 -0
  179. crackerjack/executors/README.md +591 -0
  180. crackerjack/executors/__init__.py +13 -0
  181. crackerjack/executors/async_hook_executor.py +938 -0
  182. crackerjack/executors/cached_hook_executor.py +316 -0
  183. crackerjack/executors/hook_executor.py +1295 -0
  184. crackerjack/executors/hook_lock_manager.py +708 -0
  185. crackerjack/executors/individual_hook_executor.py +739 -0
  186. crackerjack/executors/lsp_aware_hook_executor.py +349 -0
  187. crackerjack/executors/progress_hook_executor.py +282 -0
  188. crackerjack/executors/tool_proxy.py +433 -0
  189. crackerjack/hooks/README.md +485 -0
  190. crackerjack/hooks/lsp_hook.py +93 -0
  191. crackerjack/intelligence/README.md +557 -0
  192. crackerjack/intelligence/__init__.py +37 -0
  193. crackerjack/intelligence/adaptive_learning.py +693 -0
  194. crackerjack/intelligence/agent_orchestrator.py +485 -0
  195. crackerjack/intelligence/agent_registry.py +377 -0
  196. crackerjack/intelligence/agent_selector.py +439 -0
  197. crackerjack/intelligence/integration.py +250 -0
  198. crackerjack/interactive.py +719 -0
  199. crackerjack/managers/README.md +369 -0
  200. crackerjack/managers/__init__.py +11 -0
  201. crackerjack/managers/async_hook_manager.py +135 -0
  202. crackerjack/managers/hook_manager.py +585 -0
  203. crackerjack/managers/publish_manager.py +631 -0
  204. crackerjack/managers/test_command_builder.py +391 -0
  205. crackerjack/managers/test_executor.py +474 -0
  206. crackerjack/managers/test_manager.py +1357 -0
  207. crackerjack/managers/test_progress.py +187 -0
  208. crackerjack/mcp/README.md +374 -0
  209. crackerjack/mcp/__init__.py +0 -0
  210. crackerjack/mcp/cache.py +352 -0
  211. crackerjack/mcp/client_runner.py +121 -0
  212. crackerjack/mcp/context.py +802 -0
  213. crackerjack/mcp/dashboard.py +657 -0
  214. crackerjack/mcp/enhanced_progress_monitor.py +493 -0
  215. crackerjack/mcp/file_monitor.py +394 -0
  216. crackerjack/mcp/progress_components.py +607 -0
  217. crackerjack/mcp/progress_monitor.py +1016 -0
  218. crackerjack/mcp/rate_limiter.py +336 -0
  219. crackerjack/mcp/server.py +24 -0
  220. crackerjack/mcp/server_core.py +526 -0
  221. crackerjack/mcp/service_watchdog.py +505 -0
  222. crackerjack/mcp/state.py +407 -0
  223. crackerjack/mcp/task_manager.py +259 -0
  224. crackerjack/mcp/tools/README.md +27 -0
  225. crackerjack/mcp/tools/__init__.py +19 -0
  226. crackerjack/mcp/tools/core_tools.py +469 -0
  227. crackerjack/mcp/tools/error_analyzer.py +283 -0
  228. crackerjack/mcp/tools/execution_tools.py +384 -0
  229. crackerjack/mcp/tools/intelligence_tool_registry.py +46 -0
  230. crackerjack/mcp/tools/intelligence_tools.py +264 -0
  231. crackerjack/mcp/tools/monitoring_tools.py +628 -0
  232. crackerjack/mcp/tools/proactive_tools.py +367 -0
  233. crackerjack/mcp/tools/progress_tools.py +222 -0
  234. crackerjack/mcp/tools/semantic_tools.py +584 -0
  235. crackerjack/mcp/tools/utility_tools.py +358 -0
  236. crackerjack/mcp/tools/workflow_executor.py +699 -0
  237. crackerjack/mcp/websocket/README.md +31 -0
  238. crackerjack/mcp/websocket/__init__.py +14 -0
  239. crackerjack/mcp/websocket/app.py +54 -0
  240. crackerjack/mcp/websocket/endpoints.py +492 -0
  241. crackerjack/mcp/websocket/event_bridge.py +188 -0
  242. crackerjack/mcp/websocket/jobs.py +406 -0
  243. crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
  244. crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
  245. crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
  246. crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
  247. crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
  248. crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
  249. crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
  250. crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
  251. crackerjack/mcp/websocket/monitoring/factory.py +109 -0
  252. crackerjack/mcp/websocket/monitoring/filters.py +10 -0
  253. crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
  254. crackerjack/mcp/websocket/monitoring/models.py +90 -0
  255. crackerjack/mcp/websocket/monitoring/utils.py +171 -0
  256. crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
  257. crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
  258. crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
  259. crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
  260. crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
  261. crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
  262. crackerjack/mcp/websocket/monitoring_endpoints.py +21 -0
  263. crackerjack/mcp/websocket/server.py +174 -0
  264. crackerjack/mcp/websocket/websocket_handler.py +276 -0
  265. crackerjack/mcp/websocket_server.py +10 -0
  266. crackerjack/models/README.md +308 -0
  267. crackerjack/models/__init__.py +40 -0
  268. crackerjack/models/config.py +730 -0
  269. crackerjack/models/config_adapter.py +265 -0
  270. crackerjack/models/protocols.py +1535 -0
  271. crackerjack/models/pydantic_models.py +320 -0
  272. crackerjack/models/qa_config.py +145 -0
  273. crackerjack/models/qa_results.py +134 -0
  274. crackerjack/models/resource_protocols.py +299 -0
  275. crackerjack/models/results.py +35 -0
  276. crackerjack/models/semantic_models.py +258 -0
  277. crackerjack/models/task.py +173 -0
  278. crackerjack/models/test_models.py +60 -0
  279. crackerjack/monitoring/README.md +11 -0
  280. crackerjack/monitoring/__init__.py +0 -0
  281. crackerjack/monitoring/ai_agent_watchdog.py +405 -0
  282. crackerjack/monitoring/metrics_collector.py +427 -0
  283. crackerjack/monitoring/regression_prevention.py +580 -0
  284. crackerjack/monitoring/websocket_server.py +406 -0
  285. crackerjack/orchestration/README.md +340 -0
  286. crackerjack/orchestration/__init__.py +43 -0
  287. crackerjack/orchestration/advanced_orchestrator.py +894 -0
  288. crackerjack/orchestration/cache/README.md +312 -0
  289. crackerjack/orchestration/cache/__init__.py +37 -0
  290. crackerjack/orchestration/cache/memory_cache.py +338 -0
  291. crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
  292. crackerjack/orchestration/config.py +297 -0
  293. crackerjack/orchestration/coverage_improvement.py +180 -0
  294. crackerjack/orchestration/execution_strategies.py +361 -0
  295. crackerjack/orchestration/hook_orchestrator.py +1398 -0
  296. crackerjack/orchestration/strategies/README.md +401 -0
  297. crackerjack/orchestration/strategies/__init__.py +39 -0
  298. crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
  299. crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
  300. crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
  301. crackerjack/orchestration/test_progress_streamer.py +647 -0
  302. crackerjack/plugins/README.md +11 -0
  303. crackerjack/plugins/__init__.py +15 -0
  304. crackerjack/plugins/base.py +200 -0
  305. crackerjack/plugins/hooks.py +254 -0
  306. crackerjack/plugins/loader.py +335 -0
  307. crackerjack/plugins/managers.py +264 -0
  308. crackerjack/py313.py +191 -0
  309. crackerjack/security/README.md +11 -0
  310. crackerjack/security/__init__.py +0 -0
  311. crackerjack/security/audit.py +197 -0
  312. crackerjack/services/README.md +374 -0
  313. crackerjack/services/__init__.py +9 -0
  314. crackerjack/services/ai/README.md +295 -0
  315. crackerjack/services/ai/__init__.py +7 -0
  316. crackerjack/services/ai/advanced_optimizer.py +878 -0
  317. crackerjack/services/ai/contextual_ai_assistant.py +542 -0
  318. crackerjack/services/ai/embeddings.py +444 -0
  319. crackerjack/services/ai/intelligent_commit.py +328 -0
  320. crackerjack/services/ai/predictive_analytics.py +510 -0
  321. crackerjack/services/anomaly_detector.py +392 -0
  322. crackerjack/services/api_extractor.py +617 -0
  323. crackerjack/services/backup_service.py +467 -0
  324. crackerjack/services/bounded_status_operations.py +530 -0
  325. crackerjack/services/cache.py +369 -0
  326. crackerjack/services/changelog_automation.py +399 -0
  327. crackerjack/services/command_execution_service.py +305 -0
  328. crackerjack/services/config_integrity.py +132 -0
  329. crackerjack/services/config_merge.py +546 -0
  330. crackerjack/services/config_service.py +198 -0
  331. crackerjack/services/config_template.py +493 -0
  332. crackerjack/services/coverage_badge_service.py +173 -0
  333. crackerjack/services/coverage_ratchet.py +381 -0
  334. crackerjack/services/debug.py +733 -0
  335. crackerjack/services/dependency_analyzer.py +460 -0
  336. crackerjack/services/dependency_monitor.py +622 -0
  337. crackerjack/services/documentation_generator.py +493 -0
  338. crackerjack/services/documentation_service.py +704 -0
  339. crackerjack/services/enhanced_filesystem.py +497 -0
  340. crackerjack/services/enterprise_optimizer.py +865 -0
  341. crackerjack/services/error_pattern_analyzer.py +676 -0
  342. crackerjack/services/file_filter.py +221 -0
  343. crackerjack/services/file_hasher.py +149 -0
  344. crackerjack/services/file_io_service.py +361 -0
  345. crackerjack/services/file_modifier.py +615 -0
  346. crackerjack/services/filesystem.py +381 -0
  347. crackerjack/services/git.py +422 -0
  348. crackerjack/services/health_metrics.py +615 -0
  349. crackerjack/services/heatmap_generator.py +744 -0
  350. crackerjack/services/incremental_executor.py +380 -0
  351. crackerjack/services/initialization.py +823 -0
  352. crackerjack/services/input_validator.py +668 -0
  353. crackerjack/services/intelligent_commit.py +327 -0
  354. crackerjack/services/log_manager.py +289 -0
  355. crackerjack/services/logging.py +228 -0
  356. crackerjack/services/lsp_client.py +628 -0
  357. crackerjack/services/memory_optimizer.py +414 -0
  358. crackerjack/services/metrics.py +587 -0
  359. crackerjack/services/monitoring/README.md +30 -0
  360. crackerjack/services/monitoring/__init__.py +9 -0
  361. crackerjack/services/monitoring/dependency_monitor.py +678 -0
  362. crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
  363. crackerjack/services/monitoring/health_metrics.py +716 -0
  364. crackerjack/services/monitoring/metrics.py +587 -0
  365. crackerjack/services/monitoring/performance_benchmarks.py +410 -0
  366. crackerjack/services/monitoring/performance_cache.py +388 -0
  367. crackerjack/services/monitoring/performance_monitor.py +569 -0
  368. crackerjack/services/parallel_executor.py +527 -0
  369. crackerjack/services/pattern_cache.py +333 -0
  370. crackerjack/services/pattern_detector.py +478 -0
  371. crackerjack/services/patterns/__init__.py +142 -0
  372. crackerjack/services/patterns/agents.py +107 -0
  373. crackerjack/services/patterns/code/__init__.py +15 -0
  374. crackerjack/services/patterns/code/detection.py +118 -0
  375. crackerjack/services/patterns/code/imports.py +107 -0
  376. crackerjack/services/patterns/code/paths.py +159 -0
  377. crackerjack/services/patterns/code/performance.py +119 -0
  378. crackerjack/services/patterns/code/replacement.py +36 -0
  379. crackerjack/services/patterns/core.py +212 -0
  380. crackerjack/services/patterns/documentation/__init__.py +14 -0
  381. crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
  382. crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
  383. crackerjack/services/patterns/documentation/docstrings.py +89 -0
  384. crackerjack/services/patterns/formatting.py +226 -0
  385. crackerjack/services/patterns/operations.py +339 -0
  386. crackerjack/services/patterns/security/__init__.py +23 -0
  387. crackerjack/services/patterns/security/code_injection.py +122 -0
  388. crackerjack/services/patterns/security/credentials.py +190 -0
  389. crackerjack/services/patterns/security/path_traversal.py +221 -0
  390. crackerjack/services/patterns/security/unsafe_operations.py +216 -0
  391. crackerjack/services/patterns/templates.py +62 -0
  392. crackerjack/services/patterns/testing/__init__.py +18 -0
  393. crackerjack/services/patterns/testing/error_patterns.py +107 -0
  394. crackerjack/services/patterns/testing/pytest_output.py +126 -0
  395. crackerjack/services/patterns/tool_output/__init__.py +16 -0
  396. crackerjack/services/patterns/tool_output/bandit.py +72 -0
  397. crackerjack/services/patterns/tool_output/other.py +97 -0
  398. crackerjack/services/patterns/tool_output/pyright.py +67 -0
  399. crackerjack/services/patterns/tool_output/ruff.py +44 -0
  400. crackerjack/services/patterns/url_sanitization.py +114 -0
  401. crackerjack/services/patterns/utilities.py +42 -0
  402. crackerjack/services/patterns/utils.py +339 -0
  403. crackerjack/services/patterns/validation.py +46 -0
  404. crackerjack/services/patterns/versioning.py +62 -0
  405. crackerjack/services/predictive_analytics.py +523 -0
  406. crackerjack/services/profiler.py +280 -0
  407. crackerjack/services/quality/README.md +415 -0
  408. crackerjack/services/quality/__init__.py +11 -0
  409. crackerjack/services/quality/anomaly_detector.py +392 -0
  410. crackerjack/services/quality/pattern_cache.py +333 -0
  411. crackerjack/services/quality/pattern_detector.py +479 -0
  412. crackerjack/services/quality/qa_orchestrator.py +491 -0
  413. crackerjack/services/quality/quality_baseline.py +395 -0
  414. crackerjack/services/quality/quality_baseline_enhanced.py +649 -0
  415. crackerjack/services/quality/quality_intelligence.py +949 -0
  416. crackerjack/services/regex_patterns.py +58 -0
  417. crackerjack/services/regex_utils.py +483 -0
  418. crackerjack/services/secure_path_utils.py +524 -0
  419. crackerjack/services/secure_status_formatter.py +450 -0
  420. crackerjack/services/secure_subprocess.py +635 -0
  421. crackerjack/services/security.py +239 -0
  422. crackerjack/services/security_logger.py +495 -0
  423. crackerjack/services/server_manager.py +411 -0
  424. crackerjack/services/smart_scheduling.py +167 -0
  425. crackerjack/services/status_authentication.py +460 -0
  426. crackerjack/services/status_security_manager.py +315 -0
  427. crackerjack/services/terminal_utils.py +0 -0
  428. crackerjack/services/thread_safe_status_collector.py +441 -0
  429. crackerjack/services/tool_filter.py +368 -0
  430. crackerjack/services/tool_version_service.py +43 -0
  431. crackerjack/services/unified_config.py +115 -0
  432. crackerjack/services/validation_rate_limiter.py +220 -0
  433. crackerjack/services/vector_store.py +689 -0
  434. crackerjack/services/version_analyzer.py +461 -0
  435. crackerjack/services/version_checker.py +223 -0
  436. crackerjack/services/websocket_resource_limiter.py +438 -0
  437. crackerjack/services/zuban_lsp_service.py +391 -0
  438. crackerjack/slash_commands/README.md +11 -0
  439. crackerjack/slash_commands/__init__.py +59 -0
  440. crackerjack/slash_commands/init.md +112 -0
  441. crackerjack/slash_commands/run.md +197 -0
  442. crackerjack/slash_commands/status.md +127 -0
  443. crackerjack/tools/README.md +11 -0
  444. crackerjack/tools/__init__.py +30 -0
  445. crackerjack/tools/_git_utils.py +105 -0
  446. crackerjack/tools/check_added_large_files.py +139 -0
  447. crackerjack/tools/check_ast.py +105 -0
  448. crackerjack/tools/check_json.py +103 -0
  449. crackerjack/tools/check_jsonschema.py +297 -0
  450. crackerjack/tools/check_toml.py +103 -0
  451. crackerjack/tools/check_yaml.py +110 -0
  452. crackerjack/tools/codespell_wrapper.py +72 -0
  453. crackerjack/tools/end_of_file_fixer.py +202 -0
  454. crackerjack/tools/format_json.py +128 -0
  455. crackerjack/tools/mdformat_wrapper.py +114 -0
  456. crackerjack/tools/trailing_whitespace.py +198 -0
  457. crackerjack/tools/validate_input_validator_patterns.py +236 -0
  458. crackerjack/tools/validate_regex_patterns.py +188 -0
  459. crackerjack/ui/README.md +11 -0
  460. crackerjack/ui/__init__.py +1 -0
  461. crackerjack/ui/dashboard_renderer.py +28 -0
  462. crackerjack/ui/templates/README.md +11 -0
  463. crackerjack/utils/console_utils.py +13 -0
  464. crackerjack/utils/dependency_guard.py +230 -0
  465. crackerjack/utils/retry_utils.py +275 -0
  466. crackerjack/workflows/README.md +590 -0
  467. crackerjack/workflows/__init__.py +46 -0
  468. crackerjack/workflows/actions.py +811 -0
  469. crackerjack/workflows/auto_fix.py +444 -0
  470. crackerjack/workflows/container_builder.py +499 -0
  471. crackerjack/workflows/definitions.py +443 -0
  472. crackerjack/workflows/engine.py +177 -0
  473. crackerjack/workflows/event_bridge.py +242 -0
  474. crackerjack-0.45.2.dist-info/METADATA +1678 -0
  475. crackerjack-0.45.2.dist-info/RECORD +478 -0
  476. {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
  477. crackerjack-0.45.2.dist-info/entry_points.txt +2 -0
  478. crackerjack/.gitignore +0 -14
  479. crackerjack/.libcst.codemod.yaml +0 -18
  480. crackerjack/.pdm.toml +0 -1
  481. crackerjack/.pre-commit-config.yaml +0 -91
  482. crackerjack/.pytest_cache/.gitignore +0 -2
  483. crackerjack/.pytest_cache/CACHEDIR.TAG +0 -4
  484. crackerjack/.pytest_cache/README.md +0 -8
  485. crackerjack/.pytest_cache/v/cache/nodeids +0 -1
  486. crackerjack/.pytest_cache/v/cache/stepwise +0 -1
  487. crackerjack/.ruff_cache/.gitignore +0 -1
  488. crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
  489. crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
  490. crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
  491. crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
  492. crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
  493. crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
  494. crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
  495. crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
  496. crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
  497. crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
  498. crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
  499. crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
  500. crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
  501. crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
  502. crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
  503. crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
  504. crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
  505. crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
  506. crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
  507. crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
  508. crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
  509. crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
  510. crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
  511. crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
  512. crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
  513. crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
  514. crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
  515. crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
  516. crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
  517. crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
  518. crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
  519. crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
  520. crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
  521. crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
  522. crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
  523. crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
  524. crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
  525. crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
  526. crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
  527. crackerjack/.ruff_cache/CACHEDIR.TAG +0 -1
  528. crackerjack/crackerjack.py +0 -855
  529. crackerjack/pyproject.toml +0 -214
  530. crackerjack-0.18.2.dist-info/METADATA +0 -420
  531. crackerjack-0.18.2.dist-info/RECORD +0 -59
  532. crackerjack-0.18.2.dist-info/entry_points.txt +0 -4
  533. {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1307 @@
1
+ import ast
2
+ import typing as t
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+ from .errors import ErrorCode, ExecutionError
10
+ from .services.backup_service import BackupMetadata, PackageBackupService
11
+ from .services.regex_patterns import SAFE_PATTERNS
12
+ from .services.secure_path_utils import (
13
+ AtomicFileOperations,
14
+ SecurePathValidator,
15
+ )
16
+ from .services.security_logger import (
17
+ SecurityEventLevel,
18
+ SecurityEventType,
19
+ get_security_logger,
20
+ )
21
+
22
+
23
+ class SafePatternApplicator:
24
+ def apply_docstring_patterns(self, code: str) -> str:
25
+ # Intentionally a no-op for docstrings here. Actual docstring removal is
26
+ # handled by the structured AST cleaning step (_create_docstring_step).
27
+ # This keeps SafePatternApplicator focused on formatting-only changes.
28
+ return code
29
+
30
+ def apply_formatting_patterns(self, content: str) -> str:
31
+ content = SAFE_PATTERNS["spacing_after_comma"].apply(content)
32
+ content = SAFE_PATTERNS["spacing_after_colon"].apply(content)
33
+ content = SAFE_PATTERNS["multiple_spaces"].apply(content)
34
+ return content
35
+
36
+ def has_preserved_comment(self, line: str) -> bool:
37
+ # Preserve shebangs (still useful for executable scripts)
38
+ if line.strip().startswith(("#! /", "#!/")):
39
+ return True
40
+
41
+ line_lower = line.lower()
42
+ preserved_keywords = [
43
+ # Security & linting directives (critical)
44
+ "nosec",
45
+ "noqa",
46
+ "pragma",
47
+ # Type checking directives (critical)
48
+ "type: ",
49
+ # Security markers (custom)
50
+ "regex ok",
51
+ # Task tracking (useful)
52
+ "todo",
53
+ # Note: "coding:" and "encoding:" removed - obsolete since Python 3.0
54
+ ]
55
+ return any(keyword in line_lower for keyword in preserved_keywords)
56
+
57
+
58
+ _safe_applicator = SafePatternApplicator()
59
+
60
+
61
+ @dataclass
62
+ class CleaningResult:
63
+ file_path: Path
64
+ success: bool
65
+ steps_completed: list[str]
66
+ steps_failed: list[str]
67
+ warnings: list[str]
68
+ original_size: int
69
+ cleaned_size: int
70
+ backup_metadata: BackupMetadata | None = None
71
+
72
+
73
+ @dataclass
74
+ class PackageCleaningResult:
75
+ total_files: int
76
+ successful_files: int
77
+ failed_files: int
78
+ file_results: list[CleaningResult]
79
+ backup_metadata: BackupMetadata | None
80
+ backup_restored: bool = False
81
+ overall_success: bool = False
82
+
83
+
84
+ class CleaningStepProtocol(Protocol):
85
+ def __call__(self, code: str, file_path: Path) -> str: ...
86
+
87
+ @property
88
+ def name(self) -> str: ...
89
+
90
+
91
+ class FileProcessor(BaseModel):
92
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
93
+
94
+ console: t.Any
95
+ logger: t.Any = None
96
+ base_directory: Path | None = None
97
+ security_logger: t.Any = None
98
+
99
+ def model_post_init(self, _: t.Any) -> None:
100
+ if self.logger is None:
101
+ import logging
102
+
103
+ self.logger = logging.getLogger("crackerjack.code_cleaner.file_processor")
104
+
105
+ if self.security_logger is None:
106
+ self.security_logger = get_security_logger()
107
+
108
+ def read_file_safely(self, file_path: Path) -> str:
109
+ validated_path = SecurePathValidator.validate_file_path(
110
+ file_path, self.base_directory
111
+ )
112
+ SecurePathValidator.validate_file_size(validated_path)
113
+
114
+ self.security_logger.log_security_event(
115
+ SecurityEventType.FILE_CLEANED,
116
+ SecurityEventLevel.LOW,
117
+ f"Reading file for cleaning: {validated_path}",
118
+ file_path=validated_path,
119
+ )
120
+
121
+ try:
122
+ return validated_path.read_text(encoding="utf-8")
123
+
124
+ except UnicodeDecodeError:
125
+ for encoding in ("latin1", "cp1252"):
126
+ try:
127
+ content = validated_path.read_text(encoding=encoding)
128
+ self.logger.warning(
129
+ f"File {validated_path} read with {encoding} encoding",
130
+ )
131
+ return content
132
+ except UnicodeDecodeError:
133
+ continue
134
+
135
+ self.security_logger.log_validation_failed(
136
+ "encoding",
137
+ file_path,
138
+ "Could not decode file with any supported encoding",
139
+ )
140
+
141
+ raise ExecutionError(
142
+ message=f"Could not decode file {file_path}",
143
+ error_code=ErrorCode.FILE_READ_ERROR,
144
+ )
145
+
146
+ except ExecutionError:
147
+ raise
148
+
149
+ except Exception as e:
150
+ self.security_logger.log_validation_failed(
151
+ "file_read", file_path, f"Unexpected error during file read: {e}"
152
+ )
153
+
154
+ raise ExecutionError(
155
+ message=f"Failed to read file {file_path}: {e}",
156
+ error_code=ErrorCode.FILE_READ_ERROR,
157
+ ) from e
158
+
159
+ def write_file_safely(self, file_path: Path, content: str) -> None:
160
+ try:
161
+ AtomicFileOperations.atomic_write(file_path, content, self.base_directory)
162
+
163
+ self.security_logger.log_atomic_operation("write", file_path, True)
164
+
165
+ except ExecutionError:
166
+ self.security_logger.log_atomic_operation("write", file_path, False)
167
+ raise
168
+
169
+ except Exception as e:
170
+ self.security_logger.log_atomic_operation(
171
+ "write", file_path, False, error=str(e)
172
+ )
173
+
174
+ raise ExecutionError(
175
+ message=f"Failed to write file {file_path}: {e}",
176
+ error_code=ErrorCode.FILE_WRITE_ERROR,
177
+ ) from e
178
+
179
+ def backup_file(self, file_path: Path) -> Path:
180
+ try:
181
+ backup_path = AtomicFileOperations.atomic_backup_and_write(
182
+ file_path, file_path.read_bytes(), self.base_directory
183
+ )
184
+
185
+ self.security_logger.log_backup_created(file_path, backup_path)
186
+
187
+ return backup_path
188
+
189
+ except ExecutionError:
190
+ raise
191
+
192
+ except Exception as e:
193
+ self.security_logger.log_validation_failed(
194
+ "backup_creation", file_path, f"Backup creation failed: {e}"
195
+ )
196
+
197
+ raise ExecutionError(
198
+ message=f"Failed to create backup for {file_path}: {e}",
199
+ error_code=ErrorCode.FILE_WRITE_ERROR,
200
+ ) from e
201
+
202
+
203
+ class CleaningErrorHandler(BaseModel):
204
+ model_config = ConfigDict(arbitrary_types_allowed=True)
205
+
206
+ console: t.Any
207
+ logger: t.Any = None
208
+
209
+ def model_post_init(self, _: t.Any) -> None:
210
+ if self.logger is None:
211
+ import logging
212
+
213
+ self.logger = logging.getLogger("crackerjack.code_cleaner.error_handler")
214
+
215
+ def handle_file_error(self, file_path: Path, error: Exception, step: str) -> None:
216
+ self.console.print(
217
+ f"[bold bright_yellow]⚠️ Warning: {step} failed for {file_path}: {error}[/ bold bright_yellow]",
218
+ )
219
+
220
+ self.logger.warning(
221
+ "Cleaning step failed",
222
+ extra={
223
+ "file_path": str(file_path),
224
+ "step": step,
225
+ "error": str(error),
226
+ "error_type": type(error).__name__,
227
+ },
228
+ )
229
+
230
+ def log_cleaning_result(self, result: CleaningResult) -> None:
231
+ if result.success:
232
+ self.console.print(
233
+ f"[green]✅ Cleaned {result.file_path}[/ green] "
234
+ f"({result.original_size} → {result.cleaned_size} bytes)",
235
+ )
236
+ else:
237
+ self.console.print(
238
+ f"[red]❌ Failed to clean {result.file_path}[/ red] "
239
+ f"({len(result.steps_failed)} steps failed)",
240
+ )
241
+
242
+ if result.warnings:
243
+ for warning in result.warnings:
244
+ self.console.print(f"[yellow]⚠️ {warning}[/ yellow]")
245
+
246
+ self.logger.info(
247
+ "File cleaning completed",
248
+ extra={
249
+ "file_path": str(result.file_path),
250
+ "success": result.success,
251
+ "steps_completed": result.steps_completed,
252
+ "steps_failed": result.steps_failed,
253
+ "original_size": result.original_size,
254
+ "cleaned_size": result.cleaned_size,
255
+ },
256
+ )
257
+
258
+
259
+ class CleaningPipeline(BaseModel):
260
+ model_config = ConfigDict(arbitrary_types_allowed=True)
261
+
262
+ file_processor: t.Any
263
+ error_handler: t.Any
264
+ console: t.Any
265
+ logger: t.Any = None
266
+
267
+ def model_post_init(self, _: t.Any) -> None:
268
+ if self.logger is None:
269
+ import logging
270
+
271
+ self.logger = logging.getLogger("crackerjack.code_cleaner.pipeline")
272
+
273
+ def clean_file(
274
+ self,
275
+ file_path: Path,
276
+ cleaning_steps: list[CleaningStepProtocol],
277
+ ) -> CleaningResult:
278
+ self.logger.info(f"Starting clean_file for {file_path}")
279
+ try:
280
+ original_code = self.file_processor.read_file_safely(file_path)
281
+ original_size = len(original_code.encode("utf-8"))
282
+
283
+ result = self._apply_cleaning_pipeline(
284
+ original_code,
285
+ file_path,
286
+ cleaning_steps,
287
+ )
288
+
289
+ cleaned_size = original_size
290
+ if result.success and result.cleaned_code != original_code:
291
+ self.file_processor.write_file_safely(file_path, result.cleaned_code)
292
+ cleaned_size = len(result.cleaned_code.encode("utf-8"))
293
+
294
+ cleaning_result = CleaningResult(
295
+ file_path=file_path,
296
+ success=result.success,
297
+ steps_completed=result.steps_completed,
298
+ steps_failed=result.steps_failed,
299
+ warnings=result.warnings,
300
+ original_size=original_size,
301
+ cleaned_size=cleaned_size,
302
+ )
303
+
304
+ self.error_handler.log_cleaning_result(cleaning_result)
305
+ return cleaning_result
306
+
307
+ except Exception as e:
308
+ self.error_handler.handle_file_error(file_path, e, "file_processing")
309
+ return CleaningResult(
310
+ file_path=file_path,
311
+ success=False,
312
+ steps_completed=[],
313
+ steps_failed=["file_processing"],
314
+ warnings=[],
315
+ original_size=0,
316
+ cleaned_size=0,
317
+ )
318
+
319
+ @dataclass
320
+ class PipelineResult:
321
+ cleaned_code: str
322
+ success: bool
323
+ steps_completed: list[str]
324
+ steps_failed: list[str]
325
+ warnings: list[str]
326
+
327
+ def _apply_cleaning_pipeline(
328
+ self,
329
+ code: str,
330
+ file_path: Path,
331
+ cleaning_steps: list[CleaningStepProtocol],
332
+ ) -> PipelineResult:
333
+ current_code = code
334
+ steps_completed: list[str] = []
335
+ steps_failed: list[str] = []
336
+ warnings: list[str] = []
337
+ overall_success = True
338
+
339
+ for step in cleaning_steps:
340
+ try:
341
+ step_result = step(current_code, file_path)
342
+ current_code = step_result
343
+ steps_completed.append(step.name)
344
+
345
+ self.logger.debug(
346
+ "Cleaning step completed",
347
+ extra={"step": step.name, "file_path": str(file_path)},
348
+ )
349
+
350
+ except Exception as e:
351
+ self.error_handler.handle_file_error(file_path, e, step.name)
352
+ steps_failed.append(step.name)
353
+ warnings.append(f"{step.name} failed: {e}")
354
+
355
+ self.logger.warning(
356
+ "Cleaning step failed, continuing with original code",
357
+ extra={
358
+ "step": step.name,
359
+ "file_path": str(file_path),
360
+ "error": str(e),
361
+ },
362
+ )
363
+
364
+ if steps_failed:
365
+ success_ratio = len(steps_completed) / (
366
+ len(steps_completed) + len(steps_failed)
367
+ )
368
+ overall_success = success_ratio >= 0.7
369
+
370
+ return self.PipelineResult(
371
+ cleaned_code=current_code,
372
+ success=overall_success,
373
+ steps_completed=steps_completed,
374
+ steps_failed=steps_failed,
375
+ warnings=warnings,
376
+ )
377
+
378
+
379
+ class CodeCleaner(BaseModel):
380
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
381
+
382
+ console: t.Any
383
+ file_processor: t.Any = None
384
+ error_handler: t.Any = None
385
+ pipeline: t.Any = None
386
+ logger: t.Any = None
387
+ base_directory: Path | None = None
388
+ security_logger: t.Any = None
389
+ backup_service: t.Any = None
390
+
391
+ def model_post_init(self, _: t.Any) -> None:
392
+ if self.logger is None:
393
+ import logging
394
+
395
+ self.logger = logging.getLogger("crackerjack.code_cleaner")
396
+
397
+ if self.base_directory is None:
398
+ self.base_directory = Path.cwd()
399
+
400
+ if self.file_processor is None:
401
+ self.file_processor = FileProcessor(
402
+ console=self.console, base_directory=self.base_directory
403
+ )
404
+
405
+ if self.error_handler is None:
406
+ self.error_handler = CleaningErrorHandler(console=self.console)
407
+
408
+ if self.pipeline is None:
409
+ self.pipeline = CleaningPipeline(
410
+ file_processor=self.file_processor,
411
+ error_handler=self.error_handler,
412
+ console=self.console,
413
+ )
414
+
415
+ if self.security_logger is None:
416
+ self.security_logger = get_security_logger()
417
+
418
+ if self.backup_service is None:
419
+ self.backup_service = PackageBackupService()
420
+
421
+ def clean_file(self, file_path: Path) -> CleaningResult:
422
+ cleaning_steps = [
423
+ self._create_line_comment_step(),
424
+ self._create_docstring_step(),
425
+ self._create_whitespace_step(),
426
+ self._create_formatting_step(),
427
+ ]
428
+
429
+ result = self.pipeline.clean_file(file_path, cleaning_steps)
430
+ return t.cast(CleaningResult, result)
431
+
432
+ def clean_files(
433
+ self, pkg_dir: Path | None = None, use_backup: bool = True
434
+ ) -> list[CleaningResult] | PackageCleaningResult:
435
+ if use_backup:
436
+ package_result = self.clean_files_with_backup(pkg_dir)
437
+ self.logger.info(
438
+ f"Package cleaning with backup completed: "
439
+ f"success={package_result.overall_success}, "
440
+ f"restored={package_result.backup_restored}"
441
+ )
442
+ return package_result
443
+
444
+ self.console.print(
445
+ "[yellow]⚠️ WARNING: Running without backup protection. "
446
+ "Consider using use_backup=True for safety.[/yellow]"
447
+ )
448
+
449
+ if pkg_dir is None:
450
+ # Use configured base directory when no explicit path is provided
451
+ pkg_dir = self.base_directory or Path.cwd()
452
+
453
+ python_files = self._discover_package_files(pkg_dir)
454
+
455
+ files_to_process = [
456
+ file_path
457
+ for file_path in python_files
458
+ if self.should_process_file(file_path)
459
+ ]
460
+
461
+ results: list[CleaningResult] = []
462
+ self.logger.info(f"Starting clean_files for {len(files_to_process)} files")
463
+
464
+ cleaning_steps = [
465
+ self._create_line_comment_step(),
466
+ self._create_docstring_step(),
467
+ self._create_whitespace_step(),
468
+ self._create_formatting_step(),
469
+ ]
470
+
471
+ for file_path in files_to_process:
472
+ result = self.pipeline.clean_file(file_path, cleaning_steps)
473
+ results.append(result)
474
+
475
+ return results
476
+
477
+ def clean_files_with_backup(
478
+ self, pkg_dir: Path | None = None
479
+ ) -> PackageCleaningResult:
480
+ validated_pkg_dir = self._prepare_package_directory(pkg_dir)
481
+
482
+ self.logger.info(
483
+ f"Starting safe package cleaning with backup: {validated_pkg_dir}"
484
+ )
485
+ self.console.print(
486
+ "[cyan]🛡️ Starting package cleaning with backup protection...[/cyan]"
487
+ )
488
+
489
+ backup_metadata: BackupMetadata | None = None
490
+
491
+ try:
492
+ backup_metadata = self._create_backup(validated_pkg_dir)
493
+ files_to_process = self._find_files_to_process(validated_pkg_dir)
494
+
495
+ if not files_to_process:
496
+ return self._handle_no_files_to_process(backup_metadata)
497
+
498
+ cleaning_result = self._execute_cleaning_with_backup(
499
+ files_to_process, backup_metadata
500
+ )
501
+
502
+ return self._finalize_cleaning_result(cleaning_result, backup_metadata)
503
+
504
+ except Exception as e:
505
+ return self._handle_critical_error(e, backup_metadata)
506
+
507
+ def _prepare_package_directory(self, pkg_dir: Path | None) -> Path:
508
+ if pkg_dir is None:
509
+ pkg_dir = self.base_directory or Path.cwd()
510
+ # Avoid normalizing symlinks to preserve exact input path semantics
511
+ # while still enforcing base-directory containment.
512
+ if self.base_directory and not SecurePathValidator.is_within_directory(
513
+ pkg_dir, self.base_directory
514
+ ):
515
+ raise ExecutionError(
516
+ message=(
517
+ f"Path outside allowed directory: {pkg_dir} not within "
518
+ f"{self.base_directory}"
519
+ ),
520
+ error_code=ErrorCode.VALIDATION_ERROR,
521
+ )
522
+ return pkg_dir
523
+
524
+ def _create_backup(self, validated_pkg_dir: Path) -> BackupMetadata:
525
+ self.console.print(
526
+ "[yellow]📦 Creating backup of all package files...[/yellow]"
527
+ )
528
+
529
+ backup_result = self.backup_service.create_package_backup(
530
+ validated_pkg_dir, self.base_directory
531
+ )
532
+ backup_metadata: BackupMetadata = t.cast(BackupMetadata, backup_result)
533
+
534
+ self.console.print(
535
+ f"[green]✅ Backup created: {backup_metadata.backup_id}[/green] "
536
+ f"({backup_metadata.total_files} files, {backup_metadata.total_size} bytes)"
537
+ )
538
+
539
+ return backup_metadata
540
+
541
+ def _find_files_to_process(self, validated_pkg_dir: Path) -> list[Path]:
542
+ python_files = self._discover_package_files(validated_pkg_dir)
543
+ return [
544
+ file_path
545
+ for file_path in python_files
546
+ if self.should_process_file(file_path)
547
+ ]
548
+
549
+ def _discover_package_files(self, root_dir: Path) -> list[Path]:
550
+ package_dir = self._find_package_directory(root_dir)
551
+
552
+ if not package_dir or not package_dir.exists():
553
+ self.console.print(
554
+ "[yellow]⚠️ Could not determine package directory, searching for Python packages...[/yellow]"
555
+ )
556
+ return self._fallback_discover_packages(root_dir)
557
+
558
+ self.logger.debug(f"Using package directory: {package_dir}")
559
+
560
+ package_files = list[t.Any](package_dir.rglob("*.py"))
561
+
562
+ exclude_dirs = {
563
+ "__pycache__",
564
+ ".pytest_cache",
565
+ ".mypy_cache",
566
+ ".ruff_cache",
567
+ ".venv",
568
+ "venv",
569
+ }
570
+ filtered_files = [
571
+ f
572
+ for f in package_files
573
+ if not any(excl in f.parts for excl in exclude_dirs)
574
+ ]
575
+
576
+ return filtered_files
577
+
578
+ def _find_package_directory(self, root_dir: Path) -> Path | None:
579
+ pyproject_path = root_dir / "pyproject.toml"
580
+ if pyproject_path.exists():
581
+ try:
582
+ import tomllib
583
+
584
+ with pyproject_path.open("rb") as f:
585
+ config = tomllib.load(f)
586
+
587
+ project_name_raw = config.get("project", {}).get("name")
588
+ project_name: str | None = t.cast(str | None, project_name_raw)
589
+ if project_name:
590
+ package_name = project_name.replace("-", "_").lower()
591
+ package_dir = root_dir / package_name
592
+
593
+ if package_dir.exists() and (package_dir / "__init__.py").exists():
594
+ return package_dir
595
+
596
+ except Exception as e:
597
+ self.logger.debug(f"Could not parse pyproject.toml: {e}")
598
+
599
+ package_name = root_dir.name.replace("-", "_").lower()
600
+ package_dir = root_dir / package_name
601
+
602
+ if package_dir.exists() and (package_dir / "__init__.py").exists():
603
+ return package_dir
604
+
605
+ return None
606
+
607
+ def _fallback_discover_packages(self, root_dir: Path) -> list[Path]:
608
+ python_files = []
609
+ exclude_dirs = {
610
+ "__pycache__",
611
+ ".git",
612
+ ".venv",
613
+ "venv",
614
+ "site-packages",
615
+ ".pytest_cache",
616
+ "build",
617
+ "dist",
618
+ ".tox",
619
+ "node_modules",
620
+ "tests",
621
+ "test",
622
+ "examples",
623
+ "example",
624
+ "docs",
625
+ "doc",
626
+ ".mypy_cache",
627
+ ".ruff_cache",
628
+ "htmlcov",
629
+ ".coverage",
630
+ }
631
+
632
+ for item in root_dir.iterdir():
633
+ if (
634
+ not item.is_dir()
635
+ or item.name.startswith(".")
636
+ or item.name in exclude_dirs
637
+ ):
638
+ continue
639
+
640
+ if (item / "__init__.py").exists():
641
+ package_files = [
642
+ f
643
+ for f in item.rglob("*.py")
644
+ if self._should_include_file_path(f, exclude_dirs)
645
+ ]
646
+ python_files.extend(package_files)
647
+
648
+ return python_files
649
+
650
+ def _should_include_file_path(
651
+ self, file_path: Path, exclude_dirs: set[str]
652
+ ) -> bool:
653
+ path_parts = set[t.Any](file_path.parts)
654
+
655
+ return not bool(path_parts.intersection(exclude_dirs))
656
+
657
+ def _handle_no_files_to_process(
658
+ self, backup_metadata: BackupMetadata
659
+ ) -> PackageCleaningResult:
660
+ self.console.print("[yellow]⚠️ No files found to process[/yellow]")
661
+ self.backup_service.cleanup_backup(backup_metadata)
662
+
663
+ return PackageCleaningResult(
664
+ total_files=0,
665
+ successful_files=0,
666
+ failed_files=0,
667
+ file_results=[],
668
+ backup_metadata=None,
669
+ backup_restored=False,
670
+ overall_success=True,
671
+ )
672
+
673
+ def _execute_cleaning_with_backup(
674
+ self, files_to_process: list[Path], backup_metadata: BackupMetadata
675
+ ) -> dict[str, t.Any]:
676
+ self.console.print(f"[cyan]🧹 Cleaning {len(files_to_process)} files...[/cyan]")
677
+
678
+ cleaning_steps = [
679
+ self._create_line_comment_step(),
680
+ self._create_docstring_step(),
681
+ self._create_whitespace_step(),
682
+ self._create_formatting_step(),
683
+ ]
684
+
685
+ file_results: list[CleaningResult] = []
686
+ cleaning_errors: list[Exception] = []
687
+
688
+ for file_path in files_to_process:
689
+ try:
690
+ result = self.pipeline.clean_file(file_path, cleaning_steps)
691
+ result.backup_metadata = backup_metadata
692
+ file_results.append(result)
693
+
694
+ if not result.success:
695
+ cleaning_errors.append(
696
+ ExecutionError(
697
+ message=f"Cleaning failed for {file_path}: {result.steps_failed}",
698
+ error_code=ErrorCode.CODE_CLEANING_ERROR,
699
+ )
700
+ )
701
+ except Exception as e:
702
+ cleaning_errors.append(e)
703
+ file_results.append(
704
+ CleaningResult(
705
+ file_path=file_path,
706
+ success=False,
707
+ steps_completed=[],
708
+ steps_failed=["file_processing"],
709
+ warnings=[f"Exception during cleaning: {e}"],
710
+ original_size=0,
711
+ cleaned_size=0,
712
+ backup_metadata=backup_metadata,
713
+ )
714
+ )
715
+
716
+ return {
717
+ "file_results": file_results,
718
+ "cleaning_errors": cleaning_errors,
719
+ "files_to_process": files_to_process,
720
+ }
721
+
722
+ def _finalize_cleaning_result(
723
+ self, cleaning_result: dict[str, t.Any], backup_metadata: BackupMetadata
724
+ ) -> PackageCleaningResult:
725
+ file_results = cleaning_result["file_results"]
726
+ cleaning_errors = cleaning_result["cleaning_errors"]
727
+ files_to_process = cleaning_result["files_to_process"]
728
+
729
+ successful_files = sum(1 for result in file_results if result.success)
730
+ failed_files = len(file_results) - successful_files
731
+
732
+ if cleaning_errors or failed_files > 0:
733
+ return self._handle_cleaning_failure(
734
+ backup_metadata,
735
+ file_results,
736
+ files_to_process,
737
+ successful_files,
738
+ failed_files,
739
+ cleaning_errors,
740
+ )
741
+
742
+ return self._handle_cleaning_success(
743
+ backup_metadata, file_results, files_to_process, successful_files
744
+ )
745
+
746
+ def _handle_cleaning_failure(
747
+ self,
748
+ backup_metadata: BackupMetadata,
749
+ file_results: list[CleaningResult],
750
+ files_to_process: list[Path],
751
+ successful_files: int,
752
+ failed_files: int,
753
+ cleaning_errors: list[Exception],
754
+ ) -> PackageCleaningResult:
755
+ self.console.print(
756
+ f"[red]❌ Cleaning failed ({failed_files} files failed). "
757
+ f"Restoring from backup...[/red]"
758
+ )
759
+
760
+ self.logger.error(
761
+ f"Package cleaning failed with {len(cleaning_errors)} errors, "
762
+ f"restoring from backup {backup_metadata.backup_id}"
763
+ )
764
+
765
+ self.backup_service.restore_from_backup(backup_metadata, self.base_directory)
766
+
767
+ self.console.print("[green]✅ Files restored from backup successfully[/green]")
768
+
769
+ return PackageCleaningResult(
770
+ total_files=len(files_to_process),
771
+ successful_files=successful_files,
772
+ failed_files=failed_files,
773
+ file_results=file_results,
774
+ backup_metadata=backup_metadata,
775
+ backup_restored=True,
776
+ overall_success=False,
777
+ )
778
+
779
+ def _handle_cleaning_success(
780
+ self,
781
+ backup_metadata: BackupMetadata,
782
+ file_results: list[CleaningResult],
783
+ files_to_process: list[Path],
784
+ successful_files: int,
785
+ ) -> PackageCleaningResult:
786
+ self.console.print(
787
+ f"[green]✅ Package cleaning completed successfully![/green] "
788
+ f"({successful_files} files cleaned)"
789
+ )
790
+
791
+ self.backup_service.cleanup_backup(backup_metadata)
792
+
793
+ return PackageCleaningResult(
794
+ total_files=len(files_to_process),
795
+ successful_files=successful_files,
796
+ failed_files=0,
797
+ file_results=file_results,
798
+ backup_metadata=None,
799
+ backup_restored=False,
800
+ overall_success=True,
801
+ )
802
+
803
+ def _handle_critical_error(
804
+ self, error: Exception, backup_metadata: BackupMetadata | None
805
+ ) -> PackageCleaningResult:
806
+ self.logger.error(f"Critical error during package cleaning: {error}")
807
+ self.console.print(f"[red]💥 Critical error: {error}[/red]")
808
+
809
+ backup_restored = False
810
+
811
+ if backup_metadata:
812
+ backup_restored = self._attempt_emergency_restoration(backup_metadata)
813
+
814
+ return PackageCleaningResult(
815
+ total_files=0,
816
+ successful_files=0,
817
+ failed_files=0,
818
+ file_results=[],
819
+ backup_metadata=backup_metadata,
820
+ backup_restored=backup_restored,
821
+ overall_success=False,
822
+ )
823
+
824
+ def _attempt_emergency_restoration(self, backup_metadata: BackupMetadata) -> bool:
825
+ try:
826
+ self.console.print(
827
+ "[yellow]🔄 Attempting emergency restoration...[/yellow]"
828
+ )
829
+ self.backup_service.restore_from_backup(
830
+ backup_metadata, self.base_directory
831
+ )
832
+ self.console.print("[green]✅ Emergency restoration completed[/green]")
833
+ return True
834
+
835
+ except Exception as restore_error:
836
+ self.logger.error(f"Emergency restoration failed: {restore_error}")
837
+ self.console.print(
838
+ f"[red]💥 Emergency restoration failed: {restore_error}[/red]\n"
839
+ f"[yellow]⚠️ Manual restoration may be needed from: "
840
+ f"{backup_metadata.backup_directory}[/yellow]"
841
+ )
842
+ return False
843
+
844
+ def restore_from_backup_metadata(self, backup_metadata: BackupMetadata) -> None:
845
+ self.console.print(
846
+ f"[yellow]🔄 Manually restoring from backup: {backup_metadata.backup_id}[/yellow]"
847
+ )
848
+
849
+ self.backup_service.restore_from_backup(backup_metadata, self.base_directory)
850
+
851
+ self.console.print(
852
+ f"[green]✅ Manual restoration completed from backup: "
853
+ f"{backup_metadata.backup_id}[/green]"
854
+ )
855
+
856
+ def create_emergency_backup(self, pkg_dir: Path | None = None) -> BackupMetadata:
857
+ validated_pkg_dir = self._prepare_package_directory(pkg_dir)
858
+
859
+ self.console.print(
860
+ "[cyan]🛡️ Creating emergency backup before risky operation...[/cyan]"
861
+ )
862
+
863
+ backup_metadata = self._create_backup(validated_pkg_dir)
864
+
865
+ self.console.print(
866
+ f"[green]✅ Emergency backup created: {backup_metadata.backup_id}[/green]"
867
+ )
868
+
869
+ return backup_metadata
870
+
871
+ def restore_emergency_backup(self, backup_metadata: BackupMetadata) -> bool:
872
+ try:
873
+ self.console.print(
874
+ f"[yellow]🔄 Restoring emergency backup: {backup_metadata.backup_id}[/yellow]"
875
+ )
876
+
877
+ self.backup_service.restore_from_backup(
878
+ backup_metadata, self.base_directory
879
+ )
880
+
881
+ self.console.print(
882
+ f"[green]✅ Emergency backup restored successfully: {backup_metadata.backup_id}[/green]"
883
+ )
884
+
885
+ return True
886
+
887
+ except Exception as e:
888
+ self.logger.error(f"Emergency backup restoration failed: {e}")
889
+ self.console.print(
890
+ f"[red]💥 Emergency backup restoration failed: {e}[/red]\n"
891
+ f"[yellow]⚠️ Manual intervention required. Backup location: "
892
+ f"{backup_metadata.backup_directory}[/yellow]"
893
+ )
894
+
895
+ return False
896
+
897
+ def verify_backup_integrity(self, backup_metadata: BackupMetadata) -> bool:
898
+ try:
899
+ validation_result = self.backup_service._validate_backup(backup_metadata)
900
+
901
+ if validation_result.is_valid:
902
+ self.console.print(
903
+ f"[green]✅ Backup verification passed: {backup_metadata.backup_id}[/green] "
904
+ f"({validation_result.total_validated} files verified)"
905
+ )
906
+ return True
907
+ else:
908
+ self.console.print(
909
+ f"[red]❌ Backup verification failed: {backup_metadata.backup_id}[/red]"
910
+ )
911
+
912
+ for error in validation_result.validation_errors[:3]:
913
+ self.console.print(f"[red] • {error}[/red]")
914
+
915
+ if len(validation_result.validation_errors) > 3:
916
+ remaining = len(validation_result.validation_errors) - 3
917
+ self.console.print(f"[red] ... and {remaining} more errors[/red]")
918
+
919
+ return False
920
+
921
+ except Exception as e:
922
+ self.logger.error(f"Backup verification failed with exception: {e}")
923
+ self.console.print(f"[red]💥 Backup verification error: {e}[/red]")
924
+ return False
925
+
926
+ def list_available_backups(self) -> list[Path]:
927
+ if (
928
+ not self.backup_service.backup_root
929
+ or not self.backup_service.backup_root.exists()
930
+ ):
931
+ self.console.print("[yellow]⚠️ No backup root directory found[/yellow]")
932
+ return []
933
+
934
+ try:
935
+ backup_dirs = [
936
+ path
937
+ for path in self.backup_service.backup_root.iterdir()
938
+ if path.is_dir() and path.name.startswith("backup_")
939
+ ]
940
+
941
+ if backup_dirs:
942
+ self.console.print(
943
+ f"[cyan]📦 Found {len(backup_dirs)} available backups: [/cyan]"
944
+ )
945
+ for backup_dir in sorted(backup_dirs):
946
+ self.console.print(f" • {backup_dir.name}")
947
+ else:
948
+ self.console.print("[yellow]⚠️ No backups found[/yellow]")
949
+
950
+ return backup_dirs
951
+
952
+ except Exception as e:
953
+ self.logger.error(f"Failed to list[t.Any] backups: {e}")
954
+ self.console.print(f"[red]💥 Error listing backups: {e}[/red]")
955
+ return []
956
+
957
+ def should_process_file(self, file_path: Path) -> bool:
958
+ try:
959
+ validated_path = SecurePathValidator.validate_file_path(
960
+ file_path, self.base_directory
961
+ )
962
+
963
+ SecurePathValidator.validate_file_size(validated_path)
964
+
965
+ ignore_patterns = {
966
+ "__pycache__",
967
+ ".git",
968
+ ".venv",
969
+ "site-packages",
970
+ ".pytest_cache",
971
+ "build",
972
+ "dist",
973
+ "tests",
974
+ "test",
975
+ "examples",
976
+ "example",
977
+ }
978
+
979
+ for parent in validated_path.parents:
980
+ if parent.name in ignore_patterns:
981
+ return False
982
+
983
+ should_process = not (
984
+ validated_path.name.startswith(".") or validated_path.suffix != ".py"
985
+ )
986
+
987
+ if should_process:
988
+ self.security_logger.log_security_event(
989
+ SecurityEventType.FILE_CLEANED,
990
+ SecurityEventLevel.LOW,
991
+ f"File approved for processing: {validated_path}",
992
+ file_path=validated_path,
993
+ )
994
+
995
+ return should_process
996
+
997
+ except ExecutionError as e:
998
+ self.security_logger.log_validation_failed(
999
+ "file_processing_check",
1000
+ file_path,
1001
+ f"File failed security validation: {e}",
1002
+ )
1003
+
1004
+ return False
1005
+
1006
+ except Exception as e:
1007
+ self.logger.warning(f"Unexpected error checking file {file_path}: {e}")
1008
+ return False
1009
+
1010
+ def _create_line_comment_step(self) -> CleaningStepProtocol:
1011
+ return self._LineCommentStep()
1012
+
1013
+ def _create_docstring_step(self) -> CleaningStepProtocol:
1014
+ return self._DocstringStep()
1015
+
1016
+ class _DocstringStep:
1017
+ name = "remove_docstrings"
1018
+
1019
+ def _is_docstring_node(self, node: ast.AST) -> bool:
1020
+ body = getattr(node, "body", None)
1021
+ return (
1022
+ hasattr(node, "body")
1023
+ and body is not None
1024
+ and len(body) > 0
1025
+ and isinstance(body[0], ast.Expr)
1026
+ and isinstance(body[0].value, ast.Constant)
1027
+ and isinstance(body[0].value.value, str)
1028
+ )
1029
+
1030
+ def _find_docstrings(self, tree: ast.AST) -> list[ast.AST]:
1031
+ docstring_nodes: list[ast.AST] = []
1032
+ finder = self._DocstringFinder(docstring_nodes, self._is_docstring_node)
1033
+ finder.visit(tree)
1034
+ return docstring_nodes
1035
+
1036
+ class _DocstringFinder(ast.NodeVisitor):
1037
+ def __init__(
1038
+ self,
1039
+ docstring_nodes: list[ast.AST],
1040
+ is_docstring_node: t.Callable[[ast.AST], bool],
1041
+ ):
1042
+ self.docstring_nodes = docstring_nodes
1043
+ self.is_docstring_node = is_docstring_node
1044
+
1045
+ def _add_if_docstring(self, node: ast.AST) -> None:
1046
+ if self.is_docstring_node(node) and hasattr(node, "body"):
1047
+ body: list[ast.stmt] = getattr(node, "body")
1048
+ self.docstring_nodes.append(body[0])
1049
+ self.generic_visit(node)
1050
+
1051
+ def visit_Module(self, node: ast.Module) -> None:
1052
+ self._add_if_docstring(node)
1053
+
1054
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
1055
+ self._add_if_docstring(node)
1056
+
1057
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
1058
+ self._add_if_docstring(node)
1059
+
1060
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
1061
+ self._add_if_docstring(node)
1062
+
1063
+ def __call__(self, code: str, file_path: Path) -> str:
1064
+ try:
1065
+ tree = ast.parse(code, filename=str(file_path))
1066
+ except SyntaxError:
1067
+ return self._regex_fallback_removal(code)
1068
+
1069
+ docstring_nodes = self._find_docstrings(tree)
1070
+
1071
+ if not docstring_nodes:
1072
+ return code
1073
+
1074
+ lines = code.split("\n")
1075
+ lines_to_remove: set[int] = set()
1076
+
1077
+ for node in docstring_nodes:
1078
+ start_line = getattr(node, "lineno", 1)
1079
+ end_line = getattr(node, "end_lineno", start_line)
1080
+
1081
+ lines_to_remove.update(range(start_line, end_line + 1))
1082
+
1083
+ result_lines = [
1084
+ line for i, line in enumerate(lines, 1) if i not in lines_to_remove
1085
+ ]
1086
+
1087
+ result = "\n".join(result_lines)
1088
+ return self._regex_fallback_removal(result)
1089
+
1090
+ def _regex_fallback_removal(self, code: str) -> str:
1091
+ return _safe_applicator.apply_docstring_patterns(code)
1092
+
1093
+ class _LineCommentStep:
1094
+ name = "remove_line_comments"
1095
+
1096
+ def __call__(self, code: str, file_path: Path) -> str:
1097
+ lines = code.split("\n")
1098
+
1099
+ processed_lines = [self._process_line_for_comments(line) for line in lines]
1100
+ return "\n".join(processed_lines)
1101
+
1102
+ def _process_line_for_comments(self, line: str) -> str:
1103
+ if not line.strip() or self._is_preserved_comment_line(line):
1104
+ return line
1105
+ return self._remove_comment_from_line(line)
1106
+
1107
+ def _is_preserved_comment_line(self, line: str) -> bool:
1108
+ stripped = line.strip()
1109
+ if not stripped.startswith("#"):
1110
+ return False
1111
+ return self._has_preserved_pattern(stripped)
1112
+
1113
+ def _has_preserved_pattern(self, stripped_line: str) -> bool:
1114
+ return _safe_applicator.has_preserved_comment(stripped_line)
1115
+
1116
+ def _remove_comment_from_line(self, line: str) -> str:
1117
+ """Remove comment from line while preserving strings."""
1118
+ if not self._line_needs_comment_processing(line):
1119
+ return line
1120
+
1121
+ return self._process_line_for_comment_removal(line)
1122
+
1123
+ def _line_needs_comment_processing(self, line: str) -> bool:
1124
+ """Check if line needs comment processing."""
1125
+ return '"' in line or "'" in line or "#" in line
1126
+
1127
+ def _process_line_for_comment_removal(self, line: str) -> str:
1128
+ """Process line to remove comments while preserving strings and special comments."""
1129
+ result_chars = []
1130
+ string_state = {"in_string": False, "quote_char": None}
1131
+
1132
+ for i, char in enumerate(line):
1133
+ should_stop, preserve_rest = self._should_stop_for_comment(
1134
+ char, string_state, line, i
1135
+ )
1136
+ if should_stop:
1137
+ if preserve_rest:
1138
+ # Preserve the rest of the line (special comment like nosec, noqa, type:)
1139
+ result_chars.extend(line[i:])
1140
+ break
1141
+
1142
+ self._update_string_state(char, i, line, string_state)
1143
+ result_chars.append(char)
1144
+
1145
+ return "".join(result_chars).rstrip()
1146
+
1147
+ def _should_stop_for_comment(
1148
+ self, char: str, string_state: dict[str, t.Any], line: str, index: int
1149
+ ) -> tuple[bool, bool]:
1150
+ """Check if we should stop processing at comment.
1151
+
1152
+ Returns:
1153
+ (should_stop, preserve_rest): should_stop=True when comment found,
1154
+ preserve_rest=True if comment should be kept
1155
+ """
1156
+ if string_state["in_string"] or char != "#":
1157
+ return (False, False)
1158
+
1159
+ # Check if the comment portion should be preserved
1160
+ comment_part = line[index:].strip()
1161
+ if _safe_applicator.has_preserved_comment(comment_part):
1162
+ return (True, True) # Stop processing, preserve the comment
1163
+
1164
+ return (True, False) # Stop processing, discard the comment
1165
+
1166
+ def _update_string_state(
1167
+ self, char: str, index: int, line: str, string_state: dict[str, t.Any]
1168
+ ) -> None:
1169
+ """Update string parsing state."""
1170
+ if not string_state["in_string"]:
1171
+ if char in ('"', "'"):
1172
+ string_state["in_string"] = True
1173
+ string_state["quote_char"] = char
1174
+ elif char == string_state["quote_char"] and (
1175
+ index == 0 or line[index - 1] != "\\"
1176
+ ):
1177
+ string_state["in_string"] = False
1178
+ string_state["quote_char"] = None
1179
+
1180
+ def _create_docstring_finder_class(
1181
+ self,
1182
+ docstring_nodes: list[ast.AST],
1183
+ ) -> type[ast.NodeVisitor]:
1184
+ class DocstringFinder(ast.NodeVisitor):
1185
+ def _is_docstring_node(self, node: ast.AST) -> bool:
1186
+ body = getattr(node, "body", None)
1187
+ return (
1188
+ hasattr(node, "body")
1189
+ and body is not None
1190
+ and len(body) > 0
1191
+ and isinstance(body[0], ast.Expr)
1192
+ and isinstance(body[0].value, ast.Constant)
1193
+ and isinstance(body[0].value.value, str)
1194
+ )
1195
+
1196
+ def _add_if_docstring(self, node: ast.AST) -> None:
1197
+ if self._is_docstring_node(node) and hasattr(node, "body"):
1198
+ body: list[ast.stmt] = getattr(node, "body")
1199
+ docstring_nodes.append(body[0])
1200
+ self.generic_visit(node)
1201
+
1202
+ def visit_Module(self, node: ast.Module) -> None:
1203
+ self._add_if_docstring(node)
1204
+
1205
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
1206
+ self._add_if_docstring(node)
1207
+
1208
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
1209
+ self._add_if_docstring(node)
1210
+
1211
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
1212
+ self._add_if_docstring(node)
1213
+
1214
+ return DocstringFinder
1215
+
1216
+ def _create_whitespace_step(self) -> CleaningStepProtocol:
1217
+ class WhitespaceStep:
1218
+ name = "remove_extra_whitespace"
1219
+
1220
+ def __call__(self, code: str, file_path: Path) -> str:
1221
+ lines = code.split("\n")
1222
+ cleaned_lines: list[str] = []
1223
+ empty_line_count = 0
1224
+
1225
+ for line in lines:
1226
+ cleaned_line = line.rstrip()
1227
+
1228
+ if not cleaned_line.strip():
1229
+ empty_line_count += 1
1230
+ if empty_line_count <= 2:
1231
+ cleaned_lines.append("")
1232
+ else:
1233
+ empty_line_count = 0
1234
+ leading_whitespace = len(cleaned_line) - len(
1235
+ cleaned_line.lstrip()
1236
+ )
1237
+ content = cleaned_line.lstrip()
1238
+
1239
+ content = SAFE_PATTERNS["multiple_spaces"].apply(content)
1240
+
1241
+ cleaned_line = cleaned_line[:leading_whitespace] + content
1242
+ cleaned_lines.append(cleaned_line)
1243
+
1244
+ while cleaned_lines and not cleaned_lines[-1].strip():
1245
+ cleaned_lines.pop()
1246
+
1247
+ result = "\n".join(cleaned_lines)
1248
+ if result and not result.endswith("\n"):
1249
+ result += "\n"
1250
+
1251
+ return result
1252
+
1253
+ return WhitespaceStep()
1254
+
1255
+ def _create_formatting_step(self) -> CleaningStepProtocol:
1256
+ class FormattingStep:
1257
+ name = "format_code"
1258
+
1259
+ def _is_preserved_comment_line(self, line: str) -> bool:
1260
+ stripped = line.strip()
1261
+ if not stripped.startswith("#"):
1262
+ return False
1263
+ return _safe_applicator.has_preserved_comment(line)
1264
+
1265
+ def __call__(self, code: str, file_path: Path) -> str:
1266
+ lines = code.split("\n")
1267
+ formatted_lines: list[str] = []
1268
+
1269
+ for line in lines:
1270
+ if line.strip():
1271
+ if self._is_preserved_comment_line(line):
1272
+ formatted_lines.append(line)
1273
+ continue
1274
+
1275
+ leading_whitespace = len(line) - len(line.lstrip())
1276
+ content = line.lstrip()
1277
+
1278
+ content = _safe_applicator.apply_formatting_patterns(content)
1279
+
1280
+ formatted_line = line[:leading_whitespace] + content
1281
+ formatted_lines.append(formatted_line)
1282
+ else:
1283
+ formatted_lines.append(line)
1284
+
1285
+ return "\n".join(formatted_lines)
1286
+
1287
+ return FormattingStep()
1288
+
1289
+ def remove_line_comments(self, code: str, file_path: Path | None = None) -> str:
1290
+ file_path = file_path or Path("temp.py")
1291
+ step = self._create_line_comment_step()
1292
+ return step(code, file_path)
1293
+
1294
+ def remove_docstrings(self, code: str, file_path: Path | None = None) -> str:
1295
+ file_path = file_path or Path("temp.py")
1296
+ step = self._create_docstring_step()
1297
+ return step(code, file_path)
1298
+
1299
+ def remove_extra_whitespace(self, code: str, file_path: Path | None = None) -> str:
1300
+ file_path = file_path or Path("temp.py")
1301
+ step = self._create_whitespace_step()
1302
+ return step(code, file_path)
1303
+
1304
+ def format_code(self, code: str, file_path: Path | None = None) -> str:
1305
+ file_path = file_path or Path("temp.py")
1306
+ step = self._create_formatting_step()
1307
+ return step(code, file_path)