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
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { RuleCache } from "../../cache/rule-cache.js";
|
|
11
|
+
import { getSourceFiles } from "../../scan-utils.js";
|
|
11
12
|
import { TreeSitterClient } from "../../tree-sitter-client.js";
|
|
13
|
+
import { logTreeSitter } from "../../tree-sitter-logger.js";
|
|
12
14
|
import { classifyDefect } from "../diagnostic-taxonomy.js";
|
|
13
15
|
import {
|
|
14
16
|
queryLoader,
|
|
@@ -20,11 +22,300 @@ import type {
|
|
|
20
22
|
RunnerDefinition,
|
|
21
23
|
RunnerResult,
|
|
22
24
|
} from "../types.js";
|
|
25
|
+
import { PRIORITY } from "../priorities.js";
|
|
23
26
|
|
|
24
27
|
// Module-level singleton: web-tree-sitter WASM must only be initialized once per process.
|
|
25
28
|
// Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
|
|
26
29
|
// WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
|
|
27
30
|
let _sharedClient: TreeSitterClient | null = null;
|
|
31
|
+
const entitySnapshotByFile = new Map<string, Map<string, string>>();
|
|
32
|
+
const blastFileCache = new Map<string, { expiresAt: number; files: string[] }>();
|
|
33
|
+
const blastCooldownByFile = new Map<string, number>();
|
|
34
|
+
const BLAST_CACHE_TTL_MS = 30_000;
|
|
35
|
+
const MAX_BLAST_FILES = 300;
|
|
36
|
+
const MAX_BLAST_ENTITIES = 8;
|
|
37
|
+
const BLAST_MAX_FILE_BYTES = 128 * 1024;
|
|
38
|
+
const BLAST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
39
|
+
const BLAST_MAX_ELAPSED_MS = 120;
|
|
40
|
+
const BLAST_COOLDOWN_MS = 5_000;
|
|
41
|
+
|
|
42
|
+
interface EntityQueryDef {
|
|
43
|
+
id: string;
|
|
44
|
+
kind: string;
|
|
45
|
+
query: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ENTITY_QUERIES: Partial<Record<string, EntityQueryDef[]>> = {
|
|
49
|
+
typescript: [
|
|
50
|
+
{
|
|
51
|
+
id: "entity-ts-function",
|
|
52
|
+
kind: "function",
|
|
53
|
+
query: "(function_declaration name: (identifier) @NAME)",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "entity-ts-class",
|
|
57
|
+
kind: "class",
|
|
58
|
+
query: "(class_declaration name: (type_identifier) @NAME)",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "entity-ts-method",
|
|
62
|
+
kind: "method",
|
|
63
|
+
query: "(method_definition name: (property_identifier) @NAME)",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
javascript: [
|
|
67
|
+
{
|
|
68
|
+
id: "entity-js-function",
|
|
69
|
+
kind: "function",
|
|
70
|
+
query: "(function_declaration name: (identifier) @NAME)",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "entity-js-class",
|
|
74
|
+
kind: "class",
|
|
75
|
+
query: "(class_declaration name: (identifier) @NAME)",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "entity-js-method",
|
|
79
|
+
kind: "method",
|
|
80
|
+
query: "(method_definition name: (property_identifier) @NAME)",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
python: [
|
|
84
|
+
{
|
|
85
|
+
id: "entity-py-function",
|
|
86
|
+
kind: "function",
|
|
87
|
+
query: "(function_definition name: (identifier) @NAME)",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "entity-py-class",
|
|
91
|
+
kind: "class",
|
|
92
|
+
query: "(class_definition name: (identifier) @NAME)",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
go: [
|
|
96
|
+
{
|
|
97
|
+
id: "entity-go-function",
|
|
98
|
+
kind: "function",
|
|
99
|
+
query: "(function_declaration name: (identifier) @NAME)",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "entity-go-method",
|
|
103
|
+
kind: "method",
|
|
104
|
+
query: "(method_declaration name: (field_identifier) @NAME)",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "entity-go-type",
|
|
108
|
+
kind: "type",
|
|
109
|
+
query: "(type_spec name: (type_identifier) @NAME)",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
rust: [
|
|
113
|
+
{
|
|
114
|
+
id: "entity-rs-function",
|
|
115
|
+
kind: "function",
|
|
116
|
+
query: "(function_item name: (identifier) @NAME)",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "entity-rs-struct",
|
|
120
|
+
kind: "struct",
|
|
121
|
+
query: "(struct_item name: (type_identifier) @NAME)",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "entity-rs-enum",
|
|
125
|
+
kind: "enum",
|
|
126
|
+
query: "(enum_item name: (type_identifier) @NAME)",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
ruby: [
|
|
130
|
+
{
|
|
131
|
+
id: "entity-rb-method",
|
|
132
|
+
kind: "method",
|
|
133
|
+
query: "(method name: (identifier) @NAME)",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "entity-rb-class",
|
|
137
|
+
kind: "class",
|
|
138
|
+
query: "(class name: (constant) @NAME)",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "entity-rb-module",
|
|
142
|
+
kind: "module",
|
|
143
|
+
query: "(module name: (constant) @NAME)",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function escapeRegex(name: string): string {
|
|
149
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function extractEntitySnapshot(
|
|
153
|
+
client: TreeSitterClient,
|
|
154
|
+
filePath: string,
|
|
155
|
+
languageId: string,
|
|
156
|
+
): Promise<Map<string, string>> {
|
|
157
|
+
const defs = ENTITY_QUERIES[languageId] ?? [];
|
|
158
|
+
const snapshot = new Map<string, string>();
|
|
159
|
+
|
|
160
|
+
for (const def of defs) {
|
|
161
|
+
const matches = await client.runQueryOnFile(
|
|
162
|
+
{
|
|
163
|
+
id: def.id,
|
|
164
|
+
name: def.id,
|
|
165
|
+
severity: "info",
|
|
166
|
+
category: "entity",
|
|
167
|
+
language: languageId,
|
|
168
|
+
message: "",
|
|
169
|
+
query: def.query,
|
|
170
|
+
metavars: ["NAME"],
|
|
171
|
+
has_fix: false,
|
|
172
|
+
filePath: "",
|
|
173
|
+
},
|
|
174
|
+
filePath,
|
|
175
|
+
languageId,
|
|
176
|
+
{ maxResults: 200 },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
for (const match of matches) {
|
|
180
|
+
const name = match.captures.NAME?.trim();
|
|
181
|
+
if (!name) continue;
|
|
182
|
+
const key = `${def.kind}:${name}`;
|
|
183
|
+
snapshot.set(key, `${match.line}:${match.matchedText.slice(0, 400)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return snapshot;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function diffEntitySnapshot(
|
|
191
|
+
prev: Map<string, string> | undefined,
|
|
192
|
+
next: Map<string, string>,
|
|
193
|
+
): { added: string[]; removed: string[]; modified: string[] } {
|
|
194
|
+
if (!prev) {
|
|
195
|
+
return { added: [...next.keys()], removed: [], modified: [] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const added: string[] = [];
|
|
199
|
+
const removed: string[] = [];
|
|
200
|
+
const modified: string[] = [];
|
|
201
|
+
|
|
202
|
+
for (const [key, value] of next.entries()) {
|
|
203
|
+
if (!prev.has(key)) {
|
|
204
|
+
added.push(key);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (prev.get(key) !== value) {
|
|
208
|
+
modified.push(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const key of prev.keys()) {
|
|
213
|
+
if (!next.has(key)) {
|
|
214
|
+
removed.push(key);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { added, removed, modified };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getBlastFiles(cwd: string): string[] {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const cached = blastFileCache.get(cwd);
|
|
224
|
+
if (cached && cached.expiresAt > now) return cached.files;
|
|
225
|
+
|
|
226
|
+
const files = getSourceFiles(cwd).slice(0, MAX_BLAST_FILES);
|
|
227
|
+
blastFileCache.set(cwd, { files, expiresAt: now + BLAST_CACHE_TTL_MS });
|
|
228
|
+
return files;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function computeBlastRadius(
|
|
232
|
+
entityNames: string[],
|
|
233
|
+
filePath: string,
|
|
234
|
+
cwd: string,
|
|
235
|
+
): {
|
|
236
|
+
entities: Array<{ entity: string; dependentFiles: number; references: number }>;
|
|
237
|
+
scannedFiles: number;
|
|
238
|
+
scannedBytes: number;
|
|
239
|
+
totalCandidates: number;
|
|
240
|
+
truncated: boolean;
|
|
241
|
+
elapsedMs: number;
|
|
242
|
+
} {
|
|
243
|
+
const startedAt = Date.now();
|
|
244
|
+
const limited = entityNames.slice(0, MAX_BLAST_ENTITIES);
|
|
245
|
+
if (limited.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
entities: [],
|
|
248
|
+
scannedFiles: 0,
|
|
249
|
+
scannedBytes: 0,
|
|
250
|
+
totalCandidates: 0,
|
|
251
|
+
truncated: false,
|
|
252
|
+
elapsedMs: 0,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const regexByEntity = new Map(
|
|
257
|
+
limited.map((name) => [name, new RegExp(`\\b${escapeRegex(name)}\\b`, "g")]),
|
|
258
|
+
);
|
|
259
|
+
const files = getBlastFiles(cwd);
|
|
260
|
+
const stats = new Map(
|
|
261
|
+
limited.map((name) => [name, { dependentFiles: 0, references: 0 }]),
|
|
262
|
+
);
|
|
263
|
+
let scannedFiles = 0;
|
|
264
|
+
let scannedBytes = 0;
|
|
265
|
+
let truncated = false;
|
|
266
|
+
|
|
267
|
+
for (const candidate of files) {
|
|
268
|
+
if (Date.now() - startedAt > BLAST_MAX_ELAPSED_MS) {
|
|
269
|
+
truncated = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (path.resolve(candidate) === path.resolve(filePath)) continue;
|
|
274
|
+
let size = 0;
|
|
275
|
+
try {
|
|
276
|
+
size = fs.statSync(candidate).size;
|
|
277
|
+
} catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (size > BLAST_MAX_FILE_BYTES) continue;
|
|
281
|
+
if (scannedBytes + size > BLAST_MAX_TOTAL_BYTES) {
|
|
282
|
+
truncated = true;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let content = "";
|
|
287
|
+
try {
|
|
288
|
+
content = fs.readFileSync(candidate, "utf-8");
|
|
289
|
+
} catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
scannedFiles += 1;
|
|
293
|
+
scannedBytes += size;
|
|
294
|
+
|
|
295
|
+
for (const [name, regex] of regexByEntity.entries()) {
|
|
296
|
+
const matches = content.match(regex);
|
|
297
|
+
if (!matches || matches.length === 0) continue;
|
|
298
|
+
const current = stats.get(name);
|
|
299
|
+
if (!current) continue;
|
|
300
|
+
current.dependentFiles += 1;
|
|
301
|
+
current.references += matches.length;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const entities = limited
|
|
306
|
+
.map((name) => ({ entity: name, ...stats.get(name)! }))
|
|
307
|
+
.sort((a, b) => b.dependentFiles - a.dependentFiles)
|
|
308
|
+
.slice(0, 5);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
entities,
|
|
312
|
+
scannedFiles,
|
|
313
|
+
scannedBytes,
|
|
314
|
+
totalCandidates: files.length,
|
|
315
|
+
truncated,
|
|
316
|
+
elapsedMs: Date.now() - startedAt,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
28
319
|
|
|
29
320
|
const SILENT_ERROR_QUERY_IDS = new Set([
|
|
30
321
|
"empty-catch",
|
|
@@ -71,19 +362,32 @@ function getSharedClient(): TreeSitterClient {
|
|
|
71
362
|
const treeSitterRunner: RunnerDefinition = {
|
|
72
363
|
id: "tree-sitter",
|
|
73
364
|
appliesTo: ["jsts", "python", "go", "rust", "ruby"],
|
|
74
|
-
priority:
|
|
365
|
+
priority: PRIORITY.STRUCTURAL_ANALYSIS,
|
|
75
366
|
enabledByDefault: true,
|
|
76
367
|
skipTestFiles: false, // Run on test files too (structural issues matter there)
|
|
77
368
|
|
|
78
369
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
79
370
|
// Use singleton client — WASM must never be re-initialized after first call
|
|
80
371
|
const client = getSharedClient();
|
|
372
|
+
logTreeSitter({ phase: "runner_start", filePath: ctx.filePath });
|
|
81
373
|
if (!client.isAvailable()) {
|
|
374
|
+
logTreeSitter({
|
|
375
|
+
phase: "runner_skip",
|
|
376
|
+
filePath: ctx.filePath,
|
|
377
|
+
reason: "client_unavailable",
|
|
378
|
+
status: "skipped",
|
|
379
|
+
});
|
|
82
380
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
83
381
|
}
|
|
84
382
|
|
|
85
383
|
const initialized = await client.init();
|
|
86
384
|
if (!initialized) {
|
|
385
|
+
logTreeSitter({
|
|
386
|
+
phase: "runner_skip",
|
|
387
|
+
filePath: ctx.filePath,
|
|
388
|
+
reason: "client_init_failed",
|
|
389
|
+
status: "skipped",
|
|
390
|
+
});
|
|
87
391
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
88
392
|
}
|
|
89
393
|
|
|
@@ -106,6 +410,12 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
106
410
|
};
|
|
107
411
|
const languageId = EXT_TO_LANG[ext];
|
|
108
412
|
if (!languageId) {
|
|
413
|
+
logTreeSitter({
|
|
414
|
+
phase: "runner_skip",
|
|
415
|
+
filePath: ctx.filePath,
|
|
416
|
+
reason: `unsupported_extension:${ext}`,
|
|
417
|
+
status: "skipped",
|
|
418
|
+
});
|
|
109
419
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
110
420
|
}
|
|
111
421
|
|
|
@@ -132,8 +442,10 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
132
442
|
|
|
133
443
|
// Try cache
|
|
134
444
|
const cached = cache.get(ruleFiles);
|
|
445
|
+
let cacheHit = false;
|
|
135
446
|
if (cached) {
|
|
136
447
|
// Use cached queries
|
|
448
|
+
cacheHit = true;
|
|
137
449
|
languageQueries = cached.queries.map(
|
|
138
450
|
(q) =>
|
|
139
451
|
({
|
|
@@ -173,6 +485,16 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
173
485
|
}
|
|
174
486
|
|
|
175
487
|
if (languageQueries.length === 0) {
|
|
488
|
+
logTreeSitter({
|
|
489
|
+
phase: "runner_complete",
|
|
490
|
+
filePath,
|
|
491
|
+
languageId,
|
|
492
|
+
status: "succeeded",
|
|
493
|
+
diagnostics: 0,
|
|
494
|
+
blocking: 0,
|
|
495
|
+
queryCount: 0,
|
|
496
|
+
effectiveQueryCount: 0,
|
|
497
|
+
});
|
|
176
498
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
177
499
|
}
|
|
178
500
|
|
|
@@ -186,6 +508,16 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
186
508
|
)
|
|
187
509
|
: languageQueries;
|
|
188
510
|
|
|
511
|
+
logTreeSitter({
|
|
512
|
+
phase: "queries_loaded",
|
|
513
|
+
filePath,
|
|
514
|
+
languageId,
|
|
515
|
+
queryCount: languageQueries.length,
|
|
516
|
+
effectiveQueryCount: effectiveQueries.length,
|
|
517
|
+
cacheHit,
|
|
518
|
+
metadata: { blockingOnly: !!ctx.blockingOnly },
|
|
519
|
+
});
|
|
520
|
+
|
|
189
521
|
const diagnostics: Diagnostic[] = [];
|
|
190
522
|
|
|
191
523
|
// Run each query against the file
|
|
@@ -244,15 +576,151 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
244
576
|
} catch (err) {
|
|
245
577
|
// Individual query failure shouldn't stop other queries
|
|
246
578
|
console.error(`[tree-sitter] Query ${query.id} failed:`, err);
|
|
579
|
+
logTreeSitter({
|
|
580
|
+
phase: "query_error",
|
|
581
|
+
filePath,
|
|
582
|
+
languageId,
|
|
583
|
+
queryId: query.id,
|
|
584
|
+
error: err instanceof Error ? err.message : String(err),
|
|
585
|
+
});
|
|
247
586
|
}
|
|
248
587
|
}
|
|
249
588
|
|
|
250
589
|
if (diagnostics.length === 0) {
|
|
590
|
+
try {
|
|
591
|
+
const snapshot = await extractEntitySnapshot(client, filePath, languageId);
|
|
592
|
+
const prev = entitySnapshotByFile.get(filePath);
|
|
593
|
+
const diff = diffEntitySnapshot(prev, snapshot);
|
|
594
|
+
entitySnapshotByFile.set(filePath, snapshot);
|
|
595
|
+
const changedEntityKeys = [...diff.added, ...diff.modified, ...diff.removed];
|
|
596
|
+
const changedNames = [...new Set(changedEntityKeys.map((k) => k.split(":")[1]).filter(Boolean))];
|
|
597
|
+
|
|
598
|
+
if (changedEntityKeys.length > 0) {
|
|
599
|
+
logTreeSitter({
|
|
600
|
+
phase: "entity_diff",
|
|
601
|
+
filePath,
|
|
602
|
+
languageId,
|
|
603
|
+
metadata: {
|
|
604
|
+
added: diff.added,
|
|
605
|
+
modified: diff.modified,
|
|
606
|
+
removed: diff.removed,
|
|
607
|
+
totalChanged: changedEntityKeys.length,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const lastBlast = blastCooldownByFile.get(filePath) ?? 0;
|
|
612
|
+
if (Date.now() - lastBlast < BLAST_COOLDOWN_MS) {
|
|
613
|
+
logTreeSitter({
|
|
614
|
+
phase: "blast_radius",
|
|
615
|
+
filePath,
|
|
616
|
+
languageId,
|
|
617
|
+
metadata: { skipped: "cooldown", cooldownMs: BLAST_COOLDOWN_MS },
|
|
618
|
+
});
|
|
619
|
+
} else {
|
|
620
|
+
blastCooldownByFile.set(filePath, Date.now());
|
|
621
|
+
const blastRadius = computeBlastRadius(changedNames, filePath, ctx.cwd);
|
|
622
|
+
logTreeSitter({
|
|
623
|
+
phase: "blast_radius",
|
|
624
|
+
filePath,
|
|
625
|
+
languageId,
|
|
626
|
+
metadata: {
|
|
627
|
+
entities: blastRadius.entities,
|
|
628
|
+
scannedFiles: blastRadius.scannedFiles,
|
|
629
|
+
scannedBytes: blastRadius.scannedBytes,
|
|
630
|
+
totalCandidates: blastRadius.totalCandidates,
|
|
631
|
+
truncated: blastRadius.truncated,
|
|
632
|
+
elapsedMs: blastRadius.elapsedMs,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} catch {}
|
|
638
|
+
|
|
639
|
+
logTreeSitter({
|
|
640
|
+
phase: "runner_complete",
|
|
641
|
+
filePath,
|
|
642
|
+
languageId,
|
|
643
|
+
status: "succeeded",
|
|
644
|
+
diagnostics: 0,
|
|
645
|
+
blocking: 0,
|
|
646
|
+
queryCount: languageQueries.length,
|
|
647
|
+
effectiveQueryCount: effectiveQueries.length,
|
|
648
|
+
});
|
|
251
649
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
252
650
|
}
|
|
253
651
|
|
|
254
652
|
// Check if any blocking issues
|
|
255
653
|
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
654
|
+
const blockingCount = diagnostics.filter(
|
|
655
|
+
(d) => d.semantic === "blocking",
|
|
656
|
+
).length;
|
|
657
|
+
try {
|
|
658
|
+
const snapshot = await extractEntitySnapshot(client, filePath, languageId);
|
|
659
|
+
const prev = entitySnapshotByFile.get(filePath);
|
|
660
|
+
const diff = diffEntitySnapshot(prev, snapshot);
|
|
661
|
+
entitySnapshotByFile.set(filePath, snapshot);
|
|
662
|
+
const changedEntityKeys = [
|
|
663
|
+
...diff.added,
|
|
664
|
+
...diff.modified,
|
|
665
|
+
...diff.removed,
|
|
666
|
+
];
|
|
667
|
+
const changedNames = [
|
|
668
|
+
...new Set(changedEntityKeys.map((k) => k.split(":")[1]).filter(Boolean)),
|
|
669
|
+
];
|
|
670
|
+
|
|
671
|
+
if (changedEntityKeys.length > 0) {
|
|
672
|
+
logTreeSitter({
|
|
673
|
+
phase: "entity_diff",
|
|
674
|
+
filePath,
|
|
675
|
+
languageId,
|
|
676
|
+
metadata: {
|
|
677
|
+
added: diff.added,
|
|
678
|
+
modified: diff.modified,
|
|
679
|
+
removed: diff.removed,
|
|
680
|
+
totalChanged: changedEntityKeys.length,
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const lastBlast = blastCooldownByFile.get(filePath) ?? 0;
|
|
685
|
+
if (Date.now() - lastBlast < BLAST_COOLDOWN_MS) {
|
|
686
|
+
logTreeSitter({
|
|
687
|
+
phase: "blast_radius",
|
|
688
|
+
filePath,
|
|
689
|
+
languageId,
|
|
690
|
+
metadata: { skipped: "cooldown", cooldownMs: BLAST_COOLDOWN_MS },
|
|
691
|
+
});
|
|
692
|
+
} else {
|
|
693
|
+
blastCooldownByFile.set(filePath, Date.now());
|
|
694
|
+
const blastRadius = computeBlastRadius(changedNames, filePath, ctx.cwd);
|
|
695
|
+
logTreeSitter({
|
|
696
|
+
phase: "blast_radius",
|
|
697
|
+
filePath,
|
|
698
|
+
languageId,
|
|
699
|
+
metadata: {
|
|
700
|
+
entities: blastRadius.entities,
|
|
701
|
+
scannedFiles: blastRadius.scannedFiles,
|
|
702
|
+
scannedBytes: blastRadius.scannedBytes,
|
|
703
|
+
totalCandidates: blastRadius.totalCandidates,
|
|
704
|
+
truncated: blastRadius.truncated,
|
|
705
|
+
elapsedMs: blastRadius.elapsedMs,
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
// best-effort experimental telemetry only
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
logTreeSitter({
|
|
715
|
+
phase: "runner_complete",
|
|
716
|
+
filePath,
|
|
717
|
+
languageId,
|
|
718
|
+
status: hasBlocking ? "failed" : "succeeded",
|
|
719
|
+
diagnostics: diagnostics.length,
|
|
720
|
+
blocking: blockingCount,
|
|
721
|
+
queryCount: languageQueries.length,
|
|
722
|
+
effectiveQueryCount: effectiveQueries.length,
|
|
723
|
+
});
|
|
256
724
|
|
|
257
725
|
return {
|
|
258
726
|
status: hasBlocking ? "failed" : "succeeded",
|
|
@@ -16,12 +16,13 @@ import type {
|
|
|
16
16
|
RunnerDefinition,
|
|
17
17
|
RunnerResult,
|
|
18
18
|
} from "../types.js";
|
|
19
|
+
import { PRIORITY } from "../priorities.js";
|
|
19
20
|
import { readFileContent } from "./utils.js";
|
|
20
21
|
|
|
21
22
|
const tsLspRunner: RunnerDefinition = {
|
|
22
23
|
id: "ts-lsp",
|
|
23
24
|
appliesTo: ["jsts"],
|
|
24
|
-
priority:
|
|
25
|
+
priority: PRIORITY.LSP_FALLBACK,
|
|
25
26
|
enabledByDefault: true,
|
|
26
27
|
|
|
27
28
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
@@ -13,12 +13,13 @@ import type {
|
|
|
13
13
|
RunnerDefinition,
|
|
14
14
|
RunnerResult,
|
|
15
15
|
} from "../types.js";
|
|
16
|
+
import { PRIORITY } from "../priorities.js";
|
|
16
17
|
import { readFileContent } from "./utils.js";
|
|
17
18
|
|
|
18
19
|
const typeSafetyRunner: RunnerDefinition = {
|
|
19
20
|
id: "type-safety",
|
|
20
21
|
appliesTo: ["jsts"],
|
|
21
|
-
priority:
|
|
22
|
+
priority: PRIORITY.GENERAL_ANALYSIS,
|
|
22
23
|
enabledByDefault: true,
|
|
23
24
|
|
|
24
25
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
RunnerDefinition,
|
|
10
10
|
RunnerResult,
|
|
11
11
|
} from "../types.js";
|
|
12
|
+
import { PRIORITY } from "../priorities.js";
|
|
12
13
|
|
|
13
14
|
const yamllint = createAvailabilityChecker("yamllint", ".exe");
|
|
14
15
|
|
|
@@ -83,7 +84,7 @@ function parseYamllintParsable(raw: string, filePath: string): Diagnostic[] {
|
|
|
83
84
|
const yamllintRunner: RunnerDefinition = {
|
|
84
85
|
id: "yamllint",
|
|
85
86
|
appliesTo: ["yaml"],
|
|
86
|
-
priority:
|
|
87
|
+
priority: PRIORITY.YAML_LINT,
|
|
87
88
|
enabledByDefault: true,
|
|
88
89
|
skipTestFiles: false,
|
|
89
90
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ToolProfile {
|
|
2
|
+
dedupPriority: number;
|
|
3
|
+
lintLike: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TOOL_PROFILE: ToolProfile = {
|
|
7
|
+
dedupPriority: 50,
|
|
8
|
+
lintLike: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const TOOL_PROFILE_MAP: Record<string, ToolProfile> = {
|
|
12
|
+
"tree-sitter:silent-error": { dedupPriority: 200, lintLike: false },
|
|
13
|
+
lsp: { dedupPriority: 120, lintLike: false },
|
|
14
|
+
"ts-lsp": { dedupPriority: 120, lintLike: false },
|
|
15
|
+
eslint: { dedupPriority: 110, lintLike: true },
|
|
16
|
+
biome: { dedupPriority: 100, lintLike: true },
|
|
17
|
+
"biome-check-json": { dedupPriority: 100, lintLike: true },
|
|
18
|
+
"tree-sitter": { dedupPriority: 90, lintLike: false },
|
|
19
|
+
"ast-grep-napi": { dedupPriority: 80, lintLike: false },
|
|
20
|
+
"ast-grep": { dedupPriority: 80, lintLike: false },
|
|
21
|
+
"ruff-lint": { dedupPriority: 95, lintLike: true },
|
|
22
|
+
oxlint: { dedupPriority: 95, lintLike: true },
|
|
23
|
+
rubocop: { dedupPriority: 95, lintLike: true },
|
|
24
|
+
"go-vet": { dedupPriority: 95, lintLike: true },
|
|
25
|
+
"golangci-lint": { dedupPriority: 95, lintLike: true },
|
|
26
|
+
"rust-clippy": { dedupPriority: 95, lintLike: true },
|
|
27
|
+
shellcheck: { dedupPriority: 95, lintLike: true },
|
|
28
|
+
"type-safety": { dedupPriority: 95, lintLike: true },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function getToolProfile(
|
|
32
|
+
tool: string,
|
|
33
|
+
defectClass?: string,
|
|
34
|
+
): ToolProfile {
|
|
35
|
+
const t = tool.toLowerCase();
|
|
36
|
+
if (defectClass === "silent-error" && t === "tree-sitter") {
|
|
37
|
+
return TOOL_PROFILE_MAP["tree-sitter:silent-error"];
|
|
38
|
+
}
|
|
39
|
+
return TOOL_PROFILE_MAP[t] ?? DEFAULT_TOOL_PROFILE;
|
|
40
|
+
}
|
|
@@ -88,17 +88,6 @@ export interface DispatchResult {
|
|
|
88
88
|
hasBlockers: boolean;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// --- Baseline Management ---
|
|
92
|
-
|
|
93
|
-
export interface BaselineStore {
|
|
94
|
-
/** Get baseline for a file */
|
|
95
|
-
get(filePath: string): unknown[] | undefined;
|
|
96
|
-
/** Set baseline for a file */
|
|
97
|
-
set(filePath: string, diagnostics: unknown[]): void;
|
|
98
|
-
/** Clear all baselines */
|
|
99
|
-
clear(): void;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
91
|
// --- Runner Definition ---
|
|
103
92
|
|
|
104
93
|
export type RunnerMode = "all" | "fallback" | "first-success";
|
|
@@ -135,7 +124,7 @@ export interface DispatchContext {
|
|
|
135
124
|
readonly pi: PiAgentAPI;
|
|
136
125
|
readonly autofix: boolean;
|
|
137
126
|
readonly deltaMode: boolean;
|
|
138
|
-
readonly
|
|
127
|
+
readonly facts: import("./fact-store.js").FactStore;
|
|
139
128
|
/** Only run blocking rules (severity: error) - used for fast feedback on file write */
|
|
140
129
|
readonly blockingOnly?: boolean;
|
|
141
130
|
readonly modifiedRanges?: ModifiedRange[];
|
|
@@ -164,6 +153,7 @@ export interface RunnerGroup {
|
|
|
164
153
|
export interface RunnerRegistry {
|
|
165
154
|
register(runner: RunnerDefinition): void;
|
|
166
155
|
get(id: string): RunnerDefinition | undefined;
|
|
167
|
-
getForKind(kind: FileKind): RunnerDefinition[];
|
|
156
|
+
getForKind(kind: FileKind, filePath?: string): RunnerDefinition[];
|
|
168
157
|
list(): RunnerDefinition[];
|
|
158
|
+
clear(): void;
|
|
169
159
|
}
|