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
@@ -7,58 +7,17 @@
7
7
  * Requires: pyright (pip install pyright or npm install -g pyright)
8
8
  */
9
9
 
10
- import { spawnSync } from "node:child_process";
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
10
+ import { ensureTool } from "../../installer/index.js";
11
+ import { safeSpawn } from "../../safe-spawn.js";
13
12
  import type {
14
13
  Diagnostic,
15
14
  DispatchContext,
16
15
  RunnerDefinition,
17
16
  RunnerResult,
18
17
  } from "../types.js";
18
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
19
19
 
20
- // Cache pyright availability check
21
- let pyrightAvailable: boolean | null = null;
22
- let pyrightCommand: string | null = null;
23
-
24
- /**
25
- * Find pyright command, checking venv first, then global.
26
- * Looks in .venv/bin, venv/bin (Unix), .venv/Scripts, venv/Scripts (Windows)
27
- */
28
- function findPyrightCommand(cwd: string): string {
29
- // Check common venv locations
30
- const venvPaths = [
31
- ".venv/bin/pyright",
32
- "venv/bin/pyright",
33
- ".venv/Scripts/pyright.exe",
34
- "venv/Scripts/pyright.exe",
35
- ];
36
-
37
- for (const venvPath of venvPaths) {
38
- const fullPath = path.join(cwd, venvPath);
39
- if (fs.existsSync(fullPath)) {
40
- return `"${fullPath}"`; // Quote for Windows paths with spaces
41
- }
42
- }
43
-
44
- // Fall back to global
45
- return "pyright";
46
- }
47
-
48
- function isPyrightAvailable(cwd?: string): boolean {
49
- if (pyrightAvailable !== null) return pyrightAvailable;
50
-
51
- const command = findPyrightCommand(cwd || process.cwd());
52
-
53
- const check = spawnSync(command, ["--version"], {
54
- encoding: "utf-8",
55
- timeout: 5000,
56
- shell: true,
57
- });
58
- pyrightAvailable = !check.error && check.status === 0;
59
- if (pyrightAvailable) pyrightCommand = command;
60
- return pyrightAvailable;
61
- }
20
+ const pyright = createAvailabilityChecker("pyright", ".exe");
62
21
 
63
22
  const pyrightRunner: RunnerDefinition = {
64
23
  id: "pyright",
@@ -67,17 +26,24 @@ const pyrightRunner: RunnerDefinition = {
67
26
  enabledByDefault: true,
68
27
 
69
28
  async run(ctx: DispatchContext): Promise<RunnerResult> {
70
- // Skip if pyright is not installed
71
- if (!isPyrightAvailable(ctx.cwd || process.cwd())) {
72
- return { status: "skipped", diagnostics: [], semantic: "none" };
29
+ const cwd = ctx.cwd || process.cwd();
30
+
31
+ // Auto-install pyright if not available (it's one of the 4 auto-install tools)
32
+ if (!pyright.isAvailable(cwd)) {
33
+ const installed = await ensureTool("pyright");
34
+ if (!installed) {
35
+ return { status: "skipped", diagnostics: [], semantic: "none" };
36
+ }
73
37
  }
74
38
 
75
39
  // Run pyright with JSON output (use venv-local or global command)
76
- const result = spawnSync(pyrightCommand!, ["--outputjson", ctx.filePath], {
77
- encoding: "utf-8",
78
- timeout: 60000,
79
- shell: true,
80
- });
40
+ const result = safeSpawn(
41
+ pyright.getCommand()!,
42
+ ["--outputjson", ctx.filePath],
43
+ {
44
+ timeout: 60000,
45
+ },
46
+ );
81
47
 
82
48
  // Pyright returns non-zero when errors found, that's OK
83
49
  if (result.error) {
@@ -102,53 +68,44 @@ const pyrightRunner: RunnerDefinition = {
102
68
  return {
103
69
  status: hasErrors ? "failed" : "succeeded",
104
70
  diagnostics,
105
- semantic: "warning",
71
+ semantic: hasErrors ? "blocking" : "warning",
106
72
  };
107
73
  } catch {
108
- // JSON parse failed, skip
109
- return { status: "skipped", diagnostics: [], semantic: "none" };
74
+ // JSON parse error
75
+ return {
76
+ status: "failed",
77
+ diagnostics: [],
78
+ semantic: "none",
79
+ rawOutput: output.slice(0, 500),
80
+ };
110
81
  }
111
82
  },
112
83
  };
113
84
 
114
- interface PyrightDiagnostic {
115
- file: string;
116
- severity: "error" | "warning" | "information";
117
- message: string;
118
- range: {
119
- start: { line: number; character: number };
120
- end: { line: number; character: number };
121
- };
122
- rule: string;
123
- }
85
+ function parsePyrightOutput(data: any, _filePath: string): Diagnostic[] {
86
+ const diagnostics: Diagnostic[] = [];
124
87
 
125
- interface PyrightResult {
126
- generalDiagnostics: PyrightDiagnostic[];
127
- }
88
+ // Pyright JSON output has generalDiagnostics array
89
+ const generalDiags = data.generalDiagnostics || [];
90
+
91
+ for (const diag of generalDiags) {
92
+ // Skip if not for this file (pyright may output diagnostics for imports)
93
+ // For now, include all - caller will filter if needed
128
94
 
129
- function parsePyrightOutput(
130
- data: PyrightResult,
131
- filePath: string,
132
- ): Diagnostic[] {
133
- if (!data.generalDiagnostics) return [];
134
-
135
- return data.generalDiagnostics
136
- .filter((d) => {
137
- // Only include errors and warnings, skip informational
138
- return d.severity === "error" || d.severity === "warning";
139
- })
140
- .map((d) => ({
141
- id: `pyright-${d.range.start.line}-${d.rule}`,
142
- message: d.message.split("\n")[0], // First line only (pyright has multi-line messages)
143
- filePath,
144
- line: d.range.start.line + 1, // Pyright is 0-indexed, we're 1-indexed
145
- column: d.range.start.character + 1,
146
- severity: d.severity === "error" ? "error" : "warning",
147
- semantic: d.severity === "error" ? "blocking" : "warning",
95
+ diagnostics.push({
96
+ id: `pyright-${diag.rule || diag.start?.line || "unknown"}`,
97
+ message: diag.message || "Type error",
98
+ filePath: diag.file || _filePath,
99
+ line: diag.start?.line || 0,
100
+ column: diag.start?.column || 0,
101
+ severity: diag.severity === "error" ? "error" : "warning",
102
+ semantic: diag.severity === "error" ? "blocking" : "warning",
148
103
  tool: "pyright",
149
- rule: d.rule,
150
- fixable: false, // Pyright can't auto-fix, only suggest
151
- }));
104
+ rule: diag.rule,
105
+ });
106
+ }
107
+
108
+ return diagnostics;
152
109
  }
153
110
 
154
111
  export default pyrightRunner;
@@ -0,0 +1,97 @@
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
+ import { safeSpawn } from "../../safe-spawn.js";
13
+ import { createConfigFinder, isSgAvailable, } from "./utils/runner-helpers.js";
14
+ const findSlopConfig = createConfigFinder("python-slop-rules");
15
+ const pythonSlopRunner = {
16
+ id: "python-slop",
17
+ appliesTo: ["python"],
18
+ priority: 25, // Between pyright (5) and ruff (10)
19
+ enabledByDefault: true,
20
+ skipTestFiles: true, // Slop rules can be noisy in test files
21
+ async run(ctx) {
22
+ // Check if ast-grep is available
23
+ if (!isSgAvailable()) {
24
+ return { status: "skipped", diagnostics: [], semantic: "none" };
25
+ }
26
+ // Find slop config
27
+ const configPath = findSlopConfig(ctx.cwd);
28
+ if (!configPath) {
29
+ return { status: "skipped", diagnostics: [], semantic: "none" };
30
+ }
31
+ // Run ast-grep scan
32
+ const args = ["sg", "scan", "--config", configPath, "--json", ctx.filePath];
33
+ const result = safeSpawn("npx", args, {
34
+ timeout: 30000,
35
+ });
36
+ const raw = result.stdout + result.stderr;
37
+ if (result.status === 0 && !raw.trim()) {
38
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
39
+ }
40
+ // Parse results
41
+ const diagnostics = parseSlopOutput(raw, ctx.filePath);
42
+ if (diagnostics.length === 0) {
43
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
44
+ }
45
+ return {
46
+ status: "failed",
47
+ diagnostics,
48
+ semantic: "warning",
49
+ };
50
+ },
51
+ };
52
+ function parseSlopOutput(raw, filePath) {
53
+ const diagnostics = [];
54
+ try {
55
+ // Try to parse as JSON first
56
+ const data = JSON.parse(raw);
57
+ const items = Array.isArray(data) ? data : [data];
58
+ for (const item of items) {
59
+ const rule = item.rule || "slop";
60
+ const message = item.message || "Pattern detected";
61
+ const severity = item.severity || "warning";
62
+ diagnostics.push({
63
+ id: `python-slop-${rule}`,
64
+ message,
65
+ filePath,
66
+ line: item.start?.line || 0,
67
+ column: item.start?.column || 0,
68
+ severity: severity === "error" ? "error" : "warning",
69
+ semantic: severity === "error" ? "blocking" : "warning",
70
+ tool: "python-slop",
71
+ rule,
72
+ });
73
+ }
74
+ }
75
+ catch {
76
+ // Not JSON, try line-by-line parsing
77
+ const lines = raw.split("\n").filter((l) => l.trim());
78
+ for (const line of lines) {
79
+ // Try to extract line numbers from typical output formats
80
+ const match = line.match(/:(\d+):/);
81
+ if (match) {
82
+ diagnostics.push({
83
+ id: "python-slop-pattern",
84
+ message: line.trim(),
85
+ filePath,
86
+ line: parseInt(match[1], 10),
87
+ column: 0,
88
+ severity: "warning",
89
+ semantic: "warning",
90
+ tool: "python-slop",
91
+ });
92
+ }
93
+ }
94
+ }
95
+ return diagnostics;
96
+ }
97
+ export default pythonSlopRunner;
@@ -0,0 +1,203 @@
1
+ import * as fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import * as path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ function createMockContext(filePath) {
6
+ return {
7
+ filePath,
8
+ cwd: process.cwd(),
9
+ kind: "python",
10
+ autofix: false,
11
+ deltaMode: false,
12
+ baselines: { get: () => [], add: () => { }, save: () => { } },
13
+ pi: {},
14
+ hasTool: async () => false,
15
+ log: () => { },
16
+ };
17
+ }
18
+ // Helper for safe file cleanup
19
+ function safeUnlink(filePath) {
20
+ try {
21
+ if (fs.existsSync(filePath)) {
22
+ fs.unlinkSync(filePath);
23
+ }
24
+ }
25
+ catch {
26
+ // Ignore cleanup errors on Windows
27
+ }
28
+ }
29
+ describe("python-slop runner", () => {
30
+ const require = createRequire(import.meta.url);
31
+ it("should have correct runner definition", async () => {
32
+ const slopModule = await import("./python-slop.js");
33
+ const runner = slopModule.default;
34
+ expect(runner.id).toBe("python-slop");
35
+ expect(runner.appliesTo).toEqual(["python"]);
36
+ expect(runner.priority).toBe(25);
37
+ expect(runner.enabledByDefault).toBe(true);
38
+ expect(runner.skipTestFiles).toBe(true);
39
+ });
40
+ it("should detect ast-grep availability", () => {
41
+ const { spawnSync } = require("node:child_process");
42
+ const result = spawnSync("npx", ["sg", "--version"], {
43
+ encoding: "utf-8",
44
+ timeout: 10000,
45
+ shell: true,
46
+ });
47
+ expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
48
+ });
49
+ it("should detect verbose range-len pattern", async () => {
50
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_range_${Date.now()}.py`);
51
+ fs.writeFileSync(tmpFile, `# Slop: using range(len()) instead of enumerate
52
+ def process_items(items):
53
+ for i in range(len(items)):
54
+ print(items[i])
55
+ `);
56
+ try {
57
+ const slopModule = await import("./python-slop.js");
58
+ const runner = slopModule.default;
59
+ const result = await runner.run(createMockContext(tmpFile));
60
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
61
+ expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
62
+ d.message.includes("range(len") &&
63
+ d.message.includes("enumerate"))).toBe(true);
64
+ }
65
+ finally {
66
+ safeUnlink(tmpFile);
67
+ }
68
+ });
69
+ it("should detect manual min/max pattern", async () => {
70
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_minmax_${Date.now()}.py`);
71
+ fs.writeFileSync(tmpFile, `# Slop: manual min/max instead of built-in
72
+ def find_max(a, b):
73
+ if a > b:
74
+ m = a
75
+ else:
76
+ m = b
77
+ return m
78
+ `);
79
+ try {
80
+ const slopModule = await import("./python-slop.js");
81
+ const runner = slopModule.default;
82
+ const result = await runner.run(createMockContext(tmpFile));
83
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
84
+ expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
85
+ (d.message.includes("min") || d.message.includes("max")))).toBe(true);
86
+ }
87
+ finally {
88
+ safeUnlink(tmpFile);
89
+ }
90
+ });
91
+ it("should detect defensive None guard", async () => {
92
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_guard_${Date.now()}.py`);
93
+ fs.writeFileSync(tmpFile, `# Slop: defensive None guard
94
+ def process(data):
95
+ if data is None:
96
+ return None
97
+ return data.upper()
98
+ `);
99
+ try {
100
+ const slopModule = await import("./python-slop.js");
101
+ const runner = slopModule.default;
102
+ const result = await runner.run(createMockContext(tmpFile));
103
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
104
+ expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
105
+ (d.message.includes("defensive") ||
106
+ d.message.includes("guard")))).toBe(true);
107
+ }
108
+ finally {
109
+ safeUnlink(tmpFile);
110
+ }
111
+ });
112
+ it("should detect list comprehension ceremony", async () => {
113
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_list_${Date.now()}.py`);
114
+ fs.writeFileSync(tmpFile, `# Slop: redundant list comprehension
115
+ def convert(items):
116
+ return [x for x in items]
117
+ `);
118
+ try {
119
+ const slopModule = await import("./python-slop.js");
120
+ const runner = slopModule.default;
121
+ const result = await runner.run(createMockContext(tmpFile));
122
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
123
+ expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
124
+ d.message.includes("list") &&
125
+ d.message.includes("unnecessary"))).toBe(true);
126
+ }
127
+ finally {
128
+ safeUnlink(tmpFile);
129
+ }
130
+ });
131
+ it("should detect chained comparison opportunity", async () => {
132
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_chain_${Date.now()}.py`);
133
+ fs.writeFileSync(tmpFile, `# Slop: could use chained comparison
134
+ def check_range(x, a, b):
135
+ return a < x and x < b
136
+ `);
137
+ try {
138
+ const slopModule = await import("./python-slop.js");
139
+ const runner = slopModule.default;
140
+ const result = await runner.run(createMockContext(tmpFile));
141
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
142
+ expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
143
+ d.message.includes("chained"))).toBe(true);
144
+ }
145
+ finally {
146
+ safeUnlink(tmpFile);
147
+ }
148
+ });
149
+ it("should pass clean Python files", async () => {
150
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_ok_${Date.now()}.py`);
151
+ fs.writeFileSync(tmpFile, `# Clean Python code
152
+ def process_items(items):
153
+ """Process items using proper Python idioms."""
154
+ for i, item in enumerate(items):
155
+ print(f"{i}: {item}")
156
+
157
+ def find_max(a, b):
158
+ return max(a, b)
159
+
160
+ def check_range(x, min_val, max_val):
161
+ return min_val < x < max_val
162
+
163
+ def convert(items):
164
+ return list(items)
165
+ `);
166
+ try {
167
+ const slopModule = await import("./python-slop.js");
168
+ const runner = slopModule.default;
169
+ const result = await runner.run(createMockContext(tmpFile));
170
+ // Should have no slop issues
171
+ const slopIssues = result.diagnostics.filter((d) => d.tool === "python-slop");
172
+ expect(slopIssues.length).toBe(0);
173
+ }
174
+ finally {
175
+ safeUnlink(tmpFile);
176
+ }
177
+ });
178
+ it("should categorize by weight correctly", async () => {
179
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_weight_${Date.now()}.py`);
180
+ fs.writeFileSync(tmpFile, `# Multiple slop patterns - weight 3 and weight 4
181
+ def bad_code(items):
182
+ # Weight 3: range(len)
183
+ for i in range(len(items)):
184
+ print(items[i])
185
+
186
+ # Weight 3: redundant list comprehension
187
+ return [x for x in items]
188
+ `);
189
+ try {
190
+ const slopModule = await import("./python-slop.js");
191
+ const runner = slopModule.default;
192
+ const result = await runner.run(createMockContext(tmpFile));
193
+ // Should detect at least the range(len) pattern
194
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
195
+ // All should be warnings (weight 3)
196
+ const warnings = result.diagnostics.filter((d) => d.severity === "warning");
197
+ expect(warnings.length).toBeGreaterThanOrEqual(1);
198
+ }
199
+ finally {
200
+ safeUnlink(tmpFile);
201
+ }
202
+ });
203
+ });