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,506 @@
1
+ /**
2
+ * ast-grep NAPI runner for dispatch system
3
+ *
4
+ * Uses @ast-grep/napi for programmatic parsing instead of CLI.
5
+ * Handles TypeScript/JavaScript/CSS/HTML files with YAML rule support.
6
+ *
7
+ * Replaces CLI-based runners for faster performance (100x speedup).
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import type {
14
+ Diagnostic,
15
+ DispatchContext,
16
+ RunnerDefinition,
17
+ RunnerResult,
18
+ } from "../types.js";
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+
22
+ // Lazy load the napi package
23
+ let sg: typeof import("@ast-grep/napi") | undefined;
24
+
25
+ async function loadSg(): Promise<typeof import("@ast-grep/napi") | undefined> {
26
+ if (sg) return sg;
27
+ try {
28
+ sg = await import("@ast-grep/napi");
29
+ return sg;
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ // Supported extensions for NAPI
36
+ const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
37
+
38
+ function canHandle(filePath: string): boolean {
39
+ return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
40
+ }
41
+
42
+ function getLang(filePath: string, sgModule: typeof import("@ast-grep/napi")): any {
43
+ const ext = path.extname(filePath).toLowerCase();
44
+ switch (ext) {
45
+ case ".ts": return sgModule.Lang.TypeScript;
46
+ case ".tsx": return sgModule.Lang.Tsx;
47
+ case ".js":
48
+ case ".jsx": return sgModule.Lang.JavaScript;
49
+ case ".css": return sgModule.Lang.Css;
50
+ case ".html":
51
+ case ".htm": return sgModule.Lang.Html;
52
+ default: return undefined;
53
+ }
54
+ }
55
+
56
+ // YAML rule types
57
+ interface YamlRuleCondition {
58
+ kind?: string;
59
+ pattern?: string;
60
+ regex?: string;
61
+ has?: YamlRuleCondition;
62
+ any?: YamlRuleCondition[];
63
+ all?: YamlRuleCondition[];
64
+ not?: YamlRuleCondition;
65
+ }
66
+
67
+ interface YamlRule {
68
+ id: string;
69
+ language?: string;
70
+ severity?: string;
71
+ message?: string;
72
+ metadata?: { weight?: number; category?: string };
73
+ rule?: YamlRuleCondition;
74
+ }
75
+
76
+ function loadYamlRules(ruleDir: string): YamlRule[] {
77
+ const rules: YamlRule[] = [];
78
+ if (!fs.existsSync(ruleDir)) return rules;
79
+
80
+ const files = fs.readdirSync(ruleDir).filter(f => f.endsWith(".yml"));
81
+
82
+ for (const file of files) {
83
+ try {
84
+ const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
85
+ // Split by --- to handle multiple YAML documents in one file
86
+ const documents = content.split(/^---$/m).filter(d => d.trim());
87
+
88
+ for (const doc of documents) {
89
+ const rule = parseSimpleYaml(doc.trim());
90
+ if (rule && rule.id) {
91
+ rules.push(rule);
92
+ }
93
+ }
94
+ } catch {
95
+ // Skip invalid files
96
+ }
97
+ }
98
+
99
+ return rules;
100
+ }
101
+
102
+ function parseSimpleYaml(content: string): YamlRule | null {
103
+ const lines = content.split("\n");
104
+ const rule: YamlRule = { id: "", metadata: {} };
105
+ let currentSection: "root" | "rule" | "metadata" = "root";
106
+ let sectionStack: Array<{ name: string; indent: number; obj: any }> = [];
107
+ let multilineBuffer: string[] = [];
108
+ let multilineKey = "";
109
+
110
+ function getCurrentObj(): any {
111
+ if (sectionStack.length === 0) return rule;
112
+ return sectionStack[sectionStack.length - 1].obj;
113
+ }
114
+
115
+ function getIndent(line: string): number {
116
+ let count = 0;
117
+ for (const char of line) {
118
+ if (char === " ") count++;
119
+ else if (char === "\t") count += 2;
120
+ else break;
121
+ }
122
+ return count;
123
+ }
124
+
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const line = lines[i];
127
+ const trimmed = line.trim();
128
+ if (!trimmed || trimmed.startsWith("#")) continue;
129
+
130
+ if (trimmed === "---") continue;
131
+
132
+ const indent = getIndent(line);
133
+
134
+ // Pop stack if indent decreased
135
+ while (sectionStack.length > 0 && indent <= sectionStack[sectionStack.length - 1].indent) {
136
+ sectionStack.pop();
137
+ }
138
+
139
+ // Check for multiline continuation
140
+ if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
141
+ multilineBuffer.push(trimmed);
142
+ continue;
143
+ }
144
+
145
+ // Flush multiline buffer
146
+ if (multilineKey && multilineBuffer.length > 0) {
147
+ const value = multilineBuffer.join("\n");
148
+ const current = getCurrentObj();
149
+ if (multilineKey === "pattern" && current) {
150
+ current.pattern = value;
151
+ }
152
+ multilineKey = "";
153
+ multilineBuffer = [];
154
+ }
155
+
156
+ const colonIndex = trimmed.indexOf(":");
157
+ const key = colonIndex > 0 ? trimmed.substring(0, colonIndex).trim() : trimmed;
158
+ const value = colonIndex > 0 ? trimmed.substring(colonIndex + 1).trim() : "";
159
+
160
+ if (key === "id") {
161
+ rule.id = value.replace(/^["']|["']$/g, "");
162
+ } else if (key === "language") {
163
+ rule.language = value;
164
+ } else if (key === "severity") {
165
+ rule.severity = value;
166
+ } else if (key === "message") {
167
+ if (value === "|") {
168
+ multilineKey = "message";
169
+ } else {
170
+ rule.message = value.replace(/^["']|["']$/g, "");
171
+ }
172
+ } else if (key === "metadata") {
173
+ currentSection = "metadata";
174
+ const newObj = {};
175
+ rule.metadata = newObj;
176
+ sectionStack.push({ name: "metadata", indent, obj: newObj });
177
+ } else if (key === "rule") {
178
+ currentSection = "rule";
179
+ const newObj: YamlRuleCondition = {};
180
+ rule.rule = newObj;
181
+ sectionStack.push({ name: "rule", indent, obj: newObj });
182
+ } else if (sectionStack.length > 0) {
183
+ const current = getCurrentObj();
184
+ const currentSectionName = sectionStack[sectionStack.length - 1]?.name;
185
+
186
+ if (key === "weight" && currentSectionName === "metadata") {
187
+ if (!rule.metadata) rule.metadata = {};
188
+ rule.metadata.weight = parseInt(value, 10) || 3;
189
+ } else if (key === "category" && currentSectionName === "metadata") {
190
+ if (!rule.metadata) rule.metadata = {};
191
+ rule.metadata.category = value.replace(/^["']|["']$/g, "");
192
+ } else if (key === "pattern") {
193
+ if (value === "|") {
194
+ multilineKey = "pattern";
195
+ } else {
196
+ // Strip all surrounding quotes (handle nested quotes from YAML)
197
+ let stripped = value;
198
+ while (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length > 1) {
199
+ stripped = stripped.slice(1, -1);
200
+ }
201
+ while (stripped.startsWith("'") && stripped.endsWith("'") && stripped.length > 1) {
202
+ stripped = stripped.slice(1, -1);
203
+ }
204
+ current.pattern = stripped;
205
+ }
206
+ } else if (key === "kind") {
207
+ current.kind = value;
208
+ } else if (key === "regex") {
209
+ // Strip all surrounding quotes
210
+ let stripped = value;
211
+ while (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length > 1) {
212
+ stripped = stripped.slice(1, -1);
213
+ }
214
+ while (stripped.startsWith("'") && stripped.endsWith("'") && stripped.length > 1) {
215
+ stripped = stripped.slice(1, -1);
216
+ }
217
+ current.regex = stripped;
218
+ } else if (key === "has" || key === "not") {
219
+ const newObj: YamlRuleCondition = {};
220
+ current[key] = newObj;
221
+ sectionStack.push({ name: key, indent, obj: newObj });
222
+ } else if (key === "any" || key === "all") {
223
+ if (!current[key]) current[key] = [];
224
+ // Check if next lines with more indent are list items
225
+ let j = i + 1;
226
+ while (j < lines.length) {
227
+ const nextLine = lines[j];
228
+ const nextTrimmed = nextLine.trim();
229
+ if (!nextTrimmed || nextTrimmed.startsWith("#")) {
230
+ j++;
231
+ continue;
232
+ }
233
+ const nextIndent = getIndent(nextLine);
234
+ if (nextIndent <= indent) break;
235
+
236
+ if (nextTrimmed.startsWith("- ")) {
237
+ // New list item
238
+ const itemObj: YamlRuleCondition = {};
239
+ current[key].push(itemObj);
240
+ sectionStack.push({ name: key, indent: nextIndent, obj: itemObj });
241
+ // Parse the item content after "- "
242
+ const itemContent = nextTrimmed.substring(2);
243
+ if (itemContent.includes(":")) {
244
+ const [itemKey, itemVal] = itemContent.split(":", 2);
245
+ if (itemKey.trim() === "pattern") {
246
+ itemObj.pattern = itemVal.trim().replace(/^["']|["']$/g, "");
247
+ } else if (itemKey.trim() === "kind") {
248
+ itemObj.kind = itemVal.trim();
249
+ }
250
+ } else if (itemContent) {
251
+ // Assume it's a pattern
252
+ itemObj.pattern = itemContent.replace(/^["']|["']$/g, "");
253
+ }
254
+ }
255
+ j++;
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // Flush remaining multiline buffer
262
+ if (multilineKey && multilineBuffer.length > 0) {
263
+ const value = multilineBuffer.join("\n");
264
+ const current = getCurrentObj();
265
+ if (multilineKey === "pattern" && current) {
266
+ current.pattern = value;
267
+ } else if (multilineKey === "message") {
268
+ rule.message = value;
269
+ }
270
+ }
271
+
272
+ return rule.id ? rule : null;
273
+ }
274
+
275
+ /**
276
+ * Check if a rule uses structured conditions (has/any/all/not/regex)
277
+ */
278
+ function isStructuredRule(rule: YamlRule): boolean {
279
+ if (!rule.rule) return false;
280
+ return !!(rule.rule.has || rule.rule.any || rule.rule.all || rule.rule.not || rule.rule.regex);
281
+ }
282
+
283
+ /**
284
+ * Execute a structured rule using manual AST traversal
285
+ */
286
+ function executeStructuredRule(
287
+ rootNode: any,
288
+ condition: YamlRuleCondition,
289
+ matches: any[] = []
290
+ ): any[] {
291
+ // Start with finding nodes by kind or pattern
292
+ let candidates: any[] = [];
293
+
294
+ if (condition.pattern) {
295
+ // Use pattern matching via findAll
296
+ try {
297
+ candidates = rootNode.findAll(condition.pattern);
298
+ } catch {
299
+ return matches;
300
+ }
301
+ } else if (condition.kind) {
302
+ // Manual traversal for kind matching
303
+ function findByKind(node: any, kind: string): any[] {
304
+ const results: any[] = [];
305
+ if (node.kind() === kind) {
306
+ results.push(node);
307
+ }
308
+ for (const child of node.children()) {
309
+ results.push(...findByKind(child, kind));
310
+ }
311
+ return results;
312
+ }
313
+ candidates = findByKind(rootNode, condition.kind);
314
+ } else {
315
+ // No kind or pattern, search all nodes
316
+ function getAllNodes(node: any): any[] {
317
+ const results = [node];
318
+ for (const child of node.children()) {
319
+ results.push(...getAllNodes(child));
320
+ }
321
+ return results;
322
+ }
323
+ candidates = getAllNodes(rootNode);
324
+ }
325
+
326
+ // Filter candidates by conditions
327
+ for (const candidate of candidates) {
328
+ let matchesCondition = true;
329
+
330
+ // Check 'has' condition
331
+ if (condition.has && matchesCondition) {
332
+ const subMatches = executeStructuredRule(candidate, condition.has, []);
333
+ if (subMatches.length === 0) matchesCondition = false;
334
+ }
335
+
336
+ // Check 'not' condition
337
+ if (condition.not && matchesCondition) {
338
+ const subMatches = executeStructuredRule(candidate, condition.not, []);
339
+ if (subMatches.length > 0) matchesCondition = false;
340
+ }
341
+
342
+ // Check 'any' condition (at least one must match)
343
+ if (condition.any && matchesCondition) {
344
+ let anyMatches = false;
345
+ for (const subCondition of condition.any) {
346
+ const subMatches = executeStructuredRule(candidate, subCondition, []);
347
+ if (subMatches.length > 0) {
348
+ anyMatches = true;
349
+ break;
350
+ }
351
+ }
352
+ if (!anyMatches) matchesCondition = false;
353
+ }
354
+
355
+ // Check 'all' condition (all must match)
356
+ if (condition.all && matchesCondition) {
357
+ for (const subCondition of condition.all) {
358
+ const subMatches = executeStructuredRule(candidate, subCondition, []);
359
+ if (subMatches.length === 0) {
360
+ matchesCondition = false;
361
+ break;
362
+ }
363
+ }
364
+ }
365
+
366
+ // Check 'regex' condition
367
+ if (condition.regex && matchesCondition) {
368
+ const text = candidate.text();
369
+ const regex = new RegExp(condition.regex);
370
+ if (!regex.test(text)) matchesCondition = false;
371
+ }
372
+
373
+ if (matchesCondition) {
374
+ matches.push(candidate);
375
+ }
376
+ }
377
+
378
+ return matches;
379
+ }
380
+
381
+ const astGrepNapiRunner: RunnerDefinition = {
382
+ id: "ast-grep-napi",
383
+ appliesTo: ["jsts"], // TypeScript/JavaScript only
384
+ priority: 15, // Run early (after type checkers, before other linters)
385
+ enabledByDefault: true,
386
+ skipTestFiles: true,
387
+
388
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
389
+ const startTime = Date.now();
390
+
391
+ if (!canHandle(ctx.filePath)) {
392
+ return { status: "skipped", diagnostics: [], semantic: "none" };
393
+ }
394
+
395
+ const sgModule = await loadSg();
396
+ if (!sgModule) {
397
+ return { status: "skipped", diagnostics: [], semantic: "none" };
398
+ }
399
+
400
+ if (!fs.existsSync(ctx.filePath)) {
401
+ return { status: "skipped", diagnostics: [], semantic: "none" };
402
+ }
403
+
404
+ const lang = getLang(ctx.filePath, sgModule);
405
+ if (!lang) {
406
+ return { status: "skipped", diagnostics: [], semantic: "none" };
407
+ }
408
+
409
+ const content = fs.readFileSync(ctx.filePath, "utf-8");
410
+
411
+ let root: import("@ast-grep/napi").SgRoot;
412
+ try {
413
+ root = sgModule.parse(lang, content);
414
+ } catch {
415
+ return { status: "skipped", diagnostics: [], semantic: "none" };
416
+ }
417
+
418
+ const diagnostics: Diagnostic[] = [];
419
+ const rootNode = root.root();
420
+
421
+ // Load rules from ts-slop-rules only (complementary to ast-grep CLI)
422
+ const ruleDirs = [
423
+ path.join(process.cwd(), "rules/ts-slop-rules/rules"),
424
+ ];
425
+
426
+ for (const ruleDir of ruleDirs) {
427
+ const rules = loadYamlRules(ruleDir);
428
+
429
+ for (const rule of rules) {
430
+ // Skip rules for different languages (case-insensitive)
431
+ const lang = rule.language?.toLowerCase();
432
+ if (lang && lang !== "typescript" && lang !== "javascript") {
433
+ continue;
434
+ }
435
+
436
+ try {
437
+ let matches: any[] = [];
438
+
439
+ if (isStructuredRule(rule) && rule.rule) {
440
+ // Use structured rule execution
441
+ matches = executeStructuredRule(rootNode, rule.rule, []);
442
+ } else if (rule.rule?.pattern || rule.rule?.kind) {
443
+ // Use simple pattern matching
444
+ const pattern = rule.rule.pattern || rule.rule.kind;
445
+ if (pattern) {
446
+ try {
447
+ matches = rootNode.findAll(pattern);
448
+ } catch {
449
+ // Pattern failed, try manual traversal for kind
450
+ if (rule.rule.kind) {
451
+ function findByKind(node: any, kind: string): any[] {
452
+ const results: any[] = [];
453
+ if (node.kind() === kind) results.push(node);
454
+ for (const child of node.children()) {
455
+ results.push(...findByKind(child, kind));
456
+ }
457
+ return results;
458
+ }
459
+ matches = findByKind(rootNode, rule.rule.kind);
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ for (const match of matches) {
466
+ const range = match.range();
467
+ const weight = rule.metadata?.weight || 3;
468
+ const severity = weight >= 4 ? "error" : "warning";
469
+
470
+ diagnostics.push({
471
+ id: `ast-grep-napi-${range.start.line}-${rule.id}`,
472
+ message: `[${rule.metadata?.category || "slop"}] ${rule.message || rule.id}`,
473
+ filePath: ctx.filePath,
474
+ line: range.start.line + 1,
475
+ column: range.start.column + 1,
476
+ severity,
477
+ semantic: severity === "error" ? "blocking" : "warning",
478
+ tool: "ast-grep-napi",
479
+ rule: rule.id,
480
+ fixable: false,
481
+ });
482
+ }
483
+ } catch {
484
+ // Rule failed, skip
485
+ }
486
+ }
487
+ }
488
+
489
+ const elapsed = Date.now() - startTime;
490
+ if (diagnostics.length > 0 || elapsed > 50) {
491
+ console.error(`[ast-grep-napi] ${ctx.filePath}: ${elapsed}ms, ${diagnostics.length} issues`);
492
+ }
493
+
494
+ if (diagnostics.length === 0) {
495
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
496
+ }
497
+
498
+ return {
499
+ status: "failed",
500
+ diagnostics,
501
+ semantic: "warning",
502
+ };
503
+ },
504
+ };
505
+
506
+ export default astGrepNapiRunner;
@@ -6,8 +6,30 @@
6
6
  * - async/await issues
7
7
  * - security anti-patterns
8
8
  */
9
- import { spawnSync } from "node:child_process";
10
9
  import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { safeSpawn } from "../../safe-spawn.js";
12
+ // Simple YAML fix: field extractor
13
+ function extractFixFromRule(ruleId, ruleDir) {
14
+ try {
15
+ const rulePath = `${ruleDir}/${ruleId}.yml`;
16
+ if (!fs.existsSync(rulePath))
17
+ return undefined;
18
+ const content = fs.readFileSync(rulePath, "utf-8");
19
+ const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|\Z)/m);
20
+ if (fixMatch) {
21
+ return fixMatch[1]
22
+ .split("\n")
23
+ .map((line) => line.replace(/^\s*\|?\s*/, ""))
24
+ .filter((line) => line.length > 0)
25
+ .join("\n");
26
+ }
27
+ }
28
+ catch {
29
+ // Ignore errors
30
+ }
31
+ return undefined;
32
+ }
11
33
  const astGrepRunner = {
12
34
  id: "ast-grep",
13
35
  appliesTo: ["jsts", "python", "go", "rust", "cxx"],
@@ -15,11 +37,9 @@ const astGrepRunner = {
15
37
  enabledByDefault: false,
16
38
  skipTestFiles: true, // Many rules are noisy in tests
17
39
  async run(ctx) {
18
- // Check if ast-grep is available
19
- const check = spawnSync("sg", ["--version"], {
20
- encoding: "utf-8",
40
+ // Check if ast-grep is available (use npx for local installs)
41
+ const check = safeSpawn("npx", ["sg", "--version"], {
21
42
  timeout: 5000,
22
- shell: true,
23
43
  });
24
44
  if (check.error || check.status !== 0) {
25
45
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -29,19 +49,17 @@ const astGrepRunner = {
29
49
  if (!configPath) {
30
50
  return { status: "skipped", diagnostics: [], semantic: "none" };
31
51
  }
32
- // Run ast-grep scan on the file
33
- const args = ["scan", "--config", configPath, "--json", ctx.filePath];
34
- const result = spawnSync("sg", args, {
35
- encoding: "utf-8",
52
+ // Run ast-grep scan on the file (use npx for local installs)
53
+ const args = ["sg", "scan", "--config", configPath, "--json", ctx.filePath];
54
+ const result = safeSpawn("npx", args, {
36
55
  timeout: 30000,
37
- shell: true,
38
56
  });
39
57
  const raw = result.stdout + result.stderr;
40
58
  if (result.status === 0 && !raw.trim()) {
41
59
  return { status: "succeeded", diagnostics: [], semantic: "none" };
42
60
  }
43
61
  // Parse results
44
- const diagnostics = parseAstGrepOutput(raw, ctx.filePath);
62
+ const diagnostics = parseAstGrepOutput(raw, ctx.filePath, configPath);
45
63
  if (diagnostics.length === 0) {
46
64
  return { status: "succeeded", diagnostics: [], semantic: "none" };
47
65
  }
@@ -66,27 +84,52 @@ function findAstGrepConfig(cwd) {
66
84
  }
67
85
  return undefined;
68
86
  }
69
- function parseAstGrepOutput(raw, filePath) {
87
+ function parseAstGrepOutput(raw, filePath, _configPath) {
70
88
  const diagnostics = [];
71
89
  // Try to parse as JSON
90
+ // Determine rule directory for fix: extraction
91
+ const ruleDir = _configPath
92
+ ? path.dirname(_configPath).replace("/.sgconfig.yml", "/rules")
93
+ : path.join(process.cwd(), "rules", "ast-grep-rules", "rules");
72
94
  try {
73
95
  const parsed = JSON.parse(raw);
74
96
  if (Array.isArray(parsed)) {
75
97
  for (const item of parsed) {
76
98
  const line = item.range?.start?.line || 1;
99
+ const ruleId = item.rule || "unknown";
100
+ // Build message with inline fix suggestion
101
+ let message = item.message || item.lines || "";
102
+ let fixSuggestion;
103
+ if (item.replacement) {
104
+ // Show the actual code change inline in the message
105
+ const replacementPreview = item.replacement.length > 40
106
+ ? `${item.replacement.substring(0, 40)}...`
107
+ : item.replacement;
108
+ message += `\nšŸ’” Suggested fix: → "${replacementPreview}"`;
109
+ fixSuggestion = `Replace with: ${item.replacement}`;
110
+ }
111
+ else {
112
+ // Try to get fix: from rule YAML
113
+ const ruleFix = extractFixFromRule(ruleId, ruleDir);
114
+ if (ruleFix) {
115
+ const fixPreview = ruleFix.length > 60
116
+ ? `${ruleFix.substring(0, 60)}...`
117
+ : ruleFix;
118
+ message += `\nšŸ’” Suggested fix:\n${fixPreview}`;
119
+ fixSuggestion = ruleFix;
120
+ }
121
+ }
77
122
  diagnostics.push({
78
- id: `ast-grep-${line}-${item.rule || "unknown"}`,
79
- message: item.message || item.lines || "",
123
+ id: `ast-grep-${line}-${ruleId}`,
124
+ message,
80
125
  filePath,
81
126
  line,
82
127
  severity: item.severity === "error" ? "error" : "warning",
83
128
  semantic: item.severity === "error" ? "blocking" : "warning",
84
129
  tool: "ast-grep",
85
- rule: item.rule || "unknown",
86
- fixable: !!item.replacement,
87
- fixSuggestion: item.replacement
88
- ? "Run `sg fix` to auto-fix"
89
- : undefined,
130
+ rule: ruleId,
131
+ fixable: !!item.replacement || !!fixSuggestion,
132
+ fixSuggestion,
90
133
  });
91
134
  }
92
135
  }