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