@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.
- package/dist/changes/index.test.js +2 -6
- package/dist/changes/index.test.js.map +1 -1
- package/dist/cli/index.js +184 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/git/index.test.js +4 -6
- package/dist/git/index.test.js.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/opencode/index.js.map +1 -1
- package/dist/opencode/index.test.js +2 -2
- package/dist/opencode/index.test.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +12 -4
- package/dist/search/index.js.map +1 -1
- package/package.json +16 -1
- package/AGENTS.md +0 -64
- package/TODOS.md +0 -61
- package/opencode.json +0 -24
- package/src/analysis/analysis.test.ts +0 -405
- package/src/analysis/complexity.ts +0 -107
- package/src/analysis/coupling.ts +0 -106
- package/src/analysis/hotspot.ts +0 -52
- package/src/analysis/index.ts +0 -17
- package/src/boundaries/index.test.ts +0 -335
- package/src/boundaries/index.ts +0 -137
- package/src/changes/index.test.ts +0 -114
- package/src/changes/index.ts +0 -95
- package/src/cli/index.ts +0 -736
- package/src/git/index.test.ts +0 -92
- package/src/git/index.ts +0 -86
- package/src/graph/types.test.ts +0 -383
- package/src/graph/types.ts +0 -353
- package/src/mcp/mcp.test.ts +0 -176
- package/src/mcp/server.ts +0 -217
- package/src/nextjs/index.ts +0 -23
- package/src/nextjs/nextjs.test.ts +0 -233
- package/src/nextjs/pages.ts +0 -43
- package/src/nextjs/react.ts +0 -100
- package/src/nextjs/router.ts +0 -102
- package/src/nextjs/routes.ts +0 -69
- package/src/opencode/index.test.ts +0 -90
- package/src/opencode/index.ts +0 -83
- package/src/parser/index.ts +0 -339
- package/src/parser/parser.test.ts +0 -282
- package/src/plan/index.test.ts +0 -162
- package/src/plan/index.ts +0 -161
- package/src/report/index.ts +0 -128
- package/src/scanner/index.ts +0 -97
- package/src/scanner/scanner.test.ts +0 -135
- package/src/search/index.ts +0 -163
- package/src/search/search.test.ts +0 -512
- package/src/traversal/index.ts +0 -5
- package/src/traversal/traversal.test.ts +0 -266
- package/src/traversal/traversal.ts +0 -185
- package/tsconfig.json +0 -20
- 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
|
-
}
|