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,301 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
3
|
+
import { TodoScanner } from "./todo-scanner.js";
|
|
4
|
+
describe("TodoScanner", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new TodoScanner();
|
|
10
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-todo-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
describe("scanFile", () => {
|
|
16
|
+
it("should return empty array for non-existent files", () => {
|
|
17
|
+
const result = client.scanFile("/nonexistent/file.ts");
|
|
18
|
+
expect(result).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it("should find TODO comments", () => {
|
|
21
|
+
const content = `
|
|
22
|
+
// TODO: implement this function
|
|
23
|
+
function foo() {}
|
|
24
|
+
`;
|
|
25
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
26
|
+
const result = client.scanFile(filePath);
|
|
27
|
+
expect(result.length).toBe(1);
|
|
28
|
+
expect(result[0].type).toBe("TODO");
|
|
29
|
+
expect(result[0].message).toContain("implement this function");
|
|
30
|
+
});
|
|
31
|
+
it("should find FIXME comments", () => {
|
|
32
|
+
const content = `
|
|
33
|
+
// FIXME: this is broken
|
|
34
|
+
function foo() {}
|
|
35
|
+
`;
|
|
36
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
37
|
+
const result = client.scanFile(filePath);
|
|
38
|
+
expect(result.length).toBe(1);
|
|
39
|
+
expect(result[0].type).toBe("FIXME");
|
|
40
|
+
});
|
|
41
|
+
it("should find HACK comments", () => {
|
|
42
|
+
const content = `
|
|
43
|
+
// HACK: temporary workaround
|
|
44
|
+
const x = 1;
|
|
45
|
+
`;
|
|
46
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
47
|
+
const result = client.scanFile(filePath);
|
|
48
|
+
expect(result.length).toBe(1);
|
|
49
|
+
expect(result[0].type).toBe("HACK");
|
|
50
|
+
});
|
|
51
|
+
it("should find BUG comments", () => {
|
|
52
|
+
const content = `
|
|
53
|
+
// BUG: this causes a crash
|
|
54
|
+
const x = 1;
|
|
55
|
+
`;
|
|
56
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
57
|
+
const result = client.scanFile(filePath);
|
|
58
|
+
expect(result.length).toBe(1);
|
|
59
|
+
expect(result[0].type).toBe("BUG");
|
|
60
|
+
});
|
|
61
|
+
it("should find NOTE comments", () => {
|
|
62
|
+
const content = `
|
|
63
|
+
// NOTE: important design decision
|
|
64
|
+
const x = 1;
|
|
65
|
+
`;
|
|
66
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
67
|
+
const result = client.scanFile(filePath);
|
|
68
|
+
expect(result.length).toBe(1);
|
|
69
|
+
expect(result[0].type).toBe("NOTE");
|
|
70
|
+
});
|
|
71
|
+
it("should find TODO in block comments", () => {
|
|
72
|
+
const content = `
|
|
73
|
+
/*
|
|
74
|
+
* TODO: refactor this later
|
|
75
|
+
*/
|
|
76
|
+
const x = 1;
|
|
77
|
+
`;
|
|
78
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
79
|
+
const result = client.scanFile(filePath);
|
|
80
|
+
expect(result.length).toBe(1);
|
|
81
|
+
expect(result[0].type).toBe("TODO");
|
|
82
|
+
});
|
|
83
|
+
it("should find TODO in JSDoc comments", () => {
|
|
84
|
+
const content = `
|
|
85
|
+
/**
|
|
86
|
+
* TODO: add proper documentation
|
|
87
|
+
*/
|
|
88
|
+
function foo() {}
|
|
89
|
+
`;
|
|
90
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
91
|
+
const result = client.scanFile(filePath);
|
|
92
|
+
expect(result.length).toBe(1);
|
|
93
|
+
expect(result[0].type).toBe("TODO");
|
|
94
|
+
});
|
|
95
|
+
it("should find TODO in Python comments", () => {
|
|
96
|
+
const content = `
|
|
97
|
+
# TODO: implement this
|
|
98
|
+
def foo():
|
|
99
|
+
pass
|
|
100
|
+
`;
|
|
101
|
+
const filePath = createTempFile(tmpDir, "test.py", content);
|
|
102
|
+
const result = client.scanFile(filePath);
|
|
103
|
+
expect(result.length).toBe(1);
|
|
104
|
+
expect(result[0].type).toBe("TODO");
|
|
105
|
+
});
|
|
106
|
+
it("should skip TODO in strings", () => {
|
|
107
|
+
const content = `
|
|
108
|
+
const message = "TODO: buy milk";
|
|
109
|
+
`;
|
|
110
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
111
|
+
const result = client.scanFile(filePath);
|
|
112
|
+
expect(result.length).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
it("should report correct line numbers", () => {
|
|
115
|
+
const content = `
|
|
116
|
+
const x = 1;
|
|
117
|
+
const y = 2;
|
|
118
|
+
// TODO: fix this
|
|
119
|
+
const z = 3;
|
|
120
|
+
`;
|
|
121
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
122
|
+
const result = client.scanFile(filePath);
|
|
123
|
+
expect(result[0].line).toBe(4);
|
|
124
|
+
});
|
|
125
|
+
it("should find multiple annotations in one file", () => {
|
|
126
|
+
const content = `
|
|
127
|
+
// TODO: first
|
|
128
|
+
// FIXME: second
|
|
129
|
+
// HACK: third
|
|
130
|
+
`;
|
|
131
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
132
|
+
const result = client.scanFile(filePath);
|
|
133
|
+
expect(result.length).toBe(3);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("scanDirectory", () => {
|
|
137
|
+
it("should scan all files in directory", () => {
|
|
138
|
+
createTempFile(tmpDir, "file1.ts", "// TODO: task 1");
|
|
139
|
+
createTempFile(tmpDir, "file2.ts", "// FIXME: bug 1");
|
|
140
|
+
createTempFile(tmpDir, "file3.py", "# HACK: workaround");
|
|
141
|
+
const result = client.scanDirectory(tmpDir);
|
|
142
|
+
expect(result.items.length).toBe(3);
|
|
143
|
+
});
|
|
144
|
+
it("should skip node_modules", () => {
|
|
145
|
+
createTempFile(tmpDir, "src/file.ts", "// TODO: task 1");
|
|
146
|
+
createTempFile(tmpDir, "node_modules/lib/file.ts", "// TODO: should be skipped");
|
|
147
|
+
const result = client.scanDirectory(tmpDir);
|
|
148
|
+
expect(result.items.length).toBe(1);
|
|
149
|
+
expect(result.items[0].file).not.toContain("node_modules");
|
|
150
|
+
});
|
|
151
|
+
it("should group items by type", () => {
|
|
152
|
+
createTempFile(tmpDir, "file1.ts", "// TODO: task 1");
|
|
153
|
+
createTempFile(tmpDir, "file2.ts", "// TODO: task 2");
|
|
154
|
+
createTempFile(tmpDir, "file3.ts", "// FIXME: bug 1");
|
|
155
|
+
const result = client.scanDirectory(tmpDir);
|
|
156
|
+
expect(result.byType.get("TODO")?.length).toBe(2);
|
|
157
|
+
expect(result.byType.get("FIXME")?.length).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
it("should group items by file", () => {
|
|
160
|
+
createTempFile(tmpDir, "file1.ts", "// TODO: task 1\n// FIXME: bug 1");
|
|
161
|
+
createTempFile(tmpDir, "file2.ts", "// TODO: task 2");
|
|
162
|
+
const result = client.scanDirectory(tmpDir);
|
|
163
|
+
const file1Items = [...result.byFile.entries()].find(([k]) => k.includes("file1.ts"));
|
|
164
|
+
expect(file1Items?.[1].length).toBe(2);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("formatResult", () => {
|
|
168
|
+
it("should return empty string for no results", () => {
|
|
169
|
+
const result = { items: [], byType: new Map(), byFile: new Map() };
|
|
170
|
+
expect(client.formatResult(result)).toBe("");
|
|
171
|
+
});
|
|
172
|
+
it("should format results with counts", () => {
|
|
173
|
+
const result = {
|
|
174
|
+
items: [
|
|
175
|
+
{
|
|
176
|
+
type: "TODO",
|
|
177
|
+
message: "task 1",
|
|
178
|
+
file: "test.ts",
|
|
179
|
+
line: 1,
|
|
180
|
+
column: 0,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: "FIXME",
|
|
184
|
+
message: "bug 1",
|
|
185
|
+
file: "test.ts",
|
|
186
|
+
line: 2,
|
|
187
|
+
column: 0,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
byType: new Map([
|
|
191
|
+
[
|
|
192
|
+
"TODO",
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
type: "TODO",
|
|
196
|
+
message: "task 1",
|
|
197
|
+
file: "test.ts",
|
|
198
|
+
line: 1,
|
|
199
|
+
column: 0,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
],
|
|
203
|
+
]),
|
|
204
|
+
byFile: new Map([
|
|
205
|
+
[
|
|
206
|
+
"test.ts",
|
|
207
|
+
[
|
|
208
|
+
{
|
|
209
|
+
type: "TODO",
|
|
210
|
+
message: "task 1",
|
|
211
|
+
file: "test.ts",
|
|
212
|
+
line: 1,
|
|
213
|
+
column: 0,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
],
|
|
217
|
+
]),
|
|
218
|
+
};
|
|
219
|
+
const formatted = client.formatResult(result);
|
|
220
|
+
expect(formatted).toContain("2 annotation(s)");
|
|
221
|
+
expect(formatted).toContain("TODO");
|
|
222
|
+
expect(formatted).toContain("FIXME");
|
|
223
|
+
});
|
|
224
|
+
it("should prioritize FIXME/HACK before TODO", () => {
|
|
225
|
+
const result = {
|
|
226
|
+
items: [
|
|
227
|
+
{
|
|
228
|
+
type: "FIXME",
|
|
229
|
+
message: "bug",
|
|
230
|
+
file: "test.ts",
|
|
231
|
+
line: 2,
|
|
232
|
+
column: 0,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: "TODO",
|
|
236
|
+
message: "task",
|
|
237
|
+
file: "test.ts",
|
|
238
|
+
line: 1,
|
|
239
|
+
column: 0,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
byType: new Map(),
|
|
243
|
+
byFile: new Map(),
|
|
244
|
+
};
|
|
245
|
+
const formatted = client.formatResult(result);
|
|
246
|
+
// Check that FIXME line comes before TODO line in the sorted output
|
|
247
|
+
const fixmeLineIndex = formatted.indexOf("🔴");
|
|
248
|
+
const todoLineIndex = formatted.indexOf("📝");
|
|
249
|
+
expect(fixmeLineIndex).toBeLessThan(todoLineIndex);
|
|
250
|
+
});
|
|
251
|
+
it("should show correct icons", () => {
|
|
252
|
+
const result = {
|
|
253
|
+
items: [
|
|
254
|
+
{
|
|
255
|
+
type: "FIXME",
|
|
256
|
+
message: "bug",
|
|
257
|
+
file: "test.ts",
|
|
258
|
+
line: 1,
|
|
259
|
+
column: 0,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: "HACK",
|
|
263
|
+
message: "hack",
|
|
264
|
+
file: "test.ts",
|
|
265
|
+
line: 2,
|
|
266
|
+
column: 0,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
type: "BUG",
|
|
270
|
+
message: "bug",
|
|
271
|
+
file: "test.ts",
|
|
272
|
+
line: 3,
|
|
273
|
+
column: 0,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
type: "TODO",
|
|
277
|
+
message: "todo",
|
|
278
|
+
file: "test.ts",
|
|
279
|
+
line: 4,
|
|
280
|
+
column: 0,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: "NOTE",
|
|
284
|
+
message: "note",
|
|
285
|
+
file: "test.ts",
|
|
286
|
+
line: 5,
|
|
287
|
+
column: 0,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
byType: new Map(),
|
|
291
|
+
byFile: new Map(),
|
|
292
|
+
};
|
|
293
|
+
const formatted = client.formatResult(result);
|
|
294
|
+
expect(formatted).toContain("🔴"); // FIXME
|
|
295
|
+
expect(formatted).toContain("🟠"); // HACK
|
|
296
|
+
expect(formatted).toContain("🐛"); // BUG
|
|
297
|
+
expect(formatted).toContain("📝"); // TODO
|
|
298
|
+
expect(formatted).toContain("ℹ️"); // NOTE
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { setupTestEnvironment } from "./test-utils.js";
|
|
3
|
+
import { TypeCoverageClient } from "./type-coverage-client.js";
|
|
4
|
+
describe("TypeCoverageClient", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let _tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new TypeCoverageClient();
|
|
10
|
+
({ tmpDir: _tmpDir, cleanup } = setupTestEnvironment("pi-lens-typecoverage-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
17
|
+
});
|
|
18
|
+
describe("isAvailable", () => {
|
|
19
|
+
it("should check type-coverage availability", () => {
|
|
20
|
+
const available = client.isAvailable();
|
|
21
|
+
expect(typeof available).toBe("boolean");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("formatResult", () => {
|
|
25
|
+
it("should return empty string when not successful", () => {
|
|
26
|
+
const result = {
|
|
27
|
+
success: false,
|
|
28
|
+
percentage: 0,
|
|
29
|
+
typed: 0,
|
|
30
|
+
total: 0,
|
|
31
|
+
untypedLocations: [],
|
|
32
|
+
};
|
|
33
|
+
expect(client.formatResult(result)).toBe("");
|
|
34
|
+
});
|
|
35
|
+
it("should show coverage percentage", () => {
|
|
36
|
+
const result = {
|
|
37
|
+
success: true,
|
|
38
|
+
percentage: 95,
|
|
39
|
+
typed: 95,
|
|
40
|
+
total: 100,
|
|
41
|
+
untypedLocations: [],
|
|
42
|
+
};
|
|
43
|
+
const formatted = client.formatResult(result);
|
|
44
|
+
expect(formatted).toContain("95.0%");
|
|
45
|
+
expect(formatted).toContain("95/100");
|
|
46
|
+
});
|
|
47
|
+
it("should show warning for low coverage", () => {
|
|
48
|
+
const result = {
|
|
49
|
+
success: true,
|
|
50
|
+
percentage: 80,
|
|
51
|
+
typed: 80,
|
|
52
|
+
total: 100,
|
|
53
|
+
untypedLocations: [],
|
|
54
|
+
};
|
|
55
|
+
const formatted = client.formatResult(result);
|
|
56
|
+
expect(formatted).toContain("⚠");
|
|
57
|
+
});
|
|
58
|
+
it("should show checkmark for high coverage", () => {
|
|
59
|
+
const result = {
|
|
60
|
+
success: true,
|
|
61
|
+
percentage: 100,
|
|
62
|
+
typed: 100,
|
|
63
|
+
total: 100,
|
|
64
|
+
untypedLocations: [],
|
|
65
|
+
};
|
|
66
|
+
const formatted = client.formatResult(result);
|
|
67
|
+
expect(formatted).toContain("✓");
|
|
68
|
+
});
|
|
69
|
+
it("should show untyped locations", () => {
|
|
70
|
+
const result = {
|
|
71
|
+
success: true,
|
|
72
|
+
percentage: 90,
|
|
73
|
+
typed: 90,
|
|
74
|
+
total: 100,
|
|
75
|
+
untypedLocations: [
|
|
76
|
+
{ file: "test.ts", line: 10, column: 5, name: "x" },
|
|
77
|
+
{ file: "test.ts", line: 20, column: 8, name: "y" },
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
const formatted = client.formatResult(result);
|
|
81
|
+
expect(formatted).toContain("test.ts:10");
|
|
82
|
+
expect(formatted).toContain("test.ts:20");
|
|
83
|
+
expect(formatted).toContain("x");
|
|
84
|
+
expect(formatted).toContain("y");
|
|
85
|
+
});
|
|
86
|
+
it("should truncate long untyped location lists", () => {
|
|
87
|
+
const locations = Array.from({ length: 20 }, (_, i) => ({
|
|
88
|
+
file: `file${i}.ts`,
|
|
89
|
+
line: i + 1,
|
|
90
|
+
column: 0,
|
|
91
|
+
name: `var${i}`,
|
|
92
|
+
}));
|
|
93
|
+
const result = {
|
|
94
|
+
success: true,
|
|
95
|
+
percentage: 80,
|
|
96
|
+
typed: 80,
|
|
97
|
+
total: 100,
|
|
98
|
+
untypedLocations: locations,
|
|
99
|
+
};
|
|
100
|
+
const formatted = client.formatResult(result, 10);
|
|
101
|
+
expect(formatted).toContain("...");
|
|
102
|
+
expect(formatted).toContain("10 more");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
3
|
+
import { TypeScriptClient } from "./typescript-client.js";
|
|
4
|
+
describe("TypeScriptClient - Code Fixes", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new TypeScriptClient();
|
|
10
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-codefix-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
describe("getCodeFixes", () => {
|
|
16
|
+
it("should provide fix for missing property on object literal", () => {
|
|
17
|
+
// Real-world case: Missing required property in object literal
|
|
18
|
+
const content = `
|
|
19
|
+
interface Config {
|
|
20
|
+
name: string;
|
|
21
|
+
port: number;
|
|
22
|
+
debug: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config: Config = {
|
|
26
|
+
name: "my-app",
|
|
27
|
+
port: 3000
|
|
28
|
+
// Missing 'debug' property - TS2345
|
|
29
|
+
};
|
|
30
|
+
`;
|
|
31
|
+
const filePath = createTempFile(tmpDir, "missing-property.ts", content);
|
|
32
|
+
client.addFile(filePath, content);
|
|
33
|
+
const diags = client.getDiagnostics(filePath);
|
|
34
|
+
const missingPropError = diags.find((d) => d.code === 2345 || d.message.includes("missing"));
|
|
35
|
+
if (missingPropError) {
|
|
36
|
+
const line = missingPropError.range.start.line;
|
|
37
|
+
const char = missingPropError.range.start.character;
|
|
38
|
+
const fixes = client.getCodeFixes(filePath, line, char, [
|
|
39
|
+
missingPropError.code,
|
|
40
|
+
]);
|
|
41
|
+
// TypeScript should suggest adding the missing property
|
|
42
|
+
expect(fixes.length).toBeGreaterThan(0);
|
|
43
|
+
const hasAddPropertyFix = fixes.some((f) => f.description.toLowerCase().includes("add") ||
|
|
44
|
+
f.description.toLowerCase().includes("property") ||
|
|
45
|
+
f.description.toLowerCase().includes("declare"));
|
|
46
|
+
expect(hasAddPropertyFix).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it("should provide fix for missing await in async function", () => {
|
|
50
|
+
// Real-world case: Forgetting await on a Promise-returning function
|
|
51
|
+
const content = `
|
|
52
|
+
async function fetchUser(id: string): Promise<{ name: string }> {
|
|
53
|
+
return { name: "John" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getUserName(id: string): Promise<string> {
|
|
57
|
+
const user = fetchUser(id); // Missing await
|
|
58
|
+
return user.name; // Type 'Promise<{ name: string; }>' has no property 'name'
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
const filePath = createTempFile(tmpDir, "missing-await.ts", content);
|
|
62
|
+
client.addFile(filePath, content);
|
|
63
|
+
const diags = client.getDiagnostics(filePath);
|
|
64
|
+
// TS2739: Type 'Promise<{ name: string; }>' is missing 'name'
|
|
65
|
+
const propertyError = diags.find((d) => d.code === 2739 || d.message.includes("is missing"));
|
|
66
|
+
if (propertyError) {
|
|
67
|
+
const fixes = client.getAllCodeFixes(filePath);
|
|
68
|
+
// If there's an error, check if we have fixes for it
|
|
69
|
+
const lineFixes = fixes.get(propertyError.range.start.line);
|
|
70
|
+
if (lineFixes) {
|
|
71
|
+
expect(lineFixes.length).toBeGreaterThan(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Test passes if we get here - not all TS versions provide fixes for this
|
|
75
|
+
expect(true).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it("should provide fix for incorrect type assignment", () => {
|
|
78
|
+
// Real-world case: String instead of number
|
|
79
|
+
const content = `
|
|
80
|
+
function calculateTotal(price: number, tax: number): number {
|
|
81
|
+
return price + tax;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = calculateTotal("100", 10); // TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
|
|
85
|
+
`;
|
|
86
|
+
const filePath = createTempFile(tmpDir, "type-mismatch.ts", content);
|
|
87
|
+
client.addFile(filePath, content);
|
|
88
|
+
const diags = client.getDiagnostics(filePath);
|
|
89
|
+
const typeError = diags.find((d) => d.code === 2345);
|
|
90
|
+
if (typeError) {
|
|
91
|
+
const line = typeError.range.start.line;
|
|
92
|
+
const char = typeError.range.start.character;
|
|
93
|
+
const fixes = client.getCodeFixes(filePath, line, char, [2345]);
|
|
94
|
+
// TypeScript often suggests fixes for type mismatches
|
|
95
|
+
expect(fixes).toBeDefined();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it("should collect all fixes via getAllCodeFixes", () => {
|
|
99
|
+
// Multiple errors in one file
|
|
100
|
+
const content = `
|
|
101
|
+
interface Person {
|
|
102
|
+
name: string;
|
|
103
|
+
age: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const person: Person = {
|
|
107
|
+
name: "Alice"
|
|
108
|
+
// Missing age
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function greet(p: Person): string {
|
|
112
|
+
return "Hello " + p.name;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
greet({ name: "Bob" }); // Missing age in argument
|
|
116
|
+
`;
|
|
117
|
+
const filePath = createTempFile(tmpDir, "multiple-errors.ts", content);
|
|
118
|
+
client.addFile(filePath, content);
|
|
119
|
+
const allFixes = client.getAllCodeFixes(filePath);
|
|
120
|
+
// Should have fixes mapped by line number
|
|
121
|
+
expect(allFixes).toBeInstanceOf(Map);
|
|
122
|
+
// Each fix entry should have a description and changes
|
|
123
|
+
for (const [line, fixes] of allFixes.entries()) {
|
|
124
|
+
expect(typeof line).toBe("number");
|
|
125
|
+
expect(fixes.length).toBeGreaterThan(0);
|
|
126
|
+
for (const fix of fixes) {
|
|
127
|
+
expect(fix.description).toBeTruthy();
|
|
128
|
+
expect(fix.changes).toBeDefined();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("Integration with diagnostic messages", () => {
|
|
134
|
+
it("should include fix suggestions in getAllCodeFixes output", () => {
|
|
135
|
+
const content = `
|
|
136
|
+
class User {
|
|
137
|
+
constructor(public name: string) {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const user = new User(); // TS2554: Expected 1 arguments, but got 0
|
|
141
|
+
`;
|
|
142
|
+
const filePath = createTempFile(tmpDir, "constructor-args.ts", content);
|
|
143
|
+
client.addFile(filePath, content);
|
|
144
|
+
const diags = client.getDiagnostics(filePath);
|
|
145
|
+
const argError = diags.find((d) => d.code === 2554);
|
|
146
|
+
if (argError) {
|
|
147
|
+
const fixes = client.getAllCodeFixes(filePath);
|
|
148
|
+
const lineFixes = fixes.get(argError.range.start.line);
|
|
149
|
+
if (lineFixes && lineFixes.length > 0) {
|
|
150
|
+
// The runner would append this to the message
|
|
151
|
+
const suggestion = lineFixes[0].description;
|
|
152
|
+
expect(suggestion).toBeTruthy();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
3
|
+
import { TypeScriptClient } from "./typescript-client.js";
|
|
4
|
+
describe("TypeScriptClient", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let cleanup;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new TypeScriptClient();
|
|
10
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-ts-test-"));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
17
|
+
});
|
|
18
|
+
describe("isTypeScriptFile", () => {
|
|
19
|
+
it("should recognize TypeScript files", () => {
|
|
20
|
+
expect(client.isTypeScriptFile("test.ts")).toBe(true);
|
|
21
|
+
expect(client.isTypeScriptFile("test.tsx")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("should also recognize JavaScript files (for type checking)", () => {
|
|
24
|
+
expect(client.isTypeScriptFile("test.js")).toBe(true);
|
|
25
|
+
expect(client.isTypeScriptFile("test.jsx")).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it("should not recognize non-JS/TS files", () => {
|
|
28
|
+
expect(client.isTypeScriptFile("test.py")).toBe(false);
|
|
29
|
+
expect(client.isTypeScriptFile("test.md")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("updateFile and getDiagnostics", () => {
|
|
33
|
+
it("should detect type errors", () => {
|
|
34
|
+
const content = `
|
|
35
|
+
const x: number = "string"; // Type error
|
|
36
|
+
`;
|
|
37
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
38
|
+
client.updateFile(filePath, content);
|
|
39
|
+
const diags = client.getDiagnostics(filePath);
|
|
40
|
+
expect(diags.length).toBeGreaterThan(0);
|
|
41
|
+
expect(diags.some((d) => d.message.includes("string"))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it("should not report errors for valid code", () => {
|
|
44
|
+
const content = `
|
|
45
|
+
const x: number = 42;
|
|
46
|
+
const y: string = "hello";
|
|
47
|
+
`;
|
|
48
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
49
|
+
client.updateFile(filePath, content);
|
|
50
|
+
const diags = client.getDiagnostics(filePath);
|
|
51
|
+
// May have warnings but no errors for valid code
|
|
52
|
+
const errors = diags.filter((d) => d.severity === 1); // Error severity
|
|
53
|
+
expect(errors.length).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
it("should detect undefined variables", () => {
|
|
56
|
+
const content = `
|
|
57
|
+
function test() {
|
|
58
|
+
return undefinedVariable;
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
62
|
+
client.updateFile(filePath, content);
|
|
63
|
+
const diags = client.getDiagnostics(filePath);
|
|
64
|
+
expect(diags.some((d) => d.message.includes("undefined"))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it("should detect missing function arguments", () => {
|
|
67
|
+
const content = `
|
|
68
|
+
function add(a: number, b: number): number {
|
|
69
|
+
return a + b;
|
|
70
|
+
}
|
|
71
|
+
const result = add(1);
|
|
72
|
+
`;
|
|
73
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
74
|
+
client.updateFile(filePath, content);
|
|
75
|
+
const diags = client.getDiagnostics(filePath);
|
|
76
|
+
expect(diags.some((d) => d.message.includes("Expected"))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("diagnostic severity", () => {
|
|
80
|
+
it("should have correct severity levels", () => {
|
|
81
|
+
const diags = [
|
|
82
|
+
{
|
|
83
|
+
range: {
|
|
84
|
+
start: { line: 0, character: 0 },
|
|
85
|
+
end: { line: 0, character: 10 },
|
|
86
|
+
},
|
|
87
|
+
severity: 1, // Error
|
|
88
|
+
message: "Error message",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
range: {
|
|
92
|
+
start: { line: 1, character: 0 },
|
|
93
|
+
end: { line: 1, character: 10 },
|
|
94
|
+
},
|
|
95
|
+
severity: 2, // Warning
|
|
96
|
+
message: "Warning message",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
// Test that diagnostics have expected structure
|
|
100
|
+
expect(diags[0].severity).toBe(1); // Error
|
|
101
|
+
expect(diags[1].severity).toBe(2); // Warning
|
|
102
|
+
expect(diags[0].message).toContain("Error");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|