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,425 @@
1
+ /**
2
+ * Tree-sitter Query Loader
3
+ *
4
+ * Loads tree-sitter queries from YAML files in rules/tree-sitter-queries/
5
+ * and provides them to the TreeSitterClient.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ export interface TreeSitterQuery {
15
+ id: string;
16
+ name: string;
17
+ severity: "error" | "warning" | "info";
18
+ category: string;
19
+ language: string;
20
+ message: string;
21
+ description?: string;
22
+ query: string;
23
+ metavars: string[];
24
+ post_filter?: string;
25
+ // biome-ignore lint/suspicious/noExplicitAny: Flexible filter params
26
+ post_filter_params?: Record<string, any>;
27
+ tags?: string[];
28
+ has_fix: boolean;
29
+ fix_action?: string;
30
+ examples?: {
31
+ bad?: string;
32
+ good?: string;
33
+ };
34
+ filePath: string;
35
+ }
36
+
37
+ export class TreeSitterQueryLoader {
38
+ private queries: Map<string, TreeSitterQuery[]> = new Map();
39
+ private loaded = false;
40
+ private verbose: boolean;
41
+
42
+ constructor(verbose = false) {
43
+ this.verbose = verbose;
44
+ }
45
+
46
+ /** Debug logging helper */
47
+ private dbg(msg: string): void {
48
+ if (this.verbose) {
49
+ console.error(`[query-loader] ${msg}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Load all queries from the rules/tree-sitter-queries directory
55
+ */
56
+ async loadQueries(): Promise<Map<string, TreeSitterQuery[]>> {
57
+ if (this.loaded) return this.queries;
58
+
59
+ const queriesDir = path.join(process.cwd(), "rules", "tree-sitter-queries");
60
+
61
+ if (!fs.existsSync(queriesDir)) {
62
+ this.dbg(`Queries directory not found: ${queriesDir}`);
63
+ return this.queries;
64
+ }
65
+
66
+ // Load queries from each language subdirectory
67
+ const languageDirs = fs
68
+ .readdirSync(queriesDir, { withFileTypes: true })
69
+ .filter((d) => d.isDirectory())
70
+ .map((d) => d.name);
71
+
72
+ for (const lang of languageDirs) {
73
+ const langDir = path.join(queriesDir, lang);
74
+ const queryFiles = fs
75
+ .readdirSync(langDir)
76
+ .filter((f) => f.endsWith(".yml"));
77
+
78
+ const langQueries: TreeSitterQuery[] = [];
79
+
80
+ for (const file of queryFiles) {
81
+ const filePath = path.join(langDir, file);
82
+ const query = this.parseQueryFile(filePath, lang);
83
+ if (query) {
84
+ langQueries.push(query);
85
+ }
86
+ }
87
+
88
+ if (langQueries.length > 0) {
89
+ this.queries.set(lang, langQueries);
90
+ this.dbg(`Loaded ${langQueries.length} queries for ${lang}`);
91
+ }
92
+ }
93
+
94
+ this.loaded = true;
95
+ return this.queries;
96
+ }
97
+
98
+ /**
99
+ * Parse a single YAML query file
100
+ */
101
+ private parseQueryFile(
102
+ filePath: string,
103
+ language: string,
104
+ ): TreeSitterQuery | null {
105
+ try {
106
+ const content = fs.readFileSync(filePath, "utf-8");
107
+
108
+ // Simple YAML parsing (extract key: value pairs)
109
+ const parsed = this.parseYaml(content);
110
+
111
+ if (!parsed.id || !parsed.query) {
112
+ this.dbg(`Invalid query file: ${filePath}`);
113
+ return null;
114
+ }
115
+
116
+ return {
117
+ id: String(parsed.id),
118
+ name: String(parsed.name || parsed.id),
119
+ severity: this.parseSeverity(parsed.severity),
120
+ category: String(parsed.category || "general"),
121
+ language: String(parsed.language || language),
122
+ message: String(parsed.message || `Pattern: ${parsed.id}`),
123
+ description: parsed.description
124
+ ? String(parsed.description)
125
+ : undefined,
126
+ query:
127
+ this.extractMultilineValue(content, "query") || String(parsed.query),
128
+ metavars: Array.isArray(parsed.metavars)
129
+ ? parsed.metavars.map(String)
130
+ : this.extractMetavars(String(parsed.query)),
131
+ post_filter: parsed.post_filter
132
+ ? String(parsed.post_filter)
133
+ : undefined,
134
+ // biome-ignore lint/suspicious/noExplicitAny: Post filter params
135
+ post_filter_params: parsed.post_filter_params as any,
136
+ tags: Array.isArray(parsed.tags) ? parsed.tags.map(String) : undefined,
137
+ has_fix: parsed.has_fix === true || parsed.has_fix === "true",
138
+ fix_action: parsed.fix_action ? String(parsed.fix_action) : undefined,
139
+ filePath,
140
+ };
141
+ } catch (err) {
142
+ this.dbg(`Failed to parse ${filePath}: ${err}`);
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Simple YAML parser for our query files
149
+ */
150
+ private parseYaml(
151
+ content: string,
152
+ ): Record<string, string | string[] | boolean> {
153
+ const result: Record<string, string | string[] | boolean> = {};
154
+ const lines = content.split("\n");
155
+
156
+ for (let i = 0; i < lines.length; i++) {
157
+ const line = lines[i];
158
+ const match = line.match(/^([a-z_]+):\s*(.*)$/);
159
+ if (match) {
160
+ const key = match[1];
161
+ let value: string | string[] | boolean = match[2].trim();
162
+
163
+ // Handle arrays inline: metavars: [A, B, C]
164
+ if (value.startsWith("[") && value.endsWith("]")) {
165
+ value = value
166
+ .slice(1, -1)
167
+ .split(",")
168
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""));
169
+ }
170
+ // Handle multi-line arrays: metavars:\n - A\n - B
171
+ else if (value === "") {
172
+ // Check if next lines are array items ( - item)
173
+ const arrayItems: string[] = [];
174
+ const baseIndent = line.match(/^(\s*)/)?.[0].length || 0;
175
+
176
+ for (let j = i + 1; j < lines.length; j++) {
177
+ const nextLine = lines[j];
178
+ const nextIndent = nextLine.match(/^(\s*)/)?.[0].length || 0;
179
+
180
+ // Stop if we hit a line with same or less indent (new key)
181
+ if (nextIndent <= baseIndent && nextLine.match(/^[a-z_]+:/)) {
182
+ break;
183
+ }
184
+
185
+ // Check if it's an array item
186
+ const itemMatch = nextLine.match(/^\s+-\s*(.+)$/);
187
+ if (itemMatch) {
188
+ // Strip inline comments and trim
189
+ const item = itemMatch[1].trim().replace(/\s*#.*$/, "");
190
+ if (item) arrayItems.push(item);
191
+ }
192
+ }
193
+
194
+ if (arrayItems.length > 0) {
195
+ value = arrayItems;
196
+ }
197
+ }
198
+ // Handle booleans
199
+ else if (value === "true") value = true;
200
+ else if (value === "false") value = false;
201
+ // Strip quotes from strings
202
+ else if (value.startsWith('"') && value.endsWith('"')) {
203
+ value = value.slice(1, -1);
204
+ }
205
+
206
+ result[key] = value;
207
+ }
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ /**
214
+ * Extract a multiline value (like query) from YAML
215
+ */
216
+ private extractMultilineValue(content: string, key: string): string | null {
217
+ const lines = content.split("\n");
218
+ let startLine = -1;
219
+ let startIndent = 0;
220
+
221
+ const keyPrefix = `${key}:`;
222
+
223
+ // Find the key line
224
+ for (let i = 0; i < lines.length; i++) {
225
+ const trimmed = lines[i].trimStart();
226
+ if (trimmed.startsWith(keyPrefix)) {
227
+ startLine = i;
228
+ startIndent = lines[i].length - trimmed.length;
229
+ const afterKey = trimmed.slice(keyPrefix.length).trim();
230
+ // If there's content on the same line (not just |), return it
231
+ if (afterKey && afterKey !== "|") return afterKey;
232
+ break;
233
+ }
234
+ }
235
+
236
+ if (startLine === -1) return null;
237
+
238
+ // Collect all lines until we hit a new key with same or less indent
239
+ const valueLines: string[] = [];
240
+ for (let i = startLine + 1; i < lines.length; i++) {
241
+ const line = lines[i];
242
+
243
+ // Track empty lines
244
+ if (!line.trim()) {
245
+ valueLines.push("");
246
+ continue;
247
+ }
248
+
249
+ // Check indent
250
+ const indentMatch = line.match(/^(\s*)/);
251
+ const indent = indentMatch ? indentMatch[1].length : 0;
252
+ const trimmed = line.trim();
253
+
254
+ // Stop at new key with same or less indent (but not at comments)
255
+ if (
256
+ indent <= startIndent &&
257
+ trimmed.match(/^[a-z_]+:/) &&
258
+ !trimmed.startsWith("#")
259
+ ) {
260
+ break;
261
+ }
262
+
263
+ // Skip comment lines (they're not part of the value)
264
+ if (trimmed.startsWith("#")) continue;
265
+
266
+ // This is part of the multiline value
267
+ valueLines.push(line.slice(startIndent));
268
+ }
269
+
270
+ // Clean up - remove trailing empty lines
271
+ while (valueLines.length > 0 && !valueLines[valueLines.length - 1].trim()) {
272
+ valueLines.pop();
273
+ }
274
+
275
+ return valueLines.length > 0 ? valueLines.join("\n") : null;
276
+ }
277
+
278
+ /**
279
+ * Parse severity string to valid type
280
+ */
281
+ private parseSeverity(value: unknown): "error" | "warning" | "info" {
282
+ if (value === "error") return "error";
283
+ if (value === "warning") return "warning";
284
+ if (value === "info") return "info";
285
+ return "warning"; // default
286
+ }
287
+
288
+ /**
289
+ * Extract @VAR patterns from query string
290
+ */
291
+ private extractMetavars(query: string): string[] {
292
+ const matches = query.match(/@([A-Z_][A-Z0-9_]*)/g);
293
+ if (!matches) return [];
294
+ return [...new Set(matches.map((m) => m.slice(1)))];
295
+ }
296
+
297
+ /**
298
+ * Get queries for a specific language
299
+ */
300
+ getQueriesForLanguage(language: string): TreeSitterQuery[] {
301
+ return this.queries.get(language) || [];
302
+ }
303
+
304
+ /**
305
+ * Get a specific query by ID
306
+ */
307
+ getQueryById(id: string): TreeSitterQuery | undefined {
308
+ for (const langQueries of this.queries.values()) {
309
+ const query = langQueries.find((q) => q.id === id);
310
+ if (query) return query;
311
+ }
312
+ return undefined;
313
+ }
314
+
315
+ /**
316
+ * Find matching query for a pattern string
317
+ */
318
+ findMatchingQuery(
319
+ pattern: string,
320
+ language: string,
321
+ ): TreeSitterQuery | undefined {
322
+ const langQueries = this.getQueriesForLanguage(language);
323
+
324
+ // Check for pattern keywords
325
+ for (const query of langQueries) {
326
+ // Match by ID
327
+ if (pattern.includes(query.id)) return query;
328
+
329
+ // Match by keywords in pattern
330
+ switch (query.id) {
331
+ case "empty-catch":
332
+ if (pattern.includes("empty-catch") || pattern.includes("catch {}"))
333
+ return query;
334
+ break;
335
+ case "debugger-statement":
336
+ if (pattern.includes("debugger")) return query;
337
+ break;
338
+ case "await-in-loop":
339
+ if (pattern.includes("await-in-loop") || pattern.includes("await"))
340
+ return query;
341
+ break;
342
+ case "hardcoded-secrets":
343
+ if (
344
+ pattern.includes("hardcoded") ||
345
+ pattern.includes("api_key") ||
346
+ pattern.includes("password")
347
+ )
348
+ return query;
349
+ break;
350
+ case "dangerously-set-inner-html":
351
+ if (pattern.includes("dangerously") || pattern.includes("innerHTML"))
352
+ return query;
353
+ break;
354
+ case "nested-ternary":
355
+ if (pattern.includes("ternary") || pattern.includes("? :"))
356
+ return query;
357
+ break;
358
+ case "no-eval":
359
+ if (pattern.includes("eval") && !pattern.includes("console"))
360
+ return query;
361
+ break;
362
+ case "deep-promise-chain":
363
+ if (pattern.includes(".then") && pattern.includes(".catch"))
364
+ return query;
365
+ break;
366
+ case "console-statement":
367
+ if (pattern.includes("console")) return query;
368
+ break;
369
+ case "long-parameter-list":
370
+ if (pattern.includes("PARAMS")) return query;
371
+ break;
372
+ // Python queries
373
+ case "bare-except":
374
+ if (pattern.includes("bare-except") || pattern.includes("except:"))
375
+ return query;
376
+ break;
377
+ case "mutable-default-arg":
378
+ if (pattern.includes("mutable") || pattern.includes("default"))
379
+ return query;
380
+ break;
381
+ case "wildcard-import":
382
+ if (pattern.includes("wildcard") || pattern.includes("import *"))
383
+ return query;
384
+ break;
385
+ case "eval-exec":
386
+ if (pattern.includes("eval") || pattern.includes("exec"))
387
+ return query;
388
+ break;
389
+ case "is-vs-equals":
390
+ if (pattern.includes("is") || pattern.includes("equals"))
391
+ return query;
392
+ break;
393
+ case "unreachable-except":
394
+ if (pattern.includes("unreachable") || pattern.includes("except"))
395
+ return query;
396
+ break;
397
+ }
398
+ }
399
+
400
+ return undefined;
401
+ }
402
+
403
+ /**
404
+ * Get all loaded queries
405
+ */
406
+ getAllQueries(): TreeSitterQuery[] {
407
+ const all: TreeSitterQuery[] = [];
408
+ for (const queries of this.queries.values()) {
409
+ all.push(...queries);
410
+ }
411
+ return all;
412
+ }
413
+
414
+ /**
415
+ * Reload queries from disk
416
+ */
417
+ async reload(): Promise<void> {
418
+ this.queries.clear();
419
+ this.loaded = false;
420
+ await this.loadQueries();
421
+ }
422
+ }
423
+
424
+ // Singleton instance
425
+ export const queryLoader = new TreeSitterQueryLoader();
@@ -8,8 +8,8 @@
8
8
  * Requires: npm install -D type-coverage
9
9
  * Docs: https://github.com/plantain-00/type-coverage
10
10
  */
11
- import { spawnSync } from "node:child_process";
12
11
  import * as path from "node:path";
12
+ import { safeSpawn } from "./safe-spawn.js";
13
13
  // --- Client ---
14
14
  export class TypeCoverageClient {
15
15
  constructor(verbose = false) {
@@ -21,10 +21,8 @@ export class TypeCoverageClient {
21
21
  isAvailable() {
22
22
  if (this.available !== null)
23
23
  return this.available;
24
- const result = spawnSync("npx", ["type-coverage", "--version"], {
25
- encoding: "utf-8",
24
+ const result = safeSpawn("npx", ["type-coverage", "--version"], {
26
25
  timeout: 10000,
27
- shell: true,
28
26
  });
29
27
  this.available = !result.error && result.status === 0;
30
28
  return this.available;
@@ -45,17 +43,15 @@ export class TypeCoverageClient {
45
43
  };
46
44
  }
47
45
  try {
48
- const result = spawnSync("npx", [
46
+ const result = safeSpawn("npx", [
49
47
  "type-coverage",
50
48
  "--detail",
51
49
  "--strict",
52
50
  "--ignore-files",
53
51
  "**/*.d.ts",
54
52
  ], {
55
- encoding: "utf-8",
56
53
  timeout: 30000,
57
54
  cwd,
58
- shell: true,
59
55
  });
60
56
  const output = (result.stdout ?? "") + (result.stderr ?? "");
61
57
  return this.parseOutput(output, cwd);
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { spawnSync } from "node:child_process";
13
13
  import * as path from "node:path";
14
+ import { safeSpawn } from "./safe-spawn.js";
14
15
 
15
16
  // --- Types ---
16
17
 
@@ -43,10 +44,8 @@ export class TypeCoverageClient {
43
44
 
44
45
  isAvailable(): boolean {
45
46
  if (this.available !== null) return this.available;
46
- const result = spawnSync("npx", ["type-coverage", "--version"], {
47
- encoding: "utf-8",
47
+ const result = safeSpawn("npx", ["type-coverage", "--version"], {
48
48
  timeout: 10000,
49
- shell: true,
50
49
  });
51
50
  this.available = !result.error && result.status === 0;
52
51
  return this.available;
@@ -69,7 +68,7 @@ export class TypeCoverageClient {
69
68
  }
70
69
 
71
70
  try {
72
- const result = spawnSync(
71
+ const result = safeSpawn(
73
72
  "npx",
74
73
  [
75
74
  "type-coverage",
@@ -79,10 +78,8 @@ export class TypeCoverageClient {
79
78
  "**/*.d.ts",
80
79
  ],
81
80
  {
82
- encoding: "utf-8",
83
81
  timeout: 30000,
84
82
  cwd,
85
- shell: true,
86
83
  },
87
84
  );
88
85
 
@@ -0,0 +1,157 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
3
+ import { TypeScriptClient } from "./typescript-client.js";
4
+ describe("TypeScriptClient - Code Fixes", () => {
5
+ let client;
6
+ let tmpDir;
7
+ let cleanup;
8
+ beforeEach(() => {
9
+ client = new TypeScriptClient();
10
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-codefix-test-"));
11
+ });
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+ describe("getCodeFixes", () => {
16
+ it("should provide fix for missing property on object literal", () => {
17
+ // Real-world case: Missing required property in object literal
18
+ const content = `
19
+ interface Config {
20
+ name: string;
21
+ port: number;
22
+ debug: boolean;
23
+ }
24
+
25
+ const config: Config = {
26
+ name: "my-app",
27
+ port: 3000
28
+ // Missing 'debug' property - TS2345
29
+ };
30
+ `;
31
+ const filePath = createTempFile(tmpDir, "missing-property.ts", content);
32
+ client.addFile(filePath, content);
33
+ const diags = client.getDiagnostics(filePath);
34
+ const missingPropError = diags.find((d) => d.code === 2345 || d.message.includes("missing"));
35
+ if (missingPropError) {
36
+ const line = missingPropError.range.start.line;
37
+ const char = missingPropError.range.start.character;
38
+ const fixes = client.getCodeFixes(filePath, line, char, [
39
+ missingPropError.code,
40
+ ]);
41
+ // TypeScript should suggest adding the missing property
42
+ expect(fixes.length).toBeGreaterThan(0);
43
+ const hasAddPropertyFix = fixes.some((f) => f.description.toLowerCase().includes("add") ||
44
+ f.description.toLowerCase().includes("property") ||
45
+ f.description.toLowerCase().includes("declare"));
46
+ expect(hasAddPropertyFix).toBe(true);
47
+ }
48
+ });
49
+ it("should provide fix for missing await in async function", () => {
50
+ // Real-world case: Forgetting await on a Promise-returning function
51
+ const content = `
52
+ async function fetchUser(id: string): Promise<{ name: string }> {
53
+ return { name: "John" };
54
+ }
55
+
56
+ async function getUserName(id: string): Promise<string> {
57
+ const user = fetchUser(id); // Missing await
58
+ return user.name; // Type 'Promise<{ name: string; }>' has no property 'name'
59
+ }
60
+ `;
61
+ const filePath = createTempFile(tmpDir, "missing-await.ts", content);
62
+ client.addFile(filePath, content);
63
+ const diags = client.getDiagnostics(filePath);
64
+ // TS2739: Type 'Promise<{ name: string; }>' is missing 'name'
65
+ const propertyError = diags.find((d) => d.code === 2739 || d.message.includes("is missing"));
66
+ if (propertyError) {
67
+ const fixes = client.getAllCodeFixes(filePath);
68
+ // If there's an error, check if we have fixes for it
69
+ const lineFixes = fixes.get(propertyError.range.start.line);
70
+ if (lineFixes) {
71
+ expect(lineFixes.length).toBeGreaterThan(0);
72
+ }
73
+ }
74
+ // Test passes if we get here - not all TS versions provide fixes for this
75
+ expect(true).toBe(true);
76
+ });
77
+ it("should provide fix for incorrect type assignment", () => {
78
+ // Real-world case: String instead of number
79
+ const content = `
80
+ function calculateTotal(price: number, tax: number): number {
81
+ return price + tax;
82
+ }
83
+
84
+ const result = calculateTotal("100", 10); // TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
85
+ `;
86
+ const filePath = createTempFile(tmpDir, "type-mismatch.ts", content);
87
+ client.addFile(filePath, content);
88
+ const diags = client.getDiagnostics(filePath);
89
+ const typeError = diags.find((d) => d.code === 2345);
90
+ if (typeError) {
91
+ const line = typeError.range.start.line;
92
+ const char = typeError.range.start.character;
93
+ const fixes = client.getCodeFixes(filePath, line, char, [2345]);
94
+ // TypeScript often suggests fixes for type mismatches
95
+ expect(fixes).toBeDefined();
96
+ }
97
+ });
98
+ it("should collect all fixes via getAllCodeFixes", () => {
99
+ // Multiple errors in one file
100
+ const content = `
101
+ interface Person {
102
+ name: string;
103
+ age: number;
104
+ }
105
+
106
+ const person: Person = {
107
+ name: "Alice"
108
+ // Missing age
109
+ };
110
+
111
+ function greet(p: Person): string {
112
+ return "Hello " + p.name;
113
+ }
114
+
115
+ greet({ name: "Bob" }); // Missing age in argument
116
+ `;
117
+ const filePath = createTempFile(tmpDir, "multiple-errors.ts", content);
118
+ client.addFile(filePath, content);
119
+ const allFixes = client.getAllCodeFixes(filePath);
120
+ // Should have fixes mapped by line number
121
+ expect(allFixes).toBeInstanceOf(Map);
122
+ // Each fix entry should have a description and changes
123
+ for (const [line, fixes] of allFixes.entries()) {
124
+ expect(typeof line).toBe("number");
125
+ expect(fixes.length).toBeGreaterThan(0);
126
+ for (const fix of fixes) {
127
+ expect(fix.description).toBeTruthy();
128
+ expect(fix.changes).toBeDefined();
129
+ }
130
+ }
131
+ });
132
+ });
133
+ describe("Integration with diagnostic messages", () => {
134
+ it("should include fix suggestions in getAllCodeFixes output", () => {
135
+ const content = `
136
+ class User {
137
+ constructor(public name: string) {}
138
+ }
139
+
140
+ const user = new User(); // TS2554: Expected 1 arguments, but got 0
141
+ `;
142
+ const filePath = createTempFile(tmpDir, "constructor-args.ts", content);
143
+ client.addFile(filePath, content);
144
+ const diags = client.getDiagnostics(filePath);
145
+ const argError = diags.find((d) => d.code === 2554);
146
+ if (argError) {
147
+ const fixes = client.getAllCodeFixes(filePath);
148
+ const lineFixes = fixes.get(argError.range.start.line);
149
+ if (lineFixes && lineFixes.length > 0) {
150
+ // The runner would append this to the message
151
+ const suggestion = lineFixes[0].description;
152
+ expect(suggestion).toBeTruthy();
153
+ }
154
+ }
155
+ });
156
+ });
157
+ });