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,303 @@
1
+ /**
2
+ * Tests for oxlint runner
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import { createRequire } from "node:module";
7
+ import * as path from "node:path";
8
+ import { describe, expect, it } from "vitest";
9
+ import type { DispatchContext } from "../types.js";
10
+
11
+ /**
12
+ * Delay helper for Windows file cleanup
13
+ * Windows may hold file handles briefly after process exit
14
+ */
15
+ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
16
+
17
+ function createMockContext(
18
+ filePath: string,
19
+ overrides: Partial<DispatchContext> = {},
20
+ ): DispatchContext {
21
+ return {
22
+ filePath,
23
+ cwd: process.cwd(),
24
+ kind: "jsts" as any,
25
+ autofix: false,
26
+ deltaMode: false,
27
+ baselines: { get: () => [], add: () => {}, save: () => {} } as any,
28
+ pi: { getFlag: () => false, ...overrides.pi },
29
+ hasTool: async () => false,
30
+ log: () => {},
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe("oxlint runner", () => {
36
+ const require = createRequire(import.meta.url);
37
+
38
+ it("should have correct runner definition", async () => {
39
+ const oxlintModule = await import("./oxlint.js");
40
+ const runner = oxlintModule.default;
41
+
42
+ expect(runner.id).toBe("oxlint");
43
+ expect(runner.appliesTo).toEqual(["jsts"]);
44
+ expect(runner.priority).toBe(12);
45
+ expect(runner.enabledByDefault).toBe(false); // Opt-in initially
46
+ expect(runner.skipTestFiles).toBe(true);
47
+ });
48
+
49
+ it("should detect oxlint availability", () => {
50
+ const { spawnSync } =
51
+ require("node:child_process") as typeof import("node:child_process");
52
+ const result = spawnSync("oxlint", ["--version"], {
53
+ encoding: "utf-8",
54
+ timeout: 10000,
55
+ shell: true,
56
+ });
57
+ expect(
58
+ result.error || result.status !== 0 ? "not available" : "available",
59
+ ).toBeTruthy(); // May or may not be installed
60
+ });
61
+
62
+ it("should detect common lint issues", async () => {
63
+ const tmpFile = path.join(
64
+ process.env.TEMP || "/tmp",
65
+ `oxlint_test_${Date.now()}.ts`,
66
+ );
67
+ fs.writeFileSync(
68
+ tmpFile,
69
+ `// Test file with issues
70
+ function test() {
71
+ // Double negation
72
+ const flag = !!value;
73
+
74
+ // Unused variable
75
+ const unused = 42;
76
+
77
+ // Console statement
78
+ console.log("test");
79
+ }
80
+ `,
81
+ );
82
+
83
+ try {
84
+ const oxlintModule = await import("./oxlint.js");
85
+ const runner = oxlintModule.default;
86
+ const result = await runner.run(createMockContext(tmpFile));
87
+
88
+ // If oxlint is installed, should detect issues
89
+ // If not installed, will be skipped
90
+ if (result.status !== "skipped") {
91
+ // Should detect at least some issues (console, unused vars, etc.)
92
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
93
+ expect(
94
+ result.diagnostics.some(
95
+ (d) =>
96
+ d.tool === "oxlint" &&
97
+ (d.message.includes("console") ||
98
+ d.message.includes("unused") ||
99
+ d.message.includes("!!")),
100
+ ),
101
+ ).toBe(true);
102
+ }
103
+ } finally {
104
+ // Windows may hold file handles briefly - add small delay
105
+ await delay(100);
106
+ if (fs.existsSync(tmpFile)) {
107
+ fs.unlinkSync(tmpFile);
108
+ }
109
+ }
110
+ });
111
+
112
+ it("should respect no-oxlint flag", async () => {
113
+ const tmpFile = path.join(
114
+ process.env.TEMP || "/tmp",
115
+ `oxlint_flag_${Date.now()}.ts`,
116
+ );
117
+ fs.writeFileSync(
118
+ tmpFile,
119
+ `function test() { console.log("test"); }`,
120
+ );
121
+
122
+ try {
123
+ const oxlintModule = await import("./oxlint.js");
124
+ const runner = oxlintModule.default;
125
+
126
+ // Create context with no-oxlint flag set to true
127
+ const ctxWithFlag = createMockContext(tmpFile, {
128
+ pi: { getFlag: (name: string) => name === "no-oxlint" },
129
+ });
130
+
131
+ const result = await runner.run(ctxWithFlag);
132
+ expect(result.status).toBe("skipped");
133
+ } finally {
134
+ // Windows may hold file handles briefly - add small delay
135
+ await delay(100);
136
+ if (fs.existsSync(tmpFile)) {
137
+ fs.unlinkSync(tmpFile);
138
+ }
139
+ }
140
+ });
141
+
142
+ it("should provide fix suggestions when available", async () => {
143
+ const tmpFile = path.join(
144
+ process.env.TEMP || "/tmp",
145
+ `oxlint_fix_${Date.now()}.ts`,
146
+ );
147
+ fs.writeFileSync(
148
+ tmpFile,
149
+ `// File with auto-fixable issues
150
+ const x = !!value;
151
+ `,
152
+ );
153
+
154
+ try {
155
+ const oxlintModule = await import("./oxlint.js");
156
+ const runner = oxlintModule.default;
157
+ const result = await runner.run(createMockContext(tmpFile));
158
+
159
+ if (result.status !== "skipped" && result.diagnostics.length > 0) {
160
+ // Some issues should be fixable
161
+ const fixableDiags = result.diagnostics.filter((d) => d.fixable);
162
+ // At least some diagnostics should have fixes
163
+ expect(fixableDiags.length).toBeGreaterThanOrEqual(0);
164
+ }
165
+ } finally {
166
+ // Windows may hold file handles briefly - add small delay
167
+ await delay(100);
168
+ if (fs.existsSync(tmpFile)) {
169
+ fs.unlinkSync(tmpFile);
170
+ }
171
+ }
172
+ });
173
+
174
+ it("should pass clean TypeScript files", async () => {
175
+ const tmpFile = path.join(
176
+ process.env.TEMP || "/tmp",
177
+ `oxlint_ok_${Date.now()}.ts`,
178
+ );
179
+ fs.writeFileSync(
180
+ tmpFile,
181
+ `// Clean TypeScript file
182
+ function greet(name: string): string {
183
+ return \`Hello, \${name}!\`;
184
+ }
185
+
186
+ const result = greet("world");
187
+ export { greet };
188
+ `,
189
+ );
190
+
191
+ try {
192
+ const oxlintModule = await import("./oxlint.js");
193
+ const runner = oxlintModule.default;
194
+ const result = await runner.run(createMockContext(tmpFile));
195
+
196
+ if (result.status !== "skipped") {
197
+ // Clean files should have no issues
198
+ expect(result.diagnostics.length).toBe(0);
199
+ expect(result.status).toBe("succeeded");
200
+ }
201
+ } finally {
202
+ // Windows may hold file handles briefly - add small delay
203
+ await delay(100);
204
+ if (fs.existsSync(tmpFile)) {
205
+ fs.unlinkSync(tmpFile);
206
+ }
207
+ }
208
+ });
209
+
210
+ it("should handle JSON output correctly", async () => {
211
+ const tmpFile = path.join(
212
+ process.env.TEMP || "/tmp",
213
+ `oxlint_json_${Date.now()}.ts`,
214
+ );
215
+ fs.writeFileSync(
216
+ tmpFile,
217
+ `const unused = 1;`,
218
+ );
219
+
220
+ try {
221
+ const oxlintModule = await import("./oxlint.js");
222
+ const runner = oxlintModule.default;
223
+ const result = await runner.run(createMockContext(tmpFile));
224
+
225
+ if (result.status !== "skipped") {
226
+ // All diagnostics should have required fields
227
+ for (const diag of result.diagnostics) {
228
+ expect(diag.id).toBeDefined();
229
+ expect(diag.message).toBeDefined();
230
+ expect(diag.tool).toBe("oxlint");
231
+ expect(diag.line).toBeGreaterThanOrEqual(1);
232
+ expect(diag.severity).toMatch(/^(error|warning|info)$/);
233
+ }
234
+ }
235
+ } finally {
236
+ // Windows may hold file handles briefly - add small delay
237
+ await delay(100);
238
+ if (fs.existsSync(tmpFile)) {
239
+ fs.unlinkSync(tmpFile);
240
+ }
241
+ }
242
+ });
243
+
244
+ it("should skip when oxlint is not available", async () => {
245
+ const tmpFile = path.join(
246
+ process.env.TEMP || "/tmp",
247
+ `oxlint_skip_${Date.now()}.ts`,
248
+ );
249
+ fs.writeFileSync(tmpFile, `const x = 1;`);
250
+
251
+ try {
252
+ const oxlintModule = await import("./oxlint.js");
253
+ const runner = oxlintModule.default;
254
+
255
+ // Check if oxlint is available
256
+ const { spawnSync } =
257
+ require("node:child_process") as typeof import("node:child_process");
258
+ const checkResult = spawnSync("oxlint", ["--version"], {
259
+ encoding: "utf-8",
260
+ timeout: 5000,
261
+ shell: true,
262
+ });
263
+
264
+ const isAvailable = !checkResult.error && checkResult.status === 0;
265
+ const result = await runner.run(createMockContext(tmpFile));
266
+
267
+ if (!isAvailable) {
268
+ expect(result.status).toBe("skipped");
269
+ expect(result.diagnostics).toHaveLength(0);
270
+ }
271
+ } finally {
272
+ // Windows may hold file handles briefly - add small delay
273
+ await delay(100);
274
+ if (fs.existsSync(tmpFile)) {
275
+ fs.unlinkSync(tmpFile);
276
+ }
277
+ }
278
+ });
279
+
280
+ it("should handle parsing errors gracefully", async () => {
281
+ const tmpFile = path.join(
282
+ process.env.TEMP || "/tmp",
283
+ `oxlint_parse_${Date.now()}.ts`,
284
+ );
285
+ // Intentionally malformed file
286
+ fs.writeFileSync(tmpFile, `const x = `);
287
+
288
+ try {
289
+ const oxlintModule = await import("./oxlint.js");
290
+ const runner = oxlintModule.default;
291
+ const result = await runner.run(createMockContext(tmpFile));
292
+
293
+ // Should handle parse errors without crashing
294
+ expect(["succeeded", "failed", "skipped"]).toContain(result.status);
295
+ } finally {
296
+ // Windows may hold file handles briefly - add small delay
297
+ await delay(100);
298
+ if (fs.existsSync(tmpFile)) {
299
+ fs.unlinkSync(tmpFile);
300
+ }
301
+ }
302
+ });
303
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Oxlint runner for dispatch system
3
+ *
4
+ * Fast Rust-based JavaScript/TypeScript linter from the Oxc project.
5
+ * Zero-config by default, compatible with ESLint rules.
6
+ *
7
+ * Why oxlint?
8
+ * - ~100x faster than ESLint (Rust-based)
9
+ * - Zero-config (works out of the box)
10
+ * - Growing rule set (eslint, typescript, react, unicorn, etc.)
11
+ * - JSON output for programmatic use
12
+ *
13
+ * Comparison:
14
+ * - vs Biome: Similar performance, different rule philosophy
15
+ * - vs ESLint: Much faster, fewer rules but catching up
16
+ *
17
+ * Install: npm install -D oxlint
18
+ * Or: cargo install oxlint
19
+ *
20
+ * Config: .oxlintrc.json (optional, zero-config works)
21
+ */
22
+
23
+ import { safeSpawn } from "../../safe-spawn.js";
24
+ import { createAvailabilityChecker, createConfigFinder } from "./utils/runner-helpers.js";
25
+ import type {
26
+ Diagnostic,
27
+ DispatchContext,
28
+ RunnerDefinition,
29
+ RunnerResult,
30
+ } from "../types.js";
31
+
32
+ const oxlint = createAvailabilityChecker("oxlint", ".exe");
33
+ const findOxlintConfig = createConfigFinder(".oxlintrc.json");
34
+
35
+ /**
36
+ * Parse oxlint JSON output
37
+ *
38
+ * Format: Array of diagnostic objects
39
+ * [{
40
+ * "ruleId": "no-unused-vars",
41
+ * "severity": 2,
42
+ * "message": "'foo' is assigned a value but never used.",
43
+ * "line": 10,
44
+ * "column": 7,
45
+ * "nodeType": "Identifier",
46
+ * "messageId": "unusedVar",
47
+ * "endLine": 10,
48
+ * "endColumn": 10,
49
+ * "fix": { "range": [95, 108], "text": "" }
50
+ * }]
51
+ */
52
+ function parseOxlintOutput(raw: string, filePath: string): Diagnostic[] {
53
+ const diagnostics: Diagnostic[] = [];
54
+
55
+ if (!raw.trim()) {
56
+ return diagnostics;
57
+ }
58
+
59
+ try {
60
+ const parsed = JSON.parse(raw) as Array<{
61
+ ruleId?: string;
62
+ severity?: number; // 1 = warning, 2 = error
63
+ message?: string;
64
+ line?: number;
65
+ column?: number;
66
+ messageId?: string;
67
+ fix?: { range: number[]; text: string };
68
+ }>;
69
+
70
+ if (!Array.isArray(parsed)) {
71
+ return diagnostics;
72
+ }
73
+
74
+ for (const item of parsed) {
75
+ if (!item.message || !item.line) continue;
76
+
77
+ const severity = item.severity === 2 ? "error" : "warning";
78
+
79
+ diagnostics.push({
80
+ id: `oxlint-${item.line}-${item.ruleId || "unknown"}`,
81
+ message: item.message,
82
+ filePath,
83
+ line: item.line,
84
+ column: item.column || 1,
85
+ severity,
86
+ semantic: severity === "error" ? "blocking" : "warning",
87
+ tool: "oxlint",
88
+ rule: item.ruleId,
89
+ fixable: !!item.fix,
90
+ fixSuggestion: item.fix?.text,
91
+ });
92
+ }
93
+ } catch {
94
+ // If JSON parsing fails, try line-based parsing for CLI output
95
+ const lines = raw.split("\n").filter((l) => l.trim());
96
+ for (const line of lines) {
97
+ // Try to match: file.ts:10:7: Error message [ruleId]
98
+ const match = line.match(/^(\d+):(\d+)\s+(.+?)\s*\[(\w+)\]$/);
99
+ if (match) {
100
+ diagnostics.push({
101
+ id: `oxlint-${match[1]}-${match[4]}`,
102
+ message: `${match[4]}: ${match[3]}`,
103
+ filePath,
104
+ line: parseInt(match[1], 10),
105
+ column: parseInt(match[2], 10),
106
+ severity: "warning",
107
+ semantic: "warning",
108
+ tool: "oxlint",
109
+ rule: match[4],
110
+ });
111
+ }
112
+ }
113
+ }
114
+
115
+ return diagnostics;
116
+ }
117
+
118
+ const oxlintRunner: RunnerDefinition = {
119
+ id: "oxlint",
120
+ appliesTo: ["jsts"],
121
+ priority: 12, // Between biome (10) and slop (25)
122
+ enabledByDefault: false, // Opt-in initially - let users choose between biome/oxlint
123
+ skipTestFiles: true, // Test files often use patterns that trigger false positives
124
+
125
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
126
+ // Skip if oxlint is not installed
127
+ if (!oxlint.isAvailable(ctx.cwd || process.cwd())) {
128
+ return { status: "skipped", diagnostics: [], semantic: "none" };
129
+ }
130
+
131
+ // Check if user explicitly disabled oxlint (keep biome as primary)
132
+ if (ctx.pi.getFlag("no-oxlint")) {
133
+ return { status: "skipped", diagnostics: [], semantic: "none" };
134
+ }
135
+
136
+ // Build args
137
+ // --format json: JSON output
138
+ // --config: Only if config file exists (zero-config otherwise)
139
+ const args: string[] = ["--format", "json"];
140
+
141
+ // Check for config file
142
+ const configPath = findOxlintConfig(ctx.cwd);
143
+ if (configPath) {
144
+ args.push("--config", configPath);
145
+ }
146
+
147
+ // Add file path
148
+ args.push(ctx.filePath);
149
+
150
+ const result = safeSpawn(oxlint.getCommand()!, args, {
151
+ timeout: 10000, // Fast - should complete quickly
152
+ });
153
+
154
+ // oxlint exits with code 1 if issues found, 0 if clean
155
+ if (result.status === 0 && !result.stdout?.trim()) {
156
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
157
+ }
158
+
159
+ // Parse diagnostics
160
+ const raw = result.stdout + result.stderr;
161
+ const diagnostics = parseOxlintOutput(raw, ctx.filePath);
162
+
163
+ if (diagnostics.length === 0) {
164
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
165
+ }
166
+
167
+ return {
168
+ status: "failed",
169
+ diagnostics,
170
+ semantic: "warning",
171
+ };
172
+ },
173
+ };
174
+
175
+ export default oxlintRunner;
@@ -6,62 +6,27 @@
6
6
  *
7
7
  * Requires: pyright (pip install pyright or npm install -g pyright)
8
8
  */
9
- import { spawnSync } from "node:child_process";
10
- import * as fs from "node:fs";
11
- import * as path from "node:path";
12
- // Cache pyright availability check
13
- let pyrightAvailable = null;
14
- let pyrightCommand = null;
15
- /**
16
- * Find pyright command, checking venv first, then global.
17
- * Looks in .venv/bin, venv/bin (Unix), .venv/Scripts, venv/Scripts (Windows)
18
- */
19
- function findPyrightCommand(cwd) {
20
- // Check common venv locations
21
- const venvPaths = [
22
- ".venv/bin/pyright",
23
- "venv/bin/pyright",
24
- ".venv/Scripts/pyright.exe",
25
- "venv/Scripts/pyright.exe",
26
- ];
27
- for (const venvPath of venvPaths) {
28
- const fullPath = path.join(cwd, venvPath);
29
- if (fs.existsSync(fullPath)) {
30
- return `"${fullPath}"`; // Quote for Windows paths with spaces
31
- }
32
- }
33
- // Fall back to global
34
- return "pyright";
35
- }
36
- function isPyrightAvailable(cwd) {
37
- if (pyrightAvailable !== null)
38
- return pyrightAvailable;
39
- const command = findPyrightCommand(cwd || process.cwd());
40
- const check = spawnSync(command, ["--version"], {
41
- encoding: "utf-8",
42
- timeout: 5000,
43
- shell: true,
44
- });
45
- pyrightAvailable = !check.error && check.status === 0;
46
- if (pyrightAvailable)
47
- pyrightCommand = command;
48
- return pyrightAvailable;
49
- }
9
+ import { ensureTool } from "../../installer/index.js";
10
+ import { safeSpawn } from "../../safe-spawn.js";
11
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
12
+ const pyright = createAvailabilityChecker("pyright", ".exe");
50
13
  const pyrightRunner = {
51
14
  id: "pyright",
52
15
  appliesTo: ["python"],
53
16
  priority: 5, // Higher priority than ruff (10) - type errors are more important
54
17
  enabledByDefault: true,
55
18
  async run(ctx) {
56
- // Skip if pyright is not installed
57
- if (!isPyrightAvailable(ctx.cwd || process.cwd())) {
58
- return { status: "skipped", diagnostics: [], semantic: "none" };
19
+ const cwd = ctx.cwd || process.cwd();
20
+ // Auto-install pyright if not available (it's one of the 4 auto-install tools)
21
+ if (!pyright.isAvailable(cwd)) {
22
+ const installed = await ensureTool("pyright");
23
+ if (!installed) {
24
+ return { status: "skipped", diagnostics: [], semantic: "none" };
25
+ }
59
26
  }
60
27
  // Run pyright with JSON output (use venv-local or global command)
61
- const result = spawnSync(pyrightCommand, ["--outputjson", ctx.filePath], {
62
- encoding: "utf-8",
28
+ const result = safeSpawn(pyright.getCommand(), ["--outputjson", ctx.filePath], {
63
29
  timeout: 60000,
64
- shell: true,
65
30
  });
66
31
  // Pyright returns non-zero when errors found, that's OK
67
32
  if (result.error) {
@@ -81,34 +46,39 @@ const pyrightRunner = {
81
46
  return {
82
47
  status: hasErrors ? "failed" : "succeeded",
83
48
  diagnostics,
84
- semantic: "warning",
49
+ semantic: hasErrors ? "blocking" : "warning",
85
50
  };
86
51
  }
87
52
  catch {
88
- // JSON parse failed, skip
89
- return { status: "skipped", diagnostics: [], semantic: "none" };
53
+ // JSON parse error
54
+ return {
55
+ status: "failed",
56
+ diagnostics: [],
57
+ semantic: "none",
58
+ rawOutput: output.slice(0, 500),
59
+ };
90
60
  }
91
61
  },
92
62
  };
93
- function parsePyrightOutput(data, filePath) {
94
- if (!data.generalDiagnostics)
95
- return [];
96
- return data.generalDiagnostics
97
- .filter((d) => {
98
- // Only include errors and warnings, skip informational
99
- return d.severity === "error" || d.severity === "warning";
100
- })
101
- .map((d) => ({
102
- id: `pyright-${d.range.start.line}-${d.rule}`,
103
- message: d.message.split("\n")[0], // First line only (pyright has multi-line messages)
104
- filePath,
105
- line: d.range.start.line + 1, // Pyright is 0-indexed, we're 1-indexed
106
- column: d.range.start.character + 1,
107
- severity: d.severity === "error" ? "error" : "warning",
108
- semantic: d.severity === "error" ? "blocking" : "warning",
109
- tool: "pyright",
110
- rule: d.rule,
111
- fixable: false, // Pyright can't auto-fix, only suggest
112
- }));
63
+ function parsePyrightOutput(data, _filePath) {
64
+ const diagnostics = [];
65
+ // Pyright JSON output has generalDiagnostics array
66
+ const generalDiags = data.generalDiagnostics || [];
67
+ for (const diag of generalDiags) {
68
+ // Skip if not for this file (pyright may output diagnostics for imports)
69
+ // For now, include all - caller will filter if needed
70
+ diagnostics.push({
71
+ id: `pyright-${diag.rule || diag.start?.line || "unknown"}`,
72
+ message: diag.message || "Type error",
73
+ filePath: diag.file || _filePath,
74
+ line: diag.start?.line || 0,
75
+ column: diag.start?.column || 0,
76
+ severity: diag.severity === "error" ? "error" : "warning",
77
+ semantic: diag.severity === "error" ? "blocking" : "warning",
78
+ tool: "pyright",
79
+ rule: diag.rule,
80
+ });
81
+ }
82
+ return diagnostics;
113
83
  }
114
84
  export default pyrightRunner;
@@ -55,7 +55,14 @@ greet(123)
55
55
  expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
56
56
  }
57
57
  finally {
58
- fs.unlinkSync(tmpFile);
58
+ try {
59
+ if (fs.existsSync(tmpFile)) {
60
+ fs.unlinkSync(tmpFile);
61
+ }
62
+ }
63
+ catch {
64
+ // Ignore cleanup errors
65
+ }
59
66
  }
60
67
  });
61
68
  it("should pass valid Python files", async () => {
@@ -78,7 +85,14 @@ greet("world")
78
85
  expect(result.diagnostics.length).toBe(0);
79
86
  }
80
87
  finally {
81
- fs.unlinkSync(tmpFile);
88
+ try {
89
+ if (fs.existsSync(tmpFile)) {
90
+ fs.unlinkSync(tmpFile);
91
+ }
92
+ }
93
+ catch {
94
+ // Ignore cleanup errors
95
+ }
82
96
  }
83
97
  });
84
98
  });
@@ -72,7 +72,13 @@ greet(123)
72
72
  expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
73
73
  expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
74
74
  } finally {
75
- fs.unlinkSync(tmpFile);
75
+ try {
76
+ if (fs.existsSync(tmpFile)) {
77
+ fs.unlinkSync(tmpFile);
78
+ }
79
+ } catch {
80
+ // Ignore cleanup errors
81
+ }
76
82
  }
77
83
  });
78
84
 
@@ -103,7 +109,13 @@ greet("world")
103
109
  expect(result.status).toBe("succeeded");
104
110
  expect(result.diagnostics.length).toBe(0);
105
111
  } finally {
106
- fs.unlinkSync(tmpFile);
112
+ try {
113
+ if (fs.existsSync(tmpFile)) {
114
+ fs.unlinkSync(tmpFile);
115
+ }
116
+ } catch {
117
+ // Ignore cleanup errors
118
+ }
107
119
  }
108
120
  });
109
121
  });