@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.
- package/README.md +4 -4
- package/package.json +2 -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 +278 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-reader.ts +1 -0
- package/src/contract/lock-compiler.ts +125 -12
- package/src/contract/schema.ts +4 -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 +4 -0
- package/src/parser/base-extractor.ts +19 -0
- package/src/parser/boundary-checker.ts +31 -12
- package/src/parser/error-recovery.ts +647 -0
- package/src/parser/function-body-extractor.ts +248 -0
- package/src/parser/go/go-extractor.ts +249 -676
- package/src/parser/index.ts +138 -295
- 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 +400 -66
- 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/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -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 +150 -65
- package/src/utils/json.ts +1 -0
- package/src/utils/language-registry.ts +96 -5
- package/src/utils/minimatch.ts +49 -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
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import type { DependencyGraph } from './types.js'
|
|
2
|
-
import type { MikkLock } from '../contract/schema.js'
|
|
2
|
+
import type { MikkLock, MikkLockClass } from '../contract/schema.js'
|
|
3
3
|
|
|
4
4
|
// ─── Types ──────────────────────────────────────────────────────────
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Confidence level for a dead code finding.
|
|
8
|
-
*
|
|
9
|
-
* high — zero callers, no dynamic patterns, no unresolved imports in the file.
|
|
10
|
-
* Safe to remove.
|
|
11
|
-
* medium — zero callers via graph, but the file has unresolved imports
|
|
12
|
-
* (some calls may not have been traced). Review before removing.
|
|
13
|
-
* low — zero graph callers, but the function name or context suggests it
|
|
14
|
-
* may be used dynamically (generic names, lifecycle hooks, etc.).
|
|
15
|
-
* Do not remove without manual verification.
|
|
16
|
-
*/
|
|
17
6
|
export type DeadCodeConfidence = 'high' | 'medium' | 'low'
|
|
18
7
|
|
|
19
8
|
export interface DeadCodeEntry {
|
|
@@ -36,31 +25,51 @@ export interface DeadCodeResult {
|
|
|
36
25
|
|
|
37
26
|
// ─── Exemption patterns ────────────────────────────────────────────
|
|
38
27
|
|
|
39
|
-
/** Entry-point function names that are never "dead" even with 0 graph callers */
|
|
40
28
|
const ENTRY_POINT_PATTERNS = [
|
|
41
29
|
/^(main|bootstrap|start|init|setup|configure|register|mount)$/i,
|
|
42
30
|
/^(app|server|index|mod|program)$/i,
|
|
43
31
|
/Handler$/i,
|
|
44
32
|
/Middleware$/i,
|
|
45
33
|
/Controller$/i,
|
|
46
|
-
/^use[A-Z]/,
|
|
47
|
-
/^handle[A-Z]/,
|
|
48
|
-
/^on[A-Z]/,
|
|
34
|
+
/^use[A-Z]/,
|
|
35
|
+
/^handle[A-Z]/,
|
|
36
|
+
/^on[A-Z]/,
|
|
37
|
+
/Provider$/i,
|
|
38
|
+
/Provider$/,
|
|
39
|
+
/^Page$/i,
|
|
40
|
+
/^Layout$/i,
|
|
41
|
+
/^get[A-Z]/,
|
|
42
|
+
/^default$/i,
|
|
43
|
+
/Provider$/,
|
|
44
|
+
/^(getStaticProps|getServerSideProps|generateStaticParams)$/,
|
|
49
45
|
]
|
|
50
46
|
|
|
51
|
-
/** Test function patterns */
|
|
52
47
|
const TEST_PATTERNS = [
|
|
53
48
|
/^(it|describe|test|beforeAll|afterAll|beforeEach|afterEach)$/,
|
|
54
49
|
/\.test\./,
|
|
55
50
|
/\.spec\./,
|
|
56
51
|
/__test__/,
|
|
52
|
+
/_test_/,
|
|
53
|
+
/_spec_/,
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const SCRIPT_PATTERNS = [
|
|
57
|
+
/\/scripts\//,
|
|
58
|
+
/\/benchmarks\//,
|
|
59
|
+
/\/fixtures\//,
|
|
60
|
+
/\.bench\./,
|
|
61
|
+
/\.benchmark\./,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
const FRAMEWORK_ENTRY_PATTERNS = [
|
|
65
|
+
/\/app\//,
|
|
66
|
+
/\/pages\//,
|
|
67
|
+
/\/components\//,
|
|
68
|
+
/\.next\//,
|
|
69
|
+
/\.mjs$/,
|
|
70
|
+
/\.cjs$/,
|
|
57
71
|
]
|
|
58
72
|
|
|
59
|
-
/**
|
|
60
|
-
* Names that are commonly used via dynamic dispatch, string-keyed maps, or
|
|
61
|
-
* framework injection. A function matching these patterns gets LOW confidence
|
|
62
|
-
* even if no graph callers exist, because static analysis may have missed it.
|
|
63
|
-
*/
|
|
64
73
|
const DYNAMIC_USAGE_PATTERNS = [
|
|
65
74
|
/^addEventListener$/i,
|
|
66
75
|
/^removeEventListener$/i,
|
|
@@ -71,29 +80,39 @@ const DYNAMIC_USAGE_PATTERNS = [
|
|
|
71
80
|
/^componentWillUnmount$/i,
|
|
72
81
|
]
|
|
73
82
|
|
|
83
|
+
const FRAMEWORK_PATTERNS = [
|
|
84
|
+
/^componentDidCatch$/i,
|
|
85
|
+
/^getDerivedStateFromError$/i,
|
|
86
|
+
/^getDerivedStateFromProps$/i,
|
|
87
|
+
/^render$/i,
|
|
88
|
+
/^shouldComponentUpdate$/i,
|
|
89
|
+
/^componentWillReceiveProps$/i,
|
|
90
|
+
/^componentWillUpdate$/i,
|
|
91
|
+
/^UNSAFE_/,
|
|
92
|
+
/^__\w+__$/,
|
|
93
|
+
/^\$\w+/,
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const CONSTRUCTOR_PATTERNS = [
|
|
97
|
+
/^constructor$/i,
|
|
98
|
+
/^__construct$/i,
|
|
99
|
+
/^__init__$/i,
|
|
100
|
+
/^init$/i,
|
|
101
|
+
/^initialize$/i,
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
const CLASS_METHOD_PATTERNS = [
|
|
105
|
+
/\.constructor$/,
|
|
106
|
+
/^\w+\.\w+$/,
|
|
107
|
+
]
|
|
108
|
+
|
|
74
109
|
// ─── Detector ──────────────────────────────────────────────────────
|
|
75
110
|
|
|
76
|
-
/**
|
|
77
|
-
* DeadCodeDetector — walks the dependency graph to find functions with zero
|
|
78
|
-
* incoming `calls` edges after applying multi-pass exemptions.
|
|
79
|
-
*
|
|
80
|
-
* Exemptions (function is NOT reported as dead):
|
|
81
|
-
* 1. Exported symbols — may be consumed by external packages
|
|
82
|
-
* 2. Entry point name patterns — main, handler, middleware, hooks, etc.
|
|
83
|
-
* 3. Route handlers — detected via HTTP route registrations in the lock
|
|
84
|
-
* 4. Test functions — describe, it, test, lifecycle hooks
|
|
85
|
-
* 5. Constructors — called implicitly by `new`
|
|
86
|
-
* 6. Called by an exported function in the same file (transitive liveness)
|
|
87
|
-
*
|
|
88
|
-
* Each dead entry includes a confidence level:
|
|
89
|
-
* high — safe to remove
|
|
90
|
-
* medium — file has unresolved imports; verify before removing
|
|
91
|
-
* low — dynamic usage patterns detected; manual review required
|
|
92
|
-
*/
|
|
93
111
|
export class DeadCodeDetector {
|
|
94
112
|
private routeHandlers: Set<string>
|
|
95
|
-
/** Files that have at least one unresolved import (empty resolvedPath) */
|
|
96
113
|
private filesWithUnresolvedImports: Set<string>
|
|
114
|
+
private fnIndex: Map<number, string>
|
|
115
|
+
private allClasses: Map<string, MikkLockClass>
|
|
97
116
|
|
|
98
117
|
constructor(
|
|
99
118
|
private graph: DependencyGraph,
|
|
@@ -102,10 +121,37 @@ export class DeadCodeDetector {
|
|
|
102
121
|
this.routeHandlers = new Set(
|
|
103
122
|
(lock.routes ?? []).map(r => r.handler).filter(Boolean),
|
|
104
123
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// be lowered for all functions in those files without scanning per-function.
|
|
124
|
+
const rawFnIndex = (lock as any).fnIndex || []
|
|
125
|
+
this.fnIndex = new Map(rawFnIndex.map((id: string, idx: number) => [idx, id]))
|
|
108
126
|
this.filesWithUnresolvedImports = this.buildUnresolvedImportFileSet()
|
|
127
|
+
this.allClasses = this.buildClassIndex()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private resolveFnData(id: string): { name: string; file: string; isExported?: boolean; moduleId?: string } {
|
|
131
|
+
const fn = this.lock.functions[id]
|
|
132
|
+
if (fn?.name && fn?.file) return fn
|
|
133
|
+
|
|
134
|
+
const fullId = this.resolveId(id)
|
|
135
|
+
const resolvedFn = this.lock.functions[fullId]
|
|
136
|
+
if (resolvedFn?.name && resolvedFn?.file) return resolvedFn
|
|
137
|
+
|
|
138
|
+
if (fullId.startsWith('fn:')) {
|
|
139
|
+
const parts = fullId.slice(3).split(':')
|
|
140
|
+
const file = parts.slice(0, -1).join(':')
|
|
141
|
+
const name = parts[parts.length - 1]
|
|
142
|
+
return { name, file }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { name: '', file: '' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private resolveId(id: string): string {
|
|
149
|
+
if (this.lock.functions[id]) return id
|
|
150
|
+
const num = parseInt(id)
|
|
151
|
+
if (!isNaN(num)) {
|
|
152
|
+
return this.fnIndex.get(num) || id
|
|
153
|
+
}
|
|
154
|
+
return id
|
|
109
155
|
}
|
|
110
156
|
|
|
111
157
|
detect(): DeadCodeResult {
|
|
@@ -113,8 +159,14 @@ export class DeadCodeDetector {
|
|
|
113
159
|
let totalFunctions = 0
|
|
114
160
|
const byModule: DeadCodeResult['byModule'] = {}
|
|
115
161
|
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
const functionIds = Object.keys(this.lock.functions)
|
|
163
|
+
totalFunctions = functionIds.length
|
|
164
|
+
|
|
165
|
+
for (const id of functionIds) {
|
|
166
|
+
const fn = this.lock.functions[id]
|
|
167
|
+
if (!fn) continue
|
|
168
|
+
|
|
169
|
+
const fnData = this.resolveFnData(id)
|
|
118
170
|
const moduleId = fn.moduleId ?? 'unknown'
|
|
119
171
|
|
|
120
172
|
if (!byModule[moduleId]) {
|
|
@@ -122,21 +174,22 @@ export class DeadCodeDetector {
|
|
|
122
174
|
}
|
|
123
175
|
byModule[moduleId].total++
|
|
124
176
|
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
if (hasCallers) continue
|
|
177
|
+
const name = fnData.name
|
|
178
|
+
const file = fnData.file
|
|
179
|
+
const isExported = fn.isExported ?? false
|
|
129
180
|
|
|
130
|
-
if (this.isExempt(fn, id))
|
|
181
|
+
if (this.isExempt(fn, id, name, file, isExported)) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
131
184
|
|
|
132
|
-
const confidence = this.
|
|
185
|
+
const confidence = this.computeConfidence(fn, name, file)
|
|
133
186
|
const entry: DeadCodeEntry = {
|
|
134
187
|
id,
|
|
135
|
-
name
|
|
136
|
-
file
|
|
188
|
+
name,
|
|
189
|
+
file,
|
|
137
190
|
moduleId,
|
|
138
191
|
type: 'function',
|
|
139
|
-
reason: this.
|
|
192
|
+
reason: this.computeReason(fn),
|
|
140
193
|
confidence,
|
|
141
194
|
}
|
|
142
195
|
dead.push(entry)
|
|
@@ -144,7 +197,6 @@ export class DeadCodeDetector {
|
|
|
144
197
|
byModule[moduleId].items.push(entry)
|
|
145
198
|
}
|
|
146
199
|
|
|
147
|
-
// Check classes
|
|
148
200
|
if (this.lock.classes) {
|
|
149
201
|
for (const [id, cls] of Object.entries(this.lock.classes)) {
|
|
150
202
|
const moduleId = cls.moduleId ?? 'unknown'
|
|
@@ -152,9 +204,11 @@ export class DeadCodeDetector {
|
|
|
152
204
|
byModule[moduleId] = { dead: 0, total: 0, items: [] }
|
|
153
205
|
}
|
|
154
206
|
|
|
207
|
+
if (cls.isExported) continue
|
|
208
|
+
|
|
155
209
|
const inEdges = this.graph.inEdges.get(id) || []
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
210
|
+
const hasImporters = inEdges.some(e => e.type === 'imports')
|
|
211
|
+
if (hasImporters) continue
|
|
158
212
|
|
|
159
213
|
const entry: DeadCodeEntry = {
|
|
160
214
|
id,
|
|
@@ -162,7 +216,7 @@ export class DeadCodeDetector {
|
|
|
162
216
|
file: cls.file,
|
|
163
217
|
moduleId,
|
|
164
218
|
type: 'class',
|
|
165
|
-
reason: 'Class has no
|
|
219
|
+
reason: 'Class has no importers and is not exported',
|
|
166
220
|
confidence: this.filesWithUnresolvedImports.has(cls.file) ? 'medium' : 'high',
|
|
167
221
|
}
|
|
168
222
|
dead.push(entry)
|
|
@@ -182,81 +236,213 @@ export class DeadCodeDetector {
|
|
|
182
236
|
}
|
|
183
237
|
}
|
|
184
238
|
|
|
185
|
-
|
|
239
|
+
private isExempt(
|
|
240
|
+
fn: MikkLock['functions'][string],
|
|
241
|
+
id: string,
|
|
242
|
+
name: string,
|
|
243
|
+
file: string,
|
|
244
|
+
isExported: boolean,
|
|
245
|
+
): boolean {
|
|
246
|
+
if (isExported) return true
|
|
247
|
+
if (this.hasGraphCallers(id, name, file)) return true
|
|
248
|
+
if (this.hasCalledByInLock(fn)) return true
|
|
249
|
+
if (ENTRY_POINT_PATTERNS.some(p => p.test(name))) return true
|
|
250
|
+
if (this.routeHandlers.has(name)) return true
|
|
251
|
+
if (TEST_PATTERNS.some(p => p.test(name) || p.test(file))) return true
|
|
252
|
+
if (SCRIPT_PATTERNS.some(p => p.test(file))) return true
|
|
253
|
+
if (CONSTRUCTOR_PATTERNS.some(p => p.test(name))) return true
|
|
254
|
+
if (FRAMEWORK_PATTERNS.some(p => p.test(name))) return true
|
|
255
|
+
if (this.isReactComponent(name)) return true
|
|
256
|
+
if (this.isCalledByExportedInSameFile(fn, id)) return true
|
|
257
|
+
if (this.isMethodOfUsedClass(fn, name, file)) return true
|
|
258
|
+
if (this.isFrameworkEntryPoint(file, name)) return true
|
|
259
|
+
if (this.isFrameworkEntry(file)) return true
|
|
186
260
|
|
|
187
|
-
private isExempt(fn: MikkLock['functions'][string], id: string): boolean {
|
|
188
|
-
if (fn.isExported) return true
|
|
189
|
-
if (ENTRY_POINT_PATTERNS.some(p => p.test(fn.name))) return true
|
|
190
|
-
if (this.routeHandlers.has(fn.name)) return true
|
|
191
|
-
if (TEST_PATTERNS.some(p => p.test(fn.name) || p.test(fn.file))) return true
|
|
192
|
-
if (fn.name === 'constructor' || fn.name === '__init__') return true
|
|
193
|
-
if (this.isCalledByExportedInSameFile(fn)) return true
|
|
194
261
|
return false
|
|
195
262
|
}
|
|
196
263
|
|
|
197
|
-
private
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
264
|
+
private hasGraphCallers(id: string, name: string, file: string): boolean {
|
|
265
|
+
const candidates = [
|
|
266
|
+
id,
|
|
267
|
+
name,
|
|
268
|
+
`fn:${file}:${name}`,
|
|
269
|
+
file + ':' + name,
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
for (const candidate of candidates) {
|
|
273
|
+
const inEdges = this.graph.inEdges.get(candidate)
|
|
274
|
+
if (inEdges?.some(e => e.type === 'calls')) return true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const fn = this.lock.functions[id]
|
|
278
|
+
if (fn?.calledBy?.length) return true
|
|
279
|
+
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private isReactComponent(name: string): boolean {
|
|
284
|
+
if (!name || name.length < 2) return false
|
|
285
|
+
if (!/^[A-Z]/.test(name)) return false
|
|
286
|
+
if (name.includes('.') || name.includes('/') || name.includes('\\')) return false
|
|
287
|
+
if (name.includes(':') || name.includes('#')) return false
|
|
288
|
+
if (/^[A-Z][a-z]/.test(name)) return true
|
|
289
|
+
if (/^[A-Z][A-Z]/.test(name)) return true
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private isCalledByExportedInSameFile(fn: MikkLock['functions'][string] | undefined, id: string): boolean {
|
|
294
|
+
if (!fn) return false
|
|
201
295
|
const file = fn.file
|
|
296
|
+
if (!file) return false
|
|
297
|
+
|
|
298
|
+
return this.isReachableFromExported(fn, id, file)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private hasCalledByInLock(fn: MikkLock['functions'][string]): boolean {
|
|
302
|
+
if (!fn.calledBy || fn.calledBy.length === 0) return false
|
|
303
|
+
|
|
304
|
+
for (const callerId of fn.calledBy) {
|
|
305
|
+
const caller = this.lock.functions[callerId]
|
|
306
|
+
if (!caller) continue
|
|
307
|
+
if (caller.isExported) return true
|
|
308
|
+
if (this.hasGraphCallers(callerId, caller.name, caller.file)) return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private isReachableFromExported(
|
|
315
|
+
startFn: MikkLock['functions'][string],
|
|
316
|
+
startId: string,
|
|
317
|
+
file: string,
|
|
318
|
+
requireExported: boolean = true
|
|
319
|
+
): boolean {
|
|
202
320
|
const visited = new Set<string>()
|
|
203
|
-
const queue: string[] = [
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
321
|
+
const queue: string[] = [startId]
|
|
322
|
+
const maxDepth = 50
|
|
323
|
+
|
|
324
|
+
let depth = 0
|
|
325
|
+
while (queue.length > 0 && depth < maxDepth) {
|
|
326
|
+
const levelSize = queue.length
|
|
327
|
+
for (let i = 0; i < levelSize; i++) {
|
|
328
|
+
const currentId = queue.shift()!
|
|
329
|
+
if (visited.has(currentId)) continue
|
|
330
|
+
visited.add(currentId)
|
|
331
|
+
|
|
332
|
+
const current = this.lock.functions[currentId]
|
|
333
|
+
if (!current) continue
|
|
334
|
+
|
|
335
|
+
for (const callerId of current.calledBy || []) {
|
|
336
|
+
if (visited.has(callerId)) continue
|
|
337
|
+
const caller = this.lock.functions[callerId]
|
|
338
|
+
if (!caller) continue
|
|
339
|
+
if (caller.file !== file) {
|
|
340
|
+
queue.push(callerId)
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
if (caller.isExported) return true
|
|
344
|
+
if (!requireExported) return true
|
|
345
|
+
queue.push(callerId)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const callerEdges = this.graph.inEdges.get(currentId) || []
|
|
349
|
+
for (const edge of callerEdges) {
|
|
350
|
+
if (edge.type !== 'calls') continue
|
|
351
|
+
if (visited.has(edge.from)) continue
|
|
352
|
+
const caller = this.lock.functions[edge.from]
|
|
353
|
+
if (!caller) continue
|
|
354
|
+
if (caller.file !== file) {
|
|
355
|
+
queue.push(edge.from)
|
|
356
|
+
continue
|
|
357
|
+
}
|
|
358
|
+
if (caller.isExported) return true
|
|
359
|
+
if (!requireExported) return true
|
|
360
|
+
queue.push(edge.from)
|
|
361
|
+
}
|
|
222
362
|
}
|
|
363
|
+
depth++
|
|
364
|
+
}
|
|
365
|
+
return false
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private isMethodOfUsedClass(fn: MikkLock['functions'][string], name: string, file: string): boolean {
|
|
369
|
+
const classInfo = this.allClasses.get(file)
|
|
370
|
+
if (!classInfo) return false
|
|
371
|
+
|
|
372
|
+
const inEdges = this.graph.inEdges.get(`class:${file}:${classInfo.name}`) || []
|
|
373
|
+
if (inEdges.length > 0) return true
|
|
374
|
+
|
|
375
|
+
for (const [clsId, cls] of Object.entries(this.lock.classes || {})) {
|
|
376
|
+
if (cls.file !== file) continue
|
|
377
|
+
const clsInEdges = this.graph.inEdges.get(clsId) || []
|
|
378
|
+
if (clsInEdges.length > 0) return true
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private buildClassIndex(): Map<string, MikkLockClass> {
|
|
385
|
+
const index = new Map<string, MikkLockClass>()
|
|
386
|
+
if (!this.lock.classes) return index
|
|
387
|
+
|
|
388
|
+
for (const [id, cls] of Object.entries(this.lock.classes)) {
|
|
389
|
+
index.set(cls.file, cls)
|
|
390
|
+
}
|
|
391
|
+
return index
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private isFrameworkEntryPoint(file: string, name: string): boolean {
|
|
395
|
+
const frameworkExports = [
|
|
396
|
+
/^Page$/,
|
|
397
|
+
/^Layout$/,
|
|
398
|
+
/^default$/,
|
|
399
|
+
/^(getServerSideProps|getStaticProps|generateMetadata|generateViewport)$/,
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
return frameworkExports.some(p => p.test(name))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private isFrameworkEntry(file: string): boolean {
|
|
406
|
+
return FRAMEWORK_ENTRY_PATTERNS.some(p => p.test(file))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private hasCalledByFromLock(fn: MikkLock['functions'][string]): boolean {
|
|
410
|
+
if (!fn.calledBy || fn.calledBy.length === 0) return false
|
|
411
|
+
|
|
412
|
+
for (const callerId of fn.calledBy) {
|
|
413
|
+
const caller = this.lock.functions[callerId]
|
|
414
|
+
if (!caller) continue
|
|
415
|
+
|
|
416
|
+
if (caller.isExported) return true
|
|
417
|
+
if (this.hasCallersFromGraph(callerId)) return true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private hasCallersFromGraph(id: string): boolean {
|
|
424
|
+
const candidates = [id]
|
|
425
|
+
for (const candidate of candidates) {
|
|
426
|
+
const inEdges = this.graph.inEdges.get(candidate)
|
|
427
|
+
if (inEdges?.some(e => e.type === 'calls')) return true
|
|
223
428
|
}
|
|
224
429
|
return false
|
|
225
430
|
}
|
|
226
431
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
* medium — lock.calledBy has entries that didn't become graph edges:
|
|
232
|
-
* something references this function but resolution failed.
|
|
233
|
-
* medium — file has unresolved imports: the graph may be incomplete.
|
|
234
|
-
* low — function name matches common dynamic-dispatch patterns.
|
|
235
|
-
* high — none of the above: safe to remove.
|
|
236
|
-
*/
|
|
237
|
-
private inferConfidence(fn: MikkLock['functions'][string]): DeadCodeConfidence {
|
|
238
|
-
if (DYNAMIC_USAGE_PATTERNS.some(p => p.test(fn.name))) return 'low'
|
|
239
|
-
if (fn.calledBy.length > 0) return 'medium'
|
|
240
|
-
if (this.filesWithUnresolvedImports.has(fn.file)) return 'medium'
|
|
432
|
+
private computeConfidence(fn: MikkLock['functions'][string], name: string, file: string): DeadCodeConfidence {
|
|
433
|
+
if (DYNAMIC_USAGE_PATTERNS.some(p => p.test(name))) return 'low'
|
|
434
|
+
if (fn.calledBy?.length) return 'medium'
|
|
435
|
+
if (this.filesWithUnresolvedImports.has(file)) return 'medium'
|
|
241
436
|
return 'high'
|
|
242
437
|
}
|
|
243
438
|
|
|
244
|
-
private
|
|
245
|
-
if (fn.calledBy
|
|
246
|
-
return
|
|
439
|
+
private computeReason(fn: MikkLock['functions'][string]): string {
|
|
440
|
+
if (fn.calledBy?.length) {
|
|
441
|
+
return `${fn.calledBy.length} reference(s) in lock but no active call edges in graph`
|
|
247
442
|
}
|
|
248
|
-
return
|
|
443
|
+
return 'No callers found in graph'
|
|
249
444
|
}
|
|
250
445
|
|
|
251
|
-
/**
|
|
252
|
-
* Build the set of file paths that have at least one import whose
|
|
253
|
-
* resolvedPath is empty. Used to downgrade confidence for all dead
|
|
254
|
-
* findings in those files, since the graph may be incomplete.
|
|
255
|
-
*
|
|
256
|
-
* We derive this from the lock's file entries. Each file entry stores
|
|
257
|
-
* its imports; any import with an empty resolvedPath (or no match in
|
|
258
|
-
* the graph nodes) indicates an unresolved dependency.
|
|
259
|
-
*/
|
|
260
446
|
private buildUnresolvedImportFileSet(): Set<string> {
|
|
261
447
|
const result = new Set<string>()
|
|
262
448
|
if (!this.lock.files) return result
|
|
@@ -266,7 +452,7 @@ export class DeadCodeDetector {
|
|
|
266
452
|
for (const imp of imports) {
|
|
267
453
|
if (!imp.resolvedPath || imp.resolvedPath === '') {
|
|
268
454
|
result.add(filePath)
|
|
269
|
-
break
|
|
455
|
+
break
|
|
270
456
|
}
|
|
271
457
|
}
|
|
272
458
|
}
|