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,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for oxlint runner
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
/**
|
|
9
|
+
* Delay helper for Windows file cleanup
|
|
10
|
+
* Windows may hold file handles briefly after process exit
|
|
11
|
+
*/
|
|
12
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
function createMockContext(filePath, overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
filePath,
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
kind: "jsts",
|
|
18
|
+
autofix: false,
|
|
19
|
+
deltaMode: false,
|
|
20
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
21
|
+
pi: { getFlag: () => false, ...overrides.pi },
|
|
22
|
+
hasTool: async () => false,
|
|
23
|
+
log: () => { },
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe("oxlint runner", () => {
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
it("should have correct runner definition", async () => {
|
|
30
|
+
const oxlintModule = await import("./oxlint.js");
|
|
31
|
+
const runner = oxlintModule.default;
|
|
32
|
+
expect(runner.id).toBe("oxlint");
|
|
33
|
+
expect(runner.appliesTo).toEqual(["jsts"]);
|
|
34
|
+
expect(runner.priority).toBe(12);
|
|
35
|
+
expect(runner.enabledByDefault).toBe(false); // Opt-in initially
|
|
36
|
+
expect(runner.skipTestFiles).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("should detect oxlint availability", () => {
|
|
39
|
+
const { spawnSync } = require("node:child_process");
|
|
40
|
+
const result = spawnSync("oxlint", ["--version"], {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: 10000,
|
|
43
|
+
shell: true,
|
|
44
|
+
});
|
|
45
|
+
expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy(); // May or may not be installed
|
|
46
|
+
});
|
|
47
|
+
it("should detect common lint issues", async () => {
|
|
48
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_test_${Date.now()}.ts`);
|
|
49
|
+
fs.writeFileSync(tmpFile, `// Test file with issues
|
|
50
|
+
function test() {
|
|
51
|
+
// Double negation
|
|
52
|
+
const flag = !!value;
|
|
53
|
+
|
|
54
|
+
// Unused variable
|
|
55
|
+
const unused = 42;
|
|
56
|
+
|
|
57
|
+
// Console statement
|
|
58
|
+
console.log("test");
|
|
59
|
+
}
|
|
60
|
+
`);
|
|
61
|
+
try {
|
|
62
|
+
const oxlintModule = await import("./oxlint.js");
|
|
63
|
+
const runner = oxlintModule.default;
|
|
64
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
65
|
+
// If oxlint is installed, should detect issues
|
|
66
|
+
// If not installed, will be skipped
|
|
67
|
+
if (result.status !== "skipped") {
|
|
68
|
+
// Should detect at least some issues (console, unused vars, etc.)
|
|
69
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
70
|
+
expect(result.diagnostics.some((d) => d.tool === "oxlint" &&
|
|
71
|
+
(d.message.includes("console") ||
|
|
72
|
+
d.message.includes("unused") ||
|
|
73
|
+
d.message.includes("!!")))).toBe(true);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
// Windows may hold file handles briefly - add small delay
|
|
78
|
+
await delay(100);
|
|
79
|
+
if (fs.existsSync(tmpFile)) {
|
|
80
|
+
fs.unlinkSync(tmpFile);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
it("should respect no-oxlint flag", async () => {
|
|
85
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_flag_${Date.now()}.ts`);
|
|
86
|
+
fs.writeFileSync(tmpFile, `function test() { console.log("test"); }`);
|
|
87
|
+
try {
|
|
88
|
+
const oxlintModule = await import("./oxlint.js");
|
|
89
|
+
const runner = oxlintModule.default;
|
|
90
|
+
// Create context with no-oxlint flag set to true
|
|
91
|
+
const ctxWithFlag = createMockContext(tmpFile, {
|
|
92
|
+
pi: { getFlag: (name) => name === "no-oxlint" },
|
|
93
|
+
});
|
|
94
|
+
const result = await runner.run(ctxWithFlag);
|
|
95
|
+
expect(result.status).toBe("skipped");
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
// Windows may hold file handles briefly - add small delay
|
|
99
|
+
await delay(100);
|
|
100
|
+
if (fs.existsSync(tmpFile)) {
|
|
101
|
+
fs.unlinkSync(tmpFile);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
it("should provide fix suggestions when available", async () => {
|
|
106
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_fix_${Date.now()}.ts`);
|
|
107
|
+
fs.writeFileSync(tmpFile, `// File with auto-fixable issues
|
|
108
|
+
const x = !!value;
|
|
109
|
+
`);
|
|
110
|
+
try {
|
|
111
|
+
const oxlintModule = await import("./oxlint.js");
|
|
112
|
+
const runner = oxlintModule.default;
|
|
113
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
114
|
+
if (result.status !== "skipped" && result.diagnostics.length > 0) {
|
|
115
|
+
// Some issues should be fixable
|
|
116
|
+
const fixableDiags = result.diagnostics.filter((d) => d.fixable);
|
|
117
|
+
// At least some diagnostics should have fixes
|
|
118
|
+
expect(fixableDiags.length).toBeGreaterThanOrEqual(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
// Windows may hold file handles briefly - add small delay
|
|
123
|
+
await delay(100);
|
|
124
|
+
if (fs.existsSync(tmpFile)) {
|
|
125
|
+
fs.unlinkSync(tmpFile);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
it("should pass clean TypeScript files", async () => {
|
|
130
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_ok_${Date.now()}.ts`);
|
|
131
|
+
fs.writeFileSync(tmpFile, `// Clean TypeScript file
|
|
132
|
+
function greet(name: string): string {
|
|
133
|
+
return \`Hello, \${name}!\`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = greet("world");
|
|
137
|
+
export { greet };
|
|
138
|
+
`);
|
|
139
|
+
try {
|
|
140
|
+
const oxlintModule = await import("./oxlint.js");
|
|
141
|
+
const runner = oxlintModule.default;
|
|
142
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
143
|
+
if (result.status !== "skipped") {
|
|
144
|
+
// Clean files should have no issues
|
|
145
|
+
expect(result.diagnostics.length).toBe(0);
|
|
146
|
+
expect(result.status).toBe("succeeded");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
// Windows may hold file handles briefly - add small delay
|
|
151
|
+
await delay(100);
|
|
152
|
+
if (fs.existsSync(tmpFile)) {
|
|
153
|
+
fs.unlinkSync(tmpFile);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
it("should handle JSON output correctly", async () => {
|
|
158
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_json_${Date.now()}.ts`);
|
|
159
|
+
fs.writeFileSync(tmpFile, `const unused = 1;`);
|
|
160
|
+
try {
|
|
161
|
+
const oxlintModule = await import("./oxlint.js");
|
|
162
|
+
const runner = oxlintModule.default;
|
|
163
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
164
|
+
if (result.status !== "skipped") {
|
|
165
|
+
// All diagnostics should have required fields
|
|
166
|
+
for (const diag of result.diagnostics) {
|
|
167
|
+
expect(diag.id).toBeDefined();
|
|
168
|
+
expect(diag.message).toBeDefined();
|
|
169
|
+
expect(diag.tool).toBe("oxlint");
|
|
170
|
+
expect(diag.line).toBeGreaterThanOrEqual(1);
|
|
171
|
+
expect(diag.severity).toMatch(/^(error|warning|info)$/);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
// Windows may hold file handles briefly - add small delay
|
|
177
|
+
await delay(100);
|
|
178
|
+
if (fs.existsSync(tmpFile)) {
|
|
179
|
+
fs.unlinkSync(tmpFile);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
it("should skip when oxlint is not available", async () => {
|
|
184
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_skip_${Date.now()}.ts`);
|
|
185
|
+
fs.writeFileSync(tmpFile, `const x = 1;`);
|
|
186
|
+
try {
|
|
187
|
+
const oxlintModule = await import("./oxlint.js");
|
|
188
|
+
const runner = oxlintModule.default;
|
|
189
|
+
// Check if oxlint is available
|
|
190
|
+
const { spawnSync } = require("node:child_process");
|
|
191
|
+
const checkResult = spawnSync("oxlint", ["--version"], {
|
|
192
|
+
encoding: "utf-8",
|
|
193
|
+
timeout: 5000,
|
|
194
|
+
shell: true,
|
|
195
|
+
});
|
|
196
|
+
const isAvailable = !checkResult.error && checkResult.status === 0;
|
|
197
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
198
|
+
if (!isAvailable) {
|
|
199
|
+
expect(result.status).toBe("skipped");
|
|
200
|
+
expect(result.diagnostics).toHaveLength(0);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
// Windows may hold file handles briefly - add small delay
|
|
205
|
+
await delay(100);
|
|
206
|
+
if (fs.existsSync(tmpFile)) {
|
|
207
|
+
fs.unlinkSync(tmpFile);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
it("should handle parsing errors gracefully", async () => {
|
|
212
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_parse_${Date.now()}.ts`);
|
|
213
|
+
// Intentionally malformed file
|
|
214
|
+
fs.writeFileSync(tmpFile, `const x = `);
|
|
215
|
+
try {
|
|
216
|
+
const oxlintModule = await import("./oxlint.js");
|
|
217
|
+
const runner = oxlintModule.default;
|
|
218
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
219
|
+
// Should handle parse errors without crashing
|
|
220
|
+
expect(["succeeded", "failed", "skipped"]).toContain(result.status);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
// Windows may hold file handles briefly - add small delay
|
|
224
|
+
await delay(100);
|
|
225
|
+
if (fs.existsSync(tmpFile)) {
|
|
226
|
+
fs.unlinkSync(tmpFile);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
function createMockContext(filePath) {
|
|
6
|
+
return {
|
|
7
|
+
filePath,
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
kind: "python",
|
|
10
|
+
autofix: false,
|
|
11
|
+
deltaMode: false,
|
|
12
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
13
|
+
pi: {},
|
|
14
|
+
hasTool: async () => false,
|
|
15
|
+
log: () => { },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe("pyright runner", () => {
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
it("should have correct runner definition", async () => {
|
|
21
|
+
const pyrightModule = await import("./pyright.js");
|
|
22
|
+
const runner = pyrightModule.default;
|
|
23
|
+
expect(runner.id).toBe("pyright");
|
|
24
|
+
expect(runner.appliesTo).toEqual(["python"]);
|
|
25
|
+
expect(runner.priority).toBe(5); // Higher priority than ruff
|
|
26
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it("should detect pyright availability", () => {
|
|
29
|
+
const { spawnSync } = require("node:child_process");
|
|
30
|
+
const result = spawnSync("npx", ["pyright", "--version"], {
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
shell: true,
|
|
34
|
+
});
|
|
35
|
+
expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
|
|
36
|
+
});
|
|
37
|
+
it("should type-check Python files and find errors", async () => {
|
|
38
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_${Date.now()}.py`);
|
|
39
|
+
fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
|
|
40
|
+
return x + y
|
|
41
|
+
|
|
42
|
+
result: str = add(1, 2)
|
|
43
|
+
|
|
44
|
+
def greet(name: str) -> str:
|
|
45
|
+
return "Hello " + name
|
|
46
|
+
|
|
47
|
+
greet(123)
|
|
48
|
+
`);
|
|
49
|
+
try {
|
|
50
|
+
const pyrightModule = await import("./pyright.js");
|
|
51
|
+
const runner = pyrightModule.default;
|
|
52
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
53
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
|
|
54
|
+
expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
|
|
55
|
+
expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(tmpFile)) {
|
|
60
|
+
fs.unlinkSync(tmpFile);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Ignore cleanup errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
it("should pass valid Python files", async () => {
|
|
69
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_ok_${Date.now()}.py`);
|
|
70
|
+
fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
|
|
71
|
+
return x + y
|
|
72
|
+
|
|
73
|
+
result: str = str(add(1, 2))
|
|
74
|
+
|
|
75
|
+
def greet(name: str) -> str:
|
|
76
|
+
return "Hello " + name
|
|
77
|
+
|
|
78
|
+
greet("world")
|
|
79
|
+
`);
|
|
80
|
+
try {
|
|
81
|
+
const pyrightModule = await import("./pyright.js");
|
|
82
|
+
const runner = pyrightModule.default;
|
|
83
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
84
|
+
expect(result.status).toBe("succeeded");
|
|
85
|
+
expect(result.diagnostics.length).toBe(0);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(tmpFile)) {
|
|
90
|
+
fs.unlinkSync(tmpFile);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore cleanup errors
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
function createMockContext(filePath) {
|
|
6
|
+
return {
|
|
7
|
+
filePath,
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
kind: "python",
|
|
10
|
+
autofix: false,
|
|
11
|
+
deltaMode: false,
|
|
12
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
13
|
+
pi: {},
|
|
14
|
+
hasTool: async () => false,
|
|
15
|
+
log: () => { },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Helper for safe file cleanup
|
|
19
|
+
function safeUnlink(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(filePath)) {
|
|
22
|
+
fs.unlinkSync(filePath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Ignore cleanup errors on Windows
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
describe("python-slop runner", () => {
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
it("should have correct runner definition", async () => {
|
|
32
|
+
const slopModule = await import("./python-slop.js");
|
|
33
|
+
const runner = slopModule.default;
|
|
34
|
+
expect(runner.id).toBe("python-slop");
|
|
35
|
+
expect(runner.appliesTo).toEqual(["python"]);
|
|
36
|
+
expect(runner.priority).toBe(25);
|
|
37
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
38
|
+
expect(runner.skipTestFiles).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it("should detect ast-grep availability", () => {
|
|
41
|
+
const { spawnSync } = require("node:child_process");
|
|
42
|
+
const result = spawnSync("npx", ["sg", "--version"], {
|
|
43
|
+
encoding: "utf-8",
|
|
44
|
+
timeout: 10000,
|
|
45
|
+
shell: true,
|
|
46
|
+
});
|
|
47
|
+
expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
|
|
48
|
+
});
|
|
49
|
+
it("should detect verbose range-len pattern", async () => {
|
|
50
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_range_${Date.now()}.py`);
|
|
51
|
+
fs.writeFileSync(tmpFile, `# Slop: using range(len()) instead of enumerate
|
|
52
|
+
def process_items(items):
|
|
53
|
+
for i in range(len(items)):
|
|
54
|
+
print(items[i])
|
|
55
|
+
`);
|
|
56
|
+
try {
|
|
57
|
+
const slopModule = await import("./python-slop.js");
|
|
58
|
+
const runner = slopModule.default;
|
|
59
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
60
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
61
|
+
expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
|
|
62
|
+
d.message.includes("range(len") &&
|
|
63
|
+
d.message.includes("enumerate"))).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
safeUnlink(tmpFile);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("should detect manual min/max pattern", async () => {
|
|
70
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_minmax_${Date.now()}.py`);
|
|
71
|
+
fs.writeFileSync(tmpFile, `# Slop: manual min/max instead of built-in
|
|
72
|
+
def find_max(a, b):
|
|
73
|
+
if a > b:
|
|
74
|
+
m = a
|
|
75
|
+
else:
|
|
76
|
+
m = b
|
|
77
|
+
return m
|
|
78
|
+
`);
|
|
79
|
+
try {
|
|
80
|
+
const slopModule = await import("./python-slop.js");
|
|
81
|
+
const runner = slopModule.default;
|
|
82
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
83
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
84
|
+
expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
|
|
85
|
+
(d.message.includes("min") || d.message.includes("max")))).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
safeUnlink(tmpFile);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it("should detect defensive None guard", async () => {
|
|
92
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_guard_${Date.now()}.py`);
|
|
93
|
+
fs.writeFileSync(tmpFile, `# Slop: defensive None guard
|
|
94
|
+
def process(data):
|
|
95
|
+
if data is None:
|
|
96
|
+
return None
|
|
97
|
+
return data.upper()
|
|
98
|
+
`);
|
|
99
|
+
try {
|
|
100
|
+
const slopModule = await import("./python-slop.js");
|
|
101
|
+
const runner = slopModule.default;
|
|
102
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
103
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
104
|
+
expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
|
|
105
|
+
(d.message.includes("defensive") ||
|
|
106
|
+
d.message.includes("guard")))).toBe(true);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
safeUnlink(tmpFile);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
it("should detect list comprehension ceremony", async () => {
|
|
113
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_list_${Date.now()}.py`);
|
|
114
|
+
fs.writeFileSync(tmpFile, `# Slop: redundant list comprehension
|
|
115
|
+
def convert(items):
|
|
116
|
+
return [x for x in items]
|
|
117
|
+
`);
|
|
118
|
+
try {
|
|
119
|
+
const slopModule = await import("./python-slop.js");
|
|
120
|
+
const runner = slopModule.default;
|
|
121
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
122
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
123
|
+
expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
|
|
124
|
+
d.message.includes("list") &&
|
|
125
|
+
d.message.includes("unnecessary"))).toBe(true);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
safeUnlink(tmpFile);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
it("should detect chained comparison opportunity", async () => {
|
|
132
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_chain_${Date.now()}.py`);
|
|
133
|
+
fs.writeFileSync(tmpFile, `# Slop: could use chained comparison
|
|
134
|
+
def check_range(x, a, b):
|
|
135
|
+
return a < x and x < b
|
|
136
|
+
`);
|
|
137
|
+
try {
|
|
138
|
+
const slopModule = await import("./python-slop.js");
|
|
139
|
+
const runner = slopModule.default;
|
|
140
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
141
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
142
|
+
expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
|
|
143
|
+
d.message.includes("chained"))).toBe(true);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
safeUnlink(tmpFile);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it("should pass clean Python files", async () => {
|
|
150
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_ok_${Date.now()}.py`);
|
|
151
|
+
fs.writeFileSync(tmpFile, `# Clean Python code
|
|
152
|
+
def process_items(items):
|
|
153
|
+
"""Process items using proper Python idioms."""
|
|
154
|
+
for i, item in enumerate(items):
|
|
155
|
+
print(f"{i}: {item}")
|
|
156
|
+
|
|
157
|
+
def find_max(a, b):
|
|
158
|
+
return max(a, b)
|
|
159
|
+
|
|
160
|
+
def check_range(x, min_val, max_val):
|
|
161
|
+
return min_val < x < max_val
|
|
162
|
+
|
|
163
|
+
def convert(items):
|
|
164
|
+
return list(items)
|
|
165
|
+
`);
|
|
166
|
+
try {
|
|
167
|
+
const slopModule = await import("./python-slop.js");
|
|
168
|
+
const runner = slopModule.default;
|
|
169
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
170
|
+
// Should have no slop issues
|
|
171
|
+
const slopIssues = result.diagnostics.filter((d) => d.tool === "python-slop");
|
|
172
|
+
expect(slopIssues.length).toBe(0);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
safeUnlink(tmpFile);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
it("should categorize by weight correctly", async () => {
|
|
179
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_weight_${Date.now()}.py`);
|
|
180
|
+
fs.writeFileSync(tmpFile, `# Multiple slop patterns - weight 3 and weight 4
|
|
181
|
+
def bad_code(items):
|
|
182
|
+
# Weight 3: range(len)
|
|
183
|
+
for i in range(len(items)):
|
|
184
|
+
print(items[i])
|
|
185
|
+
|
|
186
|
+
# Weight 3: redundant list comprehension
|
|
187
|
+
return [x for x in items]
|
|
188
|
+
`);
|
|
189
|
+
try {
|
|
190
|
+
const slopModule = await import("./python-slop.js");
|
|
191
|
+
const runner = slopModule.default;
|
|
192
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
193
|
+
// Should detect at least the range(len) pattern
|
|
194
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
195
|
+
// All should be warnings (weight 3)
|
|
196
|
+
const warnings = result.diagnostics.filter((d) => d.severity === "warning");
|
|
197
|
+
expect(warnings.length).toBeGreaterThanOrEqual(1);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
safeUnlink(tmpFile);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
// Find all TS files
|
|
5
|
+
function findTsFiles(dir) {
|
|
6
|
+
const files = [];
|
|
7
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const fullPath = path.join(dir, entry.name);
|
|
10
|
+
// Skip node_modules, .git, etc
|
|
11
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".pi-lens") {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
files.push(...findTsFiles(fullPath));
|
|
16
|
+
}
|
|
17
|
+
else if (entry.isFile() && fullPath.endsWith(".ts") && !fullPath.endsWith(".test.ts")) {
|
|
18
|
+
files.push(fullPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
function createContext(filePath) {
|
|
24
|
+
return {
|
|
25
|
+
filePath,
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
kind: "jsts",
|
|
28
|
+
autofix: false,
|
|
29
|
+
deltaMode: false,
|
|
30
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
31
|
+
pi: {},
|
|
32
|
+
hasTool: async () => false,
|
|
33
|
+
log: () => { },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
describe("Codebase scan with NAPI runner", () => {
|
|
37
|
+
it("should scan all TypeScript files and report findings", async () => {
|
|
38
|
+
const tsFiles = findTsFiles(process.cwd());
|
|
39
|
+
console.log(`\nFound ${tsFiles.length} TypeScript files to scan\n`);
|
|
40
|
+
const runner = (await import("./ast-grep-napi.js")).default;
|
|
41
|
+
const allIssues = [];
|
|
42
|
+
let totalTime = 0;
|
|
43
|
+
let filesWithIssues = 0;
|
|
44
|
+
for (let i = 0; i < Math.min(tsFiles.length, 50); i++) { // Limit to 50 for test speed
|
|
45
|
+
const file = tsFiles[i];
|
|
46
|
+
const ctx = createContext(file);
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
const result = await runner.run(ctx);
|
|
49
|
+
const elapsed = Date.now() - start;
|
|
50
|
+
totalTime += elapsed;
|
|
51
|
+
if (result.diagnostics.length > 0) {
|
|
52
|
+
filesWithIssues++;
|
|
53
|
+
console.log(`${path.relative(process.cwd(), file)} (${elapsed}ms):`);
|
|
54
|
+
for (const d of result.diagnostics.slice(0, 5)) { // Show max 5 per file
|
|
55
|
+
const line = d.line ?? 0;
|
|
56
|
+
const rule = d.rule ?? "unknown";
|
|
57
|
+
const message = d.message?.split('\n')[0] ?? "";
|
|
58
|
+
console.log(` Line ${line}: [${rule}] ${message}`);
|
|
59
|
+
allIssues.push({
|
|
60
|
+
file: path.relative(process.cwd(), file),
|
|
61
|
+
line,
|
|
62
|
+
rule,
|
|
63
|
+
message,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (result.diagnostics.length > 5) {
|
|
67
|
+
console.log(` ... and ${result.diagnostics.length - 5} more`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.log(`\n=== SUMMARY (first 50 files) ===`);
|
|
72
|
+
console.log(`Files scanned: ${Math.min(tsFiles.length, 50)}/${tsFiles.length}`);
|
|
73
|
+
console.log(`Total time: ${totalTime}ms`);
|
|
74
|
+
console.log(`Files with issues: ${filesWithIssues}`);
|
|
75
|
+
console.log(`Total issues: ${allIssues.length}`);
|
|
76
|
+
console.log(`Avg time per file: ${(totalTime / Math.min(tsFiles.length, 50)).toFixed(1)}ms`);
|
|
77
|
+
// Group by rule
|
|
78
|
+
const byRule = {};
|
|
79
|
+
for (const issue of allIssues) {
|
|
80
|
+
byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
|
|
81
|
+
}
|
|
82
|
+
console.log(`\n=== BY RULE ===`);
|
|
83
|
+
for (const [rule, count] of Object.entries(byRule).sort((a, b) => b[1] - a[1])) {
|
|
84
|
+
console.log(` ${rule}: ${count}`);
|
|
85
|
+
}
|
|
86
|
+
// This test should pass - we're just scanning
|
|
87
|
+
expect(true).toBe(true);
|
|
88
|
+
}, 60000); // 60 second timeout for scanning
|
|
89
|
+
});
|