@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,79 @@
|
|
|
1
|
+
import type { DependencyGraph } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mikk 2.0: Query Engine
|
|
5
|
+
* Provides high-performance graph traversal and path-finding tools.
|
|
6
|
+
* Focuses on symbol-level precision.
|
|
7
|
+
*/
|
|
8
|
+
export class QueryEngine {
|
|
9
|
+
constructor(private graph: DependencyGraph) {}
|
|
10
|
+
|
|
11
|
+
/** Find all direct dependents (who calls me?) */
|
|
12
|
+
public getDependents(nodeId: string): string[] {
|
|
13
|
+
return (this.graph.inEdges.get(nodeId) || [])
|
|
14
|
+
.filter(e => e.type !== 'contains')
|
|
15
|
+
.map(e => e.from);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Find all direct dependencies (who do I call?) */
|
|
19
|
+
public getDependencies(nodeId: string): string[] {
|
|
20
|
+
return (this.graph.outEdges.get(nodeId) || [])
|
|
21
|
+
.filter(e => e.type !== 'contains')
|
|
22
|
+
.map(e => e.to);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Find the shortest path between two nodes using BFS.
|
|
27
|
+
* Returns an array of node IDs or null if no path exists.
|
|
28
|
+
*/
|
|
29
|
+
public findPath(start: string, end: string): string[] | null {
|
|
30
|
+
if (!this.graph.nodes.has(start) || !this.graph.nodes.has(end)) return null;
|
|
31
|
+
if (start === end) return [start];
|
|
32
|
+
|
|
33
|
+
const queue: { id: string, path: string[] }[] = [{ id: start, path: [start] }];
|
|
34
|
+
const visited = new Set<string>([start]);
|
|
35
|
+
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
const { id, path } = queue.shift()!;
|
|
38
|
+
|
|
39
|
+
const outwardEdges = this.graph.outEdges.get(id) || [];
|
|
40
|
+
for (const edge of outwardEdges) {
|
|
41
|
+
if (edge.type === 'contains') continue;
|
|
42
|
+
|
|
43
|
+
if (edge.to === end) {
|
|
44
|
+
return [...path, end];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!visited.has(edge.to)) {
|
|
48
|
+
visited.add(edge.to);
|
|
49
|
+
queue.push({ id: edge.to, path: [...path, edge.to] });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the full downstream impact (transitive dependents) of a node.
|
|
59
|
+
* Useful for assessing "What would break if I change X?"
|
|
60
|
+
*/
|
|
61
|
+
public getDownstreamImpact(nodeId: string): string[] {
|
|
62
|
+
const visited = new Set<string>();
|
|
63
|
+
const queue: string[] = [nodeId];
|
|
64
|
+
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const current = queue.shift()!;
|
|
67
|
+
const dependents = this.getDependents(current);
|
|
68
|
+
|
|
69
|
+
for (const dep of dependents) {
|
|
70
|
+
if (!visited.has(dep) && dep !== nodeId) {
|
|
71
|
+
visited.add(dep);
|
|
72
|
+
queue.push(dep);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Array.from(visited);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DependencyGraph, GraphNode } from './types.js'
|
|
2
|
+
|
|
3
|
+
export interface RiskContext {
|
|
4
|
+
connectedNodesCount: number;
|
|
5
|
+
dependencyDepth: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RiskModifiers {
|
|
9
|
+
isAuthOrSecurity: boolean;
|
|
10
|
+
isDatabaseOrState: boolean;
|
|
11
|
+
isPublicAPI: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mikk 2.0: Risk Engine
|
|
16
|
+
* Computes risk scores based on a quantitative mathematical model.
|
|
17
|
+
*/
|
|
18
|
+
export class RiskEngine {
|
|
19
|
+
constructor(private graph: DependencyGraph) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compute the absolute risk score (0-100) for modifying a specific node.
|
|
23
|
+
* Formula: Base Risk = (Connected Nodes * 1.5) + (Depth * 2) + Modifiers
|
|
24
|
+
*/
|
|
25
|
+
public scoreNode(nodeId: string): number {
|
|
26
|
+
const node = this.graph.nodes.get(nodeId);
|
|
27
|
+
if (!node) return 0;
|
|
28
|
+
|
|
29
|
+
const context = this.analyzeContext(nodeId);
|
|
30
|
+
const modifiers = this.analyzeModifiers(node);
|
|
31
|
+
|
|
32
|
+
let score = (context.connectedNodesCount * 1.5) + (context.dependencyDepth * 2);
|
|
33
|
+
|
|
34
|
+
// Apply strict modifiers
|
|
35
|
+
if (modifiers.isAuthOrSecurity) score += 30;
|
|
36
|
+
if (modifiers.isDatabaseOrState) score += 20;
|
|
37
|
+
if (modifiers.isPublicAPI) score += 15;
|
|
38
|
+
|
|
39
|
+
return Math.min(Math.max(score, 0), 100);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private analyzeContext(nodeId: string): RiskContext {
|
|
43
|
+
const visited = new Set<string>();
|
|
44
|
+
let maxDepth = 0;
|
|
45
|
+
|
|
46
|
+
// Use index pointer instead of queue.shift() — avoids O(n) array shift per pop.
|
|
47
|
+
const queue: Array<{ id: string, depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
48
|
+
let queueHead = 0;
|
|
49
|
+
visited.add(nodeId);
|
|
50
|
+
|
|
51
|
+
let connectedNodesCount = 0;
|
|
52
|
+
|
|
53
|
+
while (queueHead < queue.length) {
|
|
54
|
+
const current = queue[queueHead++];
|
|
55
|
+
maxDepth = Math.max(maxDepth, current.depth);
|
|
56
|
+
|
|
57
|
+
const inEdges = this.graph.inEdges.get(current.id) || [];
|
|
58
|
+
connectedNodesCount += inEdges.length;
|
|
59
|
+
|
|
60
|
+
for (const edge of inEdges) {
|
|
61
|
+
if (!visited.has(edge.from)) {
|
|
62
|
+
visited.add(edge.from);
|
|
63
|
+
queue.push({ id: edge.from, depth: current.depth + 1 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
connectedNodesCount,
|
|
70
|
+
dependencyDepth: maxDepth
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private analyzeModifiers(node: GraphNode): RiskModifiers {
|
|
75
|
+
const nameAndFile = `${node.name} ${node.file}`.toLowerCase();
|
|
76
|
+
|
|
77
|
+
const authKeywords = ['auth', 'login', 'jwt', 'verify', 'token', 'crypt', 'hash', 'password'];
|
|
78
|
+
const dbKeywords = ['db', 'query', 'sql', 'insert', 'update', 'delete', 'redis', 'cache', 'transaction'];
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
isAuthOrSecurity: authKeywords.some(kw => nameAndFile.includes(kw)),
|
|
82
|
+
isDatabaseOrState: dbKeywords.some(kw => nameAndFile.includes(kw)),
|
|
83
|
+
isPublicAPI: !!node.metadata?.isExported
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/graph/types.ts
CHANGED
|
@@ -1,82 +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
|
-
}
|
|
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
|
+
};
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
/** A single edge in the dependency graph */
|
|
31
42
|
export interface GraphEdge {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
type: EdgeType;
|
|
46
|
+
confidence: number; // 0–1
|
|
47
|
+
weight?: number; // Weight from EDGE_WEIGHT constants
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
/** The full dependency graph */
|
|
40
50
|
export interface DependencyGraph {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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]
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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}`;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
export type RiskLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
73
|
+
|
|
61
74
|
export interface ImpactResult {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
/** A cluster of files that naturally belong together */
|
|
76
101
|
export interface ModuleCluster {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
id: string;
|
|
103
|
+
files: string[];
|
|
104
|
+
confidence: number;
|
|
105
|
+
suggestedName: string;
|
|
106
|
+
functions: string[];
|
|
82
107
|
}
|
|
@@ -74,7 +74,9 @@ export class BoundaryChecker {
|
|
|
74
74
|
|
|
75
75
|
for (const file of Object.values(this.lock.files)) {
|
|
76
76
|
if (file.moduleId === 'unknown' || !file.imports?.length) continue
|
|
77
|
-
for (const
|
|
77
|
+
for (const imp of file.imports) {
|
|
78
|
+
const importedPath = imp.resolvedPath
|
|
79
|
+
if (!importedPath) continue
|
|
78
80
|
const importedFile = this.lock.files[importedPath]
|
|
79
81
|
if (!importedFile || importedFile.moduleId === 'unknown' || file.moduleId === importedFile.moduleId) continue
|
|
80
82
|
const v = this.checkFileImport(file, importedFile)
|
|
@@ -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 -------------------
|
|
@@ -54,6 +54,7 @@ const ROUTE_PATTERNS: RoutePattern[] = [
|
|
|
54
54
|
*/
|
|
55
55
|
export class GoExtractor {
|
|
56
56
|
private readonly lines: string[]
|
|
57
|
+
private cachedFunctions: ReturnType<typeof this.scanFunctions> | null = null
|
|
57
58
|
|
|
58
59
|
constructor(
|
|
59
60
|
private readonly filePath: string,
|
|
@@ -96,6 +97,8 @@ export class GoExtractor {
|
|
|
96
97
|
isExported: isExported(typeDecl.name),
|
|
97
98
|
purpose: typeDecl.purpose,
|
|
98
99
|
methods: methods.map(m => this.buildParsedFunction(m)),
|
|
100
|
+
properties: [],
|
|
101
|
+
hash: '',
|
|
99
102
|
})
|
|
100
103
|
byReceiver.delete(typeDecl.name)
|
|
101
104
|
}
|
|
@@ -110,6 +113,9 @@ export class GoExtractor {
|
|
|
110
113
|
endLine: methods[methods.length - 1]?.endLine ?? 0,
|
|
111
114
|
isExported: isExported(receiverType),
|
|
112
115
|
methods: methods.map(m => this.buildParsedFunction(m)),
|
|
116
|
+
properties: [],
|
|
117
|
+
hash: '',
|
|
118
|
+
purpose: '',
|
|
113
119
|
})
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -174,6 +180,7 @@ export class GoExtractor {
|
|
|
174
180
|
endLine: number
|
|
175
181
|
purpose: string
|
|
176
182
|
}> {
|
|
183
|
+
if (this.cachedFunctions) return this.cachedFunctions
|
|
177
184
|
const results: Array<{
|
|
178
185
|
name: string
|
|
179
186
|
receiverType?: string
|
|
@@ -237,6 +244,7 @@ export class GoExtractor {
|
|
|
237
244
|
i = bodyEnd + 1
|
|
238
245
|
}
|
|
239
246
|
|
|
247
|
+
this.cachedFunctions = results
|
|
240
248
|
return results
|
|
241
249
|
}
|
|
242
250
|
|
|
@@ -247,7 +255,7 @@ export class GoExtractor {
|
|
|
247
255
|
|
|
248
256
|
const bodyLines = this.lines.slice(raw.bodyStart - 1, raw.endLine)
|
|
249
257
|
const hash = hashContent(bodyLines.join('\n'))
|
|
250
|
-
const calls = extractCallsFromBody(bodyLines)
|
|
258
|
+
const calls = extractCallsFromBody(bodyLines, raw.bodyStart)
|
|
251
259
|
const edgeCases = extractEdgeCases(bodyLines)
|
|
252
260
|
const errorHandling = extractErrorHandling(bodyLines, raw.bodyStart)
|
|
253
261
|
|
|
@@ -547,11 +555,17 @@ function findBodyBounds(lines: string[], startLine: number): { bodyStart: number
|
|
|
547
555
|
|
|
548
556
|
if (ch === '/' && next === '/') { inLineComment = true; break }
|
|
549
557
|
if (ch === '/' && next === '*') { inBlockComment = true; j++; continue }
|
|
550
|
-
if (ch === '"' || ch === '`'
|
|
558
|
+
if (ch === '"' || ch === '`') {
|
|
551
559
|
inString = true
|
|
552
560
|
stringChar = ch
|
|
553
561
|
continue
|
|
554
562
|
}
|
|
563
|
+
if (ch === '\'') {
|
|
564
|
+
// Go rune literal: consume exactly one character (or escape) then close
|
|
565
|
+
if (next === '\\') j += 3 // '\n' or '\x00' etc.
|
|
566
|
+
else j += 2 // 'a'
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
555
569
|
|
|
556
570
|
if (ch === '{') {
|
|
557
571
|
if (bodyStart === -1) bodyStart = i
|
|
@@ -611,25 +625,30 @@ function stripStringsAndComments(code: string): string {
|
|
|
611
625
|
return out
|
|
612
626
|
}
|
|
613
627
|
|
|
614
|
-
function extractCallsFromBody(bodyLines: string[]):
|
|
615
|
-
const
|
|
616
|
-
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[] = []
|
|
617
632
|
|
|
618
633
|
// Direct calls: identifier(
|
|
619
634
|
const callRe = /\b([A-Za-z_]\w*)\s*\(/g
|
|
620
635
|
let m: RegExpExecArray | null
|
|
621
636
|
while ((m = callRe.exec(stripped)) !== null) {
|
|
622
637
|
const name = m[1]
|
|
623
|
-
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
|
+
}
|
|
624
643
|
}
|
|
625
644
|
|
|
626
645
|
// Method calls: receiver.Method(
|
|
627
646
|
const methodRe = /\b([A-Za-z_]\w+\.[A-Za-z_]\w*)\s*\(/g
|
|
628
647
|
while ((m = methodRe.exec(stripped)) !== null) {
|
|
629
|
-
calls.
|
|
648
|
+
calls.push({ name: m[1], line: baseLine, type: 'method' })
|
|
630
649
|
}
|
|
631
650
|
|
|
632
|
-
return
|
|
651
|
+
return calls
|
|
633
652
|
}
|
|
634
653
|
|
|
635
654
|
function extractEdgeCases(bodyLines: string[]): string[] {
|