@getmikk/core 2.0.14 → 2.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +2 -1
- package/src/analysis/type-flow.ts +1 -1
- package/src/cache/incremental-cache.ts +86 -80
- package/src/contract/contract-reader.ts +1 -0
- package/src/contract/lock-compiler.ts +95 -13
- package/src/contract/schema.ts +2 -0
- package/src/error-handler.ts +2 -1
- package/src/graph/cluster-detector.ts +2 -4
- package/src/graph/dead-code-detector.ts +303 -117
- package/src/graph/graph-builder.ts +21 -161
- package/src/graph/impact-analyzer.ts +1 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/rich-function-index.ts +1080 -0
- package/src/graph/symbol-table.ts +252 -0
- package/src/hash/hash-store.ts +1 -0
- package/src/index.ts +2 -0
- package/src/parser/base-extractor.ts +19 -0
- package/src/parser/boundary-checker.ts +31 -12
- package/src/parser/error-recovery.ts +5 -4
- package/src/parser/function-body-extractor.ts +248 -0
- package/src/parser/go/go-extractor.ts +249 -676
- package/src/parser/index.ts +132 -318
- package/src/parser/language-registry.ts +57 -0
- package/src/parser/oxc-parser.ts +166 -28
- package/src/parser/oxc-resolver.ts +179 -11
- package/src/parser/parser-constants.ts +1 -0
- package/src/parser/rust/rust-extractor.ts +109 -0
- package/src/parser/tree-sitter/parser.ts +369 -62
- package/src/parser/tree-sitter/queries.ts +106 -10
- package/src/parser/types.ts +20 -1
- package/src/search/bm25.ts +21 -8
- package/src/search/direct-search.ts +472 -0
- package/src/search/embedding-provider.ts +249 -0
- package/src/search/index.ts +12 -0
- package/src/search/semantic-search.ts +435 -0
- package/src/utils/artifact-transaction.ts +1 -0
- package/src/utils/atomic-write.ts +1 -0
- package/src/utils/errors.ts +89 -4
- package/src/utils/fs.ts +104 -50
- package/src/utils/json.ts +1 -0
- package/src/utils/language-registry.ts +84 -6
- package/src/utils/path.ts +26 -0
- package/tests/dead-code.test.ts +3 -2
- package/tests/direct-search.test.ts +435 -0
- package/tests/error-recovery.test.ts +143 -0
- package/tests/fixtures/simple-api/src/index.ts +1 -1
- package/tests/go-parser.test.ts +19 -335
- package/tests/js-parser.test.ts +18 -1089
- package/tests/language-registry-all.test.ts +276 -0
- package/tests/language-registry.test.ts +6 -4
- package/tests/parse-diagnostics.test.ts +9 -96
- package/tests/parser.test.ts +42 -771
- package/tests/polyglot-parser.test.ts +117 -0
- package/tests/rich-function-index.test.ts +703 -0
- package/tests/tree-sitter-parser.test.ts +108 -80
- package/tests/ts-parser.test.ts +8 -8
- package/tests/verification.test.ts +175 -0
- package/src/parser/base-parser.ts +0 -16
- package/src/parser/go/go-parser.ts +0 -43
- package/src/parser/javascript/js-extractor.ts +0 -278
- package/src/parser/javascript/js-parser.ts +0 -101
- package/src/parser/typescript/ts-extractor.ts +0 -447
- package/src/parser/typescript/ts-parser.ts +0 -36
package/src/utils/errors.ts
CHANGED
|
@@ -2,12 +2,25 @@ export class MikkError extends Error {
|
|
|
2
2
|
constructor(message: string, public code: string) {
|
|
3
3
|
super(message)
|
|
4
4
|
this.name = 'MikkError'
|
|
5
|
+
Error.captureStackTrace?.(this, this.constructor)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
toJSON() {
|
|
9
|
+
return {
|
|
10
|
+
name: this.name,
|
|
11
|
+
message: this.message,
|
|
12
|
+
code: this.code,
|
|
13
|
+
stack: this.stack,
|
|
14
|
+
}
|
|
5
15
|
}
|
|
6
16
|
}
|
|
7
17
|
|
|
8
18
|
export class ParseError extends MikkError {
|
|
9
|
-
constructor(file: string, cause: string) {
|
|
10
|
-
|
|
19
|
+
constructor(file: string, cause: string | Error) {
|
|
20
|
+
const message = cause instanceof Error
|
|
21
|
+
? `Failed to parse ${file}: ${cause.message}`
|
|
22
|
+
: `Failed to parse ${file}: ${cause}`
|
|
23
|
+
super(message, 'PARSE_ERROR')
|
|
11
24
|
}
|
|
12
25
|
}
|
|
13
26
|
|
|
@@ -18,8 +31,11 @@ export class ContractNotFoundError extends MikkError {
|
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
export class LockNotFoundError extends MikkError {
|
|
21
|
-
constructor() {
|
|
22
|
-
|
|
34
|
+
constructor(path?: string) {
|
|
35
|
+
const msg = path
|
|
36
|
+
? `No mikk.lock.json found at ${path}. Run 'mikk analyze' first.`
|
|
37
|
+
: `No mikk.lock.json found. Run 'mikk analyze' first.`
|
|
38
|
+
super(msg, 'LOCK_NOT_FOUND')
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
41
|
|
|
@@ -40,3 +56,72 @@ export class SyncStateError extends MikkError {
|
|
|
40
56
|
super(`Mikk is in ${status} state. Run 'mikk analyze' to sync.`, 'SYNC_STATE_ERROR')
|
|
41
57
|
}
|
|
42
58
|
}
|
|
59
|
+
|
|
60
|
+
export class EmbeddingError extends MikkError {
|
|
61
|
+
constructor(message: string, cause?: Error) {
|
|
62
|
+
const fullMessage = cause
|
|
63
|
+
? `${message}: ${cause.message}`
|
|
64
|
+
: message
|
|
65
|
+
super(fullMessage, 'EMBEDDING_ERROR')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class SearchError extends MikkError {
|
|
70
|
+
constructor(message: string, cause?: Error) {
|
|
71
|
+
const fullMessage = cause
|
|
72
|
+
? `${message}: ${cause.message}`
|
|
73
|
+
: message
|
|
74
|
+
super(fullMessage, 'SEARCH_ERROR')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ValidationError extends MikkError {
|
|
79
|
+
constructor(message: string) {
|
|
80
|
+
super(message, 'VALIDATION_ERROR')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class ConfigurationError extends MikkError {
|
|
85
|
+
constructor(message: string) {
|
|
86
|
+
super(message, 'CONFIGURATION_ERROR')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class TimeoutError extends MikkError {
|
|
91
|
+
constructor(operation: string, timeoutMs: number) {
|
|
92
|
+
super(`Operation '${operation}' timed out after ${timeoutMs}ms`, 'TIMEOUT')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class CacheError extends MikkError {
|
|
97
|
+
constructor(message: string, cause?: Error) {
|
|
98
|
+
const fullMessage = cause
|
|
99
|
+
? `Cache error: ${message}: ${cause.message}`
|
|
100
|
+
: `Cache error: ${message}`
|
|
101
|
+
super(fullMessage, 'CACHE_ERROR')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isMikkError(error: unknown): error is MikkError {
|
|
106
|
+
return error instanceof MikkError
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getErrorCode(error: unknown): string {
|
|
110
|
+
if (error instanceof MikkError) {
|
|
111
|
+
return error.code
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
return error.name.toUpperCase().replace(/\s+/g, '_')
|
|
115
|
+
}
|
|
116
|
+
return 'UNKNOWN'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatError(error: unknown): string {
|
|
120
|
+
if (isMikkError(error)) {
|
|
121
|
+
return `[${error.code}] ${error.message}`
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof Error) {
|
|
124
|
+
return `${error.name}: ${error.message}`
|
|
125
|
+
}
|
|
126
|
+
return String(error)
|
|
127
|
+
}
|
package/src/utils/fs.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import * as fs from 'node:fs/promises'
|
|
2
3
|
import * as path from 'node:path'
|
|
3
4
|
import fg from 'fast-glob'
|
|
@@ -184,7 +185,22 @@ export function parseMikkIgnore(content: string): string[] {
|
|
|
184
185
|
* This is technology-agnostic: it works for Prisma, Drizzle, GraphQL, SQL,
|
|
185
186
|
* Protobuf, Docker, OpenAPI, and more -- anything with a well-known file pattern.
|
|
186
187
|
*/
|
|
187
|
-
|
|
188
|
+
|
|
189
|
+
export interface DiscoverContextFilesOptions {
|
|
190
|
+
/** Maximum number of context files to return (default 20) */
|
|
191
|
+
maxFiles?: number
|
|
192
|
+
/** Callback for progress updates */
|
|
193
|
+
onProgress?: (current: number, total: number, file: string) => void
|
|
194
|
+
/** Skip reading file content - just get file list with stats */
|
|
195
|
+
metadataOnly?: boolean
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function discoverContextFiles(
|
|
199
|
+
projectRoot: string,
|
|
200
|
+
options: DiscoverContextFilesOptions = {}
|
|
201
|
+
): Promise<ContextFile[]> {
|
|
202
|
+
const { maxFiles = 20, onProgress, metadataOnly = false } = options
|
|
203
|
+
|
|
188
204
|
const mikkIgnore = await readMikkIgnore(projectRoot)
|
|
189
205
|
const files = await fg(CONTEXT_FILE_PATTERNS, {
|
|
190
206
|
cwd: projectRoot,
|
|
@@ -194,29 +210,49 @@ export async function discoverContextFiles(projectRoot: string): Promise<Context
|
|
|
194
210
|
})
|
|
195
211
|
|
|
196
212
|
const normalised = files.map(f => f.replace(/\\/g, '/'))
|
|
197
|
-
|
|
198
|
-
// Deduplicate -- some patterns overlap (e.g. models/*.ts also matched by source discovery)
|
|
199
213
|
const unique = [...new Set(normalised)]
|
|
200
214
|
|
|
201
215
|
const results: ContextFile[] = []
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
const batchSize = 10
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < unique.length; i += batchSize) {
|
|
219
|
+
const batch = unique.slice(i, i + batchSize)
|
|
220
|
+
|
|
221
|
+
const batchResults = await Promise.all(
|
|
222
|
+
batch.map(async (relPath) => {
|
|
223
|
+
const absPath = path.join(projectRoot, relPath)
|
|
224
|
+
try {
|
|
225
|
+
const stat = await fs.stat(absPath)
|
|
226
|
+
if (stat.size > MAX_CONTEXT_FILE_SIZE) return null
|
|
227
|
+
if (stat.size === 0) return null
|
|
228
|
+
|
|
229
|
+
const type = inferContextFileType(relPath)
|
|
230
|
+
|
|
231
|
+
if (onProgress) {
|
|
232
|
+
onProgress(results.length + 1, Math.min(unique.length, maxFiles), relPath)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (metadataOnly) {
|
|
236
|
+
return { path: relPath, content: '', type, size: stat.size }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const content = await fs.readFile(absPath, 'utf-8')
|
|
240
|
+
return { path: relPath, content, type, size: stat.size }
|
|
241
|
+
} catch {
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
for (const result of batchResults) {
|
|
248
|
+
if (result && results.length < maxFiles) {
|
|
249
|
+
results.push(result)
|
|
250
|
+
}
|
|
216
251
|
}
|
|
252
|
+
|
|
253
|
+
if (results.length >= maxFiles) break
|
|
217
254
|
}
|
|
218
255
|
|
|
219
|
-
// Sort: schemas/models first, then types, routes, config
|
|
220
256
|
const priority: Record<ContextFileType, number> = {
|
|
221
257
|
schema: 0,
|
|
222
258
|
model: 1,
|
|
@@ -229,9 +265,6 @@ export async function discoverContextFiles(projectRoot: string): Promise<Context
|
|
|
229
265
|
}
|
|
230
266
|
results.sort((a, b) => priority[a.type] - priority[b.type])
|
|
231
267
|
|
|
232
|
-
// If we have a schema file (e.g. prisma/schema.prisma), the migrations
|
|
233
|
-
// are redundant -- they represent historical deltas, not the current state.
|
|
234
|
-
// Including them wastes AI tokens and can be actively misleading.
|
|
235
268
|
const hasSchema = results.some(f => f.type === 'schema')
|
|
236
269
|
if (hasSchema) {
|
|
237
270
|
return results.filter(f => f.type !== 'migration')
|
|
@@ -287,19 +320,52 @@ export async function detectProjectLanguage(projectRoot: string): Promise<Projec
|
|
|
287
320
|
const matches = await fg(pattern, { cwd: projectRoot, onlyFiles: true, deep: 1 })
|
|
288
321
|
return matches.length > 0
|
|
289
322
|
}
|
|
323
|
+
|
|
324
|
+
const hasTsConfig = await exists('tsconfig.json') || await hasGlob('tsconfig.*.json')
|
|
325
|
+
const hasPackageJson = await exists('package.json')
|
|
326
|
+
const hasRust = await exists('Cargo.toml')
|
|
327
|
+
const hasGo = await exists('go.mod')
|
|
328
|
+
const hasPython = await exists('pyproject.toml') || await exists('setup.py') || await exists('requirements.txt')
|
|
329
|
+
const hasRuby = await exists('Gemfile')
|
|
330
|
+
const hasJava = await exists('pom.xml') || await exists('build.gradle') || await exists('build.gradle.kts')
|
|
331
|
+
const hasSwift = await exists('Package.swift')
|
|
332
|
+
const hasPhp = await exists('composer.json')
|
|
333
|
+
const hasCSharp = await hasGlob('*.csproj') || await hasGlob('*.sln')
|
|
334
|
+
const hasCpp = await hasGlob('CMakeLists.txt') || await hasGlob('**/*.cmake')
|
|
335
|
+
const hasC = await hasGlob('*.c') || await hasGlob('*.h')
|
|
336
|
+
|
|
337
|
+
// Count non-JS family manifests (TypeScript and JavaScript share package.json, so count them together)
|
|
338
|
+
let languageFamilyCount = 0
|
|
339
|
+
if (hasTsConfig || hasPackageJson) languageFamilyCount++ // JS family (TS or JS)
|
|
340
|
+
if (hasRust) languageFamilyCount++
|
|
341
|
+
if (hasGo) languageFamilyCount++
|
|
342
|
+
if (hasPython) languageFamilyCount++
|
|
343
|
+
if (hasRuby) languageFamilyCount++
|
|
344
|
+
if (hasJava) languageFamilyCount++
|
|
345
|
+
if (hasSwift) languageFamilyCount++
|
|
346
|
+
if (hasPhp) languageFamilyCount++
|
|
347
|
+
if (hasCSharp) languageFamilyCount++
|
|
348
|
+
if (hasCpp) languageFamilyCount++
|
|
349
|
+
if (hasC) languageFamilyCount++
|
|
350
|
+
|
|
351
|
+
// If multiple language families detected, it's polyglot
|
|
352
|
+
if (languageFamilyCount > 1) {
|
|
353
|
+
return 'polyglot'
|
|
354
|
+
}
|
|
355
|
+
|
|
290
356
|
// Check in priority order -- most specific first
|
|
291
|
-
if (
|
|
292
|
-
if (
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
295
|
-
if (
|
|
296
|
-
if (
|
|
297
|
-
if (
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
300
|
-
if (
|
|
301
|
-
if (
|
|
302
|
-
if (
|
|
357
|
+
if (hasTsConfig) return 'typescript'
|
|
358
|
+
if (hasRust) return 'rust'
|
|
359
|
+
if (hasGo) return 'go'
|
|
360
|
+
if (hasPython) return 'python'
|
|
361
|
+
if (hasRuby) return 'ruby'
|
|
362
|
+
if (hasJava) return 'java'
|
|
363
|
+
if (hasSwift) return 'swift'
|
|
364
|
+
if (hasPhp) return 'php'
|
|
365
|
+
if (hasCSharp) return 'csharp'
|
|
366
|
+
if (hasCpp) return 'cpp'
|
|
367
|
+
if (hasC) return 'c'
|
|
368
|
+
if (hasPackageJson) return 'javascript'
|
|
303
369
|
return 'unknown'
|
|
304
370
|
}
|
|
305
371
|
|
|
@@ -320,12 +386,12 @@ export function getDiscoveryPatterns(language: ProjectLanguage): { patterns: str
|
|
|
320
386
|
switch (language) {
|
|
321
387
|
case 'typescript':
|
|
322
388
|
return {
|
|
323
|
-
patterns: toPatterns(language),
|
|
389
|
+
patterns: [...toPatterns(language), '**/*.js', '**/*.jsx'],
|
|
324
390
|
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/.nuxt/**', '**/.svelte-kit/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}', '**/venv/**', '**/.venv/**'],
|
|
325
391
|
}
|
|
326
392
|
case 'javascript':
|
|
327
393
|
return {
|
|
328
|
-
patterns: toPatterns(language),
|
|
394
|
+
patterns: [...toPatterns(language), '**/*.ts', '**/*.tsx'],
|
|
329
395
|
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}', '**/venv/**', '**/.venv/**'],
|
|
330
396
|
}
|
|
331
397
|
case 'python':
|
|
@@ -345,7 +411,7 @@ export function getDiscoveryPatterns(language: ProjectLanguage): { patterns: str
|
|
|
345
411
|
}
|
|
346
412
|
case 'java':
|
|
347
413
|
return {
|
|
348
|
-
patterns: toPatterns(language),
|
|
414
|
+
patterns: [...toPatterns(language), '**/*.kt', '**/*.kts'],
|
|
349
415
|
ignore: [...commonIgnore, '**/target/**', '**/.gradle/**', '**/Test*.java', '**/*Test.java'],
|
|
350
416
|
}
|
|
351
417
|
case 'swift':
|
|
@@ -448,29 +514,17 @@ export async function fileExists(filePath: string): Promise<boolean> {
|
|
|
448
514
|
|
|
449
515
|
/**
|
|
450
516
|
* Set up the .mikk directory structure in a project root.
|
|
517
|
+
* Only creates directories that are actually used.
|
|
451
518
|
*/
|
|
452
519
|
export async function setupMikkDirectory(projectRoot: string): Promise<void> {
|
|
453
520
|
const dirs = [
|
|
454
521
|
'.mikk',
|
|
455
|
-
'.mikk/fragments',
|
|
456
|
-
'.mikk/diagrams',
|
|
457
|
-
'.mikk/diagrams/modules',
|
|
458
|
-
'.mikk/diagrams/capsules',
|
|
459
|
-
'.mikk/diagrams/flows',
|
|
460
|
-
'.mikk/diagrams/impact',
|
|
461
|
-
'.mikk/diagrams/exposure',
|
|
462
|
-
'.mikk/intent',
|
|
463
522
|
'.mikk/cache',
|
|
523
|
+
'.mikk/transactions',
|
|
464
524
|
]
|
|
465
525
|
for (const dir of dirs) {
|
|
466
526
|
await fs.mkdir(path.join(projectRoot, dir), { recursive: true })
|
|
467
527
|
}
|
|
468
|
-
|
|
469
|
-
// Create .gitkeep in impact dir
|
|
470
|
-
const impactKeep = path.join(projectRoot, '.mikk/diagrams/impact/.gitkeep')
|
|
471
|
-
if (!await fileExists(impactKeep)) {
|
|
472
|
-
await fs.writeFile(impactKeep, '', 'utf-8')
|
|
473
|
-
}
|
|
474
528
|
}
|
|
475
529
|
|
|
476
530
|
// --- .mikkignore auto-generation --------------------------------------------
|
package/src/utils/json.ts
CHANGED
|
@@ -14,15 +14,38 @@ export type RegistryLanguage =
|
|
|
14
14
|
| 'csharp'
|
|
15
15
|
| 'c'
|
|
16
16
|
| 'cpp'
|
|
17
|
+
| 'zig'
|
|
18
|
+
| 'elixir'
|
|
19
|
+
| 'haskell'
|
|
20
|
+
| 'scala'
|
|
21
|
+
| 'dart'
|
|
22
|
+
| 'lua'
|
|
23
|
+
| 'julia'
|
|
24
|
+
| 'clojure'
|
|
25
|
+
| 'fsharp'
|
|
26
|
+
| 'ocaml'
|
|
27
|
+
| 'perl'
|
|
28
|
+
| 'r'
|
|
29
|
+
| 'sql'
|
|
30
|
+
| 'terraform'
|
|
31
|
+
| 'shell'
|
|
32
|
+
| 'vue'
|
|
33
|
+
| 'svelte'
|
|
34
|
+
| 'jsx'
|
|
35
|
+
| 'tsx'
|
|
17
36
|
| 'polyglot'
|
|
18
37
|
| 'unknown'
|
|
19
38
|
|
|
20
|
-
const OXC_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'] as const
|
|
39
|
+
const OXC_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte'] as const
|
|
21
40
|
const GO_EXTENSIONS = ['.go'] as const
|
|
22
41
|
const TREE_SITTER_EXTENSIONS = [
|
|
23
42
|
'.py', '.java', '.kt', '.kts', '.swift',
|
|
24
43
|
'.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh',
|
|
25
44
|
'.cs', '.rs', '.php', '.rb',
|
|
45
|
+
'.zig', '.ex', '.exs', '.hs', '.scala', '.sc',
|
|
46
|
+
'.dart', '.lua', '.jl', '.clj', '.cljs', '.fs', '.fsx',
|
|
47
|
+
'.ml', '.mli', '.pl', '.pm', '.r', '.R', '.sql',
|
|
48
|
+
'.tf', '.sh', '.bash', '.zsh',
|
|
26
49
|
] as const
|
|
27
50
|
|
|
28
51
|
const PARSER_EXTENSIONS: Record<Exclude<ParserKind, 'unknown'>, readonly string[]> = {
|
|
@@ -32,22 +55,41 @@ const PARSER_EXTENSIONS: Record<Exclude<ParserKind, 'unknown'>, readonly string[
|
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
const LANGUAGE_EXTENSIONS: Record<RegistryLanguage, readonly string[]> = {
|
|
35
|
-
typescript: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
|
36
|
-
javascript: ['.js', '.jsx', '.mjs', '.cjs'
|
|
37
|
-
|
|
58
|
+
typescript: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'],
|
|
59
|
+
javascript: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
60
|
+
vue: ['.vue'],
|
|
61
|
+
svelte: ['.svelte'],
|
|
62
|
+
jsx: ['.jsx'],
|
|
63
|
+
tsx: ['.tsx'],
|
|
64
|
+
python: ['.py', '.pyw'],
|
|
38
65
|
go: ['.go'],
|
|
39
66
|
rust: ['.rs'],
|
|
40
67
|
kotlin: ['.kt', '.kts'],
|
|
41
|
-
java: ['.java'
|
|
68
|
+
java: ['.java'],
|
|
42
69
|
swift: ['.swift'],
|
|
43
70
|
ruby: ['.rb'],
|
|
44
71
|
php: ['.php'],
|
|
45
72
|
csharp: ['.cs'],
|
|
46
73
|
c: ['.c', '.h'],
|
|
47
74
|
cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh', '.h'],
|
|
75
|
+
zig: ['.zig'],
|
|
76
|
+
elixir: ['.ex', '.exs'],
|
|
77
|
+
haskell: ['.hs'],
|
|
78
|
+
scala: ['.scala', '.sc'],
|
|
79
|
+
dart: ['.dart'],
|
|
80
|
+
lua: ['.lua'],
|
|
81
|
+
julia: ['.jl'],
|
|
82
|
+
clojure: ['.clj', '.cljs', '.cljc'],
|
|
83
|
+
fsharp: ['.fs', '.fsx', '.fsi'],
|
|
84
|
+
ocaml: ['.ml', '.mli'],
|
|
85
|
+
perl: ['.pl', '.pm'],
|
|
86
|
+
r: ['.r', '.R'],
|
|
87
|
+
sql: ['.sql'],
|
|
88
|
+
terraform: ['.tf'],
|
|
89
|
+
shell: ['.sh', '.bash', '.zsh'],
|
|
48
90
|
polyglot: [
|
|
49
91
|
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
50
|
-
'.py',
|
|
92
|
+
'.py', '.vue', '.svelte',
|
|
51
93
|
'.go',
|
|
52
94
|
'.rs',
|
|
53
95
|
'.java', '.kt', '.kts',
|
|
@@ -56,6 +98,10 @@ const LANGUAGE_EXTENSIONS: Record<RegistryLanguage, readonly string[]> = {
|
|
|
56
98
|
'.php',
|
|
57
99
|
'.cs',
|
|
58
100
|
'.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh',
|
|
101
|
+
'.zig', '.ex', '.exs', '.hs', '.scala', '.sc',
|
|
102
|
+
'.dart', '.lua', '.jl', '.clj', '.cljs', '.cljc',
|
|
103
|
+
'.fs', '.fsx', '.fsi', '.ml', '.mli', '.pl', '.pm',
|
|
104
|
+
'.r', '.R', '.sql', '.tf', '.sh', '.bash', '.zsh',
|
|
59
105
|
],
|
|
60
106
|
unknown: ['.ts', '.tsx', '.js', '.jsx'],
|
|
61
107
|
}
|
|
@@ -82,6 +128,38 @@ export function languageForExtension(ext: string): RegistryLanguage {
|
|
|
82
128
|
return EXT_TO_LANGUAGE.get(ext.toLowerCase()) ?? 'unknown'
|
|
83
129
|
}
|
|
84
130
|
|
|
131
|
+
const VALID_PARSED_FILE_LANGUAGES = new Set([
|
|
132
|
+
// Mainstream Languages (22)
|
|
133
|
+
'javascript', 'typescript', 'python', 'java', 'csharp', 'cpp', 'c',
|
|
134
|
+
'php', 'ruby', 'swift', 'go', 'kotlin', 'rust', 'dart', 'scala',
|
|
135
|
+
'haskell', 'elixir', 'clojure', 'fsharp', 'ocaml', 'perl', 'r',
|
|
136
|
+
// Systems Languages
|
|
137
|
+
'zig',
|
|
138
|
+
// Scripting Languages
|
|
139
|
+
'lua', 'julia',
|
|
140
|
+
// Special Purpose
|
|
141
|
+
'sql', 'terraform', 'shell',
|
|
142
|
+
// Web Frameworks
|
|
143
|
+
'vue', 'svelte',
|
|
144
|
+
// Fallback
|
|
145
|
+
'unknown'
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
export function toParsedFileLanguage(lang: RegistryLanguage): ParsedFileLanguage {
|
|
149
|
+
return VALID_PARSED_FILE_LANGUAGES.has(lang)
|
|
150
|
+
? lang as ParsedFileLanguage
|
|
151
|
+
: 'unknown'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type ParsedFileLanguage =
|
|
155
|
+
| 'javascript' | 'typescript' | 'python' | 'java' | 'csharp' | 'cpp' | 'c'
|
|
156
|
+
| 'php' | 'ruby' | 'swift' | 'go' | 'kotlin' | 'rust' | 'dart' | 'scala'
|
|
157
|
+
| 'haskell' | 'elixir' | 'clojure' | 'fsharp' | 'ocaml' | 'perl' | 'r'
|
|
158
|
+
| 'zig' | 'lua' | 'julia'
|
|
159
|
+
| 'sql' | 'terraform' | 'shell'
|
|
160
|
+
| 'vue' | 'svelte'
|
|
161
|
+
| 'unknown'
|
|
162
|
+
|
|
85
163
|
export function getParserExtensions(kind: Exclude<ParserKind, 'unknown'>): readonly string[] {
|
|
86
164
|
return PARSER_EXTENSIONS[kind]
|
|
87
165
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function normalizeSlashes(filePath: string): string {
|
|
2
|
+
return filePath.replace(/\\/g, '/')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function normalizePath(filePath: string, lowercase: boolean = true): string {
|
|
6
|
+
const normalized = normalizeSlashes(filePath)
|
|
7
|
+
return lowercase ? normalized.toLowerCase() : normalized
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizePathQuiet(filePath: string): string {
|
|
11
|
+
return normalizeSlashes(filePath).toLowerCase()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getPathKey(filePath: string): string {
|
|
15
|
+
return normalizePath(filePath, true)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function pathsEqual(a: string, b: string): boolean {
|
|
19
|
+
return normalizePathQuiet(a) === normalizePathQuiet(b)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isSubPath(child: string, parent: string): boolean {
|
|
23
|
+
const childNorm = normalizePathQuiet(child)
|
|
24
|
+
const parentNorm = normalizePathQuiet(parent)
|
|
25
|
+
return childNorm.startsWith(parentNorm + '/') || childNorm === parentNorm
|
|
26
|
+
}
|
package/tests/dead-code.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { DeadCodeDetector } from '../src/graph/dead-code-detector'
|
|
3
|
-
import { buildTestGraph
|
|
4
|
-
import { GraphBuilder } from '../src/graph/graph-builder'
|
|
3
|
+
import { buildTestGraph } from './helpers'
|
|
5
4
|
import type { MikkLock } from '../src/contract/schema'
|
|
6
5
|
|
|
6
|
+
const _GraphBuilder = { addNode: () => {}, addEdge: () => {}, build: () => new Map() }
|
|
7
|
+
|
|
7
8
|
/** Helper to generate a dummy lock file from graph nodes for the detector */
|
|
8
9
|
function generateDummyLock(graphNodes: Map<string, any>): MikkLock {
|
|
9
10
|
const lock: MikkLock = {
|