@getmikk/core 1.7.1 → 1.8.1
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 +82 -412
- package/package.json +3 -1
- package/src/contract/contract-reader.ts +2 -2
- package/src/contract/lock-compiler.ts +15 -14
- package/src/contract/lock-reader.ts +14 -14
- package/src/contract/schema.ts +3 -3
- package/src/index.ts +2 -1
- package/src/parser/base-parser.ts +1 -1
- package/src/parser/boundary-checker.ts +74 -212
- package/src/parser/go/go-extractor.ts +10 -10
- package/src/parser/go/go-parser.ts +2 -2
- package/src/parser/index.ts +45 -31
- package/src/parser/javascript/js-extractor.ts +9 -9
- package/src/parser/javascript/js-parser.ts +2 -2
- package/src/parser/tree-sitter/parser.ts +228 -0
- package/src/parser/tree-sitter/queries.ts +181 -0
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +15 -15
- package/src/parser/typescript/ts-parser.ts +1 -1
- package/src/parser/typescript/ts-resolver.ts +2 -2
- package/src/search/bm25.ts +206 -0
- package/src/search/index.ts +3 -0
- package/src/utils/fs.ts +95 -31
- package/src/utils/minimatch.ts +23 -14
- package/test-output.txt +0 -0
- package/tests/go-parser.test.ts +10 -10
- package/tests/js-parser.test.ts +34 -19
- package/tests/parser.test.ts +5 -5
- package/tests/tree-sitter-parser.test.ts +168 -0
- package/tests/ts-parser.test.ts +49 -1
- package/out.log +0 -0
|
@@ -3,7 +3,7 @@ import { MikkLockSchema, type MikkLock } from './schema.js'
|
|
|
3
3
|
import { LockNotFoundError } from '../utils/errors.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* LockReader
|
|
6
|
+
* LockReader -- reads and validates mikk.lock.json from disk.
|
|
7
7
|
* Uses compact format on disk: default values are omitted to save space.
|
|
8
8
|
* Hydrates omitted fields before validation; compactifies before writing.
|
|
9
9
|
*/
|
|
@@ -17,7 +17,7 @@ export class LockReader {
|
|
|
17
17
|
throw new LockNotFoundError()
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const json = JSON.parse(content)
|
|
20
|
+
const json = JSON.parse(content.replace(/^\uFEFF/, ''))
|
|
21
21
|
const hydrated = hydrateLock(json)
|
|
22
22
|
const result = MikkLockSchema.safeParse(hydrated)
|
|
23
23
|
|
|
@@ -38,11 +38,11 @@ export class LockReader {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
|
-
// Compact format
|
|
41
|
+
// Compact format -- omit-defaults serialization
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
// Rules:
|
|
44
44
|
// 1. Never write a field whose value equals its default ([], "", undefined, "unknown")
|
|
45
|
-
// 2. id/name/file/path are derivable from the record key
|
|
45
|
+
// 2. id/name/file/path are derivable from the record key omit them
|
|
46
46
|
// 3. Line ranges become tuples: [startLine, endLine]
|
|
47
47
|
// 4. errorHandling becomes tuples: [line, type, detail]
|
|
48
48
|
// 5. detailedLines becomes tuples: [startLine, endLine, blockType]
|
|
@@ -65,7 +65,7 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
65
65
|
fnKeys.forEach((k, i) => fnIndexMap.set(k, i))
|
|
66
66
|
out.fnIndex = fnKeys
|
|
67
67
|
|
|
68
|
-
// Functions
|
|
68
|
+
// Functions -- biggest savings
|
|
69
69
|
out.functions = {}
|
|
70
70
|
for (let idx = 0; idx < fnKeys.length; idx++) {
|
|
71
71
|
const fn = lock.functions[fnKeys[idx]]
|
|
@@ -123,10 +123,10 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// Modules
|
|
126
|
+
// Modules -- keep as-is (already small)
|
|
127
127
|
out.modules = lock.modules
|
|
128
128
|
|
|
129
|
-
// Files
|
|
129
|
+
// Files -- strip redundant path (it's the key)
|
|
130
130
|
out.files = {}
|
|
131
131
|
for (const [key, file] of Object.entries(lock.files)) {
|
|
132
132
|
const c: any = {
|
|
@@ -138,12 +138,12 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
138
138
|
out.files[key] = c
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// Context files
|
|
141
|
+
// Context files -- paths/type only, no content
|
|
142
142
|
if (lock.contextFiles && lock.contextFiles.length > 0) {
|
|
143
143
|
out.contextFiles = lock.contextFiles.map(({ path, type, size }) => ({ path, type, size }))
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// Routes
|
|
146
|
+
// Routes -- keep as-is (already compact)
|
|
147
147
|
if (lock.routes && lock.routes.length > 0) {
|
|
148
148
|
out.routes = lock.routes
|
|
149
149
|
}
|
|
@@ -158,7 +158,7 @@ function hydrateLock(raw: any): any {
|
|
|
158
158
|
// If it already has the old format (functions have id/name/file), pass through
|
|
159
159
|
const firstFn = Object.values(raw.functions || {})[0] as any
|
|
160
160
|
if (firstFn && typeof firstFn === 'object' && 'id' in firstFn && 'name' in firstFn && 'file' in firstFn) {
|
|
161
|
-
return raw // Already in full format
|
|
161
|
+
return raw // Already in full format -- no hydration needed
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
const out: any = {
|
|
@@ -174,7 +174,7 @@ function hydrateLock(raw: any): any {
|
|
|
174
174
|
const fnIndex: string[] = raw.fnIndex || []
|
|
175
175
|
const hasFnIndex = fnIndex.length > 0
|
|
176
176
|
|
|
177
|
-
// P6: build file
|
|
177
|
+
// P6: build file->moduleId map before function loop
|
|
178
178
|
const fileModuleMap: Record<string, string> = {}
|
|
179
179
|
for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
|
|
180
180
|
fileModuleMap[key] = c.moduleId || 'unknown'
|
|
@@ -183,11 +183,11 @@ function hydrateLock(raw: any): any {
|
|
|
183
183
|
// Hydrate functions
|
|
184
184
|
out.functions = {}
|
|
185
185
|
for (const [key, c] of Object.entries(raw.functions || {}) as [string, any][]) {
|
|
186
|
-
// P7: key is integer index
|
|
186
|
+
// P7: key is integer index -> look up full ID via fnIndex
|
|
187
187
|
const fullId = hasFnIndex ? (fnIndex[parseInt(key)] || key) : key
|
|
188
188
|
const { name, file } = parseEntityKey(fullId, 'fn:')
|
|
189
189
|
const lines = c.lines || [c.startLine || 0, c.endLine || 0]
|
|
190
|
-
// P7: integer calls/calledBy
|
|
190
|
+
// P7: integer calls/calledBy -> resolve to full string IDs (backward compat: strings pass through)
|
|
191
191
|
const calls = (c.calls || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
|
|
192
192
|
const calledBy = (c.calledBy || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
|
|
193
193
|
|
|
@@ -277,7 +277,7 @@ function hydrateLock(raw: any): any {
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
// Modules
|
|
280
|
+
// Modules -- already in full format
|
|
281
281
|
out.modules = raw.modules
|
|
282
282
|
|
|
283
283
|
// Pass through
|
package/src/contract/schema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
// ─── mikk.json schema
|
|
3
|
+
// ─── mikk.json schema ───────────────────────────────────
|
|
4
4
|
|
|
5
5
|
export const MikkModuleSchema = z.object({
|
|
6
6
|
id: z.string(),
|
|
@@ -33,7 +33,7 @@ export const MikkContractSchema = z.object({
|
|
|
33
33
|
description: z.string(),
|
|
34
34
|
language: z.string(),
|
|
35
35
|
framework: z.string().optional(),
|
|
36
|
-
entryPoints: z.array(z.string()),
|
|
36
|
+
entryPoints: z.array(z.string()).default([]),
|
|
37
37
|
}),
|
|
38
38
|
declared: z.object({
|
|
39
39
|
modules: z.array(MikkModuleSchema),
|
|
@@ -47,7 +47,7 @@ export type MikkContract = z.infer<typeof MikkContractSchema>
|
|
|
47
47
|
export type MikkModule = z.infer<typeof MikkModuleSchema>
|
|
48
48
|
export type MikkDecision = z.infer<typeof MikkDecisionSchema>
|
|
49
49
|
|
|
50
|
-
// ─── mikk.lock.json schema
|
|
50
|
+
// ─── mikk.lock.json schema ──────────────────────────────
|
|
51
51
|
|
|
52
52
|
export const MikkLockFunctionSchema = z.object({
|
|
53
53
|
id: z.string(),
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,10 @@ export * from './parser/index.js'
|
|
|
5
5
|
export * from './graph/index.js'
|
|
6
6
|
export * from './contract/index.js'
|
|
7
7
|
export * from './hash/index.js'
|
|
8
|
+
export * from './search/index.js'
|
|
8
9
|
export * from './utils/errors.js'
|
|
9
10
|
export * from './utils/logger.js'
|
|
10
|
-
export { discoverFiles, discoverContextFiles, readFileContent, writeFileContent, fileExists, setupMikkDirectory, readMikkIgnore, parseMikkIgnore, detectProjectLanguage, getDiscoveryPatterns, generateMikkIgnore } from './utils/fs.js'
|
|
11
|
+
export { discoverFiles, discoverContextFiles, readFileContent, writeFileContent, fileExists, setupMikkDirectory, readMikkIgnore, parseMikkIgnore, detectProjectLanguage, getDiscoveryPatterns, generateMikkIgnore, updateGitIgnore, cleanupGitIgnore } from './utils/fs.js'
|
|
11
12
|
export type { ContextFile, ContextFileType, ProjectLanguage } from './utils/fs.js'
|
|
12
13
|
export { minimatch } from './utils/minimatch.js'
|
|
13
14
|
export { scoreFunctions, findFuzzyMatches, levenshtein, splitCamelCase, extractKeywords } from './utils/fuzzy-match.js'
|
|
@@ -6,7 +6,7 @@ import type { ParsedFile, ParsedImport } from './types.js'
|
|
|
6
6
|
*/
|
|
7
7
|
export abstract class BaseParser {
|
|
8
8
|
/** Given raw file content as a string, return ParsedFile */
|
|
9
|
-
abstract parse(filePath: string, content: string): ParsedFile
|
|
9
|
+
abstract parse(filePath: string, content: string): Promise<ParsedFile>
|
|
10
10
|
|
|
11
11
|
/** Given a list of parsed files, resolve all import paths to absolute project paths */
|
|
12
12
|
abstract resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[]
|
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
|
-
import type { MikkContract, MikkLock, MikkLockFunction } from '
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Types
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
2
|
+
import type { MikkContract, MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
7
3
|
|
|
8
4
|
export type ViolationSeverity = 'error' | 'warning'
|
|
9
5
|
|
|
10
6
|
export interface BoundaryViolation {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
functionId: string
|
|
14
|
-
functionName: string
|
|
15
|
-
file: string
|
|
16
|
-
moduleId: string
|
|
17
|
-
moduleName: string
|
|
18
|
-
}
|
|
19
|
-
/** The function being illegally called */
|
|
20
|
-
to: {
|
|
21
|
-
functionId: string
|
|
22
|
-
functionName: string
|
|
23
|
-
file: string
|
|
24
|
-
moduleId: string
|
|
25
|
-
moduleName: string
|
|
26
|
-
}
|
|
7
|
+
from: { functionId: string; functionName: string; file: string; moduleId: string; moduleName: string }
|
|
8
|
+
to: { functionId: string; functionName: string; file: string; moduleId: string; moduleName: string }
|
|
27
9
|
rule: string
|
|
28
10
|
severity: ViolationSeverity
|
|
29
11
|
}
|
|
@@ -34,251 +16,131 @@ export interface BoundaryCheckResult {
|
|
|
34
16
|
summary: string
|
|
35
17
|
}
|
|
36
18
|
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Rule parsing
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse constraint strings into structured allow/deny rules.
|
|
43
|
-
*
|
|
44
|
-
* Supported syntax in mikk.json constraints:
|
|
45
|
-
* "module:auth cannot import module:payments"
|
|
46
|
-
* "module:cli cannot import module:db"
|
|
47
|
-
* "module:core has no imports" → core is completely isolated
|
|
48
|
-
* "module:api can only import module:core, module:utils"
|
|
49
|
-
*/
|
|
50
19
|
interface ParsedRule {
|
|
51
20
|
type: 'deny' | 'allow_only' | 'isolated'
|
|
52
21
|
fromModuleId: string
|
|
53
|
-
toModuleIds: string[]
|
|
22
|
+
toModuleIds: string[]
|
|
54
23
|
raw: string
|
|
55
24
|
}
|
|
56
25
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// "module:X cannot import module:Y" or "module:X cannot import module:Y, module:Z"
|
|
61
|
-
const denyMatch = c.match(/^module:(\S+)\s+cannot\s+import\s+(.+)$/)
|
|
62
|
-
if (denyMatch) {
|
|
63
|
-
const toModules = denyMatch[2]
|
|
64
|
-
.split(',')
|
|
65
|
-
.map(s => s.trim().replace('module:', ''))
|
|
66
|
-
.filter(Boolean)
|
|
67
|
-
return { type: 'deny', fromModuleId: denyMatch[1], toModuleIds: toModules, raw: constraint }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// "module:X can only import module:A, module:B"
|
|
71
|
-
const allowOnlyMatch = c.match(/^module:(\S+)\s+can\s+only\s+import\s+(.+)$/)
|
|
72
|
-
if (allowOnlyMatch) {
|
|
73
|
-
const toModules = allowOnlyMatch[2]
|
|
74
|
-
.split(',')
|
|
75
|
-
.map(s => s.trim().replace('module:', ''))
|
|
76
|
-
.filter(Boolean)
|
|
77
|
-
return { type: 'allow_only', fromModuleId: allowOnlyMatch[1], toModuleIds: toModules, raw: constraint }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// "module:X has no imports" or "module:X is isolated"
|
|
81
|
-
const isolatedMatch = c.match(/^module:(\S+)\s+(has\s+no\s+imports|is\s+isolated)$/)
|
|
82
|
-
if (isolatedMatch) {
|
|
83
|
-
return { type: 'isolated', fromModuleId: isolatedMatch[1], toModuleIds: [], raw: constraint }
|
|
84
|
-
}
|
|
26
|
+
function stripPrefix(s: string): string {
|
|
27
|
+
return s.trim().replace(/^module:/, '')
|
|
28
|
+
}
|
|
85
29
|
|
|
86
|
-
|
|
30
|
+
function parseList(raw: string): string[] {
|
|
31
|
+
return raw.split(/,\s*/).map(stripPrefix).filter(Boolean)
|
|
87
32
|
}
|
|
88
33
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
34
|
+
function parseConstraint(constraint: string): ParsedRule | null {
|
|
35
|
+
const c = constraint.trim()
|
|
36
|
+
const l = c.toLowerCase()
|
|
37
|
+
const natDeny = l.match(/^(\S+)\s+(?:must\s+not|cannot|should\s+not)\s+(?:import\s+from|import|call\s+into|call)\s+(.+)$/)
|
|
38
|
+
if (natDeny) return { type: 'deny', fromModuleId: stripPrefix(natDeny[1]), toModuleIds: parseList(natDeny[2]), raw: c }
|
|
39
|
+
const natAllow = l.match(/^(\S+)\s+can\s+only\s+(?:import\s+from|import)\s+(.+)$/)
|
|
40
|
+
if (natAllow) return { type: 'allow_only', fromModuleId: stripPrefix(natAllow[1]), toModuleIds: parseList(natAllow[2]), raw: c }
|
|
41
|
+
const natIso = l.match(/^(\S+)\s+(?:is\s+isolated|has\s+no\s+imports)$/)
|
|
42
|
+
if (natIso) return { type: 'isolated', fromModuleId: stripPrefix(natIso[1]), toModuleIds: [], raw: c }
|
|
43
|
+
const legDeny = l.match(/^module:(\S+)\s+cannot\s+import\s+(.+)$/)
|
|
44
|
+
if (legDeny) return { type: 'deny', fromModuleId: legDeny[1], toModuleIds: parseList(legDeny[2]), raw: c }
|
|
45
|
+
const legAllow = l.match(/^module:(\S+)\s+can\s+only\s+import\s+(.+)$/)
|
|
46
|
+
if (legAllow) return { type: 'allow_only', fromModuleId: legAllow[1], toModuleIds: parseList(legAllow[2]), raw: c }
|
|
47
|
+
const legIso = l.match(/^module:(\S+)\s+(?:has\s+no\s+imports|is\s+isolated)$/)
|
|
48
|
+
if (legIso) return { type: 'isolated', fromModuleId: legIso[1], toModuleIds: [], raw: c }
|
|
49
|
+
console.warn(`[mikk] Constraint skipped: "${c}" - use "auth must not import from payments"`)
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
92
52
|
|
|
93
|
-
/**
|
|
94
|
-
* BoundaryChecker — walks the lock file's call graph and checks every
|
|
95
|
-
* cross-module call against the rules declared in mikk.json constraints.
|
|
96
|
-
*
|
|
97
|
-
* This is the CI-ready enforcement layer.
|
|
98
|
-
*/
|
|
99
53
|
export class BoundaryChecker {
|
|
100
54
|
private rules: ParsedRule[]
|
|
101
|
-
private moduleNames: Map<string, string>
|
|
102
|
-
|
|
103
|
-
constructor(
|
|
104
|
-
private contract: MikkContract,
|
|
105
|
-
private lock: MikkLock
|
|
106
|
-
) {
|
|
107
|
-
this.rules = contract.declared.constraints
|
|
108
|
-
.map(parseConstraint)
|
|
109
|
-
.filter((r): r is ParsedRule => r !== null)
|
|
55
|
+
private moduleNames: Map<string, string>
|
|
110
56
|
|
|
57
|
+
constructor(private contract: MikkContract, private lock: MikkLock) {
|
|
58
|
+
this.rules = contract.declared.constraints.map(parseConstraint).filter((r): r is ParsedRule => r !== null)
|
|
111
59
|
this.moduleNames = new Map(contract.declared.modules.map(m => [m.id, m.name]))
|
|
112
60
|
}
|
|
113
61
|
|
|
114
|
-
/** Run boundary check. Returns pass/fail + all violations. */
|
|
115
62
|
check(): BoundaryCheckResult {
|
|
116
63
|
const violations: BoundaryViolation[] = []
|
|
117
64
|
|
|
118
|
-
// Pass 1: Check cross-module function calls
|
|
119
65
|
for (const fn of Object.values(this.lock.functions)) {
|
|
66
|
+
if (fn.moduleId === 'unknown') continue
|
|
120
67
|
for (const calleeId of fn.calls) {
|
|
121
68
|
const callee = this.lock.functions[calleeId]
|
|
122
|
-
if (!callee) continue
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Check this cross-module call against all parsed rules
|
|
126
|
-
const violation = this.checkCall(fn, callee)
|
|
127
|
-
if (violation) violations.push(violation)
|
|
69
|
+
if (!callee || callee.moduleId === 'unknown' || fn.moduleId === callee.moduleId) continue
|
|
70
|
+
const v = this.checkCall(fn, callee)
|
|
71
|
+
if (v) violations.push(v)
|
|
128
72
|
}
|
|
129
73
|
}
|
|
130
74
|
|
|
131
|
-
// Pass 2: Check cross-module file-level imports
|
|
132
75
|
for (const file of Object.values(this.lock.files)) {
|
|
133
|
-
if (
|
|
76
|
+
if (file.moduleId === 'unknown' || !file.imports?.length) continue
|
|
134
77
|
for (const importedPath of file.imports) {
|
|
135
78
|
const importedFile = this.lock.files[importedPath]
|
|
136
|
-
if (!importedFile) continue
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const violation = this.checkFileImport(file, importedFile)
|
|
140
|
-
if (violation) violations.push(violation)
|
|
79
|
+
if (!importedFile || importedFile.moduleId === 'unknown' || file.moduleId === importedFile.moduleId) continue
|
|
80
|
+
const v = this.checkFileImport(file, importedFile)
|
|
81
|
+
if (v) violations.push(v)
|
|
141
82
|
}
|
|
142
83
|
}
|
|
143
84
|
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
85
|
+
const seen = new Set<string>()
|
|
86
|
+
const unique = violations.filter(v => {
|
|
87
|
+
const key = `${v.from.functionId}|${v.to.functionId}|${v.rule}`
|
|
88
|
+
if (seen.has(key)) return false
|
|
89
|
+
seen.add(key)
|
|
90
|
+
return true
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const fnCount = Object.keys(this.lock.functions).length
|
|
94
|
+
const fileCount = Object.keys(this.lock.files).length
|
|
95
|
+
const errorCount = unique.filter(v => v.severity === 'error').length
|
|
96
|
+
const warnCount = unique.filter(v => v.severity === 'warning').length
|
|
97
|
+
const summary = unique.length === 0
|
|
98
|
+
? `All module boundaries respected (${fnCount} functions, ${fileCount} files checked)`
|
|
99
|
+
: `${errorCount} boundary error(s), ${warnCount} warning(s) found`
|
|
100
|
+
|
|
101
|
+
return { pass: errorCount === 0, violations: unique, summary }
|
|
156
102
|
}
|
|
157
103
|
|
|
158
|
-
|
|
159
|
-
* Check a single cross-module call against parsed rules.
|
|
160
|
-
* Returns a violation if the call is forbidden, null if it's allowed.
|
|
161
|
-
*/
|
|
162
|
-
private checkCall(
|
|
163
|
-
caller: MikkLockFunction,
|
|
164
|
-
callee: MikkLockFunction
|
|
165
|
-
): BoundaryViolation | null {
|
|
104
|
+
private checkCall(caller: MikkLockFunction, callee: MikkLockFunction): BoundaryViolation | null {
|
|
166
105
|
for (const rule of this.rules) {
|
|
167
106
|
if (rule.fromModuleId !== caller.moduleId) continue
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
} else if (rule.type === 'deny') {
|
|
176
|
-
// Module may not call into these specific modules
|
|
177
|
-
forbidden = rule.toModuleIds.includes(callee.moduleId)
|
|
178
|
-
} else if (rule.type === 'allow_only') {
|
|
179
|
-
// Module may ONLY call into the listed modules (+ itself)
|
|
180
|
-
forbidden = !rule.toModuleIds.includes(callee.moduleId)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (forbidden) {
|
|
184
|
-
return {
|
|
185
|
-
from: {
|
|
186
|
-
functionId: caller.id,
|
|
187
|
-
functionName: caller.name,
|
|
188
|
-
file: caller.file,
|
|
189
|
-
moduleId: caller.moduleId,
|
|
190
|
-
moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId,
|
|
191
|
-
},
|
|
192
|
-
to: {
|
|
193
|
-
functionId: callee.id,
|
|
194
|
-
functionName: callee.name,
|
|
195
|
-
file: callee.file,
|
|
196
|
-
moduleId: callee.moduleId,
|
|
197
|
-
moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId,
|
|
198
|
-
},
|
|
199
|
-
rule: ruleDesc,
|
|
200
|
-
severity: 'error',
|
|
201
|
-
}
|
|
107
|
+
const forbidden = rule.type === 'isolated' ? true
|
|
108
|
+
: rule.type === 'deny' ? rule.toModuleIds.includes(callee.moduleId)
|
|
109
|
+
: !rule.toModuleIds.includes(callee.moduleId)
|
|
110
|
+
if (forbidden) return {
|
|
111
|
+
from: { functionId: caller.id, functionName: caller.name, file: caller.file, moduleId: caller.moduleId, moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId },
|
|
112
|
+
to: { functionId: callee.id, functionName: callee.name, file: callee.file, moduleId: callee.moduleId, moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId },
|
|
113
|
+
rule: rule.raw, severity: 'error'
|
|
202
114
|
}
|
|
203
115
|
}
|
|
204
116
|
return null
|
|
205
117
|
}
|
|
206
118
|
|
|
207
|
-
|
|
208
|
-
* Check a single cross-module file import against parsed rules.
|
|
209
|
-
* Returns a violation if the import is forbidden, null if it's allowed.
|
|
210
|
-
*/
|
|
211
|
-
private checkFileImport(
|
|
212
|
-
sourceFile: { path: string; moduleId: string },
|
|
213
|
-
targetFile: { path: string; moduleId: string }
|
|
214
|
-
): BoundaryViolation | null {
|
|
119
|
+
private checkFileImport(sourceFile: { path: string; moduleId: string }, targetFile: { path: string; moduleId: string }): BoundaryViolation | null {
|
|
215
120
|
for (const rule of this.rules) {
|
|
216
121
|
if (rule.fromModuleId !== sourceFile.moduleId) continue
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
} else if (rule.type === 'allow_only') {
|
|
225
|
-
forbidden = !rule.toModuleIds.includes(targetFile.moduleId)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (forbidden) {
|
|
229
|
-
return {
|
|
230
|
-
from: {
|
|
231
|
-
functionId: `file:${sourceFile.path}`,
|
|
232
|
-
functionName: path.basename(sourceFile.path),
|
|
233
|
-
file: sourceFile.path,
|
|
234
|
-
moduleId: sourceFile.moduleId,
|
|
235
|
-
moduleName: this.moduleNames.get(sourceFile.moduleId) ?? sourceFile.moduleId,
|
|
236
|
-
},
|
|
237
|
-
to: {
|
|
238
|
-
functionId: `file:${targetFile.path}`,
|
|
239
|
-
functionName: path.basename(targetFile.path),
|
|
240
|
-
file: targetFile.path,
|
|
241
|
-
moduleId: targetFile.moduleId,
|
|
242
|
-
moduleName: this.moduleNames.get(targetFile.moduleId) ?? targetFile.moduleId,
|
|
243
|
-
},
|
|
244
|
-
rule: rule.raw,
|
|
245
|
-
severity: 'error',
|
|
246
|
-
}
|
|
122
|
+
const forbidden = rule.type === 'isolated' ? true
|
|
123
|
+
: rule.type === 'deny' ? rule.toModuleIds.includes(targetFile.moduleId)
|
|
124
|
+
: !rule.toModuleIds.includes(targetFile.moduleId)
|
|
125
|
+
if (forbidden) return {
|
|
126
|
+
from: { functionId: `file:${sourceFile.path}`, functionName: path.basename(sourceFile.path), file: sourceFile.path, moduleId: sourceFile.moduleId, moduleName: this.moduleNames.get(sourceFile.moduleId) ?? sourceFile.moduleId },
|
|
127
|
+
to: { functionId: `file:${targetFile.path}`, functionName: path.basename(targetFile.path), file: targetFile.path, moduleId: targetFile.moduleId, moduleName: this.moduleNames.get(targetFile.moduleId) ?? targetFile.moduleId },
|
|
128
|
+
rule: rule.raw, severity: 'error'
|
|
247
129
|
}
|
|
248
130
|
}
|
|
249
131
|
return null
|
|
250
132
|
}
|
|
251
133
|
|
|
252
|
-
/** Return all cross-module call pairs (useful for generating allow rules) */
|
|
253
134
|
allCrossModuleCalls(): { from: string; to: string; count: number }[] {
|
|
254
135
|
const counts = new Map<string, number>()
|
|
255
|
-
|
|
256
|
-
// Count function-level cross-module calls
|
|
257
136
|
for (const fn of Object.values(this.lock.functions)) {
|
|
258
137
|
for (const calleeId of fn.calls) {
|
|
259
138
|
const callee = this.lock.functions[calleeId]
|
|
260
|
-
if (!callee || fn.moduleId === callee.moduleId) continue
|
|
261
|
-
const key = `${fn.moduleId}
|
|
139
|
+
if (!callee || fn.moduleId === callee.moduleId || fn.moduleId === 'unknown' || callee.moduleId === 'unknown') continue
|
|
140
|
+
const key = `${fn.moduleId}>${callee.moduleId}`
|
|
262
141
|
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
263
142
|
}
|
|
264
143
|
}
|
|
265
|
-
|
|
266
|
-
// Count file-level cross-module imports
|
|
267
|
-
for (const file of Object.values(this.lock.files)) {
|
|
268
|
-
if (!file.imports) continue
|
|
269
|
-
for (const importedPath of file.imports) {
|
|
270
|
-
const importedFile = this.lock.files[importedPath]
|
|
271
|
-
if (!importedFile || file.moduleId === importedFile.moduleId) continue
|
|
272
|
-
const key = `${file.moduleId}→${importedFile.moduleId}`
|
|
273
|
-
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return [...counts.entries()]
|
|
278
|
-
.map(([key, count]) => {
|
|
279
|
-
const [from, to] = key.split('→')
|
|
280
|
-
return { from, to, count }
|
|
281
|
-
})
|
|
282
|
-
.sort((a, b) => b.count - a.count)
|
|
144
|
+
return [...counts.entries()].map(([key, count]) => { const [from, to] = key.split('>'); return { from, to, count } }).sort((a, b) => b.count - a.count)
|
|
283
145
|
}
|
|
284
146
|
}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
ParsedParam, ParsedGeneric, ParsedRoute,
|
|
5
5
|
} from '../types.js'
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// --- Go builtins / keywords to skip when extracting calls -------------------
|
|
8
8
|
const GO_BUILTINS = new Set([
|
|
9
9
|
'if', 'else', 'for', 'switch', 'select', 'case', 'default', 'break',
|
|
10
10
|
'continue', 'goto', 'fallthrough', 'return', 'go', 'defer', 'range',
|
|
@@ -16,7 +16,7 @@ const GO_BUILTINS = new Set([
|
|
|
16
16
|
'complex64', 'complex128', 'bool', 'byte', 'rune', 'error', 'any',
|
|
17
17
|
])
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// --- Route detection patterns (Gin, Echo, Chi, Mux, net/http, Fiber) --------
|
|
20
20
|
type RoutePattern = { re: RegExp; methodGroup: number; pathGroup: number; handlerGroup: number; fixedMethod?: string }
|
|
21
21
|
|
|
22
22
|
const ROUTE_PATTERNS: RoutePattern[] = [
|
|
@@ -48,7 +48,7 @@ const ROUTE_PATTERNS: RoutePattern[] = [
|
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* GoExtractor
|
|
51
|
+
* GoExtractor -- pure regex + stateful line scanner for .go files.
|
|
52
52
|
* Extracts functions, structs (as classes), imports, exports, and HTTP routes
|
|
53
53
|
* without any external Go AST dependency.
|
|
54
54
|
*/
|
|
@@ -62,7 +62,7 @@ export class GoExtractor {
|
|
|
62
62
|
this.lines = content.split('\n')
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
//
|
|
65
|
+
// --- Public API ---------------------------------------------------------
|
|
66
66
|
|
|
67
67
|
/** Extract all top-level functions (no receiver) */
|
|
68
68
|
extractFunctions(): ParsedFunction[] {
|
|
@@ -160,7 +160,7 @@ export class GoExtractor {
|
|
|
160
160
|
return routes
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
//
|
|
163
|
+
// --- Internal scanning ---------------------------------------------------
|
|
164
164
|
|
|
165
165
|
/** Scanned raw function data (before building ParsedFunction) */
|
|
166
166
|
private scanFunctions(): Array<{
|
|
@@ -343,7 +343,7 @@ export class GoExtractor {
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
//
|
|
346
|
+
// --- Signature parsing --------------------------------------------------------
|
|
347
347
|
|
|
348
348
|
interface GoFuncSignature {
|
|
349
349
|
name: string
|
|
@@ -481,7 +481,7 @@ function cleanReturnType(ret: string): string {
|
|
|
481
481
|
return ret
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
//
|
|
484
|
+
// --- Import line parsing ------------------------------------------------------
|
|
485
485
|
|
|
486
486
|
function parseImportLine(line: string): ParsedImport | null {
|
|
487
487
|
const trimmed = line.trim()
|
|
@@ -509,7 +509,7 @@ function parseImportLine(line: string): ParsedImport | null {
|
|
|
509
509
|
return null
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
//
|
|
512
|
+
// --- Body analysis ------------------------------------------------------------
|
|
513
513
|
|
|
514
514
|
/**
|
|
515
515
|
* Statefully track brace depth through content, handling:
|
|
@@ -661,7 +661,7 @@ function extractErrorHandling(bodyLines: string[], baseLineNumber: number): { li
|
|
|
661
661
|
return errors
|
|
662
662
|
}
|
|
663
663
|
|
|
664
|
-
//
|
|
664
|
+
// --- Comment extraction -------------------------------------------------------
|
|
665
665
|
|
|
666
666
|
function extractLeadingComment(lines: string[], funcLine: number): string {
|
|
667
667
|
// Scan backwards from funcLine for consecutive comment lines
|
|
@@ -686,7 +686,7 @@ function extractLeadingComment(lines: string[], funcLine: number): string {
|
|
|
686
686
|
return ''
|
|
687
687
|
}
|
|
688
688
|
|
|
689
|
-
//
|
|
689
|
+
// --- Utility helpers ----------------------------------------------------------
|
|
690
690
|
|
|
691
691
|
function isExported(name: string): boolean {
|
|
692
692
|
return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase()
|
|
@@ -5,12 +5,12 @@ import { hashContent } from '../../hash/file-hasher.js'
|
|
|
5
5
|
import type { ParsedFile } from '../types.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* GoParser
|
|
8
|
+
* GoParser -- implements BaseParser for .go files.
|
|
9
9
|
* Uses GoExtractor (regex-based) to pull structured data from Go source
|
|
10
10
|
* without requiring the Go toolchain.
|
|
11
11
|
*/
|
|
12
12
|
export class GoParser extends BaseParser {
|
|
13
|
-
parse(filePath: string, content: string): ParsedFile {
|
|
13
|
+
async parse(filePath: string, content: string): Promise<ParsedFile> {
|
|
14
14
|
const extractor = new GoExtractor(filePath, content)
|
|
15
15
|
|
|
16
16
|
return {
|