@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.
- package/README.md +12 -3
- package/package.json +1 -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 +272 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/adr-manager.ts +5 -4
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-writer.ts +3 -2
- package/src/contract/lock-compiler.ts +34 -0
- package/src/contract/lock-reader.ts +62 -5
- package/src/contract/schema.ts +10 -0
- package/src/index.ts +14 -1
- package/src/parser/error-recovery.ts +646 -0
- package/src/parser/index.ts +330 -74
- package/src/parser/oxc-parser.ts +3 -2
- package/src/parser/tree-sitter/parser.ts +59 -9
- package/src/parser/tree-sitter/queries.ts +27 -0
- package/src/parser/types.ts +1 -1
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +176 -0
- package/src/utils/atomic-write.ts +131 -0
- package/src/utils/fs.ts +76 -25
- package/src/utils/language-registry.ts +95 -0
- package/src/utils/minimatch.ts +49 -6
- package/tests/adr-manager.test.ts +6 -0
- package/tests/artifact-transaction.test.ts +73 -0
- package/tests/contract.test.ts +12 -0
- package/tests/dead-code.test.ts +12 -0
- package/tests/esm-resolver.test.ts +6 -0
- package/tests/fs.test.ts +22 -1
- package/tests/fuzzy-match.test.ts +6 -0
- package/tests/go-parser.test.ts +7 -0
- package/tests/graph.test.ts +10 -0
- package/tests/hash.test.ts +6 -0
- package/tests/impact-classified.test.ts +13 -0
- package/tests/js-parser.test.ts +10 -0
- package/tests/language-registry.test.ts +64 -0
- package/tests/parse-diagnostics.test.ts +115 -0
- package/tests/parser.test.ts +36 -0
- package/tests/tree-sitter-parser.test.ts +201 -0
- package/tests/ts-parser.test.ts +6 -0
package/src/parser/index.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
case '
|
|
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 '
|
|
112
|
+
case 'go':
|
|
45
113
|
return new GoParser()
|
|
46
|
-
case '
|
|
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
|
-
|
|
121
|
+
let _treeSitterParserInstance: BaseParser | null = null
|
|
65
122
|
|
|
66
123
|
const createTreeSitterParser = (): BaseParser => {
|
|
67
|
-
|
|
68
|
-
|
|
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 ['
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
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
|
|
162
|
-
const
|
|
163
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
...
|
|
215
|
-
...
|
|
437
|
+
...resolvedOxcFiles,
|
|
438
|
+
...resolvedGoFiles,
|
|
216
439
|
...resolvedTreeFiles,
|
|
440
|
+
...fallbackFiles,
|
|
217
441
|
]
|
|
218
442
|
|
|
219
|
-
|
|
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
|
}
|
package/src/parser/oxc-parser.ts
CHANGED
|
@@ -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'
|
|
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(
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|