@getmikk/core 1.8.3 → 2.0.0
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/package.json +6 -4
- package/src/constants.ts +285 -0
- package/src/contract/contract-generator.ts +7 -0
- package/src/contract/index.ts +2 -3
- package/src/contract/lock-compiler.ts +66 -35
- package/src/contract/lock-reader.ts +30 -5
- package/src/contract/schema.ts +21 -0
- package/src/error-handler.ts +432 -0
- package/src/graph/cluster-detector.ts +52 -22
- package/src/graph/confidence-engine.ts +85 -0
- package/src/graph/graph-builder.ts +298 -255
- package/src/graph/impact-analyzer.ts +132 -119
- package/src/graph/index.ts +4 -0
- package/src/graph/memory-manager.ts +186 -0
- package/src/graph/query-engine.ts +76 -0
- package/src/graph/risk-engine.ts +86 -0
- package/src/graph/types.ts +89 -65
- package/src/index.ts +2 -0
- package/src/parser/change-detector.ts +99 -0
- package/src/parser/go/go-extractor.ts +18 -8
- package/src/parser/go/go-parser.ts +2 -0
- package/src/parser/index.ts +86 -36
- package/src/parser/javascript/js-extractor.ts +1 -1
- package/src/parser/javascript/js-parser.ts +2 -0
- package/src/parser/oxc-parser.ts +708 -0
- package/src/parser/oxc-resolver.ts +83 -0
- package/src/parser/tree-sitter/parser.ts +19 -10
- package/src/parser/types.ts +100 -73
- package/src/parser/typescript/ts-extractor.ts +229 -589
- package/src/parser/typescript/ts-parser.ts +16 -171
- package/src/parser/typescript/ts-resolver.ts +11 -1
- package/src/search/bm25.ts +16 -4
- package/src/utils/minimatch.ts +1 -1
- package/tests/contract.test.ts +2 -2
- package/tests/dead-code.test.ts +7 -7
- package/tests/esm-resolver.test.ts +75 -0
- package/tests/graph.test.ts +20 -20
- package/tests/helpers.ts +11 -6
- package/tests/impact-classified.test.ts +37 -41
- package/tests/parser.test.ts +7 -5
- package/tests/ts-parser.test.ts +27 -52
- package/test-output.txt +0 -373
|
@@ -1,78 +1,74 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { ImpactAnalyzer } from '../src/graph/impact-analyzer'
|
|
3
3
|
import { buildTestGraph } from './helpers'
|
|
4
|
-
import { GraphBuilder } from '../src/graph/graph-builder'
|
|
5
4
|
|
|
6
5
|
describe('ImpactAnalyzer - Classified', () => {
|
|
7
6
|
|
|
8
7
|
it('classifies impacts based on depth and module boundaries', () => {
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
// D is changed
|
|
13
|
-
// C is depth 1, crosses boundary -> CRITICAL
|
|
14
|
-
// B is depth 2 -> MEDIUM
|
|
15
|
-
// A is depth 3 -> LOW
|
|
16
|
-
|
|
8
|
+
// Calibration for Mikk 2.0 Quantitative Risk:
|
|
9
|
+
// AuthService (Auth keyword: 30) calls BStateAuthDB (State+Auth+DB keywords: 60) calls DBManager (m2, DB keyword: 20) calls PublicAPI (m3)
|
|
10
|
+
|
|
17
11
|
const graph = buildTestGraph([
|
|
18
|
-
['
|
|
19
|
-
['
|
|
20
|
-
['
|
|
21
|
-
['
|
|
12
|
+
['AuthService', 'BStateAuthDB'],
|
|
13
|
+
['BStateAuthDB', 'DBManager'],
|
|
14
|
+
['DBManager', 'PublicAPI'],
|
|
15
|
+
['PublicAPI', 'nothing']
|
|
22
16
|
])
|
|
23
17
|
|
|
24
18
|
// Assign modules manually for the test
|
|
25
|
-
graph.nodes.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
graph.nodes.get('src/D.ts')!.moduleId = 'm3'
|
|
35
|
-
graph.nodes.get('fn:src/D.ts:D')!.moduleId = 'm3'
|
|
19
|
+
for (const id of graph.nodes.keys()) {
|
|
20
|
+
if (id.includes('authservice')) graph.nodes.get(id)!.moduleId = 'm1'
|
|
21
|
+
if (id.includes('bstateauthdb')) {
|
|
22
|
+
graph.nodes.get(id)!.moduleId = 'm1'
|
|
23
|
+
graph.nodes.get(id)!.metadata!.isExported = true
|
|
24
|
+
}
|
|
25
|
+
if (id.includes('dbmanager')) graph.nodes.get(id)!.moduleId = 'm2'
|
|
26
|
+
if (id.includes('publicapi')) graph.nodes.get(id)!.moduleId = 'm3'
|
|
27
|
+
}
|
|
36
28
|
|
|
37
29
|
const analyzer = new ImpactAnalyzer(graph)
|
|
38
|
-
const result = analyzer.analyze(['fn:src/
|
|
30
|
+
const result = analyzer.analyze(['fn:src/publicapi.ts:publicapi'])
|
|
39
31
|
|
|
40
32
|
expect(result.impacted.length).toBe(3)
|
|
41
33
|
|
|
34
|
+
// Critical: DBManager (boosted to 80)
|
|
42
35
|
expect(result.classified.critical).toHaveLength(1)
|
|
43
|
-
expect(result.classified.critical[0].nodeId).toBe('fn:src/
|
|
44
|
-
|
|
45
|
-
expect(result.classified.high).toHaveLength(0) // No depth 1 in same module
|
|
36
|
+
expect(result.classified.critical[0].nodeId).toBe('fn:src/dbmanager.ts:dbmanager')
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
// High: BStateAuthDB (depth 2)
|
|
39
|
+
// Base: 3.5 + Auth(30) + DB(20) + Exported(15) = 68.5 -> HIGH.
|
|
40
|
+
expect(result.classified.high).toHaveLength(1)
|
|
41
|
+
expect(result.classified.high[0].nodeId).toBe('fn:src/bstateauthdb.ts:bstateauthdb')
|
|
49
42
|
|
|
43
|
+
// Low: AuthService (30 risk)
|
|
50
44
|
expect(result.classified.low).toHaveLength(1)
|
|
51
|
-
expect(result.classified.low[0].nodeId).toBe('fn:src/
|
|
45
|
+
expect(result.classified.low[0].nodeId).toBe('fn:src/authservice.ts:authservice')
|
|
52
46
|
})
|
|
53
47
|
|
|
54
48
|
it('classifies same-module depth-1 impact as HIGH, not CRITICAL', () => {
|
|
55
|
-
//
|
|
56
|
-
// change G
|
|
57
|
-
// F is depth 1, same module -> HIGH
|
|
58
|
-
// E is depth 2 -> MEDIUM
|
|
49
|
+
// HighRiskAuthService calls AuthDBInstance calls G
|
|
59
50
|
const graph = buildTestGraph([
|
|
60
|
-
['
|
|
61
|
-
['
|
|
51
|
+
['HighRiskAuthService', 'AuthDBInstance'],
|
|
52
|
+
['AuthDBInstance', 'G'],
|
|
62
53
|
['G', 'nothing']
|
|
63
54
|
])
|
|
64
55
|
|
|
65
56
|
for (const id of graph.nodes.keys()) {
|
|
66
57
|
graph.nodes.get(id)!.moduleId = 'm4'
|
|
58
|
+
if (id.includes('authdbinstance')) {
|
|
59
|
+
graph.nodes.get(id)!.metadata!.isExported = true
|
|
60
|
+
}
|
|
67
61
|
}
|
|
68
62
|
|
|
69
63
|
const analyzer = new ImpactAnalyzer(graph)
|
|
70
|
-
const result = analyzer.analyze(['fn:src/
|
|
64
|
+
const result = analyzer.analyze(['fn:src/g.ts:g'])
|
|
71
65
|
|
|
72
66
|
expect(result.classified.critical).toHaveLength(0)
|
|
73
67
|
expect(result.classified.high).toHaveLength(1)
|
|
74
|
-
expect(result.classified.high[0].nodeId).toBe('fn:src/
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
expect(result.classified.high[0].nodeId).toBe('fn:src/authdbinstance.ts:authdbinstance')
|
|
69
|
+
|
|
70
|
+
// HighRiskAuthService: depth 2, risk score 35.5 -> LOW
|
|
71
|
+
expect(result.classified.low).toHaveLength(1)
|
|
72
|
+
expect(result.classified.low[0].nodeId).toBe('fn:src/highriskauthservice.ts:highriskauthservice')
|
|
77
73
|
})
|
|
78
74
|
})
|
package/tests/parser.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { TypeScriptParser } from '../src/parser/typescript/ts-parser'
|
|
3
|
+
import { OxcParser } from '../src/parser/oxc-parser'
|
|
3
4
|
import { TypeScriptExtractor } from '../src/parser/typescript/ts-extractor'
|
|
4
5
|
import { TypeScriptResolver } from '../src/parser/typescript/ts-resolver'
|
|
5
6
|
import { getParser } from '../src/parser/index'
|
|
@@ -53,7 +54,7 @@ describe('TypeScriptExtractor', () => {
|
|
|
53
54
|
}
|
|
54
55
|
`)
|
|
55
56
|
const fns = extractor.extractFunctions()
|
|
56
|
-
expect(fns[0].calls
|
|
57
|
+
expect(fns[0].calls.some(c => c.name === 'jwtDecode')).toBe(true)
|
|
57
58
|
})
|
|
58
59
|
|
|
59
60
|
it('extracts imports', () => {
|
|
@@ -147,6 +148,7 @@ describe('TypeScriptParser', () => {
|
|
|
147
148
|
}
|
|
148
149
|
`)
|
|
149
150
|
expect(result.functions).toHaveLength(1)
|
|
151
|
+
expect(result.functions[0].calls.some(c => c.name === 'jwtDecode')).toBe(true)
|
|
150
152
|
expect(result.imports).toHaveLength(1)
|
|
151
153
|
expect(result.exports).toHaveLength(1)
|
|
152
154
|
expect(result.hash).toBeDefined()
|
|
@@ -202,14 +204,14 @@ describe('TypeScriptResolver', () => {
|
|
|
202
204
|
})
|
|
203
205
|
|
|
204
206
|
describe('getParser', () => {
|
|
205
|
-
it('returns
|
|
207
|
+
it('returns OxcParser for .ts files', () => {
|
|
206
208
|
const parser = getParser('src/auth.ts')
|
|
207
|
-
expect(parser).toBeInstanceOf(
|
|
209
|
+
expect(parser).toBeInstanceOf(OxcParser)
|
|
208
210
|
})
|
|
209
211
|
|
|
210
|
-
it('returns
|
|
212
|
+
it('returns OxcParser for .tsx files', () => {
|
|
211
213
|
const parser = getParser('src/App.tsx')
|
|
212
|
-
expect(parser).toBeInstanceOf(
|
|
214
|
+
expect(parser).toBeInstanceOf(OxcParser)
|
|
213
215
|
})
|
|
214
216
|
|
|
215
217
|
it('throws for unsupported extensions', () => {
|
package/tests/ts-parser.test.ts
CHANGED
|
@@ -1,56 +1,34 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
import * as fs from 'node:fs/promises'
|
|
4
|
-
import {
|
|
5
|
-
import { getParser } from '../src/parser/index'
|
|
4
|
+
import { OxcParser } from '../src/parser/oxc-parser'
|
|
6
5
|
|
|
7
|
-
describe('ts-parser config
|
|
6
|
+
describe('ts-parser config resolution', () => {
|
|
8
7
|
const FIXTURE_DIR = path.join(process.cwd(), '.test-fixture-tsconfig')
|
|
9
8
|
|
|
10
9
|
beforeAll(async () => {
|
|
11
|
-
// Create a temporary directory structure to test tsconfig extends
|
|
12
10
|
await fs.mkdir(FIXTURE_DIR, { recursive: true })
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// 1. node_modules package tsconfig
|
|
11
|
+
// Use a single tsconfig with all paths to verify OxcResolver's basic path mapping
|
|
16
12
|
await fs.writeFile(
|
|
17
|
-
path.join(FIXTURE_DIR, '
|
|
18
|
-
JSON.stringify({
|
|
19
|
-
compilerOptions: {
|
|
20
|
-
target: 'es2022',
|
|
21
|
-
module: 'commonjs',
|
|
22
|
-
paths: { '@base/*': ['src/base/*'] }
|
|
23
|
-
}
|
|
24
|
-
})
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
// 2. Local tsconfig.base.json extending the node_modules one
|
|
28
|
-
await fs.writeFile(
|
|
29
|
-
path.join(FIXTURE_DIR, 'tsconfig.base.json'),
|
|
13
|
+
path.join(FIXTURE_DIR, 'tsconfig.json'),
|
|
30
14
|
JSON.stringify({
|
|
31
|
-
extends: '@tsconfig/node20/tsconfig.json',
|
|
32
15
|
compilerOptions: {
|
|
33
16
|
baseUrl: '.',
|
|
34
17
|
paths: {
|
|
35
|
-
'@
|
|
18
|
+
'@base/*': ['src/base/*'],
|
|
19
|
+
'@lib/*': ['src/lib/*'],
|
|
20
|
+
'@app/*': ['src/app/*']
|
|
36
21
|
}
|
|
37
22
|
}
|
|
38
23
|
})
|
|
39
24
|
)
|
|
40
25
|
|
|
41
|
-
// 3. Project tsconfig.json extending local base
|
|
42
26
|
await fs.writeFile(
|
|
43
|
-
path.join(FIXTURE_DIR, '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"compilerOptions": {
|
|
49
|
-
"paths": {
|
|
50
|
-
"@app/*": ["src/app/*"]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}`
|
|
27
|
+
path.join(FIXTURE_DIR, 'package.json'),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
name: 'test-fixture',
|
|
30
|
+
type: 'module'
|
|
31
|
+
})
|
|
54
32
|
)
|
|
55
33
|
})
|
|
56
34
|
|
|
@@ -58,14 +36,18 @@ describe('ts-parser config "extends" resolution', () => {
|
|
|
58
36
|
await fs.rm(FIXTURE_DIR, { recursive: true, force: true })
|
|
59
37
|
})
|
|
60
38
|
|
|
61
|
-
it('
|
|
62
|
-
|
|
63
|
-
// and letting ts-parser resolve its imports based on the merged tsconfig
|
|
64
|
-
const parser = new TypeScriptParser()
|
|
39
|
+
it('resolves compiler paths from tsconfig', async () => {
|
|
40
|
+
const parser = new OxcParser()
|
|
65
41
|
|
|
66
|
-
// Write a test source file
|
|
67
42
|
const srcDir = path.join(FIXTURE_DIR, 'src', 'app')
|
|
68
43
|
await fs.mkdir(srcDir, { recursive: true })
|
|
44
|
+
await fs.mkdir(path.join(FIXTURE_DIR, 'src', 'base'), { recursive: true })
|
|
45
|
+
await fs.mkdir(path.join(FIXTURE_DIR, 'src', 'lib'), { recursive: true })
|
|
46
|
+
|
|
47
|
+
await fs.writeFile(path.join(FIXTURE_DIR, 'src', 'base', 'core.ts'), 'export const c = 1')
|
|
48
|
+
await fs.writeFile(path.join(FIXTURE_DIR, 'src', 'lib', 'shared.ts'), 'export const b = 1')
|
|
49
|
+
await fs.writeFile(path.join(FIXTURE_DIR, 'src', 'app', 'local.ts'), 'export const a = 1')
|
|
50
|
+
|
|
69
51
|
const filePath = path.join(srcDir, 'index.ts')
|
|
70
52
|
await fs.writeFile(filePath, `
|
|
71
53
|
import { a } from '@app/local'
|
|
@@ -77,23 +59,19 @@ describe('ts-parser config "extends" resolution', () => {
|
|
|
77
59
|
const parsed = await parser.parse(filePath, await fs.readFile(filePath, 'utf-8'))
|
|
78
60
|
const resolved = parser.resolveImports([parsed], FIXTURE_DIR)[0]
|
|
79
61
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const impBase = resolved.imports.find(i => i.source === '@base/core')
|
|
83
|
-
expect(impBase?.resolvedPath).toBe('src/base/core.ts')
|
|
62
|
+
const impApp = resolved.imports.find(i => i.source === '@app/local')
|
|
63
|
+
expect(impApp?.resolvedPath).toBe(path.join(FIXTURE_DIR, 'src/app/local.ts').replace(/\\/g, '/'))
|
|
84
64
|
|
|
85
|
-
// Mid config mapping: @lib/* -> src/lib/*
|
|
86
65
|
const impLib = resolved.imports.find(i => i.source === '@lib/shared')
|
|
87
|
-
expect(impLib?.resolvedPath).toBe('src/lib/shared.ts')
|
|
66
|
+
expect(impLib?.resolvedPath).toBe(path.join(FIXTURE_DIR, 'src/lib/shared.ts').replace(/\\/g, '/'))
|
|
88
67
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
expect(impApp?.resolvedPath).toBe('src/app/local.ts')
|
|
68
|
+
const impBase = resolved.imports.find(i => i.source === '@base/core')
|
|
69
|
+
expect(impBase?.resolvedPath).toBe(path.join(FIXTURE_DIR, 'src/base/core.ts').replace(/\\/g, '/'))
|
|
92
70
|
})
|
|
93
71
|
})
|
|
94
72
|
|
|
95
73
|
describe('TypeScriptParser Edge Cases & Fault Tolerance', () => {
|
|
96
|
-
const parser = new
|
|
74
|
+
const parser = new OxcParser()
|
|
97
75
|
|
|
98
76
|
it('handles completely empty files', async () => {
|
|
99
77
|
const result = await parser.parse('src/empty.ts', '')
|
|
@@ -117,11 +95,8 @@ describe('TypeScriptParser Edge Cases & Fault Tolerance', () => {
|
|
|
117
95
|
export class HalfClass implements {
|
|
118
96
|
`
|
|
119
97
|
const result = await parser.parse('src/broken.ts', malformedCode)
|
|
120
|
-
|
|
121
|
-
// Should not crash, and should extract whatever it can
|
|
122
98
|
expect(result.functions.length).toBeGreaterThanOrEqual(0)
|
|
123
99
|
expect(result.classes.length).toBeGreaterThanOrEqual(0)
|
|
124
|
-
// Must maintain object structure
|
|
125
100
|
expect(Array.isArray(result.imports)).toBe(true)
|
|
126
101
|
expect(typeof result.hash).toBe('string')
|
|
127
102
|
expect(result.language).toBe('typescript')
|