pi-lens 3.8.65 → 3.8.67
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 +59 -0
- package/README.md +0 -1
- package/dist/clients/actionable-warnings-logger.js +11 -36
- package/dist/clients/ast-grep-tool-logger.js +11 -36
- package/dist/clients/biome-client.js +10 -2
- package/dist/clients/bootstrap.js +1 -3
- package/dist/clients/cascade-logger.js +7 -12
- package/dist/clients/complexity-client.js +397 -623
- package/dist/clients/dead-code-logger.js +11 -22
- package/dist/clients/dependency-checker.js +15 -2
- package/dist/clients/deps/vscode-jsonrpc.js +1 -1
- package/dist/clients/diagnostic-logger.js +7 -31
- package/dist/clients/dispatch/auxiliary-lsp.js +38 -0
- package/dist/clients/dispatch/dispatcher.js +4 -39
- package/dist/clients/dispatch/fact-runner.js +9 -57
- package/dist/clients/dispatch/facts/comment-facts.js +12 -25
- package/dist/clients/dispatch/facts/function-facts.js +161 -132
- package/dist/clients/dispatch/facts/import-facts.js +164 -117
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +34 -0
- package/dist/clients/dispatch/facts/try-catch-facts.js +62 -68
- package/dist/clients/dispatch/inline-suppressions.js +61 -0
- package/dist/clients/dispatch/integration.js +94 -32
- package/dist/clients/dispatch/plan.js +0 -49
- package/dist/clients/dispatch/priorities.js +0 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +54 -0
- package/dist/clients/dispatch/rules/high-import-coupling.js +37 -0
- package/dist/clients/dispatch/rules/no-commented-credentials.js +52 -0
- package/dist/clients/dispatch/runners/index.js +2 -7
- package/dist/clients/dispatch/runners/lsp.js +18 -6
- package/dist/clients/dispatch/runners/tree-sitter.js +11 -64
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +28 -33
- package/dist/clients/dispatch/tool-profile.js +0 -1
- package/dist/clients/dispatch/types.js +1 -1
- package/dist/clients/formatters.js +10 -0
- package/dist/clients/git-guard.js +1 -1
- package/dist/clients/grammar-source.js +65 -2
- package/dist/clients/jscpd-client.js +8 -2
- package/dist/clients/language-policy.js +2 -2
- package/dist/clients/latency-logger.js +10 -15
- package/dist/clients/lsp/client.js +74 -11
- package/dist/clients/lsp/config.js +61 -3
- package/dist/clients/lsp/index.js +43 -3
- package/dist/clients/lsp/launch.js +3 -32
- package/dist/clients/mcp/session.js +0 -1
- package/dist/clients/module-report.js +16 -5
- package/dist/clients/ndjson-logger.js +150 -0
- package/dist/clients/package-manager.js +65 -0
- package/dist/clients/pipeline.js +25 -41
- package/dist/clients/project-diagnostics/extractors.js +85 -0
- package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +44 -0
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +25 -0
- package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +34 -0
- package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +38 -0
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +47 -0
- package/dist/clients/project-diagnostics/runner-adapters/trivy.js +31 -0
- package/dist/clients/project-diagnostics/scanner.js +11 -10
- package/dist/clients/read-guard-logger.js +11 -36
- package/dist/clients/review-graph/builder.js +337 -27
- package/dist/clients/review-graph/git-identity.js +150 -0
- package/dist/clients/review-graph/service.js +3 -3
- package/dist/clients/runtime-coordinator.js +83 -0
- package/dist/clients/runtime-session.js +28 -2
- package/dist/clients/runtime-tool-result.js +10 -2
- package/dist/clients/runtime-turn.js +25 -7
- package/dist/clients/sg-runner.js +15 -0
- package/dist/clients/test-runner-client.js +12 -6
- package/dist/clients/tree-sitter-cache.js +47 -14
- package/dist/clients/tree-sitter-client.js +9 -1
- package/dist/clients/tree-sitter-logger.js +7 -12
- package/dist/clients/tree-sitter-query-loader.js +1 -0
- package/dist/clients/tree-sitter-shared.js +113 -0
- package/dist/clients/tree-sitter-symbol-extractor.js +19 -12
- package/dist/index.js +4 -22
- package/dist/tools/lens-diagnostics.js +254 -20
- package/grammars/tree-sitter-yaml.wasm +0 -0
- package/grammars/tree-sitter-yaml.wasm.json +3 -3
- package/package.json +4 -3
- package/rules/ast-grep-rules/rule-tests/no-init-return-test.yml +15 -0
- package/rules/ast-grep-rules/rules/no-init-return.yml +11 -5
- package/rules/tree-sitter-queries/python/python-assert-production.yml +4 -0
- package/scripts/download-grammars.js +29 -4
- package/scripts/grammars.lock.json +14 -2
- package/scripts/install-selftest.mjs +8 -7
- package/dist/clients/deps/typescript.js +0 -15
- package/dist/clients/dispatch/rules/quality-rules.js +0 -297
- package/dist/clients/dispatch/rules/sonar-rules.js +0 -493
- package/dist/clients/dispatch/runners/biome.js +0 -69
- package/dist/clients/dispatch/runners/python-slop.js +0 -106
- package/dist/clients/dispatch/runners/ts-lsp.js +0 -109
- package/dist/clients/runner-tracker.js +0 -153
- package/dist/clients/ts-service.js +0 -130
- package/dist/clients/type-coverage-client.js +0 -128
- package/dist/clients/typescript-client.js +0 -509
- package/dist/commands/booboo.js +0 -1412
- package/rules/python-slop-rules/.sgconfig.yml +0 -4
- package/rules/python-slop-rules/rules/slop-rules.yml +0 -647
|
@@ -1,151 +1,198 @@
|
|
|
1
|
-
import { ts } from "../../deps/typescript.js";
|
|
2
1
|
import { logLatency } from "../../latency-logger.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { childrenOfType, firstChildOfType, parseFactTree, walk, } from "./tree-sitter-facts.js";
|
|
3
|
+
// JS/TS extensions this provider handles (→ tree-sitter typescript/tsx/javascript
|
|
4
|
+
// grammars via resolveTreeSitterLanguage). Ported off the `typescript` compiler API
|
|
5
|
+
// onto tree-sitter (#402) — the parse is served from the shared, cached client.
|
|
6
|
+
const JSTS_EXTS = new Set([
|
|
7
|
+
".ts",
|
|
8
|
+
".tsx",
|
|
9
|
+
".mts",
|
|
10
|
+
".cts",
|
|
11
|
+
".js",
|
|
12
|
+
".jsx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".cjs",
|
|
15
|
+
]);
|
|
16
|
+
function stripQuotes(raw) {
|
|
17
|
+
return raw.replace(/^["'`]+|["'`]+$/g, "");
|
|
18
|
+
}
|
|
19
|
+
/** Parse a top-level `import_statement` node into an ImportEntry. */
|
|
20
|
+
function parseStaticImport(node, moduleType) {
|
|
21
|
+
const sourceNode = firstChildOfType(node, "string");
|
|
22
|
+
if (!sourceNode)
|
|
23
|
+
return null;
|
|
24
|
+
const source = stripQuotes(sourceNode.text);
|
|
25
|
+
const clause = firstChildOfType(node, "import_clause");
|
|
26
|
+
if (!clause) {
|
|
27
|
+
// `import "reflect-metadata";` — side-effect only, no bindings.
|
|
28
|
+
return { source, names: [], moduleType };
|
|
29
|
+
}
|
|
30
|
+
const entry = { source, names: [], moduleType };
|
|
31
|
+
for (const child of clause.children ?? []) {
|
|
32
|
+
if (!child)
|
|
33
|
+
continue;
|
|
34
|
+
if (child.type === "identifier") {
|
|
35
|
+
// `import React from ...` — default binding is a bare identifier.
|
|
36
|
+
entry.defaultName = child.text;
|
|
9
37
|
}
|
|
10
|
-
else if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
hasCjs = true;
|
|
38
|
+
else if (child.type === "namespace_import") {
|
|
39
|
+
// `import * as fs from ...`
|
|
40
|
+
const id = firstChildOfType(child, "identifier");
|
|
41
|
+
if (id)
|
|
42
|
+
entry.namespace = id.text;
|
|
16
43
|
}
|
|
17
|
-
else if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
else if (child.type === "named_imports") {
|
|
45
|
+
// `import { a, b as c } from ...` — the local binding is the alias when
|
|
46
|
+
// present (last identifier of the specifier), else the imported name.
|
|
47
|
+
for (const spec of childrenOfType(child, "import_specifier")) {
|
|
48
|
+
const ids = childrenOfType(spec, "identifier");
|
|
49
|
+
const local = ids.length ? ids[ids.length - 1].text : undefined;
|
|
50
|
+
if (local)
|
|
51
|
+
entry.names.push(local);
|
|
52
|
+
}
|
|
22
53
|
}
|
|
23
|
-
ts.forEachChild(node, visit);
|
|
24
54
|
}
|
|
25
|
-
|
|
26
|
-
if (hasEsm && !hasCjs)
|
|
27
|
-
return "esm";
|
|
28
|
-
if (hasCjs && !hasEsm)
|
|
29
|
-
return "cjs";
|
|
30
|
-
if (hasEsm || hasCjs)
|
|
31
|
-
return "esm"; // mixed — treat as ESM (static imports present)
|
|
32
|
-
return "unknown";
|
|
55
|
+
return entry;
|
|
33
56
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
isDynamic: true,
|
|
46
|
-
moduleType,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
// require('specifier') — CJS import
|
|
50
|
-
else if (ts.isCallExpression(node) &&
|
|
51
|
-
ts.isIdentifier(node.expression) &&
|
|
52
|
-
node.expression.text === "require" &&
|
|
53
|
-
node.arguments.length === 1 &&
|
|
54
|
-
ts.isStringLiteral(node.arguments[0])) {
|
|
55
|
-
entries.push({
|
|
56
|
-
source: node.arguments[0].text,
|
|
57
|
-
names: [],
|
|
58
|
-
moduleType: "cjs",
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
ts.forEachChild(node, visit);
|
|
57
|
+
/** Parse a top-level `export_statement` into a ReExportEntry, or null if it isn't a
|
|
58
|
+
* re-export (a plain `export const x` has no `from` source). */
|
|
59
|
+
function parseReExport(node) {
|
|
60
|
+
const sourceNode = firstChildOfType(node, "string");
|
|
61
|
+
if (!sourceNode)
|
|
62
|
+
return null; // not `export ... from '...'`
|
|
63
|
+
const source = stripQuotes(sourceNode.text);
|
|
64
|
+
const clause = firstChildOfType(node, "export_clause");
|
|
65
|
+
if (!clause) {
|
|
66
|
+
// `export * from '...'`
|
|
67
|
+
return { source, names: [] };
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
continue;
|
|
71
|
-
const source = stmt.moduleSpecifier.text;
|
|
72
|
-
if (!stmt.exportClause) {
|
|
73
|
-
// export * from '...'
|
|
74
|
-
entries.push({ source, names: [] });
|
|
75
|
-
}
|
|
76
|
-
else if (ts.isNamedExports(stmt.exportClause)) {
|
|
77
|
-
const names = stmt.exportClause.elements.map((e) => e.name.text);
|
|
78
|
-
entries.push({ source, names });
|
|
79
|
-
}
|
|
69
|
+
const names = [];
|
|
70
|
+
for (const spec of childrenOfType(clause, "export_specifier")) {
|
|
71
|
+
const ids = childrenOfType(spec, "identifier");
|
|
72
|
+
// Exported name is the alias when present (`a as b` → b), else the name.
|
|
73
|
+
const name = ids.length ? ids[ids.length - 1].text : undefined;
|
|
74
|
+
if (name)
|
|
75
|
+
names.push(name);
|
|
80
76
|
}
|
|
81
|
-
return
|
|
77
|
+
return { source, names };
|
|
82
78
|
}
|
|
83
|
-
const EXT_TO_SCRIPT_KIND = {
|
|
84
|
-
".ts": ts.ScriptKind.TS,
|
|
85
|
-
".tsx": ts.ScriptKind.TSX,
|
|
86
|
-
".mts": ts.ScriptKind.TS,
|
|
87
|
-
".cts": ts.ScriptKind.TS,
|
|
88
|
-
".js": ts.ScriptKind.JS,
|
|
89
|
-
".jsx": ts.ScriptKind.JSX,
|
|
90
|
-
".mjs": ts.ScriptKind.JS,
|
|
91
|
-
".cjs": ts.ScriptKind.JS,
|
|
92
|
-
};
|
|
93
79
|
export const importFactProvider = {
|
|
94
80
|
id: "fact.file.imports",
|
|
95
81
|
provides: ["file.imports", "file.reexports"],
|
|
96
82
|
requires: ["file.content"],
|
|
97
83
|
appliesTo(ctx) {
|
|
98
84
|
const ext = ctx.filePath.slice(ctx.filePath.lastIndexOf(".")).toLowerCase();
|
|
99
|
-
return ext
|
|
85
|
+
return JSTS_EXTS.has(ext);
|
|
100
86
|
},
|
|
101
|
-
run(ctx, store) {
|
|
87
|
+
async run(ctx, store) {
|
|
102
88
|
const content = store.getFileFact(ctx.filePath, "file.content");
|
|
103
|
-
|
|
89
|
+
const setEmpty = () => {
|
|
104
90
|
store.setFileFact(ctx.filePath, "file.imports", []);
|
|
105
91
|
store.setFileFact(ctx.filePath, "file.reexports", []);
|
|
92
|
+
};
|
|
93
|
+
if (!content) {
|
|
94
|
+
setEmpty();
|
|
106
95
|
return;
|
|
107
96
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
97
|
+
// Grammar unavailable / parse failed / wasm aborted — degrade to empty
|
|
98
|
+
// (there is no typescript-compiler fallback by design, #402).
|
|
99
|
+
const root = await parseFactTree(ctx.filePath, content);
|
|
100
|
+
if (!root) {
|
|
101
|
+
setEmpty();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// --- module-type detection + static import/re-export node collection ---
|
|
105
|
+
let hasEsm = false;
|
|
106
|
+
let hasCjs = false;
|
|
107
|
+
const importNodes = [];
|
|
108
|
+
const reExportNodes = [];
|
|
109
|
+
for (const child of root.children ?? []) {
|
|
110
|
+
if (!child)
|
|
121
111
|
continue;
|
|
112
|
+
if (child.type === "import_statement") {
|
|
113
|
+
hasEsm = true;
|
|
114
|
+
importNodes.push(child);
|
|
122
115
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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")) {
|
|
122
|
+
hasEsm = true;
|
|
123
|
+
}
|
|
124
|
+
if (isReExport)
|
|
125
|
+
reExportNodes.push(child);
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
}
|
|
128
|
+
// --- dynamic import() / require() / module.exports (anywhere in the tree) ---
|
|
129
|
+
const dynamicRaw = [];
|
|
130
|
+
walk(root, (node) => {
|
|
131
|
+
if (node.type === "call_expression") {
|
|
132
|
+
const callee = node.children?.[0];
|
|
133
|
+
const args = firstChildOfType(node, "arguments");
|
|
134
|
+
const strArg = args ? firstChildOfType(args, "string") : undefined;
|
|
135
|
+
if (!strArg)
|
|
136
|
+
return; // non-string arg (e.g. template literal) — skip
|
|
137
|
+
const source = stripQuotes(strArg.text);
|
|
138
|
+
if (callee?.type === "import") {
|
|
139
|
+
dynamicRaw.push({ source, kind: "import" });
|
|
130
140
|
}
|
|
131
|
-
else {
|
|
132
|
-
|
|
141
|
+
else if (callee?.type === "identifier" && callee.text === "require") {
|
|
142
|
+
dynamicRaw.push({ source, kind: "require" });
|
|
143
|
+
hasCjs = true;
|
|
133
144
|
}
|
|
134
145
|
}
|
|
135
|
-
|
|
146
|
+
else if (node.type === "member_expression") {
|
|
147
|
+
// module.exports = ...
|
|
148
|
+
const obj = node.children?.[0];
|
|
149
|
+
if (obj?.type === "identifier" && obj.text === "module") {
|
|
150
|
+
const prop = firstChildOfType(node, "property_identifier");
|
|
151
|
+
if (prop?.text === "exports")
|
|
152
|
+
hasCjs = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const moduleType = hasEsm && !hasCjs
|
|
157
|
+
? "esm"
|
|
158
|
+
: hasCjs && !hasEsm
|
|
159
|
+
? "cjs"
|
|
160
|
+
: hasEsm || hasCjs
|
|
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
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
imports.push({ source: d.source, names: [], moduleType: "cjs" });
|
|
182
|
+
}
|
|
183
|
+
dynamicCount++;
|
|
136
184
|
}
|
|
137
|
-
// Dynamic imports and require() calls
|
|
138
|
-
const dynamic = collectDynamicImports(sourceFile, moduleType);
|
|
139
|
-
imports.push(...dynamic);
|
|
140
185
|
store.setFileFact(ctx.filePath, "file.imports", imports);
|
|
141
|
-
|
|
142
|
-
const
|
|
186
|
+
const reexports = [];
|
|
187
|
+
for (const node of reExportNodes) {
|
|
188
|
+
const r = parseReExport(node);
|
|
189
|
+
if (r)
|
|
190
|
+
reexports.push(r);
|
|
191
|
+
}
|
|
143
192
|
store.setFileFact(ctx.filePath, "file.reexports", reexports);
|
|
144
193
|
// Telemetry: log when a file has dynamic imports or re-exports so we can
|
|
145
194
|
// measure coverage and validate the implementation across real projects.
|
|
146
|
-
|
|
147
|
-
const reexportCount = reexports.length;
|
|
148
|
-
if (dynamicCount > 0 || reexportCount > 0) {
|
|
195
|
+
if (dynamicCount > 0 || reexports.length > 0) {
|
|
149
196
|
logLatency({
|
|
150
197
|
type: "call_graph_facts",
|
|
151
198
|
filePath: ctx.filePath,
|
|
@@ -154,7 +201,7 @@ export const importFactProvider = {
|
|
|
154
201
|
moduleType,
|
|
155
202
|
staticImports: imports.length - dynamicCount,
|
|
156
203
|
dynamicImports: dynamicCount,
|
|
157
|
-
reexports:
|
|
204
|
+
reexports: reexports.length,
|
|
158
205
|
starReexports: reexports.filter((r) => r.names.length === 0).length,
|
|
159
206
|
},
|
|
160
207
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fact-provider-specific tree-sitter helper (#402).
|
|
3
|
+
*
|
|
4
|
+
* The generic node-walk utilities (`walk`, `childrenOfType`, `firstChildOfType`,
|
|
5
|
+
* `parseTreeSitterRoot`, `TsNode`) live in `clients/tree-sitter-shared.ts` so both
|
|
6
|
+
* the fact extractors and the complexity client share them. This module re-exports
|
|
7
|
+
* them for the extractors and adds the FactProvider-specific `extractFactsFromTree`
|
|
8
|
+
* shell.
|
|
9
|
+
*/
|
|
10
|
+
import { childrenOfType, firstChildOfType, parseTreeSitterRoot, walk, } from "../../tree-sitter-shared.js";
|
|
11
|
+
export { childrenOfType, firstChildOfType, walk, };
|
|
12
|
+
/** Parse `content` for `filePath` into a root node (or null). Alias of the shared helper. */
|
|
13
|
+
export const parseFactTree = parseTreeSitterRoot;
|
|
14
|
+
/**
|
|
15
|
+
* Provider shell for tree-sitter-backed fact extractors: read `file.content`,
|
|
16
|
+
* parse via the shared client, and hand the root node to `extract`. On empty
|
|
17
|
+
* content / parse failure / wasm abort it writes the empty `defaults` and returns
|
|
18
|
+
* (there is no typescript-compiler fallback by design, #402). Centralising this
|
|
19
|
+
* keeps each provider to just its extraction logic (and de-duplicates the prologue).
|
|
20
|
+
*/
|
|
21
|
+
export async function extractFactsFromTree(ctx, store, defaults, extract) {
|
|
22
|
+
const writeAll = (facts) => {
|
|
23
|
+
for (const key of Object.keys(defaults)) {
|
|
24
|
+
store.setFileFact(ctx.filePath, key, facts[key] ?? defaults[key]);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const content = store.getFileFact(ctx.filePath, "file.content");
|
|
28
|
+
if (!content)
|
|
29
|
+
return writeAll(defaults);
|
|
30
|
+
const root = await parseFactTree(ctx.filePath, content);
|
|
31
|
+
if (!root)
|
|
32
|
+
return writeAll(defaults);
|
|
33
|
+
writeAll(extract(root, content));
|
|
34
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractFactsFromTree, firstChildOfType, walk, } from "./tree-sitter-facts.js";
|
|
2
2
|
// --- Helpers ---
|
|
3
3
|
function isOnlyWhitespaceOrComments(text) {
|
|
4
4
|
let stripped = text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -29,8 +29,10 @@ function detectBoundaryCategory(tryText) {
|
|
|
29
29
|
function detectTryResolvesLocalValues(tryText) {
|
|
30
30
|
// Heuristic: no await, no IO calls, no mutations via known side-effectful APIs
|
|
31
31
|
const hasAwait = /\bawait\b/.test(tryText);
|
|
32
|
-
const hasIO = DB_PATTERN.test(tryText) ||
|
|
33
|
-
|
|
32
|
+
const hasIO = DB_PATTERN.test(tryText) ||
|
|
33
|
+
NETWORK_PATTERN.test(tryText) ||
|
|
34
|
+
FS_PATTERN.test(tryText) ||
|
|
35
|
+
PROCESS_PATTERN.test(tryText);
|
|
34
36
|
return !hasAwait && !hasIO;
|
|
35
37
|
}
|
|
36
38
|
// --- Provider ---
|
|
@@ -41,72 +43,64 @@ export const tryCatchFactProvider = {
|
|
|
41
43
|
appliesTo(ctx) {
|
|
42
44
|
return /\.tsx?$/.test(ctx.filePath);
|
|
43
45
|
},
|
|
44
|
-
run(ctx, store) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
async run(ctx, store) {
|
|
47
|
+
await extractFactsFromTree(ctx, store, { "file.tryCatchSummaries": [] }, (root) => {
|
|
48
|
+
const summaries = [];
|
|
49
|
+
walk(root, (node) => {
|
|
50
|
+
if (node.type !== "try_statement")
|
|
51
|
+
return;
|
|
52
|
+
// The direct `statement_block` child is the try body; the catch/finally
|
|
53
|
+
// blocks are nested inside their own clauses (not direct children).
|
|
54
|
+
const tryBlock = firstChildOfType(node, "statement_block");
|
|
55
|
+
const catchClause = firstChildOfType(node, "catch_clause");
|
|
56
|
+
if (!tryBlock || !catchClause)
|
|
57
|
+
return;
|
|
58
|
+
const tryText = tryBlock.text;
|
|
55
59
|
const tryResolvesLocalValues = detectTryResolvesLocalValues(tryText);
|
|
56
60
|
const boundaryCategory = detectBoundaryCategory(tryText);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
tryResolvesLocalValues,
|
|
102
|
-
isFilesystemExistenceProbe,
|
|
103
|
-
boundaryCategory,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
ts.forEachChild(node, visit);
|
|
108
|
-
}
|
|
109
|
-
visit(sourceFile);
|
|
110
|
-
store.setFileFact(ctx.filePath, "file.tryCatchSummaries", summaries);
|
|
61
|
+
// Catch binding: `catch (e)` → identifier param; `catch {}` or a destructuring
|
|
62
|
+
// pattern → null (matching the old `ts.isIdentifier` gate).
|
|
63
|
+
const paramNode = firstChildOfType(catchClause, "identifier");
|
|
64
|
+
const catchParam = paramNode ? paramNode.text : null;
|
|
65
|
+
const catchBody = firstChildOfType(catchClause, "statement_block");
|
|
66
|
+
const bodyText = catchBody
|
|
67
|
+
? catchBody.text.replace(/^\{/, "").replace(/\}$/, "").trim()
|
|
68
|
+
: "";
|
|
69
|
+
const isEmpty = isOnlyWhitespaceOrComments(bodyText);
|
|
70
|
+
const hasRethrow = /\bthrow\b/.test(bodyText);
|
|
71
|
+
const hasLogging = /\bconsole\.(log|warn|error)\b/.test(bodyText) ||
|
|
72
|
+
/\blogger\./.test(bodyText);
|
|
73
|
+
// Derived enrichment fields
|
|
74
|
+
const catchReturnsDefault = DEFAULT_VALUE_PATTERN.test(bodyText);
|
|
75
|
+
const catchReturnsStructuredError = STRUCTURED_ERROR_PATTERN.test(bodyText);
|
|
76
|
+
const isDocumentedLocalFallback = EXPLAINING_COMMENT_PATTERN.test(bodyText);
|
|
77
|
+
const catchLogsOnly = hasLogging &&
|
|
78
|
+
!hasRethrow &&
|
|
79
|
+
!catchReturnsDefault &&
|
|
80
|
+
!catchReturnsStructuredError &&
|
|
81
|
+
!/\b(?:set|update|notify|emit|dispatch|resolve|reject)\b/.test(bodyText);
|
|
82
|
+
// Filesystem existence probe: try reads a file/path, catch returns a default
|
|
83
|
+
const isFilesystemExistenceProbe = boundaryCategory === "fs" &&
|
|
84
|
+
FS_PROBE_PATTERN.test(tryText) &&
|
|
85
|
+
catchReturnsDefault;
|
|
86
|
+
summaries.push({
|
|
87
|
+
line: catchClause.startPosition.row + 1,
|
|
88
|
+
column: catchClause.startPosition.column + 1,
|
|
89
|
+
catchParam,
|
|
90
|
+
bodyText,
|
|
91
|
+
isEmpty,
|
|
92
|
+
hasRethrow,
|
|
93
|
+
hasLogging,
|
|
94
|
+
catchLogsOnly,
|
|
95
|
+
catchReturnsDefault,
|
|
96
|
+
catchReturnsStructuredError,
|
|
97
|
+
isDocumentedLocalFallback,
|
|
98
|
+
tryResolvesLocalValues,
|
|
99
|
+
isFilesystemExistenceProbe,
|
|
100
|
+
boundaryCategory,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return { "file.tryCatchSummaries": summaries };
|
|
104
|
+
});
|
|
111
105
|
},
|
|
112
106
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline `pi-lens-ignore` suppression — shared between the per-edit dispatch
|
|
3
|
+
* pipeline (`lens_diagnostics mode=all`) and the project-wide `mode=full` sweep so
|
|
4
|
+
* BOTH honor the same comments (#442). Previously this lived privately in the
|
|
5
|
+
* dispatcher, so a site suppressed on the write path reappeared as blocking in the
|
|
6
|
+
* full scan, making `mode=full` unusable as a clean gate.
|
|
7
|
+
*
|
|
8
|
+
* Syntax: `// pi-lens-ignore: rule-id` (JS/TS) or `# pi-lens-ignore: rule-id`
|
|
9
|
+
* (Python/Ruby/…), comma-separated for multiple rules, on the same line as the
|
|
10
|
+
* diagnostic or the line immediately above it.
|
|
11
|
+
*/
|
|
12
|
+
const SUPPRESS_RE = /(?:\/\/|#)\s*pi-lens-ignore:\s*(.+)/;
|
|
13
|
+
/**
|
|
14
|
+
* Normalize a rule id to the form a user writes in a `pi-lens-ignore` comment.
|
|
15
|
+
* The napi scan and the ast-grep LSP tag the same rule as `ast-grep:<id>` /
|
|
16
|
+
* `<id>-js` in some surfaces (see `normalizeRuleForDedup` in lens-diagnostics);
|
|
17
|
+
* a user's bare `<id>` must still suppress those, so we match the normalized form
|
|
18
|
+
* as well as the raw one.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeSuppressRule(ruleId) {
|
|
21
|
+
return ruleId.replace(/^ast-grep:/, "").replace(/-js$/, "");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drop diagnostics suppressed by an inline `pi-lens-ignore: <rule[,rule2]>`
|
|
25
|
+
* comment in `content` (the file the diagnostics belong to). A diagnostic is
|
|
26
|
+
* suppressed when its rule id — raw OR normalized — is listed on its own line or
|
|
27
|
+
* the line immediately above. Returns the surviving diagnostics (same array if
|
|
28
|
+
* nothing is suppressed).
|
|
29
|
+
*/
|
|
30
|
+
export function applyInlineSuppressions(diagnostics, content) {
|
|
31
|
+
if (!content || !diagnostics.length)
|
|
32
|
+
return diagnostics;
|
|
33
|
+
// Build the set of (1-based line, rule-id) pairs that are suppressed.
|
|
34
|
+
const suppressed = new Set();
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
const m = SUPPRESS_RE.exec(lines[i]);
|
|
38
|
+
if (!m)
|
|
39
|
+
continue;
|
|
40
|
+
const rules = m[1]
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((r) => r.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
const suppressedLine = i + 1; // same line (1-based)
|
|
45
|
+
const nextLine = i + 2; // next line (1-based)
|
|
46
|
+
for (const ruleId of rules) {
|
|
47
|
+
suppressed.add(`${suppressedLine}:${ruleId}`);
|
|
48
|
+
suppressed.add(`${nextLine}:${ruleId}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (suppressed.size === 0)
|
|
52
|
+
return diagnostics;
|
|
53
|
+
return diagnostics.filter((d) => {
|
|
54
|
+
const rawId = d.rule ?? d.id ?? "";
|
|
55
|
+
const line = d.line ?? 1;
|
|
56
|
+
if (suppressed.has(`${line}:${rawId}`))
|
|
57
|
+
return false;
|
|
58
|
+
const normId = normalizeSuppressRule(rawId);
|
|
59
|
+
return normId === rawId || !suppressed.has(`${line}:${normId}`);
|
|
60
|
+
});
|
|
61
|
+
}
|