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
@@ -8,11 +8,9 @@
8
8
  */
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ import { calculateRuleComplexity, isStructuredRule, loadYamlRules, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
13
12
  // Lazy load the napi package
14
13
  let sg;
15
- let _sgLoadError;
16
14
  let sgLoadAttempted = false;
17
15
  async function loadSg() {
18
16
  if (sg)
@@ -24,40 +22,9 @@ async function loadSg() {
24
22
  sg = await import("@ast-grep/napi");
25
23
  return sg;
26
24
  }
27
- catch (err) {
28
- _sgLoadError = err instanceof Error ? err : new Error(String(err));
29
- return undefined;
30
- }
31
- }
32
- const rulesCache = new Map();
33
- /** Get cached rules or reload if cache is stale */
34
- function _getCachedRules(ruleDir) {
35
- // Check if directory exists
36
- if (!fs.existsSync(ruleDir)) {
37
- return [];
38
- }
39
- // Get directory mtime to detect changes
40
- let currentMtime = 0;
41
- try {
42
- const stats = fs.statSync(ruleDir);
43
- currentMtime = stats.mtimeMs;
44
- }
45
25
  catch {
46
- return [];
47
- }
48
- // Check cache
49
- const cached = rulesCache.get(ruleDir);
50
- if (cached && cached.mtime === currentMtime) {
51
- return cached.rules;
26
+ return undefined;
52
27
  }
53
- // Load and cache
54
- const rules = loadYamlRulesUncached(ruleDir);
55
- rulesCache.set(ruleDir, { rules, mtime: currentMtime });
56
- return rules;
57
- }
58
- /** Clear rules cache (useful for testing or when rules change) */
59
- export function clearRulesCache() {
60
- rulesCache.clear();
61
28
  }
62
29
  // Supported extensions for NAPI
63
30
  const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
@@ -65,49 +32,10 @@ const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
65
32
  const MAX_MATCHES_PER_RULE = 10;
66
33
  /** Maximum total diagnostics per file to prevent output spam */
67
34
  const MAX_TOTAL_DIAGNOSTICS = 50;
68
- /** Threshold for warning about overly broad patterns that match everything */
69
- const _EXCESSIVE_MATCHES_THRESHOLD = 50;
70
- /** Maximum recursion depth for structured rule execution to prevent stack overflow */
71
- const _MAX_RECURSION_DEPTH = 10;
72
35
  /** Maximum AST depth to traverse to prevent stack overflow on deeply nested files */
73
- const _MAX_AST_DEPTH = 50;
36
+ const MAX_AST_DEPTH = 50;
74
37
  /** Maximum recursion depth for structured rule execution */
75
- const _MAX_RULE_DEPTH = 5;
76
- /** Overly broad patterns that match everything (cause false positive explosions) */
77
- const OVERLY_BROAD_PATTERNS = [
78
- "$NAME", // Matches every identifier
79
- "$FIELD", // Matches every field access
80
- "$_", // Matches every node
81
- "$X", // Common catch-all variable
82
- "$VAR", // Common catch-all variable
83
- "$EXPR", // Common catch-all expression
84
- ];
85
- /** Check if a pattern is overly broad and will cause false positive explosions */
86
- function isOverlyBroadPattern(pattern) {
87
- if (!pattern)
88
- return false;
89
- // Check exact matches and simple patterns that are just variables
90
- if (OVERLY_BROAD_PATTERNS.includes(pattern.trim()))
91
- return true;
92
- // Check if pattern is just a single meta-variable (starts with $ and has no other content)
93
- if (/^\$[A-Z_]+$/i.test(pattern.trim()))
94
- return true;
95
- return false;
96
- }
97
- /** Check if a rule condition is valid (not empty) */
98
- function _isValidCondition(condition) {
99
- if (!condition)
100
- return false;
101
- // Check for empty 'all' or 'any' arrays
102
- if (condition.all !== undefined && condition.all.length === 0)
103
- return false;
104
- if (condition.any !== undefined && condition.any.length === 0)
105
- return false;
106
- // Check for overly broad pattern
107
- if (isOverlyBroadPattern(condition.pattern))
108
- return false;
109
- return true;
110
- }
38
+ const MAX_RULE_DEPTH = 5;
111
39
  function canHandle(filePath) {
112
40
  return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
113
41
  }
@@ -130,251 +58,14 @@ function getLang(filePath, sgModule) {
130
58
  return undefined;
131
59
  }
132
60
  }
133
- function loadYamlRulesUncached(ruleDir) {
134
- const rules = [];
135
- if (!fs.existsSync(ruleDir))
136
- return rules;
137
- const files = fs.readdirSync(ruleDir).filter((f) => f.endsWith(".yml"));
138
- for (const file of files) {
139
- try {
140
- const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
141
- // Split by --- to handle multiple YAML documents in one file
142
- const documents = content.split(/^---$/m).filter((d) => d.trim());
143
- for (const doc of documents) {
144
- const rule = parseSimpleYaml(doc.trim());
145
- if (rule?.id) {
146
- rules.push(rule);
147
- }
148
- }
149
- }
150
- catch {
151
- // Skip invalid files
152
- }
153
- }
154
- return rules;
155
- }
156
- /** Load rules with caching - use this for production */
157
- function loadYamlRules(ruleDir) {
158
- return _getCachedRules(ruleDir);
159
- }
160
- function parseSimpleYaml(content) {
161
- const lines = content.split("\n");
162
- const rule = { id: "", metadata: {} };
163
- let _currentSection = "root";
164
- const sectionStack = [];
165
- let multilineBuffer = [];
166
- let multilineKey = "";
167
- function getCurrentObj() {
168
- if (sectionStack.length === 0)
169
- return rule;
170
- return sectionStack[sectionStack.length - 1].obj;
171
- }
172
- function getIndent(line) {
173
- let count = 0;
174
- for (const char of line) {
175
- if (char === " ")
176
- count++;
177
- else if (char === "\t")
178
- count += 2;
179
- else
180
- break;
181
- }
182
- return count;
183
- }
184
- for (let i = 0; i < lines.length; i++) {
185
- const line = lines[i];
186
- const trimmed = line.trim();
187
- if (!trimmed || trimmed.startsWith("#"))
188
- continue;
189
- if (trimmed === "---")
190
- continue;
191
- const indent = getIndent(line);
192
- // Pop stack if indent decreased
193
- while (sectionStack.length > 0 &&
194
- indent <= sectionStack[sectionStack.length - 1].indent) {
195
- sectionStack.pop();
196
- }
197
- // Check for multiline continuation
198
- if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
199
- multilineBuffer.push(trimmed);
200
- continue;
201
- }
202
- // Flush multiline buffer
203
- if (multilineKey && multilineBuffer.length > 0) {
204
- const value = multilineBuffer.join("\n");
205
- const current = getCurrentObj();
206
- if (multilineKey === "pattern" && current) {
207
- current.pattern = value;
208
- }
209
- multilineKey = "";
210
- multilineBuffer = [];
211
- }
212
- const colonIndex = trimmed.indexOf(":");
213
- const key = colonIndex > 0 ? trimmed.substring(0, colonIndex).trim() : trimmed;
214
- const value = colonIndex > 0 ? trimmed.substring(colonIndex + 1).trim() : "";
215
- if (key === "id") {
216
- rule.id = value.replace(/^["']|["']$/g, "");
217
- }
218
- else if (key === "language") {
219
- rule.language = value;
220
- }
221
- else if (key === "severity") {
222
- rule.severity = value;
223
- }
224
- else if (key === "message") {
225
- if (value === "|") {
226
- multilineKey = "message";
227
- }
228
- else {
229
- rule.message = value.replace(/^["']|["']$/g, "");
230
- }
231
- }
232
- else if (key === "metadata") {
233
- _currentSection = "metadata";
234
- const newObj = {};
235
- rule.metadata = newObj;
236
- sectionStack.push({ name: "metadata", indent, obj: newObj });
237
- }
238
- else if (key === "rule") {
239
- _currentSection = "rule";
240
- const newObj = {};
241
- rule.rule = newObj;
242
- sectionStack.push({ name: "rule", indent, obj: newObj });
243
- }
244
- else if (sectionStack.length > 0) {
245
- const current = getCurrentObj();
246
- const currentSectionName = sectionStack[sectionStack.length - 1]?.name;
247
- if (key === "weight" && currentSectionName === "metadata") {
248
- if (!rule.metadata)
249
- rule.metadata = {};
250
- rule.metadata.weight = parseInt(value, 10) || 3;
251
- }
252
- else if (key === "category" && currentSectionName === "metadata") {
253
- if (!rule.metadata)
254
- rule.metadata = {};
255
- rule.metadata.category = value.replace(/^["']|["']$/g, "");
256
- }
257
- else if (key === "pattern") {
258
- if (value === "|") {
259
- multilineKey = "pattern";
260
- }
261
- else {
262
- // Strip all surrounding quotes (handle nested quotes from YAML)
263
- let stripped = value;
264
- while (stripped.startsWith('"') &&
265
- stripped.endsWith('"') &&
266
- stripped.length > 1) {
267
- stripped = stripped.slice(1, -1);
268
- }
269
- while (stripped.startsWith("'") &&
270
- stripped.endsWith("'") &&
271
- stripped.length > 1) {
272
- stripped = stripped.slice(1, -1);
273
- }
274
- current.pattern = stripped;
275
- }
276
- }
277
- else if (key === "kind") {
278
- current.kind = value;
279
- }
280
- else if (key === "regex") {
281
- // Strip all surrounding quotes
282
- let stripped = value;
283
- while (stripped.startsWith('"') &&
284
- stripped.endsWith('"') &&
285
- stripped.length > 1) {
286
- stripped = stripped.slice(1, -1);
287
- }
288
- while (stripped.startsWith("'") &&
289
- stripped.endsWith("'") &&
290
- stripped.length > 1) {
291
- stripped = stripped.slice(1, -1);
292
- }
293
- current.regex = stripped;
294
- }
295
- else if (key === "has" || key === "not") {
296
- const newObj = {};
297
- current[key] = newObj;
298
- sectionStack.push({ name: key, indent, obj: newObj });
299
- }
300
- else if (key === "any" || key === "all") {
301
- if (!current[key])
302
- current[key] = [];
303
- // Check if next lines with more indent are list items
304
- let j = i + 1;
305
- while (j < lines.length) {
306
- const nextLine = lines[j];
307
- const nextTrimmed = nextLine.trim();
308
- if (!nextTrimmed || nextTrimmed.startsWith("#")) {
309
- j++;
310
- continue;
311
- }
312
- const nextIndent = getIndent(nextLine);
313
- if (nextIndent <= indent)
314
- break;
315
- if (nextTrimmed.startsWith("- ")) {
316
- // New list item
317
- const itemObj = {};
318
- current[key].push(itemObj);
319
- sectionStack.push({ name: key, indent: nextIndent, obj: itemObj });
320
- // Parse the item content after "- "
321
- const itemContent = nextTrimmed.substring(2);
322
- if (itemContent.includes(":")) {
323
- const [itemKey, itemVal] = itemContent.split(":", 2);
324
- if (itemKey.trim() === "pattern") {
325
- itemObj.pattern = itemVal.trim().replace(/^["']|["']$/g, "");
326
- }
327
- else if (itemKey.trim() === "kind") {
328
- itemObj.kind = itemVal.trim();
329
- }
330
- }
331
- else if (itemContent) {
332
- // Assume it's a pattern
333
- itemObj.pattern = itemContent.replace(/^["']|["']$/g, "");
334
- }
335
- }
336
- j++;
337
- }
338
- }
339
- }
340
- }
341
- // Flush remaining multiline buffer
342
- if (multilineKey && multilineBuffer.length > 0) {
343
- const value = multilineBuffer.join("\n");
344
- const current = getCurrentObj();
345
- if (multilineKey === "pattern" && current) {
346
- current.pattern = value;
347
- }
348
- else if (multilineKey === "message") {
349
- rule.message = value;
350
- }
351
- }
352
- return rule.id ? rule : null;
353
- }
354
- /**
355
- * Check if a rule uses structured conditions (has/any/all/not/regex)
356
- */
357
- function isStructuredRule(rule) {
358
- if (!rule.rule)
359
- return false;
360
- return !!(rule.rule.has ||
361
- rule.rule.any ||
362
- rule.rule.all ||
363
- rule.rule.not ||
364
- rule.rule.regex);
365
- }
366
61
  /**
367
62
  * Execute a structured rule using manual AST traversal
368
63
  */
369
64
  function executeStructuredRule(rootNode, condition, matches = [], depth = 0) {
370
- // Prevent infinite recursion from nested rules
371
- if (depth > _MAX_RULE_DEPTH) {
65
+ if (depth > MAX_RULE_DEPTH)
372
66
  return matches;
373
- }
374
- // Start with finding nodes by kind or pattern
375
67
  let candidates = [];
376
68
  if (condition.pattern) {
377
- // Use pattern matching via findAll
378
69
  try {
379
70
  candidates = rootNode.findAll(condition.pattern);
380
71
  }
@@ -383,33 +74,28 @@ function executeStructuredRule(rootNode, condition, matches = [], depth = 0) {
383
74
  }
384
75
  }
385
76
  else if (condition.kind) {
386
- // Manual traversal for kind matching with depth limit
387
77
  candidates = findByKind(rootNode, condition.kind, 0);
388
78
  }
389
79
  else {
390
- // No kind or pattern, search all nodes with depth limit
391
80
  candidates = getAllNodes(rootNode, 0);
392
81
  }
393
- // Filter candidates by conditions
394
82
  for (const candidate of candidates) {
83
+ const node = candidate;
395
84
  let matchesCondition = true;
396
- // Check 'has' condition
397
85
  if (condition.has && matchesCondition) {
398
- const subMatches = executeStructuredRule(candidate, condition.has, [], depth + 1);
86
+ const subMatches = executeStructuredRule(node, condition.has, [], depth + 1);
399
87
  if (subMatches.length === 0)
400
88
  matchesCondition = false;
401
89
  }
402
- // Check 'not' condition
403
90
  if (condition.not && matchesCondition) {
404
- const subMatches = executeStructuredRule(candidate, condition.not, [], depth + 1);
91
+ const subMatches = executeStructuredRule(node, condition.not, [], depth + 1);
405
92
  if (subMatches.length > 0)
406
93
  matchesCondition = false;
407
94
  }
408
- // Check 'any' condition (at least one must match)
409
95
  if (condition.any && matchesCondition) {
410
96
  let anyMatches = false;
411
97
  for (const subCondition of condition.any) {
412
- const subMatches = executeStructuredRule(candidate, subCondition, [], depth + 1);
98
+ const subMatches = executeStructuredRule(node, subCondition, [], depth + 1);
413
99
  if (subMatches.length > 0) {
414
100
  anyMatches = true;
415
101
  break;
@@ -418,31 +104,28 @@ function executeStructuredRule(rootNode, condition, matches = [], depth = 0) {
418
104
  if (!anyMatches)
419
105
  matchesCondition = false;
420
106
  }
421
- // Check 'all' condition (all must match)
422
107
  if (condition.all && matchesCondition) {
423
108
  for (const subCondition of condition.all) {
424
- const subMatches = executeStructuredRule(candidate, subCondition, [], depth + 1);
109
+ const subMatches = executeStructuredRule(node, subCondition, [], depth + 1);
425
110
  if (subMatches.length === 0) {
426
111
  matchesCondition = false;
427
112
  break;
428
113
  }
429
114
  }
430
115
  }
431
- // Check 'regex' condition with error handling
432
116
  if (condition.regex && matchesCondition) {
433
117
  try {
434
- const text = candidate.text();
118
+ const text = node.text();
435
119
  const regex = new RegExp(condition.regex);
436
120
  if (!regex.test(text))
437
121
  matchesCondition = false;
438
122
  }
439
123
  catch {
440
- // Invalid regex, skip this condition
441
124
  matchesCondition = false;
442
125
  }
443
126
  }
444
127
  if (matchesCondition) {
445
- matches.push(candidate);
128
+ matches.push(node);
446
129
  }
447
130
  }
448
131
  return matches;
@@ -451,13 +134,11 @@ function executeStructuredRule(rootNode, condition, matches = [], depth = 0) {
451
134
  * Find all nodes of a specific kind with depth limit
452
135
  */
453
136
  function findByKind(node, kind, currentDepth) {
454
- if (currentDepth > _MAX_AST_DEPTH) {
137
+ if (currentDepth > MAX_AST_DEPTH)
455
138
  return [];
456
- }
457
139
  const results = [];
458
- if (node.kind() === kind) {
140
+ if (node.kind() === kind)
459
141
  results.push(node);
460
- }
461
142
  for (const child of node.children()) {
462
143
  results.push(...findByKind(child, kind, currentDepth + 1));
463
144
  }
@@ -467,20 +148,22 @@ function findByKind(node, kind, currentDepth) {
467
148
  * Get all nodes with depth limit to prevent stack overflow
468
149
  */
469
150
  function getAllNodes(node, currentDepth) {
470
- if (currentDepth > _MAX_AST_DEPTH) {
151
+ if (currentDepth > MAX_AST_DEPTH)
471
152
  return [];
472
- }
473
153
  const results = [node];
474
154
  for (const child of node.children()) {
475
155
  results.push(...getAllNodes(child, currentDepth + 1));
476
156
  }
477
157
  return results;
478
158
  }
159
+ // --- Runner Definition ---
479
160
  const astGrepNapiRunner = {
480
161
  id: "ast-grep-napi",
481
- appliesTo: ["jsts"], // TypeScript/JavaScript only
482
- priority: 15, // Run early (after type checkers, before other linters)
483
- enabledByDefault: true,
162
+ appliesTo: ["jsts"],
163
+ priority: 15,
164
+ // Post-write disabled in plan.ts (removed from TOOL_PLANS.jsts.groups).
165
+ // Still enabled for /lens-booboo via FULL_LINT_PLANS.
166
+ enabledByDefault: false,
484
167
  skipTestFiles: true,
485
168
  async run(ctx) {
486
169
  if (!canHandle(ctx.filePath)) {
@@ -497,10 +180,8 @@ const astGrepNapiRunner = {
497
180
  if (!lang) {
498
181
  return { status: "skipped", diagnostics: [], semantic: "none" };
499
182
  }
500
- // Check file size to avoid parsing extremely large files
501
183
  const stats = fs.statSync(ctx.filePath);
502
- const MAX_FILE_SIZE = 1024 * 1024; // 1MB
503
- if (stats.size > MAX_FILE_SIZE) {
184
+ if (stats.size > 1024 * 1024) {
504
185
  return { status: "skipped", diagnostics: [], semantic: "none" };
505
186
  }
506
187
  let content;
@@ -512,13 +193,11 @@ const astGrepNapiRunner = {
512
193
  }
513
194
  let root;
514
195
  try {
515
- // Use the language object's parse method directly
516
196
  root = lang.parse(content);
517
197
  }
518
198
  catch {
519
199
  return { status: "skipped", diagnostics: [], semantic: "none" };
520
200
  }
521
- const diagnostics = [];
522
201
  let rootNode;
523
202
  try {
524
203
  rootNode = root.root();
@@ -526,70 +205,54 @@ const astGrepNapiRunner = {
526
205
  catch {
527
206
  return { status: "skipped", diagnostics: [], semantic: "none" };
528
207
  }
529
- // CONSOLIDATED: Use ast-grep-rules (unified with CLI tools)
530
- // Includes both security/architecture rules + slop patterns
208
+ const diagnostics = [];
531
209
  const ruleDirs = [
532
210
  path.join(process.cwd(), "rules/ast-grep-rules/rules"),
533
- path.join(process.cwd(), "rules/ast-grep-rules"), // For slop-patterns.yml
211
+ path.join(process.cwd(), "rules/ast-grep-rules"),
534
212
  ];
535
213
  for (const ruleDir of ruleDirs) {
536
214
  let rules;
537
215
  try {
538
- rules = loadYamlRules(ruleDir);
216
+ rules = loadYamlRules(ruleDir, ctx.blockingOnly ? "error" : undefined);
539
217
  }
540
218
  catch {
541
- continue; // Skip this rule directory on error
219
+ continue;
542
220
  }
543
221
  for (const rule of rules) {
544
- // Skip rules for different languages (case-insensitive)
545
222
  const lang = rule.language?.toLowerCase();
546
223
  if (lang && lang !== "typescript" && lang !== "javascript") {
547
224
  continue;
548
225
  }
549
- // When blockingOnly is set, only run BLOCKING rules (severity: error)
550
- if (ctx.blockingOnly && rule.severity !== "error") {
551
- continue;
226
+ if (ctx.blockingOnly && rule.rule) {
227
+ const complexity = calculateRuleComplexity(rule.rule);
228
+ if (complexity > MAX_BLOCKING_RULE_COMPLEXITY) {
229
+ continue;
230
+ }
552
231
  }
553
232
  try {
554
233
  let matches = [];
555
234
  if (isStructuredRule(rule) && rule.rule) {
556
- // Use structured rule execution
557
235
  matches = executeStructuredRule(rootNode, rule.rule, []);
558
236
  }
559
237
  else if (rule.rule?.pattern || rule.rule?.kind) {
560
- // Use simple pattern matching
561
238
  const pattern = rule.rule.pattern || rule.rule.kind;
562
239
  if (pattern) {
563
240
  try {
564
241
  matches = rootNode.findAll(pattern);
565
242
  }
566
243
  catch {
567
- // Pattern failed, try manual traversal for kind
568
244
  if (rule.rule.kind) {
569
- const findByKindLocal = (node, kind, depth = 0) => {
570
- if (depth > _MAX_AST_DEPTH)
571
- return [];
572
- const results = [];
573
- if (node.kind() === kind)
574
- results.push(node);
575
- for (const child of node.children()) {
576
- results.push(...findByKindLocal(child, kind, depth + 1));
577
- }
578
- return results;
579
- };
580
- matches = findByKindLocal(rootNode, rule.rule.kind);
245
+ matches = findByKind(rootNode, rule.rule.kind, 0);
581
246
  }
582
247
  }
583
248
  }
584
249
  }
585
- // Limit matches per rule to prevent excessive false positives
586
250
  const limitedMatches = matches.slice(0, MAX_MATCHES_PER_RULE);
587
251
  for (const match of limitedMatches) {
588
- // Skip if we've hit the total diagnostic limit
589
- if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) {
252
+ if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS)
590
253
  break;
591
- }
592
- const range = match.range();
254
+ const node = match;
255
+ const range = node.range();
593
256
  const weight = rule.metadata?.weight || 3;
594
257
  const severity = weight >= 4 ? "error" : "warning";
595
258
  diagnostics.push({
@@ -605,17 +268,14 @@ const astGrepNapiRunner = {
605
268
  fixable: false,
606
269
  });
607
270
  }
608
- // Stop processing more rules if we've hit the limit
609
- if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) {
271
+ if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS)
610
272
  break;
611
- }
612
273
  }
613
274
  catch {
614
275
  // Rule failed, skip
615
276
  }
616
277
  }
617
278
  }
618
- // Return succeeded even when finding diagnostics - they are warnings, not runner failures
619
279
  return {
620
280
  status: "succeeded",
621
281
  diagnostics,