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.
- package/CHANGELOG.md +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- 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
|
+
}
|
|
@@ -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;
|