pi-lens 3.3.1 → 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 +83 -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/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
package/clients/dispatch/plan.js
CHANGED
|
@@ -31,10 +31,12 @@ export const TOOL_PLANS = {
|
|
|
31
31
|
// Tree-sitter native structural analysis (blocking rules: constructor-super, dangerouslySetInnerHTML, etc.)
|
|
32
32
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["jsts"] },
|
|
33
33
|
// AST structural analysis (blocking: no-dupe-keys, no-hardcoded-secrets, jwt-no-verify, etc.)
|
|
34
|
-
//
|
|
35
|
-
|
|
34
|
+
// Only error-severity rules fire inline (blockingOnly=true). Warnings are booboo-only.
|
|
35
|
+
{ mode: "all", runnerIds: ["ast-grep-napi"], filterKinds: ["jsts"] },
|
|
36
36
|
// Type safety checks (has some blocking errors)
|
|
37
37
|
{ mode: "fallback", runnerIds: ["type-safety"], filterKinds: ["jsts"] },
|
|
38
|
+
// Similarity detection — warns about duplicated/reusable code
|
|
39
|
+
{ mode: "fallback", runnerIds: ["similarity"], filterKinds: ["jsts"] },
|
|
38
40
|
// Note: ast-grep CLI kept for ast_grep_search/ast_grep_replace tools only
|
|
39
41
|
// Note: biome, oxlint handled by direct auto-fix calls in index.ts (not in dispatch)
|
|
40
42
|
// Architectural rules (guidance only, not blocking) - runs via /lens-booboo only
|
|
@@ -163,6 +165,7 @@ export const FULL_LINT_PLANS = {
|
|
|
163
165
|
filterKinds: ["jsts"],
|
|
164
166
|
},
|
|
165
167
|
{ mode: "fallback", runnerIds: ["type-safety"], filterKinds: ["jsts"] },
|
|
168
|
+
{ mode: "fallback", runnerIds: ["similarity"], filterKinds: ["jsts"] },
|
|
166
169
|
{ mode: "fallback", runnerIds: ["architect"], filterKinds: ["jsts"] },
|
|
167
170
|
],
|
|
168
171
|
},
|
package/clients/dispatch/plan.ts
CHANGED
|
@@ -35,10 +35,12 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
|
|
|
35
35
|
// Tree-sitter native structural analysis (blocking rules: constructor-super, dangerouslySetInnerHTML, etc.)
|
|
36
36
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["jsts"] },
|
|
37
37
|
// AST structural analysis (blocking: no-dupe-keys, no-hardcoded-secrets, jwt-no-verify, etc.)
|
|
38
|
-
//
|
|
39
|
-
|
|
38
|
+
// Only error-severity rules fire inline (blockingOnly=true). Warnings are booboo-only.
|
|
39
|
+
{ mode: "all", runnerIds: ["ast-grep-napi"], filterKinds: ["jsts"] },
|
|
40
40
|
// Type safety checks (has some blocking errors)
|
|
41
41
|
{ mode: "fallback", runnerIds: ["type-safety"], filterKinds: ["jsts"] },
|
|
42
|
+
// Similarity detection — warns about duplicated/reusable code
|
|
43
|
+
{ mode: "fallback", runnerIds: ["similarity"], filterKinds: ["jsts"] },
|
|
42
44
|
// Note: ast-grep CLI kept for ast_grep_search/ast_grep_replace tools only
|
|
43
45
|
// Note: biome, oxlint handled by direct auto-fix calls in index.ts (not in dispatch)
|
|
44
46
|
// Architectural rules (guidance only, not blocking) - runs via /lens-booboo only
|
|
@@ -178,6 +180,7 @@ export const FULL_LINT_PLANS: Record<string, ToolPlan> = {
|
|
|
178
180
|
filterKinds: ["jsts"],
|
|
179
181
|
},
|
|
180
182
|
{ mode: "fallback", runnerIds: ["type-safety"], filterKinds: ["jsts"] },
|
|
183
|
+
{ mode: "fallback", runnerIds: ["similarity"], filterKinds: ["jsts"] },
|
|
181
184
|
{ mode: "fallback", runnerIds: ["architect"], filterKinds: ["jsts"] },
|
|
182
185
|
],
|
|
183
186
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
-
import { calculateRuleComplexity, isStructuredRule, loadYamlRules, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
|
|
11
|
+
import { calculateRuleComplexity, hasUnsupportedConditions, isOverlyBroadPattern, isStructuredRule, loadYamlRules, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
|
|
12
12
|
// Lazy load the napi package
|
|
13
13
|
let sg;
|
|
14
14
|
let sgLoadAttempted = false;
|
|
@@ -32,6 +32,14 @@ const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
|
|
|
32
32
|
const MAX_MATCHES_PER_RULE = 10;
|
|
33
33
|
/** Maximum total diagnostics per file to prevent output spam */
|
|
34
34
|
const MAX_TOTAL_DIAGNOSTICS = 50;
|
|
35
|
+
/** Rules already covered by tree-sitter runner (priority 14, runs first) */
|
|
36
|
+
const TREE_SITTER_OVERLAP = new Set([
|
|
37
|
+
"constructor-super",
|
|
38
|
+
"empty-catch",
|
|
39
|
+
"long-parameter-list",
|
|
40
|
+
"nested-ternary",
|
|
41
|
+
"no-dupe-class-members",
|
|
42
|
+
]);
|
|
35
43
|
/** Maximum AST depth to traverse to prevent stack overflow on deeply nested files */
|
|
36
44
|
const MAX_AST_DEPTH = 50;
|
|
37
45
|
/** Maximum recursion depth for structured rule execution */
|
|
@@ -59,76 +67,170 @@ function getLang(filePath, sgModule) {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
62
|
-
*
|
|
70
|
+
* Check if a single node matches a condition (without searching descendants).
|
|
71
|
+
* In ast-grep semantics:
|
|
72
|
+
* - pattern/kind/regex: check the node itself
|
|
73
|
+
* - all: node must match ALL sub-conditions
|
|
74
|
+
* - any: node must match at least ONE sub-condition
|
|
75
|
+
* - not: node must NOT match the sub-condition
|
|
76
|
+
* - has: node must have a DESCENDANT matching the sub-condition
|
|
63
77
|
*/
|
|
64
|
-
function
|
|
78
|
+
function nodeMatchesCondition(node, condition, depth = 0) {
|
|
65
79
|
if (depth > MAX_RULE_DEPTH)
|
|
66
|
-
return
|
|
67
|
-
|
|
80
|
+
return false;
|
|
81
|
+
// Check kind constraint
|
|
82
|
+
if (condition.kind && node.kind() !== condition.kind)
|
|
83
|
+
return false;
|
|
84
|
+
// Check pattern constraint (node itself must match)
|
|
68
85
|
if (condition.pattern) {
|
|
86
|
+
try {
|
|
87
|
+
const matches = node.findAll(condition.pattern);
|
|
88
|
+
// Check if the node itself is among the matches (same start position)
|
|
89
|
+
const nodeRange = node.range();
|
|
90
|
+
let selfMatch = false;
|
|
91
|
+
for (const m of matches) {
|
|
92
|
+
const mr = m.range();
|
|
93
|
+
if (mr.start.line === nodeRange.start.line &&
|
|
94
|
+
mr.start.column === nodeRange.start.column &&
|
|
95
|
+
mr.end.line === nodeRange.end.line &&
|
|
96
|
+
mr.end.column === nodeRange.end.column) {
|
|
97
|
+
selfMatch = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!selfMatch)
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Check regex constraint
|
|
109
|
+
if (condition.regex) {
|
|
110
|
+
try {
|
|
111
|
+
const text = node.text();
|
|
112
|
+
if (!new RegExp(condition.regex).test(text))
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Check has (descendant must match)
|
|
120
|
+
if (condition.has) {
|
|
121
|
+
const descendants = findMatchingNodes(node, condition.has, depth + 1);
|
|
122
|
+
if (descendants.length === 0)
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// Check not (node must NOT match this condition)
|
|
126
|
+
if (condition.not) {
|
|
127
|
+
if (nodeMatchesCondition(node, condition.not, depth + 1))
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
// Check all (node must match ALL sub-conditions)
|
|
131
|
+
if (condition.all) {
|
|
132
|
+
for (const sub of condition.all) {
|
|
133
|
+
if (!nodeMatchesCondition(node, sub, depth + 1))
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Check any (node must match at least one sub-condition)
|
|
138
|
+
if (condition.any) {
|
|
139
|
+
let anyMatch = false;
|
|
140
|
+
for (const sub of condition.any) {
|
|
141
|
+
if (nodeMatchesCondition(node, sub, depth + 1)) {
|
|
142
|
+
anyMatch = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!anyMatch)
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Find all nodes in the tree that match a condition.
|
|
153
|
+
* This is the "search" function - traverses the tree and checks each node.
|
|
154
|
+
*/
|
|
155
|
+
function findMatchingNodes(rootNode, condition, depth = 0) {
|
|
156
|
+
if (depth > MAX_RULE_DEPTH)
|
|
157
|
+
return [];
|
|
158
|
+
const matches = [];
|
|
159
|
+
// Optimization: if the condition has a kind, only check nodes of that kind
|
|
160
|
+
// If it has a pattern, use findAll for initial candidates
|
|
161
|
+
let candidates;
|
|
162
|
+
if (condition.pattern && !condition.all && !condition.any) {
|
|
163
|
+
// Use findAll for pattern-only conditions (fast path)
|
|
69
164
|
try {
|
|
70
165
|
candidates = rootNode.findAll(condition.pattern);
|
|
71
166
|
}
|
|
72
167
|
catch {
|
|
73
|
-
return
|
|
168
|
+
return [];
|
|
74
169
|
}
|
|
75
170
|
}
|
|
76
|
-
else if (condition.kind) {
|
|
171
|
+
else if (condition.kind && !condition.all && !condition.any) {
|
|
172
|
+
// Use findByKind for kind-only conditions (fast path)
|
|
77
173
|
candidates = findByKind(rootNode, condition.kind, 0);
|
|
78
174
|
}
|
|
175
|
+
else if (condition.all) {
|
|
176
|
+
// For `all`, find the narrowest sub-condition to generate candidates
|
|
177
|
+
candidates = getCandidatesForAll(rootNode, condition.all);
|
|
178
|
+
}
|
|
179
|
+
else if (condition.any) {
|
|
180
|
+
// For `any`, union candidates from all sub-conditions
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
candidates = [];
|
|
183
|
+
for (const sub of condition.any) {
|
|
184
|
+
const subMatches = findMatchingNodes(rootNode, sub, depth + 1);
|
|
185
|
+
for (const m of subMatches) {
|
|
186
|
+
const r = m.range();
|
|
187
|
+
const key = `${r.start.line}:${r.start.column}`;
|
|
188
|
+
if (!seen.has(key)) {
|
|
189
|
+
seen.add(key);
|
|
190
|
+
candidates.push(m);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
79
195
|
else {
|
|
196
|
+
// Fallback: traverse all nodes
|
|
80
197
|
candidates = getAllNodes(rootNode, 0);
|
|
81
198
|
}
|
|
82
199
|
for (const candidate of candidates) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (condition.has && matchesCondition) {
|
|
86
|
-
const subMatches = executeStructuredRule(node, condition.has, [], depth + 1);
|
|
87
|
-
if (subMatches.length === 0)
|
|
88
|
-
matchesCondition = false;
|
|
89
|
-
}
|
|
90
|
-
if (condition.not && matchesCondition) {
|
|
91
|
-
const subMatches = executeStructuredRule(node, condition.not, [], depth + 1);
|
|
92
|
-
if (subMatches.length > 0)
|
|
93
|
-
matchesCondition = false;
|
|
94
|
-
}
|
|
95
|
-
if (condition.any && matchesCondition) {
|
|
96
|
-
let anyMatches = false;
|
|
97
|
-
for (const subCondition of condition.any) {
|
|
98
|
-
const subMatches = executeStructuredRule(node, subCondition, [], depth + 1);
|
|
99
|
-
if (subMatches.length > 0) {
|
|
100
|
-
anyMatches = true;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (!anyMatches)
|
|
105
|
-
matchesCondition = false;
|
|
200
|
+
if (nodeMatchesCondition(candidate, condition, depth)) {
|
|
201
|
+
matches.push(candidate);
|
|
106
202
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
}
|
|
204
|
+
return matches;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* For an `all` condition, find the narrowest sub-condition to generate
|
|
208
|
+
* initial candidates. This avoids scanning all nodes when one sub-condition
|
|
209
|
+
* has a specific kind or pattern.
|
|
210
|
+
*/
|
|
211
|
+
function getCandidatesForAll(rootNode, subs) {
|
|
212
|
+
// Prefer kind-based narrowing first, then pattern-based
|
|
213
|
+
for (const sub of subs) {
|
|
214
|
+
if (sub.kind) {
|
|
215
|
+
return findByKind(rootNode, sub.kind, 0);
|
|
115
216
|
}
|
|
116
|
-
|
|
217
|
+
}
|
|
218
|
+
for (const sub of subs) {
|
|
219
|
+
if (sub.pattern) {
|
|
117
220
|
try {
|
|
118
|
-
|
|
119
|
-
const regex = new RegExp(condition.regex);
|
|
120
|
-
if (!regex.test(text))
|
|
121
|
-
matchesCondition = false;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
matchesCondition = false;
|
|
221
|
+
return rootNode.findAll(sub.pattern);
|
|
125
222
|
}
|
|
126
|
-
|
|
127
|
-
if (matchesCondition) {
|
|
128
|
-
matches.push(node);
|
|
223
|
+
catch { }
|
|
129
224
|
}
|
|
130
225
|
}
|
|
131
|
-
|
|
226
|
+
// No narrowing possible, scan all
|
|
227
|
+
return getAllNodes(rootNode, 0);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Legacy wrapper - execute a structured rule using the new two-phase approach.
|
|
231
|
+
*/
|
|
232
|
+
function executeStructuredRule(rootNode, condition, matches = [], depth = 0) {
|
|
233
|
+
return findMatchingNodes(rootNode, condition, depth);
|
|
132
234
|
}
|
|
133
235
|
/**
|
|
134
236
|
* Find all nodes of a specific kind with depth limit
|
|
@@ -161,9 +263,7 @@ const astGrepNapiRunner = {
|
|
|
161
263
|
id: "ast-grep-napi",
|
|
162
264
|
appliesTo: ["jsts"],
|
|
163
265
|
priority: 15,
|
|
164
|
-
|
|
165
|
-
// Still enabled for /lens-booboo via FULL_LINT_PLANS.
|
|
166
|
-
enabledByDefault: false,
|
|
266
|
+
enabledByDefault: true,
|
|
167
267
|
skipTestFiles: true,
|
|
168
268
|
async run(ctx) {
|
|
169
269
|
if (!canHandle(ctx.filePath)) {
|
|
@@ -219,6 +319,21 @@ const astGrepNapiRunner = {
|
|
|
219
319
|
continue;
|
|
220
320
|
}
|
|
221
321
|
for (const rule of rules) {
|
|
322
|
+
// Skip rules already handled by tree-sitter runner (priority 14)
|
|
323
|
+
if (TREE_SITTER_OVERLAP.has(rule.id))
|
|
324
|
+
continue;
|
|
325
|
+
// Skip rules using conditions we can't execute (inside, follows,
|
|
326
|
+
// precedes, stopBy, field, nthChild, constraints). Running these
|
|
327
|
+
// with only partial condition evaluation causes false positives.
|
|
328
|
+
if (hasUnsupportedConditions(rule))
|
|
329
|
+
continue;
|
|
330
|
+
// Skip rules whose top-level pattern is overly broad ($NAME, $X, etc.)
|
|
331
|
+
// without additional structural constraints to narrow matches.
|
|
332
|
+
if (rule.rule &&
|
|
333
|
+
isOverlyBroadPattern(rule.rule.pattern) &&
|
|
334
|
+
!isStructuredRule(rule)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
222
337
|
const lang = rule.language?.toLowerCase();
|
|
223
338
|
if (lang && lang !== "typescript" && lang !== "javascript") {
|
|
224
339
|
continue;
|
|
@@ -253,8 +368,7 @@ const astGrepNapiRunner = {
|
|
|
253
368
|
break;
|
|
254
369
|
const node = match;
|
|
255
370
|
const range = node.range();
|
|
256
|
-
const
|
|
257
|
-
const severity = weight >= 4 ? "error" : "warning";
|
|
371
|
+
const severity = rule.severity === "error" ? "error" : "warning";
|
|
258
372
|
diagnostics.push({
|
|
259
373
|
id: `ast-grep-napi-${range.start.line}-${rule.id}`,
|
|
260
374
|
message: `[${rule.metadata?.category || "slop"}] ${rule.message || rule.id}`,
|
|
@@ -276,10 +390,15 @@ const astGrepNapiRunner = {
|
|
|
276
390
|
}
|
|
277
391
|
}
|
|
278
392
|
}
|
|
393
|
+
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
279
394
|
return {
|
|
280
395
|
status: "succeeded",
|
|
281
396
|
diagnostics,
|
|
282
|
-
semantic:
|
|
397
|
+
semantic: hasBlocking
|
|
398
|
+
? "blocking"
|
|
399
|
+
: diagnostics.length > 0
|
|
400
|
+
? "warning"
|
|
401
|
+
: "none",
|
|
283
402
|
};
|
|
284
403
|
},
|
|
285
404
|
};
|
|
@@ -64,7 +64,8 @@ function riskyOperation() {
|
|
|
64
64
|
console.log("NAPI result diagnostics count:", napiResult.diagnostics?.length);
|
|
65
65
|
// Should complete successfully (not skipped, not failed)
|
|
66
66
|
expect(napiResult.status).toBe("succeeded");
|
|
67
|
-
|
|
67
|
+
// Semantic reflects the highest severity: "blocking" if any error-severity rules matched
|
|
68
|
+
expect(["warning", "blocking"]).toContain(napiResult.semantic);
|
|
68
69
|
// Log findings
|
|
69
70
|
console.log("NAPI found:", napiResult.diagnostics.length, "issues");
|
|
70
71
|
console.log("\n=== NAPI FINDINGS ===");
|
|
@@ -83,7 +83,8 @@ function riskyOperation() {
|
|
|
83
83
|
|
|
84
84
|
// Should complete successfully (not skipped, not failed)
|
|
85
85
|
expect(napiResult.status).toBe("succeeded");
|
|
86
|
-
|
|
86
|
+
// Semantic reflects the highest severity: "blocking" if any error-severity rules matched
|
|
87
|
+
expect(["warning", "blocking"]).toContain(napiResult.semantic);
|
|
87
88
|
|
|
88
89
|
// Log findings
|
|
89
90
|
console.log("NAPI found:", napiResult.diagnostics.length, "issues");
|