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.
Files changed (96) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +0 -1
  3. package/dist/clients/actionable-warnings-logger.js +11 -36
  4. package/dist/clients/ast-grep-tool-logger.js +11 -36
  5. package/dist/clients/biome-client.js +10 -2
  6. package/dist/clients/bootstrap.js +1 -3
  7. package/dist/clients/cascade-logger.js +7 -12
  8. package/dist/clients/complexity-client.js +397 -623
  9. package/dist/clients/dead-code-logger.js +11 -22
  10. package/dist/clients/dependency-checker.js +15 -2
  11. package/dist/clients/deps/vscode-jsonrpc.js +1 -1
  12. package/dist/clients/diagnostic-logger.js +7 -31
  13. package/dist/clients/dispatch/auxiliary-lsp.js +38 -0
  14. package/dist/clients/dispatch/dispatcher.js +4 -39
  15. package/dist/clients/dispatch/fact-runner.js +9 -57
  16. package/dist/clients/dispatch/facts/comment-facts.js +12 -25
  17. package/dist/clients/dispatch/facts/function-facts.js +161 -132
  18. package/dist/clients/dispatch/facts/import-facts.js +164 -117
  19. package/dist/clients/dispatch/facts/tree-sitter-facts.js +34 -0
  20. package/dist/clients/dispatch/facts/try-catch-facts.js +62 -68
  21. package/dist/clients/dispatch/inline-suppressions.js +61 -0
  22. package/dist/clients/dispatch/integration.js +94 -32
  23. package/dist/clients/dispatch/plan.js +0 -49
  24. package/dist/clients/dispatch/priorities.js +0 -1
  25. package/dist/clients/dispatch/rules/cors-wildcard.js +54 -0
  26. package/dist/clients/dispatch/rules/high-import-coupling.js +37 -0
  27. package/dist/clients/dispatch/rules/no-commented-credentials.js +52 -0
  28. package/dist/clients/dispatch/runners/index.js +2 -7
  29. package/dist/clients/dispatch/runners/lsp.js +18 -6
  30. package/dist/clients/dispatch/runners/tree-sitter.js +11 -64
  31. package/dist/clients/dispatch/runners/utils/runner-helpers.js +28 -33
  32. package/dist/clients/dispatch/tool-profile.js +0 -1
  33. package/dist/clients/dispatch/types.js +1 -1
  34. package/dist/clients/formatters.js +10 -0
  35. package/dist/clients/git-guard.js +1 -1
  36. package/dist/clients/grammar-source.js +65 -2
  37. package/dist/clients/jscpd-client.js +8 -2
  38. package/dist/clients/language-policy.js +2 -2
  39. package/dist/clients/latency-logger.js +10 -15
  40. package/dist/clients/lsp/client.js +74 -11
  41. package/dist/clients/lsp/config.js +61 -3
  42. package/dist/clients/lsp/index.js +43 -3
  43. package/dist/clients/lsp/launch.js +3 -32
  44. package/dist/clients/mcp/session.js +0 -1
  45. package/dist/clients/module-report.js +16 -5
  46. package/dist/clients/ndjson-logger.js +150 -0
  47. package/dist/clients/package-manager.js +65 -0
  48. package/dist/clients/pipeline.js +25 -41
  49. package/dist/clients/project-diagnostics/extractors.js +85 -0
  50. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +44 -0
  51. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +25 -0
  52. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +34 -0
  53. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +38 -0
  54. package/dist/clients/project-diagnostics/runner-adapters/madge.js +47 -0
  55. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +31 -0
  56. package/dist/clients/project-diagnostics/scanner.js +11 -10
  57. package/dist/clients/read-guard-logger.js +11 -36
  58. package/dist/clients/review-graph/builder.js +337 -27
  59. package/dist/clients/review-graph/git-identity.js +150 -0
  60. package/dist/clients/review-graph/service.js +3 -3
  61. package/dist/clients/runtime-coordinator.js +83 -0
  62. package/dist/clients/runtime-session.js +28 -2
  63. package/dist/clients/runtime-tool-result.js +10 -2
  64. package/dist/clients/runtime-turn.js +25 -7
  65. package/dist/clients/sg-runner.js +15 -0
  66. package/dist/clients/test-runner-client.js +12 -6
  67. package/dist/clients/tree-sitter-cache.js +47 -14
  68. package/dist/clients/tree-sitter-client.js +9 -1
  69. package/dist/clients/tree-sitter-logger.js +7 -12
  70. package/dist/clients/tree-sitter-query-loader.js +1 -0
  71. package/dist/clients/tree-sitter-shared.js +113 -0
  72. package/dist/clients/tree-sitter-symbol-extractor.js +19 -12
  73. package/dist/index.js +4 -22
  74. package/dist/tools/lens-diagnostics.js +254 -20
  75. package/grammars/tree-sitter-yaml.wasm +0 -0
  76. package/grammars/tree-sitter-yaml.wasm.json +3 -3
  77. package/package.json +4 -3
  78. package/rules/ast-grep-rules/rule-tests/no-init-return-test.yml +15 -0
  79. package/rules/ast-grep-rules/rules/no-init-return.yml +11 -5
  80. package/rules/tree-sitter-queries/python/python-assert-production.yml +4 -0
  81. package/scripts/download-grammars.js +29 -4
  82. package/scripts/grammars.lock.json +14 -2
  83. package/scripts/install-selftest.mjs +8 -7
  84. package/dist/clients/deps/typescript.js +0 -15
  85. package/dist/clients/dispatch/rules/quality-rules.js +0 -297
  86. package/dist/clients/dispatch/rules/sonar-rules.js +0 -493
  87. package/dist/clients/dispatch/runners/biome.js +0 -69
  88. package/dist/clients/dispatch/runners/python-slop.js +0 -106
  89. package/dist/clients/dispatch/runners/ts-lsp.js +0 -109
  90. package/dist/clients/runner-tracker.js +0 -153
  91. package/dist/clients/ts-service.js +0 -130
  92. package/dist/clients/type-coverage-client.js +0 -128
  93. package/dist/clients/typescript-client.js +0 -509
  94. package/dist/commands/booboo.js +0 -1412
  95. package/rules/python-slop-rules/.sgconfig.yml +0 -4
  96. 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
- function detectModuleType(sourceFile) {
4
- let hasEsm = false;
5
- let hasCjs = false;
6
- function visit(node) {
7
- if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
8
- hasEsm = true;
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 (ts.isCallExpression(node) &&
11
- ts.isIdentifier(node.expression) &&
12
- node.expression.text === "require" &&
13
- node.arguments.length === 1 &&
14
- ts.isStringLiteral(node.arguments[0])) {
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 (ts.isPropertyAccessExpression(node) &&
18
- ts.isIdentifier(node.expression) &&
19
- node.expression.text === "module" &&
20
- node.name.text === "exports") {
21
- hasCjs = true;
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
- ts.forEachChild(sourceFile, visit);
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
- function collectDynamicImports(sourceFile, moduleType) {
35
- const entries = [];
36
- function visit(node) {
37
- // import('specifier') dynamic ESM import
38
- if (ts.isCallExpression(node) &&
39
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
40
- node.arguments.length === 1 &&
41
- ts.isStringLiteral(node.arguments[0])) {
42
- entries.push({
43
- source: node.arguments[0].text,
44
- names: [],
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
- ts.forEachChild(sourceFile, visit);
64
- return entries;
65
- }
66
- function collectReExports(sourceFile) {
67
- const entries = [];
68
- for (const stmt of sourceFile.statements) {
69
- if (!ts.isExportDeclaration(stmt) || !stmt.moduleSpecifier)
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 entries;
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 in EXT_TO_SCRIPT_KIND;
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
- if (!content) {
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
- const ext = ctx.filePath.slice(ctx.filePath.lastIndexOf(".")).toLowerCase();
109
- const scriptKind = EXT_TO_SCRIPT_KIND[ext] ?? ts.ScriptKind.JS;
110
- const sourceFile = ts.createSourceFile(ctx.filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
111
- const moduleType = detectModuleType(sourceFile);
112
- const imports = [];
113
- // Static import declarations
114
- for (const stmt of sourceFile.statements) {
115
- if (!ts.isImportDeclaration(stmt))
116
- continue;
117
- const source = stmt.moduleSpecifier.text;
118
- const clause = stmt.importClause;
119
- if (!clause) {
120
- imports.push({ source, names: [], moduleType });
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
- const entry = { source, names: [], moduleType };
124
- if (clause.name) {
125
- entry.defaultName = clause.name.text;
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
- if (clause.namedBindings) {
128
- if (ts.isNamespaceImport(clause.namedBindings)) {
129
- entry.namespace = clause.namedBindings.name.text;
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
- entry.names = clause.namedBindings.elements.map((e) => e.name.text);
141
+ else if (callee?.type === "identifier" && callee.text === "require") {
142
+ dynamicRaw.push({ source, kind: "require" });
143
+ hasCjs = true;
133
144
  }
134
145
  }
135
- imports.push(entry);
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
- // Re-export edges (used by call graph for barrel-file traversal)
142
- const reexports = collectReExports(sourceFile);
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
- const dynamicCount = dynamic.length;
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: reexportCount,
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 { ts } from "../../deps/typescript.js";
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) || NETWORK_PATTERN.test(tryText) ||
33
- FS_PATTERN.test(tryText) || PROCESS_PATTERN.test(tryText);
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
- const content = store.getFileFact(ctx.filePath, "file.content");
46
- if (!content) {
47
- store.setFileFact(ctx.filePath, "file.tryCatchSummaries", []);
48
- return;
49
- }
50
- const sourceFile = ts.createSourceFile(ctx.filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
51
- const summaries = [];
52
- function visit(node) {
53
- if (ts.isTryStatement(node)) {
54
- const tryText = node.tryBlock.getText(sourceFile);
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
- if (node.catchClause) {
58
- const clause = node.catchClause;
59
- const pos = sourceFile.getLineAndCharacterOfPosition(clause.getStart(sourceFile));
60
- let catchParam = null;
61
- if (clause.variableDeclaration) {
62
- const name = clause.variableDeclaration.name;
63
- if (ts.isIdentifier(name)) {
64
- catchParam = name.text;
65
- }
66
- }
67
- const bodyText = clause.block
68
- .getText(sourceFile)
69
- .replace(/^\{/, "")
70
- .replace(/\}$/, "")
71
- .trim();
72
- const isEmpty = isOnlyWhitespaceOrComments(bodyText);
73
- const hasRethrow = /\bthrow\b/.test(bodyText);
74
- const hasLogging = /\bconsole\.(log|warn|error)\b/.test(bodyText) ||
75
- /\blogger\./.test(bodyText);
76
- // Derived enrichment fields
77
- const catchReturnsDefault = DEFAULT_VALUE_PATTERN.test(bodyText);
78
- const catchReturnsStructuredError = STRUCTURED_ERROR_PATTERN.test(bodyText);
79
- const isDocumentedLocalFallback = EXPLAINING_COMMENT_PATTERN.test(bodyText);
80
- const catchLogsOnly = hasLogging &&
81
- !hasRethrow &&
82
- !catchReturnsDefault &&
83
- !catchReturnsStructuredError &&
84
- !/\b(?:set|update|notify|emit|dispatch|resolve|reject)\b/.test(bodyText);
85
- // Filesystem existence probe: try reads a file/path, catch returns a default
86
- const isFilesystemExistenceProbe = boundaryCategory === "fs" &&
87
- FS_PROBE_PATTERN.test(tryText) &&
88
- catchReturnsDefault;
89
- summaries.push({
90
- line: pos.line + 1,
91
- column: pos.character + 1,
92
- catchParam,
93
- bodyText,
94
- isEmpty,
95
- hasRethrow,
96
- hasLogging,
97
- catchLogsOnly,
98
- catchReturnsDefault,
99
- catchReturnsStructuredError,
100
- isDocumentedLocalFallback,
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
+ }