@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
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
import type { DependencyGraph } from './types.js'
|
|
2
|
+
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
3
|
+
|
|
4
|
+
export interface RichFunction {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
file: string
|
|
8
|
+
moduleId: string
|
|
9
|
+
startLine: number
|
|
10
|
+
endLine: number
|
|
11
|
+
params: RichParam[]
|
|
12
|
+
returnType: string
|
|
13
|
+
isExported: boolean
|
|
14
|
+
isAsync: boolean
|
|
15
|
+
isGenerator: boolean
|
|
16
|
+
typeParameters: string[]
|
|
17
|
+
body: string
|
|
18
|
+
purpose: string
|
|
19
|
+
docComment: string
|
|
20
|
+
decorators: string[]
|
|
21
|
+
calls: RichCall[]
|
|
22
|
+
calledBy: string[]
|
|
23
|
+
edgeCasesHandled: string[]
|
|
24
|
+
errorHandling: RichErrorHandling[]
|
|
25
|
+
complexity: number
|
|
26
|
+
cyclomaticComplexity: number
|
|
27
|
+
cognitiveComplexity: number
|
|
28
|
+
dependencies: string[]
|
|
29
|
+
affectedBy: string[]
|
|
30
|
+
keywords: string[]
|
|
31
|
+
signature: string
|
|
32
|
+
fullSignature: string
|
|
33
|
+
contentHash?: string
|
|
34
|
+
signatureHash?: string
|
|
35
|
+
paramHashes?: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RichParam {
|
|
39
|
+
name: string
|
|
40
|
+
type: string
|
|
41
|
+
optional: boolean
|
|
42
|
+
defaultValue?: string
|
|
43
|
+
destructured?: boolean
|
|
44
|
+
rest?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RichCall {
|
|
48
|
+
name: string
|
|
49
|
+
line: number
|
|
50
|
+
column: number
|
|
51
|
+
type: 'function' | 'method' | 'property' | 'new' | 'await' | 'yield'
|
|
52
|
+
targetId?: string
|
|
53
|
+
arguments: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RichErrorHandling {
|
|
57
|
+
line: number
|
|
58
|
+
endLine: number
|
|
59
|
+
type: 'try-catch' | 'throw' | 'return-error' | 'if-error'
|
|
60
|
+
detail: string
|
|
61
|
+
caughtTypes: string[]
|
|
62
|
+
handled: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SearchQuery {
|
|
66
|
+
text?: string
|
|
67
|
+
name?: string
|
|
68
|
+
file?: string
|
|
69
|
+
moduleId?: string
|
|
70
|
+
exactName?: string
|
|
71
|
+
nameContains?: string
|
|
72
|
+
nameStartsWith?: string
|
|
73
|
+
namePattern?: RegExp
|
|
74
|
+
returnType?: string
|
|
75
|
+
returnTypeContains?: string
|
|
76
|
+
paramTypes?: string[]
|
|
77
|
+
hasParam?: string
|
|
78
|
+
hasDecorator?: string
|
|
79
|
+
isExported?: boolean
|
|
80
|
+
isAsync?: boolean
|
|
81
|
+
isGenerator?: boolean
|
|
82
|
+
minParams?: number
|
|
83
|
+
maxParams?: number
|
|
84
|
+
keyword?: string
|
|
85
|
+
calls?: string
|
|
86
|
+
calledBy?: string
|
|
87
|
+
inFile?: string
|
|
88
|
+
inModule?: string
|
|
89
|
+
exportedFrom?: string
|
|
90
|
+
limit?: number
|
|
91
|
+
offset?: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SearchResult {
|
|
95
|
+
function: RichFunction
|
|
96
|
+
score: number
|
|
97
|
+
matchReasons: string[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ContextRequest {
|
|
101
|
+
functionId: string
|
|
102
|
+
include?: 'signature' | 'full' | 'body' | 'calls' | 'calledBy' | 'all'
|
|
103
|
+
maxBodyLines?: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface FunctionContext {
|
|
107
|
+
signature: string
|
|
108
|
+
fullSignature: string
|
|
109
|
+
body?: string
|
|
110
|
+
purpose?: string
|
|
111
|
+
docComment?: string
|
|
112
|
+
params: RichParam[]
|
|
113
|
+
returnType: string
|
|
114
|
+
calls: RichCall[]
|
|
115
|
+
calledBy: string[]
|
|
116
|
+
decorators: string[]
|
|
117
|
+
file: string
|
|
118
|
+
startLine: number
|
|
119
|
+
endLine: number
|
|
120
|
+
errorHandling?: RichErrorHandling[]
|
|
121
|
+
edgeCases?: string[]
|
|
122
|
+
keywords: string[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class RichFunctionIndex {
|
|
126
|
+
private functions: Map<string, RichFunction> = new Map()
|
|
127
|
+
private byName: Map<string, string[]> = new Map()
|
|
128
|
+
private byFile: Map<string, string[]> = new Map()
|
|
129
|
+
private byModule: Map<string, string[]> = new Map()
|
|
130
|
+
private byExport: Map<boolean, string[]> = new Map()
|
|
131
|
+
private byReturnType: Map<string, string[]> = new Map()
|
|
132
|
+
private byParamType: Map<string, Set<string>> = new Map()
|
|
133
|
+
private byDecorator: Map<string, string[]> = new Map()
|
|
134
|
+
private byKeyword: Map<string, Set<string>> = new Map()
|
|
135
|
+
private byCall: Map<string, Set<string>> = new Map()
|
|
136
|
+
private byCalledBy: Map<string, Set<string>> = new Map()
|
|
137
|
+
private nameIndex: Map<string, string> = new Map()
|
|
138
|
+
private textIndex: Map<string, Set<string>> = new Map()
|
|
139
|
+
private allKeywords: Set<string> = new Set()
|
|
140
|
+
|
|
141
|
+
private bySignatureHash: Map<string, string> = new Map()
|
|
142
|
+
private byContentHash: Map<string, string> = new Map()
|
|
143
|
+
private byParamHash: Map<string, string[]> = new Map()
|
|
144
|
+
|
|
145
|
+
constructor() {}
|
|
146
|
+
|
|
147
|
+
index(lock: MikkLock, graph?: DependencyGraph): void {
|
|
148
|
+
this.clear()
|
|
149
|
+
|
|
150
|
+
for (const [id, fn] of Object.entries(lock.functions)) {
|
|
151
|
+
const rich = this.enrichFunction(fn, lock, graph)
|
|
152
|
+
this.addFunction(rich)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private simpleHash(str: string): string {
|
|
157
|
+
let hash = 0
|
|
158
|
+
for (let i = 0; i < str.length; i++) {
|
|
159
|
+
const char = str.charCodeAt(i)
|
|
160
|
+
hash = ((hash << 5) - hash) + char
|
|
161
|
+
hash = hash & hash
|
|
162
|
+
}
|
|
163
|
+
return Math.abs(hash).toString(36)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private enrichFunction(fn: MikkLockFunction, lock: MikkLock, graph?: DependencyGraph): RichFunction {
|
|
167
|
+
const id = fn.id
|
|
168
|
+
const name = fn.name || this.parseNameFromId(id)
|
|
169
|
+
const file = fn.file || this.parseFileFromId(id)
|
|
170
|
+
|
|
171
|
+
const calls = this.extractCalls(fn, lock)
|
|
172
|
+
const calledBy = this.extractCalledBy(fn, lock)
|
|
173
|
+
const keywords = this.extractKeywords(fn, calls)
|
|
174
|
+
const signature = this.buildSignature(fn)
|
|
175
|
+
const fullSignature = this.buildFullSignature(fn)
|
|
176
|
+
|
|
177
|
+
const richParams: RichParam[] = (fn.params || []).map(p => ({
|
|
178
|
+
name: p.name,
|
|
179
|
+
type: p.type,
|
|
180
|
+
optional: p.optional ?? false,
|
|
181
|
+
defaultValue: undefined,
|
|
182
|
+
destructured: false,
|
|
183
|
+
rest: false,
|
|
184
|
+
}))
|
|
185
|
+
|
|
186
|
+
const richErrors: RichErrorHandling[] = (fn.errorHandling || []).map(e => ({
|
|
187
|
+
line: e.line,
|
|
188
|
+
endLine: e.line,
|
|
189
|
+
type: e.type,
|
|
190
|
+
detail: e.detail,
|
|
191
|
+
caughtTypes: [],
|
|
192
|
+
handled: true,
|
|
193
|
+
}))
|
|
194
|
+
|
|
195
|
+
const signatureHash = this.simpleHash(fullSignature)
|
|
196
|
+
const contentHash = fn.hash || this.simpleHash(`${file}:${name}:${signatureHash}`)
|
|
197
|
+
const paramHashes = richParams.map(p => this.simpleHash(`${p.name}:${p.type}`))
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
name,
|
|
202
|
+
file,
|
|
203
|
+
moduleId: fn.moduleId || 'unknown',
|
|
204
|
+
startLine: fn.startLine || 0,
|
|
205
|
+
endLine: fn.endLine || 0,
|
|
206
|
+
params: richParams,
|
|
207
|
+
returnType: fn.returnType || 'void',
|
|
208
|
+
isExported: fn.isExported || false,
|
|
209
|
+
isAsync: fn.isAsync || false,
|
|
210
|
+
isGenerator: false,
|
|
211
|
+
typeParameters: [],
|
|
212
|
+
body: '',
|
|
213
|
+
purpose: fn.purpose || this.inferPurpose(name, fn),
|
|
214
|
+
docComment: '',
|
|
215
|
+
decorators: [],
|
|
216
|
+
calls,
|
|
217
|
+
calledBy,
|
|
218
|
+
edgeCasesHandled: fn.edgeCasesHandled || [],
|
|
219
|
+
errorHandling: richErrors,
|
|
220
|
+
complexity: this.calculateComplexity(fn),
|
|
221
|
+
cyclomaticComplexity: this.calculateCyclomaticComplexity(fn),
|
|
222
|
+
cognitiveComplexity: 0,
|
|
223
|
+
dependencies: calls.map(c => c.name),
|
|
224
|
+
affectedBy: calledBy,
|
|
225
|
+
keywords,
|
|
226
|
+
signature,
|
|
227
|
+
fullSignature,
|
|
228
|
+
contentHash,
|
|
229
|
+
signatureHash,
|
|
230
|
+
paramHashes,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private extractCalls(fn: MikkLockFunction, lock: MikkLock): RichCall[] {
|
|
235
|
+
const calls: RichCall[] = []
|
|
236
|
+
|
|
237
|
+
for (const calleeId of fn.calls || []) {
|
|
238
|
+
const callee = lock.functions[calleeId]
|
|
239
|
+
if (callee) {
|
|
240
|
+
calls.push({
|
|
241
|
+
name: callee.name || this.parseNameFromId(calleeId),
|
|
242
|
+
line: 0,
|
|
243
|
+
column: 0,
|
|
244
|
+
type: 'function',
|
|
245
|
+
targetId: calleeId,
|
|
246
|
+
arguments: [],
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return calls
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private extractCalledBy(fn: MikkLockFunction, lock: MikkLock): string[] {
|
|
255
|
+
const calledBy: string[] = []
|
|
256
|
+
|
|
257
|
+
for (const callerId of fn.calledBy || []) {
|
|
258
|
+
const caller = lock.functions[callerId]
|
|
259
|
+
if (caller) {
|
|
260
|
+
calledBy.push(callerId)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return calledBy
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private extractKeywords(fn: MikkLockFunction, calls: RichCall[]): string[] {
|
|
268
|
+
const keywords = new Set<string>()
|
|
269
|
+
|
|
270
|
+
const name = fn.name || ''
|
|
271
|
+
const words = name.match(/[A-Z][a-z]+|[a-z]+/g) || []
|
|
272
|
+
words.forEach(w => keywords.add(w.toLowerCase()))
|
|
273
|
+
|
|
274
|
+
if (fn.purpose) {
|
|
275
|
+
const purposeWords = fn.purpose.match(/[a-z]{3,}/g) || []
|
|
276
|
+
purposeWords.forEach(w => keywords.add(w.toLowerCase()))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const returnType = fn.returnType || ''
|
|
280
|
+
if (returnType.includes('Promise')) keywords.add('async')
|
|
281
|
+
if (returnType.includes('Error') || returnType.includes('Result')) keywords.add('error-handling')
|
|
282
|
+
if (returnType !== 'void' && returnType !== 'never') keywords.add('returns-value')
|
|
283
|
+
|
|
284
|
+
if (fn.isAsync) keywords.add('async')
|
|
285
|
+
|
|
286
|
+
for (const call of calls) {
|
|
287
|
+
keywords.add(call.name.toLowerCase())
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [...keywords]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private buildSignature(fn: MikkLockFunction): string {
|
|
294
|
+
const params = (fn.params || [])
|
|
295
|
+
.map(p => `${p.name}${p.optional ? '?' : ''}: ${p.type}`)
|
|
296
|
+
.join(', ')
|
|
297
|
+
return `${fn.name || 'anonymous'}(${params})`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private buildFullSignature(fn: MikkLockFunction): string {
|
|
301
|
+
const asyncPrefix = fn.isAsync ? 'async ' : ''
|
|
302
|
+
const params = (fn.params || [])
|
|
303
|
+
.map(p => {
|
|
304
|
+
let str = p.name
|
|
305
|
+
if (p.optional) str += '?'
|
|
306
|
+
str += `: ${p.type}`
|
|
307
|
+
return str
|
|
308
|
+
})
|
|
309
|
+
.join(', ')
|
|
310
|
+
const returnType = fn.returnType || 'void'
|
|
311
|
+
return `${asyncPrefix}${fn.name || 'anonymous'}(${params}): ${returnType}`
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private parseNameFromId(id: string): string {
|
|
315
|
+
const parts = id.split(':')
|
|
316
|
+
return parts[parts.length - 1] || 'anonymous'
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private parseFileFromId(id: string): string {
|
|
320
|
+
const withoutPrefix = id.replace(/^(fn|class|type|intf|enum):/, '')
|
|
321
|
+
const parts = withoutPrefix.split(':')
|
|
322
|
+
return parts.slice(0, -1).join(':')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private inferPurpose(name: string, fn: MikkLockFunction): string {
|
|
326
|
+
const lower = name.toLowerCase()
|
|
327
|
+
|
|
328
|
+
if (lower.startsWith('get') || lower.startsWith('fetch') || lower.startsWith('load')) {
|
|
329
|
+
return `Retrieves data`
|
|
330
|
+
}
|
|
331
|
+
if (lower.startsWith('set') || lower.startsWith('update') || lower.startsWith('save')) {
|
|
332
|
+
return `Modifies or persists data`
|
|
333
|
+
}
|
|
334
|
+
if (lower.startsWith('create') || lower.startsWith('add') || lower.startsWith('new')) {
|
|
335
|
+
return `Creates a new entity`
|
|
336
|
+
}
|
|
337
|
+
if (lower.startsWith('delete') || lower.startsWith('remove') || lower.startsWith('destroy')) {
|
|
338
|
+
return `Removes an entity`
|
|
339
|
+
}
|
|
340
|
+
if (lower.startsWith('is') || lower.startsWith('has') || lower.startsWith('can')) {
|
|
341
|
+
return `Checks a condition`
|
|
342
|
+
}
|
|
343
|
+
if (lower.startsWith('validate') || lower.startsWith('check')) {
|
|
344
|
+
return `Validates input`
|
|
345
|
+
}
|
|
346
|
+
if (lower.startsWith('parse') || lower.startsWith('transform') || lower.startsWith('convert')) {
|
|
347
|
+
return `Transforms data format`
|
|
348
|
+
}
|
|
349
|
+
if (lower.startsWith('handle') || lower.startsWith('process')) {
|
|
350
|
+
return `Handles processing logic`
|
|
351
|
+
}
|
|
352
|
+
if (lower.startsWith('render') || lower.startsWith('display')) {
|
|
353
|
+
return `Renders output`
|
|
354
|
+
}
|
|
355
|
+
if (lower.startsWith('init') || lower.startsWith('setup') || lower.startsWith('configure')) {
|
|
356
|
+
return `Initializes configuration`
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return ''
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private calculateComplexity(fn: MikkLockFunction): number {
|
|
363
|
+
let score = 1
|
|
364
|
+
if (fn.params && fn.params.length > 3) score += 1
|
|
365
|
+
if (fn.isAsync) score += 1
|
|
366
|
+
if ((fn.calls || []).length > 10) score += Math.floor((fn.calls || []).length / 10)
|
|
367
|
+
return score
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private calculateCyclomaticComplexity(fn: MikkLockFunction): number {
|
|
371
|
+
let complexity = 1
|
|
372
|
+
if ((fn.errorHandling || []).length > 0) {
|
|
373
|
+
complexity += (fn.errorHandling || []).filter(e => e.type === 'try-catch').length
|
|
374
|
+
}
|
|
375
|
+
return complexity
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private addFunction(rich: RichFunction): void {
|
|
379
|
+
this.functions.set(rich.id, rich)
|
|
380
|
+
|
|
381
|
+
const nameLower = rich.name.toLowerCase()
|
|
382
|
+
|
|
383
|
+
const nameSet = this.byName.get(nameLower) || []
|
|
384
|
+
nameSet.push(rich.id)
|
|
385
|
+
this.byName.set(nameLower, nameSet)
|
|
386
|
+
|
|
387
|
+
const fileSet = this.byFile.get(rich.file) || []
|
|
388
|
+
fileSet.push(rich.id)
|
|
389
|
+
this.byFile.set(rich.file, fileSet)
|
|
390
|
+
|
|
391
|
+
const moduleSet = this.byModule.get(rich.moduleId) || []
|
|
392
|
+
moduleSet.push(rich.id)
|
|
393
|
+
this.byModule.set(rich.moduleId, moduleSet)
|
|
394
|
+
|
|
395
|
+
const exportedSet = this.byExport.get(rich.isExported) || []
|
|
396
|
+
exportedSet.push(rich.id)
|
|
397
|
+
this.byExport.set(rich.isExported, exportedSet)
|
|
398
|
+
|
|
399
|
+
const returnTypeLower = rich.returnType.toLowerCase()
|
|
400
|
+
const returnSet = this.byReturnType.get(returnTypeLower) || []
|
|
401
|
+
returnSet.push(rich.id)
|
|
402
|
+
this.byReturnType.set(returnTypeLower, returnSet)
|
|
403
|
+
|
|
404
|
+
for (const param of rich.params) {
|
|
405
|
+
const typeLower = param.type.toLowerCase()
|
|
406
|
+
if (!this.byParamType.has(typeLower)) {
|
|
407
|
+
this.byParamType.set(typeLower, new Set())
|
|
408
|
+
}
|
|
409
|
+
this.byParamType.get(typeLower)!.add(rich.id)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const decorator of rich.decorators) {
|
|
413
|
+
const decLower = decorator.toLowerCase()
|
|
414
|
+
const decSet = this.byDecorator.get(decLower) || []
|
|
415
|
+
decSet.push(rich.id)
|
|
416
|
+
this.byDecorator.set(decLower, decSet)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const keyword of rich.keywords) {
|
|
420
|
+
if (!this.byKeyword.has(keyword)) {
|
|
421
|
+
this.byKeyword.set(keyword, new Set())
|
|
422
|
+
}
|
|
423
|
+
this.byKeyword.get(keyword)!.add(rich.id)
|
|
424
|
+
this.allKeywords.add(keyword)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const call of rich.calls) {
|
|
428
|
+
if (!this.byCall.has(call.name)) {
|
|
429
|
+
this.byCall.set(call.name, new Set())
|
|
430
|
+
}
|
|
431
|
+
this.byCall.get(call.name)!.add(rich.id)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for (const callerId of rich.calledBy) {
|
|
435
|
+
if (!this.byCalledBy.has(callerId)) {
|
|
436
|
+
this.byCalledBy.set(callerId, new Set())
|
|
437
|
+
}
|
|
438
|
+
this.byCalledBy.get(callerId)!.add(rich.id)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.nameIndex.set(rich.name, rich.id)
|
|
442
|
+
|
|
443
|
+
if (rich.signatureHash) {
|
|
444
|
+
this.bySignatureHash.set(rich.signatureHash, rich.id)
|
|
445
|
+
}
|
|
446
|
+
if (rich.contentHash) {
|
|
447
|
+
this.byContentHash.set(rich.contentHash, rich.id)
|
|
448
|
+
}
|
|
449
|
+
for (const paramHash of rich.paramHashes || []) {
|
|
450
|
+
const existing = this.byParamHash.get(paramHash) || []
|
|
451
|
+
existing.push(rich.id)
|
|
452
|
+
this.byParamHash.set(paramHash, existing)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const fullText = [
|
|
456
|
+
rich.name,
|
|
457
|
+
rich.file,
|
|
458
|
+
rich.purpose,
|
|
459
|
+
rich.returnType,
|
|
460
|
+
...rich.params.map(p => p.name + ' ' + p.type),
|
|
461
|
+
...rich.keywords,
|
|
462
|
+
].join(' ').toLowerCase()
|
|
463
|
+
|
|
464
|
+
const tokens = fullText.split(/\s+/)
|
|
465
|
+
for (const token of tokens) {
|
|
466
|
+
if (token.length >= 2) {
|
|
467
|
+
if (!this.textIndex.has(token)) {
|
|
468
|
+
this.textIndex.set(token, new Set())
|
|
469
|
+
}
|
|
470
|
+
this.textIndex.get(token)!.add(rich.id)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private clear(): void {
|
|
476
|
+
this.functions.clear()
|
|
477
|
+
this.byName.clear()
|
|
478
|
+
this.byFile.clear()
|
|
479
|
+
this.byModule.clear()
|
|
480
|
+
this.byExport.clear()
|
|
481
|
+
this.byReturnType.clear()
|
|
482
|
+
this.byParamType.clear()
|
|
483
|
+
this.byDecorator.clear()
|
|
484
|
+
this.byKeyword.clear()
|
|
485
|
+
this.byCall.clear()
|
|
486
|
+
this.byCalledBy.clear()
|
|
487
|
+
this.nameIndex.clear()
|
|
488
|
+
this.textIndex.clear()
|
|
489
|
+
this.allKeywords.clear()
|
|
490
|
+
this.bySignatureHash.clear()
|
|
491
|
+
this.byContentHash.clear()
|
|
492
|
+
this.byParamHash.clear()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
getBySignatureHash(hash: string): RichFunction | undefined {
|
|
496
|
+
const id = this.bySignatureHash.get(hash)
|
|
497
|
+
return id ? this.functions.get(id) : undefined
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
getByContentHash(hash: string): RichFunction | undefined {
|
|
501
|
+
const id = this.byContentHash.get(hash)
|
|
502
|
+
return id ? this.functions.get(id) : undefined
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
findBySignature(signature: string): RichFunction | undefined {
|
|
506
|
+
const hash = this.simpleHash(signature)
|
|
507
|
+
return this.getBySignatureHash(hash)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
findByParamTypes(paramTypes: string[]): RichFunction[] {
|
|
511
|
+
if (paramTypes.length === 0) return []
|
|
512
|
+
|
|
513
|
+
const paramHashes = paramTypes.map(pt => this.simpleHash(pt))
|
|
514
|
+
const candidates = this.byParamHash.get(paramHashes[0]) || []
|
|
515
|
+
|
|
516
|
+
if (paramTypes.length === 1) {
|
|
517
|
+
return candidates.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return candidates
|
|
521
|
+
.map(id => this.functions.get(id))
|
|
522
|
+
.filter((fn): fn is RichFunction => {
|
|
523
|
+
if (!fn) return false
|
|
524
|
+
const fnParamHashes = fn.paramHashes || []
|
|
525
|
+
return paramHashes.every(ph => fnParamHashes.includes(ph))
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
findByLocation(file: string, line: number): RichFunction | undefined {
|
|
530
|
+
const fnsInFile = this.byFile.get(file)
|
|
531
|
+
if (!fnsInFile) return undefined
|
|
532
|
+
|
|
533
|
+
for (const id of fnsInFile) {
|
|
534
|
+
const fn = this.functions.get(id)
|
|
535
|
+
if (fn && line >= fn.startLine && line <= fn.endLine) {
|
|
536
|
+
return fn
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return undefined
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
findBySignatureAndParams(signature: string, paramTypes?: string[]): RichFunction | undefined {
|
|
543
|
+
const fn = this.findBySignature(signature)
|
|
544
|
+
if (!fn) return undefined
|
|
545
|
+
|
|
546
|
+
if (paramTypes && paramTypes.length > 0) {
|
|
547
|
+
const fnParamTypes = fn.params.map(p => p.type)
|
|
548
|
+
const matches = paramTypes.every(pt => fnParamTypes.some(fpt => fpt.includes(pt)))
|
|
549
|
+
if (!matches) return undefined
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return fn
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
get(id: string): RichFunction | undefined {
|
|
556
|
+
return this.functions.get(id)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
getByName(name: string): RichFunction | undefined {
|
|
560
|
+
const id = this.nameIndex.get(name)
|
|
561
|
+
return id ? this.functions.get(id) : undefined
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
getByExactName(name: string): RichFunction | undefined {
|
|
565
|
+
const ids = this.byName.get(name.toLowerCase())
|
|
566
|
+
if (ids && ids.length > 0) {
|
|
567
|
+
return this.functions.get(ids[0])
|
|
568
|
+
}
|
|
569
|
+
return undefined
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
getByFile(file: string): RichFunction[] {
|
|
573
|
+
const ids = this.byFile.get(file) || []
|
|
574
|
+
return ids.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
getByModule(moduleId: string): RichFunction[] {
|
|
578
|
+
const ids = this.byModule.get(moduleId) || []
|
|
579
|
+
return ids.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
getExported(): RichFunction[] {
|
|
583
|
+
const ids = this.byExport.get(true) || []
|
|
584
|
+
return ids.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
getAll(): RichFunction[] {
|
|
588
|
+
return [...this.functions.values()]
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
getCount(): number {
|
|
592
|
+
return this.functions.size
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
search(query: SearchQuery): SearchResult[] {
|
|
596
|
+
let candidateIds: Set<string> | null = null
|
|
597
|
+
const matchReasons: string[] = []
|
|
598
|
+
|
|
599
|
+
if (query.name) {
|
|
600
|
+
const ids = this.byName.get(query.name.toLowerCase())
|
|
601
|
+
if (ids) {
|
|
602
|
+
candidateIds = new Set(ids)
|
|
603
|
+
matchReasons.push(`name match: ${query.name}`)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (query.exactName) {
|
|
608
|
+
const fn = this.getByExactName(query.exactName)
|
|
609
|
+
if (fn) {
|
|
610
|
+
candidateIds = new Set([fn.id])
|
|
611
|
+
matchReasons.push(`exact name: ${query.exactName}`)
|
|
612
|
+
} else {
|
|
613
|
+
return []
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (query.nameContains) {
|
|
618
|
+
const lower = query.nameContains.toLowerCase()
|
|
619
|
+
const matching = [...this.byName.entries()]
|
|
620
|
+
.filter(([name]) => name.includes(lower))
|
|
621
|
+
.flatMap(([, ids]) => ids)
|
|
622
|
+
|
|
623
|
+
if (candidateIds) {
|
|
624
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
625
|
+
} else {
|
|
626
|
+
candidateIds = new Set(matching)
|
|
627
|
+
}
|
|
628
|
+
matchReasons.push(`name contains: ${query.nameContains}`)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (query.nameStartsWith) {
|
|
632
|
+
const lower = query.nameStartsWith.toLowerCase()
|
|
633
|
+
const matching = [...this.byName.entries()]
|
|
634
|
+
.filter(([name]) => name.startsWith(lower))
|
|
635
|
+
.flatMap(([, ids]) => ids)
|
|
636
|
+
|
|
637
|
+
if (candidateIds) {
|
|
638
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
639
|
+
} else {
|
|
640
|
+
candidateIds = new Set(matching)
|
|
641
|
+
}
|
|
642
|
+
matchReasons.push(`name starts with: ${query.nameStartsWith}`)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (query.file) {
|
|
646
|
+
const ids = this.byFile.get(query.file) || []
|
|
647
|
+
if (candidateIds) {
|
|
648
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
|
|
649
|
+
} else {
|
|
650
|
+
candidateIds = new Set(ids)
|
|
651
|
+
}
|
|
652
|
+
matchReasons.push(`file: ${query.file}`)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (query.inFile) {
|
|
656
|
+
const lower = query.inFile.toLowerCase()
|
|
657
|
+
const matching = [...this.byFile.entries()]
|
|
658
|
+
.filter(([file]) => file.toLowerCase().includes(lower))
|
|
659
|
+
.flatMap(([, ids]) => ids)
|
|
660
|
+
|
|
661
|
+
if (candidateIds) {
|
|
662
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
663
|
+
} else {
|
|
664
|
+
candidateIds = new Set(matching)
|
|
665
|
+
}
|
|
666
|
+
matchReasons.push(`in file containing: ${query.inFile}`)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (query.moduleId) {
|
|
670
|
+
const ids = this.byModule.get(query.moduleId) || []
|
|
671
|
+
if (candidateIds) {
|
|
672
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
|
|
673
|
+
} else {
|
|
674
|
+
candidateIds = new Set(ids)
|
|
675
|
+
}
|
|
676
|
+
matchReasons.push(`module: ${query.moduleId}`)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (query.isExported !== undefined) {
|
|
680
|
+
const ids = this.byExport.get(query.isExported) || []
|
|
681
|
+
if (candidateIds) {
|
|
682
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
|
|
683
|
+
} else {
|
|
684
|
+
candidateIds = new Set(ids)
|
|
685
|
+
}
|
|
686
|
+
matchReasons.push(`isExported: ${query.isExported}`)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (query.isAsync !== undefined && query.isAsync) {
|
|
690
|
+
const asyncFns = [...this.functions.values()].filter(f => f.isAsync).map(f => f.id)
|
|
691
|
+
if (candidateIds) {
|
|
692
|
+
candidateIds = new Set([...candidateIds].filter(id => asyncFns.includes(id)))
|
|
693
|
+
} else {
|
|
694
|
+
candidateIds = new Set(asyncFns)
|
|
695
|
+
}
|
|
696
|
+
matchReasons.push('isAsync: true')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (query.returnType) {
|
|
700
|
+
const ids = this.byReturnType.get(query.returnType.toLowerCase()) || []
|
|
701
|
+
if (candidateIds) {
|
|
702
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
|
|
703
|
+
} else {
|
|
704
|
+
candidateIds = new Set(ids)
|
|
705
|
+
}
|
|
706
|
+
matchReasons.push(`returnType: ${query.returnType}`)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (query.returnTypeContains) {
|
|
710
|
+
const lower = query.returnTypeContains.toLowerCase()
|
|
711
|
+
const matching = [...this.functions.values()]
|
|
712
|
+
.filter(f => f.returnType.toLowerCase().includes(lower))
|
|
713
|
+
.map(f => f.id)
|
|
714
|
+
|
|
715
|
+
if (candidateIds) {
|
|
716
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
717
|
+
} else {
|
|
718
|
+
candidateIds = new Set(matching)
|
|
719
|
+
}
|
|
720
|
+
matchReasons.push(`returnType contains: ${query.returnTypeContains}`)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (query.hasParam) {
|
|
724
|
+
const matching = [...this.functions.values()]
|
|
725
|
+
.filter(f => f.params.some(p => p.name.toLowerCase() === query.hasParam!.toLowerCase()))
|
|
726
|
+
.map(f => f.id)
|
|
727
|
+
|
|
728
|
+
if (candidateIds) {
|
|
729
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
730
|
+
} else {
|
|
731
|
+
candidateIds = new Set(matching)
|
|
732
|
+
}
|
|
733
|
+
matchReasons.push(`has param: ${query.hasParam}`)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (query.paramTypes && query.paramTypes.length > 0) {
|
|
737
|
+
const matching = [...this.functions.values()]
|
|
738
|
+
.filter(f => {
|
|
739
|
+
const paramTypeStr = f.params.map(p => p.type.toLowerCase()).join(',')
|
|
740
|
+
return query.paramTypes!.every(t => paramTypeStr.includes(t.toLowerCase()))
|
|
741
|
+
})
|
|
742
|
+
.map(f => f.id)
|
|
743
|
+
|
|
744
|
+
if (candidateIds) {
|
|
745
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
746
|
+
} else {
|
|
747
|
+
candidateIds = new Set(matching)
|
|
748
|
+
}
|
|
749
|
+
matchReasons.push(`param types: ${query.paramTypes.join(', ')}`)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (query.hasDecorator) {
|
|
753
|
+
const lower = query.hasDecorator.toLowerCase()
|
|
754
|
+
const ids = this.byDecorator.get(lower) || []
|
|
755
|
+
if (candidateIds) {
|
|
756
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
|
|
757
|
+
} else {
|
|
758
|
+
candidateIds = new Set(ids)
|
|
759
|
+
}
|
|
760
|
+
matchReasons.push(`decorator: ${query.hasDecorator}`)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (query.keyword) {
|
|
764
|
+
const lower = query.keyword.toLowerCase()
|
|
765
|
+
const ids = this.byKeyword.get(lower)
|
|
766
|
+
if (ids && ids.size > 0) {
|
|
767
|
+
if (candidateIds) {
|
|
768
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.has(id)))
|
|
769
|
+
} else {
|
|
770
|
+
candidateIds = new Set(ids)
|
|
771
|
+
}
|
|
772
|
+
matchReasons.push(`keyword: ${query.keyword}`)
|
|
773
|
+
} else if (!candidateIds) {
|
|
774
|
+
candidateIds = new Set()
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (query.text) {
|
|
779
|
+
const tokens = query.text.toLowerCase().split(/\s+/).filter(t => t.length >= 2)
|
|
780
|
+
const tokenMatches = tokens.map(token => this.textIndex.get(token) || new Set())
|
|
781
|
+
|
|
782
|
+
const matchingIds = [...this.functions.keys()].filter(id => {
|
|
783
|
+
return tokenMatches.every(set => set.has(id))
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
if (candidateIds) {
|
|
787
|
+
candidateIds = new Set([...candidateIds].filter(id => matchingIds.includes(id)))
|
|
788
|
+
} else {
|
|
789
|
+
candidateIds = new Set(matchingIds)
|
|
790
|
+
}
|
|
791
|
+
matchReasons.push(`text search: ${query.text}`)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (query.calls) {
|
|
795
|
+
const lower = query.calls.toLowerCase()
|
|
796
|
+
const ids = this.byCall.get(lower)
|
|
797
|
+
if (ids && ids.size > 0) {
|
|
798
|
+
if (candidateIds) {
|
|
799
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.has(id)))
|
|
800
|
+
} else {
|
|
801
|
+
candidateIds = new Set(ids)
|
|
802
|
+
}
|
|
803
|
+
matchReasons.push(`calls: ${query.calls}`)
|
|
804
|
+
} else if (!candidateIds) {
|
|
805
|
+
candidateIds = new Set()
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (query.calledBy) {
|
|
810
|
+
const ids = this.byCalledBy.get(query.calledBy)
|
|
811
|
+
if (ids && ids.size > 0) {
|
|
812
|
+
if (candidateIds) {
|
|
813
|
+
candidateIds = new Set([...candidateIds].filter(id => ids.has(id)))
|
|
814
|
+
} else {
|
|
815
|
+
candidateIds = new Set(ids)
|
|
816
|
+
}
|
|
817
|
+
matchReasons.push(`calledBy: ${query.calledBy}`)
|
|
818
|
+
} else if (!candidateIds) {
|
|
819
|
+
candidateIds = new Set()
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (query.minParams !== undefined) {
|
|
824
|
+
const matching = [...this.functions.values()]
|
|
825
|
+
.filter(f => f.params.length >= query.minParams!)
|
|
826
|
+
.map(f => f.id)
|
|
827
|
+
|
|
828
|
+
if (candidateIds) {
|
|
829
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
830
|
+
} else {
|
|
831
|
+
candidateIds = new Set(matching)
|
|
832
|
+
}
|
|
833
|
+
matchReasons.push(`minParams: ${query.minParams}`)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (query.maxParams !== undefined) {
|
|
837
|
+
const matching = [...this.functions.values()]
|
|
838
|
+
.filter(f => f.params.length <= query.maxParams!)
|
|
839
|
+
.map(f => f.id)
|
|
840
|
+
|
|
841
|
+
if (candidateIds) {
|
|
842
|
+
candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
|
|
843
|
+
} else {
|
|
844
|
+
candidateIds = new Set(matching)
|
|
845
|
+
}
|
|
846
|
+
matchReasons.push(`maxParams: ${query.maxParams}`)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (!candidateIds) {
|
|
850
|
+
candidateIds = new Set(this.functions.keys())
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const results: SearchResult[] = []
|
|
854
|
+
for (const id of candidateIds) {
|
|
855
|
+
const fn = this.functions.get(id)
|
|
856
|
+
if (!fn) continue
|
|
857
|
+
|
|
858
|
+
let score = 1.0
|
|
859
|
+
|
|
860
|
+
if (query.name && fn.name.toLowerCase() === query.name.toLowerCase()) {
|
|
861
|
+
score *= 2.0
|
|
862
|
+
}
|
|
863
|
+
if (query.isExported !== undefined && fn.isExported === query.isExported) {
|
|
864
|
+
score *= 1.5
|
|
865
|
+
}
|
|
866
|
+
if (query.keyword) {
|
|
867
|
+
const kw = query.keyword.toLowerCase()
|
|
868
|
+
if (fn.keywords.includes(kw)) score *= 1.5
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
results.push({
|
|
872
|
+
function: fn,
|
|
873
|
+
score,
|
|
874
|
+
matchReasons,
|
|
875
|
+
})
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
results.sort((a, b) => b.score - a.score)
|
|
879
|
+
|
|
880
|
+
const offset = query.offset || 0
|
|
881
|
+
const limit = query.limit || 100
|
|
882
|
+
|
|
883
|
+
return results.slice(offset, offset + limit)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
searchText(text: string, limit: number = 20): SearchResult[] {
|
|
887
|
+
return this.search({ text, limit })
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
searchByName(name: string, limit: number = 20): SearchResult[] {
|
|
891
|
+
return this.search({ nameContains: name, limit })
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
getCallers(functionId: string): RichFunction[] {
|
|
895
|
+
const fn = this.functions.get(functionId)
|
|
896
|
+
if (!fn) return []
|
|
897
|
+
return fn.calledBy
|
|
898
|
+
.map(id => this.functions.get(id))
|
|
899
|
+
.filter(Boolean) as RichFunction[]
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
getCallees(functionId: string): RichFunction[] {
|
|
903
|
+
const fn = this.functions.get(functionId)
|
|
904
|
+
if (!fn) return []
|
|
905
|
+
return fn.calls
|
|
906
|
+
.map(c => c.targetId)
|
|
907
|
+
.filter(Boolean)
|
|
908
|
+
.map(id => this.functions.get(id!))
|
|
909
|
+
.filter(Boolean) as RichFunction[]
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
getRelated(functionId: string, depth: number = 1): RichFunction[] {
|
|
913
|
+
const related = new Set<string>()
|
|
914
|
+
const queue: { id: string; d: number }[] = [{ id: functionId, d: 0 }]
|
|
915
|
+
|
|
916
|
+
while (queue.length > 0) {
|
|
917
|
+
const { id, d } = queue.shift()!
|
|
918
|
+
if (d >= depth) continue
|
|
919
|
+
|
|
920
|
+
const fn = this.functions.get(id)
|
|
921
|
+
if (!fn) continue
|
|
922
|
+
|
|
923
|
+
for (const callerId of fn.calledBy) {
|
|
924
|
+
if (!related.has(callerId)) {
|
|
925
|
+
related.add(callerId)
|
|
926
|
+
queue.push({ id: callerId, d: d + 1 })
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
for (const callee of fn.calls) {
|
|
931
|
+
if (callee.targetId && !related.has(callee.targetId)) {
|
|
932
|
+
related.add(callee.targetId)
|
|
933
|
+
queue.push({ id: callee.targetId, d: d + 1 })
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return [...related]
|
|
939
|
+
.map(id => this.functions.get(id))
|
|
940
|
+
.filter(Boolean) as RichFunction[]
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
getContext(request: ContextRequest): FunctionContext | null {
|
|
944
|
+
const fn = this.functions.get(request.functionId)
|
|
945
|
+
if (!fn) return null
|
|
946
|
+
|
|
947
|
+
const include = request.include || 'full'
|
|
948
|
+
|
|
949
|
+
const context: FunctionContext = {
|
|
950
|
+
signature: fn.signature,
|
|
951
|
+
fullSignature: fn.fullSignature,
|
|
952
|
+
params: fn.params,
|
|
953
|
+
returnType: fn.returnType,
|
|
954
|
+
calls: include === 'signature' ? [] : fn.calls,
|
|
955
|
+
calledBy: include === 'signature' ? [] : fn.calledBy,
|
|
956
|
+
decorators: include === 'signature' ? [] : fn.decorators,
|
|
957
|
+
file: fn.file,
|
|
958
|
+
startLine: fn.startLine,
|
|
959
|
+
endLine: fn.endLine,
|
|
960
|
+
keywords: fn.keywords,
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (include === 'full' || include === 'body' || include === 'all') {
|
|
964
|
+
context.purpose = fn.purpose
|
|
965
|
+
context.docComment = fn.docComment
|
|
966
|
+
context.errorHandling = fn.errorHandling
|
|
967
|
+
context.edgeCases = fn.edgeCasesHandled
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (fn.body && (include === 'body' || include === 'all')) {
|
|
971
|
+
if (request.maxBodyLines && request.maxBodyLines > 0) {
|
|
972
|
+
const lines = fn.body.split('\n')
|
|
973
|
+
context.body = lines.slice(0, request.maxBodyLines).join('\n')
|
|
974
|
+
if (lines.length > request.maxBodyLines) {
|
|
975
|
+
context.body += `\n... ${lines.length - request.maxBodyLines} more lines`
|
|
976
|
+
}
|
|
977
|
+
} else {
|
|
978
|
+
context.body = fn.body
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return context
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
getSignatures(functionIds: string[]): string[] {
|
|
986
|
+
return functionIds
|
|
987
|
+
.map(id => this.functions.get(id))
|
|
988
|
+
.filter(Boolean)
|
|
989
|
+
.map(fn => fn!.fullSignature)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
getSignaturesMap(functionIds: string[]): Record<string, string> {
|
|
993
|
+
const map: Record<string, string> = {}
|
|
994
|
+
for (const id of functionIds) {
|
|
995
|
+
const fn = this.functions.get(id)
|
|
996
|
+
if (fn) {
|
|
997
|
+
map[id] = fn.fullSignature
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return map
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
getSummaries(functionIds: string[]): Array<{ id: string; name: string; signature: string; purpose: string; file: string }> {
|
|
1004
|
+
return functionIds
|
|
1005
|
+
.map(id => {
|
|
1006
|
+
const fn = this.functions.get(id)
|
|
1007
|
+
if (!fn) return null
|
|
1008
|
+
return {
|
|
1009
|
+
id: fn.id,
|
|
1010
|
+
name: fn.name,
|
|
1011
|
+
signature: fn.fullSignature,
|
|
1012
|
+
purpose: fn.purpose,
|
|
1013
|
+
file: fn.file,
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
1016
|
+
.filter(Boolean) as any
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
getAllSignatures(): Map<string, string> {
|
|
1020
|
+
const map = new Map<string, string>()
|
|
1021
|
+
for (const [id, fn] of this.functions) {
|
|
1022
|
+
map.set(id, fn.fullSignature)
|
|
1023
|
+
}
|
|
1024
|
+
return map
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
getAllSummaries(): Array<{ id: string; name: string; signature: string; purpose: string; file: string }> {
|
|
1028
|
+
return [...this.functions.values()].map(fn => ({
|
|
1029
|
+
id: fn.id,
|
|
1030
|
+
name: fn.name,
|
|
1031
|
+
signature: fn.fullSignature,
|
|
1032
|
+
purpose: fn.purpose,
|
|
1033
|
+
file: fn.file,
|
|
1034
|
+
}))
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
getKeywords(): string[] {
|
|
1038
|
+
return [...this.allKeywords]
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
getStats(): {
|
|
1042
|
+
totalFunctions: number
|
|
1043
|
+
exportedCount: number
|
|
1044
|
+
asyncCount: number
|
|
1045
|
+
byModule: Record<string, number>
|
|
1046
|
+
byReturnType: Record<string, number>
|
|
1047
|
+
byFile: Record<string, number>
|
|
1048
|
+
} {
|
|
1049
|
+
const functions = [...this.functions.values()]
|
|
1050
|
+
|
|
1051
|
+
const byModule: Record<string, number> = {}
|
|
1052
|
+
const byReturnType: Record<string, number> = {}
|
|
1053
|
+
const byFile: Record<string, number> = {}
|
|
1054
|
+
|
|
1055
|
+
let exportedCount = 0
|
|
1056
|
+
let asyncCount = 0
|
|
1057
|
+
|
|
1058
|
+
for (const fn of functions) {
|
|
1059
|
+
if (fn.isExported) exportedCount++
|
|
1060
|
+
if (fn.isAsync) asyncCount++
|
|
1061
|
+
|
|
1062
|
+
byModule[fn.moduleId] = (byModule[fn.moduleId] || 0) + 1
|
|
1063
|
+
|
|
1064
|
+
const returnType = fn.returnType || 'unknown'
|
|
1065
|
+
byReturnType[returnType] = (byReturnType[returnType] || 0) + 1
|
|
1066
|
+
|
|
1067
|
+
const fileName = fn.file.split('/').pop() || fn.file
|
|
1068
|
+
byFile[fileName] = (byFile[fileName] || 0) + 1
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
totalFunctions: functions.length,
|
|
1073
|
+
exportedCount,
|
|
1074
|
+
asyncCount,
|
|
1075
|
+
byModule,
|
|
1076
|
+
byReturnType,
|
|
1077
|
+
byFile,
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|