@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.
Files changed (43) hide show
  1. package/package.json +3 -1
  2. package/src/constants.ts +285 -0
  3. package/src/contract/contract-generator.ts +7 -0
  4. package/src/contract/index.ts +2 -3
  5. package/src/contract/lock-compiler.ts +74 -42
  6. package/src/contract/lock-reader.ts +24 -4
  7. package/src/contract/schema.ts +27 -1
  8. package/src/error-handler.ts +430 -0
  9. package/src/graph/cluster-detector.ts +45 -20
  10. package/src/graph/confidence-engine.ts +60 -0
  11. package/src/graph/dead-code-detector.ts +27 -5
  12. package/src/graph/graph-builder.ts +298 -238
  13. package/src/graph/impact-analyzer.ts +131 -114
  14. package/src/graph/index.ts +4 -0
  15. package/src/graph/memory-manager.ts +345 -0
  16. package/src/graph/query-engine.ts +79 -0
  17. package/src/graph/risk-engine.ts +86 -0
  18. package/src/graph/types.ts +89 -64
  19. package/src/parser/boundary-checker.ts +3 -1
  20. package/src/parser/change-detector.ts +99 -0
  21. package/src/parser/go/go-extractor.ts +28 -9
  22. package/src/parser/go/go-parser.ts +2 -0
  23. package/src/parser/index.ts +88 -38
  24. package/src/parser/javascript/js-extractor.ts +1 -1
  25. package/src/parser/javascript/js-parser.ts +2 -0
  26. package/src/parser/oxc-parser.ts +675 -0
  27. package/src/parser/oxc-resolver.ts +83 -0
  28. package/src/parser/tree-sitter/parser.ts +27 -15
  29. package/src/parser/types.ts +100 -73
  30. package/src/parser/typescript/ts-extractor.ts +241 -537
  31. package/src/parser/typescript/ts-parser.ts +16 -171
  32. package/src/parser/typescript/ts-resolver.ts +11 -1
  33. package/src/search/bm25.ts +5 -2
  34. package/src/utils/minimatch.ts +1 -1
  35. package/tests/contract.test.ts +2 -2
  36. package/tests/dead-code.test.ts +7 -7
  37. package/tests/esm-resolver.test.ts +75 -0
  38. package/tests/graph.test.ts +20 -20
  39. package/tests/helpers.ts +11 -6
  40. package/tests/impact-classified.test.ts +37 -41
  41. package/tests/parser.test.ts +7 -5
  42. package/tests/ts-parser.test.ts +27 -52
  43. 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
+ }
@@ -1,82 +1,107 @@
1
- /**
2
- * Graph types — nodes, edges, and the dependency graph itself.
3
- */
1
+ export type NodeType =
2
+ | "file"
3
+ | "class"
4
+ | "function"
5
+ | "variable"
6
+ | "generic";
4
7
 
5
- export type NodeType = 'function' | 'file' | 'module' | 'class' | 'generic'
6
- export type EdgeType = 'calls' | 'imports' | 'exports' | 'contains'
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
- id: string // "fn:src/auth/verify.ts:verifyToken"
11
- type: NodeType
12
- label: string // "verifyToken"
13
- file: string // "src/auth/verify.ts"
14
- moduleId?: string // "auth" which declared module this belongs to
15
- metadata: {
16
- startLine?: number
17
- endLine?: number
18
- isExported?: boolean
19
- isAsync?: boolean
20
- hash?: string
21
- purpose?: string
22
- params?: { name: string; type: string; optional?: boolean }[]
23
- returnType?: string
24
- edgeCasesHandled?: string[]
25
- errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[]
26
- detailedLines?: { startLine: number; endLine: number; blockType: string }[]
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
- source: string // "fn:src/auth/verify.ts:verifyToken"
33
- target: string // "fn:src/utils/jwt.ts:jwtDecode"
34
- type: EdgeType
35
- weight?: number // How often this call happens (for coupling metrics)
36
- confidence?: number // 0.0–1.0: 1.0 = direct AST call, 0.8 = via interface, 0.5 = fuzzy/inferred
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
- nodes: Map<string, GraphNode>
42
- edges: GraphEdge[]
43
- outEdges: Map<string, GraphEdge[]> // node → [edges going out]
44
- inEdges: Map<string, GraphEdge[]> // node → [edges coming in]
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
- /** Risk level for an impacted node */
48
- export type RiskLevel = 'critical' | 'high' | 'medium' | 'low'
49
-
50
- /** A single node in the classified impact result */
51
- export interface ClassifiedImpact {
52
- nodeId: string
53
- label: string
54
- file: string
55
- moduleId?: string
56
- risk: RiskLevel
57
- depth: number // hops from change
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
- /** Result of impact analysis */
72
+ export type RiskLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
73
+
61
74
  export interface ImpactResult {
62
- changed: string[] // The directly changed nodes
63
- impacted: string[] // Everything that depends on changed nodes
64
- depth: number // How many hops from change to furthest impact
65
- confidence: 'high' | 'medium' | 'low'
66
- /** Risk-classified breakdown of impacted nodes */
67
- classified: {
68
- critical: ClassifiedImpact[]
69
- high: ClassifiedImpact[]
70
- medium: ClassifiedImpact[]
71
- low: ClassifiedImpact[]
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
- id: string
78
- files: string[]
79
- confidence: number // 0.0 to 1.0
80
- suggestedName: string // inferred from folder names
81
- functions: string[] // function IDs in this cluster
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 importedPath of file.imports) {
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 === '`' || 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[]): string[] {
615
- const stripped = stripStringsAndComments(bodyLines.join('\n'))
616
- const calls = new Set<string>()
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)) calls.add(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.add(m[1])
648
+ calls.push({ name: m[1], line: baseLine, type: 'method' })
630
649
  }
631
650
 
632
- return [...calls]
651
+ return calls
633
652
  }
634
653
 
635
654
  function extractEdgeCases(bodyLines: string[]): string[] {
@@ -22,6 +22,8 @@ export class GoParser extends BaseParser {
22
22
  imports: extractor.extractImports(),
23
23
  exports: extractor.extractExports(),
24
24
  routes: extractor.extractRoutes(),
25
+ variables: [],
26
+ calls: [],
25
27
  hash: hashContent(content),
26
28
  parsedAt: Date.now(),
27
29
  }