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,542 @@
1
+ // JS/TS symbol parser — uses @babel/parser (already a project dep).
2
+ //
3
+ // Extracts: function declarations, class declarations, methods, exports,
4
+ // const-assigned arrow functions (`const foo = () => …`), TS interfaces
5
+ // and types. Skips overlapping anonymous IIFEs and inline lambdas.
6
+ //
7
+ // References: every CallExpression inside a tracked function/method
8
+ // becomes an edge (source = enclosing symbol, target = best-match by name).
9
+ // Method calls (`obj.method()`) match by method name across the project.
10
+ // Cross-file resolution borrows the file-mode imports — if `foo` was
11
+ // imported, we prefer symbols in the imported file.
12
+
13
+ 'use strict'
14
+
15
+ const parser = require('@babel/parser')
16
+ const traverse = require('@babel/traverse').default
17
+
18
+ // Built-ins / globals to skip when emitting identifier-reference
19
+ // edges. Otherwise every `console.log`, `Math.max`, `Array.from` etc.
20
+ // would generate a spurious edge to a same-named user symbol.
21
+ const JS_BUILTINS = new Set([
22
+ 'console','window','document','globalThis','process','require','module',
23
+ 'exports','__dirname','__filename','Buffer','Math','Object','Array',
24
+ 'String','Number','Boolean','Date','RegExp','Error','TypeError',
25
+ 'RangeError','SyntaxError','Promise','Symbol','Map','Set','WeakMap',
26
+ 'WeakSet','JSON','Reflect','Proxy','Function','undefined','null','true',
27
+ 'false','NaN','Infinity','this','self','super','arguments','typeof',
28
+ 'instanceof','void','delete','new','in','of','yield','await','async',
29
+ 'function','class','const','let','var','if','else','for','while','do',
30
+ 'switch','case','break','continue','return','throw','try','catch',
31
+ 'finally','default','export','import','from','as','static','public',
32
+ 'private','protected','readonly','abstract','enum','interface','type',
33
+ 'namespace','module','declare','React','setTimeout','setInterval',
34
+ 'clearTimeout','clearInterval','fetch','URL','URLSearchParams',
35
+ 'parseInt','parseFloat','isNaN','isFinite','encodeURI','decodeURI',
36
+ 'encodeURIComponent','decodeURIComponent',
37
+ ])
38
+
39
+ const JS_PLUGINS = [
40
+ 'jsx', 'typescript', 'classProperties', 'classPrivateProperties',
41
+ 'classPrivateMethods', 'decorators-legacy', 'topLevelAwait',
42
+ 'optionalChaining', 'nullishCoalescingOperator', 'logicalAssignment',
43
+ 'numericSeparator', 'dynamicImport', 'importMeta',
44
+ 'exportDefaultFrom', 'exportNamespaceFrom',
45
+ ]
46
+
47
+ function parseAst(content, ext) {
48
+ // tsx/jsx need their respective plugin to actually parse JSX.
49
+ const plugins = JS_PLUGINS.filter((p) =>
50
+ !(p === 'typescript' && (ext === 'js' || ext === 'jsx')))
51
+ try {
52
+ return parser.parse(content, {
53
+ sourceType: 'module',
54
+ allowImportExportEverywhere: true,
55
+ allowAwaitOutsideFunction: true,
56
+ allowReturnOutsideFunction: true,
57
+ allowUndeclaredExports: true,
58
+ errorRecovery: true,
59
+ plugins,
60
+ })
61
+ } catch {
62
+ return null
63
+ }
64
+ }
65
+
66
+ function mkId(file, name, line) { return `${file}#${name}@${line}` }
67
+
68
+ // Resolve a type-position node to its top identifier name.
69
+ // `Bar` → "Bar"
70
+ // `Bar.Baz` → "Baz" (rightmost segment)
71
+ // `Generic<T>` (TSTypeReference) → "Generic"
72
+ // `Foo extends Bar` superClass → handled by caller's recursion
73
+ function extractTypeName(node) {
74
+ if (!node) return null
75
+ switch (node.type) {
76
+ case 'Identifier': return node.name
77
+ case 'MemberExpression': return extractTypeName(node.property)
78
+ case 'TSTypeReference': return extractTypeName(node.typeName)
79
+ case 'TSQualifiedName': return extractTypeName(node.right)
80
+ case 'TSExpressionWithTypeArguments': return extractTypeName(node.expression)
81
+ case 'CallExpression': return extractTypeName(node.callee) // mixin patterns
82
+ }
83
+ return null
84
+ }
85
+
86
+ // Extract a one-line signature from a function/class declaration.
87
+ function signatureOf(node, content) {
88
+ if (!node?.loc) return ''
89
+ const startIdx = node.start ?? 0
90
+ // Cut at first '{' or ';' (signature only), max 200 chars
91
+ let end = content.indexOf('{', startIdx)
92
+ if (end < 0 || end - startIdx > 200) end = startIdx + 200
93
+ return content.slice(startIdx, end).trim().replace(/\s+/g, ' ')
94
+ }
95
+
96
+ // Best-guess docstring: the comment block immediately above `node`.
97
+ function docOf(node) {
98
+ if (!node?.leadingComments) return ''
99
+ const last = node.leadingComments[node.leadingComments.length - 1]
100
+ if (!last) return ''
101
+ return last.value.replace(/^\s*\*\s?/gm, '').trim().slice(0, 400)
102
+ }
103
+
104
+ // Enclosing symbol id while traversing. We push/pop as we enter/leave
105
+ // function/method/class scopes so a CallExpression knows its source.
106
+ function makeEnclosingStack() {
107
+ const stack = []
108
+ return {
109
+ push: (id) => stack.push(id),
110
+ pop: () => stack.pop(),
111
+ top: () => stack[stack.length - 1] || null,
112
+ }
113
+ }
114
+
115
+ function extractSymbols(content, fileId) {
116
+ const ext = (fileId.split('.').pop() || '').toLowerCase()
117
+ const ast = parseAst(content, ext)
118
+ if (!ast) return []
119
+ const symbols = []
120
+ let currentClass = null
121
+
122
+ traverse(ast, {
123
+ FunctionDeclaration(path) {
124
+ const n = path.node
125
+ const name = n.id?.name || '(anonymous)'
126
+ if (!n.loc) return
127
+ symbols.push({
128
+ id: mkId(fileId, name, n.loc.start.line),
129
+ name,
130
+ qualifiedName: name,
131
+ kind: 'function',
132
+ file: fileId,
133
+ startLine: n.loc.start.line,
134
+ endLine: n.loc.end.line,
135
+ signature: signatureOf(n, content),
136
+ doc: docOf(n),
137
+ exported: path.parent?.type?.startsWith('Export') ?? false,
138
+ })
139
+ },
140
+ ClassDeclaration: {
141
+ enter(path) {
142
+ const n = path.node
143
+ if (!n.id || !n.loc) return
144
+ currentClass = n.id.name
145
+ symbols.push({
146
+ id: mkId(fileId, n.id.name, n.loc.start.line),
147
+ name: n.id.name,
148
+ qualifiedName: n.id.name,
149
+ kind: 'class',
150
+ file: fileId,
151
+ startLine: n.loc.start.line,
152
+ endLine: n.loc.end.line,
153
+ signature: signatureOf(n, content),
154
+ doc: docOf(n),
155
+ exported: path.parent?.type?.startsWith('Export') ?? false,
156
+ })
157
+ },
158
+ exit() { currentClass = null },
159
+ },
160
+ ClassMethod(path) {
161
+ const n = path.node
162
+ if (!n.key || !n.loc) return
163
+ const name = n.key.name || n.key.value || '(method)'
164
+ const qualifiedName = currentClass ? `${currentClass}.${name}` : name
165
+ symbols.push({
166
+ id: mkId(fileId, qualifiedName, n.loc.start.line),
167
+ name,
168
+ qualifiedName,
169
+ kind: name === 'constructor' ? 'function' : 'method',
170
+ file: fileId,
171
+ startLine: n.loc.start.line,
172
+ endLine: n.loc.end.line,
173
+ signature: signatureOf(n, content),
174
+ doc: docOf(n),
175
+ exported: false,
176
+ })
177
+ },
178
+ VariableDeclarator(path) {
179
+ const n = path.node
180
+ // const foo = () => {} | const foo = function() {}
181
+ const init = n.init
182
+ if (!init || !n.loc) return
183
+ const t = init.type
184
+ if (t !== 'ArrowFunctionExpression' && t !== 'FunctionExpression') return
185
+ const name = n.id?.name
186
+ if (!name) return
187
+ symbols.push({
188
+ id: mkId(fileId, name, n.loc.start.line),
189
+ name,
190
+ qualifiedName: name,
191
+ kind: 'function',
192
+ file: fileId,
193
+ startLine: n.loc.start.line,
194
+ endLine: n.loc.end.line,
195
+ signature: signatureOf(n, content),
196
+ doc: docOf(path.parentPath?.parent),
197
+ exported: false,
198
+ })
199
+ },
200
+ TSInterfaceDeclaration(path) {
201
+ const n = path.node
202
+ if (!n.loc || !n.id) return
203
+ symbols.push({
204
+ id: mkId(fileId, n.id.name, n.loc.start.line),
205
+ name: n.id.name,
206
+ qualifiedName: n.id.name,
207
+ kind: 'interface',
208
+ file: fileId,
209
+ startLine: n.loc.start.line,
210
+ endLine: n.loc.end.line,
211
+ signature: signatureOf(n, content),
212
+ doc: docOf(n),
213
+ exported: path.parent?.type?.startsWith('Export') ?? false,
214
+ })
215
+ },
216
+ TSTypeAliasDeclaration(path) {
217
+ const n = path.node
218
+ if (!n.loc || !n.id) return
219
+ symbols.push({
220
+ id: mkId(fileId, n.id.name, n.loc.start.line),
221
+ name: n.id.name,
222
+ qualifiedName: n.id.name,
223
+ kind: 'type',
224
+ file: fileId,
225
+ startLine: n.loc.start.line,
226
+ endLine: n.loc.end.line,
227
+ signature: signatureOf(n, content),
228
+ doc: docOf(n),
229
+ exported: path.parent?.type?.startsWith('Export') ?? false,
230
+ })
231
+ },
232
+ })
233
+
234
+ return symbols
235
+ }
236
+
237
+ function extractReferences(content, fileId, index) {
238
+ const ext = (fileId.split('.').pop() || '').toLowerCase()
239
+ const ast = parseAst(content, ext)
240
+ if (!ast) return []
241
+ const edges = []
242
+ const enclosing = makeEnclosingStack()
243
+ let currentClass = null
244
+
245
+ // Two modes mirroring the tree-sitter parser. Calls allow the
246
+ // any-file fallback (`foo()` is a strong signal); plain references
247
+ // stay strict (same-file or imported file only) so noise edges
248
+ // don't proliferate when a local variable shares a name with
249
+ // some unrelated user symbol.
250
+ function resolveCall(name) {
251
+ return index.resolveCall ? index.resolveCall(fileId, name, { allowAny: true }) : null
252
+ }
253
+ function resolveRef(name) {
254
+ return index.resolveCall ? index.resolveCall(fileId, name, { allowAny: false }) : null
255
+ }
256
+
257
+ function pushEnclosing(name, startLine) {
258
+ enclosing.push(mkId(fileId, name, startLine))
259
+ }
260
+
261
+ // Per-function variable → type maps. Pushed/popped with enclosing
262
+ // so a `const user = new User()` only affects calls inside that
263
+ // function. Best-effort: `new X()` and TS `let x: X = ...`.
264
+ const typeStack = []
265
+ function pushTypes() { typeStack.push(new Map()) }
266
+ function popTypes() { typeStack.pop() }
267
+ function topTypes() { return typeStack[typeStack.length - 1] || null }
268
+ function lookupVarType(name) {
269
+ for (let i = typeStack.length - 1; i >= 0; i--) {
270
+ const t = typeStack[i].get(name)
271
+ if (t) return t
272
+ }
273
+ return null
274
+ }
275
+ function harvestTypesFrom(fnNode) {
276
+ const types = topTypes()
277
+ if (!types) return
278
+ // 1) TypeScript parameter annotations: `function f(user: User)`
279
+ // Also catches destructured/rest patterns when the annotation
280
+ // is on the simple identifier directly.
281
+ if (Array.isArray(fnNode.params)) {
282
+ for (const p of fnNode.params) {
283
+ if (p?.type === 'Identifier') {
284
+ const ann = p.typeAnnotation?.typeAnnotation
285
+ const annName = extractTypeName(ann)
286
+ if (annName) types.set(p.name, annName)
287
+ }
288
+ // `function f({ name }: User)` — destructured param with type
289
+ if (p?.type === 'ObjectPattern' && p.typeAnnotation) {
290
+ const annName = extractTypeName(p.typeAnnotation.typeAnnotation)
291
+ for (const prop of p.properties || []) {
292
+ if (prop.value?.type === 'Identifier' && annName) {
293
+ types.set(prop.value.name, annName)
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ // 2) Body-level declarations
300
+ const body = fnNode.body?.body
301
+ if (!Array.isArray(body)) return
302
+ for (const stmt of body) {
303
+ if (stmt.type !== 'VariableDeclaration') continue
304
+ for (const d of stmt.declarations || []) {
305
+ if (d.id?.type !== 'Identifier') continue
306
+ const varName = d.id.name
307
+ // TS annotation: `let user: User`
308
+ const ann = d.id.typeAnnotation?.typeAnnotation
309
+ const annName = extractTypeName(ann)
310
+ if (annName) { types.set(varName, annName); continue }
311
+ // `new User()` / `new User(args)` initializer
312
+ if (d.init?.type === 'NewExpression') {
313
+ const cls = extractTypeName(d.init.callee)
314
+ if (cls) { types.set(varName, cls); continue }
315
+ }
316
+ // 3) `const u = User.find(...)` / `User.create(...)` — assume
317
+ // static factory returns the same type. Heuristic but
318
+ // extremely common in ORM/DDD code.
319
+ if (d.init?.type === 'CallExpression'
320
+ && d.init.callee?.type === 'MemberExpression'
321
+ && d.init.callee.object?.type === 'Identifier'
322
+ && /^[A-Z]/.test(d.init.callee.object.name)
323
+ && /^(find|findOne|findFirst|findMany|create|build|new|of|from|get|fetch)/.test(
324
+ d.init.callee.property?.name || '')) {
325
+ types.set(varName, d.init.callee.object.name)
326
+ }
327
+ // 4) `const u = makeUser()` / `getUser()` — heuristic on
328
+ // factory names that contain a capitalised noun: `makeUser`
329
+ // → User, `getOrder` → Order.
330
+ if (d.init?.type === 'CallExpression'
331
+ && d.init.callee?.type === 'Identifier') {
332
+ const fnName = d.init.callee.name
333
+ const m = fnName.match(/^(?:make|create|build|get|fetch|find|new|of|to)?([A-Z][a-zA-Z0-9]+)$/)
334
+ if (m) types.set(varName, m[1])
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ traverse(ast, {
341
+ FunctionDeclaration: {
342
+ enter(path) {
343
+ const n = path.node
344
+ // babel/traverse rejects any non-undefined return from a
345
+ // visitor enter/exit — `return x.push(...)` (number) and
346
+ // `return x.pop()` (element) both throw "Unexpected return
347
+ // value". Use bare `return` instead.
348
+ if (!n.id?.name || !n.loc) { enclosing.push(null); return }
349
+ pushEnclosing(n.id.name, n.loc.start.line)
350
+ pushTypes(); harvestTypesFrom(n)
351
+ },
352
+ exit() { enclosing.pop(); popTypes() },
353
+ },
354
+ ClassDeclaration: {
355
+ enter(path) {
356
+ const n = path.node
357
+ currentClass = n.id?.name || null
358
+ if (currentClass && n.loc) {
359
+ const classId = mkId(fileId, currentClass, n.loc.start.line)
360
+ // `class Foo extends Bar` / `extends Bar.Baz` / `extends Generic<T>`
361
+ const superName = extractTypeName(n.superClass)
362
+ if (superName) {
363
+ const t = resolveCall(superName)
364
+ if (t) edges.push({ source: classId, target: t.id, kind: 'extends', line: n.loc.start.line })
365
+ }
366
+ // TypeScript `class Foo implements IFoo, IBar`
367
+ if (Array.isArray(n.implements)) {
368
+ for (const imp of n.implements) {
369
+ const name = extractTypeName(imp.expression || imp)
370
+ if (!name) continue
371
+ const t = resolveCall(name)
372
+ if (t) edges.push({ source: classId, target: t.id, kind: 'implements', line: n.loc.start.line })
373
+ }
374
+ }
375
+ }
376
+ },
377
+ exit() { currentClass = null },
378
+ },
379
+ // TS `interface Foo extends Bar, Baz`
380
+ TSInterfaceDeclaration: {
381
+ enter(path) {
382
+ const n = path.node
383
+ if (!n.id || !n.loc) return
384
+ const ifaceId = mkId(fileId, n.id.name, n.loc.start.line)
385
+ if (Array.isArray(n.extends)) {
386
+ for (const ext of n.extends) {
387
+ const name = extractTypeName(ext.expression || ext)
388
+ if (!name) continue
389
+ const t = resolveCall(name)
390
+ if (t) edges.push({ source: ifaceId, target: t.id, kind: 'extends', line: n.loc.start.line })
391
+ }
392
+ }
393
+ },
394
+ },
395
+ ClassMethod: {
396
+ enter(path) {
397
+ const n = path.node
398
+ if (!n.key || !n.loc) { enclosing.push(null); return }
399
+ const name = n.key.name || n.key.value || '(method)'
400
+ const qualified = currentClass ? `${currentClass}.${name}` : name
401
+ pushEnclosing(qualified, n.loc.start.line)
402
+ pushTypes(); harvestTypesFrom(n)
403
+ },
404
+ exit() { enclosing.pop(); popTypes() },
405
+ },
406
+ ArrowFunctionExpression: {
407
+ enter(path) {
408
+ const parent = path.parentPath?.node
409
+ if (parent?.type !== 'VariableDeclarator') { enclosing.push(null); return }
410
+ const name = parent.id?.name
411
+ if (!name || !path.node.loc) { enclosing.push(null); return }
412
+ pushEnclosing(name, path.node.loc.start.line)
413
+ pushTypes(); harvestTypesFrom(path.node)
414
+ },
415
+ exit(path) {
416
+ const parent = path.parentPath?.node
417
+ if (parent?.type !== 'VariableDeclarator') { enclosing.pop(); return }
418
+ const name = parent.id?.name
419
+ if (name && path.node.loc) { enclosing.pop(); popTypes() }
420
+ else enclosing.pop()
421
+ },
422
+ },
423
+ CallExpression(path) {
424
+ const src = enclosing.top()
425
+ if (!src) return
426
+ const callee = path.node.callee
427
+ let name = null
428
+ let receiverClass = null
429
+ if (callee.type === 'Identifier') name = callee.name
430
+ else if (callee.type === 'MemberExpression' && callee.property?.type === 'Identifier') {
431
+ name = callee.property.name
432
+ // Type-aware: receiver is `user` → look up `user`'s declared
433
+ // type → resolve `User.method` qualified name first.
434
+ const obj = callee.object
435
+ if (obj?.type === 'Identifier') {
436
+ const t = lookupVarType(obj.name)
437
+ if (t) receiverClass = t
438
+ } else if (obj?.type === 'ThisExpression' && currentClass) {
439
+ receiverClass = currentClass
440
+ }
441
+ }
442
+ if (!name) return
443
+ // Try the type-qualified lookup first; fall back to the loose
444
+ // call resolver if nothing matches.
445
+ let target = null
446
+ if (receiverClass) {
447
+ target = resolveCall(`${receiverClass}.${name}`)
448
+ }
449
+ if (!target) target = resolveCall(name)
450
+ if (!target || target.id === src) return
451
+ edges.push({
452
+ source: src,
453
+ target: target.id,
454
+ kind: 'call',
455
+ line: path.node.loc?.start.line || 0,
456
+ })
457
+ },
458
+ // Expression-level references — non-call identifier usage. Lets us
459
+ // see "Foo is used here" even when it's not being invoked
460
+ // (passed as argument, assigned, type-annotated, etc.). codegraph
461
+ // counts every identifier as a node; we instead emit a `ref` edge
462
+ // to the matched symbol, which keeps the graph file-cheap.
463
+ Identifier(path) {
464
+ const src = enclosing.top()
465
+ if (!src) return
466
+ const name = path.node.name
467
+ if (!name || JS_BUILTINS.has(name)) return
468
+ // Skip identifiers that are the *declaration* itself (parameter
469
+ // names, the var/let/const id, the function/class name) — only
470
+ // count usages.
471
+ const parent = path.parent
472
+ if (!parent) return
473
+ if (parent.type === 'VariableDeclarator' && parent.id === path.node) return
474
+ if (parent.type === 'FunctionDeclaration' && parent.id === path.node) return
475
+ if (parent.type === 'ClassDeclaration' && parent.id === path.node) return
476
+ if (parent.type === 'Identifier') return // shouldn't happen but guard
477
+ if ((parent.type === 'CallExpression' || parent.type === 'NewExpression')
478
+ && parent.callee === path.node) return // already counted as call
479
+ if (parent.type === 'MemberExpression' && parent.property === path.node && !parent.computed) return
480
+ if (parent.type === 'ObjectProperty' && parent.key === path.node && !parent.computed) return
481
+ if (parent.type === 'ImportSpecifier' || parent.type === 'ImportDefaultSpecifier') return
482
+ if (parent.type === 'FunctionExpression' && parent.id === path.node) return
483
+ if (parent.type === 'ArrowFunctionExpression') return // params handled below
484
+ if (parent.type === 'AssignmentPattern' || parent.type === 'RestElement') return
485
+ // Skip parameters of the enclosing function.
486
+ const fnParent = path.findParent((p) => p.isFunction?.() || p.isClassMethod?.())
487
+ if (fnParent && fnParent.node.params?.some?.((p) => p === path.parent || p === path.node)) return
488
+ const target = resolveRef(name)
489
+ if (!target || target.id === src) return
490
+ edges.push({ source: src, target: target.id, kind: 'ref', line: path.node.loc?.start.line || 0 })
491
+ },
492
+ // Member access (`obj.method`) where `method` matches a known
493
+ // symbol — we treat it as a reference even without an invocation
494
+ // (e.g. `const x = obj.method` or `passing obj.method as cb`).
495
+ MemberExpression(path) {
496
+ const src = enclosing.top()
497
+ if (!src) return
498
+ if (path.parent?.type === 'CallExpression' && path.parent.callee === path.node) return
499
+ const prop = path.node.property
500
+ if (!prop || prop.type !== 'Identifier') return
501
+ const target = resolveRef(prop.name)
502
+ if (!target || target.id === src) return
503
+ edges.push({ source: src, target: target.id, kind: 'ref', line: path.node.loc?.start.line || 0 })
504
+ },
505
+ // JSX `<Component … />` — the element name is a symbol reference.
506
+ JSXIdentifier(path) {
507
+ const src = enclosing.top()
508
+ if (!src) return
509
+ const name = path.node.name
510
+ // Tag names that start lowercase are HTML primitives, not React
511
+ // components. React component naming convention catches the rest.
512
+ if (!name || !/^[A-Z]/.test(name)) return
513
+ const target = resolveRef(name)
514
+ if (!target || target.id === src) return
515
+ edges.push({ source: src, target: target.id, kind: 'jsx-ref', line: path.node.loc?.start.line || 0 })
516
+ },
517
+ // TypeScript type annotations / generic params — `x: Foo`,
518
+ // `Array<Foo>`, `function f(): Foo`, etc.
519
+ TSTypeReference(path) {
520
+ const src = enclosing.top()
521
+ if (!src) return
522
+ const name = path.node.typeName?.name
523
+ || path.node.typeName?.right?.name // qualified Name
524
+ if (!name) return
525
+ const target = resolveRef(name)
526
+ if (!target || target.id === src) return
527
+ edges.push({ source: src, target: target.id, kind: 'type-ref', line: path.node.loc?.start.line || 0 })
528
+ },
529
+ })
530
+ // De-dup (same source→target multiple times is noise)
531
+ const seen = new Set()
532
+ const dedup = []
533
+ for (const e of edges) {
534
+ const key = e.source + '|' + e.target + '|' + e.kind
535
+ if (seen.has(key)) continue
536
+ seen.add(key)
537
+ dedup.push(e)
538
+ }
539
+ return dedup
540
+ }
541
+
542
+ module.exports = { extractSymbols, extractReferences }