crackerjack 0.30.3__py3-none-any.whl → 0.31.4__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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.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)
|