pi-lens 3.8.72 → 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 +1162 -0
- package/README.md +45 -28
- 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/deps/js-yaml.js +1 -1
- 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 +84 -8
- 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/instance-registry.js +12 -1
- 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 +113 -31
- 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 +45976 -39387
- 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-diagnostics.js +126 -9
- 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 -139
- 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/no-equality-in-for-condition.yml +1 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
- 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
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
* Fact-provider-specific tree-sitter helper (#402).
|
|
3
3
|
*
|
|
4
4
|
* The generic node-walk utilities (`walk`, `childrenOfType`, `firstChildOfType`,
|
|
5
|
-
* `
|
|
5
|
+
* `withTreeSitterRoot`, `TsNode`) live in `clients/tree-sitter-shared.ts` so both
|
|
6
6
|
* the fact extractors and the complexity client share them. This module re-exports
|
|
7
7
|
* them for the extractors and adds the FactProvider-specific `extractFactsFromTree`
|
|
8
8
|
* shell.
|
|
9
9
|
*/
|
|
10
|
-
import { childrenOfType, firstChildOfType,
|
|
10
|
+
import { childrenOfType, firstChildOfType, withTreeSitterRoot, walk, } from "../../tree-sitter-shared.js";
|
|
11
11
|
export { childrenOfType, firstChildOfType, walk, };
|
|
12
|
-
|
|
13
|
-
export const parseFactTree = parseTreeSitterRoot;
|
|
12
|
+
export const withFactTree = withTreeSitterRoot;
|
|
14
13
|
/**
|
|
15
14
|
* Provider shell for tree-sitter-backed fact extractors: read `file.content`,
|
|
16
15
|
* parse via the shared client, and hand the root node to `extract`. On empty
|
|
@@ -27,8 +26,6 @@ export async function extractFactsFromTree(ctx, store, defaults, extract) {
|
|
|
27
26
|
const content = store.getFileFact(ctx.filePath, "file.content");
|
|
28
27
|
if (!content)
|
|
29
28
|
return writeAll(defaults);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
return writeAll(defaults);
|
|
33
|
-
writeAll(extract(root, content));
|
|
29
|
+
const parsed = await withFactTree(ctx.filePath, content, (root) => extract(root, content));
|
|
30
|
+
writeAll(parsed.parsed ? parsed.value : defaults);
|
|
34
31
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { extractFactsFromTree, firstChildOfType, walk, } from "./tree-sitter-facts.js";
|
|
2
2
|
// --- Helpers ---
|
|
3
|
+
/** Does `node`'s subtree contain an `assignment_expression` (`x = ...`, `x += ...`, …)? */
|
|
4
|
+
function hasAssignmentExpression(node) {
|
|
5
|
+
if (!node)
|
|
6
|
+
return false;
|
|
7
|
+
let found = false;
|
|
8
|
+
walk(node, (n) => {
|
|
9
|
+
if (n.type === "assignment_expression")
|
|
10
|
+
found = true;
|
|
11
|
+
});
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
3
14
|
function isOnlyWhitespaceOrComments(text) {
|
|
4
15
|
let stripped = text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
5
16
|
stripped = stripped.replace(/\/\/[^\n]*/g, "");
|
|
@@ -74,6 +85,7 @@ export const tryCatchFactProvider = {
|
|
|
74
85
|
const catchReturnsDefault = DEFAULT_VALUE_PATTERN.test(bodyText);
|
|
75
86
|
const catchReturnsStructuredError = STRUCTURED_ERROR_PATTERN.test(bodyText);
|
|
76
87
|
const isDocumentedLocalFallback = EXPLAINING_COMMENT_PATTERN.test(bodyText);
|
|
88
|
+
const catchHasFallbackAssignment = hasAssignmentExpression(catchBody);
|
|
77
89
|
const catchLogsOnly = hasLogging &&
|
|
78
90
|
!hasRethrow &&
|
|
79
91
|
!catchReturnsDefault &&
|
|
@@ -95,6 +107,7 @@ export const tryCatchFactProvider = {
|
|
|
95
107
|
catchReturnsDefault,
|
|
96
108
|
catchReturnsStructuredError,
|
|
97
109
|
isDocumentedLocalFallback,
|
|
110
|
+
catchHasFallbackAssignment,
|
|
98
111
|
tryResolvesLocalValues,
|
|
99
112
|
isFilesystemExistenceProbe,
|
|
100
113
|
boundaryCategory,
|
|
@@ -25,8 +25,8 @@ import { getServersForFileWithConfig } from "../lsp/config.js";
|
|
|
25
25
|
import { getLSPService } from "../lsp/index.js";
|
|
26
26
|
import { isExternalOrVendorFile, normalizeMapKey } from "../path-utils.js";
|
|
27
27
|
import { getProjectIgnoreMatcher } from "../file-utils.js";
|
|
28
|
-
import { clearReviewGraphWorkspaceCache, getLastGraphBuildInfo, } from "../review-graph/builder.js";
|
|
29
|
-
import { buildReverseDependencyIndexFromGraph, getAffectedFilesFromIndex, writeReverseDependencyIndexToSnapshot, } from "../reverse-deps.js";
|
|
28
|
+
import { clearReviewGraphWorkspaceCache, getGraphImportChanges, getLastGraphBuildInfo, } from "../review-graph/builder.js";
|
|
29
|
+
import { buildReverseDependencyIndexFromGraph, getAffectedFilesFromIndex, patchReverseDependencyIndex, writeReverseDependencyIndexToSnapshot, } from "../reverse-deps.js";
|
|
30
30
|
import { buildOrUpdateGraph, computeImpactCascade, computeTransitiveImpact, formatImpactCascade, } from "../review-graph/service.js";
|
|
31
31
|
import { clearModuleGraphCache } from "../review-graph/workspace-modules.js";
|
|
32
32
|
import { RUNTIME_CONFIG } from "../runtime-config.js";
|
|
@@ -498,17 +498,15 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
498
498
|
// `wordIndex`/`fileContent` doc comments for the cold/no-forward-index
|
|
499
499
|
// no-op rules.
|
|
500
500
|
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// catch, so the key shape here must match the build path's, not the
|
|
511
|
-
// cascade/graph's own (different) normalization scheme.
|
|
501
|
+
// Keyed by `path.resolve(filePath)` (native separators, tool-input casing).
|
|
502
|
+
// Since #1025 the word index's path maps are `PathKeyedMap`s that fold every
|
|
503
|
+
// key through `wordIndexKey` (`normalizeEphemeralMapKey` — slash-fold +
|
|
504
|
+
// win32-lowercase) INTERNALLY, so this per-edit key and the build path's own
|
|
505
|
+
// walk-derived key (`collectWordIndexDocs` → `collectSourceFilesAsync`)
|
|
506
|
+
// collapse to the same entry regardless of on-disk-vs-input casing/separator.
|
|
507
|
+
// The old hazard — this update silently orphaning a SECOND entry next to the
|
|
508
|
+
// walker's original-cased one — is now structurally impossible at the map
|
|
509
|
+
// layer, so this seam no longer has to hand-match the build path's key shape.
|
|
512
510
|
updateWordIndexForCascade({
|
|
513
511
|
wordIndex,
|
|
514
512
|
filePath: nodePath.resolve(filePath),
|
|
@@ -544,15 +542,32 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
544
542
|
const graphBuildInfo = getLastGraphBuildInfo();
|
|
545
543
|
const workspaceKey = normalizeMapKey(cwd);
|
|
546
544
|
const cachedReverseDeps = reverseDepsIndexCache.get(workspaceKey);
|
|
545
|
+
const importDelta = getGraphImportChanges(graph);
|
|
546
|
+
// A one-step delta is only usable against an index cached at exactly the
|
|
547
|
+
// delta's predecessor generation. Builds minted elsewhere (mcp analyze,
|
|
548
|
+
// lens-map, session warm) advance generations whose import changes this
|
|
549
|
+
// delta does not cover — reusing/patching across that gap stamps a stale
|
|
550
|
+
// index as current, and generation equality then hides the loss forever
|
|
551
|
+
// (#939 review finding 1).
|
|
552
|
+
const deltaContiguous = importDelta !== undefined &&
|
|
553
|
+
importDelta.fromGeneration !== undefined &&
|
|
554
|
+
cachedReverseDeps !== undefined &&
|
|
555
|
+
cachedReverseDeps.generation === importDelta.fromGeneration;
|
|
556
|
+
const importChanges = deltaContiguous ? importDelta.changes : undefined;
|
|
557
|
+
const importsChanged = importChanges?.some((change) => change.existedBefore !== change.existsAfter ||
|
|
558
|
+
change.priorTargets.length !== change.newTargets.length ||
|
|
559
|
+
change.priorTargets.some((target, index) => target !== change.newTargets[index]));
|
|
547
560
|
const canReuse = reverseDepsReuseEnabled() &&
|
|
548
561
|
cachedReverseDeps !== undefined &&
|
|
549
|
-
graph.buildGeneration !== undefined &&
|
|
550
|
-
|
|
562
|
+
((graph.buildGeneration !== undefined &&
|
|
563
|
+
cachedReverseDeps.generation === graph.buildGeneration) ||
|
|
564
|
+
(importChanges !== undefined && !importsChanged));
|
|
551
565
|
let reverseDepsIndex;
|
|
552
566
|
let reverseDepsSaved;
|
|
553
567
|
if (canReuse && cachedReverseDeps) {
|
|
554
568
|
reverseDepsIndex = cachedReverseDeps.index;
|
|
555
569
|
reverseDepsSaved = cachedReverseDeps.savedToSnapshot;
|
|
570
|
+
cachedReverseDeps.generation = graph.buildGeneration;
|
|
556
571
|
logCascade({
|
|
557
572
|
phase: "reverse_deps_cache",
|
|
558
573
|
filePath,
|
|
@@ -565,6 +580,29 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
565
580
|
},
|
|
566
581
|
});
|
|
567
582
|
}
|
|
583
|
+
else if (cachedReverseDeps && importChanges && importsChanged) {
|
|
584
|
+
reverseDepsIndex = patchReverseDependencyIndex(cachedReverseDeps.index, importChanges);
|
|
585
|
+
reverseDepsSaved = writeReverseDependencyIndexToSnapshot({
|
|
586
|
+
cwd,
|
|
587
|
+
index: reverseDepsIndex,
|
|
588
|
+
dbg,
|
|
589
|
+
});
|
|
590
|
+
reverseDepsIndexCache.set(workspaceKey, {
|
|
591
|
+
index: reverseDepsIndex,
|
|
592
|
+
savedToSnapshot: reverseDepsSaved,
|
|
593
|
+
generation: graph.buildGeneration,
|
|
594
|
+
});
|
|
595
|
+
logCascade({
|
|
596
|
+
phase: "reverse_deps_cache",
|
|
597
|
+
filePath,
|
|
598
|
+
durationMs: Date.now() - graphStart,
|
|
599
|
+
metadata: {
|
|
600
|
+
action: "patched_import_changes",
|
|
601
|
+
changedFileCount: importChanges.length,
|
|
602
|
+
savedToSnapshot: reverseDepsSaved,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
568
606
|
else {
|
|
569
607
|
reverseDepsIndex = buildReverseDependencyIndexFromGraph({
|
|
570
608
|
cwd,
|
|
@@ -614,6 +652,29 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
614
652
|
},
|
|
615
653
|
});
|
|
616
654
|
impact = computeImpactCascade(graph, normalizedFile, cwd);
|
|
655
|
+
// #1023: buildOrUpdateGraph returns an EMPTY graph (seeded only with the
|
|
656
|
+
// changed file's own symbols) when the repo is over
|
|
657
|
+
// PI_LENS_REVIEW_GRAPH_MAX_FILES (`too_many_files`) or the root is unsafe
|
|
658
|
+
// (`unsafe_root`) — both stamp mode "skipped" on the already-read
|
|
659
|
+
// graphBuildInfo. Computing impact against that empty graph yields zero
|
|
660
|
+
// neighbors for EVERY edit, indistinguishable from a genuine clean leaf.
|
|
661
|
+
// Thread the ALREADY-KNOWN degraded state (never re-derived) onto the
|
|
662
|
+
// result so the turn-end seam surfaces an honest advisory instead of a
|
|
663
|
+
// silent all-clear (#533). Keyed strictly off the degraded build mode —
|
|
664
|
+
// NOT off `neighborFiles.length === 0` — so a healthy build with a real
|
|
665
|
+
// empty dependent set stays silent (no crying wolf).
|
|
666
|
+
if (graphBuildInfo.mode === "skipped" && !impact.indeterminate) {
|
|
667
|
+
impact.indeterminate = {
|
|
668
|
+
reason: "graph_degraded",
|
|
669
|
+
detail: graphBuildInfo.skipReason === "too_many_files"
|
|
670
|
+
? `review graph disabled — ${graphBuildInfo.sourceFileCount ?? "?"} files over the ${graphBuildInfo.maxFileCount ?? "?"} cap`
|
|
671
|
+
: graphBuildInfo.skipReason === "unsafe_root"
|
|
672
|
+
? "review graph skipped — workspace root is at/above home dir"
|
|
673
|
+
: `review graph unavailable (${graphBuildInfo.skipReason ?? "skipped"})`,
|
|
674
|
+
sourceFileCount: graphBuildInfo.sourceFileCount,
|
|
675
|
+
maxFileCount: graphBuildInfo.maxFileCount,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
617
678
|
const reverseDepNeighbors = getAffectedFilesFromIndex(reverseDepsIndex, normalizedFile, 1, MAX_FILES * 2);
|
|
618
679
|
logCascade({
|
|
619
680
|
phase: "reverse_deps_cache",
|
|
@@ -1117,6 +1178,20 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1117
1178
|
cascadeSessionStats.diagnosticsSurfaced += diagCount;
|
|
1118
1179
|
cascadeSessionStats.coldSnapshotTouches += coldSnapshotPaths.length;
|
|
1119
1180
|
if (!formatted) {
|
|
1181
|
+
// #1023: an indeterminate compute (degraded/cold/missing-node graph) must
|
|
1182
|
+
// NOT collapse into "no_neighbors" — that is the exact silent all-clear the
|
|
1183
|
+
// bug is about. Distinguish it by the marker threaded onto `impact`, never
|
|
1184
|
+
// by `visibleNeighbors.length === 0` alone (a healthy leaf is also empty).
|
|
1185
|
+
if (impact.indeterminate) {
|
|
1186
|
+
return {
|
|
1187
|
+
filePath,
|
|
1188
|
+
result: undefined,
|
|
1189
|
+
neighborCount: visibleNeighbors.length,
|
|
1190
|
+
diagnosticCount: diagCount,
|
|
1191
|
+
skipReason: "indeterminate",
|
|
1192
|
+
indeterminate: impact.indeterminate,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1120
1195
|
const skipReason = visibleNeighbors.length === 0 ? "no_neighbors" : "clean";
|
|
1121
1196
|
return {
|
|
1122
1197
|
filePath,
|
|
@@ -1132,6 +1207,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1132
1207
|
result: { filePath, impact, neighbors: visibleNeighbors, formatted },
|
|
1133
1208
|
neighborCount: visibleNeighbors.length,
|
|
1134
1209
|
diagnosticCount: diagCount,
|
|
1210
|
+
// #1023: even when some fallback neighbors surfaced, a degraded graph means
|
|
1211
|
+
// the dependent set is INCOMPLETE — carry the marker so the turn-end seam
|
|
1212
|
+
// still notes downstream impact was under-computed this turn.
|
|
1213
|
+
...(impact.indeterminate && { indeterminate: impact.indeterminate }),
|
|
1135
1214
|
};
|
|
1136
1215
|
}
|
|
1137
1216
|
function diagnosticDeltaKey(diagnostic) {
|
|
@@ -18,6 +18,7 @@ export const asyncNoiseRule = {
|
|
|
18
18
|
!fn.hasAwait &&
|
|
19
19
|
!fn.hasReturnAwaitCall &&
|
|
20
20
|
!fn.isPassThroughWrapper &&
|
|
21
|
+
!fn.hasExplicitPromiseReturnType &&
|
|
21
22
|
!ASYNC_INTERFACE_PATTERN.test(fn.name) &&
|
|
22
23
|
// Skip single-statement functions — likely interface stubs or thin wrappers
|
|
23
24
|
fn.statementCount > 1) {
|
|
@@ -53,6 +53,13 @@ function hasCatchCoverage(fn, catches) {
|
|
|
53
53
|
// Catch body contains a return statement → structured error result
|
|
54
54
|
if (/\breturn\b/.test(c.bodyText))
|
|
55
55
|
return true;
|
|
56
|
+
// Catch body falls back by mutating existing state (e.g. extending a
|
|
57
|
+
// validity window on refresh failure) instead of rethrowing/logging/
|
|
58
|
+
// returning. That's still a real handler — the exception doesn't
|
|
59
|
+
// escape uncaught — just expressed via assignment rather than one of
|
|
60
|
+
// the three shapes above (#969).
|
|
61
|
+
if (c.catchHasFallbackAssignment)
|
|
62
|
+
return true;
|
|
56
63
|
return false;
|
|
57
64
|
});
|
|
58
65
|
}
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { loadAstGrepNapi, } from "../../deps/ast-grep-napi.js";
|
|
12
|
+
import { minimatch } from "../../deps/minimatch.js";
|
|
12
13
|
import { getAstGrepRuleSources, } from "../../sgconfig.js";
|
|
14
|
+
import { logLatency } from "../../latency-logger.js";
|
|
13
15
|
import { hasEslintConfig } from "../../tool-policy.js";
|
|
14
16
|
import { enabledAuxiliaryLspServerIds } from "../auxiliary-lsp.js";
|
|
15
17
|
import { classifyDefect } from "../diagnostic-taxonomy.js";
|
|
18
|
+
import { isAuxiliaryLspAlive } from "../../lsp/index.js";
|
|
19
|
+
import { resolveAstGrepNativeExe } from "../../lsp/wait-policy/index.js";
|
|
16
20
|
import { PRIORITY } from "../priorities.js";
|
|
17
21
|
import { calculateRuleComplexity, isOverlyBroadPattern, isStructuredRule, loadYamlRules, loadYamlRulesFresh, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
|
|
18
22
|
// Lazy load the napi package
|
|
@@ -117,28 +121,48 @@ function explicitRuleFixSuggestion(rule) {
|
|
|
117
121
|
function normalizeRuleId(ruleId) {
|
|
118
122
|
return ruleId.replace(/-js$/, "");
|
|
119
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* `filePath` relative to `root`, forward-slashed, for matching a rule's
|
|
126
|
+
* `ignores` globs (#965). Falls back to the absolute (slash-normalized) path
|
|
127
|
+
* when `filePath` isn't under `root` (e.g. an out-of-tree temp file), so a
|
|
128
|
+
* glob like `scripts/**` simply never matches rather than throwing.
|
|
129
|
+
*/
|
|
130
|
+
function relativeForIgnoreGlob(filePath, root) {
|
|
131
|
+
const rel = path.relative(root, filePath);
|
|
132
|
+
return (rel.startsWith("..") ? filePath : rel).split(path.sep).join("/");
|
|
133
|
+
}
|
|
134
|
+
function matchesRuleIgnores(filePath, root, patterns) {
|
|
135
|
+
if (!patterns || patterns.length === 0)
|
|
136
|
+
return false;
|
|
137
|
+
const rel = relativeForIgnoreGlob(filePath, root);
|
|
138
|
+
return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
|
|
139
|
+
}
|
|
120
140
|
export function canHandle(filePath) {
|
|
121
141
|
return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
|
|
122
142
|
}
|
|
123
143
|
/**
|
|
124
144
|
* The TypeScript grammar is a syntactic superset of JavaScript, so a
|
|
125
145
|
* `JavaScript`-tagged rule using generic node kinds (`variable_declarator`,
|
|
126
|
-
* `assignment_expression`, …) still matches against a parsed `.ts
|
|
127
|
-
*
|
|
146
|
+
* `assignment_expression`, …) still matches against a parsed `.ts` root
|
|
147
|
+
* — and vice versa isn't an issue since JS files never parse
|
|
128
148
|
* TS-only syntax, but a `TypeScript`-tagged rule with a plain-JS-compatible
|
|
129
149
|
* body would equally double-fire alongside a `JavaScript` twin on a `.ts`
|
|
130
150
|
* file. Without this, `language:` reads as a real filter but isn't one for
|
|
131
151
|
* ts↔js pairs, so twin rules sharing a base name (e.g. `hardcoded-url` /
|
|
132
152
|
* `hardcoded-url-js`) both match the same construct in the SAME runner
|
|
133
|
-
* invocation (#657).
|
|
134
|
-
*
|
|
153
|
+
* invocation (#657). TSX is deliberately its own grammar here: the primary
|
|
154
|
+
* ast-grep CLI/LSP also treats `tsx` as distinct from `typescript`, so a
|
|
155
|
+
* language-tagged rule only runs on the exact grammar it names. Returns
|
|
156
|
+
* undefined for extensions this scoping doesn't apply to (css/html), where
|
|
157
|
+
* no filtering is added.
|
|
135
158
|
*/
|
|
136
159
|
export function ruleLanguageForFile(filePath) {
|
|
137
160
|
const ext = path.extname(filePath).toLowerCase();
|
|
138
161
|
switch (ext) {
|
|
139
162
|
case ".ts":
|
|
140
|
-
case ".tsx":
|
|
141
163
|
return "typescript";
|
|
164
|
+
case ".tsx":
|
|
165
|
+
return "tsx";
|
|
142
166
|
case ".js":
|
|
143
167
|
case ".jsx":
|
|
144
168
|
return "javascript";
|
|
@@ -213,13 +237,36 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
213
237
|
const maxTotalDiagnostics = options.maxTotalDiagnostics ?? MAX_TOTAL_DIAGNOSTICS;
|
|
214
238
|
const blockingOnly = options.blockingOnly === true;
|
|
215
239
|
const log = options.log;
|
|
240
|
+
const unsupportedLanguageLog = options.unsupportedLanguageLog ?? new Set();
|
|
216
241
|
const diagnostics = [];
|
|
217
242
|
const seenRuleIds = new Set();
|
|
218
243
|
const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
|
|
219
244
|
const fileLang = ruleLanguageForFile(filePath);
|
|
245
|
+
// Unsupported-language skips are expected in bulk (every non-jsts rule in the
|
|
246
|
+
// catalog, e.g. ~30 Python rules) — aggregate them into ONE latency-log entry
|
|
247
|
+
// per evaluation instead of per-rule terminal lines (#282 follow-up).
|
|
248
|
+
const newlyUnsupported = new Map();
|
|
249
|
+
const flushUnsupportedRuleSkips = () => {
|
|
250
|
+
if (newlyUnsupported.size === 0)
|
|
251
|
+
return;
|
|
252
|
+
logLatency({
|
|
253
|
+
type: "phase",
|
|
254
|
+
phase: "astgrep_napi_unsupported_rules_skipped",
|
|
255
|
+
filePath,
|
|
256
|
+
durationMs: 0,
|
|
257
|
+
metadata: {
|
|
258
|
+
skippedByLanguage: Object.fromEntries(Array.from(newlyUnsupported.entries(), ([language, ruleIds]) => [
|
|
259
|
+
language,
|
|
260
|
+
{ count: ruleIds.length, ruleIds },
|
|
261
|
+
])),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
newlyUnsupported.clear();
|
|
265
|
+
};
|
|
220
266
|
// Shared with the raw sgconfig materializer so both surfaces walk the same
|
|
221
267
|
// workspace-rooted sources in the same precedence order.
|
|
222
|
-
const
|
|
268
|
+
const ignoreRoot = options.projectRoot ?? cwd;
|
|
269
|
+
const ruleSources = getAstGrepRuleSources(ignoreRoot);
|
|
223
270
|
for (const source of ruleSources) {
|
|
224
271
|
let rules;
|
|
225
272
|
try {
|
|
@@ -233,6 +280,7 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
233
280
|
}
|
|
234
281
|
const duplicates = duplicateRuleIds(rules);
|
|
235
282
|
if (appendDuplicateRuleDiagnostics(diagnostics, seenRuleIds, duplicates, source, filePath, maxTotalDiagnostics)) {
|
|
283
|
+
flushUnsupportedRuleSkips();
|
|
236
284
|
return diagnostics;
|
|
237
285
|
}
|
|
238
286
|
const duplicateSet = new Set(duplicates);
|
|
@@ -245,6 +293,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
245
293
|
seenRuleIds.add(rule.id);
|
|
246
294
|
if (blockingOnly && rule.severity !== "error")
|
|
247
295
|
continue;
|
|
296
|
+
// Per-rule path carve-out (#965): a rule that's noise on CLI scripts or
|
|
297
|
+
// a project's own logging sink (e.g. no-console-except-error firing
|
|
298
|
+
// inside scripts/** or lib/logger.ts) opts out via `ignores`.
|
|
299
|
+
if (matchesRuleIgnores(filePath, ignoreRoot, rule.ignores))
|
|
300
|
+
continue;
|
|
248
301
|
if (suppressLinterOverlap &&
|
|
249
302
|
LINTER_OVERLAP.has(normalizeRuleId(rule.id)) &&
|
|
250
303
|
!NON_SUPPRESSIBLE.has(normalizeRuleId(rule.id))) {
|
|
@@ -258,7 +311,16 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
258
311
|
continue;
|
|
259
312
|
}
|
|
260
313
|
const lang = rule.language?.toLowerCase();
|
|
261
|
-
if (lang &&
|
|
314
|
+
if (lang &&
|
|
315
|
+
lang !== "typescript" &&
|
|
316
|
+
lang !== "tsx" &&
|
|
317
|
+
lang !== "javascript") {
|
|
318
|
+
if (!unsupportedLanguageLog.has(rule.id)) {
|
|
319
|
+
unsupportedLanguageLog.add(rule.id);
|
|
320
|
+
const ids = newlyUnsupported.get(lang) ?? [];
|
|
321
|
+
ids.push(rule.id);
|
|
322
|
+
newlyUnsupported.set(lang, ids);
|
|
323
|
+
}
|
|
262
324
|
continue;
|
|
263
325
|
}
|
|
264
326
|
// Scope TypeScript/JavaScript-tagged rules to the file's actual
|
|
@@ -338,6 +400,7 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
338
400
|
}
|
|
339
401
|
}
|
|
340
402
|
}
|
|
403
|
+
flushUnsupportedRuleSkips();
|
|
341
404
|
return diagnostics;
|
|
342
405
|
}
|
|
343
406
|
// --- Runner Definition ---
|
|
@@ -357,7 +420,20 @@ const astGrepNapiRunner = {
|
|
|
357
420
|
// report against the LSP's `tool: ast-grep` diagnostics. Resume ONLY as the
|
|
358
421
|
// fallback when the binary is absent / can't spawn (Gate B).
|
|
359
422
|
const astGrepLspEnabled = enabledAuxiliaryLspServerIds((f) => ctx.pi?.getFlag?.(f)).includes("ast-grep");
|
|
360
|
-
|
|
423
|
+
// Gate B asks whether the LSP will handle this file, not whether a bare
|
|
424
|
+
// `ast-grep` command happens to be on PATH. The launcher first tries the
|
|
425
|
+
// platform-native package binary, then PATH; mirror that resolution here.
|
|
426
|
+
// A live client covers the already-warm case, while a resolvable binary
|
|
427
|
+
// covers the cold case before the LSP has spawned for this root.
|
|
428
|
+
const astGrepLspAlive = astGrepLspEnabled
|
|
429
|
+
? await isAuxiliaryLspAlive("ast-grep", ctx.filePath)
|
|
430
|
+
: false;
|
|
431
|
+
let astGrepBinaryResolvable = false;
|
|
432
|
+
if (astGrepLspEnabled && !astGrepLspAlive) {
|
|
433
|
+
astGrepBinaryResolvable =
|
|
434
|
+
Boolean(resolveAstGrepNativeExe()) || (await ctx.hasTool("ast-grep"));
|
|
435
|
+
}
|
|
436
|
+
if (astGrepLspEnabled && (astGrepLspAlive || astGrepBinaryResolvable)) {
|
|
361
437
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
362
438
|
}
|
|
363
439
|
const sgModule = await loadSg();
|
|
@@ -19,7 +19,16 @@ let psAnalyzerAvailable = undefined;
|
|
|
19
19
|
const PS_TIMEOUT_MS = 30000;
|
|
20
20
|
function spawnPs(cmd, args, timeoutMs = PS_TIMEOUT_MS) {
|
|
21
21
|
return new Promise((resolve) => {
|
|
22
|
-
|
|
22
|
+
let child;
|
|
23
|
+
try {
|
|
24
|
+
child = spawn(cmd, args, { windowsHide: true, shell: false });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the pidusage
|
|
28
|
+
// bug class, #533) — this best-effort runner must resolve, not reject.
|
|
29
|
+
resolve({ stdout: "", stderr: "", status: null });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
23
32
|
let stdout = "";
|
|
24
33
|
let stderr = "";
|
|
25
34
|
let settled = false;
|