@getmikk/core 2.0.11 → 2.0.13
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/contract/adr-manager.ts +5 -4
- package/src/contract/contract-generator.ts +0 -2
- package/src/contract/contract-writer.ts +3 -3
- package/src/contract/lock-compiler.ts +4 -3
- package/src/contract/lock-reader.ts +62 -5
- package/src/contract/schema.ts +8 -0
- package/src/index.ts +12 -1
- package/src/parser/index.ts +301 -74
- package/src/parser/oxc-parser.ts +3 -2
- package/src/parser/tree-sitter/parser.ts +24 -1
- package/src/parser/tree-sitter/queries.ts +27 -0
- package/src/parser/types.ts +1 -1
- package/src/utils/artifact-transaction.ts +176 -0
- package/src/utils/atomic-write.ts +131 -0
- package/src/utils/fs.ts +33 -13
- package/src/utils/language-registry.ts +82 -0
- 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,14 @@ 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 {
|
|
9
|
+
parserKindForExtension,
|
|
10
|
+
languageForExtension,
|
|
11
|
+
getParserExtensions,
|
|
12
|
+
isTreeSitterExtension,
|
|
13
|
+
type ParserKind,
|
|
14
|
+
} from '../utils/language-registry.js'
|
|
7
15
|
|
|
8
16
|
export type {
|
|
9
17
|
ParsedFile,
|
|
@@ -30,42 +38,93 @@ export { JavaScriptResolver } from './javascript/js-resolver.js'
|
|
|
30
38
|
export { BoundaryChecker } from './boundary-checker.js'
|
|
31
39
|
export { TreeSitterParser } from './tree-sitter/parser.js'
|
|
32
40
|
|
|
41
|
+
export type ParseDiagnosticStage = 'read' | 'parse' | 'resolve-imports'
|
|
42
|
+
export type ParseDiagnosticReason =
|
|
43
|
+
| 'read-error'
|
|
44
|
+
| 'parse-error'
|
|
45
|
+
| 'resolve-error'
|
|
46
|
+
| 'unsupported-extension'
|
|
47
|
+
| 'parser-unavailable'
|
|
48
|
+
|
|
49
|
+
export interface ParseDiagnostic {
|
|
50
|
+
filePath: string
|
|
51
|
+
extension: string
|
|
52
|
+
parser: ParserKind
|
|
53
|
+
stage: ParseDiagnosticStage
|
|
54
|
+
reason: ParseDiagnosticReason
|
|
55
|
+
message: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ParseFilesSummary {
|
|
59
|
+
requestedFiles: number
|
|
60
|
+
parsedFiles: number
|
|
61
|
+
fallbackFiles: number
|
|
62
|
+
unreadableFiles: number
|
|
63
|
+
unsupportedFiles: number
|
|
64
|
+
diagnostics: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ParseFilesResult {
|
|
68
|
+
files: ParsedFile[]
|
|
69
|
+
diagnostics: ParseDiagnostic[]
|
|
70
|
+
summary: ParseFilesSummary
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isLikelyParserUnavailable = (parser: ParserKind, message: string): boolean => {
|
|
74
|
+
if (parser !== 'tree-sitter') return false
|
|
75
|
+
const normalized = message.toLowerCase()
|
|
76
|
+
return normalized.includes('web-tree-sitter') ||
|
|
77
|
+
normalized.includes('tree-sitter') ||
|
|
78
|
+
normalized.includes('cannot find module')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
const buildFallbackParsedFile = (filePath: string, content: string, ext: string): ParsedFile => ({
|
|
83
|
+
path: filePath,
|
|
84
|
+
language: languageForExtension(ext) as ParsedFile['language'],
|
|
85
|
+
functions: [],
|
|
86
|
+
classes: [],
|
|
87
|
+
generics: [],
|
|
88
|
+
imports: [],
|
|
89
|
+
exports: [],
|
|
90
|
+
routes: [],
|
|
91
|
+
variables: [],
|
|
92
|
+
calls: [],
|
|
93
|
+
hash: hashContent(content),
|
|
94
|
+
parsedAt: Date.now(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const normalizeErrorMessage = (err: unknown): string => {
|
|
98
|
+
if (!err) return 'Unknown error'
|
|
99
|
+
if (err instanceof Error) return err.message
|
|
100
|
+
return String(err)
|
|
101
|
+
}
|
|
102
|
+
|
|
33
103
|
/** Get the appropriate parser for a file based on its extension */
|
|
34
104
|
export function getParser(filePath: string): BaseParser {
|
|
35
105
|
const ext = nodePath.extname(filePath).toLowerCase()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
case '
|
|
40
|
-
case '.mjs':
|
|
41
|
-
case '.cjs':
|
|
42
|
-
case '.jsx':
|
|
106
|
+
const parserKind = parserKindForExtension(ext)
|
|
107
|
+
|
|
108
|
+
switch (parserKind) {
|
|
109
|
+
case 'oxc':
|
|
43
110
|
return new OxcParser()
|
|
44
|
-
case '
|
|
111
|
+
case 'go':
|
|
45
112
|
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
|
|
113
|
+
case 'tree-sitter':
|
|
58
114
|
return createTreeSitterParser()
|
|
59
115
|
default:
|
|
60
|
-
throw new UnsupportedLanguageError(ext)
|
|
116
|
+
throw new UnsupportedLanguageError(ext || '<no extension>')
|
|
61
117
|
}
|
|
62
118
|
}
|
|
63
119
|
|
|
64
|
-
|
|
120
|
+
let _treeSitterParserInstance: BaseParser | null = null
|
|
65
121
|
|
|
66
122
|
const createTreeSitterParser = (): BaseParser => {
|
|
67
|
-
|
|
68
|
-
|
|
123
|
+
if (!_treeSitterParserInstance) {
|
|
124
|
+
// Return a lazy-loading wrapper that handles missing tree-sitter gracefully.
|
|
125
|
+
_treeSitterParserInstance = new LazyTreeSitterParser()
|
|
126
|
+
}
|
|
127
|
+
return _treeSitterParserInstance
|
|
69
128
|
}
|
|
70
129
|
|
|
71
130
|
class LazyTreeSitterParser extends BaseParser {
|
|
@@ -96,26 +155,15 @@ class LazyTreeSitterParser extends BaseParser {
|
|
|
96
155
|
}
|
|
97
156
|
|
|
98
157
|
getSupportedExtensions(): string[] {
|
|
99
|
-
return ['
|
|
158
|
+
return [...getParserExtensions('tree-sitter')]
|
|
100
159
|
}
|
|
101
160
|
|
|
102
161
|
private buildEmptyFile(filePath: string, content: string): ParsedFile {
|
|
103
162
|
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
|
-
}
|
|
163
|
+
const lang = languageForExtension(ext)
|
|
116
164
|
return {
|
|
117
165
|
path: filePath,
|
|
118
|
-
language: lang,
|
|
166
|
+
language: lang as ParsedFile['language'],
|
|
119
167
|
functions: [],
|
|
120
168
|
classes: [],
|
|
121
169
|
generics: [],
|
|
@@ -124,97 +172,276 @@ class LazyTreeSitterParser extends BaseParser {
|
|
|
124
172
|
routes: [],
|
|
125
173
|
variables: [],
|
|
126
174
|
calls: [],
|
|
127
|
-
hash:
|
|
175
|
+
hash: hashContent(content),
|
|
128
176
|
parsedAt: Date.now(),
|
|
129
177
|
}
|
|
130
178
|
}
|
|
131
179
|
}
|
|
132
180
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
export interface ParseFilesOptions {
|
|
182
|
+
strictParserPreflight?: boolean
|
|
183
|
+
treeSitterRuntimeAvailable?: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function isTreeSitterRuntimeAvailable(): Promise<boolean> {
|
|
187
|
+
try {
|
|
188
|
+
const { TreeSitterParser } = await import('./tree-sitter/parser.js')
|
|
189
|
+
const parser = new TreeSitterParser()
|
|
190
|
+
if (typeof (parser as any).isRuntimeAvailable !== 'function') {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
return await (parser as any).isRuntimeAvailable()
|
|
194
|
+
} catch {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function parseFilesWithDiagnostics(
|
|
143
200
|
filePaths: string[],
|
|
144
201
|
projectRoot: string,
|
|
145
|
-
readFile: (fp: string) => Promise<string
|
|
146
|
-
|
|
147
|
-
|
|
202
|
+
readFile: (fp: string) => Promise<string>,
|
|
203
|
+
options: ParseFilesOptions = {},
|
|
204
|
+
): Promise<ParseFilesResult> {
|
|
205
|
+
// Shared parser instances — avoid re-initialisation overhead per file.
|
|
148
206
|
const oxcParser = new OxcParser()
|
|
149
207
|
const goParser = new GoParser()
|
|
150
208
|
|
|
151
|
-
// Lazily loaded to avoid mandatory
|
|
209
|
+
// Lazily loaded to avoid mandatory dependency on tree-sitter for TS/JS-only projects.
|
|
152
210
|
let treeSitterParser: BaseParser | null = null
|
|
153
211
|
const getTreeSitter = async (): Promise<BaseParser> => {
|
|
154
212
|
if (!treeSitterParser) {
|
|
155
213
|
const { TreeSitterParser } = await import('./tree-sitter/parser.js')
|
|
156
214
|
treeSitterParser = new TreeSitterParser()
|
|
157
215
|
}
|
|
158
|
-
return treeSitterParser
|
|
216
|
+
return treeSitterParser
|
|
159
217
|
}
|
|
160
218
|
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
219
|
+
const diagnostics: ParseDiagnostic[] = []
|
|
220
|
+
const addDiagnostic = (diagnostic: ParseDiagnostic) => diagnostics.push(diagnostic)
|
|
221
|
+
|
|
222
|
+
const treeSitterNeeded = filePaths.some(fp => {
|
|
223
|
+
const ext = nodePath.extname(fp).toLowerCase()
|
|
224
|
+
return isTreeSitterExtension(ext)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
let treeSitterAvailable = true
|
|
228
|
+
if (treeSitterNeeded) {
|
|
229
|
+
treeSitterAvailable =
|
|
230
|
+
typeof options.treeSitterRuntimeAvailable === 'boolean'
|
|
231
|
+
? options.treeSitterRuntimeAvailable
|
|
232
|
+
: await isTreeSitterRuntimeAvailable()
|
|
233
|
+
if (!treeSitterAvailable) {
|
|
234
|
+
addDiagnostic({
|
|
235
|
+
filePath: '*',
|
|
236
|
+
extension: '*',
|
|
237
|
+
parser: 'tree-sitter',
|
|
238
|
+
stage: 'parse',
|
|
239
|
+
reason: 'parser-unavailable',
|
|
240
|
+
message: 'Tree-sitter runtime unavailable. Install web-tree-sitter and language grammars.',
|
|
241
|
+
})
|
|
242
|
+
if (options.strictParserPreflight) {
|
|
243
|
+
return {
|
|
244
|
+
files: [],
|
|
245
|
+
diagnostics,
|
|
246
|
+
summary: {
|
|
247
|
+
requestedFiles: filePaths.length,
|
|
248
|
+
parsedFiles: 0,
|
|
249
|
+
fallbackFiles: 0,
|
|
250
|
+
unreadableFiles: 0,
|
|
251
|
+
unsupportedFiles: 0,
|
|
252
|
+
diagnostics: diagnostics.length,
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
164
258
|
|
|
165
|
-
//
|
|
259
|
+
// Normalized project root for absolute path construction.
|
|
166
260
|
const normalizedRoot = nodePath.resolve(projectRoot).replace(/\\/g, '/')
|
|
167
261
|
|
|
168
|
-
// Group by parser to enable batch resolveImports
|
|
262
|
+
// Group by parser to enable batch resolveImports.
|
|
169
263
|
const oxcFiles: ParsedFile[] = []
|
|
170
264
|
const goFiles: ParsedFile[] = []
|
|
171
265
|
const treeFiles: ParsedFile[] = []
|
|
266
|
+
const fallbackFiles: ParsedFile[] = []
|
|
267
|
+
|
|
268
|
+
let parsedFilesCount = 0
|
|
269
|
+
let fallbackFilesCount = 0
|
|
270
|
+
let unreadableFiles = 0
|
|
271
|
+
let unsupportedFiles = 0
|
|
172
272
|
|
|
173
273
|
// Parse sequentially to avoid races in parser implementations that keep
|
|
174
274
|
// mutable per-instance state (e.g. language switching/counters).
|
|
175
275
|
for (const fp of filePaths) {
|
|
176
276
|
const ext = nodePath.extname(fp).toLowerCase()
|
|
277
|
+
const parserKind = parserKindForExtension(ext)
|
|
177
278
|
|
|
178
|
-
// Build absolute posix path — this is the single source of truth for all IDs
|
|
279
|
+
// Build absolute posix path — this is the single source of truth for all IDs.
|
|
179
280
|
const absoluteFp = nodePath.resolve(normalizedRoot, fp).replace(/\\/g, '/')
|
|
180
281
|
|
|
181
282
|
let content: string
|
|
182
283
|
try {
|
|
183
284
|
content = await readFile(absoluteFp)
|
|
184
|
-
} catch {
|
|
185
|
-
|
|
285
|
+
} catch (err: unknown) {
|
|
286
|
+
unreadableFiles += 1
|
|
287
|
+
addDiagnostic({
|
|
288
|
+
filePath: absoluteFp,
|
|
289
|
+
extension: ext,
|
|
290
|
+
parser: parserKind,
|
|
291
|
+
stage: 'read',
|
|
292
|
+
reason: 'read-error',
|
|
293
|
+
message: normalizeErrorMessage(err),
|
|
294
|
+
})
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (parserKind === 'unknown') {
|
|
299
|
+
unsupportedFiles += 1
|
|
300
|
+
fallbackFilesCount += 1
|
|
301
|
+
fallbackFiles.push(buildFallbackParsedFile(absoluteFp, content, ext))
|
|
302
|
+
addDiagnostic({
|
|
303
|
+
filePath: absoluteFp,
|
|
304
|
+
extension: ext,
|
|
305
|
+
parser: parserKind,
|
|
306
|
+
stage: 'parse',
|
|
307
|
+
reason: 'unsupported-extension',
|
|
308
|
+
message: `Unsupported extension: ${ext || '<none>'}`,
|
|
309
|
+
})
|
|
186
310
|
continue
|
|
187
311
|
}
|
|
188
312
|
|
|
189
313
|
try {
|
|
190
|
-
if (
|
|
314
|
+
if (parserKind === 'oxc') {
|
|
191
315
|
const parsed = await oxcParser.parse(absoluteFp, content)
|
|
192
316
|
oxcFiles.push(parsed)
|
|
193
|
-
|
|
317
|
+
parsedFilesCount += 1
|
|
318
|
+
} else if (parserKind === 'go') {
|
|
194
319
|
const parsed = await goParser.parse(absoluteFp, content)
|
|
195
320
|
goFiles.push(parsed)
|
|
196
|
-
|
|
321
|
+
parsedFilesCount += 1
|
|
322
|
+
} else {
|
|
323
|
+
if (!treeSitterAvailable) {
|
|
324
|
+
fallbackFilesCount += 1
|
|
325
|
+
fallbackFiles.push(buildFallbackParsedFile(absoluteFp, content, ext))
|
|
326
|
+
addDiagnostic({
|
|
327
|
+
filePath: absoluteFp,
|
|
328
|
+
extension: ext,
|
|
329
|
+
parser: 'tree-sitter',
|
|
330
|
+
stage: 'parse',
|
|
331
|
+
reason: 'parser-unavailable',
|
|
332
|
+
message: 'Tree-sitter runtime unavailable. Falling back to empty parsed file.',
|
|
333
|
+
})
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
197
336
|
const ts = await getTreeSitter()
|
|
198
337
|
const parsed = await ts.parse(absoluteFp, content)
|
|
199
338
|
treeFiles.push(parsed)
|
|
339
|
+
parsedFilesCount += 1
|
|
200
340
|
}
|
|
201
|
-
} catch {
|
|
202
|
-
|
|
341
|
+
} catch (err: unknown) {
|
|
342
|
+
fallbackFilesCount += 1
|
|
343
|
+
const message = normalizeErrorMessage(err)
|
|
344
|
+
const reason: ParseDiagnosticReason = isLikelyParserUnavailable(parserKind, message)
|
|
345
|
+
? 'parser-unavailable'
|
|
346
|
+
: 'parse-error'
|
|
347
|
+
|
|
348
|
+
fallbackFiles.push(buildFallbackParsedFile(absoluteFp, content, ext))
|
|
349
|
+
addDiagnostic({
|
|
350
|
+
filePath: absoluteFp,
|
|
351
|
+
extension: ext,
|
|
352
|
+
parser: parserKind,
|
|
353
|
+
stage: 'parse',
|
|
354
|
+
reason,
|
|
355
|
+
message,
|
|
356
|
+
})
|
|
203
357
|
}
|
|
204
358
|
}
|
|
205
359
|
|
|
206
|
-
// Resolve imports batch-wise per parser (each has its own resolver)
|
|
207
|
-
let
|
|
360
|
+
// Resolve imports batch-wise per parser (each has its own resolver).
|
|
361
|
+
let resolvedOxcFiles = oxcFiles
|
|
362
|
+
if (oxcFiles.length > 0) {
|
|
363
|
+
try {
|
|
364
|
+
resolvedOxcFiles = await oxcParser.resolveImports(oxcFiles, normalizedRoot)
|
|
365
|
+
} catch (err: unknown) {
|
|
366
|
+
addDiagnostic({
|
|
367
|
+
filePath: '*',
|
|
368
|
+
extension: '*',
|
|
369
|
+
parser: 'oxc',
|
|
370
|
+
stage: 'resolve-imports',
|
|
371
|
+
reason: 'resolve-error',
|
|
372
|
+
message: normalizeErrorMessage(err),
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let resolvedGoFiles = goFiles
|
|
378
|
+
if (goFiles.length > 0) {
|
|
379
|
+
try {
|
|
380
|
+
resolvedGoFiles = await goParser.resolveImports(goFiles, normalizedRoot)
|
|
381
|
+
} catch (err: unknown) {
|
|
382
|
+
addDiagnostic({
|
|
383
|
+
filePath: '*',
|
|
384
|
+
extension: '*',
|
|
385
|
+
parser: 'go',
|
|
386
|
+
stage: 'resolve-imports',
|
|
387
|
+
reason: 'resolve-error',
|
|
388
|
+
message: normalizeErrorMessage(err),
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let resolvedTreeFiles = treeFiles
|
|
208
394
|
if (treeFiles.length > 0) {
|
|
209
|
-
|
|
210
|
-
|
|
395
|
+
try {
|
|
396
|
+
const treeParser = treeSitterParser ?? await getTreeSitter()
|
|
397
|
+
resolvedTreeFiles = await treeParser.resolveImports(treeFiles, normalizedRoot)
|
|
398
|
+
} catch (err: unknown) {
|
|
399
|
+
addDiagnostic({
|
|
400
|
+
filePath: '*',
|
|
401
|
+
extension: '*',
|
|
402
|
+
parser: 'tree-sitter',
|
|
403
|
+
stage: 'resolve-imports',
|
|
404
|
+
reason: 'resolve-error',
|
|
405
|
+
message: normalizeErrorMessage(err),
|
|
406
|
+
})
|
|
407
|
+
}
|
|
211
408
|
}
|
|
212
409
|
|
|
213
410
|
const resolved: ParsedFile[] = [
|
|
214
|
-
...
|
|
215
|
-
...
|
|
411
|
+
...resolvedOxcFiles,
|
|
412
|
+
...resolvedGoFiles,
|
|
216
413
|
...resolvedTreeFiles,
|
|
414
|
+
...fallbackFiles,
|
|
217
415
|
]
|
|
218
416
|
|
|
219
|
-
return
|
|
417
|
+
return {
|
|
418
|
+
files: resolved,
|
|
419
|
+
diagnostics,
|
|
420
|
+
summary: {
|
|
421
|
+
requestedFiles: filePaths.length,
|
|
422
|
+
parsedFiles: parsedFilesCount,
|
|
423
|
+
fallbackFiles: fallbackFilesCount,
|
|
424
|
+
unreadableFiles,
|
|
425
|
+
unsupportedFiles,
|
|
426
|
+
diagnostics: diagnostics.length,
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Parse multiple files, resolve their imports, and return ParsedFile[].
|
|
433
|
+
*
|
|
434
|
+
* Path contract (critical for graph correctness):
|
|
435
|
+
* - filePaths come from discoverFiles() as project-root-relative strings
|
|
436
|
+
* - We resolve them to ABSOLUTE posix paths before passing to parse()
|
|
437
|
+
* - ParsedFile.path is therefore always absolute + forward-slash
|
|
438
|
+
* - OxcResolver also returns absolute paths → import edges always consistent
|
|
439
|
+
*/
|
|
440
|
+
export async function parseFiles(
|
|
441
|
+
filePaths: string[],
|
|
442
|
+
projectRoot: string,
|
|
443
|
+
readFile: (fp: string) => Promise<string>
|
|
444
|
+
): Promise<ParsedFile[]> {
|
|
445
|
+
const result = await parseFilesWithDiagnostics(filePaths, projectRoot, readFile)
|
|
446
|
+
return result.files
|
|
220
447
|
}
|
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()
|
|
@@ -591,12 +601,19 @@ export class TreeSitterParser extends BaseParser {
|
|
|
591
601
|
return { lang: await this.loadLang('python'), query: Queries.PYTHON_QUERIES }
|
|
592
602
|
case '.java':
|
|
593
603
|
return { lang: await this.loadLang('java'), query: Queries.JAVA_QUERIES }
|
|
604
|
+
case '.kt':
|
|
605
|
+
case '.kts':
|
|
606
|
+
return { lang: await this.loadLang('kotlin'), query: Queries.KOTLIN_QUERIES }
|
|
607
|
+
case '.swift':
|
|
608
|
+
return { lang: await this.loadLang('swift'), query: Queries.SWIFT_QUERIES }
|
|
594
609
|
case '.c':
|
|
595
610
|
case '.h':
|
|
596
611
|
return { lang: await this.loadLang('c'), query: Queries.C_QUERIES }
|
|
597
612
|
case '.cpp':
|
|
598
613
|
case '.cc':
|
|
614
|
+
case '.cxx':
|
|
599
615
|
case '.hpp':
|
|
616
|
+
case '.hxx':
|
|
600
617
|
case '.hh':
|
|
601
618
|
return { lang: await this.loadLang('cpp'), query: Queries.CPP_QUERIES }
|
|
602
619
|
case '.cs':
|
|
@@ -619,8 +636,14 @@ function extensionToLanguage(ext: string): ParsedFile['language'] {
|
|
|
619
636
|
switch (ext) {
|
|
620
637
|
case '.py': return 'python'
|
|
621
638
|
case '.java': return 'java'
|
|
639
|
+
case '.kt':
|
|
640
|
+
case '.kts':
|
|
641
|
+
return 'kotlin'
|
|
642
|
+
case '.swift':
|
|
643
|
+
return 'swift'
|
|
622
644
|
case '.c': case '.h': return 'c'
|
|
623
645
|
case '.cpp': case '.cc': case '.hpp': return 'cpp'
|
|
646
|
+
case '.cxx': case '.hxx': case '.hh': return 'cpp'
|
|
624
647
|
case '.cs': return 'csharp'
|
|
625
648
|
case '.go': return 'go'
|
|
626
649
|
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
|
package/src/parser/types.ts
CHANGED
|
@@ -116,7 +116,7 @@ export interface ParsedRoute {
|
|
|
116
116
|
/** Everything extracted from a single file */
|
|
117
117
|
export interface ParsedFile {
|
|
118
118
|
path: string; // normalized absolute path
|
|
119
|
-
language: 'python' | 'go' | 'typescript' | 'javascript' | 'java' | 'c' | 'cpp' | 'csharp' | 'rust' | 'php' | 'ruby' | 'unknown';
|
|
119
|
+
language: 'python' | 'go' | 'typescript' | 'javascript' | 'java' | 'kotlin' | 'swift' | 'c' | 'cpp' | 'csharp' | 'rust' | 'php' | 'ruby' | 'unknown';
|
|
120
120
|
functions: ParsedFunction[];
|
|
121
121
|
classes: ParsedClass[];
|
|
122
122
|
variables: ParsedVariable[];
|