codegraph-cli 2.0.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.
- codegraph_cli/__init__.py +4 -0
- codegraph_cli/agents.py +191 -0
- codegraph_cli/bug_detector.py +386 -0
- codegraph_cli/chat_agent.py +352 -0
- codegraph_cli/chat_session.py +220 -0
- codegraph_cli/cli.py +330 -0
- codegraph_cli/cli_chat.py +367 -0
- codegraph_cli/cli_diagnose.py +133 -0
- codegraph_cli/cli_refactor.py +230 -0
- codegraph_cli/cli_setup.py +470 -0
- codegraph_cli/cli_test.py +177 -0
- codegraph_cli/cli_v2.py +267 -0
- codegraph_cli/codegen_agent.py +265 -0
- codegraph_cli/config.py +31 -0
- codegraph_cli/config_manager.py +341 -0
- codegraph_cli/context_manager.py +500 -0
- codegraph_cli/crew_agents.py +123 -0
- codegraph_cli/crew_chat.py +159 -0
- codegraph_cli/crew_tools.py +497 -0
- codegraph_cli/diff_engine.py +265 -0
- codegraph_cli/embeddings.py +241 -0
- codegraph_cli/graph_export.py +144 -0
- codegraph_cli/llm.py +642 -0
- codegraph_cli/models.py +47 -0
- codegraph_cli/models_v2.py +185 -0
- codegraph_cli/orchestrator.py +49 -0
- codegraph_cli/parser.py +800 -0
- codegraph_cli/performance_analyzer.py +223 -0
- codegraph_cli/project_context.py +230 -0
- codegraph_cli/rag.py +200 -0
- codegraph_cli/refactor_agent.py +452 -0
- codegraph_cli/security_scanner.py +366 -0
- codegraph_cli/storage.py +390 -0
- codegraph_cli/templates/graph_interactive.html +257 -0
- codegraph_cli/testgen_agent.py +316 -0
- codegraph_cli/validation_engine.py +285 -0
- codegraph_cli/vector_store.py +293 -0
- codegraph_cli-2.0.0.dist-info/METADATA +318 -0
- codegraph_cli-2.0.0.dist-info/RECORD +43 -0
- codegraph_cli-2.0.0.dist-info/WHEEL +5 -0
- codegraph_cli-2.0.0.dist-info/entry_points.txt +2 -0
- codegraph_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_cli-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""TestGenAgent for graph-powered test generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from .llm import LocalLLM
|
|
10
|
+
from .models_v2 import TestCase
|
|
11
|
+
from .storage import GraphStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestGenAgent:
|
|
15
|
+
"""Generates tests based on call graph analysis and usage patterns."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, store: GraphStore, llm: Optional[LocalLLM] = None):
|
|
18
|
+
"""Initialize TestGenAgent.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
store: Graph store for analyzing code
|
|
22
|
+
llm: Optional LLM for generating test code
|
|
23
|
+
"""
|
|
24
|
+
self.store = store
|
|
25
|
+
self.llm = llm
|
|
26
|
+
|
|
27
|
+
def generate_unit_tests(self, symbol: str) -> List[TestCase]:
|
|
28
|
+
"""Generate unit tests for a function.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
symbol: Function name to generate tests for
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of generated test cases
|
|
35
|
+
"""
|
|
36
|
+
# Get function info from graph
|
|
37
|
+
node = self.store.get_node(symbol)
|
|
38
|
+
if not node:
|
|
39
|
+
raise ValueError(f"Symbol '{symbol}' not found")
|
|
40
|
+
|
|
41
|
+
# Analyze function signature and dependencies
|
|
42
|
+
test_cases = []
|
|
43
|
+
|
|
44
|
+
# Parse function to understand parameters
|
|
45
|
+
try:
|
|
46
|
+
tree = ast.parse(node["code"])
|
|
47
|
+
func_def = None
|
|
48
|
+
for node_ast in ast.walk(tree):
|
|
49
|
+
if isinstance(node_ast, ast.FunctionDef):
|
|
50
|
+
func_def = node_ast
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
if func_def:
|
|
54
|
+
# Generate tests based on function analysis
|
|
55
|
+
test_cases.extend(self._generate_basic_tests(symbol, func_def, node))
|
|
56
|
+
test_cases.extend(self._generate_edge_case_tests(symbol, func_def, node))
|
|
57
|
+
test_cases.extend(self._generate_error_tests(symbol, func_def, node))
|
|
58
|
+
|
|
59
|
+
except Exception:
|
|
60
|
+
# Fallback: generate basic test template
|
|
61
|
+
test_cases.append(self._generate_basic_test_template(symbol, node))
|
|
62
|
+
|
|
63
|
+
return test_cases
|
|
64
|
+
|
|
65
|
+
def generate_integration_tests(self, flow_description: str) -> List[TestCase]:
|
|
66
|
+
"""Generate integration tests for a user flow.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
flow_description: Description of the flow to test
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of integration test cases
|
|
73
|
+
"""
|
|
74
|
+
# Use LLM to generate integration test if available
|
|
75
|
+
if self.llm:
|
|
76
|
+
prompt = self._build_integration_test_prompt(flow_description)
|
|
77
|
+
test_code = self.llm.explain(prompt)
|
|
78
|
+
|
|
79
|
+
return [TestCase(
|
|
80
|
+
name=f"test_{flow_description.lower().replace(' ', '_')}",
|
|
81
|
+
target_function=flow_description,
|
|
82
|
+
test_code=test_code,
|
|
83
|
+
description=f"Integration test for {flow_description}",
|
|
84
|
+
test_type="integration"
|
|
85
|
+
)]
|
|
86
|
+
|
|
87
|
+
# Fallback: basic template
|
|
88
|
+
return [self._generate_integration_template(flow_description)]
|
|
89
|
+
|
|
90
|
+
def analyze_coverage_impact(self, tests: List[TestCase]) -> dict:
|
|
91
|
+
"""Predict coverage improvement from tests.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
tests: List of test cases
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Coverage analysis dictionary
|
|
98
|
+
"""
|
|
99
|
+
# Simple heuristic: estimate based on number of tests
|
|
100
|
+
# In production, would use actual coverage tools
|
|
101
|
+
|
|
102
|
+
total_functions = len(self.store.get_nodes())
|
|
103
|
+
tested_functions = len(set(t.target_function for t in tests))
|
|
104
|
+
|
|
105
|
+
current_coverage = 0.0 # Would get from coverage tool
|
|
106
|
+
estimated_new_coverage = min(100.0, current_coverage + (tested_functions / max(total_functions, 1)) * 100)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"current_coverage": current_coverage,
|
|
110
|
+
"estimated_coverage": estimated_new_coverage,
|
|
111
|
+
"coverage_increase": estimated_new_coverage - current_coverage,
|
|
112
|
+
"tests_generated": len(tests),
|
|
113
|
+
"functions_covered": tested_functions
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def _generate_basic_tests(
|
|
117
|
+
self,
|
|
118
|
+
symbol: str,
|
|
119
|
+
func_def: ast.FunctionDef,
|
|
120
|
+
node: dict
|
|
121
|
+
) -> List[TestCase]:
|
|
122
|
+
"""Generate basic happy-path tests."""
|
|
123
|
+
tests = []
|
|
124
|
+
|
|
125
|
+
# Get parameter names
|
|
126
|
+
params = [arg.arg for arg in func_def.args.args if arg.arg != 'self']
|
|
127
|
+
|
|
128
|
+
# Generate basic test
|
|
129
|
+
test_name = f"test_{symbol}_basic"
|
|
130
|
+
test_code = self._generate_test_code(
|
|
131
|
+
test_name,
|
|
132
|
+
symbol,
|
|
133
|
+
params,
|
|
134
|
+
"Basic functionality test"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
tests.append(TestCase(
|
|
138
|
+
name=test_name,
|
|
139
|
+
target_function=symbol,
|
|
140
|
+
test_code=test_code,
|
|
141
|
+
description=f"Test basic functionality of {symbol}",
|
|
142
|
+
test_type="unit"
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
return tests
|
|
146
|
+
|
|
147
|
+
def _generate_edge_case_tests(
|
|
148
|
+
self,
|
|
149
|
+
symbol: str,
|
|
150
|
+
func_def: ast.FunctionDef,
|
|
151
|
+
node: dict
|
|
152
|
+
) -> List[TestCase]:
|
|
153
|
+
"""Generate edge case tests."""
|
|
154
|
+
tests = []
|
|
155
|
+
|
|
156
|
+
params = [arg.arg for arg in func_def.args.args if arg.arg != 'self']
|
|
157
|
+
|
|
158
|
+
# Test with empty/None values
|
|
159
|
+
if params:
|
|
160
|
+
test_name = f"test_{symbol}_empty_input"
|
|
161
|
+
test_code = self._generate_test_code(
|
|
162
|
+
test_name,
|
|
163
|
+
symbol,
|
|
164
|
+
params,
|
|
165
|
+
"Empty/None input test",
|
|
166
|
+
use_empty=True
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
tests.append(TestCase(
|
|
170
|
+
name=test_name,
|
|
171
|
+
target_function=symbol,
|
|
172
|
+
test_code=test_code,
|
|
173
|
+
description=f"Test {symbol} with empty/None inputs",
|
|
174
|
+
test_type="unit"
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
return tests
|
|
178
|
+
|
|
179
|
+
def _generate_error_tests(
|
|
180
|
+
self,
|
|
181
|
+
symbol: str,
|
|
182
|
+
func_def: ast.FunctionDef,
|
|
183
|
+
node: dict
|
|
184
|
+
) -> List[TestCase]:
|
|
185
|
+
"""Generate error handling tests."""
|
|
186
|
+
tests = []
|
|
187
|
+
|
|
188
|
+
# Check if function has error handling
|
|
189
|
+
has_try_except = False
|
|
190
|
+
for node_ast in ast.walk(func_def):
|
|
191
|
+
if isinstance(node_ast, ast.Try):
|
|
192
|
+
has_try_except = True
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if has_try_except:
|
|
196
|
+
test_name = f"test_{symbol}_error_handling"
|
|
197
|
+
params = [arg.arg for arg in func_def.args.args if arg.arg != 'self']
|
|
198
|
+
|
|
199
|
+
test_code = self._generate_test_code(
|
|
200
|
+
test_name,
|
|
201
|
+
symbol,
|
|
202
|
+
params,
|
|
203
|
+
"Error handling test",
|
|
204
|
+
test_error=True
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
tests.append(TestCase(
|
|
208
|
+
name=test_name,
|
|
209
|
+
target_function=symbol,
|
|
210
|
+
test_code=test_code,
|
|
211
|
+
description=f"Test error handling in {symbol}",
|
|
212
|
+
test_type="unit"
|
|
213
|
+
))
|
|
214
|
+
|
|
215
|
+
return tests
|
|
216
|
+
|
|
217
|
+
def _generate_test_code(
|
|
218
|
+
self,
|
|
219
|
+
test_name: str,
|
|
220
|
+
function_name: str,
|
|
221
|
+
params: List[str],
|
|
222
|
+
description: str,
|
|
223
|
+
use_empty: bool = False,
|
|
224
|
+
test_error: bool = False
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Generate actual test code."""
|
|
227
|
+
lines = []
|
|
228
|
+
|
|
229
|
+
lines.append(f"def {test_name}():")
|
|
230
|
+
lines.append(f' """{description}."""')
|
|
231
|
+
|
|
232
|
+
# Generate test inputs
|
|
233
|
+
if use_empty:
|
|
234
|
+
args = ", ".join(["None" for _ in params])
|
|
235
|
+
else:
|
|
236
|
+
# Generate reasonable test values
|
|
237
|
+
args = ", ".join([self._generate_test_value(p) for p in params])
|
|
238
|
+
|
|
239
|
+
if test_error:
|
|
240
|
+
lines.append(" with pytest.raises(Exception):")
|
|
241
|
+
lines.append(f" result = {function_name}({args})")
|
|
242
|
+
else:
|
|
243
|
+
lines.append(f" result = {function_name}({args})")
|
|
244
|
+
lines.append(" assert result is not None")
|
|
245
|
+
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
def _generate_test_value(self, param_name: str) -> str:
|
|
249
|
+
"""Generate a test value based on parameter name."""
|
|
250
|
+
param_lower = param_name.lower()
|
|
251
|
+
|
|
252
|
+
if 'id' in param_lower:
|
|
253
|
+
return "1"
|
|
254
|
+
elif 'name' in param_lower or 'username' in param_lower:
|
|
255
|
+
return '"test_user"'
|
|
256
|
+
elif 'email' in param_lower:
|
|
257
|
+
return '"test@example.com"'
|
|
258
|
+
elif 'password' in param_lower:
|
|
259
|
+
return '"password123"'
|
|
260
|
+
elif 'count' in param_lower or 'num' in param_lower:
|
|
261
|
+
return "10"
|
|
262
|
+
elif 'data' in param_lower:
|
|
263
|
+
return '{"key": "value"}'
|
|
264
|
+
else:
|
|
265
|
+
return '"test_value"'
|
|
266
|
+
|
|
267
|
+
def _generate_basic_test_template(self, symbol: str, node: dict) -> TestCase:
|
|
268
|
+
"""Generate a basic test template."""
|
|
269
|
+
test_code = f"""def test_{symbol}():
|
|
270
|
+
\"\"\"Test {symbol} function.\"\"\"
|
|
271
|
+
# TODO: Add test implementation
|
|
272
|
+
result = {symbol}()
|
|
273
|
+
assert result is not None
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
return TestCase(
|
|
277
|
+
name=f"test_{symbol}",
|
|
278
|
+
target_function=symbol,
|
|
279
|
+
test_code=test_code,
|
|
280
|
+
description=f"Basic test for {symbol}",
|
|
281
|
+
test_type="unit"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _generate_integration_template(self, flow: str) -> TestCase:
|
|
285
|
+
"""Generate integration test template."""
|
|
286
|
+
test_name = f"test_{flow.lower().replace(' ', '_')}"
|
|
287
|
+
|
|
288
|
+
test_code = f"""def {test_name}():
|
|
289
|
+
\"\"\"Integration test for {flow}.\"\"\"
|
|
290
|
+
# TODO: Implement integration test
|
|
291
|
+
# 1. Setup test data
|
|
292
|
+
# 2. Execute flow
|
|
293
|
+
# 3. Verify results
|
|
294
|
+
pass
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
return TestCase(
|
|
298
|
+
name=test_name,
|
|
299
|
+
target_function=flow,
|
|
300
|
+
test_code=test_code,
|
|
301
|
+
description=f"Integration test for {flow}",
|
|
302
|
+
test_type="integration"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _build_integration_test_prompt(self, flow: str) -> str:
|
|
306
|
+
"""Build prompt for LLM to generate integration test."""
|
|
307
|
+
return f"""Generate a Python integration test for the following user flow: {flow}
|
|
308
|
+
|
|
309
|
+
Include:
|
|
310
|
+
1. Test setup (fixtures, test data)
|
|
311
|
+
2. Step-by-step flow execution
|
|
312
|
+
3. Assertions to verify each step
|
|
313
|
+
4. Cleanup
|
|
314
|
+
|
|
315
|
+
Use pytest framework. Output only the test code.
|
|
316
|
+
"""
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""ValidationEngine for detecting and fixing code errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from .models_v2 import FileChange, ValidationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ValidationEngine:
|
|
14
|
+
"""Validates code and suggests/applies fixes for common errors."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize ValidationEngine."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def diagnose_project(self, project_path: Path) -> List[dict]:
|
|
21
|
+
"""Find all syntax errors in a project.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
project_path: Path to project root
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of error dictionaries with file, line, error info
|
|
28
|
+
"""
|
|
29
|
+
errors = []
|
|
30
|
+
|
|
31
|
+
for py_file in project_path.rglob("*.py"):
|
|
32
|
+
file_errors = self.check_file(py_file)
|
|
33
|
+
errors.extend(file_errors)
|
|
34
|
+
|
|
35
|
+
return errors
|
|
36
|
+
|
|
37
|
+
def check_file(self, file_path: Path) -> List[dict]:
|
|
38
|
+
"""Check a single file for syntax errors.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
file_path: Path to Python file
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of errors found
|
|
45
|
+
"""
|
|
46
|
+
errors = []
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
content = file_path.read_text()
|
|
50
|
+
ast.parse(content)
|
|
51
|
+
except SyntaxError as e:
|
|
52
|
+
errors.append({
|
|
53
|
+
"file": str(file_path),
|
|
54
|
+
"line": e.lineno,
|
|
55
|
+
"column": e.offset,
|
|
56
|
+
"error": str(e.msg),
|
|
57
|
+
"type": "SyntaxError"
|
|
58
|
+
})
|
|
59
|
+
except IndentationError as e:
|
|
60
|
+
errors.append({
|
|
61
|
+
"file": str(file_path),
|
|
62
|
+
"line": e.lineno,
|
|
63
|
+
"column": e.offset,
|
|
64
|
+
"error": str(e.msg),
|
|
65
|
+
"type": "IndentationError"
|
|
66
|
+
})
|
|
67
|
+
except Exception as e:
|
|
68
|
+
errors.append({
|
|
69
|
+
"file": str(file_path),
|
|
70
|
+
"line": 0,
|
|
71
|
+
"column": 0,
|
|
72
|
+
"error": str(e),
|
|
73
|
+
"type": type(e).__name__
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return errors
|
|
77
|
+
|
|
78
|
+
def fix_common_errors(self, file_path: Path) -> Optional[FileChange]:
|
|
79
|
+
"""Attempt to fix common syntax errors automatically.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
file_path: Path to file with errors
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
FileChange if fixes were applied, None otherwise
|
|
86
|
+
"""
|
|
87
|
+
original_content = file_path.read_text()
|
|
88
|
+
fixed_content = original_content
|
|
89
|
+
fixes_applied = []
|
|
90
|
+
|
|
91
|
+
# Check for errors
|
|
92
|
+
errors = self.check_file(file_path)
|
|
93
|
+
if not errors:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Try common fixes
|
|
97
|
+
for error in errors:
|
|
98
|
+
if "invalid syntax" in error["error"].lower():
|
|
99
|
+
# Try fixing missing parentheses, brackets, quotes
|
|
100
|
+
fixed_content, fix_msg = self._fix_missing_delimiters(fixed_content, error)
|
|
101
|
+
if fix_msg:
|
|
102
|
+
fixes_applied.append(fix_msg)
|
|
103
|
+
|
|
104
|
+
elif "indentation" in error["error"].lower():
|
|
105
|
+
# Fix indentation issues
|
|
106
|
+
fixed_content, fix_msg = self._fix_indentation(fixed_content)
|
|
107
|
+
if fix_msg:
|
|
108
|
+
fixes_applied.append(fix_msg)
|
|
109
|
+
|
|
110
|
+
elif "eol while scanning" in error["error"].lower():
|
|
111
|
+
# Fix unclosed strings
|
|
112
|
+
fixed_content, fix_msg = self._fix_unclosed_strings(fixed_content, error)
|
|
113
|
+
if fix_msg:
|
|
114
|
+
fixes_applied.append(fix_msg)
|
|
115
|
+
|
|
116
|
+
# Verify fix worked
|
|
117
|
+
try:
|
|
118
|
+
ast.parse(fixed_content)
|
|
119
|
+
# Success!
|
|
120
|
+
return FileChange(
|
|
121
|
+
file_path=str(file_path),
|
|
122
|
+
change_type="modify",
|
|
123
|
+
original_content=original_content,
|
|
124
|
+
new_content=fixed_content
|
|
125
|
+
)
|
|
126
|
+
except:
|
|
127
|
+
# Fix didn't work, return None
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def _fix_missing_delimiters(self, content: str, error: dict) -> Tuple[str, Optional[str]]:
|
|
131
|
+
"""Try to fix missing parentheses, brackets, or braces.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
content: File content
|
|
135
|
+
error: Error information
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
(fixed_content, fix_message)
|
|
139
|
+
"""
|
|
140
|
+
lines = content.splitlines(keepends=True)
|
|
141
|
+
line_num = error["line"] - 1
|
|
142
|
+
|
|
143
|
+
if line_num >= len(lines):
|
|
144
|
+
return content, None
|
|
145
|
+
|
|
146
|
+
line = lines[line_num]
|
|
147
|
+
|
|
148
|
+
# Count delimiters
|
|
149
|
+
open_parens = line.count('(') - line.count(')')
|
|
150
|
+
open_brackets = line.count('[') - line.count(']')
|
|
151
|
+
open_braces = line.count('{') - line.count('}')
|
|
152
|
+
|
|
153
|
+
fixed_line = line
|
|
154
|
+
fix_msg = None
|
|
155
|
+
|
|
156
|
+
if open_parens > 0:
|
|
157
|
+
fixed_line = fixed_line.rstrip() + ')' * open_parens + '\n'
|
|
158
|
+
fix_msg = f"Added {open_parens} closing parenthesis"
|
|
159
|
+
elif open_brackets > 0:
|
|
160
|
+
fixed_line = fixed_line.rstrip() + ']' * open_brackets + '\n'
|
|
161
|
+
fix_msg = f"Added {open_brackets} closing bracket"
|
|
162
|
+
elif open_braces > 0:
|
|
163
|
+
fixed_line = fixed_line.rstrip() + '}' * open_braces + '\n'
|
|
164
|
+
fix_msg = f"Added {open_braces} closing brace"
|
|
165
|
+
|
|
166
|
+
if fix_msg:
|
|
167
|
+
lines[line_num] = fixed_line
|
|
168
|
+
return "".join(lines), fix_msg
|
|
169
|
+
|
|
170
|
+
return content, None
|
|
171
|
+
|
|
172
|
+
def _fix_indentation(self, content: str) -> Tuple[str, Optional[str]]:
|
|
173
|
+
"""Fix indentation issues (tabs vs spaces).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
content: File content
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
(fixed_content, fix_message)
|
|
180
|
+
"""
|
|
181
|
+
# Convert all tabs to 4 spaces
|
|
182
|
+
if '\t' in content:
|
|
183
|
+
fixed = content.replace('\t', ' ')
|
|
184
|
+
return fixed, "Converted tabs to 4 spaces"
|
|
185
|
+
|
|
186
|
+
return content, None
|
|
187
|
+
|
|
188
|
+
def _fix_unclosed_strings(self, content: str, error: dict) -> Tuple[str, Optional[str]]:
|
|
189
|
+
"""Fix unclosed string literals.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
content: File content
|
|
193
|
+
error: Error information
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
(fixed_content, fix_message)
|
|
197
|
+
"""
|
|
198
|
+
lines = content.splitlines(keepends=True)
|
|
199
|
+
line_num = error["line"] - 1
|
|
200
|
+
|
|
201
|
+
if line_num >= len(lines):
|
|
202
|
+
return content, None
|
|
203
|
+
|
|
204
|
+
line = lines[line_num]
|
|
205
|
+
|
|
206
|
+
# Count quotes
|
|
207
|
+
single_quotes = line.count("'") - line.count("\\'")
|
|
208
|
+
double_quotes = line.count('"') - line.count('\\"')
|
|
209
|
+
|
|
210
|
+
fixed_line = line
|
|
211
|
+
fix_msg = None
|
|
212
|
+
|
|
213
|
+
if single_quotes % 2 == 1:
|
|
214
|
+
# Odd number of single quotes
|
|
215
|
+
fixed_line = fixed_line.rstrip() + "'\n"
|
|
216
|
+
fix_msg = "Added closing single quote"
|
|
217
|
+
elif double_quotes % 2 == 1:
|
|
218
|
+
# Odd number of double quotes
|
|
219
|
+
fixed_line = fixed_line.rstrip() + '"\n'
|
|
220
|
+
fix_msg = "Added closing double quote"
|
|
221
|
+
|
|
222
|
+
if fix_msg:
|
|
223
|
+
lines[line_num] = fixed_line
|
|
224
|
+
return "".join(lines), fix_msg
|
|
225
|
+
|
|
226
|
+
return content, None
|
|
227
|
+
|
|
228
|
+
def validate_syntax(self, code: str) -> ValidationResult:
|
|
229
|
+
"""Check if code has valid Python syntax.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
code: Python code to validate
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
ValidationResult
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
ast.parse(code)
|
|
239
|
+
return ValidationResult(valid=True)
|
|
240
|
+
except SyntaxError as e:
|
|
241
|
+
return ValidationResult(
|
|
242
|
+
valid=False,
|
|
243
|
+
errors=[f"SyntaxError at line {e.lineno}: {e.msg}"]
|
|
244
|
+
)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return ValidationResult(
|
|
247
|
+
valid=False,
|
|
248
|
+
errors=[f"{type(e).__name__}: {str(e)}"]
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def validate_imports(self, code: str) -> ValidationResult:
|
|
252
|
+
"""Check if all imports are available.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
code: Python code to validate
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
ValidationResult
|
|
259
|
+
"""
|
|
260
|
+
warnings = []
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
tree = ast.parse(code)
|
|
264
|
+
except:
|
|
265
|
+
return ValidationResult(valid=False, errors=["Cannot parse code"])
|
|
266
|
+
|
|
267
|
+
for node in ast.walk(tree):
|
|
268
|
+
if isinstance(node, ast.Import):
|
|
269
|
+
for alias in node.names:
|
|
270
|
+
try:
|
|
271
|
+
__import__(alias.name)
|
|
272
|
+
except ImportError:
|
|
273
|
+
warnings.append(f"Import '{alias.name}' may not be available")
|
|
274
|
+
|
|
275
|
+
elif isinstance(node, ast.ImportFrom):
|
|
276
|
+
if node.module:
|
|
277
|
+
try:
|
|
278
|
+
__import__(node.module)
|
|
279
|
+
except ImportError:
|
|
280
|
+
warnings.append(f"Module '{node.module}' may not be available")
|
|
281
|
+
|
|
282
|
+
return ValidationResult(
|
|
283
|
+
valid=True,
|
|
284
|
+
warnings=warnings
|
|
285
|
+
)
|