pi-lens 3.8.39 → 3.8.41
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 +84 -5
- package/README.md +37 -1
- package/clients/biome-client.ts +5 -4
- package/clients/cache/rule-cache.ts +1 -1
- package/clients/complexity-client.ts +1 -1
- package/clients/dependency-checker.ts +1 -1
- package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
- package/clients/dispatch/dispatcher.ts +9 -0
- package/clients/dispatch/fact-scheduler.ts +1 -1
- package/clients/dispatch/integration.ts +58 -3
- package/clients/dispatch/runners/index.ts +2 -0
- package/clients/dispatch/runners/semgrep.ts +269 -0
- package/clients/dispatch/runners/shellcheck.ts +2 -8
- package/clients/dispatch/runners/tree-sitter.ts +32 -11
- package/clients/dispatch/tool-profile.ts +1 -0
- package/clients/format-service.ts +10 -0
- package/clients/formatters.ts +22 -8
- package/clients/installer/index.ts +3 -3
- package/clients/knip-client.ts +360 -362
- package/clients/lsp/aggregation.ts +91 -0
- package/clients/lsp/client.ts +91 -38
- package/clients/lsp/index.ts +88 -72
- package/clients/lsp/launch.ts +107 -34
- package/clients/lsp/server-strategies.ts +71 -0
- package/clients/lsp/server.ts +76 -57
- package/clients/path-utils.ts +17 -0
- package/clients/pipeline.ts +23 -5
- package/clients/production-readiness.ts +2 -2
- package/clients/read-guard-logger.ts +41 -1
- package/clients/read-guard-tool-lines.ts +17 -4
- package/clients/read-guard.ts +95 -46
- package/clients/runtime-agent-end.ts +3 -0
- package/clients/runtime-session.ts +5 -0
- package/clients/runtime-tool-result.ts +48 -1
- package/clients/runtime-turn.ts +48 -4
- package/clients/sanitize.ts +1 -1
- package/clients/semgrep-config.ts +213 -0
- package/clients/tool-policy.ts +1982 -1936
- package/clients/tree-sitter-client.ts +1 -1
- package/clients/widget-state.ts +283 -0
- package/commands/booboo.ts +34 -2
- package/index.ts +231 -17
- package/package.json +3 -2
- package/rules/rule-catalog.json +25 -1
- package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
- package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
- package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
- package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
- package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
- package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
- package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
- package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
- package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
- package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
package/index.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
|
7
7
|
import { AstGrepClient } from "./clients/ast-grep-client.js";
|
|
8
8
|
import { loadBootstrapClients } from "./clients/bootstrap.js";
|
|
9
9
|
import { CacheManager } from "./clients/cache-manager.js";
|
|
10
|
+
import {
|
|
11
|
+
clearWidgetState,
|
|
12
|
+
renderWidget,
|
|
13
|
+
setRenderCallback,
|
|
14
|
+
} from "./clients/widget-state.js";
|
|
10
15
|
import { getDiagnosticTracker } from "./clients/diagnostic-tracker.js";
|
|
11
16
|
import {
|
|
12
17
|
getCascadeSessionStats,
|
|
@@ -46,8 +51,21 @@ import {
|
|
|
46
51
|
} from "./clients/runtime-context.js";
|
|
47
52
|
import { RuntimeCoordinator } from "./clients/runtime-coordinator.js";
|
|
48
53
|
import { handleSessionStart } from "./clients/runtime-session.js";
|
|
49
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
clearLastAnalyzedStateCache,
|
|
56
|
+
handleToolResult,
|
|
57
|
+
} from "./clients/runtime-tool-result.js";
|
|
50
58
|
import { cancelLSPIdleReset, handleTurnEnd } from "./clients/runtime-turn.js";
|
|
59
|
+
import { isExternalOrVendorFile } from "./clients/path-utils.js";
|
|
60
|
+
import { safeSpawnAsync } from "./clients/safe-spawn.js";
|
|
61
|
+
import {
|
|
62
|
+
createStarterSemgrepConfig,
|
|
63
|
+
findLocalSemgrepConfig,
|
|
64
|
+
loadPiLensSemgrepConfig,
|
|
65
|
+
removePiLensSemgrepConfig,
|
|
66
|
+
resolveSemgrepConfig,
|
|
67
|
+
savePiLensSemgrepConfig,
|
|
68
|
+
} from "./clients/semgrep-config.js";
|
|
51
69
|
import { TreeSitterClient } from "./clients/tree-sitter-client.js";
|
|
52
70
|
import { handleBooboo } from "./commands/booboo.js";
|
|
53
71
|
import { initI18n, t } from "./i18n.js";
|
|
@@ -201,12 +219,16 @@ function isLspCapableFile(filePath: string): boolean {
|
|
|
201
219
|
return LANGUAGE_POLICY[kind]?.lspCapable !== false;
|
|
202
220
|
}
|
|
203
221
|
|
|
204
|
-
function shouldSkipLspAutoTouch(
|
|
222
|
+
function shouldSkipLspAutoTouch(
|
|
223
|
+
filePath: string,
|
|
224
|
+
projectRoot: string,
|
|
225
|
+
): boolean {
|
|
205
226
|
const normalized = path.resolve(filePath).replace(/\\/g, "/").toLowerCase();
|
|
206
227
|
const base = path.basename(filePath).toLowerCase();
|
|
207
228
|
|
|
208
229
|
if (normalized.includes("/.pi-lens/")) return true;
|
|
209
230
|
if (normalized.includes("/.harness/")) return true;
|
|
231
|
+
if (isExternalOrVendorFile(filePath, projectRoot)) return true;
|
|
210
232
|
if (
|
|
211
233
|
base === "stdout.jsonl" ||
|
|
212
234
|
base === "stderr.txt" ||
|
|
@@ -295,6 +317,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
295
317
|
|
|
296
318
|
// --- Flags ---
|
|
297
319
|
|
|
320
|
+
pi.registerFlag("no-lens", {
|
|
321
|
+
description:
|
|
322
|
+
"Start pi-lens disabled for this session. Re-enable with /lens-toggle.",
|
|
323
|
+
type: "boolean",
|
|
324
|
+
default: false,
|
|
325
|
+
});
|
|
326
|
+
|
|
298
327
|
pi.registerFlag("no-lsp", {
|
|
299
328
|
description:
|
|
300
329
|
"Disable unified LSP diagnostics and use language-specific fallbacks (for example ts-lsp, pyright)",
|
|
@@ -341,14 +370,148 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
370
|
default: false,
|
|
342
371
|
});
|
|
343
372
|
|
|
373
|
+
pi.registerFlag("lens-semgrep", {
|
|
374
|
+
description:
|
|
375
|
+
"Enable Semgrep dispatch when a Semgrep config is available (or with --lens-semgrep-config)",
|
|
376
|
+
type: "boolean",
|
|
377
|
+
default: false,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
pi.registerFlag("lens-semgrep-config", {
|
|
381
|
+
description:
|
|
382
|
+
"Semgrep config for dispatch: local path, auto, p/<pack>, or r/<rule>. Requires --lens-semgrep.",
|
|
383
|
+
type: "string",
|
|
384
|
+
default: "",
|
|
385
|
+
});
|
|
386
|
+
|
|
344
387
|
pi.registerFlag("no-read-guard", {
|
|
345
388
|
description: "Disable read-before-edit behavior monitor",
|
|
346
389
|
type: "boolean",
|
|
347
390
|
default: false,
|
|
348
391
|
});
|
|
349
392
|
|
|
393
|
+
let lensEnabled = !pi.getFlag("no-lens");
|
|
394
|
+
|
|
350
395
|
// --- Commands ---
|
|
351
396
|
|
|
397
|
+
pi.registerCommand("lens-toggle", {
|
|
398
|
+
description:
|
|
399
|
+
"Toggle pi-lens on/off for the current session. Usage: /lens-toggle",
|
|
400
|
+
handler: async (_args, ctx) => {
|
|
401
|
+
lensEnabled = !lensEnabled;
|
|
402
|
+
ctx.ui.notify(
|
|
403
|
+
lensEnabled
|
|
404
|
+
? "pi-lens enabled for this session."
|
|
405
|
+
: "pi-lens disabled for this session. Run /lens-toggle again to resume.",
|
|
406
|
+
lensEnabled ? "info" : "warning",
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
pi.registerCommand("lens-semgrep", {
|
|
412
|
+
description:
|
|
413
|
+
"Manage Semgrep dispatch. Usage: /lens-semgrep status | enable [--config <auto|p/pack|path>] | disable | init",
|
|
414
|
+
handler: async (args, ctx) => {
|
|
415
|
+
const parts = normalizeCommandArgs(args);
|
|
416
|
+
const action = parts[0] ?? "status";
|
|
417
|
+
const cwd = ctx.cwd ?? runtime.projectRoot;
|
|
418
|
+
|
|
419
|
+
function readConfigArg(): string | undefined {
|
|
420
|
+
const flagIndex = parts.findIndex(
|
|
421
|
+
(part) => part === "--config" || part === "-c",
|
|
422
|
+
);
|
|
423
|
+
if (flagIndex >= 0) return parts[flagIndex + 1];
|
|
424
|
+
return parts[1] && !parts[1].startsWith("-") ? parts[1] : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (action === "enable") {
|
|
428
|
+
const config = readConfigArg();
|
|
429
|
+
const localConfig = findLocalSemgrepConfig(cwd);
|
|
430
|
+
if (!config && !localConfig) {
|
|
431
|
+
ctx.ui.notify(
|
|
432
|
+
[
|
|
433
|
+
"Semgrep dispatch not enabled yet: no local .semgrep.yml was found.",
|
|
434
|
+
"Use `/lens-semgrep init` to create a starter local config, or `/lens-semgrep enable --config auto` / `p/<pack>` if you want Semgrep registry/platform configuration.",
|
|
435
|
+
"pi-lens will not auto-install Semgrep; install it with pipx/uv/brew first and login only if your chosen Semgrep config requires it.",
|
|
436
|
+
].join("\n"),
|
|
437
|
+
"warning",
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const savedPath = savePiLensSemgrepConfig(cwd, {
|
|
443
|
+
enabled: true,
|
|
444
|
+
...(config ? { config } : {}),
|
|
445
|
+
});
|
|
446
|
+
ctx.ui.notify(
|
|
447
|
+
`Semgrep dispatch enabled (${config ? `config: ${config}` : `local config: ${localConfig}`}). Saved ${savedPath}`,
|
|
448
|
+
"info",
|
|
449
|
+
);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (action === "disable") {
|
|
454
|
+
const savedPath = savePiLensSemgrepConfig(cwd, { enabled: false });
|
|
455
|
+
ctx.ui.notify(`Semgrep dispatch disabled. Saved ${savedPath}`, "info");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (action === "clear") {
|
|
460
|
+
const removed = removePiLensSemgrepConfig(cwd);
|
|
461
|
+
ctx.ui.notify(
|
|
462
|
+
removed
|
|
463
|
+
? "Removed .pi-lens/semgrep.json; Semgrep now auto-enables only when local .semgrep.yml exists."
|
|
464
|
+
: "No .pi-lens/semgrep.json found.",
|
|
465
|
+
"info",
|
|
466
|
+
);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (action === "init") {
|
|
471
|
+
const configPath = createStarterSemgrepConfig(cwd);
|
|
472
|
+
const savedPath = savePiLensSemgrepConfig(cwd, { enabled: true });
|
|
473
|
+
ctx.ui.notify(
|
|
474
|
+
`Created starter Semgrep config at ${configPath} and enabled Semgrep dispatch (${savedPath}).`,
|
|
475
|
+
"info",
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (action !== "status") {
|
|
481
|
+
ctx.ui.notify(
|
|
482
|
+
"Usage: /lens-semgrep status | enable [--config <auto|p/pack|path>] | disable | clear | init",
|
|
483
|
+
"warning",
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const localConfig = findLocalSemgrepConfig(cwd);
|
|
489
|
+
const piLensConfig = loadPiLensSemgrepConfig(cwd);
|
|
490
|
+
const resolved = resolveSemgrepConfig(cwd, {
|
|
491
|
+
enabled: Boolean(pi.getFlag("lens-semgrep")),
|
|
492
|
+
config: pi.getFlag("lens-semgrep-config"),
|
|
493
|
+
});
|
|
494
|
+
const version = await safeSpawnAsync("semgrep", ["--version"], {
|
|
495
|
+
cwd,
|
|
496
|
+
timeout: 5000,
|
|
497
|
+
});
|
|
498
|
+
const lines = [
|
|
499
|
+
"🔎 SEMGREP DISPATCH",
|
|
500
|
+
`CLI: ${!version.error && version.status === 0 ? `installed (${(version.stdout || version.stderr).trim()})` : "not found on PATH"}`,
|
|
501
|
+
`Local config: ${localConfig ?? "none"}`,
|
|
502
|
+
`pi-lens config: ${piLensConfig ? JSON.stringify(piLensConfig) : "none"}`,
|
|
503
|
+
`Effective: ${resolved.enabled ? "enabled" : "disabled"}`,
|
|
504
|
+
`Config arg: ${resolved.configArg ?? "none"}`,
|
|
505
|
+
];
|
|
506
|
+
if (resolved.reason) lines.push(`Reason: ${resolved.reason}`);
|
|
507
|
+
lines.push(
|
|
508
|
+
"",
|
|
509
|
+
"No auto-install. Token/login is only needed for Semgrep AppSec/Pro/managed configs; local .semgrep.yml scans do not require a token.",
|
|
510
|
+
);
|
|
511
|
+
ctx.ui.notify(lines.join("\n"), resolved.enabled ? "info" : "warning");
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
|
|
352
515
|
pi.registerCommand("lens-booboo", {
|
|
353
516
|
description:
|
|
354
517
|
"Full codebase review: design smells, complexity, AI slop detection, TODOs, dead code, duplicates, type coverage. Results saved to .pi-lens/reviews/. Usage: /lens-booboo [path]",
|
|
@@ -762,6 +925,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
925
|
resetLSPService,
|
|
763
926
|
});
|
|
764
927
|
ctx.ui && updateLspStatus(ctx.ui.setStatus, ctx.ui.theme);
|
|
928
|
+
clearWidgetState();
|
|
929
|
+
if (ctx.ui?.setWidget) {
|
|
930
|
+
ctx.ui.setWidget(
|
|
931
|
+
"pi-lens",
|
|
932
|
+
(tui: any, theme: any) => {
|
|
933
|
+
setRenderCallback(() => tui.requestRender());
|
|
934
|
+
return {
|
|
935
|
+
render: (width: number) => renderWidget(width, theme),
|
|
936
|
+
invalidate: () => setRenderCallback(() => {}),
|
|
937
|
+
};
|
|
938
|
+
},
|
|
939
|
+
{ placement: "belowEditor" },
|
|
940
|
+
);
|
|
941
|
+
}
|
|
765
942
|
} catch (sessionErr) {
|
|
766
943
|
dbg(`session_start crashed: ${sessionErr}`);
|
|
767
944
|
dbg(`session_start crash stack: ${(sessionErr as Error).stack}`);
|
|
@@ -770,6 +947,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
770
947
|
|
|
771
948
|
pi.on("tool_call", async (event, ctx) => {
|
|
772
949
|
const toolName = (event as { toolName?: string }).toolName ?? "";
|
|
950
|
+
if (!lensEnabled) return;
|
|
773
951
|
if (
|
|
774
952
|
pi.getFlag("lens-guard") &&
|
|
775
953
|
isGitCommitOrPushAttempt(toolName, event.input)
|
|
@@ -812,8 +990,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
812
990
|
);
|
|
813
991
|
if (!nodeFs.existsSync(filePath)) return;
|
|
814
992
|
|
|
993
|
+
const isExternalOrVendor = isExternalOrVendorFile(
|
|
994
|
+
filePath,
|
|
995
|
+
runtime.projectRoot,
|
|
996
|
+
);
|
|
997
|
+
|
|
815
998
|
const lspCapableFile = isLspCapableFile(filePath);
|
|
816
|
-
const lspAutoTouchSkipped = shouldSkipLspAutoTouch(
|
|
999
|
+
const lspAutoTouchSkipped = shouldSkipLspAutoTouch(
|
|
1000
|
+
filePath,
|
|
1001
|
+
runtime.projectRoot,
|
|
1002
|
+
);
|
|
817
1003
|
const lspAutoTouchEligible = lspCapableFile && !lspAutoTouchSkipped;
|
|
818
1004
|
const shouldWarmReadLsp =
|
|
819
1005
|
toolName === "read" &&
|
|
@@ -907,6 +1093,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
907
1093
|
if (
|
|
908
1094
|
toolName === "read" &&
|
|
909
1095
|
!pi.getFlag("no-lsp") &&
|
|
1096
|
+
!isExternalOrVendor &&
|
|
910
1097
|
filePath &&
|
|
911
1098
|
readInput &&
|
|
912
1099
|
requestedReadLimit != null &&
|
|
@@ -961,7 +1148,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
961
1148
|
}
|
|
962
1149
|
|
|
963
1150
|
// --- Read-Before-Edit Guard: record reads ---
|
|
964
|
-
if (toolName === "read" && filePath) {
|
|
1151
|
+
if (toolName === "read" && filePath && !isExternalOrVendor) {
|
|
965
1152
|
const totalLines = countFileLines(filePath);
|
|
966
1153
|
const deliveredLimit = effectiveReadLimit ?? 1;
|
|
967
1154
|
logReadGuardEvent({
|
|
@@ -1002,6 +1189,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1002
1189
|
// Record complexity baseline for historical tracking (booboo/tdi).
|
|
1003
1190
|
// Not shown inline - just captured for delta analysis.
|
|
1004
1191
|
if (
|
|
1192
|
+
!isExternalOrVendor &&
|
|
1005
1193
|
complexityClient.isSupportedFile(filePath) &&
|
|
1006
1194
|
!runtime.complexityBaselines.has(filePath)
|
|
1007
1195
|
) {
|
|
@@ -1073,14 +1261,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1073
1261
|
.map(({ label, value, corrected }) => {
|
|
1074
1262
|
const preview = value.trimStart().slice(0, 60).replace(/\n/g, "↵");
|
|
1075
1263
|
return (
|
|
1076
|
-
`${label} ("${preview}…")
|
|
1077
|
-
`
|
|
1264
|
+
`${label} ("${preview}…") has mismatched indentation (tabs vs spaces).\n` +
|
|
1265
|
+
`Replace ${label} with this verbatim (do not shorten, do not change newText):\n\n${corrected}`
|
|
1078
1266
|
);
|
|
1079
1267
|
})
|
|
1080
1268
|
.join("\n\n");
|
|
1081
1269
|
return {
|
|
1082
1270
|
block: true,
|
|
1083
|
-
reason:
|
|
1271
|
+
reason:
|
|
1272
|
+
`🔄 RETRYABLE — Indentation mismatch detected\n\n` +
|
|
1273
|
+
`The file uses a different indentation style than your oldText. ` +
|
|
1274
|
+
`Retry the same edit call immediately with the corrected oldText shown below — copy it exactly as-is.\n\n` +
|
|
1275
|
+
details,
|
|
1084
1276
|
};
|
|
1085
1277
|
}
|
|
1086
1278
|
}
|
|
@@ -1090,11 +1282,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1090
1282
|
typeof readGuard?.isNewFile !== "function" ||
|
|
1091
1283
|
!readGuard.isNewFile(filePath);
|
|
1092
1284
|
if (readGuard && isExistingFile) {
|
|
1093
|
-
const { touchedLines, preflightError } =
|
|
1094
|
-
event,
|
|
1095
|
-
filePath,
|
|
1096
|
-
runtime.telemetrySessionId,
|
|
1097
|
-
);
|
|
1285
|
+
const { touchedLines, editRanges, preflightError } =
|
|
1286
|
+
getTouchedLinesForGuard(event, filePath, runtime.telemetrySessionId);
|
|
1098
1287
|
if (preflightError) {
|
|
1099
1288
|
return { block: true, reason: preflightError };
|
|
1100
1289
|
}
|
|
@@ -1110,7 +1299,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1110
1299
|
});
|
|
1111
1300
|
const verdict =
|
|
1112
1301
|
typeof readGuard.checkEdit === "function"
|
|
1113
|
-
? readGuard.checkEdit(filePath, touchedLines)
|
|
1302
|
+
? readGuard.checkEdit(filePath, touchedLines, editRanges)
|
|
1114
1303
|
: { action: "allow" as const };
|
|
1115
1304
|
if (verdict.action === "block") {
|
|
1116
1305
|
return {
|
|
@@ -1133,12 +1322,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
1133
1322
|
const dupeWarnings: string[] = [];
|
|
1134
1323
|
const exportRe =
|
|
1135
1324
|
/export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+(\w+)/g;
|
|
1325
|
+
// Read current on-disk content once so we can check whether the file
|
|
1326
|
+
// being written already owns a given export (e.g. it IS the source and
|
|
1327
|
+
// another file merely re-exports from it). cachedExports only tracks one
|
|
1328
|
+
// file per name — whichever was scanned first — so a re-exporter can
|
|
1329
|
+
// win the slot and incorrectly shadow the original definition.
|
|
1330
|
+
let currentFileExports: Set<string> | undefined;
|
|
1331
|
+
if (filePath && nodeFs.existsSync(filePath)) {
|
|
1332
|
+
try {
|
|
1333
|
+
const currentContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
1334
|
+
currentFileExports = new Set<string>();
|
|
1335
|
+
for (const m of currentContent.matchAll(exportRe)) {
|
|
1336
|
+
currentFileExports.add(m[1]);
|
|
1337
|
+
}
|
|
1338
|
+
} catch {
|
|
1339
|
+
// non-fatal — fall back to no current-export knowledge
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1136
1342
|
for (const match of newContent.matchAll(exportRe)) {
|
|
1137
1343
|
const name = match[1];
|
|
1138
1344
|
const existingFile = runtime.cachedExports.get(name);
|
|
1139
1345
|
if (
|
|
1140
1346
|
existingFile &&
|
|
1141
|
-
path.resolve(existingFile) !== path.resolve(filePath)
|
|
1347
|
+
path.resolve(existingFile) !== path.resolve(filePath) &&
|
|
1348
|
+
!currentFileExports?.has(name)
|
|
1142
1349
|
) {
|
|
1143
1350
|
dupeWarnings.push(
|
|
1144
1351
|
`\`${name}\` already exists in ${path.relative(runtime.projectRoot, existingFile)}`,
|
|
@@ -1148,7 +1355,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1148
1355
|
if (dupeWarnings.length > 0) {
|
|
1149
1356
|
return {
|
|
1150
1357
|
block: true,
|
|
1151
|
-
reason:
|
|
1358
|
+
reason:
|
|
1359
|
+
"🔴 STOP - Redefining existing export(s). Import instead:\n" +
|
|
1360
|
+
dupeWarnings.map((w) => " • " + w).join("\n"),
|
|
1152
1361
|
};
|
|
1153
1362
|
}
|
|
1154
1363
|
|
|
@@ -1241,6 +1450,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1241
1450
|
// Real-time feedback on file writes/edits
|
|
1242
1451
|
// biome-ignore lint/suspicious/noExplicitAny: pi.on overload mismatch for tool_result event type
|
|
1243
1452
|
(pi as any).on("tool_result", async (event: any) => {
|
|
1453
|
+
if (!lensEnabled) return;
|
|
1244
1454
|
updateRuntimeIdentityFromEvent(event);
|
|
1245
1455
|
const { biomeClient, ruffClient, metricsClient, agentBehaviorClient } =
|
|
1246
1456
|
await loadBootstrapClients();
|
|
@@ -1263,11 +1473,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1263
1473
|
|
|
1264
1474
|
// --- Turn end: batch jscpd/madge on collected files, then clear state ---
|
|
1265
1475
|
// Clear cascade snapshot at start of each new turn so stale data never leaks
|
|
1266
|
-
pi.on("turn_start", () => {
|
|
1476
|
+
pi.on("turn_start", (_event: any) => {
|
|
1267
1477
|
runtime.beginTurn();
|
|
1478
|
+
clearLastAnalyzedStateCache();
|
|
1268
1479
|
});
|
|
1269
1480
|
|
|
1270
1481
|
pi.on("agent_end", async (_event, ctx) => {
|
|
1482
|
+
if (!lensEnabled) return;
|
|
1271
1483
|
try {
|
|
1272
1484
|
await handleAgentEnd({
|
|
1273
1485
|
ctxCwd: ctx.cwd,
|
|
@@ -1286,7 +1498,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1286
1498
|
}
|
|
1287
1499
|
});
|
|
1288
1500
|
|
|
1289
|
-
pi.on("turn_end", async (_event, ctx) => {
|
|
1501
|
+
pi.on("turn_end", async (_event: any, ctx) => {
|
|
1502
|
+
if (!lensEnabled) return;
|
|
1290
1503
|
try {
|
|
1291
1504
|
const { jscpdClient, knipClient, depChecker, testRunnerClient } =
|
|
1292
1505
|
await loadBootstrapClients();
|
|
@@ -1331,6 +1544,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1331
1544
|
event: { messages?: Array<{ role: string; content: unknown }> } | unknown,
|
|
1332
1545
|
ctx: { cwd?: string },
|
|
1333
1546
|
) => {
|
|
1547
|
+
if (!lensEnabled) return;
|
|
1334
1548
|
try {
|
|
1335
1549
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1336
1550
|
const turnEndFindings = consumeTurnEndFindings(cacheManager, cwd);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.41",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Real-time code feedback for pi
|
|
5
|
+
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/apmantza/pi-lens.git"
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@ast-grep/napi": "^0.42.1",
|
|
66
|
+
"@mariozechner/pi-tui": "^0.72.1",
|
|
66
67
|
"minimatch": "^10.2.5",
|
|
67
68
|
"typebox": "^1.0.0",
|
|
68
69
|
"typescript": "^6.0.3",
|
package/rules/rule-catalog.json
CHANGED
|
@@ -110,6 +110,30 @@
|
|
|
110
110
|
{ "rule_id": "raise-application-error-codes", "engine": "tree-sitter", "language": "plsql", "family": "reliability", "scope": "function", "canonical_concept": "invalid-error-code", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
111
111
|
{ "rule_id": "no-synchronize", "engine": "tree-sitter", "language": "plsql", "family": "reliability", "scope": "function", "canonical_concept": "synchronize-usage", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
112
112
|
{ "rule_id": "send-file-mimetype", "engine": "tree-sitter", "language": "python", "family": "reliability", "scope": "function", "canonical_concept": "send-file-missing-mimetype", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
113
|
-
{ "rule_id": "noexcept-functions", "engine": "tree-sitter", "language": "cpp", "family": "reliability", "scope": "function", "canonical_concept": "missing-noexcept", "severity_default": "error", "confidence": "medium", "status": "active" }
|
|
113
|
+
{ "rule_id": "noexcept-functions", "engine": "tree-sitter", "language": "cpp", "family": "reliability", "scope": "function", "canonical_concept": "missing-noexcept", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
114
|
+
{ "rule_id": "no-octal-values", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "file", "canonical_concept": "octal-literal-usage", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
115
|
+
{ "rule_id": "short-circuit-logic", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "function", "canonical_concept": "non-short-circuit-operator", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
116
|
+
{ "rule_id": "infinite-loop", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "function", "canonical_concept": "infinite-loop", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
117
|
+
{ "rule_id": "infinite-recursion", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "function", "canonical_concept": "infinite-recursion", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
118
|
+
{ "rule_id": "name-capitalization-conflict", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "class", "canonical_concept": "name-capitalization-conflict", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
119
|
+
{ "rule_id": "no-super-torchscript", "engine": "tree-sitter", "language": "python", "family": "reliability", "scope": "function", "canonical_concept": "torchscript-super-call", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
120
|
+
{ "rule_id": "unnecessary-bit-ops", "engine": "tree-sitter", "language": "cpp", "family": "maintainability", "scope": "function", "canonical_concept": "unnecessary-bit-operation", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
121
|
+
{ "rule_id": "unnecessary-bit-ops-java", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "function", "canonical_concept": "unnecessary-bit-operation", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
122
|
+
{ "rule_id": "switch-case-termination-js", "engine": "tree-sitter", "language": "javascript", "family": "reliability", "scope": "function", "canonical_concept": "switch-case-no-termination", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
123
|
+
{ "rule_id": "nchar-nvarchar2-bytes", "engine": "tree-sitter", "language": "plsql", "family": "reliability", "scope": "function", "canonical_concept": "nchar-size-in-bytes", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
124
|
+
{ "rule_id": "tests-include-assertions", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "function", "canonical_concept": "test-without-assertion", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
125
|
+
{ "rule_id": "mockito-initialized", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "class", "canonical_concept": "mockito-not-initialized", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
126
|
+
{ "rule_id": "resources-closed", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "function", "canonical_concept": "resource-leak", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
127
|
+
{ "rule_id": "lock-table", "engine": "tree-sitter", "language": "plsql", "family": "maintainability", "scope": "function", "canonical_concept": "lock-table-usage", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
128
|
+
{ "rule_id": "lock-table-cobol", "engine": "tree-sitter", "language": "cobol", "family": "maintainability", "scope": "file", "canonical_concept": "lock-table-usage", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
129
|
+
{ "rule_id": "await-in-loop", "engine": "tree-sitter", "language": "typescript", "family": "performance", "scope": "function", "canonical_concept": "await-inside-loop", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
130
|
+
{ "rule_id": "switch-case-termination", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "switch-case-no-termination", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
131
|
+
{ "rule_id": "no-octal-values", "engine": "tree-sitter", "language": "typescript", "family": "maintainability", "scope": "file", "canonical_concept": "octal-literal-usage", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
132
|
+
{ "rule_id": "short-circuit-logic", "engine": "tree-sitter", "language": "typescript", "family": "maintainability", "scope": "function", "canonical_concept": "non-short-circuit-operator", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
133
|
+
{ "rule_id": "infinite-loop", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "infinite-loop", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
134
|
+
{ "rule_id": "self-assignment", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "self-assignment", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
135
|
+
{ "rule_id": "duplicate-function-arg", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "duplicate-parameter-name", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
136
|
+
{ "rule_id": "empty-switch-case", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "empty-switch-case", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
137
|
+
{ "rule_id": "default-not-last", "engine": "tree-sitter", "language": "typescript", "family": "maintainability", "scope": "function", "canonical_concept": "default-clause-not-last", "severity_default": "error", "confidence": "high", "status": "active" }
|
|
114
138
|
]
|
|
115
139
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# COBOL LOCK TABLE
|
|
2
|
+
# Detects LOCK TABLE in COBOL
|
|
3
|
+
id: lock-table-cobol
|
|
4
|
+
name: LOCK TABLE Should Not Be Used
|
|
5
|
+
severity: error
|
|
6
|
+
category: maintainability
|
|
7
|
+
defect_class: correctness
|
|
8
|
+
inline_tier: blocking
|
|
9
|
+
language: cobol
|
|
10
|
+
|
|
11
|
+
message: "LOCK TABLE should not be used"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
LOCK TABLE causes concurrency issues. Use row-level locking.
|
|
15
|
+
|
|
16
|
+
query: |
|
|
17
|
+
(lock_table_statement
|
|
18
|
+
(LOCK) @LOCK)
|
|
19
|
+
|
|
20
|
+
metavars:
|
|
21
|
+
- LOCK
|
|
22
|
+
|
|
23
|
+
tags:
|
|
24
|
+
- maintainability
|
|
25
|
+
- cobol
|
|
26
|
+
- bad-practice
|
|
27
|
+
|
|
28
|
+
examples:
|
|
29
|
+
bad: |
|
|
30
|
+
EXEC SQL LOCK TABLE employees IN EXCLUSIVE MODE END-EXEC -- BAD
|
|
31
|
+
|
|
32
|
+
good: |
|
|
33
|
+
EXEC SQL SELECT * FROM employees FOR UPDATE END-EXEC -- GOOD
|
|
34
|
+
|
|
35
|
+
has_fix: false
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Unnecessary Bit Operations
|
|
2
|
+
# Detects bit operations that have no effect
|
|
3
|
+
id: unnecessary-bit-ops
|
|
4
|
+
name: Unnecessary Bit Operations Should Not Be Performed
|
|
5
|
+
severity: error
|
|
6
|
+
category: maintainability
|
|
7
|
+
defect_class: correctness
|
|
8
|
+
inline_tier: blocking
|
|
9
|
+
language: cpp
|
|
10
|
+
|
|
11
|
+
message: "Unnecessary bit operation - has no effect"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
Operations like (x | 0), (x & -1), (x ^ 0) have no effect.
|
|
15
|
+
They indicate confusion or incomplete refactoring. Remove them.
|
|
16
|
+
|
|
17
|
+
✅ FIX: Remove unnecessary operation
|
|
18
|
+
|
|
19
|
+
```cpp
|
|
20
|
+
int y = x; // GOOD - simplified
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
query: |
|
|
24
|
+
(binary_expression
|
|
25
|
+
(identifier) @VAR
|
|
26
|
+
"|" @OP
|
|
27
|
+
(number_literal) @ZERO (#eq? @ZERO "0"))
|
|
28
|
+
(binary_expression
|
|
29
|
+
(identifier) @VAR
|
|
30
|
+
"&" @OP
|
|
31
|
+
(number_literal) @VAL (#match? @VAL "^-?1+$"))
|
|
32
|
+
(binary_expression
|
|
33
|
+
(identifier) @VAR
|
|
34
|
+
"^" @OP
|
|
35
|
+
(number_literal) @ZERO (#eq? @ZERO "0"))
|
|
36
|
+
|
|
37
|
+
metavars:
|
|
38
|
+
- VAR
|
|
39
|
+
- OP
|
|
40
|
+
- ZERO
|
|
41
|
+
- VAL
|
|
42
|
+
|
|
43
|
+
tags:
|
|
44
|
+
- maintainability
|
|
45
|
+
- cpp
|
|
46
|
+
- suspicious
|
|
47
|
+
|
|
48
|
+
examples:
|
|
49
|
+
bad: |
|
|
50
|
+
int y = x | 0; // BAD - no effect
|
|
51
|
+
int z = x & -1; // BAD - no effect
|
|
52
|
+
int w = x ^ 0; // BAD - no effect
|
|
53
|
+
|
|
54
|
+
good: |
|
|
55
|
+
int y = x; // GOOD - simplified
|
|
56
|
+
|
|
57
|
+
has_fix: true
|
|
58
|
+
fix_action: remove_bit_operation
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Infinite Loop
|
|
2
|
+
# Detects potentially infinite while(true) loops without break
|
|
3
|
+
id: infinite-loop
|
|
4
|
+
name: Loops Should Not Be Infinite
|
|
5
|
+
severity: error
|
|
6
|
+
category: reliability
|
|
7
|
+
defect_class: correctness
|
|
8
|
+
inline_tier: blocking
|
|
9
|
+
language: java
|
|
10
|
+
|
|
11
|
+
message: "Loop appears to be infinite with no break condition"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
while(true) or for(;;) without a break condition creates an
|
|
15
|
+
infinite loop. Ensure there's an exit condition with break,
|
|
16
|
+
return, or System.exit().
|
|
17
|
+
|
|
18
|
+
✅ FIX: Add break condition or use scheduled executor
|
|
19
|
+
|
|
20
|
+
```java
|
|
21
|
+
while (running) { // GOOD - control flag
|
|
22
|
+
if (shouldStop()) break;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
query: |
|
|
27
|
+
(while_statement
|
|
28
|
+
condition: (true) @COND
|
|
29
|
+
body: (block) @BODY)
|
|
30
|
+
(for_statement
|
|
31
|
+
condition: (null)
|
|
32
|
+
body: (block) @BODY)
|
|
33
|
+
|
|
34
|
+
metavars:
|
|
35
|
+
- COND
|
|
36
|
+
- BODY
|
|
37
|
+
|
|
38
|
+
post_filter: no_break_or_return
|
|
39
|
+
|
|
40
|
+
tags:
|
|
41
|
+
- reliability
|
|
42
|
+
- java
|
|
43
|
+
- cert
|
|
44
|
+
- bugs
|
|
45
|
+
|
|
46
|
+
examples:
|
|
47
|
+
bad: |
|
|
48
|
+
while (true) { // BAD - infinite
|
|
49
|
+
doWork();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
good: |
|
|
53
|
+
while (true) { // GOOD - has exit
|
|
54
|
+
if (shouldStop()) break;
|
|
55
|
+
doWork();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
has_fix: false
|