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.
Files changed (53) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +175 -13
  3. package/clients/cache/rule-cache.js +72 -0
  4. package/clients/cache/rule-cache.ts +104 -0
  5. package/clients/dispatch/integration.js +48 -1
  6. package/clients/dispatch/integration.ts +60 -2
  7. package/clients/dispatch/plan.js +5 -2
  8. package/clients/dispatch/plan.ts +5 -2
  9. package/clients/dispatch/runners/ast-grep-napi.js +175 -56
  10. package/clients/dispatch/runners/ast-grep-napi.test.js +2 -1
  11. package/clients/dispatch/runners/ast-grep-napi.test.ts +2 -1
  12. package/clients/dispatch/runners/ast-grep-napi.ts +191 -79
  13. package/clients/dispatch/runners/similarity.js +1 -1
  14. package/clients/dispatch/runners/similarity.ts +2 -2
  15. package/clients/dispatch/runners/tree-sitter.js +137 -10
  16. package/clients/dispatch/runners/tree-sitter.ts +168 -13
  17. package/clients/dispatch/runners/ts-lsp.js +3 -2
  18. package/clients/dispatch/runners/ts-lsp.ts +3 -2
  19. package/clients/dispatch/runners/yaml-rule-parser.js +70 -2
  20. package/clients/dispatch/runners/yaml-rule-parser.ts +71 -2
  21. package/clients/dispatch/types.js +1 -1
  22. package/clients/dispatch/types.ts +1 -1
  23. package/clients/lsp/__tests__/service.test.js +3 -0
  24. package/clients/lsp/__tests__/service.test.ts +3 -0
  25. package/clients/lsp/client.js +42 -0
  26. package/clients/lsp/client.ts +79 -0
  27. package/clients/lsp/index.js +27 -0
  28. package/clients/lsp/index.ts +35 -0
  29. package/clients/lsp/launch.js +11 -6
  30. package/clients/lsp/launch.ts +11 -6
  31. package/clients/metrics-client.js +3 -160
  32. package/clients/metrics-client.tdr.test.js +78 -0
  33. package/clients/metrics-client.test.js +30 -43
  34. package/clients/metrics-client.test.ts +30 -54
  35. package/clients/metrics-client.ts +5 -219
  36. package/clients/metrics-history.js +33 -7
  37. package/clients/metrics-history.ts +47 -10
  38. package/clients/pipeline.js +272 -0
  39. package/clients/pipeline.ts +371 -0
  40. package/clients/sg-runner.js +21 -3
  41. package/clients/sg-runner.ts +22 -3
  42. package/clients/tree-sitter-client.js +23 -2
  43. package/clients/tree-sitter-client.ts +27 -2
  44. package/index.ts +604 -771
  45. package/package.json +1 -1
  46. package/rules/ast-grep-rules/rules/no-architecture-violation.yml +7 -4
  47. package/rules/ast-grep-rules/rules/no-single-char-var.yml +3 -3
  48. package/rules/ast-grep-rules/slop-patterns.yml +85 -62
  49. package/skills/ast-grep/SKILL.md +42 -1
  50. package/skills/lsp-navigation/SKILL.md +62 -0
  51. package/tsconfig.json +1 -1
  52. package/rules/ast-grep-rules/rules/no-console-log.yml +0 -10
  53. 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
- * Execute a structured rule using manual AST traversal
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 executeStructuredRule(
86
- rootNode: any,
102
+ function nodeMatchesCondition(
103
+ node: any,
87
104
  condition: YamlRuleCondition,
88
- matches: unknown[] = [],
89
105
  depth = 0,
90
- ): unknown[] {
91
- if (depth > MAX_RULE_DEPTH) return matches;
106
+ ): boolean {
107
+ if (depth > MAX_RULE_DEPTH) return false;
92
108
 
93
- let candidates: unknown[] = [];
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
- candidates = rootNode.findAll(condition.pattern);
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 matches;
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
- for (const candidate of candidates) {
108
- const node = candidate as {
109
- text(): string;
110
- kind(): string;
111
- children(): unknown[];
112
- };
113
- let matchesCondition = true;
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
- if (condition.not && matchesCondition) {
126
- const subMatches = executeStructuredRule(
127
- node,
128
- condition.not,
129
- [],
130
- depth + 1,
131
- );
132
- if (subMatches.length > 0) matchesCondition = false;
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
- if (condition.any && matchesCondition) {
136
- let anyMatches = false;
137
- for (const subCondition of condition.any) {
138
- const subMatches = executeStructuredRule(
139
- node,
140
- subCondition,
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
- if (condition.all && matchesCondition) {
153
- for (const subCondition of condition.all) {
154
- const subMatches = executeStructuredRule(
155
- node,
156
- subCondition,
157
- [],
158
- depth + 1,
159
- );
160
- if (subMatches.length === 0) {
161
- matchesCondition = false;
162
- break;
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
- if (condition.regex && matchesCondition) {
168
- try {
169
- const text = node.text();
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
- if (matchesCondition) {
178
- matches.push(node);
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
- return matches;
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
- // 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,
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 weight = rule.metadata?.weight || 3;
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: diagnostics.length > 0 ? "warning" : ("none" as const),
456
+ semantic: hasBlocking
457
+ ? "blocking"
458
+ : diagnostics.length > 0
459
+ ? "warning"
460
+ : ("none" as const),
349
461
  };
350
462
  },
351
463
  };
@@ -90,7 +90,7 @@ const similarityRunner = {
90
90
  };
91
91
  },
92
92
  };
93
- function extractFunctions(sourceFile, _fullContent) {
93
+ export function extractFunctions(sourceFile, _fullContent) {
94
94
  const functions = [];
95
95
  function visit(node) {
96
96
  // Function declarations
@@ -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 path from "node:path";
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
- // Load queries if not already loaded
60
- if (!queryLoader.getAllQueries().length) {
61
- await queryLoader.loadQueries();
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, { maxResults: 10, fileFilter: (f) => f === filePath });
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;