mne-docs-mcp 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/cache/manager.d.ts +41 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +123 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +120 -0
- package/dist/config.js.map +1 -0
- package/dist/forum/client.d.ts +9 -0
- package/dist/forum/client.d.ts.map +1 -0
- package/dist/forum/client.js +48 -0
- package/dist/forum/client.js.map +1 -0
- package/dist/github/client.d.ts +33 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +198 -0
- package/dist/github/client.js.map +1 -0
- package/dist/github/types.d.ts +238 -0
- package/dist/github/types.d.ts.map +1 -0
- package/dist/github/types.js +2 -0
- package/dist/github/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/bridge.d.ts +46 -0
- package/dist/parser/bridge.d.ts.map +1 -0
- package/dist/parser/bridge.js +225 -0
- package/dist/parser/bridge.js.map +1 -0
- package/dist/parser/types.d.ts +36 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +2 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +218 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/findMneSymbol.d.ts +22 -0
- package/dist/tools/findMneSymbol.d.ts.map +1 -0
- package/dist/tools/findMneSymbol.js +74 -0
- package/dist/tools/findMneSymbol.js.map +1 -0
- package/dist/tools/getMneChangelog.d.ts +31 -0
- package/dist/tools/getMneChangelog.d.ts.map +1 -0
- package/dist/tools/getMneChangelog.js +233 -0
- package/dist/tools/getMneChangelog.js.map +1 -0
- package/dist/tools/getMneDoc.d.ts +19 -0
- package/dist/tools/getMneDoc.d.ts.map +1 -0
- package/dist/tools/getMneDoc.js +248 -0
- package/dist/tools/getMneDoc.js.map +1 -0
- package/dist/tools/getMneExample.d.ts +31 -0
- package/dist/tools/getMneExample.d.ts.map +1 -0
- package/dist/tools/getMneExample.js +212 -0
- package/dist/tools/getMneExample.js.map +1 -0
- package/dist/tools/getMneFile.d.ts +3 -0
- package/dist/tools/getMneFile.d.ts.map +1 -0
- package/dist/tools/getMneFile.js +60 -0
- package/dist/tools/getMneFile.js.map +1 -0
- package/dist/tools/getMneForumTopic.d.ts +33 -0
- package/dist/tools/getMneForumTopic.d.ts.map +1 -0
- package/dist/tools/getMneForumTopic.js +136 -0
- package/dist/tools/getMneForumTopic.js.map +1 -0
- package/dist/tools/getMneIssue.d.ts +7 -0
- package/dist/tools/getMneIssue.d.ts.map +1 -0
- package/dist/tools/getMneIssue.js +22 -0
- package/dist/tools/getMneIssue.js.map +1 -0
- package/dist/tools/getMneIssueComments.d.ts +10 -0
- package/dist/tools/getMneIssueComments.d.ts.map +1 -0
- package/dist/tools/getMneIssueComments.js +26 -0
- package/dist/tools/getMneIssueComments.js.map +1 -0
- package/dist/tools/getRelatedSymbols.d.ts +26 -0
- package/dist/tools/getRelatedSymbols.d.ts.map +1 -0
- package/dist/tools/getRelatedSymbols.js +120 -0
- package/dist/tools/getRelatedSymbols.js.map +1 -0
- package/dist/tools/getSymbolReferences.d.ts +11 -0
- package/dist/tools/getSymbolReferences.d.ts.map +1 -0
- package/dist/tools/getSymbolReferences.js +251 -0
- package/dist/tools/getSymbolReferences.js.map +1 -0
- package/dist/tools/listMneVersions.d.ts +8 -0
- package/dist/tools/listMneVersions.d.ts.map +1 -0
- package/dist/tools/listMneVersions.js +32 -0
- package/dist/tools/listMneVersions.js.map +1 -0
- package/dist/tools/lookupMneError.d.ts +59 -0
- package/dist/tools/lookupMneError.d.ts.map +1 -0
- package/dist/tools/lookupMneError.js +280 -0
- package/dist/tools/lookupMneError.js.map +1 -0
- package/dist/tools/searchMneDocs.d.ts +3 -0
- package/dist/tools/searchMneDocs.d.ts.map +1 -0
- package/dist/tools/searchMneDocs.js +58 -0
- package/dist/tools/searchMneDocs.js.map +1 -0
- package/dist/tools/searchMneForum.d.ts +9 -0
- package/dist/tools/searchMneForum.d.ts.map +1 -0
- package/dist/tools/searchMneForum.js +29 -0
- package/dist/tools/searchMneForum.js.map +1 -0
- package/dist/tools/searchMneIssues.d.ts +8 -0
- package/dist/tools/searchMneIssues.d.ts.map +1 -0
- package/dist/tools/searchMneIssues.js +23 -0
- package/dist/tools/searchMneIssues.js.map +1 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/docstring.d.ts +26 -0
- package/dist/utils/docstring.d.ts.map +1 -0
- package/dist/utils/docstring.js +261 -0
- package/dist/utils/docstring.js.map +1 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +73 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/metrics.d.ts +38 -0
- package/dist/utils/metrics.d.ts.map +1 -0
- package/dist/utils/metrics.js +57 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +71 -0
- package/python/ast_extractor.py +422 -0
- package/python/parser.py +68 -0
- package/python/requirements.txt +2 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
|
|
4
|
+
def extract_docstring_params(docstring: str) -> List[Dict[str, str]]:
|
|
5
|
+
"""Parse numpy-style docstring to extract parameter info."""
|
|
6
|
+
params = []
|
|
7
|
+
if not docstring:
|
|
8
|
+
return params
|
|
9
|
+
|
|
10
|
+
lines = docstring.split('\n')
|
|
11
|
+
in_params = False
|
|
12
|
+
current_param = None
|
|
13
|
+
|
|
14
|
+
for line in lines:
|
|
15
|
+
line = line.strip()
|
|
16
|
+
if line == 'Parameters':
|
|
17
|
+
in_params = True
|
|
18
|
+
continue
|
|
19
|
+
if line.startswith('----------') or line == 'Returns':
|
|
20
|
+
if in_params:
|
|
21
|
+
break
|
|
22
|
+
|
|
23
|
+
if in_params and line:
|
|
24
|
+
# Check for parameter definition "name : type"
|
|
25
|
+
if ' : ' in line:
|
|
26
|
+
parts = line.split(' : ')
|
|
27
|
+
name = parts[0].strip()
|
|
28
|
+
type_info = parts[1].strip()
|
|
29
|
+
current_param = {'name': name, 'type': type_info, 'description': ''}
|
|
30
|
+
params.append(current_param)
|
|
31
|
+
elif current_param:
|
|
32
|
+
# Append description
|
|
33
|
+
if current_param['description']:
|
|
34
|
+
current_param['description'] += ' ' + line
|
|
35
|
+
else:
|
|
36
|
+
current_param['description'] = line
|
|
37
|
+
|
|
38
|
+
return params
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_docstring_examples(docstring: str) -> List[str]:
|
|
42
|
+
"""Extract code examples from numpy-style docstring."""
|
|
43
|
+
examples = []
|
|
44
|
+
if not docstring:
|
|
45
|
+
return examples
|
|
46
|
+
|
|
47
|
+
lines = docstring.split('\n')
|
|
48
|
+
in_examples = False
|
|
49
|
+
skip_separator = False
|
|
50
|
+
current_example = []
|
|
51
|
+
|
|
52
|
+
for line in lines:
|
|
53
|
+
stripped = line.strip()
|
|
54
|
+
|
|
55
|
+
# Check for Examples section
|
|
56
|
+
if stripped == 'Examples':
|
|
57
|
+
in_examples = True
|
|
58
|
+
skip_separator = True # Skip the next line (-------)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Skip the separator line after section header
|
|
62
|
+
if skip_separator:
|
|
63
|
+
skip_separator = False
|
|
64
|
+
if stripped.startswith('---'):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Check for end of Examples section (next section header)
|
|
68
|
+
if in_examples and stripped in ('See Also', 'Notes', 'References', 'Attributes', 'Methods', 'Returns', 'Parameters'):
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if in_examples:
|
|
72
|
+
# Detect start of code block with >>>
|
|
73
|
+
if stripped.startswith('>>>'):
|
|
74
|
+
if current_example:
|
|
75
|
+
examples.append('\n'.join(current_example))
|
|
76
|
+
current_example = [stripped[4:] if stripped.startswith('>>> ') else stripped[3:]]
|
|
77
|
+
elif stripped.startswith('...'):
|
|
78
|
+
# Continuation line
|
|
79
|
+
if current_example:
|
|
80
|
+
current_example.append(stripped[4:] if stripped.startswith('... ') else stripped[3:])
|
|
81
|
+
elif current_example and stripped and not stripped.startswith('---'):
|
|
82
|
+
# Output line - include it as comment
|
|
83
|
+
current_example.append('# Output: ' + stripped)
|
|
84
|
+
|
|
85
|
+
# Don't forget the last example
|
|
86
|
+
if current_example:
|
|
87
|
+
examples.append('\n'.join(current_example))
|
|
88
|
+
|
|
89
|
+
return examples[:5] # Limit to 5 examples
|
|
90
|
+
|
|
91
|
+
def get_decorators(node: ast.AST) -> List[str]:
|
|
92
|
+
decorators = []
|
|
93
|
+
if hasattr(node, 'decorator_list'):
|
|
94
|
+
for decorator in node.decorator_list:
|
|
95
|
+
if isinstance(decorator, ast.Name):
|
|
96
|
+
decorators.append(decorator.id)
|
|
97
|
+
elif isinstance(decorator, ast.Call):
|
|
98
|
+
if isinstance(decorator.func, ast.Name):
|
|
99
|
+
decorators.append(decorator.func.id)
|
|
100
|
+
elif isinstance(decorator.func, ast.Attribute):
|
|
101
|
+
decorators.append(decorator.func.attr)
|
|
102
|
+
return decorators
|
|
103
|
+
|
|
104
|
+
def get_signature(node: ast.FunctionDef) -> str:
|
|
105
|
+
args = []
|
|
106
|
+
# Handle positional args
|
|
107
|
+
for arg in node.args.args:
|
|
108
|
+
arg_str = arg.arg
|
|
109
|
+
if arg.annotation:
|
|
110
|
+
if isinstance(arg.annotation, ast.Name):
|
|
111
|
+
arg_str += f": {arg.annotation.id}"
|
|
112
|
+
elif isinstance(arg.annotation, ast.Constant):
|
|
113
|
+
arg_str += f": {arg.annotation.value}"
|
|
114
|
+
args.append(arg_str)
|
|
115
|
+
|
|
116
|
+
# Handle defaults (naive approach, matching from end)
|
|
117
|
+
if node.args.defaults:
|
|
118
|
+
for i, default in enumerate(reversed(node.args.defaults)):
|
|
119
|
+
if isinstance(default, ast.Constant):
|
|
120
|
+
args[-(i+1)] += f"={default.value}"
|
|
121
|
+
|
|
122
|
+
return f"({', '.join(args)})"
|
|
123
|
+
|
|
124
|
+
class SymbolVisitor(ast.NodeVisitor):
|
|
125
|
+
def __init__(self, module_name: str = "", file_url: str = ""):
|
|
126
|
+
self.symbols = []
|
|
127
|
+
self.module_name = module_name
|
|
128
|
+
self.file_url = file_url
|
|
129
|
+
self.current_class = None
|
|
130
|
+
|
|
131
|
+
def visit_ClassDef(self, node):
|
|
132
|
+
qualified_name = f"{self.module_name}.{node.name}" if self.module_name else node.name
|
|
133
|
+
|
|
134
|
+
symbol = {
|
|
135
|
+
'name': node.name,
|
|
136
|
+
'qualifiedName': qualified_name,
|
|
137
|
+
'kind': 'class',
|
|
138
|
+
'signature': f"class {node.name}",
|
|
139
|
+
'docstring': ast.get_docstring(node) or "",
|
|
140
|
+
'lineno': node.lineno,
|
|
141
|
+
'endLineno': node.end_lineno,
|
|
142
|
+
'decorators': get_decorators(node),
|
|
143
|
+
'baseClasses': [b.id for b in node.bases if isinstance(b, ast.Name)],
|
|
144
|
+
'parameters': extract_docstring_params(ast.get_docstring(node) or "")
|
|
145
|
+
}
|
|
146
|
+
self.symbols.append(symbol)
|
|
147
|
+
|
|
148
|
+
old_class = self.current_class
|
|
149
|
+
self.current_class = qualified_name
|
|
150
|
+
self.generic_visit(node)
|
|
151
|
+
self.current_class = old_class
|
|
152
|
+
|
|
153
|
+
def visit_FunctionDef(self, node):
|
|
154
|
+
is_method = self.current_class is not None
|
|
155
|
+
name = node.name
|
|
156
|
+
qualified_name = f"{self.current_class}.{name}" if is_method else (f"{self.module_name}.{name}" if self.module_name else name)
|
|
157
|
+
|
|
158
|
+
symbol = {
|
|
159
|
+
'name': name,
|
|
160
|
+
'qualifiedName': qualified_name,
|
|
161
|
+
'kind': 'method' if is_method else 'function',
|
|
162
|
+
'signature': f"def {name}{get_signature(node)}",
|
|
163
|
+
'docstring': ast.get_docstring(node) or "",
|
|
164
|
+
'lineno': node.lineno,
|
|
165
|
+
'endLineno': node.end_lineno,
|
|
166
|
+
'decorators': get_decorators(node),
|
|
167
|
+
'parameters': extract_docstring_params(ast.get_docstring(node) or "")
|
|
168
|
+
}
|
|
169
|
+
self.symbols.append(symbol)
|
|
170
|
+
self.generic_visit(node)
|
|
171
|
+
|
|
172
|
+
def parse_file(source: str, module_name: str = "", file_url: str = "") -> List[Dict[str, Any]]:
|
|
173
|
+
try:
|
|
174
|
+
tree = ast.parse(source)
|
|
175
|
+
visitor = SymbolVisitor(module_name, file_url)
|
|
176
|
+
visitor.visit(tree)
|
|
177
|
+
return visitor.symbols
|
|
178
|
+
except Exception:
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
def extract_symbol(source: str, symbol_name: str, module_name: str = "", file_url: str = "") -> Optional[Dict[str, Any]]:
|
|
182
|
+
symbols = parse_file(source, module_name, file_url)
|
|
183
|
+
result = None
|
|
184
|
+
|
|
185
|
+
# Try exact match first
|
|
186
|
+
for s in symbols:
|
|
187
|
+
if s['name'] == symbol_name or s['qualifiedName'] == symbol_name:
|
|
188
|
+
result = s
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
# Try partial match for methods (e.g. "Epochs.plot" matching "plot" inside "Epochs")
|
|
192
|
+
if result is None and '.' in symbol_name:
|
|
193
|
+
parts = symbol_name.split('.')
|
|
194
|
+
short_name = parts[-1]
|
|
195
|
+
for s in symbols:
|
|
196
|
+
if s['name'] == short_name and (s['qualifiedName'].endswith(symbol_name) or symbol_name.endswith(s['qualifiedName'])):
|
|
197
|
+
result = s
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
if result:
|
|
201
|
+
# Extract examples from docstring
|
|
202
|
+
examples = extract_docstring_examples(result.get('docstring', ''))
|
|
203
|
+
if examples:
|
|
204
|
+
result['examples'] = examples
|
|
205
|
+
|
|
206
|
+
# If we found a class, also include its methods with full details
|
|
207
|
+
if result['kind'] == 'class':
|
|
208
|
+
class_qualified_name = result['qualifiedName']
|
|
209
|
+
methods = []
|
|
210
|
+
for s in symbols:
|
|
211
|
+
if s['kind'] == 'method' and s['qualifiedName'].startswith(class_qualified_name + '.'):
|
|
212
|
+
# Include __init__ and public methods
|
|
213
|
+
if not s['name'].startswith('_') or s['name'] == '__init__':
|
|
214
|
+
# Get a meaningful docstring summary (first line or first sentence)
|
|
215
|
+
docstring = s.get('docstring', '')
|
|
216
|
+
if docstring:
|
|
217
|
+
# Get first non-empty line as summary
|
|
218
|
+
lines = [l.strip() for l in docstring.split('\n') if l.strip()]
|
|
219
|
+
summary = lines[0] if lines else ''
|
|
220
|
+
# Truncate if too long
|
|
221
|
+
if len(summary) > 200:
|
|
222
|
+
summary = summary[:197] + '...'
|
|
223
|
+
else:
|
|
224
|
+
summary = ''
|
|
225
|
+
|
|
226
|
+
methods.append({
|
|
227
|
+
'name': s['name'],
|
|
228
|
+
'signature': s['signature'],
|
|
229
|
+
'docstring': summary,
|
|
230
|
+
'lineno': s.get('lineno'),
|
|
231
|
+
'parameters': s.get('parameters', [])[:5], # First 5 params
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
# Sort methods: __init__ first, then alphabetically
|
|
235
|
+
methods.sort(key=lambda m: (0 if m['name'] == '__init__' else 1, m['name']))
|
|
236
|
+
result['methods'] = methods[:30] # Limit to 30 methods
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
def find_symbols(source: str, query: str, module_name: str = "", max_results: int = 20) -> List[Dict[str, Any]]:
|
|
241
|
+
symbols = parse_file(source, module_name)
|
|
242
|
+
query = query.lower()
|
|
243
|
+
matches = []
|
|
244
|
+
|
|
245
|
+
for s in symbols:
|
|
246
|
+
if query in s['name'].lower() or query in s['qualifiedName'].lower():
|
|
247
|
+
matches.append(s)
|
|
248
|
+
|
|
249
|
+
return matches[:max_results]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class CrossReferenceVisitor(ast.NodeVisitor):
|
|
253
|
+
"""Extract function calls and attribute accesses from source code."""
|
|
254
|
+
|
|
255
|
+
def __init__(self, module_name: str = ""):
|
|
256
|
+
self.module_name = module_name
|
|
257
|
+
self.imports = {} # alias -> full module path
|
|
258
|
+
self.from_imports = {} # name -> (module, original_name)
|
|
259
|
+
self.calls = [] # List of function/method calls
|
|
260
|
+
self.current_function = None
|
|
261
|
+
self.current_class = None
|
|
262
|
+
|
|
263
|
+
def visit_Import(self, node):
|
|
264
|
+
for alias in node.names:
|
|
265
|
+
name = alias.asname or alias.name
|
|
266
|
+
self.imports[name] = alias.name
|
|
267
|
+
self.generic_visit(node)
|
|
268
|
+
|
|
269
|
+
def visit_ImportFrom(self, node):
|
|
270
|
+
module = node.module or ''
|
|
271
|
+
for alias in node.names:
|
|
272
|
+
name = alias.asname or alias.name
|
|
273
|
+
self.from_imports[name] = (module, alias.name)
|
|
274
|
+
self.generic_visit(node)
|
|
275
|
+
|
|
276
|
+
def visit_ClassDef(self, node):
|
|
277
|
+
old_class = self.current_class
|
|
278
|
+
self.current_class = node.name
|
|
279
|
+
self.generic_visit(node)
|
|
280
|
+
self.current_class = old_class
|
|
281
|
+
|
|
282
|
+
def visit_FunctionDef(self, node):
|
|
283
|
+
old_func = self.current_function
|
|
284
|
+
if self.current_class:
|
|
285
|
+
self.current_function = f"{self.current_class}.{node.name}"
|
|
286
|
+
else:
|
|
287
|
+
self.current_function = node.name
|
|
288
|
+
self.generic_visit(node)
|
|
289
|
+
self.current_function = old_func
|
|
290
|
+
|
|
291
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
292
|
+
|
|
293
|
+
def visit_Call(self, node):
|
|
294
|
+
call_info = self._resolve_call(node)
|
|
295
|
+
if call_info:
|
|
296
|
+
call_info['caller'] = self.current_function
|
|
297
|
+
call_info['lineno'] = node.lineno
|
|
298
|
+
self.calls.append(call_info)
|
|
299
|
+
self.generic_visit(node)
|
|
300
|
+
|
|
301
|
+
def _resolve_call(self, node) -> Optional[Dict[str, Any]]:
|
|
302
|
+
"""Resolve a Call node to a callee name."""
|
|
303
|
+
func = node.func
|
|
304
|
+
|
|
305
|
+
if isinstance(func, ast.Name):
|
|
306
|
+
# Direct function call: func()
|
|
307
|
+
name = func.id
|
|
308
|
+
if name in self.from_imports:
|
|
309
|
+
module, orig_name = self.from_imports[name]
|
|
310
|
+
return {'callee': f"{module}.{orig_name}", 'kind': 'function'}
|
|
311
|
+
return {'callee': name, 'kind': 'function'}
|
|
312
|
+
|
|
313
|
+
elif isinstance(func, ast.Attribute):
|
|
314
|
+
# Method/attribute call: obj.method()
|
|
315
|
+
parts = []
|
|
316
|
+
current = func
|
|
317
|
+
while isinstance(current, ast.Attribute):
|
|
318
|
+
parts.append(current.attr)
|
|
319
|
+
current = current.value
|
|
320
|
+
|
|
321
|
+
if isinstance(current, ast.Name):
|
|
322
|
+
parts.append(current.id)
|
|
323
|
+
parts.reverse()
|
|
324
|
+
|
|
325
|
+
# Check if first part is an import alias
|
|
326
|
+
first = parts[0]
|
|
327
|
+
if first in self.imports:
|
|
328
|
+
parts[0] = self.imports[first]
|
|
329
|
+
elif first in self.from_imports:
|
|
330
|
+
module, orig_name = self.from_imports[first]
|
|
331
|
+
parts[0] = f"{module}.{orig_name}"
|
|
332
|
+
|
|
333
|
+
return {'callee': '.'.join(parts), 'kind': 'method'}
|
|
334
|
+
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def extract_cross_references(source: str, symbol_name: str, module_name: str = "") -> Dict[str, Any]:
|
|
339
|
+
"""
|
|
340
|
+
Extract cross-references for a symbol: what it calls (callees) and what calls it (callers).
|
|
341
|
+
|
|
342
|
+
symbol_name can be:
|
|
343
|
+
- "function_name" for module-level functions
|
|
344
|
+
- "ClassName.method_name" for class methods
|
|
345
|
+
- "ClassName" for all methods in a class
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
tree = ast.parse(source)
|
|
349
|
+
except SyntaxError:
|
|
350
|
+
return {'symbol': symbol_name, 'callees': [], 'callers': [], 'error': 'Syntax error in source'}
|
|
351
|
+
|
|
352
|
+
visitor = CrossReferenceVisitor(module_name)
|
|
353
|
+
visitor.visit(tree)
|
|
354
|
+
|
|
355
|
+
# Parse symbol_name to handle Class.method format
|
|
356
|
+
class_context = None
|
|
357
|
+
method_name = symbol_name
|
|
358
|
+
if '.' in symbol_name:
|
|
359
|
+
parts = symbol_name.split('.')
|
|
360
|
+
# Check if first part looks like a class (PascalCase)
|
|
361
|
+
if parts[0] and parts[0][0].isupper():
|
|
362
|
+
class_context = parts[0]
|
|
363
|
+
method_name = parts[1] if len(parts) > 1 else None
|
|
364
|
+
|
|
365
|
+
# Find callees (what the symbol calls)
|
|
366
|
+
callees = []
|
|
367
|
+
# Find callers (what calls the symbol)
|
|
368
|
+
callers = []
|
|
369
|
+
|
|
370
|
+
for call in visitor.calls:
|
|
371
|
+
caller = call.get('caller')
|
|
372
|
+
callee = call.get('callee', '')
|
|
373
|
+
|
|
374
|
+
# Check if this call is FROM our symbol (symbol is the caller)
|
|
375
|
+
caller_matches = False
|
|
376
|
+
if caller:
|
|
377
|
+
if class_context and method_name:
|
|
378
|
+
# Looking for ClassName.method_name
|
|
379
|
+
caller_matches = caller == f"{class_context}.{method_name}"
|
|
380
|
+
elif class_context and not method_name:
|
|
381
|
+
# Looking for any method in ClassName
|
|
382
|
+
caller_matches = caller.startswith(f"{class_context}.")
|
|
383
|
+
else:
|
|
384
|
+
# Looking for a function name
|
|
385
|
+
caller_matches = (caller == symbol_name or
|
|
386
|
+
caller.endswith(f".{symbol_name}"))
|
|
387
|
+
|
|
388
|
+
if caller_matches:
|
|
389
|
+
callees.append({
|
|
390
|
+
'name': callee,
|
|
391
|
+
'kind': call.get('kind'),
|
|
392
|
+
'lineno': call.get('lineno')
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
# Check if this call is TO our symbol (symbol is the callee)
|
|
396
|
+
callee_matches = (callee == symbol_name or
|
|
397
|
+
callee.endswith(f".{symbol_name}") or
|
|
398
|
+
(method_name and callee.endswith(f".{method_name}")))
|
|
399
|
+
|
|
400
|
+
if callee_matches:
|
|
401
|
+
callers.append({
|
|
402
|
+
'name': caller or '<module>',
|
|
403
|
+
'kind': 'function' if caller else 'module',
|
|
404
|
+
'lineno': call.get('lineno')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
# Deduplicate callees by name
|
|
408
|
+
seen_callees = set()
|
|
409
|
+
unique_callees = []
|
|
410
|
+
for c in callees:
|
|
411
|
+
if c['name'] not in seen_callees:
|
|
412
|
+
seen_callees.add(c['name'])
|
|
413
|
+
unique_callees.append(c)
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
'symbol': symbol_name,
|
|
417
|
+
'module': module_name,
|
|
418
|
+
'callees': unique_callees[:50], # Limit results
|
|
419
|
+
'callers': callers[:50],
|
|
420
|
+
'totalCallees': len(unique_callees),
|
|
421
|
+
'totalCallers': len(callers)
|
|
422
|
+
}
|
package/python/parser.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Python AST parser for MNE documentation extraction.
|
|
4
|
+
Communicates with TypeScript via stdin/stdout JSON.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
from typing import Optional, List, Dict, Any
|
|
12
|
+
from ast_extractor import extract_symbol, find_symbols, parse_file, extract_cross_references
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
"""Main entry point - read command from stdin, write result to stdout."""
|
|
16
|
+
# Ensure stdout is using utf-8
|
|
17
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
18
|
+
sys.stdin.reconfigure(encoding='utf-8')
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# Read all input from stdin
|
|
22
|
+
input_data = sys.stdin.read()
|
|
23
|
+
if not input_data:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
command = json.loads(input_data)
|
|
27
|
+
action = command.get('action')
|
|
28
|
+
|
|
29
|
+
result = None
|
|
30
|
+
|
|
31
|
+
if action == 'parse_file':
|
|
32
|
+
result = parse_file(
|
|
33
|
+
command['source'],
|
|
34
|
+
command.get('moduleName', ''),
|
|
35
|
+
command.get('fileUrl', '')
|
|
36
|
+
)
|
|
37
|
+
elif action == 'extract_symbol':
|
|
38
|
+
result = extract_symbol(
|
|
39
|
+
command['source'],
|
|
40
|
+
command['symbolName'],
|
|
41
|
+
command.get('moduleName', ''),
|
|
42
|
+
command.get('fileUrl', '')
|
|
43
|
+
)
|
|
44
|
+
elif action == 'find_symbols':
|
|
45
|
+
result = find_symbols(
|
|
46
|
+
command['source'],
|
|
47
|
+
command['query'],
|
|
48
|
+
command.get('moduleName', ''),
|
|
49
|
+
command.get('maxResults', 20)
|
|
50
|
+
)
|
|
51
|
+
elif action == 'extract_cross_references':
|
|
52
|
+
result = extract_cross_references(
|
|
53
|
+
command['source'],
|
|
54
|
+
command['symbolName'],
|
|
55
|
+
command.get('moduleName', '')
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
result = {'error': f'Unknown action: {action}'}
|
|
59
|
+
|
|
60
|
+
json.dump({'success': True, 'data': result}, sys.stdout)
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
# Capture full traceback for debugging
|
|
64
|
+
tb = traceback.format_exc()
|
|
65
|
+
json.dump({'success': False, 'error': str(e), 'traceback': tb}, sys.stdout)
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
main()
|