pi-lens 3.1.2 → 3.2.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 (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -9,19 +9,23 @@
9
9
 
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
- import { fileURLToPath } from "node:url";
13
12
  import type {
14
13
  Diagnostic,
15
14
  DispatchContext,
16
15
  RunnerDefinition,
17
16
  RunnerResult,
18
17
  } from "../types.js";
19
-
20
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ import {
19
+ calculateRuleComplexity,
20
+ isStructuredRule,
21
+ loadYamlRules,
22
+ MAX_BLOCKING_RULE_COMPLEXITY,
23
+ type YamlRule,
24
+ type YamlRuleCondition,
25
+ } from "./yaml-rule-parser.js";
21
26
 
22
27
  // Lazy load the napi package
23
28
  let sg: typeof import("@ast-grep/napi") | undefined;
24
- let _sgLoadError: Error | undefined;
25
29
  let sgLoadAttempted = false;
26
30
 
27
31
  async function loadSg(): Promise<typeof import("@ast-grep/napi") | undefined> {
@@ -31,52 +35,9 @@ async function loadSg(): Promise<typeof import("@ast-grep/napi") | undefined> {
31
35
  try {
32
36
  sg = await import("@ast-grep/napi");
33
37
  return sg;
34
- } catch (err) {
35
- _sgLoadError = err instanceof Error ? err : new Error(String(err));
36
- return undefined;
37
- }
38
- }
39
-
40
- // --- Rule Caching ---
41
- // Cache parsed YAML rules to avoid re-parsing on every file edit
42
- interface CachedRules {
43
- rules: YamlRule[];
44
- mtime: number;
45
- }
46
-
47
- const rulesCache = new Map<string, CachedRules>();
48
-
49
- /** Get cached rules or reload if cache is stale */
50
- function _getCachedRules(ruleDir: string): YamlRule[] {
51
- // Check if directory exists
52
- if (!fs.existsSync(ruleDir)) {
53
- return [];
54
- }
55
-
56
- // Get directory mtime to detect changes
57
- let currentMtime = 0;
58
- try {
59
- const stats = fs.statSync(ruleDir);
60
- currentMtime = stats.mtimeMs;
61
38
  } catch {
62
- return [];
63
- }
64
-
65
- // Check cache
66
- const cached = rulesCache.get(ruleDir);
67
- if (cached && cached.mtime === currentMtime) {
68
- return cached.rules;
39
+ return undefined;
69
40
  }
70
-
71
- // Load and cache
72
- const rules = loadYamlRulesUncached(ruleDir);
73
- rulesCache.set(ruleDir, { rules, mtime: currentMtime });
74
- return rules;
75
- }
76
-
77
- /** Clear rules cache (useful for testing or when rules change) */
78
- export function clearRulesCache(): void {
79
- rulesCache.clear();
80
41
  }
81
42
 
82
43
  // Supported extensions for NAPI
@@ -88,57 +49,17 @@ const MAX_MATCHES_PER_RULE = 10;
88
49
  /** Maximum total diagnostics per file to prevent output spam */
89
50
  const MAX_TOTAL_DIAGNOSTICS = 50;
90
51
 
91
- /** Threshold for warning about overly broad patterns that match everything */
92
- const _EXCESSIVE_MATCHES_THRESHOLD = 50;
93
-
94
- /** Maximum recursion depth for structured rule execution to prevent stack overflow */
95
- const _MAX_RECURSION_DEPTH = 10;
96
-
97
52
  /** Maximum AST depth to traverse to prevent stack overflow on deeply nested files */
98
- const _MAX_AST_DEPTH = 50;
53
+ const MAX_AST_DEPTH = 50;
99
54
 
100
55
  /** Maximum recursion depth for structured rule execution */
101
- const _MAX_RULE_DEPTH = 5;
102
-
103
- /** Overly broad patterns that match everything (cause false positive explosions) */
104
- const OVERLY_BROAD_PATTERNS = [
105
- "$NAME", // Matches every identifier
106
- "$FIELD", // Matches every field access
107
- "$_", // Matches every node
108
- "$X", // Common catch-all variable
109
- "$VAR", // Common catch-all variable
110
- "$EXPR", // Common catch-all expression
111
- ];
112
-
113
- /** Check if a pattern is overly broad and will cause false positive explosions */
114
- function isOverlyBroadPattern(pattern: string | undefined): boolean {
115
- if (!pattern) return false;
116
- // Check exact matches and simple patterns that are just variables
117
- if (OVERLY_BROAD_PATTERNS.includes(pattern.trim())) return true;
118
- // Check if pattern is just a single meta-variable (starts with $ and has no other content)
119
- if (/^\$[A-Z_]+$/i.test(pattern.trim())) return true;
120
- return false;
121
- }
122
-
123
- /** Check if a rule condition is valid (not empty) */
124
- function _isValidCondition(condition: YamlRuleCondition | undefined): boolean {
125
- if (!condition) return false;
126
- // Check for empty 'all' or 'any' arrays
127
- if (condition.all !== undefined && condition.all.length === 0) return false;
128
- if (condition.any !== undefined && condition.any.length === 0) return false;
129
- // Check for overly broad pattern
130
- if (isOverlyBroadPattern(condition.pattern)) return false;
131
- return true;
132
- }
56
+ const MAX_RULE_DEPTH = 5;
133
57
 
134
58
  function canHandle(filePath: string): boolean {
135
59
  return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
136
60
  }
137
61
 
138
- function getLang(
139
- filePath: string,
140
- sgModule: typeof import("@ast-grep/napi"),
141
- ): any {
62
+ function getLang(filePath: string, sgModule: typeof import("@ast-grep/napi")) {
142
63
  const ext = path.extname(filePath).toLowerCase();
143
64
  switch (ext) {
144
65
  case ".ts":
@@ -158,305 +79,42 @@ function getLang(
158
79
  }
159
80
  }
160
81
 
161
- // YAML rule types
162
- interface YamlRuleCondition {
163
- kind?: string;
164
- pattern?: string;
165
- regex?: string;
166
- has?: YamlRuleCondition;
167
- any?: YamlRuleCondition[];
168
- all?: YamlRuleCondition[];
169
- not?: YamlRuleCondition;
170
- }
171
-
172
- interface YamlRule {
173
- id: string;
174
- language?: string;
175
- severity?: string;
176
- message?: string;
177
- metadata?: { weight?: number; category?: string };
178
- rule?: YamlRuleCondition;
179
- }
180
-
181
- function loadYamlRulesUncached(ruleDir: string): YamlRule[] {
182
- const rules: YamlRule[] = [];
183
- if (!fs.existsSync(ruleDir)) return rules;
184
-
185
- const files = fs.readdirSync(ruleDir).filter((f) => f.endsWith(".yml"));
186
-
187
- for (const file of files) {
188
- try {
189
- const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
190
- // Split by --- to handle multiple YAML documents in one file
191
- const documents = content.split(/^---$/m).filter((d) => d.trim());
192
-
193
- for (const doc of documents) {
194
- const rule = parseSimpleYaml(doc.trim());
195
- if (rule?.id) {
196
- rules.push(rule);
197
- }
198
- }
199
- } catch {
200
- // Skip invalid files
201
- }
202
- }
203
-
204
- return rules;
205
- }
206
-
207
- /** Load rules with caching - use this for production */
208
- function loadYamlRules(ruleDir: string): YamlRule[] {
209
- return _getCachedRules(ruleDir);
210
- }
211
-
212
- function parseSimpleYaml(content: string): YamlRule | null {
213
- const lines = content.split("\n");
214
- const rule: YamlRule = { id: "", metadata: {} };
215
- let _currentSection: "root" | "rule" | "metadata" = "root";
216
- const sectionStack: Array<{ name: string; indent: number; obj: any }> = [];
217
- let multilineBuffer: string[] = [];
218
- let multilineKey = "";
219
-
220
- function getCurrentObj(): any {
221
- if (sectionStack.length === 0) return rule;
222
- return sectionStack[sectionStack.length - 1].obj;
223
- }
224
-
225
- function getIndent(line: string): number {
226
- let count = 0;
227
- for (const char of line) {
228
- if (char === " ") count++;
229
- else if (char === "\t") count += 2;
230
- else break;
231
- }
232
- return count;
233
- }
234
-
235
- for (let i = 0; i < lines.length; i++) {
236
- const line = lines[i];
237
- const trimmed = line.trim();
238
- if (!trimmed || trimmed.startsWith("#")) continue;
239
-
240
- if (trimmed === "---") continue;
241
-
242
- const indent = getIndent(line);
243
-
244
- // Pop stack if indent decreased
245
- while (
246
- sectionStack.length > 0 &&
247
- indent <= sectionStack[sectionStack.length - 1].indent
248
- ) {
249
- sectionStack.pop();
250
- }
251
-
252
- // Check for multiline continuation
253
- if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
254
- multilineBuffer.push(trimmed);
255
- continue;
256
- }
257
-
258
- // Flush multiline buffer
259
- if (multilineKey && multilineBuffer.length > 0) {
260
- const value = multilineBuffer.join("\n");
261
- const current = getCurrentObj();
262
- if (multilineKey === "pattern" && current) {
263
- current.pattern = value;
264
- }
265
- multilineKey = "";
266
- multilineBuffer = [];
267
- }
268
-
269
- const colonIndex = trimmed.indexOf(":");
270
- const key =
271
- colonIndex > 0 ? trimmed.substring(0, colonIndex).trim() : trimmed;
272
- const value =
273
- colonIndex > 0 ? trimmed.substring(colonIndex + 1).trim() : "";
274
-
275
- if (key === "id") {
276
- rule.id = value.replace(/^["']|["']$/g, "");
277
- } else if (key === "language") {
278
- rule.language = value;
279
- } else if (key === "severity") {
280
- rule.severity = value;
281
- } else if (key === "message") {
282
- if (value === "|") {
283
- multilineKey = "message";
284
- } else {
285
- rule.message = value.replace(/^["']|["']$/g, "");
286
- }
287
- } else if (key === "metadata") {
288
- _currentSection = "metadata";
289
- const newObj = {};
290
- rule.metadata = newObj;
291
- sectionStack.push({ name: "metadata", indent, obj: newObj });
292
- } else if (key === "rule") {
293
- _currentSection = "rule";
294
- const newObj: YamlRuleCondition = {};
295
- rule.rule = newObj;
296
- sectionStack.push({ name: "rule", indent, obj: newObj });
297
- } else if (sectionStack.length > 0) {
298
- const current = getCurrentObj();
299
- const currentSectionName = sectionStack[sectionStack.length - 1]?.name;
300
-
301
- if (key === "weight" && currentSectionName === "metadata") {
302
- if (!rule.metadata) rule.metadata = {};
303
- rule.metadata.weight = parseInt(value, 10) || 3;
304
- } else if (key === "category" && currentSectionName === "metadata") {
305
- if (!rule.metadata) rule.metadata = {};
306
- rule.metadata.category = value.replace(/^["']|["']$/g, "");
307
- } else if (key === "pattern") {
308
- if (value === "|") {
309
- multilineKey = "pattern";
310
- } else {
311
- // Strip all surrounding quotes (handle nested quotes from YAML)
312
- let stripped = value;
313
- while (
314
- stripped.startsWith('"') &&
315
- stripped.endsWith('"') &&
316
- stripped.length > 1
317
- ) {
318
- stripped = stripped.slice(1, -1);
319
- }
320
- while (
321
- stripped.startsWith("'") &&
322
- stripped.endsWith("'") &&
323
- stripped.length > 1
324
- ) {
325
- stripped = stripped.slice(1, -1);
326
- }
327
- current.pattern = stripped;
328
- }
329
- } else if (key === "kind") {
330
- current.kind = value;
331
- } else if (key === "regex") {
332
- // Strip all surrounding quotes
333
- let stripped = value;
334
- while (
335
- stripped.startsWith('"') &&
336
- stripped.endsWith('"') &&
337
- stripped.length > 1
338
- ) {
339
- stripped = stripped.slice(1, -1);
340
- }
341
- while (
342
- stripped.startsWith("'") &&
343
- stripped.endsWith("'") &&
344
- stripped.length > 1
345
- ) {
346
- stripped = stripped.slice(1, -1);
347
- }
348
- current.regex = stripped;
349
- } else if (key === "has" || key === "not") {
350
- const newObj: YamlRuleCondition = {};
351
- current[key] = newObj;
352
- sectionStack.push({ name: key, indent, obj: newObj });
353
- } else if (key === "any" || key === "all") {
354
- if (!current[key]) current[key] = [];
355
- // Check if next lines with more indent are list items
356
- let j = i + 1;
357
- while (j < lines.length) {
358
- const nextLine = lines[j];
359
- const nextTrimmed = nextLine.trim();
360
- if (!nextTrimmed || nextTrimmed.startsWith("#")) {
361
- j++;
362
- continue;
363
- }
364
- const nextIndent = getIndent(nextLine);
365
- if (nextIndent <= indent) break;
366
-
367
- if (nextTrimmed.startsWith("- ")) {
368
- // New list item
369
- const itemObj: YamlRuleCondition = {};
370
- current[key].push(itemObj);
371
- sectionStack.push({ name: key, indent: nextIndent, obj: itemObj });
372
- // Parse the item content after "- "
373
- const itemContent = nextTrimmed.substring(2);
374
- if (itemContent.includes(":")) {
375
- const [itemKey, itemVal] = itemContent.split(":", 2);
376
- if (itemKey.trim() === "pattern") {
377
- itemObj.pattern = itemVal.trim().replace(/^["']|["']$/g, "");
378
- } else if (itemKey.trim() === "kind") {
379
- itemObj.kind = itemVal.trim();
380
- }
381
- } else if (itemContent) {
382
- // Assume it's a pattern
383
- itemObj.pattern = itemContent.replace(/^["']|["']$/g, "");
384
- }
385
- }
386
- j++;
387
- }
388
- }
389
- }
390
- }
391
-
392
- // Flush remaining multiline buffer
393
- if (multilineKey && multilineBuffer.length > 0) {
394
- const value = multilineBuffer.join("\n");
395
- const current = getCurrentObj();
396
- if (multilineKey === "pattern" && current) {
397
- current.pattern = value;
398
- } else if (multilineKey === "message") {
399
- rule.message = value;
400
- }
401
- }
402
-
403
- return rule.id ? rule : null;
404
- }
405
-
406
- /**
407
- * Check if a rule uses structured conditions (has/any/all/not/regex)
408
- */
409
- function isStructuredRule(rule: YamlRule): boolean {
410
- if (!rule.rule) return false;
411
- return !!(
412
- rule.rule.has ||
413
- rule.rule.any ||
414
- rule.rule.all ||
415
- rule.rule.not ||
416
- rule.rule.regex
417
- );
418
- }
419
-
420
82
  /**
421
83
  * Execute a structured rule using manual AST traversal
422
84
  */
423
85
  function executeStructuredRule(
424
86
  rootNode: any,
425
87
  condition: YamlRuleCondition,
426
- matches: any[] = [],
88
+ matches: unknown[] = [],
427
89
  depth = 0,
428
- ): any[] {
429
- // Prevent infinite recursion from nested rules
430
- if (depth > _MAX_RULE_DEPTH) {
431
- return matches;
432
- }
90
+ ): unknown[] {
91
+ if (depth > MAX_RULE_DEPTH) return matches;
433
92
 
434
- // Start with finding nodes by kind or pattern
435
- let candidates: any[] = [];
93
+ let candidates: unknown[] = [];
436
94
 
437
95
  if (condition.pattern) {
438
- // Use pattern matching via findAll
439
96
  try {
440
97
  candidates = rootNode.findAll(condition.pattern);
441
98
  } catch {
442
99
  return matches;
443
100
  }
444
101
  } else if (condition.kind) {
445
- // Manual traversal for kind matching with depth limit
446
102
  candidates = findByKind(rootNode, condition.kind, 0);
447
103
  } else {
448
- // No kind or pattern, search all nodes with depth limit
449
104
  candidates = getAllNodes(rootNode, 0);
450
105
  }
451
106
 
452
- // Filter candidates by conditions
453
107
  for (const candidate of candidates) {
108
+ const node = candidate as {
109
+ text(): string;
110
+ kind(): string;
111
+ children(): unknown[];
112
+ };
454
113
  let matchesCondition = true;
455
114
 
456
- // Check 'has' condition
457
115
  if (condition.has && matchesCondition) {
458
116
  const subMatches = executeStructuredRule(
459
- candidate,
117
+ node,
460
118
  condition.has,
461
119
  [],
462
120
  depth + 1,
@@ -464,10 +122,9 @@ function executeStructuredRule(
464
122
  if (subMatches.length === 0) matchesCondition = false;
465
123
  }
466
124
 
467
- // Check 'not' condition
468
125
  if (condition.not && matchesCondition) {
469
126
  const subMatches = executeStructuredRule(
470
- candidate,
127
+ node,
471
128
  condition.not,
472
129
  [],
473
130
  depth + 1,
@@ -475,12 +132,11 @@ function executeStructuredRule(
475
132
  if (subMatches.length > 0) matchesCondition = false;
476
133
  }
477
134
 
478
- // Check 'any' condition (at least one must match)
479
135
  if (condition.any && matchesCondition) {
480
136
  let anyMatches = false;
481
137
  for (const subCondition of condition.any) {
482
138
  const subMatches = executeStructuredRule(
483
- candidate,
139
+ node,
484
140
  subCondition,
485
141
  [],
486
142
  depth + 1,
@@ -493,11 +149,10 @@ function executeStructuredRule(
493
149
  if (!anyMatches) matchesCondition = false;
494
150
  }
495
151
 
496
- // Check 'all' condition (all must match)
497
152
  if (condition.all && matchesCondition) {
498
153
  for (const subCondition of condition.all) {
499
154
  const subMatches = executeStructuredRule(
500
- candidate,
155
+ node,
501
156
  subCondition,
502
157
  [],
503
158
  depth + 1,
@@ -509,20 +164,18 @@ function executeStructuredRule(
509
164
  }
510
165
  }
511
166
 
512
- // Check 'regex' condition with error handling
513
167
  if (condition.regex && matchesCondition) {
514
168
  try {
515
- const text = candidate.text();
169
+ const text = node.text();
516
170
  const regex = new RegExp(condition.regex);
517
171
  if (!regex.test(text)) matchesCondition = false;
518
172
  } catch {
519
- // Invalid regex, skip this condition
520
173
  matchesCondition = false;
521
174
  }
522
175
  }
523
176
 
524
177
  if (matchesCondition) {
525
- matches.push(candidate);
178
+ matches.push(node);
526
179
  }
527
180
  }
528
181
 
@@ -532,14 +185,10 @@ function executeStructuredRule(
532
185
  /**
533
186
  * Find all nodes of a specific kind with depth limit
534
187
  */
535
- function findByKind(node: any, kind: string, currentDepth: number): any[] {
536
- if (currentDepth > _MAX_AST_DEPTH) {
537
- return [];
538
- }
539
- const results: any[] = [];
540
- if (node.kind() === kind) {
541
- results.push(node);
542
- }
188
+ function findByKind(node: any, kind: string, currentDepth: number): unknown[] {
189
+ if (currentDepth > MAX_AST_DEPTH) return [];
190
+ const results: unknown[] = [];
191
+ if (node.kind() === kind) results.push(node);
543
192
  for (const child of node.children()) {
544
193
  results.push(...findByKind(child, kind, currentDepth + 1));
545
194
  }
@@ -549,10 +198,8 @@ function findByKind(node: any, kind: string, currentDepth: number): any[] {
549
198
  /**
550
199
  * Get all nodes with depth limit to prevent stack overflow
551
200
  */
552
- function getAllNodes(node: any, currentDepth: number): any[] {
553
- if (currentDepth > _MAX_AST_DEPTH) {
554
- return [];
555
- }
201
+ function getAllNodes(node: any, currentDepth: number): unknown[] {
202
+ if (currentDepth > MAX_AST_DEPTH) return [];
556
203
  const results = [node];
557
204
  for (const child of node.children()) {
558
205
  results.push(...getAllNodes(child, currentDepth + 1));
@@ -560,11 +207,15 @@ function getAllNodes(node: any, currentDepth: number): any[] {
560
207
  return results;
561
208
  }
562
209
 
210
+ // --- Runner Definition ---
211
+
563
212
  const astGrepNapiRunner: RunnerDefinition = {
564
213
  id: "ast-grep-napi",
565
- appliesTo: ["jsts"], // TypeScript/JavaScript only
566
- priority: 15, // Run early (after type checkers, before other linters)
567
- enabledByDefault: true,
214
+ appliesTo: ["jsts"],
215
+ priority: 15,
216
+ // Post-write disabled in plan.ts (removed from TOOL_PLANS.jsts.groups).
217
+ // Still enabled for /lens-booboo via FULL_LINT_PLANS.
218
+ enabledByDefault: false,
568
219
  skipTestFiles: true,
569
220
 
570
221
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -586,10 +237,8 @@ const astGrepNapiRunner: RunnerDefinition = {
586
237
  return { status: "skipped", diagnostics: [], semantic: "none" };
587
238
  }
588
239
 
589
- // Check file size to avoid parsing extremely large files
590
240
  const stats = fs.statSync(ctx.filePath);
591
- const MAX_FILE_SIZE = 1024 * 1024; // 1MB
592
- if (stats.size > MAX_FILE_SIZE) {
241
+ if (stats.size > 1024 * 1024) {
593
242
  return { status: "skipped", diagnostics: [], semantic: "none" };
594
243
  }
595
244
 
@@ -602,13 +251,11 @@ const astGrepNapiRunner: RunnerDefinition = {
602
251
 
603
252
  let root: import("@ast-grep/napi").SgRoot;
604
253
  try {
605
- // Use the language object's parse method directly
606
254
  root = lang.parse(content);
607
255
  } catch {
608
256
  return { status: "skipped", diagnostics: [], semantic: "none" };
609
257
  }
610
258
 
611
- const diagnostics: Diagnostic[] = [];
612
259
  let rootNode: any;
613
260
  try {
614
261
  rootNode = root.root();
@@ -616,77 +263,61 @@ const astGrepNapiRunner: RunnerDefinition = {
616
263
  return { status: "skipped", diagnostics: [], semantic: "none" };
617
264
  }
618
265
 
619
- // CONSOLIDATED: Use ast-grep-rules (unified with CLI tools)
620
- // Includes both security/architecture rules + slop patterns
266
+ const diagnostics: Diagnostic[] = [];
267
+
621
268
  const ruleDirs = [
622
269
  path.join(process.cwd(), "rules/ast-grep-rules/rules"),
623
- path.join(process.cwd(), "rules/ast-grep-rules"), // For slop-patterns.yml
270
+ path.join(process.cwd(), "rules/ast-grep-rules"),
624
271
  ];
625
272
 
626
273
  for (const ruleDir of ruleDirs) {
627
274
  let rules: YamlRule[];
628
275
  try {
629
- rules = loadYamlRules(ruleDir);
276
+ rules = loadYamlRules(ruleDir, ctx.blockingOnly ? "error" : undefined);
630
277
  } catch {
631
- continue; // Skip this rule directory on error
278
+ continue;
632
279
  }
633
280
 
634
281
  for (const rule of rules) {
635
- // Skip rules for different languages (case-insensitive)
636
282
  const lang = rule.language?.toLowerCase();
637
283
  if (lang && lang !== "typescript" && lang !== "javascript") {
638
284
  continue;
639
285
  }
640
286
 
641
- // When blockingOnly is set, only run BLOCKING rules (severity: error)
642
- if (ctx.blockingOnly && rule.severity !== "error") {
643
- continue;
287
+ if (ctx.blockingOnly && rule.rule) {
288
+ const complexity = calculateRuleComplexity(rule.rule);
289
+ if (complexity > MAX_BLOCKING_RULE_COMPLEXITY) {
290
+ continue;
291
+ }
644
292
  }
645
293
 
646
294
  try {
647
- let matches: any[] = [];
295
+ let matches: unknown[] = [];
648
296
 
649
297
  if (isStructuredRule(rule) && rule.rule) {
650
- // Use structured rule execution
651
298
  matches = executeStructuredRule(rootNode, rule.rule, []);
652
299
  } else if (rule.rule?.pattern || rule.rule?.kind) {
653
- // Use simple pattern matching
654
300
  const pattern = rule.rule.pattern || rule.rule.kind;
655
301
  if (pattern) {
656
302
  try {
657
303
  matches = rootNode.findAll(pattern);
658
304
  } catch {
659
- // Pattern failed, try manual traversal for kind
660
305
  if (rule.rule.kind) {
661
- const findByKindLocal = (
662
- node: any,
663
- kind: string,
664
- depth = 0,
665
- ): any[] => {
666
- if (depth > _MAX_AST_DEPTH) return [];
667
- const results: any[] = [];
668
- if (node.kind() === kind) results.push(node);
669
- for (const child of node.children()) {
670
- results.push(...findByKindLocal(child, kind, depth + 1));
671
- }
672
- return results;
673
- };
674
- matches = findByKindLocal(rootNode, rule.rule.kind);
306
+ matches = findByKind(rootNode, rule.rule.kind, 0);
675
307
  }
676
308
  }
677
309
  }
678
310
  }
679
311
 
680
- // Limit matches per rule to prevent excessive false positives
681
312
  const limitedMatches = matches.slice(0, MAX_MATCHES_PER_RULE);
682
313
 
683
314
  for (const match of limitedMatches) {
684
- // Skip if we've hit the total diagnostic limit
685
- if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) {
686
- break;
687
- }
315
+ if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) break;
688
316
 
689
- const range = match.range();
317
+ const node = match as {
318
+ range(): { start: { line: number; column: number } };
319
+ };
320
+ const range = node.range();
690
321
  const weight = rule.metadata?.weight || 3;
691
322
  const severity = weight >= 4 ? "error" : "warning";
692
323
 
@@ -704,21 +335,17 @@ const astGrepNapiRunner: RunnerDefinition = {
704
335
  });
705
336
  }
706
337
 
707
- // Stop processing more rules if we've hit the limit
708
- if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) {
709
- break;
710
- }
338
+ if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) break;
711
339
  } catch {
712
340
  // Rule failed, skip
713
341
  }
714
342
  }
715
343
  }
716
344
 
717
- // Return succeeded even when finding diagnostics - they are warnings, not runner failures
718
345
  return {
719
346
  status: "succeeded",
720
347
  diagnostics,
721
- semantic: diagnostics.length > 0 ? "warning" : "none",
348
+ semantic: diagnostics.length > 0 ? "warning" : ("none" as const),
722
349
  };
723
350
  },
724
351
  };