ruth-code 0.1.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 (102) hide show
  1. frontend/dist/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
  2. frontend/dist/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
  3. frontend/dist/assets/geist-mono-cyrillic-500-normal-CJBLNVQT.woff2 +0 -0
  4. frontend/dist/assets/geist-mono-cyrillic-500-normal-mNhfPmgl.woff +0 -0
  5. frontend/dist/assets/geist-mono-cyrillic-600-normal-CGND36d7.woff2 +0 -0
  6. frontend/dist/assets/geist-mono-cyrillic-600-normal-DrylrLu6.woff +0 -0
  7. frontend/dist/assets/geist-mono-cyrillic-700-normal-DH5Q319x.woff +0 -0
  8. frontend/dist/assets/geist-mono-cyrillic-700-normal-VCNRadI3.woff2 +0 -0
  9. frontend/dist/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
  10. frontend/dist/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
  11. frontend/dist/assets/geist-mono-latin-500-normal-D3o2eNa9.woff2 +0 -0
  12. frontend/dist/assets/geist-mono-latin-500-normal-DOxI7kZ4.woff +0 -0
  13. frontend/dist/assets/geist-mono-latin-600-normal-DQQBcVN0.woff2 +0 -0
  14. frontend/dist/assets/geist-mono-latin-600-normal-DsVeri3b.woff +0 -0
  15. frontend/dist/assets/geist-mono-latin-700-normal-D6izGJRP.woff2 +0 -0
  16. frontend/dist/assets/geist-mono-latin-700-normal-QGw08Lff.woff +0 -0
  17. frontend/dist/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
  18. frontend/dist/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
  19. frontend/dist/assets/geist-mono-latin-ext-500-normal-CQcGuCNt.woff2 +0 -0
  20. frontend/dist/assets/geist-mono-latin-ext-500-normal-diTenJ8L.woff +0 -0
  21. frontend/dist/assets/geist-mono-latin-ext-600-normal-CJwYYto2.woff2 +0 -0
  22. frontend/dist/assets/geist-mono-latin-ext-600-normal-EvIRCXgu.woff +0 -0
  23. frontend/dist/assets/geist-mono-latin-ext-700-normal-BX9f1BHp.woff +0 -0
  24. frontend/dist/assets/geist-mono-latin-ext-700-normal-YOllDaLV.woff2 +0 -0
  25. frontend/dist/assets/index-AEO_WTHY.js +59 -0
  26. frontend/dist/assets/index-JUssvikZ.css +1 -0
  27. frontend/dist/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  28. frontend/dist/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  29. frontend/dist/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  30. frontend/dist/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  31. frontend/dist/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  32. frontend/dist/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  33. frontend/dist/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  34. frontend/dist/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  35. frontend/dist/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  36. frontend/dist/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  37. frontend/dist/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  38. frontend/dist/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  39. frontend/dist/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  40. frontend/dist/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  41. frontend/dist/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  42. frontend/dist/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  43. frontend/dist/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  44. frontend/dist/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  45. frontend/dist/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  46. frontend/dist/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  47. frontend/dist/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  48. frontend/dist/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  49. frontend/dist/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  50. frontend/dist/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  51. frontend/dist/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  52. frontend/dist/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  53. frontend/dist/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  54. frontend/dist/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  55. frontend/dist/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  56. frontend/dist/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  57. frontend/dist/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  58. frontend/dist/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  59. frontend/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  60. frontend/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  61. frontend/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  62. frontend/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  63. frontend/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  64. frontend/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  65. frontend/dist/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  66. frontend/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  67. frontend/dist/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  68. frontend/dist/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  69. frontend/dist/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  70. frontend/dist/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  71. frontend/dist/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  72. frontend/dist/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  73. frontend/dist/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  74. frontend/dist/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  75. frontend/dist/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  76. frontend/dist/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  77. frontend/dist/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  78. frontend/dist/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  79. frontend/dist/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  80. frontend/dist/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  81. frontend/dist/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  82. frontend/dist/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  83. frontend/dist/favicon.svg +1 -0
  84. frontend/dist/icons.svg +24 -0
  85. frontend/dist/index.html +15 -0
  86. frontend/dist/logo.svg +1 -0
  87. ruth/__init__.py +3 -0
  88. ruth/annotations/__init__.py +1 -0
  89. ruth/annotations/complexity.py +128 -0
  90. ruth/annotations/coverage.py +106 -0
  91. ruth/cli.py +167 -0
  92. ruth/graph/__init__.py +1 -0
  93. ruth/graph/engine.py +383 -0
  94. ruth/parser/__init__.py +1 -0
  95. ruth/parser/discovery.py +226 -0
  96. ruth/parser/symbols.py +656 -0
  97. ruth/server.py +162 -0
  98. ruth_code-0.1.0.dist-info/METADATA +106 -0
  99. ruth_code-0.1.0.dist-info/RECORD +102 -0
  100. ruth_code-0.1.0.dist-info/WHEEL +4 -0
  101. ruth_code-0.1.0.dist-info/entry_points.txt +2 -0
  102. ruth_code-0.1.0.dist-info/licenses/LICENSE +21 -0
ruth/parser/symbols.py ADDED
@@ -0,0 +1,656 @@
1
+ """Multi-language symbol extraction using regex-based parsing.
2
+
3
+ This module extracts imports, classes, functions, and call sites from source
4
+ files using language-specific regex patterns. It's designed as the initial
5
+ parser — a tree-sitter implementation can replace it for higher accuracy.
6
+
7
+ Supported: Python, TypeScript/JavaScript, Rust, Go, Java, Ruby, C/C++.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from typing import Literal
15
+
16
+
17
+ # ── Extracted symbol types ─────────────────────────────────────────────
18
+
19
+ @dataclass
20
+ class ImportInfo:
21
+ """A detected import statement."""
22
+ module: str # What's being imported (e.g., "os.path")
23
+ alias: str | None = None # Import alias
24
+ names: list[str] = field(default_factory=list) # Specific names imported
25
+ line: int = 0
26
+
27
+
28
+ @dataclass
29
+ class ClassInfo:
30
+ """A detected class definition."""
31
+ name: str
32
+ parent_class: str | None = None
33
+ methods: list[str] = field(default_factory=list)
34
+ properties: list[str] = field(default_factory=list)
35
+ line: int = 0
36
+ end_line: int = 0
37
+ is_exported: bool = False
38
+
39
+
40
+ @dataclass
41
+ class FunctionInfo:
42
+ """A detected function/method definition."""
43
+ name: str
44
+ params: list[str] = field(default_factory=list)
45
+ return_type: str | None = None
46
+ is_async: bool = False
47
+ is_exported: bool = False
48
+ line: int = 0
49
+ end_line: int = 0
50
+ body: str = ""
51
+
52
+
53
+ @dataclass
54
+ class CallSite:
55
+ """A detected function call."""
56
+ callee: str # Function being called
57
+ line: int = 0
58
+
59
+
60
+ @dataclass
61
+ class FileSymbols:
62
+ """All symbols extracted from a single file."""
63
+ imports: list[ImportInfo] = field(default_factory=list)
64
+ classes: list[ClassInfo] = field(default_factory=list)
65
+ functions: list[FunctionInfo] = field(default_factory=list)
66
+ calls: list[CallSite] = field(default_factory=list)
67
+ export_count: int = 0
68
+
69
+
70
+ # ═══════════════════════════════════════════════════════════════════════
71
+ # Python parser
72
+ # ═══════════════════════════════════════════════════════════════════════
73
+
74
+ _PY_IMPORT = re.compile(
75
+ r"^(?:from\s+([\w.]+)\s+)?import\s+(.+)", re.MULTILINE
76
+ )
77
+ _PY_CLASS = re.compile(
78
+ r"^class\s+(\w+)(?:\(([^)]*)\))?:", re.MULTILINE
79
+ )
80
+ _PY_FUNCTION = re.compile(
81
+ r"^(\s*)(async\s+)?def\s+(\w+)\(([^)]*)\)(?:\s*->\s*([^\s:]+))?:", re.MULTILINE
82
+ )
83
+ _PY_CALL = re.compile(
84
+ r"(?<![.\w])(\w[\w.]*)\s*\(", re.MULTILINE
85
+ )
86
+ _PY_ALL = re.compile(
87
+ r"^__all__\s*=\s*\[([^\]]*)\]", re.MULTILINE
88
+ )
89
+
90
+
91
+ def _parse_python(content: str) -> FileSymbols:
92
+ symbols = FileSymbols()
93
+ lines = content.split("\n")
94
+
95
+ # Imports
96
+ for m in _PY_IMPORT.finditer(content):
97
+ from_module = m.group(1)
98
+ imports_str = m.group(2).strip()
99
+ line = content[:m.start()].count("\n") + 1
100
+
101
+ if from_module:
102
+ names = [n.strip().split(" as ")[0].strip()
103
+ for n in imports_str.split(",")]
104
+ symbols.imports.append(ImportInfo(
105
+ module=from_module, names=names, line=line,
106
+ ))
107
+ else:
108
+ for part in imports_str.split(","):
109
+ part = part.strip()
110
+ parts = part.split(" as ")
111
+ symbols.imports.append(ImportInfo(
112
+ module=parts[0].strip(),
113
+ alias=parts[1].strip() if len(parts) > 1 else None,
114
+ line=line,
115
+ ))
116
+
117
+ # Classes
118
+ for m in _PY_CLASS.finditer(content):
119
+ name = m.group(1)
120
+ parent = m.group(2).strip() if m.group(2) else None
121
+ line = content[:m.start()].count("\n") + 1
122
+
123
+ # Find methods within the class (indented defs)
124
+ methods = []
125
+ properties = []
126
+ class_indent = len(m.group(0)) - len(m.group(0).lstrip())
127
+ in_class = False
128
+ for i, ln in enumerate(lines[line:], start=line + 1):
129
+ stripped = ln.strip()
130
+ if not stripped or stripped.startswith("#"):
131
+ continue
132
+ indent = len(ln) - len(ln.lstrip())
133
+ if in_class and indent <= class_indent and stripped:
134
+ break
135
+ in_class = True
136
+ if indent > class_indent:
137
+ mf = re.match(r"\s*(?:async\s+)?def\s+(\w+)", ln)
138
+ if mf:
139
+ methods.append(mf.group(1))
140
+ mp = re.match(r"\s*self\.(\w+)\s*=", ln)
141
+ if mp:
142
+ properties.append(mp.group(1))
143
+
144
+ symbols.classes.append(ClassInfo(
145
+ name=name, parent_class=parent,
146
+ methods=methods, properties=list(set(properties)),
147
+ line=line, is_exported=not name.startswith("_"),
148
+ ))
149
+
150
+ # Functions (module-level only — indent = 0)
151
+ for m in _PY_FUNCTION.finditer(content):
152
+ indent = m.group(1)
153
+ is_async = bool(m.group(2))
154
+ name = m.group(3)
155
+ params_str = m.group(4)
156
+ return_type = m.group(5)
157
+ line = content[:m.start()].count("\n") + 1
158
+
159
+ if len(indent) == 0: # module-level
160
+ params = [p.strip().split(":")[0].strip()
161
+ for p in params_str.split(",") if p.strip()]
162
+ params = [p for p in params if p != "self" and p != "cls"]
163
+ symbols.functions.append(FunctionInfo(
164
+ name=name, params=params, return_type=return_type,
165
+ is_async=is_async, is_exported=not name.startswith("_"),
166
+ line=line,
167
+ ))
168
+
169
+ # Calls
170
+ skip_calls = {"print", "len", "range", "str", "int", "float", "bool",
171
+ "list", "dict", "set", "tuple", "type", "super", "isinstance",
172
+ "issubclass", "hasattr", "getattr", "setattr", "enumerate",
173
+ "zip", "map", "filter", "sorted", "reversed", "any", "all",
174
+ "open", "next", "iter", "if", "for", "while", "with", "return"}
175
+ for m in _PY_CALL.finditer(content):
176
+ callee = m.group(1)
177
+ if callee not in skip_calls and not callee.startswith("_"):
178
+ line = content[:m.start()].count("\n") + 1
179
+ symbols.calls.append(CallSite(callee=callee, line=line))
180
+
181
+ # Export count
182
+ all_match = _PY_ALL.search(content)
183
+ if all_match:
184
+ symbols.export_count = len([
185
+ n.strip().strip("'\"")
186
+ for n in all_match.group(1).split(",") if n.strip()
187
+ ])
188
+ else:
189
+ symbols.export_count = sum(
190
+ 1 for f in symbols.functions if f.is_exported
191
+ ) + sum(
192
+ 1 for c in symbols.classes if c.is_exported
193
+ )
194
+
195
+ return symbols
196
+
197
+
198
+ # ═══════════════════════════════════════════════════════════════════════
199
+ # TypeScript / JavaScript parser
200
+ # ═══════════════════════════════════════════════════════════════════════
201
+
202
+ _TS_IMPORT = re.compile(
203
+ r"""import\s+(?:(?:\{([^}]*)\}|(\w+))\s+from\s+)?['"]([@\w./-]+)['"]""",
204
+ re.MULTILINE,
205
+ )
206
+ _TS_REQUIRE = re.compile(
207
+ r"""(?:const|let|var)\s+(?:\{([^}]*)\}|(\w+))\s*=\s*require\(['"]([@\w./-]+)['"]\)""",
208
+ re.MULTILINE,
209
+ )
210
+ _TS_CLASS = re.compile(
211
+ r"^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?",
212
+ re.MULTILINE,
213
+ )
214
+ _TS_FUNCTION = re.compile(
215
+ r"^(?:export\s+)?(?:(async)\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\w[\w<>\[\]|&, ]*))?",
216
+ re.MULTILINE,
217
+ )
218
+ _TS_ARROW = re.compile(
219
+ r"^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:(async)\s+)?\(([^)]*)\)(?:\s*:\s*(\w[\w<>\[\]|&, ]*))?\s*=>",
220
+ re.MULTILINE,
221
+ )
222
+ _TS_EXPORT = re.compile(
223
+ r"^export\s+(?:default\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)",
224
+ re.MULTILINE,
225
+ )
226
+ _TS_CALL = re.compile(
227
+ r"(?<![.\w])(\w[\w.]*)\s*\(", re.MULTILINE
228
+ )
229
+
230
+
231
+ def _parse_typescript(content: str) -> FileSymbols:
232
+ symbols = FileSymbols()
233
+
234
+ # Imports (ES6 + require)
235
+ for m in _TS_IMPORT.finditer(content):
236
+ named = m.group(1)
237
+ default = m.group(2)
238
+ module = m.group(3)
239
+ line = content[:m.start()].count("\n") + 1
240
+ names = []
241
+ if named:
242
+ names = [n.strip().split(" as ")[0].strip()
243
+ for n in named.split(",") if n.strip()]
244
+ if default:
245
+ names = [default]
246
+ symbols.imports.append(ImportInfo(module=module, names=names, line=line))
247
+
248
+ for m in _TS_REQUIRE.finditer(content):
249
+ named = m.group(1)
250
+ default = m.group(2)
251
+ module = m.group(3)
252
+ line = content[:m.start()].count("\n") + 1
253
+ names = []
254
+ if named:
255
+ names = [n.strip() for n in named.split(",") if n.strip()]
256
+ if default:
257
+ names = [default]
258
+ symbols.imports.append(ImportInfo(module=module, names=names, line=line))
259
+
260
+ # Classes
261
+ for m in _TS_CLASS.finditer(content):
262
+ name = m.group(1)
263
+ parent = m.group(2)
264
+ line = content[:m.start()].count("\n") + 1
265
+ is_exported = "export" in content[max(0,m.start()-20):m.start()+10]
266
+
267
+ # Find methods (simplified)
268
+ methods = []
269
+ brace_count = 0
270
+ started = False
271
+ method_re = re.compile(r"^\s+(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(\w+)\s*\(")
272
+ for ln in content[m.end():].split("\n"):
273
+ brace_count += ln.count("{") - ln.count("}")
274
+ if "{" in ln and not started:
275
+ started = True
276
+ if started and brace_count <= 0:
277
+ break
278
+ mm = method_re.match(ln)
279
+ if mm and mm.group(1) != "constructor":
280
+ methods.append(mm.group(1))
281
+
282
+ symbols.classes.append(ClassInfo(
283
+ name=name, parent_class=parent, methods=methods,
284
+ line=line, is_exported=is_exported,
285
+ ))
286
+
287
+ # Functions
288
+ for m in _TS_FUNCTION.finditer(content):
289
+ is_async = bool(m.group(1))
290
+ name = m.group(2)
291
+ params_str = m.group(3)
292
+ return_type = m.group(4)
293
+ line = content[:m.start()].count("\n") + 1
294
+ is_exported = "export" in content[max(0,m.start()-20):m.start()+10]
295
+ params = [p.strip().split(":")[0].strip().split("=")[0].strip()
296
+ for p in params_str.split(",") if p.strip()]
297
+ symbols.functions.append(FunctionInfo(
298
+ name=name, params=params, return_type=return_type,
299
+ is_async=is_async, is_exported=is_exported, line=line,
300
+ ))
301
+
302
+ # Arrow functions
303
+ for m in _TS_ARROW.finditer(content):
304
+ name = m.group(1)
305
+ is_async = bool(m.group(2))
306
+ params_str = m.group(3)
307
+ return_type = m.group(4)
308
+ line = content[:m.start()].count("\n") + 1
309
+ is_exported = "export" in content[max(0,m.start()-20):m.start()+10]
310
+ params = [p.strip().split(":")[0].strip().split("=")[0].strip()
311
+ for p in params_str.split(",") if p.strip()]
312
+ symbols.functions.append(FunctionInfo(
313
+ name=name, params=params, return_type=return_type,
314
+ is_async=is_async, is_exported=is_exported, line=line,
315
+ ))
316
+
317
+ # Exports count
318
+ symbols.export_count = len(_TS_EXPORT.findall(content))
319
+
320
+ # Calls
321
+ skip_calls = {"if", "for", "while", "switch", "catch", "return", "throw",
322
+ "new", "typeof", "void", "delete", "console", "require",
323
+ "import", "export", "from", "const", "let", "var"}
324
+ for m in _TS_CALL.finditer(content):
325
+ callee = m.group(1)
326
+ if callee not in skip_calls:
327
+ line = content[:m.start()].count("\n") + 1
328
+ symbols.calls.append(CallSite(callee=callee, line=line))
329
+
330
+ return symbols
331
+
332
+
333
+ # ═══════════════════════════════════════════════════════════════════════
334
+ # Rust parser
335
+ # ═══════════════════════════════════════════════════════════════════════
336
+
337
+ _RS_USE = re.compile(r"^use\s+([\w:{}*,\s]+);", re.MULTILINE)
338
+ _RS_STRUCT = re.compile(r"^(?:pub\s+)?struct\s+(\w+)", re.MULTILINE)
339
+ _RS_IMPL = re.compile(r"^impl(?:<[^>]*>)?\s+(\w+)", re.MULTILINE)
340
+ _RS_FN = re.compile(
341
+ r"^(\s*)(?:pub\s+)?(?:(async)\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*->\s*(\S+))?",
342
+ re.MULTILINE,
343
+ )
344
+ _RS_CALL = re.compile(r"(?<![.\w])(\w[\w:]*)\s*\(", re.MULTILINE)
345
+
346
+
347
+ def _parse_rust(content: str) -> FileSymbols:
348
+ symbols = FileSymbols()
349
+
350
+ for m in _RS_USE.finditer(content):
351
+ path = m.group(1).strip()
352
+ line = content[:m.start()].count("\n") + 1
353
+ module = path.split("::")[0]
354
+ symbols.imports.append(ImportInfo(module=module, line=line))
355
+
356
+ for m in _RS_STRUCT.finditer(content):
357
+ name = m.group(1)
358
+ line = content[:m.start()].count("\n") + 1
359
+ is_exported = "pub" in content[max(0,m.start()-10):m.start()+5]
360
+ methods = []
361
+ # Find impl block methods
362
+ for im in _RS_IMPL.finditer(content):
363
+ if im.group(1) == name:
364
+ brace_count = 0
365
+ started = False
366
+ for ln in content[im.end():].split("\n"):
367
+ brace_count += ln.count("{") - ln.count("}")
368
+ if "{" in ln and not started:
369
+ started = True
370
+ if started and brace_count <= 0:
371
+ break
372
+ fn_match = re.match(r"\s+(?:pub\s+)?(?:async\s+)?fn\s+(\w+)", ln)
373
+ if fn_match:
374
+ methods.append(fn_match.group(1))
375
+ symbols.classes.append(ClassInfo(
376
+ name=name, methods=methods, line=line, is_exported=is_exported,
377
+ ))
378
+
379
+ for m in _RS_FN.finditer(content):
380
+ indent = m.group(1)
381
+ is_async = bool(m.group(2))
382
+ name = m.group(3)
383
+ params_str = m.group(4)
384
+ return_type = m.group(5)
385
+ line = content[:m.start()].count("\n") + 1
386
+ if len(indent) == 0: # module-level
387
+ params = [p.strip().split(":")[0].strip()
388
+ for p in params_str.split(",")
389
+ if p.strip() and "self" not in p]
390
+ is_exported = "pub" in content[max(0,m.start()-10):m.start()+5]
391
+ symbols.functions.append(FunctionInfo(
392
+ name=name, params=params, return_type=return_type,
393
+ is_async=is_async, is_exported=is_exported, line=line,
394
+ ))
395
+
396
+ for m in _RS_CALL.finditer(content):
397
+ callee = m.group(1)
398
+ if callee not in {"if", "for", "while", "match", "loop", "return",
399
+ "Some", "None", "Ok", "Err", "vec", "println",
400
+ "eprintln", "format", "write", "writeln"}:
401
+ line = content[:m.start()].count("\n") + 1
402
+ symbols.calls.append(CallSite(callee=callee, line=line))
403
+
404
+ symbols.export_count = content.count("pub fn ") + content.count("pub struct ")
405
+ return symbols
406
+
407
+
408
+ # ═══════════════════════════════════════════════════════════════════════
409
+ # Go parser
410
+ # ═══════════════════════════════════════════════════════════════════════
411
+
412
+ _GO_IMPORT_SINGLE = re.compile(r'^import\s+"([^"]+)"', re.MULTILINE)
413
+ _GO_IMPORT_BLOCK = re.compile(r"^import\s*\((.*?)\)", re.MULTILINE | re.DOTALL)
414
+ _GO_STRUCT = re.compile(r"^type\s+(\w+)\s+struct\s*{", re.MULTILINE)
415
+ _GO_FUNC = re.compile(
416
+ r"^func\s+(?:\(\w+\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|(\w[\w*. ]*)))?",
417
+ re.MULTILINE,
418
+ )
419
+ _GO_CALL = re.compile(r"(?<![.\w])(\w[\w.]*)\s*\(", re.MULTILINE)
420
+
421
+
422
+ def _parse_go(content: str) -> FileSymbols:
423
+ symbols = FileSymbols()
424
+
425
+ for m in _GO_IMPORT_SINGLE.finditer(content):
426
+ module = m.group(1)
427
+ line = content[:m.start()].count("\n") + 1
428
+ symbols.imports.append(ImportInfo(module=module, line=line))
429
+
430
+ for m in _GO_IMPORT_BLOCK.finditer(content):
431
+ block = m.group(1)
432
+ base_line = content[:m.start()].count("\n") + 1
433
+ for i, ln in enumerate(block.strip().split("\n")):
434
+ ln = ln.strip().strip('"')
435
+ if ln:
436
+ symbols.imports.append(ImportInfo(module=ln, line=base_line + i))
437
+
438
+ for m in _GO_STRUCT.finditer(content):
439
+ name = m.group(1)
440
+ line = content[:m.start()].count("\n") + 1
441
+ is_exported = name[0].isupper()
442
+ symbols.classes.append(ClassInfo(
443
+ name=name, line=line, is_exported=is_exported,
444
+ ))
445
+
446
+ for m in _GO_FUNC.finditer(content):
447
+ receiver_type = m.group(1)
448
+ name = m.group(2)
449
+ params_str = m.group(3)
450
+ return_multi = m.group(4)
451
+ return_single = m.group(5)
452
+ line = content[:m.start()].count("\n") + 1
453
+ is_exported = name[0].isupper() if name else False
454
+
455
+ params = [p.strip().split(" ")[0] for p in params_str.split(",") if p.strip()]
456
+ return_type = return_single or return_multi
457
+
458
+ if receiver_type:
459
+ # Method — add to struct's methods
460
+ for cls in symbols.classes:
461
+ if cls.name == receiver_type:
462
+ cls.methods.append(name)
463
+ break
464
+ else:
465
+ symbols.functions.append(FunctionInfo(
466
+ name=name, params=params, return_type=return_type,
467
+ is_exported=is_exported, line=line,
468
+ ))
469
+
470
+ for m in _GO_CALL.finditer(content):
471
+ callee = m.group(1)
472
+ if callee not in {"if", "for", "range", "switch", "select", "go",
473
+ "defer", "return", "make", "new", "len", "cap",
474
+ "append", "copy", "delete", "close", "panic",
475
+ "recover", "print", "println", "fmt"}:
476
+ line = content[:m.start()].count("\n") + 1
477
+ symbols.calls.append(CallSite(callee=callee, line=line))
478
+
479
+ symbols.export_count = sum(
480
+ 1 for f in symbols.functions if f.is_exported
481
+ ) + sum(
482
+ 1 for c in symbols.classes if c.is_exported
483
+ )
484
+ return symbols
485
+
486
+
487
+ # ═══════════════════════════════════════════════════════════════════════
488
+ # Java parser
489
+ # ═══════════════════════════════════════════════════════════════════════
490
+
491
+ _JV_IMPORT = re.compile(r"^import\s+([\w.]+)(?:\.\*)?;", re.MULTILINE)
492
+ _JV_CLASS = re.compile(
493
+ r"^(?:public\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?",
494
+ re.MULTILINE,
495
+ )
496
+ _JV_METHOD = re.compile(
497
+ r"^\s+(?:public|private|protected)?\s*(?:static\s+)?(?:(synchronized)\s+)?(\w[\w<>\[\],\s]*)\s+(\w+)\s*\(([^)]*)\)",
498
+ re.MULTILINE,
499
+ )
500
+
501
+
502
+ def _parse_java(content: str) -> FileSymbols:
503
+ symbols = FileSymbols()
504
+
505
+ for m in _JV_IMPORT.finditer(content):
506
+ module = m.group(1)
507
+ line = content[:m.start()].count("\n") + 1
508
+ symbols.imports.append(ImportInfo(module=module, line=line))
509
+
510
+ for m in _JV_CLASS.finditer(content):
511
+ name = m.group(1)
512
+ parent = m.group(2)
513
+ line = content[:m.start()].count("\n") + 1
514
+ symbols.classes.append(ClassInfo(
515
+ name=name, parent_class=parent, line=line, is_exported=True,
516
+ ))
517
+
518
+ for m in _JV_METHOD.finditer(content):
519
+ return_type = m.group(2).strip()
520
+ name = m.group(3)
521
+ params_str = m.group(4)
522
+ line = content[:m.start()].count("\n") + 1
523
+ params = [p.strip().split(" ")[-1] for p in params_str.split(",") if p.strip()]
524
+ vis = content[max(0,m.start()-30):m.start()+20]
525
+ symbols.functions.append(FunctionInfo(
526
+ name=name, params=params, return_type=return_type,
527
+ is_exported="public" in vis, line=line,
528
+ ))
529
+
530
+ symbols.export_count = sum(1 for f in symbols.functions if f.is_exported)
531
+ return symbols
532
+
533
+
534
+ # ═══════════════════════════════════════════════════════════════════════
535
+ # Ruby parser
536
+ # ═══════════════════════════════════════════════════════════════════════
537
+
538
+ _RB_REQUIRE = re.compile(r"^require\s+['\"]([^'\"]+)['\"]", re.MULTILINE)
539
+ _RB_CLASS = re.compile(r"^class\s+(\w+)(?:\s*<\s*(\w+))?", re.MULTILINE)
540
+ _RB_DEF = re.compile(r"^(\s*)def\s+(self\.)?(\w+)(?:\(([^)]*)\))?", re.MULTILINE)
541
+
542
+
543
+ def _parse_ruby(content: str) -> FileSymbols:
544
+ symbols = FileSymbols()
545
+
546
+ for m in _RB_REQUIRE.finditer(content):
547
+ module = m.group(1)
548
+ line = content[:m.start()].count("\n") + 1
549
+ symbols.imports.append(ImportInfo(module=module, line=line))
550
+
551
+ for m in _RB_CLASS.finditer(content):
552
+ name = m.group(1)
553
+ parent = m.group(2)
554
+ line = content[:m.start()].count("\n") + 1
555
+ symbols.classes.append(ClassInfo(
556
+ name=name, parent_class=parent, line=line, is_exported=True,
557
+ ))
558
+
559
+ for m in _RB_DEF.finditer(content):
560
+ indent = m.group(1)
561
+ name = m.group(3)
562
+ params_str = m.group(4) or ""
563
+ line = content[:m.start()].count("\n") + 1
564
+ if len(indent) == 0:
565
+ params = [p.strip() for p in params_str.split(",") if p.strip()]
566
+ symbols.functions.append(FunctionInfo(
567
+ name=name, params=params, is_exported=True, line=line,
568
+ ))
569
+
570
+ symbols.export_count = len(symbols.functions) + len(symbols.classes)
571
+ return symbols
572
+
573
+
574
+ # ═══════════════════════════════════════════════════════════════════════
575
+ # C / C++ parser (simplified)
576
+ # ═══════════════════════════════════════════════════════════════════════
577
+
578
+ _C_INCLUDE = re.compile(r'^#include\s+[<"]([^>"]+)[>"]', re.MULTILINE)
579
+ _C_FUNC = re.compile(
580
+ r"^(\w[\w*\s]+)\s+(\w+)\s*\(([^)]*)\)\s*{",
581
+ re.MULTILINE,
582
+ )
583
+ _CPP_CLASS = re.compile(
584
+ r"^class\s+(\w+)(?:\s*:\s*(?:public|private|protected)\s+(\w+))?",
585
+ re.MULTILINE,
586
+ )
587
+
588
+
589
+ def _parse_c_cpp(content: str) -> FileSymbols:
590
+ symbols = FileSymbols()
591
+
592
+ for m in _C_INCLUDE.finditer(content):
593
+ module = m.group(1)
594
+ line = content[:m.start()].count("\n") + 1
595
+ symbols.imports.append(ImportInfo(module=module, line=line))
596
+
597
+ for m in _CPP_CLASS.finditer(content):
598
+ name = m.group(1)
599
+ parent = m.group(2)
600
+ line = content[:m.start()].count("\n") + 1
601
+ symbols.classes.append(ClassInfo(
602
+ name=name, parent_class=parent, line=line, is_exported=True,
603
+ ))
604
+
605
+ for m in _C_FUNC.finditer(content):
606
+ return_type = m.group(1).strip()
607
+ name = m.group(2)
608
+ params_str = m.group(3)
609
+ line = content[:m.start()].count("\n") + 1
610
+ if name not in {"if", "for", "while", "switch", "main"}:
611
+ params = [p.strip().split(" ")[-1].strip("*&")
612
+ for p in params_str.split(",") if p.strip() and p.strip() != "void"]
613
+ symbols.functions.append(FunctionInfo(
614
+ name=name, params=params, return_type=return_type,
615
+ is_exported=True, line=line,
616
+ ))
617
+
618
+ symbols.export_count = len(symbols.functions)
619
+ return symbols
620
+
621
+
622
+ # ═══════════════════════════════════════════════════════════════════════
623
+ # Dispatch
624
+ # ═══════════════════════════════════════════════════════════════════════
625
+
626
+ PARSERS: dict[str, type] = {
627
+ "python": _parse_python,
628
+ "typescript": _parse_typescript,
629
+ "javascript": _parse_typescript, # Same syntax
630
+ "rust": _parse_rust,
631
+ "go": _parse_go,
632
+ "java": _parse_java,
633
+ "ruby": _parse_ruby,
634
+ "c": _parse_c_cpp,
635
+ "cpp": _parse_c_cpp,
636
+ }
637
+
638
+
639
+ def parse_file(content: str, language: str) -> FileSymbols:
640
+ """Parse a source file and extract all symbols.
641
+
642
+ Args:
643
+ content: The file content as a string.
644
+ language: The detected language (python, typescript, etc.)
645
+
646
+ Returns:
647
+ FileSymbols with all extracted imports, classes, functions, calls.
648
+ """
649
+ parser = PARSERS.get(language)
650
+ if parser is None:
651
+ return FileSymbols()
652
+ try:
653
+ return parser(content)
654
+ except Exception:
655
+ # Parsing should never crash — return empty on error
656
+ return FileSymbols()