pi-lens 3.8.65 → 3.8.67
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 +59 -0
- package/README.md +0 -1
- package/dist/clients/actionable-warnings-logger.js +11 -36
- package/dist/clients/ast-grep-tool-logger.js +11 -36
- package/dist/clients/biome-client.js +10 -2
- package/dist/clients/bootstrap.js +1 -3
- package/dist/clients/cascade-logger.js +7 -12
- package/dist/clients/complexity-client.js +397 -623
- package/dist/clients/dead-code-logger.js +11 -22
- package/dist/clients/dependency-checker.js +15 -2
- package/dist/clients/deps/vscode-jsonrpc.js +1 -1
- package/dist/clients/diagnostic-logger.js +7 -31
- package/dist/clients/dispatch/auxiliary-lsp.js +38 -0
- package/dist/clients/dispatch/dispatcher.js +4 -39
- package/dist/clients/dispatch/fact-runner.js +9 -57
- package/dist/clients/dispatch/facts/comment-facts.js +12 -25
- package/dist/clients/dispatch/facts/function-facts.js +161 -132
- package/dist/clients/dispatch/facts/import-facts.js +164 -117
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +34 -0
- package/dist/clients/dispatch/facts/try-catch-facts.js +62 -68
- package/dist/clients/dispatch/inline-suppressions.js +61 -0
- package/dist/clients/dispatch/integration.js +94 -32
- package/dist/clients/dispatch/plan.js +0 -49
- package/dist/clients/dispatch/priorities.js +0 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +54 -0
- package/dist/clients/dispatch/rules/high-import-coupling.js +37 -0
- package/dist/clients/dispatch/rules/no-commented-credentials.js +52 -0
- package/dist/clients/dispatch/runners/index.js +2 -7
- package/dist/clients/dispatch/runners/lsp.js +18 -6
- package/dist/clients/dispatch/runners/tree-sitter.js +11 -64
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +28 -33
- package/dist/clients/dispatch/tool-profile.js +0 -1
- package/dist/clients/dispatch/types.js +1 -1
- package/dist/clients/formatters.js +10 -0
- package/dist/clients/git-guard.js +1 -1
- package/dist/clients/grammar-source.js +65 -2
- package/dist/clients/jscpd-client.js +8 -2
- package/dist/clients/language-policy.js +2 -2
- package/dist/clients/latency-logger.js +10 -15
- package/dist/clients/lsp/client.js +74 -11
- package/dist/clients/lsp/config.js +61 -3
- package/dist/clients/lsp/index.js +43 -3
- package/dist/clients/lsp/launch.js +3 -32
- package/dist/clients/mcp/session.js +0 -1
- package/dist/clients/module-report.js +16 -5
- package/dist/clients/ndjson-logger.js +150 -0
- package/dist/clients/package-manager.js +65 -0
- package/dist/clients/pipeline.js +25 -41
- package/dist/clients/project-diagnostics/extractors.js +85 -0
- package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +44 -0
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +25 -0
- package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +34 -0
- package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +38 -0
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +47 -0
- package/dist/clients/project-diagnostics/runner-adapters/trivy.js +31 -0
- package/dist/clients/project-diagnostics/scanner.js +11 -10
- package/dist/clients/read-guard-logger.js +11 -36
- package/dist/clients/review-graph/builder.js +337 -27
- package/dist/clients/review-graph/git-identity.js +150 -0
- package/dist/clients/review-graph/service.js +3 -3
- package/dist/clients/runtime-coordinator.js +83 -0
- package/dist/clients/runtime-session.js +28 -2
- package/dist/clients/runtime-tool-result.js +10 -2
- package/dist/clients/runtime-turn.js +25 -7
- package/dist/clients/sg-runner.js +15 -0
- package/dist/clients/test-runner-client.js +12 -6
- package/dist/clients/tree-sitter-cache.js +47 -14
- package/dist/clients/tree-sitter-client.js +9 -1
- package/dist/clients/tree-sitter-logger.js +7 -12
- package/dist/clients/tree-sitter-query-loader.js +1 -0
- package/dist/clients/tree-sitter-shared.js +113 -0
- package/dist/clients/tree-sitter-symbol-extractor.js +19 -12
- package/dist/index.js +4 -22
- package/dist/tools/lens-diagnostics.js +254 -20
- package/grammars/tree-sitter-yaml.wasm +0 -0
- package/grammars/tree-sitter-yaml.wasm.json +3 -3
- package/package.json +4 -3
- package/rules/ast-grep-rules/rule-tests/no-init-return-test.yml +15 -0
- package/rules/ast-grep-rules/rules/no-init-return.yml +11 -5
- package/rules/tree-sitter-queries/python/python-assert-production.yml +4 -0
- package/scripts/download-grammars.js +29 -4
- package/scripts/grammars.lock.json +14 -2
- package/scripts/install-selftest.mjs +8 -7
- package/dist/clients/deps/typescript.js +0 -15
- package/dist/clients/dispatch/rules/quality-rules.js +0 -297
- package/dist/clients/dispatch/rules/sonar-rules.js +0 -493
- package/dist/clients/dispatch/runners/biome.js +0 -69
- package/dist/clients/dispatch/runners/python-slop.js +0 -106
- package/dist/clients/dispatch/runners/ts-lsp.js +0 -109
- package/dist/clients/runner-tracker.js +0 -153
- package/dist/clients/ts-service.js +0 -130
- package/dist/clients/type-coverage-client.js +0 -128
- package/dist/clients/typescript-client.js +0 -509
- package/dist/commands/booboo.js +0 -1412
- package/rules/python-slop-rules/.sgconfig.yml +0 -4
- package/rules/python-slop-rules/rules/slop-rules.yml +0 -647
|
@@ -7,22 +7,14 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { RuleCache } from "../../cache/rule-cache.js";
|
|
10
|
+
import { isTestFile } from "../../file-utils.js";
|
|
10
11
|
import { resolvePackagePath } from "../../package-root.js";
|
|
11
12
|
import { buildOrUpdateGraph, computeImpactCascade, recordEntitySnapshotDiff, } from "../../review-graph/service.js";
|
|
12
|
-
import {
|
|
13
|
+
import { getSharedTreeSitterClient, isTreeSitterWasmAborted, markTreeSitterWasmAborted, resolveTreeSitterLanguage, } from "../../tree-sitter-shared.js";
|
|
13
14
|
import { logTreeSitter } from "../../tree-sitter-logger.js";
|
|
14
15
|
import { isDisabledQueryFilePath, queryLoader, } from "../../tree-sitter-query-loader.js";
|
|
15
16
|
import { classifyDefect } from "../diagnostic-taxonomy.js";
|
|
16
17
|
import { PRIORITY } from "../priorities.js";
|
|
17
|
-
// Module-level singleton: web-tree-sitter WASM must only be initialized once per process.
|
|
18
|
-
// Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
|
|
19
|
-
// WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
|
|
20
|
-
let _sharedClient = null;
|
|
21
|
-
// Once the wasm runtime aborts, the entire module-level wasm heap is corrupted — no
|
|
22
|
-
// recovery is possible within this process. Flag it and skip all further tree-sitter work
|
|
23
|
-
// rather than re-invoking the dead runtime (which prints "Aborted()" on every call and
|
|
24
|
-
// leaks memory on each retry).
|
|
25
|
-
let _wasmAborted = false;
|
|
26
18
|
const blastCooldownByFile = new Map();
|
|
27
19
|
const BLAST_COOLDOWN_MS = 5_000;
|
|
28
20
|
function runBlastRadiusInBackground(cwd, filePath, languageId, facts) {
|
|
@@ -286,14 +278,6 @@ function isLineInModifiedRanges(line, ranges) {
|
|
|
286
278
|
return true;
|
|
287
279
|
return ranges.some((r) => line >= r.start && line <= r.end);
|
|
288
280
|
}
|
|
289
|
-
function getSharedClient() {
|
|
290
|
-
if (_wasmAborted)
|
|
291
|
-
return null;
|
|
292
|
-
if (!_sharedClient) {
|
|
293
|
-
_sharedClient = new TreeSitterClient();
|
|
294
|
-
}
|
|
295
|
-
return _sharedClient;
|
|
296
|
-
}
|
|
297
281
|
/**
|
|
298
282
|
* Calculate total lines changed in modified ranges.
|
|
299
283
|
* Used to skip expensive entity extraction for trivial changes.
|
|
@@ -305,46 +289,6 @@ function getTotalLinesChanged(ranges) {
|
|
|
305
289
|
}
|
|
306
290
|
/** Threshold: skip entity extraction for changes under 5 lines */
|
|
307
291
|
const ENTITY_EXTRACTION_LINE_THRESHOLD = 5;
|
|
308
|
-
function resolveTreeSitterLanguage(filePath) {
|
|
309
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
310
|
-
const EXT_TO_LANG = {
|
|
311
|
-
".ts": "typescript",
|
|
312
|
-
".mts": "typescript",
|
|
313
|
-
".cts": "typescript",
|
|
314
|
-
".tsx": "tsx",
|
|
315
|
-
".js": "javascript",
|
|
316
|
-
".mjs": "javascript",
|
|
317
|
-
".cjs": "javascript",
|
|
318
|
-
".jsx": "javascript",
|
|
319
|
-
".py": "python",
|
|
320
|
-
".go": "go",
|
|
321
|
-
".rs": "rust",
|
|
322
|
-
".rb": "ruby",
|
|
323
|
-
".c": "c",
|
|
324
|
-
".h": "c",
|
|
325
|
-
".cc": "cpp",
|
|
326
|
-
".cpp": "cpp",
|
|
327
|
-
".cxx": "cpp",
|
|
328
|
-
".c++": "cpp",
|
|
329
|
-
".hh": "cpp",
|
|
330
|
-
".hpp": "cpp",
|
|
331
|
-
".hxx": "cpp",
|
|
332
|
-
".inl": "cpp",
|
|
333
|
-
".ipp": "cpp",
|
|
334
|
-
".tpp": "cpp",
|
|
335
|
-
".txx": "cpp",
|
|
336
|
-
".cu": "cpp",
|
|
337
|
-
".hip": "cpp",
|
|
338
|
-
".cs": "csharp",
|
|
339
|
-
".php": "php",
|
|
340
|
-
".phtml": "php",
|
|
341
|
-
".php3": "php",
|
|
342
|
-
".php4": "php",
|
|
343
|
-
".php5": "php",
|
|
344
|
-
".css": "css",
|
|
345
|
-
};
|
|
346
|
-
return EXT_TO_LANG[ext];
|
|
347
|
-
}
|
|
348
292
|
const treeSitterRunner = {
|
|
349
293
|
id: "tree-sitter",
|
|
350
294
|
appliesTo: ["jsts", "python", "go", "rust", "ruby", "cxx", "csharp", "php", "css"],
|
|
@@ -353,13 +297,13 @@ const treeSitterRunner = {
|
|
|
353
297
|
skipTestFiles: false, // Run on test files too (structural issues matter there)
|
|
354
298
|
async run(ctx) {
|
|
355
299
|
// Use singleton client — WASM must never be re-initialized after first call
|
|
356
|
-
const client =
|
|
300
|
+
const client = getSharedTreeSitterClient();
|
|
357
301
|
logTreeSitter({ phase: "runner_start", filePath: ctx.filePath });
|
|
358
302
|
if (!client || !client.isAvailable()) {
|
|
359
303
|
logTreeSitter({
|
|
360
304
|
phase: "runner_skip",
|
|
361
305
|
filePath: ctx.filePath,
|
|
362
|
-
reason:
|
|
306
|
+
reason: isTreeSitterWasmAborted() ? "wasm_aborted" : "client_unavailable",
|
|
363
307
|
status: "skipped",
|
|
364
308
|
});
|
|
365
309
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -460,9 +404,13 @@ const treeSitterRunner = {
|
|
|
460
404
|
// Run all queries regardless of blockingOnly — warning-tier results are logged
|
|
461
405
|
// for diagnostic history but filtered from agent output by the dispatcher.
|
|
462
406
|
// Only skip "review" tier queries on write (too noisy / expensive).
|
|
463
|
-
|
|
407
|
+
// Per-rule test-file carve-out (#440): rules that are noise in tests (e.g.
|
|
408
|
+
// python-assert-production — `assert` is the idiomatic test assertion) opt
|
|
409
|
+
// out via `skip_test_files` while the runner otherwise runs on test files.
|
|
410
|
+
const fileIsTest = isTestFile(filePath);
|
|
411
|
+
const effectiveQueries = (ctx.blockingOnly
|
|
464
412
|
? languageQueries.filter((q) => q.inline_tier !== "review")
|
|
465
|
-
: languageQueries;
|
|
413
|
+
: languageQueries).filter((q) => !(fileIsTest && q.skip_test_files));
|
|
466
414
|
logTreeSitter({
|
|
467
415
|
phase: "queries_loaded",
|
|
468
416
|
filePath,
|
|
@@ -547,8 +495,7 @@ const treeSitterRunner = {
|
|
|
547
495
|
// Emscripten abort() corrupts the entire module-level wasm heap.
|
|
548
496
|
// Poison the singleton so no further queries attempt to use the dead runtime.
|
|
549
497
|
if (msg.includes("Aborted") || msg.includes("abort()")) {
|
|
550
|
-
|
|
551
|
-
_sharedClient = null;
|
|
498
|
+
markTreeSitterWasmAborted();
|
|
552
499
|
logTreeSitter({
|
|
553
500
|
phase: "query_error",
|
|
554
501
|
filePath,
|
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
import { getGlobalPiLensDir } from "../../../file-utils.js";
|
|
13
13
|
import { ensureTool } from "../../../installer/index.js";
|
|
14
14
|
import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/config.js";
|
|
15
|
+
import { findGlobalBinary } from "../../../package-manager.js";
|
|
15
16
|
import { safeSpawnAsync } from "../../../safe-spawn.js";
|
|
16
17
|
import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
|
|
17
18
|
/**
|
|
@@ -257,34 +258,6 @@ export async function resolveAvailableOrInstall(checker, toolId, cwd) {
|
|
|
257
258
|
return installed ?? null;
|
|
258
259
|
}
|
|
259
260
|
// =============================================================================
|
|
260
|
-
// CONFIG FILE FINDER FACTORY
|
|
261
|
-
// =============================================================================
|
|
262
|
-
/**
|
|
263
|
-
* Create a config file finder for rule directories.
|
|
264
|
-
* Common pattern used by slop runners and similar tools.
|
|
265
|
-
*/
|
|
266
|
-
export function createConfigFinder(ruleDirName) {
|
|
267
|
-
return (cwd) => {
|
|
268
|
-
// Check for local config first
|
|
269
|
-
const localPath = path.join(cwd, "rules", ruleDirName, ".sgconfig.yml");
|
|
270
|
-
if (fs.existsSync(localPath)) {
|
|
271
|
-
return localPath;
|
|
272
|
-
}
|
|
273
|
-
// Fall back to extension rules
|
|
274
|
-
const extensionPaths = [
|
|
275
|
-
`rules/${ruleDirName}/.sgconfig.yml`,
|
|
276
|
-
`../rules/${ruleDirName}/.sgconfig.yml`,
|
|
277
|
-
];
|
|
278
|
-
for (const candidate of extensionPaths) {
|
|
279
|
-
const fullPath = path.resolve(cwd, candidate);
|
|
280
|
-
if (fs.existsSync(fullPath)) {
|
|
281
|
-
return fullPath;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return undefined;
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
// =============================================================================
|
|
288
261
|
// SHARED AST-GREP AVAILABILITY
|
|
289
262
|
// =============================================================================
|
|
290
263
|
// Shared ast-grep availability cache across all slop runners
|
|
@@ -354,6 +327,17 @@ export async function isSgAvailableAsync() {
|
|
|
354
327
|
return true;
|
|
355
328
|
}
|
|
356
329
|
}
|
|
330
|
+
// 2b. Any package manager's global bin dir (npm/pnpm/yarn/bun) — catches
|
|
331
|
+
// `pnpm add -g @ast-grep/cli` installs whose bin dir is off PATH (#375).
|
|
332
|
+
for (const name of ["ast-grep", "sg"]) {
|
|
333
|
+
const globalBin = await findGlobalBinary(name);
|
|
334
|
+
if (globalBin && (await probeAstGrepCommandAsync(globalBin))) {
|
|
335
|
+
sgCmd = globalBin;
|
|
336
|
+
sgCmdArgs = [];
|
|
337
|
+
sgAvailable = true;
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
357
341
|
// 3. npx --no (cache-only, no silent download).
|
|
358
342
|
if (await probeAstGrepCommandAsync("npx", ["--no", "--", "ast-grep"])) {
|
|
359
343
|
sgCmd = "npx";
|
|
@@ -378,10 +362,14 @@ export function getSgCommand() {
|
|
|
378
362
|
// LOCAL-FIRST BINARY RESOLUTION
|
|
379
363
|
// =============================================================================
|
|
380
364
|
/**
|
|
381
|
-
* Find a tool binary preferring local node_modules/.bin
|
|
382
|
-
*
|
|
365
|
+
* Find a tool binary preferring local node_modules/.bin, then any installed
|
|
366
|
+
* package manager's global bin dir (npm/pnpm/yarn/bun), then global PATH. Only
|
|
367
|
+
* falls back to `npx --no` as a last resort — the universal cache-only exec
|
|
368
|
+
* (npx ships with node and never silently downloads), so this stays
|
|
369
|
+
* manager-agnostic without risking a surprise `dlx` fetch on pnpm/yarn/bun.
|
|
383
370
|
*
|
|
384
|
-
* Returns: { cmd, args } where args may include ["
|
|
371
|
+
* Returns: { cmd, args } where args may include the `["--no", toolName]` npx
|
|
372
|
+
* preamble.
|
|
385
373
|
*/
|
|
386
374
|
export async function resolveLocalFirstAsync(toolName, cwd, windowsExt = ".cmd") {
|
|
387
375
|
const isWin = process.platform === "win32";
|
|
@@ -390,14 +378,21 @@ export async function resolveLocalFirstAsync(toolName, cwd, windowsExt = ".cmd")
|
|
|
390
378
|
const local = path.join(cwd, "node_modules", ".bin", binName);
|
|
391
379
|
if (fs.existsSync(local))
|
|
392
380
|
return { cmd: local, args: [] };
|
|
393
|
-
// 2. Global
|
|
381
|
+
// 2. Global bin dir of ANY installed manager (npm/pnpm/yarn/bun) — direct
|
|
382
|
+
// file lookup, so it finds tools installed via `pnpm add -g` / `bun add -g`
|
|
383
|
+
// (whose bin dirs are often off PATH) and survives PATH staleness after an
|
|
384
|
+
// `install -g`. No spawn.
|
|
385
|
+
const globalBin = await findGlobalBinary(toolName, windowsExt);
|
|
386
|
+
if (globalBin)
|
|
387
|
+
return { cmd: globalBin, args: [] };
|
|
388
|
+
// 3. Global PATH (already installed system-wide, on PATH)
|
|
394
389
|
const globalCheck = await safeSpawnAsync(toolName, ["--version"], {
|
|
395
390
|
timeout: 3000,
|
|
396
391
|
});
|
|
397
392
|
if (!globalCheck.error && globalCheck.status === 0) {
|
|
398
393
|
return { cmd: toolName, args: [] };
|
|
399
394
|
}
|
|
400
|
-
//
|
|
395
|
+
// 4. npx --no fallback — universal cache-only exec (no silent download)
|
|
401
396
|
return { cmd: "npx", args: ["--no", toolName] };
|
|
402
397
|
}
|
|
403
398
|
// =============================================================================
|
|
@@ -5,7 +5,6 @@ const DEFAULT_TOOL_PROFILE = {
|
|
|
5
5
|
const TOOL_PROFILE_MAP = {
|
|
6
6
|
"tree-sitter:silent-error": { dedupPriority: 200, lintLike: false },
|
|
7
7
|
lsp: { dedupPriority: 120, lintLike: false },
|
|
8
|
-
"ts-lsp": { dedupPriority: 120, lintLike: false },
|
|
9
8
|
eslint: { dedupPriority: 110, lintLike: true },
|
|
10
9
|
biome: { dedupPriority: 100, lintLike: true },
|
|
11
10
|
"biome-check-json": { dedupPriority: 100, lintLike: true },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Redesigned Dispatch Types for pi-lens
|
|
3
3
|
*
|
|
4
4
|
* Key insight: Different clients have different OUTPUT SEMANTICS:
|
|
5
|
-
* - BLOCKING: Errors that stop the agent (architect,
|
|
5
|
+
* - BLOCKING: Errors that stop the agent (architect, lsp errors)
|
|
6
6
|
* - WARNING: Non-blocking issues (biome warnings, type-safety)
|
|
7
7
|
* - FIXABLE: Issues with auto-fix available
|
|
8
8
|
* - SILENT: Metrics tracked but not shown (complexity)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from "node:fs/promises";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { logLatency } from "./latency-logger.js";
|
|
14
|
+
import { findGlobalBinary } from "./package-manager.js";
|
|
14
15
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
15
16
|
import { getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOxfmtConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasVitePlusConfig, } from "./tool-policy.js";
|
|
16
17
|
const _lazyInstallAttempts = new Set();
|
|
@@ -251,6 +252,11 @@ export const biomeFormatter = {
|
|
|
251
252
|
const local = await findInNodeModules("biome", cwd);
|
|
252
253
|
if (local)
|
|
253
254
|
return [local, "format", "--write", ...editorConfigFlag, filePath];
|
|
255
|
+
// Any package manager's global bin dir (npm/pnpm/yarn/bun) before we
|
|
256
|
+
// auto-install — catches a `pnpm add -g @biomejs/biome` PATH misses (#375).
|
|
257
|
+
const global = await findGlobalBinary("biome");
|
|
258
|
+
if (global)
|
|
259
|
+
return [global, "format", "--write", ...editorConfigFlag, filePath];
|
|
254
260
|
const toolId = getAutoInstallToolIdForFormatter("biome");
|
|
255
261
|
if (!toolId)
|
|
256
262
|
return null;
|
|
@@ -291,6 +297,10 @@ export const prettierFormatter = {
|
|
|
291
297
|
const local = await findInNodeModules("prettier", cwd);
|
|
292
298
|
if (local)
|
|
293
299
|
return [local, "--write", filePath];
|
|
300
|
+
// Global bin of any manager (npm/pnpm/yarn/bun) before auto-install (#375).
|
|
301
|
+
const global = await findGlobalBinary("prettier");
|
|
302
|
+
if (global)
|
|
303
|
+
return [global, "--write", filePath];
|
|
294
304
|
return resolveManagedSmartDefaultCommand("prettier", filePath, ["--write"]);
|
|
295
305
|
},
|
|
296
306
|
extensions: [
|
|
@@ -27,6 +27,6 @@ export function evaluateGitGuard(runtime, cacheManager, cwd) {
|
|
|
27
27
|
: "";
|
|
28
28
|
return {
|
|
29
29
|
block: true,
|
|
30
|
-
reason: `🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.${details}\nRun
|
|
30
|
+
reason: `🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.${details}\nRun lens_diagnostics mode=all for full details, then commit again.`,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
@@ -15,6 +15,22 @@ import * as path from "node:path";
|
|
|
15
15
|
export const TREE_SITTER_WASMS_VERSION = "0.1.13";
|
|
16
16
|
/** unpkg mirror of the tree-sitter-wasms artifacts. */
|
|
17
17
|
export const GRAMMAR_CDN_BASE = `https://unpkg.com/tree-sitter-wasms@${TREE_SITTER_WASMS_VERSION}/out`;
|
|
18
|
+
export const GRAMMAR_SOURCE_OVERRIDES = {
|
|
19
|
+
"tree-sitter-lua.wasm": {
|
|
20
|
+
package: "@tree-sitter-grammars/tree-sitter-lua",
|
|
21
|
+
version: "0.4.1",
|
|
22
|
+
url: "https://unpkg.com/@tree-sitter-grammars/tree-sitter-lua@0.4.1/tree-sitter-lua.wasm",
|
|
23
|
+
},
|
|
24
|
+
"tree-sitter-yaml.wasm": {
|
|
25
|
+
package: "@tree-sitter-grammars/tree-sitter-yaml",
|
|
26
|
+
version: "0.7.1",
|
|
27
|
+
url: "https://unpkg.com/@tree-sitter-grammars/tree-sitter-yaml@0.7.1/tree-sitter-yaml.wasm",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
/** The URL a grammar wasm is fetched from (override if any, else the aggregator). */
|
|
31
|
+
export function grammarSourceUrl(filename) {
|
|
32
|
+
return GRAMMAR_SOURCE_OVERRIDES[filename]?.url ?? `${GRAMMAR_CDN_BASE}/${filename}`;
|
|
33
|
+
}
|
|
18
34
|
/** Language id → grammar wasm filename. */
|
|
19
35
|
export const LANGUAGE_TO_GRAMMAR = {
|
|
20
36
|
typescript: "tree-sitter-typescript.wasm",
|
|
@@ -48,15 +64,62 @@ export const LANGUAGE_TO_GRAMMAR = {
|
|
|
48
64
|
export const GRAMMAR_FILES = [
|
|
49
65
|
...new Set(Object.values(LANGUAGE_TO_GRAMMAR)),
|
|
50
66
|
];
|
|
67
|
+
/**
|
|
68
|
+
* Grammars that FATALLY crash the host runtime on specific engines/versions and
|
|
69
|
+
* therefore must NOT be loaded there. The crash is an uncatchable process abort
|
|
70
|
+
* (V8 Turboshaft WASM OOM — `Fatal process out of memory: Zone`), so it cannot be
|
|
71
|
+
* degraded in-process with try/catch; the only safe option is to skip the load
|
|
72
|
+
* entirely and degrade the feature (no structural symbols for that language).
|
|
73
|
+
*
|
|
74
|
+
* Membership is GUARD-DRIVEN, not hand-maintained: the grammar-health nightly
|
|
75
|
+
* (`scripts/check-grammar-load.mjs`) is what proves a grammar crashes and thus
|
|
76
|
+
* belongs here, and what proves a future build is safe enough to remove it.
|
|
77
|
+
*
|
|
78
|
+
* tree-sitter-swift crashes on Node >= 24 across all OSes under memory pressure
|
|
79
|
+
* (#423/#432); building from source (an earlier vendoring attempt, #426) does not
|
|
80
|
+
* reliably dodge it. bun (JavaScriptCore) and Node 20/22 are unaffected, so the
|
|
81
|
+
* block is gated on V8 + Node major.
|
|
82
|
+
*/
|
|
83
|
+
export const BLOCKED_GRAMMARS = {
|
|
84
|
+
"tree-sitter-swift.wasm": {
|
|
85
|
+
blocked: ({ isV8, nodeMajor }) => isV8 && nodeMajor >= 24,
|
|
86
|
+
reason: "tree-sitter-swift crashes the runtime on Node >= 24 (V8 Turboshaft WASM OOM, #423/#432); skipped so the process degrades gracefully instead of aborting.",
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/** Runtime signals for the currently-running process. */
|
|
90
|
+
export function currentGrammarRuntime() {
|
|
91
|
+
const m = /^v?(\d+)/.exec(process.versions?.node ?? "");
|
|
92
|
+
return {
|
|
93
|
+
nodeMajor: m ? Number(m[1]) : 0,
|
|
94
|
+
isV8: !process.versions.bun && Boolean(process.versions.v8),
|
|
95
|
+
platform: process.platform,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The block reason if `filename` must NOT be loaded on `rt` (default: the running
|
|
100
|
+
* process), else null. Callers skip the load and degrade to "grammar unavailable".
|
|
101
|
+
*/
|
|
102
|
+
export function grammarBlockReason(filename, rt = currentGrammarRuntime()) {
|
|
103
|
+
// Diagnostic escape hatch (grammar-health probe only): force-load a blocked
|
|
104
|
+
// grammar to test whether a fresh build / newer runtime survives — the signal
|
|
105
|
+
// that a block can be LIFTED. Never set in normal operation: it disables the
|
|
106
|
+
// crash protection and can abort the process.
|
|
107
|
+
if (process.env.PILENS_UNSAFE_FORCE_GRAMMAR_LOAD === "1")
|
|
108
|
+
return null;
|
|
109
|
+
const block = BLOCKED_GRAMMARS[filename];
|
|
110
|
+
return block?.blocked(rt) ? block.reason : null;
|
|
111
|
+
}
|
|
51
112
|
/**
|
|
52
113
|
* Fetch one grammar wasm into `destDir` (atomic via a temp file). Returns true
|
|
53
114
|
* on success. Never throws — a failed fetch (offline, 4xx) degrades to "grammar
|
|
54
|
-
* unavailable" so callers can decide how to handle it.
|
|
115
|
+
* unavailable" so callers can decide how to handle it. A grammar that crashes the
|
|
116
|
+
* runtime is protected at LOAD time (BLOCKED_GRAMMARS / grammarBlockReason), not
|
|
117
|
+
* by refusing to download it.
|
|
55
118
|
*/
|
|
56
119
|
export async function downloadGrammar(destDir, filename) {
|
|
57
120
|
try {
|
|
58
121
|
fs.mkdirSync(destDir, { recursive: true });
|
|
59
|
-
const res = await fetch(
|
|
122
|
+
const res = await fetch(grammarSourceUrl(filename));
|
|
60
123
|
if (!res.ok)
|
|
61
124
|
throw new Error(`HTTP ${res.status}`);
|
|
62
125
|
const data = Buffer.from(await res.arrayBuffer());
|
|
@@ -12,6 +12,7 @@ import { mkdtempSync } from "node:fs";
|
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { getExcludedDirGlobs, getProjectIgnoreGlobs, getProjectIgnoreMatcher, isExcludedDirName, } from "./file-utils.js";
|
|
15
|
+
import { findNodeToolBinary } from "./package-manager.js";
|
|
15
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
16
17
|
const EMPTY_RESULT = {
|
|
17
18
|
success: false,
|
|
@@ -192,8 +193,13 @@ export class JscpdClient {
|
|
|
192
193
|
}
|
|
193
194
|
const ignorePattern = baseIgnores.join(",");
|
|
194
195
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
// Prefer a local/global-installed jscpd (any manager) over npx (#375).
|
|
197
|
+
const bin = await findNodeToolBinary("jscpd", cwd);
|
|
198
|
+
const { cmd, prefix } = bin
|
|
199
|
+
? { cmd: bin, prefix: [] }
|
|
200
|
+
: { cmd: "npx", prefix: ["jscpd"] };
|
|
201
|
+
const result = await safeSpawnAsync(cmd, [
|
|
202
|
+
...prefix,
|
|
197
203
|
".",
|
|
198
204
|
"--min-lines",
|
|
199
205
|
String(minLines),
|
|
@@ -67,7 +67,7 @@ export const LANGUAGE_POLICY = {
|
|
|
67
67
|
const PRIMARY_DISPATCH_GROUPS = {
|
|
68
68
|
jsts: {
|
|
69
69
|
mode: "fallback",
|
|
70
|
-
runnerIds: ["lsp"
|
|
70
|
+
runnerIds: ["lsp"],
|
|
71
71
|
filterKinds: ["jsts"],
|
|
72
72
|
},
|
|
73
73
|
python: {
|
|
@@ -212,7 +212,7 @@ export function getPrimaryDispatchGroup(kind, lspEnabled) {
|
|
|
212
212
|
return undefined;
|
|
213
213
|
const ids = lspEnabled
|
|
214
214
|
? [...base.runnerIds]
|
|
215
|
-
: base.runnerIds.filter((id) => id !== "lsp"
|
|
215
|
+
: base.runnerIds.filter((id) => id !== "lsp");
|
|
216
216
|
if (ids.length === 0)
|
|
217
217
|
return undefined;
|
|
218
218
|
return {
|
|
@@ -2,27 +2,23 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { isTestMode } from "./env-utils.js";
|
|
4
4
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
5
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
6
|
const LATENCY_LOG_DIR = getGlobalPiLensDir();
|
|
6
7
|
const LATENCY_LOG_FILE = path.join(LATENCY_LOG_DIR, "latency.log");
|
|
7
|
-
|
|
8
|
-
if (!fs.existsSync(LATENCY_LOG_DIR)) {
|
|
9
|
-
fs.mkdirSync(LATENCY_LOG_DIR, { recursive: true });
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
catch { }
|
|
8
|
+
const writer = createNdjsonLogger({ filePath: LATENCY_LOG_FILE });
|
|
13
9
|
export function logLatency(entry) {
|
|
14
10
|
if (isTestMode()) {
|
|
15
11
|
return;
|
|
16
12
|
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
fs.appendFileSync(LATENCY_LOG_FILE, line);
|
|
20
|
-
}
|
|
21
|
-
catch { }
|
|
13
|
+
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
22
14
|
}
|
|
23
15
|
export function getLatencyLogPath() {
|
|
24
16
|
return LATENCY_LOG_FILE;
|
|
25
17
|
}
|
|
18
|
+
/** Resolve once all enqueued latency writes are on disk (tests/shutdown). */
|
|
19
|
+
export function flushLatencyLog() {
|
|
20
|
+
return writer.flush();
|
|
21
|
+
}
|
|
26
22
|
export function readLatencyLog(limit = 100) {
|
|
27
23
|
try {
|
|
28
24
|
const content = fs.readFileSync(LATENCY_LOG_FILE, "utf-8");
|
|
@@ -37,8 +33,7 @@ export function readLatencyLog(limit = 100) {
|
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
export function clearLatencyLog() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
catch { }
|
|
36
|
+
// Enqueue the truncate in the same serialized queue so a clear cannot race a
|
|
37
|
+
// pending drain. Await flushLatencyLog() if you need the file empty on disk.
|
|
38
|
+
writer.truncate();
|
|
44
39
|
}
|
|
@@ -15,7 +15,8 @@ import { withTimeout } from "../deadline-utils.js";
|
|
|
15
15
|
import { logLatency } from "../latency-logger.js";
|
|
16
16
|
// vscode-jsonrpc v9 ships an `exports` map exposing the Node entry as the
|
|
17
17
|
// `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
|
|
18
|
-
import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "../deps/vscode-jsonrpc.js";
|
|
18
|
+
import { CancellationTokenSource, createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "../deps/vscode-jsonrpc.js";
|
|
19
|
+
import { getAmbientAbortSignal } from "../safe-spawn.js";
|
|
19
20
|
import { applyWorkspaceEdit } from "./edits.js";
|
|
20
21
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
21
22
|
import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
|
|
@@ -789,14 +790,19 @@ export async function navRequest(state, method, params,
|
|
|
789
790
|
// When provided, the request is dropped if the document's version advances
|
|
790
791
|
// (an edit landed) between send and response. Omit for non-single-file
|
|
791
792
|
// requests (workspaceSymbol, call-hierarchy follow-ups) that have no version.
|
|
792
|
-
staleCheckPath, timeoutMs = NAV_REQUEST_TIMEOUT_MS
|
|
793
|
+
staleCheckPath, timeoutMs = NAV_REQUEST_TIMEOUT_MS,
|
|
794
|
+
// Cancels the in-flight request (LSP `$/cancelRequest`) when the turn is
|
|
795
|
+
// abandoned. Defaults to the ambient abort signal set around dispatch/tool
|
|
796
|
+
// handling, so callers get cancellation for free without a signature change
|
|
797
|
+
// (#238 Item 1). Pass explicitly in tests.
|
|
798
|
+
signal = getAmbientAbortSignal()) {
|
|
793
799
|
if (!isClientAlive(state))
|
|
794
800
|
return null;
|
|
795
801
|
const normalizedPath = staleCheckPath !== undefined ? normalizeMapKey(staleCheckPath) : undefined;
|
|
796
802
|
const requestVersion = normalizedPath !== undefined
|
|
797
803
|
? state.documentVersions.get(normalizedPath)
|
|
798
804
|
: undefined;
|
|
799
|
-
const result = (await withTimeout(safeSendRequest(state.connection, method, params), timeoutMs).catch((err) => {
|
|
805
|
+
const result = (await withTimeout(safeSendRequest(state.connection, method, params, signal), timeoutMs).catch((err) => {
|
|
800
806
|
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
801
807
|
return undefined;
|
|
802
808
|
}
|
|
@@ -1297,17 +1303,74 @@ async function safeSendNotification(connection, method, params) {
|
|
|
1297
1303
|
}
|
|
1298
1304
|
}
|
|
1299
1305
|
// Helper to safely send requests - catches stream destruction
|
|
1300
|
-
async function safeSendRequest(connection, method, params
|
|
1301
|
-
|
|
1302
|
-
|
|
1306
|
+
async function safeSendRequest(connection, method, params,
|
|
1307
|
+
// When provided, aborting the signal cancels the in-flight request via
|
|
1308
|
+
// vscode-jsonrpc's CancellationToken → an LSP `$/cancelRequest` notification,
|
|
1309
|
+
// so a server stops computing a result the agent has already abandoned (#238
|
|
1310
|
+
// Item 1). The rejection that follows is swallowed (treated as `undefined`).
|
|
1311
|
+
signal) {
|
|
1312
|
+
// Already abandoned before we even sent — don't bother the server.
|
|
1313
|
+
if (signal?.aborted)
|
|
1314
|
+
return undefined;
|
|
1315
|
+
let tokenSource;
|
|
1316
|
+
let onAbort;
|
|
1317
|
+
if (signal) {
|
|
1318
|
+
tokenSource = new CancellationTokenSource();
|
|
1319
|
+
onAbort = () => tokenSource?.cancel();
|
|
1320
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1303
1321
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1322
|
+
// Only pass a token when cancellation is wired, so the call shape is unchanged
|
|
1323
|
+
// for the (many) requests without a signal.
|
|
1324
|
+
const send = () => tokenSource
|
|
1325
|
+
? connection.sendRequest(method, params, tokenSource.token)
|
|
1326
|
+
: connection.sendRequest(method, params);
|
|
1327
|
+
try {
|
|
1328
|
+
// One safe retry on ContentModified (-32801): the document changed under
|
|
1329
|
+
// us, so the server discarded the request. A single retry beats returning
|
|
1330
|
+
// empty — correctness-under-edit is pi-lens's whole hot path (#238 Item 2).
|
|
1331
|
+
const MAX_ATTEMPTS = 2;
|
|
1332
|
+
for (let attempt = 1;; attempt++) {
|
|
1333
|
+
try {
|
|
1334
|
+
return (await send());
|
|
1335
|
+
}
|
|
1336
|
+
catch (err) {
|
|
1337
|
+
if (isStreamError(err) || isCancellationError(err)) {
|
|
1338
|
+
// Stream destroyed, or we cancelled the request on abort — either
|
|
1339
|
+
// way there is no result to return.
|
|
1340
|
+
return undefined;
|
|
1341
|
+
}
|
|
1342
|
+
if (isContentModifiedError(err)) {
|
|
1343
|
+
// Retry once (unless we've since been aborted); if it's still
|
|
1344
|
+
// ContentModified after that, return empty rather than throwing a
|
|
1345
|
+
// code callers don't understand. RequestFailed (-32803) and other
|
|
1346
|
+
// codes are permanent and fall through to the rethrow below.
|
|
1347
|
+
if (attempt < MAX_ATTEMPTS && !signal?.aborted)
|
|
1348
|
+
continue;
|
|
1349
|
+
return undefined;
|
|
1350
|
+
}
|
|
1351
|
+
throw err;
|
|
1352
|
+
}
|
|
1308
1353
|
}
|
|
1309
|
-
throw err;
|
|
1310
1354
|
}
|
|
1355
|
+
finally {
|
|
1356
|
+
if (signal && onAbort)
|
|
1357
|
+
signal.removeEventListener("abort", onAbort);
|
|
1358
|
+
tokenSource?.dispose();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// vscode-jsonrpc rejects a token-cancelled request with a `ResponseError` whose
|
|
1362
|
+
// code is `RequestCancelled` (-32800) or `ServerCancelled` (-32802). Treat both
|
|
1363
|
+
// as "no result" rather than a failure. (isStreamError also matches the
|
|
1364
|
+
// "cancelled" message text; this adds the structured error-code path.)
|
|
1365
|
+
function isCancellationError(err) {
|
|
1366
|
+
const code = err?.code;
|
|
1367
|
+
return code === -32800 || code === -32802;
|
|
1368
|
+
}
|
|
1369
|
+
// `ContentModified` (-32801): the document changed while the request was in
|
|
1370
|
+
// flight, so the server couldn't answer against a consistent state. Retryable —
|
|
1371
|
+
// the only LSP error code worth a second attempt on the edit hot path (#238).
|
|
1372
|
+
function isContentModifiedError(err) {
|
|
1373
|
+
return err?.code === -32801;
|
|
1311
1374
|
}
|
|
1312
1375
|
// Helper to detect stream destruction / connection disposal errors.
|
|
1313
1376
|
// vscode-jsonrpc throws these when the LSP server process exits while
|