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,230 @@
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: "jsts" 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("ts-slop runner", () => {
33
+ const require = createRequire(import.meta.url);
34
+
35
+ it("should have correct runner definition", async () => {
36
+ const slopModule = await import("./ts-slop.js");
37
+ const runner = slopModule.default;
38
+
39
+ expect(runner.id).toBe("ts-slop");
40
+ // NOTE: TS/JS slop is now handled by ast-grep-napi
41
+ // This CLI runner is disabled by default as fallback
42
+ expect(runner.appliesTo).toEqual([]); // Disabled - use ast-grep-napi
43
+ expect(runner.priority).toBe(20);
44
+ expect(runner.enabledByDefault).toBe(false);
45
+ expect(runner.skipTestFiles).toBe(true);
46
+ });
47
+
48
+ it("should detect ast-grep availability", () => {
49
+ const { spawnSync } =
50
+ require("node:child_process") as typeof import("node:child_process");
51
+ const result = spawnSync("npx", ["sg", "--version"], {
52
+ encoding: "utf-8",
53
+ timeout: 10000,
54
+ shell: true,
55
+ });
56
+ expect(
57
+ result.error || result.status !== 0 ? "not available" : "available",
58
+ ).toBe("available");
59
+ });
60
+
61
+ it("should detect for-index-length pattern (or other slop)", async () => {
62
+ const tmpFile = path.join(
63
+ process.env.TEMP || "/tmp",
64
+ `ts_slop_test_for_${Date.now()}.ts`,
65
+ );
66
+ fs.writeFileSync(
67
+ tmpFile,
68
+ `// Slop: using index loop instead of for-of
69
+ function processItems(items: string[]) {
70
+ for (let i = 0; i < items.length; i++) {
71
+ console.log(items[i]);
72
+ }
73
+ }
74
+ `,
75
+ );
76
+
77
+ try {
78
+ const slopModule = await import("./ts-slop.js");
79
+ const runner = slopModule.default;
80
+ const result = await runner.run(createMockContext(tmpFile));
81
+
82
+ // Should detect at least some slop patterns
83
+ // (specific patterns may vary based on ast-grep rule accuracy)
84
+ expect(result.status).not.toBe("skipped");
85
+ } finally {
86
+ safeUnlink(tmpFile);
87
+ }
88
+ });
89
+
90
+ it("should detect manual Math min/max pattern (or other slop)", async () => {
91
+ const tmpFile = path.join(
92
+ process.env.TEMP || "/tmp",
93
+ `ts_slop_test_minmax_${Date.now()}.ts`,
94
+ );
95
+ fs.writeFileSync(
96
+ tmpFile,
97
+ `// Slop: manual min/max instead of Math
98
+ function getMax(a: number, b: number): number {
99
+ if (a > b) {
100
+ const m = a;
101
+ } else {
102
+ const m = b;
103
+ }
104
+ return m;
105
+ }
106
+ `,
107
+ );
108
+
109
+ try {
110
+ const slopModule = await import("./ts-slop.js");
111
+ const runner = slopModule.default;
112
+ const result = await runner.run(createMockContext(tmpFile));
113
+
114
+ // Should detect at least some slop patterns
115
+ // (specific patterns may vary based on ast-grep rule accuracy)
116
+ expect(result.status).not.toBe("skipped");
117
+ } finally {
118
+ safeUnlink(tmpFile);
119
+ }
120
+ });
121
+
122
+ it("should detect indexOf !== -1 pattern (or other slop)", async () => {
123
+ const tmpFile = path.join(
124
+ process.env.TEMP || "/tmp",
125
+ `ts_slop_test_indexof_${Date.now()}.ts`,
126
+ );
127
+ fs.writeFileSync(
128
+ tmpFile,
129
+ `// Slop: indexOf check instead of includes
130
+ function hasItem(arr: string[], item: string): boolean {
131
+ if (arr.indexOf(item) !== -1) {
132
+ return true;
133
+ }
134
+ return false;
135
+ }
136
+ `,
137
+ );
138
+
139
+ try {
140
+ const slopModule = await import("./ts-slop.js");
141
+ const runner = slopModule.default;
142
+ const result = await runner.run(createMockContext(tmpFile));
143
+
144
+ // Should detect at least some slop patterns
145
+ // (specific patterns may vary based on ast-grep rule accuracy)
146
+ expect(result.status).not.toBe("skipped");
147
+ } finally {
148
+ safeUnlink(tmpFile);
149
+ }
150
+ });
151
+
152
+ it("should detect array length > 0 pattern", async () => {
153
+ const tmpFile = path.join(
154
+ process.env.TEMP || "/tmp",
155
+ `ts_slop_test_length_${Date.now()}.ts`,
156
+ );
157
+ fs.writeFileSync(
158
+ tmpFile,
159
+ `// Slop: length check instead of truthiness
160
+ function processItems(arr: string[]): void {
161
+ if (arr.length > 0) {
162
+ console.log("has items");
163
+ }
164
+ }
165
+ `,
166
+ );
167
+
168
+ try {
169
+ const slopModule = await import("./ts-slop.js");
170
+ const runner = slopModule.default;
171
+ const result = await runner.run(createMockContext(tmpFile));
172
+
173
+ // This pattern may or may not be detected depending on rule specificity
174
+ // Just verify the scan ran without errors
175
+ expect(result.status).toBe("succeeded");
176
+ } finally {
177
+ safeUnlink(tmpFile);
178
+ }
179
+ });
180
+
181
+ it("should pass clean TypeScript files", async () => {
182
+ const tmpFile = path.join(
183
+ process.env.TEMP || "/tmp",
184
+ `ts_slop_test_ok_${Date.now()}.ts`,
185
+ );
186
+ fs.writeFileSync(
187
+ tmpFile,
188
+ `// Clean TypeScript code
189
+ function processItems(items: string[]): void {
190
+ for (const item of items) {
191
+ console.log(item);
192
+ }
193
+ }
194
+
195
+ function findMax(a: number, b: number): number {
196
+ return Math.max(a, b);
197
+ }
198
+
199
+ function contains(arr: string[], item: string): boolean {
200
+ return arr.includes(item);
201
+ }
202
+
203
+ function hasItems(arr: string[]): boolean {
204
+ return arr.length > 0; // This is actually OK, but let's see
205
+ }
206
+ `,
207
+ );
208
+
209
+ try {
210
+ const slopModule = await import("./ts-slop.js");
211
+ const runner = slopModule.default;
212
+ const result = await runner.run(createMockContext(tmpFile));
213
+
214
+ // Should have minimal or no slop issues for clean code
215
+ const slopIssues = result.diagnostics.filter(
216
+ (d) => d.tool === "ts-slop",
217
+ );
218
+ // Allow for minor issues - the length check might still trigger
219
+ expect(slopIssues.length).toBeLessThanOrEqual(1);
220
+ } finally {
221
+ try {
222
+ if (fs.existsSync(tmpFile)) {
223
+ safeUnlink(tmpFile);
224
+ }
225
+ } catch {
226
+ // Ignore cleanup errors
227
+ }
228
+ }
229
+ });
230
+ });
@@ -0,0 +1,142 @@
1
+ /**
2
+ * TypeScript Slop runner for dispatch system
3
+ *
4
+ * Detects "slop" patterns in TypeScript/JavaScript 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 patterns adapted for TypeScript
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("ts-slop-rules");
27
+
28
+ const tsSlopRunner: RunnerDefinition = {
29
+ id: "ts-slop",
30
+ // NOTE: TypeScript/JavaScript slop detection is now handled by ast-grep-napi
31
+ // This CLI runner is kept as fallback for edge cases but disabled by default
32
+ appliesTo: [], // Disabled - use ast-grep-napi instead
33
+ priority: 20,
34
+ enabledByDefault: false,
35
+ skipTestFiles: true,
36
+
37
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
38
+ // Check if ast-grep is available
39
+ if (!isSgAvailable()) {
40
+ return { status: "skipped", diagnostics: [], semantic: "none" };
41
+ }
42
+
43
+ // Find slop config
44
+ const configPath = findSlopConfig(ctx.cwd);
45
+ if (!configPath) {
46
+ return { status: "skipped", diagnostics: [], semantic: "none" };
47
+ }
48
+
49
+ // Run ast-grep scan
50
+ const args = ["sg", "scan", "--config", configPath, "--json", ctx.filePath];
51
+
52
+ const result = safeSpawn("npx", args, {
53
+ timeout: 30000,
54
+ });
55
+
56
+ const raw = result.stdout + result.stderr;
57
+
58
+ if (result.status === 0 && !raw.trim()) {
59
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
60
+ }
61
+
62
+ // Parse results
63
+ const diagnostics = parseSlopOutput(raw, ctx.filePath);
64
+
65
+ if (diagnostics.length === 0) {
66
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
67
+ }
68
+
69
+ return {
70
+ status: "failed",
71
+ diagnostics,
72
+ semantic: "warning",
73
+ };
74
+ },
75
+ };
76
+
77
+ function parseSlopOutput(raw: string, filePath: string): Diagnostic[] {
78
+ const diagnostics: Diagnostic[] = [];
79
+
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ if (Array.isArray(parsed)) {
83
+ for (const item of parsed) {
84
+ const line = item.range?.start?.line || 1;
85
+ const ruleId = item.rule || "unknown";
86
+ const message = item.message || "";
87
+
88
+ // Categorize by severity based on weight from metadata
89
+ const weight = item.metadata?.weight || 3;
90
+ const severity = weight >= 4 ? "error" : "warning";
91
+ const category = item.metadata?.category || "slop";
92
+
93
+ // Add slop category indicator to message
94
+ let enhancedMessage = `[${category}] ${message}`;
95
+ if (item.replacement) {
96
+ const preview =
97
+ item.replacement.length > 40
98
+ ? `${item.replacement.substring(0, 40)}...`
99
+ : item.replacement;
100
+ enhancedMessage += `\n💡 Suggested fix: → "${preview}"`;
101
+ }
102
+
103
+ diagnostics.push({
104
+ id: `ts-slop-${line}-${ruleId}`,
105
+ message: enhancedMessage,
106
+ filePath,
107
+ line,
108
+ column: item.range?.start?.column || 0,
109
+ severity,
110
+ semantic: severity === "error" ? "blocking" : "warning",
111
+ tool: "ts-slop",
112
+ rule: ruleId,
113
+ fixable: !!item.replacement,
114
+ fixSuggestion: item.replacement,
115
+ });
116
+ }
117
+ }
118
+ } catch {
119
+ // JSON parse failed, try line-by-line
120
+ const lines = raw.split("\n");
121
+ for (const line of lines) {
122
+ if (line.includes(":") && line.includes("L")) {
123
+ const match = line.match(/L(\d+):?\s*(.+)/);
124
+ if (match) {
125
+ diagnostics.push({
126
+ id: `ts-slop-${match[1]}-line`,
127
+ message: `[slop] ${match[2].trim()}`,
128
+ filePath,
129
+ line: parseInt(match[1], 10),
130
+ severity: "warning",
131
+ semantic: "warning",
132
+ tool: "ts-slop",
133
+ });
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ return diagnostics;
140
+ }
141
+
142
+ export default tsSlopRunner;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Shared diagnostic output parsers for pi-lens runners
3
+ *
4
+ * Common patterns for parsing tool output into standardized diagnostics.
5
+ * Supports the common `file:line:col: message` format used by most linters.
6
+ */
7
+ /**
8
+ * Create a parser for line-based tool output.
9
+ * Common format: file:line:col: message (with variations)
10
+ */
11
+ export function createLineParser(config) {
12
+ return (raw, filePath) => {
13
+ const diagnostics = [];
14
+ // Optionally strip ANSI codes (for tools that output colored text)
15
+ const clean = config.stripAnsi !== false
16
+ ? raw.replace(/\x1b\[[0-9;]*m/g, "")
17
+ : raw;
18
+ const lines = clean.split("\n").filter((l) => l.trim());
19
+ for (const line of lines) {
20
+ const match = line.match(config.regex);
21
+ if (!match)
22
+ continue;
23
+ const lineNum = parseInt(match[2], 10);
24
+ const colNum = parseInt(match[3], 10);
25
+ const severity = config.getSeverity
26
+ ? config.getSeverity(line, match)
27
+ : "warning";
28
+ const fixable = typeof config.fixable === "function"
29
+ ? config.fixable(match)
30
+ : config.fixable ?? false;
31
+ diagnostics.push({
32
+ id: config.generateId(match),
33
+ message: config.extractMessage(match),
34
+ filePath,
35
+ line: lineNum,
36
+ column: colNum,
37
+ severity,
38
+ semantic: severity === "error" ? "blocking" : "warning",
39
+ tool: config.tool,
40
+ rule: config.extractRule?.(match),
41
+ fixable,
42
+ });
43
+ }
44
+ return diagnostics;
45
+ };
46
+ }
47
+ // =============================================================================
48
+ // PRE-BUILT PARSERS FOR COMMON TOOLS
49
+ // =============================================================================
50
+ /**
51
+ * Parse Ruff output: file:line:col: CODE message
52
+ */
53
+ export const parseRuffOutput = createLineParser({
54
+ tool: "ruff",
55
+ regex: /^(.+?):(\d+):(\d+):\s*(\w+)\s*(.+)/,
56
+ extractMessage: (m) => `${m[4]}: ${m[5]}`, // CODE: message
57
+ extractRule: (m) => m[4],
58
+ generateId: (m) => `ruff-${m[4]}`,
59
+ fixable: true, // Ruff can fix most issues
60
+ });
61
+ /**
62
+ * Parse Go vet output: file:line:col: message
63
+ */
64
+ export const parseGoVetOutput = createLineParser({
65
+ tool: "go-vet",
66
+ regex: /^(.+?):(\d+):(\d+):\s*(.+)/,
67
+ extractMessage: (m) => m[4],
68
+ generateId: (m) => `go-vet-${m[2]}`,
69
+ });
70
+ /**
71
+ * Parse Biome output: file:line:col message (category)
72
+ * With autofix support for fix suggestions
73
+ */
74
+ export function createBiomeParser(autofix = false) {
75
+ return createLineParser({
76
+ tool: "biome",
77
+ regex: /^(.+?):(\d+):(\d+)\s+(.+?)\s*\((.+?)\)/,
78
+ extractMessage: (m) => `${m[5]}: ${m[4]}`, // category: message
79
+ extractRule: (m) => m[5],
80
+ generateId: (m) => `biome-${m[2]}-${m[5]}`,
81
+ getSeverity: (line) => (line.includes("error") ? "error" : "warning"),
82
+ fixable: true,
83
+ });
84
+ }
85
+ // Backward-compatible default biome parser
86
+ export const parseBiomeOutput = createBiomeParser(false);
87
+ // =============================================================================
88
+ // GENERIC PARSER FACTORY
89
+ // =============================================================================
90
+ /**
91
+ * Create a simple parser for tools using standard file:line:col format.
92
+ * Format variations: :line:col:, line:col, (line,col), etc.
93
+ */
94
+ export function createSimpleParser(tool, options = {}) {
95
+ const sep = options.separator ?? ":";
96
+ const severity = options.severity ?? "warning";
97
+ const fixable = options.fixable ?? false;
98
+ // Build regex based on separator type
99
+ const escapedSep = sep === " " ? "\\s+" : escapeRegExp(sep);
100
+ const regex = options.includesFileName
101
+ ? new RegExp(`^(.+?)${escapedSep}(\\d+)${escapedSep}(\\d+)${escapedSep}(.+)`)
102
+ : new RegExp(`^(\\d+)${escapedSep}(\\d+)${escapedSep}(.+)`);
103
+ return (raw, filePath) => {
104
+ const diagnostics = [];
105
+ const lines = raw.split("\n").filter((l) => l.trim());
106
+ for (const line of lines) {
107
+ const match = line.match(regex);
108
+ if (!match)
109
+ continue;
110
+ const lineNum = options.includesFileName
111
+ ? parseInt(match[2], 10)
112
+ : parseInt(match[1], 10);
113
+ const colNum = options.includesFileName
114
+ ? parseInt(match[3], 10)
115
+ : parseInt(match[2], 10);
116
+ const message = options.includesFileName ? match[4] : match[3];
117
+ diagnostics.push({
118
+ id: `${tool}-${lineNum}`,
119
+ message: message.trim(),
120
+ filePath,
121
+ line: lineNum,
122
+ column: colNum,
123
+ severity,
124
+ semantic: severity === "error" ? "blocking" : "warning",
125
+ tool,
126
+ fixable,
127
+ });
128
+ }
129
+ return diagnostics;
130
+ };
131
+ }
132
+ function escapeRegExp(string) {
133
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
134
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Shared diagnostic output parsers for pi-lens runners
3
+ *
4
+ * Common patterns for parsing tool output into standardized diagnostics.
5
+ * Supports the common `file:line:col: message` format used by most linters.
6
+ */
7
+
8
+ import type { Diagnostic } from "../../types.js";
9
+
10
+ export interface LineParserConfig {
11
+ /** Tool name for diagnostic identification */
12
+ tool: string;
13
+ /** Regex pattern to match lines. Must capture: [fullMatch, file?, line?, col?, ...messageParts] */
14
+ regex: RegExp;
15
+ /** Extract message from regex match groups */
16
+ extractMessage: (match: RegExpMatchArray) => string;
17
+ /** Extract rule/code from regex match groups (optional) */
18
+ extractRule?: (match: RegExpMatchArray) => string | undefined;
19
+ /** Generate diagnostic ID from match */
20
+ generateId: (match: RegExpMatchArray) => string;
21
+ /** Determine severity from line content or match (defaults to warning) */
22
+ getSeverity?: (line: string, match: RegExpMatchArray) => "error" | "warning" | "info";
23
+ /** Whether this diagnostic is fixable (defaults to false) */
24
+ fixable?: boolean | ((match: RegExpMatchArray) => boolean);
25
+ /** Strip ANSI escape codes before parsing (defaults to true) */
26
+ stripAnsi?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Create a parser for line-based tool output.
31
+ * Common format: file:line:col: message (with variations)
32
+ */
33
+ export function createLineParser(config: LineParserConfig) {
34
+ return (raw: string, filePath: string): Diagnostic[] => {
35
+ const diagnostics: Diagnostic[] = [];
36
+
37
+ // Optionally strip ANSI codes (for tools that output colored text)
38
+ const clean =
39
+ config.stripAnsi !== false
40
+ ? raw.replace(/\x1b\[[0-9;]*m/g, "")
41
+ : raw;
42
+
43
+ const lines = clean.split("\n").filter((l) => l.trim());
44
+
45
+ for (const line of lines) {
46
+ const match = line.match(config.regex);
47
+ if (!match) continue;
48
+
49
+ const lineNum = parseInt(match[2], 10);
50
+ const colNum = parseInt(match[3], 10);
51
+
52
+ const severity = config.getSeverity
53
+ ? config.getSeverity(line, match)
54
+ : "warning";
55
+
56
+ const fixable =
57
+ typeof config.fixable === "function"
58
+ ? config.fixable(match)
59
+ : config.fixable ?? false;
60
+
61
+ diagnostics.push({
62
+ id: config.generateId(match),
63
+ message: config.extractMessage(match),
64
+ filePath,
65
+ line: lineNum,
66
+ column: colNum,
67
+ severity,
68
+ semantic: severity === "error" ? "blocking" : "warning",
69
+ tool: config.tool,
70
+ rule: config.extractRule?.(match),
71
+ fixable,
72
+ });
73
+ }
74
+
75
+ return diagnostics;
76
+ };
77
+ }
78
+
79
+ // =============================================================================
80
+ // PRE-BUILT PARSERS FOR COMMON TOOLS
81
+ // =============================================================================
82
+
83
+ /**
84
+ * Parse Ruff output: file:line:col: CODE message
85
+ */
86
+ export const parseRuffOutput = createLineParser({
87
+ tool: "ruff",
88
+ regex: /^(.+?):(\d+):(\d+):\s*(\w+)\s*(.+)/,
89
+ extractMessage: (m) => `${m[4]}: ${m[5]}`, // CODE: message
90
+ extractRule: (m) => m[4],
91
+ generateId: (m) => `ruff-${m[4]}`,
92
+ fixable: true, // Ruff can fix most issues
93
+ });
94
+
95
+ /**
96
+ * Parse Go vet output: file:line:col: message
97
+ */
98
+ export const parseGoVetOutput = createLineParser({
99
+ tool: "go-vet",
100
+ regex: /^(.+?):(\d+):(\d+):\s*(.+)/,
101
+ extractMessage: (m) => m[4],
102
+ generateId: (m) => `go-vet-${m[2]}`,
103
+ });
104
+
105
+ /**
106
+ * Parse Biome output: file:line:col message (category)
107
+ * With autofix support for fix suggestions
108
+ */
109
+ export function createBiomeParser(autofix: boolean = false) {
110
+ return createLineParser({
111
+ tool: "biome",
112
+ regex: /^(.+?):(\d+):(\d+)\s+(.+?)\s*\((.+?)\)/,
113
+ extractMessage: (m) => `${m[5]}: ${m[4]}`, // category: message
114
+ extractRule: (m) => m[5],
115
+ generateId: (m) => `biome-${m[2]}-${m[5]}`,
116
+ getSeverity: (line) => (line.includes("error") ? "error" : "warning"),
117
+ fixable: true,
118
+ });
119
+ }
120
+
121
+ // Backward-compatible default biome parser
122
+ export const parseBiomeOutput = createBiomeParser(false);
123
+
124
+ // =============================================================================
125
+ // GENERIC PARSER FACTORY
126
+ // =============================================================================
127
+
128
+ /**
129
+ * Create a simple parser for tools using standard file:line:col format.
130
+ * Format variations: :line:col:, line:col, (line,col), etc.
131
+ */
132
+ export function createSimpleParser(
133
+ tool: string,
134
+ options: {
135
+ separator?: ":" | " " | ",";
136
+ includesFileName?: boolean;
137
+ severity?: "error" | "warning" | "info";
138
+ fixable?: boolean;
139
+ } = {},
140
+ ): (raw: string, filePath: string) => Diagnostic[] {
141
+ const sep = options.separator ?? ":";
142
+ const severity = options.severity ?? "warning";
143
+ const fixable = options.fixable ?? false;
144
+
145
+ // Build regex based on separator type
146
+ const escapedSep = sep === " " ? "\\s+" : escapeRegExp(sep);
147
+ const regex = options.includesFileName
148
+ ? new RegExp(`^(.+?)${escapedSep}(\\d+)${escapedSep}(\\d+)${escapedSep}(.+)`)
149
+ : new RegExp(`^(\\d+)${escapedSep}(\\d+)${escapedSep}(.+)`);
150
+
151
+ return (raw: string, filePath: string): Diagnostic[] => {
152
+ const diagnostics: Diagnostic[] = [];
153
+ const lines = raw.split("\n").filter((l) => l.trim());
154
+
155
+ for (const line of lines) {
156
+ const match = line.match(regex);
157
+ if (!match) continue;
158
+
159
+ const lineNum = options.includesFileName
160
+ ? parseInt(match[2], 10)
161
+ : parseInt(match[1], 10);
162
+ const colNum = options.includesFileName
163
+ ? parseInt(match[3], 10)
164
+ : parseInt(match[2], 10);
165
+ const message = options.includesFileName ? match[4] : match[3];
166
+
167
+ diagnostics.push({
168
+ id: `${tool}-${lineNum}`,
169
+ message: message.trim(),
170
+ filePath,
171
+ line: lineNum,
172
+ column: colNum,
173
+ severity,
174
+ semantic: severity === "error" ? "blocking" : "warning",
175
+ tool,
176
+ fixable,
177
+ });
178
+ }
179
+
180
+ return diagnostics;
181
+ };
182
+ }
183
+
184
+ function escapeRegExp(string: string): string {
185
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
186
+ }