@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/parser.test.ts
CHANGED
|
@@ -1,790 +1,61 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
`)
|
|
18
|
-
const fns = extractor.extractFunctions()
|
|
19
|
-
expect(fns).toHaveLength(1)
|
|
20
|
-
expect(fns[0].name).toBe('verifyToken')
|
|
21
|
-
expect(fns[0].isExported).toBe(true)
|
|
22
|
-
expect(fns[0].params[0].name).toBe('token')
|
|
23
|
-
expect(fns[0].params[0].type).toBe('string')
|
|
24
|
-
expect(fns[0].returnType).toBe('boolean')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('extracts arrow functions assigned to const', () => {
|
|
28
|
-
const extractor = new TypeScriptExtractor('src/utils.ts', `
|
|
29
|
-
export const greet = (name: string): string => {
|
|
30
|
-
return 'Hello ' + name
|
|
31
|
-
}
|
|
32
|
-
`)
|
|
33
|
-
const fns = extractor.extractFunctions()
|
|
34
|
-
expect(fns).toHaveLength(1)
|
|
35
|
-
expect(fns[0].name).toBe('greet')
|
|
36
|
-
expect(fns[0].isExported).toBe(true)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('extracts async functions', () => {
|
|
40
|
-
const extractor = new TypeScriptExtractor('src/db.ts', `
|
|
41
|
-
export async function findUser(id: string): Promise<User> {
|
|
42
|
-
return await db.find(id)
|
|
43
|
-
}
|
|
44
|
-
`)
|
|
45
|
-
const fns = extractor.extractFunctions()
|
|
46
|
-
expect(fns[0].isAsync).toBe(true)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('extracts generator functions', () => {
|
|
50
|
-
const extractor = new TypeScriptExtractor('src/gen.ts', `
|
|
51
|
-
function* numberGenerator(): Generator<number> {
|
|
52
|
-
yield 1
|
|
53
|
-
yield 2
|
|
54
|
-
yield 3
|
|
55
|
-
}
|
|
56
|
-
`)
|
|
57
|
-
const fns = extractor.extractFunctions()
|
|
58
|
-
expect(fns).toHaveLength(1)
|
|
59
|
-
expect(fns[0].name).toBe('numberGenerator')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('extracts function overloads', () => {
|
|
63
|
-
const extractor = new TypeScriptExtractor('src/overload.ts', `
|
|
64
|
-
function parse(value: string): number
|
|
65
|
-
function parse(value: string, base: number): number
|
|
66
|
-
function parse(value: string, base?: number): number {
|
|
67
|
-
return parseInt(value, base || 10)
|
|
68
|
-
}
|
|
69
|
-
`)
|
|
70
|
-
const fns = extractor.extractFunctions()
|
|
71
|
-
expect(fns.length).toBeGreaterThanOrEqual(1)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('extracts call expressions from function bodies', () => {
|
|
75
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
76
|
-
import { jwtDecode } from './jwt'
|
|
77
|
-
function verifyToken(token: string) {
|
|
78
|
-
const decoded = jwtDecode(token)
|
|
79
|
-
console.log(decoded)
|
|
80
|
-
return decoded.exp > Date.now()
|
|
81
|
-
}
|
|
82
|
-
`)
|
|
83
|
-
const fns = extractor.extractFunctions()
|
|
84
|
-
expect(fns[0].calls.some(c => c.name === 'jwtDecode')).toBe(true)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('extracts nested function calls', () => {
|
|
88
|
-
const extractor = new TypeScriptExtractor('src/nested.ts', `
|
|
89
|
-
function outer() {
|
|
90
|
-
return inner().value
|
|
91
|
-
}
|
|
92
|
-
function inner() {
|
|
93
|
-
return { value: 42 }
|
|
94
|
-
}
|
|
95
|
-
`)
|
|
96
|
-
const fns = extractor.extractFunctions()
|
|
97
|
-
const outer = fns.find(f => f.name === 'outer')
|
|
98
|
-
expect(outer?.calls.some(c => c.name === 'inner')).toBe(true)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('extracts methods on objects', () => {
|
|
102
|
-
const extractor = new TypeScriptExtractor('src/obj.ts', `
|
|
103
|
-
function getUser() { return { name: 'test' } }
|
|
104
|
-
function getAge() { return 25 }
|
|
105
|
-
`)
|
|
106
|
-
const fns = extractor.extractFunctions()
|
|
107
|
-
expect(fns.length).toBe(2)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('extracts class methods', () => {
|
|
111
|
-
const extractor = new TypeScriptExtractor('src/service.ts', `
|
|
112
|
-
export class AuthService {
|
|
113
|
-
verify(token: string): boolean {
|
|
114
|
-
return true
|
|
115
|
-
}
|
|
116
|
-
async refresh(): Promise<string> {
|
|
117
|
-
return 'new-token'
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
`)
|
|
121
|
-
const classes = extractor.extractClasses()
|
|
122
|
-
expect(classes).toHaveLength(1)
|
|
123
|
-
expect(classes[0].name).toBe('AuthService')
|
|
124
|
-
expect(classes[0].methods).toHaveLength(2)
|
|
125
|
-
expect(classes[0].methods[0].name).toBe('AuthService.verify')
|
|
126
|
-
expect(classes[0].methods[1].isAsync).toBe(true)
|
|
127
|
-
expect(classes[0].isExported).toBe(true)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('extracts static methods', () => {
|
|
131
|
-
const extractor = new TypeScriptExtractor('src/static.ts', `
|
|
132
|
-
class Config {
|
|
133
|
-
static default(): Config {
|
|
134
|
-
return new Config()
|
|
135
|
-
}
|
|
136
|
-
static load(path: string): Config {
|
|
137
|
-
return new Config()
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
`)
|
|
141
|
-
const classes = extractor.extractClasses()
|
|
142
|
-
const staticMethods = classes[0]?.methods.filter(m => m.name.includes('default') || m.name.includes('load'))
|
|
143
|
-
expect(staticMethods?.length).toBeGreaterThanOrEqual(2)
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe('Import Extraction', () => {
|
|
148
|
-
it('extracts named imports', () => {
|
|
149
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
150
|
-
import { jwtDecode, jwtSign } from '../utils/jwt'
|
|
151
|
-
`)
|
|
152
|
-
const imports = extractor.extractImports()
|
|
153
|
-
expect(imports).toHaveLength(1)
|
|
154
|
-
expect(imports[0].source).toBe('../utils/jwt')
|
|
155
|
-
expect(imports[0].names).toContain('jwtDecode')
|
|
156
|
-
expect(imports[0].names).toContain('jwtSign')
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('extracts default imports', () => {
|
|
160
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
161
|
-
import express from 'express'
|
|
162
|
-
`)
|
|
163
|
-
const imports = extractor.extractImports()
|
|
164
|
-
expect(imports).toHaveLength(1)
|
|
165
|
-
expect(imports[0].source).toBe('express')
|
|
166
|
-
expect(imports[0].isDefault).toBe(true)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('extracts namespace imports', () => {
|
|
170
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
171
|
-
import * as utils from './utils'
|
|
172
|
-
`)
|
|
173
|
-
const imports = extractor.extractImports()
|
|
174
|
-
expect(imports).toHaveLength(1)
|
|
175
|
-
expect(imports[0].source).toBe('./utils')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('extracts mixed imports', () => {
|
|
179
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
180
|
-
import express, { Router, Request, Response } from 'express'
|
|
181
|
-
`)
|
|
182
|
-
const imports = extractor.extractImports()
|
|
183
|
-
expect(imports).toHaveLength(1)
|
|
184
|
-
expect(imports[0].isDefault).toBe(true)
|
|
185
|
-
expect(imports[0].names.length).toBeGreaterThan(0)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('extracts type-only imports', () => {
|
|
189
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
190
|
-
import type { User, Profile } from './types'
|
|
191
|
-
import { verifyToken } from './verify'
|
|
192
|
-
`)
|
|
193
|
-
const imports = extractor.extractImports()
|
|
194
|
-
expect(imports).toHaveLength(1)
|
|
195
|
-
expect(imports[0].source).toBe('./verify')
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('extracts re-exported imports', () => {
|
|
199
|
-
const extractor = new TypeScriptExtractor('src/index.ts', `
|
|
200
|
-
export function verifyToken() {}
|
|
201
|
-
`)
|
|
202
|
-
const imports = extractor.extractImports()
|
|
203
|
-
expect(imports).toBeDefined()
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('handles relative path imports', () => {
|
|
207
|
-
const extractor = new TypeScriptExtractor('src/auth/login.ts', `
|
|
208
|
-
import { db } from '../db'
|
|
209
|
-
import { config } from './config'
|
|
210
|
-
`)
|
|
211
|
-
const imports = extractor.extractImports()
|
|
212
|
-
expect(imports.length).toBe(2)
|
|
213
|
-
})
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
describe('Export Extraction', () => {
|
|
217
|
-
it('extracts named exports', () => {
|
|
218
|
-
const extractor = new TypeScriptExtractor('src/auth.ts', `
|
|
219
|
-
export function verifyToken() {}
|
|
220
|
-
export const SECRET = 'abc'
|
|
221
|
-
export class AuthService {}
|
|
222
|
-
`)
|
|
223
|
-
const exports = extractor.extractExports()
|
|
224
|
-
expect(exports.length).toBeGreaterThanOrEqual(3)
|
|
225
|
-
expect(exports.find(e => e.name === 'verifyToken')?.type).toBe('function')
|
|
226
|
-
expect(exports.find(e => e.name === 'SECRET')?.type).toBe('const')
|
|
227
|
-
expect(exports.find(e => e.name === 'AuthService')?.type).toBe('class')
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('extracts default exports', () => {
|
|
231
|
-
const extractor = new TypeScriptExtractor('src/default.ts', `
|
|
232
|
-
export default function AuthService() {}
|
|
233
|
-
`)
|
|
234
|
-
const exports = extractor.extractExports()
|
|
235
|
-
expect(exports.length).toBeGreaterThanOrEqual(1)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('extracts re-exports', () => {
|
|
239
|
-
const extractor = new TypeScriptExtractor('src/index.ts', `
|
|
240
|
-
export function combined() {}
|
|
241
|
-
`)
|
|
242
|
-
const exports = extractor.extractExports()
|
|
243
|
-
expect(exports.length).toBeGreaterThanOrEqual(1)
|
|
244
|
-
})
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
describe('Parameter Handling', () => {
|
|
248
|
-
it('handles optional parameters', () => {
|
|
249
|
-
const extractor = new TypeScriptExtractor('src/utils.ts', `
|
|
250
|
-
function greet(name: string, greeting?: string) {}
|
|
251
|
-
`)
|
|
252
|
-
const fns = extractor.extractFunctions()
|
|
253
|
-
expect(fns[0].params[1].optional).toBe(true)
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('handles default parameter values', () => {
|
|
257
|
-
const extractor = new TypeScriptExtractor('src/utils.ts', `
|
|
258
|
-
function config(port: number = 3000, host: string = 'localhost') {}
|
|
259
|
-
`)
|
|
260
|
-
const fns = extractor.extractFunctions()
|
|
261
|
-
expect(fns[0].params[0].optional).toBe(true)
|
|
262
|
-
expect(fns[0].params[0].defaultValue).toBeDefined()
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('handles rest parameters', () => {
|
|
266
|
-
const extractor = new TypeScriptExtractor('src/rest.ts', `
|
|
267
|
-
function sum(...numbers: number[]): number {
|
|
268
|
-
return numbers.reduce((a, b) => a + b, 0)
|
|
269
|
-
}
|
|
270
|
-
`)
|
|
271
|
-
const fns = extractor.extractFunctions()
|
|
272
|
-
expect(fns[0].params[0].name).toBe('numbers')
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('handles destructured parameters', () => {
|
|
276
|
-
const extractor = new TypeScriptExtractor('src/dest.ts', `
|
|
277
|
-
function process({ name, age }: User): void {}
|
|
278
|
-
function processArray([first, ...rest]: number[]): void {}
|
|
279
|
-
`)
|
|
280
|
-
const fns = extractor.extractFunctions()
|
|
281
|
-
expect(fns.length).toBe(2)
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('handles typed parameters', () => {
|
|
285
|
-
const extractor = new TypeScriptExtractor('src/typed.ts', `
|
|
286
|
-
function createUser(name: string, age: number, options?: UserOptions): User {}
|
|
287
|
-
`)
|
|
288
|
-
const fns = extractor.extractFunctions()
|
|
289
|
-
expect(fns[0].params.length).toBe(3)
|
|
290
|
-
expect(fns[0].params[0].type).toBe('string')
|
|
291
|
-
expect(fns[0].params[1].type).toBe('number')
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
describe('Class Extraction', () => {
|
|
296
|
-
it('extracts class with inheritance', () => {
|
|
297
|
-
const extractor = new TypeScriptExtractor('src/class.ts', `
|
|
298
|
-
export class UserService extends BaseService implements IUserService {
|
|
299
|
-
private users: User[] = []
|
|
300
|
-
|
|
301
|
-
constructor(private db: Database) {
|
|
302
|
-
super()
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async findById(id: string): Promise<User | null> {
|
|
306
|
-
return this.users.find(u => u.id === id)
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
`)
|
|
310
|
-
const classes = extractor.extractClasses()
|
|
311
|
-
expect(classes).toHaveLength(1)
|
|
312
|
-
expect(classes[0].name).toBe('UserService')
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('extracts class getters and setters', () => {
|
|
316
|
-
const extractor = new TypeScriptExtractor('src/getset.ts', `
|
|
317
|
-
class User {
|
|
318
|
-
private _name: string = ''
|
|
319
|
-
|
|
320
|
-
get name(): string {
|
|
321
|
-
return this._name
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
set name(value: string) {
|
|
325
|
-
this._name = value
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
`)
|
|
329
|
-
const classes = extractor.extractClasses()
|
|
330
|
-
// Getters/setters may be captured as part of class
|
|
331
|
-
expect(classes.length).toBe(1)
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
it('extracts abstract classes', () => {
|
|
335
|
-
const extractor = new TypeScriptExtractor('src/abstract.ts', `
|
|
336
|
-
abstract class BaseService {
|
|
337
|
-
abstract initialize(): void
|
|
338
|
-
protected log(message: string): void {
|
|
339
|
-
console.log(message)
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
`)
|
|
343
|
-
const classes = extractor.extractClasses()
|
|
344
|
-
expect(classes).toHaveLength(1)
|
|
345
|
-
expect(classes[0].name).toBe('BaseService')
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
it('extracts interfaces', () => {
|
|
349
|
-
const extractor = new TypeScriptExtractor('src/interface.ts', `
|
|
350
|
-
interface User {
|
|
351
|
-
name: string
|
|
352
|
-
age: number
|
|
353
|
-
}
|
|
354
|
-
`)
|
|
355
|
-
const fns = extractor.extractFunctions()
|
|
356
|
-
expect(fns.length).toBe(0)
|
|
357
|
-
})
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
describe('Edge Cases', () => {
|
|
361
|
-
it('handles empty files', () => {
|
|
362
|
-
const extractor = new TypeScriptExtractor('src/empty.ts', '')
|
|
363
|
-
expect(extractor.extractFunctions()).toHaveLength(0)
|
|
364
|
-
expect(extractor.extractClasses()).toHaveLength(0)
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
it('handles comment-only files', () => {
|
|
368
|
-
const extractor = new TypeScriptExtractor('src/comments.ts', `
|
|
369
|
-
// This is a comment
|
|
370
|
-
/* Multi-line comment
|
|
371
|
-
spanning lines */
|
|
372
|
-
/// Documentation comment
|
|
373
|
-
`)
|
|
374
|
-
expect(extractor.extractFunctions()).toHaveLength(0)
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
it('handles malformed syntax gracefully', () => {
|
|
378
|
-
const extractor = new TypeScriptExtractor('src/malformed.ts', `
|
|
379
|
-
function broken(() {
|
|
380
|
-
this is not valid syntax
|
|
381
|
-
`)
|
|
382
|
-
// Should not throw
|
|
383
|
-
expect(() => extractor.extractFunctions()).not.toThrow()
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
it('handles deeply nested code', () => {
|
|
387
|
-
const extractor = new TypeScriptExtractor('src/deep.ts', `
|
|
388
|
-
function level1() {
|
|
389
|
-
function level2() {
|
|
390
|
-
function level3() {
|
|
391
|
-
return level4()
|
|
392
|
-
}
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { TypescriptExtractor } from '../src/parser/oxc-parser'
|
|
3
|
+
import { LanguageRegistry } from '../src/parser/index'
|
|
4
|
+
|
|
5
|
+
const _TreeSitterParser = { parse: () => Promise.resolve({}) }
|
|
6
|
+
const _UnsupportedLanguageError = class extends Error {}
|
|
7
|
+
|
|
8
|
+
describe('Unified Parser Integration', () => {
|
|
9
|
+
describe('TypescriptExtractor (OXC)', () => {
|
|
10
|
+
it('extracts function declarations and calls', async () => {
|
|
11
|
+
const extractor = new TypescriptExtractor()
|
|
12
|
+
const result = await extractor.extract('src/auth.ts', `
|
|
13
|
+
import { jwtDecode } from './jwt'
|
|
14
|
+
export function verifyToken(token: string) {
|
|
15
|
+
return jwtDecode(token).exp > Date.now()
|
|
393
16
|
}
|
|
394
|
-
}
|
|
395
|
-
`)
|
|
396
|
-
const fns = extractor.extractFunctions()
|
|
397
|
-
expect(fns.length).toBeGreaterThanOrEqual(1)
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
it('handles template literals', () => {
|
|
401
|
-
const extractor = new TypeScriptExtractor('src/template.ts', `
|
|
402
|
-
const message = \`Hello \${name}!\`
|
|
403
|
-
`)
|
|
404
|
-
const fns = extractor.extractFunctions()
|
|
405
|
-
expect(fns.length).toBe(0) // template literal is const, not function
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
it('handles decorators', () => {
|
|
409
|
-
const extractor = new TypeScriptExtractor('src/decorator.ts', `
|
|
410
|
-
@injectable()
|
|
411
|
-
@lazy
|
|
412
|
-
class UserService {}
|
|
413
|
-
`)
|
|
414
|
-
const classes = extractor.extractClasses()
|
|
415
|
-
expect(classes.length).toBe(1)
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
it('handles async generators', () => {
|
|
419
|
-
const extractor = new TypeScriptExtractor('src/async-gen.ts', `
|
|
420
|
-
async function* streamData(): AsyncGenerator<Data> {
|
|
421
|
-
yield await fetchData()
|
|
422
|
-
}
|
|
423
|
-
`)
|
|
424
|
-
const fns = extractor.extractFunctions()
|
|
425
|
-
expect(fns[0].isAsync).toBe(true)
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
it('handles conditional types', () => {
|
|
429
|
-
const extractor = new TypeScriptExtractor('src/cond.ts', `
|
|
430
|
-
type NonNullable<T> = T extends null | undefined ? never : T
|
|
431
|
-
`)
|
|
432
|
-
const classes = extractor.extractClasses()
|
|
433
|
-
// Should not crash
|
|
434
|
-
expect(classes).toBeDefined()
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('handles enum members', () => {
|
|
438
|
-
const extractor = new TypeScriptExtractor('src/enum.ts', `
|
|
439
|
-
enum Status {
|
|
440
|
-
Pending = 'pending',
|
|
441
|
-
Active = 'active',
|
|
442
|
-
Completed = 'completed'
|
|
443
|
-
}
|
|
444
|
-
`)
|
|
445
|
-
const classes = extractor.extractClasses()
|
|
446
|
-
// Enum may or may not be captured depending on parser
|
|
447
|
-
expect(classes).toBeDefined()
|
|
448
|
-
})
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
describe('Type Extraction', () => {
|
|
452
|
-
it('extracts return types', () => {
|
|
453
|
-
const extractor = new TypeScriptExtractor('src/return.ts', `
|
|
454
|
-
function getUser(): User {
|
|
455
|
-
return {} as User
|
|
456
|
-
}
|
|
457
|
-
`)
|
|
458
|
-
const fns = extractor.extractFunctions()
|
|
459
|
-
expect(fns[0].returnType).toBe('User')
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
it('extracts generic functions', () => {
|
|
463
|
-
const extractor = new TypeScriptExtractor('src/generic.ts', `
|
|
464
|
-
function identity<T>(value: T): T {
|
|
465
|
-
return value
|
|
466
|
-
}
|
|
467
|
-
`)
|
|
468
|
-
const fns = extractor.extractFunctions()
|
|
469
|
-
expect(fns[0].name).toBe('identity')
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
it('extracts union return types', () => {
|
|
473
|
-
const extractor = new TypeScriptExtractor('src/union.ts', `
|
|
474
|
-
function parse(value: string): string | number {
|
|
475
|
-
return parseInt(value)
|
|
476
|
-
}
|
|
477
17
|
`)
|
|
478
|
-
const fns = extractor.extractFunctions()
|
|
479
|
-
expect(fns[0].returnType).toContain('string')
|
|
480
|
-
})
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
describe('Purpose & Documentation', () => {
|
|
484
|
-
it('extracts function purpose from comments', () => {
|
|
485
|
-
const extractor = new TypeScriptExtractor('src/docs.ts', `
|
|
486
|
-
/**
|
|
487
|
-
* Verifies a JWT token and returns the payload
|
|
488
|
-
* @param token - The JWT token to verify
|
|
489
|
-
* @returns The decoded token payload
|
|
490
|
-
*/
|
|
491
|
-
export function verifyToken(token: string): Payload {
|
|
492
|
-
return jwtDecode(token)
|
|
493
|
-
}
|
|
494
|
-
`)
|
|
495
|
-
const fns = extractor.extractFunctions()
|
|
496
|
-
expect(fns[0].purpose).toBeDefined()
|
|
497
|
-
expect(fns[0].purpose).toContain('Verifies')
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
it('extracts single-line doc comments', () => {
|
|
501
|
-
const extractor = new TypeScriptExtractor('src/single.ts', `
|
|
502
|
-
/// Gets the current user
|
|
503
|
-
function getCurrentUser(): User | null {
|
|
504
|
-
return null
|
|
505
|
-
}
|
|
506
|
-
`)
|
|
507
|
-
const fns = extractor.extractFunctions()
|
|
508
|
-
expect(fns[0].purpose).toBeDefined()
|
|
509
|
-
})
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
describe('TypeScriptParser - Comprehensive', () => {
|
|
514
|
-
const parser = new TypeScriptParser()
|
|
515
|
-
|
|
516
|
-
describe('Basic Parsing', () => {
|
|
517
|
-
it('returns correct language', async () => {
|
|
518
|
-
const result = await parser.parse('src/test.ts', 'const x = 1')
|
|
519
|
-
expect(result.language).toBe('typescript')
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
it('parses a complete file', async () => {
|
|
523
|
-
const result = await parser.parse('src/auth.ts', `
|
|
524
|
-
import { jwtDecode } from '../utils/jwt'
|
|
525
|
-
export function verifyToken(token: string): boolean {
|
|
526
|
-
return jwtDecode(token).exp > Date.now()
|
|
527
|
-
}
|
|
528
|
-
`)
|
|
529
18
|
expect(result.functions).toHaveLength(1)
|
|
19
|
+
expect(result.functions[0].name).toBe('verifyToken')
|
|
530
20
|
expect(result.functions[0].calls.some(c => c.name === 'jwtDecode')).toBe(true)
|
|
531
|
-
expect(result.imports).toHaveLength(1)
|
|
532
|
-
expect(result.exports).toHaveLength(1)
|
|
533
|
-
expect(result.hash).toBeDefined()
|
|
534
|
-
expect(result.path).toBe('src/auth.ts')
|
|
535
21
|
})
|
|
536
22
|
|
|
537
|
-
it('
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
expect(result.
|
|
23
|
+
it('extracts class methods', async () => {
|
|
24
|
+
const extractor = new TypescriptExtractor()
|
|
25
|
+
const result = await extractor.extract('src/service.ts', `
|
|
26
|
+
export class AuthService {
|
|
27
|
+
verify(token: string): boolean { return true }
|
|
28
|
+
}
|
|
29
|
+
`)
|
|
30
|
+
expect(result.classes).toHaveLength(1)
|
|
31
|
+
expect(result.classes[0].name).toBe('AuthService')
|
|
32
|
+
expect(result.classes[0].methods[0].name).toBe('AuthService.verify')
|
|
546
33
|
})
|
|
547
34
|
})
|
|
548
35
|
|
|
549
|
-
describe('
|
|
550
|
-
it('
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
36
|
+
describe('LanguageRegistry Dispatch', () => {
|
|
37
|
+
it('dispatches to OXC for TypeScript/JavaScript', () => {
|
|
38
|
+
const registry = LanguageRegistry.getInstance()
|
|
39
|
+
const ts = registry.getForFile('test.ts')
|
|
40
|
+
const js = registry.getForFile('test.js')
|
|
41
|
+
expect(ts?.name).toBe('typescript')
|
|
42
|
+
expect(js?.name).toBe('javascript')
|
|
43
|
+
expect(ts?.extractor).toBeInstanceOf(TypescriptExtractor)
|
|
554
44
|
})
|
|
555
45
|
|
|
556
|
-
it('
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
expect(result.parsedAt).toBeLessThanOrEqual(after)
|
|
46
|
+
it('dispatches to TreeSitter for other languages', () => {
|
|
47
|
+
const registry = LanguageRegistry.getInstance()
|
|
48
|
+
const py = registry.getForFile('test.py')
|
|
49
|
+
expect(py?.name).toBe('python')
|
|
50
|
+
// TreeSitterParser is registered for python
|
|
562
51
|
})
|
|
563
52
|
})
|
|
564
53
|
|
|
565
54
|
describe('Error Handling', () => {
|
|
566
55
|
it('handles completely empty files', async () => {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
it('handles syntax errors gracefully', async () => {
|
|
573
|
-
const result = await parser.parse('src/error.ts', 'function broken(() {')
|
|
574
|
-
// Should not throw, return partial results
|
|
575
|
-
expect(result.path).toBe('src/error.ts')
|
|
576
|
-
})
|
|
577
|
-
})
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
describe('TypeScriptResolver - Comprehensive', () => {
|
|
581
|
-
describe('Import Resolution', () => {
|
|
582
|
-
it('resolves relative imports', () => {
|
|
583
|
-
const resolver = new TypeScriptResolver('/project')
|
|
584
|
-
const result = resolver.resolve(
|
|
585
|
-
{ source: '../utils/jwt', names: ['jwtDecode'], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
586
|
-
'src/auth/verify.ts',
|
|
587
|
-
['src/utils/jwt.ts']
|
|
588
|
-
)
|
|
589
|
-
expect(result.resolvedPath).toBe('src/utils/jwt.ts')
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
it('resolves index files', () => {
|
|
593
|
-
const resolver = new TypeScriptResolver('/project')
|
|
594
|
-
const result = resolver.resolve(
|
|
595
|
-
{ source: '../utils', names: ['helper'], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
596
|
-
'src/auth/verify.ts',
|
|
597
|
-
['src/utils/index.ts']
|
|
598
|
-
)
|
|
599
|
-
expect(result.resolvedPath).toBe('src/utils/index.ts')
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
it('resolves with file extension variations', () => {
|
|
603
|
-
const resolver = new TypeScriptResolver('/project')
|
|
604
|
-
const result = resolver.resolve(
|
|
605
|
-
{ source: '../utils/jwt', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
606
|
-
'src/auth/verify.ts',
|
|
607
|
-
['src/utils/jwt.ts', 'src/utils/jwt.tsx']
|
|
608
|
-
)
|
|
609
|
-
expect(result.resolvedPath).toBeDefined()
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
it('skips external packages', () => {
|
|
613
|
-
const resolver = new TypeScriptResolver('/project')
|
|
614
|
-
const result = resolver.resolve(
|
|
615
|
-
{ source: 'express', names: ['default'], resolvedPath: '', isDefault: true, isDynamic: false },
|
|
616
|
-
'src/index.ts'
|
|
617
|
-
)
|
|
618
|
-
expect(result.resolvedPath).toBe('')
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
it('resolves path aliases', () => {
|
|
622
|
-
const resolver = new TypeScriptResolver('/project', {
|
|
623
|
-
'@/*': ['src/*'],
|
|
624
|
-
})
|
|
625
|
-
const result = resolver.resolve(
|
|
626
|
-
{ source: '@/utils/jwt', names: ['jwtDecode'], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
627
|
-
'src/auth/verify.ts',
|
|
628
|
-
['src/utils/jwt.ts']
|
|
629
|
-
)
|
|
630
|
-
expect(result.resolvedPath).toBe('src/utils/jwt.ts')
|
|
56
|
+
const extractor = new TypescriptExtractor()
|
|
57
|
+
const result = await extractor.extract('src/empty.ts', '')
|
|
58
|
+
expect(result.functions).toHaveLength(0)
|
|
631
59
|
})
|
|
632
|
-
|
|
633
|
-
it('handles multiple aliases', () => {
|
|
634
|
-
const resolver = new TypeScriptResolver('/project', {
|
|
635
|
-
'@/*': ['src/*'],
|
|
636
|
-
'#/*': ['types/*'],
|
|
637
|
-
})
|
|
638
|
-
const result = resolver.resolve(
|
|
639
|
-
{ source: '@/utils', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
640
|
-
'src/auth/verify.ts',
|
|
641
|
-
['src/utils/index.ts']
|
|
642
|
-
)
|
|
643
|
-
expect(result.resolvedPath).toBe('src/utils/index.ts')
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
it('resolves nested path aliases', () => {
|
|
647
|
-
const resolver = new TypeScriptResolver('/project', {
|
|
648
|
-
'@components/*': ['src/components/*'],
|
|
649
|
-
})
|
|
650
|
-
const result = resolver.resolve(
|
|
651
|
-
{ source: '@/utils', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
652
|
-
'src/App.tsx',
|
|
653
|
-
['src/utils/index.ts']
|
|
654
|
-
)
|
|
655
|
-
expect(result.resolvedPath).toBeDefined()
|
|
656
|
-
})
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
describe('Edge Cases', () => {
|
|
660
|
-
it('handles empty files list', () => {
|
|
661
|
-
const resolver = new TypeScriptResolver('/project')
|
|
662
|
-
const result = resolver.resolve(
|
|
663
|
-
{ source: './utils', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
664
|
-
'src/index.ts',
|
|
665
|
-
[]
|
|
666
|
-
)
|
|
667
|
-
expect(result.resolvedPath).toContain('.ts')
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
it('handles non-matching imports', () => {
|
|
671
|
-
const resolver = new TypeScriptResolver('/project')
|
|
672
|
-
const result = resolver.resolve(
|
|
673
|
-
{ source: '../nonexistent', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
674
|
-
'src/index.ts',
|
|
675
|
-
['src/utils.ts']
|
|
676
|
-
)
|
|
677
|
-
// Resolver may return fallback path
|
|
678
|
-
expect(result.resolvedPath).toBeDefined()
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
it('handles resolveAll', () => {
|
|
682
|
-
const resolver = new TypeScriptResolver('/project')
|
|
683
|
-
const imports = [
|
|
684
|
-
{ source: './a', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
685
|
-
{ source: './b', names: [], resolvedPath: '', isDefault: false, isDynamic: false },
|
|
686
|
-
]
|
|
687
|
-
const result = resolver.resolveAll(imports, 'src/index.ts', ['src/a.ts', 'src/b.ts'])
|
|
688
|
-
expect(result.length).toBe(2)
|
|
689
|
-
expect(result[0].resolvedPath).toBe('src/a.ts')
|
|
690
|
-
})
|
|
691
|
-
})
|
|
692
|
-
})
|
|
693
|
-
|
|
694
|
-
describe('getParser - Comprehensive', () => {
|
|
695
|
-
it('returns OxcParser for .ts files', () => {
|
|
696
|
-
const parser = getParser('src/auth.ts')
|
|
697
|
-
expect(parser).toBeInstanceOf(OxcParser)
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
it('returns OxcParser for .tsx files', () => {
|
|
701
|
-
const parser = getParser('src/App.tsx')
|
|
702
|
-
expect(parser).toBeInstanceOf(OxcParser)
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
it('returns OxcParser for .js files', () => {
|
|
706
|
-
const parser = getParser('src/app.js')
|
|
707
|
-
expect(parser).toBeInstanceOf(OxcParser)
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
it('returns OxcParser for .jsx files', () => {
|
|
711
|
-
const parser = getParser('src/App.jsx')
|
|
712
|
-
expect(parser).toBeInstanceOf(OxcParser)
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
it('throws for unsupported extensions', () => {
|
|
716
|
-
expect(() => getParser('src/auth.xyz')).toThrow(UnsupportedLanguageError)
|
|
717
|
-
})
|
|
718
|
-
|
|
719
|
-
it('throws for no extension', () => {
|
|
720
|
-
expect(() => getParser('src/Makefile')).toThrow(UnsupportedLanguageError)
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
it('handles files with dots in name', () => {
|
|
724
|
-
const parser = getParser('src/app.config.ts')
|
|
725
|
-
expect(parser).toBeInstanceOf(OxcParser)
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
it('handles path with directories', () => {
|
|
729
|
-
const parser = getParser('src/lib/utils/helper.ts')
|
|
730
|
-
expect(parser).toBeInstanceOf(OxcParser)
|
|
731
|
-
})
|
|
732
|
-
|
|
733
|
-
it('supports C++ variant extensions via tree-sitter parser', () => {
|
|
734
|
-
const cxxParser = getParser('src/engine.cxx')
|
|
735
|
-
const hxxParser = getParser('src/engine.hxx')
|
|
736
|
-
const hhParser = getParser('src/engine.hh')
|
|
737
|
-
|
|
738
|
-
expect(cxxParser.getSupportedExtensions()).toContain('.cxx')
|
|
739
|
-
expect(hxxParser.getSupportedExtensions()).toContain('.hxx')
|
|
740
|
-
expect(hhParser.getSupportedExtensions()).toContain('.hh')
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
it('supports Rust/C#/Swift extensions via tree-sitter parser', () => {
|
|
744
|
-
const rustParser = getParser('src/lib.rs')
|
|
745
|
-
const csharpParser = getParser('src/Program.cs')
|
|
746
|
-
const swiftParser = getParser('Sources/App/main.swift')
|
|
747
|
-
|
|
748
|
-
expect(rustParser.getSupportedExtensions()).toContain('.rs')
|
|
749
|
-
expect(csharpParser.getSupportedExtensions()).toContain('.cs')
|
|
750
|
-
expect(swiftParser.getSupportedExtensions()).toContain('.swift')
|
|
751
|
-
})
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
describe('OxcParser - Direct', () => {
|
|
755
|
-
const parser = new OxcParser()
|
|
756
|
-
|
|
757
|
-
it('parses TypeScript file', async () => {
|
|
758
|
-
const result = await parser.parse('complex.ts', `
|
|
759
|
-
const x = 1
|
|
760
|
-
export function test() { return x }
|
|
761
|
-
`)
|
|
762
|
-
|
|
763
|
-
expect(result.functions.length).toBeGreaterThan(0)
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
it('handles large files', async () => {
|
|
767
|
-
const content = Array.from({ length: 1000 }, (_, i) =>
|
|
768
|
-
`export function function_${i}() { return ${i} }`
|
|
769
|
-
).join('\n')
|
|
770
|
-
|
|
771
|
-
const result = await parser.parse('large.ts', content)
|
|
772
|
-
expect(result.functions.length).toBe(1000)
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
it('extracts direct and method call expressions', async () => {
|
|
776
|
-
const result = await parser.parse('calls.ts', `
|
|
777
|
-
function b() { return 1 }
|
|
778
|
-
const svc = { run() { return 2 } }
|
|
779
|
-
export function a() {
|
|
780
|
-
b()
|
|
781
|
-
svc.run()
|
|
782
|
-
}
|
|
783
|
-
`)
|
|
784
|
-
|
|
785
|
-
const a = result.functions.find(f => f.name === 'a')
|
|
786
|
-
expect(a).toBeDefined()
|
|
787
|
-
expect(a!.calls.some(c => c.name === 'b')).toBe(true)
|
|
788
|
-
expect(a!.calls.some(c => c.name === 'svc.run')).toBe(true)
|
|
789
60
|
})
|
|
790
61
|
})
|