pi-lens 3.3.0 → 3.6.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 +91 -0
- package/README.md +175 -13
- package/clients/cache/rule-cache.js +72 -0
- package/clients/cache/rule-cache.ts +104 -0
- package/clients/dispatch/integration.js +48 -1
- package/clients/dispatch/integration.ts +60 -2
- package/clients/dispatch/plan.js +5 -2
- package/clients/dispatch/plan.ts +5 -2
- package/clients/dispatch/runners/ast-grep-napi.js +175 -56
- package/clients/dispatch/runners/ast-grep-napi.test.js +2 -1
- package/clients/dispatch/runners/ast-grep-napi.test.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +191 -79
- package/clients/dispatch/runners/similarity.js +1 -1
- package/clients/dispatch/runners/similarity.ts +2 -2
- package/clients/dispatch/runners/tree-sitter.js +137 -10
- package/clients/dispatch/runners/tree-sitter.ts +168 -13
- package/clients/dispatch/runners/ts-lsp.js +3 -2
- package/clients/dispatch/runners/ts-lsp.ts +3 -2
- package/clients/dispatch/runners/yaml-rule-parser.js +70 -2
- package/clients/dispatch/runners/yaml-rule-parser.ts +71 -2
- package/clients/dispatch/types.js +1 -1
- package/clients/dispatch/types.ts +1 -1
- package/clients/lsp/__tests__/service.test.js +3 -0
- package/clients/lsp/__tests__/service.test.ts +3 -0
- package/clients/lsp/client.js +42 -0
- package/clients/lsp/client.ts +79 -0
- package/clients/lsp/index.js +27 -0
- package/clients/lsp/index.ts +35 -0
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/metrics-client.js +3 -160
- package/clients/metrics-client.tdr.test.js +78 -0
- package/clients/metrics-client.test.js +30 -43
- package/clients/metrics-client.test.ts +30 -54
- package/clients/metrics-client.ts +5 -219
- package/clients/metrics-history.js +33 -7
- package/clients/metrics-history.ts +47 -10
- package/clients/pipeline.js +272 -0
- package/clients/pipeline.ts +371 -0
- package/clients/sg-runner.js +21 -3
- package/clients/sg-runner.ts +22 -3
- package/clients/tree-sitter-client.js +23 -2
- package/clients/tree-sitter-client.ts +27 -2
- package/index.ts +604 -771
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-architecture-violation.yml +7 -4
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +3 -3
- package/rules/ast-grep-rules/slop-patterns.yml +85 -62
- package/skills/ast-grep/SKILL.md +42 -1
- package/skills/lsp-navigation/SKILL.md +62 -0
- package/tsconfig.json +1 -1
- package/rules/ast-grep-rules/rules/no-console-log.yml +0 -10
- package/rules/ast-grep-rules/rules/no-default-export.yml +0 -19
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
} from "../types.js";
|
|
18
18
|
import {
|
|
19
19
|
calculateRuleComplexity,
|
|
20
|
+
hasUnsupportedConditions,
|
|
21
|
+
isOverlyBroadPattern,
|
|
20
22
|
isStructuredRule,
|
|
21
23
|
loadYamlRules,
|
|
22
24
|
MAX_BLOCKING_RULE_COMPLEXITY,
|
|
@@ -49,6 +51,15 @@ const MAX_MATCHES_PER_RULE = 10;
|
|
|
49
51
|
/** Maximum total diagnostics per file to prevent output spam */
|
|
50
52
|
const MAX_TOTAL_DIAGNOSTICS = 50;
|
|
51
53
|
|
|
54
|
+
/** Rules already covered by tree-sitter runner (priority 14, runs first) */
|
|
55
|
+
const TREE_SITTER_OVERLAP = new Set([
|
|
56
|
+
"constructor-super",
|
|
57
|
+
"empty-catch",
|
|
58
|
+
"long-parameter-list",
|
|
59
|
+
"nested-ternary",
|
|
60
|
+
"no-dupe-class-members",
|
|
61
|
+
]);
|
|
62
|
+
|
|
52
63
|
/** Maximum AST depth to traverse to prevent stack overflow on deeply nested files */
|
|
53
64
|
const MAX_AST_DEPTH = 50;
|
|
54
65
|
|
|
@@ -80,106 +91,187 @@ function getLang(filePath: string, sgModule: typeof import("@ast-grep/napi")) {
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
83
|
-
*
|
|
94
|
+
* Check if a single node matches a condition (without searching descendants).
|
|
95
|
+
* In ast-grep semantics:
|
|
96
|
+
* - pattern/kind/regex: check the node itself
|
|
97
|
+
* - all: node must match ALL sub-conditions
|
|
98
|
+
* - any: node must match at least ONE sub-condition
|
|
99
|
+
* - not: node must NOT match the sub-condition
|
|
100
|
+
* - has: node must have a DESCENDANT matching the sub-condition
|
|
84
101
|
*/
|
|
85
|
-
function
|
|
86
|
-
|
|
102
|
+
function nodeMatchesCondition(
|
|
103
|
+
node: any,
|
|
87
104
|
condition: YamlRuleCondition,
|
|
88
|
-
matches: unknown[] = [],
|
|
89
105
|
depth = 0,
|
|
90
|
-
):
|
|
91
|
-
if (depth > MAX_RULE_DEPTH) return
|
|
106
|
+
): boolean {
|
|
107
|
+
if (depth > MAX_RULE_DEPTH) return false;
|
|
92
108
|
|
|
93
|
-
|
|
109
|
+
// Check kind constraint
|
|
110
|
+
if (condition.kind && node.kind() !== condition.kind) return false;
|
|
94
111
|
|
|
112
|
+
// Check pattern constraint (node itself must match)
|
|
95
113
|
if (condition.pattern) {
|
|
96
114
|
try {
|
|
97
|
-
|
|
115
|
+
const matches = node.findAll(condition.pattern);
|
|
116
|
+
// Check if the node itself is among the matches (same start position)
|
|
117
|
+
const nodeRange = node.range();
|
|
118
|
+
let selfMatch = false;
|
|
119
|
+
for (const m of matches) {
|
|
120
|
+
const mr = (m as any).range();
|
|
121
|
+
if (
|
|
122
|
+
mr.start.line === nodeRange.start.line &&
|
|
123
|
+
mr.start.column === nodeRange.start.column &&
|
|
124
|
+
mr.end.line === nodeRange.end.line &&
|
|
125
|
+
mr.end.column === nodeRange.end.column
|
|
126
|
+
) {
|
|
127
|
+
selfMatch = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!selfMatch) return false;
|
|
98
132
|
} catch {
|
|
99
|
-
return
|
|
133
|
+
return false;
|
|
100
134
|
}
|
|
101
|
-
} else if (condition.kind) {
|
|
102
|
-
candidates = findByKind(rootNode, condition.kind, 0);
|
|
103
|
-
} else {
|
|
104
|
-
candidates = getAllNodes(rootNode, 0);
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (condition.has && matchesCondition) {
|
|
116
|
-
const subMatches = executeStructuredRule(
|
|
117
|
-
node,
|
|
118
|
-
condition.has,
|
|
119
|
-
[],
|
|
120
|
-
depth + 1,
|
|
121
|
-
);
|
|
122
|
-
if (subMatches.length === 0) matchesCondition = false;
|
|
137
|
+
// Check regex constraint
|
|
138
|
+
if (condition.regex) {
|
|
139
|
+
try {
|
|
140
|
+
const text = node.text();
|
|
141
|
+
if (!new RegExp(condition.regex).test(text)) return false;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
123
144
|
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check has (descendant must match)
|
|
148
|
+
if (condition.has) {
|
|
149
|
+
const descendants = findMatchingNodes(node, condition.has, depth + 1);
|
|
150
|
+
if (descendants.length === 0) return false;
|
|
151
|
+
}
|
|
124
152
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
// Check not (node must NOT match this condition)
|
|
154
|
+
if (condition.not) {
|
|
155
|
+
if (nodeMatchesCondition(node, condition.not, depth + 1)) return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check all (node must match ALL sub-conditions)
|
|
159
|
+
if (condition.all) {
|
|
160
|
+
for (const sub of condition.all) {
|
|
161
|
+
if (!nodeMatchesCondition(node, sub, depth + 1)) return false;
|
|
133
162
|
}
|
|
163
|
+
}
|
|
134
164
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
depth + 1,
|
|
143
|
-
);
|
|
144
|
-
if (subMatches.length > 0) {
|
|
145
|
-
anyMatches = true;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
165
|
+
// Check any (node must match at least one sub-condition)
|
|
166
|
+
if (condition.any) {
|
|
167
|
+
let anyMatch = false;
|
|
168
|
+
for (const sub of condition.any) {
|
|
169
|
+
if (nodeMatchesCondition(node, sub, depth + 1)) {
|
|
170
|
+
anyMatch = true;
|
|
171
|
+
break;
|
|
148
172
|
}
|
|
149
|
-
if (!anyMatches) matchesCondition = false;
|
|
150
173
|
}
|
|
174
|
+
if (!anyMatch) return false;
|
|
175
|
+
}
|
|
151
176
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Find all nodes in the tree that match a condition.
|
|
182
|
+
* This is the "search" function - traverses the tree and checks each node.
|
|
183
|
+
*/
|
|
184
|
+
function findMatchingNodes(
|
|
185
|
+
rootNode: any,
|
|
186
|
+
condition: YamlRuleCondition,
|
|
187
|
+
depth = 0,
|
|
188
|
+
): unknown[] {
|
|
189
|
+
if (depth > MAX_RULE_DEPTH) return [];
|
|
190
|
+
|
|
191
|
+
const matches: unknown[] = [];
|
|
192
|
+
|
|
193
|
+
// Optimization: if the condition has a kind, only check nodes of that kind
|
|
194
|
+
// If it has a pattern, use findAll for initial candidates
|
|
195
|
+
let candidates: unknown[];
|
|
196
|
+
|
|
197
|
+
if (condition.pattern && !condition.all && !condition.any) {
|
|
198
|
+
// Use findAll for pattern-only conditions (fast path)
|
|
199
|
+
try {
|
|
200
|
+
candidates = rootNode.findAll(condition.pattern);
|
|
201
|
+
} catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
} else if (condition.kind && !condition.all && !condition.any) {
|
|
205
|
+
// Use findByKind for kind-only conditions (fast path)
|
|
206
|
+
candidates = findByKind(rootNode, condition.kind, 0);
|
|
207
|
+
} else if (condition.all) {
|
|
208
|
+
// For `all`, find the narrowest sub-condition to generate candidates
|
|
209
|
+
candidates = getCandidatesForAll(rootNode, condition.all);
|
|
210
|
+
} else if (condition.any) {
|
|
211
|
+
// For `any`, union candidates from all sub-conditions
|
|
212
|
+
const seen = new Set<string>();
|
|
213
|
+
candidates = [];
|
|
214
|
+
for (const sub of condition.any) {
|
|
215
|
+
const subMatches = findMatchingNodes(rootNode, sub, depth + 1);
|
|
216
|
+
for (const m of subMatches) {
|
|
217
|
+
const r = (m as any).range();
|
|
218
|
+
const key = `${r.start.line}:${r.start.column}`;
|
|
219
|
+
if (!seen.has(key)) {
|
|
220
|
+
seen.add(key);
|
|
221
|
+
candidates.push(m);
|
|
163
222
|
}
|
|
164
223
|
}
|
|
165
224
|
}
|
|
225
|
+
} else {
|
|
226
|
+
// Fallback: traverse all nodes
|
|
227
|
+
candidates = getAllNodes(rootNode, 0);
|
|
228
|
+
}
|
|
166
229
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const regex = new RegExp(condition.regex);
|
|
171
|
-
if (!regex.test(text)) matchesCondition = false;
|
|
172
|
-
} catch {
|
|
173
|
-
matchesCondition = false;
|
|
174
|
-
}
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
if (nodeMatchesCondition(candidate, condition, depth)) {
|
|
232
|
+
matches.push(candidate);
|
|
175
233
|
}
|
|
234
|
+
}
|
|
176
235
|
|
|
177
|
-
|
|
178
|
-
|
|
236
|
+
return matches;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* For an `all` condition, find the narrowest sub-condition to generate
|
|
241
|
+
* initial candidates. This avoids scanning all nodes when one sub-condition
|
|
242
|
+
* has a specific kind or pattern.
|
|
243
|
+
*/
|
|
244
|
+
function getCandidatesForAll(
|
|
245
|
+
rootNode: any,
|
|
246
|
+
subs: YamlRuleCondition[],
|
|
247
|
+
): unknown[] {
|
|
248
|
+
// Prefer kind-based narrowing first, then pattern-based
|
|
249
|
+
for (const sub of subs) {
|
|
250
|
+
if (sub.kind) {
|
|
251
|
+
return findByKind(rootNode, sub.kind, 0);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const sub of subs) {
|
|
255
|
+
if (sub.pattern) {
|
|
256
|
+
try {
|
|
257
|
+
return rootNode.findAll(sub.pattern);
|
|
258
|
+
} catch {}
|
|
179
259
|
}
|
|
180
260
|
}
|
|
261
|
+
// No narrowing possible, scan all
|
|
262
|
+
return getAllNodes(rootNode, 0);
|
|
263
|
+
}
|
|
181
264
|
|
|
182
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Legacy wrapper - execute a structured rule using the new two-phase approach.
|
|
267
|
+
*/
|
|
268
|
+
function executeStructuredRule(
|
|
269
|
+
rootNode: any,
|
|
270
|
+
condition: YamlRuleCondition,
|
|
271
|
+
matches: unknown[] = [],
|
|
272
|
+
depth = 0,
|
|
273
|
+
): unknown[] {
|
|
274
|
+
return findMatchingNodes(rootNode, condition, depth);
|
|
183
275
|
}
|
|
184
276
|
|
|
185
277
|
/**
|
|
@@ -213,9 +305,7 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
213
305
|
id: "ast-grep-napi",
|
|
214
306
|
appliesTo: ["jsts"],
|
|
215
307
|
priority: 15,
|
|
216
|
-
|
|
217
|
-
// Still enabled for /lens-booboo via FULL_LINT_PLANS.
|
|
218
|
-
enabledByDefault: false,
|
|
308
|
+
enabledByDefault: true,
|
|
219
309
|
skipTestFiles: true,
|
|
220
310
|
|
|
221
311
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
@@ -279,6 +369,24 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
279
369
|
}
|
|
280
370
|
|
|
281
371
|
for (const rule of rules) {
|
|
372
|
+
// Skip rules already handled by tree-sitter runner (priority 14)
|
|
373
|
+
if (TREE_SITTER_OVERLAP.has(rule.id)) continue;
|
|
374
|
+
|
|
375
|
+
// Skip rules using conditions we can't execute (inside, follows,
|
|
376
|
+
// precedes, stopBy, field, nthChild, constraints). Running these
|
|
377
|
+
// with only partial condition evaluation causes false positives.
|
|
378
|
+
if (hasUnsupportedConditions(rule)) continue;
|
|
379
|
+
|
|
380
|
+
// Skip rules whose top-level pattern is overly broad ($NAME, $X, etc.)
|
|
381
|
+
// without additional structural constraints to narrow matches.
|
|
382
|
+
if (
|
|
383
|
+
rule.rule &&
|
|
384
|
+
isOverlyBroadPattern(rule.rule.pattern) &&
|
|
385
|
+
!isStructuredRule(rule)
|
|
386
|
+
) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
282
390
|
const lang = rule.language?.toLowerCase();
|
|
283
391
|
if (lang && lang !== "typescript" && lang !== "javascript") {
|
|
284
392
|
continue;
|
|
@@ -318,8 +426,7 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
318
426
|
range(): { start: { line: number; column: number } };
|
|
319
427
|
};
|
|
320
428
|
const range = node.range();
|
|
321
|
-
const
|
|
322
|
-
const severity = weight >= 4 ? "error" : "warning";
|
|
429
|
+
const severity = rule.severity === "error" ? "error" : "warning";
|
|
323
430
|
|
|
324
431
|
diagnostics.push({
|
|
325
432
|
id: `ast-grep-napi-${range.start.line}-${rule.id}`,
|
|
@@ -342,10 +449,15 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
342
449
|
}
|
|
343
450
|
}
|
|
344
451
|
|
|
452
|
+
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
345
453
|
return {
|
|
346
454
|
status: "succeeded",
|
|
347
455
|
diagnostics,
|
|
348
|
-
semantic:
|
|
456
|
+
semantic: hasBlocking
|
|
457
|
+
? "blocking"
|
|
458
|
+
: diagnostics.length > 0
|
|
459
|
+
? "warning"
|
|
460
|
+
: ("none" as const),
|
|
349
461
|
};
|
|
350
462
|
},
|
|
351
463
|
};
|
|
@@ -139,7 +139,7 @@ const similarityRunner: RunnerDefinition = {
|
|
|
139
139
|
// Function Extraction
|
|
140
140
|
// ============================================================================
|
|
141
141
|
|
|
142
|
-
interface ExtractedFunction {
|
|
142
|
+
export interface ExtractedFunction {
|
|
143
143
|
name: string;
|
|
144
144
|
line: number;
|
|
145
145
|
column: number;
|
|
@@ -148,7 +148,7 @@ interface ExtractedFunction {
|
|
|
148
148
|
signature: string;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function extractFunctions(
|
|
151
|
+
export function extractFunctions(
|
|
152
152
|
sourceFile: ts.SourceFile,
|
|
153
153
|
_fullContent: string,
|
|
154
154
|
): ExtractedFunction[] {
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* for fast AST-based pattern matching.
|
|
6
6
|
* Updated: ast-grep-napi test
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { RuleCache } from "../../cache/rule-cache.js";
|
|
11
|
+
import { normalizeMapKey } from "../../path-utils.js";
|
|
9
12
|
import { TreeSitterClient } from "../../tree-sitter-client.js";
|
|
10
|
-
import { queryLoader } from "../../tree-sitter-query-loader.js";
|
|
13
|
+
import { queryLoader, } from "../../tree-sitter-query-loader.js";
|
|
11
14
|
// Module-level singleton: web-tree-sitter WASM must only be initialized once per process.
|
|
12
15
|
// Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
|
|
13
16
|
// WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
|
|
@@ -18,6 +21,80 @@ function getSharedClient() {
|
|
|
18
21
|
}
|
|
19
22
|
return _sharedClient;
|
|
20
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if a code block is effectively empty (ignoring comments and whitespace)
|
|
26
|
+
*/
|
|
27
|
+
function isEmptyBlock(blockContent) {
|
|
28
|
+
// Remove comments, whitespace, and check if anything remains
|
|
29
|
+
const cleaned = blockContent
|
|
30
|
+
.replace(/\/\/.*$/gm, "") // Remove single-line comments
|
|
31
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
|
|
32
|
+
.replace(/\s+/g, "") // Remove all whitespace
|
|
33
|
+
.trim();
|
|
34
|
+
return cleaned.length === 0 || cleaned === "{}";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract parameter count from match text
|
|
38
|
+
*/
|
|
39
|
+
function countParameters(matchText) {
|
|
40
|
+
// Count commas in parameter list, or check for non-empty params
|
|
41
|
+
// Simple heuristic: count commas + 1, or 0 if empty
|
|
42
|
+
const paramsMatch = matchText.match(/\((.*)\)/);
|
|
43
|
+
if (!paramsMatch)
|
|
44
|
+
return 0;
|
|
45
|
+
const params = paramsMatch[1].trim();
|
|
46
|
+
if (!params)
|
|
47
|
+
return 0;
|
|
48
|
+
return params.split(",").length;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Apply post-filter to determine if a match should be reported
|
|
52
|
+
*/
|
|
53
|
+
function applyPostFilter(query, captures) {
|
|
54
|
+
if (!query.post_filter)
|
|
55
|
+
return true; // No filter = always include
|
|
56
|
+
switch (query.post_filter) {
|
|
57
|
+
case "empty_body": {
|
|
58
|
+
// Check if the BODY capture is effectively empty
|
|
59
|
+
const body = captures.BODY || captures.body || "";
|
|
60
|
+
return isEmptyBlock(body);
|
|
61
|
+
}
|
|
62
|
+
case "count_params": {
|
|
63
|
+
// Check if parameter count meets minimum
|
|
64
|
+
const minParams = query.post_filter_params?.min_params || 6;
|
|
65
|
+
// Get PARAMS capture which contains the parameter list like "(a, b, c)"
|
|
66
|
+
const params = captures.PARAMS || captures.params || captures.PARAM || "";
|
|
67
|
+
const paramCount = countParameters(params);
|
|
68
|
+
return paramCount >= minParams;
|
|
69
|
+
}
|
|
70
|
+
case "not_dbg_method":
|
|
71
|
+
// Exclude debug methods (for console-statement)
|
|
72
|
+
return !/\b(dbg|debug|logDebug)\b/i.test(captures.METHOD || "");
|
|
73
|
+
default:
|
|
74
|
+
// Unknown filter - include by default (safer than excluding)
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if variable name matches secret patterns
|
|
80
|
+
* This handles the #match? predicate from tree-sitter queries
|
|
81
|
+
*/
|
|
82
|
+
function matchesSecretPattern(varName) {
|
|
83
|
+
const secretPatterns = [
|
|
84
|
+
/api[_-]?key/i,
|
|
85
|
+
/api[_-]?secret/i,
|
|
86
|
+
/password/i,
|
|
87
|
+
/secret/i,
|
|
88
|
+
/token/i,
|
|
89
|
+
/auth/i,
|
|
90
|
+
/private[_-]?key/i,
|
|
91
|
+
/access[_-]?token/i,
|
|
92
|
+
/credentials/i,
|
|
93
|
+
/aws[_-]?secret/i,
|
|
94
|
+
/github[_-]?token/i,
|
|
95
|
+
];
|
|
96
|
+
return secretPatterns.some((pattern) => pattern.test(varName));
|
|
97
|
+
}
|
|
21
98
|
const treeSitterRunner = {
|
|
22
99
|
id: "tree-sitter",
|
|
23
100
|
appliesTo: ["jsts", "python"],
|
|
@@ -56,14 +133,49 @@ const treeSitterRunner = {
|
|
|
56
133
|
else {
|
|
57
134
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
58
135
|
}
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
136
|
+
// Try cache first, fall back to loading from disk
|
|
137
|
+
let languageQueries = [];
|
|
138
|
+
const cache = new RuleCache(languageId);
|
|
139
|
+
// Get all rule files for this language (use ctx.cwd for project root)
|
|
140
|
+
const rulesDir = path.join(ctx.cwd, "rules", "tree-sitter-queries", languageId);
|
|
141
|
+
const ruleFiles = [];
|
|
142
|
+
if (fs.existsSync(rulesDir)) {
|
|
143
|
+
ruleFiles.push(...fs
|
|
144
|
+
.readdirSync(rulesDir)
|
|
145
|
+
.filter((f) => f.endsWith(".yml"))
|
|
146
|
+
.map((f) => path.join(rulesDir, f)));
|
|
147
|
+
}
|
|
148
|
+
// Try cache
|
|
149
|
+
const cached = cache.get(ruleFiles);
|
|
150
|
+
if (cached) {
|
|
151
|
+
// Use cached queries
|
|
152
|
+
languageQueries = cached.queries.map((q) => ({
|
|
153
|
+
...q,
|
|
154
|
+
has_fix: false,
|
|
155
|
+
filePath: "",
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Load from disk
|
|
160
|
+
if (!queryLoader.getAllQueries().length) {
|
|
161
|
+
await queryLoader.loadQueries();
|
|
162
|
+
}
|
|
163
|
+
const allQueries = queryLoader.getAllQueries();
|
|
164
|
+
languageQueries = allQueries.filter((q) => q.language === languageId ||
|
|
165
|
+
(isJavaScript && q.language === "typescript"));
|
|
166
|
+
// Save to cache
|
|
167
|
+
cache.set(ruleFiles, languageQueries.map((q) => ({
|
|
168
|
+
id: q.id,
|
|
169
|
+
name: q.name,
|
|
170
|
+
severity: q.severity,
|
|
171
|
+
language: q.language,
|
|
172
|
+
message: q.message,
|
|
173
|
+
query: q.query,
|
|
174
|
+
metavars: q.metavars,
|
|
175
|
+
post_filter: q.post_filter,
|
|
176
|
+
post_filter_params: q.post_filter_params,
|
|
177
|
+
})));
|
|
62
178
|
}
|
|
63
|
-
// Get all loaded queries for this language
|
|
64
|
-
const allQueries = queryLoader.getAllQueries();
|
|
65
|
-
const languageQueries = allQueries.filter((q) => q.language === languageId ||
|
|
66
|
-
(isJavaScript && q.language === "typescript"));
|
|
67
179
|
if (languageQueries.length === 0) {
|
|
68
180
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
69
181
|
}
|
|
@@ -74,8 +186,23 @@ const treeSitterRunner = {
|
|
|
74
186
|
// Extract directory from file path (use path.dirname for cross-platform)
|
|
75
187
|
const rootDir = path.dirname(filePath);
|
|
76
188
|
const matches = await client.structuralSearch(query.id, // Use query ID as pattern (findMatchingQuery will resolve it)
|
|
77
|
-
languageId, rootDir, {
|
|
189
|
+
languageId, rootDir, {
|
|
190
|
+
maxResults: 10,
|
|
191
|
+
fileFilter: (f) => normalizeMapKey(f) === normalizeMapKey(filePath),
|
|
192
|
+
});
|
|
78
193
|
for (const match of matches) {
|
|
194
|
+
// Apply post-filter if defined (pass captures for proper filtering)
|
|
195
|
+
if (!applyPostFilter(query, match.captures)) {
|
|
196
|
+
continue; // Skip this match - filter didn't pass
|
|
197
|
+
}
|
|
198
|
+
// For hardcoded-secrets, also check variable name pattern
|
|
199
|
+
if (query.id === "hardcoded-secrets") {
|
|
200
|
+
// Extract variable name from captures
|
|
201
|
+
const varName = match.captures?.VARNAME || "";
|
|
202
|
+
if (!varName || !matchesSecretPattern(varName)) {
|
|
203
|
+
continue; // Skip - no variable name or doesn't match secret patterns
|
|
204
|
+
}
|
|
205
|
+
}
|
|
79
206
|
// Get line/column from match (already 0-indexed from tree-sitter)
|
|
80
207
|
const line = match.line;
|
|
81
208
|
const column = match.column;
|