codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""Pattern-based quick fixes for common errors.
|
|
2
|
+
|
|
3
|
+
Fixes common errors without requiring LLM calls:
|
|
4
|
+
- ModuleNotFoundError → install package
|
|
5
|
+
- ImportError → add import statement
|
|
6
|
+
- NameError → add missing import
|
|
7
|
+
- SyntaxError → common syntax fixes
|
|
8
|
+
- IndentationError → fix indentation
|
|
9
|
+
|
|
10
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional, Callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FixType(str, Enum):
|
|
21
|
+
"""Type of fix to apply."""
|
|
22
|
+
|
|
23
|
+
INSTALL_PACKAGE = "install_package"
|
|
24
|
+
ADD_IMPORT = "add_import"
|
|
25
|
+
FIX_SYNTAX = "fix_syntax"
|
|
26
|
+
FIX_INDENTATION = "fix_indentation"
|
|
27
|
+
ADD_MISSING_FILE = "add_missing_file"
|
|
28
|
+
FIX_TYPE_HINT = "fix_type_hint"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class QuickFix:
|
|
33
|
+
"""A quick fix that can be applied without LLM.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
fix_type: Category of fix
|
|
37
|
+
description: Human-readable description
|
|
38
|
+
command: Shell command to run (for INSTALL_PACKAGE)
|
|
39
|
+
file_path: File to modify (for code fixes)
|
|
40
|
+
old_content: Content to find/replace
|
|
41
|
+
new_content: Replacement content
|
|
42
|
+
insert_line: Line number to insert at (1-based)
|
|
43
|
+
insert_content: Content to insert
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
fix_type: FixType
|
|
47
|
+
description: str
|
|
48
|
+
command: Optional[str] = None
|
|
49
|
+
file_path: Optional[str] = None
|
|
50
|
+
old_content: Optional[str] = None
|
|
51
|
+
new_content: Optional[str] = None
|
|
52
|
+
insert_line: Optional[int] = None
|
|
53
|
+
insert_content: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Mapping from common import names to package names
|
|
57
|
+
# (when they differ, e.g., import PIL → pip install Pillow)
|
|
58
|
+
PACKAGE_ALIASES = {
|
|
59
|
+
"PIL": "Pillow",
|
|
60
|
+
"cv2": "opencv-python",
|
|
61
|
+
"sklearn": "scikit-learn",
|
|
62
|
+
"yaml": "pyyaml",
|
|
63
|
+
"bs4": "beautifulsoup4",
|
|
64
|
+
"dateutil": "python-dateutil",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Common standard library modules (shouldn't be installed)
|
|
68
|
+
STDLIB_MODULES = {
|
|
69
|
+
"os", "sys", "re", "json", "datetime", "time", "math", "random",
|
|
70
|
+
"collections", "itertools", "functools", "typing", "pathlib",
|
|
71
|
+
"subprocess", "threading", "multiprocessing", "asyncio", "contextlib",
|
|
72
|
+
"dataclasses", "enum", "abc", "copy", "io", "tempfile", "shutil",
|
|
73
|
+
"logging", "unittest", "argparse", "configparser", "hashlib", "hmac",
|
|
74
|
+
"base64", "struct", "pickle", "sqlite3", "csv", "xml", "html",
|
|
75
|
+
"http", "urllib", "email", "mimetypes", "socket", "ssl", "select",
|
|
76
|
+
"signal", "platform", "getpass", "glob", "fnmatch", "textwrap",
|
|
77
|
+
"string", "decimal", "fractions", "statistics", "secrets", "uuid",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def detect_package_manager(repo_path: Path) -> str:
|
|
82
|
+
"""Detect the package manager to use.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
repo_path: Path to the repository
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Package manager command (uv, pip, npm, etc.)
|
|
89
|
+
"""
|
|
90
|
+
# Check for Poetry first (poetry.lock is definitive)
|
|
91
|
+
if (repo_path / "poetry.lock").exists():
|
|
92
|
+
return "poetry add"
|
|
93
|
+
|
|
94
|
+
# Check for uv lock file (definitive for uv)
|
|
95
|
+
if (repo_path / "uv.lock").exists():
|
|
96
|
+
return "uv pip install"
|
|
97
|
+
|
|
98
|
+
# Check pyproject.toml for tool-specific markers
|
|
99
|
+
pyproject = repo_path / "pyproject.toml"
|
|
100
|
+
if pyproject.exists():
|
|
101
|
+
content = pyproject.read_text()
|
|
102
|
+
# Check for uv-specific configuration
|
|
103
|
+
if "[tool.uv]" in content:
|
|
104
|
+
return "uv pip install"
|
|
105
|
+
# Check for Poetry-specific configuration
|
|
106
|
+
if "[tool.poetry]" in content:
|
|
107
|
+
return "poetry add"
|
|
108
|
+
# Generic pyproject.toml - default to uv
|
|
109
|
+
return "uv pip install"
|
|
110
|
+
|
|
111
|
+
if (repo_path / "requirements.txt").exists():
|
|
112
|
+
return "pip install"
|
|
113
|
+
|
|
114
|
+
if (repo_path / "Pipfile").exists():
|
|
115
|
+
return "pipenv install"
|
|
116
|
+
|
|
117
|
+
# Check for Node.js package managers
|
|
118
|
+
if (repo_path / "package-lock.json").exists():
|
|
119
|
+
return "npm install"
|
|
120
|
+
|
|
121
|
+
if (repo_path / "yarn.lock").exists():
|
|
122
|
+
return "yarn add"
|
|
123
|
+
|
|
124
|
+
if (repo_path / "pnpm-lock.yaml").exists():
|
|
125
|
+
return "pnpm add"
|
|
126
|
+
|
|
127
|
+
# Default to pip
|
|
128
|
+
return "pip install"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def match_module_not_found(error: str) -> Optional[QuickFix]:
|
|
132
|
+
"""Match ModuleNotFoundError and generate install fix.
|
|
133
|
+
|
|
134
|
+
Patterns:
|
|
135
|
+
- ModuleNotFoundError: No module named 'X'
|
|
136
|
+
- ImportError: No module named 'X'
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
error: Error message
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
QuickFix if matched, None otherwise
|
|
143
|
+
"""
|
|
144
|
+
patterns = [
|
|
145
|
+
r"ModuleNotFoundError: No module named ['\"]([^'\"]+)['\"]",
|
|
146
|
+
r"ImportError: No module named ['\"]([^'\"]+)['\"]",
|
|
147
|
+
r"No module named ['\"]([^'\"]+)['\"]",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
for pattern in patterns:
|
|
151
|
+
match = re.search(pattern, error, re.IGNORECASE)
|
|
152
|
+
if match:
|
|
153
|
+
module = match.group(1).split('.')[0] # Get top-level module
|
|
154
|
+
|
|
155
|
+
# Skip standard library modules
|
|
156
|
+
if module in STDLIB_MODULES:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# Get actual package name
|
|
160
|
+
package = PACKAGE_ALIASES.get(module, module)
|
|
161
|
+
|
|
162
|
+
return QuickFix(
|
|
163
|
+
fix_type=FixType.INSTALL_PACKAGE,
|
|
164
|
+
description=f"Install missing package: {package}",
|
|
165
|
+
command=f"{{package_manager}} {package}",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def match_import_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
|
|
172
|
+
"""Match ImportError for specific name and suggest import.
|
|
173
|
+
|
|
174
|
+
Patterns:
|
|
175
|
+
- ImportError: cannot import name 'X' from 'Y'
|
|
176
|
+
- cannot import name 'X' from 'Y'
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
error: Error message
|
|
180
|
+
file_content: Content of the file with the error
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
QuickFix if matched, None otherwise
|
|
184
|
+
"""
|
|
185
|
+
pattern = r"cannot import name ['\"]([^'\"]+)['\"] from ['\"]([^'\"]+)['\"]"
|
|
186
|
+
match = re.search(pattern, error, re.IGNORECASE)
|
|
187
|
+
|
|
188
|
+
if match:
|
|
189
|
+
name = match.group(1)
|
|
190
|
+
module = match.group(2)
|
|
191
|
+
|
|
192
|
+
return QuickFix(
|
|
193
|
+
fix_type=FixType.ADD_IMPORT,
|
|
194
|
+
description=f"Fix import: from {module} import {name}",
|
|
195
|
+
insert_line=1, # Insert at top of file
|
|
196
|
+
insert_content=f"from {module} import {name}\n",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def match_name_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
|
|
203
|
+
"""Match NameError and suggest import if it's a known module/type.
|
|
204
|
+
|
|
205
|
+
Patterns:
|
|
206
|
+
- NameError: name 'X' is not defined
|
|
207
|
+
- name 'X' is not defined
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
error: Error message
|
|
211
|
+
file_content: Content of the file with the error
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
QuickFix if matched, None otherwise
|
|
215
|
+
"""
|
|
216
|
+
pattern = r"(?:NameError: )?name ['\"]([^'\"]+)['\"] is not defined"
|
|
217
|
+
match = re.search(pattern, error, re.IGNORECASE)
|
|
218
|
+
|
|
219
|
+
if match:
|
|
220
|
+
name = match.group(1)
|
|
221
|
+
|
|
222
|
+
# Common imports that cause NameError
|
|
223
|
+
common_imports = {
|
|
224
|
+
# Typing
|
|
225
|
+
"Optional": "from typing import Optional",
|
|
226
|
+
"List": "from typing import List",
|
|
227
|
+
"Dict": "from typing import Dict",
|
|
228
|
+
"Any": "from typing import Any",
|
|
229
|
+
"Union": "from typing import Union",
|
|
230
|
+
"Callable": "from typing import Callable",
|
|
231
|
+
"TypeVar": "from typing import TypeVar",
|
|
232
|
+
# Dataclasses
|
|
233
|
+
"dataclass": "from dataclasses import dataclass",
|
|
234
|
+
"field": "from dataclasses import field",
|
|
235
|
+
# Enum
|
|
236
|
+
"Enum": "from enum import Enum",
|
|
237
|
+
# Path
|
|
238
|
+
"Path": "from pathlib import Path",
|
|
239
|
+
# Datetime
|
|
240
|
+
"datetime": "from datetime import datetime",
|
|
241
|
+
"timedelta": "from datetime import timedelta",
|
|
242
|
+
"timezone": "from datetime import timezone",
|
|
243
|
+
# JSON
|
|
244
|
+
"json": "import json",
|
|
245
|
+
# Re
|
|
246
|
+
"re": "import re",
|
|
247
|
+
# OS
|
|
248
|
+
"os": "import os",
|
|
249
|
+
# Sys
|
|
250
|
+
"sys": "import sys",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if name in common_imports:
|
|
254
|
+
return QuickFix(
|
|
255
|
+
fix_type=FixType.ADD_IMPORT,
|
|
256
|
+
description=f"Add missing import for {name}",
|
|
257
|
+
insert_line=1,
|
|
258
|
+
insert_content=common_imports[name] + "\n",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def match_syntax_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
|
|
265
|
+
"""Match common SyntaxError patterns and suggest fixes.
|
|
266
|
+
|
|
267
|
+
Patterns handled:
|
|
268
|
+
- Missing colon after def/class/if/for/while
|
|
269
|
+
- Unclosed brackets/parentheses
|
|
270
|
+
- Invalid syntax near common patterns
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
error: Error message
|
|
274
|
+
file_content: Content of the file with the error
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
QuickFix if matched, None otherwise
|
|
278
|
+
"""
|
|
279
|
+
# Extract line number if present
|
|
280
|
+
line_match = re.search(r'line (\d+)', error, re.IGNORECASE)
|
|
281
|
+
line_num = int(line_match.group(1)) if line_match else None
|
|
282
|
+
|
|
283
|
+
# Missing colon patterns
|
|
284
|
+
if "expected ':'" in error.lower() or "SyntaxError: invalid syntax" in error:
|
|
285
|
+
if file_content and line_num:
|
|
286
|
+
lines = file_content.split('\n')
|
|
287
|
+
if 0 < line_num <= len(lines):
|
|
288
|
+
line = lines[line_num - 1]
|
|
289
|
+
# Check if it's a def/class/if/for/while without colon
|
|
290
|
+
if re.match(r'^\s*(def|class|if|elif|else|for|while|try|except|finally|with)\s+.+[^:]\s*$', line):
|
|
291
|
+
return QuickFix(
|
|
292
|
+
fix_type=FixType.FIX_SYNTAX,
|
|
293
|
+
description=f"Add missing colon at line {line_num}",
|
|
294
|
+
old_content=line,
|
|
295
|
+
new_content=line.rstrip() + ":",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# f-string without f prefix
|
|
299
|
+
if "unterminated string literal" in error.lower() or "invalid syntax" in error.lower():
|
|
300
|
+
if file_content and line_num:
|
|
301
|
+
lines = file_content.split('\n')
|
|
302
|
+
if 0 < line_num <= len(lines):
|
|
303
|
+
line = lines[line_num - 1]
|
|
304
|
+
# Pattern to match string literals with optional prefix
|
|
305
|
+
# Captures: (1) prefix like r/u/b/br/fr, (2) opening quote, (3) body, (4) closing quote
|
|
306
|
+
string_pattern = r'([rRuUbBfF]*)(["\'])([^"\']*\{[^}]+\}[^"\']*)\2'
|
|
307
|
+
match = re.search(string_pattern, line)
|
|
308
|
+
if match:
|
|
309
|
+
prefix = match.group(1)
|
|
310
|
+
quote = match.group(2)
|
|
311
|
+
body = match.group(3)
|
|
312
|
+
|
|
313
|
+
# Skip if already has 'f' prefix
|
|
314
|
+
if 'f' in prefix.lower():
|
|
315
|
+
pass # Already an f-string
|
|
316
|
+
# Skip byte strings - can't add 'f' to 'b'
|
|
317
|
+
elif 'b' in prefix.lower():
|
|
318
|
+
pass # Byte string, can't be f-string
|
|
319
|
+
# Skip unicode strings - 'u' prefix is for Python 2 compatibility
|
|
320
|
+
elif 'u' in prefix.lower():
|
|
321
|
+
pass # Unicode string, don't add 'f' prefix
|
|
322
|
+
else:
|
|
323
|
+
# Add 'f' to prefix (before 'r' if present, e.g., 'r' -> 'rf')
|
|
324
|
+
if 'r' in prefix.lower():
|
|
325
|
+
# Keep the case of r, add f before it
|
|
326
|
+
new_prefix = 'f' + prefix
|
|
327
|
+
else:
|
|
328
|
+
new_prefix = 'f' + prefix
|
|
329
|
+
# Build the new string literal
|
|
330
|
+
old_literal = f"{prefix}{quote}{body}{quote}"
|
|
331
|
+
new_literal = f"{new_prefix}{quote}{body}{quote}"
|
|
332
|
+
new_line = line.replace(old_literal, new_literal, 1)
|
|
333
|
+
if new_line != line:
|
|
334
|
+
return QuickFix(
|
|
335
|
+
fix_type=FixType.FIX_SYNTAX,
|
|
336
|
+
description=f"Add f-string prefix at line {line_num}",
|
|
337
|
+
old_content=line,
|
|
338
|
+
new_content=new_line,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def match_indentation_error(error: str, file_content: Optional[str] = None) -> Optional[QuickFix]:
|
|
345
|
+
"""Match IndentationError and suggest fix.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
error: Error message
|
|
349
|
+
file_content: Content of the file with the error
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
QuickFix if matched, None otherwise
|
|
353
|
+
"""
|
|
354
|
+
if "IndentationError" not in error and "indentation" not in error.lower():
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
line_match = re.search(r'line (\d+)', error, re.IGNORECASE)
|
|
358
|
+
if not line_match or not file_content:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
line_num = int(line_match.group(1))
|
|
362
|
+
lines = file_content.split('\n')
|
|
363
|
+
|
|
364
|
+
if not (0 < line_num <= len(lines)):
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
current_line = lines[line_num - 1]
|
|
368
|
+
|
|
369
|
+
# Detect mixed tabs/spaces
|
|
370
|
+
if '\t' in current_line and ' ' in current_line[:len(current_line) - len(current_line.lstrip())]:
|
|
371
|
+
# Convert to consistent spaces
|
|
372
|
+
leading = current_line[:len(current_line) - len(current_line.lstrip())]
|
|
373
|
+
# Replace tabs with 4 spaces
|
|
374
|
+
new_leading = leading.replace('\t', ' ')
|
|
375
|
+
new_line = new_leading + current_line.lstrip()
|
|
376
|
+
|
|
377
|
+
return QuickFix(
|
|
378
|
+
fix_type=FixType.FIX_INDENTATION,
|
|
379
|
+
description=f"Fix mixed indentation at line {line_num}",
|
|
380
|
+
old_content=current_line,
|
|
381
|
+
new_content=new_line,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Unexpected indent - try to match previous line's indentation
|
|
385
|
+
if "unexpected indent" in error.lower() and line_num > 1:
|
|
386
|
+
prev_line = lines[line_num - 2]
|
|
387
|
+
prev_indent = len(prev_line) - len(prev_line.lstrip())
|
|
388
|
+
|
|
389
|
+
# If previous line ends with colon, add 4 spaces
|
|
390
|
+
if prev_line.rstrip().endswith(':'):
|
|
391
|
+
expected_indent = prev_indent + 4
|
|
392
|
+
else:
|
|
393
|
+
expected_indent = prev_indent
|
|
394
|
+
|
|
395
|
+
new_line = ' ' * expected_indent + current_line.lstrip()
|
|
396
|
+
|
|
397
|
+
return QuickFix(
|
|
398
|
+
fix_type=FixType.FIX_INDENTATION,
|
|
399
|
+
description=f"Fix unexpected indentation at line {line_num}",
|
|
400
|
+
old_content=current_line,
|
|
401
|
+
new_content=new_line,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def match_type_error(error: str) -> Optional[QuickFix]:
|
|
408
|
+
"""Match TypeError for missing type hints.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
error: Error message
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
QuickFix if matched, None otherwise
|
|
415
|
+
"""
|
|
416
|
+
# This is more complex and usually needs LLM help
|
|
417
|
+
# But we can catch some simple cases
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# All pattern matchers in order of precedence
|
|
422
|
+
PATTERN_MATCHERS: list[Callable] = [
|
|
423
|
+
match_module_not_found,
|
|
424
|
+
match_import_error,
|
|
425
|
+
match_name_error,
|
|
426
|
+
match_syntax_error,
|
|
427
|
+
match_indentation_error,
|
|
428
|
+
match_type_error,
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def find_quick_fix(
|
|
433
|
+
error: str,
|
|
434
|
+
file_path: Optional[Path] = None,
|
|
435
|
+
repo_path: Optional[Path] = None,
|
|
436
|
+
) -> Optional[QuickFix]:
|
|
437
|
+
"""Find a quick fix for an error without LLM.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
error: Error message
|
|
441
|
+
file_path: Path to the file with the error (if known)
|
|
442
|
+
repo_path: Path to the repository root
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
QuickFix if a pattern match is found, None otherwise
|
|
446
|
+
"""
|
|
447
|
+
# Read file content if we have a path
|
|
448
|
+
file_content = None
|
|
449
|
+
if file_path and file_path.exists():
|
|
450
|
+
try:
|
|
451
|
+
file_content = file_path.read_text()
|
|
452
|
+
except Exception:
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
# Try each pattern matcher
|
|
456
|
+
for matcher in PATTERN_MATCHERS:
|
|
457
|
+
try:
|
|
458
|
+
# Check if matcher accepts file_content
|
|
459
|
+
import inspect
|
|
460
|
+
sig = inspect.signature(matcher)
|
|
461
|
+
if 'file_content' in sig.parameters:
|
|
462
|
+
fix = matcher(error, file_content)
|
|
463
|
+
else:
|
|
464
|
+
fix = matcher(error)
|
|
465
|
+
|
|
466
|
+
if fix:
|
|
467
|
+
# Fill in package manager if needed
|
|
468
|
+
if fix.command and "{package_manager}" in fix.command and repo_path:
|
|
469
|
+
pm = detect_package_manager(repo_path)
|
|
470
|
+
fix.command = fix.command.replace("{package_manager}", pm)
|
|
471
|
+
|
|
472
|
+
# Set file path if we have it
|
|
473
|
+
if file_path and not fix.file_path:
|
|
474
|
+
fix.file_path = str(file_path)
|
|
475
|
+
|
|
476
|
+
return fix
|
|
477
|
+
|
|
478
|
+
except Exception:
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def apply_quick_fix(
|
|
485
|
+
fix: QuickFix,
|
|
486
|
+
repo_path: Path,
|
|
487
|
+
dry_run: bool = False,
|
|
488
|
+
) -> tuple[bool, str]:
|
|
489
|
+
"""Apply a quick fix.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
fix: The fix to apply
|
|
493
|
+
repo_path: Repository root path
|
|
494
|
+
dry_run: If True, don't make changes
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Tuple of (success, message)
|
|
498
|
+
"""
|
|
499
|
+
try:
|
|
500
|
+
if fix.fix_type == FixType.INSTALL_PACKAGE:
|
|
501
|
+
if not fix.command:
|
|
502
|
+
return False, "No install command specified"
|
|
503
|
+
|
|
504
|
+
if dry_run:
|
|
505
|
+
return True, f"Would run: {fix.command}"
|
|
506
|
+
|
|
507
|
+
import subprocess
|
|
508
|
+
result = subprocess.run(
|
|
509
|
+
fix.command.split(),
|
|
510
|
+
cwd=repo_path,
|
|
511
|
+
capture_output=True,
|
|
512
|
+
text=True,
|
|
513
|
+
timeout=120,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if result.returncode == 0:
|
|
517
|
+
return True, f"Installed package: {fix.command}"
|
|
518
|
+
else:
|
|
519
|
+
return False, f"Install failed: {result.stderr}"
|
|
520
|
+
|
|
521
|
+
elif fix.fix_type in {FixType.ADD_IMPORT, FixType.FIX_SYNTAX, FixType.FIX_INDENTATION}:
|
|
522
|
+
if not fix.file_path:
|
|
523
|
+
return False, "No file path specified"
|
|
524
|
+
|
|
525
|
+
file_path = repo_path / fix.file_path
|
|
526
|
+
if not file_path.exists():
|
|
527
|
+
return False, f"File not found: {fix.file_path}"
|
|
528
|
+
|
|
529
|
+
content = file_path.read_text()
|
|
530
|
+
|
|
531
|
+
if fix.old_content and fix.new_content:
|
|
532
|
+
# Replace content
|
|
533
|
+
if fix.old_content not in content:
|
|
534
|
+
return False, f"Content to replace not found in {fix.file_path}"
|
|
535
|
+
|
|
536
|
+
if dry_run:
|
|
537
|
+
return True, f"Would replace in {fix.file_path}"
|
|
538
|
+
|
|
539
|
+
new_content = content.replace(fix.old_content, fix.new_content, 1)
|
|
540
|
+
file_path.write_text(new_content)
|
|
541
|
+
return True, f"Fixed: {fix.description}"
|
|
542
|
+
|
|
543
|
+
elif fix.insert_line and fix.insert_content:
|
|
544
|
+
# Insert content at line
|
|
545
|
+
lines = content.split('\n')
|
|
546
|
+
insert_idx = max(0, fix.insert_line - 1)
|
|
547
|
+
|
|
548
|
+
if dry_run:
|
|
549
|
+
return True, f"Would insert at line {fix.insert_line} in {fix.file_path}"
|
|
550
|
+
|
|
551
|
+
lines.insert(insert_idx, fix.insert_content.rstrip('\n'))
|
|
552
|
+
file_path.write_text('\n'.join(lines))
|
|
553
|
+
return True, f"Inserted: {fix.description}"
|
|
554
|
+
|
|
555
|
+
return False, "Unknown fix type or missing parameters"
|
|
556
|
+
|
|
557
|
+
except Exception as e:
|
|
558
|
+
return False, f"Error applying fix: {e}"
|