pi-lens 2.2.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
@@ -0,0 +1,298 @@
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
+ import type { DispatchContext } from "../types.js";
6
+
7
+ function createMockContext(filePath: string): DispatchContext {
8
+ return {
9
+ filePath,
10
+ cwd: process.cwd(),
11
+ kind: "python" as any,
12
+ autofix: false,
13
+ deltaMode: false,
14
+ baselines: { get: () => [], add: () => {}, save: () => {} } as any,
15
+ pi: {} as any,
16
+ hasTool: async () => false,
17
+ log: () => {},
18
+ };
19
+ }
20
+
21
+ // Helper for safe file cleanup
22
+ function safeUnlink(filePath: string): void {
23
+ try {
24
+ if (fs.existsSync(filePath)) {
25
+ fs.unlinkSync(filePath);
26
+ }
27
+ } catch {
28
+ // Ignore cleanup errors on Windows
29
+ }
30
+ }
31
+
32
+ describe("python-slop runner", () => {
33
+ const require = createRequire(import.meta.url);
34
+
35
+ it("should have correct runner definition", async () => {
36
+ const slopModule = await import("./python-slop.js");
37
+ const runner = slopModule.default;
38
+
39
+ expect(runner.id).toBe("python-slop");
40
+ expect(runner.appliesTo).toEqual(["python"]);
41
+ expect(runner.priority).toBe(25);
42
+ expect(runner.enabledByDefault).toBe(true);
43
+ expect(runner.skipTestFiles).toBe(true);
44
+ });
45
+
46
+ it("should detect ast-grep availability", () => {
47
+ const { spawnSync } =
48
+ require("node:child_process") as typeof import("node:child_process");
49
+ const result = spawnSync("npx", ["sg", "--version"], {
50
+ encoding: "utf-8",
51
+ timeout: 10000,
52
+ shell: true,
53
+ });
54
+ expect(
55
+ result.error || result.status !== 0 ? "not available" : "available",
56
+ ).toBe("available");
57
+ });
58
+
59
+ it("should detect verbose range-len pattern", async () => {
60
+ const tmpFile = path.join(
61
+ process.env.TEMP || "/tmp",
62
+ `slop_test_range_${Date.now()}.py`,
63
+ );
64
+ fs.writeFileSync(
65
+ tmpFile,
66
+ `# Slop: using range(len()) instead of enumerate
67
+ def process_items(items):
68
+ for i in range(len(items)):
69
+ print(items[i])
70
+ `,
71
+ );
72
+
73
+ try {
74
+ const slopModule = await import("./python-slop.js");
75
+ const runner = slopModule.default;
76
+ const result = await runner.run(createMockContext(tmpFile));
77
+
78
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
79
+ expect(
80
+ result.diagnostics.some(
81
+ (d) =>
82
+ d.tool === "python-slop" &&
83
+ d.message.includes("range(len") &&
84
+ d.message.includes("enumerate"),
85
+ ),
86
+ ).toBe(true);
87
+ } finally {
88
+ safeUnlink(tmpFile);
89
+ }
90
+ });
91
+
92
+ it("should detect manual min/max pattern", async () => {
93
+ const tmpFile = path.join(
94
+ process.env.TEMP || "/tmp",
95
+ `slop_test_minmax_${Date.now()}.py`,
96
+ );
97
+ fs.writeFileSync(
98
+ tmpFile,
99
+ `# Slop: manual min/max instead of built-in
100
+ def find_max(a, b):
101
+ if a > b:
102
+ m = a
103
+ else:
104
+ m = b
105
+ return m
106
+ `,
107
+ );
108
+
109
+ try {
110
+ const slopModule = await import("./python-slop.js");
111
+ const runner = slopModule.default;
112
+ const result = await runner.run(createMockContext(tmpFile));
113
+
114
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
115
+ expect(
116
+ result.diagnostics.some(
117
+ (d) =>
118
+ d.tool === "python-slop" &&
119
+ (d.message.includes("min") || d.message.includes("max")),
120
+ ),
121
+ ).toBe(true);
122
+ } finally {
123
+ safeUnlink(tmpFile);
124
+ }
125
+ });
126
+
127
+ it("should detect defensive None guard", async () => {
128
+ const tmpFile = path.join(
129
+ process.env.TEMP || "/tmp",
130
+ `slop_test_guard_${Date.now()}.py`,
131
+ );
132
+ fs.writeFileSync(
133
+ tmpFile,
134
+ `# Slop: defensive None guard
135
+ def process(data):
136
+ if data is None:
137
+ return None
138
+ return data.upper()
139
+ `,
140
+ );
141
+
142
+ try {
143
+ const slopModule = await import("./python-slop.js");
144
+ const runner = slopModule.default;
145
+ const result = await runner.run(createMockContext(tmpFile));
146
+
147
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
148
+ expect(
149
+ result.diagnostics.some(
150
+ (d) =>
151
+ d.tool === "python-slop" &&
152
+ (d.message.includes("defensive") ||
153
+ d.message.includes("guard")),
154
+ ),
155
+ ).toBe(true);
156
+ } finally {
157
+ safeUnlink(tmpFile);
158
+ }
159
+ });
160
+
161
+ it("should detect list comprehension ceremony", async () => {
162
+ const tmpFile = path.join(
163
+ process.env.TEMP || "/tmp",
164
+ `slop_test_list_${Date.now()}.py`,
165
+ );
166
+ fs.writeFileSync(
167
+ tmpFile,
168
+ `# Slop: redundant list comprehension
169
+ def convert(items):
170
+ return [x for x in items]
171
+ `,
172
+ );
173
+
174
+ try {
175
+ const slopModule = await import("./python-slop.js");
176
+ const runner = slopModule.default;
177
+ const result = await runner.run(createMockContext(tmpFile));
178
+
179
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
180
+ expect(
181
+ result.diagnostics.some(
182
+ (d) =>
183
+ d.tool === "python-slop" &&
184
+ d.message.includes("list") &&
185
+ d.message.includes("unnecessary"),
186
+ ),
187
+ ).toBe(true);
188
+ } finally {
189
+ safeUnlink(tmpFile);
190
+ }
191
+ });
192
+
193
+ it("should detect chained comparison opportunity", async () => {
194
+ const tmpFile = path.join(
195
+ process.env.TEMP || "/tmp",
196
+ `slop_test_chain_${Date.now()}.py`,
197
+ );
198
+ fs.writeFileSync(
199
+ tmpFile,
200
+ `# Slop: could use chained comparison
201
+ def check_range(x, a, b):
202
+ return a < x and x < b
203
+ `,
204
+ );
205
+
206
+ try {
207
+ const slopModule = await import("./python-slop.js");
208
+ const runner = slopModule.default;
209
+ const result = await runner.run(createMockContext(tmpFile));
210
+
211
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
212
+ expect(
213
+ result.diagnostics.some(
214
+ (d) =>
215
+ d.tool === "python-slop" &&
216
+ d.message.includes("chained"),
217
+ ),
218
+ ).toBe(true);
219
+ } finally {
220
+ safeUnlink(tmpFile);
221
+ }
222
+ });
223
+
224
+ it("should pass clean Python files", async () => {
225
+ const tmpFile = path.join(
226
+ process.env.TEMP || "/tmp",
227
+ `slop_test_ok_${Date.now()}.py`,
228
+ );
229
+ fs.writeFileSync(
230
+ tmpFile,
231
+ `# Clean Python code
232
+ def process_items(items):
233
+ """Process items using proper Python idioms."""
234
+ for i, item in enumerate(items):
235
+ print(f"{i}: {item}")
236
+
237
+ def find_max(a, b):
238
+ return max(a, b)
239
+
240
+ def check_range(x, min_val, max_val):
241
+ return min_val < x < max_val
242
+
243
+ def convert(items):
244
+ return list(items)
245
+ `,
246
+ );
247
+
248
+ try {
249
+ const slopModule = await import("./python-slop.js");
250
+ const runner = slopModule.default;
251
+ const result = await runner.run(createMockContext(tmpFile));
252
+
253
+ // Should have no slop issues
254
+ const slopIssues = result.diagnostics.filter(
255
+ (d) => d.tool === "python-slop",
256
+ );
257
+ expect(slopIssues.length).toBe(0);
258
+ } finally {
259
+ safeUnlink(tmpFile);
260
+ }
261
+ });
262
+
263
+ it("should categorize by weight correctly", async () => {
264
+ const tmpFile = path.join(
265
+ process.env.TEMP || "/tmp",
266
+ `slop_test_weight_${Date.now()}.py`,
267
+ );
268
+ fs.writeFileSync(
269
+ tmpFile,
270
+ `# Multiple slop patterns - weight 3 and weight 4
271
+ def bad_code(items):
272
+ # Weight 3: range(len)
273
+ for i in range(len(items)):
274
+ print(items[i])
275
+
276
+ # Weight 3: redundant list comprehension
277
+ return [x for x in items]
278
+ `,
279
+ );
280
+
281
+ try {
282
+ const slopModule = await import("./python-slop.js");
283
+ const runner = slopModule.default;
284
+ const result = await runner.run(createMockContext(tmpFile));
285
+
286
+ // Should detect at least the range(len) pattern
287
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
288
+
289
+ // All should be warnings (weight 3)
290
+ const warnings = result.diagnostics.filter(
291
+ (d) => d.severity === "warning",
292
+ );
293
+ expect(warnings.length).toBeGreaterThanOrEqual(1);
294
+ } finally {
295
+ safeUnlink(tmpFile);
296
+ }
297
+ });
298
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Python Slop runner for dispatch system
3
+ *
4
+ * Detects "slop" patterns in Python code:
5
+ * - Verbose patterns (ceremony that adds no value)
6
+ * - Defensive over-checking (excessive guards)
7
+ * - Manual reimplementation of builtins
8
+ * - Unnecessary object allocations
9
+ *
10
+ * Based on slop-code-bench: https://github.com/SprocketLab/slop-code-bench
11
+ */
12
+
13
+ import { spawnSync } from "node:child_process";
14
+ import { safeSpawn } from "../../safe-spawn.js";
15
+ import {
16
+ createConfigFinder,
17
+ isSgAvailable,
18
+ } from "./utils/runner-helpers.js";
19
+ import type {
20
+ Diagnostic,
21
+ DispatchContext,
22
+ RunnerDefinition,
23
+ RunnerResult,
24
+ } from "../types.js";
25
+
26
+ const findSlopConfig = createConfigFinder("python-slop-rules");
27
+
28
+ const pythonSlopRunner: RunnerDefinition = {
29
+ id: "python-slop",
30
+ appliesTo: ["python"],
31
+ priority: 25, // Between pyright (5) and ruff (10)
32
+ enabledByDefault: true,
33
+ skipTestFiles: true, // Slop rules can be noisy in test files
34
+
35
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
36
+ // Check if ast-grep is available
37
+ if (!isSgAvailable()) {
38
+ return { status: "skipped", diagnostics: [], semantic: "none" };
39
+ }
40
+
41
+ // Find slop config
42
+ const configPath = findSlopConfig(ctx.cwd);
43
+ if (!configPath) {
44
+ return { status: "skipped", diagnostics: [], semantic: "none" };
45
+ }
46
+
47
+ // Run ast-grep scan
48
+ const args = ["sg", "scan", "--config", configPath, "--json", ctx.filePath];
49
+
50
+ const result = safeSpawn("npx", args, {
51
+ timeout: 30000,
52
+ });
53
+
54
+ const raw = result.stdout + result.stderr;
55
+
56
+ if (result.status === 0 && !raw.trim()) {
57
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
58
+ }
59
+
60
+ // Parse results
61
+ const diagnostics = parseSlopOutput(raw, ctx.filePath);
62
+
63
+ if (diagnostics.length === 0) {
64
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
65
+ }
66
+
67
+ return {
68
+ status: "failed",
69
+ diagnostics,
70
+ semantic: "warning",
71
+ };
72
+ },
73
+ };
74
+
75
+ function parseSlopOutput(raw: string, filePath: string): Diagnostic[] {
76
+ const diagnostics: Diagnostic[] = [];
77
+
78
+ try {
79
+ // Try to parse as JSON first
80
+ const data = JSON.parse(raw);
81
+ const items = Array.isArray(data) ? data : [data];
82
+
83
+ for (const item of items) {
84
+ const rule = item.rule || "slop";
85
+ const message = item.message || "Pattern detected";
86
+ const severity = item.severity || "warning";
87
+
88
+ diagnostics.push({
89
+ id: `python-slop-${rule}`,
90
+ message,
91
+ filePath,
92
+ line: item.start?.line || 0,
93
+ column: item.start?.column || 0,
94
+ severity: severity === "error" ? "error" : "warning",
95
+ semantic: severity === "error" ? "blocking" : "warning",
96
+ tool: "python-slop",
97
+ rule,
98
+ });
99
+ }
100
+ } catch {
101
+ // Not JSON, try line-by-line parsing
102
+ const lines = raw.split("\n").filter((l) => l.trim());
103
+ for (const line of lines) {
104
+ // Try to extract line numbers from typical output formats
105
+ const match = line.match(/:(\d+):/);
106
+ if (match) {
107
+ diagnostics.push({
108
+ id: "python-slop-pattern",
109
+ message: line.trim(),
110
+ filePath,
111
+ line: parseInt(match[1], 10),
112
+ column: 0,
113
+ severity: "warning",
114
+ semantic: "warning",
115
+ tool: "python-slop",
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ return diagnostics;
122
+ }
123
+
124
+ export default pythonSlopRunner;
@@ -4,63 +4,33 @@
4
4
  * Ruff handles both formatting and linting for Python files.
5
5
  * Supports venv-local installations.
6
6
  */
7
- import { spawnSync } from "node:child_process";
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
7
+ import { ensureTool } from "../../installer/index.js";
8
+ import { safeSpawn } from "../../safe-spawn.js";
10
9
  import { stripAnsi } from "../../sanitize.js";
11
- // Cache ruff availability check
12
- let ruffAvailable = null;
13
- let ruffCommand = null;
14
- /**
15
- * Find ruff command, checking venv first, then global.
16
- */
17
- function findRuffCommand(cwd) {
18
- const venvPaths = [
19
- ".venv/bin/ruff",
20
- "venv/bin/ruff",
21
- ".venv/Scripts/ruff.exe",
22
- "venv/Scripts/ruff.exe",
23
- ];
24
- for (const venvPath of venvPaths) {
25
- const fullPath = path.join(cwd, venvPath);
26
- if (fs.existsSync(fullPath)) {
27
- return `"${fullPath}"`;
28
- }
29
- }
30
- return "ruff";
31
- }
32
- function isRuffAvailable(cwd) {
33
- if (ruffAvailable !== null)
34
- return ruffAvailable;
35
- const command = findRuffCommand(cwd || process.cwd());
36
- const check = spawnSync(command, ["--version"], {
37
- encoding: "utf-8",
38
- timeout: 5000,
39
- shell: true,
40
- });
41
- ruffAvailable = !check.error && check.status === 0;
42
- if (ruffAvailable)
43
- ruffCommand = command;
44
- return ruffAvailable;
45
- }
10
+ import { parseRuffOutput } from "./utils/diagnostic-parsers.js";
11
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
12
+ const ruff = createAvailabilityChecker("ruff", ".exe");
46
13
  const ruffRunner = {
47
14
  id: "ruff-lint",
48
15
  appliesTo: ["python"],
49
16
  priority: 10,
50
17
  enabledByDefault: true,
51
18
  async run(ctx) {
52
- // Skip if ruff is not installed
53
- if (!isRuffAvailable(ctx.cwd || process.cwd())) {
54
- return { status: "skipped", diagnostics: [], semantic: "none" };
19
+ const cwd = ctx.cwd || process.cwd();
20
+ // Auto-install ruff if not available (it's one of the 4 auto-install tools)
21
+ if (!ruff.isAvailable(cwd)) {
22
+ const installed = await ensureTool("ruff");
23
+ if (!installed) {
24
+ return { status: "skipped", diagnostics: [], semantic: "none" };
25
+ }
55
26
  }
56
- // Run ruff check
57
- const args = ctx.autofix
58
- ? ["check", "--fix", ctx.filePath]
59
- : ["check", ctx.filePath];
60
- const result = spawnSync(ruffCommand, args, {
61
- encoding: "utf-8",
27
+ // IMPORTANT: Never use --fix in dispatch runner to prevent infinite loops.
28
+ // Writing to the file would trigger another tool_result event, which would
29
+ // call dispatchLint again, creating a feedback loop.
30
+ // Fixes should be applied through explicit commands or user edits.
31
+ const args = ["check", ctx.filePath];
32
+ const result = safeSpawn(ruff.getCommand(), args, {
62
33
  timeout: 30000,
63
- shell: true,
64
34
  });
65
35
  const raw = stripAnsi(result.stdout + result.stderr);
66
36
  if (result.status === 0) {
@@ -75,27 +45,4 @@ const ruffRunner = {
75
45
  };
76
46
  },
77
47
  };
78
- function parseRuffOutput(raw, filePath) {
79
- const lines = raw.split("\n").filter((l) => l.trim());
80
- const diagnostics = [];
81
- for (const line of lines) {
82
- // Parse ruff output: file:line:col: message (code)
83
- const match = line.match(/^(.+?):(\d+):(\d+):\s*(.+?)\s+\((.+?)\)/);
84
- if (match) {
85
- diagnostics.push({
86
- id: `ruff-${match[2]}-${match[5]}`,
87
- message: `${match[5]}: ${match[4]}`,
88
- filePath,
89
- line: parseInt(match[2], 10),
90
- column: parseInt(match[3], 10),
91
- severity: line.includes("error") ? "error" : "warning",
92
- semantic: "warning",
93
- tool: "ruff",
94
- rule: match[5],
95
- fixable: true,
96
- });
97
- }
98
- }
99
- return diagnostics;
100
- }
101
48
  export default ruffRunner;
@@ -5,56 +5,18 @@
5
5
  * Supports venv-local installations.
6
6
  */
7
7
 
8
- import { spawnSync } from "node:child_process";
9
- import * as fs from "node:fs";
10
- import * as path from "node:path";
8
+ import { ensureTool } from "../../installer/index.js";
9
+ import { safeSpawn } from "../../safe-spawn.js";
11
10
  import { stripAnsi } from "../../sanitize.js";
12
11
  import type {
13
- Diagnostic,
14
12
  DispatchContext,
15
13
  RunnerDefinition,
16
14
  RunnerResult,
17
15
  } from "../types.js";
16
+ import { parseRuffOutput } from "./utils/diagnostic-parsers.js";
17
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
18
18
 
19
- // Cache ruff availability check
20
- let ruffAvailable: boolean | null = null;
21
- let ruffCommand: string | null = null;
22
-
23
- /**
24
- * Find ruff command, checking venv first, then global.
25
- */
26
- function findRuffCommand(cwd: string): string {
27
- const venvPaths = [
28
- ".venv/bin/ruff",
29
- "venv/bin/ruff",
30
- ".venv/Scripts/ruff.exe",
31
- "venv/Scripts/ruff.exe",
32
- ];
33
-
34
- for (const venvPath of venvPaths) {
35
- const fullPath = path.join(cwd, venvPath);
36
- if (fs.existsSync(fullPath)) {
37
- return `"${fullPath}"`;
38
- }
39
- }
40
-
41
- return "ruff";
42
- }
43
-
44
- function isRuffAvailable(cwd?: string): boolean {
45
- if (ruffAvailable !== null) return ruffAvailable;
46
-
47
- const command = findRuffCommand(cwd || process.cwd());
48
- const check = spawnSync(command, ["--version"], {
49
- encoding: "utf-8",
50
- timeout: 5000,
51
- shell: true,
52
- });
53
-
54
- ruffAvailable = !check.error && check.status === 0;
55
- if (ruffAvailable) ruffCommand = command;
56
- return ruffAvailable;
57
- }
19
+ const ruff = createAvailabilityChecker("ruff", ".exe");
58
20
 
59
21
  const ruffRunner: RunnerDefinition = {
60
22
  id: "ruff-lint",
@@ -63,20 +25,24 @@ const ruffRunner: RunnerDefinition = {
63
25
  enabledByDefault: true,
64
26
 
65
27
  async run(ctx: DispatchContext): Promise<RunnerResult> {
66
- // Skip if ruff is not installed
67
- if (!isRuffAvailable(ctx.cwd || process.cwd())) {
68
- return { status: "skipped", diagnostics: [], semantic: "none" };
28
+ const cwd = ctx.cwd || process.cwd();
29
+
30
+ // Auto-install ruff if not available (it's one of the 4 auto-install tools)
31
+ if (!ruff.isAvailable(cwd)) {
32
+ const installed = await ensureTool("ruff");
33
+ if (!installed) {
34
+ return { status: "skipped", diagnostics: [], semantic: "none" };
35
+ }
69
36
  }
70
37
 
71
- // Run ruff check
72
- const args = ctx.autofix
73
- ? ["check", "--fix", ctx.filePath]
74
- : ["check", ctx.filePath];
38
+ // IMPORTANT: Never use --fix in dispatch runner to prevent infinite loops.
39
+ // Writing to the file would trigger another tool_result event, which would
40
+ // call dispatchLint again, creating a feedback loop.
41
+ // Fixes should be applied through explicit commands or user edits.
42
+ const args = ["check", ctx.filePath];
75
43
 
76
- const result = spawnSync(ruffCommand!, args, {
77
- encoding: "utf-8",
44
+ const result = safeSpawn(ruff.getCommand()!, args, {
78
45
  timeout: 30000,
79
- shell: true,
80
46
  });
81
47
 
82
48
  const raw = stripAnsi(result.stdout + result.stderr);
@@ -96,30 +62,4 @@ const ruffRunner: RunnerDefinition = {
96
62
  },
97
63
  };
98
64
 
99
- function parseRuffOutput(raw: string, filePath: string): Diagnostic[] {
100
- const lines = raw.split("\n").filter((l) => l.trim());
101
- const diagnostics: Diagnostic[] = [];
102
-
103
- for (const line of lines) {
104
- // Parse ruff output: file:line:col: message (code)
105
- const match = line.match(/^(.+?):(\d+):(\d+):\s*(.+?)\s+\((.+?)\)/);
106
- if (match) {
107
- diagnostics.push({
108
- id: `ruff-${match[2]}-${match[5]}`,
109
- message: `${match[5]}: ${match[4]}`,
110
- filePath,
111
- line: parseInt(match[2], 10),
112
- column: parseInt(match[3], 10),
113
- severity: line.includes("error") ? "error" : "warning",
114
- semantic: "warning",
115
- tool: "ruff",
116
- rule: match[5],
117
- fixable: true,
118
- });
119
- }
120
- }
121
-
122
- return diagnostics;
123
- }
124
-
125
65
  export default ruffRunner;