@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/src/report/index.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { Graph } from "../graph/types.js";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { analyzeCoupling, findHotspots } from "../analysis/index.js";
|
|
5
|
-
import { checkBoundaries, loadBoundariesConfig } from "../boundaries/index.js";
|
|
6
|
-
import { getStale } from "../changes/index.js";
|
|
7
|
-
|
|
8
|
-
export interface ReportOptions {
|
|
9
|
-
rootDir?: string;
|
|
10
|
-
includeBoundaries?: boolean;
|
|
11
|
-
includeStale?: boolean;
|
|
12
|
-
includeHotspots?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function generateReport(graph: Graph, opts: ReportOptions = {}): string {
|
|
16
|
-
const lines: string[] = [];
|
|
17
|
-
|
|
18
|
-
lines.push("# tsgraph Report");
|
|
19
|
-
lines.push("");
|
|
20
|
-
lines.push(`Generated at: ${graph.generatedAt}`);
|
|
21
|
-
lines.push(`Root: \`${graph.root}\``);
|
|
22
|
-
lines.push("");
|
|
23
|
-
|
|
24
|
-
lines.push("## Packages");
|
|
25
|
-
lines.push("");
|
|
26
|
-
for (const pkg of graph.packages) {
|
|
27
|
-
lines.push(`- **${pkg.name}** — ${pkg.files.length} files`);
|
|
28
|
-
}
|
|
29
|
-
lines.push("");
|
|
30
|
-
|
|
31
|
-
if (graph.dependencies.length > 0) {
|
|
32
|
-
lines.push("## Dependencies");
|
|
33
|
-
lines.push("");
|
|
34
|
-
lines.push("| Module | Version |");
|
|
35
|
-
lines.push("| --- | --- |");
|
|
36
|
-
for (const dep of graph.dependencies) {
|
|
37
|
-
lines.push(`| \`${dep.module}\` | \`${dep.version}\` |`);
|
|
38
|
-
}
|
|
39
|
-
lines.push("");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
lines.push("## Symbols");
|
|
43
|
-
lines.push("");
|
|
44
|
-
const kindCounts = new Map<string, number>();
|
|
45
|
-
for (const sym of graph.symbols) {
|
|
46
|
-
kindCounts.set(sym.kind, (kindCounts.get(sym.kind) ?? 0) + 1);
|
|
47
|
-
}
|
|
48
|
-
for (const [kind, count] of [...kindCounts.entries()].sort()) {
|
|
49
|
-
lines.push(`- **${kind}**: ${count}`);
|
|
50
|
-
}
|
|
51
|
-
lines.push("");
|
|
52
|
-
|
|
53
|
-
if (opts.includeHotspots) {
|
|
54
|
-
lines.push("## Hotspots");
|
|
55
|
-
lines.push("");
|
|
56
|
-
const hotspots = findHotspots(graph, 10);
|
|
57
|
-
if (hotspots.length > 0) {
|
|
58
|
-
lines.push("| File | Score | Complexity | Lines |");
|
|
59
|
-
lines.push("| --- | --- | --- | --- |");
|
|
60
|
-
for (const h of hotspots) {
|
|
61
|
-
lines.push(`| \`${h.file}\` | ${h.score} | ${h.totalComplexity} | ${h.lines} |`);
|
|
62
|
-
}
|
|
63
|
-
lines.push("");
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (opts.includeBoundaries && opts.rootDir) {
|
|
68
|
-
const config = loadBoundariesConfig(opts.rootDir);
|
|
69
|
-
if (config) {
|
|
70
|
-
const result = checkBoundaries(graph, config);
|
|
71
|
-
lines.push("## Architecture Boundaries");
|
|
72
|
-
lines.push("");
|
|
73
|
-
lines.push(`- Layers: ${config.layers.map((l) => l.name).join(", ")}`);
|
|
74
|
-
lines.push(`- Allowed imports: ${result.allowed}`);
|
|
75
|
-
lines.push(`- Violations: ${result.violations.length}`);
|
|
76
|
-
if (result.violations.length > 0) {
|
|
77
|
-
lines.push("");
|
|
78
|
-
for (const v of result.violations.slice(0, 20)) {
|
|
79
|
-
lines.push(`- ❌ \`${v.fromFile}\` → \`${v.toFile}\`: ${v.rule}`);
|
|
80
|
-
}
|
|
81
|
-
if (result.violations.length > 20) {
|
|
82
|
-
lines.push(`- ... and ${result.violations.length - 20} more`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
lines.push("");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (opts.includeStale && opts.rootDir) {
|
|
90
|
-
const stale = getStale(graph, opts.rootDir, 90);
|
|
91
|
-
if (stale.totalFiles > 0) {
|
|
92
|
-
lines.push("## Stale Files");
|
|
93
|
-
lines.push("");
|
|
94
|
-
lines.push(`Files untouched in 90+ days: ${stale.totalFiles}`);
|
|
95
|
-
for (const f of stale.files.slice(0, 20)) {
|
|
96
|
-
lines.push(`- \`${f.path}\` — ${f.symbolCount} symbol(s)`);
|
|
97
|
-
}
|
|
98
|
-
if (stale.files.length > 20) {
|
|
99
|
-
lines.push(`- ... and ${stale.files.length - 20} more`);
|
|
100
|
-
}
|
|
101
|
-
lines.push("");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (graph.packages.length > 1) {
|
|
106
|
-
lines.push("## Coupling");
|
|
107
|
-
lines.push("");
|
|
108
|
-
const coupling = analyzeCoupling(graph).slice(0, 15);
|
|
109
|
-
if (coupling.length > 0) {
|
|
110
|
-
lines.push("| Package | Coupled To | Imports | Files |");
|
|
111
|
-
lines.push("| --- | --- | --- | --- |");
|
|
112
|
-
for (const c of coupling) {
|
|
113
|
-
lines.push(`| ${c.packageName} | ${c.coupledTo} | ${c.importCount} | ${c.fileCount} |`);
|
|
114
|
-
}
|
|
115
|
-
lines.push("");
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
lines.push("## Summary");
|
|
120
|
-
lines.push("");
|
|
121
|
-
lines.push(`- Files: ${graph.files.length}`);
|
|
122
|
-
lines.push(`- Symbols: ${graph.symbols.length}`);
|
|
123
|
-
lines.push(`- Calls: ${graph.calls.length}`);
|
|
124
|
-
lines.push(`- Imports: ${graph.imports.length}`);
|
|
125
|
-
lines.push(`- Dependencies: ${graph.dependencies.length}`);
|
|
126
|
-
|
|
127
|
-
return lines.join("\n") + "\n";
|
|
128
|
-
}
|
package/src/scanner/index.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import ignore from "ignore";
|
|
4
|
-
|
|
5
|
-
export type FileKind = "ts" | "tsx" | "js" | "jsx" | "json" | "other";
|
|
6
|
-
|
|
7
|
-
export interface ScannedFile {
|
|
8
|
-
path: string;
|
|
9
|
-
relativePath: string;
|
|
10
|
-
kind: FileKind;
|
|
11
|
-
isGenerated: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ScanResult {
|
|
15
|
-
files: ScannedFile[];
|
|
16
|
-
errors: Error[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const ALWAYS_IGNORE = ["node_modules", ".git", "dist", ".tsgraph", ".gitignore"];
|
|
20
|
-
|
|
21
|
-
const GENERATED_PATTERNS = [
|
|
22
|
-
/@generated/,
|
|
23
|
-
/Code generated by/,
|
|
24
|
-
/auto-generated/,
|
|
25
|
-
/This file is generated/,
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const FILE_EXT_MAP: Record<string, FileKind> = {
|
|
29
|
-
".ts": "ts",
|
|
30
|
-
".tsx": "tsx",
|
|
31
|
-
".js": "js",
|
|
32
|
-
".jsx": "jsx",
|
|
33
|
-
".json": "json",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function classifyFile(filePath: string): FileKind {
|
|
37
|
-
return FILE_EXT_MAP[path.extname(filePath).toLowerCase()] ?? "other";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function checkGenerated(filePath: string): boolean {
|
|
41
|
-
try {
|
|
42
|
-
const fd = fs.openSync(filePath, "r");
|
|
43
|
-
const buffer = Buffer.alloc(256);
|
|
44
|
-
const bytesRead = fs.readSync(fd, buffer, 0, 256, 0);
|
|
45
|
-
fs.closeSync(fd);
|
|
46
|
-
const firstChunk = buffer.toString("utf-8", 0, bytesRead).split("\n")[0];
|
|
47
|
-
return GENERATED_PATTERNS.some((p) => p.test(firstChunk));
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function scanFiles(rootDir: string): ScanResult {
|
|
54
|
-
const ig = ignore().add(ALWAYS_IGNORE);
|
|
55
|
-
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
56
|
-
try {
|
|
57
|
-
ig.add(fs.readFileSync(gitignorePath, "utf-8"));
|
|
58
|
-
} catch {
|
|
59
|
-
// no .gitignore — built-in rules suffice
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const files: ScannedFile[] = [];
|
|
63
|
-
const errors: Error[] = [];
|
|
64
|
-
|
|
65
|
-
function walk(dir: string) {
|
|
66
|
-
let entries: fs.Dirent[];
|
|
67
|
-
try {
|
|
68
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
69
|
-
} catch (err) {
|
|
70
|
-
errors.push(err as Error);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (const entry of entries) {
|
|
75
|
-
const fullPath = path.join(dir, entry.name);
|
|
76
|
-
const relativePath = path.relative(rootDir, fullPath);
|
|
77
|
-
|
|
78
|
-
if (entry.isDirectory()) {
|
|
79
|
-
if (ig.ignores(relativePath + "/")) continue;
|
|
80
|
-
walk(fullPath);
|
|
81
|
-
} else if (entry.isFile()) {
|
|
82
|
-
if (ig.ignores(relativePath)) continue;
|
|
83
|
-
const kind = classifyFile(fullPath);
|
|
84
|
-
files.push({
|
|
85
|
-
path: fullPath,
|
|
86
|
-
relativePath,
|
|
87
|
-
kind,
|
|
88
|
-
isGenerated: checkGenerated(fullPath),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
walk(rootDir);
|
|
95
|
-
|
|
96
|
-
return { files, errors };
|
|
97
|
-
}
|
|
@@ -1,135 +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 { scanFiles } from "./index.js";
|
|
6
|
-
|
|
7
|
-
function createTempDir(): string {
|
|
8
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), "tsgraph-test-"));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function writeFile(dir: string, relativePath: string, content: string) {
|
|
12
|
-
const fullPath = path.join(dir, relativePath);
|
|
13
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
14
|
-
fs.writeFileSync(fullPath, content, "utf-8");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe("scanFiles", () => {
|
|
18
|
-
it("finds .ts files in a flat directory", () => {
|
|
19
|
-
const dir = createTempDir();
|
|
20
|
-
writeFile(dir, "index.ts", "");
|
|
21
|
-
writeFile(dir, "util.ts", "");
|
|
22
|
-
|
|
23
|
-
const result = scanFiles(dir);
|
|
24
|
-
const paths = result.files.map((f) => f.relativePath).sort();
|
|
25
|
-
expect(paths).toEqual(["index.ts", "util.ts"]);
|
|
26
|
-
expect(result.errors).toEqual([]);
|
|
27
|
-
|
|
28
|
-
fs.rmSync(dir, { recursive: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("ignores node_modules by default", () => {
|
|
32
|
-
const dir = createTempDir();
|
|
33
|
-
writeFile(dir, "index.ts", "");
|
|
34
|
-
writeFile(dir, "node_modules/foo/index.ts", "");
|
|
35
|
-
writeFile(dir, "node_modules/bar/index.js", "");
|
|
36
|
-
|
|
37
|
-
const result = scanFiles(dir);
|
|
38
|
-
const paths = result.files.map((f) => f.relativePath);
|
|
39
|
-
expect(paths).toEqual(["index.ts"]);
|
|
40
|
-
fs.rmSync(dir, { recursive: true });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("ignores .git, dist, .tsgraph by default", () => {
|
|
44
|
-
const dir = createTempDir();
|
|
45
|
-
writeFile(dir, "src/index.ts", "");
|
|
46
|
-
writeFile(dir, ".git/HEAD", "");
|
|
47
|
-
writeFile(dir, "dist/bundle.js", "");
|
|
48
|
-
writeFile(dir, ".tsgraph/graph.json", "");
|
|
49
|
-
|
|
50
|
-
const result = scanFiles(dir);
|
|
51
|
-
const paths = result.files.map((f) => f.relativePath).sort();
|
|
52
|
-
expect(paths).toEqual(["src/index.ts"]);
|
|
53
|
-
fs.rmSync(dir, { recursive: true });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("respects .gitignore patterns", () => {
|
|
57
|
-
const dir = createTempDir();
|
|
58
|
-
writeFile(dir, ".gitignore", "*.log\n/build");
|
|
59
|
-
writeFile(dir, "src/index.ts", "");
|
|
60
|
-
writeFile(dir, "server.log", "");
|
|
61
|
-
writeFile(dir, "build/output.js", "");
|
|
62
|
-
|
|
63
|
-
const result = scanFiles(dir);
|
|
64
|
-
const paths = result.files.map((f) => f.relativePath);
|
|
65
|
-
expect(paths).toEqual(["src/index.ts"]);
|
|
66
|
-
fs.rmSync(dir, { recursive: true });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("classifies file kinds correctly", () => {
|
|
70
|
-
const dir = createTempDir();
|
|
71
|
-
writeFile(dir, "a.ts", "");
|
|
72
|
-
writeFile(dir, "b.tsx", "");
|
|
73
|
-
writeFile(dir, "c.js", "");
|
|
74
|
-
writeFile(dir, "d.jsx", "");
|
|
75
|
-
writeFile(dir, "e.json", "");
|
|
76
|
-
writeFile(dir, "f.css", "");
|
|
77
|
-
|
|
78
|
-
const result = scanFiles(dir);
|
|
79
|
-
const kinds = Object.fromEntries(
|
|
80
|
-
result.files.map((f) => [f.relativePath, f.kind]),
|
|
81
|
-
);
|
|
82
|
-
expect(kinds["a.ts"]).toBe("ts");
|
|
83
|
-
expect(kinds["b.tsx"]).toBe("tsx");
|
|
84
|
-
expect(kinds["c.js"]).toBe("js");
|
|
85
|
-
expect(kinds["d.jsx"]).toBe("jsx");
|
|
86
|
-
expect(kinds["e.json"]).toBe("json");
|
|
87
|
-
expect(kinds["f.css"]).toBe("other");
|
|
88
|
-
fs.rmSync(dir, { recursive: true });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("detects generated files by first-line comment", () => {
|
|
92
|
-
const dir = createTempDir();
|
|
93
|
-
writeFile(dir, "generated.ts", "// @generated\nconst x = 1;\n");
|
|
94
|
-
writeFile(dir, "normal.ts", "const x = 1;\n");
|
|
95
|
-
writeFile(dir, "auto.ts", "// auto-generated\nconst y = 2;\n");
|
|
96
|
-
writeFile(dir, "codegen.ts", "// Code generated by protoc\nconst z = 3;\n");
|
|
97
|
-
|
|
98
|
-
const result = scanFiles(dir);
|
|
99
|
-
const genMap = Object.fromEntries(
|
|
100
|
-
result.files.map((f) => [f.relativePath, f.isGenerated]),
|
|
101
|
-
);
|
|
102
|
-
expect(genMap["generated.ts"]).toBe(true);
|
|
103
|
-
expect(genMap["normal.ts"]).toBe(false);
|
|
104
|
-
expect(genMap["auto.ts"]).toBe(true);
|
|
105
|
-
expect(genMap["codegen.ts"]).toBe(true);
|
|
106
|
-
fs.rmSync(dir, { recursive: true });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("handles missing .gitignore gracefully", () => {
|
|
110
|
-
const dir = createTempDir();
|
|
111
|
-
writeFile(dir, "index.ts", "");
|
|
112
|
-
// Intentionally no .gitignore
|
|
113
|
-
|
|
114
|
-
const result = scanFiles(dir);
|
|
115
|
-
expect(result.files.length).toBe(1);
|
|
116
|
-
expect(result.errors).toEqual([]);
|
|
117
|
-
fs.rmSync(dir, { recursive: true });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("walks nested directories", () => {
|
|
121
|
-
const dir = createTempDir();
|
|
122
|
-
writeFile(dir, "src/index.ts", "");
|
|
123
|
-
writeFile(dir, "src/components/button.tsx", "");
|
|
124
|
-
writeFile(dir, "src/utils/helpers.ts", "");
|
|
125
|
-
|
|
126
|
-
const result = scanFiles(dir);
|
|
127
|
-
const paths = result.files.map((f) => f.relativePath).sort();
|
|
128
|
-
expect(paths).toEqual([
|
|
129
|
-
"src/components/button.tsx",
|
|
130
|
-
"src/index.ts",
|
|
131
|
-
"src/utils/helpers.ts",
|
|
132
|
-
]);
|
|
133
|
-
fs.rmSync(dir, { recursive: true });
|
|
134
|
-
});
|
|
135
|
-
});
|
package/src/search/index.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type {
|
|
4
|
-
Graph,
|
|
5
|
-
SymbolNode,
|
|
6
|
-
CallEdge,
|
|
7
|
-
ImportEdge,
|
|
8
|
-
FileNode,
|
|
9
|
-
PackageNode,
|
|
10
|
-
} from "../graph/types.js";
|
|
11
|
-
import { deserialize } from "../graph/types.js";
|
|
12
|
-
|
|
13
|
-
export interface CallerResult {
|
|
14
|
-
callerSymbol: SymbolNode;
|
|
15
|
-
edges: CallEdge[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface CalleeResult {
|
|
19
|
-
calleeRaw: string;
|
|
20
|
-
edges: CallEdge[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FocusResult {
|
|
24
|
-
pkg: PackageNode;
|
|
25
|
-
files: FileNode[];
|
|
26
|
-
symbols: SymbolNode[];
|
|
27
|
-
imports: ImportEdge[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ContextResult {
|
|
31
|
-
node?: SymbolNode;
|
|
32
|
-
source?: string;
|
|
33
|
-
callers: CallerResult[];
|
|
34
|
-
callees: CalleeResult[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function loadGraph(graphPath: string): Graph {
|
|
38
|
-
const raw = fs.readFileSync(graphPath, "utf-8");
|
|
39
|
-
return deserialize(raw);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function findCallers(graph: Graph, name: string): CallerResult[] {
|
|
43
|
-
const matchingEdges = graph.calls.filter(
|
|
44
|
-
(c) => c.calleeRaw === name,
|
|
45
|
-
);
|
|
46
|
-
const callerMap = new Map<string, CallerResult>();
|
|
47
|
-
for (const edge of matchingEdges) {
|
|
48
|
-
const caller = graph.symbols.find((s) => s.id === edge.callerSymbolId);
|
|
49
|
-
if (!caller) continue;
|
|
50
|
-
const key = caller.id;
|
|
51
|
-
let result = callerMap.get(key);
|
|
52
|
-
if (!result) {
|
|
53
|
-
result = { callerSymbol: caller, edges: [] };
|
|
54
|
-
callerMap.set(key, result);
|
|
55
|
-
}
|
|
56
|
-
result.edges.push(edge);
|
|
57
|
-
}
|
|
58
|
-
return [...callerMap.values()];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function findCallees(graph: Graph, name: string): CalleeResult[] {
|
|
62
|
-
const sym = graph.symbols.find((s) => s.name === name);
|
|
63
|
-
if (!sym) return [];
|
|
64
|
-
const edges = graph.calls.filter((c) => c.callerSymbolId === sym.id);
|
|
65
|
-
const calleeMap = new Map<string, CalleeResult>();
|
|
66
|
-
for (const edge of edges) {
|
|
67
|
-
let result = calleeMap.get(edge.calleeRaw);
|
|
68
|
-
if (!result) {
|
|
69
|
-
result = { calleeRaw: edge.calleeRaw, edges: [] };
|
|
70
|
-
calleeMap.set(edge.calleeRaw, result);
|
|
71
|
-
}
|
|
72
|
-
result.edges.push(edge);
|
|
73
|
-
}
|
|
74
|
-
return [...calleeMap.values()];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function findNode(graph: Graph, name: string): SymbolNode | undefined {
|
|
78
|
-
return graph.symbols.find((s) => s.name === name);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function readSource(graph: Graph, symbol: SymbolNode): string {
|
|
82
|
-
const filePath = path.join(graph.root, symbol.file);
|
|
83
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
84
|
-
const lines = content.split("\n");
|
|
85
|
-
const start = Math.max(0, symbol.line - 1);
|
|
86
|
-
const end = Math.min(lines.length, symbol.endLine);
|
|
87
|
-
const snippet = lines.slice(start, end);
|
|
88
|
-
const padLen = String(end).length;
|
|
89
|
-
return snippet
|
|
90
|
-
.map((line, i) => {
|
|
91
|
-
const lineNum = symbol.line + i;
|
|
92
|
-
return `${String(lineNum).padStart(padLen)} ${line}`;
|
|
93
|
-
})
|
|
94
|
-
.join("\n");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function querySymbols(
|
|
98
|
-
graph: Graph,
|
|
99
|
-
pattern: string,
|
|
100
|
-
): SymbolNode[] {
|
|
101
|
-
const lower = pattern.toLowerCase();
|
|
102
|
-
return graph.symbols.filter(
|
|
103
|
-
(s) =>
|
|
104
|
-
s.name.toLowerCase().includes(lower) ||
|
|
105
|
-
s.file.toLowerCase().includes(lower) ||
|
|
106
|
-
s.packageName.toLowerCase().includes(lower),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function findImports(
|
|
111
|
-
graph: Graph,
|
|
112
|
-
importPath: string,
|
|
113
|
-
): ImportEdge[] {
|
|
114
|
-
const lower = importPath.toLowerCase();
|
|
115
|
-
return graph.imports.filter((i) =>
|
|
116
|
-
i.importPath.toLowerCase().includes(lower),
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function findPublic(
|
|
121
|
-
graph: Graph,
|
|
122
|
-
packageName?: string,
|
|
123
|
-
): SymbolNode[] {
|
|
124
|
-
return graph.symbols.filter(
|
|
125
|
-
(s) =>
|
|
126
|
-
s.isExported &&
|
|
127
|
-
(packageName === undefined || s.packageName === packageName),
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function focusPackage(
|
|
132
|
-
graph: Graph,
|
|
133
|
-
packageName: string,
|
|
134
|
-
): FocusResult | undefined {
|
|
135
|
-
const pkg = graph.packages.find((p) => p.name === packageName);
|
|
136
|
-
if (!pkg) return undefined;
|
|
137
|
-
const files = graph.files.filter((f) => f.packageName === packageName);
|
|
138
|
-
const symbols = graph.symbols.filter(
|
|
139
|
-
(s) => s.packageName === packageName,
|
|
140
|
-
);
|
|
141
|
-
const imports = graph.imports.filter(
|
|
142
|
-
(i) => i.fromPackage === packageName,
|
|
143
|
-
);
|
|
144
|
-
return { pkg, files, symbols, imports };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function context(
|
|
148
|
-
graph: Graph,
|
|
149
|
-
symbolName: string,
|
|
150
|
-
): ContextResult {
|
|
151
|
-
const node = findNode(graph, symbolName);
|
|
152
|
-
let source: string | undefined;
|
|
153
|
-
if (node) {
|
|
154
|
-
try {
|
|
155
|
-
source = readSource(graph, node);
|
|
156
|
-
} catch {
|
|
157
|
-
// source file not available
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const callers = findCallers(graph, symbolName);
|
|
161
|
-
const callees = findCallees(graph, symbolName);
|
|
162
|
-
return { node, source, callers, callees };
|
|
163
|
-
}
|