@getmikk/core 1.8.2 → 1.9.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 +3 -1
- 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 +74 -42
- package/src/contract/lock-reader.ts +24 -4
- package/src/contract/schema.ts +27 -1
- package/src/error-handler.ts +430 -0
- package/src/graph/cluster-detector.ts +45 -20
- package/src/graph/confidence-engine.ts +60 -0
- package/src/graph/dead-code-detector.ts +27 -5
- package/src/graph/graph-builder.ts +298 -238
- package/src/graph/impact-analyzer.ts +131 -114
- package/src/graph/index.ts +4 -0
- package/src/graph/memory-manager.ts +345 -0
- package/src/graph/query-engine.ts +79 -0
- package/src/graph/risk-engine.ts +86 -0
- package/src/graph/types.ts +89 -64
- package/src/parser/boundary-checker.ts +3 -1
- package/src/parser/change-detector.ts +99 -0
- package/src/parser/go/go-extractor.ts +28 -9
- package/src/parser/go/go-parser.ts +2 -0
- package/src/parser/index.ts +88 -38
- package/src/parser/javascript/js-extractor.ts +1 -1
- package/src/parser/javascript/js-parser.ts +2 -0
- package/src/parser/oxc-parser.ts +675 -0
- package/src/parser/oxc-resolver.ts +83 -0
- package/src/parser/tree-sitter/parser.ts +27 -15
- package/src/parser/types.ts +100 -73
- package/src/parser/typescript/ts-extractor.ts +241 -537
- package/src/parser/typescript/ts-parser.ts +16 -171
- package/src/parser/typescript/ts-resolver.ts +11 -1
- package/src/search/bm25.ts +5 -2
- 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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ResolverFactory } from 'oxc-resolver';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import type { ParsedFile } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OxcResolver — Rust-backed compiler-grade module resolution.
|
|
8
|
+
*
|
|
9
|
+
* Resolution strategy:
|
|
10
|
+
* 1. If the resolved path is inside projectRoot → return project-relative posix path
|
|
11
|
+
* 2. If the resolved path is outside projectRoot (node_modules, monorepo peer) → return ''
|
|
12
|
+
* (external deps produce no graph edges; they're not in our file set)
|
|
13
|
+
* 3. On any error → return '' (unresolved, no false edges)
|
|
14
|
+
*
|
|
15
|
+
* All returned paths use forward slashes and are ABSOLUTE, matching what
|
|
16
|
+
* parseFiles passes to parse(). graph-builder only creates import edges when
|
|
17
|
+
* the target path exists as a file node — so no path format inconsistency can
|
|
18
|
+
* create false positive edges.
|
|
19
|
+
*/
|
|
20
|
+
export class OxcResolver {
|
|
21
|
+
private resolver: any;
|
|
22
|
+
private readonly normalizedRoot: string;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly projectRoot: string) {
|
|
25
|
+
this.normalizedRoot = path.resolve(projectRoot).replace(/\\/g, '/');
|
|
26
|
+
|
|
27
|
+
const tsconfigPath = path.resolve(projectRoot, 'tsconfig.json');
|
|
28
|
+
const hasTsConfig = fs.existsSync(tsconfigPath);
|
|
29
|
+
|
|
30
|
+
this.resolver = new ResolverFactory({
|
|
31
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.mjs', '.cjs', '.mts', '.cts'],
|
|
32
|
+
mainFields: ['module', 'main', 'jsnext:main'],
|
|
33
|
+
mainFiles: ['index', 'main', 'app'],
|
|
34
|
+
conditionNames: ['import', 'require', 'node', 'default', 'types', 'browser'],
|
|
35
|
+
symlinks: true,
|
|
36
|
+
modules: ['node_modules'],
|
|
37
|
+
tsconfig: hasTsConfig ? {
|
|
38
|
+
configFile: tsconfigPath,
|
|
39
|
+
references: 'auto',
|
|
40
|
+
} : undefined,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a single import source string relative to fromFile.
|
|
46
|
+
* fromFile MUST be an absolute path (as produced by parseFiles).
|
|
47
|
+
* Returns an absolute posix path, or '' if unresolvable/external.
|
|
48
|
+
*/
|
|
49
|
+
public resolve(source: string, fromFile: string): string {
|
|
50
|
+
try {
|
|
51
|
+
const absFrom = path.isAbsolute(fromFile)
|
|
52
|
+
? fromFile
|
|
53
|
+
: path.resolve(this.projectRoot, fromFile);
|
|
54
|
+
const dir = path.dirname(absFrom);
|
|
55
|
+
|
|
56
|
+
const result = this.resolver.sync(dir, source);
|
|
57
|
+
if (!result?.path) return '';
|
|
58
|
+
|
|
59
|
+
const resolved = result.path.replace(/\\/g, '/');
|
|
60
|
+
|
|
61
|
+
// Only include files within our project root in the graph.
|
|
62
|
+
// node_modules, hoisted workspace deps, etc. are external.
|
|
63
|
+
if (!resolved.startsWith(this.normalizedRoot + '/') && resolved !== this.normalizedRoot) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return resolved;
|
|
68
|
+
} catch {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve all imports for a batch of files in one pass */
|
|
74
|
+
public resolveBatch(files: ParsedFile[]): ParsedFile[] {
|
|
75
|
+
return files.map(file => ({
|
|
76
|
+
...file,
|
|
77
|
+
imports: file.imports.map(imp => ({
|
|
78
|
+
...imp,
|
|
79
|
+
resolvedPath: this.resolve(imp.source, file.path),
|
|
80
|
+
})),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -109,8 +109,8 @@ function findFirstChild(node: any, predicate: (n: any) => boolean): any {
|
|
|
109
109
|
function assignCallsToFunctions(
|
|
110
110
|
functions: ParsedFunction[],
|
|
111
111
|
callEntries: Array<{ name: string; line: number }>
|
|
112
|
-
): string
|
|
113
|
-
const unassigned: string
|
|
112
|
+
): Array<{ name: string; line: number }> {
|
|
113
|
+
const unassigned: Array<{ name: string; line: number }> = []
|
|
114
114
|
for (const { name, line } of callEntries) {
|
|
115
115
|
// Find the innermost (smallest range) function that contains this line
|
|
116
116
|
let best: ParsedFunction | null = null
|
|
@@ -125,11 +125,11 @@ function assignCallsToFunctions(
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
if (best) {
|
|
128
|
-
if (!best.calls.
|
|
129
|
-
best.calls.push(name)
|
|
128
|
+
if (!best.calls.some(c => c.name === name && c.line === line)) {
|
|
129
|
+
best.calls.push({ name, line, type: 'function' })
|
|
130
130
|
}
|
|
131
131
|
} else {
|
|
132
|
-
unassigned.push(name)
|
|
132
|
+
unassigned.push({ name, line })
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
return unassigned
|
|
@@ -142,6 +142,7 @@ function assignCallsToFunctions(
|
|
|
142
142
|
export class TreeSitterParser extends BaseParser {
|
|
143
143
|
private parser: any = null
|
|
144
144
|
private languages = new Map<string, any>()
|
|
145
|
+
private nameCounter = new Map<string, number>()
|
|
145
146
|
|
|
146
147
|
getSupportedExtensions(): string[] {
|
|
147
148
|
return ['.py', '.java', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.go', '.rs', '.php', '.rb']
|
|
@@ -155,6 +156,7 @@ export class TreeSitterParser extends BaseParser {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
async parse(filePath: string, content: string): Promise<ParsedFile> {
|
|
159
|
+
this.nameCounter.clear()
|
|
158
160
|
await this.init()
|
|
159
161
|
const ext = path.extname(filePath).toLowerCase()
|
|
160
162
|
const config = await this.getLanguageConfig(ext)
|
|
@@ -185,10 +187,13 @@ export class TreeSitterParser extends BaseParser {
|
|
|
185
187
|
// --- Calls: record name and line position ---
|
|
186
188
|
if (captures['call.name']) {
|
|
187
189
|
const callNode = captures['call.name']
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
const name = callNode.text
|
|
191
|
+
if (name) {
|
|
192
|
+
callEntries.push({
|
|
193
|
+
name,
|
|
194
|
+
line: (callNode.startPosition?.row ?? 0) + 1,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
192
197
|
continue
|
|
193
198
|
}
|
|
194
199
|
|
|
@@ -215,11 +220,12 @@ export class TreeSitterParser extends BaseParser {
|
|
|
215
220
|
const startLine = defNode.startPosition.row + 1
|
|
216
221
|
const endLine = defNode.endPosition.row + 1
|
|
217
222
|
const nodeText = defNode.text ?? ''
|
|
223
|
+
const count = (this.nameCounter.get(fnName) ?? 0) + 1
|
|
224
|
+
this.nameCounter.set(fnName, count)
|
|
218
225
|
|
|
219
|
-
// Unique ID:
|
|
220
|
-
let fnId = `fn:${filePath}:${fnName}:${
|
|
226
|
+
// Unique ID: use stable format with counter for collisions
|
|
227
|
+
let fnId = count === 1 ? `fn:${filePath}:${fnName}` : `fn:${filePath}:${fnName}#${count}`
|
|
221
228
|
if (seenFnIds.has(fnId)) {
|
|
222
|
-
// Extremely rare duplicate — skip rather than corrupt
|
|
223
229
|
continue
|
|
224
230
|
}
|
|
225
231
|
seenFnIds.add(fnId)
|
|
@@ -269,7 +275,7 @@ export class TreeSitterParser extends BaseParser {
|
|
|
269
275
|
const startLine = defNode.startPosition.row + 1
|
|
270
276
|
const endLine = defNode.endPosition.row + 1
|
|
271
277
|
const nodeText = defNode.text ?? ''
|
|
272
|
-
const clsId = `
|
|
278
|
+
const clsId = `class:${filePath}:${clsName}` // consistent with ts-extractor
|
|
273
279
|
|
|
274
280
|
if (!classesMap.has(clsId)) {
|
|
275
281
|
classesMap.set(clsId, {
|
|
@@ -279,7 +285,9 @@ export class TreeSitterParser extends BaseParser {
|
|
|
279
285
|
startLine,
|
|
280
286
|
endLine,
|
|
281
287
|
methods: [],
|
|
288
|
+
properties: [],
|
|
282
289
|
isExported: isExportedByLanguage(ext, clsName, nodeText),
|
|
290
|
+
hash: hashContent(nodeText),
|
|
283
291
|
})
|
|
284
292
|
}
|
|
285
293
|
}
|
|
@@ -300,9 +308,9 @@ export class TreeSitterParser extends BaseParser {
|
|
|
300
308
|
endLine: lineCount || 1,
|
|
301
309
|
params: [],
|
|
302
310
|
returnType: 'void',
|
|
303
|
-
isExported:
|
|
311
|
+
isExported: false, // Don't export the synthetic module function
|
|
304
312
|
isAsync: false,
|
|
305
|
-
calls:
|
|
313
|
+
calls: unassignedCalls.map(c => ({ name: c.name, line: c.line, type: 'function' })),
|
|
306
314
|
hash: '',
|
|
307
315
|
purpose: 'Module-level initialization code',
|
|
308
316
|
edgeCasesHandled: [],
|
|
@@ -330,6 +338,8 @@ export class TreeSitterParser extends BaseParser {
|
|
|
330
338
|
file: filePath,
|
|
331
339
|
})),
|
|
332
340
|
routes: [],
|
|
341
|
+
variables: [],
|
|
342
|
+
calls: [],
|
|
333
343
|
hash: hashContent(content),
|
|
334
344
|
parsedAt: Date.now(),
|
|
335
345
|
}
|
|
@@ -352,6 +362,8 @@ export class TreeSitterParser extends BaseParser {
|
|
|
352
362
|
imports: [],
|
|
353
363
|
exports: [],
|
|
354
364
|
routes: [],
|
|
365
|
+
variables: [],
|
|
366
|
+
calls: [],
|
|
355
367
|
hash: hashContent(content),
|
|
356
368
|
parsedAt: Date.now(),
|
|
357
369
|
}
|
package/src/parser/types.ts
CHANGED
|
@@ -1,103 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parser types — data shapes that flow through the entire Mikk system.
|
|
3
|
-
* Parser produces them, graph consumes them, contract stores them.
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
/** A single parameter in a function signature */
|
|
7
6
|
export interface ParsedParam {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
optional: boolean;
|
|
10
|
+
defaultValue?: string;
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
/** A single call expression found in code (Mikk 2.0) */
|
|
14
|
+
export interface CallExpression {
|
|
15
|
+
name: string;
|
|
16
|
+
line: number;
|
|
17
|
+
type: 'function' | 'method' | 'property';
|
|
18
|
+
arguments?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A detailed function declaration */
|
|
14
22
|
export interface ParsedFunction {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
id: string; // unique normalized ID (file::name)
|
|
24
|
+
name: string;
|
|
25
|
+
file: string;
|
|
26
|
+
moduleId?: string;
|
|
27
|
+
startLine: number;
|
|
28
|
+
endLine: number;
|
|
29
|
+
params: ParsedParam[];
|
|
30
|
+
returnType: string;
|
|
31
|
+
isExported: boolean;
|
|
32
|
+
isAsync: boolean;
|
|
33
|
+
isGenerator?: boolean;
|
|
34
|
+
typeParameters?: string[];
|
|
35
|
+
calls: CallExpression[]; // Behavioral tracking (Upgraded from string[])
|
|
36
|
+
hash: string;
|
|
37
|
+
purpose: string;
|
|
38
|
+
edgeCasesHandled: string[];
|
|
39
|
+
errorHandling: { line: number; type: 'try-catch' | 'throw'; detail: string }[];
|
|
40
|
+
detailedLines: { startLine: number; endLine: number; blockType: string }[];
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/** A single import statement */
|
|
36
44
|
export interface ParsedImport {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
source: string;
|
|
46
|
+
resolvedPath: string;
|
|
47
|
+
names: string[];
|
|
48
|
+
isDefault: boolean;
|
|
49
|
+
isDynamic: boolean;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/** A single exported symbol */
|
|
45
53
|
export interface ParsedExport {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
name: string;
|
|
55
|
+
type: 'function' | 'class' | 'const' | 'type' | 'default' | 'interface' | 'variable';
|
|
56
|
+
file: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A single variable or property */
|
|
60
|
+
export interface ParsedVariable {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
type: string;
|
|
64
|
+
file: string;
|
|
65
|
+
line: number;
|
|
66
|
+
isExported: boolean;
|
|
67
|
+
isStatic?: boolean;
|
|
68
|
+
purpose?: string;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
/** A parsed class */
|
|
52
72
|
export interface ParsedClass {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
file: string;
|
|
76
|
+
moduleId?: string;
|
|
77
|
+
startLine: number;
|
|
78
|
+
endLine: number;
|
|
79
|
+
methods: ParsedFunction[];
|
|
80
|
+
properties: ParsedVariable[];
|
|
81
|
+
extends?: string;
|
|
82
|
+
implements?: string[];
|
|
83
|
+
isExported: boolean;
|
|
84
|
+
decorators?: string[];
|
|
85
|
+
typeParameters?: string[];
|
|
86
|
+
hash: string;
|
|
87
|
+
purpose?: string;
|
|
88
|
+
edgeCasesHandled?: string[];
|
|
89
|
+
errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[];
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
/** A
|
|
69
|
-
export interface
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
/** A generic declaration (interface, type aliase, etc.) */
|
|
93
|
+
export interface ParsedGeneric {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
type: string; // "interface" | "type"
|
|
97
|
+
file: string;
|
|
98
|
+
startLine: number;
|
|
99
|
+
endLine: number;
|
|
100
|
+
isExported: boolean;
|
|
101
|
+
typeParameters?: string[];
|
|
102
|
+
hash: string;
|
|
103
|
+
purpose?: string;
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
/** A
|
|
79
|
-
export interface
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
isExported: boolean
|
|
87
|
-
typeParameters?: string[] // ["T", "K"] for generic interfaces/types
|
|
88
|
-
purpose?: string
|
|
106
|
+
/** A detected HTTP route registration */
|
|
107
|
+
export interface ParsedRoute {
|
|
108
|
+
method: string;
|
|
109
|
+
path: string;
|
|
110
|
+
handler: string;
|
|
111
|
+
middlewares: string[];
|
|
112
|
+
file: string;
|
|
113
|
+
line: number;
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
/** Everything extracted from a single file */
|
|
92
117
|
export interface ParsedFile {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
path: string; // normalized absolute path
|
|
119
|
+
language: 'python' | 'go' | 'typescript' | 'javascript' | 'java' | 'c' | 'cpp' | 'csharp' | 'rust' | 'php' | 'ruby' | 'unknown';
|
|
120
|
+
functions: ParsedFunction[];
|
|
121
|
+
classes: ParsedClass[];
|
|
122
|
+
variables: ParsedVariable[];
|
|
123
|
+
generics: ParsedGeneric[];
|
|
124
|
+
imports: ParsedImport[];
|
|
125
|
+
exports: ParsedExport[];
|
|
126
|
+
routes: ParsedRoute[];
|
|
127
|
+
calls: CallExpression[]; // module-level calls
|
|
128
|
+
hash: string;
|
|
129
|
+
parsedAt: number;
|
|
103
130
|
}
|