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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logLatency } from "../../latency-logger.js";
|
|
2
|
-
import { childrenOfType, firstChildOfType,
|
|
2
|
+
import { childrenOfType, firstChildOfType, withFactTree, walk, } from "./tree-sitter-facts.js";
|
|
3
3
|
// JS/TS extensions this provider handles (→ tree-sitter typescript/tsx/javascript
|
|
4
4
|
// grammars via resolveTreeSitterLanguage). Ported off the `typescript` compiler API
|
|
5
5
|
// onto tree-sitter (#402) — the parse is served from the shared, cached client.
|
|
@@ -96,115 +96,114 @@ export const importFactProvider = {
|
|
|
96
96
|
}
|
|
97
97
|
// Grammar unavailable / parse failed / wasm aborted — degrade to empty
|
|
98
98
|
// (there is no typescript-compiler fallback by design, #402).
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for (const child of root.children ?? []) {
|
|
110
|
-
if (!child)
|
|
111
|
-
continue;
|
|
112
|
-
if (child.type === "import_statement") {
|
|
113
|
-
hasEsm = true;
|
|
114
|
-
importNodes.push(child);
|
|
115
|
-
}
|
|
116
|
-
else if (child.type === "export_statement") {
|
|
117
|
-
const isReExport = Boolean(firstChildOfType(child, "string"));
|
|
118
|
-
// `export { x }` / `export * from` / `export { x } from` are ESM
|
|
119
|
-
// declarations (like TS's isExportDeclaration); `export const x` /
|
|
120
|
-
// `export default` (a wrapped declaration) is NOT counted here.
|
|
121
|
-
if (isReExport || firstChildOfType(child, "export_clause")) {
|
|
99
|
+
const parsed = await withFactTree(ctx.filePath, content, (root) => {
|
|
100
|
+
// --- module-type detection + static import/re-export node collection ---
|
|
101
|
+
let hasEsm = false;
|
|
102
|
+
let hasCjs = false;
|
|
103
|
+
const importNodes = [];
|
|
104
|
+
const reExportNodes = [];
|
|
105
|
+
for (const child of root.children ?? []) {
|
|
106
|
+
if (!child)
|
|
107
|
+
continue;
|
|
108
|
+
if (child.type === "import_statement") {
|
|
122
109
|
hasEsm = true;
|
|
110
|
+
importNodes.push(child);
|
|
111
|
+
}
|
|
112
|
+
else if (child.type === "export_statement") {
|
|
113
|
+
const isReExport = Boolean(firstChildOfType(child, "string"));
|
|
114
|
+
// `export { x }` / `export * from` / `export { x } from` are ESM
|
|
115
|
+
// declarations (like TS's isExportDeclaration); `export const x` /
|
|
116
|
+
// `export default` (a wrapped declaration) is NOT counted here.
|
|
117
|
+
if (isReExport || firstChildOfType(child, "export_clause")) {
|
|
118
|
+
hasEsm = true;
|
|
119
|
+
}
|
|
120
|
+
if (isReExport)
|
|
121
|
+
reExportNodes.push(child);
|
|
123
122
|
}
|
|
124
|
-
if (isReExport)
|
|
125
|
-
reExportNodes.push(child);
|
|
126
123
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
// --- dynamic import() / require() / module.exports (anywhere in the tree) ---
|
|
125
|
+
const dynamicRaw = [];
|
|
126
|
+
walk(root, (node) => {
|
|
127
|
+
if (node.type === "call_expression") {
|
|
128
|
+
const callee = node.children?.[0];
|
|
129
|
+
const args = firstChildOfType(node, "arguments");
|
|
130
|
+
const strArg = args ? firstChildOfType(args, "string") : undefined;
|
|
131
|
+
if (!strArg)
|
|
132
|
+
return; // non-string arg (e.g. template literal) — skip
|
|
133
|
+
const source = stripQuotes(strArg.text);
|
|
134
|
+
if (callee?.type === "import") {
|
|
135
|
+
dynamicRaw.push({ source, kind: "import" });
|
|
136
|
+
}
|
|
137
|
+
else if (callee?.type === "identifier" && callee.text === "require") {
|
|
138
|
+
dynamicRaw.push({ source, kind: "require" });
|
|
139
|
+
hasCjs = true;
|
|
140
|
+
}
|
|
140
141
|
}
|
|
141
|
-
else if (
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
else if (node.type === "member_expression") {
|
|
143
|
+
// module.exports = ...
|
|
144
|
+
const obj = node.children?.[0];
|
|
145
|
+
if (obj?.type === "identifier" && obj.text === "module") {
|
|
146
|
+
const prop = firstChildOfType(node, "property_identifier");
|
|
147
|
+
if (prop?.text === "exports")
|
|
148
|
+
hasCjs = true;
|
|
149
|
+
}
|
|
144
150
|
}
|
|
151
|
+
});
|
|
152
|
+
const moduleType = hasEsm && !hasCjs
|
|
153
|
+
? "esm"
|
|
154
|
+
: hasCjs && !hasEsm
|
|
155
|
+
? "cjs"
|
|
156
|
+
: hasEsm || hasCjs
|
|
157
|
+
? "esm" // mixed — static imports present, treat as ESM
|
|
158
|
+
: "unknown";
|
|
159
|
+
// --- build entries ---
|
|
160
|
+
const imports = [];
|
|
161
|
+
for (const node of importNodes) {
|
|
162
|
+
const entry = parseStaticImport(node, moduleType);
|
|
163
|
+
if (entry)
|
|
164
|
+
imports.push(entry);
|
|
145
165
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
let dynamicCount = 0;
|
|
167
|
+
for (const d of dynamicRaw) {
|
|
168
|
+
if (d.kind === "import") {
|
|
169
|
+
imports.push({
|
|
170
|
+
source: d.source,
|
|
171
|
+
names: [],
|
|
172
|
+
isDynamic: true,
|
|
173
|
+
moduleType,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
imports.push({ source: d.source, names: [], moduleType: "cjs" });
|
|
153
178
|
}
|
|
179
|
+
dynamicCount++;
|
|
154
180
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
? "esm" // mixed — static imports present, treat as ESM
|
|
162
|
-
: "unknown";
|
|
163
|
-
// --- build entries ---
|
|
164
|
-
const imports = [];
|
|
165
|
-
for (const node of importNodes) {
|
|
166
|
-
const entry = parseStaticImport(node, moduleType);
|
|
167
|
-
if (entry)
|
|
168
|
-
imports.push(entry);
|
|
169
|
-
}
|
|
170
|
-
let dynamicCount = 0;
|
|
171
|
-
for (const d of dynamicRaw) {
|
|
172
|
-
if (d.kind === "import") {
|
|
173
|
-
imports.push({
|
|
174
|
-
source: d.source,
|
|
175
|
-
names: [],
|
|
176
|
-
isDynamic: true,
|
|
177
|
-
moduleType,
|
|
178
|
-
});
|
|
181
|
+
store.setFileFact(ctx.filePath, "file.imports", imports);
|
|
182
|
+
const reexports = [];
|
|
183
|
+
for (const node of reExportNodes) {
|
|
184
|
+
const r = parseReExport(node);
|
|
185
|
+
if (r)
|
|
186
|
+
reexports.push(r);
|
|
179
187
|
}
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
store.setFileFact(ctx.filePath, "file.reexports", reexports);
|
|
189
|
+
// Telemetry: log when a file has dynamic imports or re-exports so we can
|
|
190
|
+
// measure coverage and validate the implementation across real projects.
|
|
191
|
+
if (dynamicCount > 0 || reexports.length > 0) {
|
|
192
|
+
logLatency({
|
|
193
|
+
type: "call_graph_facts",
|
|
194
|
+
filePath: ctx.filePath,
|
|
195
|
+
durationMs: 0,
|
|
196
|
+
metadata: {
|
|
197
|
+
moduleType,
|
|
198
|
+
staticImports: imports.length - dynamicCount,
|
|
199
|
+
dynamicImports: dynamicCount,
|
|
200
|
+
reexports: reexports.length,
|
|
201
|
+
starReexports: reexports.filter((r) => r.names.length === 0).length,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
182
204
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const reexports = [];
|
|
187
|
-
for (const node of reExportNodes) {
|
|
188
|
-
const r = parseReExport(node);
|
|
189
|
-
if (r)
|
|
190
|
-
reexports.push(r);
|
|
191
|
-
}
|
|
192
|
-
store.setFileFact(ctx.filePath, "file.reexports", reexports);
|
|
193
|
-
// Telemetry: log when a file has dynamic imports or re-exports so we can
|
|
194
|
-
// measure coverage and validate the implementation across real projects.
|
|
195
|
-
if (dynamicCount > 0 || reexports.length > 0) {
|
|
196
|
-
logLatency({
|
|
197
|
-
type: "call_graph_facts",
|
|
198
|
-
filePath: ctx.filePath,
|
|
199
|
-
durationMs: 0,
|
|
200
|
-
metadata: {
|
|
201
|
-
moduleType,
|
|
202
|
-
staticImports: imports.length - dynamicCount,
|
|
203
|
-
dynamicImports: dynamicCount,
|
|
204
|
-
reexports: reexports.length,
|
|
205
|
-
starReexports: reexports.filter((r) => r.names.length === 0).length,
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
}
|
|
205
|
+
});
|
|
206
|
+
if (!parsed.parsed)
|
|
207
|
+
setEmpty();
|
|
209
208
|
},
|
|
210
209
|
};
|
|
@@ -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,6 +9,7 @@
|
|
|
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";
|
|
13
14
|
import { logLatency } from "../../latency-logger.js";
|
|
14
15
|
import { hasEslintConfig } from "../../tool-policy.js";
|
|
@@ -120,6 +121,22 @@ function explicitRuleFixSuggestion(rule) {
|
|
|
120
121
|
function normalizeRuleId(ruleId) {
|
|
121
122
|
return ruleId.replace(/-js$/, "");
|
|
122
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
|
+
}
|
|
123
140
|
export function canHandle(filePath) {
|
|
124
141
|
return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
|
|
125
142
|
}
|
|
@@ -248,7 +265,8 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
248
265
|
};
|
|
249
266
|
// Shared with the raw sgconfig materializer so both surfaces walk the same
|
|
250
267
|
// workspace-rooted sources in the same precedence order.
|
|
251
|
-
const
|
|
268
|
+
const ignoreRoot = options.projectRoot ?? cwd;
|
|
269
|
+
const ruleSources = getAstGrepRuleSources(ignoreRoot);
|
|
252
270
|
for (const source of ruleSources) {
|
|
253
271
|
let rules;
|
|
254
272
|
try {
|
|
@@ -275,6 +293,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
275
293
|
seenRuleIds.add(rule.id);
|
|
276
294
|
if (blockingOnly && rule.severity !== "error")
|
|
277
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;
|
|
278
301
|
if (suppressLinterOverlap &&
|
|
279
302
|
LINTER_OVERLAP.has(normalizeRuleId(rule.id)) &&
|
|
280
303
|
!NON_SUPPRESSIBLE.has(normalizeRuleId(rule.id))) {
|
|
@@ -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;
|