@getmikk/core 2.0.12 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +12 -3
  2. package/package.json +1 -1
  3. package/src/analysis/index.ts +9 -0
  4. package/src/analysis/taint-analysis.ts +419 -0
  5. package/src/analysis/type-flow.ts +247 -0
  6. package/src/cache/incremental-cache.ts +272 -0
  7. package/src/cache/index.ts +1 -0
  8. package/src/contract/adr-manager.ts +5 -4
  9. package/src/contract/contract-generator.ts +31 -3
  10. package/src/contract/contract-writer.ts +3 -2
  11. package/src/contract/lock-compiler.ts +34 -0
  12. package/src/contract/lock-reader.ts +62 -5
  13. package/src/contract/schema.ts +10 -0
  14. package/src/index.ts +14 -1
  15. package/src/parser/error-recovery.ts +646 -0
  16. package/src/parser/index.ts +330 -74
  17. package/src/parser/oxc-parser.ts +3 -2
  18. package/src/parser/tree-sitter/parser.ts +59 -9
  19. package/src/parser/tree-sitter/queries.ts +27 -0
  20. package/src/parser/types.ts +1 -1
  21. package/src/security/index.ts +1 -0
  22. package/src/security/scanner.ts +342 -0
  23. package/src/utils/artifact-transaction.ts +176 -0
  24. package/src/utils/atomic-write.ts +131 -0
  25. package/src/utils/fs.ts +76 -25
  26. package/src/utils/language-registry.ts +95 -0
  27. package/src/utils/minimatch.ts +49 -6
  28. package/tests/adr-manager.test.ts +6 -0
  29. package/tests/artifact-transaction.test.ts +73 -0
  30. package/tests/contract.test.ts +12 -0
  31. package/tests/dead-code.test.ts +12 -0
  32. package/tests/esm-resolver.test.ts +6 -0
  33. package/tests/fs.test.ts +22 -1
  34. package/tests/fuzzy-match.test.ts +6 -0
  35. package/tests/go-parser.test.ts +7 -0
  36. package/tests/graph.test.ts +10 -0
  37. package/tests/hash.test.ts +6 -0
  38. package/tests/impact-classified.test.ts +13 -0
  39. package/tests/js-parser.test.ts +10 -0
  40. package/tests/language-registry.test.ts +64 -0
  41. package/tests/parse-diagnostics.test.ts +115 -0
  42. package/tests/parser.test.ts +36 -0
  43. package/tests/tree-sitter-parser.test.ts +201 -0
  44. package/tests/ts-parser.test.ts +6 -0
@@ -4,6 +4,15 @@ import { OxcParser } from './oxc-parser.js'
4
4
  import { GoParser } from './go/go-parser.js'
5
5
  import { UnsupportedLanguageError } from '../utils/errors.js'
6
6
  import type { ParsedFile } from './types.js'
7
+ import { hashContent } from '../hash/file-hasher.js'
8
+ import { IncrementalCache } from '../cache/incremental-cache.js'
9
+ import {
10
+ parserKindForExtension,
11
+ languageForExtension,
12
+ getParserExtensions,
13
+ isTreeSitterExtension,
14
+ type ParserKind,
15
+ } from '../utils/language-registry.js'
7
16
 
8
17
  export type {
9
18
  ParsedFile,
@@ -30,42 +39,93 @@ export { JavaScriptResolver } from './javascript/js-resolver.js'
30
39
  export { BoundaryChecker } from './boundary-checker.js'
31
40
  export { TreeSitterParser } from './tree-sitter/parser.js'
32
41
 
42
+ export type ParseDiagnosticStage = 'read' | 'parse' | 'resolve-imports'
43
+ export type ParseDiagnosticReason =
44
+ | 'read-error'
45
+ | 'parse-error'
46
+ | 'resolve-error'
47
+ | 'unsupported-extension'
48
+ | 'parser-unavailable'
49
+
50
+ export interface ParseDiagnostic {
51
+ filePath: string
52
+ extension: string
53
+ parser: ParserKind
54
+ stage: ParseDiagnosticStage
55
+ reason: ParseDiagnosticReason
56
+ message: string
57
+ }
58
+
59
+ export interface ParseFilesSummary {
60
+ requestedFiles: number
61
+ parsedFiles: number
62
+ fallbackFiles: number
63
+ unreadableFiles: number
64
+ unsupportedFiles: number
65
+ diagnostics: number
66
+ }
67
+
68
+ export interface ParseFilesResult {
69
+ files: ParsedFile[]
70
+ diagnostics: ParseDiagnostic[]
71
+ summary: ParseFilesSummary
72
+ }
73
+
74
+ const isLikelyParserUnavailable = (parser: ParserKind, message: string): boolean => {
75
+ if (parser !== 'tree-sitter') return false
76
+ const normalized = message.toLowerCase()
77
+ return normalized.includes('web-tree-sitter') ||
78
+ normalized.includes('tree-sitter') ||
79
+ normalized.includes('cannot find module')
80
+ }
81
+
82
+
83
+ const buildFallbackParsedFile = (filePath: string, content: string, ext: string): ParsedFile => ({
84
+ path: filePath,
85
+ language: languageForExtension(ext) as ParsedFile['language'],
86
+ functions: [],
87
+ classes: [],
88
+ generics: [],
89
+ imports: [],
90
+ exports: [],
91
+ routes: [],
92
+ variables: [],
93
+ calls: [],
94
+ hash: hashContent(content),
95
+ parsedAt: Date.now(),
96
+ })
97
+
98
+ const normalizeErrorMessage = (err: unknown): string => {
99
+ if (!err) return 'Unknown error'
100
+ if (err instanceof Error) return err.message
101
+ return String(err)
102
+ }
103
+
33
104
  /** Get the appropriate parser for a file based on its extension */
34
105
  export function getParser(filePath: string): BaseParser {
35
106
  const ext = nodePath.extname(filePath).toLowerCase()
36
- switch (ext) {
37
- case '.ts':
38
- case '.tsx':
39
- case '.js':
40
- case '.mjs':
41
- case '.cjs':
42
- case '.jsx':
107
+ const parserKind = parserKindForExtension(ext)
108
+
109
+ switch (parserKind) {
110
+ case 'oxc':
43
111
  return new OxcParser()
44
- case '.go':
112
+ case 'go':
45
113
  return new GoParser()
46
- case '.py':
47
- case '.java':
48
- case '.c':
49
- case '.h':
50
- case '.cpp':
51
- case '.cc':
52
- case '.hpp':
53
- case '.cs':
54
- case '.rs':
55
- case '.php':
56
- case '.rb':
57
- // Tree-sitter parser - dynamically imported to handle missing web-tree-sitter
114
+ case 'tree-sitter':
58
115
  return createTreeSitterParser()
59
116
  default:
60
- throw new UnsupportedLanguageError(ext)
117
+ throw new UnsupportedLanguageError(ext || '<no extension>')
61
118
  }
62
119
  }
63
120
 
64
- const _treeSitterParserInstance: BaseParser | null = null
121
+ let _treeSitterParserInstance: BaseParser | null = null
65
122
 
66
123
  const createTreeSitterParser = (): BaseParser => {
67
- // Return a lazy-loading wrapper that handles missing tree-sitter gracefully
68
- return new LazyTreeSitterParser()
124
+ if (!_treeSitterParserInstance) {
125
+ // Return a lazy-loading wrapper that handles missing tree-sitter gracefully.
126
+ _treeSitterParserInstance = new LazyTreeSitterParser()
127
+ }
128
+ return _treeSitterParserInstance
69
129
  }
70
130
 
71
131
  class LazyTreeSitterParser extends BaseParser {
@@ -96,26 +156,15 @@ class LazyTreeSitterParser extends BaseParser {
96
156
  }
97
157
 
98
158
  getSupportedExtensions(): string[] {
99
- return ['.py', '.java', '.c', '.h', '.cpp', '.cc', '.hpp', '.cs', '.rs', '.php', '.rb']
159
+ return [...getParserExtensions('tree-sitter')]
100
160
  }
101
161
 
102
162
  private buildEmptyFile(filePath: string, content: string): ParsedFile {
103
163
  const ext = nodePath.extname(filePath).toLowerCase()
104
- let lang: ParsedFile['language'] = 'unknown'
105
- switch (ext) {
106
- case '.py': lang = 'python'; break
107
- case '.java': lang = 'java'; break
108
- case '.c': case '.h': lang = 'c'; break
109
- case '.cpp': case '.cc': case '.hpp': lang = 'cpp'; break
110
- case '.cs': lang = 'csharp'; break
111
- case '.go': lang = 'go'; break
112
- case '.rs': lang = 'rust'; break
113
- case '.php': lang = 'php'; break
114
- case '.rb': lang = 'ruby'; break
115
- }
164
+ const lang = languageForExtension(ext)
116
165
  return {
117
166
  path: filePath,
118
- language: lang,
167
+ language: lang as ParsedFile['language'],
119
168
  functions: [],
120
169
  classes: [],
121
170
  generics: [],
@@ -124,97 +173,304 @@ class LazyTreeSitterParser extends BaseParser {
124
173
  routes: [],
125
174
  variables: [],
126
175
  calls: [],
127
- hash: '',
176
+ hash: hashContent(content),
128
177
  parsedAt: Date.now(),
129
178
  }
130
179
  }
131
180
  }
132
181
 
133
- /**
134
- * Parse multiple files, resolve their imports, and return ParsedFile[].
135
- *
136
- * Path contract (critical for graph correctness):
137
- * - filePaths come from discoverFiles() as project-root-relative strings
138
- * - We resolve them to ABSOLUTE posix paths before passing to parse()
139
- * - ParsedFile.path is therefore always absolute + forward-slash
140
- * - OxcResolver also returns absolute paths → import edges always consistent
141
- */
142
- export async function parseFiles(
182
+ export interface ParseFilesOptions {
183
+ strictParserPreflight?: boolean
184
+ treeSitterRuntimeAvailable?: boolean
185
+ }
186
+
187
+ async function isTreeSitterRuntimeAvailable(): Promise<boolean> {
188
+ try {
189
+ const { TreeSitterParser } = await import('./tree-sitter/parser.js')
190
+ const parser = new TreeSitterParser()
191
+ if (typeof (parser as any).isRuntimeAvailable !== 'function') {
192
+ return true
193
+ }
194
+ return await (parser as any).isRuntimeAvailable()
195
+ } catch {
196
+ return false
197
+ }
198
+ }
199
+
200
+ export async function parseFilesWithDiagnostics(
143
201
  filePaths: string[],
144
202
  projectRoot: string,
145
- readFile: (fp: string) => Promise<string>
146
- ): Promise<ParsedFile[]> {
147
- // Shared parser instances — avoid re-initialisation overhead per file
203
+ readFile: (fp: string) => Promise<string>,
204
+ options: ParseFilesOptions = {},
205
+ ): Promise<ParseFilesResult> {
206
+ // Shared parser instances — avoid re-initialisation overhead per file.
148
207
  const oxcParser = new OxcParser()
149
208
  const goParser = new GoParser()
150
209
 
151
- // Lazily loaded to avoid mandatory dep on tree-sitter
210
+ // Lazily loaded to avoid mandatory dependency on tree-sitter for TS/JS-only projects.
152
211
  let treeSitterParser: BaseParser | null = null
153
212
  const getTreeSitter = async (): Promise<BaseParser> => {
154
213
  if (!treeSitterParser) {
155
214
  const { TreeSitterParser } = await import('./tree-sitter/parser.js')
156
215
  treeSitterParser = new TreeSitterParser()
157
216
  }
158
- return treeSitterParser!
217
+ return treeSitterParser
159
218
  }
160
219
 
161
- const tsExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'])
162
- const goExtensions = new Set(['.go'])
163
- const treeSitterExtensions = new Set(['.py', '.java', '.c', '.h', '.cpp', '.cc', '.hpp', '.cs', '.rs', '.php', '.rb'])
220
+ const diagnostics: ParseDiagnostic[] = []
221
+ const addDiagnostic = (diagnostic: ParseDiagnostic) => diagnostics.push(diagnostic)
222
+
223
+ const treeSitterNeeded = filePaths.some(fp => {
224
+ const ext = nodePath.extname(fp).toLowerCase()
225
+ return isTreeSitterExtension(ext)
226
+ })
227
+
228
+ let treeSitterAvailable = true
229
+ if (treeSitterNeeded) {
230
+ treeSitterAvailable =
231
+ typeof options.treeSitterRuntimeAvailable === 'boolean'
232
+ ? options.treeSitterRuntimeAvailable
233
+ : await isTreeSitterRuntimeAvailable()
234
+ if (!treeSitterAvailable) {
235
+ addDiagnostic({
236
+ filePath: '*',
237
+ extension: '*',
238
+ parser: 'tree-sitter',
239
+ stage: 'parse',
240
+ reason: 'parser-unavailable',
241
+ message: 'Tree-sitter runtime unavailable. Install web-tree-sitter and language grammars.',
242
+ })
243
+ if (options.strictParserPreflight) {
244
+ return {
245
+ files: [],
246
+ diagnostics,
247
+ summary: {
248
+ requestedFiles: filePaths.length,
249
+ parsedFiles: 0,
250
+ fallbackFiles: 0,
251
+ unreadableFiles: 0,
252
+ unsupportedFiles: 0,
253
+ diagnostics: diagnostics.length,
254
+ },
255
+ }
256
+ }
257
+ }
258
+ }
164
259
 
165
- // Normalised project root for absolute path construction
260
+ // Initialize incremental cache
261
+ const cache = new IncrementalCache(projectRoot)
262
+
263
+ // Normalized project root for absolute path construction.
166
264
  const normalizedRoot = nodePath.resolve(projectRoot).replace(/\\/g, '/')
167
265
 
168
- // Group by parser to enable batch resolveImports
266
+ // Group by parser to enable batch resolveImports.
169
267
  const oxcFiles: ParsedFile[] = []
170
268
  const goFiles: ParsedFile[] = []
171
269
  const treeFiles: ParsedFile[] = []
270
+ const fallbackFiles: ParsedFile[] = []
271
+
272
+ let parsedFilesCount = 0
273
+ let fallbackFilesCount = 0
274
+ let unreadableFiles = 0
275
+ let unsupportedFiles = 0
172
276
 
173
277
  // Parse sequentially to avoid races in parser implementations that keep
174
278
  // mutable per-instance state (e.g. language switching/counters).
175
279
  for (const fp of filePaths) {
176
280
  const ext = nodePath.extname(fp).toLowerCase()
281
+ const parserKind = parserKindForExtension(ext)
177
282
 
178
- // Build absolute posix path — this is the single source of truth for all IDs
283
+ // Build absolute posix path — this is the single source of truth for all IDs.
179
284
  const absoluteFp = nodePath.resolve(normalizedRoot, fp).replace(/\\/g, '/')
180
285
 
181
286
  let content: string
182
287
  try {
183
288
  content = await readFile(absoluteFp)
184
- } catch {
185
- // File unreadable — skip silently (deleted, permission error, binary)
289
+ } catch (err: unknown) {
290
+ unreadableFiles += 1
291
+ addDiagnostic({
292
+ filePath: absoluteFp,
293
+ extension: ext,
294
+ parser: parserKind,
295
+ stage: 'read',
296
+ reason: 'read-error',
297
+ message: normalizeErrorMessage(err),
298
+ })
299
+ continue
300
+ }
301
+
302
+ if (parserKind === 'unknown') {
303
+ unsupportedFiles += 1
304
+ fallbackFilesCount += 1
305
+ fallbackFiles.push(buildFallbackParsedFile(absoluteFp, content, ext))
306
+ addDiagnostic({
307
+ filePath: absoluteFp,
308
+ extension: ext,
309
+ parser: parserKind,
310
+ stage: 'parse',
311
+ reason: 'unsupported-extension',
312
+ message: `Unsupported extension: ${ext || '<none>'}`,
313
+ })
186
314
  continue
187
315
  }
188
316
 
189
317
  try {
190
- if (tsExtensions.has(ext)) {
318
+ // Compute content hash for cache lookup
319
+ const contentHash = hashContent(content)
320
+
321
+ // Check cache first
322
+ const cached = await cache.get(absoluteFp, contentHash)
323
+ if (cached) {
324
+ // Cache hit — reuse parsed result
325
+ if (parserKind === 'oxc') {
326
+ oxcFiles.push(cached)
327
+ } else if (parserKind === 'go') {
328
+ goFiles.push(cached)
329
+ } else {
330
+ treeFiles.push(cached)
331
+ }
332
+ parsedFilesCount += 1
333
+ continue
334
+ }
335
+
336
+ // Cache miss — parse and store
337
+ if (parserKind === 'oxc') {
191
338
  const parsed = await oxcParser.parse(absoluteFp, content)
339
+ await cache.set(absoluteFp, contentHash, parsed)
192
340
  oxcFiles.push(parsed)
193
- } else if (goExtensions.has(ext)) {
341
+ parsedFilesCount += 1
342
+ } else if (parserKind === 'go') {
194
343
  const parsed = await goParser.parse(absoluteFp, content)
344
+ await cache.set(absoluteFp, contentHash, parsed)
195
345
  goFiles.push(parsed)
196
- } else if (treeSitterExtensions.has(ext)) {
346
+ parsedFilesCount += 1
347
+ } else {
348
+ if (!treeSitterAvailable) {
349
+ fallbackFilesCount += 1
350
+ fallbackFiles.push(buildFallbackParsedFile(absoluteFp, content, ext))
351
+ addDiagnostic({
352
+ filePath: absoluteFp,
353
+ extension: ext,
354
+ parser: 'tree-sitter',
355
+ stage: 'parse',
356
+ reason: 'parser-unavailable',
357
+ message: 'Tree-sitter runtime unavailable. Falling back to empty parsed file.',
358
+ })
359
+ continue
360
+ }
197
361
  const ts = await getTreeSitter()
198
362
  const parsed = await ts.parse(absoluteFp, content)
363
+ await cache.set(absoluteFp, contentHash, parsed)
199
364
  treeFiles.push(parsed)
365
+ parsedFilesCount += 1
200
366
  }
201
- } catch {
202
- // Parser error — skip this file, don't abort the whole run
367
+ } catch (err: unknown) {
368
+ fallbackFilesCount += 1
369
+ const message = normalizeErrorMessage(err)
370
+ const reason: ParseDiagnosticReason = isLikelyParserUnavailable(parserKind, message)
371
+ ? 'parser-unavailable'
372
+ : 'parse-error'
373
+
374
+ fallbackFiles.push(buildFallbackParsedFile(absoluteFp, content, ext))
375
+ addDiagnostic({
376
+ filePath: absoluteFp,
377
+ extension: ext,
378
+ parser: parserKind,
379
+ stage: 'parse',
380
+ reason,
381
+ message,
382
+ })
383
+ }
384
+ }
385
+
386
+ // Resolve imports batch-wise per parser (each has its own resolver).
387
+ let resolvedOxcFiles = oxcFiles
388
+ if (oxcFiles.length > 0) {
389
+ try {
390
+ resolvedOxcFiles = await oxcParser.resolveImports(oxcFiles, normalizedRoot)
391
+ } catch (err: unknown) {
392
+ addDiagnostic({
393
+ filePath: '*',
394
+ extension: '*',
395
+ parser: 'oxc',
396
+ stage: 'resolve-imports',
397
+ reason: 'resolve-error',
398
+ message: normalizeErrorMessage(err),
399
+ })
203
400
  }
204
401
  }
205
402
 
206
- // Resolve imports batch-wise per parser (each has its own resolver)
207
- let resolvedTreeFiles: ParsedFile[] = treeFiles
403
+ let resolvedGoFiles = goFiles
404
+ if (goFiles.length > 0) {
405
+ try {
406
+ resolvedGoFiles = await goParser.resolveImports(goFiles, normalizedRoot)
407
+ } catch (err: unknown) {
408
+ addDiagnostic({
409
+ filePath: '*',
410
+ extension: '*',
411
+ parser: 'go',
412
+ stage: 'resolve-imports',
413
+ reason: 'resolve-error',
414
+ message: normalizeErrorMessage(err),
415
+ })
416
+ }
417
+ }
418
+
419
+ let resolvedTreeFiles = treeFiles
208
420
  if (treeFiles.length > 0) {
209
- const treeParser = treeSitterParser ?? await getTreeSitter()
210
- resolvedTreeFiles = await treeParser.resolveImports(treeFiles, normalizedRoot)
421
+ try {
422
+ const treeParser = treeSitterParser ?? await getTreeSitter()
423
+ resolvedTreeFiles = await treeParser.resolveImports(treeFiles, normalizedRoot)
424
+ } catch (err: unknown) {
425
+ addDiagnostic({
426
+ filePath: '*',
427
+ extension: '*',
428
+ parser: 'tree-sitter',
429
+ stage: 'resolve-imports',
430
+ reason: 'resolve-error',
431
+ message: normalizeErrorMessage(err),
432
+ })
433
+ }
211
434
  }
212
435
 
213
436
  const resolved: ParsedFile[] = [
214
- ...await oxcParser.resolveImports(oxcFiles, normalizedRoot),
215
- ...await goParser.resolveImports(goFiles, normalizedRoot),
437
+ ...resolvedOxcFiles,
438
+ ...resolvedGoFiles,
216
439
  ...resolvedTreeFiles,
440
+ ...fallbackFiles,
217
441
  ]
218
442
 
219
- return resolved
443
+ // Persist cache metadata
444
+ cache.flush()
445
+
446
+ return {
447
+ files: resolved,
448
+ diagnostics,
449
+ summary: {
450
+ requestedFiles: filePaths.length,
451
+ parsedFiles: parsedFilesCount,
452
+ fallbackFiles: fallbackFilesCount,
453
+ unreadableFiles,
454
+ unsupportedFiles,
455
+ diagnostics: diagnostics.length,
456
+ },
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Parse multiple files, resolve their imports, and return ParsedFile[].
462
+ *
463
+ * Path contract (critical for graph correctness):
464
+ * - filePaths come from discoverFiles() as project-root-relative strings
465
+ * - We resolve them to ABSOLUTE posix paths before passing to parse()
466
+ * - ParsedFile.path is therefore always absolute + forward-slash
467
+ * - OxcResolver also returns absolute paths → import edges always consistent
468
+ */
469
+ export async function parseFiles(
470
+ filePaths: string[],
471
+ projectRoot: string,
472
+ readFile: (fp: string) => Promise<string>
473
+ ): Promise<ParsedFile[]> {
474
+ const result = await parseFilesWithDiagnostics(filePaths, projectRoot, readFile)
475
+ return result.files
220
476
  }
@@ -219,12 +219,13 @@ function extractCalls(node: any, lineIndex: LineIndex): CallExpression[] {
219
219
  const walk = (n: any): void => {
220
220
  if (!n || typeof n !== 'object') return;
221
221
 
222
- if (n.type === 'CallExpression' && n.span) {
222
+ if (n.type === 'CallExpression') {
223
223
  const { name, type } = resolveCallIdentity(n.callee);
224
224
  if (name) {
225
+ const span = getSpan(n);
225
226
  calls.push({
226
227
  name,
227
- line: lineIndex.getLine(n.span.start),
228
+ line: lineIndex.getLine(span.start),
228
229
  type,
229
230
  });
230
231
  }
@@ -36,6 +36,11 @@ function isExportedByLanguage(ext: string, name: string, nodeText: string): bool
36
36
  return !name.startsWith('_')
37
37
  case '.java':
38
38
  return /\bpublic\b/.test(nodeText)
39
+ case '.kt':
40
+ case '.kts':
41
+ return !/\bprivate\b/.test(nodeText) && !/\binternal\b/.test(nodeText) && !/\bprotected\b/.test(nodeText)
42
+ case '.swift':
43
+ return !/\bprivate\b/.test(nodeText) && !/\bfileprivate\b/.test(nodeText)
39
44
  case '.cs':
40
45
  return /\bpublic\b/.test(nodeText) && !/\binternal\b/.test(nodeText)
41
46
  case '.go':
@@ -181,7 +186,7 @@ export class TreeSitterParser extends BaseParser {
181
186
  private wasmLoadError = false
182
187
 
183
188
  getSupportedExtensions(): string[] {
184
- return ['.py', '.java', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.go', '.rs', '.php', '.rb']
189
+ return ['.py', '.java', '.kt', '.kts', '.swift', '.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.hh', '.cs', '.go', '.rs', '.php', '.rb']
185
190
  }
186
191
 
187
192
  private async init() {
@@ -193,6 +198,11 @@ export class TreeSitterParser extends BaseParser {
193
198
  }
194
199
  }
195
200
 
201
+ async isRuntimeAvailable(): Promise<boolean> {
202
+ await this.init()
203
+ return Boolean(this.parser)
204
+ }
205
+
196
206
  async parse(filePath: string, content: string): Promise<ParsedFile> {
197
207
  this.nameCounter.clear()
198
208
  await this.init()
@@ -509,13 +519,41 @@ export class TreeSitterParser extends BaseParser {
509
519
  try {
510
520
  const nameForFile = name.replace(/-/g, '_')
511
521
 
512
- // Try multiple possible WASM locations
513
- const possiblePaths = [
514
- path.resolve('node_modules/tree-sitter-wasms/out', `tree-sitter-${nameForFile}.wasm`),
515
- path.resolve('./node_modules/tree-sitter-wasms/out', `tree-sitter-${nameForFile}.wasm`),
516
- path.resolve(process.cwd(), 'node_modules/tree-sitter-wasms/out', `tree-sitter-${nameForFile}.wasm`),
517
- path.resolve(process.cwd(), 'node_modules', 'tree-sitter-wasms', 'out', `tree-sitter-${nameForFile}.wasm`),
518
- ]
522
+ // Try multiple possible WASM locations, including parent directories and siblings for monorepos
523
+ const baseDirs = new Set<string>()
524
+ baseDirs.add(process.cwd())
525
+
526
+ // Add parent directories (up to 4 levels) for monorepo setups
527
+ let current = process.cwd()
528
+ let parentDir = ''
529
+ for (let i = 0; i < 4; i++) {
530
+ parentDir = path.dirname(current)
531
+ if (parentDir === current) break
532
+ baseDirs.add(parentDir)
533
+ baseDirs.add(path.join(parentDir, 'node_modules'))
534
+
535
+ // Also check sibling directories in the parent for monorepo setups
536
+ // (e.g., metis and Mesh are siblings under the same parent)
537
+ try {
538
+ const fs = await import('node:fs')
539
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true })
540
+ for (const entry of entries) {
541
+ if (entry.isDirectory() && entry.name !== path.basename(current)) {
542
+ baseDirs.add(path.join(parentDir, entry.name, 'node_modules'))
543
+ }
544
+ }
545
+ } catch { /* skip */ }
546
+
547
+ current = parentDir
548
+ }
549
+
550
+ const possiblePaths: string[] = []
551
+ for (const baseDir of baseDirs) {
552
+ if (!baseDir) continue
553
+ possiblePaths.push(
554
+ path.join(baseDir, 'node_modules/tree-sitter-wasms/out', `tree-sitter-${nameForFile}.wasm`),
555
+ )
556
+ }
519
557
 
520
558
  let wasmPath = ''
521
559
  for (const p of possiblePaths) {
@@ -553,7 +591,6 @@ export class TreeSitterParser extends BaseParser {
553
591
 
554
592
  if (!wasmPath) {
555
593
  // WASM not found - but don't mark as permanent error, just skip this language
556
- console.warn(`Tree-sitter WASM not found for ${name}`)
557
594
  return null
558
595
  }
559
596
 
@@ -591,12 +628,19 @@ export class TreeSitterParser extends BaseParser {
591
628
  return { lang: await this.loadLang('python'), query: Queries.PYTHON_QUERIES }
592
629
  case '.java':
593
630
  return { lang: await this.loadLang('java'), query: Queries.JAVA_QUERIES }
631
+ case '.kt':
632
+ case '.kts':
633
+ return { lang: await this.loadLang('kotlin'), query: Queries.KOTLIN_QUERIES }
634
+ case '.swift':
635
+ return { lang: await this.loadLang('swift'), query: Queries.SWIFT_QUERIES }
594
636
  case '.c':
595
637
  case '.h':
596
638
  return { lang: await this.loadLang('c'), query: Queries.C_QUERIES }
597
639
  case '.cpp':
598
640
  case '.cc':
641
+ case '.cxx':
599
642
  case '.hpp':
643
+ case '.hxx':
600
644
  case '.hh':
601
645
  return { lang: await this.loadLang('cpp'), query: Queries.CPP_QUERIES }
602
646
  case '.cs':
@@ -619,8 +663,14 @@ function extensionToLanguage(ext: string): ParsedFile['language'] {
619
663
  switch (ext) {
620
664
  case '.py': return 'python'
621
665
  case '.java': return 'java'
666
+ case '.kt':
667
+ case '.kts':
668
+ return 'kotlin'
669
+ case '.swift':
670
+ return 'swift'
622
671
  case '.c': case '.h': return 'c'
623
672
  case '.cpp': case '.cc': case '.hpp': return 'cpp'
673
+ case '.cxx': case '.hxx': case '.hh': return 'cpp'
624
674
  case '.cs': return 'csharp'
625
675
  case '.go': return 'go'
626
676
  case '.rs': return 'rust'
@@ -65,6 +65,33 @@ export const JAVA_QUERIES = `
65
65
  (class_declaration name: (identifier) @heritage.class (super_interfaces (type_list (type_identifier) @heritage.implements))) @heritage.impl
66
66
  `;
67
67
 
68
+ export const KOTLIN_QUERIES = `
69
+ (class_declaration name: (type_identifier) @name) @definition.class
70
+ (object_declaration name: (type_identifier) @name) @definition.class
71
+ (function_declaration name: (simple_identifier) @name) @definition.function
72
+ (property_declaration (variable_declaration (simple_identifier) @name)) @definition.property
73
+ (type_alias (type_identifier) @name) @definition.type
74
+ (import_header (identifier) @import.source) @import
75
+ (call_expression (simple_identifier) @call.name) @call
76
+ (call_expression
77
+ (navigation_expression
78
+ (navigation_suffix (simple_identifier) @call.name))) @call
79
+ (constructor_invocation
80
+ (user_type (type_identifier) @call.name)) @call
81
+ `;
82
+
83
+ export const SWIFT_QUERIES = `
84
+ (class_declaration name: (type_identifier) @name) @definition.class
85
+ (protocol_declaration name: (type_identifier) @name) @definition.interface
86
+ (function_declaration name: (simple_identifier) @name) @definition.function
87
+ (property_declaration (pattern (simple_identifier) @name)) @definition.property
88
+ (import_declaration (identifier) @import.source) @import
89
+ (call_expression (simple_identifier) @call.name) @call
90
+ (call_expression
91
+ (navigation_expression
92
+ (navigation_suffix (simple_identifier) @call.name))) @call
93
+ `;
94
+
68
95
  export const C_QUERIES = `
69
96
  (function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function
70
97
  (declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function