@getmikk/core 2.0.13 → 2.0.15
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/README.md +4 -4
- package/package.json +2 -1
- package/src/analysis/index.ts +9 -0
- package/src/analysis/taint-analysis.ts +419 -0
- package/src/analysis/type-flow.ts +247 -0
- package/src/cache/incremental-cache.ts +278 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-reader.ts +1 -0
- package/src/contract/lock-compiler.ts +125 -12
- package/src/contract/schema.ts +4 -0
- package/src/error-handler.ts +2 -1
- package/src/graph/cluster-detector.ts +2 -4
- package/src/graph/dead-code-detector.ts +303 -117
- package/src/graph/graph-builder.ts +21 -161
- package/src/graph/impact-analyzer.ts +1 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/rich-function-index.ts +1080 -0
- package/src/graph/symbol-table.ts +252 -0
- package/src/hash/hash-store.ts +1 -0
- package/src/index.ts +4 -0
- package/src/parser/base-extractor.ts +19 -0
- package/src/parser/boundary-checker.ts +31 -12
- package/src/parser/error-recovery.ts +647 -0
- package/src/parser/function-body-extractor.ts +248 -0
- package/src/parser/go/go-extractor.ts +249 -676
- package/src/parser/index.ts +138 -295
- package/src/parser/language-registry.ts +57 -0
- package/src/parser/oxc-parser.ts +166 -28
- package/src/parser/oxc-resolver.ts +179 -11
- package/src/parser/parser-constants.ts +1 -0
- package/src/parser/rust/rust-extractor.ts +109 -0
- package/src/parser/tree-sitter/parser.ts +400 -66
- package/src/parser/tree-sitter/queries.ts +106 -10
- package/src/parser/types.ts +20 -1
- package/src/search/bm25.ts +21 -8
- package/src/search/direct-search.ts +472 -0
- package/src/search/embedding-provider.ts +249 -0
- package/src/search/index.ts +12 -0
- package/src/search/semantic-search.ts +435 -0
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +1 -0
- package/src/utils/atomic-write.ts +1 -0
- package/src/utils/errors.ts +89 -4
- package/src/utils/fs.ts +150 -65
- package/src/utils/json.ts +1 -0
- package/src/utils/language-registry.ts +96 -5
- package/src/utils/minimatch.ts +49 -6
- package/src/utils/path.ts +26 -0
- package/tests/dead-code.test.ts +3 -2
- package/tests/direct-search.test.ts +435 -0
- package/tests/error-recovery.test.ts +143 -0
- package/tests/fixtures/simple-api/src/index.ts +1 -1
- package/tests/go-parser.test.ts +19 -335
- package/tests/js-parser.test.ts +18 -1089
- package/tests/language-registry-all.test.ts +276 -0
- package/tests/language-registry.test.ts +6 -4
- package/tests/parse-diagnostics.test.ts +9 -96
- package/tests/parser.test.ts +42 -771
- package/tests/polyglot-parser.test.ts +117 -0
- package/tests/rich-function-index.test.ts +703 -0
- package/tests/tree-sitter-parser.test.ts +108 -80
- package/tests/ts-parser.test.ts +8 -8
- package/tests/verification.test.ts +175 -0
- package/src/parser/base-parser.ts +0 -16
- package/src/parser/go/go-parser.ts +0 -43
- package/src/parser/javascript/js-extractor.ts +0 -278
- package/src/parser/javascript/js-parser.ts +0 -101
- package/src/parser/typescript/ts-extractor.ts +0 -447
- package/src/parser/typescript/ts-parser.ts +0 -36
package/tests/js-parser.test.ts
CHANGED
|
@@ -1,1105 +1,34 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
import { JavaScriptParser } from '../src/parser/javascript/js-parser'
|
|
4
|
-
import { JavaScriptResolver } from '../src/parser/javascript/js-resolver'
|
|
5
|
-
import { TypeScriptExtractor } from '../src/parser/typescript/ts-extractor'
|
|
6
|
-
import { getParser } from '../src/parser/index'
|
|
2
|
+
import { TypescriptExtractor } from '../src/parser/oxc-parser'
|
|
7
3
|
|
|
8
|
-
// ─── Sample JS source files ───────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
/** Plain CommonJS module — require + module.exports */
|
|
11
4
|
const CJS_MODULE = `
|
|
12
|
-
'use strict'
|
|
13
|
-
|
|
14
|
-
const crypto = require('crypto')
|
|
15
|
-
const bcrypt = require('bcryptjs')
|
|
16
|
-
const { sign, verify } = require('jsonwebtoken')
|
|
17
5
|
const db = require('./db')
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Hash a plain-text password using bcrypt.
|
|
21
|
-
*/
|
|
22
|
-
function hashPassword(password) {
|
|
23
|
-
if (!password) throw new Error('password required')
|
|
24
|
-
return bcrypt.hash(password, 10)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Verify a JWT token and return the decoded payload.
|
|
29
|
-
*/
|
|
30
|
-
const verifyToken = function verifyJwt(token, secret) {
|
|
31
|
-
if (!token) return null
|
|
32
|
-
try {
|
|
33
|
-
return verify(token, secret)
|
|
34
|
-
} catch {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function getUser(id) {
|
|
40
|
-
if (!id) throw new Error('id required')
|
|
41
|
-
return db.findById(id)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = { hashPassword, verifyToken, getUser }
|
|
6
|
+
function hashPassword(password) { return password }
|
|
7
|
+
module.exports = { hashPassword }
|
|
45
8
|
`
|
|
46
9
|
|
|
47
|
-
/** ESM module */
|
|
48
10
|
const ESM_MODULE = `
|
|
49
|
-
import path from 'path'
|
|
50
11
|
import { readFile } from 'fs/promises'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
export async function loadConfig(configPath) {
|
|
54
|
-
const raw = await readFile(path.resolve(configPath), 'utf-8')
|
|
55
|
-
return JSON.parse(raw)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const formatTimestamp = (ts) => formatDate(new Date(ts))
|
|
59
|
-
|
|
60
|
-
export default function bootstrap(opts = {}) {
|
|
61
|
-
return { ...opts, started: true }
|
|
62
|
-
}
|
|
63
|
-
`
|
|
64
|
-
|
|
65
|
-
/** JSX component file */
|
|
66
|
-
const JSX_COMPONENT = `
|
|
67
|
-
import React from 'react'
|
|
68
|
-
|
|
69
|
-
// UserCard component — displays user info
|
|
70
|
-
function UserCard({ user, onEdit }) {
|
|
71
|
-
if (!user) return null
|
|
72
|
-
return (
|
|
73
|
-
<div className="card">
|
|
74
|
-
<h2>{user.name}</h2>
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const Avatar = ({ src, alt = 'avatar' }) => (
|
|
80
|
-
<img src={src} alt={alt} />
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
export { UserCard, Avatar }
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
/** module.exports = function patterns */
|
|
87
|
-
const MODULE_EXPORTS_FN = `
|
|
88
|
-
/**
|
|
89
|
-
* Handle HTTP login request.
|
|
90
|
-
*/
|
|
91
|
-
module.exports = function handleLogin(req, res) {
|
|
92
|
-
if (!req.body.email) {
|
|
93
|
-
return res.status(400).json({ error: 'email required' })
|
|
94
|
-
}
|
|
95
|
-
res.json({ ok: true })
|
|
96
|
-
}
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
/** exports.x = function patterns */
|
|
100
|
-
const EXPORTS_DOT_X = `
|
|
101
|
-
exports.createUser = function(data) {
|
|
102
|
-
if (!data.name) throw new Error('name required')
|
|
103
|
-
return { id: Date.now(), ...data }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
exports.deleteUser = async (id) => {
|
|
107
|
-
if (!id) throw new Error('id required')
|
|
108
|
-
return true
|
|
109
|
-
}
|
|
110
|
-
`
|
|
111
|
-
|
|
112
|
-
/** module.exports = object with functions */
|
|
113
|
-
const MODULE_EXPORTS_OBJ = `
|
|
114
|
-
function internalHelper(x) { return x * 2 }
|
|
115
|
-
|
|
116
|
-
module.exports = {
|
|
117
|
-
double: internalHelper,
|
|
118
|
-
triple: function(x) { return x * 3 },
|
|
119
|
-
square: (x) => x * x,
|
|
120
|
-
}
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
/** Express route definitions */
|
|
124
|
-
const EXPRESS_ROUTES = `
|
|
125
|
-
const express = require('express')
|
|
126
|
-
const router = express.Router()
|
|
127
|
-
|
|
128
|
-
const { getUser, createUser, deleteUser } = require('./controllers/users')
|
|
129
|
-
const authMiddleware = require('./middleware/auth')
|
|
130
|
-
|
|
131
|
-
router.get('/users', getUser)
|
|
132
|
-
router.post('/users', authMiddleware, createUser)
|
|
133
|
-
router.delete('/users/:id', authMiddleware, deleteUser)
|
|
134
|
-
|
|
135
|
-
module.exports = router
|
|
136
|
-
`
|
|
137
|
-
|
|
138
|
-
/** Edge cases */
|
|
139
|
-
const EDGE_CASES = `
|
|
140
|
-
// Dynamic require with a variable — should NOT be captured as a static import
|
|
141
|
-
const dynamic = require(someVariable)
|
|
142
|
-
|
|
143
|
-
// require.resolve — should NOT be captured (it's a property access on the require object)
|
|
144
|
-
const resolved = require.resolve('./module')
|
|
145
|
-
|
|
146
|
-
// Conditional require
|
|
147
|
-
const isNode = typeof window === 'undefined'
|
|
148
|
-
const platform = isNode ? require('node:os') : null
|
|
149
|
-
|
|
150
|
-
// Nested function in module.exports
|
|
151
|
-
module.exports = {
|
|
152
|
-
outer: function(x) {
|
|
153
|
-
function inner(y) { return y + 1 }
|
|
154
|
-
return inner(x)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// module.exports spread — graceful no-crash
|
|
159
|
-
const base = {}
|
|
160
|
-
module.exports = { ...base, extra: 1 }
|
|
161
|
-
`
|
|
162
|
-
|
|
163
|
-
/** Empty file */
|
|
164
|
-
const EMPTY_FILE = ``
|
|
165
|
-
|
|
166
|
-
/** Comments and whitespace only */
|
|
167
|
-
const COMMENTS_ONLY = `
|
|
168
|
-
// This file is intentionally left blank
|
|
169
|
-
/* Another comment block */
|
|
12
|
+
export async function loadConfig(configPath) { return {} }
|
|
170
13
|
`
|
|
171
14
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
export function combined() {
|
|
179
|
-
return defaultExport()
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
module.exports.legacy = function() {}
|
|
183
|
-
`
|
|
184
|
-
|
|
185
|
-
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
186
|
-
|
|
187
|
-
describe('JavaScriptExtractor', () => {
|
|
188
|
-
|
|
189
|
-
describe('CommonJS require() imports', () => {
|
|
190
|
-
const ext = new JavaScriptExtractor('src/auth.js', CJS_MODULE)
|
|
191
|
-
|
|
192
|
-
test('extracts plain require() as default import', () => {
|
|
193
|
-
const imports = ext.extractImports()
|
|
194
|
-
const crypto = imports.find(i => i.source === 'crypto')
|
|
195
|
-
expect(crypto).toBeDefined()
|
|
196
|
-
expect(crypto!.isDefault).toBe(true)
|
|
197
|
-
expect(crypto!.isDynamic).toBe(false)
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
test('extracts destructured require() as named imports', () => {
|
|
201
|
-
const imports = ext.extractImports()
|
|
202
|
-
const jwt = imports.find(i => i.source === 'jsonwebtoken')
|
|
203
|
-
expect(jwt).toBeDefined()
|
|
204
|
-
expect(jwt!.names).toContain('sign')
|
|
205
|
-
expect(jwt!.names).toContain('verify')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('extracts relative require(./db)', () => {
|
|
209
|
-
const imports = ext.extractImports()
|
|
210
|
-
const dbImp = imports.find(i => i.source === './db')
|
|
211
|
-
expect(dbImp).toBeDefined()
|
|
212
|
-
expect(dbImp!.names).toContain('db')
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
test('does NOT capture require(variable) — dynamic require skipped', () => {
|
|
216
|
-
const edgeExt = new JavaScriptExtractor('src/edge.js', EDGE_CASES)
|
|
217
|
-
const imports = edgeExt.extractImports()
|
|
218
|
-
// someVariable is not a StringLiteral — must not appear
|
|
219
|
-
const bad = imports.find(i => i.source === '' || i.source === 'someVariable')
|
|
220
|
-
expect(bad).toBeUndefined()
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
test('does NOT capture require.resolve() as an import', () => {
|
|
224
|
-
const edgeExt = new JavaScriptExtractor('src/edge.js', EDGE_CASES)
|
|
225
|
-
const imports = edgeExt.extractImports()
|
|
226
|
-
// require.resolve('./module') — node.expression is a PropertyAccessExpression,
|
|
227
|
-
// not an Identifier, so it must NOT be captured
|
|
228
|
-
const bad = imports.find(i => i.source === './module')
|
|
229
|
-
expect(bad).toBeUndefined()
|
|
230
|
-
})
|
|
15
|
+
describe('JavaScript Recognition (via OXC)', () => {
|
|
16
|
+
test('extracts CJS exports as functions', async () => {
|
|
17
|
+
const extractor = new TypescriptExtractor()
|
|
18
|
+
const result = await extractor.extract('src/auth.js', CJS_MODULE)
|
|
19
|
+
expect(result.functions.some(f => f.name === 'hashPassword')).toBe(true)
|
|
231
20
|
})
|
|
232
21
|
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const names = exports.map(e => e.name)
|
|
239
|
-
expect(names).toContain('hashPassword')
|
|
240
|
-
expect(names).toContain('verifyToken')
|
|
241
|
-
expect(names).toContain('getUser')
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
describe('module.exports = function pattern', () => {
|
|
246
|
-
const ext = new JavaScriptExtractor('src/login.js', MODULE_EXPORTS_FN)
|
|
247
|
-
|
|
248
|
-
test('extracts named function expression from module.exports', () => {
|
|
249
|
-
const fns = ext.extractFunctions()
|
|
250
|
-
const fn = fns.find(f => f.name === 'handleLogin')
|
|
251
|
-
expect(fn).toBeDefined()
|
|
252
|
-
expect(fn!.isExported).toBe(true)
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
test('extracted function has correct file and purpose', () => {
|
|
256
|
-
const fns = ext.extractFunctions()
|
|
257
|
-
const fn = fns.find(f => f.name === 'handleLogin')
|
|
258
|
-
expect(fn!.file).toBe('src/login.js')
|
|
259
|
-
expect(fn!.purpose).toMatch(/handle http login/i)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test('extracts params from module.exports function', () => {
|
|
263
|
-
const fns = ext.extractFunctions()
|
|
264
|
-
const fn = fns.find(f => f.name === 'handleLogin')
|
|
265
|
-
expect(fn!.params.map(p => p.name)).toEqual(['req', 'res'])
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
test('detects edge cases (early return guard)', () => {
|
|
269
|
-
const fns = ext.extractFunctions()
|
|
270
|
-
const fn = fns.find(f => f.name === 'handleLogin')
|
|
271
|
-
expect(fn!.edgeCasesHandled.length).toBeGreaterThan(0)
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
describe('exports.x = function pattern', () => {
|
|
276
|
-
const ext = new JavaScriptExtractor('src/users.js', EXPORTS_DOT_X)
|
|
277
|
-
|
|
278
|
-
test('extracts function assigned to exports.createUser', () => {
|
|
279
|
-
const fns = ext.extractFunctions()
|
|
280
|
-
const fn = fns.find(f => f.name === 'createUser')
|
|
281
|
-
expect(fn).toBeDefined()
|
|
282
|
-
expect(fn!.isExported).toBe(true)
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
test('extracts async arrow function assigned to exports.deleteUser', () => {
|
|
286
|
-
const fns = ext.extractFunctions()
|
|
287
|
-
const fn = fns.find(f => f.name === 'deleteUser')
|
|
288
|
-
expect(fn).toBeDefined()
|
|
289
|
-
expect(fn!.isAsync).toBe(true)
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
test('exports.x appears in extractExports()', () => {
|
|
293
|
-
const exports = ext.extractExports()
|
|
294
|
-
const names = exports.map(e => e.name)
|
|
295
|
-
expect(names).toContain('createUser')
|
|
296
|
-
expect(names).toContain('deleteUser')
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
test('detects throw as error handling', () => {
|
|
300
|
-
const fns = ext.extractFunctions()
|
|
301
|
-
const fn = fns.find(f => f.name === 'createUser')
|
|
302
|
-
expect(fn!.errorHandling.some(e => e.type === 'throw')).toBe(true)
|
|
303
|
-
})
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
describe('module.exports = { inline functions }', () => {
|
|
307
|
-
test('exports names from object literal', () => {
|
|
308
|
-
const ext = new JavaScriptExtractor('src/math.js', MODULE_EXPORTS_OBJ)
|
|
309
|
-
const exports = ext.extractExports()
|
|
310
|
-
const names = exports.map(e => e.name)
|
|
311
|
-
expect(names).toContain('double')
|
|
312
|
-
expect(names).toContain('triple')
|
|
313
|
-
expect(names).toContain('square')
|
|
314
|
-
})
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
describe('ESM imports and exports', () => {
|
|
318
|
-
const ext = new JavaScriptExtractor('src/loader.js', ESM_MODULE)
|
|
319
|
-
|
|
320
|
-
test('extracts static ESM imports', () => {
|
|
321
|
-
const imports = ext.extractImports()
|
|
322
|
-
const pathImp = imports.find(i => i.source === 'path')
|
|
323
|
-
expect(pathImp).toBeDefined()
|
|
324
|
-
const fsImp = imports.find(i => i.source === 'fs/promises')
|
|
325
|
-
expect(fsImp).toBeDefined()
|
|
326
|
-
expect(fsImp!.names).toContain('readFile')
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
test('extracts named ESM function export', () => {
|
|
330
|
-
const fns = ext.extractFunctions()
|
|
331
|
-
const fn = fns.find(f => f.name === 'loadConfig')
|
|
332
|
-
expect(fn).toBeDefined()
|
|
333
|
-
expect(fn!.isExported).toBe(true)
|
|
334
|
-
expect(fn!.isAsync).toBe(true)
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
test('extracts arrow function export', () => {
|
|
338
|
-
const fns = ext.extractFunctions()
|
|
339
|
-
const fn = fns.find(f => f.name === 'formatTimestamp')
|
|
340
|
-
expect(fn).toBeDefined()
|
|
341
|
-
expect(fn!.isExported).toBe(true)
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
test('extracts export default function', () => {
|
|
345
|
-
const fns = ext.extractFunctions()
|
|
346
|
-
const fn = fns.find(f => f.name === 'bootstrap')
|
|
347
|
-
expect(fn).toBeDefined()
|
|
348
|
-
})
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
describe('JSX components', () => {
|
|
352
|
-
const ext = new JavaScriptExtractor('src/UserCard.jsx', JSX_COMPONENT)
|
|
353
|
-
|
|
354
|
-
test('parses JSX without crashing', () => {
|
|
355
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test('extracts function component', () => {
|
|
359
|
-
const fns = ext.extractFunctions()
|
|
360
|
-
const fn = fns.find(f => f.name === 'UserCard')
|
|
361
|
-
expect(fn).toBeDefined()
|
|
362
|
-
expect(fn!.params[0].name).toBe('{ user, onEdit }')
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
test('extracts arrow function component', () => {
|
|
366
|
-
const fns = ext.extractFunctions()
|
|
367
|
-
const fn = fns.find(f => f.name === 'Avatar')
|
|
368
|
-
expect(fn).toBeDefined()
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
test('extracts purpose from comment above JSX component', () => {
|
|
372
|
-
const fns = ext.extractFunctions()
|
|
373
|
-
const fn = fns.find(f => f.name === 'UserCard')
|
|
374
|
-
expect(fn!.purpose).toMatch(/user.*card/i)
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
test('detects early-return edge case (if !user return null)', () => {
|
|
378
|
-
const fns = ext.extractFunctions()
|
|
379
|
-
const fn = fns.find(f => f.name === 'UserCard')
|
|
380
|
-
expect(fn!.edgeCasesHandled.length).toBeGreaterThan(0)
|
|
381
|
-
})
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
describe('async functions', () => {
|
|
385
|
-
test('marks async functions correctly', () => {
|
|
386
|
-
const ext = new JavaScriptExtractor('src/auth.js', CJS_MODULE)
|
|
387
|
-
const fns = ext.extractFunctions()
|
|
388
|
-
const fn = fns.find(f => f.name === 'getUser')
|
|
389
|
-
expect(fn).toBeDefined()
|
|
390
|
-
expect(fn!.isAsync).toBe(true)
|
|
391
|
-
})
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
describe('Express route detection', () => {
|
|
395
|
-
const ext = new JavaScriptExtractor('src/routes.js', EXPRESS_ROUTES)
|
|
396
|
-
|
|
397
|
-
test('detects GET route', () => {
|
|
398
|
-
const routes = ext.extractRoutes()
|
|
399
|
-
const get = routes.find(r => r.method === 'GET' && r.path === '/users')
|
|
400
|
-
expect(get).toBeDefined()
|
|
401
|
-
expect(get!.handler).toBe('getUser')
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
test('detects POST route with middleware', () => {
|
|
405
|
-
const routes = ext.extractRoutes()
|
|
406
|
-
const post = routes.find(r => r.method === 'POST' && r.path === '/users')
|
|
407
|
-
expect(post).toBeDefined()
|
|
408
|
-
expect(post!.middlewares).toContain('authMiddleware')
|
|
409
|
-
expect(post!.handler).toBe('createUser')
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
test('detects DELETE route', () => {
|
|
413
|
-
const routes = ext.extractRoutes()
|
|
414
|
-
const del = routes.find(r => r.method === 'DELETE')
|
|
415
|
-
expect(del).toBeDefined()
|
|
416
|
-
expect(del!.path).toBe('/users/:id')
|
|
417
|
-
})
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
describe('edge cases', () => {
|
|
421
|
-
test('empty file parses without error and returns empty arrays', () => {
|
|
422
|
-
const ext = new JavaScriptExtractor('src/empty.js', EMPTY_FILE)
|
|
423
|
-
expect(ext.extractFunctions()).toEqual([])
|
|
424
|
-
expect(ext.extractImports()).toEqual([])
|
|
425
|
-
expect(ext.extractExports()).toEqual([])
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
test('comments-only file parses without error', () => {
|
|
429
|
-
const ext = new JavaScriptExtractor('src/empty.js', COMMENTS_ONLY)
|
|
430
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
431
|
-
expect(ext.extractFunctions()).toEqual([])
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
test('module.exports spread literal does not crash', () => {
|
|
435
|
-
const ext = new JavaScriptExtractor('src/edge.js', EDGE_CASES)
|
|
436
|
-
expect(() => ext.extractExports()).not.toThrow()
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
test('mixed ESM + CJS file captures both import and require', () => {
|
|
440
|
-
const ext = new JavaScriptExtractor('src/mixed.js', MIXED_ESM_CJS)
|
|
441
|
-
const imports = ext.extractImports()
|
|
442
|
-
const esmImp = imports.find(i => i.source === './base.js')
|
|
443
|
-
const cjsImp = imports.find(i => i.source === './extra')
|
|
444
|
-
expect(esmImp).toBeDefined()
|
|
445
|
-
expect(cjsImp).toBeDefined()
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
test('mixed ESM + CJS: captures both ESM and CJS exports', () => {
|
|
449
|
-
const ext = new JavaScriptExtractor('src/mixed.js', MIXED_ESM_CJS)
|
|
450
|
-
const exports = ext.extractExports()
|
|
451
|
-
const names = exports.map(e => e.name)
|
|
452
|
-
expect(names).toContain('combined') // ESM export
|
|
453
|
-
expect(names).toContain('legacy') // exports.legacy = function()
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
test('no duplicate imports when same source appears in both ESM and CJS', () => {
|
|
457
|
-
// Unlikely but guard: same source in require and import
|
|
458
|
-
const src = `import x from './foo'; const y = require('./foo')`
|
|
459
|
-
const ext = new JavaScriptExtractor('src/dup.js', src)
|
|
460
|
-
const imports = ext.extractImports()
|
|
461
|
-
const fooImports = imports.filter(i => i.source === './foo')
|
|
462
|
-
expect(fooImports.length).toBe(1)
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
test('no duplicate exports when CJS and ESM both declare same name', () => {
|
|
466
|
-
const src = `export function greet() {} \nexports.greet = function() {}`
|
|
467
|
-
const ext = new JavaScriptExtractor('src/dup.js', src)
|
|
468
|
-
const exports = ext.extractExports()
|
|
469
|
-
const greetExports = exports.filter(e => e.name === 'greet')
|
|
470
|
-
expect(greetExports.length).toBe(1)
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
test('malformed code gracefully degrades without crashing', () => {
|
|
474
|
-
const malformed = `
|
|
475
|
-
function breakMe() {
|
|
476
|
-
const x =
|
|
477
|
-
if (true) {
|
|
478
|
-
// missing braces, missing assignments
|
|
479
|
-
`
|
|
480
|
-
const ext = new JavaScriptExtractor('src/malformed.js', malformed)
|
|
481
|
-
// TS compiler API is very fault tolerant, so it might extract breakMe anyway,
|
|
482
|
-
// but the key assertion is that it doesn't throw.
|
|
483
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
484
|
-
const fn = ext.extractFunctions().find(f => f.name === 'breakMe')
|
|
485
|
-
expect(fn).toBeDefined()
|
|
486
|
-
})
|
|
487
|
-
})
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
describe('JavaScriptParser', () => {
|
|
491
|
-
const parser = new JavaScriptParser()
|
|
492
|
-
|
|
493
|
-
test('getSupportedExtensions includes .js .mjs .cjs .jsx', () => {
|
|
494
|
-
const exts = parser.getSupportedExtensions()
|
|
495
|
-
expect(exts).toContain('.js')
|
|
496
|
-
expect(exts).toContain('.mjs')
|
|
497
|
-
expect(exts).toContain('.cjs')
|
|
498
|
-
expect(exts).toContain('.jsx')
|
|
22
|
+
test('extracts ESM imports and exports', async () => {
|
|
23
|
+
const extractor = new TypescriptExtractor()
|
|
24
|
+
const result = await extractor.extract('src/loader.js', ESM_MODULE)
|
|
25
|
+
expect(result.imports.some(i => i.source === 'fs/promises')).toBe(true)
|
|
26
|
+
expect(result.functions.some(f => f.name === 'loadConfig')).toBe(true)
|
|
499
27
|
})
|
|
500
28
|
|
|
501
|
-
test('
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
test('parse includes hash and parsedAt', async () => {
|
|
507
|
-
const result = await parser.parse('src/index.js', CJS_MODULE)
|
|
508
|
-
expect(typeof result.hash).toBe('string')
|
|
509
|
-
expect(result.hash.length).toBe(64) // SHA-256 hex
|
|
510
|
-
expect(typeof result.parsedAt).toBe('number')
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
test('CJS-exported functions are marked isExported via cross-reference', async () => {
|
|
514
|
-
const result = await parser.parse('src/auth.js', CJS_MODULE)
|
|
515
|
-
const hashPw = result.functions.find((f: any) => f.name === 'hashPassword')
|
|
516
|
-
expect(hashPw).toBeDefined()
|
|
517
|
-
expect(hashPw!.isExported).toBe(true)
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
test('resolveImports resolves relative paths with .js extension probing', async () => {
|
|
521
|
-
const files = await Promise.all([
|
|
522
|
-
parser.parse('src/auth.js', CJS_MODULE),
|
|
523
|
-
parser.parse('src/loader.js', ESM_MODULE),
|
|
524
|
-
])
|
|
525
|
-
// When no allProjectFiles list is passed to resolveImports, the resolver
|
|
526
|
-
// falls back to extension probing without filesystem validation and resolves
|
|
527
|
-
// relative imports to their most-likely path (e.g. './db' → 'src/db.js').
|
|
528
|
-
//
|
|
529
|
-
// Previously the test relied on the broken behaviour where the resolver
|
|
530
|
-
// always probed through even when the file wasn't in the provided list.
|
|
531
|
-
// The correct fix is to call resolveImports without a restrictive file list,
|
|
532
|
-
// which is what happens in production (the parser computes allFilePaths
|
|
533
|
-
// from the full project scan, not just the two files under test).
|
|
534
|
-
//
|
|
535
|
-
// We simulate a "full project" by telling the resolver that src/db.js exists.
|
|
536
|
-
const allProjectFiles = [
|
|
537
|
-
'src/auth.js',
|
|
538
|
-
'src/loader.js',
|
|
539
|
-
'src/db.js', // ← the file that auth.js imports
|
|
540
|
-
]
|
|
541
|
-
// resolveImports in JavaScriptParser uses files.map(f => f.path) internally,
|
|
542
|
-
// so to inject a richer file list we call the resolver directly here.
|
|
543
|
-
const resolver = new JavaScriptResolver('/project')
|
|
544
|
-
const authFile = files.find((f: any) => f.path === 'src/auth.js')!
|
|
545
|
-
const resolvedImports = resolver.resolveAll(authFile.imports, authFile.path, allProjectFiles)
|
|
546
|
-
const dbImport = resolvedImports.find((i: any) => i.source === './db')
|
|
547
|
-
expect(dbImport).toBeDefined()
|
|
548
|
-
expect(dbImport!.resolvedPath).toMatch(/src\/db/)
|
|
549
|
-
expect(dbImport!.resolvedPath).toMatch(/\.js$/)
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
test('resolveImports leaves external packages unresolved (empty resolvedPath)', async () => {
|
|
553
|
-
const files = [await parser.parse('src/auth.js', CJS_MODULE)]
|
|
554
|
-
const resolved = await parser.resolveImports(files, '/project')
|
|
555
|
-
const file = resolved[0]
|
|
556
|
-
const cryptoImp = file.imports.find((i: any) => i.source === 'crypto')
|
|
557
|
-
expect(cryptoImp!.resolvedPath).toBe('')
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
test('parse .jsx file language is javascript', async () => {
|
|
561
|
-
const result = await parser.parse('src/UserCard.jsx', JSX_COMPONENT)
|
|
562
|
-
expect(result.language).toBe('javascript')
|
|
563
|
-
})
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
describe('JavaScriptResolver', () => {
|
|
567
|
-
const resolver = new JavaScriptResolver('/project')
|
|
568
|
-
|
|
569
|
-
test('resolves relative import with .js extension', () => {
|
|
570
|
-
const imp = { source: './utils', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
|
|
571
|
-
const result = resolver.resolve(imp, 'src/index.js', ['src/utils.js'])
|
|
572
|
-
expect(result.resolvedPath).toBe('src/utils.js')
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
test('resolves relative import with /index.js fallback', () => {
|
|
576
|
-
const imp = { source: './utils', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
|
|
577
|
-
const result = resolver.resolve(imp, 'src/index.js', ['src/utils/index.js'])
|
|
578
|
-
expect(result.resolvedPath).toBe('src/utils/index.js')
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
test('falls back to .ts for mixed TS/JS project', () => {
|
|
582
|
-
const imp = { source: './shared', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
|
|
583
|
-
const result = resolver.resolve(imp, 'src/index.js', ['src/shared.ts'])
|
|
584
|
-
expect(result.resolvedPath).toBe('src/shared.ts')
|
|
585
|
-
})
|
|
586
|
-
|
|
587
|
-
test('leaves external packages unresolved', () => {
|
|
588
|
-
const imp = { source: 'lodash', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
|
|
589
|
-
const result = resolver.resolve(imp, 'src/index.js')
|
|
590
|
-
expect(result.resolvedPath).toBe('')
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
test('resolves with no known files list (defaults to .js suffix)', () => {
|
|
594
|
-
const imp = { source: './foo', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
|
|
595
|
-
const result = resolver.resolve(imp, 'src/index.js', [])
|
|
596
|
-
expect(result.resolvedPath).toMatch(/foo\.js$/)
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
test('source with existing .js extension returned as-is', () => {
|
|
600
|
-
const imp = { source: './utils.js', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
|
|
601
|
-
const result = resolver.resolve(imp, 'src/index.js', ['src/utils.js'])
|
|
602
|
-
expect(result.resolvedPath).toBe('src/utils.js')
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
test('resolves path alias when aliases provided', () => {
|
|
606
|
-
const resolver2 = new JavaScriptResolver('/project', { '@/*': ['src/*'] })
|
|
607
|
-
const imp = { source: '@/utils', resolvedPath: '', names: [], isDefault: false, isDynamic: false }
|
|
608
|
-
const result = resolver2.resolve(imp, 'src/components/Button.js', ['src/utils.js'])
|
|
609
|
-
expect(result.resolvedPath).toBe('src/utils.js')
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
test('resolveAll resolves all imports in a list', () => {
|
|
613
|
-
const imports = [
|
|
614
|
-
{ source: './a', resolvedPath: '', names: [], isDefault: false, isDynamic: false },
|
|
615
|
-
{ source: 'lodash', resolvedPath: '', names: [], isDefault: false, isDynamic: false },
|
|
616
|
-
]
|
|
617
|
-
const results = resolver.resolveAll(imports, 'src/index.js', ['src/a.js'])
|
|
618
|
-
expect(results[0].resolvedPath).toBe('src/a.js')
|
|
619
|
-
expect(results[1].resolvedPath).toBe('')
|
|
620
|
-
})
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
describe('getParser — JS extensions', () => {
|
|
624
|
-
test('returns JavaScriptParser for .js', () => {
|
|
625
|
-
const p = getParser('src/index.js')
|
|
626
|
-
expect(p.getSupportedExtensions()).toContain('.js')
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
test('returns JavaScriptParser for .mjs', () => {
|
|
630
|
-
const p = getParser('src/index.mjs')
|
|
631
|
-
expect(p.getSupportedExtensions()).toContain('.mjs')
|
|
632
|
-
})
|
|
633
|
-
|
|
634
|
-
test('returns JavaScriptParser for .cjs', () => {
|
|
635
|
-
const p = getParser('src/index.cjs')
|
|
636
|
-
expect(p.getSupportedExtensions()).toContain('.cjs')
|
|
637
|
-
})
|
|
638
|
-
|
|
639
|
-
test('returns JavaScriptParser for .jsx', () => {
|
|
640
|
-
const p = getParser('src/App.jsx')
|
|
641
|
-
expect(p.getSupportedExtensions()).toContain('.jsx')
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
test('still returns TypeScriptParser for .ts', () => {
|
|
645
|
-
const p = getParser('src/index.ts')
|
|
646
|
-
expect(p.getSupportedExtensions()).toContain('.ts')
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
test('still throws UnsupportedLanguageError for .xyz', () => {
|
|
650
|
-
expect(() => getParser('src/app.xyz')).toThrow()
|
|
651
|
-
})
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
// ==========================================
|
|
655
|
-
// ADDITIONAL COMPREHENSIVE TESTS
|
|
656
|
-
// ==========================================
|
|
657
|
-
|
|
658
|
-
describe('JavaScript - Additional Edge Cases', () => {
|
|
659
|
-
|
|
660
|
-
describe('Dynamic Imports', () => {
|
|
661
|
-
test('handles dynamic import()', () => {
|
|
662
|
-
const src = `
|
|
663
|
-
const module = await import('./dynamic')
|
|
664
|
-
const mod = await import('lodash')
|
|
665
|
-
`
|
|
666
|
-
const ext = new JavaScriptExtractor('src/app.js', src)
|
|
667
|
-
const imports = ext.extractImports()
|
|
668
|
-
expect(imports.length).toBeGreaterThanOrEqual(0)
|
|
669
|
-
})
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
describe('Class Syntax', () => {
|
|
673
|
-
test('extracts ES6 classes', () => {
|
|
674
|
-
const src = `
|
|
675
|
-
class User {
|
|
676
|
-
constructor(name) {
|
|
677
|
-
this.name = name
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
getName() {
|
|
681
|
-
return this.name
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
static create(data) {
|
|
685
|
-
return new User(data.name)
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
class Admin extends User {
|
|
690
|
-
constructor(name, role) {
|
|
691
|
-
super(name)
|
|
692
|
-
this.role = role
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
`
|
|
696
|
-
const ext = new JavaScriptExtractor('src/user.js', src)
|
|
697
|
-
const classes = ext.extractClasses()
|
|
698
|
-
expect(classes.length).toBe(2)
|
|
699
|
-
expect(classes[0].name).toBe('User')
|
|
700
|
-
expect(classes[1].name).toBe('Admin')
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
test('extracts class methods', () => {
|
|
704
|
-
const src = `
|
|
705
|
-
class Calculator {
|
|
706
|
-
add(a, b) { return a + b }
|
|
707
|
-
subtract(a, b) { return a - b }
|
|
708
|
-
}
|
|
709
|
-
`
|
|
710
|
-
const ext = new JavaScriptExtractor('src/calc.js', src)
|
|
711
|
-
const classes = ext.extractClasses()
|
|
712
|
-
expect(classes[0].methods.length).toBe(2)
|
|
713
|
-
})
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
describe('Object Patterns', () => {
|
|
717
|
-
test('handles object literal functions', () => {
|
|
718
|
-
const src = `
|
|
719
|
-
function formatDate() { return 'date' }
|
|
720
|
-
function parseJSON() { return 'json' }
|
|
721
|
-
`
|
|
722
|
-
const ext = new JavaScriptExtractor('src/utils.js', src)
|
|
723
|
-
const fns = ext.extractFunctions()
|
|
724
|
-
expect(fns.length).toBe(2)
|
|
725
|
-
})
|
|
726
|
-
|
|
727
|
-
test('handles computed property names', () => {
|
|
728
|
-
const src = `
|
|
729
|
-
const obj = {
|
|
730
|
-
[key]: value,
|
|
731
|
-
['computed' + 'Key']() {}
|
|
732
|
-
}
|
|
733
|
-
`
|
|
734
|
-
const ext = new JavaScriptExtractor('src/obj.js', src)
|
|
735
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
736
|
-
})
|
|
737
|
-
})
|
|
738
|
-
|
|
739
|
-
describe('Arrow Functions', () => {
|
|
740
|
-
test('handles various arrow function patterns', () => {
|
|
741
|
-
const src = `
|
|
742
|
-
const add = (a, b) => a + b
|
|
743
|
-
const greet = name => \`Hello \${name}\`
|
|
744
|
-
const promise = () => new Promise(resolve => resolve())
|
|
745
|
-
const multi = (a, b) => {
|
|
746
|
-
return a + b
|
|
747
|
-
}
|
|
748
|
-
`
|
|
749
|
-
const ext = new JavaScriptExtractor('src/arrow.js', src)
|
|
750
|
-
const fns = ext.extractFunctions()
|
|
751
|
-
expect(fns.length).toBe(4)
|
|
752
|
-
})
|
|
753
|
-
})
|
|
754
|
-
|
|
755
|
-
describe('Callback Patterns', () => {
|
|
756
|
-
test('handles callback functions', () => {
|
|
757
|
-
const src = `
|
|
758
|
-
items.forEach(item => console.log(item))
|
|
759
|
-
const filtered = items.filter(x => x > 0)
|
|
760
|
-
const mapped = items.map(x => x * 2)
|
|
761
|
-
const found = items.find(x => x.id === id)
|
|
762
|
-
`
|
|
763
|
-
const ext = new JavaScriptExtractor('src/callbacks.js', src)
|
|
764
|
-
const fns = ext.extractFunctions()
|
|
765
|
-
expect(fns.length).toBe(0) // callbacks aren't function declarations
|
|
766
|
-
})
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
describe('Error Handling in Functions', () => {
|
|
770
|
-
test('detects try-catch blocks', () => {
|
|
771
|
-
const src = `
|
|
772
|
-
function safeParse(json) {
|
|
773
|
-
try {
|
|
774
|
-
return JSON.parse(json)
|
|
775
|
-
} catch (e) {
|
|
776
|
-
console.error(e)
|
|
777
|
-
return null
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
`
|
|
781
|
-
const ext = new JavaScriptExtractor('src/safe.js', src)
|
|
782
|
-
const fns = ext.extractFunctions()
|
|
783
|
-
expect(fns[0].errorHandling.length).toBeGreaterThan(0)
|
|
784
|
-
})
|
|
785
|
-
|
|
786
|
-
test('detects throw statements', () => {
|
|
787
|
-
const src = `
|
|
788
|
-
function validate(value) {
|
|
789
|
-
if (!value) {
|
|
790
|
-
throw new Error('value required')
|
|
791
|
-
}
|
|
792
|
-
return true
|
|
793
|
-
}
|
|
794
|
-
`
|
|
795
|
-
const ext = new JavaScriptExtractor('src/validate.js', src)
|
|
796
|
-
const fns = ext.extractFunctions()
|
|
797
|
-
expect(fns[0].errorHandling.some(e => e.type === 'throw')).toBe(true)
|
|
798
|
-
})
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
describe('Complex Return Statements', () => {
|
|
802
|
-
test('handles early returns', () => {
|
|
803
|
-
const src = `
|
|
804
|
-
function findUser(id) {
|
|
805
|
-
if (!id) return null
|
|
806
|
-
const user = db.find(id)
|
|
807
|
-
if (!user) return null
|
|
808
|
-
return user
|
|
809
|
-
}
|
|
810
|
-
`
|
|
811
|
-
const ext = new JavaScriptExtractor('src/find.js', src)
|
|
812
|
-
const fns = ext.extractFunctions()
|
|
813
|
-
expect(fns[0].edgeCasesHandled.length).toBeGreaterThan(0)
|
|
814
|
-
})
|
|
815
|
-
|
|
816
|
-
test('handles conditional returns', () => {
|
|
817
|
-
const src = `
|
|
818
|
-
function getStatus(isActive) {
|
|
819
|
-
return isActive ? 'active' : 'inactive'
|
|
820
|
-
}
|
|
821
|
-
`
|
|
822
|
-
const ext = new JavaScriptExtractor('src/status.js', src)
|
|
823
|
-
const fns = ext.extractFunctions()
|
|
824
|
-
expect(fns.length).toBe(1)
|
|
825
|
-
})
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
describe('Async/Await', () => {
|
|
829
|
-
test('handles async arrow functions', () => {
|
|
830
|
-
const src = `
|
|
831
|
-
const fetchData = async (url) => {
|
|
832
|
-
const res = await fetch(url)
|
|
833
|
-
return res.json()
|
|
834
|
-
}
|
|
835
|
-
`
|
|
836
|
-
const ext = new JavaScriptExtractor('src/async.js', src)
|
|
837
|
-
const fns = ext.extractFunctions()
|
|
838
|
-
expect(fns[0].isAsync).toBe(true)
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
test('handles await without async wrapper', () => {
|
|
842
|
-
const src = `
|
|
843
|
-
async function main() {
|
|
844
|
-
await doSomething()
|
|
845
|
-
}
|
|
846
|
-
`
|
|
847
|
-
const ext = new JavaScriptExtractor('src/main.js', src)
|
|
848
|
-
const fns = ext.extractFunctions()
|
|
849
|
-
expect(fns[0].isAsync).toBe(true)
|
|
850
|
-
})
|
|
851
|
-
|
|
852
|
-
test('handles Promise.all', () => {
|
|
853
|
-
const src = `
|
|
854
|
-
async function loadAll(urls) {
|
|
855
|
-
return Promise.all(urls.map(fetch))
|
|
856
|
-
}
|
|
857
|
-
`
|
|
858
|
-
const ext = new JavaScriptExtractor('src/load.js', src)
|
|
859
|
-
const fns = ext.extractFunctions()
|
|
860
|
-
expect(fns[0].isAsync).toBe(true)
|
|
861
|
-
})
|
|
862
|
-
})
|
|
863
|
-
|
|
864
|
-
describe('Generator Functions', () => {
|
|
865
|
-
test('handles generator functions', () => {
|
|
866
|
-
const src = `
|
|
867
|
-
function* numberGenerator() {
|
|
868
|
-
yield 1
|
|
869
|
-
yield 2
|
|
870
|
-
yield 3
|
|
871
|
-
}
|
|
872
|
-
`
|
|
873
|
-
const ext = new JavaScriptExtractor('src/gen.js', src)
|
|
874
|
-
const fns = ext.extractFunctions()
|
|
875
|
-
expect(fns.length).toBe(1)
|
|
876
|
-
})
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
describe('Destructuring', () => {
|
|
880
|
-
test('handles destructured parameters', () => {
|
|
881
|
-
const src = `
|
|
882
|
-
function process({ name, age }, [first, ...rest]) {
|
|
883
|
-
return name + age + first
|
|
884
|
-
}
|
|
885
|
-
`
|
|
886
|
-
const ext = new JavaScriptExtractor('src/dest.js', src)
|
|
887
|
-
const fns = ext.extractFunctions()
|
|
888
|
-
expect(fns[0].params.length).toBe(2)
|
|
889
|
-
})
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
describe('Rest/Spread', () => {
|
|
893
|
-
test('handles rest parameters', () => {
|
|
894
|
-
const src = `
|
|
895
|
-
function sum(...numbers) {
|
|
896
|
-
return numbers.reduce((a, b) => a + b, 0)
|
|
897
|
-
}
|
|
898
|
-
`
|
|
899
|
-
const ext = new JavaScriptExtractor('src/rest.js', src)
|
|
900
|
-
const fns = ext.extractFunctions()
|
|
901
|
-
expect(fns[0].params[0].name).toBe('numbers')
|
|
902
|
-
})
|
|
903
|
-
|
|
904
|
-
test('handles spread in function calls', () => {
|
|
905
|
-
const src = `
|
|
906
|
-
function apply(...args) {
|
|
907
|
-
return fn(...args)
|
|
908
|
-
}
|
|
909
|
-
`
|
|
910
|
-
const ext = new JavaScriptExtractor('src/spread.js', src)
|
|
911
|
-
const fns = ext.extractFunctions()
|
|
912
|
-
expect(fns.length).toBe(1)
|
|
913
|
-
})
|
|
914
|
-
})
|
|
915
|
-
|
|
916
|
-
describe('Template Literals', () => {
|
|
917
|
-
test('handles template literals', () => {
|
|
918
|
-
const src = `
|
|
919
|
-
function greet(name) {
|
|
920
|
-
return \`Hello, \${name}!\`
|
|
921
|
-
}
|
|
922
|
-
`
|
|
923
|
-
const ext = new JavaScriptExtractor('src/tmpl.js', src)
|
|
924
|
-
const fns = ext.extractFunctions()
|
|
925
|
-
expect(fns.length).toBe(1)
|
|
926
|
-
})
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
describe('Regex Patterns', () => {
|
|
930
|
-
test('handles regex in code', () => {
|
|
931
|
-
const src = `
|
|
932
|
-
function validateEmail(email) {
|
|
933
|
-
const re = /^[a-zA-Z0-9@]+.[a-zA-Z0-9@]+$/
|
|
934
|
-
return re.test(email)
|
|
935
|
-
}
|
|
936
|
-
`
|
|
937
|
-
const ext = new JavaScriptExtractor('src/regex.js', src)
|
|
938
|
-
const fns = ext.extractFunctions()
|
|
939
|
-
expect(fns.length).toBe(1)
|
|
940
|
-
})
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
describe('Module Patterns', () => {
|
|
944
|
-
test('handles IIFE', () => {
|
|
945
|
-
const src = `
|
|
946
|
-
(function() {
|
|
947
|
-
const privateVar = 'secret'
|
|
948
|
-
window.init = function() {}
|
|
949
|
-
})()
|
|
950
|
-
|
|
951
|
-
(async () => {
|
|
952
|
-
await load()
|
|
953
|
-
})()
|
|
954
|
-
`
|
|
955
|
-
const ext = new JavaScriptExtractor('src/iife.js', src)
|
|
956
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
test('handles UMD pattern', () => {
|
|
960
|
-
const src = `
|
|
961
|
-
(function(root, factory) {
|
|
962
|
-
if (typeof module === 'object') {
|
|
963
|
-
module.exports = factory()
|
|
964
|
-
} else {
|
|
965
|
-
root.MyLib = factory()
|
|
966
|
-
}
|
|
967
|
-
}(this, function() {
|
|
968
|
-
return { version: '1.0' }
|
|
969
|
-
}))
|
|
970
|
-
`
|
|
971
|
-
const ext = new JavaScriptExtractor('src/umd.js', src)
|
|
972
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
973
|
-
})
|
|
974
|
-
})
|
|
975
|
-
|
|
976
|
-
describe('Complex Types', () => {
|
|
977
|
-
test('handles JSDoc comments', () => {
|
|
978
|
-
const src = `
|
|
979
|
-
/**
|
|
980
|
-
* Adds two numbers
|
|
981
|
-
* @param {number} a - First number
|
|
982
|
-
* @param {number} b - Second number
|
|
983
|
-
* @returns {number} Sum
|
|
984
|
-
*/
|
|
985
|
-
function add(a, b) {
|
|
986
|
-
return a + b
|
|
987
|
-
}
|
|
988
|
-
`
|
|
989
|
-
const ext = new TypeScriptExtractor('src/add.js', src)
|
|
990
|
-
const fns = ext.extractFunctions()
|
|
991
|
-
expect(fns[0].purpose).toBeDefined()
|
|
992
|
-
})
|
|
993
|
-
})
|
|
994
|
-
|
|
995
|
-
describe('Decorator-like Patterns', () => {
|
|
996
|
-
test('handles higher-order functions', () => {
|
|
997
|
-
const src = `
|
|
998
|
-
function withLogging(fn) {
|
|
999
|
-
return function(...args) {
|
|
1000
|
-
console.log('Calling', fn.name)
|
|
1001
|
-
return fn.apply(this, args)
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
@withLogging
|
|
1006
|
-
function decorated() {}
|
|
1007
|
-
`
|
|
1008
|
-
const ext = new JavaScriptExtractor('src/decorator.js', src)
|
|
1009
|
-
const fns = ext.extractFunctions()
|
|
1010
|
-
expect(fns.length).toBe(2)
|
|
1011
|
-
})
|
|
1012
|
-
})
|
|
1013
|
-
|
|
1014
|
-
describe('Web/Node APIs', () => {
|
|
1015
|
-
test('handles fetch API', async () => {
|
|
1016
|
-
const src = `
|
|
1017
|
-
async function fetchData(url) {
|
|
1018
|
-
const response = await fetch(url)
|
|
1019
|
-
const data = await response.json()
|
|
1020
|
-
return data
|
|
1021
|
-
}
|
|
1022
|
-
`
|
|
1023
|
-
const ext = new JavaScriptExtractor('src/fetch.js', src)
|
|
1024
|
-
const fns = ext.extractFunctions()
|
|
1025
|
-
expect(fns[0].isAsync).toBe(true)
|
|
1026
|
-
expect(fns[0].calls.length).toBe(2)
|
|
1027
|
-
})
|
|
1028
|
-
|
|
1029
|
-
test('handles Express routers', () => {
|
|
1030
|
-
const src = `
|
|
1031
|
-
const express = require('express')
|
|
1032
|
-
const router = express.Router()
|
|
1033
|
-
|
|
1034
|
-
router.get('/users', getUsers)
|
|
1035
|
-
router.post('/users', createUser)
|
|
1036
|
-
router.put('/users/:id', updateUser)
|
|
1037
|
-
router.delete('/users/:id', deleteUser)
|
|
1038
|
-
`
|
|
1039
|
-
const ext = new JavaScriptExtractor('src/routes.js', src)
|
|
1040
|
-
const routes = ext.extractRoutes()
|
|
1041
|
-
expect(routes.length).toBe(4)
|
|
1042
|
-
})
|
|
1043
|
-
})
|
|
1044
|
-
|
|
1045
|
-
describe('Deterministic parsing', () => {
|
|
1046
|
-
test('produces identical file hash for identical source', async () => {
|
|
1047
|
-
const parser = new JavaScriptParser()
|
|
1048
|
-
const source = 'export function stable() { return 1 }'
|
|
1049
|
-
const a = await parser.parse('src/stable.js', source)
|
|
1050
|
-
const b = await parser.parse('src/stable.js', source)
|
|
1051
|
-
expect(a.hash).toBe(b.hash)
|
|
1052
|
-
})
|
|
1053
|
-
})
|
|
1054
|
-
|
|
1055
|
-
describe('Chained Methods', () => {
|
|
1056
|
-
test('handles method chaining', () => {
|
|
1057
|
-
const src = `
|
|
1058
|
-
const result = items
|
|
1059
|
-
.filter(x => x.active)
|
|
1060
|
-
.map(x => x.value)
|
|
1061
|
-
.reduce((a, b) => a + b, 0)
|
|
1062
|
-
`
|
|
1063
|
-
const ext = new JavaScriptExtractor('src/chain.js', src)
|
|
1064
|
-
expect(() => ext.extractFunctions()).not.toThrow()
|
|
1065
|
-
})
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
describe('Large Files', () => {
|
|
1069
|
-
test('handles many functions', () => {
|
|
1070
|
-
const fns = Array.from({ length: 500 }, (_, i) =>
|
|
1071
|
-
`function fn${i}() { return ${i} }`
|
|
1072
|
-
).join('\n')
|
|
1073
|
-
|
|
1074
|
-
const ext = new JavaScriptExtractor('src/many.js', fns)
|
|
1075
|
-
const result = ext.extractFunctions()
|
|
1076
|
-
expect(result.length).toBe(500)
|
|
1077
|
-
})
|
|
1078
|
-
})
|
|
1079
|
-
|
|
1080
|
-
describe('Unicode and Special Chars', () => {
|
|
1081
|
-
test('handles unicode in function names', () => {
|
|
1082
|
-
const src = `
|
|
1083
|
-
function 验证() {
|
|
1084
|
-
return true
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const 用户 = { name: 'test' }
|
|
1088
|
-
`
|
|
1089
|
-
const ext = new JavaScriptExtractor('src/unicode.js', src)
|
|
1090
|
-
const fns = ext.extractFunctions()
|
|
1091
|
-
expect(fns.length).toBe(1)
|
|
1092
|
-
})
|
|
1093
|
-
|
|
1094
|
-
test('handles emoji', () => {
|
|
1095
|
-
const src = `
|
|
1096
|
-
function 🎉() {
|
|
1097
|
-
return 'celebration'
|
|
1098
|
-
}
|
|
1099
|
-
`
|
|
1100
|
-
const ext = new JavaScriptExtractor('src/emoji.js', src)
|
|
1101
|
-
const fns = ext.extractFunctions()
|
|
1102
|
-
expect(fns.length).toBe(1)
|
|
1103
|
-
})
|
|
29
|
+
test('parses JSX without crashing', async () => {
|
|
30
|
+
const extractor = new TypescriptExtractor()
|
|
31
|
+
const result = await extractor.extract('src/App.jsx', 'function App() { return <div></div> }')
|
|
32
|
+
expect(result.functions.some(f => f.name === 'App')).toBe(true)
|
|
1104
33
|
})
|
|
1105
34
|
})
|