pi-lens 3.8.69 → 3.8.70
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 +75 -0
- package/README.md +15 -13
- package/dist/clients/bootstrap.js +3 -1
- package/dist/clients/bus-events-logger.js +51 -0
- package/dist/clients/bus-publish.js +45 -2
- package/dist/clients/code-quality-warnings.js +27 -0
- package/dist/clients/dead-code-client.js +5 -23
- package/dist/clients/diagnostics-publish.js +38 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +62 -6
- package/dist/clients/dispatch/facts/function-facts.js +83 -0
- 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 +22 -6
- 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 +71 -2
- package/dist/clients/knip-client.js +35 -38
- package/dist/clients/language-profile.js +7 -11
- package/dist/clients/lens-engine.js +12 -0
- package/dist/clients/log-cleanup.js +61 -19
- package/dist/clients/lsp/cascade-tier.js +31 -19
- package/dist/clients/lsp/client.js +52 -7
- package/dist/clients/lsp/config.js +27 -1
- package/dist/clients/lsp/index.js +547 -59
- package/dist/clients/lsp/server-strategies.js +17 -8
- package/dist/clients/lsp/server.js +63 -27
- package/dist/clients/lsp-budget.js +148 -0
- package/dist/clients/mcp/session.js +1 -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/path-utils.js +40 -0
- package/dist/clients/pipeline.js +1 -1
- 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/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/resource-sampler.js +254 -0
- package/dist/clients/review-graph/builder.js +265 -11
- package/dist/clients/review-graph/symbol-id.js +57 -0
- package/dist/clients/runtime-session.js +40 -6
- package/dist/clients/runtime-turn.js +53 -11
- package/dist/clients/safe-spawn.js +48 -2
- package/dist/clients/source-filter.js +12 -25
- package/dist/clients/source-walker.js +66 -0
- package/dist/clients/startup-scan.js +37 -42
- 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/widget-state.js +61 -1
- package/dist/clients/write-ordering-guard.js +58 -0
- package/dist/clients/zizmor-config.js +27 -0
- package/dist/index.js +6640 -2947
- 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-diagnostics.js +290 -30
- package/dist/tools/lsp-diagnostics.js +544 -104
- package/dist/tools/module-report.js +18 -5
- package/docs/agent-tooling-improvement-plan.md +660 -0
- package/docs/agent-tools.md +129 -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/env_variables.md +10 -0
- package/docs/environment-variables.md +67 -0
- package/docs/fable.md +139 -0
- package/docs/features.md +338 -0
- package/docs/globalconfig.md +79 -0
- package/docs/inspiration_gograph.md +601 -0
- package/docs/inspiration_neovim.md +1795 -0
- package/docs/inspiration_neovim2.md +402 -0
- package/docs/inspiration_tree_sitter_rules.md +369 -0
- package/docs/inspiration_tree_sitter_rules_investigation.md +328 -0
- package/docs/inspiration_tree_sitter_rules_round2.md +470 -0
- package/docs/language-coverage.md +48 -0
- package/docs/lsp-bridge-analysis.md +444 -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/newinspiration.md +338 -0
- package/docs/servercapabilities.md +118 -0
- package/docs/subagent-compat.md +281 -0
- package/docs/tools.md +13 -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/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-toml.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
- package/package.json +5 -1
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -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 +1 -1
- 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
|
@@ -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";
|
|
@@ -16,7 +16,7 @@ import { RUNTIME_CONFIG } from "../../runtime-config.js";
|
|
|
16
16
|
import { PRIORITY } from "../priorities.js";
|
|
17
17
|
import { resolveRunnerPath } from "../runner-context.js";
|
|
18
18
|
import { convertLspDiagnostics } from "../utils/lsp-diagnostics.js";
|
|
19
|
-
import { enabledAuxiliaryLspServerIds, findAuxiliaryProfileForSource, } from "../auxiliary-lsp.js";
|
|
19
|
+
import { enabledAuxiliaryLspServerIds, findAuxiliaryProfileForSource, isAuxiliaryDiagnosticSuppressed, } from "../auxiliary-lsp.js";
|
|
20
20
|
import { readFileContent } from "./utils.js";
|
|
21
21
|
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
22
22
|
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
@@ -104,6 +104,11 @@ const lspRunner = {
|
|
|
104
104
|
// spawn that didn't complete in the budget, or LSP unavailable for this
|
|
105
105
|
// file) — distinct from `[]`, which means the server replied with zero.
|
|
106
106
|
let lspClientReady = true;
|
|
107
|
+
// True when touchFile ran but couldn't confirm its result within
|
|
108
|
+
// budget (notify write and/or diagnostics wait timed out on at least
|
|
109
|
+
// one spawned server) — an empty `lspDiags` in that case is NOT a
|
|
110
|
+
// confirmed clean result and must not be reported as one (#570).
|
|
111
|
+
let diagnosticsInconclusive = false;
|
|
107
112
|
let failureReason = "";
|
|
108
113
|
const content = readFileContent(ctx.filePath);
|
|
109
114
|
if (!content) {
|
|
@@ -133,6 +138,7 @@ const lspRunner = {
|
|
|
133
138
|
}
|
|
134
139
|
else {
|
|
135
140
|
lspDiags = touched;
|
|
141
|
+
diagnosticsInconclusive = touched.inconclusive === true;
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
catch (err) {
|
|
@@ -173,6 +179,18 @@ const lspRunner = {
|
|
|
173
179
|
// published late still land in the client cache and surface then.
|
|
174
180
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
175
181
|
}
|
|
182
|
+
if (diagnosticsInconclusive) {
|
|
183
|
+
// The touch ran and a client was ready, but the notify write and/or
|
|
184
|
+
// diagnostics wait hit their deadline before the server confirmed
|
|
185
|
+
// completion — `lspDiags` (even if non-empty) is not a trustworthy
|
|
186
|
+
// merged result. Same treatment as `!lspClientReady`: report
|
|
187
|
+
// "skipped" rather than "succeeded" with a possibly-incomplete
|
|
188
|
+
// diagnostics list, so the coverage notice flags the gap instead of
|
|
189
|
+
// the footer reading this as a confirmed clean/partial result (#570).
|
|
190
|
+
// Diagnostics that do arrive late still land in the client cache and
|
|
191
|
+
// surface on the next edit.
|
|
192
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
193
|
+
}
|
|
176
194
|
if (lspDiags.length === 0) {
|
|
177
195
|
return {
|
|
178
196
|
status: "succeeded",
|
|
@@ -216,11 +234,9 @@ const lspRunner = {
|
|
|
216
234
|
const profile = findAuxiliaryProfileForSource(validLspDiags[i]?.source);
|
|
217
235
|
if (!profile)
|
|
218
236
|
continue;
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
237
|
+
if (isAuxiliaryDiagnosticSuppressed(validLspDiags[i], content)) {
|
|
238
|
+
suppressedIndices.add(i);
|
|
239
|
+
continue;
|
|
224
240
|
}
|
|
225
241
|
let blockingAllowed = blockingAllowedByProfile.get(profile);
|
|
226
242
|
if (blockingAllowed === undefined) {
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* - `gitleaks` reference in `package.json` deps, OR
|
|
18
18
|
* - a git pre-commit hook (.husky/, .git/hooks/) referencing gitleaks
|
|
19
19
|
*
|
|
20
|
+
* `lens_diagnostics mode=full`'s fresh-fetch path (`clients/project-diagnostics/
|
|
21
|
+
* fresh-fetch.ts`) uses a looser "smart-default" gate instead — any tracked
|
|
22
|
+
* git repo, via `hasGitRepo` — since that's an explicitly-requested
|
|
23
|
+
* comprehensive review where gitleaks's low cost and advisory-only findings
|
|
24
|
+
* make the stricter default needlessly conservative. session_start and
|
|
25
|
+
* per-edit dispatch keep the strict gate above unchanged.
|
|
26
|
+
*
|
|
20
27
|
* If the gate trips, the runner auto-installs gitleaks from GitHub releases
|
|
21
28
|
* (installer entry registered in clients/installer/index.ts) and runs
|
|
22
29
|
* `gitleaks detect --no-git --report-format json` against the analysis root.
|
|
@@ -95,6 +102,26 @@ export function hasGitleaksSignal(cwd) {
|
|
|
95
102
|
}
|
|
96
103
|
return false;
|
|
97
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* "Smart-default" tier from #130's own considered-but-unshipped options:
|
|
107
|
+
* fire whenever the project is a tracked git repo, not only when an explicit
|
|
108
|
+
* gitleaks signal is present. gitleaks's own scan target is "a git repo's
|
|
109
|
+
* history/tree" — nearly every project qualifies, so this is meaningfully
|
|
110
|
+
* looser than {@link hasGitleaksSignal}. Used ONLY by `mode=full`'s
|
|
111
|
+
* fresh-fetch path (an explicitly-requested comprehensive review, where
|
|
112
|
+
* gitleaks's low cost — ~10MB binary, no external DB pull — and advisory-only
|
|
113
|
+
* findings make the stricter opt-in gate needlessly conservative); session_start
|
|
114
|
+
* and per-edit dispatch keep the strict {@link hasGitleaksSignal} gate
|
|
115
|
+
* unchanged, so day-to-day noise/cost stays exactly as conservative as before.
|
|
116
|
+
*/
|
|
117
|
+
export function hasGitRepo(cwd) {
|
|
118
|
+
try {
|
|
119
|
+
return fs.existsSync(path.join(cwd, ".git"));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
98
125
|
// --- Client ---
|
|
99
126
|
export class GitleaksClient extends SecurityScanClient {
|
|
100
127
|
constructor(verbose = false) {
|
|
@@ -107,6 +134,10 @@ export class GitleaksClient extends SecurityScanClient {
|
|
|
107
134
|
static hasGitleaksSignal(cwd) {
|
|
108
135
|
return hasGitleaksSignal(cwd);
|
|
109
136
|
}
|
|
137
|
+
/** Smart-default tier (#130) — see {@link hasGitRepo}'s doc comment. */
|
|
138
|
+
static hasGitRepo(cwd) {
|
|
139
|
+
return hasGitRepo(cwd);
|
|
140
|
+
}
|
|
110
141
|
/**
|
|
111
142
|
* Auto-install via the GitHub-release path (registered in
|
|
112
143
|
* `clients/installer/index.ts`) when gitleaks isn't already on PATH.
|
|
@@ -118,18 +149,22 @@ export class GitleaksClient extends SecurityScanClient {
|
|
|
118
149
|
/**
|
|
119
150
|
* Scan a directory tree for secrets.
|
|
120
151
|
*
|
|
121
|
-
* Skips early when the directory shows no gitleaks opt-in signal
|
|
122
|
-
*
|
|
123
|
-
*
|
|
152
|
+
* Skips early when the directory shows no gitleaks opt-in signal — unless
|
|
153
|
+
* `requireSignal: false` (the `mode=full` fresh-fetch path uses this to
|
|
154
|
+
* apply the looser #130 "smart-default" gate, {@link hasGitRepo}, instead;
|
|
155
|
+
* session_start and per-edit dispatch never pass this, so their behavior
|
|
156
|
+
* is unchanged). When gitleaks is unavailable, returns an empty result
|
|
157
|
+
* with an explanatory summary rather than failing the session_start task.
|
|
124
158
|
*
|
|
125
159
|
* Re-entrancy safe: concurrent calls against the same root share a
|
|
126
160
|
* single gitleaks process (mirrors `KnipClient` / `JscpdClient` /
|
|
127
161
|
* `GovulncheckClient`).
|
|
128
162
|
*/
|
|
129
|
-
async scan(cwd) {
|
|
163
|
+
async scan(cwd, options) {
|
|
130
164
|
const targetDir = path.resolve(cwd);
|
|
131
165
|
const scannedAt = new Date().toISOString();
|
|
132
|
-
|
|
166
|
+
const requireSignal = options?.requireSignal ?? true;
|
|
167
|
+
if (requireSignal && !GitleaksClient.hasGitleaksSignal(targetDir)) {
|
|
133
168
|
return {
|
|
134
169
|
...EMPTY_RESULT,
|
|
135
170
|
success: true,
|