@getmikk/core 2.0.13 → 2.0.15

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 (71) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -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 +278 -0
  7. package/src/cache/index.ts +1 -0
  8. package/src/contract/contract-generator.ts +31 -3
  9. package/src/contract/contract-reader.ts +1 -0
  10. package/src/contract/lock-compiler.ts +125 -12
  11. package/src/contract/schema.ts +4 -0
  12. package/src/error-handler.ts +2 -1
  13. package/src/graph/cluster-detector.ts +2 -4
  14. package/src/graph/dead-code-detector.ts +303 -117
  15. package/src/graph/graph-builder.ts +21 -161
  16. package/src/graph/impact-analyzer.ts +1 -0
  17. package/src/graph/index.ts +2 -0
  18. package/src/graph/rich-function-index.ts +1080 -0
  19. package/src/graph/symbol-table.ts +252 -0
  20. package/src/hash/hash-store.ts +1 -0
  21. package/src/index.ts +4 -0
  22. package/src/parser/base-extractor.ts +19 -0
  23. package/src/parser/boundary-checker.ts +31 -12
  24. package/src/parser/error-recovery.ts +647 -0
  25. package/src/parser/function-body-extractor.ts +248 -0
  26. package/src/parser/go/go-extractor.ts +249 -676
  27. package/src/parser/index.ts +138 -295
  28. package/src/parser/language-registry.ts +57 -0
  29. package/src/parser/oxc-parser.ts +166 -28
  30. package/src/parser/oxc-resolver.ts +179 -11
  31. package/src/parser/parser-constants.ts +1 -0
  32. package/src/parser/rust/rust-extractor.ts +109 -0
  33. package/src/parser/tree-sitter/parser.ts +400 -66
  34. package/src/parser/tree-sitter/queries.ts +106 -10
  35. package/src/parser/types.ts +20 -1
  36. package/src/search/bm25.ts +21 -8
  37. package/src/search/direct-search.ts +472 -0
  38. package/src/search/embedding-provider.ts +249 -0
  39. package/src/search/index.ts +12 -0
  40. package/src/search/semantic-search.ts +435 -0
  41. package/src/security/index.ts +1 -0
  42. package/src/security/scanner.ts +342 -0
  43. package/src/utils/artifact-transaction.ts +1 -0
  44. package/src/utils/atomic-write.ts +1 -0
  45. package/src/utils/errors.ts +89 -4
  46. package/src/utils/fs.ts +150 -65
  47. package/src/utils/json.ts +1 -0
  48. package/src/utils/language-registry.ts +96 -5
  49. package/src/utils/minimatch.ts +49 -6
  50. package/src/utils/path.ts +26 -0
  51. package/tests/dead-code.test.ts +3 -2
  52. package/tests/direct-search.test.ts +435 -0
  53. package/tests/error-recovery.test.ts +143 -0
  54. package/tests/fixtures/simple-api/src/index.ts +1 -1
  55. package/tests/go-parser.test.ts +19 -335
  56. package/tests/js-parser.test.ts +18 -1089
  57. package/tests/language-registry-all.test.ts +276 -0
  58. package/tests/language-registry.test.ts +6 -4
  59. package/tests/parse-diagnostics.test.ts +9 -96
  60. package/tests/parser.test.ts +42 -771
  61. package/tests/polyglot-parser.test.ts +117 -0
  62. package/tests/rich-function-index.test.ts +703 -0
  63. package/tests/tree-sitter-parser.test.ts +108 -80
  64. package/tests/ts-parser.test.ts +8 -8
  65. package/tests/verification.test.ts +175 -0
  66. package/src/parser/base-parser.ts +0 -16
  67. package/src/parser/go/go-parser.ts +0 -43
  68. package/src/parser/javascript/js-extractor.ts +0 -278
  69. package/src/parser/javascript/js-parser.ts +0 -101
  70. package/src/parser/typescript/ts-extractor.ts +0 -447
  71. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -0,0 +1,252 @@
1
+ import type { ParsedFile, ParsedImport } from '../parser/types.js'
2
+ import { normalizePathQuiet } from '../utils/path.js'
3
+
4
+ export interface SymbolDefinition {
5
+ id: string
6
+ name: string
7
+ type: 'function' | 'class' | 'variable' | 'interface' | 'type' | 'enum'
8
+ file: string
9
+ isExported: boolean
10
+ }
11
+
12
+ export interface ImportAlias {
13
+ localName: string
14
+ exportedName: string
15
+ sourcePath: string
16
+ }
17
+
18
+ /**
19
+ * GlobalSymbolTable — The brain of Mikk's semantic reasoning.
20
+ * Indexes all exported symbols and provides resolution logic for calls.
21
+ * Supports: named imports, default imports, aliased imports, local symbols, and fuzzy matching.
22
+ *
23
+ * Case sensitivity: Symbol names are case-sensitive for accurate resolution.
24
+ * Fallback to case-insensitive matching only when no exact match exists.
25
+ */
26
+ export class GlobalSymbolTable {
27
+ private exportsByFile = new Map<string, Map<string, SymbolDefinition>>()
28
+ private symbolsByName = new Map<string, SymbolDefinition[]>()
29
+ private importAliases: ImportAlias[] = []
30
+ private reexportsByFile = new Map<string, Map<string, string>>()
31
+ private resolveCache = new Map<string, string | null>()
32
+ private resolveCacheOrder: string[] = []
33
+ private importsByFile = new Map<string, ParsedImport[]>()
34
+
35
+ private static readonly MAX_RESOLVE_CACHE_SIZE = 10000
36
+
37
+ private cacheResolve(key: string, value: string | null): void {
38
+ if (this.resolveCache.size >= GlobalSymbolTable.MAX_RESOLVE_CACHE_SIZE) {
39
+ const oldestKey = this.resolveCacheOrder.shift()
40
+ if (oldestKey) this.resolveCache.delete(oldestKey)
41
+ }
42
+ this.resolveCache.set(key, value)
43
+ this.resolveCacheOrder.push(key)
44
+ }
45
+
46
+ register(file: ParsedFile): void {
47
+ const fileExports = new Map<string, SymbolDefinition>()
48
+ const filePath = normalizePathQuiet(file.path)
49
+ const fileReexports = new Map<string, string>()
50
+
51
+ const registerSymbol = (name: string, type: SymbolDefinition['type'], id: string, isExported: boolean) => {
52
+ const def: SymbolDefinition = { id, name, type, file: filePath, isExported }
53
+
54
+ if (isExported) {
55
+ fileExports.set(name, def)
56
+ }
57
+
58
+ const byName = this.symbolsByName.get(name) ?? []
59
+ byName.push(def)
60
+ this.symbolsByName.set(name, byName)
61
+ }
62
+
63
+ for (const fn of file.functions) registerSymbol(fn.name, 'function', fn.id, fn.isExported)
64
+ for (const cls of file.classes ?? []) {
65
+ registerSymbol(cls.name, 'class', cls.id, cls.isExported)
66
+ for (const m of cls.methods) {
67
+ const fullName = `${cls.name}.${m.name}`
68
+ registerSymbol(fullName, 'function', m.id, false)
69
+ const byName = this.symbolsByName.get(m.name) ?? []
70
+ byName.push({ id: m.id, name: m.name, type: 'function', file: filePath, isExported: false })
71
+ this.symbolsByName.set(m.name, byName)
72
+ }
73
+ }
74
+ for (const gen of file.generics ?? []) {
75
+ const validType = gen.type === 'interface' || gen.type === 'type' ? gen.type : 'type'
76
+ registerSymbol(gen.name, validType, gen.id, gen.isExported)
77
+ }
78
+ for (const v of file.variables ?? []) registerSymbol(v.name, 'variable', v.id, v.isExported)
79
+
80
+ for (const imp of file.imports) {
81
+ if (!imp.resolvedPath || imp.isDynamic) continue
82
+ const resolvedPath = normalizePathQuiet(imp.resolvedPath)
83
+ for (const spec of imp.specifiers ?? []) {
84
+ if (spec.imported && spec.local && spec.imported !== spec.local) {
85
+ this.importAliases.push({
86
+ localName: spec.local,
87
+ exportedName: spec.imported,
88
+ sourcePath: resolvedPath,
89
+ })
90
+ }
91
+ }
92
+ }
93
+
94
+ for (const re of file.reexports ?? []) {
95
+ if (re.sourceResolved) {
96
+ fileReexports.set(re.name, normalizePathQuiet(re.sourceResolved))
97
+ }
98
+ }
99
+
100
+ this.exportsByFile.set(filePath, fileExports)
101
+ if (fileReexports.size > 0) {
102
+ this.reexportsByFile.set(filePath, fileReexports)
103
+ }
104
+ const fileImports = file.imports.filter(i => i.resolvedPath && !i.isDynamic)
105
+ this.importsByFile.set(filePath, fileImports)
106
+ }
107
+
108
+ resolve(callName: string, contextFile: string, imports: ParsedImport[]): string | null {
109
+ const hasDot = callName.includes('.')
110
+ const receiver = hasDot ? callName.split('.')[0] : null
111
+ const simpleName = hasDot ? callName.split('.').pop()! : callName
112
+ const normalizedContextFile = normalizePathQuiet(contextFile)
113
+
114
+ const cacheKey = `${normalizedContextFile}:${callName}`
115
+ if (this.resolveCache.has(cacheKey)) {
116
+ return this.resolveCache.get(cacheKey)!
117
+ }
118
+
119
+ const importsToUse = this.importsByFile.get(normalizedContextFile) ?? imports.filter(i => i.resolvedPath && !i.isDynamic)
120
+ let result: string | null = null
121
+
122
+ for (const imp of importsToUse) {
123
+ if (!imp.resolvedPath) continue
124
+ const resolvedPath = normalizePathQuiet(imp.resolvedPath)
125
+
126
+ if (imp.isDynamic) continue
127
+
128
+ if (hasDot && receiver) {
129
+ if (imp.isDefault && imp.names.some(n => n === receiver || n.toLowerCase() === receiver.toLowerCase())) {
130
+ const target = this.getExport(resolvedPath, simpleName)
131
+ if (target) { result = target.id; break }
132
+ const methodTarget = this.getMethodByClassCaseInsensitive(resolvedPath, receiver, simpleName)
133
+ if (methodTarget) { result = methodTarget; break }
134
+ }
135
+
136
+ const alias = this.importAliases.find(
137
+ a => a.localName === receiver && a.sourcePath === resolvedPath
138
+ )
139
+ if (alias) {
140
+ const target = this.getExport(resolvedPath, alias.exportedName)
141
+ if (target) { result = target.id; break }
142
+ }
143
+
144
+ const receiverLower = receiver.toLowerCase()
145
+ const aliasLower = this.importAliases.find(
146
+ a => a.localName.toLowerCase() === receiverLower && a.sourcePath === resolvedPath
147
+ )
148
+ if (aliasLower) {
149
+ const target = this.getExport(resolvedPath, aliasLower.exportedName)
150
+ if (target) { result = target.id; break }
151
+ }
152
+
153
+ const methodTarget = this.getMethodByClassCaseInsensitive(resolvedPath, receiver, simpleName)
154
+ if (methodTarget) { result = methodTarget; break }
155
+ }
156
+
157
+ const matchedLocal = imp.names.find(n =>
158
+ n === callName ||
159
+ n === simpleName ||
160
+ n.toLowerCase() === callName.toLowerCase() ||
161
+ n.toLowerCase() === simpleName.toLowerCase() ||
162
+ this.importAliases.some(a => a.localName === n && (a.exportedName === callName || a.exportedName === simpleName)) ||
163
+ this.importAliases.some(a => a.localName.toLowerCase() === n.toLowerCase() && (a.exportedName.toLowerCase() === callName.toLowerCase() || a.exportedName.toLowerCase() === simpleName.toLowerCase()))
164
+ )
165
+ if (matchedLocal) {
166
+ const alias = this.importAliases.find(a => a.localName === matchedLocal)
167
+ const exportedName = alias ? alias.exportedName : simpleName
168
+ const target = this.getExport(resolvedPath, exportedName)
169
+ if (target) { result = target.id; break }
170
+ }
171
+ }
172
+
173
+ if (!result) {
174
+ const localTarget = this.getExport(normalizedContextFile, callName)
175
+ if (localTarget) { result = localTarget.id }
176
+ }
177
+
178
+ if (!result) {
179
+ const globalMatches = this.symbolsByName.get(callName) ?? this.symbolsByName.get(simpleName)
180
+ if (globalMatches?.length === 1) {
181
+ result = globalMatches[0].id
182
+ }
183
+ }
184
+
185
+ if (!result) {
186
+ const lowerCallName = callName.toLowerCase()
187
+ const lowerSimpleName = simpleName.toLowerCase()
188
+ const lowerGlobalMatches = this.symbolsByName.get(lowerCallName) ?? this.symbolsByName.get(lowerSimpleName)
189
+ if (lowerGlobalMatches?.length === 1) {
190
+ result = lowerGlobalMatches[0].id
191
+ }
192
+ }
193
+
194
+ if (!result && hasDot && receiver) {
195
+ const methodTarget = this.getMethodGlobally(receiver, simpleName)
196
+ if (methodTarget) { result = methodTarget }
197
+ }
198
+
199
+ this.cacheResolve(cacheKey, result)
200
+ return result
201
+ }
202
+
203
+ private getExport(filePath: string, name: string, visited: Set<string> = new Set()): SymbolDefinition | undefined {
204
+ if (visited.has(filePath + ':' + name)) return undefined
205
+ visited.add(filePath + ':' + name)
206
+
207
+ const exports = this.exportsByFile.get(filePath)
208
+ if (exports) {
209
+ const def = exports.get(name) ?? exports.get(name.toLowerCase())
210
+ if (def) return def
211
+ }
212
+
213
+ const reexports = this.reexportsByFile.get(filePath)
214
+ if (reexports) {
215
+ const reexportPath = reexports.get(name) ?? reexports.get(name.toLowerCase())
216
+ if (reexportPath) {
217
+ return this.getExport(reexportPath, name, visited)
218
+ }
219
+ }
220
+
221
+ return undefined
222
+ }
223
+
224
+ private getMethodByClassCaseInsensitive(filePath: string, className: string, methodName: string): string | null {
225
+ const exports = this.exportsByFile.get(filePath)
226
+ if (!exports) return null
227
+
228
+ for (const [name, def] of exports) {
229
+ if (def.type === 'class' && name.toLowerCase() === className.toLowerCase()) {
230
+ const methodFullName = `${name}.${methodName}`
231
+ const method = this.symbolsByName.get(methodFullName)
232
+ if (method?.length === 1) return method[0].id
233
+
234
+ const methodLowerName = `${name.toLowerCase()}.${methodName.toLowerCase()}`
235
+ const methodLower = this.symbolsByName.get(methodLowerName)
236
+ if (methodLower?.length === 1) return methodLower[0].id
237
+ }
238
+ }
239
+ return null
240
+ }
241
+
242
+ private getMethodGlobally(className: string, methodName: string): string | null {
243
+ for (const [name, defs] of this.symbolsByName) {
244
+ if (defs[0]?.type === 'class' && name.toLowerCase() === className.toLowerCase()) {
245
+ const methodFullName = `${name}.${methodName}`
246
+ const method = this.symbolsByName.get(methodFullName)
247
+ if (method?.length === 1) return method[0].id
248
+ }
249
+ }
250
+ return null
251
+ }
252
+ }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import * as path from 'node:path'
2
3
  import * as fs from 'node:fs'
3
4
  import Database from 'better-sqlite3'
package/src/index.ts CHANGED
@@ -6,6 +6,9 @@ export * from './graph/index.js'
6
6
  export * from './contract/index.js'
7
7
  export * from './hash/index.js'
8
8
  export * from './search/index.js'
9
+ export * from './cache/index.js'
10
+ export * from './security/index.js'
11
+ export * from './analysis/index.js'
9
12
  export * from './utils/errors.js'
10
13
  export * from './utils/logger.js'
11
14
  export { MikkError, ErrorHandler, ErrorBuilder, ErrorCategory, FileSystemError, ModuleLoadError, GraphError, TokenBudgetError, ValidationError, createDefaultErrorListener, createFileNotFoundError, createFileTooLargeError, createPermissionDeniedError, createModuleNotFoundError, createModuleLoadFailedError, createGraphBuildFailedError, createNodeNotFoundError, createTokenBudgetExceededError, createValidationError, isMikkError, getRootCause, toMikkError } from './error-handler.js'
@@ -16,6 +19,7 @@ export { minimatch } from './utils/minimatch.js'
16
19
  export { scoreFunctions, findFuzzyMatches, levenshtein, splitCamelCase, extractKeywords } from './utils/fuzzy-match.js'
17
20
  export { writeFileAtomic, writeJsonAtomic } from './utils/atomic-write.js'
18
21
  export type { AtomicWriteOptions } from './utils/atomic-write.js'
22
+ export { normalizeSlashes, normalizePath, normalizePathQuiet, getPathKey, pathsEqual, isSubPath } from './utils/path.js'
19
23
  export {
20
24
  runArtifactWriteTransaction,
21
25
  recoverArtifactWriteTransactions,
@@ -0,0 +1,19 @@
1
+ import type { ParsedFile } from './types.js';
2
+
3
+ /**
4
+ * Abstract base class for language-specific metadata extraction.
5
+ * Extractors can use different engines (Tree-Sitter, OXC, etc.) internally.
6
+ */
7
+ export abstract class BaseExtractor {
8
+ /**
9
+ * Main entry point to parse and extract metadata from a file
10
+ */
11
+ abstract extract(filePath: string, content: string): Promise<ParsedFile>;
12
+
13
+ /**
14
+ * Resolve imports for a list of files (optional, default to simple pass-through)
15
+ */
16
+ async resolveImports(files: ParsedFile[], _projectRoot: string): Promise<ParsedFile[]> {
17
+ return files;
18
+ }
19
+ }
@@ -53,21 +53,40 @@ function parseConstraint(constraint: string): ParsedRule | null {
53
53
  export class BoundaryChecker {
54
54
  private rules: ParsedRule[]
55
55
  private moduleNames: Map<string, string>
56
+ private fileModuleMap: Map<string, string>
56
57
 
57
58
  constructor(private contract: MikkContract, private lock: MikkLock) {
58
59
  this.rules = contract.declared.constraints.map(parseConstraint).filter((r): r is ParsedRule => r !== null)
59
60
  this.moduleNames = new Map(contract.declared.modules.map(m => [m.id, m.name]))
61
+
62
+ this.fileModuleMap = new Map()
63
+ for (const [filePath, fileData] of Object.entries(this.lock.files)) {
64
+ if (fileData.moduleId && fileData.moduleId !== 'unknown') {
65
+ this.fileModuleMap.set(filePath.toLowerCase(), fileData.moduleId)
66
+ }
67
+ }
68
+ }
69
+
70
+ private getModuleId(fn: any): string {
71
+ if (fn.moduleId && fn.moduleId !== 'unknown') return fn.moduleId
72
+ const normalized = (fn.file ?? '').replace(/\\/g, '/').toLowerCase()
73
+ return this.fileModuleMap.get(normalized) ?? 'unknown'
60
74
  }
61
75
 
62
76
  check(): BoundaryCheckResult {
63
77
  const violations: BoundaryViolation[] = []
78
+ const fnIndex = (this.lock as any).fnIndex ?? []
64
79
 
65
80
  for (const fn of Object.values(this.lock.functions)) {
66
- if (fn.moduleId === 'unknown') continue
67
- for (const calleeId of fn.calls) {
81
+ const fnModuleId = this.getModuleId(fn)
82
+ if (fnModuleId === 'unknown') continue
83
+ const calls = (fn.calls ?? []).map((c: any) => typeof c === 'number' ? fnIndex[c] : c).filter(Boolean)
84
+ for (const calleeId of calls) {
68
85
  const callee = this.lock.functions[calleeId]
69
- if (!callee || callee.moduleId === 'unknown' || fn.moduleId === callee.moduleId) continue
70
- const v = this.checkCall(fn, callee)
86
+ if (!callee) continue
87
+ const calleeModuleId = this.getModuleId(callee)
88
+ if (calleeModuleId === 'unknown' || fnModuleId === calleeModuleId) continue
89
+ const v = this.checkCall(fn, callee, fnModuleId, calleeModuleId)
71
90
  if (v) violations.push(v)
72
91
  }
73
92
  }
@@ -97,21 +116,21 @@ export class BoundaryChecker {
97
116
  const errorCount = unique.filter(v => v.severity === 'error').length
98
117
  const warnCount = unique.filter(v => v.severity === 'warning').length
99
118
  const summary = unique.length === 0
100
- ? `All module boundaries respected (${fnCount} functions, ${fileCount} files checked)`
101
- : `${errorCount} boundary error(s), ${warnCount} warning(s) found`
119
+ ? `All module boundaries respected (${fnCount} fns, ${fileCount} files checked)`
120
+ : `${errorCount} error(s), ${warnCount} warning(s) found`
102
121
 
103
122
  return { pass: errorCount === 0, violations: unique, summary }
104
123
  }
105
124
 
106
- private checkCall(caller: MikkLockFunction, callee: MikkLockFunction): BoundaryViolation | null {
125
+ private checkCall(caller: any, callee: any, callerModuleId: string, calleeModuleId: string): BoundaryViolation | null {
107
126
  for (const rule of this.rules) {
108
- if (rule.fromModuleId !== caller.moduleId) continue
127
+ if (rule.fromModuleId !== callerModuleId) continue
109
128
  const forbidden = rule.type === 'isolated' ? true
110
- : rule.type === 'deny' ? rule.toModuleIds.includes(callee.moduleId)
111
- : !rule.toModuleIds.includes(callee.moduleId)
129
+ : rule.type === 'deny' ? rule.toModuleIds.includes(calleeModuleId)
130
+ : !rule.toModuleIds.includes(calleeModuleId)
112
131
  if (forbidden) return {
113
- from: { functionId: caller.id, functionName: caller.name, file: caller.file, moduleId: caller.moduleId, moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId },
114
- to: { functionId: callee.id, functionName: callee.name, file: callee.file, moduleId: callee.moduleId, moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId },
132
+ from: { functionId: caller.id, functionName: caller.name, file: caller.file, moduleId: callerModuleId, moduleName: this.moduleNames.get(callerModuleId) ?? callerModuleId },
133
+ to: { functionId: callee.id, functionName: callee.name, file: callee.file, moduleId: calleeModuleId, moduleName: this.moduleNames.get(calleeModuleId) ?? calleeModuleId },
115
134
  rule: rule.raw, severity: 'error'
116
135
  }
117
136
  }