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
@@ -0,0 +1,292 @@
1
+ /**
2
+ * YAML Rule Parser for ast-grep
3
+ *
4
+ * Parses simplified YAML rule files for structural code analysis.
5
+ * Supports pattern matching, kind matching, and structured conditions
6
+ * (has/any/all/not/regex).
7
+ *
8
+ * Features:
9
+ * - Caching with mtime-based invalidation
10
+ * - Severity filtering (error-only for blocking mode)
11
+ * - Complexity scoring for performance optimization
12
+ * - Overly broad pattern detection
13
+ */
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ // --- Constants ---
17
+ /** Overly broad patterns that match everything (cause false positive explosions) */
18
+ export const OVERLY_BROAD_PATTERNS = [
19
+ "$NAME",
20
+ "$FIELD",
21
+ "$_",
22
+ "$X",
23
+ "$VAR",
24
+ "$EXPR",
25
+ ];
26
+ /** Maximum complexity score for rules in blockingOnly mode */
27
+ export const MAX_BLOCKING_RULE_COMPLEXITY = 8;
28
+ // --- Caches ---
29
+ const rulesCache = new Map();
30
+ const blockingRulesCache = new Map();
31
+ // --- Public API ---
32
+ export function clearRulesCache() {
33
+ rulesCache.clear();
34
+ blockingRulesCache.clear();
35
+ }
36
+ export function loadYamlRules(ruleDir, severityFilter) {
37
+ return getCachedRules(ruleDir, severityFilter);
38
+ }
39
+ export function loadYamlRulesUncached(ruleDir, severityFilter) {
40
+ const rules = [];
41
+ if (!fs.existsSync(ruleDir))
42
+ return rules;
43
+ const files = fs.readdirSync(ruleDir).filter((f) => f.endsWith(".yml"));
44
+ for (const file of files) {
45
+ try {
46
+ const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
47
+ const documents = content.split(/^---$/m).filter((d) => d.trim());
48
+ for (const doc of documents) {
49
+ const rule = parseSimpleYaml(doc.trim());
50
+ if (rule?.id) {
51
+ if (severityFilter && rule.severity !== severityFilter) {
52
+ continue;
53
+ }
54
+ rules.push(rule);
55
+ }
56
+ }
57
+ }
58
+ catch {
59
+ // Skip invalid files
60
+ }
61
+ }
62
+ return rules;
63
+ }
64
+ export function getCachedRules(ruleDir, severityFilter) {
65
+ if (!fs.existsSync(ruleDir)) {
66
+ return [];
67
+ }
68
+ let currentMtime = 0;
69
+ try {
70
+ currentMtime = fs.statSync(ruleDir).mtimeMs;
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ const cache = severityFilter === "error" ? blockingRulesCache : rulesCache;
76
+ const cached = cache.get(ruleDir);
77
+ if (cached && cached.mtime === currentMtime) {
78
+ return cached.rules;
79
+ }
80
+ const rules = loadYamlRulesUncached(ruleDir, severityFilter);
81
+ cache.set(ruleDir, { rules, mtime: currentMtime });
82
+ return rules;
83
+ }
84
+ export function isOverlyBroadPattern(pattern) {
85
+ if (!pattern)
86
+ return false;
87
+ if (OVERLY_BROAD_PATTERNS.includes(pattern.trim()))
88
+ return true;
89
+ return /^\$[A-Z_]+$/i.test(pattern.trim());
90
+ }
91
+ export function isValidCondition(condition) {
92
+ if (!condition)
93
+ return false;
94
+ if (condition.all !== undefined && condition.all.length === 0)
95
+ return false;
96
+ if (condition.any !== undefined && condition.any.length === 0)
97
+ return false;
98
+ if (isOverlyBroadPattern(condition.pattern))
99
+ return false;
100
+ return true;
101
+ }
102
+ export function isStructuredRule(rule) {
103
+ if (!rule.rule)
104
+ return false;
105
+ return !!(rule.rule.has ||
106
+ rule.rule.any ||
107
+ rule.rule.all ||
108
+ rule.rule.not ||
109
+ rule.rule.regex);
110
+ }
111
+ export function calculateRuleComplexity(condition) {
112
+ if (!condition)
113
+ return 0;
114
+ let score = 0;
115
+ if (condition.has)
116
+ score += 3;
117
+ if (condition.not)
118
+ score += 2;
119
+ if (condition.regex)
120
+ score += 2;
121
+ if (condition.any)
122
+ score += condition.any.length * 2;
123
+ if (condition.all)
124
+ score += condition.all.length * 3;
125
+ if (condition.has)
126
+ score += calculateRuleComplexity(condition.has);
127
+ if (condition.not)
128
+ score += calculateRuleComplexity(condition.not);
129
+ if (condition.any) {
130
+ for (const sub of condition.any)
131
+ score += calculateRuleComplexity(sub);
132
+ }
133
+ if (condition.all) {
134
+ for (const sub of condition.all)
135
+ score += calculateRuleComplexity(sub);
136
+ }
137
+ return score;
138
+ }
139
+ // --- YAML Parser ---
140
+ function getIndent(line) {
141
+ let count = 0;
142
+ for (const char of line) {
143
+ if (char === " ")
144
+ count++;
145
+ else if (char === "\t")
146
+ count += 2;
147
+ else
148
+ break;
149
+ }
150
+ return count;
151
+ }
152
+ function stripQuotes(value) {
153
+ let s = value;
154
+ while (s.startsWith('"') && s.endsWith('"') && s.length > 1)
155
+ s = s.slice(1, -1);
156
+ while (s.startsWith("'") && s.endsWith("'") && s.length > 1)
157
+ s = s.slice(1, -1);
158
+ return s;
159
+ }
160
+ export function parseSimpleYaml(content) {
161
+ const lines = content.split("\n");
162
+ const rule = { id: "", metadata: {} };
163
+ const stack = [];
164
+ let multilineBuffer = [];
165
+ let multilineKey = "";
166
+ const currentObj = () => stack.length === 0
167
+ ? rule
168
+ : stack[stack.length - 1].obj;
169
+ const flushMultiline = () => {
170
+ if (!multilineKey || multilineBuffer.length === 0)
171
+ return;
172
+ const value = multilineBuffer.join("\n");
173
+ const obj = currentObj();
174
+ if (multilineKey === "pattern")
175
+ obj.pattern = value;
176
+ else if (multilineKey === "message")
177
+ rule.message = value;
178
+ multilineKey = "";
179
+ multilineBuffer = [];
180
+ };
181
+ for (let i = 0; i < lines.length; i++) {
182
+ const line = lines[i];
183
+ const trimmed = line.trim();
184
+ if (!trimmed || trimmed.startsWith("#") || trimmed === "---")
185
+ continue;
186
+ const indent = getIndent(line);
187
+ while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
188
+ stack.pop();
189
+ }
190
+ if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
191
+ multilineBuffer.push(trimmed);
192
+ continue;
193
+ }
194
+ flushMultiline();
195
+ const colonIdx = trimmed.indexOf(":");
196
+ const key = colonIdx > 0 ? trimmed.substring(0, colonIdx).trim() : trimmed;
197
+ const value = colonIdx > 0 ? trimmed.substring(colonIdx + 1).trim() : "";
198
+ if (key === "id") {
199
+ rule.id = stripQuotes(value);
200
+ }
201
+ else if (key === "language") {
202
+ rule.language = value;
203
+ }
204
+ else if (key === "severity") {
205
+ rule.severity = value;
206
+ }
207
+ else if (key === "message") {
208
+ value === "|"
209
+ ? (multilineKey = "message")
210
+ : (rule.message = stripQuotes(value));
211
+ }
212
+ else if (key === "metadata") {
213
+ rule.metadata = {};
214
+ stack.push({ name: "metadata", indent, obj: rule.metadata });
215
+ }
216
+ else if (key === "rule") {
217
+ rule.rule = {};
218
+ stack.push({ name: "rule", indent, obj: rule.rule });
219
+ }
220
+ else if (stack.length > 0) {
221
+ const obj = currentObj();
222
+ const section = stack[stack.length - 1].name;
223
+ if (key === "weight" && section === "metadata") {
224
+ if (!rule.metadata)
225
+ rule.metadata = {};
226
+ rule.metadata.weight = parseInt(value, 10) || 3;
227
+ }
228
+ else if (key === "category" && section === "metadata") {
229
+ if (!rule.metadata)
230
+ rule.metadata = {};
231
+ rule.metadata.category = stripQuotes(value);
232
+ }
233
+ else if (key === "pattern") {
234
+ value === "|"
235
+ ? (multilineKey = "pattern")
236
+ : (obj.pattern = stripQuotes(value));
237
+ }
238
+ else if (key === "kind") {
239
+ obj.kind = value;
240
+ }
241
+ else if (key === "regex") {
242
+ obj.regex = stripQuotes(value);
243
+ }
244
+ else if (key === "has" || key === "not") {
245
+ obj[key] = {};
246
+ stack.push({ name: key, indent, obj: obj[key] });
247
+ }
248
+ else if (key === "any" || key === "all") {
249
+ if (!obj[key])
250
+ obj[key] = [];
251
+ const list = obj[key];
252
+ let j = i + 1;
253
+ while (j < lines.length) {
254
+ const nextLine = lines[j];
255
+ const nextTrimmed = nextLine.trim();
256
+ if (!nextTrimmed || nextTrimmed.startsWith("#")) {
257
+ j++;
258
+ continue;
259
+ }
260
+ const nextIndent = getIndent(nextLine);
261
+ if (nextIndent <= indent)
262
+ break;
263
+ if (nextTrimmed.startsWith("- ")) {
264
+ const item = {};
265
+ list.push(item);
266
+ stack.push({
267
+ name: key,
268
+ indent: nextIndent,
269
+ obj: item,
270
+ });
271
+ const itemContent = nextTrimmed.substring(2);
272
+ if (itemContent.includes(":")) {
273
+ const [itemKey, itemVal] = itemContent.split(":", 2);
274
+ if (itemKey.trim() === "pattern") {
275
+ item.pattern = stripQuotes(itemVal.trim());
276
+ }
277
+ else if (itemKey.trim() === "kind") {
278
+ item.kind = itemVal.trim();
279
+ }
280
+ }
281
+ else if (itemContent) {
282
+ item.pattern = stripQuotes(itemContent);
283
+ }
284
+ }
285
+ j++;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ flushMultiline();
291
+ return rule.id ? rule : null;
292
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * YAML Rule Parser for ast-grep
3
+ *
4
+ * Parses simplified YAML rule files for structural code analysis.
5
+ * Supports pattern matching, kind matching, and structured conditions
6
+ * (has/any/all/not/regex).
7
+ *
8
+ * Features:
9
+ * - Caching with mtime-based invalidation
10
+ * - Severity filtering (error-only for blocking mode)
11
+ * - Complexity scoring for performance optimization
12
+ * - Overly broad pattern detection
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+
18
+ // --- Types ---
19
+
20
+ export interface YamlRuleCondition {
21
+ kind?: string;
22
+ pattern?: string;
23
+ regex?: string;
24
+ has?: YamlRuleCondition;
25
+ any?: YamlRuleCondition[];
26
+ all?: YamlRuleCondition[];
27
+ not?: YamlRuleCondition;
28
+ }
29
+
30
+ export interface YamlRule {
31
+ id: string;
32
+ language?: string;
33
+ severity?: string;
34
+ message?: string;
35
+ metadata?: { weight?: number; category?: string };
36
+ rule?: YamlRuleCondition;
37
+ }
38
+
39
+ interface CachedRules {
40
+ rules: YamlRule[];
41
+ mtime: number;
42
+ }
43
+
44
+ // Internal type for YAML parsing (allows dynamic property access)
45
+ interface YamlNode {
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ // --- Constants ---
50
+
51
+ /** Overly broad patterns that match everything (cause false positive explosions) */
52
+ export const OVERLY_BROAD_PATTERNS = [
53
+ "$NAME",
54
+ "$FIELD",
55
+ "$_",
56
+ "$X",
57
+ "$VAR",
58
+ "$EXPR",
59
+ ];
60
+
61
+ /** Maximum complexity score for rules in blockingOnly mode */
62
+ export const MAX_BLOCKING_RULE_COMPLEXITY = 8;
63
+
64
+ // --- Caches ---
65
+
66
+ const rulesCache = new Map<string, CachedRules>();
67
+ const blockingRulesCache = new Map<string, CachedRules>();
68
+
69
+ // --- Public API ---
70
+
71
+ export function clearRulesCache(): void {
72
+ rulesCache.clear();
73
+ blockingRulesCache.clear();
74
+ }
75
+
76
+ export function loadYamlRules(
77
+ ruleDir: string,
78
+ severityFilter?: "error",
79
+ ): YamlRule[] {
80
+ return getCachedRules(ruleDir, severityFilter);
81
+ }
82
+
83
+ export function loadYamlRulesUncached(
84
+ ruleDir: string,
85
+ severityFilter?: "error",
86
+ ): YamlRule[] {
87
+ const rules: YamlRule[] = [];
88
+ if (!fs.existsSync(ruleDir)) return rules;
89
+
90
+ const files = fs.readdirSync(ruleDir).filter((f) => f.endsWith(".yml"));
91
+
92
+ for (const file of files) {
93
+ try {
94
+ const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
95
+ const documents = content.split(/^---$/m).filter((d) => d.trim());
96
+
97
+ for (const doc of documents) {
98
+ const rule = parseSimpleYaml(doc.trim());
99
+ if (rule?.id) {
100
+ if (severityFilter && rule.severity !== severityFilter) {
101
+ continue;
102
+ }
103
+ rules.push(rule);
104
+ }
105
+ }
106
+ } catch {
107
+ // Skip invalid files
108
+ }
109
+ }
110
+
111
+ return rules;
112
+ }
113
+
114
+ export function getCachedRules(
115
+ ruleDir: string,
116
+ severityFilter?: "error",
117
+ ): YamlRule[] {
118
+ if (!fs.existsSync(ruleDir)) {
119
+ return [];
120
+ }
121
+
122
+ let currentMtime = 0;
123
+ try {
124
+ currentMtime = fs.statSync(ruleDir).mtimeMs;
125
+ } catch {
126
+ return [];
127
+ }
128
+
129
+ const cache = severityFilter === "error" ? blockingRulesCache : rulesCache;
130
+ const cached = cache.get(ruleDir);
131
+ if (cached && cached.mtime === currentMtime) {
132
+ return cached.rules;
133
+ }
134
+
135
+ const rules = loadYamlRulesUncached(ruleDir, severityFilter);
136
+ cache.set(ruleDir, { rules, mtime: currentMtime });
137
+ return rules;
138
+ }
139
+
140
+ export function isOverlyBroadPattern(pattern: string | undefined): boolean {
141
+ if (!pattern) return false;
142
+ if (OVERLY_BROAD_PATTERNS.includes(pattern.trim())) return true;
143
+ return /^\$[A-Z_]+$/i.test(pattern.trim());
144
+ }
145
+
146
+ export function isValidCondition(
147
+ condition: YamlRuleCondition | undefined,
148
+ ): boolean {
149
+ if (!condition) return false;
150
+ if (condition.all !== undefined && condition.all.length === 0) return false;
151
+ if (condition.any !== undefined && condition.any.length === 0) return false;
152
+ if (isOverlyBroadPattern(condition.pattern)) return false;
153
+ return true;
154
+ }
155
+
156
+ export function isStructuredRule(rule: YamlRule): boolean {
157
+ if (!rule.rule) return false;
158
+ return !!(
159
+ rule.rule.has ||
160
+ rule.rule.any ||
161
+ rule.rule.all ||
162
+ rule.rule.not ||
163
+ rule.rule.regex
164
+ );
165
+ }
166
+
167
+ export function calculateRuleComplexity(
168
+ condition: YamlRuleCondition | undefined,
169
+ ): number {
170
+ if (!condition) return 0;
171
+
172
+ let score = 0;
173
+ if (condition.has) score += 3;
174
+ if (condition.not) score += 2;
175
+ if (condition.regex) score += 2;
176
+ if (condition.any) score += condition.any.length * 2;
177
+ if (condition.all) score += condition.all.length * 3;
178
+
179
+ if (condition.has) score += calculateRuleComplexity(condition.has);
180
+ if (condition.not) score += calculateRuleComplexity(condition.not);
181
+ if (condition.any) {
182
+ for (const sub of condition.any) score += calculateRuleComplexity(sub);
183
+ }
184
+ if (condition.all) {
185
+ for (const sub of condition.all) score += calculateRuleComplexity(sub);
186
+ }
187
+
188
+ return score;
189
+ }
190
+
191
+ // --- YAML Parser ---
192
+
193
+ function getIndent(line: string): number {
194
+ let count = 0;
195
+ for (const char of line) {
196
+ if (char === " ") count++;
197
+ else if (char === "\t") count += 2;
198
+ else break;
199
+ }
200
+ return count;
201
+ }
202
+
203
+ function stripQuotes(value: string): string {
204
+ let s = value;
205
+ while (s.startsWith('"') && s.endsWith('"') && s.length > 1)
206
+ s = s.slice(1, -1);
207
+ while (s.startsWith("'") && s.endsWith("'") && s.length > 1)
208
+ s = s.slice(1, -1);
209
+ return s;
210
+ }
211
+
212
+ export function parseSimpleYaml(content: string): YamlRule | null {
213
+ const lines = content.split("\n");
214
+ const rule: YamlRule = { id: "", metadata: {} };
215
+ const stack: Array<{ name: string; indent: number; obj: YamlNode }> = [];
216
+ let multilineBuffer: string[] = [];
217
+ let multilineKey = "";
218
+
219
+ const currentObj = (): YamlNode =>
220
+ stack.length === 0
221
+ ? (rule as unknown as YamlNode)
222
+ : stack[stack.length - 1].obj;
223
+
224
+ const flushMultiline = () => {
225
+ if (!multilineKey || multilineBuffer.length === 0) return;
226
+ const value = multilineBuffer.join("\n");
227
+ const obj = currentObj();
228
+ if (multilineKey === "pattern") obj.pattern = value;
229
+ else if (multilineKey === "message")
230
+ (rule as unknown as YamlNode).message = value;
231
+ multilineKey = "";
232
+ multilineBuffer = [];
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("#") || trimmed === "---") continue;
239
+
240
+ const indent = getIndent(line);
241
+
242
+ while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
243
+ stack.pop();
244
+ }
245
+
246
+ if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
247
+ multilineBuffer.push(trimmed);
248
+ continue;
249
+ }
250
+
251
+ flushMultiline();
252
+
253
+ const colonIdx = trimmed.indexOf(":");
254
+ const key = colonIdx > 0 ? trimmed.substring(0, colonIdx).trim() : trimmed;
255
+ const value = colonIdx > 0 ? trimmed.substring(colonIdx + 1).trim() : "";
256
+
257
+ if (key === "id") {
258
+ rule.id = stripQuotes(value);
259
+ } else if (key === "language") {
260
+ rule.language = value;
261
+ } else if (key === "severity") {
262
+ rule.severity = value;
263
+ } else if (key === "message") {
264
+ value === "|"
265
+ ? (multilineKey = "message")
266
+ : (rule.message = stripQuotes(value));
267
+ } else if (key === "metadata") {
268
+ rule.metadata = {};
269
+ stack.push({ name: "metadata", indent, obj: rule.metadata as YamlNode });
270
+ } else if (key === "rule") {
271
+ rule.rule = {};
272
+ stack.push({ name: "rule", indent, obj: rule.rule as YamlNode });
273
+ } else if (stack.length > 0) {
274
+ const obj = currentObj();
275
+ const section = stack[stack.length - 1].name;
276
+
277
+ if (key === "weight" && section === "metadata") {
278
+ if (!rule.metadata) rule.metadata = {};
279
+ rule.metadata.weight = parseInt(value, 10) || 3;
280
+ } else if (key === "category" && section === "metadata") {
281
+ if (!rule.metadata) rule.metadata = {};
282
+ rule.metadata.category = stripQuotes(value);
283
+ } else if (key === "pattern") {
284
+ value === "|"
285
+ ? (multilineKey = "pattern")
286
+ : (obj.pattern = stripQuotes(value));
287
+ } else if (key === "kind") {
288
+ obj.kind = value;
289
+ } else if (key === "regex") {
290
+ obj.regex = stripQuotes(value);
291
+ } else if (key === "has" || key === "not") {
292
+ obj[key] = {} as YamlRuleCondition;
293
+ stack.push({ name: key, indent, obj: obj[key] as YamlNode });
294
+ } else if (key === "any" || key === "all") {
295
+ if (!obj[key]) obj[key] = [];
296
+ const list = obj[key] as YamlRuleCondition[];
297
+
298
+ let j = i + 1;
299
+ while (j < lines.length) {
300
+ const nextLine = lines[j];
301
+ const nextTrimmed = nextLine.trim();
302
+ if (!nextTrimmed || nextTrimmed.startsWith("#")) {
303
+ j++;
304
+ continue;
305
+ }
306
+ const nextIndent = getIndent(nextLine);
307
+ if (nextIndent <= indent) break;
308
+
309
+ if (nextTrimmed.startsWith("- ")) {
310
+ const item: YamlRuleCondition = {};
311
+ list.push(item);
312
+ stack.push({
313
+ name: key,
314
+ indent: nextIndent,
315
+ obj: item as YamlNode,
316
+ });
317
+
318
+ const itemContent = nextTrimmed.substring(2);
319
+ if (itemContent.includes(":")) {
320
+ const [itemKey, itemVal] = itemContent.split(":", 2);
321
+ if (itemKey.trim() === "pattern") {
322
+ item.pattern = stripQuotes(itemVal.trim());
323
+ } else if (itemKey.trim() === "kind") {
324
+ item.kind = itemVal.trim();
325
+ }
326
+ } else if (itemContent) {
327
+ item.pattern = stripQuotes(itemContent);
328
+ }
329
+ }
330
+ j++;
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ flushMultiline();
337
+ return rule.id ? rule : null;
338
+ }
@@ -10,4 +10,7 @@
10
10
  *
11
11
  * The dispatcher must handle these semantics consistently.
12
12
  */
13
+ // --- Registry ---
14
+ // Test edit - adding unused variable to check inline diagnostics flow
15
+ const _unusedTestVariable = "checking pre-write and post-write flow";
13
16
  export {};
@@ -148,6 +148,9 @@ export interface RunnerGroup {
148
148
 
149
149
  // --- Registry ---
150
150
 
151
+ // Test edit - adding unused variable to check inline diagnostics flow
152
+ const _unusedTestVariable = "checking pre-write and post-write flow";
153
+
151
154
  export interface RunnerRegistry {
152
155
  register(runner: RunnerDefinition): void;
153
156
  get(id: string): RunnerDefinition | undefined;