codesynapt 0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +686 -0
  3. package/LICENSES.md +141 -0
  4. package/README.md +331 -0
  5. package/electron/main.cjs +2849 -0
  6. package/electron/plugin-loader.cjs +184 -0
  7. package/electron/preload.cjs +108 -0
  8. package/package.json +216 -0
  9. package/packages/core/bin/codesynapt-mcp.cjs +611 -0
  10. package/packages/core/bin/codesynapt.cjs +1933 -0
  11. package/packages/core/legacy.js +300 -0
  12. package/packages/core/lib/control-server.cjs +1539 -0
  13. package/packages/core/lib/embedding.cjs +89 -0
  14. package/packages/core/lib/logger.cjs +63 -0
  15. package/packages/core/lib/search-cache.cjs +140 -0
  16. package/packages/core/lib/search-worker.cjs +255 -0
  17. package/packages/core/lib/search.cjs +211 -0
  18. package/packages/core/lib/symbol-graph.cjs +402 -0
  19. package/packages/core/lib/symbol-parser-js.cjs +542 -0
  20. package/packages/core/lib/symbol-parser-misc.cjs +394 -0
  21. package/packages/core/lib/symbol-parser-py.cjs +215 -0
  22. package/packages/core/lib/symbol-parser-treesitter.cjs +658 -0
  23. package/packages/core/lib/symbol-parser-tsc.cjs +332 -0
  24. package/packages/core/monorepo.js +310 -0
  25. package/packages/core/parser.js +2234 -0
  26. package/packages/core/scanner.js +623 -0
  27. package/plugin-api/LICENSE +21 -0
  28. package/plugin-api/README.md +114 -0
  29. package/plugin-api/docs/01-getting-started.md +197 -0
  30. package/plugin-api/docs/02-concepts.md +269 -0
  31. package/plugin-api/docs/api-reference.md +463 -0
  32. package/plugin-api/docs/troubleshooting.md +332 -0
  33. package/plugin-api/docs/types/exporter.md +377 -0
  34. package/plugin-api/docs/types/theme.md +312 -0
  35. package/plugin-api/examples/hello-world-plugin/README.md +70 -0
  36. package/plugin-api/examples/hello-world-plugin/main.js +36 -0
  37. package/plugin-api/examples/hello-world-plugin/manifest.json +12 -0
  38. package/plugin-api/examples/mermaid-exporter/README.md +125 -0
  39. package/plugin-api/examples/mermaid-exporter/main.js +58 -0
  40. package/plugin-api/examples/mermaid-exporter/manifest.json +12 -0
  41. package/plugin-api/examples/rust-parser/README.md +71 -0
  42. package/plugin-api/examples/rust-parser/main.js +123 -0
  43. package/plugin-api/examples/rust-parser/manifest.json +12 -0
  44. package/plugin-api/examples/sunset-theme/README.md +95 -0
  45. package/plugin-api/examples/sunset-theme/manifest.json +12 -0
  46. package/plugin-api/examples/sunset-theme/theme.css +31 -0
  47. package/plugin-api/package.json +20 -0
  48. package/plugin-api/types.d.ts +395 -0
  49. package/public/app.js +6837 -0
  50. package/public/backend.js +285 -0
  51. package/public/index.html +647 -0
  52. package/public/plugin-host.js +321 -0
  53. package/public/style.css +4359 -0
  54. package/public/vendor/three.module.js +53044 -0
  55. package/scripts/competitor-watch.mjs +144 -0
  56. package/scripts/copy-vendor.js +21 -0
  57. package/scripts/download-bundled-node.cjs +53 -0
  58. package/scripts/fuses-after-pack.cjs +34 -0
  59. package/scripts/license-check.js +119 -0
  60. package/scripts/perf-test.js +200 -0
  61. package/server.js +132 -0
@@ -0,0 +1,394 @@
1
+ // Symbol parsers for Go, Rust, Java/Kotlin, Swift — regex baseline.
2
+ //
3
+ // These are intentionally simple — they catch top-level declarations
4
+ // and obvious method definitions, and resolve calls by name-match
5
+ // (preferring same-file matches). Stage 3 (tree-sitter) will replace
6
+ // them for accuracy parity with codegraph.
7
+ //
8
+ // One module exports one parser per language to keep things easy to
9
+ // reason about; each parser conforms to the
10
+ // { extractSymbols, extractReferences } shape consumed by SymbolGraph.
11
+
12
+ 'use strict'
13
+
14
+ function mkId(file, name, line) { return `${file}#${name}@${line}` }
15
+
16
+ // Regex parsers only emit `call` edges (no expression-level ref
17
+ // pass), so the any-file fallback is on by default — name + `(`
18
+ // is already a strong signal.
19
+ function makeResolver(fileId, index) {
20
+ return function resolve(name) {
21
+ return index.resolveCall ? index.resolveCall(fileId, name, { allowAny: true }) : null
22
+ }
23
+ }
24
+
25
+ // Build a per-file "which symbol contains this line" lookup.
26
+ function makeEnclosingLookup(fileId, index, kinds = ['function', 'method']) {
27
+ const ids = index.byFile.get(fileId)
28
+ if (!ids) return () => null
29
+ const ranges = []
30
+ for (const id of ids) {
31
+ const n = index.nodes.get(id)
32
+ if (!n) continue
33
+ if (kinds && !kinds.includes(n.kind)) continue
34
+ ranges.push({ id, start: n.startLine, end: n.endLine })
35
+ }
36
+ ranges.sort((a, b) => a.start - b.start)
37
+ return function enclosingId(lineNum) {
38
+ let best = null
39
+ for (const r of ranges) {
40
+ if (r.start <= lineNum && lineNum <= r.end) {
41
+ if (!best || r.start > best.start) best = r
42
+ }
43
+ }
44
+ return best?.id || null
45
+ }
46
+ }
47
+
48
+ // Approximate end-line by walking forward tracking brace depth from a
49
+ // `{` at the end of `startLine`. Returns startLine if no brace found
50
+ // nearby (e.g. struct field, single-line decl).
51
+ function braceBlockEnd(lines, startLine) {
52
+ // 1-based startLine → 0-based index
53
+ let i = startLine - 1
54
+ let depth = 0
55
+ let started = false
56
+ while (i < lines.length) {
57
+ const line = lines[i]
58
+ for (let j = 0; j < line.length; j++) {
59
+ const c = line[j]
60
+ if (c === '{') { depth++; started = true }
61
+ else if (c === '}') {
62
+ depth--
63
+ if (started && depth === 0) return i + 1
64
+ }
65
+ }
66
+ i++
67
+ }
68
+ return Math.min(startLine + 50, lines.length)
69
+ }
70
+
71
+ // ─── Go ─────────────────────────────────────────────────────────
72
+ // - `func Name(` or `func (r *Recv) Name(`
73
+ // - `type Name struct/interface`
74
+ const RE_GO_FUNC = /^func\s+(?:\(([^)]+)\)\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*[\(\[]/
75
+ const RE_GO_TYPE = /^type\s+([A-Za-z_][A-Za-z0-9_]*)\s+(struct|interface)\b/
76
+
77
+ const go = {
78
+ extractSymbols(content, fileId) {
79
+ const lines = content.split('\n')
80
+ const out = []
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const ln = lines[i]
83
+ const lineNum = i + 1
84
+ let m
85
+ if ((m = ln.match(RE_GO_FUNC))) {
86
+ const recv = m[1]?.trim()
87
+ const name = m[2]
88
+ const receiverType = recv ? recv.replace(/^\*?\s*\w+\s+\*?/, '').replace(/^\*/, '').trim() : null
89
+ const qualifiedName = receiverType ? `${receiverType}.${name}` : name
90
+ out.push({
91
+ id: mkId(fileId, qualifiedName, lineNum),
92
+ name, qualifiedName,
93
+ kind: receiverType ? 'method' : 'function',
94
+ file: fileId,
95
+ startLine: lineNum,
96
+ endLine: braceBlockEnd(lines, lineNum),
97
+ signature: ln.trim().replace(/\{?\s*$/, ''),
98
+ doc: leadingLineComments(lines, i),
99
+ exported: /^[A-Z]/.test(name),
100
+ })
101
+ } else if ((m = ln.match(RE_GO_TYPE))) {
102
+ const name = m[1]
103
+ out.push({
104
+ id: mkId(fileId, name, lineNum),
105
+ name, qualifiedName: name,
106
+ kind: m[2] === 'struct' ? 'struct' : 'interface',
107
+ file: fileId,
108
+ startLine: lineNum,
109
+ endLine: braceBlockEnd(lines, lineNum),
110
+ signature: ln.trim().replace(/\{?\s*$/, ''),
111
+ doc: leadingLineComments(lines, i),
112
+ exported: /^[A-Z]/.test(name),
113
+ })
114
+ }
115
+ }
116
+ return out
117
+ },
118
+ extractReferences(content, fileId, index) {
119
+ const lines = content.split('\n')
120
+ const resolve = makeResolver(fileId, index)
121
+ const enclosing = makeEnclosingLookup(fileId, index)
122
+ const edges = []
123
+ const seen = new Set()
124
+ const RE_CALL = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const src = enclosing(i + 1)
127
+ if (!src) continue
128
+ RE_CALL.lastIndex = 0
129
+ let m
130
+ while ((m = RE_CALL.exec(lines[i]))) {
131
+ const name = m[1]
132
+ if (GO_KEYWORDS.has(name)) continue
133
+ const target = resolve(name)
134
+ if (!target || target.id === src) continue
135
+ const key = src + '|' + target.id
136
+ if (seen.has(key)) continue
137
+ seen.add(key)
138
+ edges.push({ source: src, target: target.id, kind: 'call', line: i + 1 })
139
+ }
140
+ }
141
+ return edges
142
+ },
143
+ }
144
+ const GO_KEYWORDS = new Set([
145
+ 'func','for','if','else','range','return','break','continue','switch',
146
+ 'case','default','select','go','defer','make','new','len','cap','append',
147
+ 'copy','delete','panic','recover','close','print','println','interface',
148
+ 'struct','type','var','const','import','package','map','chan','true','false','nil',
149
+ ])
150
+
151
+ // ─── Rust ───────────────────────────────────────────────────────
152
+ // - `fn name(` / `pub fn name(` / `async fn …`
153
+ // - `struct/enum/trait Name`
154
+ // - `impl …` blocks (we tag methods inside as kind:'method')
155
+ const RE_RS_FN = /^\s*(?:pub(?:\([\w:]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)/
156
+ const RE_RS_STRUCT = /^\s*(?:pub(?:\([\w:]+\))?\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)/
157
+ const RE_RS_ENUM = /^\s*(?:pub(?:\([\w:]+\))?\s+)?enum\s+([A-Za-z_][A-Za-z0-9_]*)/
158
+ const RE_RS_TRAIT = /^\s*(?:pub(?:\([\w:]+\))?\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)/
159
+ const RE_RS_IMPL = /^\s*impl(?:<[^>]*>)?\s+(?:[A-Za-z_][\w:<>]*\s+for\s+)?([A-Za-z_][\w:<>]*)/
160
+
161
+ const rust = {
162
+ extractSymbols(content, fileId) {
163
+ const lines = content.split('\n')
164
+ const out = []
165
+ let implFor = null // type name we're currently impl'ing, or null
166
+ let implEndLine = 0
167
+ for (let i = 0; i < lines.length; i++) {
168
+ const ln = lines[i]
169
+ const lineNum = i + 1
170
+ let m
171
+ if ((m = ln.match(RE_RS_IMPL))) {
172
+ implFor = m[1].replace(/<.*$/, '') // strip generic params
173
+ implEndLine = braceBlockEnd(lines, lineNum)
174
+ continue
175
+ }
176
+ if (lineNum > implEndLine) implFor = null
177
+ if ((m = ln.match(RE_RS_FN))) {
178
+ const name = m[1]
179
+ const qn = implFor ? `${implFor}.${name}` : name
180
+ out.push({
181
+ id: mkId(fileId, qn, lineNum),
182
+ name, qualifiedName: qn,
183
+ kind: implFor ? 'method' : 'function',
184
+ file: fileId,
185
+ startLine: lineNum,
186
+ endLine: braceBlockEnd(lines, lineNum),
187
+ signature: ln.trim().replace(/\{?\s*$/, ''),
188
+ doc: leadingLineComments(lines, i, '///'),
189
+ exported: ln.includes('pub '),
190
+ })
191
+ } else if ((m = ln.match(RE_RS_STRUCT)) || (m = ln.match(RE_RS_ENUM)) || (m = ln.match(RE_RS_TRAIT))) {
192
+ const name = m[1]
193
+ const kind = ln.match(/struct/) ? 'struct' : (ln.match(/enum/) ? 'enum' : 'interface')
194
+ out.push({
195
+ id: mkId(fileId, name, lineNum),
196
+ name, qualifiedName: name, kind,
197
+ file: fileId,
198
+ startLine: lineNum,
199
+ endLine: braceBlockEnd(lines, lineNum),
200
+ signature: ln.trim().replace(/\{?\s*$/, ''),
201
+ doc: leadingLineComments(lines, i, '///'),
202
+ exported: ln.includes('pub '),
203
+ })
204
+ }
205
+ }
206
+ return out
207
+ },
208
+ extractReferences(content, fileId, index) {
209
+ return genericReferences(content, fileId, index, RS_KEYWORDS)
210
+ },
211
+ }
212
+ const RS_KEYWORDS = new Set([
213
+ 'fn','let','mut','if','else','while','for','loop','match','return','break',
214
+ 'continue','use','mod','pub','crate','self','super','impl','trait','struct',
215
+ 'enum','type','as','where','async','await','dyn','ref','move','in','Box',
216
+ 'Vec','String','Some','None','Ok','Err','Result','Option','true','false',
217
+ 'unsafe','extern','static','const',
218
+ ])
219
+
220
+ // ─── Java / Kotlin ──────────────────────────────────────────────
221
+ const RE_JAVA_CLASS = /^\s*(?:public\s+|private\s+|protected\s+|abstract\s+|final\s+|static\s+|sealed\s+)*\s*(?:class|interface|record|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/
222
+ // method: visibility + (static)? + type + name(
223
+ const RE_JAVA_METHOD = /^\s*(?:public|private|protected|static|final|synchronized|abstract|default)(?:\s+(?:public|private|protected|static|final|synchronized|abstract|default))*\s+(?:<[^>]+>\s+)?[A-Za-z_][\w<>\[\],?\s.]*\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/
224
+ const RE_KT_FN = /^\s*(?:public\s+|private\s+|internal\s+|protected\s+|override\s+|open\s+|inline\s+|suspend\s+)*fun\s+(?:<[^>]+>\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(/
225
+
226
+ const javaKt = {
227
+ extractSymbols(content, fileId) {
228
+ const ext = (fileId.split('.').pop() || '').toLowerCase()
229
+ const lines = content.split('\n')
230
+ const out = []
231
+ const classStack = [] // [{ name, endLine }]
232
+ for (let i = 0; i < lines.length; i++) {
233
+ const ln = lines[i]
234
+ const lineNum = i + 1
235
+ while (classStack.length && lineNum > classStack[classStack.length - 1].endLine) {
236
+ classStack.pop()
237
+ }
238
+ let m
239
+ if ((m = ln.match(RE_JAVA_CLASS))) {
240
+ const name = m[1]
241
+ const end = braceBlockEnd(lines, lineNum)
242
+ out.push({
243
+ id: mkId(fileId, name, lineNum),
244
+ name, qualifiedName: classStack.map((c) => c.name).concat(name).join('.'),
245
+ kind: ln.includes('interface') ? 'interface' : (ln.includes('enum') ? 'enum' : 'class'),
246
+ file: fileId, startLine: lineNum, endLine: end,
247
+ signature: ln.trim().replace(/\{?\s*$/, ''),
248
+ doc: leadingLineComments(lines, i, '//'),
249
+ exported: ln.includes('public'),
250
+ })
251
+ classStack.push({ name, endLine: end })
252
+ continue
253
+ }
254
+ const re = ext === 'kt' ? RE_KT_FN : RE_JAVA_METHOD
255
+ if ((m = ln.match(re))) {
256
+ const name = m[1]
257
+ if (JAVA_KEYWORDS.has(name)) continue
258
+ const cls = classStack[classStack.length - 1]?.name
259
+ const qn = cls ? `${cls}.${name}` : name
260
+ out.push({
261
+ id: mkId(fileId, qn, lineNum),
262
+ name, qualifiedName: qn,
263
+ kind: cls ? 'method' : 'function',
264
+ file: fileId, startLine: lineNum, endLine: braceBlockEnd(lines, lineNum),
265
+ signature: ln.trim().replace(/\{?\s*$/, ''),
266
+ doc: leadingLineComments(lines, i, '//'),
267
+ exported: ln.includes('public'),
268
+ })
269
+ }
270
+ }
271
+ return out
272
+ },
273
+ extractReferences(content, fileId, index) {
274
+ return genericReferences(content, fileId, index, JAVA_KEYWORDS)
275
+ },
276
+ }
277
+ const JAVA_KEYWORDS = new Set([
278
+ 'if','else','while','for','do','switch','case','break','continue','return',
279
+ 'new','this','super','try','catch','finally','throw','throws','class',
280
+ 'interface','enum','extends','implements','public','private','protected',
281
+ 'static','final','abstract','synchronized','volatile','transient','native',
282
+ 'void','int','long','short','byte','char','boolean','float','double',
283
+ 'String','Object','Integer','Long','Boolean','true','false','null','var',
284
+ 'import','package','assert','instanceof','default','record','sealed','permits',
285
+ // Kotlin
286
+ 'fun','val','val','val','val','val','val','val','val','val',
287
+ ])
288
+
289
+ // ─── Swift ──────────────────────────────────────────────────────
290
+ const RE_SW_FUNC = /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+|open\s+|static\s+|class\s+|override\s+|final\s+|@\w+\s+)*func\s+([A-Za-z_][A-Za-z0-9_]*)/
291
+ const RE_SW_TYPE = /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+|open\s+|final\s+|indirect\s+|@\w+\s+)*(class|struct|enum|protocol|extension|actor)\s+([A-Za-z_][A-Za-z0-9_]*)/
292
+
293
+ const swift = {
294
+ extractSymbols(content, fileId) {
295
+ const lines = content.split('\n')
296
+ const out = []
297
+ const typeStack = []
298
+ for (let i = 0; i < lines.length; i++) {
299
+ const ln = lines[i]
300
+ const lineNum = i + 1
301
+ while (typeStack.length && lineNum > typeStack[typeStack.length - 1].endLine) {
302
+ typeStack.pop()
303
+ }
304
+ let m
305
+ if ((m = ln.match(RE_SW_TYPE))) {
306
+ const name = m[2]
307
+ const end = braceBlockEnd(lines, lineNum)
308
+ const kindWord = m[1]
309
+ out.push({
310
+ id: mkId(fileId, name, lineNum),
311
+ name, qualifiedName: typeStack.map((t) => t.name).concat(name).join('.'),
312
+ kind: kindWord === 'protocol' ? 'interface'
313
+ : kindWord === 'struct' ? 'struct'
314
+ : kindWord === 'enum' ? 'enum'
315
+ : 'class',
316
+ file: fileId, startLine: lineNum, endLine: end,
317
+ signature: ln.trim().replace(/\{?\s*$/, ''),
318
+ doc: leadingLineComments(lines, i, '//'),
319
+ exported: ln.match(/\b(public|open)\b/) ? true : false,
320
+ })
321
+ typeStack.push({ name, endLine: end })
322
+ continue
323
+ }
324
+ if ((m = ln.match(RE_SW_FUNC))) {
325
+ const name = m[1]
326
+ const t = typeStack[typeStack.length - 1]?.name
327
+ const qn = t ? `${t}.${name}` : name
328
+ out.push({
329
+ id: mkId(fileId, qn, lineNum),
330
+ name, qualifiedName: qn,
331
+ kind: t ? 'method' : 'function',
332
+ file: fileId, startLine: lineNum, endLine: braceBlockEnd(lines, lineNum),
333
+ signature: ln.trim().replace(/\{?\s*$/, ''),
334
+ doc: leadingLineComments(lines, i, '//'),
335
+ exported: ln.match(/\b(public|open)\b/) ? true : false,
336
+ })
337
+ }
338
+ }
339
+ return out
340
+ },
341
+ extractReferences(content, fileId, index) {
342
+ return genericReferences(content, fileId, index, SWIFT_KEYWORDS)
343
+ },
344
+ }
345
+ const SWIFT_KEYWORDS = new Set([
346
+ 'if','else','for','in','while','repeat','do','switch','case','break','continue',
347
+ 'return','throw','throws','try','catch','rethrows','defer','guard','where',
348
+ 'as','is','let','var','func','class','struct','enum','protocol','extension',
349
+ 'import','self','super','init','deinit','static','final','public','private',
350
+ 'internal','open','fileprivate','true','false','nil','some','any','Self',
351
+ 'Optional','print','String','Int','Bool','Double','Float','Array','Dictionary',
352
+ ])
353
+
354
+ // ─── Shared helpers ─────────────────────────────────────────────
355
+ function leadingLineComments(lines, i, prefix = '//') {
356
+ const parts = []
357
+ let j = i - 1
358
+ while (j >= 0) {
359
+ const t = lines[j].trim()
360
+ if (!t) break
361
+ if (t.startsWith(prefix)) parts.unshift(t.slice(prefix.length).trim())
362
+ else break
363
+ j--
364
+ }
365
+ return parts.join(' ').slice(0, 400)
366
+ }
367
+
368
+ function genericReferences(content, fileId, index, kwSet) {
369
+ const lines = content.split('\n')
370
+ const resolve = makeResolver(fileId, index)
371
+ const enclosing = makeEnclosingLookup(fileId, index)
372
+ const edges = []
373
+ const seen = new Set()
374
+ const RE_CALL = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g
375
+ for (let i = 0; i < lines.length; i++) {
376
+ const src = enclosing(i + 1)
377
+ if (!src) continue
378
+ RE_CALL.lastIndex = 0
379
+ let m
380
+ while ((m = RE_CALL.exec(lines[i]))) {
381
+ const name = m[1]
382
+ if (kwSet && kwSet.has(name)) continue
383
+ const target = resolve(name)
384
+ if (!target || target.id === src) continue
385
+ const key = src + '|' + target.id
386
+ if (seen.has(key)) continue
387
+ seen.add(key)
388
+ edges.push({ source: src, target: target.id, kind: 'call', line: i + 1 })
389
+ }
390
+ }
391
+ return edges
392
+ }
393
+
394
+ module.exports = { go, rust, javaKt, swift }
@@ -0,0 +1,215 @@
1
+ // Python symbol parser — regex + brace/indent depth, no AST.
2
+ //
3
+ // Extracts:
4
+ // - top-level `def`/`async def`/`class` declarations
5
+ // - methods (def inside a class block; tracked by indent)
6
+ // - module-level constants written ALL_CAPS = …
7
+ //
8
+ // References (call edges) are detected by regex on `name(` inside
9
+ // the body of each tracked function/method; resolution prefers
10
+ // same-file matches first, then any project-wide name match.
11
+
12
+ 'use strict'
13
+
14
+ const RE_DEF = /^(\s*)(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/
15
+ const RE_CLASS = /^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\s*[\(:]/
16
+ const RE_CONST = /^[A-Z_][A-Z0-9_]+\s*[:=]/ // ALL_CAPS module constant
17
+ const RE_DOC_TRIPLE = /^\s*(?:"""|''')(.*?)(?:"""|''')\s*$/
18
+
19
+ function mkId(file, name, line) { return `${file}#${name}@${line}` }
20
+
21
+ function leadingDocstring(lines, startLine) {
22
+ // startLine is 1-based — Python convention is the first statement
23
+ // of a def/class body, which is the next non-blank line after the
24
+ // signature. The signature itself may span multiple lines (with
25
+ // trailing `,\n` in arguments) so we scan forward up to 8 lines for
26
+ // the first `"""` or `'''` block.
27
+ for (let i = startLine; i < Math.min(startLine + 8, lines.length); i++) {
28
+ const t = (lines[i] || '').trim()
29
+ if (!t) continue
30
+ if (t.startsWith('"""') || t.startsWith("'''")) {
31
+ // Single-line docstring
32
+ const m = t.match(RE_DOC_TRIPLE)
33
+ if (m) return m[1].trim().slice(0, 400)
34
+ // Multi-line — collect until closing
35
+ const quote = t.slice(0, 3)
36
+ const body = [t.slice(3)]
37
+ for (let j = i + 1; j < Math.min(j + 30, lines.length); j++) {
38
+ const tj = lines[j] || ''
39
+ const close = tj.indexOf(quote)
40
+ if (close >= 0) {
41
+ body.push(tj.slice(0, close))
42
+ return body.join(' ').replace(/\s+/g, ' ').trim().slice(0, 400)
43
+ }
44
+ body.push(tj)
45
+ }
46
+ return body.join(' ').replace(/\s+/g, ' ').trim().slice(0, 400)
47
+ }
48
+ break // first non-blank non-docstring line — no docstring
49
+ }
50
+ return ''
51
+ }
52
+
53
+ // Compute end-line of a def/class block by walking forward until the
54
+ // indent returns to ≤ the def's own indent (or EOF). Simple, not
55
+ // perfect (no triple-quoted block edge-cases) but good enough.
56
+ function blockEnd(lines, startIdx, baseIndent) {
57
+ for (let i = startIdx + 1; i < lines.length; i++) {
58
+ const line = lines[i]
59
+ if (!line.trim()) continue // blank lines don't terminate
60
+ const indent = line.match(/^(\s*)/)[1].length
61
+ if (indent <= baseIndent) return i // 1-based caller adds +1 if needed
62
+ }
63
+ return lines.length
64
+ }
65
+
66
+ function extractSymbols(content, fileId) {
67
+ const lines = content.split('\n')
68
+ const symbols = []
69
+ // Stack of currently-open classes, by indent → name
70
+ const classStack = [] // [{ name, indent, line }]
71
+
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const line = lines[i]
74
+ const lineNum = i + 1
75
+
76
+ // pop classes whose indent is now equal-or-greater than current line
77
+ const lineIndent = line.match(/^(\s*)/)[1].length
78
+ while (classStack.length && lineIndent <= classStack[classStack.length - 1].indent && line.trim()) {
79
+ classStack.pop()
80
+ }
81
+
82
+ let m
83
+ if ((m = line.match(RE_CLASS))) {
84
+ const indent = m[1].length
85
+ const name = m[2]
86
+ const end = blockEnd(lines, i, indent)
87
+ symbols.push({
88
+ id: mkId(fileId, name, lineNum),
89
+ name,
90
+ qualifiedName: classStack.map((c) => c.name).concat(name).join('.'),
91
+ kind: 'class',
92
+ file: fileId,
93
+ startLine: lineNum,
94
+ endLine: end,
95
+ signature: line.trim().replace(/:$/, ''),
96
+ doc: leadingDocstring(lines, lineNum),
97
+ exported: !name.startsWith('_'),
98
+ })
99
+ classStack.push({ name, indent, line: lineNum })
100
+ continue
101
+ }
102
+ if ((m = line.match(RE_DEF))) {
103
+ const indent = m[1].length
104
+ const name = m[2]
105
+ const enclosingClass = classStack.length
106
+ ? classStack[classStack.length - 1]
107
+ : null
108
+ // A def inside a class (indent > class indent) is a method.
109
+ const isMethod = enclosingClass && indent > enclosingClass.indent
110
+ const end = blockEnd(lines, i, indent)
111
+ const qualifiedName = isMethod
112
+ ? `${classStack.map((c) => c.name).join('.')}.${name}`
113
+ : name
114
+ symbols.push({
115
+ id: mkId(fileId, isMethod ? qualifiedName : name, lineNum),
116
+ name,
117
+ qualifiedName,
118
+ kind: isMethod ? 'method' : 'function',
119
+ file: fileId,
120
+ startLine: lineNum,
121
+ endLine: end,
122
+ signature: line.trim().replace(/:$/, ''),
123
+ doc: leadingDocstring(lines, lineNum),
124
+ exported: !name.startsWith('_'),
125
+ })
126
+ continue
127
+ }
128
+ if (lineIndent === 0 && RE_CONST.test(line)) {
129
+ const name = line.match(/^([A-Z_][A-Z0-9_]+)/)[1]
130
+ symbols.push({
131
+ id: mkId(fileId, name, lineNum),
132
+ name,
133
+ qualifiedName: name,
134
+ kind: 'const',
135
+ file: fileId,
136
+ startLine: lineNum,
137
+ endLine: lineNum,
138
+ signature: line.trim().slice(0, 120),
139
+ doc: '',
140
+ exported: !name.startsWith('_'),
141
+ })
142
+ }
143
+ }
144
+ return symbols
145
+ }
146
+
147
+ function extractReferences(content, fileId, index) {
148
+ // Build a per-file map of symbol-id → [startLine, endLine] so we can
149
+ // attribute each call to the enclosing function.
150
+ const fileSyms = index.byFile.get(fileId)
151
+ if (!fileSyms) return []
152
+ const ranges = []
153
+ for (const id of fileSyms) {
154
+ const n = index.nodes.get(id)
155
+ if (!n) continue
156
+ if (n.kind !== 'function' && n.kind !== 'method') continue
157
+ ranges.push({ id, start: n.startLine, end: n.endLine })
158
+ }
159
+ ranges.sort((a, b) => a.start - b.start)
160
+
161
+ function enclosingId(lineNum) {
162
+ // Pick the innermost (largest start that is <= lineNum) range that
163
+ // contains lineNum. Linear scan is fine — usually <500 ranges/file.
164
+ let best = null
165
+ for (const r of ranges) {
166
+ if (r.start <= lineNum && lineNum <= r.end) {
167
+ if (!best || r.start > best.start) best = r
168
+ }
169
+ }
170
+ return best?.id || null
171
+ }
172
+
173
+ // Python regex parser only emits `call` edges (no expression-level
174
+ // ref pass yet), so it always wants the loose any-file fallback.
175
+ function resolve(name) {
176
+ return index.resolveCall ? index.resolveCall(fileId, name, { allowAny: true }) : null
177
+ }
178
+
179
+ const lines = content.split('\n')
180
+ const edges = []
181
+ const seen = new Set()
182
+ const RE_CALL = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g
183
+ // Python keywords / builtins to skip — they aren't user-defined symbols.
184
+ const SKIP = new Set([
185
+ 'if','elif','while','for','in','not','and','or','is','return','print',
186
+ 'len','range','int','str','float','bool','list','dict','set','tuple',
187
+ 'isinstance','type','super','self','cls','open','sorted','enumerate',
188
+ 'zip','map','filter','any','all','sum','min','max','abs','round',
189
+ 'getattr','setattr','hasattr','format','repr','hash','id','iter','next',
190
+ 'object','property','staticmethod','classmethod','None','True','False',
191
+ 'except','raise','try','finally','with','as','from','import','def','class',
192
+ 'lambda','yield','pass','break','continue','global','nonlocal',
193
+ ])
194
+
195
+ for (let i = 0; i < lines.length; i++) {
196
+ const src = enclosingId(i + 1)
197
+ if (!src) continue
198
+ const line = lines[i]
199
+ let m
200
+ RE_CALL.lastIndex = 0
201
+ while ((m = RE_CALL.exec(line))) {
202
+ const name = m[1]
203
+ if (SKIP.has(name)) continue
204
+ const target = resolve(name)
205
+ if (!target || target.id === src) continue
206
+ const key = src + '|' + target.id + '|call'
207
+ if (seen.has(key)) continue
208
+ seen.add(key)
209
+ edges.push({ source: src, target: target.id, kind: 'call', line: i + 1 })
210
+ }
211
+ }
212
+ return edges
213
+ }
214
+
215
+ module.exports = { extractSymbols, extractReferences }