crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (156) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +227 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +170 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +657 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +409 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +585 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +826 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +433 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +443 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +114 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +621 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +372 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +217 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +565 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/coverage_improvement.py +223 -0
  107. crackerjack/orchestration/execution_strategies.py +341 -0
  108. crackerjack/orchestration/test_progress_streamer.py +636 -0
  109. crackerjack/plugins/__init__.py +15 -0
  110. crackerjack/plugins/base.py +200 -0
  111. crackerjack/plugins/hooks.py +246 -0
  112. crackerjack/plugins/loader.py +335 -0
  113. crackerjack/plugins/managers.py +259 -0
  114. crackerjack/py313.py +8 -3
  115. crackerjack/services/__init__.py +22 -0
  116. crackerjack/services/cache.py +314 -0
  117. crackerjack/services/config.py +358 -0
  118. crackerjack/services/config_integrity.py +99 -0
  119. crackerjack/services/contextual_ai_assistant.py +516 -0
  120. crackerjack/services/coverage_ratchet.py +356 -0
  121. crackerjack/services/debug.py +736 -0
  122. crackerjack/services/dependency_monitor.py +617 -0
  123. crackerjack/services/enhanced_filesystem.py +439 -0
  124. crackerjack/services/file_hasher.py +151 -0
  125. crackerjack/services/filesystem.py +421 -0
  126. crackerjack/services/git.py +176 -0
  127. crackerjack/services/health_metrics.py +611 -0
  128. crackerjack/services/initialization.py +873 -0
  129. crackerjack/services/log_manager.py +286 -0
  130. crackerjack/services/logging.py +174 -0
  131. crackerjack/services/metrics.py +578 -0
  132. crackerjack/services/pattern_cache.py +362 -0
  133. crackerjack/services/pattern_detector.py +515 -0
  134. crackerjack/services/performance_benchmarks.py +653 -0
  135. crackerjack/services/security.py +163 -0
  136. crackerjack/services/server_manager.py +234 -0
  137. crackerjack/services/smart_scheduling.py +144 -0
  138. crackerjack/services/tool_version_service.py +61 -0
  139. crackerjack/services/unified_config.py +437 -0
  140. crackerjack/services/version_checker.py +248 -0
  141. crackerjack/slash_commands/__init__.py +14 -0
  142. crackerjack/slash_commands/init.md +122 -0
  143. crackerjack/slash_commands/run.md +163 -0
  144. crackerjack/slash_commands/status.md +127 -0
  145. crackerjack-0.31.7.dist-info/METADATA +742 -0
  146. crackerjack-0.31.7.dist-info/RECORD +149 -0
  147. crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
  148. crackerjack/.gitignore +0 -34
  149. crackerjack/.libcst.codemod.yaml +0 -18
  150. crackerjack/.pdm.toml +0 -1
  151. crackerjack/crackerjack.py +0 -3805
  152. crackerjack/pyproject.toml +0 -286
  153. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  154. crackerjack-0.30.3.dist-info/RECORD +0 -16
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
  156. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,669 @@
1
+ import ast
2
+ import operator
3
+ import typing as t
4
+ from contextlib import suppress
5
+ from pathlib import Path
6
+
7
+ from .base import (
8
+ FixResult,
9
+ Issue,
10
+ IssueType,
11
+ SubAgent,
12
+ agent_registry,
13
+ )
14
+
15
+
16
+ class PerformanceAgent(SubAgent):
17
+ """Agent specialized in detecting and fixing performance issues and anti-patterns."""
18
+
19
+ def get_supported_types(self) -> set[IssueType]:
20
+ return {IssueType.PERFORMANCE}
21
+
22
+ async def can_handle(self, issue: Issue) -> float:
23
+ if issue.type == IssueType.PERFORMANCE:
24
+ return 0.85
25
+ return 0.0
26
+
27
+ async def analyze_and_fix(self, issue: Issue) -> FixResult:
28
+ self.log(f"Analyzing performance issue: {issue.message}")
29
+
30
+ validation_result = self._validate_performance_issue(issue)
31
+ if validation_result:
32
+ return validation_result
33
+
34
+ if issue.file_path is None:
35
+ return FixResult(
36
+ success=False,
37
+ confidence=0.0,
38
+ remaining_issues=["No file path provided for performance issue"],
39
+ )
40
+
41
+ file_path = Path(issue.file_path)
42
+
43
+ try:
44
+ return await self._process_performance_optimization(file_path)
45
+ except Exception as e:
46
+ return self._create_performance_error_result(e)
47
+
48
+ def _validate_performance_issue(self, issue: Issue) -> FixResult | None:
49
+ """Validate the performance issue has required information."""
50
+ if not issue.file_path:
51
+ return FixResult(
52
+ success=False,
53
+ confidence=0.0,
54
+ remaining_issues=["No file path specified for performance issue"],
55
+ )
56
+
57
+ file_path = Path(issue.file_path)
58
+ if not file_path.exists():
59
+ return FixResult(
60
+ success=False,
61
+ confidence=0.0,
62
+ remaining_issues=[f"File not found: {file_path}"],
63
+ )
64
+
65
+ return None
66
+
67
+ async def _process_performance_optimization(self, file_path: Path) -> FixResult:
68
+ """Process performance issue detection and optimization for a file."""
69
+ content = self.context.get_file_content(file_path)
70
+ if not content:
71
+ return FixResult(
72
+ success=False,
73
+ confidence=0.0,
74
+ remaining_issues=[f"Could not read file: {file_path}"],
75
+ )
76
+
77
+ performance_issues = self._detect_performance_issues(content, file_path)
78
+
79
+ if not performance_issues:
80
+ return FixResult(
81
+ success=True,
82
+ confidence=0.7,
83
+ recommendations=["No performance issues detected"],
84
+ )
85
+
86
+ return self._apply_and_save_optimizations(
87
+ file_path,
88
+ content,
89
+ performance_issues,
90
+ )
91
+
92
+ def _apply_and_save_optimizations(
93
+ self,
94
+ file_path: Path,
95
+ content: str,
96
+ issues: list[dict[str, t.Any]],
97
+ ) -> FixResult:
98
+ """Apply performance optimizations and save changes."""
99
+ optimized_content = self._apply_performance_optimizations(content, issues)
100
+
101
+ if optimized_content == content:
102
+ return self._create_no_optimization_result()
103
+
104
+ success = self.context.write_file_content(file_path, optimized_content)
105
+ if not success:
106
+ return FixResult(
107
+ success=False,
108
+ confidence=0.0,
109
+ remaining_issues=[f"Failed to write optimized file: {file_path}"],
110
+ )
111
+
112
+ return FixResult(
113
+ success=True,
114
+ confidence=0.8,
115
+ fixes_applied=[
116
+ f"Optimized {len(issues)} performance issues",
117
+ "Applied algorithmic improvements",
118
+ ],
119
+ files_modified=[str(file_path)],
120
+ recommendations=["Test performance improvements with benchmarks"],
121
+ )
122
+
123
+ def _create_no_optimization_result(self) -> FixResult:
124
+ """Create result for when no optimizations could be applied."""
125
+ return FixResult(
126
+ success=False,
127
+ confidence=0.6,
128
+ remaining_issues=["Could not automatically optimize performance"],
129
+ recommendations=[
130
+ "Manual optimization required",
131
+ "Consider algorithm complexity improvements",
132
+ "Review data structure choices",
133
+ "Profile code execution for bottlenecks",
134
+ ],
135
+ )
136
+
137
+ def _create_performance_error_result(self, error: Exception) -> FixResult:
138
+ """Create result for performance processing errors."""
139
+ return FixResult(
140
+ success=False,
141
+ confidence=0.0,
142
+ remaining_issues=[f"Error processing file: {error}"],
143
+ )
144
+
145
+ def _detect_performance_issues(
146
+ self,
147
+ content: str,
148
+ file_path: Path,
149
+ ) -> list[dict[str, t.Any]]:
150
+ """Detect various performance anti-patterns in the code."""
151
+ issues: list[dict[str, t.Any]] = []
152
+
153
+ with suppress(SyntaxError):
154
+ tree = ast.parse(content)
155
+
156
+ # Detect nested loops
157
+ issues.extend(self._detect_nested_loops(tree))
158
+
159
+ # Detect inefficient list operations
160
+ issues.extend(self._detect_inefficient_list_ops(content, tree))
161
+
162
+ # Detect repeated expensive operations
163
+ issues.extend(self._detect_repeated_operations(content, tree))
164
+
165
+ # Detect inefficient string operations
166
+ issues.extend(self._detect_string_inefficiencies(content))
167
+
168
+ return issues
169
+
170
+ def _detect_nested_loops(self, tree: ast.AST) -> list[dict[str, t.Any]]:
171
+ """Detect nested loops that might have O(n²) or worse complexity."""
172
+ issues: list[dict[str, t.Any]] = []
173
+
174
+ class NestedLoopAnalyzer(ast.NodeVisitor):
175
+ def __init__(self) -> None:
176
+ self.loop_stack: list[tuple[str, ast.AST]] = []
177
+ self.nested_loops: list[dict[str, t.Any]] = []
178
+
179
+ def visit_For(self, node: ast.For) -> None:
180
+ self.loop_stack.append(("for", node))
181
+ if len(self.loop_stack) > 1:
182
+ self.nested_loops.append(
183
+ {
184
+ "line_number": node.lineno,
185
+ "type": "nested_for_loop",
186
+ "depth": len(self.loop_stack),
187
+ "node": node,
188
+ },
189
+ )
190
+ self.generic_visit(node)
191
+ self.loop_stack.pop()
192
+
193
+ def visit_While(self, node: ast.While) -> None:
194
+ self.loop_stack.append(("while", node))
195
+ if len(self.loop_stack) > 1:
196
+ self.nested_loops.append(
197
+ {
198
+ "line_number": node.lineno,
199
+ "type": "nested_while_loop",
200
+ "depth": len(self.loop_stack),
201
+ "node": node,
202
+ },
203
+ )
204
+ self.generic_visit(node)
205
+ self.loop_stack.pop()
206
+
207
+ analyzer = NestedLoopAnalyzer()
208
+ analyzer.visit(tree)
209
+
210
+ if analyzer.nested_loops:
211
+ issues.append(
212
+ {
213
+ "type": "nested_loops",
214
+ "instances": analyzer.nested_loops,
215
+ "suggestion": "Consider flattening loops or using more efficient algorithms",
216
+ },
217
+ )
218
+
219
+ return issues
220
+
221
+ def _detect_inefficient_list_ops(
222
+ self,
223
+ content: str,
224
+ tree: ast.AST,
225
+ ) -> list[dict[str, t.Any]]:
226
+ """Detect inefficient list operations like repeated appends or concatenations."""
227
+ issues: list[dict[str, t.Any]] = []
228
+ content.split("\n")
229
+
230
+ # Pattern: list += [item] or list = list + [item] in loops
231
+
232
+ class ListOpAnalyzer(ast.NodeVisitor):
233
+ def __init__(self) -> None:
234
+ self.in_loop = False
235
+ self.list_ops: list[dict[str, t.Any]] = []
236
+
237
+ def visit_For(self, node: ast.For) -> None:
238
+ old_in_loop = self.in_loop
239
+ self.in_loop = True
240
+ self.generic_visit(node)
241
+ self.in_loop = old_in_loop
242
+
243
+ def visit_While(self, node: ast.While) -> None:
244
+ old_in_loop = self.in_loop
245
+ self.in_loop = True
246
+ self.generic_visit(node)
247
+ self.in_loop = old_in_loop
248
+
249
+ def visit_AugAssign(self, node: ast.AugAssign) -> None:
250
+ if self.in_loop and isinstance(node.op, ast.Add):
251
+ if isinstance(node.value, ast.List):
252
+ self.list_ops.append(
253
+ {
254
+ "line_number": node.lineno,
255
+ "type": "list_concat_in_loop",
256
+ "pattern": "list += [item]",
257
+ },
258
+ )
259
+ self.generic_visit(node)
260
+
261
+ analyzer = ListOpAnalyzer()
262
+ analyzer.visit(tree)
263
+
264
+ if analyzer.list_ops:
265
+ issues.append(
266
+ {
267
+ "type": "inefficient_list_operations",
268
+ "instances": analyzer.list_ops,
269
+ "suggestion": "Use list.append() or collect items first, then extend",
270
+ },
271
+ )
272
+
273
+ return issues
274
+
275
+ def _detect_repeated_operations(
276
+ self,
277
+ content: str,
278
+ tree: ast.AST,
279
+ ) -> list[dict[str, t.Any]]:
280
+ """Detect repeated expensive operations that could be cached."""
281
+ lines = content.split("\n")
282
+ repeated_calls = self._find_expensive_operations_in_loops(lines)
283
+
284
+ return self._create_repeated_operations_issues(repeated_calls)
285
+
286
+ def _find_expensive_operations_in_loops(
287
+ self,
288
+ lines: list[str],
289
+ ) -> list[dict[str, t.Any]]:
290
+ """Find expensive operations that occur within loop contexts."""
291
+ repeated_calls: list[dict[str, t.Any]] = []
292
+ expensive_patterns = self._get_expensive_operation_patterns()
293
+
294
+ for i, line in enumerate(lines):
295
+ stripped = line.strip()
296
+ if self._contains_expensive_operation(stripped, expensive_patterns):
297
+ if self._is_in_loop_context(lines, i):
298
+ repeated_calls.append(self._create_operation_record(i, stripped))
299
+
300
+ return repeated_calls
301
+
302
+ def _get_expensive_operation_patterns(self) -> tuple[str, ...]:
303
+ """Get patterns for expensive operations to detect."""
304
+ return (
305
+ ".exists()",
306
+ ".read_text()",
307
+ ".glob(",
308
+ ".rglob(",
309
+ "Path(",
310
+ "len(",
311
+ ".get(",
312
+ )
313
+
314
+ def _contains_expensive_operation(
315
+ self,
316
+ line: str,
317
+ patterns: tuple[str, ...],
318
+ ) -> bool:
319
+ """Check if line contains any expensive operation patterns."""
320
+ return any(pattern in line for pattern in patterns)
321
+
322
+ def _is_in_loop_context(self, lines: list[str], line_index: int) -> bool:
323
+ """Check if line is within a loop context using simple heuristic."""
324
+ context_start = max(0, line_index - 5)
325
+ context_lines = lines[context_start : line_index + 1]
326
+ # Performance: Use compiled patterns and single check per line
327
+ loop_keywords = ("for ", "while ")
328
+ return any(
329
+ any(keyword in ctx_line for keyword in loop_keywords)
330
+ for ctx_line in context_lines
331
+ )
332
+
333
+ def _create_operation_record(
334
+ self,
335
+ line_index: int,
336
+ content: str,
337
+ ) -> dict[str, t.Any]:
338
+ """Create a record for an expensive operation found in a loop."""
339
+ return {
340
+ "line_number": line_index + 1,
341
+ "content": content,
342
+ "type": "expensive_operation_in_loop",
343
+ }
344
+
345
+ def _create_repeated_operations_issues(
346
+ self,
347
+ repeated_calls: list[dict[str, t.Any]],
348
+ ) -> list[dict[str, t.Any]]:
349
+ """Create issues list for repeated operations if threshold is met."""
350
+ if len(repeated_calls) >= 2:
351
+ return [
352
+ {
353
+ "type": "repeated_expensive_operations",
354
+ "instances": repeated_calls,
355
+ "suggestion": "Cache expensive operations outside loops",
356
+ },
357
+ ]
358
+ return []
359
+
360
+ def _detect_string_inefficiencies(self, content: str) -> list[dict[str, t.Any]]:
361
+ """Detect inefficient string operations."""
362
+ issues: list[dict[str, t.Any]] = []
363
+ lines = content.split("\n")
364
+
365
+ # Pattern: String concatenation in loops
366
+ string_concat_in_loop: list[dict[str, t.Any]] = []
367
+
368
+ for i, line in enumerate(lines):
369
+ stripped = line.strip()
370
+ if "+=" in stripped and any(quote in stripped for quote in ('"', "'")):
371
+ # Check if in loop context
372
+ context_start = max(0, i - 5)
373
+ context_lines = lines[context_start : i + 1]
374
+ # Performance: Use tuple lookup for faster keyword matching
375
+ loop_keywords = ("for ", "while ")
376
+ if any(
377
+ any(keyword in ctx_line for keyword in loop_keywords)
378
+ for ctx_line in context_lines
379
+ ):
380
+ string_concat_in_loop.append(
381
+ {
382
+ "line_number": i + 1,
383
+ "content": stripped,
384
+ },
385
+ )
386
+
387
+ if len(string_concat_in_loop) >= 2:
388
+ issues.append(
389
+ {
390
+ "type": "string_concatenation_in_loop",
391
+ "instances": string_concat_in_loop,
392
+ "suggestion": 'Use list.append() and "".join() for string building',
393
+ },
394
+ )
395
+
396
+ return issues
397
+
398
+ def _apply_performance_optimizations(
399
+ self,
400
+ content: str,
401
+ issues: list[dict[str, t.Any]],
402
+ ) -> str:
403
+ """Apply performance optimizations for detected issues."""
404
+ lines = content.split("\n")
405
+ modified = False
406
+
407
+ for issue in issues:
408
+ if issue["type"] == "inefficient_list_operations":
409
+ lines, changed = self._fix_list_operations(lines, issue)
410
+ modified = modified or changed
411
+ elif issue["type"] == "string_concatenation_in_loop":
412
+ lines, changed = self._fix_string_concatenation(lines, issue)
413
+ modified = modified or changed
414
+ elif issue["type"] == "repeated_expensive_operations":
415
+ lines, changed = self._fix_repeated_operations(lines, issue)
416
+ modified = modified or changed
417
+
418
+ return "\n".join(lines) if modified else content
419
+
420
+ def _fix_list_operations(
421
+ self,
422
+ lines: list[str],
423
+ issue: dict[str, t.Any],
424
+ ) -> tuple[list[str], bool]:
425
+ """Fix inefficient list operations by replacing list += [item] with list.append(item)."""
426
+ modified = False
427
+
428
+ # Process instances in reverse order (highest line numbers first) to avoid line number shifts
429
+ instances = sorted(
430
+ issue["instances"],
431
+ key=operator.itemgetter("line_number"),
432
+ reverse=True,
433
+ )
434
+
435
+ for instance in instances:
436
+ line_idx = instance["line_number"] - 1
437
+ if line_idx < len(lines):
438
+ original_line = t.cast(str, lines[line_idx])
439
+
440
+ # Transform: list += [item] -> list.append(item)
441
+ # Pattern: variable_name += [expression]
442
+ import re
443
+
444
+ pattern = r"(\s*)(\w+)\s*\+=\s*\[([^]]+)\]"
445
+ match = re.match(pattern, original_line)
446
+
447
+ if match:
448
+ indent, var_name, item_expr = match.groups()
449
+ optimized_line = f"{indent}{var_name}.append({item_expr})"
450
+ lines[line_idx] = optimized_line
451
+ modified = True
452
+
453
+ # Add comment explaining the optimization
454
+ comment = (
455
+ f"{indent}# Performance: Changed += [item] to .append(item)"
456
+ )
457
+ lines.insert(line_idx, comment)
458
+
459
+ return lines, modified
460
+
461
+ def _fix_string_concatenation(
462
+ self,
463
+ lines: list[str],
464
+ issue: dict[str, t.Any],
465
+ ) -> tuple[list[str], bool]:
466
+ """Fix inefficient string concatenation in loops by transforming to list.append + join pattern."""
467
+ var_groups = self._group_concatenation_instances(lines, issue["instances"])
468
+ return self._apply_concatenation_optimizations(lines, var_groups)
469
+
470
+ def _group_concatenation_instances(
471
+ self,
472
+ lines: list[str],
473
+ instances: list[dict[str, t.Any]],
474
+ ) -> dict[str, list[dict[str, t.Any]]]:
475
+ """Group string concatenation instances by variable name."""
476
+ var_groups: dict[str, list[dict[str, t.Any]]] = {}
477
+
478
+ for instance in instances:
479
+ line_info = self._parse_concatenation_line(lines, instance)
480
+ if line_info:
481
+ var_name = line_info["var_name"]
482
+ if var_name not in var_groups:
483
+ var_groups[var_name] = []
484
+ var_groups[var_name].append(line_info)
485
+
486
+ return var_groups
487
+
488
+ def _parse_concatenation_line(
489
+ self,
490
+ lines: list[str],
491
+ instance: dict[str, t.Any],
492
+ ) -> dict[str, t.Any] | None:
493
+ """Parse a string concatenation line to extract variable info."""
494
+ line_idx = instance["line_number"] - 1
495
+ if line_idx >= len(lines):
496
+ return None
497
+
498
+ original_line = t.cast(str, lines[line_idx])
499
+
500
+ import re
501
+
502
+ pattern = r"(\s*)(\w+)\s*\+=\s*(.+)"
503
+ match = re.match(pattern, original_line)
504
+
505
+ if match:
506
+ indent, var_name, expr = match.groups()
507
+ return {
508
+ "line_idx": line_idx,
509
+ "indent": indent,
510
+ "var_name": var_name,
511
+ "expr": expr.strip(),
512
+ "original_line": original_line,
513
+ }
514
+ return None
515
+
516
+ def _apply_concatenation_optimizations(
517
+ self,
518
+ lines: list[str],
519
+ var_groups: dict[str, list[dict[str, t.Any]]],
520
+ ) -> tuple[list[str], bool]:
521
+ """Apply string building optimizations for each variable group."""
522
+ modified = False
523
+
524
+ for var_name, instances in var_groups.items():
525
+ if instances:
526
+ first_instance = instances[0]
527
+ loop_start = self._find_loop_start(lines, first_instance["line_idx"])
528
+
529
+ if loop_start is not None:
530
+ optimization_applied = self._apply_string_building_optimization(
531
+ lines, var_name, instances, loop_start
532
+ )
533
+ modified = modified or optimization_applied
534
+
535
+ return lines, modified
536
+
537
+ def _find_loop_start(self, lines: list[str], start_idx: int) -> int | None:
538
+ """Find the start of the loop that contains the given line."""
539
+ for i in range(start_idx, -1, -1):
540
+ line = lines[i].strip()
541
+ if line.startswith(("for ", "while ")):
542
+ return i
543
+ # Stop if we hit a function definition or class definition
544
+ if line.startswith(("def ", "class ")):
545
+ break
546
+ return None
547
+
548
+ def _apply_string_building_optimization(
549
+ self,
550
+ lines: list[str],
551
+ var_name: str,
552
+ instances: list[dict[str, t.Any]],
553
+ loop_start: int,
554
+ ) -> bool:
555
+ """Apply string building optimization using list.append + join pattern."""
556
+ if not instances:
557
+ return False
558
+
559
+ first_instance = instances[0]
560
+ indent = first_instance["indent"]
561
+
562
+ init_line_idx = self._find_variable_initialization(lines, var_name, loop_start)
563
+ if init_line_idx is not None:
564
+ self._transform_string_initialization(
565
+ lines, init_line_idx, var_name, indent
566
+ )
567
+ self._replace_concatenations_with_appends(
568
+ lines, instances, var_name, indent
569
+ )
570
+ self._add_join_after_loop(lines, var_name, indent, loop_start)
571
+ return True
572
+
573
+ return False
574
+
575
+ def _find_variable_initialization(
576
+ self,
577
+ lines: list[str],
578
+ var_name: str,
579
+ loop_start: int,
580
+ ) -> int | None:
581
+ """Find the line where the string variable is initialized."""
582
+ search_start = max(0, loop_start - 10)
583
+
584
+ for i in range(loop_start - 1, search_start - 1, -1):
585
+ line = lines[i].strip()
586
+ if f"{var_name} =" in line and '""' in line:
587
+ return i
588
+ return None
589
+
590
+ def _transform_string_initialization(
591
+ self,
592
+ lines: list[str],
593
+ init_line_idx: int,
594
+ var_name: str,
595
+ indent: str,
596
+ ) -> None:
597
+ """Transform string initialization to list initialization."""
598
+ lines[init_line_idx] = (
599
+ f"{indent}{var_name}_parts = [] # Performance: Use list for string building"
600
+ )
601
+
602
+ def _replace_concatenations_with_appends(
603
+ self,
604
+ lines: list[str],
605
+ instances: list[dict[str, t.Any]],
606
+ var_name: str,
607
+ indent: str,
608
+ ) -> None:
609
+ """Replace string concatenations with list appends."""
610
+ for instance in instances:
611
+ line_idx = instance["line_idx"]
612
+ expr = instance["expr"]
613
+ lines[line_idx] = f"{indent}{var_name}_parts.append({expr})"
614
+
615
+ def _add_join_after_loop(
616
+ self,
617
+ lines: list[str],
618
+ var_name: str,
619
+ indent: str,
620
+ loop_start: int,
621
+ ) -> None:
622
+ """Add join operation after the loop ends."""
623
+ loop_end = self._find_loop_end(lines, loop_start)
624
+ if loop_end is not None:
625
+ join_line = f"{indent}{var_name} = ''.join({var_name}_parts) # Performance: Join string parts"
626
+ lines.insert(loop_end + 1, join_line)
627
+
628
+ def _find_loop_end(self, lines: list[str], loop_start: int) -> int | None:
629
+ """Find the end of the loop (last line with same or greater indentation)."""
630
+ if loop_start >= len(lines):
631
+ return None
632
+
633
+ loop_indent = len(lines[loop_start]) - len(lines[loop_start].lstrip())
634
+
635
+ for i in range(loop_start + 1, len(lines)):
636
+ line = lines[i]
637
+ if line.strip() == "": # Skip empty lines
638
+ continue
639
+
640
+ current_indent = len(line) - len(line.lstrip())
641
+ if current_indent <= loop_indent:
642
+ return i - 1
643
+
644
+ return len(lines) - 1
645
+
646
+ def _fix_repeated_operations(
647
+ self,
648
+ lines: list[str],
649
+ issue: dict[str, t.Any],
650
+ ) -> tuple[list[str], bool]:
651
+ """Add comments suggesting caching for repeated expensive operations."""
652
+ modified = False
653
+
654
+ for instance in issue["instances"]:
655
+ line_idx = instance["line_number"] - 1
656
+ if line_idx < len(lines):
657
+ original_line = t.cast(str, lines[line_idx])
658
+ indent_level = len(original_line) - len(original_line.lstrip())
659
+ indent_str = " " * indent_level
660
+
661
+ # Add performance comment
662
+ comment = f"{indent_str}# Performance: Consider caching this expensive operation outside the loop"
663
+ lines.insert(line_idx, comment)
664
+ modified = True
665
+
666
+ return lines, modified
667
+
668
+
669
+ agent_registry.register(PerformanceAgent)