@shvmgyl15/tsgraph 0.1.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 (187) hide show
  1. package/AGENTS.md +64 -0
  2. package/README.md +128 -0
  3. package/TODOS.md +61 -0
  4. package/dist/analysis/analysis.test.d.ts +2 -0
  5. package/dist/analysis/analysis.test.d.ts.map +1 -0
  6. package/dist/analysis/analysis.test.js +359 -0
  7. package/dist/analysis/analysis.test.js.map +1 -0
  8. package/dist/analysis/complexity.d.ts +8 -0
  9. package/dist/analysis/complexity.d.ts.map +1 -0
  10. package/dist/analysis/complexity.js +88 -0
  11. package/dist/analysis/complexity.js.map +1 -0
  12. package/dist/analysis/coupling.d.ts +17 -0
  13. package/dist/analysis/coupling.d.ts.map +1 -0
  14. package/dist/analysis/coupling.js +71 -0
  15. package/dist/analysis/coupling.js.map +1 -0
  16. package/dist/analysis/hotspot.d.ts +10 -0
  17. package/dist/analysis/hotspot.d.ts.map +1 -0
  18. package/dist/analysis/hotspot.js +33 -0
  19. package/dist/analysis/hotspot.js.map +1 -0
  20. package/dist/analysis/index.d.ts +9 -0
  21. package/dist/analysis/index.d.ts.map +1 -0
  22. package/dist/analysis/index.js +5 -0
  23. package/dist/analysis/index.js.map +1 -0
  24. package/dist/boundaries/index.d.ts +25 -0
  25. package/dist/boundaries/index.d.ts.map +1 -0
  26. package/dist/boundaries/index.js +103 -0
  27. package/dist/boundaries/index.js.map +1 -0
  28. package/dist/boundaries/index.test.d.ts +2 -0
  29. package/dist/boundaries/index.test.d.ts.map +1 -0
  30. package/dist/boundaries/index.test.js +293 -0
  31. package/dist/boundaries/index.test.js.map +1 -0
  32. package/dist/changes/index.d.ts +28 -0
  33. package/dist/changes/index.d.ts.map +1 -0
  34. package/dist/changes/index.js +48 -0
  35. package/dist/changes/index.js.map +1 -0
  36. package/dist/changes/index.test.d.ts +2 -0
  37. package/dist/changes/index.test.d.ts.map +1 -0
  38. package/dist/changes/index.test.js +104 -0
  39. package/dist/changes/index.test.js.map +1 -0
  40. package/dist/cli/index.d.ts +3 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +659 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/git/index.d.ts +16 -0
  45. package/dist/git/index.d.ts.map +1 -0
  46. package/dist/git/index.js +73 -0
  47. package/dist/git/index.js.map +1 -0
  48. package/dist/git/index.test.d.ts +2 -0
  49. package/dist/git/index.test.d.ts.map +1 -0
  50. package/dist/git/index.test.js +78 -0
  51. package/dist/git/index.test.js.map +1 -0
  52. package/dist/graph/types.d.ts +156 -0
  53. package/dist/graph/types.d.ts.map +1 -0
  54. package/dist/graph/types.js +166 -0
  55. package/dist/graph/types.js.map +1 -0
  56. package/dist/graph/types.test.d.ts +2 -0
  57. package/dist/graph/types.test.d.ts.map +1 -0
  58. package/dist/graph/types.test.js +326 -0
  59. package/dist/graph/types.test.js.map +1 -0
  60. package/dist/mcp/mcp.test.d.ts +2 -0
  61. package/dist/mcp/mcp.test.d.ts.map +1 -0
  62. package/dist/mcp/mcp.test.js +151 -0
  63. package/dist/mcp/mcp.test.js.map +1 -0
  64. package/dist/mcp/server.d.ts +2 -0
  65. package/dist/mcp/server.d.ts.map +1 -0
  66. package/dist/mcp/server.js +209 -0
  67. package/dist/mcp/server.js.map +1 -0
  68. package/dist/nextjs/index.d.ts +8 -0
  69. package/dist/nextjs/index.d.ts.map +1 -0
  70. package/dist/nextjs/index.js +16 -0
  71. package/dist/nextjs/index.js.map +1 -0
  72. package/dist/nextjs/nextjs.test.d.ts +2 -0
  73. package/dist/nextjs/nextjs.test.d.ts.map +1 -0
  74. package/dist/nextjs/nextjs.test.js +190 -0
  75. package/dist/nextjs/nextjs.test.js.map +1 -0
  76. package/dist/nextjs/pages.d.ts +4 -0
  77. package/dist/nextjs/pages.d.ts.map +1 -0
  78. package/dist/nextjs/pages.js +36 -0
  79. package/dist/nextjs/pages.js.map +1 -0
  80. package/dist/nextjs/react.d.ts +3 -0
  81. package/dist/nextjs/react.d.ts.map +1 -0
  82. package/dist/nextjs/react.js +86 -0
  83. package/dist/nextjs/react.js.map +1 -0
  84. package/dist/nextjs/router.d.ts +4 -0
  85. package/dist/nextjs/router.d.ts.map +1 -0
  86. package/dist/nextjs/router.js +86 -0
  87. package/dist/nextjs/router.js.map +1 -0
  88. package/dist/nextjs/routes.d.ts +4 -0
  89. package/dist/nextjs/routes.d.ts.map +1 -0
  90. package/dist/nextjs/routes.js +58 -0
  91. package/dist/nextjs/routes.js.map +1 -0
  92. package/dist/opencode/index.d.ts +7 -0
  93. package/dist/opencode/index.d.ts.map +1 -0
  94. package/dist/opencode/index.js +71 -0
  95. package/dist/opencode/index.js.map +1 -0
  96. package/dist/opencode/index.test.d.ts +2 -0
  97. package/dist/opencode/index.test.d.ts.map +1 -0
  98. package/dist/opencode/index.test.js +71 -0
  99. package/dist/opencode/index.test.js.map +1 -0
  100. package/dist/parser/index.d.ts +4 -0
  101. package/dist/parser/index.d.ts.map +1 -0
  102. package/dist/parser/index.js +282 -0
  103. package/dist/parser/index.js.map +1 -0
  104. package/dist/parser/parser.test.d.ts +2 -0
  105. package/dist/parser/parser.test.d.ts.map +1 -0
  106. package/dist/parser/parser.test.js +225 -0
  107. package/dist/parser/parser.test.js.map +1 -0
  108. package/dist/plan/index.d.ts +32 -0
  109. package/dist/plan/index.d.ts.map +1 -0
  110. package/dist/plan/index.js +107 -0
  111. package/dist/plan/index.js.map +1 -0
  112. package/dist/plan/index.test.d.ts +2 -0
  113. package/dist/plan/index.test.d.ts.map +1 -0
  114. package/dist/plan/index.test.js +143 -0
  115. package/dist/plan/index.test.js.map +1 -0
  116. package/dist/report/index.d.ts +9 -0
  117. package/dist/report/index.d.ts.map +1 -0
  118. package/dist/report/index.js +108 -0
  119. package/dist/report/index.js.map +1 -0
  120. package/dist/scanner/index.d.ts +13 -0
  121. package/dist/scanner/index.d.ts.map +1 -0
  122. package/dist/scanner/index.js +78 -0
  123. package/dist/scanner/index.js.map +1 -0
  124. package/dist/scanner/scanner.test.d.ts +2 -0
  125. package/dist/scanner/scanner.test.d.ts.map +1 -0
  126. package/dist/scanner/scanner.test.js +113 -0
  127. package/dist/scanner/scanner.test.js.map +1 -0
  128. package/dist/search/index.d.ts +32 -0
  129. package/dist/search/index.d.ts.map +1 -0
  130. package/dist/search/index.js +97 -0
  131. package/dist/search/index.js.map +1 -0
  132. package/dist/search/search.test.d.ts +2 -0
  133. package/dist/search/search.test.d.ts.map +1 -0
  134. package/dist/search/search.test.js +446 -0
  135. package/dist/search/search.test.js.map +1 -0
  136. package/dist/traversal/index.d.ts +5 -0
  137. package/dist/traversal/index.d.ts.map +1 -0
  138. package/dist/traversal/index.js +3 -0
  139. package/dist/traversal/index.js.map +1 -0
  140. package/dist/traversal/traversal.d.ts +31 -0
  141. package/dist/traversal/traversal.d.ts.map +1 -0
  142. package/dist/traversal/traversal.js +130 -0
  143. package/dist/traversal/traversal.js.map +1 -0
  144. package/dist/traversal/traversal.test.d.ts +2 -0
  145. package/dist/traversal/traversal.test.d.ts.map +1 -0
  146. package/dist/traversal/traversal.test.js +224 -0
  147. package/dist/traversal/traversal.test.js.map +1 -0
  148. package/opencode.json +24 -0
  149. package/package.json +29 -0
  150. package/src/analysis/analysis.test.ts +405 -0
  151. package/src/analysis/complexity.ts +107 -0
  152. package/src/analysis/coupling.ts +106 -0
  153. package/src/analysis/hotspot.ts +52 -0
  154. package/src/analysis/index.ts +17 -0
  155. package/src/boundaries/index.test.ts +335 -0
  156. package/src/boundaries/index.ts +137 -0
  157. package/src/changes/index.test.ts +114 -0
  158. package/src/changes/index.ts +95 -0
  159. package/src/cli/index.ts +736 -0
  160. package/src/git/index.test.ts +92 -0
  161. package/src/git/index.ts +86 -0
  162. package/src/graph/types.test.ts +383 -0
  163. package/src/graph/types.ts +353 -0
  164. package/src/mcp/mcp.test.ts +176 -0
  165. package/src/mcp/server.ts +217 -0
  166. package/src/nextjs/index.ts +23 -0
  167. package/src/nextjs/nextjs.test.ts +233 -0
  168. package/src/nextjs/pages.ts +43 -0
  169. package/src/nextjs/react.ts +100 -0
  170. package/src/nextjs/router.ts +102 -0
  171. package/src/nextjs/routes.ts +69 -0
  172. package/src/opencode/index.test.ts +90 -0
  173. package/src/opencode/index.ts +83 -0
  174. package/src/parser/index.ts +339 -0
  175. package/src/parser/parser.test.ts +282 -0
  176. package/src/plan/index.test.ts +162 -0
  177. package/src/plan/index.ts +161 -0
  178. package/src/report/index.ts +128 -0
  179. package/src/scanner/index.ts +97 -0
  180. package/src/scanner/scanner.test.ts +135 -0
  181. package/src/search/index.ts +163 -0
  182. package/src/search/search.test.ts +512 -0
  183. package/src/traversal/index.ts +5 -0
  184. package/src/traversal/traversal.test.ts +266 -0
  185. package/src/traversal/traversal.ts +185 -0
  186. package/tsconfig.json +20 -0
  187. package/vitest.config.ts +7 -0
@@ -0,0 +1,405 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import {
6
+ makeGraph,
7
+ makeSymbolNode,
8
+ makeCallEdge,
9
+ makeImportEdge,
10
+ makePackageNode,
11
+ makeFileNode,
12
+ } from "../graph/types.js";
13
+ import { cyclomaticComplexity, analyzeComplexity } from "./complexity.js";
14
+ import { findHotspots } from "./hotspot.js";
15
+ import { analyzeCoupling, dependencyTree } from "./coupling.js";
16
+
17
+ function createTempDir(): string {
18
+ return fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-test-"));
19
+ }
20
+
21
+ describe("cyclomaticComplexity", () => {
22
+ it("returns 1 for a function with no branches", () => {
23
+ const code = `function greet() { return "hello"; }`;
24
+ expect(cyclomaticComplexity(code)).toBe(1);
25
+ });
26
+
27
+ it("counts if statements", () => {
28
+ const code = `function f() {
29
+ if (a) return 1;
30
+ if (b) return 2;
31
+ }`;
32
+ expect(cyclomaticComplexity(code)).toBe(3);
33
+ });
34
+
35
+ it("counts if-else as single branch point", () => {
36
+ const code = `function f() {
37
+ if (a) { return 1; }
38
+ else { return 2; }
39
+ }`;
40
+ expect(cyclomaticComplexity(code)).toBe(2);
41
+ });
42
+
43
+ it("counts else-if as additional branch", () => {
44
+ const code = `function f() {
45
+ if (a) { return 1; }
46
+ else if (b) { return 2; }
47
+ else { return 3; }
48
+ }`;
49
+ expect(cyclomaticComplexity(code)).toBe(3);
50
+ });
51
+
52
+ it("counts for loops", () => {
53
+ const code = `function f() {
54
+ for (let i = 0; i < 10; i++) {}
55
+ }`;
56
+ expect(cyclomaticComplexity(code)).toBe(2);
57
+ });
58
+
59
+ it("counts while loops", () => {
60
+ const code = `function f() {
61
+ while (true) { break; }
62
+ }`;
63
+ expect(cyclomaticComplexity(code)).toBe(2);
64
+ });
65
+
66
+ it("counts case labels", () => {
67
+ const code = `function f(x: number) {
68
+ switch (x) {
69
+ case 1: return "a";
70
+ case 2: return "b";
71
+ case 3: return "c";
72
+ }
73
+ }`;
74
+ expect(cyclomaticComplexity(code)).toBe(4);
75
+ });
76
+
77
+ it("counts catch clauses", () => {
78
+ const code = `function f() {
79
+ try { doStuff(); }
80
+ catch (e) { handle(); }
81
+ }`;
82
+ expect(cyclomaticComplexity(code)).toBe(2);
83
+ });
84
+
85
+ it("counts ternary operators", () => {
86
+ const code = `function f() {
87
+ return a ? b : c;
88
+ }`;
89
+ expect(cyclomaticComplexity(code)).toBe(2);
90
+ });
91
+
92
+ it("counts logical && and ||", () => {
93
+ const code = `function f() {
94
+ if (a && b || c) return 1;
95
+ }`;
96
+ expect(cyclomaticComplexity(code)).toBe(4);
97
+ });
98
+
99
+ it("avoids counting string literals", () => {
100
+ const code = `function f() {
101
+ const s = "if (true) { for(;;) {} }";
102
+ return s;
103
+ }`;
104
+ expect(cyclomaticComplexity(code)).toBe(1);
105
+ });
106
+
107
+ it("ignores comments", () => {
108
+ const code = `function f() {
109
+ // if (true) { return 1; }
110
+ /* for(;;) {} */
111
+ return 0;
112
+ }`;
113
+ expect(cyclomaticComplexity(code)).toBe(1);
114
+ });
115
+ });
116
+
117
+ describe("analyzeComplexity", () => {
118
+ it("returns complexity for all functions in a project", () => {
119
+ const dir = createTempDir();
120
+ const filePath = path.join(dir, "src/lib.ts");
121
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
122
+ fs.writeFileSync(
123
+ filePath,
124
+ [
125
+ "export function simple() { return 1; }",
126
+ "export function complex() {",
127
+ " if (a) { for(;;) {} }",
128
+ " return 0;",
129
+ "}",
130
+ ].join("\n"),
131
+ "utf-8",
132
+ );
133
+
134
+ const graph = makeGraph({
135
+ root: dir,
136
+ files: [makeFileNode({ path: "src/lib.ts", lines: 5 })],
137
+ symbols: [
138
+ makeSymbolNode({
139
+ id: "simple",
140
+ name: "simple",
141
+ kind: "function",
142
+ file: "src/lib.ts",
143
+ line: 1,
144
+ endLine: 1,
145
+ packageName: "app",
146
+ isExported: true,
147
+ }),
148
+ makeSymbolNode({
149
+ id: "complex",
150
+ name: "complex",
151
+ kind: "function",
152
+ file: "src/lib.ts",
153
+ line: 2,
154
+ endLine: 5,
155
+ packageName: "app",
156
+ isExported: true,
157
+ }),
158
+ ],
159
+ });
160
+
161
+ const results = analyzeComplexity(graph);
162
+ expect(results).toHaveLength(2);
163
+ const simple = results.find((r) => r.symbol.name === "simple");
164
+ const complex = results.find((r) => r.symbol.name === "complex");
165
+ expect(simple!.complexity).toBe(1);
166
+ expect(complex!.complexity).toBeGreaterThanOrEqual(2);
167
+
168
+ fs.rmSync(dir, { recursive: true });
169
+ });
170
+
171
+ it("filters by file when specified", () => {
172
+ const dir = createTempDir();
173
+ const filePath = path.join(dir, "src/a.ts");
174
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
175
+ fs.writeFileSync(filePath, "export function foo() {}", "utf-8");
176
+
177
+ const graph = makeGraph({
178
+ root: dir,
179
+ symbols: [
180
+ makeSymbolNode({
181
+ id: "foo",
182
+ name: "foo",
183
+ kind: "function",
184
+ file: "src/a.ts",
185
+ line: 1,
186
+ endLine: 1,
187
+ }),
188
+ makeSymbolNode({
189
+ id: "bar",
190
+ name: "bar",
191
+ kind: "function",
192
+ file: "src/b.ts",
193
+ line: 1,
194
+ endLine: 1,
195
+ }),
196
+ ],
197
+ });
198
+
199
+ const results = analyzeComplexity(graph, "a.ts");
200
+ expect(results).toHaveLength(1);
201
+ expect(results[0].symbol.name).toBe("foo");
202
+
203
+ fs.rmSync(dir, { recursive: true });
204
+ });
205
+ });
206
+
207
+ describe("findHotspots", () => {
208
+ it("returns top hotspots sorted by score", () => {
209
+ const dir = createTempDir();
210
+ const filePath = path.join(dir, "src/hot.ts");
211
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
212
+ fs.writeFileSync(
213
+ filePath,
214
+ [
215
+ "export function a() {",
216
+ " if (x) { for(;;) {} }",
217
+ " if (y) { while(z) {} }",
218
+ " return 0;",
219
+ "}",
220
+ "export function b() { return 1; }",
221
+ ].join("\n"),
222
+ "utf-8",
223
+ );
224
+
225
+ const graph = makeGraph({
226
+ root: dir,
227
+ files: [makeFileNode({ path: "src/hot.ts", lines: 6 })],
228
+ symbols: [
229
+ makeSymbolNode({
230
+ id: "a",
231
+ name: "a",
232
+ kind: "function",
233
+ file: "src/hot.ts",
234
+ line: 1,
235
+ endLine: 5,
236
+ packageName: "app",
237
+ }),
238
+ makeSymbolNode({
239
+ id: "b",
240
+ name: "b",
241
+ kind: "function",
242
+ file: "src/hot.ts",
243
+ line: 6,
244
+ endLine: 6,
245
+ packageName: "app",
246
+ }),
247
+ ],
248
+ });
249
+
250
+ const hotspots = findHotspots(graph, 5);
251
+ expect(hotspots.length).toBeGreaterThanOrEqual(1);
252
+ expect(hotspots[0].file).toBe("src/hot.ts");
253
+
254
+ fs.rmSync(dir, { recursive: true });
255
+ });
256
+ });
257
+
258
+ describe("analyzeCoupling", () => {
259
+ it("finds packages importing from other modules", () => {
260
+ const graph = makeGraph({
261
+ files: [
262
+ makeFileNode({ path: "src/orders.ts", packageName: "orders" }),
263
+ makeFileNode({ path: "src/payment.ts", packageName: "payment" }),
264
+ makeFileNode({ path: "src/notify.ts", packageName: "notify" }),
265
+ ],
266
+ imports: [
267
+ makeImportEdge({
268
+ fromFile: "src/orders.ts",
269
+ fromPackage: "orders",
270
+ importPath: "payment",
271
+ alias: "payment",
272
+ isDefault: true,
273
+ }),
274
+ makeImportEdge({
275
+ fromFile: "src/orders.ts",
276
+ fromPackage: "orders",
277
+ importPath: "notify",
278
+ alias: "notify",
279
+ isDefault: true,
280
+ }),
281
+ makeImportEdge({
282
+ fromFile: "src/orders.ts",
283
+ fromPackage: "orders",
284
+ importPath: "payment",
285
+ alias: "processPayment",
286
+ isDefault: false,
287
+ }),
288
+ ],
289
+ });
290
+
291
+ const results = analyzeCoupling(graph);
292
+ expect(results).toHaveLength(2);
293
+ const paymentCoupling = results.find((r) => r.coupledTo === "payment");
294
+ expect(paymentCoupling).toBeTruthy();
295
+ expect(paymentCoupling!.importCount).toBe(2);
296
+ expect(paymentCoupling!.packageName).toBe("orders");
297
+ });
298
+ });
299
+
300
+ describe("dependencyTree", () => {
301
+ it("builds a tree from call edges", () => {
302
+ const graph = makeGraph({
303
+ symbols: [
304
+ makeSymbolNode({
305
+ id: "src/main.ts::serve",
306
+ name: "serve",
307
+ kind: "function",
308
+ file: "src/main.ts",
309
+ line: 1,
310
+ endLine: 5,
311
+ packageName: "app",
312
+ }),
313
+ makeSymbolNode({
314
+ id: "src/main.ts::greet",
315
+ name: "greet",
316
+ kind: "function",
317
+ file: "src/main.ts",
318
+ line: 6,
319
+ endLine: 8,
320
+ packageName: "app",
321
+ }),
322
+ makeSymbolNode({
323
+ id: "src/main.ts::log",
324
+ name: "log",
325
+ kind: "function",
326
+ file: "src/main.ts",
327
+ line: 9,
328
+ endLine: 11,
329
+ packageName: "app",
330
+ }),
331
+ ],
332
+ calls: [
333
+ makeCallEdge({
334
+ callerSymbolId: "src/main.ts::serve",
335
+ callerName: "serve",
336
+ calleeRaw: "greet",
337
+ file: "src/main.ts",
338
+ line: 3,
339
+ }),
340
+ makeCallEdge({
341
+ callerSymbolId: "src/main.ts::serve",
342
+ callerName: "serve",
343
+ calleeRaw: "log",
344
+ file: "src/main.ts",
345
+ line: 4,
346
+ }),
347
+ ],
348
+ });
349
+
350
+ const tree = dependencyTree(graph, "serve");
351
+ expect(tree).toBeTruthy();
352
+ expect(tree!.name).toBe("serve");
353
+ expect(tree!.children).toHaveLength(2);
354
+ const childNames = tree!.children.map((c) => c.name).sort();
355
+ expect(childNames).toEqual(["greet", "log"]);
356
+ });
357
+
358
+ it("returns undefined for unknown symbol", () => {
359
+ const graph = makeGraph();
360
+ const tree = dependencyTree(graph, "noop");
361
+ expect(tree).toBeUndefined();
362
+ });
363
+
364
+ it("respects max depth", () => {
365
+ const graph = makeGraph({
366
+ symbols: [
367
+ makeSymbolNode({
368
+ id: "a",
369
+ name: "a",
370
+ kind: "function",
371
+ file: "a.ts",
372
+ line: 1,
373
+ endLine: 1,
374
+ packageName: "app",
375
+ }),
376
+ makeSymbolNode({
377
+ id: "b",
378
+ name: "b",
379
+ kind: "function",
380
+ file: "b.ts",
381
+ line: 1,
382
+ endLine: 1,
383
+ packageName: "app",
384
+ }),
385
+ makeSymbolNode({
386
+ id: "c",
387
+ name: "c",
388
+ kind: "function",
389
+ file: "c.ts",
390
+ line: 1,
391
+ endLine: 1,
392
+ packageName: "app",
393
+ }),
394
+ ],
395
+ calls: [
396
+ makeCallEdge({ callerSymbolId: "a", callerName: "a", calleeRaw: "b", file: "a.ts", line: 1 }),
397
+ makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "c", file: "b.ts", line: 1 }),
398
+ ],
399
+ });
400
+
401
+ const tree = dependencyTree(graph, "a", 1);
402
+ expect(tree!.children).toHaveLength(1);
403
+ expect(tree!.children[0].children).toHaveLength(0);
404
+ });
405
+ });
@@ -0,0 +1,107 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { Graph, SymbolNode } from "../graph/types.js";
4
+
5
+ interface DecisionPattern {
6
+ regex: RegExp;
7
+ label: string;
8
+ }
9
+
10
+ const DECISION_PATTERNS: DecisionPattern[] = [
11
+ { regex: /\bif\s*\(/g, label: "if" },
12
+ { regex: /\belse\s+if\s*\(/g, label: "elseif" },
13
+ { regex: /\bfor\s*\(/g, label: "for" },
14
+ { regex: /\bwhile\s*\(/g, label: "while" },
15
+ { regex: /\bdo\s*\{/g, label: "do" },
16
+ { regex: /\bcatch\s*\(/g, label: "catch" },
17
+ { regex: /\bcase\s+/g, label: "case" },
18
+ { regex: /\|\|/g, label: "or" },
19
+ { regex: /&&/g, label: "and" },
20
+ ];
21
+
22
+ function stripComments(code: string): string {
23
+ return code
24
+ .replace(/\/\/.*$/gm, "")
25
+ .replace(/\/\*[\s\S]*?\*\//g, "");
26
+ }
27
+
28
+ function stripStrings(code: string): string {
29
+ const inString = (s: string) => {
30
+ let result = "";
31
+ let i = 0;
32
+ while (i < s.length) {
33
+ if (s[i] === '"' || s[i] === "'" || s[i] === "`") {
34
+ const quote = s[i];
35
+ i++;
36
+ while (i < s.length && s[i] !== quote) {
37
+ if (s[i] === "\\") i++;
38
+ i++;
39
+ }
40
+ } else {
41
+ result += s[i];
42
+ }
43
+ i++;
44
+ }
45
+ return result;
46
+ };
47
+ return inString(code);
48
+ }
49
+
50
+ export function cyclomaticComplexity(sourceCode: string): number {
51
+ const cleaned = stripStrings(stripComments(sourceCode));
52
+ let decisions = 0;
53
+
54
+ for (const dp of DECISION_PATTERNS) {
55
+ const matches = cleaned.match(dp.regex);
56
+ if (matches) decisions += matches.length;
57
+ }
58
+
59
+ const elseIfCount = (cleaned.match(/\belse\s+if\s*\(/g) || []).length;
60
+ decisions -= elseIfCount;
61
+
62
+ const ternaryMatches = cleaned.match(/\?/g);
63
+ if (ternaryMatches) {
64
+ for (const m of ternaryMatches) {
65
+ const idx = cleaned.indexOf(m);
66
+ const before = cleaned[idx - 1];
67
+ if (before !== "?" && before !== "!" && before !== "=") {
68
+ decisions++;
69
+ }
70
+ }
71
+ }
72
+
73
+ return Math.max(1, 1 + decisions);
74
+ }
75
+
76
+ export interface ComplexityResult {
77
+ symbol: SymbolNode;
78
+ complexity: number;
79
+ }
80
+
81
+ export function analyzeComplexity(
82
+ graph: Graph,
83
+ fileFilter?: string,
84
+ ): ComplexityResult[] {
85
+ const results: ComplexityResult[] = [];
86
+ const root = graph.root;
87
+
88
+ for (const sym of graph.symbols) {
89
+ if (fileFilter && !sym.file.includes(fileFilter)) continue;
90
+ if (sym.kind !== "function" && sym.kind !== "method") continue;
91
+
92
+ const filePath = path.join(root, sym.file);
93
+ try {
94
+ const content = fs.readFileSync(filePath, "utf-8");
95
+ const lines = content.split("\n");
96
+ const start = Math.max(0, sym.line - 1);
97
+ const end = Math.min(lines.length, sym.endLine);
98
+ const snippet = lines.slice(start, end).join("\n");
99
+ const complexity = cyclomaticComplexity(snippet);
100
+ results.push({ symbol: sym, complexity });
101
+ } catch {
102
+ continue;
103
+ }
104
+ }
105
+
106
+ return results;
107
+ }
@@ -0,0 +1,106 @@
1
+ import type { Graph, ImportEdge, SymbolNode } from "../graph/types.js";
2
+
3
+ export interface CouplingResult {
4
+ packageName: string;
5
+ coupledTo: string;
6
+ importCount: number;
7
+ fileCount: number;
8
+ }
9
+
10
+ export interface DepsNode {
11
+ name: string;
12
+ kind: string;
13
+ file: string;
14
+ line: number;
15
+ children: DepsNode[];
16
+ }
17
+
18
+ export function analyzeCoupling(graph: Graph): CouplingResult[] {
19
+ const pkgFiles = new Map<string, Set<string>>();
20
+ for (const f of graph.files) {
21
+ const files = pkgFiles.get(f.packageName) ?? new Set();
22
+ files.add(f.path);
23
+ pkgFiles.set(f.packageName, files);
24
+ }
25
+
26
+ const pkgImportTargets = new Map<string, Map<string, { files: Set<string>; count: number }>>();
27
+ for (const imp of graph.imports) {
28
+ const target = imp.importPath.split("/")[0];
29
+ const byPkg = pkgImportTargets.get(imp.fromPackage) ?? new Map();
30
+ const entry = byPkg.get(target) ?? { files: new Set<string>(), count: 0 };
31
+ entry.files.add(imp.fromFile);
32
+ entry.count++;
33
+ byPkg.set(target, entry);
34
+ pkgImportTargets.set(imp.fromPackage, byPkg);
35
+ }
36
+
37
+ const results: CouplingResult[] = [];
38
+ for (const [pkg, targets] of pkgImportTargets) {
39
+ for (const [target, entry] of targets) {
40
+ if (pkg === target) continue;
41
+ results.push({
42
+ packageName: pkg,
43
+ coupledTo: target,
44
+ importCount: entry.count,
45
+ fileCount: entry.files.size,
46
+ });
47
+ }
48
+ }
49
+
50
+ results.sort((a, b) => b.importCount - a.importCount);
51
+ return results;
52
+ }
53
+
54
+ export function dependencyTree(
55
+ graph: Graph,
56
+ symbolName: string,
57
+ maxDepth: number = 3,
58
+ ): DepsNode | undefined {
59
+ const sym = graph.symbols.find((s) => s.name === symbolName);
60
+ if (!sym) return undefined;
61
+
62
+ function buildTree(
63
+ current: SymbolNode,
64
+ depth: number,
65
+ visited: Set<string>,
66
+ ): DepsNode {
67
+ const node: DepsNode = {
68
+ name: current.name,
69
+ kind: current.kind,
70
+ file: current.file,
71
+ line: current.line,
72
+ children: [],
73
+ };
74
+
75
+ if (depth >= maxDepth) return node;
76
+
77
+ const calleeEdges = graph.calls.filter(
78
+ (c) => c.callerSymbolId === current.id,
79
+ );
80
+
81
+ for (const edge of calleeEdges) {
82
+ if (visited.has(edge.calleeRaw)) continue;
83
+ visited.add(edge.calleeRaw);
84
+
85
+ const calleeSym = graph.symbols.find(
86
+ (s) => s.name === edge.calleeRaw && s.file === edge.file,
87
+ );
88
+
89
+ if (calleeSym) {
90
+ node.children.push(buildTree(calleeSym, depth + 1, visited));
91
+ } else {
92
+ node.children.push({
93
+ name: edge.calleeRaw,
94
+ kind: "unknown",
95
+ file: edge.file,
96
+ line: edge.line,
97
+ children: [],
98
+ });
99
+ }
100
+ }
101
+
102
+ return node;
103
+ }
104
+
105
+ return buildTree(sym, 0, new Set([symbolName]));
106
+ }
@@ -0,0 +1,52 @@
1
+ import type { Graph } from "../graph/types.js";
2
+ import { analyzeComplexity } from "./complexity.js";
3
+
4
+ export interface HotspotResult {
5
+ file: string;
6
+ score: number;
7
+ symbolCount: number;
8
+ totalComplexity: number;
9
+ lines: number;
10
+ }
11
+
12
+ export function findHotspots(
13
+ graph: Graph,
14
+ topN: number = 10,
15
+ ): HotspotResult[] {
16
+ const fileNodes = new Map(graph.files.map((f) => [f.path, f]));
17
+ const complexityResults = analyzeComplexity(graph);
18
+ const fileScores = new Map<
19
+ string,
20
+ { totalComplexity: number; symbolCount: number; lines: number }
21
+ >();
22
+
23
+ for (const r of complexityResults) {
24
+ const entry = fileScores.get(r.symbol.file) ?? {
25
+ totalComplexity: 0,
26
+ symbolCount: 0,
27
+ lines: 1,
28
+ };
29
+ entry.totalComplexity += r.complexity;
30
+ entry.symbolCount++;
31
+ fileScores.set(r.symbol.file, entry);
32
+ }
33
+
34
+ for (const [filePath, entry] of fileScores) {
35
+ const fn = fileNodes.get(filePath);
36
+ entry.lines = fn?.lines ?? 1;
37
+ }
38
+
39
+ const results: HotspotResult[] = [];
40
+ for (const [file, data] of fileScores) {
41
+ results.push({
42
+ file,
43
+ score: data.totalComplexity * data.lines,
44
+ symbolCount: data.symbolCount,
45
+ totalComplexity: data.totalComplexity,
46
+ lines: data.lines,
47
+ });
48
+ }
49
+
50
+ results.sort((a, b) => b.score - a.score);
51
+ return results.slice(0, topN);
52
+ }
@@ -0,0 +1,17 @@
1
+ import type { Graph } from "../graph/types.js";
2
+ import { analyzeComplexity, cyclomaticComplexity } from "./complexity.js";
3
+ import type { ComplexityResult } from "./complexity.js";
4
+ import { findHotspots } from "./hotspot.js";
5
+ import type { HotspotResult } from "./hotspot.js";
6
+ import { analyzeCoupling, dependencyTree } from "./coupling.js";
7
+ import type { CouplingResult, DepsNode } from "./coupling.js";
8
+
9
+ export {
10
+ analyzeComplexity,
11
+ cyclomaticComplexity,
12
+ findHotspots,
13
+ analyzeCoupling,
14
+ dependencyTree,
15
+ };
16
+
17
+ export type { ComplexityResult, HotspotResult, CouplingResult, DepsNode };