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,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 }
|