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.
- package/CHANGELOG.md +17 -0
- package/LICENSE +686 -0
- package/LICENSES.md +141 -0
- package/README.md +331 -0
- package/electron/main.cjs +2849 -0
- package/electron/plugin-loader.cjs +184 -0
- package/electron/preload.cjs +108 -0
- package/package.json +216 -0
- package/packages/core/bin/codesynapt-mcp.cjs +611 -0
- package/packages/core/bin/codesynapt.cjs +1933 -0
- package/packages/core/legacy.js +300 -0
- package/packages/core/lib/control-server.cjs +1539 -0
- package/packages/core/lib/embedding.cjs +89 -0
- package/packages/core/lib/logger.cjs +63 -0
- package/packages/core/lib/search-cache.cjs +140 -0
- package/packages/core/lib/search-worker.cjs +255 -0
- package/packages/core/lib/search.cjs +211 -0
- package/packages/core/lib/symbol-graph.cjs +402 -0
- package/packages/core/lib/symbol-parser-js.cjs +542 -0
- package/packages/core/lib/symbol-parser-misc.cjs +394 -0
- package/packages/core/lib/symbol-parser-py.cjs +215 -0
- package/packages/core/lib/symbol-parser-treesitter.cjs +658 -0
- package/packages/core/lib/symbol-parser-tsc.cjs +332 -0
- package/packages/core/monorepo.js +310 -0
- package/packages/core/parser.js +2234 -0
- package/packages/core/scanner.js +623 -0
- package/plugin-api/LICENSE +21 -0
- package/plugin-api/README.md +114 -0
- package/plugin-api/docs/01-getting-started.md +197 -0
- package/plugin-api/docs/02-concepts.md +269 -0
- package/plugin-api/docs/api-reference.md +463 -0
- package/plugin-api/docs/troubleshooting.md +332 -0
- package/plugin-api/docs/types/exporter.md +377 -0
- package/plugin-api/docs/types/theme.md +312 -0
- package/plugin-api/examples/hello-world-plugin/README.md +70 -0
- package/plugin-api/examples/hello-world-plugin/main.js +36 -0
- package/plugin-api/examples/hello-world-plugin/manifest.json +12 -0
- package/plugin-api/examples/mermaid-exporter/README.md +125 -0
- package/plugin-api/examples/mermaid-exporter/main.js +58 -0
- package/plugin-api/examples/mermaid-exporter/manifest.json +12 -0
- package/plugin-api/examples/rust-parser/README.md +71 -0
- package/plugin-api/examples/rust-parser/main.js +123 -0
- package/plugin-api/examples/rust-parser/manifest.json +12 -0
- package/plugin-api/examples/sunset-theme/README.md +95 -0
- package/plugin-api/examples/sunset-theme/manifest.json +12 -0
- package/plugin-api/examples/sunset-theme/theme.css +31 -0
- package/plugin-api/package.json +20 -0
- package/plugin-api/types.d.ts +395 -0
- package/public/app.js +6837 -0
- package/public/backend.js +285 -0
- package/public/index.html +647 -0
- package/public/plugin-host.js +321 -0
- package/public/style.css +4359 -0
- package/public/vendor/three.module.js +53044 -0
- package/scripts/competitor-watch.mjs +144 -0
- package/scripts/copy-vendor.js +21 -0
- package/scripts/download-bundled-node.cjs +53 -0
- package/scripts/fuses-after-pack.cjs +34 -0
- package/scripts/license-check.js +119 -0
- package/scripts/perf-test.js +200 -0
- 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 }
|