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
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
-
import {
|
|
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
|
|
36
|
+
const MAX_AST_DEPTH = 50;
|
|
74
37
|
/** Maximum recursion depth for structured rule execution */
|
|
75
|
-
const
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 >
|
|
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 >
|
|
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"],
|
|
482
|
-
priority: 15,
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"),
|
|
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;
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
589
|
-
if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS) {
|
|
252
|
+
if (diagnostics.length >= MAX_TOTAL_DIAGNOSTICS)
|
|
590
253
|
break;
|
|
591
|
-
|
|
592
|
-
const 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
|
-
|
|
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,
|