raggiecode 0.2.1__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.
- Agent/__init__.py +0 -0
- Agent/agent.py +891 -0
- Agent/chat_history_db.py +1500 -0
- Agent/command.py +49 -0
- Agent/config.py +46 -0
- Agent/effort_levels.py +33 -0
- Agent/git_manager.py +727 -0
- Agent/tools.py +35 -0
- Commands/__init__.py +18 -0
- Commands/effort.py +42 -0
- Commands/global_todo.py +23 -0
- Commands/help.py +22 -0
- Commands/reasoning.py +24 -0
- Commands/redo.py +11 -0
- Commands/reindex.py +27 -0
- Commands/shell.py +28 -0
- Commands/stream.py +24 -0
- Commands/undo.py +13 -0
- Commands/unlimited_effort.py +8 -0
- Commands/window_size.py +29 -0
- RAG/__init__.py +0 -0
- RAG/document.py +119 -0
- RAG/find.py +408 -0
- RAG/graph.py +231 -0
- Tools/GetFileCodeStructure.py +43 -0
- Tools/GetSymbolSourceCode.py +27 -0
- Tools/__init__.py +39 -0
- Tools/ask_user.py +102 -0
- Tools/dispatch_subagent.py +215 -0
- Tools/document.py +35 -0
- Tools/edit_symbol.py +250 -0
- Tools/fuzzy_search.py +119 -0
- Tools/list_dir.py +51 -0
- Tools/read.py +49 -0
- Tools/read_image.py +75 -0
- Tools/remove.py +75 -0
- Tools/replace.py +305 -0
- Tools/search.py +41 -0
- Tools/shell.py +149 -0
- Tools/shell_kill.py +87 -0
- Tools/temp_background_service.py +113 -0
- Tools/todo_list.py +481 -0
- Tools/utils.py +116 -0
- Tools/view_changes.py +179 -0
- Tools/walk_call_tree.py +30 -0
- Tools/web_fetch.py +175 -0
- Tools/web_search.py +69 -0
- Tools/write.py +48 -0
- cli.py +111 -0
- config/__init__.py +0 -0
- config/coder_system_prompt.md +119 -0
- config/roles.json +43 -0
- config/tools.json +709 -0
- indexing/__init__.py +0 -0
- indexing/cli.py +128 -0
- indexing/code_index_sdk.py +832 -0
- indexing/code_indexer.py +1763 -0
- indexing/db_schema.py +396 -0
- indexing/export_to_json.py +346 -0
- indexing/extractors.py +189 -0
- indexing/file_utils.py +97 -0
- indexing/frontend/__init__.py +0 -0
- indexing/frontend/css_extractor.py +195 -0
- indexing/frontend/css_parser.py +387 -0
- indexing/frontend/css_selector_utils.py +226 -0
- indexing/frontend/edit_safety.py +573 -0
- indexing/frontend/graph.py +838 -0
- indexing/frontend/html_extractor.py +496 -0
- indexing/frontend/html_parser.py +314 -0
- indexing/frontend/jsx_extractor.py +1204 -0
- indexing/frontend/location_lookup.py +247 -0
- indexing/frontend/resolver.py +485 -0
- indexing/frontend/runtime_resolver.py +862 -0
- indexing/frontend/semantic_output.py +705 -0
- indexing/frontend/source_location.py +69 -0
- indexing/frontend_config.py +72 -0
- indexing/frontend_models.py +347 -0
- indexing/language_config.py +360 -0
- indexing/models.py +284 -0
- indexing/node_utils.py +1112 -0
- indexing/parse_worker.py +1082 -0
- indexing/queries.py +1542 -0
- indexing/sdk_examples.py +426 -0
- interactive.py +248 -0
- raggie.py +673 -0
- raggiecode-0.2.1.dist-info/METADATA +944 -0
- raggiecode-0.2.1.dist-info/RECORD +93 -0
- raggiecode-0.2.1.dist-info/WHEEL +5 -0
- raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
- raggiecode-0.2.1.dist-info/top_level.txt +10 -0
- skills/__init__.py +3 -0
- skills/manager.py +114 -0
- skills/tool.py +121 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Code Index SDK for AI Agents
|
|
4
|
+
Provides a high-level API for querying and traversing code indices stored in SQLite.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sqlite3
|
|
9
|
+
from collections import deque
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Dict, Optional, Any, Union
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
from indexing.models import (
|
|
15
|
+
Function, Class, File, Dependency,
|
|
16
|
+
)
|
|
17
|
+
from indexing.queries import QueryMixin, DescriptionMixin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CodeIndexSDK(QueryMixin, DescriptionMixin):
|
|
21
|
+
"""
|
|
22
|
+
SDK for querying and traversing code indices.
|
|
23
|
+
|
|
24
|
+
Example usage:
|
|
25
|
+
sdk = CodeIndexSDK("code_index.db")
|
|
26
|
+
|
|
27
|
+
# Get all files
|
|
28
|
+
files = sdk.get_files()
|
|
29
|
+
|
|
30
|
+
# Search for functions by name
|
|
31
|
+
functions = sdk.search_functions("render")
|
|
32
|
+
|
|
33
|
+
# Get a class and its methods
|
|
34
|
+
cls = sdk.get_class_by_name("UserService")
|
|
35
|
+
methods = sdk.get_class_methods(cls.id)
|
|
36
|
+
|
|
37
|
+
# Get all functions in a file
|
|
38
|
+
file_funcs = sdk.get_file_functions(file_id)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, db_path: str, root_dir: str = None):
|
|
42
|
+
"""Initialize the SDK with a database path and optional root directory for indexing."""
|
|
43
|
+
self.db_path = db_path
|
|
44
|
+
self.root_dir = root_dir
|
|
45
|
+
self.conn = None
|
|
46
|
+
self._connect()
|
|
47
|
+
|
|
48
|
+
def index_directory(self, force_reindex: bool = False, verbose: bool = False):
|
|
49
|
+
"""Index the codebase directory into the database.
|
|
50
|
+
|
|
51
|
+
Delegates to the internal CodeIndexer. This is the public API for
|
|
52
|
+
triggering a re-index; external consumers should not import CodeIndexer directly.
|
|
53
|
+
"""
|
|
54
|
+
from indexing.code_indexer import CodeIndexer
|
|
55
|
+
indexer = CodeIndexer(
|
|
56
|
+
root_dir=self.root_dir,
|
|
57
|
+
db_path=self.db_path,
|
|
58
|
+
force_reindex=force_reindex,
|
|
59
|
+
verbose=verbose,
|
|
60
|
+
)
|
|
61
|
+
indexer.index_directory()
|
|
62
|
+
self._connect()
|
|
63
|
+
|
|
64
|
+
def _connect(self):
|
|
65
|
+
"""Establish database connection. Does not raise if the database does not exist yet
|
|
66
|
+
(it will be created by index_directory)."""
|
|
67
|
+
if not Path(self.db_path).exists():
|
|
68
|
+
self.conn = None
|
|
69
|
+
return
|
|
70
|
+
self.conn = sqlite3.connect(self.db_path, timeout=30) # Increase timeout for concurrent access
|
|
71
|
+
self.conn.row_factory = sqlite3.Row
|
|
72
|
+
|
|
73
|
+
def close(self):
|
|
74
|
+
"""Close the database connection."""
|
|
75
|
+
if self.conn:
|
|
76
|
+
self.conn.close()
|
|
77
|
+
self.conn = None
|
|
78
|
+
|
|
79
|
+
def __enter__(self):
|
|
80
|
+
"""Context manager entry."""
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
84
|
+
"""Context manager exit."""
|
|
85
|
+
self.close()
|
|
86
|
+
|
|
87
|
+
# ==================== Convenience Methods ====================
|
|
88
|
+
|
|
89
|
+
def get_file_summary(self, file_id: int) -> Dict[str, Any]:
|
|
90
|
+
"""Get a summary of all code elements in a file."""
|
|
91
|
+
file = self.get_file_by_id(file_id)
|
|
92
|
+
if not file:
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
'file': asdict(file),
|
|
97
|
+
'functions': [asdict(f) for f in self.get_file_functions(file_id)],
|
|
98
|
+
'classes': [asdict(c) for c in self.get_file_classes(file_id)],
|
|
99
|
+
'variables': [asdict(v) for v in self.get_file_variables(file_id)],
|
|
100
|
+
'type_aliases': [asdict(t) for t in self.get_type_aliases(file_id)],
|
|
101
|
+
'structs': [asdict(s) for s in self.get_structs(file_id)],
|
|
102
|
+
'interfaces': [asdict(i) for i in self.get_interfaces(file_id)]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def get_class_summary(self, class_id: int) -> Dict[str, Any]:
|
|
106
|
+
"""Get a summary of a class including its methods and variables."""
|
|
107
|
+
cls = self.get_class_by_id(class_id)
|
|
108
|
+
if not cls:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
'class': asdict(cls),
|
|
113
|
+
'methods': [asdict(m) for m in self.get_class_methods(class_id)],
|
|
114
|
+
'variables': [asdict(v) for v in self.get_class_variables(class_id)],
|
|
115
|
+
'nested_classes': [asdict(c) for c in self.get_nested_classes(class_id)]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def search_all(self, pattern: str) -> Dict[str, List[Dict]]:
|
|
119
|
+
"""Search for pattern across all entity types."""
|
|
120
|
+
return {
|
|
121
|
+
'functions': [asdict(f) for f in self.search_functions(pattern)],
|
|
122
|
+
'classes': [asdict(c) for c in self.search_classes(pattern)],
|
|
123
|
+
'variables': [asdict(v) for v in self.search_variables(pattern)],
|
|
124
|
+
'type_aliases': [asdict(t) for t in self.search_type_aliases(pattern)]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def search_symbols(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
128
|
+
"""Search for symbols across functions, classes, and variables by name and description.
|
|
129
|
+
|
|
130
|
+
Uses SQL LIKE for substring matching on both name and description fields.
|
|
131
|
+
Results are ranked: exact name matches first, then partial name matches,
|
|
132
|
+
then description matches.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
query: Search query string
|
|
136
|
+
limit: Maximum number of results to return
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of dicts with keys: type, name, file_path, description, match_reason
|
|
140
|
+
"""
|
|
141
|
+
cursor = self.conn.cursor()
|
|
142
|
+
like_pattern = f"%{query}%"
|
|
143
|
+
results = []
|
|
144
|
+
|
|
145
|
+
# Exact name matches (highest priority)
|
|
146
|
+
for table, label in [('functions', 'function'), ('classes', 'class'), ('variables', 'variable')]:
|
|
147
|
+
cursor.execute(
|
|
148
|
+
f"SELECT name, file_id, description FROM {table} WHERE name = ? LIMIT ?",
|
|
149
|
+
(query, limit)
|
|
150
|
+
)
|
|
151
|
+
for row in cursor.fetchall():
|
|
152
|
+
file = self.get_file_by_id(row['file_id'])
|
|
153
|
+
if file:
|
|
154
|
+
results.append({
|
|
155
|
+
'type': label,
|
|
156
|
+
'name': row['name'],
|
|
157
|
+
'file_path': file.path,
|
|
158
|
+
'description': row['description'] if 'description' in row.keys() else None,
|
|
159
|
+
'match_reason': 'exact name match',
|
|
160
|
+
'rank': 0,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
# Partial name matches
|
|
164
|
+
for table, label in [('functions', 'function'), ('classes', 'class'), ('variables', 'variable')]:
|
|
165
|
+
cursor.execute(
|
|
166
|
+
f"SELECT name, file_id, description FROM {table} WHERE name LIKE ? AND name != ? LIMIT ?",
|
|
167
|
+
(like_pattern, query, limit)
|
|
168
|
+
)
|
|
169
|
+
for row in cursor.fetchall():
|
|
170
|
+
file = self.get_file_by_id(row['file_id'])
|
|
171
|
+
if file:
|
|
172
|
+
results.append({
|
|
173
|
+
'type': label,
|
|
174
|
+
'name': row['name'],
|
|
175
|
+
'file_path': file.path,
|
|
176
|
+
'description': row['description'] if 'description' in row.keys() else None,
|
|
177
|
+
'match_reason': 'partial name match',
|
|
178
|
+
'rank': 1,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
# Description matches (lower priority)
|
|
182
|
+
for table, label in [('functions', 'function'), ('classes', 'class'), ('variables', 'variable')]:
|
|
183
|
+
cursor.execute(
|
|
184
|
+
f"SELECT name, file_id, description FROM {table} WHERE description LIKE ? AND name NOT LIKE ? LIMIT ?",
|
|
185
|
+
(like_pattern, like_pattern, limit)
|
|
186
|
+
)
|
|
187
|
+
for row in cursor.fetchall():
|
|
188
|
+
file = self.get_file_by_id(row['file_id'])
|
|
189
|
+
if file:
|
|
190
|
+
results.append({
|
|
191
|
+
'type': label,
|
|
192
|
+
'name': row['name'],
|
|
193
|
+
'file_path': file.path,
|
|
194
|
+
'description': row['description'],
|
|
195
|
+
'match_reason': 'description match',
|
|
196
|
+
'rank': 2,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
# Sort by rank, then by name
|
|
200
|
+
results.sort(key=lambda r: (r['rank'], r['name']))
|
|
201
|
+
return results[:limit]
|
|
202
|
+
|
|
203
|
+
def _render_callable_deps(self, lines: List[str], deps: Dict[str, List], indent: str) -> None:
|
|
204
|
+
"""Append 'depending on:' lines for a function/method's grouped deps."""
|
|
205
|
+
lines.append(f'{indent}depending on:')
|
|
206
|
+
dep_indent = indent + ' '
|
|
207
|
+
seen_deps: set = set()
|
|
208
|
+
|
|
209
|
+
type_labels = [
|
|
210
|
+
('function_call', 'function', '()'),
|
|
211
|
+
('method_call', 'method', '()'),
|
|
212
|
+
('class_reference', 'class', ''),
|
|
213
|
+
('variable_reference', 'variable', ''),
|
|
214
|
+
]
|
|
215
|
+
resolvers = {
|
|
216
|
+
'function_call': self._resolve_function_definition,
|
|
217
|
+
'method_call': self._resolve_method_definition,
|
|
218
|
+
'class_reference': self._resolve_class_definition,
|
|
219
|
+
'variable_reference': self._resolve_variable_definition,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for dep_type, label, suffix in type_labels:
|
|
223
|
+
for dep in deps[dep_type]:
|
|
224
|
+
if self._is_likely_literal(dep.name):
|
|
225
|
+
continue
|
|
226
|
+
dep_key = f"{label}:{dep.name}"
|
|
227
|
+
if dep_key in seen_deps:
|
|
228
|
+
continue
|
|
229
|
+
def_info = resolvers[dep_type](dep.name)
|
|
230
|
+
if def_info:
|
|
231
|
+
lines.append(f'{dep_indent}- {label} {dep.name}{suffix} in file "{def_info["file_path"]}"')
|
|
232
|
+
seen_deps.add(dep_key)
|
|
233
|
+
|
|
234
|
+
if not seen_deps:
|
|
235
|
+
lines.append(f'{dep_indent}NONE')
|
|
236
|
+
|
|
237
|
+
def _render_class_members(self, lines: List[str], cls: 'Class', method_indent: str) -> None:
|
|
238
|
+
"""Append member lines (methods + variables) for a class."""
|
|
239
|
+
methods = self.get_class_methods(cls.id)
|
|
240
|
+
class_vars = self.get_class_variables(cls.id)
|
|
241
|
+
|
|
242
|
+
if not methods and not class_vars:
|
|
243
|
+
lines.append(f'{method_indent} NONE')
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
for method in methods:
|
|
247
|
+
params_str = self._format_parameters(method.parameters)
|
|
248
|
+
desc_annotation = f" # {method.description}" if method.description else ""
|
|
249
|
+
lines.append(f'{method_indent}- method {method.name}({params_str}){desc_annotation}:')
|
|
250
|
+
deps = self.get_function_dependencies_grouped(method.id)
|
|
251
|
+
self._render_callable_deps(lines, deps, method_indent + ' ')
|
|
252
|
+
|
|
253
|
+
for var in class_vars:
|
|
254
|
+
type_suffix = f" : {var.field_type}" if var.field_type else ""
|
|
255
|
+
desc_annotation = f" # {var.description}" if var.description else ""
|
|
256
|
+
lines.append(f'{method_indent}- variable {var.name}{type_suffix}{desc_annotation}')
|
|
257
|
+
|
|
258
|
+
def _render_nested_classes(self, lines: List[str], func: 'Function', file: 'File') -> None:
|
|
259
|
+
"""Append 'contains: nested classes' block for a function if any."""
|
|
260
|
+
cursor = self.conn.cursor()
|
|
261
|
+
cursor.execute(
|
|
262
|
+
"SELECT * FROM classes WHERE parent_id = ? AND file_id = ?",
|
|
263
|
+
(func.id, file.id)
|
|
264
|
+
)
|
|
265
|
+
nested_rows = cursor.fetchall()
|
|
266
|
+
if not nested_rows:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
lines.append(' contains:')
|
|
270
|
+
for row in nested_rows:
|
|
271
|
+
nested_cls = Class.from_row(row, file.path)
|
|
272
|
+
desc_annotation = f" # {nested_cls.description}" if nested_cls.description else ""
|
|
273
|
+
lines.append(f' - class {nested_cls.name}{desc_annotation}:')
|
|
274
|
+
lines.append(f' members:')
|
|
275
|
+
self._render_class_members(lines, nested_cls, ' ')
|
|
276
|
+
|
|
277
|
+
def get_dependency_graph(self, file_path: str) -> str:
|
|
278
|
+
"""Get a YAML-like formatted dependency graph for a file.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
file_path: Relative path to the file in the database
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
YAML-like formatted string showing the dependency graph
|
|
285
|
+
"""
|
|
286
|
+
normalized_path = file_path[2:] if file_path.startswith('./') else file_path
|
|
287
|
+
file = self.get_file_by_path(normalized_path)
|
|
288
|
+
|
|
289
|
+
if not file:
|
|
290
|
+
filename = Path(normalized_path).name
|
|
291
|
+
cursor = self.conn.cursor()
|
|
292
|
+
cursor.execute("SELECT * FROM files WHERE path LIKE ?", (f"%{filename}",))
|
|
293
|
+
row = cursor.fetchone()
|
|
294
|
+
if row:
|
|
295
|
+
file = File.from_row(row)
|
|
296
|
+
|
|
297
|
+
if not file:
|
|
298
|
+
return f"Error: File not found in database: {file_path}"
|
|
299
|
+
|
|
300
|
+
lines = [f'file "{file_path}":']
|
|
301
|
+
|
|
302
|
+
for func in self.get_file_functions(file.id):
|
|
303
|
+
params_str = self._format_parameters(func.parameters)
|
|
304
|
+
desc_annotation = f" # {func.description}" if func.description else ""
|
|
305
|
+
lines.append(f' - function {func.name}({params_str}) in file "{file_path}"{desc_annotation}')
|
|
306
|
+
deps = self.get_function_dependencies_grouped(func.id)
|
|
307
|
+
self._render_callable_deps(lines, deps, ' ')
|
|
308
|
+
self._render_nested_classes(lines, func, file)
|
|
309
|
+
|
|
310
|
+
seen_vars: set = set()
|
|
311
|
+
for var in self.get_file_variables(file.id):
|
|
312
|
+
if var.name in seen_vars:
|
|
313
|
+
continue
|
|
314
|
+
seen_vars.add(var.name)
|
|
315
|
+
type_suffix = f" : {var.field_type}" if var.field_type else ""
|
|
316
|
+
desc_annotation = f" # {var.description}" if var.description else ""
|
|
317
|
+
lines.append(f' - variable {var.name}{type_suffix} in file "{file_path}"{desc_annotation}')
|
|
318
|
+
|
|
319
|
+
for imp in self.get_file_imports(file.id):
|
|
320
|
+
lines.append(f' - imported {imp.name} in file "{file_path}"')
|
|
321
|
+
|
|
322
|
+
for ns in self.get_file_namespaces(file.id):
|
|
323
|
+
lines.append(f' - namespace {ns.name} in file "{file_path}"')
|
|
324
|
+
|
|
325
|
+
for cls in self.get_file_classes(file.id):
|
|
326
|
+
desc_annotation = f" # {cls.description}" if cls.description else ""
|
|
327
|
+
ns_annotation = f" # namespace: {cls.namespace}" if cls.namespace else ""
|
|
328
|
+
lines.append(f' - class {cls.name} in file "{file_path}"{desc_annotation}{ns_annotation}:')
|
|
329
|
+
lines.append(f' members:')
|
|
330
|
+
self._render_class_members(lines, cls, ' ')
|
|
331
|
+
|
|
332
|
+
return '\n'.join(lines)
|
|
333
|
+
|
|
334
|
+
def _resolve_function_definition(self, name: str) -> Optional[Dict]:
|
|
335
|
+
"""Resolve a function name to its definition location."""
|
|
336
|
+
functions = self.get_function_by_name(name)
|
|
337
|
+
if functions:
|
|
338
|
+
# Return the first match
|
|
339
|
+
func = functions[0]
|
|
340
|
+
return {"file_path": func.file_path}
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
def _resolve_method_definition(self, name: str) -> Optional[Dict]:
|
|
344
|
+
"""Resolve a method name to its definition location."""
|
|
345
|
+
# Strip the object part (e.g., "self.init" -> "init", "context.SaveChangesAsync" -> "SaveChangesAsync")
|
|
346
|
+
method_name = name.rsplit('.', 1)[-1] if '.' in name else name
|
|
347
|
+
cursor = self.conn.cursor()
|
|
348
|
+
cursor.execute(
|
|
349
|
+
"SELECT f.*, file.path FROM functions f JOIN files file ON f.file_id = file.id WHERE f.name = ? AND f.parent_type = 'class' LIMIT 1",
|
|
350
|
+
(method_name,)
|
|
351
|
+
)
|
|
352
|
+
row = cursor.fetchone()
|
|
353
|
+
if row:
|
|
354
|
+
return {"file_path": row['path']}
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
def _resolve_class_definition(self, name: str) -> Optional[Dict]:
|
|
358
|
+
"""Resolve a class name to its definition location."""
|
|
359
|
+
classes = self.get_class_by_name(name)
|
|
360
|
+
if classes:
|
|
361
|
+
cls = classes[0]
|
|
362
|
+
return {"file_path": cls.file_path}
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
def _resolve_variable_definition(self, name: str) -> Optional[Dict]:
|
|
366
|
+
"""Resolve a variable name to its definition location."""
|
|
367
|
+
variables = self.get_variable_by_name(name)
|
|
368
|
+
if variables:
|
|
369
|
+
var = variables[0]
|
|
370
|
+
return {"file_path": var.file_path}
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
def _is_likely_literal(self, name: str) -> bool:
|
|
374
|
+
"""Check if a dependency name is likely a literal string or unimplemented function.
|
|
375
|
+
|
|
376
|
+
Returns True if the name appears to be:
|
|
377
|
+
- A string literal (contains quotes, spaces, or special characters)
|
|
378
|
+
- A temporary/inline expression
|
|
379
|
+
"""
|
|
380
|
+
# Check for string literals with quotes
|
|
381
|
+
if '"' in name or "'" in name:
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
# Check for spaces (indicates multi-word string literal)
|
|
385
|
+
if ' ' in name:
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
# Check for common literal patterns (command-like)
|
|
389
|
+
if name.startswith(('echo ', 'print ', 'return ', 'cd ', 'ls ', 'cat ')):
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
# Check for format patterns (string formatting)
|
|
393
|
+
if '.format(' in name or ('%' in name and '(' in name and name.count('%') > 1):
|
|
394
|
+
return True
|
|
395
|
+
|
|
396
|
+
# Check for obvious expressions (multiple operators without function-like structure)
|
|
397
|
+
# Allow single operators like "x++" or "x--" but filter complex expressions
|
|
398
|
+
operator_count = sum(1 for op in ['+', '-', '*', '/', '=', '==', '!=', '<', '>', '<=', '>='] if op in name)
|
|
399
|
+
if operator_count >= 2:
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
def _format_parameters(self, parameters: Union[List[Dict], str]) -> str:
|
|
405
|
+
"""Format parameters list into a string."""
|
|
406
|
+
if not parameters:
|
|
407
|
+
return ""
|
|
408
|
+
|
|
409
|
+
# Handle case where parameters might be a string (JSON not parsed)
|
|
410
|
+
if isinstance(parameters, str):
|
|
411
|
+
try:
|
|
412
|
+
parameters = json.loads(parameters)
|
|
413
|
+
except:
|
|
414
|
+
return parameters
|
|
415
|
+
|
|
416
|
+
param_strs = []
|
|
417
|
+
for param in parameters:
|
|
418
|
+
if isinstance(param, str):
|
|
419
|
+
param_strs.append(param)
|
|
420
|
+
elif isinstance(param, dict):
|
|
421
|
+
name = param.get('name', '')
|
|
422
|
+
param_type = param.get('type', '')
|
|
423
|
+
if param_type:
|
|
424
|
+
param_strs.append(f"{name}: {param_type}")
|
|
425
|
+
else:
|
|
426
|
+
param_strs.append(name)
|
|
427
|
+
|
|
428
|
+
return ', '.join(param_strs)
|
|
429
|
+
|
|
430
|
+
# ==================== Source Body Reading ====================
|
|
431
|
+
|
|
432
|
+
def _read_source_lines(self, file_id: int, start_line: int, end_line: int) -> Optional[str]:
|
|
433
|
+
"""Read lines [start_line, end_line] (1-indexed, inclusive) from a source file.
|
|
434
|
+
|
|
435
|
+
Uses File.absolute_path from the index; falls back to resolving the relative
|
|
436
|
+
path against cwd and cwd/src if the absolute path no longer exists.
|
|
437
|
+
Returns None if the file cannot be found.
|
|
438
|
+
"""
|
|
439
|
+
db_file = self.get_file_by_id(file_id)
|
|
440
|
+
if not db_file:
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
source = Path(db_file.absolute_path)
|
|
444
|
+
if not source.exists():
|
|
445
|
+
for base in (Path.cwd(), Path.cwd() / "src"):
|
|
446
|
+
candidate = base / db_file.path
|
|
447
|
+
if candidate.exists():
|
|
448
|
+
source = candidate
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
if not source.exists():
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
with open(source, 'r', encoding='utf-8') as f:
|
|
455
|
+
lines = f.read().split('\n')
|
|
456
|
+
|
|
457
|
+
return '\n'.join(lines[start_line - 1:end_line])
|
|
458
|
+
|
|
459
|
+
def get_function_body(self, function_name: str, file_path: Optional[str] = None) -> Optional[str]:
|
|
460
|
+
"""Get the source body of a function or method by name.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
function_name: Name of the function/method.
|
|
464
|
+
file_path: Optional relative file path to disambiguate same-name functions.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Source code string with description, or None if not found / file unresolvable.
|
|
468
|
+
"""
|
|
469
|
+
file_id = None
|
|
470
|
+
if file_path:
|
|
471
|
+
f = self.get_file_by_path(file_path)
|
|
472
|
+
if f:
|
|
473
|
+
file_id = f.id
|
|
474
|
+
|
|
475
|
+
matches = self.get_function_by_name(function_name, file_id)
|
|
476
|
+
if not matches:
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
# Prefer implementations (methods inside classes) over interface declarations
|
|
480
|
+
# Interface declarations are standalone functions with no body — implementations
|
|
481
|
+
# have parent_type='class' and contain actual code
|
|
482
|
+
func = matches[0]
|
|
483
|
+
if len(matches) > 1:
|
|
484
|
+
impls = [m for m in matches if m.parent_type == 'class']
|
|
485
|
+
if impls:
|
|
486
|
+
func = impls[0]
|
|
487
|
+
body = self._read_source_lines(func.file_id, func.location.start_line, func.location.end_line)
|
|
488
|
+
|
|
489
|
+
if func.description:
|
|
490
|
+
return f"# Description: {func.description}\n\n{body}"
|
|
491
|
+
return body
|
|
492
|
+
|
|
493
|
+
def walk_call_tree(self, symbol_name: str, file_path: Optional[str] = None,
|
|
494
|
+
max_depth: int = 5, include_external: bool = False,
|
|
495
|
+
exclude: Optional[List[str]] = None) -> str:
|
|
496
|
+
"""Walk the call tree starting from a function/method, depth-limited with cycle detection.
|
|
497
|
+
|
|
498
|
+
Returns JSON lines (one JSON object per line), sorted by depth then name. Each node has:
|
|
499
|
+
id: unique ID for follow-up queries (e.g. "f:123")
|
|
500
|
+
name: symbol name
|
|
501
|
+
kind: "function" or "method"
|
|
502
|
+
file: relative file path
|
|
503
|
+
line: start line number
|
|
504
|
+
snippet: first line of the body (one-liner)
|
|
505
|
+
depth: distance from root
|
|
506
|
+
cycle: true if this node appeared earlier in the path
|
|
507
|
+
callees: list of child node IDs
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
symbol_name: Name of the starting function/method. Can be "ClassName.method" syntax.
|
|
511
|
+
file_path: Optional file path to disambiguate same-name symbols.
|
|
512
|
+
max_depth: Maximum depth to traverse (default 5).
|
|
513
|
+
include_external: If True, include external/third-party calls (unresolved).
|
|
514
|
+
exclude: Optional list of path prefixes to exclude (e.g. ["tests/"]).
|
|
515
|
+
"""
|
|
516
|
+
file_id = None
|
|
517
|
+
if file_path:
|
|
518
|
+
f = self.get_file_by_path(file_path)
|
|
519
|
+
if f:
|
|
520
|
+
file_id = f.id
|
|
521
|
+
|
|
522
|
+
# First check if it's a class name (for walking all methods)
|
|
523
|
+
classes = self.get_class_by_name(symbol_name, file_id)
|
|
524
|
+
if classes:
|
|
525
|
+
# Class-based walk: walk all methods of the class
|
|
526
|
+
cls = classes[0]
|
|
527
|
+
methods = self.get_class_methods(cls.id)
|
|
528
|
+
if not methods:
|
|
529
|
+
return json.dumps({"error": f"Class '{symbol_name}' has no methods"})
|
|
530
|
+
|
|
531
|
+
# Return information about the class and its methods
|
|
532
|
+
result = {
|
|
533
|
+
"type": "class",
|
|
534
|
+
"name": cls.name,
|
|
535
|
+
"file": cls.file_path,
|
|
536
|
+
"line": cls.location.start_line,
|
|
537
|
+
"methods": [
|
|
538
|
+
{
|
|
539
|
+
"id": f"f:{m.id}",
|
|
540
|
+
"name": m.name,
|
|
541
|
+
"line": m.location.start_line,
|
|
542
|
+
"snippet": self._read_one_line_snippet(m.file_id, m.location.start_line)
|
|
543
|
+
}
|
|
544
|
+
for m in methods
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
return json.dumps(result, ensure_ascii=False)
|
|
548
|
+
|
|
549
|
+
# Handle Class.method syntax
|
|
550
|
+
if '.' in symbol_name:
|
|
551
|
+
parts = symbol_name.split('.', 1)
|
|
552
|
+
class_name = parts[0]
|
|
553
|
+
method_name = parts[1]
|
|
554
|
+
|
|
555
|
+
# Find the class
|
|
556
|
+
classes = self.get_class_by_name(class_name, file_id)
|
|
557
|
+
if not classes:
|
|
558
|
+
return json.dumps({"error": f"Class not found: {class_name}"})
|
|
559
|
+
|
|
560
|
+
cls = classes[0]
|
|
561
|
+
# Find the method within the class
|
|
562
|
+
method = self.get_method_by_name(cls.id, method_name)
|
|
563
|
+
if not method:
|
|
564
|
+
return json.dumps({"error": f"Method '{method_name}' not found in class '{class_name}'"})
|
|
565
|
+
|
|
566
|
+
root_func = method
|
|
567
|
+
else:
|
|
568
|
+
# Handle plain function/method name
|
|
569
|
+
functions = self.get_function_by_name(symbol_name, file_id)
|
|
570
|
+
if not functions:
|
|
571
|
+
return json.dumps({"error": f"Symbol not found: {symbol_name}"})
|
|
572
|
+
root_func = functions[0]
|
|
573
|
+
# Prefer implementations (methods inside classes) over interface declarations
|
|
574
|
+
if len(functions) > 1:
|
|
575
|
+
impls = [f for f in functions if f.parent_type == 'class']
|
|
576
|
+
if impls:
|
|
577
|
+
root_func = impls[0]
|
|
578
|
+
|
|
579
|
+
root_id = f"f:{root_func.id}"
|
|
580
|
+
root_snippet = self._read_one_line_snippet(root_func.file_id, root_func.location.start_line)
|
|
581
|
+
|
|
582
|
+
nodes: Dict[str, Dict[str, Any]] = {}
|
|
583
|
+
nodes[root_id] = {
|
|
584
|
+
"id": root_id,
|
|
585
|
+
"name": root_func.name,
|
|
586
|
+
"kind": root_func.type,
|
|
587
|
+
"file": root_func.file_path,
|
|
588
|
+
"line": root_func.location.start_line,
|
|
589
|
+
"snippet": root_snippet,
|
|
590
|
+
"depth": 0,
|
|
591
|
+
"cycle": False,
|
|
592
|
+
"callees": [],
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
queue: deque = deque()
|
|
596
|
+
queue.append((root_func.id, root_id, 0, frozenset([root_func.id])))
|
|
597
|
+
|
|
598
|
+
cursor = self.conn.cursor()
|
|
599
|
+
|
|
600
|
+
while queue:
|
|
601
|
+
func_id, parent_node_id, depth, ancestors = queue.popleft()
|
|
602
|
+
if depth >= max_depth:
|
|
603
|
+
continue
|
|
604
|
+
|
|
605
|
+
# Get the file_id for this function to pass to resolver
|
|
606
|
+
cursor.execute("SELECT file_id FROM functions WHERE id = ?", (func_id,))
|
|
607
|
+
row = cursor.fetchone()
|
|
608
|
+
source_file_id = row['file_id'] if row else None
|
|
609
|
+
|
|
610
|
+
cursor.execute(
|
|
611
|
+
"""SELECT * FROM dependencies
|
|
612
|
+
WHERE source_function_id = ?
|
|
613
|
+
AND dependency_type IN ('function_call', 'method_call', 'class_reference')
|
|
614
|
+
ORDER BY name""",
|
|
615
|
+
(func_id,)
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
seen_callees = set() # Track seen callees to avoid duplicates
|
|
619
|
+
for row in cursor.fetchall():
|
|
620
|
+
dep = Dependency.from_row(row, "")
|
|
621
|
+
if self._is_likely_literal(dep.name):
|
|
622
|
+
continue
|
|
623
|
+
|
|
624
|
+
# Create a unique key for deduplication (include dependency_type to distinguish class_reference from function_call)
|
|
625
|
+
callee_key = (dep.name, dep.target_function_id, dep.dependency_type)
|
|
626
|
+
if callee_key in seen_callees:
|
|
627
|
+
continue
|
|
628
|
+
seen_callees.add(callee_key)
|
|
629
|
+
|
|
630
|
+
callee_func = self._resolve_call_target(dep, source_file_id)
|
|
631
|
+
if callee_func is None:
|
|
632
|
+
if include_external and dep.name:
|
|
633
|
+
ext_id = f"ext:{dep.name}"
|
|
634
|
+
if ext_id not in nodes:
|
|
635
|
+
nodes[ext_id] = {
|
|
636
|
+
"id": ext_id,
|
|
637
|
+
"name": dep.name,
|
|
638
|
+
"kind": "external",
|
|
639
|
+
"file": "",
|
|
640
|
+
"line": 0,
|
|
641
|
+
"snippet": "",
|
|
642
|
+
"depth": depth + 1,
|
|
643
|
+
"cycle": False,
|
|
644
|
+
"callees": [],
|
|
645
|
+
}
|
|
646
|
+
# Only add to parent's callees if not already present
|
|
647
|
+
if ext_id not in nodes[parent_node_id]["callees"]:
|
|
648
|
+
nodes[parent_node_id]["callees"].append(ext_id)
|
|
649
|
+
continue
|
|
650
|
+
|
|
651
|
+
# Skip callees in excluded paths
|
|
652
|
+
if exclude and any(callee_func.file_path.startswith(p) for p in exclude):
|
|
653
|
+
continue
|
|
654
|
+
|
|
655
|
+
callee_id = f"f:{callee_func.id}"
|
|
656
|
+
is_cycle = callee_func.id in ancestors
|
|
657
|
+
|
|
658
|
+
if callee_id not in nodes:
|
|
659
|
+
snippet = self._read_one_line_snippet(
|
|
660
|
+
callee_func.file_id, callee_func.location.start_line
|
|
661
|
+
)
|
|
662
|
+
nodes[callee_id] = {
|
|
663
|
+
"id": callee_id,
|
|
664
|
+
"name": callee_func.name,
|
|
665
|
+
"kind": callee_func.type,
|
|
666
|
+
"file": callee_func.file_path,
|
|
667
|
+
"line": callee_func.location.start_line,
|
|
668
|
+
"snippet": snippet,
|
|
669
|
+
"depth": depth + 1,
|
|
670
|
+
"cycle": is_cycle,
|
|
671
|
+
"callees": [],
|
|
672
|
+
}
|
|
673
|
+
elif is_cycle:
|
|
674
|
+
nodes[callee_id]["cycle"] = True
|
|
675
|
+
|
|
676
|
+
# Only add to parent's callees if not already present
|
|
677
|
+
if callee_id not in nodes[parent_node_id]["callees"]:
|
|
678
|
+
nodes[parent_node_id]["callees"].append(callee_id)
|
|
679
|
+
|
|
680
|
+
if not is_cycle and depth + 1 < max_depth:
|
|
681
|
+
queue.append((callee_func.id, callee_id, depth + 1,
|
|
682
|
+
ancestors | {callee_func.id}))
|
|
683
|
+
|
|
684
|
+
sorted_nodes = sorted(nodes.values(), key=lambda n: (n["depth"], n["name"]))
|
|
685
|
+
lines = [json.dumps(node, ensure_ascii=False) for node in sorted_nodes]
|
|
686
|
+
return "\n".join(lines)
|
|
687
|
+
|
|
688
|
+
def _resolve_call_target(self, dep: Dependency, source_file_id: Optional[int] = None) -> Optional['Function']:
|
|
689
|
+
"""Resolve a function_call, method_call, or class_reference dependency to its Function definition.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
dep: The dependency to resolve
|
|
693
|
+
source_file_id: Optional file ID of the source making the call, used for disambiguation
|
|
694
|
+
"""
|
|
695
|
+
if dep.dependency_type == 'function_call':
|
|
696
|
+
# Prefer functions in the same file if available
|
|
697
|
+
if source_file_id:
|
|
698
|
+
funcs = self.get_function_by_name(dep.name, source_file_id)
|
|
699
|
+
if funcs:
|
|
700
|
+
return funcs[0]
|
|
701
|
+
# Fall back to any function with that name
|
|
702
|
+
funcs = self.get_function_by_name(dep.name)
|
|
703
|
+
if funcs:
|
|
704
|
+
return funcs[0]
|
|
705
|
+
elif dep.dependency_type == 'method_call':
|
|
706
|
+
# For method calls, strip the object part (e.g., "self.init" -> "init")
|
|
707
|
+
method_name = dep.name.rsplit('.', 1)[-1] if '.' in dep.name else dep.name
|
|
708
|
+
cursor = self.conn.cursor()
|
|
709
|
+
# Prefer methods in the same file
|
|
710
|
+
if source_file_id:
|
|
711
|
+
cursor.execute(
|
|
712
|
+
"SELECT f.*, file.path FROM functions f JOIN files file ON f.file_id = file.id "
|
|
713
|
+
"WHERE f.name = ? AND f.parent_type = 'class' AND f.file_id = ? LIMIT 1",
|
|
714
|
+
(method_name, source_file_id)
|
|
715
|
+
)
|
|
716
|
+
row = cursor.fetchone()
|
|
717
|
+
if row:
|
|
718
|
+
return Function.from_row(row, row['path'])
|
|
719
|
+
# Fall back to any method with that name
|
|
720
|
+
cursor.execute(
|
|
721
|
+
"SELECT f.*, file.path FROM functions f JOIN files file ON f.file_id = file.id "
|
|
722
|
+
"WHERE f.name = ? AND f.parent_type = 'class' LIMIT 1",
|
|
723
|
+
(method_name,)
|
|
724
|
+
)
|
|
725
|
+
row = cursor.fetchone()
|
|
726
|
+
if row:
|
|
727
|
+
return Function.from_row(row, row['path'])
|
|
728
|
+
elif dep.dependency_type == 'class_reference':
|
|
729
|
+
# For class references (instantiations), try to find the class's __init__ method
|
|
730
|
+
cursor = self.conn.cursor()
|
|
731
|
+
|
|
732
|
+
# Use target_class_id if available (already resolved during indexing)
|
|
733
|
+
class_id = dep.target_class_id
|
|
734
|
+
|
|
735
|
+
# Otherwise, find the class by name
|
|
736
|
+
if class_id is None:
|
|
737
|
+
if source_file_id:
|
|
738
|
+
cursor.execute(
|
|
739
|
+
"SELECT id FROM classes WHERE name = ? AND file_id = ? LIMIT 1",
|
|
740
|
+
(dep.name, source_file_id)
|
|
741
|
+
)
|
|
742
|
+
row = cursor.fetchone()
|
|
743
|
+
if row:
|
|
744
|
+
class_id = row['id']
|
|
745
|
+
|
|
746
|
+
if class_id is None:
|
|
747
|
+
cursor.execute(
|
|
748
|
+
"SELECT id FROM classes WHERE name = ? LIMIT 1",
|
|
749
|
+
(dep.name,)
|
|
750
|
+
)
|
|
751
|
+
row = cursor.fetchone()
|
|
752
|
+
if row:
|
|
753
|
+
class_id = row['id']
|
|
754
|
+
|
|
755
|
+
# If we found the class, get its __init__ method
|
|
756
|
+
if class_id is not None:
|
|
757
|
+
cursor.execute(
|
|
758
|
+
"SELECT f.*, file.path FROM functions f JOIN files file ON f.file_id = file.id "
|
|
759
|
+
"WHERE f.name = '__init__' AND f.parent_id = ? AND f.parent_type = 'class' LIMIT 1",
|
|
760
|
+
(class_id,)
|
|
761
|
+
)
|
|
762
|
+
row = cursor.fetchone()
|
|
763
|
+
if row:
|
|
764
|
+
return Function.from_row(row, row['path'])
|
|
765
|
+
return None
|
|
766
|
+
|
|
767
|
+
def _read_one_line_snippet(self, file_id: int, start_line: int) -> str:
|
|
768
|
+
"""Read the first actual code line after the function signature as a one-line snippet.
|
|
769
|
+
|
|
770
|
+
Skips blank lines, comments, docstrings, and nested function definitions to find the first executable line.
|
|
771
|
+
"""
|
|
772
|
+
# Read up to 50 lines to find the first actual code line (increased for robustness)
|
|
773
|
+
max_lines = 50
|
|
774
|
+
for offset in range(1, max_lines + 1):
|
|
775
|
+
line = self._read_source_lines(file_id, start_line + offset, start_line + offset)
|
|
776
|
+
if not line:
|
|
777
|
+
break
|
|
778
|
+
|
|
779
|
+
stripped = line.strip()
|
|
780
|
+
# Skip empty lines
|
|
781
|
+
if not stripped:
|
|
782
|
+
continue
|
|
783
|
+
# Skip comment lines (Python #, C-style //, etc.)
|
|
784
|
+
if stripped.startswith('#') or stripped.startswith('//') or stripped.startswith('/*'):
|
|
785
|
+
continue
|
|
786
|
+
# Skip docstring-like lines (triple quotes)
|
|
787
|
+
if stripped.startswith('"""') or stripped.startswith("'''"):
|
|
788
|
+
continue
|
|
789
|
+
# Skip decorator lines
|
|
790
|
+
if stripped.startswith('@'):
|
|
791
|
+
continue
|
|
792
|
+
# Skip nested function/class definitions (def, class)
|
|
793
|
+
if stripped.startswith('def ') or stripped.startswith('class '):
|
|
794
|
+
continue
|
|
795
|
+
# Skip opening/closing braces (C#, Java, etc.)
|
|
796
|
+
if stripped in ('{', '}', '};'):
|
|
797
|
+
continue
|
|
798
|
+
# Skip print statements that might be debug output
|
|
799
|
+
if stripped.startswith('print('):
|
|
800
|
+
continue
|
|
801
|
+
|
|
802
|
+
# Found a code line - return it truncated
|
|
803
|
+
return stripped[:120]
|
|
804
|
+
|
|
805
|
+
return ""
|
|
806
|
+
|
|
807
|
+
def get_class_body(self, class_name: str, file_path: Optional[str] = None) -> Optional[str]:
|
|
808
|
+
"""Get the source body of a class by name.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
class_name: Name of the class.
|
|
812
|
+
file_path: Optional relative file path to disambiguate same-name classes.
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
Source code string with description, or None if not found / file unresolvable.
|
|
816
|
+
"""
|
|
817
|
+
file_id = None
|
|
818
|
+
if file_path:
|
|
819
|
+
f = self.get_file_by_path(file_path)
|
|
820
|
+
if f:
|
|
821
|
+
file_id = f.id
|
|
822
|
+
|
|
823
|
+
matches = self.get_class_by_name(class_name, file_id)
|
|
824
|
+
if not matches:
|
|
825
|
+
return None
|
|
826
|
+
|
|
827
|
+
cls = matches[0]
|
|
828
|
+
body = self._read_source_lines(cls.file_id, cls.location.start_line, cls.location.end_line)
|
|
829
|
+
|
|
830
|
+
if cls.description:
|
|
831
|
+
return f"# Description: {cls.description}\n\n{body}"
|
|
832
|
+
return body
|