pi-lens 3.8.73 → 3.8.74
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 +1116 -0
- package/README.md +37 -21
- package/dist/clients/atomic-write.js +3 -3
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/cache/rule-cache.js +43 -2
- package/dist/clients/cache-observability.js +211 -0
- package/dist/clients/call-graph.js +20 -0
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/complexity-client.js +4 -7
- package/dist/clients/dependency-checker.js +151 -20
- package/dist/clients/diagnostic-dispositions.js +22 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +16 -1
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +14 -0
- package/dist/clients/dispatch/facts/import-facts.js +100 -101
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/integration.js +94 -15
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +24 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +86 -31
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/generated-artifacts.js +21 -0
- package/dist/clients/gzip-stage-write.js +84 -0
- package/dist/clients/installer/index.js +358 -129
- package/dist/clients/knip-client.js +64 -1
- package/dist/clients/language-profile.js +61 -72
- package/dist/clients/latency-logger.js +6 -16
- package/dist/clients/lens-config.js +128 -165
- package/dist/clients/lens-engine.js +39 -5
- package/dist/clients/lens-flag-registry.js +285 -0
- package/dist/clients/lens-map.js +2 -7
- package/dist/clients/log-cleanup.js +6 -17
- package/dist/clients/lsp/cascade-tier.js +22 -2
- package/dist/clients/lsp/client.js +74 -10
- package/dist/clients/lsp/config.js +62 -9
- package/dist/clients/lsp/index.js +34 -24
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +8 -6
- package/dist/clients/lsp/server.js +84 -29
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/mcp/analyze.js +5 -4
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/module-report.js +100 -80
- package/dist/clients/ndjson-logger.js +51 -21
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +35 -0
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +7 -0
- package/dist/clients/project-changes.js +77 -4
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +109 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +247 -127
- package/dist/clients/project-lens-config.js +53 -26
- package/dist/clients/project-report.js +39 -14
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +611 -7
- package/dist/clients/quiet-window.js +9 -1
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard.js +78 -0
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +133 -14
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +1522 -179
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph-logger.js +24 -0
- package/dist/clients/runtime-agent-end.js +4 -2
- package/dist/clients/runtime-coordinator.js +2 -2
- package/dist/clients/runtime-session.js +433 -59
- package/dist/clients/runtime-tool-call.js +46 -14
- package/dist/clients/runtime-turn.js +97 -26
- package/dist/clients/safe-spawn.js +102 -15
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +2 -1
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +48 -33
- package/dist/clients/source-filter.js +49 -25
- package/dist/clients/startup-marker.js +6 -0
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tree-sitter-cache.js +147 -139
- package/dist/clients/tree-sitter-client.js +1340 -67
- package/dist/clients/tree-sitter-logger.js +43 -1
- package/dist/clients/tree-sitter-query-loader.js +110 -11
- package/dist/clients/tree-sitter-shared.js +43 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
- package/dist/clients/widget-state.js +50 -14
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +252 -35
- package/dist/clients/workspace-topology.js +11 -1
- package/dist/index.js +45544 -39255
- package/dist/mcp/analyze-cli.js +3 -0
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +63 -8
- package/dist/tools/lens-diagnostics.js +47 -5
- package/dist/tools/lsp-navigation.js +5 -28
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/environment-variables.md +153 -11
- package/docs/features.md +4 -3
- package/docs/globalconfig.md +56 -4
- package/docs/language-coverage.md +2 -2
- package/docs/mcp.md +17 -16
- package/docs/servercapabilities.md +5 -2
- package/docs/settings.md +181 -0
- package/docs/tools.md +14 -0
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +13 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/config-enabled-shape.js +0 -39
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
* Executes all loaded tree-sitter query files from rules/tree-sitter-queries/
|
|
5
5
|
* for fast AST-based pattern matching.
|
|
6
6
|
*/
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
7
|
import * as path from "node:path";
|
|
9
8
|
import { RuleCache } from "../../cache/rule-cache.js";
|
|
9
|
+
import { minimatch } from "../../deps/minimatch.js";
|
|
10
10
|
import { isTestFile } from "../../file-utils.js";
|
|
11
|
-
import { resolvePackagePath } from "../../package-root.js";
|
|
12
11
|
import { buildOrUpdateGraph, computeImpactCascade, recordEntitySnapshotDiff, } from "../../review-graph/service.js";
|
|
13
12
|
import { getSharedTreeSitterClient, isTreeSitterWasmAborted, markTreeSitterWasmAborted, resolveTreeSitterLanguage, } from "../../tree-sitter-shared.js";
|
|
14
13
|
import { logTreeSitter } from "../../tree-sitter-logger.js";
|
|
15
|
-
import { isDisabledQueryFilePath, queryLoader, } from "../../tree-sitter-query-loader.js";
|
|
14
|
+
import { isDisabledQueryFilePath, queriesForLanguage, queryLoader, ruleFilesForLanguage, ruleSourceLanguages, } from "../../tree-sitter-query-loader.js";
|
|
16
15
|
import { classifyDefect } from "../diagnostic-taxonomy.js";
|
|
17
16
|
import { PRIORITY } from "../priorities.js";
|
|
18
17
|
const blastCooldownByFile = new Map();
|
|
@@ -222,7 +221,7 @@ const ENTITY_QUERIES = {
|
|
|
222
221
|
},
|
|
223
222
|
],
|
|
224
223
|
};
|
|
225
|
-
async function extractEntitySnapshot(client, filePath, languageId) {
|
|
224
|
+
async function extractEntitySnapshot(client, filePath, languageId, contentOverride) {
|
|
226
225
|
const defs = ENTITY_QUERIES[languageId] ?? [];
|
|
227
226
|
const snapshot = new Map();
|
|
228
227
|
for (const def of defs) {
|
|
@@ -237,7 +236,11 @@ async function extractEntitySnapshot(client, filePath, languageId) {
|
|
|
237
236
|
metavars: ["NAME"],
|
|
238
237
|
has_fix: false,
|
|
239
238
|
filePath: "",
|
|
240
|
-
}, filePath, languageId, { maxResults: 200 }
|
|
239
|
+
}, filePath, languageId, { maxResults: 200 },
|
|
240
|
+
// Parse the same buffer content the diagnostics phase used (#885).
|
|
241
|
+
// Without this, unsaved buffers snapshot stale disk content and
|
|
242
|
+
// thrash the parse-cache entry the rule walk just populated.
|
|
243
|
+
contentOverride);
|
|
241
244
|
for (const match of matches) {
|
|
242
245
|
const name = match.captures.NAME?.trim();
|
|
243
246
|
if (!name)
|
|
@@ -289,6 +292,23 @@ function getTotalLinesChanged(ranges) {
|
|
|
289
292
|
}
|
|
290
293
|
/** Threshold: skip entity extraction for changes under 5 lines */
|
|
291
294
|
const ENTITY_EXTRACTION_LINE_THRESHOLD = 5;
|
|
295
|
+
/**
|
|
296
|
+
* `filePath` relative to `root`, forward-slashed, for matching a rule's
|
|
297
|
+
* `ignore_paths` globs. Falls back to the absolute (slash-normalized) path
|
|
298
|
+
* when `filePath` isn't under `root` (e.g. an out-of-tree temp file), so a
|
|
299
|
+
* glob like `scripts/**` simply never matches rather than throwing.
|
|
300
|
+
*/
|
|
301
|
+
function relativeForIgnoreGlob(filePath, root) {
|
|
302
|
+
const rel = path.relative(root, filePath);
|
|
303
|
+
const normalized = (rel.startsWith("..") ? filePath : rel).split(path.sep).join("/");
|
|
304
|
+
return normalized;
|
|
305
|
+
}
|
|
306
|
+
function matchesIgnorePaths(filePath, root, patterns) {
|
|
307
|
+
if (!patterns || patterns.length === 0)
|
|
308
|
+
return false;
|
|
309
|
+
const rel = relativeForIgnoreGlob(filePath, root);
|
|
310
|
+
return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
|
|
311
|
+
}
|
|
292
312
|
const treeSitterRunner = {
|
|
293
313
|
id: "tree-sitter",
|
|
294
314
|
appliesTo: ["jsts", "python", "go", "rust", "ruby", "cxx", "csharp", "php", "css"],
|
|
@@ -334,20 +354,11 @@ const treeSitterRunner = {
|
|
|
334
354
|
// Try cache first, fall back to loading from disk
|
|
335
355
|
let languageQueries = [];
|
|
336
356
|
const cache = new RuleCache(languageId, ctx.cwd);
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
for (const dir of [rulesDir, builtinRulesDir]) {
|
|
343
|
-
if (fs.existsSync(dir)) {
|
|
344
|
-
for (const f of fs.readdirSync(dir)) {
|
|
345
|
-
if (f.endsWith(".yml"))
|
|
346
|
-
ruleFileSet.add(path.join(dir, f));
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
const ruleFiles = [...ruleFileSet];
|
|
357
|
+
// Fingerprint EVERY rule file the effective rule set for this language is
|
|
358
|
+
// built from — project-local AND bundled built-ins, across all rule-source
|
|
359
|
+
// languages (tsx also runs the typescript rules; #878). The loader owns
|
|
360
|
+
// that composition so this can't drift from `queriesForLanguage`.
|
|
361
|
+
const ruleFiles = ruleFilesForLanguage(languageId, ctx.cwd);
|
|
351
362
|
// Try cache
|
|
352
363
|
const cached = cache.get(ruleFiles);
|
|
353
364
|
let cacheHit = false;
|
|
@@ -363,14 +374,18 @@ const treeSitterRunner = {
|
|
|
363
374
|
.filter((q) => !isDisabledQueryFilePath(q.filePath));
|
|
364
375
|
}
|
|
365
376
|
else {
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
// A miss means the rule-file fingerprint moved (or no cache yet), so the
|
|
378
|
+
// loader's in-memory memo may hold the PRE-edit rules — without `force`
|
|
379
|
+
// it would hand those back and cache.set below would persist stale rules
|
|
380
|
+
// under the fresh fingerprint, poisoning every future process (#878).
|
|
381
|
+
await queryLoader.loadQueries(ctx.cwd, { force: true });
|
|
382
|
+
// The effective rule set is composed by the loader (tsx also runs the
|
|
383
|
+
// typescript rules; javascript deliberately does not — see
|
|
384
|
+
// ruleSourceLanguages in tree-sitter-query-loader.ts).
|
|
385
|
+
languageQueries = queriesForLanguage(new Map(ruleSourceLanguages(languageId).map((lang) => [
|
|
386
|
+
lang,
|
|
387
|
+
queryLoader.getQueriesForLanguage(lang),
|
|
388
|
+
])), languageId);
|
|
374
389
|
// Save to cache
|
|
375
390
|
cache.set(ruleFiles, languageQueries.map((q) => ({
|
|
376
391
|
id: q.id,
|
|
@@ -384,7 +399,10 @@ const treeSitterRunner = {
|
|
|
384
399
|
post_filter_params: q.post_filter_params,
|
|
385
400
|
defect_class: q.defect_class,
|
|
386
401
|
inline_tier: q.inline_tier,
|
|
402
|
+
skip_test_files: q.skip_test_files,
|
|
403
|
+
ignore_paths: q.ignore_paths,
|
|
387
404
|
has_fix: q.has_fix,
|
|
405
|
+
fix_action: q.fix_action,
|
|
388
406
|
filePath: q.filePath,
|
|
389
407
|
})));
|
|
390
408
|
}
|
|
@@ -408,9 +426,15 @@ const treeSitterRunner = {
|
|
|
408
426
|
// python-assert-production — `assert` is the idiomatic test assertion) opt
|
|
409
427
|
// out via `skip_test_files` while the runner otherwise runs on test files.
|
|
410
428
|
const fileIsTest = isTestFile(filePath);
|
|
429
|
+
// Per-rule path carve-out (#965): a rule that's noise on CLI scripts or a
|
|
430
|
+
// project's own logging sink (e.g. no-console-except-error firing inside
|
|
431
|
+
// scripts/** or lib/logger.ts) opts out via `ignore_paths`, the same way
|
|
432
|
+
// `skip_test_files` opts a rule out of test files above.
|
|
433
|
+
const ignoreRoot = ctx.projectRoot ?? ctx.cwd;
|
|
411
434
|
const effectiveQueries = (ctx.blockingOnly
|
|
412
435
|
? languageQueries.filter((q) => q.inline_tier !== "review")
|
|
413
|
-
: languageQueries).filter((q) => !(fileIsTest && q.skip_test_files)
|
|
436
|
+
: languageQueries).filter((q) => !(fileIsTest && q.skip_test_files) &&
|
|
437
|
+
!matchesIgnorePaths(filePath, ignoreRoot, q.ignore_paths));
|
|
414
438
|
logTreeSitter({
|
|
415
439
|
phase: "queries_loaded",
|
|
416
440
|
filePath,
|
|
@@ -424,110 +448,109 @@ const treeSitterRunner = {
|
|
|
424
448
|
const contentOverride = contentFromFacts !== undefined && contentFromFacts !== null
|
|
425
449
|
? contentFromFacts
|
|
426
450
|
: undefined;
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (ctx.blockingOnly &&
|
|
452
|
-
isSeverityBlocking &&
|
|
453
|
-
!isLineInModifiedRanges(line + 1, ctx.modifiedRanges)) {
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
// Map severity to semantic
|
|
457
|
-
const semantic = query.severity === "error"
|
|
458
|
-
? "blocking"
|
|
459
|
-
: query.severity === "warning"
|
|
460
|
-
? "warning"
|
|
461
|
-
: "none";
|
|
462
|
-
const defectClass = query.defect_class ??
|
|
463
|
-
classifyDefect(query.id, "tree-sitter", query.message);
|
|
464
|
-
const suggestion = query.has_fix && query.fix_action
|
|
465
|
-
? `${query.fix_action} this statement`
|
|
466
|
-
: semantic === "blocking"
|
|
467
|
-
? defaultFixSuggestion(defectClass, query.id)
|
|
468
|
-
: undefined;
|
|
469
|
-
const hasSuggestedFix = !!query.has_fix;
|
|
470
|
-
queryDiagnostics.push({
|
|
471
|
-
id: `tree-sitter:${query.id}:${line}`,
|
|
472
|
-
message: query.message.replace(/\{\{(\w+)\}\}/g, (_, name) => match.captures[name]?.trim() ?? `{{${name}}}`),
|
|
473
|
-
filePath,
|
|
474
|
-
line: line + 1, // 1-indexed
|
|
475
|
-
column: column + 1, // 1-indexed
|
|
476
|
-
severity: query.severity,
|
|
477
|
-
semantic,
|
|
478
|
-
tool: "tree-sitter",
|
|
479
|
-
rule: query.id,
|
|
480
|
-
defectClass,
|
|
481
|
-
// Surface fix intent to agent — tree-sitter never auto-applies;
|
|
482
|
-
// linters (biome/ruff/eslint) own the autofix phase.
|
|
483
|
-
fixable: hasSuggestedFix,
|
|
484
|
-
autoFixAvailable: false,
|
|
485
|
-
fixKind: hasSuggestedFix ? "suggestion" : undefined,
|
|
486
|
-
fixSuggestion: suggestion,
|
|
487
|
-
matchedText: match.matchedText || undefined,
|
|
488
|
-
astNodeType: match.nodeType || undefined,
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
catch (err) {
|
|
493
|
-
// pi-lens-ignore: missing-error-propagation — per-query resilience loop, intentional
|
|
494
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
495
|
-
// Emscripten abort() corrupts the entire module-level wasm heap.
|
|
496
|
-
// Poison the singleton so no further queries attempt to use the dead runtime.
|
|
497
|
-
if (msg.includes("Aborted") || msg.includes("abort()")) {
|
|
498
|
-
markTreeSitterWasmAborted();
|
|
499
|
-
logTreeSitter({
|
|
500
|
-
phase: "query_error",
|
|
501
|
-
filePath,
|
|
502
|
-
languageId,
|
|
503
|
-
queryId: query.id,
|
|
504
|
-
error: "wasm_aborted_fatal",
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
console.error(`[tree-sitter] Query ${query.id} failed:`, err);
|
|
509
|
-
logTreeSitter({
|
|
510
|
-
phase: "query_error",
|
|
511
|
-
filePath,
|
|
512
|
-
languageId,
|
|
513
|
-
queryId: query.id,
|
|
514
|
-
error: msg,
|
|
515
|
-
});
|
|
516
|
-
}
|
|
451
|
+
// ONE tree walk for the whole effective rule set, not one per rule (#888).
|
|
452
|
+
// The per-rule loop re-walked the tree ~30-40 times per edit; the work is
|
|
453
|
+
// synchronous WASM, so the concurrency-limit batching only chunked CPU work
|
|
454
|
+
// without parallelizing anything. runQueriesOnFile concatenates the patterns
|
|
455
|
+
// in rule order and maps each match back to its owning rule, so the per-rule
|
|
456
|
+
// maxResults(10) cap, metavars, predicates and post-filters still apply and
|
|
457
|
+
// results stay grouped in rule order. The post-match gating below (modified
|
|
458
|
+
// ranges, severity mapping) is unchanged.
|
|
459
|
+
const diagnostics = [];
|
|
460
|
+
try {
|
|
461
|
+
const found = await client.runQueriesOnFile(effectiveQueries, filePath, languageId, { maxResults: 10 }, contentOverride);
|
|
462
|
+
for (const { queryDef: query, match } of found) {
|
|
463
|
+
// match.line/column are already 1-indexed — the client emits
|
|
464
|
+
// startPosition.row + 1 (tree-sitter-client searchFileWithQuery).
|
|
465
|
+
const { line, column } = match;
|
|
466
|
+
// Modified-ranges gate only applies to blocking-tier diagnostics.
|
|
467
|
+
// Warning-tier diagnostics always flow through for logging.
|
|
468
|
+
const isSeverityBlocking = query.severity === "error" ||
|
|
469
|
+
query.inline_tier === "blocking" ||
|
|
470
|
+
SILENT_ERROR_QUERY_IDS.has(query.id);
|
|
471
|
+
if (ctx.blockingOnly &&
|
|
472
|
+
isSeverityBlocking &&
|
|
473
|
+
!isLineInModifiedRanges(line, ctx.modifiedRanges)) {
|
|
474
|
+
continue;
|
|
517
475
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
476
|
+
// Map severity to semantic
|
|
477
|
+
const semantic = query.severity === "error"
|
|
478
|
+
? "blocking"
|
|
479
|
+
: query.severity === "warning"
|
|
480
|
+
? "warning"
|
|
481
|
+
: "none";
|
|
482
|
+
const defectClass = query.defect_class ??
|
|
483
|
+
classifyDefect(query.id, "tree-sitter", query.message);
|
|
484
|
+
const suggestion = query.has_fix && query.fix_action
|
|
485
|
+
? `${query.fix_action} this statement`
|
|
486
|
+
: semantic === "blocking"
|
|
487
|
+
? defaultFixSuggestion(defectClass, query.id)
|
|
488
|
+
: undefined;
|
|
489
|
+
const hasSuggestedFix = !!query.has_fix;
|
|
490
|
+
diagnostics.push({
|
|
491
|
+
id: `tree-sitter:${query.id}:${line}`,
|
|
492
|
+
message: query.message.replace(/\{\{(\w+)\}\}/g, (_, name) => match.captures[name]?.trim() ?? `{{${name}}}`),
|
|
493
|
+
filePath,
|
|
494
|
+
line,
|
|
495
|
+
column,
|
|
496
|
+
severity: query.severity,
|
|
497
|
+
semantic,
|
|
498
|
+
tool: "tree-sitter",
|
|
499
|
+
rule: query.id,
|
|
500
|
+
defectClass,
|
|
501
|
+
// Surface fix intent to agent — tree-sitter never auto-applies;
|
|
502
|
+
// linters (biome/ruff/eslint) own the autofix phase.
|
|
503
|
+
fixable: hasSuggestedFix,
|
|
504
|
+
autoFixAvailable: false,
|
|
505
|
+
fixKind: hasSuggestedFix ? "suggestion" : undefined,
|
|
506
|
+
fixSuggestion: suggestion,
|
|
507
|
+
matchedText: match.matchedText || undefined,
|
|
508
|
+
astNodeType: match.nodeType || undefined,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
521
511
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
512
|
+
catch (err) {
|
|
513
|
+
// pi-lens-ignore: missing-error-propagation — per-dispatch resilience, intentional
|
|
514
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
515
|
+
// Emscripten abort() corrupts the entire module-level wasm heap.
|
|
516
|
+
// Poison the singleton so no further queries attempt to use the dead runtime.
|
|
517
|
+
if (msg.includes("Aborted") || msg.includes("abort()")) {
|
|
518
|
+
markTreeSitterWasmAborted();
|
|
519
|
+
logTreeSitter({
|
|
520
|
+
phase: "query_error",
|
|
521
|
+
filePath,
|
|
522
|
+
languageId,
|
|
523
|
+
error: "wasm_aborted_fatal",
|
|
524
|
+
metadata: { queryIds: effectiveQueries.map((q) => q.id) },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
console.error("[tree-sitter] Batched query run failed:", err);
|
|
529
|
+
logTreeSitter({
|
|
530
|
+
phase: "query_error",
|
|
531
|
+
filePath,
|
|
532
|
+
languageId,
|
|
533
|
+
error: msg,
|
|
534
|
+
metadata: { queryIds: effectiveQueries.map((q) => q.id) },
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Skip expensive entity extraction for trivial changes (< 5 lines).
|
|
539
|
+
// Before #885 this threshold only guarded the zero-diagnostics early
|
|
540
|
+
// return below; a second extractEntitySnapshot block ran UNCONDITIONALLY
|
|
541
|
+
// after it, so small edits (skipEntityExtraction=true) still paid the
|
|
542
|
+
// ~500-800ms snapshot cost on every dispatch. One guarded block now
|
|
543
|
+
// serves both paths.
|
|
544
|
+
// Absent/empty modifiedRanges means "whole file" (full-file dispatches,
|
|
545
|
+
// fresh writes — see isLineInModifiedRanges above), never "trivial edit":
|
|
546
|
+
// entity extraction must run in that case.
|
|
526
547
|
const totalLinesChanged = getTotalLinesChanged(ctx.modifiedRanges);
|
|
527
|
-
const skipEntityExtraction =
|
|
528
|
-
|
|
548
|
+
const skipEntityExtraction = ctx.modifiedRanges != null &&
|
|
549
|
+
ctx.modifiedRanges.length > 0 &&
|
|
550
|
+
totalLinesChanged < ENTITY_EXTRACTION_LINE_THRESHOLD;
|
|
551
|
+
if (!skipEntityExtraction) {
|
|
529
552
|
try {
|
|
530
|
-
const snapshot = await extractEntitySnapshot(client, filePath, languageId);
|
|
553
|
+
const snapshot = await extractEntitySnapshot(client, filePath, languageId, contentOverride);
|
|
531
554
|
const diff = recordEntitySnapshotDiff(ctx.facts, filePath, snapshot);
|
|
532
555
|
const changedEntityKeys = [
|
|
533
556
|
...diff.added,
|
|
@@ -564,6 +587,8 @@ const treeSitterRunner = {
|
|
|
564
587
|
catch {
|
|
565
588
|
/* entity snapshot / blast-radius enrichment is best-effort */
|
|
566
589
|
}
|
|
590
|
+
}
|
|
591
|
+
if (diagnostics.length === 0) {
|
|
567
592
|
logTreeSitter({
|
|
568
593
|
phase: "runner_complete",
|
|
569
594
|
filePath,
|
|
@@ -579,44 +604,6 @@ const treeSitterRunner = {
|
|
|
579
604
|
// Check if any blocking issues
|
|
580
605
|
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
581
606
|
const blockingCount = diagnostics.filter((d) => d.semantic === "blocking").length;
|
|
582
|
-
try {
|
|
583
|
-
const snapshot = await extractEntitySnapshot(client, filePath, languageId);
|
|
584
|
-
const diff = recordEntitySnapshotDiff(ctx.facts, filePath, snapshot);
|
|
585
|
-
const changedEntityKeys = [
|
|
586
|
-
...diff.added,
|
|
587
|
-
...diff.modified,
|
|
588
|
-
...diff.removed,
|
|
589
|
-
];
|
|
590
|
-
if (changedEntityKeys.length > 0) {
|
|
591
|
-
logTreeSitter({
|
|
592
|
-
phase: "entity_diff",
|
|
593
|
-
filePath,
|
|
594
|
-
languageId,
|
|
595
|
-
metadata: {
|
|
596
|
-
added: diff.added,
|
|
597
|
-
modified: diff.modified,
|
|
598
|
-
removed: diff.removed,
|
|
599
|
-
totalChanged: changedEntityKeys.length,
|
|
600
|
-
},
|
|
601
|
-
});
|
|
602
|
-
const lastBlast = blastCooldownByFile.get(filePath) ?? 0;
|
|
603
|
-
if (Date.now() - lastBlast < BLAST_COOLDOWN_MS) {
|
|
604
|
-
logTreeSitter({
|
|
605
|
-
phase: "blast_radius",
|
|
606
|
-
filePath,
|
|
607
|
-
languageId,
|
|
608
|
-
metadata: { skipped: "cooldown", cooldownMs: BLAST_COOLDOWN_MS },
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
blastCooldownByFile.set(filePath, Date.now());
|
|
613
|
-
runBlastRadiusInBackground(ctx.cwd, filePath, languageId, ctx.facts);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
// best-effort experimental telemetry only
|
|
619
|
-
}
|
|
620
607
|
logTreeSitter({
|
|
621
608
|
phase: "runner_complete",
|
|
622
609
|
filePath,
|
|
@@ -10,7 +10,7 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { getGlobalPiLensDir } from "../../../file-utils.js";
|
|
13
|
-
import { ensureTool } from "../../../installer/index.js";
|
|
13
|
+
import { ensureTool, isSpawnableCommand } from "../../../installer/index.js";
|
|
14
14
|
import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/config.js";
|
|
15
15
|
import { findGlobalBinary } from "../../../package-manager.js";
|
|
16
16
|
import { safeSpawnAsync } from "../../../safe-spawn.js";
|
|
@@ -207,27 +207,26 @@ export async function resolveToolCommandWithInstallFallback(cwd, toolId, timeout
|
|
|
207
207
|
return resolveCommandWithInstallFallback(resolveToolCommand(cwd, toolId) ?? spec.command, spec.managedToolId ?? toolId, cwd, spec.versionArgs ?? ["--version"], timeout);
|
|
208
208
|
}
|
|
209
209
|
async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
// Skip the --version spawn when the command isn't even on disk — the ~μs
|
|
211
|
+
// stat/PATH walk beats a guaranteed-to-fail spawn round-trip.
|
|
212
|
+
if (await isSpawnableCommand(command)) {
|
|
213
|
+
const versionCheck = await safeSpawnAsync(command, versionArgs, {
|
|
214
|
+
timeout,
|
|
215
|
+
cwd,
|
|
216
|
+
});
|
|
217
|
+
if (!versionCheck.error && versionCheck.status === 0) {
|
|
218
|
+
return command;
|
|
219
|
+
}
|
|
216
220
|
}
|
|
217
221
|
if (!shouldAutoInstallTool(toolId)) {
|
|
218
222
|
return null;
|
|
219
223
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
227
|
-
if (installedCheck.error || installedCheck.status !== 0) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
return installed;
|
|
224
|
+
// ensureTool's result is already trusted: a probe-cache hit validated
|
|
225
|
+
// path+mtime, and a fresh install verified the binary. Re-probing
|
|
226
|
+
// --version here booted the tool's node shim once per invocation for
|
|
227
|
+
// zero new information; a broken install now surfaces as the runner's
|
|
228
|
+
// own spawn error instead of a silent null.
|
|
229
|
+
return (await ensureTool(toolId)) ?? null;
|
|
231
230
|
}
|
|
232
231
|
export async function resolveCommandArgsWithInstallFallback(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
|
|
233
232
|
const versionCheck = await safeSpawnAsync(command.cmd, [...command.args, ...versionArgs], { timeout, cwd });
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
function normalizeHintInput(value) {
|
|
2
|
-
return value.replace(/\\/g, "/")
|
|
2
|
+
return value.replace(/\\/g, "/");
|
|
3
|
+
}
|
|
4
|
+
function hasHintToken(value, tokens) {
|
|
5
|
+
const escaped = tokens.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
6
|
+
const alternatives = escaped.join("|");
|
|
7
|
+
// Path/word boundaries are deliberately explicit: a token must occupy a
|
|
8
|
+
// segment (or a dot/dash/underscore-separated word), never an arbitrary
|
|
9
|
+
// substring. The second expression preserves camelCase names such as
|
|
10
|
+
// authStore and DatabaseClient without making `adb` match `db`.
|
|
11
|
+
return (new RegExp(`(?:^|[/_.\\-\\s])(?:${alternatives})(?=[/_.\\-\\s]|$)`, "i").test(value) ||
|
|
12
|
+
new RegExp(`(?:^|(?<=[a-z0-9]))(?:${tokens
|
|
13
|
+
.flatMap((token) => [
|
|
14
|
+
token[0].toUpperCase() + token.slice(1),
|
|
15
|
+
...(token === "openai" ? ["OpenAI"] : []),
|
|
16
|
+
...(token === "github" ? ["GitHub"] : []),
|
|
17
|
+
...(token === "gitlab" ? ["GitLab"] : []),
|
|
18
|
+
])
|
|
19
|
+
.join("|")})(?=[A-Z]|[/_.\\-\\s]|$)`).test(value));
|
|
3
20
|
}
|
|
4
21
|
/**
|
|
5
22
|
* Deterministic feature-kind hint derived from package/entity/file names.
|
|
@@ -7,10 +24,19 @@ function normalizeHintInput(value) {
|
|
|
7
24
|
*/
|
|
8
25
|
export function inferFeatureKind(nameOrPath) {
|
|
9
26
|
const normalized = normalizeHintInput(nameOrPath);
|
|
10
|
-
if (
|
|
27
|
+
if (hasHintToken(normalized, [
|
|
28
|
+
"config",
|
|
29
|
+
"store",
|
|
30
|
+
"db",
|
|
31
|
+
"database",
|
|
32
|
+
"github",
|
|
33
|
+
"openai",
|
|
34
|
+
"sync",
|
|
35
|
+
"service",
|
|
36
|
+
])) {
|
|
11
37
|
return "service";
|
|
12
38
|
}
|
|
13
|
-
if (/(^|[/_.\-\s])(cli|command|bin)([/_.\-\s]|$)
|
|
39
|
+
if (/(^|[/_.\-\s])(cli|command|bin)([/_.\-\s]|$)/i.test(normalized)) {
|
|
14
40
|
return "cli-command";
|
|
15
41
|
}
|
|
16
42
|
return "library";
|
|
@@ -22,20 +48,40 @@ export function inferFeatureKind(nameOrPath) {
|
|
|
22
48
|
export function inferTrustBoundaries(nameOrPath) {
|
|
23
49
|
const normalized = normalizeHintInput(nameOrPath);
|
|
24
50
|
const boundaries = new Set();
|
|
25
|
-
if (
|
|
51
|
+
if (hasHintToken(normalized, [
|
|
52
|
+
"config",
|
|
53
|
+
"store",
|
|
54
|
+
"db",
|
|
55
|
+
"database",
|
|
56
|
+
"repo",
|
|
57
|
+
"repository",
|
|
58
|
+
"model",
|
|
59
|
+
"migration",
|
|
60
|
+
])) {
|
|
26
61
|
boundaries.add("filesystem");
|
|
27
62
|
boundaries.add("database");
|
|
28
63
|
}
|
|
29
|
-
if (
|
|
64
|
+
if (hasHintToken(normalized, [
|
|
65
|
+
"github",
|
|
66
|
+
"gitlab",
|
|
67
|
+
"openai",
|
|
68
|
+
"anthropic",
|
|
69
|
+
"stripe",
|
|
70
|
+
"slack",
|
|
71
|
+
"sync",
|
|
72
|
+
"webhook",
|
|
73
|
+
"api",
|
|
74
|
+
"client",
|
|
75
|
+
])) {
|
|
30
76
|
boundaries.add("network");
|
|
31
77
|
boundaries.add("external-api");
|
|
32
78
|
boundaries.add("serialization");
|
|
33
79
|
}
|
|
34
|
-
if (/(^|[/_.\-\s])(cli|command|bin|exec|spawn|process|shell)([/_.\-\s]|$)
|
|
80
|
+
if (/(^|[/_.\-\s])(cli|command|bin|exec|spawn|process|shell)([/_.\-\s]|$)/i.test(normalized)) {
|
|
35
81
|
boundaries.add("user-input");
|
|
36
82
|
boundaries.add("process-exec");
|
|
37
83
|
}
|
|
38
|
-
if (
|
|
84
|
+
if (hasHintToken(normalized, ["auth", "login", "token", "session", "oauth", "jwt"])) {
|
|
39
85
|
boundaries.add("auth");
|
|
40
86
|
boundaries.add("user-input");
|
|
41
87
|
}
|