@shvmgyl15/tsgraph 0.1.0 → 0.1.1

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 (42) hide show
  1. package/package.json +16 -1
  2. package/AGENTS.md +0 -64
  3. package/TODOS.md +0 -61
  4. package/opencode.json +0 -24
  5. package/src/analysis/analysis.test.ts +0 -405
  6. package/src/analysis/complexity.ts +0 -107
  7. package/src/analysis/coupling.ts +0 -106
  8. package/src/analysis/hotspot.ts +0 -52
  9. package/src/analysis/index.ts +0 -17
  10. package/src/boundaries/index.test.ts +0 -335
  11. package/src/boundaries/index.ts +0 -137
  12. package/src/changes/index.test.ts +0 -114
  13. package/src/changes/index.ts +0 -95
  14. package/src/cli/index.ts +0 -736
  15. package/src/git/index.test.ts +0 -92
  16. package/src/git/index.ts +0 -86
  17. package/src/graph/types.test.ts +0 -383
  18. package/src/graph/types.ts +0 -353
  19. package/src/mcp/mcp.test.ts +0 -176
  20. package/src/mcp/server.ts +0 -217
  21. package/src/nextjs/index.ts +0 -23
  22. package/src/nextjs/nextjs.test.ts +0 -233
  23. package/src/nextjs/pages.ts +0 -43
  24. package/src/nextjs/react.ts +0 -100
  25. package/src/nextjs/router.ts +0 -102
  26. package/src/nextjs/routes.ts +0 -69
  27. package/src/opencode/index.test.ts +0 -90
  28. package/src/opencode/index.ts +0 -83
  29. package/src/parser/index.ts +0 -339
  30. package/src/parser/parser.test.ts +0 -282
  31. package/src/plan/index.test.ts +0 -162
  32. package/src/plan/index.ts +0 -161
  33. package/src/report/index.ts +0 -128
  34. package/src/scanner/index.ts +0 -97
  35. package/src/scanner/scanner.test.ts +0 -135
  36. package/src/search/index.ts +0 -163
  37. package/src/search/search.test.ts +0 -512
  38. package/src/traversal/index.ts +0 -5
  39. package/src/traversal/traversal.test.ts +0 -266
  40. package/src/traversal/traversal.ts +0 -185
  41. package/tsconfig.json +0 -20
  42. package/vitest.config.ts +0 -7
@@ -1,266 +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
- makeTestEdge,
10
- makeFileNode,
11
- } from "../graph/types.js";
12
- import { impact, findPath, findOrphans, trace } from "./traversal.js";
13
-
14
- function createTempDir(): string {
15
- return fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-test-"));
16
- }
17
-
18
- describe("impact", () => {
19
- it("finds direct callers", () => {
20
- const graph = makeGraph({
21
- symbols: [
22
- makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
23
- makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
24
- makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
25
- ],
26
- calls: [
27
- makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 }),
28
- makeCallEdge({ callerSymbolId: "c", callerName: "c", calleeRaw: "a", file: "c.ts", line: 1 }),
29
- ],
30
- });
31
-
32
- const results = impact(graph, "a");
33
- expect(results).toHaveLength(2);
34
- const names = results.map((r) => r.symbol.name).sort();
35
- expect(names).toEqual(["b", "c"]);
36
- expect(results.every((r) => r.depth === 1)).toBe(true);
37
- });
38
-
39
- it("traverses multiple levels", () => {
40
- const graph = makeGraph({
41
- symbols: [
42
- makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
43
- makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
44
- makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
45
- ],
46
- calls: [
47
- makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 }),
48
- makeCallEdge({ callerSymbolId: "c", callerName: "c", calleeRaw: "b", file: "c.ts", line: 1 }),
49
- ],
50
- });
51
-
52
- const results = impact(graph, "a");
53
- expect(results).toHaveLength(2);
54
- const b = results.find((r) => r.symbol.name === "b");
55
- const c = results.find((r) => r.symbol.name === "c");
56
- expect(b).toBeTruthy();
57
- expect(c).toBeTruthy();
58
- expect(b!.depth).toBe(1);
59
- expect(c!.depth).toBe(2);
60
- });
61
-
62
- it("respects max depth", () => {
63
- const graph = makeGraph({
64
- symbols: [
65
- makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
66
- makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
67
- makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
68
- ],
69
- calls: [
70
- makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 }),
71
- makeCallEdge({ callerSymbolId: "c", callerName: "c", calleeRaw: "b", file: "c.ts", line: 1 }),
72
- ],
73
- });
74
-
75
- const results = impact(graph, "a", 1);
76
- expect(results).toHaveLength(1);
77
- expect(results[0].symbol.name).toBe("b");
78
- });
79
-
80
- it("returns empty for symbol with no callers", () => {
81
- const graph = makeGraph({
82
- symbols: [makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 })],
83
- });
84
- const results = impact(graph, "a");
85
- expect(results).toHaveLength(0);
86
- });
87
-
88
- it("returns empty for unknown symbol", () => {
89
- const graph = makeGraph();
90
- const results = impact(graph, "noop");
91
- expect(results).toHaveLength(0);
92
- });
93
- });
94
-
95
- describe("findPath", () => {
96
- it("finds a direct path between two symbols", () => {
97
- const graph = makeGraph({
98
- symbols: [
99
- makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
100
- makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
101
- ],
102
- calls: [makeCallEdge({ callerSymbolId: "a", callerName: "a", calleeRaw: "b", file: "a.ts", line: 1 })],
103
- });
104
-
105
- const p = findPath(graph, "a", "b");
106
- expect(p).toBeTruthy();
107
- expect(p!.map((n) => n.name)).toEqual(["a", "b"]);
108
- });
109
-
110
- it("finds a multi-hop path", () => {
111
- const graph = makeGraph({
112
- symbols: [
113
- makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
114
- makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
115
- makeSymbolNode({ id: "c", name: "c", kind: "function", file: "c.ts", line: 1, endLine: 1 }),
116
- ],
117
- calls: [
118
- makeCallEdge({ callerSymbolId: "a", callerName: "a", calleeRaw: "b", file: "a.ts", line: 1 }),
119
- makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "c", file: "b.ts", line: 1 }),
120
- ],
121
- });
122
-
123
- const p = findPath(graph, "a", "c");
124
- expect(p).toBeTruthy();
125
- expect(p!.map((n) => n.name)).toEqual(["a", "b", "c"]);
126
- });
127
-
128
- it("returns self-path when from === to", () => {
129
- const graph = makeGraph({
130
- symbols: [makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 })],
131
- });
132
- const p = findPath(graph, "a", "a");
133
- expect(p).toBeTruthy();
134
- expect(p!.map((n) => n.name)).toEqual(["a"]);
135
- });
136
-
137
- it("returns undefined when no path exists", () => {
138
- const graph = makeGraph({
139
- symbols: [
140
- makeSymbolNode({ id: "a", name: "a", kind: "function", file: "a.ts", line: 1, endLine: 1 }),
141
- makeSymbolNode({ id: "b", name: "b", kind: "function", file: "b.ts", line: 1, endLine: 1 }),
142
- ],
143
- });
144
- const p = findPath(graph, "a", "b");
145
- expect(p).toBeUndefined();
146
- });
147
-
148
- it("returns undefined for unknown symbols", () => {
149
- const graph = makeGraph();
150
- expect(findPath(graph, "a", "b")).toBeUndefined();
151
- expect(findPath(graph, "a", "a")).toBeUndefined();
152
- });
153
- });
154
-
155
- describe("findOrphans", () => {
156
- it("skips unexported symbols that are called", () => {
157
- const graph = makeGraph({
158
- symbols: [
159
- makeSymbolNode({ id: "a", name: "a", kind: "function", isExported: false }),
160
- makeSymbolNode({ id: "b", name: "b", kind: "function", isExported: true }),
161
- ],
162
- calls: [makeCallEdge({ callerSymbolId: "b", callerName: "b", calleeRaw: "a", file: "b.ts", line: 1 })],
163
- });
164
-
165
- const orphans = findOrphans(graph);
166
- const deadNames = orphans.map((o) => o.symbol.name);
167
- expect(deadNames).not.toContain("a"); // "a" is called by "b"
168
- });
169
-
170
- it("flags unexported symbols with no callers", () => {
171
- const graph = makeGraph({
172
- symbols: [
173
- makeSymbolNode({ id: "dead", name: "dead", kind: "function", isExported: false }),
174
- makeSymbolNode({ id: "alive", name: "alive", kind: "function", isExported: false }),
175
- ],
176
- calls: [makeCallEdge({ callerSymbolId: "alive", callerName: "alive", calleeRaw: "nonexistent", file: "x.ts", line: 1 })],
177
- });
178
-
179
- const orphans = findOrphans(graph);
180
- expect(orphans.some((o) => o.symbol.name === "dead")).toBe(true);
181
- });
182
-
183
- it("flags exported symbols with no callers or tests", () => {
184
- const graph = makeGraph({
185
- symbols: [makeSymbolNode({ id: "unused", name: "unused", kind: "function", isExported: true })],
186
- });
187
-
188
- const orphans = findOrphans(graph);
189
- expect(orphans).toHaveLength(1);
190
- expect(orphans[0].symbol.name).toBe("unused");
191
- });
192
-
193
- it("excludes symbols referenced in tests", () => {
194
- const graph = makeGraph({
195
- symbols: [makeSymbolNode({ id: "tested", name: "tested", kind: "function", isExported: false })],
196
- testEdges: [makeTestEdge({ testFunc: "test.ts", target: "tested", file: "test.ts", line: 1 })],
197
- });
198
-
199
- const orphans = findOrphans(graph);
200
- expect(orphans).toHaveLength(0);
201
- });
202
- });
203
-
204
- describe("trace", () => {
205
- it("finds string matches in symbol bodies and their callers", () => {
206
- const dir = createTempDir();
207
- const filePath = path.join(dir, "src/lib.ts");
208
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
209
- fs.writeFileSync(
210
- filePath,
211
- [
212
- 'function doStuff() {',
213
- ' throw new Error("boom");',
214
- '}',
215
- 'function caller() {',
216
- ' doStuff();',
217
- '}',
218
- ].join("\n"),
219
- "utf-8",
220
- );
221
-
222
- const graph = makeGraph({
223
- root: dir,
224
- symbols: [
225
- makeSymbolNode({ id: "doStuff", name: "doStuff", kind: "function", file: "src/lib.ts", line: 1, endLine: 3 }),
226
- makeSymbolNode({ id: "caller", name: "caller", kind: "function", file: "src/lib.ts", line: 4, endLine: 6 }),
227
- ],
228
- calls: [
229
- makeCallEdge({ callerSymbolId: "caller", callerName: "caller", calleeRaw: "doStuff", file: "src/lib.ts", line: 5 }),
230
- ],
231
- });
232
-
233
- const results = trace(graph, "boom");
234
- expect(results).toHaveLength(1);
235
- expect(results[0].match.symbol.name).toBe("doStuff");
236
- expect(results[0].match.contextLine).toContain("boom");
237
- expect(results[0].callers).toHaveLength(1);
238
- expect(results[0].callers[0].symbol.name).toBe("caller");
239
-
240
- fs.rmSync(dir, { recursive: true });
241
- });
242
-
243
- it("returns empty for no match", () => {
244
- const graph = makeGraph({
245
- symbols: [makeSymbolNode({ id: "foo", name: "foo", kind: "function", file: "x.ts", line: 1, endLine: 1 })],
246
- });
247
- const results = trace(graph, "nonexistent");
248
- expect(results).toHaveLength(0);
249
- });
250
-
251
- it("is case-insensitive", () => {
252
- const dir = createTempDir();
253
- const filePath = path.join(dir, "x.ts");
254
- fs.writeFileSync(filePath, 'function foo() { return "HELLO"; }', "utf-8");
255
-
256
- const graph = makeGraph({
257
- root: dir,
258
- symbols: [makeSymbolNode({ id: "foo", name: "foo", kind: "function", file: "x.ts", line: 1, endLine: 1 })],
259
- });
260
-
261
- const results = trace(graph, "hello");
262
- expect(results).toHaveLength(1);
263
-
264
- fs.rmSync(dir, { recursive: true });
265
- });
266
- });
@@ -1,185 +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
- export interface ImpactNode {
6
- symbol: SymbolNode;
7
- depth: number;
8
- callChain: string[];
9
- }
10
-
11
- export function impact(
12
- graph: Graph,
13
- symbolName: string,
14
- maxDepth: number = 5,
15
- ): ImpactNode[] {
16
- const start = graph.symbols.find((s) => s.name === symbolName);
17
- if (!start) return [];
18
-
19
- const results: ImpactNode[] = [];
20
- const visited = new Set<string>();
21
- const queue: { sym: SymbolNode; depth: number; chain: string[] }[] = [
22
- { sym: start, depth: 0, chain: [start.name] },
23
- ];
24
- visited.add(start.id);
25
-
26
- while (queue.length > 0) {
27
- const { sym, depth, chain } = queue.shift()!;
28
- if (depth > 0) {
29
- results.push({ symbol: sym, depth, callChain: chain });
30
- }
31
- if (depth >= maxDepth) continue;
32
-
33
- const callers = graph.calls.filter((c) => c.calleeRaw === sym.name);
34
- for (const edge of callers) {
35
- const caller = graph.symbols.find((s) => s.id === edge.callerSymbolId);
36
- if (!caller || visited.has(caller.id)) continue;
37
- visited.add(caller.id);
38
- queue.push({
39
- sym: caller,
40
- depth: depth + 1,
41
- chain: [...chain, caller.name],
42
- });
43
- }
44
- }
45
-
46
- return results;
47
- }
48
-
49
- export interface PathNode {
50
- name: string;
51
- kind: string;
52
- file: string;
53
- line: number;
54
- }
55
-
56
- export function findPath(
57
- graph: Graph,
58
- fromName: string,
59
- toName: string,
60
- maxDepth: number = 10,
61
- ): PathNode[] | undefined {
62
- const from = graph.symbols.find((s) => s.name === fromName);
63
- const to = graph.symbols.find((s) => s.name === toName);
64
- if (!from || !to) return undefined;
65
- if (from.id === to.id) return [{ name: from.name, kind: from.kind, file: from.file, line: from.line }];
66
-
67
- const visited = new Set<string>();
68
- const parent = new Map<string, { prev: string; edge: { name: string; kind: string; file: string; line: number } }>();
69
- const queue: string[] = [from.id];
70
- visited.add(from.id);
71
-
72
- while (queue.length > 0) {
73
- const currentId = queue.shift()!;
74
- const current = graph.symbols.find((s) => s.id === currentId);
75
- if (!current) continue;
76
-
77
- const calleeEdges = graph.calls.filter((c) => c.callerSymbolId === currentId);
78
- for (const edge of calleeEdges) {
79
- const callee = graph.symbols.find(
80
- (s) => s.name === edge.calleeRaw,
81
- );
82
- if (!callee || visited.has(callee.id)) continue;
83
- visited.add(callee.id);
84
- parent.set(callee.id, {
85
- prev: currentId,
86
- edge: { name: callee.name, kind: callee.kind, file: callee.file, line: callee.line },
87
- });
88
- if (callee.id === to.id) {
89
- const pathNodes: PathNode[] = [];
90
- let step: string | undefined = to.id;
91
- while (step) {
92
- const sym = graph.symbols.find((s) => s.id === step)!;
93
- pathNodes.unshift({ name: sym.name, kind: sym.kind, file: sym.file, line: sym.line });
94
- const p = parent.get(step);
95
- step = p?.prev;
96
- }
97
- return pathNodes;
98
- }
99
- queue.push(callee.id);
100
- }
101
- }
102
-
103
- return undefined;
104
- }
105
-
106
- export interface OrphanResult {
107
- symbol: SymbolNode;
108
- reason: string;
109
- }
110
-
111
- export function findOrphans(graph: Graph): OrphanResult[] {
112
- const calledNames = new Set(graph.calls.map((c) => c.calleeRaw));
113
- const testTargets = new Set(graph.testEdges.map((t) => t.target));
114
-
115
- const results: OrphanResult[] = [];
116
-
117
- for (const sym of graph.symbols) {
118
- if (sym.isExported) {
119
- const incomingCallers = graph.calls.filter((c) => c.calleeRaw === sym.name);
120
- if (incomingCallers.length === 0 && !testTargets.has(sym.name)) {
121
- results.push({ symbol: sym, reason: "exported but no callers or tests" });
122
- }
123
- } else {
124
- if (!calledNames.has(sym.name) && !testTargets.has(sym.name)) {
125
- results.push({ symbol: sym, reason: "unexported, no callers or tests" });
126
- }
127
- }
128
- }
129
-
130
- return results;
131
- }
132
-
133
- export interface TraceMatch {
134
- symbol: SymbolNode;
135
- file: string;
136
- line: number;
137
- contextLine: string;
138
- }
139
-
140
- export interface TraceResult {
141
- match: TraceMatch;
142
- callers: ImpactNode[];
143
- }
144
-
145
- export function trace(
146
- graph: Graph,
147
- searchString: string,
148
- maxDepth: number = 5,
149
- ): TraceResult[] {
150
- const lowerSearch = searchString.toLowerCase();
151
- const results: TraceResult[] = [];
152
-
153
- for (const sym of graph.symbols) {
154
- if (sym.kind !== "function" && sym.kind !== "method") continue;
155
-
156
- const filePath = path.join(graph.root, sym.file);
157
- let content: string;
158
- try {
159
- content = fs.readFileSync(filePath, "utf-8");
160
- } catch {
161
- continue;
162
- }
163
-
164
- const lines = content.split("\n");
165
- const start = Math.max(0, sym.line - 1);
166
- const end = Math.min(lines.length, sym.endLine);
167
-
168
- for (let i = start; i < end; i++) {
169
- if (lines[i].toLowerCase().includes(lowerSearch)) {
170
- const impactNodes = impact(graph, sym.name, maxDepth);
171
- results.push({
172
- match: {
173
- symbol: sym,
174
- file: sym.file,
175
- line: i + 1,
176
- contextLine: lines[i].trim(),
177
- },
178
- callers: impactNodes,
179
- });
180
- }
181
- }
182
- }
183
-
184
- return results;
185
- }
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "lib": ["ESNext"],
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "skipLibCheck": true,
10
- "forceConsistentCasingInFileNames": true,
11
- "resolveJsonModule": true,
12
- "declaration": true,
13
- "declarationMap": true,
14
- "sourceMap": true,
15
- "outDir": "dist",
16
- "rootDir": "src"
17
- },
18
- "include": ["src/**/*.ts"],
19
- "exclude": ["node_modules", "dist"]
20
- }
package/vitest.config.ts DELETED
@@ -1,7 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- },
7
- });