pi-lens 3.8.69 → 3.8.70
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 +75 -0
- package/README.md +15 -13
- package/dist/clients/bootstrap.js +3 -1
- package/dist/clients/bus-events-logger.js +51 -0
- package/dist/clients/bus-publish.js +45 -2
- package/dist/clients/code-quality-warnings.js +27 -0
- package/dist/clients/dead-code-client.js +5 -23
- package/dist/clients/diagnostics-publish.js +38 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +62 -6
- package/dist/clients/dispatch/facts/function-facts.js +83 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -1
- package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
- package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
- package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
- package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
- package/dist/clients/dispatch/rules/high-complexity.js +12 -1
- package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
- package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
- package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
- package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
- package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
- package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
- package/dist/clients/dispatch/runners/lsp.js +22 -6
- package/dist/clients/gitleaks-client.js +40 -5
- package/dist/clients/installer/index.js +123 -6
- package/dist/clients/instance-reaper.js +229 -2
- package/dist/clients/instance-registry.js +71 -2
- package/dist/clients/knip-client.js +35 -38
- package/dist/clients/language-profile.js +7 -11
- package/dist/clients/lens-engine.js +12 -0
- package/dist/clients/log-cleanup.js +61 -19
- package/dist/clients/lsp/cascade-tier.js +31 -19
- package/dist/clients/lsp/client.js +52 -7
- package/dist/clients/lsp/config.js +27 -1
- package/dist/clients/lsp/index.js +547 -59
- package/dist/clients/lsp/server-strategies.js +17 -8
- package/dist/clients/lsp/server.js +63 -27
- package/dist/clients/lsp-budget.js +148 -0
- package/dist/clients/mcp/session.js +1 -0
- package/dist/clients/middle-man-analysis.js +260 -0
- package/dist/clients/module-report.js +173 -11
- package/dist/clients/ndjson-logger.js +24 -0
- package/dist/clients/opengrep-client.js +217 -0
- package/dist/clients/path-utils.js +40 -0
- package/dist/clients/pipeline.js +1 -1
- package/dist/clients/project-diagnostics/extractors.js +21 -1
- package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
- package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
- package/dist/clients/quiet-window.js +57 -2
- package/dist/clients/read-expansion.js +2 -3
- package/dist/clients/read-guard-tool-lines.js +10 -0
- package/dist/clients/resource-sampler.js +254 -0
- package/dist/clients/review-graph/builder.js +265 -11
- package/dist/clients/review-graph/symbol-id.js +57 -0
- package/dist/clients/runtime-session.js +40 -6
- package/dist/clients/runtime-turn.js +53 -11
- package/dist/clients/safe-spawn.js +48 -2
- package/dist/clients/source-filter.js +12 -25
- package/dist/clients/source-walker.js +66 -0
- package/dist/clients/startup-scan.js +37 -42
- package/dist/clients/symbol-containment.js +24 -0
- package/dist/clients/test-runner-client.js +544 -29
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/widget-state.js +61 -1
- package/dist/clients/write-ordering-guard.js +58 -0
- package/dist/clients/zizmor-config.js +27 -0
- package/dist/index.js +6640 -2947
- package/dist/mcp/server.js +95 -19
- package/dist/tools/activate-tools.js +72 -0
- package/dist/tools/ast-dump.js +2 -8
- package/dist/tools/lens-diagnostics.js +290 -30
- package/dist/tools/lsp-diagnostics.js +544 -104
- package/dist/tools/module-report.js +18 -5
- package/docs/agent-tooling-improvement-plan.md +660 -0
- package/docs/agent-tools.md +129 -0
- package/docs/ast-grep_rules_catalog.md +564 -0
- package/docs/astplayground.md +178 -0
- package/docs/audit1.md +177 -0
- package/docs/custom-rules.md +231 -0
- package/docs/dependencies.md +54 -0
- package/docs/env_variables.md +10 -0
- package/docs/environment-variables.md +67 -0
- package/docs/fable.md +139 -0
- package/docs/features.md +338 -0
- package/docs/globalconfig.md +79 -0
- package/docs/inspiration_gograph.md +601 -0
- package/docs/inspiration_neovim.md +1795 -0
- package/docs/inspiration_neovim2.md +402 -0
- package/docs/inspiration_tree_sitter_rules.md +369 -0
- package/docs/inspiration_tree_sitter_rules_investigation.md +328 -0
- package/docs/inspiration_tree_sitter_rules_round2.md +470 -0
- package/docs/language-coverage.md +48 -0
- package/docs/lsp-bridge-analysis.md +444 -0
- package/docs/lsp-capability-matrix.md +157 -0
- package/docs/lsp-latency-benchmark.md +169 -0
- package/docs/mcp.md +438 -0
- package/docs/module-report-read-symbol.md +258 -0
- package/docs/newinspiration.md +338 -0
- package/docs/servercapabilities.md +118 -0
- package/docs/subagent-compat.md +281 -0
- package/docs/tools.md +13 -0
- package/docs/tools_improvement2.md +345 -0
- package/docs/tree-sitter_rules_catalog.md +296 -0
- package/docs/usage.md +129 -0
- package/docs/word-index.md +150 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-toml.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
- package/package.json +5 -1
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
- package/skills/pi-lens-ast-grep/SKILL.md +3 -1
- package/dist/clients/production-readiness.js +0 -493
|
@@ -63,7 +63,7 @@ export function loadBootstrapClients() {
|
|
|
63
63
|
return [];
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
const [ruffClient, biomeClient, knipClient, todoScanner, jscpdClient, depChecker, testRunnerClient, metricsClient, complexityClient, goClient, govulncheckClient, gitleaksClient, trivyClient, rustClient, agentBehaviorClient, deadCodeClients,] = await Promise.all([
|
|
66
|
+
const [ruffClient, biomeClient, knipClient, todoScanner, jscpdClient, depChecker, testRunnerClient, metricsClient, complexityClient, goClient, govulncheckClient, gitleaksClient, trivyClient, opengrepClient, rustClient, agentBehaviorClient, deadCodeClients,] = await Promise.all([
|
|
67
67
|
load("ruff", async () => new (await import("./ruff-client.js")).RuffClient()),
|
|
68
68
|
load("biome", async () => new (await import("./biome-client.js")).BiomeClient()),
|
|
69
69
|
load("knip", async () => new (await import("./knip-client.js")).KnipClient()),
|
|
@@ -77,6 +77,7 @@ export function loadBootstrapClients() {
|
|
|
77
77
|
load("govulncheck", async () => new (await import("./govulncheck-client.js")).GovulncheckClient()),
|
|
78
78
|
load("gitleaks", async () => new (await import("./gitleaks-client.js")).GitleaksClient()),
|
|
79
79
|
load("trivy", async () => new (await import("./trivy-client.js")).TrivyClient()),
|
|
80
|
+
load("opengrep", async () => new (await import("./opengrep-client.js")).OpengrepClient()),
|
|
80
81
|
load("rust", async () => new (await import("./rust-client.js")).RustClient()),
|
|
81
82
|
load("agent-behavior", async () => new (await import("./agent-behavior-client.js")).AgentBehaviorClient()),
|
|
82
83
|
loadList("dead-code", async () => (await import("./dead-code-client.js")).getDeadCodeClients()),
|
|
@@ -97,6 +98,7 @@ export function loadBootstrapClients() {
|
|
|
97
98
|
govulncheckClient,
|
|
98
99
|
gitleaksClient,
|
|
99
100
|
trivyClient,
|
|
101
|
+
opengrepClient,
|
|
100
102
|
rustClient,
|
|
101
103
|
agentBehaviorClient,
|
|
102
104
|
deadCodeClients,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent NDJSON trace of `pi.events` bus publish attempts
|
|
3
|
+
* (`pilens:files:touched` #482 / `pilens:diagnostics` #502).
|
|
4
|
+
*
|
|
5
|
+
* Both producers (clients/bus-publish.ts, clients/diagnostics-publish.ts) are
|
|
6
|
+
* fire-and-forget: on failure or on a structural no-op (never wired, kill
|
|
7
|
+
* switch off) they only invoke an optional `dbg` callback, which varies by
|
|
8
|
+
* host and is a documented no-op in the MCP host (clients/mcp/session.ts's
|
|
9
|
+
* `dbg: noop`). That leaves bus-publish outcomes invisible in exactly the
|
|
10
|
+
* context where they matter most — same failure shape as the #544 MCP
|
|
11
|
+
* session_start incident this repo already fixed once.
|
|
12
|
+
*
|
|
13
|
+
* This module gives bus events the same durable trace every other pi-lens
|
|
14
|
+
* subsystem already has (latency.log, cascade.log, read-guard.log, ...) —
|
|
15
|
+
* see clients/latency-logger.ts for the house pattern this mirrors exactly:
|
|
16
|
+
* one shared `createNdjsonLogger` writer, `isTestMode()` no-op guard,
|
|
17
|
+
* `getBusEventsLogPath()` for testability.
|
|
18
|
+
*
|
|
19
|
+
* Logging volume: `emitted` and `emit_failed` are logged on every call —
|
|
20
|
+
* they're the two outcomes an operator actually needs a per-event trace for.
|
|
21
|
+
* `skipped_unwired` and `skipped_disabled` are process-lifetime-static facts
|
|
22
|
+
* (wiring happens once at extension factory time; the kill switch is an env
|
|
23
|
+
* var read once at startup) — logging them on every publish attempt would
|
|
24
|
+
* spam one identical line per write batch for an entire session with zero
|
|
25
|
+
* new information after the first. Both are gated log-once-per-process, the
|
|
26
|
+
* same `hasLoggedFailure` shape the producers already use for emit_failed.
|
|
27
|
+
* The empty-batch branch (`paths.length === 0` / `files.length === 0`) is
|
|
28
|
+
* NOT logged at all: every call site already guards against invoking these
|
|
29
|
+
* functions with nothing to report (see clients/pipeline.ts,
|
|
30
|
+
* clients/runtime-agent-end.ts), so it's a normal, frequent no-op rather
|
|
31
|
+
* than a real event worth a log line.
|
|
32
|
+
*/
|
|
33
|
+
import * as path from "node:path";
|
|
34
|
+
import { isTestMode } from "./env-utils.js";
|
|
35
|
+
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
36
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
37
|
+
const BUS_EVENTS_LOG_FILE = path.join(getGlobalPiLensDir(), "bus-events.log");
|
|
38
|
+
const writer = createNdjsonLogger({ filePath: BUS_EVENTS_LOG_FILE });
|
|
39
|
+
export function logBusEvent(entry) {
|
|
40
|
+
if (isTestMode()) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
44
|
+
}
|
|
45
|
+
export function getBusEventsLogPath() {
|
|
46
|
+
return BUS_EVENTS_LOG_FILE;
|
|
47
|
+
}
|
|
48
|
+
/** Resolve once all enqueued bus-event writes are on disk (tests/shutdown). */
|
|
49
|
+
export function flushBusEventsLog() {
|
|
50
|
+
return writer.flush();
|
|
51
|
+
}
|
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
* callback is invoked at most once on first failure so a wired caller can log
|
|
18
18
|
* it without spamming.
|
|
19
19
|
*/
|
|
20
|
+
import { logBusEvent } from "./bus-events-logger.js";
|
|
20
21
|
import { normalizeFilePath } from "./path-utils.js";
|
|
21
22
|
import { appendRecentTouches } from "./recent-touches.js";
|
|
22
23
|
export const BUS_FILES_TOUCHED_EVENT = "pilens:files:touched";
|
|
23
24
|
export const BUS_FILES_TOUCHED_VERSION = 1;
|
|
24
25
|
let busEmit;
|
|
25
26
|
let hasLoggedFailure = false;
|
|
27
|
+
// Log-once-per-process guards for the two structural-no-op outcomes below
|
|
28
|
+
// (see bus-events-logger.ts's module doc for why these aren't logged on
|
|
29
|
+
// every call — both are static, session-lifetime facts, not transient
|
|
30
|
+
// per-event failures).
|
|
31
|
+
let hasLoggedUnwired = false;
|
|
32
|
+
let hasLoggedDisabled = false;
|
|
26
33
|
/**
|
|
27
34
|
* Wire the emit function from pi's `pi.events` bus. Called once at extension
|
|
28
35
|
* factory time from index.ts (module-level singleton, same pattern as
|
|
@@ -37,6 +44,8 @@ export function wireBusEmitter(emitFn) {
|
|
|
37
44
|
export function _resetForTests() {
|
|
38
45
|
busEmit = undefined;
|
|
39
46
|
hasLoggedFailure = false;
|
|
47
|
+
hasLoggedUnwired = false;
|
|
48
|
+
hasLoggedDisabled = false;
|
|
40
49
|
_envCache = undefined;
|
|
41
50
|
}
|
|
42
51
|
let _envCache;
|
|
@@ -73,8 +82,18 @@ export function publishFilesTouched(args) {
|
|
|
73
82
|
return;
|
|
74
83
|
if (args.paths.length === 0)
|
|
75
84
|
return;
|
|
76
|
-
if (!isBusPublishEnabled())
|
|
85
|
+
if (!isBusPublishEnabled()) {
|
|
86
|
+
if (!hasLoggedDisabled) {
|
|
87
|
+
hasLoggedDisabled = true;
|
|
88
|
+
logBusEvent({
|
|
89
|
+
event: BUS_FILES_TOUCHED_EVENT,
|
|
90
|
+
outcome: "skipped_disabled",
|
|
91
|
+
cwd: normalizeFilePath(args.cwd),
|
|
92
|
+
reason: args.reason,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
77
95
|
return;
|
|
96
|
+
}
|
|
78
97
|
void appendRecentTouches({
|
|
79
98
|
cwd: args.cwd,
|
|
80
99
|
reason: args.reason,
|
|
@@ -83,8 +102,18 @@ export function publishFilesTouched(args) {
|
|
|
83
102
|
}).catch((err) => {
|
|
84
103
|
args.dbg?.(`bus-publish: recent-touches append failed: ${err}`);
|
|
85
104
|
});
|
|
86
|
-
if (!busEmit)
|
|
105
|
+
if (!busEmit) {
|
|
106
|
+
if (!hasLoggedUnwired) {
|
|
107
|
+
hasLoggedUnwired = true;
|
|
108
|
+
logBusEvent({
|
|
109
|
+
event: BUS_FILES_TOUCHED_EVENT,
|
|
110
|
+
outcome: "skipped_unwired",
|
|
111
|
+
cwd: normalizeFilePath(args.cwd),
|
|
112
|
+
reason: args.reason,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
87
115
|
return;
|
|
116
|
+
}
|
|
88
117
|
try {
|
|
89
118
|
const payload = {
|
|
90
119
|
v: BUS_FILES_TOUCHED_VERSION,
|
|
@@ -100,8 +129,22 @@ export function publishFilesTouched(args) {
|
|
|
100
129
|
}));
|
|
101
130
|
}
|
|
102
131
|
busEmit(BUS_FILES_TOUCHED_EVENT, payload);
|
|
132
|
+
logBusEvent({
|
|
133
|
+
event: BUS_FILES_TOUCHED_EVENT,
|
|
134
|
+
outcome: "emitted",
|
|
135
|
+
cwd: payload.cwd,
|
|
136
|
+
reason: args.reason,
|
|
137
|
+
fileCount: payload.paths.length,
|
|
138
|
+
});
|
|
103
139
|
}
|
|
104
140
|
catch (err) {
|
|
141
|
+
logBusEvent({
|
|
142
|
+
event: BUS_FILES_TOUCHED_EVENT,
|
|
143
|
+
outcome: "emit_failed",
|
|
144
|
+
cwd: normalizeFilePath(args.cwd),
|
|
145
|
+
reason: args.reason,
|
|
146
|
+
error: String(err),
|
|
147
|
+
});
|
|
105
148
|
if (!hasLoggedFailure) {
|
|
106
149
|
hasLoggedFailure = true;
|
|
107
150
|
args.dbg?.(`bus-publish: pilens:files:touched emit failed (further failures suppressed): ${err}`);
|
|
@@ -135,6 +135,33 @@ export function buildCodeQualityWarningsReport(args) {
|
|
|
135
135
|
},
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* #557 audit (same race class as #555's LSP client fix and #560's
|
|
140
|
+
* `widget-state.ts` `recordDiagnostics` fix): pi-lens deliberately allows
|
|
141
|
+
* concurrent pipeline runs for the SAME file across DIFFERENT same-turn
|
|
142
|
+
* edits (dedupe key is `filePath + contentHash`, not just `filePath` — see
|
|
143
|
+
* `clients/runtime-tool-result.ts`), so a per-key cache fed directly from
|
|
144
|
+
* those concurrent pipeline runs needs an ordering guard (`WriteOrderingGuard`
|
|
145
|
+
* from `clients/write-ordering-guard.ts`) or an older edit's slower pipeline
|
|
146
|
+
* can silently overwrite a newer edit's fresher write.
|
|
147
|
+
*
|
|
148
|
+
* This call site is NOT exposed to that race. Unlike `widget-state.ts`'s
|
|
149
|
+
* `recordDiagnostics` (called directly from `clients/pipeline.ts`, once per
|
|
150
|
+
* pipeline run — i.e. potentially several times per turn, out of completion
|
|
151
|
+
* order), `writeCodeQualityWarningsReport` has exactly one caller:
|
|
152
|
+
* `handleTurnEnd` in `clients/runtime-turn.ts`, which per-edit pipeline runs
|
|
153
|
+
* never call directly — they only feed `runtime.recordCodeQualityWarnings`
|
|
154
|
+
* (an accumulating, order-independent Map). `handleTurnEnd` itself reads that
|
|
155
|
+
* accumulator once via `runtime.peekCodeQualityWarnings()` and writes the
|
|
156
|
+
* aggregate report exactly once per turn-end invocation — the same single
|
|
157
|
+
* sequential turn-end-only shape already confirmed safe for
|
|
158
|
+
* `writeActionableWarningsReport` in `clients/actionable-warnings.ts`, right
|
|
159
|
+
* next to this call site in `handleTurnEnd`. No second writer, so no
|
|
160
|
+
* ordering token to guard against — see
|
|
161
|
+
* `tests/clients/code-quality-warnings.test.ts`'s
|
|
162
|
+
* "single sequential caller" test, which pins this invariant so a future
|
|
163
|
+
* change that adds a second call site has a better chance of being caught.
|
|
164
|
+
*/
|
|
138
165
|
export function writeCodeQualityWarningsReport(cacheManager, cwd, report) {
|
|
139
166
|
cacheManager.writeCache("code-quality-warnings", report, cwd);
|
|
140
167
|
}
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
* Phase 1 ships Python via `vulture`. Future phases add Go/Rust/etc. by
|
|
12
12
|
* implementing DeadCodeClient and adding to getDeadCodeClients().
|
|
13
13
|
*/
|
|
14
|
-
import * as fs from "node:fs";
|
|
15
|
-
import * as os from "node:os";
|
|
16
14
|
import * as path from "node:path";
|
|
17
|
-
import {
|
|
15
|
+
import { findNearestMarkerRoot } from "./path-utils.js";
|
|
18
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
19
17
|
function emptyResult(language) {
|
|
20
18
|
return {
|
|
@@ -108,34 +106,18 @@ export class PythonDeadCodeClient {
|
|
|
108
106
|
* Nearest dir with a Python project marker, never at/above $HOME and never
|
|
109
107
|
* escaping a VCS boundary — same containment rules as KnipClient so a scan
|
|
110
108
|
* launched from a bare cwd can't recurse the whole home tree (#250/#296).
|
|
109
|
+
* Delegates to the shared path-utils helper (refs #625) rather than
|
|
110
|
+
* hand-rolling the climb; only the marker list differs from KnipClient's.
|
|
111
111
|
*/
|
|
112
112
|
resolveProjectRoot(startDir, homeDirOverride) {
|
|
113
|
-
|
|
113
|
+
return findNearestMarkerRoot(startDir, [
|
|
114
114
|
"pyproject.toml",
|
|
115
115
|
"setup.py",
|
|
116
116
|
"setup.cfg",
|
|
117
117
|
"requirements.txt",
|
|
118
118
|
"Pipfile",
|
|
119
119
|
"tox.ini",
|
|
120
|
-
];
|
|
121
|
-
const boundaries = [".git", ".hg", ".svn"];
|
|
122
|
-
const homeDir = path.resolve(homeDirOverride ?? os.homedir());
|
|
123
|
-
let current = path.resolve(startDir);
|
|
124
|
-
for (let depth = 0; depth < 64; depth++) {
|
|
125
|
-
if (isAtOrAboveHomeDir(current, homeDir))
|
|
126
|
-
return null;
|
|
127
|
-
if (markers.some((m) => fs.existsSync(path.join(current, m)))) {
|
|
128
|
-
return current;
|
|
129
|
-
}
|
|
130
|
-
if (boundaries.some((m) => fs.existsSync(path.join(current, m)))) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const parent = path.dirname(current);
|
|
134
|
-
if (parent === current)
|
|
135
|
-
return null;
|
|
136
|
-
current = parent;
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
120
|
+
], { boundaries: [".git", ".hg", ".svn"], homeDir: homeDirOverride });
|
|
139
121
|
}
|
|
140
122
|
async ensureAvailable() {
|
|
141
123
|
if (this.available !== null)
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
* fields may be added under `v: 1`; a breaking change to an existing field's
|
|
60
60
|
* meaning must bump `v`.
|
|
61
61
|
*/
|
|
62
|
+
import { logBusEvent } from "./bus-events-logger.js";
|
|
62
63
|
import { normalizeFilePath } from "./path-utils.js";
|
|
63
64
|
import { isBusPublishEnabled } from "./bus-publish.js";
|
|
64
65
|
export const BUS_DIAGNOSTICS_EVENT = "pilens:diagnostics";
|
|
@@ -67,6 +68,8 @@ export const BUS_DIAGNOSTICS_VERSION = 1;
|
|
|
67
68
|
export const MAX_DIAGNOSTICS_PER_FILE_EVENT = 12;
|
|
68
69
|
let busEmit;
|
|
69
70
|
let hasLoggedFailure = false;
|
|
71
|
+
let hasLoggedUnwired = false;
|
|
72
|
+
let hasLoggedDisabled = false;
|
|
70
73
|
let seqCounter = 0;
|
|
71
74
|
/** Paths this producer has reported with at least one non-empty diagnostics array, so we know when to fire the one-time clean-transition event. */
|
|
72
75
|
const reportedDirtyPaths = new Set();
|
|
@@ -82,6 +85,8 @@ export function wireDiagnosticsBusEmitter(emitFn) {
|
|
|
82
85
|
export function _resetDiagnosticsPublishForTests() {
|
|
83
86
|
busEmit = undefined;
|
|
84
87
|
hasLoggedFailure = false;
|
|
88
|
+
hasLoggedUnwired = false;
|
|
89
|
+
hasLoggedDisabled = false;
|
|
85
90
|
seqCounter = 0;
|
|
86
91
|
reportedDirtyPaths.clear();
|
|
87
92
|
}
|
|
@@ -129,10 +134,28 @@ export function publishDiagnostics(args) {
|
|
|
129
134
|
return;
|
|
130
135
|
if (args.files.length === 0)
|
|
131
136
|
return;
|
|
132
|
-
if (!isBusPublishEnabled())
|
|
137
|
+
if (!isBusPublishEnabled()) {
|
|
138
|
+
if (!hasLoggedDisabled) {
|
|
139
|
+
hasLoggedDisabled = true;
|
|
140
|
+
logBusEvent({
|
|
141
|
+
event: BUS_DIAGNOSTICS_EVENT,
|
|
142
|
+
outcome: "skipped_disabled",
|
|
143
|
+
cwd: normalizeFilePath(args.cwd),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
133
146
|
return;
|
|
134
|
-
|
|
147
|
+
}
|
|
148
|
+
if (!busEmit) {
|
|
149
|
+
if (!hasLoggedUnwired) {
|
|
150
|
+
hasLoggedUnwired = true;
|
|
151
|
+
logBusEvent({
|
|
152
|
+
event: BUS_DIAGNOSTICS_EVENT,
|
|
153
|
+
outcome: "skipped_unwired",
|
|
154
|
+
cwd: normalizeFilePath(args.cwd),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
135
157
|
return;
|
|
158
|
+
}
|
|
136
159
|
try {
|
|
137
160
|
const fileEntries = args.files.map((f) => {
|
|
138
161
|
const normPath = normalizeFilePath(f.path);
|
|
@@ -161,8 +184,21 @@ export function publishDiagnostics(args) {
|
|
|
161
184
|
files: fileEntries,
|
|
162
185
|
};
|
|
163
186
|
busEmit(BUS_DIAGNOSTICS_EVENT, payload);
|
|
187
|
+
logBusEvent({
|
|
188
|
+
event: BUS_DIAGNOSTICS_EVENT,
|
|
189
|
+
outcome: "emitted",
|
|
190
|
+
cwd: payload.cwd,
|
|
191
|
+
fileCount: payload.files.length,
|
|
192
|
+
seq: payload.seq,
|
|
193
|
+
});
|
|
164
194
|
}
|
|
165
195
|
catch (err) {
|
|
196
|
+
logBusEvent({
|
|
197
|
+
event: BUS_DIAGNOSTICS_EVENT,
|
|
198
|
+
outcome: "emit_failed",
|
|
199
|
+
cwd: normalizeFilePath(args.cwd),
|
|
200
|
+
error: String(err),
|
|
201
|
+
});
|
|
166
202
|
if (!hasLoggedFailure) {
|
|
167
203
|
hasLoggedFailure = true;
|
|
168
204
|
args.dbg?.(`diagnostics-publish: pilens:diagnostics emit failed (further failures suppressed): ${err}`);
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* Adding a new cross-cutting tool = register an `LSPServerInfo` with
|
|
17
17
|
* `role:"auxiliary"` (clients/lsp/server.ts) + one profile entry here.
|
|
18
18
|
*/
|
|
19
|
+
import { shouldDegradeAuxiliaryLsp } from "../lsp-budget.js";
|
|
19
20
|
import { findLocalOpengrepConfig } from "../opengrep-config.js";
|
|
20
21
|
import { findLocalTyposConfig } from "../typos-config.js";
|
|
21
22
|
import { findLocalZizmorConfig } from "../zizmor-config.js";
|
|
@@ -56,6 +57,11 @@ export function isNosemgrepSuppressed(d, content) {
|
|
|
56
57
|
return (checkLine(lines[startLine], false) ||
|
|
57
58
|
checkLine(lines[startLine - 1], true));
|
|
58
59
|
}
|
|
60
|
+
// #277 R7: every profile below used this exact same rule (ERROR-severity
|
|
61
|
+
// findings block only when the workspace opted into curated/authored rules;
|
|
62
|
+
// everything else stays advisory) — shared here instead of copy-pasted per
|
|
63
|
+
// profile so a future policy change (e.g. a WARNING-blocking tier) is one edit.
|
|
64
|
+
const blockOnErrorWhenAllowed = (d, { blockingAllowed }) => (blockingAllowed && d.severity === 1 ? "blocking" : "warning");
|
|
59
65
|
export const AUXILIARY_LSP_PROFILES = [
|
|
60
66
|
{
|
|
61
67
|
serverId: "opengrep",
|
|
@@ -71,7 +77,7 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
71
77
|
// supplies its own curated rules (the author's deliberate severity); the
|
|
72
78
|
// auto set is advisory. Either way, findings surface via lens_diagnostics.
|
|
73
79
|
allowBlocking: (cwd) => Boolean(findLocalOpengrepConfig(cwd)),
|
|
74
|
-
semantic:
|
|
80
|
+
semantic: blockOnErrorWhenAllowed,
|
|
75
81
|
defectClass: (d) => classifyDefect(String(d.code ?? ""), "opengrep", d.message ?? ""),
|
|
76
82
|
// Honor the canonical Semgrep suppression the user already knows (#441).
|
|
77
83
|
isSuppressed: isNosemgrepSuppressed,
|
|
@@ -88,7 +94,7 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
88
94
|
// deliberate, so preserve ast-grep's severity semantics: ERROR can block,
|
|
89
95
|
// WARNING/INFO stay advisory.
|
|
90
96
|
allowBlocking: () => true,
|
|
91
|
-
semantic:
|
|
97
|
+
semantic: blockOnErrorWhenAllowed,
|
|
92
98
|
defectClass: (d) => classifyDefect(String(d.code ?? ""), "ast-grep", d.message ?? ""),
|
|
93
99
|
},
|
|
94
100
|
{
|
|
@@ -104,7 +110,7 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
104
110
|
// ignores). Advisory otherwise — findings still surface via lens_diagnostics.
|
|
105
111
|
// zizmor maps High→ERROR(1), Medium/Low→WARNING(2), Informational→INFO(3).
|
|
106
112
|
allowBlocking: (cwd) => Boolean(findLocalZizmorConfig(cwd)),
|
|
107
|
-
semantic:
|
|
113
|
+
semantic: blockOnErrorWhenAllowed,
|
|
108
114
|
defectClass: (d) => classifyDefect(String(d.code ?? ""), "zizmor", d.message ?? ""),
|
|
109
115
|
},
|
|
110
116
|
{
|
|
@@ -122,23 +128,73 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
122
128
|
// severity is WARNING, so even with a config it stays advisory unless the
|
|
123
129
|
// repo raises `diagnostic-severity` to Error.
|
|
124
130
|
allowBlocking: (cwd) => Boolean(findLocalTyposConfig(cwd)),
|
|
125
|
-
semantic:
|
|
131
|
+
semantic: blockOnErrorWhenAllowed,
|
|
126
132
|
// A misspelling is a documentation/quality defect — not security or
|
|
127
133
|
// correctness. "style" is the closest taxonomy class.
|
|
128
134
|
defectClass: () => "style",
|
|
129
135
|
},
|
|
130
136
|
];
|
|
131
137
|
/** The auxiliary server ids enabled for this turn (the lsp runner passes these
|
|
132
|
-
* to `touchFile` since it — not the LSP service — owns flag access).
|
|
138
|
+
* to `touchFile` since it — not the LSP service — owns flag access).
|
|
139
|
+
*
|
|
140
|
+
* #449 slice 2 (prototype): when this process decided at `session_start` that
|
|
141
|
+
* the machine-wide LSP budget is exceeded (`clients/lsp-budget.ts`), auxiliary
|
|
142
|
+
* servers are skipped entirely for the rest of the session — the primary
|
|
143
|
+
* language server per file is unaffected. This is a per-SESSION degrade, not
|
|
144
|
+
* per-file: once over budget, this session never spawns its auxiliary fleet,
|
|
145
|
+
* rather than flip-flopping file to file. */
|
|
133
146
|
export function enabledAuxiliaryLspServerIds(getFlag) {
|
|
147
|
+
if (shouldDegradeAuxiliaryLsp())
|
|
148
|
+
return [];
|
|
134
149
|
return AUXILIARY_LSP_PROFILES.flatMap((p) => p.enabledByDefault &&
|
|
135
150
|
!(p.killSwitchFlag && getFlag(p.killSwitchFlag) === true)
|
|
136
151
|
? [p.serverId]
|
|
137
152
|
: []);
|
|
138
153
|
}
|
|
154
|
+
// #277 R7: `findAuxiliaryProfileForSource` is called once per diagnostic, and a
|
|
155
|
+
// file's diagnostics are typically dominated by a handful of distinct `source`
|
|
156
|
+
// strings (one per tool that fired). Memoizing by exact `source` turns an
|
|
157
|
+
// O(profiles) regex scan per diagnostic into one scan per distinct source seen
|
|
158
|
+
// — safe because `AUXILIARY_LSP_PROFILES` is a fixed module-level const, never
|
|
159
|
+
// mutated at runtime, so a source's matching profile never changes.
|
|
160
|
+
const profileForSourceCache = new Map();
|
|
139
161
|
/** Find the profile whose server emitted a diagnostic with this `source`. */
|
|
140
162
|
export function findAuxiliaryProfileForSource(source) {
|
|
141
163
|
if (!source)
|
|
142
164
|
return undefined;
|
|
143
|
-
|
|
165
|
+
const cached = profileForSourceCache.get(source);
|
|
166
|
+
if (cached !== undefined || profileForSourceCache.has(source))
|
|
167
|
+
return cached;
|
|
168
|
+
const found = AUXILIARY_LSP_PROFILES.find((p) => p.sourceMatch.test(source));
|
|
169
|
+
profileForSourceCache.set(source, found);
|
|
170
|
+
return found;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Single-diagnostic suppression check (#586): look up the diagnostic's
|
|
174
|
+
* auxiliary profile by `source` and, if that profile declares an
|
|
175
|
+
* `isSuppressed` callback (currently only opengrep's `# nosemgrep`, #441),
|
|
176
|
+
* apply it. Returns false for diagnostics with no matching profile or whose
|
|
177
|
+
* profile has no native suppression syntax — the common case for plain
|
|
178
|
+
* language-server diagnostics.
|
|
179
|
+
*
|
|
180
|
+
* This is the ONE lookup+apply implementation; every call site that decides
|
|
181
|
+
* whether to drop a diagnostic for its tool's own inline suppression comment
|
|
182
|
+
* should go through this (or `applyAuxiliarySuppressions` below) rather than
|
|
183
|
+
* re-deriving the profile lookup.
|
|
184
|
+
*/
|
|
185
|
+
export function isAuxiliaryDiagnosticSuppressed(d, content) {
|
|
186
|
+
const profile = findAuxiliaryProfileForSource(d.source);
|
|
187
|
+
return Boolean(profile?.isSuppressed?.(d, content));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Filter a diagnostic list down to the ones NOT suppressed by their
|
|
191
|
+
* auxiliary profile's native inline-comment syntax (#586). This is the
|
|
192
|
+
* shared helper `tools/lsp-diagnostics.ts` and `clients/lsp/index.ts`'s
|
|
193
|
+
* `runWorkspaceDiagnostics` use so a `// nosemgrep` (or any future profile's
|
|
194
|
+
* equivalent) suppresses a finding identically whether it's seen via the
|
|
195
|
+
* per-edit dispatch runner or a standalone diagnostics query — previously
|
|
196
|
+
* only the former honored it (#586).
|
|
197
|
+
*/
|
|
198
|
+
export function applyAuxiliarySuppressions(diagnostics, content) {
|
|
199
|
+
return diagnostics.filter((d) => !isAuxiliaryDiagnosticSuppressed(d, content));
|
|
144
200
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findOwnerName } from "../../symbol-containment.js";
|
|
1
2
|
import { extractFactsFromTree, firstChildOfType, walk, } from "./tree-sitter-facts.js";
|
|
2
3
|
const BOUNDARY_PREFIXES = [
|
|
3
4
|
"fetch",
|
|
@@ -160,6 +161,62 @@ function collectOutgoingCalls(body) {
|
|
|
160
161
|
});
|
|
161
162
|
return [...calls];
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* `obj.method()` call sites with a simple-identifier receiver (refs #655
|
|
166
|
+
* phase 2). Only the plain shape `identifier.identifier(...)` is captured —
|
|
167
|
+
* chained (`a.b.c()`), computed (`a[x]()`), and `this.`-receiver calls are
|
|
168
|
+
* left out of this list (they still show up in `outgoingCalls`'s flattened
|
|
169
|
+
* text form as before); those richer shapes need real type inference to
|
|
170
|
+
* resolve safely, which is out of scope for this bounded slice.
|
|
171
|
+
*/
|
|
172
|
+
function collectMemberCallSites(body) {
|
|
173
|
+
const sites = [];
|
|
174
|
+
walk(body, (node) => {
|
|
175
|
+
if (node.type !== "call_expression")
|
|
176
|
+
return;
|
|
177
|
+
const callee = (node.children ?? [])[0];
|
|
178
|
+
if (!callee || callee.type !== "member_expression")
|
|
179
|
+
return;
|
|
180
|
+
const object = (callee.children ?? [])[0];
|
|
181
|
+
const property = (callee.children ?? []).find((c) => c?.type === "property_identifier");
|
|
182
|
+
if (!object || object.type !== "identifier" || !property)
|
|
183
|
+
return;
|
|
184
|
+
sites.push({ receiver: object.text, method: property.text });
|
|
185
|
+
});
|
|
186
|
+
return sites;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Best-effort receiver -> class-name map for one function (refs #655 phase
|
|
190
|
+
* 2). Two clear, common shapes only — see {@link FunctionSummary.receiverTypes}.
|
|
191
|
+
*/
|
|
192
|
+
function collectReceiverTypes(body, params) {
|
|
193
|
+
const types = {};
|
|
194
|
+
for (const param of params) {
|
|
195
|
+
const id = firstNamedChild(param);
|
|
196
|
+
if (!id || id.type !== "identifier")
|
|
197
|
+
continue;
|
|
198
|
+
const typeAnnotation = (param.children ?? []).find((c) => c?.type === "type_annotation");
|
|
199
|
+
const typeId = typeAnnotation
|
|
200
|
+
? firstChildOfType(typeAnnotation, "type_identifier")
|
|
201
|
+
: undefined;
|
|
202
|
+
if (typeId)
|
|
203
|
+
types[id.text] = typeId.text;
|
|
204
|
+
}
|
|
205
|
+
walk(body, (node) => {
|
|
206
|
+
if (node.type !== "variable_declarator")
|
|
207
|
+
return;
|
|
208
|
+
const id = firstChildOfType(node, "identifier");
|
|
209
|
+
if (!id)
|
|
210
|
+
return;
|
|
211
|
+
const value = (node.children ?? []).find((c) => c?.type === "new_expression");
|
|
212
|
+
if (!value)
|
|
213
|
+
return;
|
|
214
|
+
const ctor = firstNamedChild(value);
|
|
215
|
+
if (ctor?.type === "identifier")
|
|
216
|
+
types[id.text] = ctor.text;
|
|
217
|
+
});
|
|
218
|
+
return types;
|
|
219
|
+
}
|
|
163
220
|
function hasAwaitInNode(node) {
|
|
164
221
|
let found = false;
|
|
165
222
|
walk(node, (n) => {
|
|
@@ -192,6 +249,13 @@ export const functionFactProvider = {
|
|
|
192
249
|
async run(ctx, store) {
|
|
193
250
|
await extractFactsFromTree(ctx, store, { "file.functionSummaries": [] }, (root) => {
|
|
194
251
|
const summaries = [];
|
|
252
|
+
// Class/interface declarations, collected in the SAME walk over the
|
|
253
|
+
// already-parsed tree (refs #655 phase 2) so owner/qualified-name
|
|
254
|
+
// computation for jsts needs no second parse — see
|
|
255
|
+
// `symbol-containment.ts`'s doc comment for why this can't literally
|
|
256
|
+
// share code with module-report.ts's tree-sitter-symbol-extractor path
|
|
257
|
+
// (different tree-sitter integration), only the same algorithm.
|
|
258
|
+
const containers = [];
|
|
195
259
|
const addSummary = (node) => {
|
|
196
260
|
const body = getBody(node);
|
|
197
261
|
if (!body)
|
|
@@ -210,6 +274,7 @@ export const functionFactProvider = {
|
|
|
210
274
|
name: getFunctionName(node),
|
|
211
275
|
line: node.startPosition.row + 1,
|
|
212
276
|
column: node.startPosition.column + 1,
|
|
277
|
+
endLine: body.endPosition.row + 1,
|
|
213
278
|
isAsync: Boolean(firstChildOfType(node, "async")),
|
|
214
279
|
hasAwait: hasAwaitInNode(body),
|
|
215
280
|
hasReturnAwaitCall: hasReturnAwaitCall(body),
|
|
@@ -221,12 +286,30 @@ export const functionFactProvider = {
|
|
|
221
286
|
cyclomaticComplexity: calcCyclomaticComplexity(body),
|
|
222
287
|
maxNestingDepth: calcMaxNestingDepth(body),
|
|
223
288
|
outgoingCalls: collectOutgoingCalls(body),
|
|
289
|
+
memberCallSites: collectMemberCallSites(body),
|
|
290
|
+
receiverTypes: collectReceiverTypes(body, params),
|
|
224
291
|
});
|
|
225
292
|
};
|
|
226
293
|
walk(root, (node) => {
|
|
227
294
|
if (FUNCTION_TYPES.has(node.type))
|
|
228
295
|
addSummary(node);
|
|
296
|
+
if (node.type === "class_declaration" ||
|
|
297
|
+
node.type === "interface_declaration") {
|
|
298
|
+
const nameNode = firstChildOfType(node, "type_identifier");
|
|
299
|
+
if (nameNode) {
|
|
300
|
+
containers.push({
|
|
301
|
+
name: nameNode.text,
|
|
302
|
+
startLine: node.startPosition.row + 1,
|
|
303
|
+
endLine: node.endPosition.row + 1,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
229
307
|
});
|
|
308
|
+
for (const summary of summaries) {
|
|
309
|
+
const owner = findOwnerName(containers, summary.line, summary.endLine ?? summary.line);
|
|
310
|
+
if (owner)
|
|
311
|
+
summary.owner = owner;
|
|
312
|
+
}
|
|
230
313
|
return { "file.functionSummaries": summaries };
|
|
231
314
|
});
|
|
232
315
|
},
|
|
@@ -12,7 +12,7 @@ export const asyncUnnecessaryWrapperRule = {
|
|
|
12
12
|
continue;
|
|
13
13
|
diagnostics.push({
|
|
14
14
|
id: `async-unnecessary-wrapper:${ctx.filePath}:${f.line}`,
|
|
15
|
-
tool: "
|
|
15
|
+
tool: "fact-rules",
|
|
16
16
|
rule: "async-unnecessary-wrapper",
|
|
17
17
|
filePath: ctx.filePath,
|
|
18
18
|
line: f.line,
|
|
@@ -16,7 +16,8 @@ export const errorObscuringRule = {
|
|
|
16
16
|
!s.bodyText.includes(s.catchParam)) {
|
|
17
17
|
diagnostics.push({
|
|
18
18
|
id: `error-obscuring:${ctx.filePath}:${s.line}:${s.column}`,
|
|
19
|
-
tool: "
|
|
19
|
+
tool: "fact-rules",
|
|
20
|
+
rule: "error-obscuring",
|
|
20
21
|
filePath: ctx.filePath,
|
|
21
22
|
line: s.line,
|
|
22
23
|
column: s.column,
|
|
@@ -13,7 +13,8 @@ export const errorSwallowingRule = {
|
|
|
13
13
|
if (s.isEmpty && !s.isDocumentedLocalFallback && s.boundaryCategory !== "fs") {
|
|
14
14
|
diagnostics.push({
|
|
15
15
|
id: `error-swallowing:${ctx.filePath}:${s.line}:${s.column}`,
|
|
16
|
-
tool: "
|
|
16
|
+
tool: "fact-rules",
|
|
17
|
+
rule: "error-swallowing",
|
|
17
18
|
filePath: ctx.filePath,
|
|
18
19
|
line: s.line,
|
|
19
20
|
column: s.column,
|