pi-lens 3.8.69 → 3.8.71
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 +99 -0
- package/README.md +31 -13
- package/dist/clients/bootstrap.js +3 -1
- package/dist/clients/bus-events-logger.js +53 -0
- package/dist/clients/bus-publish.js +45 -2
- package/dist/clients/cache/rule-cache.js +9 -5
- package/dist/clients/cache-manager.js +19 -13
- package/dist/clients/code-quality-warnings.js +27 -0
- package/dist/clients/dead-code-client.js +5 -23
- package/dist/clients/diagnostic-dispositions.js +298 -0
- package/dist/clients/diagnostics-publish.js +38 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +144 -6
- package/dist/clients/dispatch/dispatcher.js +6 -1
- package/dist/clients/dispatch/facts/function-facts.js +83 -0
- package/dist/clients/dispatch/integration.js +19 -4
- package/dist/clients/dispatch/rules/async-noise.js +1 -1
- package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
- package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
- package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
- package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
- package/dist/clients/dispatch/rules/high-complexity.js +12 -1
- package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
- package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
- package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
- package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
- package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
- package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
- package/dist/clients/dispatch/runners/lsp.js +23 -34
- package/dist/clients/dispatch/runners/oxlint.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +68 -0
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +4 -1
- package/dist/clients/disposition-logger.js +39 -0
- package/dist/clients/disposition-publish.js +109 -0
- package/dist/clients/file-utils.js +94 -33
- package/dist/clients/format-events-publish.js +299 -0
- package/dist/clients/git-tracked-ignore.js +208 -0
- package/dist/clients/gitleaks-client.js +40 -5
- package/dist/clients/installer/index.js +123 -6
- package/dist/clients/instance-reaper.js +229 -2
- package/dist/clients/instance-registry.js +141 -2
- package/dist/clients/json-cache-read.js +65 -0
- package/dist/clients/knip-client.js +35 -38
- package/dist/clients/language-profile.js +11 -11
- package/dist/clients/lens-engine.js +12 -0
- package/dist/clients/lens-map.js +1079 -0
- package/dist/clients/log-cleanup.js +61 -19
- package/dist/clients/lsp/aggregation.js +69 -2
- package/dist/clients/lsp/cascade-tier.js +31 -19
- package/dist/clients/lsp/client.js +124 -14
- package/dist/clients/lsp/config.js +27 -1
- package/dist/clients/lsp/index.js +1028 -68
- package/dist/clients/lsp/server-strategies.js +17 -8
- package/dist/clients/lsp/server.js +80 -28
- package/dist/clients/lsp/tsserver-sync.js +145 -0
- package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
- package/dist/clients/lsp-budget.js +148 -0
- package/dist/clients/mcp/session.js +9 -0
- package/dist/clients/middle-man-analysis.js +260 -0
- package/dist/clients/module-report.js +173 -11
- package/dist/clients/ndjson-logger.js +24 -0
- package/dist/clients/opengrep-client.js +217 -0
- package/dist/clients/opengrep-config.js +2 -10
- package/dist/clients/path-utils.js +66 -0
- package/dist/clients/pipeline.js +5 -1
- package/dist/clients/project-diagnostics/cache.js +5 -12
- package/dist/clients/project-diagnostics/extractors.js +21 -1
- package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
- package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
- package/dist/clients/project-snapshot.js +3 -6
- package/dist/clients/quiet-window.js +57 -2
- package/dist/clients/read-expansion.js +2 -3
- package/dist/clients/read-guard-tool-lines.js +10 -0
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/resource-sampler.js +254 -0
- package/dist/clients/review-graph/builder.js +322 -42
- package/dist/clients/review-graph/import-resolvers.js +53 -19
- package/dist/clients/review-graph/symbol-id.js +57 -0
- package/dist/clients/runtime-agent-end.js +26 -0
- package/dist/clients/runtime-coordinator.js +9 -1
- package/dist/clients/runtime-session.js +93 -17
- package/dist/clients/runtime-tool-call.js +789 -0
- package/dist/clients/runtime-tool-result.js +15 -2
- package/dist/clients/runtime-turn.js +62 -13
- package/dist/clients/safe-spawn.js +48 -2
- package/dist/clients/session-state-store.js +6 -9
- package/dist/clients/sgconfig.js +2 -9
- package/dist/clients/source-filter.js +17 -25
- package/dist/clients/source-walker.js +66 -0
- package/dist/clients/startup-scan.js +105 -43
- package/dist/clients/symbol-containment.js +24 -0
- package/dist/clients/test-runner-client.js +544 -29
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/typos-config.js +2 -11
- package/dist/clients/widget-state.js +83 -1
- package/dist/clients/write-ordering-guard.js +58 -0
- package/dist/clients/zizmor-config.js +29 -10
- package/dist/index.js +25511 -19414
- package/dist/mcp/server.js +95 -19
- package/dist/tools/activate-tools.js +72 -0
- package/dist/tools/ast-dump.js +2 -8
- package/dist/tools/lens-diagnostic-mark.js +158 -0
- package/dist/tools/lens-diagnostics.js +327 -33
- package/dist/tools/lsp-diagnostics.js +525 -105
- package/dist/tools/module-report.js +18 -5
- package/docs/agent-tools.md +136 -0
- package/docs/ast-grep_rules_catalog.md +564 -0
- package/docs/astplayground.md +178 -0
- package/docs/audit1.md +177 -0
- package/docs/custom-rules.md +231 -0
- package/docs/dependencies.md +54 -0
- package/docs/dispositions.md +85 -0
- package/docs/env_variables.md +10 -0
- package/docs/environment-variables.md +83 -0
- package/docs/fable.md +140 -0
- package/docs/features.md +338 -0
- package/docs/globalconfig.md +79 -0
- package/docs/language-coverage.md +48 -0
- package/docs/lsp-capability-matrix.md +157 -0
- package/docs/lsp-latency-benchmark.md +169 -0
- package/docs/mcp.md +438 -0
- package/docs/module-report-read-symbol.md +258 -0
- package/docs/servercapabilities.md +118 -0
- package/docs/subagent-compat.md +281 -0
- package/docs/tools.md +16 -0
- package/docs/tools_improvement2.md +345 -0
- package/docs/tree-sitter_rules_catalog.md +296 -0
- package/docs/usage.md +129 -0
- package/docs/word-index.md +150 -0
- package/package.json +10 -1
- package/rules/ast-grep-rules/rule-tests/find-import-file-without-extension-test.yml +4 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +5 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +9 -0
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -1
- package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +13 -1
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
- package/skills/pi-lens-ast-grep/SKILL.md +3 -1
- package/dist/clients/production-readiness.js +0 -493
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findOwnerName } from "../../symbol-containment.js";
|
|
1
2
|
import { extractFactsFromTree, firstChildOfType, walk, } from "./tree-sitter-facts.js";
|
|
2
3
|
const BOUNDARY_PREFIXES = [
|
|
3
4
|
"fetch",
|
|
@@ -160,6 +161,62 @@ function collectOutgoingCalls(body) {
|
|
|
160
161
|
});
|
|
161
162
|
return [...calls];
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* `obj.method()` call sites with a simple-identifier receiver (refs #655
|
|
166
|
+
* phase 2). Only the plain shape `identifier.identifier(...)` is captured —
|
|
167
|
+
* chained (`a.b.c()`), computed (`a[x]()`), and `this.`-receiver calls are
|
|
168
|
+
* left out of this list (they still show up in `outgoingCalls`'s flattened
|
|
169
|
+
* text form as before); those richer shapes need real type inference to
|
|
170
|
+
* resolve safely, which is out of scope for this bounded slice.
|
|
171
|
+
*/
|
|
172
|
+
function collectMemberCallSites(body) {
|
|
173
|
+
const sites = [];
|
|
174
|
+
walk(body, (node) => {
|
|
175
|
+
if (node.type !== "call_expression")
|
|
176
|
+
return;
|
|
177
|
+
const callee = (node.children ?? [])[0];
|
|
178
|
+
if (!callee || callee.type !== "member_expression")
|
|
179
|
+
return;
|
|
180
|
+
const object = (callee.children ?? [])[0];
|
|
181
|
+
const property = (callee.children ?? []).find((c) => c?.type === "property_identifier");
|
|
182
|
+
if (!object || object.type !== "identifier" || !property)
|
|
183
|
+
return;
|
|
184
|
+
sites.push({ receiver: object.text, method: property.text });
|
|
185
|
+
});
|
|
186
|
+
return sites;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Best-effort receiver -> class-name map for one function (refs #655 phase
|
|
190
|
+
* 2). Two clear, common shapes only — see {@link FunctionSummary.receiverTypes}.
|
|
191
|
+
*/
|
|
192
|
+
function collectReceiverTypes(body, params) {
|
|
193
|
+
const types = {};
|
|
194
|
+
for (const param of params) {
|
|
195
|
+
const id = firstNamedChild(param);
|
|
196
|
+
if (!id || id.type !== "identifier")
|
|
197
|
+
continue;
|
|
198
|
+
const typeAnnotation = (param.children ?? []).find((c) => c?.type === "type_annotation");
|
|
199
|
+
const typeId = typeAnnotation
|
|
200
|
+
? firstChildOfType(typeAnnotation, "type_identifier")
|
|
201
|
+
: undefined;
|
|
202
|
+
if (typeId)
|
|
203
|
+
types[id.text] = typeId.text;
|
|
204
|
+
}
|
|
205
|
+
walk(body, (node) => {
|
|
206
|
+
if (node.type !== "variable_declarator")
|
|
207
|
+
return;
|
|
208
|
+
const id = firstChildOfType(node, "identifier");
|
|
209
|
+
if (!id)
|
|
210
|
+
return;
|
|
211
|
+
const value = (node.children ?? []).find((c) => c?.type === "new_expression");
|
|
212
|
+
if (!value)
|
|
213
|
+
return;
|
|
214
|
+
const ctor = firstNamedChild(value);
|
|
215
|
+
if (ctor?.type === "identifier")
|
|
216
|
+
types[id.text] = ctor.text;
|
|
217
|
+
});
|
|
218
|
+
return types;
|
|
219
|
+
}
|
|
163
220
|
function hasAwaitInNode(node) {
|
|
164
221
|
let found = false;
|
|
165
222
|
walk(node, (n) => {
|
|
@@ -192,6 +249,13 @@ export const functionFactProvider = {
|
|
|
192
249
|
async run(ctx, store) {
|
|
193
250
|
await extractFactsFromTree(ctx, store, { "file.functionSummaries": [] }, (root) => {
|
|
194
251
|
const summaries = [];
|
|
252
|
+
// Class/interface declarations, collected in the SAME walk over the
|
|
253
|
+
// already-parsed tree (refs #655 phase 2) so owner/qualified-name
|
|
254
|
+
// computation for jsts needs no second parse — see
|
|
255
|
+
// `symbol-containment.ts`'s doc comment for why this can't literally
|
|
256
|
+
// share code with module-report.ts's tree-sitter-symbol-extractor path
|
|
257
|
+
// (different tree-sitter integration), only the same algorithm.
|
|
258
|
+
const containers = [];
|
|
195
259
|
const addSummary = (node) => {
|
|
196
260
|
const body = getBody(node);
|
|
197
261
|
if (!body)
|
|
@@ -210,6 +274,7 @@ export const functionFactProvider = {
|
|
|
210
274
|
name: getFunctionName(node),
|
|
211
275
|
line: node.startPosition.row + 1,
|
|
212
276
|
column: node.startPosition.column + 1,
|
|
277
|
+
endLine: body.endPosition.row + 1,
|
|
213
278
|
isAsync: Boolean(firstChildOfType(node, "async")),
|
|
214
279
|
hasAwait: hasAwaitInNode(body),
|
|
215
280
|
hasReturnAwaitCall: hasReturnAwaitCall(body),
|
|
@@ -221,12 +286,30 @@ export const functionFactProvider = {
|
|
|
221
286
|
cyclomaticComplexity: calcCyclomaticComplexity(body),
|
|
222
287
|
maxNestingDepth: calcMaxNestingDepth(body),
|
|
223
288
|
outgoingCalls: collectOutgoingCalls(body),
|
|
289
|
+
memberCallSites: collectMemberCallSites(body),
|
|
290
|
+
receiverTypes: collectReceiverTypes(body, params),
|
|
224
291
|
});
|
|
225
292
|
};
|
|
226
293
|
walk(root, (node) => {
|
|
227
294
|
if (FUNCTION_TYPES.has(node.type))
|
|
228
295
|
addSummary(node);
|
|
296
|
+
if (node.type === "class_declaration" ||
|
|
297
|
+
node.type === "interface_declaration") {
|
|
298
|
+
const nameNode = firstChildOfType(node, "type_identifier");
|
|
299
|
+
if (nameNode) {
|
|
300
|
+
containers.push({
|
|
301
|
+
name: nameNode.text,
|
|
302
|
+
startLine: node.startPosition.row + 1,
|
|
303
|
+
endLine: node.endPosition.row + 1,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
229
307
|
});
|
|
308
|
+
for (const summary of summaries) {
|
|
309
|
+
const owner = findOwnerName(containers, summary.line, summary.endLine ?? summary.line);
|
|
310
|
+
if (owner)
|
|
311
|
+
summary.owner = owner;
|
|
312
|
+
}
|
|
230
313
|
return { "file.functionSummaries": summaries };
|
|
231
314
|
});
|
|
232
315
|
},
|
|
@@ -821,7 +821,13 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
821
821
|
coldSnapshotPaths.push(neighborPath);
|
|
822
822
|
continue;
|
|
823
823
|
}
|
|
824
|
-
|
|
824
|
+
// #692: `source: "cascade"` used to be passed here to label `rule`
|
|
825
|
+
// (`cascade:<code>`) — that override is gone (identity must come from
|
|
826
|
+
// the diagnostic's own source; see `scanOrigin`'s doc comment), and
|
|
827
|
+
// cascade neighbor diagnostics are ephemeral display-only output
|
|
828
|
+
// (never reconciled into persisted widget/dedup state), so the label
|
|
829
|
+
// had no remaining purpose and is simply dropped rather than migrated.
|
|
830
|
+
const diags = convertLspDiagnostics(entry.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
|
|
825
831
|
producedLspData = true;
|
|
826
832
|
const durationMs = Date.now() - neighborStart;
|
|
827
833
|
logCascade({
|
|
@@ -994,7 +1000,11 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
994
1000
|
});
|
|
995
1001
|
if (!rawDiags)
|
|
996
1002
|
return undefined;
|
|
997
|
-
|
|
1003
|
+
// #692: `source: "cascade"` no longer overrides `rule` (see the
|
|
1004
|
+
// doc comment on the sibling call above) — dropped rather than
|
|
1005
|
+
// migrated to `scanOrigin` since cascade output never touches
|
|
1006
|
+
// persisted widget/dedup state.
|
|
1007
|
+
const diags = convertLspDiagnostics(rawDiags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
|
|
998
1008
|
const durationMs = Date.now() - neighborStart;
|
|
999
1009
|
// Update cache for this neighbor at the current write sequence
|
|
1000
1010
|
if (writeSeq != null) {
|
|
@@ -1051,10 +1061,13 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1051
1061
|
error: String(result.reason),
|
|
1052
1062
|
});
|
|
1053
1063
|
const entry = allDiags.get(normalizeMapKey(neighborPath));
|
|
1064
|
+
// #692: `source: "cascade"` dropped (see the doc comment above the
|
|
1065
|
+
// first cascade call site in this file) — no longer affects `rule`
|
|
1066
|
+
// and cascade output never touches persisted widget/dedup state.
|
|
1054
1067
|
const diags = entry && Date.now() - entry.ts < CASCADE_TTL_MS
|
|
1055
1068
|
? convertLspDiagnostics(entry.diags
|
|
1056
1069
|
.filter((d) => d.severity === 1)
|
|
1057
|
-
.slice(0, MAX_PER_FILE), neighborPath
|
|
1070
|
+
.slice(0, MAX_PER_FILE), neighborPath)
|
|
1058
1071
|
: [];
|
|
1059
1072
|
neighbors.push({
|
|
1060
1073
|
filePath: neighborPath,
|
|
@@ -1162,7 +1175,9 @@ function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd) {
|
|
|
1162
1175
|
continue;
|
|
1163
1176
|
if (now - ts > CASCADE_TTL_MS)
|
|
1164
1177
|
continue;
|
|
1165
|
-
|
|
1178
|
+
// #692: `source: "cascade"` dropped — see the doc comment above the
|
|
1179
|
+
// first cascade `convertLspDiagnostics` call site in this file.
|
|
1180
|
+
const errors = convertLspDiagnostics(diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), diagPath);
|
|
1166
1181
|
if (errors.length === 0)
|
|
1167
1182
|
continue;
|
|
1168
1183
|
neighbors.push({
|
|
@@ -12,7 +12,7 @@ export const asyncUnnecessaryWrapperRule = {
|
|
|
12
12
|
continue;
|
|
13
13
|
diagnostics.push({
|
|
14
14
|
id: `async-unnecessary-wrapper:${ctx.filePath}:${f.line}`,
|
|
15
|
-
tool: "
|
|
15
|
+
tool: "fact-rules",
|
|
16
16
|
rule: "async-unnecessary-wrapper",
|
|
17
17
|
filePath: ctx.filePath,
|
|
18
18
|
line: f.line,
|
|
@@ -16,7 +16,8 @@ export const errorObscuringRule = {
|
|
|
16
16
|
!s.bodyText.includes(s.catchParam)) {
|
|
17
17
|
diagnostics.push({
|
|
18
18
|
id: `error-obscuring:${ctx.filePath}:${s.line}:${s.column}`,
|
|
19
|
-
tool: "
|
|
19
|
+
tool: "fact-rules",
|
|
20
|
+
rule: "error-obscuring",
|
|
20
21
|
filePath: ctx.filePath,
|
|
21
22
|
line: s.line,
|
|
22
23
|
column: s.column,
|
|
@@ -13,7 +13,8 @@ export const errorSwallowingRule = {
|
|
|
13
13
|
if (s.isEmpty && !s.isDocumentedLocalFallback && s.boundaryCategory !== "fs") {
|
|
14
14
|
diagnostics.push({
|
|
15
15
|
id: `error-swallowing:${ctx.filePath}:${s.line}:${s.column}`,
|
|
16
|
-
tool: "
|
|
16
|
+
tool: "fact-rules",
|
|
17
|
+
rule: "error-swallowing",
|
|
17
18
|
filePath: ctx.filePath,
|
|
18
19
|
line: s.line,
|
|
19
20
|
column: s.column,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test-framework-call recognition for `high-fan-out` and `high-complexity`
|
|
3
|
+
* (#577). A normal `it()`/`describe()` body naturally invokes many assertion/mock
|
|
4
|
+
* helpers — that's test structure, not a coordination smell or genuine complexity.
|
|
5
|
+
*
|
|
6
|
+
* Two complementary heuristics, both call-name-based (no `ctx.fileRole` gating —
|
|
7
|
+
* these calls only exist in test-framework code, so the signal is safe to apply
|
|
8
|
+
* unconditionally):
|
|
9
|
+
*
|
|
10
|
+
* 1. `isTestFrameworkNoiseCall` — filters individual "meaningful calls" the same
|
|
11
|
+
* way `console.*`/`Math.*`/etc. are already excluded in high-fan-out. Handles
|
|
12
|
+
* assertion-heavy `it()` bodies directly.
|
|
13
|
+
* 2. `isTestSuiteOrganizer` — a `describe()`/`it()`/`test()` wrapper's own
|
|
14
|
+
* FunctionSummary aggregates every call and branch from ALL of its nested test
|
|
15
|
+
* bodies (the shared tree-sitter walk in function-facts.ts does not stop at
|
|
16
|
+
* nested-function boundaries, since each nested callback also gets its own,
|
|
17
|
+
* correctly-scoped, FunctionSummary). That aggregation is not real fan-out or
|
|
18
|
+
* complexity — it's the sum of independent nested tests. A function is an
|
|
19
|
+
* "organizer" if it directly invokes `it`/`test`/`describe` (or their
|
|
20
|
+
* `.only`/`.skip`/`.each` variants) — i.e. it exists to group other tests, not
|
|
21
|
+
* to implement logic itself. This does NOT exempt genuinely complex/tangled
|
|
22
|
+
* test HELPER functions, since those don't call `it`/`describe`/`test`
|
|
23
|
+
* themselves.
|
|
24
|
+
*/
|
|
25
|
+
const EXPECT_CHAIN_PREFIX = "expect(";
|
|
26
|
+
const TEST_LIFECYCLE_NAMES = new Set([
|
|
27
|
+
"it",
|
|
28
|
+
"test",
|
|
29
|
+
"describe",
|
|
30
|
+
"beforeeach",
|
|
31
|
+
"aftereach",
|
|
32
|
+
"beforeall",
|
|
33
|
+
"afterall",
|
|
34
|
+
]);
|
|
35
|
+
const TEST_LIFECYCLE_PREFIXES = ["it.", "test.", "describe."];
|
|
36
|
+
const MOCK_LIBRARY_PREFIXES = ["vi.", "jest."];
|
|
37
|
+
/**
|
|
38
|
+
* True if `call` (a raw callee string from `FunctionSummary.outgoingCalls`) is
|
|
39
|
+
* test-framework noise: an `expect(...)` assertion chain, a test lifecycle call
|
|
40
|
+
* (`it`/`describe`/`test`/`beforeEach`/etc., including `.only`/`.skip`/`.each`
|
|
41
|
+
* variants), or a mock-library call (`vi.*`/`jest.*`).
|
|
42
|
+
*/
|
|
43
|
+
export function isTestFrameworkNoiseCall(call) {
|
|
44
|
+
const lower = call.toLowerCase();
|
|
45
|
+
if (lower === "expect" || lower.startsWith(EXPECT_CHAIN_PREFIX))
|
|
46
|
+
return true;
|
|
47
|
+
if (TEST_LIFECYCLE_NAMES.has(lower))
|
|
48
|
+
return true;
|
|
49
|
+
if (TEST_LIFECYCLE_PREFIXES.some((p) => lower.startsWith(p)))
|
|
50
|
+
return true;
|
|
51
|
+
if (MOCK_LIBRARY_PREFIXES.some((p) => lower.startsWith(p)))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* True if this function's (unfiltered) outgoing calls include a direct call to
|
|
57
|
+
* `it`/`test`/`describe` — i.e. it groups nested test bodies rather than
|
|
58
|
+
* implementing logic itself. Its aggregated fan-out/complexity reflects the sum
|
|
59
|
+
* of independent nested tests, not one function's real signal.
|
|
60
|
+
*/
|
|
61
|
+
export function isTestSuiteOrganizer(calls) {
|
|
62
|
+
return calls.some((call) => {
|
|
63
|
+
const lower = call.toLowerCase();
|
|
64
|
+
const base = lower.split("(")[0].split(".")[0];
|
|
65
|
+
return base === "it" || base === "test" || base === "describe";
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTestSuiteOrganizer } from "./framework-call-noise.js";
|
|
1
2
|
// Defaults match the historical hardcoded values so behavior is unchanged for
|
|
2
3
|
// projects without a config. Project-specific overrides are read from the
|
|
3
4
|
// per-dispatch context; these mutable fallbacks exist for tests/legacy direct
|
|
@@ -35,6 +36,16 @@ export const highComplexityRule = {
|
|
|
35
36
|
const configuredCcThreshold = ctx.projectConfig?.rules["high-complexity"]?.threshold;
|
|
36
37
|
const activeCcThreshold = configuredCcThreshold ?? ccThreshold;
|
|
37
38
|
for (const f of fns) {
|
|
39
|
+
// A describe()/it()/test() wrapper's own complexity/nesting-depth
|
|
40
|
+
// aggregates branches from ALL of its nested test bodies (#577) — e.g. a
|
|
41
|
+
// `for` loop inside each of several `it()`s sums into the enclosing
|
|
42
|
+
// `describe()`'s cyclomatic complexity, even though no single test is
|
|
43
|
+
// complex. Unlike high-fan-out's call-name filter, there's no per-call
|
|
44
|
+
// signal to exclude for branch counting, so the equivalent fix here is to
|
|
45
|
+
// skip the organizer function itself. Genuinely complex test HELPER
|
|
46
|
+
// functions don't call it/describe/test themselves, so they're unaffected.
|
|
47
|
+
if (isTestSuiteOrganizer(f.outgoingCalls))
|
|
48
|
+
continue;
|
|
38
49
|
const ccBreached = f.cyclomaticComplexity >= activeCcThreshold;
|
|
39
50
|
const depthBreached = f.maxNestingDepth >= depthThreshold;
|
|
40
51
|
if (!ccBreached && !depthBreached)
|
|
@@ -46,7 +57,7 @@ export const highComplexityRule = {
|
|
|
46
57
|
parts.push(`nesting depth ${f.maxNestingDepth}`);
|
|
47
58
|
diagnostics.push({
|
|
48
59
|
id: `high-complexity:${ctx.filePath}:${f.line}`,
|
|
49
|
-
tool: "
|
|
60
|
+
tool: "fact-rules",
|
|
50
61
|
rule: "high-complexity",
|
|
51
62
|
filePath: ctx.filePath,
|
|
52
63
|
line: f.line,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTestFrameworkNoiseCall, isTestSuiteOrganizer, } from "./framework-call-noise.js";
|
|
1
2
|
// Default matches the historical hardcoded value so behavior is unchanged for
|
|
2
3
|
// projects without a config. Project-specific overrides are read from the
|
|
3
4
|
// per-dispatch context; this mutable fallback exists for tests/legacy direct
|
|
@@ -27,7 +28,16 @@ export const highFanOutRule = {
|
|
|
27
28
|
const configuredFanOutThreshold = ctx.projectConfig?.rules["high-fan-out"]?.threshold;
|
|
28
29
|
const activeFanOutThreshold = configuredFanOutThreshold ?? fanOutThreshold;
|
|
29
30
|
for (const f of fns) {
|
|
30
|
-
//
|
|
31
|
+
// A describe()/it()/test() wrapper's own call list aggregates every call
|
|
32
|
+
// from ALL of its nested test bodies (#577) — not real fan-out. Genuinely
|
|
33
|
+
// complex test HELPER functions don't call it/describe/test themselves, so
|
|
34
|
+
// they're unaffected.
|
|
35
|
+
if (isTestSuiteOrganizer(f.outgoingCalls))
|
|
36
|
+
continue;
|
|
37
|
+
// Filter out noise: utility calls, logging, type assertions, and
|
|
38
|
+
// test-framework calls (expect/it/describe/vi.*/jest.*, #577) — assertion-
|
|
39
|
+
// and mock-heavy test bodies naturally call many of these; that's test
|
|
40
|
+
// structure, not a coordination smell.
|
|
31
41
|
const meaningful = f.outgoingCalls.filter((c) => {
|
|
32
42
|
const lower = c.toLowerCase();
|
|
33
43
|
return (!lower.startsWith("console.") &&
|
|
@@ -42,13 +52,14 @@ export const highFanOutRule = {
|
|
|
42
52
|
c !== "resolve" &&
|
|
43
53
|
c !== "reject" &&
|
|
44
54
|
c !== "next" &&
|
|
45
|
-
c !== "done"
|
|
55
|
+
c !== "done" &&
|
|
56
|
+
!isTestFrameworkNoiseCall(c));
|
|
46
57
|
});
|
|
47
58
|
if (meaningful.length < activeFanOutThreshold)
|
|
48
59
|
continue;
|
|
49
60
|
diagnostics.push({
|
|
50
61
|
id: `high-fan-out:${ctx.filePath}:${f.line}`,
|
|
51
|
-
tool: "
|
|
62
|
+
tool: "fact-rules",
|
|
52
63
|
rule: "high-fan-out",
|
|
53
64
|
filePath: ctx.filePath,
|
|
54
65
|
line: f.line,
|
|
@@ -46,7 +46,7 @@ export const missingErrorPropagationRule = {
|
|
|
46
46
|
continue;
|
|
47
47
|
diagnostics.push({
|
|
48
48
|
id: `missing-error-propagation:${ctx.filePath}:${c.line}`,
|
|
49
|
-
tool: "
|
|
49
|
+
tool: "fact-rules",
|
|
50
50
|
rule: "missing-error-propagation",
|
|
51
51
|
filePath: ctx.filePath,
|
|
52
52
|
line: c.line,
|
|
@@ -34,7 +34,7 @@ export const commentedCredentialsRule = {
|
|
|
34
34
|
if (p.test(line)) {
|
|
35
35
|
diagnostics.push({
|
|
36
36
|
id: `no-commented-credentials:${ctx.filePath}:${i + 1}`,
|
|
37
|
-
tool: "
|
|
37
|
+
tool: "fact-rules",
|
|
38
38
|
rule: "no-commented-credentials",
|
|
39
39
|
filePath: ctx.filePath,
|
|
40
40
|
line: i + 1,
|
|
@@ -23,7 +23,7 @@ export const passThroughWrappersRule = {
|
|
|
23
23
|
continue;
|
|
24
24
|
diagnostics.push({
|
|
25
25
|
id: `pass-through-wrapper:${ctx.filePath}:${fn.line}:${fn.column}`,
|
|
26
|
-
tool: "
|
|
26
|
+
tool: "fact-rules",
|
|
27
27
|
filePath: ctx.filePath,
|
|
28
28
|
line: fn.line,
|
|
29
29
|
column: fn.column,
|
|
@@ -25,7 +25,7 @@ export const placeholderCommentsRule = {
|
|
|
25
25
|
continue;
|
|
26
26
|
diagnostics.push({
|
|
27
27
|
id: `placeholder-comments:${ctx.filePath}:${comment.line}:1`,
|
|
28
|
-
tool: "
|
|
28
|
+
tool: "fact-rules",
|
|
29
29
|
filePath: ctx.filePath,
|
|
30
30
|
line: comment.line,
|
|
31
31
|
column: 1,
|
|
@@ -38,14 +38,31 @@ const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
|
|
|
38
38
|
const MAX_MATCHES_PER_RULE = 10;
|
|
39
39
|
/** Maximum total diagnostics per file to prevent output spam */
|
|
40
40
|
const MAX_TOTAL_DIAGNOSTICS = 50;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
/**
|
|
42
|
+
* #660: this runner used to skip a hardcoded set of rule ids
|
|
43
|
+
* (`constructor-super`, `empty-catch`, `long-parameter-list`,
|
|
44
|
+
* `nested-ternary`, `no-dupe-class-members`) on the assumption that the
|
|
45
|
+
* tree-sitter query runner (priority 14) already covered them, to avoid
|
|
46
|
+
* double-reporting. That assumption was false for every entry: three of
|
|
47
|
+
* them (`nested-ternary`, `long-parameter-list`, `no-dupe-class-members`)
|
|
48
|
+
* have no active tree-sitter query — their would-be queries either live
|
|
49
|
+
* under `rules/tree-sitter-queries/typescript-disabled/` (excluded from
|
|
50
|
+
* loading, see clients/tree-sitter-query-loader.ts) or were never written —
|
|
51
|
+
* so those three rule ids had ZERO coverage in the NAPI fallback runner
|
|
52
|
+
* (used when the ast-grep binary isn't installed) despite having a
|
|
53
|
+
* perfectly good, shipped, active ast-grep rule sitting right there. The
|
|
54
|
+
* other two (`constructor-super`, `empty-catch`) are disabled everywhere
|
|
55
|
+
* (ast-grep AND tree-sitter, see rules-disabled/, #206), so skipping them
|
|
56
|
+
* was already a no-op. The whole skip-set has been removed; if tree-sitter
|
|
57
|
+
* coverage is ever added back for one of these rule ids, reintroduce a
|
|
58
|
+
* scoped skip alongside the query that actually covers it — don't recreate
|
|
59
|
+
* a blanket assumption-based list.
|
|
60
|
+
*
|
|
61
|
+
* Note: `no-dupe-class-members` didn't actually fire immediately
|
|
62
|
+
* post-removal — its rule YAML uses a top-level `utils:` block that this
|
|
63
|
+
* runner's native-config passthrough dropped entirely, a separate bug
|
|
64
|
+
* (affecting 5 shipped rules, not just this one) fixed in #663.
|
|
65
|
+
*/
|
|
49
66
|
/**
|
|
50
67
|
* Rules commonly covered by ESLint/Biome correctness checks.
|
|
51
68
|
* We can suppress these from ast-grep in lint-enabled projects to reduce noise.
|
|
@@ -103,6 +120,32 @@ function normalizeRuleId(ruleId) {
|
|
|
103
120
|
export function canHandle(filePath) {
|
|
104
121
|
return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
|
|
105
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The TypeScript grammar is a syntactic superset of JavaScript, so a
|
|
125
|
+
* `JavaScript`-tagged rule using generic node kinds (`variable_declarator`,
|
|
126
|
+
* `assignment_expression`, …) still matches against a parsed `.ts`/`.tsx`
|
|
127
|
+
* root — and vice versa isn't an issue since JS files never parse
|
|
128
|
+
* TS-only syntax, but a `TypeScript`-tagged rule with a plain-JS-compatible
|
|
129
|
+
* body would equally double-fire alongside a `JavaScript` twin on a `.ts`
|
|
130
|
+
* file. Without this, `language:` reads as a real filter but isn't one for
|
|
131
|
+
* ts↔js pairs, so twin rules sharing a base name (e.g. `hardcoded-url` /
|
|
132
|
+
* `hardcoded-url-js`) both match the same construct in the SAME runner
|
|
133
|
+
* invocation (#657). Returns undefined for extensions this scoping doesn't
|
|
134
|
+
* apply to (css/html), where no filtering is added.
|
|
135
|
+
*/
|
|
136
|
+
export function ruleLanguageForFile(filePath) {
|
|
137
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
138
|
+
switch (ext) {
|
|
139
|
+
case ".ts":
|
|
140
|
+
case ".tsx":
|
|
141
|
+
return "typescript";
|
|
142
|
+
case ".js":
|
|
143
|
+
case ".jsx":
|
|
144
|
+
return "javascript";
|
|
145
|
+
default:
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
106
149
|
export function getLang(filePath, sgModule) {
|
|
107
150
|
const ext = path.extname(filePath).toLowerCase();
|
|
108
151
|
switch (ext) {
|
|
@@ -169,9 +212,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
169
212
|
const maxMatchesPerRule = options.maxMatchesPerRule ?? MAX_MATCHES_PER_RULE;
|
|
170
213
|
const maxTotalDiagnostics = options.maxTotalDiagnostics ?? MAX_TOTAL_DIAGNOSTICS;
|
|
171
214
|
const blockingOnly = options.blockingOnly === true;
|
|
215
|
+
const log = options.log;
|
|
172
216
|
const diagnostics = [];
|
|
173
217
|
const seenRuleIds = new Set();
|
|
174
218
|
const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
|
|
219
|
+
const fileLang = ruleLanguageForFile(filePath);
|
|
175
220
|
// Shared with the raw sgconfig materializer so both surfaces walk the same
|
|
176
221
|
// workspace-rooted sources in the same precedence order.
|
|
177
222
|
const ruleSources = getAstGrepRuleSources(options.projectRoot ?? cwd);
|
|
@@ -205,9 +250,6 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
205
250
|
!NON_SUPPRESSIBLE.has(normalizeRuleId(rule.id))) {
|
|
206
251
|
continue;
|
|
207
252
|
}
|
|
208
|
-
// Skip rules already handled by tree-sitter runner (priority 14)
|
|
209
|
-
if (TREE_SITTER_OVERLAP.has(rule.id))
|
|
210
|
-
continue;
|
|
211
253
|
// Skip rules whose top-level pattern is overly broad ($NAME, $X, etc.)
|
|
212
254
|
// without additional structural constraints to narrow matches.
|
|
213
255
|
if (rule.rule &&
|
|
@@ -219,6 +261,12 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
219
261
|
if (lang && lang !== "typescript" && lang !== "javascript") {
|
|
220
262
|
continue;
|
|
221
263
|
}
|
|
264
|
+
// Scope TypeScript/JavaScript-tagged rules to the file's actual
|
|
265
|
+
// grammar (#657) — otherwise a `-js` twin sharing generic node
|
|
266
|
+
// kinds with its TS sibling double-fires on every .ts file.
|
|
267
|
+
if (lang && fileLang && lang !== fileLang) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
222
270
|
if (blockingOnly && rule.rule) {
|
|
223
271
|
const complexity = calculateRuleComplexity(rule.rule);
|
|
224
272
|
if (complexity > MAX_BLOCKING_RULE_COMPLEXITY) {
|
|
@@ -232,18 +280,26 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
|
|
|
232
280
|
// Delegate matching to napi's native engine, which handles the
|
|
233
281
|
// full ast-grep rule grammar (pattern, kind, has/inside/follows/
|
|
234
282
|
// precedes/stopBy/field/nthChild, any/all/not) plus metavariable
|
|
235
|
-
// `constraints` (#206)
|
|
283
|
+
// `constraints` (#206) AND top-level `utils` — reusable named
|
|
284
|
+
// matchers referenced via `matches: <name>` inside `rule`
|
|
285
|
+
// (#663; `NapiConfig.utils: Record<string, Rule>` per
|
|
286
|
+
// @ast-grep/napi's types, same shape napi already expects for
|
|
287
|
+
// `rule`/`constraints`). A faithful js-yaml parse feeds the rule
|
|
236
288
|
// object straight through. If napi rejects the rule (a malformed
|
|
237
|
-
// or invalid-kind rule
|
|
238
|
-
// through a partial
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
289
|
+
// or invalid-kind rule, or an unresolved `matches:` reference),
|
|
290
|
+
// skip it — never silently match nothing through a partial
|
|
291
|
+
// interpreter.
|
|
292
|
+
const nativeConfig = { rule: rule.rule };
|
|
293
|
+
if (rule.constraints)
|
|
294
|
+
nativeConfig.constraints = rule.constraints;
|
|
295
|
+
if (rule.utils)
|
|
296
|
+
nativeConfig.utils = rule.utils;
|
|
242
297
|
try {
|
|
243
298
|
matches = rootNode.findAll(nativeConfig);
|
|
244
299
|
}
|
|
245
|
-
catch {
|
|
300
|
+
catch (err) {
|
|
246
301
|
matches = [];
|
|
302
|
+
log?.(`ast-grep-napi: rule "${rule.id}" rejected by native engine (${err instanceof Error ? err.message : String(err)})`);
|
|
247
303
|
}
|
|
248
304
|
const limitedMatches = matches.slice(0, maxMatchesPerRule);
|
|
249
305
|
for (const match of limitedMatches) {
|
|
@@ -355,6 +411,7 @@ const astGrepNapiRunner = {
|
|
|
355
411
|
const diagnostics = evaluateAstGrepRules(ctx.filePath, rootNode, ctx.cwd, ctx.kind, {
|
|
356
412
|
blockingOnly: ctx.blockingOnly,
|
|
357
413
|
projectRoot: ctx.projectRoot,
|
|
414
|
+
log: (message) => ctx.log(message),
|
|
358
415
|
});
|
|
359
416
|
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
360
417
|
let semantic = "none";
|