@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
package/src/graph/types.ts
CHANGED
|
@@ -1,83 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export type NodeType =
|
|
2
|
+
| "file"
|
|
3
|
+
| "class"
|
|
4
|
+
| "function"
|
|
5
|
+
| "variable"
|
|
6
|
+
| "generic";
|
|
4
7
|
|
|
5
|
-
export type
|
|
6
|
-
|
|
8
|
+
export type EdgeType =
|
|
9
|
+
| "imports"
|
|
10
|
+
| "calls"
|
|
11
|
+
| "extends"
|
|
12
|
+
| "implements"
|
|
13
|
+
| "accesses"
|
|
14
|
+
| "contains"; // Keeping for containment edges
|
|
7
15
|
|
|
8
|
-
/** A single node in the dependency graph */
|
|
9
16
|
export interface GraphNode {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
id: string; // unique (normalized file::name)
|
|
18
|
+
type: NodeType;
|
|
19
|
+
name: string;
|
|
20
|
+
file: string;
|
|
21
|
+
moduleId?: string; // Original cluster feature
|
|
22
|
+
|
|
23
|
+
metadata?: {
|
|
24
|
+
isExported?: boolean;
|
|
25
|
+
inheritsFrom?: string[];
|
|
26
|
+
implements?: string[];
|
|
27
|
+
className?: string; // for methods
|
|
28
|
+
startLine?: number;
|
|
29
|
+
endLine?: number;
|
|
30
|
+
isAsync?: boolean;
|
|
31
|
+
hash?: string;
|
|
32
|
+
purpose?: string;
|
|
33
|
+
genericKind?: string;
|
|
34
|
+
params?: { name: string; type: string; optional?: boolean }[];
|
|
35
|
+
returnType?: string;
|
|
36
|
+
edgeCasesHandled?: string[];
|
|
37
|
+
errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[];
|
|
38
|
+
detailedLines?: { startLine: number; endLine: number; blockType: string }[];
|
|
39
|
+
};
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
/** A single edge in the dependency graph */
|
|
32
42
|
export interface GraphEdge {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
type: EdgeType;
|
|
46
|
+
confidence: number; // 0–1
|
|
47
|
+
weight?: number; // Weight from EDGE_WEIGHT constants
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
/** The full dependency graph */
|
|
41
50
|
export interface DependencyGraph {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
nodes: Map<string, GraphNode>;
|
|
52
|
+
edges: GraphEdge[];
|
|
53
|
+
outEdges: Map<string, GraphEdge[]>; // node → [edges going out]
|
|
54
|
+
inEdges: Map<string, GraphEdge[]>; // node → [edges coming in]
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Canonical ID helpers.
|
|
59
|
+
* Function IDs: fn:<absolute-posix-path>:<FunctionName>
|
|
60
|
+
* Class IDs: class:<absolute-posix-path>:<ClassName>
|
|
61
|
+
* Type/enum IDs: type:<absolute-posix-path>:<Name> | enum:<absolute-posix-path>:<Name>
|
|
62
|
+
* File IDs: <absolute-posix-path> (no prefix)
|
|
63
|
+
*
|
|
64
|
+
* NOTE: The old normalizeId() that used `file::name` (double-colon, lowercase)
|
|
65
|
+
* was removed — it did not match any current ID format and would produce IDs
|
|
66
|
+
* that never matched any graph node.
|
|
67
|
+
*/
|
|
68
|
+
export function makeFnId(file: string, name: string): string {
|
|
69
|
+
return `fn:${file.replace(/\\/g, '/')}:${name}`;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
export type RiskLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
73
|
+
|
|
62
74
|
export interface ImpactResult {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
changed: string[];
|
|
76
|
+
impacted: string[];
|
|
77
|
+
allImpacted: ClassifiedImpact[]; // New field for Decision Engine
|
|
78
|
+
depth: number;
|
|
79
|
+
entryPoints: string[];
|
|
80
|
+
criticalModules: string[];
|
|
81
|
+
paths: string[][];
|
|
82
|
+
confidence: number;
|
|
83
|
+
riskScore: number;
|
|
84
|
+
classified: {
|
|
85
|
+
critical: ClassifiedImpact[];
|
|
86
|
+
high: ClassifiedImpact[];
|
|
87
|
+
medium: ClassifiedImpact[];
|
|
88
|
+
low: ClassifiedImpact[];
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ClassifiedImpact {
|
|
93
|
+
nodeId: string;
|
|
94
|
+
label: string;
|
|
95
|
+
file: string;
|
|
96
|
+
risk: RiskLevel;
|
|
97
|
+
riskScore: number; // numeric score for precise policy checks
|
|
98
|
+
depth: number;
|
|
74
99
|
}
|
|
75
100
|
|
|
76
|
-
/** A cluster of files that naturally belong together */
|
|
77
101
|
export interface ModuleCluster {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
id: string;
|
|
103
|
+
files: string[];
|
|
104
|
+
confidence: number;
|
|
105
|
+
suggestedName: string;
|
|
106
|
+
functions: string[];
|
|
83
107
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from './hash/index.js'
|
|
|
8
8
|
export * from './search/index.js'
|
|
9
9
|
export * from './utils/errors.js'
|
|
10
10
|
export * from './utils/logger.js'
|
|
11
|
+
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'
|
|
12
|
+
export type { } from './error-handler.js'
|
|
11
13
|
export { discoverFiles, discoverContextFiles, readFileContent, writeFileContent, fileExists, setupMikkDirectory, readMikkIgnore, parseMikkIgnore, detectProjectLanguage, getDiscoveryPatterns, generateMikkIgnore, updateGitIgnore, cleanupGitIgnore } from './utils/fs.js'
|
|
12
14
|
export type { ContextFile, ContextFileType, ProjectLanguage } from './utils/fs.js'
|
|
13
15
|
export { minimatch } from './utils/minimatch.js'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ParsedFile } from './types.js'
|
|
2
|
+
|
|
3
|
+
export interface ChangeSet {
|
|
4
|
+
added: string[]; // Symbol IDs
|
|
5
|
+
removed: string[]; // Symbol IDs
|
|
6
|
+
modified: string[]; // Symbol IDs (hash mismatch)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mikk 2.0: Change Detector
|
|
11
|
+
* Compares parsed file versions to identify precisely which symbols
|
|
12
|
+
* (functions, classes, etc.) have changed using AST hashes.
|
|
13
|
+
*/
|
|
14
|
+
export class ChangeDetector {
|
|
15
|
+
/**
|
|
16
|
+
* Compare two versions of a file and return the changed symbol IDs.
|
|
17
|
+
*/
|
|
18
|
+
public detectSymbolChanges(oldFile: ParsedFile | undefined, newFile: ParsedFile): ChangeSet {
|
|
19
|
+
const added: string[] = [];
|
|
20
|
+
const removed: string[] = [];
|
|
21
|
+
const modified: string[] = [];
|
|
22
|
+
|
|
23
|
+
if (!oldFile) {
|
|
24
|
+
// Everything is new
|
|
25
|
+
return {
|
|
26
|
+
added: [
|
|
27
|
+
...newFile.functions.map(f => f.id),
|
|
28
|
+
...newFile.classes.map(c => c.id),
|
|
29
|
+
...newFile.generics.map(g => g.id),
|
|
30
|
+
...newFile.variables.map(v => v.id),
|
|
31
|
+
],
|
|
32
|
+
removed: [],
|
|
33
|
+
modified: []
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (oldFile.hash === newFile.hash) {
|
|
38
|
+
return { added: [], removed: [], modified: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Compare Functions ---
|
|
42
|
+
const oldFns = new Map(oldFile.functions.map(f => [f.id, f.hash]));
|
|
43
|
+
const newFns = new Map(newFile.functions.map(f => [f.id, f.hash]));
|
|
44
|
+
|
|
45
|
+
for (const [id, hash] of newFns) {
|
|
46
|
+
if (!oldFns.has(id)) added.push(id);
|
|
47
|
+
else if (oldFns.get(id) !== hash) modified.push(id);
|
|
48
|
+
}
|
|
49
|
+
for (const id of oldFns.keys()) {
|
|
50
|
+
if (!newFns.has(id)) removed.push(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Compare Classes ---
|
|
54
|
+
const oldClasses = new Map(oldFile.classes.map(c => [c.id, c.hash || 'no-hash'])); // ParsedClass might not have hash yet, using placeholder
|
|
55
|
+
const newClasses = new Map(newFile.classes.map(c => [c.id, c.hash || 'no-hash']));
|
|
56
|
+
|
|
57
|
+
// Note: If ParsedClass doesn't have a hash, we might need to compare methods/properties hashes
|
|
58
|
+
// For Mikk 2.0, we'll assume classes have a summary hash eventually or just compare their structure.
|
|
59
|
+
|
|
60
|
+
for (const [id, hash] of newClasses) {
|
|
61
|
+
if (!oldClasses.has(id)) added.push(id);
|
|
62
|
+
else if (oldClasses.get(id) !== hash) modified.push(id);
|
|
63
|
+
}
|
|
64
|
+
for (const id of oldClasses.keys()) {
|
|
65
|
+
if (!newClasses.has(id)) removed.push(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Compare Generics ---
|
|
69
|
+
const oldGenerics = new Map(oldFile.generics.map(g => [g.id, g.id])); // Simplified
|
|
70
|
+
const newGenerics = new Map(newFile.generics.map(g => [g.id, g.id]));
|
|
71
|
+
|
|
72
|
+
for (const id of newGenerics.keys()) {
|
|
73
|
+
if (!oldGenerics.has(id)) added.push(id);
|
|
74
|
+
}
|
|
75
|
+
for (const id of oldGenerics.keys()) {
|
|
76
|
+
if (!newGenerics.has(id)) removed.push(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { added, removed, modified };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compare two sets of files and return all modified symbol IDs across the project.
|
|
84
|
+
*/
|
|
85
|
+
public detectBatchChanges(oldFiles: Map<string, ParsedFile>, newFiles: ParsedFile[]): string[] {
|
|
86
|
+
const allChangedIds = new Set<string>();
|
|
87
|
+
|
|
88
|
+
for (const file of newFiles) {
|
|
89
|
+
const old = oldFiles.get(file.path);
|
|
90
|
+
const diff = this.detectSymbolChanges(old, file);
|
|
91
|
+
|
|
92
|
+
diff.added.forEach(id => allChangedIds.add(id));
|
|
93
|
+
diff.modified.forEach(id => allChangedIds.add(id));
|
|
94
|
+
// Removal is handled by graph-builder removing stale nodes, but we might want to track it for impact
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Array.from(allChangedIds);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hashContent } from '../../hash/file-hasher.js'
|
|
2
2
|
import type {
|
|
3
3
|
ParsedFunction, ParsedClass, ParsedImport, ParsedExport,
|
|
4
|
-
ParsedParam, ParsedGeneric, ParsedRoute,
|
|
4
|
+
ParsedParam, ParsedGeneric, ParsedRoute, CallExpression
|
|
5
5
|
} from '../types.js'
|
|
6
6
|
|
|
7
7
|
// --- Go builtins / keywords to skip when extracting calls -------------------
|
|
@@ -97,6 +97,8 @@ export class GoExtractor {
|
|
|
97
97
|
isExported: isExported(typeDecl.name),
|
|
98
98
|
purpose: typeDecl.purpose,
|
|
99
99
|
methods: methods.map(m => this.buildParsedFunction(m)),
|
|
100
|
+
properties: [],
|
|
101
|
+
hash: '',
|
|
100
102
|
})
|
|
101
103
|
byReceiver.delete(typeDecl.name)
|
|
102
104
|
}
|
|
@@ -111,6 +113,9 @@ export class GoExtractor {
|
|
|
111
113
|
endLine: methods[methods.length - 1]?.endLine ?? 0,
|
|
112
114
|
isExported: isExported(receiverType),
|
|
113
115
|
methods: methods.map(m => this.buildParsedFunction(m)),
|
|
116
|
+
properties: [],
|
|
117
|
+
hash: '',
|
|
118
|
+
purpose: '',
|
|
114
119
|
})
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -250,7 +255,7 @@ export class GoExtractor {
|
|
|
250
255
|
|
|
251
256
|
const bodyLines = this.lines.slice(raw.bodyStart - 1, raw.endLine)
|
|
252
257
|
const hash = hashContent(bodyLines.join('\n'))
|
|
253
|
-
const calls = extractCallsFromBody(bodyLines)
|
|
258
|
+
const calls = extractCallsFromBody(bodyLines, raw.bodyStart)
|
|
254
259
|
const edgeCases = extractEdgeCases(bodyLines)
|
|
255
260
|
const errorHandling = extractErrorHandling(bodyLines, raw.bodyStart)
|
|
256
261
|
|
|
@@ -620,25 +625,30 @@ function stripStringsAndComments(code: string): string {
|
|
|
620
625
|
return out
|
|
621
626
|
}
|
|
622
627
|
|
|
623
|
-
function extractCallsFromBody(bodyLines: string[]):
|
|
624
|
-
const
|
|
625
|
-
const
|
|
628
|
+
function extractCallsFromBody(bodyLines: string[], baseLine: number = 1): CallExpression[] {
|
|
629
|
+
const code = bodyLines.join('\n')
|
|
630
|
+
const stripped = stripStringsAndComments(code)
|
|
631
|
+
const calls: CallExpression[] = []
|
|
626
632
|
|
|
627
633
|
// Direct calls: identifier(
|
|
628
634
|
const callRe = /\b([A-Za-z_]\w*)\s*\(/g
|
|
629
635
|
let m: RegExpExecArray | null
|
|
630
636
|
while ((m = callRe.exec(stripped)) !== null) {
|
|
631
637
|
const name = m[1]
|
|
632
|
-
if (!GO_BUILTINS.has(name))
|
|
638
|
+
if (!GO_BUILTINS.has(name)) {
|
|
639
|
+
// Heuristic for line number: find the line in bodyLines
|
|
640
|
+
// This is a rough estimation but good enough for static analysis
|
|
641
|
+
calls.push({ name, line: baseLine, type: 'function' })
|
|
642
|
+
}
|
|
633
643
|
}
|
|
634
644
|
|
|
635
645
|
// Method calls: receiver.Method(
|
|
636
646
|
const methodRe = /\b([A-Za-z_]\w+\.[A-Za-z_]\w*)\s*\(/g
|
|
637
647
|
while ((m = methodRe.exec(stripped)) !== null) {
|
|
638
|
-
calls.
|
|
648
|
+
calls.push({ name: m[1], line: baseLine, type: 'method' })
|
|
639
649
|
}
|
|
640
650
|
|
|
641
|
-
return
|
|
651
|
+
return calls
|
|
642
652
|
}
|
|
643
653
|
|
|
644
654
|
function extractEdgeCases(bodyLines: string[]): string[] {
|
package/src/parser/index.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as nodePath from 'node:path'
|
|
2
2
|
import { BaseParser } from './base-parser.js'
|
|
3
|
-
import {
|
|
3
|
+
import { OxcParser } from './oxc-parser.js'
|
|
4
4
|
import { GoParser } from './go/go-parser.js'
|
|
5
|
-
import { JavaScriptParser } from './javascript/js-parser.js'
|
|
6
5
|
import { UnsupportedLanguageError } from '../utils/errors.js'
|
|
7
6
|
import type { ParsedFile } from './types.js'
|
|
8
7
|
|
|
9
|
-
export type {
|
|
8
|
+
export type {
|
|
9
|
+
ParsedFile,
|
|
10
|
+
ParsedFunction,
|
|
11
|
+
ParsedImport,
|
|
12
|
+
ParsedExport,
|
|
13
|
+
ParsedClass,
|
|
14
|
+
ParsedParam,
|
|
15
|
+
ParsedVariable,
|
|
16
|
+
CallExpression,
|
|
17
|
+
ParsedGeneric,
|
|
18
|
+
ParsedRoute
|
|
19
|
+
} from './types.js'
|
|
10
20
|
export { BaseParser } from './base-parser.js'
|
|
11
21
|
export { TypeScriptParser } from './typescript/ts-parser.js'
|
|
12
22
|
export { TypeScriptExtractor } from './typescript/ts-extractor.js'
|
|
@@ -19,22 +29,20 @@ export { JavaScriptExtractor } from './javascript/js-extractor.js'
|
|
|
19
29
|
export { JavaScriptResolver } from './javascript/js-resolver.js'
|
|
20
30
|
export { BoundaryChecker } from './boundary-checker.js'
|
|
21
31
|
export { TreeSitterParser } from './tree-sitter/parser.js'
|
|
22
|
-
import { TreeSitterParser } from './tree-sitter/parser.js'
|
|
23
32
|
|
|
24
33
|
/** Get the appropriate parser for a file based on its extension */
|
|
25
34
|
export function getParser(filePath: string): BaseParser {
|
|
26
|
-
const ext =
|
|
35
|
+
const ext = nodePath.extname(filePath).toLowerCase()
|
|
27
36
|
switch (ext) {
|
|
28
37
|
case '.ts':
|
|
29
38
|
case '.tsx':
|
|
30
|
-
return new TypeScriptParser()
|
|
31
39
|
case '.js':
|
|
32
40
|
case '.mjs':
|
|
33
41
|
case '.cjs':
|
|
34
42
|
case '.jsx':
|
|
35
|
-
return new
|
|
43
|
+
return new OxcParser()
|
|
36
44
|
case '.go':
|
|
37
|
-
return new GoParser()
|
|
45
|
+
return new GoParser()
|
|
38
46
|
case '.py':
|
|
39
47
|
case '.java':
|
|
40
48
|
case '.c':
|
|
@@ -46,55 +54,97 @@ export function getParser(filePath: string): BaseParser {
|
|
|
46
54
|
case '.rs':
|
|
47
55
|
case '.php':
|
|
48
56
|
case '.rb':
|
|
49
|
-
|
|
57
|
+
throw new UnsupportedLanguageError(ext)
|
|
50
58
|
default:
|
|
51
59
|
throw new UnsupportedLanguageError(ext)
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Parse multiple files, resolve their imports, and return ParsedFile[].
|
|
65
|
+
*
|
|
66
|
+
* Path contract (critical for graph correctness):
|
|
67
|
+
* - filePaths come from discoverFiles() as project-root-relative strings
|
|
68
|
+
* - We resolve them to ABSOLUTE posix paths before passing to parse()
|
|
69
|
+
* - ParsedFile.path is therefore always absolute + forward-slash
|
|
70
|
+
* - OxcResolver also returns absolute paths → import edges always consistent
|
|
71
|
+
*/
|
|
56
72
|
export async function parseFiles(
|
|
57
73
|
filePaths: string[],
|
|
58
74
|
projectRoot: string,
|
|
59
75
|
readFile: (fp: string) => Promise<string>
|
|
60
76
|
): Promise<ParsedFile[]> {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const tsParser = new TypeScriptParser()
|
|
64
|
-
const jsParser = new JavaScriptParser()
|
|
77
|
+
// Shared parser instances — avoid re-initialisation overhead per file
|
|
78
|
+
const oxcParser = new OxcParser()
|
|
65
79
|
const goParser = new GoParser()
|
|
66
|
-
const treeSitterParser = new TreeSitterParser()
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
default: return null
|
|
81
|
+
// Lazily loaded to avoid mandatory dep on tree-sitter
|
|
82
|
+
let treeSitterParser: BaseParser | null = null
|
|
83
|
+
const getTreeSitter = async (): Promise<BaseParser> => {
|
|
84
|
+
if (!treeSitterParser) {
|
|
85
|
+
const { TreeSitterParser } = await import('./tree-sitter/parser.js')
|
|
86
|
+
treeSitterParser = new TreeSitterParser()
|
|
75
87
|
}
|
|
88
|
+
return treeSitterParser!
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
const tsExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'])
|
|
92
|
+
const goExtensions = new Set(['.go'])
|
|
93
|
+
const treeSitterExtensions = new Set(['.py', '.java', '.c', '.h', '.cpp', '.cc', '.hpp', '.cs', '.rs', '.php', '.rb'])
|
|
94
|
+
|
|
95
|
+
// Normalised project root for absolute path construction
|
|
96
|
+
const normalizedRoot = nodePath.resolve(projectRoot).replace(/\\/g, '/')
|
|
97
|
+
|
|
98
|
+
// Group by parser to enable batch resolveImports
|
|
99
|
+
const oxcFiles: ParsedFile[] = []
|
|
100
|
+
const goFiles: ParsedFile[] = []
|
|
101
|
+
const treeFiles: ParsedFile[] = []
|
|
102
|
+
|
|
103
|
+
// Parse sequentially to avoid races in parser implementations that keep
|
|
104
|
+
// mutable per-instance state (e.g. language switching/counters).
|
|
78
105
|
for (const fp of filePaths) {
|
|
79
|
-
const ext =
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
const ext = nodePath.extname(fp).toLowerCase()
|
|
107
|
+
|
|
108
|
+
// Build absolute posix path — this is the single source of truth for all IDs
|
|
109
|
+
const absoluteFp = nodePath.resolve(normalizedRoot, fp).replace(/\\/g, '/')
|
|
82
110
|
|
|
111
|
+
let content: string
|
|
83
112
|
try {
|
|
84
|
-
|
|
85
|
-
const parsed = await parser.parse(fp, content)
|
|
86
|
-
|
|
87
|
-
if (!parsersMap.has(parser)) parsersMap.set(parser, [])
|
|
88
|
-
parsersMap.get(parser)!.push(parsed)
|
|
113
|
+
content = await readFile(absoluteFp)
|
|
89
114
|
} catch {
|
|
90
|
-
//
|
|
115
|
+
// File unreadable — skip silently (deleted, permission error, binary)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (tsExtensions.has(ext)) {
|
|
121
|
+
const parsed = await oxcParser.parse(absoluteFp, content)
|
|
122
|
+
oxcFiles.push(parsed)
|
|
123
|
+
} else if (goExtensions.has(ext)) {
|
|
124
|
+
const parsed = await goParser.parse(absoluteFp, content)
|
|
125
|
+
goFiles.push(parsed)
|
|
126
|
+
} else if (treeSitterExtensions.has(ext)) {
|
|
127
|
+
const ts = await getTreeSitter()
|
|
128
|
+
const parsed = await ts.parse(absoluteFp, content)
|
|
129
|
+
treeFiles.push(parsed)
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Parser error — skip this file, don't abort the whole run
|
|
91
133
|
}
|
|
92
134
|
}
|
|
93
135
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
// Resolve imports batch-wise per parser (each has its own resolver)
|
|
137
|
+
let resolvedTreeFiles: ParsedFile[] = treeFiles
|
|
138
|
+
if (treeFiles.length > 0) {
|
|
139
|
+
const treeParser = treeSitterParser ?? await getTreeSitter()
|
|
140
|
+
resolvedTreeFiles = treeParser.resolveImports(treeFiles, normalizedRoot)
|
|
97
141
|
}
|
|
98
142
|
|
|
99
|
-
|
|
143
|
+
const resolved: ParsedFile[] = [
|
|
144
|
+
...oxcParser.resolveImports(oxcFiles, normalizedRoot),
|
|
145
|
+
...goParser.resolveImports(goFiles, normalizedRoot),
|
|
146
|
+
...resolvedTreeFiles,
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return resolved
|
|
100
150
|
}
|
|
@@ -231,7 +231,7 @@ export class JavaScriptExtractor extends TypeScriptExtractor {
|
|
|
231
231
|
returnType: rhs.type ? rhs.type.getText(this.sourceFile) : 'void',
|
|
232
232
|
isExported: true,
|
|
233
233
|
isAsync,
|
|
234
|
-
calls: this.
|
|
234
|
+
calls: this.extractCallsFromNode(rhs),
|
|
235
235
|
hash: hashContent(rhs.getText(this.sourceFile)),
|
|
236
236
|
purpose: this.extractPurpose(node),
|
|
237
237
|
edgeCasesHandled: this.extractEdgeCases(rhs),
|