@shvmgyl15/tsgraph 0.1.0 → 0.2.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 (55) hide show
  1. package/dist/changes/index.test.js +2 -6
  2. package/dist/changes/index.test.js.map +1 -1
  3. package/dist/cli/index.js +184 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/git/index.test.js +4 -6
  6. package/dist/git/index.test.js.map +1 -1
  7. package/dist/opencode/index.js +1 -1
  8. package/dist/opencode/index.js.map +1 -1
  9. package/dist/opencode/index.test.js +2 -2
  10. package/dist/opencode/index.test.js.map +1 -1
  11. package/dist/search/index.d.ts.map +1 -1
  12. package/dist/search/index.js +12 -4
  13. package/dist/search/index.js.map +1 -1
  14. package/package.json +16 -1
  15. package/AGENTS.md +0 -64
  16. package/TODOS.md +0 -61
  17. package/opencode.json +0 -24
  18. package/src/analysis/analysis.test.ts +0 -405
  19. package/src/analysis/complexity.ts +0 -107
  20. package/src/analysis/coupling.ts +0 -106
  21. package/src/analysis/hotspot.ts +0 -52
  22. package/src/analysis/index.ts +0 -17
  23. package/src/boundaries/index.test.ts +0 -335
  24. package/src/boundaries/index.ts +0 -137
  25. package/src/changes/index.test.ts +0 -114
  26. package/src/changes/index.ts +0 -95
  27. package/src/cli/index.ts +0 -736
  28. package/src/git/index.test.ts +0 -92
  29. package/src/git/index.ts +0 -86
  30. package/src/graph/types.test.ts +0 -383
  31. package/src/graph/types.ts +0 -353
  32. package/src/mcp/mcp.test.ts +0 -176
  33. package/src/mcp/server.ts +0 -217
  34. package/src/nextjs/index.ts +0 -23
  35. package/src/nextjs/nextjs.test.ts +0 -233
  36. package/src/nextjs/pages.ts +0 -43
  37. package/src/nextjs/react.ts +0 -100
  38. package/src/nextjs/router.ts +0 -102
  39. package/src/nextjs/routes.ts +0 -69
  40. package/src/opencode/index.test.ts +0 -90
  41. package/src/opencode/index.ts +0 -83
  42. package/src/parser/index.ts +0 -339
  43. package/src/parser/parser.test.ts +0 -282
  44. package/src/plan/index.test.ts +0 -162
  45. package/src/plan/index.ts +0 -161
  46. package/src/report/index.ts +0 -128
  47. package/src/scanner/index.ts +0 -97
  48. package/src/scanner/scanner.test.ts +0 -135
  49. package/src/search/index.ts +0 -163
  50. package/src/search/search.test.ts +0 -512
  51. package/src/traversal/index.ts +0 -5
  52. package/src/traversal/traversal.test.ts +0 -266
  53. package/src/traversal/traversal.ts +0 -185
  54. package/tsconfig.json +0 -20
  55. package/vitest.config.ts +0 -7
package/AGENTS.md DELETED
@@ -1,64 +0,0 @@
1
- # tsgraph — Project DNA
2
-
3
- ## Vision
4
- A fast, local-only CLI tool that indexes Next.js / React / TypeScript codebases using AST
5
- parsing into a queryable graph.json for AI coding agents. Equivalent to gograph but for TS.
6
- Output is Markdown + JSON. No network calls, no telemetry, no SaaS backend.
7
-
8
- ## Design Philosophy (Go → TS Adaptation)
9
- tsgraph is inspired by gograph (Go) but adapted for TypeScript/React/Next.js — NOT a blind copy.
10
- Key language-driven differences:
11
- - **Export model**: TS `export` keyword maps to `isExported`; class methods are always considered exported (default `public`)
12
- - **No goroutines/channels** — replaced by async/promise/setTimeout concurrency
13
- - **No struct tags** — TS has no native equivalent
14
- - **Router detection** is Next.js App Router / Pages Router, not Gin/mux
15
- - **React components** tracked via `isClientComponent` / `isServerComponent`
16
- - **Interface satisfaction** is structural in TS — `ImplementsEdge` may be dropped if unused
17
- - Go-specific concepts (structs with fields+tests, `MutationEdge`, `StructField.tags`) are present but may be removed if they don't earn their keep for TS
18
-
19
- ## Tech Stack
20
- - Runtime: Node.js + tsx (dev) / tsc (build)
21
- - AST: ts-morph (wraps TypeScript compiler API)
22
- - CLI: commander
23
- - MCP: @modelcontextprotocol/sdk
24
- - Testing: vitest
25
- - Linting: none (tsc strict mode is sufficient)
26
-
27
- ## Agent Rules
28
-
29
- ### Task Management
30
- - READ TODOS.md at session start to know what's done and what's next
31
- - UPDATE TODOS.md when you start/finish a task (`[.]` in-progress, `[x]` done)
32
- - Work in phase order unless a task has no blockers
33
-
34
- ### Orchestration
35
- - This is a single-orchestrator project. When a task has multiple independent
36
- sub-tasks, delegate via the `task` tool (`subagent_type: general`) rather
37
- than doing them sequentially.
38
- - For each delegated sub-task, specify:
39
- 1. Exact files the sub-agent may modify
40
- 2. Which phase from TODOS.md it belongs to
41
- 3. What to return (never let sub-agents commit or merge)
42
- - After all sub-tasks complete, run `npm run build && npm test` and fix
43
- any issues directly. Do NOT re-delegate broken builds.
44
-
45
- ### Quality
46
- - Run `npm run build` (tsc) AND `npm test` (vitest) after every task completion
47
- - Fix all type errors and test failures before marking `[x]`
48
- - If build is already broken when you start, note it in TODOS.md and fix it first
49
-
50
- ### Research
51
- - Use webfetch when unsure about an API — check ts-morph docs, Next.js docs,
52
- or reference gograph's Go source at https://github.com/ozgurcd/gograph
53
- - DO NOT guess API signatures
54
-
55
- ### Code Style
56
- - No comments in source files unless logic is non-obvious
57
- - Named exports only (no default exports)
58
- - Strict TypeScript everywhere, avoid `any`
59
- - Follow patterns from adjacent files in the codebase
60
- - No emojis in source code or commit messages
61
-
62
- ### Communication
63
- - Be concise. Use TODOS.md for status, respond with only what's needed.
64
- - If stuck, explain the blocker clearly rather than overthinking.
package/TODOS.md DELETED
@@ -1,61 +0,0 @@
1
- # tsgraph — Implementation Plan
2
-
3
- ## Phase 1: Project Scaffold
4
- - [x] Update opencode.json with AGENTS.md reference
5
- - [x] Create AGENTS.md with project DNA
6
- - [x] Create TODOS.md (this file)
7
- - [x] Initialize package.json with dependencies
8
- - [x] Configure tsconfig.json (strict, ESNext)
9
- - [x] Setup vitest config
10
- - [x] Create src directory structure
11
-
12
- ## Phase 2: Core Data Model
13
- - [x] Define graph types (Graph, PackageNode, FileNode, SymbolNode, etc.)
14
- - [x] Add JSON serialization / deserialization
15
- - [x] Write unit tests for graph types
16
-
17
- ## Phase 3: Scanner + Parser Core
18
- - [x] Implement file scanner (walk tree, gitignore support, file classification)
19
- - [x] Implement symbol extractor (ts-morph: functions, classes, interfaces, types, enums, vars)
20
- - [x] Implement call expression extractor
21
- - [x] Implement import edge + dependency (package.json) extractor
22
- - [x] Wire up `build` command end-to-end
23
- - [x] Write parser/scanner unit tests
24
-
25
- ## Phase 4: Query Commands
26
- - [x] callers / callees
27
- - [x] node / source / query
28
- - [x] context (bundle — node + source + callers + callees + tests)
29
- - [x] imports / public / focus
30
- - [x] Write query command tests
31
-
32
- ## Phase 5: Next.js / React Extractors
33
- - [x] App Router tree detection (page/layout/loading/error/route files)
34
- - [x] Pages Router detection
35
- - [x] 'use client' / 'use server' + hooks analysis
36
- - [x] Route extraction from API / route handlers
37
- - [x] Write extractor tests
38
-
39
- ## Phase 6: Analysis Commands
40
- - [x] complexity (cyclomatic)
41
- - [x] hotspot / coupling / deps
42
- - [x] Write analysis tests
43
-
44
- ## Phase 7: Graph Traversal
45
- - [x] impact (BFS downstream blast radius)
46
- - [x] path (BFS shortest path between symbols)
47
- - [x] orphans (dead code detection)
48
- - [x] trace / errorflow (reverse BFS from string literal)
49
- - [x] Write traversal tests
50
-
51
- ## Phase 8: MCP Server
52
- - [x] MCP stdio server wrapping all query tools
53
- - [x] Tool definition for each search/query command
54
- - [x] MCP integration test
55
-
56
- ## Phase 9: Advanced Features
57
- - [x] boundaries (architecture enforcement via .tsgraph/boundaries.json)
58
- - [x] changes / stale (git-aware incremental analysis)
59
- - [x] plan / review (change planning reports)
60
- - [x] add-opencode-plugin (auto-configure opencode MCP + agent)
61
- - [x] Enhanced GRAPH_REPORT.md (hotspots, boundaries, coupling, stale)
package/opencode.json DELETED
@@ -1,24 +0,0 @@
1
- {
2
- "$schema": "https://opencode.ai/config.json",
3
- "model": "opencode-go/deepseek-v4-flash",
4
- "default_agent": "plan",
5
- "instructions": [
6
- "AGENTS.md"
7
- ],
8
- "permission": {
9
- "read": {
10
- "*": "allow",
11
- "*.env": "allow",
12
- "*.env.*": "allow"
13
- },
14
- "external_directory": {
15
- "/tmp/**": "allow"
16
- },
17
- "grep": "allow",
18
- "glob": "allow",
19
- "lsp": "allow",
20
- "skill": "allow",
21
- "webfetch": "allow",
22
- "websearch": "allow"
23
- }
24
- }
@@ -1,405 +0,0 @@
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
- });
@@ -1,107 +0,0 @@
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
- }