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,250 @@
1
+ /**
2
+ * AstGrep Client for pi-lens
3
+ *
4
+ * Structural code analysis using ast-grep CLI.
5
+ * Scans files against YAML rule definitions.
6
+ *
7
+ * Requires: npm install -D @ast-grep/cli
8
+ * Rules: ./rules/ directory
9
+ */
10
+ import { spawnSync } from "node:child_process";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { AstGrepParser } from "./ast-grep-parser.js";
14
+ import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
15
+ import { SgRunner } from "./sg-runner.js";
16
+ const _getExtensionDir = () => {
17
+ if (typeof __dirname !== "undefined") {
18
+ return __dirname;
19
+ }
20
+ return ".";
21
+ };
22
+ // --- Client ---
23
+ export class AstGrepClient {
24
+ available = null;
25
+ ruleDir;
26
+ log;
27
+ ruleManager;
28
+ runner;
29
+ constructor(ruleDir, verbose = false) {
30
+ this.ruleDir = ruleDir || path.join(process.cwd(), "rules");
31
+ this.log = verbose
32
+ ? (msg) => console.error(`[ast-grep] ${msg}`)
33
+ : () => { };
34
+ this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
35
+ this.runner = new SgRunner(verbose);
36
+ }
37
+ /**
38
+ * Check if ast-grep CLI is available
39
+ */
40
+ isAvailable() {
41
+ if (this.available !== null)
42
+ return this.available;
43
+ this.available = this.runner.isAvailable();
44
+ if (this.available) {
45
+ this.log("ast-grep available");
46
+ }
47
+ return this.available;
48
+ }
49
+ /**
50
+ * Search for AST patterns in files
51
+ */
52
+ async search(pattern, lang, paths, options) {
53
+ const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
54
+ if (options?.selector) {
55
+ args.push("--selector", options.selector);
56
+ }
57
+ if (options?.context !== undefined) {
58
+ args.push("--context", String(options.context));
59
+ }
60
+ args.push(...paths);
61
+ return this.runner.exec(args);
62
+ }
63
+ /**
64
+ * Search and replace AST patterns
65
+ */
66
+ async replace(pattern, rewrite, lang, paths, apply = false) {
67
+ const args = [
68
+ "run",
69
+ "-p",
70
+ pattern,
71
+ "-r",
72
+ rewrite,
73
+ "--lang",
74
+ lang,
75
+ "--json=compact",
76
+ ];
77
+ if (apply)
78
+ args.push("--update-all");
79
+ args.push(...paths);
80
+ const result = await this.runner.exec(args);
81
+ return { matches: result.matches, applied: apply, error: result.error };
82
+ }
83
+ /**
84
+ * Run a one-off scan with a temporary rule and configuration
85
+ */
86
+ runTempScan(dir, ruleId, ruleYaml, timeout = 30000) {
87
+ if (!this.isAvailable())
88
+ return [];
89
+ return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
90
+ }
91
+ /**
92
+ * Find similar functions by comparing normalized AST structure
93
+ */
94
+ async findSimilarFunctions(dir, lang = "typescript") {
95
+ const ruleYaml = `id: find-functions
96
+ language: ${lang}
97
+ rule:
98
+ kind: function_declaration
99
+ severity: info
100
+ message: found
101
+ `;
102
+ const matches = this.runTempScan(dir, "find-functions", ruleYaml);
103
+ if (matches.length === 0)
104
+ return [];
105
+ return this.groupSimilarFunctions(matches);
106
+ }
107
+ groupSimilarFunctions(matches) {
108
+ const grouped = new Map();
109
+ for (const item of matches) {
110
+ const name = this.extractFunctionName(item.text);
111
+ if (!name)
112
+ continue;
113
+ const signature = this.normalizeFunction(item.text);
114
+ const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) +
115
+ 1;
116
+ const group = grouped.get(signature) ?? [];
117
+ group.push({ name, file: item.file, line });
118
+ grouped.set(signature, group);
119
+ }
120
+ return Array.from(grouped.entries())
121
+ .filter(([_, functions]) => functions.length > 1)
122
+ .map(([pattern, functions]) => ({ pattern, functions }));
123
+ }
124
+ /**
125
+ * Extract function name from match text
126
+ */
127
+ extractFunctionName(text) {
128
+ return text.match(/function\s+(\w+)/)?.[1] ?? null;
129
+ }
130
+ normalizeFunction(text) {
131
+ const normalizedText = text
132
+ .replace(/function\s+\w+/, "function FN")
133
+ .replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR")
134
+ .replace(/["'].*?["']/g, "STR")
135
+ .replace(/`[^`]*`/g, "TMPL")
136
+ .replace(/\b\d+\b/g, "NUM")
137
+ .replace(/\btrue\b|\bfalse\b/g, "BOOL")
138
+ .replace(/\/\/.*/g, "")
139
+ .replace(/\/\*[\s\S]*?\*\//g, "")
140
+ .replace(/\s+/g, " ")
141
+ .trim();
142
+ // Extract just the body structure
143
+ const bodyMatch = normalizedText.match(/\{(.*)\}/);
144
+ const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
145
+ // Use first 200 chars as signature
146
+ return body.slice(0, 200);
147
+ }
148
+ /**
149
+ * Scan for exported function names in a directory
150
+ */
151
+ async scanExports(dir, lang = "typescript") {
152
+ const exports = new Map();
153
+ const ruleYaml = `id: find-functions
154
+ language: ${lang}
155
+ rule:
156
+ kind: function_declaration
157
+ severity: info
158
+ message: found
159
+ `;
160
+ const matches = this.runTempScan(dir, "find-functions", ruleYaml, 15000);
161
+ this.log(`scanExports output length: ${matches.length}`);
162
+ for (const item of matches) {
163
+ const text = item.text || "";
164
+ const nameMatch = text.match(/function\s+(\w+)/);
165
+ if (nameMatch?.[1]) {
166
+ this.log(`scanExports found: ${nameMatch[1]} in ${item.file}`);
167
+ exports.set(nameMatch[1], item.file);
168
+ }
169
+ }
170
+ return exports;
171
+ }
172
+ formatMatches(matches, isDryRun = false, showModeIndicator = false) {
173
+ return this.runner.formatMatches(matches, isDryRun, 50, showModeIndicator);
174
+ }
175
+ /**
176
+ * Scan a file against all rules
177
+ */
178
+ scanFile(filePath) {
179
+ if (!this.isAvailable())
180
+ return [];
181
+ const absolutePath = path.resolve(filePath);
182
+ if (!fs.existsSync(absolutePath))
183
+ return [];
184
+ const configPath = path.join(this.ruleDir, ".sgconfig.yml");
185
+ try {
186
+ const result = spawnSync("npx", ["sg", "scan", "--config", configPath, "--json", absolutePath], {
187
+ encoding: "utf-8",
188
+ timeout: 15000,
189
+ shell: process.platform === "win32",
190
+ });
191
+ // ast-grep exits 1 when it finds issues
192
+ const output = result.stdout || result.stderr || "";
193
+ if (!output.trim())
194
+ return [];
195
+ const parser = new AstGrepParser((id) => this.getRuleDescription(id), (sev) => this.mapSeverity(sev));
196
+ return parser.parseOutput(output, absolutePath);
197
+ }
198
+ catch (err) {
199
+ this.log(`Scan error: ${err instanceof Error ? err.message : String(err)}`);
200
+ return [];
201
+ }
202
+ }
203
+ /**
204
+ * Format diagnostics for LLM consumption
205
+ */
206
+ formatDiagnostics(diags) {
207
+ if (diags.length === 0)
208
+ return "";
209
+ const errors = diags.filter((d) => d.severity === "error");
210
+ const warnings = diags.filter((d) => d.severity === "warning");
211
+ const hints = diags.filter((d) => d.severity === "hint");
212
+ let output = `[ast-grep] ${diags.length} structural issue(s)`;
213
+ if (errors.length)
214
+ output += ` — ${errors.length} error(s)`;
215
+ if (warnings.length)
216
+ output += ` — ${warnings.length} warning(s)`;
217
+ if (hints.length)
218
+ output += ` — ${hints.length} hint(s)`;
219
+ output += ":\n";
220
+ for (const d of diags.slice(0, 10)) {
221
+ const loc = d.line === d.endLine ? `L${d.line}` : `L${d.line}-${d.endLine}`;
222
+ const ruleInfo = d.ruleDescription
223
+ ? `${d.rule}: ${d.ruleDescription.message}`
224
+ : d.rule;
225
+ const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
226
+ output += ` ${ruleInfo} (${loc})${fix}\n`;
227
+ if (d.ruleDescription?.note) {
228
+ const shortNote = d.ruleDescription.note.split("\n")[0];
229
+ output += ` → ${shortNote}\n`;
230
+ }
231
+ }
232
+ if (diags.length > 10) {
233
+ output += ` ... and ${diags.length - 10} more\n`;
234
+ }
235
+ return output;
236
+ }
237
+ getRuleDescription(ruleId) {
238
+ return this.ruleManager.loadRuleDescriptions().get(ruleId);
239
+ }
240
+ mapSeverity(severity) {
241
+ const lower = severity.toLowerCase();
242
+ if (lower === "error")
243
+ return "error";
244
+ if (lower === "warning")
245
+ return "warning";
246
+ if (lower === "info")
247
+ return "info";
248
+ return "hint";
249
+ }
250
+ }
@@ -0,0 +1,86 @@
1
+ import * as path from "node:path";
2
+ export class AstGrepParser {
3
+ getRuleDescription;
4
+ mapSeverity;
5
+ constructor(getRuleDescription, mapSeverity) {
6
+ this.getRuleDescription = getRuleDescription;
7
+ this.mapSeverity = mapSeverity;
8
+ }
9
+ parseOutput(output, filterFile) {
10
+ const resolvedFilterFile = path.resolve(filterFile);
11
+ try {
12
+ const items = JSON.parse(output);
13
+ if (Array.isArray(items)) {
14
+ return items
15
+ .map((item) => this.parseDiagnostic(item, resolvedFilterFile))
16
+ .filter((d) => d !== null);
17
+ }
18
+ }
19
+ catch (err) {
20
+ void err;
21
+ }
22
+ return output
23
+ .split("\n")
24
+ .filter((l) => l.trim())
25
+ .map((line) => {
26
+ try {
27
+ return this.parseDiagnostic(JSON.parse(line), resolvedFilterFile);
28
+ }
29
+ catch (err) {
30
+ void err;
31
+ return null;
32
+ }
33
+ })
34
+ .filter((d) => d !== null);
35
+ }
36
+ parseDiagnostic(item, filterFile) {
37
+ if (item.labels?.length) {
38
+ return this.parseNewFormat(item, filterFile);
39
+ }
40
+ if (item.spans?.length) {
41
+ return this.parseLegacyFormat(item, filterFile);
42
+ }
43
+ return null;
44
+ }
45
+ parseNewFormat(item, filterFile) {
46
+ const label = item.labels.find((l) => l.style === "primary") || item.labels[0];
47
+ const filePath = path.resolve(label.file || filterFile);
48
+ if (filePath !== filterFile)
49
+ return null;
50
+ const start = label.range?.start || { line: 0, column: 0 };
51
+ const end = label.range?.end || start;
52
+ return {
53
+ line: start.line + 1,
54
+ column: start.column,
55
+ endLine: end.line + 1,
56
+ endColumn: end.column,
57
+ severity: this.mapSeverity(item.severity),
58
+ message: item.message || "Unknown issue",
59
+ rule: item.ruleId || "unknown",
60
+ ruleDescription: this.getRuleDescription(item.ruleId || "unknown"),
61
+ file: filePath,
62
+ };
63
+ }
64
+ parseLegacyFormat(item, filterFile) {
65
+ const span = item.spans?.[0];
66
+ if (!span)
67
+ return null;
68
+ const filePath = path.resolve(span.file || filterFile);
69
+ if (filePath !== filterFile)
70
+ return null;
71
+ const start = span.range?.start || { line: 0, column: 0 };
72
+ const end = span.range?.end || start;
73
+ const ruleId = item.name || item.ruleId || "unknown";
74
+ return {
75
+ line: start.line + 1,
76
+ column: start.column,
77
+ endLine: end.line + 1,
78
+ endColumn: end.column,
79
+ severity: this.mapSeverity(item.severity || item.Severity || "warning"),
80
+ message: item.Message?.text || item.message || "Unknown issue",
81
+ rule: ruleId,
82
+ ruleDescription: this.getRuleDescription(ruleId),
83
+ file: filePath,
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,91 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export class AstGrepRuleManager {
4
+ ruleDir;
5
+ log;
6
+ ruleDescriptions = null;
7
+ constructor(ruleDir, log) {
8
+ this.ruleDir = ruleDir;
9
+ this.log = log;
10
+ }
11
+ loadRuleDescriptions() {
12
+ if (this.ruleDescriptions !== null)
13
+ return this.ruleDescriptions;
14
+ const descriptions = new Map();
15
+ const possiblePaths = [
16
+ path.join(this.ruleDir, "ast-grep-rules", "rules"),
17
+ path.join(this.ruleDir, "rules"),
18
+ this.ruleDir,
19
+ ];
20
+ const rulesPath = possiblePaths.find((p) => fs.existsSync(p));
21
+ if (!rulesPath) {
22
+ this.log(`Rule descriptions: no rules directory found in ${possiblePaths.join(", ")}`);
23
+ this.ruleDescriptions = descriptions;
24
+ return descriptions;
25
+ }
26
+ try {
27
+ const files = fs.readdirSync(rulesPath).filter((f) => f.endsWith(".yml"));
28
+ this.log(`Loaded ${files.length} rule descriptions from ${rulesPath}`);
29
+ for (const file of files) {
30
+ const filePath = path.join(rulesPath, file);
31
+ const content = fs.readFileSync(filePath, "utf-8");
32
+ const rule = this.parseRuleYaml(content);
33
+ if (rule) {
34
+ descriptions.set(rule.id, rule);
35
+ }
36
+ }
37
+ }
38
+ catch (err) {
39
+ this.log(`Failed to load rule descriptions: ${err.message}`);
40
+ }
41
+ this.ruleDescriptions = descriptions;
42
+ return descriptions;
43
+ }
44
+ parseRuleYaml(content) {
45
+ const result = {};
46
+ const idMatch = content.match(/^id:\s*(.+)$/m);
47
+ if (idMatch)
48
+ result.id = idMatch[1].trim();
49
+ const msgMatch = content.match(/^message:\s*"([^"]+)"/m) ||
50
+ content.match(/^message:\s*'([^']+)'/m) ||
51
+ content.match(/^message:\s*(.+)$/m);
52
+ if (msgMatch)
53
+ result.message = (msgMatch[3] || msgMatch[2] || msgMatch[1]).trim();
54
+ const noteMatch = content.match(/^note:\s*\|([\s\S]*?)(?=^\w|\n\n|\nrule:)/m);
55
+ if (noteMatch) {
56
+ result.note = noteMatch[1]
57
+ .split("\n")
58
+ .map((line) => line.trim())
59
+ .filter((line) => line.length > 0)
60
+ .join(" ");
61
+ }
62
+ const sevMatch = content.match(/^severity:\s*(.+)$/m);
63
+ if (sevMatch)
64
+ result.severity = this.mapSeverity(sevMatch[1].trim());
65
+ const gradeMatch = content.match(/Grade\s+(\d+\.\d+)/i);
66
+ if (gradeMatch)
67
+ result.grade = parseFloat(gradeMatch[1]);
68
+ const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|Z)/m);
69
+ if (fixMatch) {
70
+ result.fix = fixMatch[1]
71
+ .split("\n")
72
+ .map((line) => line.replace(/^\s*\|?\s*/, ""))
73
+ .filter((line) => line.length > 0)
74
+ .join("\n");
75
+ }
76
+ if (result.id && result.message) {
77
+ return result;
78
+ }
79
+ return null;
80
+ }
81
+ mapSeverity(severity) {
82
+ const lower = severity.toLowerCase();
83
+ if (lower === "error")
84
+ return "error";
85
+ if (lower === "warning")
86
+ return "warning";
87
+ if (lower === "info")
88
+ return "info";
89
+ return "hint";
90
+ }
91
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared types for ast-grep client, parser, and rule manager.
3
+ *
4
+ * Extracted to prevent circular dependencies between:
5
+ * - ast-grep-client.ts
6
+ * - ast-grep-parser.ts
7
+ * - ast-grep-rule-manager.ts
8
+ */
9
+ export {};