@getmikk/core 1.8.3 → 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 +66 -35
- package/src/contract/lock-reader.ts +24 -4
- package/src/contract/schema.ts +21 -0
- 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/graph-builder.ts +298 -255
- package/src/graph/impact-analyzer.ts +130 -119
- 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 -65
- 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 +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 +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 +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,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
|
}
|
|
@@ -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[] {
|