@getmikk/core 2.0.14 → 2.0.16

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 (64) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/type-flow.ts +1 -1
  4. package/src/cache/incremental-cache.ts +86 -80
  5. package/src/contract/contract-reader.ts +1 -0
  6. package/src/contract/lock-compiler.ts +95 -13
  7. package/src/contract/schema.ts +2 -0
  8. package/src/error-handler.ts +2 -1
  9. package/src/graph/cluster-detector.ts +2 -4
  10. package/src/graph/dead-code-detector.ts +303 -117
  11. package/src/graph/graph-builder.ts +21 -161
  12. package/src/graph/impact-analyzer.ts +1 -0
  13. package/src/graph/index.ts +2 -0
  14. package/src/graph/rich-function-index.ts +1080 -0
  15. package/src/graph/symbol-table.ts +252 -0
  16. package/src/hash/hash-store.ts +1 -0
  17. package/src/index.ts +2 -0
  18. package/src/parser/base-extractor.ts +19 -0
  19. package/src/parser/boundary-checker.ts +31 -12
  20. package/src/parser/error-recovery.ts +5 -4
  21. package/src/parser/function-body-extractor.ts +248 -0
  22. package/src/parser/go/go-extractor.ts +249 -676
  23. package/src/parser/index.ts +132 -318
  24. package/src/parser/language-registry.ts +57 -0
  25. package/src/parser/oxc-parser.ts +166 -28
  26. package/src/parser/oxc-resolver.ts +179 -11
  27. package/src/parser/parser-constants.ts +1 -0
  28. package/src/parser/rust/rust-extractor.ts +109 -0
  29. package/src/parser/tree-sitter/parser.ts +369 -62
  30. package/src/parser/tree-sitter/queries.ts +106 -10
  31. package/src/parser/types.ts +20 -1
  32. package/src/search/bm25.ts +21 -8
  33. package/src/search/direct-search.ts +472 -0
  34. package/src/search/embedding-provider.ts +249 -0
  35. package/src/search/index.ts +12 -0
  36. package/src/search/semantic-search.ts +435 -0
  37. package/src/utils/artifact-transaction.ts +1 -0
  38. package/src/utils/atomic-write.ts +1 -0
  39. package/src/utils/errors.ts +89 -4
  40. package/src/utils/fs.ts +104 -50
  41. package/src/utils/json.ts +1 -0
  42. package/src/utils/language-registry.ts +84 -6
  43. package/src/utils/path.ts +26 -0
  44. package/tests/dead-code.test.ts +3 -2
  45. package/tests/direct-search.test.ts +435 -0
  46. package/tests/error-recovery.test.ts +143 -0
  47. package/tests/fixtures/simple-api/src/index.ts +1 -1
  48. package/tests/go-parser.test.ts +19 -335
  49. package/tests/js-parser.test.ts +18 -1089
  50. package/tests/language-registry-all.test.ts +276 -0
  51. package/tests/language-registry.test.ts +6 -4
  52. package/tests/parse-diagnostics.test.ts +9 -96
  53. package/tests/parser.test.ts +42 -771
  54. package/tests/polyglot-parser.test.ts +117 -0
  55. package/tests/rich-function-index.test.ts +703 -0
  56. package/tests/tree-sitter-parser.test.ts +108 -80
  57. package/tests/ts-parser.test.ts +8 -8
  58. package/tests/verification.test.ts +175 -0
  59. package/src/parser/base-parser.ts +0 -16
  60. package/src/parser/go/go-parser.ts +0 -43
  61. package/src/parser/javascript/js-extractor.ts +0 -278
  62. package/src/parser/javascript/js-parser.ts +0 -101
  63. package/src/parser/typescript/ts-extractor.ts +0 -447
  64. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -1,1105 +1,34 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { JavaScriptExtractor } from '../src/parser/javascript/js-extractor'
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
- import { formatDate } from './utils/dates.js'
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
- /** Mixed ESM + CJS (unusual, but Babel-transpiled code can look like this) */
173
- const MIXED_ESM_CJS = `
174
- import defaultExport from './base.js'
175
-
176
- const extra = require('./extra')
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
- describe('CommonJS module.exports = { } exports', () => {
234
- const ext = new JavaScriptExtractor('src/auth.js', CJS_MODULE)
235
-
236
- test('module.exports = { foo, bar } marks names as exports', () => {
237
- const exports = ext.extractExports()
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('parse returns language: javascript', async () => {
502
- const result = await parser.parse('src/index.js', CJS_MODULE)
503
- expect(result.language).toBe('javascript')
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
  })