pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
107 lines (106 loc) • 5.5 kB
JavaScript
import { extractFactsFromTree, firstChildOfType, walk, } from "./tree-sitter-facts.js";
// --- Helpers ---
function isOnlyWhitespaceOrComments(text) {
let stripped = text.replace(/\/\*[\s\S]*?\*\//g, "");
stripped = stripped.replace(/\/\/[^\n]*/g, "");
return stripped.trim().length === 0;
}
const DEFAULT_VALUE_PATTERN = /\breturn\s+(null|undefined|false|true|0|""|''|``|\[\]|\{\}|new\s+\w+\(\))/;
const STRUCTURED_ERROR_PATTERN = /\breturn\s+\{[^}]*(?:success\s*:\s*false|error\s*:)/;
// Any non-trivial comment (≥ 4 non-space chars) counts as documented intent.
// This covers patterns like: // continue, /* not found */, // best-effort, etc.
const EXPLAINING_COMMENT_PATTERN = /(?:\/\/\s*\S.{3,}|\/\*\s*\S[\s\S]{3,}?\*\/)/;
const FS_PROBE_PATTERN = /\b(?:existsSync|statSync|lstatSync|readFileSync|accessSync)\b/;
const DB_PATTERN = /\b(?:query|execute|findOne|findMany|findById|insert|update|delete|select|prisma\.|knex\.|sequelize\.)/;
const NETWORK_PATTERN = /\b(?:fetch|axios|http\.|https\.|request\.|got\.|undici\.)/;
const FS_PATTERN = /\b(?:readFileSync?|writeFileSync?|appendFileSync?|readdirSync?|mkdirSync?|statSync?|unlinkSync?|existsSync|accessSync?|copyFileSync?|renameSync?)\b/;
const PROCESS_PATTERN = /\b(?:spawn|exec|execSync|spawnSync|child_process\.)\b/;
function detectBoundaryCategory(tryText) {
if (DB_PATTERN.test(tryText))
return "db";
if (NETWORK_PATTERN.test(tryText))
return "network";
if (FS_PATTERN.test(tryText))
return "fs";
if (PROCESS_PATTERN.test(tryText))
return "process";
return "none";
}
function detectTryResolvesLocalValues(tryText) {
// Heuristic: no await, no IO calls, no mutations via known side-effectful APIs
const hasAwait = /\bawait\b/.test(tryText);
const hasIO = DB_PATTERN.test(tryText) ||
NETWORK_PATTERN.test(tryText) ||
FS_PATTERN.test(tryText) ||
PROCESS_PATTERN.test(tryText);
return !hasAwait && !hasIO;
}
// --- Provider ---
export const tryCatchFactProvider = {
id: "tryCatchFacts",
provides: ["file.tryCatchSummaries"],
requires: ["file.content"],
appliesTo(ctx) {
return /\.tsx?$/.test(ctx.filePath);
},
async run(ctx, store) {
await extractFactsFromTree(ctx, store, { "file.tryCatchSummaries": [] }, (root) => {
const summaries = [];
walk(root, (node) => {
if (node.type !== "try_statement")
return;
// The direct `statement_block` child is the try body; the catch/finally
// blocks are nested inside their own clauses (not direct children).
const tryBlock = firstChildOfType(node, "statement_block");
const catchClause = firstChildOfType(node, "catch_clause");
if (!tryBlock || !catchClause)
return;
const tryText = tryBlock.text;
const tryResolvesLocalValues = detectTryResolvesLocalValues(tryText);
const boundaryCategory = detectBoundaryCategory(tryText);
// Catch binding: `catch (e)` → identifier param; `catch {}` or a destructuring
// pattern → null (matching the old `ts.isIdentifier` gate).
const paramNode = firstChildOfType(catchClause, "identifier");
const catchParam = paramNode ? paramNode.text : null;
const catchBody = firstChildOfType(catchClause, "statement_block");
const bodyText = catchBody
? catchBody.text.replace(/^\{/, "").replace(/\}$/, "").trim()
: "";
const isEmpty = isOnlyWhitespaceOrComments(bodyText);
const hasRethrow = /\bthrow\b/.test(bodyText);
const hasLogging = /\bconsole\.(log|warn|error)\b/.test(bodyText) ||
/\blogger\./.test(bodyText);
// Derived enrichment fields
const catchReturnsDefault = DEFAULT_VALUE_PATTERN.test(bodyText);
const catchReturnsStructuredError = STRUCTURED_ERROR_PATTERN.test(bodyText);
const isDocumentedLocalFallback = EXPLAINING_COMMENT_PATTERN.test(bodyText);
const catchLogsOnly = hasLogging &&
!hasRethrow &&
!catchReturnsDefault &&
!catchReturnsStructuredError &&
!/\b(?:set|update|notify|emit|dispatch|resolve|reject)\b/.test(bodyText);
// Filesystem existence probe: try reads a file/path, catch returns a default
const isFilesystemExistenceProbe = boundaryCategory === "fs" &&
FS_PROBE_PATTERN.test(tryText) &&
catchReturnsDefault;
summaries.push({
line: catchClause.startPosition.row + 1,
column: catchClause.startPosition.column + 1,
catchParam,
bodyText,
isEmpty,
hasRethrow,
hasLogging,
catchLogsOnly,
catchReturnsDefault,
catchReturnsStructuredError,
isDocumentedLocalFallback,
tryResolvesLocalValues,
isFilesystemExistenceProbe,
boundaryCategory,
});
});
return { "file.tryCatchSummaries": summaries };
});
},
};