pi-lens 3.2.0 → 3.3.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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -10
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/agent-behavior-client.test.js +94 -0
- package/clients/biome-client.test.js +144 -0
- package/clients/cache-manager.test.js +197 -0
- package/clients/complexity-client.test.js +234 -0
- package/clients/dependency-checker.test.js +60 -0
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.test.js +116 -0
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
- package/clients/dispatch/runners/lsp.js +42 -5
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/pyright.test.js +98 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/utils/format-utils.js +1 -6
- package/clients/dispatch/utils/format-utils.ts +1 -6
- package/clients/dogfood.test.js +201 -0
- package/clients/file-kinds.test.js +169 -0
- package/clients/formatters.js +1 -1
- package/clients/go-client.test.js +127 -0
- package/clients/jscpd-client.test.js +127 -0
- package/clients/knip-client.test.js +112 -0
- package/clients/lsp/__tests__/client.test.js +310 -0
- package/clients/lsp/__tests__/client.test.ts +1 -46
- package/clients/lsp/__tests__/config.test.js +167 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/launch.test.js +313 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/service.test.js +435 -0
- package/clients/lsp/client.js +32 -44
- package/clients/lsp/client.ts +36 -45
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/lsp/server.js +27 -2
- package/clients/metrics-client.test.js +141 -0
- package/clients/ruff-client.test.js +132 -0
- package/clients/rust-client.test.js +108 -0
- package/clients/sanitize.test.js +177 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/test-runner-client.test.js +192 -0
- package/clients/todo-scanner.test.js +301 -0
- package/clients/type-coverage-client.test.js +105 -0
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.test.js +105 -0
- package/commands/rate.test.js +119 -0
- package/index.ts +66 -72
- package/package.json +1 -1
- package/clients/bus/bus.js +0 -191
- package/clients/bus/bus.ts +0 -251
- package/clients/bus/events.js +0 -214
- package/clients/bus/events.ts +0 -279
- package/clients/bus/index.js +0 -8
- package/clients/bus/index.ts +0 -9
- package/clients/bus/integration.js +0 -158
- package/clients/bus/integration.ts +0 -227
- package/clients/dispatch/bus-dispatcher.js +0 -178
- package/clients/dispatch/bus-dispatcher.ts +0 -258
- package/clients/services/__tests__/effect-integration.test.ts +0 -111
- package/clients/services/effect-integration.js +0 -198
- package/clients/services/effect-integration.ts +0 -276
- package/clients/services/index.js +0 -7
- package/clients/services/index.ts +0 -8
- package/clients/services/runner-service.js +0 -134
- package/clients/services/runner-service.ts +0 -225
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { CacheManager, } from "./cache-manager.js";
|
|
6
|
+
describe("CacheManager", () => {
|
|
7
|
+
let manager;
|
|
8
|
+
let testDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
manager = new CacheManager();
|
|
11
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-cache-test-"));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
describe("scanner cache", () => {
|
|
17
|
+
it("should return null for missing cache", () => {
|
|
18
|
+
const result = manager.readCache("knip", testDir);
|
|
19
|
+
expect(result).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
it("should write and read cache", () => {
|
|
22
|
+
const data = { files: ["a.ts", "b.ts"], unused: ["x"] };
|
|
23
|
+
manager.writeCache("knip", data, testDir, { scanDurationMs: 1500 });
|
|
24
|
+
const result = manager.readCache("knip", testDir);
|
|
25
|
+
expect(result).not.toBeNull();
|
|
26
|
+
expect(result?.data).toEqual(data);
|
|
27
|
+
expect(result?.meta.scanDurationMs).toBe(1500);
|
|
28
|
+
expect(result?.meta.timestamp).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
it("should return null for stale cache", () => {
|
|
31
|
+
const data = { files: [] };
|
|
32
|
+
manager.writeCache("jscpd", data, testDir);
|
|
33
|
+
// Manually set old timestamp
|
|
34
|
+
const metaPath = path.join(testDir, ".pi-lens", "cache", "jscpd.meta.json");
|
|
35
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
36
|
+
meta.timestamp = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // 1 hour ago
|
|
37
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta));
|
|
38
|
+
const result = manager.readCache("jscpd", testDir, 30 * 60 * 1000);
|
|
39
|
+
expect(result).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
it("should respect custom maxAge", () => {
|
|
42
|
+
const data = { files: [] };
|
|
43
|
+
manager.writeCache("madge", data, testDir);
|
|
44
|
+
// Cache is 45 min old
|
|
45
|
+
const metaPath = path.join(testDir, ".pi-lens", "cache", "madge.meta.json");
|
|
46
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
47
|
+
meta.timestamp = new Date(Date.now() - 45 * 60 * 1000).toISOString();
|
|
48
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta));
|
|
49
|
+
// Default 30 min → stale
|
|
50
|
+
expect(manager.readCache("madge", testDir)).toBeNull();
|
|
51
|
+
// Custom 60 min → fresh
|
|
52
|
+
const result = manager.readCache("madge", testDir, 60 * 60 * 1000);
|
|
53
|
+
expect(result).not.toBeNull();
|
|
54
|
+
});
|
|
55
|
+
it("should check cache freshness", () => {
|
|
56
|
+
expect(manager.isCacheFresh("knip", testDir)).toBe(false);
|
|
57
|
+
manager.writeCache("knip", {}, testDir);
|
|
58
|
+
expect(manager.isCacheFresh("knip", testDir)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it("should clear cache", () => {
|
|
61
|
+
manager.writeCache("jscpd", { clones: [] }, testDir);
|
|
62
|
+
expect(manager.isCacheFresh("jscpd", testDir)).toBe(true);
|
|
63
|
+
manager.clearCache("jscpd", testDir);
|
|
64
|
+
expect(manager.isCacheFresh("jscpd", testDir)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// Helper to get test file path (absolute)
|
|
68
|
+
const testFile = (name) => path.join(testDir, name);
|
|
69
|
+
describe("turn state", () => {
|
|
70
|
+
it("should return default state when no file exists", () => {
|
|
71
|
+
const state = manager.readTurnState(testDir);
|
|
72
|
+
expect(state.files).toEqual({});
|
|
73
|
+
expect(state.turnCycles).toBe(0);
|
|
74
|
+
expect(state.maxCycles).toBe(3);
|
|
75
|
+
});
|
|
76
|
+
it("should write and read turn state", () => {
|
|
77
|
+
const state = {
|
|
78
|
+
files: {
|
|
79
|
+
"src/a.ts": {
|
|
80
|
+
modifiedRanges: [{ start: 1, end: 10 }],
|
|
81
|
+
importsChanged: true,
|
|
82
|
+
lastEdit: new Date().toISOString(),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
turnCycles: 1,
|
|
86
|
+
maxCycles: 3,
|
|
87
|
+
lastUpdated: "",
|
|
88
|
+
};
|
|
89
|
+
manager.writeTurnState(state, testDir);
|
|
90
|
+
const read = manager.readTurnState(testDir);
|
|
91
|
+
expect(read.turnCycles).toBe(1);
|
|
92
|
+
expect(read.files["src/a.ts"].modifiedRanges).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
it("should add modified ranges and merge overlapping", () => {
|
|
95
|
+
manager.addModifiedRange(testFile("src/a.ts"), { start: 1, end: 10 }, false, testDir);
|
|
96
|
+
manager.addModifiedRange(testFile("src/a.ts"), { start: 8, end: 20 }, true, testDir);
|
|
97
|
+
const state = manager.readTurnState(testDir);
|
|
98
|
+
const key = "src/a.ts";
|
|
99
|
+
const ranges = state.files[key]?.modifiedRanges;
|
|
100
|
+
expect(ranges).toHaveLength(1); // Merged into one
|
|
101
|
+
expect(ranges?.[0]).toEqual({ start: 1, end: 20 });
|
|
102
|
+
expect(state.files[key].importsChanged).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it("should track imports_changed flag", () => {
|
|
105
|
+
// First edit without import change
|
|
106
|
+
manager.addModifiedRange(testFile("src/a.ts"), { start: 1, end: 5 }, false, testDir);
|
|
107
|
+
// Second edit with import change
|
|
108
|
+
manager.addModifiedRange(testFile("src/a.ts"), { start: 10, end: 15 }, true, testDir);
|
|
109
|
+
const state = manager.readTurnState(testDir);
|
|
110
|
+
expect(state.files["src/a.ts"].importsChanged).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it("should increment turn cycle", () => {
|
|
113
|
+
manager.incrementTurnCycle(testDir);
|
|
114
|
+
manager.incrementTurnCycle(testDir);
|
|
115
|
+
const state = manager.readTurnState(testDir);
|
|
116
|
+
expect(state.turnCycles).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
it("should detect max cycles exceeded", () => {
|
|
119
|
+
expect(manager.isMaxCyclesExceeded(testDir)).toBe(false);
|
|
120
|
+
manager.incrementTurnCycle(testDir);
|
|
121
|
+
manager.incrementTurnCycle(testDir);
|
|
122
|
+
manager.incrementTurnCycle(testDir);
|
|
123
|
+
expect(manager.isMaxCyclesExceeded(testDir)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it("should clear turn state", () => {
|
|
126
|
+
manager.addModifiedRange(testFile("src/a.ts"), { start: 1, end: 10 }, true, testDir);
|
|
127
|
+
manager.incrementTurnCycle(testDir);
|
|
128
|
+
manager.clearTurnState(testDir);
|
|
129
|
+
const state = manager.readTurnState(testDir);
|
|
130
|
+
expect(Object.keys(state.files)).toHaveLength(0);
|
|
131
|
+
expect(state.turnCycles).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("file queries", () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
// Clear any previous state from other tests
|
|
137
|
+
manager.clearTurnState(testDir);
|
|
138
|
+
// Now add our test files
|
|
139
|
+
manager.addModifiedRange(testFile("a.ts"), { start: 1, end: 10 }, false, testDir);
|
|
140
|
+
manager.addModifiedRange(testFile("b.ts"), { start: 5, end: 20 }, true, testDir);
|
|
141
|
+
manager.addModifiedRange(testFile("c.ts"), { start: 1, end: 5 }, true, testDir);
|
|
142
|
+
});
|
|
143
|
+
it("should get all files for jscpd", () => {
|
|
144
|
+
const files = manager.getFilesForJscpd(testDir);
|
|
145
|
+
expect(files).toHaveLength(3);
|
|
146
|
+
});
|
|
147
|
+
it("should get only files with import changes for madge", () => {
|
|
148
|
+
// Verify state was recorded correctly
|
|
149
|
+
const state = manager.readTurnState(testDir);
|
|
150
|
+
const fileKeys = Object.keys(state.files);
|
|
151
|
+
// Only b.ts and c.ts have importsChanged: true
|
|
152
|
+
const madgeFiles = manager.getFilesForMadge(testDir);
|
|
153
|
+
const filesWithImportsTrue = fileKeys.filter((k) => state.files[k].importsChanged);
|
|
154
|
+
expect(madgeFiles).toHaveLength(filesWithImportsTrue.length);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe("range utilities", () => {
|
|
158
|
+
it("should merge non-overlapping ranges", () => {
|
|
159
|
+
const ranges = [
|
|
160
|
+
{ start: 1, end: 5 },
|
|
161
|
+
{ start: 10, end: 15 },
|
|
162
|
+
{ start: 20, end: 25 },
|
|
163
|
+
];
|
|
164
|
+
expect(manager.mergeRanges(ranges)).toHaveLength(3);
|
|
165
|
+
});
|
|
166
|
+
it("should merge overlapping ranges", () => {
|
|
167
|
+
const ranges = [
|
|
168
|
+
{ start: 1, end: 10 },
|
|
169
|
+
{ start: 5, end: 15 },
|
|
170
|
+
];
|
|
171
|
+
const merged = manager.mergeRanges(ranges);
|
|
172
|
+
expect(merged).toHaveLength(1);
|
|
173
|
+
expect(merged[0]).toEqual({ start: 1, end: 15 });
|
|
174
|
+
});
|
|
175
|
+
it("should merge adjacent ranges", () => {
|
|
176
|
+
const ranges = [
|
|
177
|
+
{ start: 1, end: 10 },
|
|
178
|
+
{ start: 11, end: 20 },
|
|
179
|
+
];
|
|
180
|
+
const merged = manager.mergeRanges(ranges);
|
|
181
|
+
expect(merged).toHaveLength(1);
|
|
182
|
+
expect(merged[0]).toEqual({ start: 1, end: 20 });
|
|
183
|
+
});
|
|
184
|
+
it("should detect line in modified range", () => {
|
|
185
|
+
const ranges = [
|
|
186
|
+
{ start: 10, end: 20 },
|
|
187
|
+
{ start: 30, end: 40 },
|
|
188
|
+
];
|
|
189
|
+
expect(manager.isLineInModifiedRange(5, ranges)).toBe(false);
|
|
190
|
+
expect(manager.isLineInModifiedRange(10, ranges)).toBe(true);
|
|
191
|
+
expect(manager.isLineInModifiedRange(15, ranges)).toBe(true);
|
|
192
|
+
expect(manager.isLineInModifiedRange(20, ranges)).toBe(true);
|
|
193
|
+
expect(manager.isLineInModifiedRange(25, ranges)).toBe(false);
|
|
194
|
+
expect(manager.isLineInModifiedRange(35, ranges)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { ComplexityClient } from "./complexity-client.js";
|
|
3
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
4
|
+
describe("ComplexityClient", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new ComplexityClient();
|
|
10
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-complexity-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
describe("isSupportedFile", () => {
|
|
16
|
+
it("should support TypeScript files", () => {
|
|
17
|
+
expect(client.isSupportedFile("test.ts")).toBe(true);
|
|
18
|
+
expect(client.isSupportedFile("test.tsx")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it("should support JavaScript files", () => {
|
|
21
|
+
expect(client.isSupportedFile("test.js")).toBe(true);
|
|
22
|
+
expect(client.isSupportedFile("test.jsx")).toBe(true);
|
|
23
|
+
expect(client.isSupportedFile("test.mjs")).toBe(true);
|
|
24
|
+
expect(client.isSupportedFile("test.cjs")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it("should not support non-TS/JS files", () => {
|
|
27
|
+
expect(client.isSupportedFile("test.py")).toBe(false);
|
|
28
|
+
expect(client.isSupportedFile("test.json")).toBe(false);
|
|
29
|
+
expect(client.isSupportedFile("test.md")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("analyzeFile", () => {
|
|
33
|
+
it("should return null for non-existent files", () => {
|
|
34
|
+
const result = client.analyzeFile("/nonexistent/file.ts");
|
|
35
|
+
expect(result).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("should analyze a simple function", () => {
|
|
38
|
+
try {
|
|
39
|
+
const content = `
|
|
40
|
+
function greet(name: string): string {
|
|
41
|
+
return "Hello, " + name;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
const filePath = createTempFile(tmpDir, "simple.ts", content);
|
|
45
|
+
const result = client.analyzeFile(filePath);
|
|
46
|
+
expect(result).not.toBeNull();
|
|
47
|
+
expect(result?.functionCount).toBe(1);
|
|
48
|
+
expect(result?.cyclomaticComplexity).toBe(1);
|
|
49
|
+
expect(result?.cognitiveComplexity).toBe(0);
|
|
50
|
+
expect(result?.maxNestingDepth).toBeGreaterThanOrEqual(1);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("should detect if statements in cyclomatic complexity", () => {
|
|
56
|
+
try {
|
|
57
|
+
const content = `
|
|
58
|
+
function check(x: number): string {
|
|
59
|
+
if (x > 0) {
|
|
60
|
+
return "positive";
|
|
61
|
+
} else if (x < 0) {
|
|
62
|
+
return "negative";
|
|
63
|
+
} else {
|
|
64
|
+
return "zero";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
const filePath = createTempFile(tmpDir, "if-test.ts", content);
|
|
69
|
+
const result = client.analyzeFile(filePath);
|
|
70
|
+
expect(result).not.toBeNull();
|
|
71
|
+
// 1 base + 1 if + 1 else-if = 3
|
|
72
|
+
expect(result?.cyclomaticComplexity).toBeGreaterThanOrEqual(3);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it("should calculate maintainability index", () => {
|
|
78
|
+
try {
|
|
79
|
+
const content = `
|
|
80
|
+
function simple(): number {
|
|
81
|
+
return 42;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
const filePath = createTempFile(tmpDir, "mi-test.ts", content);
|
|
85
|
+
const result = client.analyzeFile(filePath);
|
|
86
|
+
expect(result).not.toBeNull();
|
|
87
|
+
expect(result?.maintainabilityIndex).toBeGreaterThan(0);
|
|
88
|
+
expect(result?.maintainabilityIndex).toBeLessThanOrEqual(100);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("should detect deep nesting", () => {
|
|
94
|
+
try {
|
|
95
|
+
const content = `
|
|
96
|
+
function deepNest(arr: number[][][][]): number {
|
|
97
|
+
for (let i = 0; i < arr.length; i++) {
|
|
98
|
+
for (let j = 0; j < arr[i].length; j++) {
|
|
99
|
+
for (let k = 0; k < arr[i][j].length; k++) {
|
|
100
|
+
for (let l = 0; l < arr[i][j][k].length; l++) {
|
|
101
|
+
if (arr[i][j][k][l] > 0) {
|
|
102
|
+
return arr[i][j][k][l];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
const filePath = createTempFile(tmpDir, "nesting-test.ts", content);
|
|
112
|
+
const result = client.analyzeFile(filePath);
|
|
113
|
+
expect(result).not.toBeNull();
|
|
114
|
+
expect(result?.maxNestingDepth).toBeGreaterThanOrEqual(5);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
it("should count cognitive complexity with nesting penalty", () => {
|
|
120
|
+
try {
|
|
121
|
+
const content = `
|
|
122
|
+
function nested(x: number, y: number): number {
|
|
123
|
+
if (x > 0) {
|
|
124
|
+
if (y > 0) {
|
|
125
|
+
if (x > y) {
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
const filePath = createTempFile(tmpDir, "cognitive-test.ts", content);
|
|
134
|
+
const result = client.analyzeFile(filePath);
|
|
135
|
+
expect(result).not.toBeNull();
|
|
136
|
+
// Cognitive: 1 (if) + 2 (nested if) + 3 (deeply nested if) = 6
|
|
137
|
+
expect(result?.cognitiveComplexity).toBeGreaterThanOrEqual(6);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
it("should calculate halstead volume", () => {
|
|
143
|
+
try {
|
|
144
|
+
const content = `
|
|
145
|
+
function add(a: number, b: number): number {
|
|
146
|
+
return a + b;
|
|
147
|
+
}
|
|
148
|
+
`;
|
|
149
|
+
const filePath = createTempFile(tmpDir, "halstead-test.ts", content);
|
|
150
|
+
const result = client.analyzeFile(filePath);
|
|
151
|
+
expect(result).not.toBeNull();
|
|
152
|
+
expect(result?.halsteadVolume).toBeGreaterThan(0);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
it("should measure function length", () => {
|
|
158
|
+
try {
|
|
159
|
+
const shortContent = `function short() { return 1; }`;
|
|
160
|
+
const longContent = `
|
|
161
|
+
function long(): number {
|
|
162
|
+
const a = 1;
|
|
163
|
+
const b = 2;
|
|
164
|
+
const c = 3;
|
|
165
|
+
const d = 4;
|
|
166
|
+
const e = 5;
|
|
167
|
+
const f = 6;
|
|
168
|
+
const g = 7;
|
|
169
|
+
const h = 8;
|
|
170
|
+
const i = 9;
|
|
171
|
+
const j = 10;
|
|
172
|
+
return a + b + c + d + e + f + g + h + i + j;
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
const shortPath = createTempFile(tmpDir, "short.ts", shortContent);
|
|
176
|
+
const longPath = createTempFile(tmpDir, "long.ts", longContent);
|
|
177
|
+
const shortResult = client.analyzeFile(shortPath);
|
|
178
|
+
const longResult = client.analyzeFile(longPath);
|
|
179
|
+
expect(shortResult?.maxFunctionLength ?? 0).toBeLessThan(longResult?.maxFunctionLength ?? 0);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe("formatMetrics", () => {
|
|
186
|
+
it("should format metrics for display", () => {
|
|
187
|
+
const metrics = {
|
|
188
|
+
filePath: "test.ts",
|
|
189
|
+
maxNestingDepth: 4,
|
|
190
|
+
avgFunctionLength: 15,
|
|
191
|
+
maxFunctionLength: 30,
|
|
192
|
+
functionCount: 3,
|
|
193
|
+
cyclomaticComplexity: 4,
|
|
194
|
+
maxCyclomaticComplexity: 8,
|
|
195
|
+
cognitiveComplexity: 12,
|
|
196
|
+
halsteadVolume: 200,
|
|
197
|
+
maintainabilityIndex: 75,
|
|
198
|
+
linesOfCode: 100,
|
|
199
|
+
commentLines: 10,
|
|
200
|
+
codeEntropy: 0.5,
|
|
201
|
+
maxParamsInFunction: 3,
|
|
202
|
+
aiCommentPatterns: 1,
|
|
203
|
+
singleUseFunctions: 0,
|
|
204
|
+
tryCatchCount: 1,
|
|
205
|
+
};
|
|
206
|
+
const formatted = client.formatMetrics(metrics);
|
|
207
|
+
expect(formatted).toContain("test.ts");
|
|
208
|
+
expect(formatted).toContain("75/100");
|
|
209
|
+
});
|
|
210
|
+
it("should warn about low maintainability", () => {
|
|
211
|
+
const metrics = {
|
|
212
|
+
filePath: "bad.ts",
|
|
213
|
+
maxNestingDepth: 8,
|
|
214
|
+
avgFunctionLength: 60,
|
|
215
|
+
maxFunctionLength: 100,
|
|
216
|
+
functionCount: 5,
|
|
217
|
+
cyclomaticComplexity: 15,
|
|
218
|
+
maxCyclomaticComplexity: 25,
|
|
219
|
+
cognitiveComplexity: 50,
|
|
220
|
+
halsteadVolume: 800,
|
|
221
|
+
maintainabilityIndex: 25,
|
|
222
|
+
linesOfCode: 500,
|
|
223
|
+
commentLines: 10,
|
|
224
|
+
codeEntropy: 0.5,
|
|
225
|
+
maxParamsInFunction: 4,
|
|
226
|
+
aiCommentPatterns: 2,
|
|
227
|
+
singleUseFunctions: 1,
|
|
228
|
+
tryCatchCount: 2,
|
|
229
|
+
};
|
|
230
|
+
const formatted = client.formatMetrics(metrics);
|
|
231
|
+
expect(formatted).toContain("✗"); // Low MI indicator
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { DependencyChecker } from "./dependency-checker.js";
|
|
3
|
+
import { setupTestEnvironment } from "./test-utils.js";
|
|
4
|
+
describe("DependencyChecker", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new DependencyChecker();
|
|
10
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-dep-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
describe("isAvailable", () => {
|
|
16
|
+
it("should check madge availability", () => {
|
|
17
|
+
const available = client.isAvailable();
|
|
18
|
+
expect(typeof available).toBe("boolean");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe("checkFile", () => {
|
|
22
|
+
it("should return no circular deps for non-existent files", () => {
|
|
23
|
+
const result = client.checkFile("/nonexistent/file.ts");
|
|
24
|
+
expect(result.hasCircular).toBe(false);
|
|
25
|
+
expect(result.circular).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
it("should return correct structure when not available", () => {
|
|
28
|
+
const mockChecker = new DependencyChecker();
|
|
29
|
+
if (mockChecker.isAvailable())
|
|
30
|
+
return; // Skip if available
|
|
31
|
+
const result = mockChecker.checkFile("/some/file.ts");
|
|
32
|
+
expect(result).toHaveProperty("hasCircular");
|
|
33
|
+
expect(result).toHaveProperty("circular");
|
|
34
|
+
expect(result).toHaveProperty("checked");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("scanProject", () => {
|
|
38
|
+
it("should return correct structure", () => {
|
|
39
|
+
const mockChecker = new DependencyChecker();
|
|
40
|
+
// When not available, should still return expected structure
|
|
41
|
+
const result = mockChecker.scanProject(tmpDir);
|
|
42
|
+
expect(result).toHaveProperty("circular");
|
|
43
|
+
expect(result).toHaveProperty("count");
|
|
44
|
+
expect(Array.isArray(result.circular)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("formatWarning", () => {
|
|
48
|
+
it("should format circular dependency warning", () => {
|
|
49
|
+
const circularDeps = ["b.ts", "c.ts", "a.ts"];
|
|
50
|
+
const formatted = client.formatWarning("a.ts", circularDeps);
|
|
51
|
+
expect(formatted).toContain("cycle");
|
|
52
|
+
expect(formatted).toContain("a.ts");
|
|
53
|
+
});
|
|
54
|
+
it("should show the circular path", () => {
|
|
55
|
+
const circularDeps = ["b.ts", "a.ts"];
|
|
56
|
+
const formatted = client.formatWarning("a.ts", circularDeps);
|
|
57
|
+
expect(formatted).toContain("b.ts");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|