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
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* Updated: ast-grep-napi test
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { RuleCache } from "../../cache/rule-cache.js";
|
|
12
|
+
import { normalizeMapKey } from "../../path-utils.js";
|
|
10
13
|
import { TreeSitterClient } from "../../tree-sitter-client.js";
|
|
11
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
queryLoader,
|
|
16
|
+
type TreeSitterQuery,
|
|
17
|
+
} from "../../tree-sitter-query-loader.js";
|
|
12
18
|
import type {
|
|
13
19
|
Diagnostic,
|
|
14
20
|
DispatchContext,
|
|
@@ -27,6 +33,88 @@ function getSharedClient(): TreeSitterClient {
|
|
|
27
33
|
return _sharedClient;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check if a code block is effectively empty (ignoring comments and whitespace)
|
|
38
|
+
*/
|
|
39
|
+
function isEmptyBlock(blockContent: string): boolean {
|
|
40
|
+
// Remove comments, whitespace, and check if anything remains
|
|
41
|
+
const cleaned = blockContent
|
|
42
|
+
.replace(/\/\/.*$/gm, "") // Remove single-line comments
|
|
43
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
|
|
44
|
+
.replace(/\s+/g, "") // Remove all whitespace
|
|
45
|
+
.trim();
|
|
46
|
+
return cleaned.length === 0 || cleaned === "{}";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract parameter count from match text
|
|
51
|
+
*/
|
|
52
|
+
function countParameters(matchText: string): number {
|
|
53
|
+
// Count commas in parameter list, or check for non-empty params
|
|
54
|
+
// Simple heuristic: count commas + 1, or 0 if empty
|
|
55
|
+
const paramsMatch = matchText.match(/\((.*)\)/);
|
|
56
|
+
if (!paramsMatch) return 0;
|
|
57
|
+
const params = paramsMatch[1].trim();
|
|
58
|
+
if (!params) return 0;
|
|
59
|
+
return params.split(",").length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply post-filter to determine if a match should be reported
|
|
64
|
+
*/
|
|
65
|
+
function applyPostFilter(
|
|
66
|
+
query: TreeSitterQuery,
|
|
67
|
+
captures: Record<string, string>,
|
|
68
|
+
): boolean {
|
|
69
|
+
if (!query.post_filter) return true; // No filter = always include
|
|
70
|
+
|
|
71
|
+
switch (query.post_filter) {
|
|
72
|
+
case "empty_body": {
|
|
73
|
+
// Check if the BODY capture is effectively empty
|
|
74
|
+
const body = captures.BODY || captures.body || "";
|
|
75
|
+
return isEmptyBlock(body);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case "count_params": {
|
|
79
|
+
// Check if parameter count meets minimum
|
|
80
|
+
const minParams = query.post_filter_params?.min_params || 6;
|
|
81
|
+
// Get PARAMS capture which contains the parameter list like "(a, b, c)"
|
|
82
|
+
const params = captures.PARAMS || captures.params || captures.PARAM || "";
|
|
83
|
+
const paramCount = countParameters(params);
|
|
84
|
+
return paramCount >= minParams;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case "not_dbg_method":
|
|
88
|
+
// Exclude debug methods (for console-statement)
|
|
89
|
+
return !/\b(dbg|debug|logDebug)\b/i.test(captures.METHOD || "");
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
// Unknown filter - include by default (safer than excluding)
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if variable name matches secret patterns
|
|
99
|
+
* This handles the #match? predicate from tree-sitter queries
|
|
100
|
+
*/
|
|
101
|
+
function matchesSecretPattern(varName: string): boolean {
|
|
102
|
+
const secretPatterns = [
|
|
103
|
+
/api[_-]?key/i,
|
|
104
|
+
/api[_-]?secret/i,
|
|
105
|
+
/password/i,
|
|
106
|
+
/secret/i,
|
|
107
|
+
/token/i,
|
|
108
|
+
/auth/i,
|
|
109
|
+
/private[_-]?key/i,
|
|
110
|
+
/access[_-]?token/i,
|
|
111
|
+
/credentials/i,
|
|
112
|
+
/aws[_-]?secret/i,
|
|
113
|
+
/github[_-]?token/i,
|
|
114
|
+
];
|
|
115
|
+
return secretPatterns.some((pattern) => pattern.test(varName));
|
|
116
|
+
}
|
|
117
|
+
|
|
30
118
|
const treeSitterRunner: RunnerDefinition = {
|
|
31
119
|
id: "tree-sitter",
|
|
32
120
|
appliesTo: ["jsts", "python"],
|
|
@@ -66,18 +154,68 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
66
154
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
67
155
|
}
|
|
68
156
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
157
|
+
// Try cache first, fall back to loading from disk
|
|
158
|
+
let languageQueries: TreeSitterQuery[] = [];
|
|
159
|
+
const cache = new RuleCache(languageId);
|
|
73
160
|
|
|
74
|
-
// Get all
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
161
|
+
// Get all rule files for this language (use ctx.cwd for project root)
|
|
162
|
+
const rulesDir = path.join(
|
|
163
|
+
ctx.cwd,
|
|
164
|
+
"rules",
|
|
165
|
+
"tree-sitter-queries",
|
|
166
|
+
languageId,
|
|
80
167
|
);
|
|
168
|
+
const ruleFiles: string[] = [];
|
|
169
|
+
if (fs.existsSync(rulesDir)) {
|
|
170
|
+
ruleFiles.push(
|
|
171
|
+
...fs
|
|
172
|
+
.readdirSync(rulesDir)
|
|
173
|
+
.filter((f) => f.endsWith(".yml"))
|
|
174
|
+
.map((f) => path.join(rulesDir, f)),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try cache
|
|
179
|
+
const cached = cache.get(ruleFiles);
|
|
180
|
+
if (cached) {
|
|
181
|
+
// Use cached queries
|
|
182
|
+
languageQueries = cached.queries.map(
|
|
183
|
+
(q) =>
|
|
184
|
+
({
|
|
185
|
+
...q,
|
|
186
|
+
has_fix: false,
|
|
187
|
+
filePath: "",
|
|
188
|
+
}) as TreeSitterQuery,
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
// Load from disk
|
|
192
|
+
if (!queryLoader.getAllQueries().length) {
|
|
193
|
+
await queryLoader.loadQueries();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const allQueries = queryLoader.getAllQueries();
|
|
197
|
+
languageQueries = allQueries.filter(
|
|
198
|
+
(q) =>
|
|
199
|
+
q.language === languageId ||
|
|
200
|
+
(isJavaScript && q.language === "typescript"),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Save to cache
|
|
204
|
+
cache.set(
|
|
205
|
+
ruleFiles,
|
|
206
|
+
languageQueries.map((q) => ({
|
|
207
|
+
id: q.id,
|
|
208
|
+
name: q.name,
|
|
209
|
+
severity: q.severity,
|
|
210
|
+
language: q.language,
|
|
211
|
+
message: q.message,
|
|
212
|
+
query: q.query,
|
|
213
|
+
metavars: q.metavars,
|
|
214
|
+
post_filter: q.post_filter,
|
|
215
|
+
post_filter_params: q.post_filter_params,
|
|
216
|
+
})),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
81
219
|
|
|
82
220
|
if (languageQueries.length === 0) {
|
|
83
221
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
@@ -95,10 +233,27 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
95
233
|
query.id, // Use query ID as pattern (findMatchingQuery will resolve it)
|
|
96
234
|
languageId,
|
|
97
235
|
rootDir,
|
|
98
|
-
{
|
|
236
|
+
{
|
|
237
|
+
maxResults: 10,
|
|
238
|
+
fileFilter: (f) => normalizeMapKey(f) === normalizeMapKey(filePath),
|
|
239
|
+
},
|
|
99
240
|
);
|
|
100
241
|
|
|
101
242
|
for (const match of matches) {
|
|
243
|
+
// Apply post-filter if defined (pass captures for proper filtering)
|
|
244
|
+
if (!applyPostFilter(query, match.captures)) {
|
|
245
|
+
continue; // Skip this match - filter didn't pass
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// For hardcoded-secrets, also check variable name pattern
|
|
249
|
+
if (query.id === "hardcoded-secrets") {
|
|
250
|
+
// Extract variable name from captures
|
|
251
|
+
const varName = match.captures?.VARNAME || "";
|
|
252
|
+
if (!varName || !matchesSecretPattern(varName)) {
|
|
253
|
+
continue; // Skip - no variable name or doesn't match secret patterns
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
102
257
|
// Get line/column from match (already 0-indexed from tree-sitter)
|
|
103
258
|
const line = match.line;
|
|
104
259
|
const column = match.column;
|
|
@@ -19,9 +19,10 @@ const tsLspRunner = {
|
|
|
19
19
|
if (!ctx.filePath.match(/\.tsx?$/)) {
|
|
20
20
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
21
21
|
}
|
|
22
|
-
//
|
|
22
|
+
// When --lens-lsp is active, the `lsp` runner (priority 4) already
|
|
23
|
+
// handles TypeScript via the LSP service. Skip to avoid duplicate work.
|
|
23
24
|
if (ctx.pi.getFlag("lens-lsp")) {
|
|
24
|
-
return
|
|
25
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
25
26
|
}
|
|
26
27
|
// DEPRECATED: Fall back to built-in TypeScriptClient
|
|
27
28
|
// This path is deprecated and will be removed in a future release
|
|
@@ -29,9 +29,10 @@ const tsLspRunner: RunnerDefinition = {
|
|
|
29
29
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// When --lens-lsp is active, the `lsp` runner (priority 4) already
|
|
33
|
+
// handles TypeScript via the LSP service. Skip to avoid duplicate work.
|
|
33
34
|
if (ctx.pi.getFlag("lens-lsp")) {
|
|
34
|
-
return
|
|
35
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// DEPRECATED: Fall back to built-in TypeScriptClient
|
|
@@ -108,6 +108,46 @@ export function isStructuredRule(rule) {
|
|
|
108
108
|
rule.rule.not ||
|
|
109
109
|
rule.rule.regex);
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if a rule or any of its nested conditions use features
|
|
113
|
+
* not supported by the NAPI runner (inside, follows, precedes,
|
|
114
|
+
* stopBy, field, nthChild). Rules using these must be skipped
|
|
115
|
+
* to prevent false positives from incomplete condition evaluation.
|
|
116
|
+
*/
|
|
117
|
+
export function hasUnsupportedConditions(rule) {
|
|
118
|
+
if (rule.constraints)
|
|
119
|
+
return true;
|
|
120
|
+
if (!rule.rule)
|
|
121
|
+
return false;
|
|
122
|
+
return conditionHasUnsupported(rule.rule);
|
|
123
|
+
}
|
|
124
|
+
function conditionHasUnsupported(c) {
|
|
125
|
+
if (c.inside ||
|
|
126
|
+
c.follows ||
|
|
127
|
+
c.precedes ||
|
|
128
|
+
c.stopBy ||
|
|
129
|
+
c.field ||
|
|
130
|
+
c.nthChild) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (c.has && conditionHasUnsupported(c.has))
|
|
134
|
+
return true;
|
|
135
|
+
if (c.not && conditionHasUnsupported(c.not))
|
|
136
|
+
return true;
|
|
137
|
+
if (c.any) {
|
|
138
|
+
for (const sub of c.any) {
|
|
139
|
+
if (conditionHasUnsupported(sub))
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (c.all) {
|
|
144
|
+
for (const sub of c.all) {
|
|
145
|
+
if (conditionHasUnsupported(sub))
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
111
151
|
export function calculateRuleComplexity(condition) {
|
|
112
152
|
if (!condition)
|
|
113
153
|
return 0;
|
|
@@ -209,6 +249,14 @@ export function parseSimpleYaml(content) {
|
|
|
209
249
|
? (multilineKey = "message")
|
|
210
250
|
: (rule.message = stripQuotes(value));
|
|
211
251
|
}
|
|
252
|
+
else if (key === "constraints") {
|
|
253
|
+
rule.constraints = {};
|
|
254
|
+
stack.push({
|
|
255
|
+
name: "constraints",
|
|
256
|
+
indent,
|
|
257
|
+
obj: rule.constraints,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
212
260
|
else if (key === "metadata") {
|
|
213
261
|
rule.metadata = {};
|
|
214
262
|
stack.push({ name: "metadata", indent, obj: rule.metadata });
|
|
@@ -241,6 +289,21 @@ export function parseSimpleYaml(content) {
|
|
|
241
289
|
else if (key === "regex") {
|
|
242
290
|
obj.regex = stripQuotes(value);
|
|
243
291
|
}
|
|
292
|
+
else if (key === "inside" || key === "follows" || key === "precedes") {
|
|
293
|
+
// Mark as present for unsupported-condition detection
|
|
294
|
+
obj[key] = {};
|
|
295
|
+
stack.push({ name: key, indent, obj: obj[key] });
|
|
296
|
+
}
|
|
297
|
+
else if (key === "stopBy") {
|
|
298
|
+
obj.stopBy = stripQuotes(value) || "end";
|
|
299
|
+
}
|
|
300
|
+
else if (key === "field") {
|
|
301
|
+
obj.field = stripQuotes(value);
|
|
302
|
+
}
|
|
303
|
+
else if (key === "nthChild") {
|
|
304
|
+
obj.nthChild = value || true;
|
|
305
|
+
stack.push({ name: "nthChild", indent, obj: {} });
|
|
306
|
+
}
|
|
244
307
|
else if (key === "has" || key === "not") {
|
|
245
308
|
obj[key] = {};
|
|
246
309
|
stack.push({ name: key, indent, obj: obj[key] });
|
|
@@ -269,14 +332,19 @@ export function parseSimpleYaml(content) {
|
|
|
269
332
|
obj: item,
|
|
270
333
|
});
|
|
271
334
|
const itemContent = nextTrimmed.substring(2);
|
|
272
|
-
|
|
273
|
-
|
|
335
|
+
const colonPos = itemContent.indexOf(":");
|
|
336
|
+
if (colonPos !== -1) {
|
|
337
|
+
const itemKey = itemContent.substring(0, colonPos);
|
|
338
|
+
const itemVal = itemContent.substring(colonPos + 1);
|
|
274
339
|
if (itemKey.trim() === "pattern") {
|
|
275
340
|
item.pattern = stripQuotes(itemVal.trim());
|
|
276
341
|
}
|
|
277
342
|
else if (itemKey.trim() === "kind") {
|
|
278
343
|
item.kind = itemVal.trim();
|
|
279
344
|
}
|
|
345
|
+
else if (itemKey.trim() === "regex") {
|
|
346
|
+
item.regex = stripQuotes(itemVal.trim());
|
|
347
|
+
}
|
|
280
348
|
}
|
|
281
349
|
else if (itemContent) {
|
|
282
350
|
item.pattern = stripQuotes(itemContent);
|
|
@@ -25,6 +25,14 @@ export interface YamlRuleCondition {
|
|
|
25
25
|
any?: YamlRuleCondition[];
|
|
26
26
|
all?: YamlRuleCondition[];
|
|
27
27
|
not?: YamlRuleCondition;
|
|
28
|
+
// Conditions parsed but NOT supported by the NAPI runner.
|
|
29
|
+
// Rules using these are skipped to prevent false positives.
|
|
30
|
+
inside?: YamlRuleCondition;
|
|
31
|
+
follows?: YamlRuleCondition;
|
|
32
|
+
precedes?: YamlRuleCondition;
|
|
33
|
+
stopBy?: string;
|
|
34
|
+
field?: string;
|
|
35
|
+
nthChild?: unknown;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export interface YamlRule {
|
|
@@ -34,6 +42,7 @@ export interface YamlRule {
|
|
|
34
42
|
message?: string;
|
|
35
43
|
metadata?: { weight?: number; category?: string };
|
|
36
44
|
rule?: YamlRuleCondition;
|
|
45
|
+
constraints?: Record<string, { regex?: string }>;
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
interface CachedRules {
|
|
@@ -164,6 +173,44 @@ export function isStructuredRule(rule: YamlRule): boolean {
|
|
|
164
173
|
);
|
|
165
174
|
}
|
|
166
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Check if a rule or any of its nested conditions use features
|
|
178
|
+
* not supported by the NAPI runner (inside, follows, precedes,
|
|
179
|
+
* stopBy, field, nthChild). Rules using these must be skipped
|
|
180
|
+
* to prevent false positives from incomplete condition evaluation.
|
|
181
|
+
*/
|
|
182
|
+
export function hasUnsupportedConditions(rule: YamlRule): boolean {
|
|
183
|
+
if (rule.constraints) return true;
|
|
184
|
+
if (!rule.rule) return false;
|
|
185
|
+
return conditionHasUnsupported(rule.rule);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function conditionHasUnsupported(c: YamlRuleCondition): boolean {
|
|
189
|
+
if (
|
|
190
|
+
c.inside ||
|
|
191
|
+
c.follows ||
|
|
192
|
+
c.precedes ||
|
|
193
|
+
c.stopBy ||
|
|
194
|
+
c.field ||
|
|
195
|
+
c.nthChild
|
|
196
|
+
) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (c.has && conditionHasUnsupported(c.has)) return true;
|
|
200
|
+
if (c.not && conditionHasUnsupported(c.not)) return true;
|
|
201
|
+
if (c.any) {
|
|
202
|
+
for (const sub of c.any) {
|
|
203
|
+
if (conditionHasUnsupported(sub)) return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (c.all) {
|
|
207
|
+
for (const sub of c.all) {
|
|
208
|
+
if (conditionHasUnsupported(sub)) return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
167
214
|
export function calculateRuleComplexity(
|
|
168
215
|
condition: YamlRuleCondition | undefined,
|
|
169
216
|
): number {
|
|
@@ -264,6 +311,13 @@ export function parseSimpleYaml(content: string): YamlRule | null {
|
|
|
264
311
|
value === "|"
|
|
265
312
|
? (multilineKey = "message")
|
|
266
313
|
: (rule.message = stripQuotes(value));
|
|
314
|
+
} else if (key === "constraints") {
|
|
315
|
+
rule.constraints = {};
|
|
316
|
+
stack.push({
|
|
317
|
+
name: "constraints",
|
|
318
|
+
indent,
|
|
319
|
+
obj: rule.constraints as unknown as YamlNode,
|
|
320
|
+
});
|
|
267
321
|
} else if (key === "metadata") {
|
|
268
322
|
rule.metadata = {};
|
|
269
323
|
stack.push({ name: "metadata", indent, obj: rule.metadata as YamlNode });
|
|
@@ -288,6 +342,17 @@ export function parseSimpleYaml(content: string): YamlRule | null {
|
|
|
288
342
|
obj.kind = value;
|
|
289
343
|
} else if (key === "regex") {
|
|
290
344
|
obj.regex = stripQuotes(value);
|
|
345
|
+
} else if (key === "inside" || key === "follows" || key === "precedes") {
|
|
346
|
+
// Mark as present for unsupported-condition detection
|
|
347
|
+
obj[key] = {} as YamlRuleCondition;
|
|
348
|
+
stack.push({ name: key, indent, obj: obj[key] as YamlNode });
|
|
349
|
+
} else if (key === "stopBy") {
|
|
350
|
+
obj.stopBy = stripQuotes(value) || "end";
|
|
351
|
+
} else if (key === "field") {
|
|
352
|
+
obj.field = stripQuotes(value);
|
|
353
|
+
} else if (key === "nthChild") {
|
|
354
|
+
obj.nthChild = value || true;
|
|
355
|
+
stack.push({ name: "nthChild", indent, obj: {} as YamlNode });
|
|
291
356
|
} else if (key === "has" || key === "not") {
|
|
292
357
|
obj[key] = {} as YamlRuleCondition;
|
|
293
358
|
stack.push({ name: key, indent, obj: obj[key] as YamlNode });
|
|
@@ -316,12 +381,16 @@ export function parseSimpleYaml(content: string): YamlRule | null {
|
|
|
316
381
|
});
|
|
317
382
|
|
|
318
383
|
const itemContent = nextTrimmed.substring(2);
|
|
319
|
-
|
|
320
|
-
|
|
384
|
+
const colonPos = itemContent.indexOf(":");
|
|
385
|
+
if (colonPos !== -1) {
|
|
386
|
+
const itemKey = itemContent.substring(0, colonPos);
|
|
387
|
+
const itemVal = itemContent.substring(colonPos + 1);
|
|
321
388
|
if (itemKey.trim() === "pattern") {
|
|
322
389
|
item.pattern = stripQuotes(itemVal.trim());
|
|
323
390
|
} else if (itemKey.trim() === "kind") {
|
|
324
391
|
item.kind = itemVal.trim();
|
|
392
|
+
} else if (itemKey.trim() === "regex") {
|
|
393
|
+
item.regex = stripQuotes(itemVal.trim());
|
|
325
394
|
}
|
|
326
395
|
} else if (itemContent) {
|
|
327
396
|
item.pattern = stripQuotes(itemContent);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - BLOCKING: Errors that stop the agent (architect, ts-lsp errors)
|
|
6
6
|
* - WARNING: Non-blocking issues (biome warnings, type-safety)
|
|
7
7
|
* - FIXABLE: Issues with auto-fix available
|
|
8
|
-
* - SILENT: Metrics tracked but not shown (complexity
|
|
8
|
+
* - SILENT: Metrics tracked but not shown (complexity)
|
|
9
9
|
* - INFORMATIONAL: Shown in session summary only
|
|
10
10
|
*
|
|
11
11
|
* The dispatcher must handle these semantics consistently.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - BLOCKING: Errors that stop the agent (architect, ts-lsp errors)
|
|
6
6
|
* - WARNING: Non-blocking issues (biome warnings, type-safety)
|
|
7
7
|
* - FIXABLE: Issues with auto-fix available
|
|
8
|
-
* - SILENT: Metrics tracked but not shown (complexity
|
|
8
|
+
* - SILENT: Metrics tracked but not shown (complexity)
|
|
9
9
|
* - INFORMATIONAL: Shown in session summary only
|
|
10
10
|
*
|
|
11
11
|
* The dispatcher must handle these semantics consistently.
|
|
@@ -430,6 +430,9 @@ function createMockClient(diagnostics = []) {
|
|
|
430
430
|
documentSymbol: vi.fn().mockResolvedValue([]),
|
|
431
431
|
workspaceSymbol: vi.fn().mockResolvedValue([]),
|
|
432
432
|
implementation: vi.fn().mockResolvedValue([]),
|
|
433
|
+
prepareCallHierarchy: vi.fn().mockResolvedValue([]),
|
|
434
|
+
incomingCalls: vi.fn().mockResolvedValue([]),
|
|
435
|
+
outgoingCalls: vi.fn().mockResolvedValue([]),
|
|
433
436
|
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
434
437
|
};
|
|
435
438
|
}
|
|
@@ -522,6 +522,9 @@ function createMockClient(diagnostics: any[] = []): LSPClientInfo {
|
|
|
522
522
|
documentSymbol: vi.fn().mockResolvedValue([]),
|
|
523
523
|
workspaceSymbol: vi.fn().mockResolvedValue([]),
|
|
524
524
|
implementation: vi.fn().mockResolvedValue([]),
|
|
525
|
+
prepareCallHierarchy: vi.fn().mockResolvedValue([]),
|
|
526
|
+
incomingCalls: vi.fn().mockResolvedValue([]),
|
|
527
|
+
outgoingCalls: vi.fn().mockResolvedValue([]),
|
|
525
528
|
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
526
529
|
};
|
|
527
530
|
}
|
package/clients/lsp/client.js
CHANGED
|
@@ -263,6 +263,48 @@ export async function createLSPClient(options) {
|
|
|
263
263
|
return [];
|
|
264
264
|
}
|
|
265
265
|
},
|
|
266
|
+
// --- Call Hierarchy Methods ---
|
|
267
|
+
async prepareCallHierarchy(filePath, line, character) {
|
|
268
|
+
const uri = pathToFileURL(filePath).href;
|
|
269
|
+
try {
|
|
270
|
+
const result = await connection.sendRequest("textDocument/prepareCallHierarchy", {
|
|
271
|
+
textDocument: { uri },
|
|
272
|
+
position: { line, character },
|
|
273
|
+
});
|
|
274
|
+
if (!result)
|
|
275
|
+
return [];
|
|
276
|
+
return Array.isArray(result) ? result : [result];
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
async incomingCalls(item) {
|
|
283
|
+
try {
|
|
284
|
+
const result = await connection.sendRequest("callHierarchy/incomingCalls", {
|
|
285
|
+
item,
|
|
286
|
+
});
|
|
287
|
+
if (!result)
|
|
288
|
+
return [];
|
|
289
|
+
return Array.isArray(result) ? result : [];
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
async outgoingCalls(item) {
|
|
296
|
+
try {
|
|
297
|
+
const result = await connection.sendRequest("callHierarchy/outgoingCalls", {
|
|
298
|
+
item,
|
|
299
|
+
});
|
|
300
|
+
if (!result)
|
|
301
|
+
return [];
|
|
302
|
+
return Array.isArray(result) ? result : [];
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
},
|
|
266
308
|
async shutdown() {
|
|
267
309
|
// Clear pending timers
|
|
268
310
|
for (const timer of pendingDiagnostics.values()) {
|
package/clients/lsp/client.ts
CHANGED
|
@@ -59,6 +59,26 @@ export interface LSPSymbol {
|
|
|
59
59
|
children?: LSPSymbol[];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// --- Call Hierarchy Types ---
|
|
63
|
+
|
|
64
|
+
export interface LSPCallHierarchyItem {
|
|
65
|
+
name: string;
|
|
66
|
+
kind: number;
|
|
67
|
+
uri: string;
|
|
68
|
+
range: LSPLocation["range"];
|
|
69
|
+
selectionRange: LSPLocation["range"];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LSPCallHierarchyIncomingCall {
|
|
73
|
+
from: LSPCallHierarchyItem;
|
|
74
|
+
fromRanges: LSPLocation["range"][];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LSPCallHierarchyOutgoingCall {
|
|
78
|
+
to: LSPCallHierarchyItem;
|
|
79
|
+
fromRanges: LSPLocation["range"][];
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
export interface LSPClientInfo {
|
|
63
83
|
serverId: string;
|
|
64
84
|
root: string;
|
|
@@ -100,6 +120,16 @@ export interface LSPClientInfo {
|
|
|
100
120
|
line: number,
|
|
101
121
|
character: number,
|
|
102
122
|
): Promise<LSPLocation[]>;
|
|
123
|
+
/** Prepare call hierarchy at position */
|
|
124
|
+
prepareCallHierarchy(
|
|
125
|
+
filePath: string,
|
|
126
|
+
line: number,
|
|
127
|
+
character: number,
|
|
128
|
+
): Promise<LSPCallHierarchyItem[]>;
|
|
129
|
+
/** Find incoming calls (callers) */
|
|
130
|
+
incomingCalls(item: LSPCallHierarchyItem): Promise<LSPCallHierarchyIncomingCall[]>;
|
|
131
|
+
/** Find outgoing calls (callees) */
|
|
132
|
+
outgoingCalls(item: LSPCallHierarchyItem): Promise<LSPCallHierarchyOutgoingCall[]>;
|
|
103
133
|
shutdown(): Promise<void>;
|
|
104
134
|
}
|
|
105
135
|
|
|
@@ -402,6 +432,55 @@ export async function createLSPClient(options: {
|
|
|
402
432
|
}
|
|
403
433
|
},
|
|
404
434
|
|
|
435
|
+
// --- Call Hierarchy Methods ---
|
|
436
|
+
|
|
437
|
+
async prepareCallHierarchy(filePath, line, character) {
|
|
438
|
+
const uri = pathToFileURL(filePath).href;
|
|
439
|
+
try {
|
|
440
|
+
const result = await connection.sendRequest(
|
|
441
|
+
"textDocument/prepareCallHierarchy",
|
|
442
|
+
{
|
|
443
|
+
textDocument: { uri },
|
|
444
|
+
position: { line, character },
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
if (!result) return [];
|
|
448
|
+
return Array.isArray(result) ? result : [result];
|
|
449
|
+
} catch {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
async incomingCalls(item) {
|
|
455
|
+
try {
|
|
456
|
+
const result = await connection.sendRequest(
|
|
457
|
+
"callHierarchy/incomingCalls",
|
|
458
|
+
{
|
|
459
|
+
item,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
if (!result) return [];
|
|
463
|
+
return Array.isArray(result) ? result : [];
|
|
464
|
+
} catch {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
async outgoingCalls(item) {
|
|
470
|
+
try {
|
|
471
|
+
const result = await connection.sendRequest(
|
|
472
|
+
"callHierarchy/outgoingCalls",
|
|
473
|
+
{
|
|
474
|
+
item,
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
if (!result) return [];
|
|
478
|
+
return Array.isArray(result) ? result : [];
|
|
479
|
+
} catch {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
|
|
405
484
|
async shutdown() {
|
|
406
485
|
// Clear pending timers
|
|
407
486
|
for (const timer of pendingDiagnostics.values()) {
|
package/clients/lsp/index.js
CHANGED
|
@@ -191,6 +191,33 @@ export class LSPService {
|
|
|
191
191
|
return [];
|
|
192
192
|
return spawned.client.implementation(filePath, line, character);
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Navigation: prepare call hierarchy at position
|
|
196
|
+
*/
|
|
197
|
+
async prepareCallHierarchy(filePath, line, character) {
|
|
198
|
+
const spawned = await this.getClientForFile(filePath);
|
|
199
|
+
if (!spawned)
|
|
200
|
+
return [];
|
|
201
|
+
return spawned.client.prepareCallHierarchy(filePath, line, character);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Navigation: find incoming calls (callers)
|
|
205
|
+
*/
|
|
206
|
+
async incomingCalls(item) {
|
|
207
|
+
const spawned = await this.getClientForFile(item.uri.replace("file://", ""));
|
|
208
|
+
if (!spawned)
|
|
209
|
+
return [];
|
|
210
|
+
return spawned.client.incomingCalls(item);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Navigation: find outgoing calls (callees)
|
|
214
|
+
*/
|
|
215
|
+
async outgoingCalls(item) {
|
|
216
|
+
const spawned = await this.getClientForFile(item.uri.replace("file://", ""));
|
|
217
|
+
if (!spawned)
|
|
218
|
+
return [];
|
|
219
|
+
return spawned.client.outgoingCalls(item);
|
|
220
|
+
}
|
|
194
221
|
/**
|
|
195
222
|
* Get all diagnostics across all tracked files (for cascade checking)
|
|
196
223
|
*/
|