@side-quest/kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +54 -352
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +156 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -2509
- package/dist/index.js.map +1 -0
- package/dist/lib/ast/index.d.ts +11 -0
- package/dist/lib/ast/index.d.ts.map +1 -0
- package/dist/lib/ast/index.js +15 -0
- package/dist/lib/ast/index.js.map +1 -0
- package/dist/lib/ast/languages.d.ts +55 -0
- package/dist/lib/ast/languages.d.ts.map +1 -0
- package/dist/lib/ast/languages.js +146 -0
- package/dist/lib/ast/languages.js.map +1 -0
- package/dist/lib/ast/pattern.d.ts +84 -0
- package/dist/lib/ast/pattern.d.ts.map +1 -0
- package/dist/lib/ast/pattern.js +268 -0
- package/dist/lib/ast/pattern.js.map +1 -0
- package/dist/lib/ast/searcher.d.ts +89 -0
- package/dist/lib/ast/searcher.d.ts.map +1 -0
- package/dist/lib/ast/searcher.js +316 -0
- package/dist/lib/ast/searcher.js.map +1 -0
- package/dist/lib/ast/types.d.ts +93 -0
- package/dist/lib/ast/types.d.ts.map +1 -0
- package/dist/lib/ast/types.js +23 -0
- package/dist/lib/ast/types.js.map +1 -0
- package/dist/lib/commands/callers.d.ts +20 -0
- package/dist/lib/commands/callers.d.ts.map +1 -0
- package/dist/lib/commands/callers.js +162 -0
- package/dist/lib/commands/callers.js.map +1 -0
- package/dist/lib/commands/find.d.ts +15 -0
- package/dist/lib/commands/find.d.ts.map +1 -0
- package/dist/lib/commands/find.js +113 -0
- package/dist/lib/commands/find.js.map +1 -0
- package/dist/lib/commands/overview.d.ts +6 -0
- package/dist/lib/commands/overview.d.ts.map +1 -0
- package/dist/lib/commands/overview.js +52 -0
- package/dist/lib/commands/overview.js.map +1 -0
- package/dist/lib/commands/prime.d.ts +16 -0
- package/dist/lib/commands/prime.d.ts.map +1 -0
- package/dist/lib/commands/prime.js +168 -0
- package/dist/lib/commands/prime.js.map +1 -0
- package/dist/lib/commands/search.d.ts +20 -0
- package/dist/lib/commands/search.d.ts.map +1 -0
- package/dist/lib/commands/search.js +111 -0
- package/dist/lib/commands/search.js.map +1 -0
- package/dist/lib/errors.d.ts +80 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +189 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/formatters/output.d.ts +5 -0
- package/dist/lib/formatters/output.d.ts.map +1 -0
- package/dist/lib/formatters/output.js +5 -0
- package/dist/lib/formatters/output.js.map +1 -0
- package/dist/lib/formatters.d.ts +29 -0
- package/dist/lib/formatters.d.ts.map +1 -0
- package/dist/lib/formatters.js +141 -0
- package/dist/lib/formatters.js.map +1 -0
- package/dist/lib/index-tools.d.ts +108 -0
- package/dist/lib/index-tools.d.ts.map +1 -0
- package/dist/lib/index-tools.js +311 -0
- package/dist/lib/index-tools.js.map +1 -0
- package/dist/lib/index.d.ts +21 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +42 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/kit-wrapper.d.ts +70 -0
- package/dist/lib/kit-wrapper.d.ts.map +1 -0
- package/dist/lib/kit-wrapper.js +462 -0
- package/dist/lib/kit-wrapper.js.map +1 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +39 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/types.d.ts +179 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +48 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils/args.d.ts +40 -0
- package/dist/lib/utils/args.d.ts.map +1 -0
- package/dist/lib/utils/args.js +58 -0
- package/dist/lib/utils/args.js.map +1 -0
- package/dist/lib/utils/git.d.ts +23 -0
- package/dist/lib/utils/git.d.ts.map +1 -0
- package/dist/lib/utils/git.js +50 -0
- package/dist/lib/utils/git.js.map +1 -0
- package/dist/lib/utils/index-parser.d.ts +155 -0
- package/dist/lib/utils/index-parser.d.ts.map +1 -0
- package/dist/lib/utils/index-parser.js +252 -0
- package/dist/lib/utils/index-parser.js.map +1 -0
- package/dist/lib/validators.d.ts +138 -0
- package/dist/lib/validators.d.ts.map +1 -0
- package/dist/lib/validators.js +302 -0
- package/dist/lib/validators.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +769 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +5 -2
- package/src/cli.ts +170 -0
- package/src/lib/ast/index.ts +32 -0
- package/src/lib/ast/languages.ts +172 -0
- package/src/lib/ast/pattern.ts +299 -0
- package/src/lib/ast/searcher.ts +381 -0
- package/src/lib/ast/types.ts +99 -0
- package/src/lib/commands/callers.ts +226 -0
- package/src/lib/commands/find.ts +159 -0
- package/src/lib/commands/overview.ts +73 -0
- package/src/lib/commands/prime.ts +271 -0
- package/src/lib/commands/search.ts +146 -0
- package/src/lib/errors.ts +221 -0
- package/src/lib/formatters/output.ts +9 -0
- package/src/lib/formatters.ts +189 -0
- package/src/lib/index-tools.ts +471 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/kit-wrapper.ts +675 -0
- package/src/lib/logger.ts +57 -0
- package/src/lib/types.ts +228 -0
- package/src/lib/utils/args.ts +72 -0
- package/src/lib/utils/git.ts +65 -0
- package/src/lib/utils/index-parser.ts +350 -0
- package/src/lib/validators.ts +437 -0
- package/src/mcp/index.ts +144 -79
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Pattern
|
|
3
|
+
*
|
|
4
|
+
* Compiles and matches AST search patterns against tree-sitter nodes.
|
|
5
|
+
* Ported from Kit's ASTPattern class in ast_search.py.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAstLogger } from '../logger.js'
|
|
9
|
+
import type { SyntaxNode } from './languages.js'
|
|
10
|
+
import { type PatternCriteria, SearchMode } from './types.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Node types that represent function definitions across languages.
|
|
14
|
+
*/
|
|
15
|
+
const FUNCTION_NODE_TYPES = [
|
|
16
|
+
// TypeScript/JavaScript
|
|
17
|
+
'function_declaration',
|
|
18
|
+
'function_expression',
|
|
19
|
+
'arrow_function',
|
|
20
|
+
'method_definition',
|
|
21
|
+
'generator_function_declaration',
|
|
22
|
+
// Python
|
|
23
|
+
'function_definition',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Node types that represent class definitions across languages.
|
|
28
|
+
*/
|
|
29
|
+
const CLASS_NODE_TYPES = [
|
|
30
|
+
// TypeScript/JavaScript
|
|
31
|
+
'class_declaration',
|
|
32
|
+
'class_expression',
|
|
33
|
+
// Python
|
|
34
|
+
'class_definition',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Node types that represent try/catch statements.
|
|
39
|
+
*/
|
|
40
|
+
const TRY_NODE_TYPES = [
|
|
41
|
+
'try_statement', // TypeScript/JavaScript and Python
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ASTPattern compiles search patterns and matches them against AST nodes.
|
|
46
|
+
*
|
|
47
|
+
* Supports two modes:
|
|
48
|
+
* - `simple`: Natural language patterns like "async function", "class"
|
|
49
|
+
* - `pattern`: JSON object criteria like {"type": "function_declaration"}
|
|
50
|
+
*/
|
|
51
|
+
export class ASTPattern {
|
|
52
|
+
readonly pattern: string
|
|
53
|
+
readonly mode: SearchMode
|
|
54
|
+
|
|
55
|
+
// Simple mode flags (parsed from natural language)
|
|
56
|
+
private isAsync = false
|
|
57
|
+
private isDef = false
|
|
58
|
+
private isClass = false
|
|
59
|
+
private isTry = false
|
|
60
|
+
private isImport = false
|
|
61
|
+
private isExport = false
|
|
62
|
+
|
|
63
|
+
// Pattern mode criteria (parsed from JSON)
|
|
64
|
+
private criteria: PatternCriteria = {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a new AST pattern.
|
|
68
|
+
*
|
|
69
|
+
* @param pattern - The search pattern string
|
|
70
|
+
* @param mode - The search mode (simple or pattern)
|
|
71
|
+
*/
|
|
72
|
+
constructor(pattern: string, mode: SearchMode = SearchMode.SIMPLE) {
|
|
73
|
+
this.pattern = pattern
|
|
74
|
+
this.mode = mode
|
|
75
|
+
this.compile()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compile the pattern based on mode.
|
|
80
|
+
*/
|
|
81
|
+
private compile(): void {
|
|
82
|
+
if (this.mode === SearchMode.SIMPLE) {
|
|
83
|
+
this.compileSimple()
|
|
84
|
+
} else if (this.mode === SearchMode.PATTERN) {
|
|
85
|
+
this.compilePattern()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compile simple mode pattern from natural language.
|
|
91
|
+
* Parses keywords like "async", "function", "class", etc.
|
|
92
|
+
*/
|
|
93
|
+
private compileSimple(): void {
|
|
94
|
+
const lower = this.pattern.toLowerCase()
|
|
95
|
+
|
|
96
|
+
// Check for keywords
|
|
97
|
+
this.isAsync = lower.includes('async')
|
|
98
|
+
this.isDef =
|
|
99
|
+
lower.includes('function') ||
|
|
100
|
+
lower.includes('def') ||
|
|
101
|
+
lower.includes('method')
|
|
102
|
+
this.isClass = lower.includes('class')
|
|
103
|
+
this.isTry = lower.includes('try') || lower.includes('catch')
|
|
104
|
+
this.isImport = lower.includes('import')
|
|
105
|
+
this.isExport = lower.includes('export')
|
|
106
|
+
|
|
107
|
+
// If no specific keywords matched, treat as text search
|
|
108
|
+
if (
|
|
109
|
+
!this.isAsync &&
|
|
110
|
+
!this.isDef &&
|
|
111
|
+
!this.isClass &&
|
|
112
|
+
!this.isTry &&
|
|
113
|
+
!this.isImport &&
|
|
114
|
+
!this.isExport
|
|
115
|
+
) {
|
|
116
|
+
// Fall back to text matching mode
|
|
117
|
+
this.criteria = { textMatch: this.pattern }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compile pattern mode from JSON criteria.
|
|
123
|
+
*/
|
|
124
|
+
private compilePattern(): void {
|
|
125
|
+
try {
|
|
126
|
+
this.criteria = JSON.parse(this.pattern)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// If JSON parsing fails, treat as text match
|
|
129
|
+
// Log at debug level since this is often intentional (user typed plain text)
|
|
130
|
+
const logger = getAstLogger()
|
|
131
|
+
logger.debug('Pattern JSON parse failed, using text match fallback', {
|
|
132
|
+
pattern: this.pattern,
|
|
133
|
+
error: error instanceof Error ? error.message : String(error),
|
|
134
|
+
})
|
|
135
|
+
this.criteria = { textMatch: this.pattern }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a node matches this pattern.
|
|
141
|
+
*
|
|
142
|
+
* @param node - The tree-sitter syntax node
|
|
143
|
+
* @param source - The source code string
|
|
144
|
+
* @returns True if the node matches
|
|
145
|
+
*/
|
|
146
|
+
matches(node: SyntaxNode, source: string): boolean {
|
|
147
|
+
if (this.mode === SearchMode.SIMPLE) {
|
|
148
|
+
return this.matchesSimple(node, source)
|
|
149
|
+
} else if (this.mode === SearchMode.PATTERN) {
|
|
150
|
+
return this.matchesPattern(node, source)
|
|
151
|
+
}
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Match a node against simple mode pattern.
|
|
157
|
+
*/
|
|
158
|
+
private matchesSimple(node: SyntaxNode, source: string): boolean {
|
|
159
|
+
const nodeType = node.type
|
|
160
|
+
|
|
161
|
+
// If we're looking for specific constructs
|
|
162
|
+
if (
|
|
163
|
+
this.isDef ||
|
|
164
|
+
this.isClass ||
|
|
165
|
+
this.isTry ||
|
|
166
|
+
this.isImport ||
|
|
167
|
+
this.isExport
|
|
168
|
+
) {
|
|
169
|
+
// Check construct type
|
|
170
|
+
if (this.isDef && !this.isFunctionNode(nodeType)) return false
|
|
171
|
+
if (this.isClass && !this.isClassNode(nodeType)) return false
|
|
172
|
+
if (this.isTry && !TRY_NODE_TYPES.includes(nodeType)) return false
|
|
173
|
+
if (this.isImport && !nodeType.includes('import')) return false
|
|
174
|
+
if (this.isExport && !nodeType.includes('export')) return false
|
|
175
|
+
|
|
176
|
+
// Check async modifier (only for functions)
|
|
177
|
+
if (this.isAsync && this.isDef) {
|
|
178
|
+
if (!this.hasAsyncModifier(node)) return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Text match fallback
|
|
185
|
+
if (this.criteria.textMatch) {
|
|
186
|
+
const text = this.getNodeText(node, source)
|
|
187
|
+
return text.includes(this.criteria.textMatch)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Match a node against pattern mode criteria.
|
|
195
|
+
*/
|
|
196
|
+
private matchesPattern(node: SyntaxNode, source: string): boolean {
|
|
197
|
+
// Match node type
|
|
198
|
+
if (this.criteria.type && node.type !== this.criteria.type) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Match async modifier
|
|
203
|
+
if (this.criteria.async !== undefined) {
|
|
204
|
+
if (this.hasAsyncModifier(node) !== this.criteria.async) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Match symbol name
|
|
210
|
+
if (this.criteria.name) {
|
|
211
|
+
const name = this.getNodeName(node, source)
|
|
212
|
+
if (name !== this.criteria.name) {
|
|
213
|
+
return false
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Match text content
|
|
218
|
+
if (this.criteria.textMatch) {
|
|
219
|
+
const text = this.getNodeText(node, source)
|
|
220
|
+
if (!text.includes(this.criteria.textMatch)) {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// If no criteria specified, don't match anything
|
|
226
|
+
if (
|
|
227
|
+
!this.criteria.type &&
|
|
228
|
+
!this.criteria.async &&
|
|
229
|
+
!this.criteria.name &&
|
|
230
|
+
!this.criteria.textMatch
|
|
231
|
+
) {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a node type is a function node.
|
|
240
|
+
*/
|
|
241
|
+
private isFunctionNode(type: string): boolean {
|
|
242
|
+
return FUNCTION_NODE_TYPES.includes(type)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a node type is a class node.
|
|
247
|
+
*/
|
|
248
|
+
private isClassNode(type: string): boolean {
|
|
249
|
+
return CLASS_NODE_TYPES.includes(type)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if a node has an async modifier.
|
|
254
|
+
* Works for both direct 'async' child and keyword in text.
|
|
255
|
+
*/
|
|
256
|
+
private hasAsyncModifier(node: SyntaxNode): boolean {
|
|
257
|
+
// Check for 'async' child node (TypeScript/JavaScript)
|
|
258
|
+
for (const child of node.children) {
|
|
259
|
+
if (child && child.type === 'async') return true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check for async keyword in first few characters
|
|
263
|
+
// This handles cases where async is part of the node text
|
|
264
|
+
const firstPart = node.text?.slice(0, 10) ?? ''
|
|
265
|
+
return firstPart.includes('async')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get the name of a named node (function, class, etc.)
|
|
270
|
+
*/
|
|
271
|
+
private getNodeName(node: SyntaxNode, source: string): string | undefined {
|
|
272
|
+
// Look for identifier or name child
|
|
273
|
+
for (const child of node.children) {
|
|
274
|
+
if (!child) continue
|
|
275
|
+
if (
|
|
276
|
+
child.type === 'identifier' ||
|
|
277
|
+
child.type === 'type_identifier' ||
|
|
278
|
+
child.type === 'property_identifier'
|
|
279
|
+
) {
|
|
280
|
+
return source.substring(child.startIndex, child.endIndex)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check named children
|
|
285
|
+
const nameChild = node.childForFieldName('name')
|
|
286
|
+
if (nameChild) {
|
|
287
|
+
return source.substring(nameChild.startIndex, nameChild.endIndex)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return undefined
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get the text content of a node.
|
|
295
|
+
*/
|
|
296
|
+
private getNodeText(node: SyntaxNode, source: string): string {
|
|
297
|
+
return source.substring(node.startIndex, node.endIndex)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Searcher
|
|
3
|
+
*
|
|
4
|
+
* Searches files using tree-sitter AST patterns.
|
|
5
|
+
* Ported from Kit's ASTSearcher class in ast_search.py.
|
|
6
|
+
*
|
|
7
|
+
* Performance optimizations:
|
|
8
|
+
* - Uses fs/promises for non-blocking I/O
|
|
9
|
+
* - Parses files in parallel chunks to maximize throughput
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
13
|
+
import { join, relative } from 'node:path'
|
|
14
|
+
import { processInParallelChunks } from '@side-quest/core/concurrency'
|
|
15
|
+
import { getAstLogger } from '../logger.js'
|
|
16
|
+
import { getDefaultKitPath } from '../types.js'
|
|
17
|
+
import type { SyntaxNode } from './languages.js'
|
|
18
|
+
import { detectLanguage, getParser, isSupported } from './languages.js'
|
|
19
|
+
import { ASTPattern } from './pattern.js'
|
|
20
|
+
import {
|
|
21
|
+
type ASTMatch,
|
|
22
|
+
type ASTMatchContext,
|
|
23
|
+
type ASTSearchOptions,
|
|
24
|
+
type ASTSearchResult,
|
|
25
|
+
SearchMode,
|
|
26
|
+
} from './types.js'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maximum file size to parse (5MB).
|
|
30
|
+
* Larger files are skipped to prevent memory issues.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum text length to include in match results.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_TEXT_LENGTH = 500
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Number of files to parse in parallel.
|
|
41
|
+
* Balances throughput vs memory usage.
|
|
42
|
+
*/
|
|
43
|
+
const PARALLEL_CHUNK_SIZE = 10
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Directories to skip during traversal.
|
|
47
|
+
*/
|
|
48
|
+
const SKIP_DIRS = new Set([
|
|
49
|
+
'node_modules',
|
|
50
|
+
'.git',
|
|
51
|
+
'.svn',
|
|
52
|
+
'.hg',
|
|
53
|
+
'__pycache__',
|
|
54
|
+
'.pytest_cache',
|
|
55
|
+
'.mypy_cache',
|
|
56
|
+
'dist',
|
|
57
|
+
'build',
|
|
58
|
+
'.next',
|
|
59
|
+
'.nuxt',
|
|
60
|
+
'coverage',
|
|
61
|
+
'.coverage',
|
|
62
|
+
'venv',
|
|
63
|
+
'.venv',
|
|
64
|
+
'env',
|
|
65
|
+
'.env',
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ASTSearcher performs AST-based code search using tree-sitter.
|
|
70
|
+
*
|
|
71
|
+
* It traverses the file system, parses supported files, and matches
|
|
72
|
+
* AST nodes against the given pattern.
|
|
73
|
+
*/
|
|
74
|
+
export class ASTSearcher {
|
|
75
|
+
private readonly repoPath: string
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a new AST searcher.
|
|
79
|
+
*
|
|
80
|
+
* @param repoPath - Repository path to search (defaults to KIT_DEFAULT_PATH or cwd)
|
|
81
|
+
*/
|
|
82
|
+
constructor(repoPath?: string) {
|
|
83
|
+
this.repoPath = repoPath ?? getDefaultKitPath()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Search for AST patterns in the repository.
|
|
88
|
+
*
|
|
89
|
+
* Files are parsed in parallel chunks for better performance.
|
|
90
|
+
*
|
|
91
|
+
* @param options - Search options
|
|
92
|
+
* @returns Search results with matching AST nodes
|
|
93
|
+
*/
|
|
94
|
+
async searchPattern(options: ASTSearchOptions): Promise<ASTSearchResult> {
|
|
95
|
+
const {
|
|
96
|
+
pattern,
|
|
97
|
+
mode = SearchMode.SIMPLE,
|
|
98
|
+
filePattern,
|
|
99
|
+
maxResults = 100,
|
|
100
|
+
} = options
|
|
101
|
+
|
|
102
|
+
const astPattern = new ASTPattern(pattern, mode)
|
|
103
|
+
|
|
104
|
+
// Get all files to search (async directory traversal)
|
|
105
|
+
const files = await this.getMatchingFiles(filePattern)
|
|
106
|
+
|
|
107
|
+
// Debug logging for MCP issues
|
|
108
|
+
const logger = getAstLogger()
|
|
109
|
+
logger.debug('File search results', {
|
|
110
|
+
fileCount: files.length,
|
|
111
|
+
repoPath: this.repoPath,
|
|
112
|
+
filePattern,
|
|
113
|
+
firstFiles: files.slice(0, 3),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Process files in parallel chunks using core utility
|
|
117
|
+
const matches = await processInParallelChunks({
|
|
118
|
+
items: files,
|
|
119
|
+
chunkSize: PARALLEL_CHUNK_SIZE,
|
|
120
|
+
maxResults,
|
|
121
|
+
processor: (filePath) => this.searchFile(filePath, astPattern),
|
|
122
|
+
onError: (filePath, error) => {
|
|
123
|
+
logger.error('Error parsing file', {
|
|
124
|
+
filePath,
|
|
125
|
+
error: error.message,
|
|
126
|
+
})
|
|
127
|
+
return []
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
count: matches.length,
|
|
133
|
+
matches,
|
|
134
|
+
pattern,
|
|
135
|
+
mode,
|
|
136
|
+
path: this.repoPath,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get all files matching the pattern.
|
|
142
|
+
*
|
|
143
|
+
* @param filePattern - Optional glob pattern to filter files
|
|
144
|
+
* @returns Array of absolute file paths
|
|
145
|
+
*/
|
|
146
|
+
private async getMatchingFiles(filePattern?: string): Promise<string[]> {
|
|
147
|
+
const files: string[] = []
|
|
148
|
+
await this.walkDirectory(this.repoPath, files, filePattern)
|
|
149
|
+
return files
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Recursively walk directory and collect matching files.
|
|
154
|
+
* Uses async fs methods to avoid blocking the event loop.
|
|
155
|
+
*/
|
|
156
|
+
private async walkDirectory(
|
|
157
|
+
dir: string,
|
|
158
|
+
files: string[],
|
|
159
|
+
filePattern?: string,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
const logger = getAstLogger()
|
|
162
|
+
|
|
163
|
+
let entries: string[]
|
|
164
|
+
try {
|
|
165
|
+
entries = await readdir(dir)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Directory doesn't exist or can't be read
|
|
168
|
+
logger.warn('Failed to read directory', {
|
|
169
|
+
dir,
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
})
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Process entries in parallel for faster traversal
|
|
176
|
+
await Promise.all(
|
|
177
|
+
entries.map(async (entry) => {
|
|
178
|
+
// Skip hidden and ignored directories
|
|
179
|
+
if (SKIP_DIRS.has(entry) || entry.startsWith('.')) return
|
|
180
|
+
|
|
181
|
+
const fullPath = join(dir, entry)
|
|
182
|
+
|
|
183
|
+
let stats: Awaited<ReturnType<typeof stat>>
|
|
184
|
+
try {
|
|
185
|
+
stats = await stat(fullPath)
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.debug('Failed to stat entry', {
|
|
188
|
+
path: fullPath,
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
})
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (stats.isDirectory()) {
|
|
195
|
+
await this.walkDirectory(fullPath, files, filePattern)
|
|
196
|
+
} else if (stats.isFile()) {
|
|
197
|
+
// Skip large files
|
|
198
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
199
|
+
logger.warn('Skipping large file', {
|
|
200
|
+
path: fullPath,
|
|
201
|
+
size: stats.size,
|
|
202
|
+
maxSize: MAX_FILE_SIZE,
|
|
203
|
+
})
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if file is supported for parsing
|
|
208
|
+
if (!isSupported(fullPath)) return
|
|
209
|
+
|
|
210
|
+
// Apply file pattern filter if provided
|
|
211
|
+
if (filePattern && !this.matchesFilePattern(fullPath, filePattern)) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
files.push(fullPath)
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if a file matches the given pattern.
|
|
223
|
+
* Uses Bun.Glob for proper glob pattern matching.
|
|
224
|
+
*/
|
|
225
|
+
private matchesFilePattern(filePath: string, pattern: string): boolean {
|
|
226
|
+
const relativePath = relative(this.repoPath, filePath)
|
|
227
|
+
|
|
228
|
+
// Use Bun.Glob for proper glob matching
|
|
229
|
+
const glob = new Bun.Glob(pattern)
|
|
230
|
+
return glob.match(relativePath)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Search a single file for pattern matches.
|
|
235
|
+
*
|
|
236
|
+
* @param filePath - Absolute path to the file
|
|
237
|
+
* @param pattern - AST pattern to match
|
|
238
|
+
* @returns Array of matches found in the file
|
|
239
|
+
*/
|
|
240
|
+
private async searchFile(
|
|
241
|
+
filePath: string,
|
|
242
|
+
pattern: ASTPattern,
|
|
243
|
+
): Promise<ASTMatch[]> {
|
|
244
|
+
const language = detectLanguage(filePath)
|
|
245
|
+
if (!language) return []
|
|
246
|
+
|
|
247
|
+
const parser = await getParser(language)
|
|
248
|
+
const source = await readFile(filePath, 'utf8')
|
|
249
|
+
const tree = parser.parse(source)
|
|
250
|
+
|
|
251
|
+
// Handle parse failure
|
|
252
|
+
if (!tree) return []
|
|
253
|
+
|
|
254
|
+
const matches: ASTMatch[] = []
|
|
255
|
+
this.searchNode(tree.rootNode, source, pattern, filePath, matches)
|
|
256
|
+
return matches
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Recursively search AST nodes for pattern matches.
|
|
261
|
+
*/
|
|
262
|
+
private searchNode(
|
|
263
|
+
node: SyntaxNode,
|
|
264
|
+
source: string,
|
|
265
|
+
pattern: ASTPattern,
|
|
266
|
+
filePath: string,
|
|
267
|
+
matches: ASTMatch[],
|
|
268
|
+
): void {
|
|
269
|
+
// Check if this node matches the pattern
|
|
270
|
+
if (pattern.matches(node, source)) {
|
|
271
|
+
const match = this.createMatch(node, source, filePath)
|
|
272
|
+
matches.push(match)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Recurse into children
|
|
276
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
277
|
+
const child = node.child(i)
|
|
278
|
+
if (child) {
|
|
279
|
+
this.searchNode(child, source, pattern, filePath, matches)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Create an ASTMatch from a matching node.
|
|
286
|
+
*/
|
|
287
|
+
private createMatch(
|
|
288
|
+
node: SyntaxNode,
|
|
289
|
+
source: string,
|
|
290
|
+
filePath: string,
|
|
291
|
+
): ASTMatch {
|
|
292
|
+
return {
|
|
293
|
+
file: relative(this.repoPath, filePath),
|
|
294
|
+
line: node.startPosition.row + 1,
|
|
295
|
+
column: node.startPosition.column,
|
|
296
|
+
nodeType: node.type,
|
|
297
|
+
text: this.truncateText(source.substring(node.startIndex, node.endIndex)),
|
|
298
|
+
context: this.getContext(node, source),
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get context information about a matched node.
|
|
304
|
+
*/
|
|
305
|
+
private getContext(node: SyntaxNode, source: string): ASTMatchContext {
|
|
306
|
+
const context: ASTMatchContext = { nodeType: node.type }
|
|
307
|
+
|
|
308
|
+
// Walk up the tree to find parent function or class
|
|
309
|
+
let parent = node.parent
|
|
310
|
+
while (parent) {
|
|
311
|
+
if (this.isFunctionNode(parent.type)) {
|
|
312
|
+
context.parentFunction = this.getNodeName(parent, source)
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
if (this.isClassNode(parent.type)) {
|
|
316
|
+
context.parentClass = this.getNodeName(parent, source)
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
parent = parent.parent
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return context
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get the name of a named node (function, class, etc.).
|
|
327
|
+
*/
|
|
328
|
+
private getNodeName(node: SyntaxNode, source: string): string | undefined {
|
|
329
|
+
// Try named children first
|
|
330
|
+
const nameChild = node.childForFieldName('name')
|
|
331
|
+
if (nameChild) {
|
|
332
|
+
return source.substring(nameChild.startIndex, nameChild.endIndex)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Look for identifier children
|
|
336
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
337
|
+
const child = node.child(i)
|
|
338
|
+
if (
|
|
339
|
+
child &&
|
|
340
|
+
(child.type === 'identifier' || child.type === 'type_identifier')
|
|
341
|
+
) {
|
|
342
|
+
return source.substring(child.startIndex, child.endIndex)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return undefined
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if a node type is a function.
|
|
351
|
+
*/
|
|
352
|
+
private isFunctionNode(type: string): boolean {
|
|
353
|
+
return [
|
|
354
|
+
'function_declaration',
|
|
355
|
+
'function_definition',
|
|
356
|
+
'function_expression',
|
|
357
|
+
'arrow_function',
|
|
358
|
+
'method_definition',
|
|
359
|
+
'generator_function_declaration',
|
|
360
|
+
].includes(type)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check if a node type is a class.
|
|
365
|
+
*/
|
|
366
|
+
private isClassNode(type: string): boolean {
|
|
367
|
+
return [
|
|
368
|
+
'class_declaration',
|
|
369
|
+
'class_definition',
|
|
370
|
+
'class_expression',
|
|
371
|
+
].includes(type)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Truncate text to maximum length.
|
|
376
|
+
*/
|
|
377
|
+
private truncateText(text: string): string {
|
|
378
|
+
if (text.length <= MAX_TEXT_LENGTH) return text
|
|
379
|
+
return `${text.slice(0, MAX_TEXT_LENGTH - 3)}...`
|
|
380
|
+
}
|
|
381
|
+
}
|