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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -10
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/format-service.test.js +245 -0
  5. package/clients/__tests__/formatters.test.js +271 -0
  6. package/clients/agent-behavior-client.test.js +94 -0
  7. package/clients/biome-client.test.js +144 -0
  8. package/clients/cache-manager.test.js +197 -0
  9. package/clients/complexity-client.test.js +234 -0
  10. package/clients/dependency-checker.test.js +60 -0
  11. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  12. package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
  13. package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
  14. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  15. package/clients/dispatch/dispatcher.format.test.js +46 -0
  16. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  17. package/clients/dispatch/dispatcher.test.js +116 -0
  18. package/clients/dispatch/runners/architect.test.js +138 -0
  19. package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
  20. package/clients/dispatch/runners/lsp.js +42 -5
  21. package/clients/dispatch/runners/oxlint.test.js +230 -0
  22. package/clients/dispatch/runners/pyright.test.js +98 -0
  23. package/clients/dispatch/runners/python-slop.test.js +203 -0
  24. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  25. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  26. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  27. package/clients/dispatch/utils/format-utils.js +1 -6
  28. package/clients/dispatch/utils/format-utils.ts +1 -6
  29. package/clients/dogfood.test.js +201 -0
  30. package/clients/file-kinds.test.js +169 -0
  31. package/clients/formatters.js +1 -1
  32. package/clients/go-client.test.js +127 -0
  33. package/clients/jscpd-client.test.js +127 -0
  34. package/clients/knip-client.test.js +112 -0
  35. package/clients/lsp/__tests__/client.test.js +310 -0
  36. package/clients/lsp/__tests__/client.test.ts +1 -46
  37. package/clients/lsp/__tests__/config.test.js +167 -0
  38. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  39. package/clients/lsp/__tests__/integration.test.js +127 -0
  40. package/clients/lsp/__tests__/launch.test.js +313 -0
  41. package/clients/lsp/__tests__/server.test.js +259 -0
  42. package/clients/lsp/__tests__/service.test.js +435 -0
  43. package/clients/lsp/client.js +32 -44
  44. package/clients/lsp/client.ts +36 -45
  45. package/clients/lsp/launch.js +11 -6
  46. package/clients/lsp/launch.ts +11 -6
  47. package/clients/lsp/server.js +27 -2
  48. package/clients/metrics-client.test.js +141 -0
  49. package/clients/ruff-client.test.js +132 -0
  50. package/clients/rust-client.test.js +108 -0
  51. package/clients/sanitize.test.js +177 -0
  52. package/clients/secrets-scanner.test.js +100 -0
  53. package/clients/test-runner-client.test.js +192 -0
  54. package/clients/todo-scanner.test.js +301 -0
  55. package/clients/type-coverage-client.test.js +105 -0
  56. package/clients/typescript-client.codefix.test.js +157 -0
  57. package/clients/typescript-client.test.js +105 -0
  58. package/commands/rate.test.js +119 -0
  59. package/index.ts +66 -72
  60. package/package.json +1 -1
  61. package/clients/bus/bus.js +0 -191
  62. package/clients/bus/bus.ts +0 -251
  63. package/clients/bus/events.js +0 -214
  64. package/clients/bus/events.ts +0 -279
  65. package/clients/bus/index.js +0 -8
  66. package/clients/bus/index.ts +0 -9
  67. package/clients/bus/integration.js +0 -158
  68. package/clients/bus/integration.ts +0 -227
  69. package/clients/dispatch/bus-dispatcher.js +0 -178
  70. package/clients/dispatch/bus-dispatcher.ts +0 -258
  71. package/clients/services/__tests__/effect-integration.test.ts +0 -111
  72. package/clients/services/effect-integration.js +0 -198
  73. package/clients/services/effect-integration.ts +0 -276
  74. package/clients/services/index.js +0 -7
  75. package/clients/services/index.ts +0 -8
  76. package/clients/services/runner-service.js +0 -134
  77. 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
+ });