pi-lens 3.8.21 → 3.8.23
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 +28 -0
- package/README.md +2 -0
- package/clients/dispatch/dispatcher.ts +75 -91
- package/clients/dispatch/fact-provider-types.ts +22 -0
- package/clients/dispatch/fact-rule-runner.ts +22 -0
- package/clients/dispatch/fact-runner.ts +28 -0
- package/clients/dispatch/fact-scheduler.ts +78 -0
- package/clients/dispatch/fact-store.ts +67 -0
- package/clients/dispatch/facts/comment-facts.ts +59 -0
- package/clients/dispatch/facts/file-content.ts +20 -0
- package/clients/dispatch/facts/function-facts.ts +177 -0
- package/clients/dispatch/facts/try-catch-facts.ts +80 -0
- package/clients/dispatch/integration.ts +130 -24
- package/clients/dispatch/priorities.ts +22 -0
- package/clients/dispatch/rules/async-noise.ts +43 -0
- package/clients/dispatch/rules/error-obscuring.ts +40 -0
- package/clients/dispatch/rules/error-swallowing.ts +35 -0
- package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
- package/clients/dispatch/rules/placeholder-comments.ts +47 -0
- package/clients/dispatch/runners/architect.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
- package/clients/dispatch/runners/biome-check.ts +40 -8
- package/clients/dispatch/runners/biome.ts +2 -1
- package/clients/dispatch/runners/eslint.ts +34 -6
- package/clients/dispatch/runners/go-vet.ts +2 -1
- package/clients/dispatch/runners/golangci-lint.ts +2 -1
- package/clients/dispatch/runners/index.ts +29 -27
- package/clients/dispatch/runners/lsp.ts +60 -4
- package/clients/dispatch/runners/oxlint.ts +2 -1
- package/clients/dispatch/runners/pyright.ts +2 -1
- package/clients/dispatch/runners/python-slop.ts +2 -1
- package/clients/dispatch/runners/rubocop.ts +2 -1
- package/clients/dispatch/runners/ruff.ts +2 -1
- package/clients/dispatch/runners/rust-clippy.ts +2 -1
- package/clients/dispatch/runners/shellcheck.ts +2 -1
- package/clients/dispatch/runners/similarity.ts +2 -1
- package/clients/dispatch/runners/spellcheck.ts +2 -1
- package/clients/dispatch/runners/sqlfluff.ts +2 -1
- package/clients/dispatch/runners/tree-sitter.ts +469 -1
- package/clients/dispatch/runners/ts-lsp.ts +2 -1
- package/clients/dispatch/runners/type-safety.ts +2 -1
- package/clients/dispatch/runners/yamllint.ts +2 -1
- package/clients/dispatch/tool-profile.ts +40 -0
- package/clients/dispatch/types.ts +3 -13
- package/clients/lsp/client.ts +366 -12
- package/clients/lsp/index.ts +374 -76
- package/clients/lsp/launch.ts +42 -2
- package/clients/lsp/server.ts +186 -12
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-context.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- package/clients/session-summary.ts +21 -0
- package/clients/tree-sitter-client.ts +162 -0
- package/clients/tree-sitter-logger.ts +47 -0
- package/clients/tree-sitter-query-loader.ts +13 -2
- package/index.ts +67 -17
- package/package.json +3 -1
- package/rules/rule-catalog.json +64 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
- package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
- package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
- package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
- package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
- package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
- package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
- package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
- package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
- package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
- package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
- package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
- package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
- package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
- package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
- package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
- package/scripts/validate-rule-catalog.mjs +227 -0
- package/skills/lsp-navigation/SKILL.md +15 -3
- package/tools/lsp-navigation.js +466 -79
- package/tools/lsp-navigation.ts +587 -85
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const STRICT = process.argv.includes("--strict");
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const catalogPath = path.join(root, "rules", "rule-catalog.json");
|
|
8
|
+
const treeSitterRoot = path.join(root, "rules", "tree-sitter-queries");
|
|
9
|
+
const astGrepRoot = path.join(root, "rules", "ast-grep-rules", "rules");
|
|
10
|
+
|
|
11
|
+
const TRACKED_AST_GREP_IDS = new Set([
|
|
12
|
+
"no-sql-in-code",
|
|
13
|
+
"no-sql-in-code-js",
|
|
14
|
+
"no-open-redirect",
|
|
15
|
+
"no-open-redirect-js",
|
|
16
|
+
"no-javascript-url",
|
|
17
|
+
"no-javascript-url-js",
|
|
18
|
+
"no-insecure-randomness",
|
|
19
|
+
"no-insecure-randomness-js",
|
|
20
|
+
"no-implied-eval",
|
|
21
|
+
"no-implied-eval-js",
|
|
22
|
+
"no-hardcoded-secrets",
|
|
23
|
+
"no-hardcoded-secrets-js",
|
|
24
|
+
"no-global-eval-js",
|
|
25
|
+
"jwt-no-verify",
|
|
26
|
+
"jwt-no-verify-js",
|
|
27
|
+
"toctou",
|
|
28
|
+
"toctou-js",
|
|
29
|
+
"missed-concurrency",
|
|
30
|
+
"missed-concurrency-js",
|
|
31
|
+
"no-await-in-loop",
|
|
32
|
+
"no-await-in-loop-js",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function walkYaml(dir, acc = []) {
|
|
36
|
+
if (!fs.existsSync(dir)) return acc;
|
|
37
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
+
const full = path.join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
walkYaml(full, acc);
|
|
41
|
+
} else if (entry.name.endsWith(".yml")) {
|
|
42
|
+
acc.push(full);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return acc;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readYamlScalar(text, key) {
|
|
49
|
+
const match = text.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
50
|
+
if (!match) return undefined;
|
|
51
|
+
return match[1].trim().replace(/^['\"]|['\"]$/g, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectTreeSitterSecurityConcurrency() {
|
|
55
|
+
const files = walkYaml(treeSitterRoot);
|
|
56
|
+
const rules = [];
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const text = fs.readFileSync(file, "utf8");
|
|
59
|
+
const id = readYamlScalar(text, "id");
|
|
60
|
+
const category = readYamlScalar(text, "category");
|
|
61
|
+
if (!id || !category) continue;
|
|
62
|
+
if (category !== "security" && category !== "concurrency") continue;
|
|
63
|
+
const language = readYamlScalar(text, "language") ?? path.basename(path.dirname(file));
|
|
64
|
+
rules.push({
|
|
65
|
+
id,
|
|
66
|
+
category,
|
|
67
|
+
language,
|
|
68
|
+
file: path.relative(root, file).replaceAll("\\", "/"),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return rules;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectAstGrepTrackedRules() {
|
|
75
|
+
const files = walkYaml(astGrepRoot);
|
|
76
|
+
const rules = [];
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const text = fs.readFileSync(file, "utf8");
|
|
79
|
+
const id = readYamlScalar(text, "id");
|
|
80
|
+
if (!id || !TRACKED_AST_GREP_IDS.has(id)) continue;
|
|
81
|
+
const language = readYamlScalar(text, "language") ?? "unknown";
|
|
82
|
+
rules.push({
|
|
83
|
+
id,
|
|
84
|
+
language,
|
|
85
|
+
file: path.relative(root, file).replaceAll("\\", "/"),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return rules;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateCatalog(catalog) {
|
|
92
|
+
const required = [
|
|
93
|
+
"rule_id",
|
|
94
|
+
"engine",
|
|
95
|
+
"language",
|
|
96
|
+
"family",
|
|
97
|
+
"scope",
|
|
98
|
+
"canonical_concept",
|
|
99
|
+
"severity_default",
|
|
100
|
+
"confidence",
|
|
101
|
+
"status",
|
|
102
|
+
];
|
|
103
|
+
const validEngine = new Set(["tree-sitter", "ast-grep", "architect"]);
|
|
104
|
+
const validSeverity = new Set(["error", "warning", "info", "review"]);
|
|
105
|
+
const validConfidence = new Set(["low", "medium", "high"]);
|
|
106
|
+
const validStatus = new Set(["experimental", "active", "deprecated"]);
|
|
107
|
+
|
|
108
|
+
const errors = [];
|
|
109
|
+
const warnings = [];
|
|
110
|
+
const byRuleId = new Map();
|
|
111
|
+
const activeByConceptScopeLang = new Map();
|
|
112
|
+
|
|
113
|
+
for (const [index, entry] of catalog.entries.entries()) {
|
|
114
|
+
for (const field of required) {
|
|
115
|
+
if (!entry[field]) {
|
|
116
|
+
errors.push(`entries[${index}] missing required field '${field}'`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (entry.engine && !validEngine.has(entry.engine)) {
|
|
121
|
+
errors.push(`entries[${index}] has invalid engine '${entry.engine}'`);
|
|
122
|
+
}
|
|
123
|
+
if (entry.severity_default && !validSeverity.has(entry.severity_default)) {
|
|
124
|
+
errors.push(
|
|
125
|
+
`entries[${index}] has invalid severity_default '${entry.severity_default}'`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (entry.confidence && !validConfidence.has(entry.confidence)) {
|
|
129
|
+
errors.push(`entries[${index}] has invalid confidence '${entry.confidence}'`);
|
|
130
|
+
}
|
|
131
|
+
if (entry.status && !validStatus.has(entry.status)) {
|
|
132
|
+
errors.push(`entries[${index}] has invalid status '${entry.status}'`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (entry.rule_id) {
|
|
136
|
+
if (byRuleId.has(entry.rule_id)) {
|
|
137
|
+
errors.push(`duplicate rule_id '${entry.rule_id}' in rule catalog`);
|
|
138
|
+
}
|
|
139
|
+
byRuleId.set(entry.rule_id, entry);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (entry.status === "active" && !entry.allow_overlap) {
|
|
143
|
+
const key = `${entry.language}::${entry.scope}::${entry.canonical_concept}`;
|
|
144
|
+
const prev = activeByConceptScopeLang.get(key);
|
|
145
|
+
if (prev) {
|
|
146
|
+
warnings.push(
|
|
147
|
+
`possible overlap for ${key}: '${prev.rule_id}' and '${entry.rule_id}' (consider allow_overlap or concept split)`,
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
activeByConceptScopeLang.set(key, entry);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { errors, warnings, byRuleId };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(catalogPath)) {
|
|
159
|
+
console.error(`[rule-catalog] missing ${catalogPath}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const catalogRaw = fs.readFileSync(catalogPath, "utf8");
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(catalogRaw);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(`[rule-catalog] invalid JSON: ${error}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
173
|
+
const treeRules = collectTreeSitterSecurityConcurrency();
|
|
174
|
+
const astRules = collectAstGrepTrackedRules();
|
|
175
|
+
const { errors, warnings, byRuleId } = validateCatalog({ entries });
|
|
176
|
+
|
|
177
|
+
for (const rule of treeRules) {
|
|
178
|
+
if (!byRuleId.has(rule.id)) {
|
|
179
|
+
warnings.push(
|
|
180
|
+
`missing catalog entry for ${rule.id} (${rule.category}, ${rule.language}) at ${rule.file}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (entry.engine !== "tree-sitter") continue;
|
|
187
|
+
const exists = treeRules.some((rule) => rule.id === entry.rule_id);
|
|
188
|
+
if (!exists) {
|
|
189
|
+
warnings.push(
|
|
190
|
+
`catalog entry '${entry.rule_id}' has no matching tree-sitter rule file (maybe removed or renamed)`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const rule of astRules) {
|
|
196
|
+
if (!byRuleId.has(rule.id)) {
|
|
197
|
+
warnings.push(
|
|
198
|
+
`missing catalog entry for ${rule.id} (ast-grep, ${rule.language}) at ${rule.file}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (entry.engine !== "ast-grep") continue;
|
|
205
|
+
const exists = astRules.some((rule) => rule.id === entry.rule_id);
|
|
206
|
+
if (!exists) {
|
|
207
|
+
warnings.push(
|
|
208
|
+
`catalog entry '${entry.rule_id}' has no matching tracked ast-grep rule file`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const report = {
|
|
214
|
+
catalogEntries: entries.length,
|
|
215
|
+
trackedTreeSitterSecurityConcurrencyRules: treeRules.length,
|
|
216
|
+
trackedAstGrepRules: astRules.length,
|
|
217
|
+
errors: errors.length,
|
|
218
|
+
warnings: warnings.length,
|
|
219
|
+
strict: STRICT,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
console.log(JSON.stringify(report, null, 2));
|
|
223
|
+
for (const err of errors) console.error(`[rule-catalog][error] ${err}`);
|
|
224
|
+
for (const warn of warnings) console.error(`[rule-catalog][warn] ${warn}`);
|
|
225
|
+
|
|
226
|
+
if (errors.length > 0) process.exit(1);
|
|
227
|
+
if (STRICT && warnings.length > 0) process.exit(1);
|
|
@@ -16,11 +16,25 @@ Use `lsp_navigation` as **PRIMARY** for code intelligence. Do NOT use grep/glob/
|
|
|
16
16
|
| "Where is this defined?" | `definition` | filePath, line, character |
|
|
17
17
|
| "Find all usages" | `references` | filePath, line, character |
|
|
18
18
|
| "What type is this?" | `hover` | filePath, line, character |
|
|
19
|
+
| "Show call signature here" | `signatureHelp` | filePath, line, character (at call-site args) |
|
|
19
20
|
| "What symbols in this file?" | `documentSymbol` | filePath |
|
|
20
|
-
| "Find symbol across project" | `workspaceSymbol` | filePath
|
|
21
|
+
| "Find symbol across project" | `workspaceSymbol` | query + **filePath strongly recommended** |
|
|
22
|
+
| "What quick fixes are available?" | `codeAction` | filePath, line, character, endLine, endCharacter |
|
|
23
|
+
| "Rename symbol safely" | `rename` | filePath, line, character, newName |
|
|
21
24
|
| "Who implements this interface?" | `implementation` | filePath, line, character |
|
|
22
25
|
| "Who calls this function?" | `prepareCallHierarchy` → `incomingCalls` | filePath, line, character |
|
|
23
26
|
| "What does this function call?" | `prepareCallHierarchy` → `outgoingCalls` | filePath, line, character |
|
|
27
|
+
| "Show tracked LSP diagnostics" | `workspaceDiagnostics` | optional filePath (snapshot, not full pull workspace) |
|
|
28
|
+
|
|
29
|
+
## Operational Guidance (From Field Tests)
|
|
30
|
+
|
|
31
|
+
- Always pass `filePath` for `workspaceSymbol` when possible. Unscoped queries are best-effort and often empty.
|
|
32
|
+
- For `references`, prefer querying from the definition site for broader cross-file coverage; usage-site queries can be partial.
|
|
33
|
+
- Use `signatureHelp` only at call-site argument positions; declaration positions often return empty.
|
|
34
|
+
- Treat `workspaceDiagnostics` as tracked push snapshot (`publishDiagnostics`), not protocol pull `workspace/diagnostic` coverage.
|
|
35
|
+
- For `codeAction`, separate `quickfix` from generic refactors (for example "Move to new file"). Do not treat generic refactors as error fixes.
|
|
36
|
+
- `prepareCallHierarchy` is server-capability dependent; if unsupported, skip incoming/outgoing calls.
|
|
37
|
+
- If TypeScript returns `No Project` on `workspaceSymbol`, retry after opening the scoped file context.
|
|
24
38
|
|
|
25
39
|
## Call Hierarchy Pattern
|
|
26
40
|
|
|
@@ -36,14 +50,12 @@ const items = await lsp_navigation({
|
|
|
36
50
|
// Step 2: Get callers (who calls this function)
|
|
37
51
|
const callers = await lsp_navigation({
|
|
38
52
|
operation: "incomingCalls",
|
|
39
|
-
filePath: "src/api.ts",
|
|
40
53
|
callHierarchyItem: items[0]
|
|
41
54
|
});
|
|
42
55
|
|
|
43
56
|
// Step 2: Get callees (what this function calls)
|
|
44
57
|
const callees = await lsp_navigation({
|
|
45
58
|
operation: "outgoingCalls",
|
|
46
|
-
filePath: "src/api.ts",
|
|
47
59
|
callHierarchyItem: items[0]
|
|
48
60
|
});
|
|
49
61
|
```
|