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.
Files changed (43) hide show
  1. codegraph_cli/__init__.py +4 -0
  2. codegraph_cli/agents.py +191 -0
  3. codegraph_cli/bug_detector.py +386 -0
  4. codegraph_cli/chat_agent.py +352 -0
  5. codegraph_cli/chat_session.py +220 -0
  6. codegraph_cli/cli.py +330 -0
  7. codegraph_cli/cli_chat.py +367 -0
  8. codegraph_cli/cli_diagnose.py +133 -0
  9. codegraph_cli/cli_refactor.py +230 -0
  10. codegraph_cli/cli_setup.py +470 -0
  11. codegraph_cli/cli_test.py +177 -0
  12. codegraph_cli/cli_v2.py +267 -0
  13. codegraph_cli/codegen_agent.py +265 -0
  14. codegraph_cli/config.py +31 -0
  15. codegraph_cli/config_manager.py +341 -0
  16. codegraph_cli/context_manager.py +500 -0
  17. codegraph_cli/crew_agents.py +123 -0
  18. codegraph_cli/crew_chat.py +159 -0
  19. codegraph_cli/crew_tools.py +497 -0
  20. codegraph_cli/diff_engine.py +265 -0
  21. codegraph_cli/embeddings.py +241 -0
  22. codegraph_cli/graph_export.py +144 -0
  23. codegraph_cli/llm.py +642 -0
  24. codegraph_cli/models.py +47 -0
  25. codegraph_cli/models_v2.py +185 -0
  26. codegraph_cli/orchestrator.py +49 -0
  27. codegraph_cli/parser.py +800 -0
  28. codegraph_cli/performance_analyzer.py +223 -0
  29. codegraph_cli/project_context.py +230 -0
  30. codegraph_cli/rag.py +200 -0
  31. codegraph_cli/refactor_agent.py +452 -0
  32. codegraph_cli/security_scanner.py +366 -0
  33. codegraph_cli/storage.py +390 -0
  34. codegraph_cli/templates/graph_interactive.html +257 -0
  35. codegraph_cli/testgen_agent.py +316 -0
  36. codegraph_cli/validation_engine.py +285 -0
  37. codegraph_cli/vector_store.py +293 -0
  38. codegraph_cli-2.0.0.dist-info/METADATA +318 -0
  39. codegraph_cli-2.0.0.dist-info/RECORD +43 -0
  40. codegraph_cli-2.0.0.dist-info/WHEEL +5 -0
  41. codegraph_cli-2.0.0.dist-info/entry_points.txt +2 -0
  42. codegraph_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
  43. 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
+ )