pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
212 lines (210 loc) • 6.7 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
collectSourceFiles
} from "./chunk-6QIUX4EP.js";
import "./chunk-VHQLPIRJ.js";
import "./chunk-VN2AXLEX.js";
import "./chunk-QYF5S6P6.js";
import "./chunk-5ZEJHV35.js";
import "./chunk-DQ2NZ7C4.js";
import "./chunk-VNMT7C64.js";
import "./chunk-2EECBNC5.js";
import "./chunk-N3YQJI6O.js";
import "./chunk-O6TQT6RI.js";
import "./chunk-5THXD6X5.js";
import "./chunk-UK3CMEAL.js";
import "./chunk-LJDODBBZ.js";
import "./chunk-ABBOA7UD.js";
import "./chunk-Q5U6FMDI.js";
import "./chunk-NXL4FFQQ.js";
// dist/clients/todo-scanner.js
import * as fs from "node:fs";
import * as path from "node:path";
var MAX_SCAN_FILE_BYTES = 512 * 1024;
var MARKDOWN_FILE_RE = /\.(?:md|mdx|markdown)$/i;
var TodoScanner = class {
/**
* Pattern matches actionable annotations only.
* Excludes NOTE and DEPRECATED — these are documentation, not work items.
* Case-sensitive to avoid matching "Note:" in prose.
*/
pattern = /\b(TODO|FIXME|HACK|XXX|BUG)\b\s*[(:]?\s*(.+)/g;
/**
* Check if a match position is inside a comment context.
* Handles: // line comments, star-slash block comments, * JSDoc lines, # Python comments
*/
isInComment(line, matchIndex, markdown = false) {
if (markdown) {
const beforeMatch2 = line.slice(0, matchIndex);
const open = beforeMatch2.lastIndexOf("<!--");
return open !== -1 && beforeMatch2.lastIndexOf("-->") < open;
}
const trimmed = line.trimStart();
if (/^\/\/|^\/\*|^\*|^#/.test(trimmed))
return true;
const beforeMatch = line.slice(0, matchIndex);
const lineCommentPos = beforeMatch.lastIndexOf("//");
if (lineCommentPos !== -1) {
const beforeComment = beforeMatch.slice(0, lineCommentPos);
const singleQuotes = (beforeComment.match(/'/g) || []).length;
const doubleQuotes = (beforeComment.match(/"/g) || []).length;
const backticks = (beforeComment.match(/`/g) || []).length;
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
return true;
}
}
const blockOpen = beforeMatch.lastIndexOf("/*");
const blockClose = beforeMatch.lastIndexOf("*/");
if (blockOpen !== -1 && blockClose < blockOpen)
return true;
const hashPos = beforeMatch.lastIndexOf("#");
if (hashPos !== -1) {
const beforeHash = beforeMatch.slice(0, hashPos);
const singleQuotes = (beforeHash.match(/'/g) || []).length;
const doubleQuotes = (beforeHash.match(/"/g) || []).length;
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
return true;
}
}
return false;
}
/**
* Scan a single file for TODOs.
*/
scanFile(filePath) {
const absolutePath = path.resolve(filePath);
try {
const stat = fs.statSync(absolutePath);
if (!stat.isFile() || stat.size > MAX_SCAN_FILE_BYTES)
return [];
} catch {
return [];
}
let content;
try {
content = fs.readFileSync(absolutePath, "utf-8");
} catch {
return [];
}
const markdown = MARKDOWN_FILE_RE.test(absolutePath);
const lines = content.split("\n");
const items = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const matches = line.matchAll(this.pattern);
for (const match of matches) {
if (!this.isInComment(line, match.index ?? 0, markdown))
continue;
const type = match[1];
const message = (match[2] || "").trim().replace(/\s*\*\/\s*$/, "");
items.push({
type,
message: message.slice(0, 200),
// Limit message length
file: path.relative(process.cwd(), absolutePath),
line: i + 1,
column: match.index || 0
});
}
}
return items;
}
/**
* Scan a list of pre-filtered files (recommended — uses source-filter module).
* Callers should use collectSourceFiles() to get deduplicated source files.
*/
scanFiles(filePaths) {
const items = [];
for (const filePath of filePaths) {
if (filePath.endsWith("todo-scanner.ts") || filePath.endsWith("todo-scanner.js"))
continue;
if (/\.(test|spec)\.[jt]sx?$/.test(filePath))
continue;
items.push(...this.scanFile(filePath));
}
return this.groupResults(items);
}
/**
* Scan a directory recursively using the source-filter module to exclude build artifacts.
* This is the preferred entry point for new callers.
*/
scanDirectory(dirPath) {
const sourceFiles = collectSourceFiles(dirPath);
return this.scanFiles(sourceFiles);
}
/**
* Group scan results by type and file.
*/
groupResults(items) {
const byType = /* @__PURE__ */ new Map();
for (const item of items) {
const existing = byType.get(item.type) || [];
existing.push(item);
byType.set(item.type, existing);
}
const byFile = /* @__PURE__ */ new Map();
for (const item of items) {
const existing = byFile.get(item.file) || [];
existing.push(item);
byFile.set(item.file, existing);
}
return { items, byType, byFile };
}
/**
* Format scan results for LLM consumption.
*/
formatResult(result, maxItems = 30) {
if (result.items.length === 0)
return "";
let output = `[TODOs] ${result.items.length} annotation(s) found`;
const typeCounts = [];
for (const [type, items] of result.byType) {
typeCounts.push(`${items.length} ${type}`);
}
if (typeCounts.length > 0) {
output += ` (${typeCounts.join(", ")})`;
}
output += ":\n";
const priorityOrder = [
"FIXME",
"HACK",
"BUG",
"TODO",
"XXX"
];
const sorted = [...result.items].sort((a, b) => {
const aIdx = priorityOrder.indexOf(a.type);
const bIdx = priorityOrder.indexOf(b.type);
return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
});
for (const item of sorted.slice(0, maxItems)) {
const icon = this.getIcon(item.type);
output += ` ${icon} ${item.file}:${item.line} \u2014 ${item.type}: ${item.message}
`;
}
if (result.items.length > maxItems) {
output += ` ... and ${result.items.length - maxItems} more
`;
}
return output;
}
getIcon(type) {
switch (type) {
case "FIXME":
return "\u{1F534}";
case "HACK":
return "\u{1F7E0}";
case "BUG":
return "\u{1F41B}";
case "TODO":
return "\u{1F4DD}";
case "XXX":
return "\u274C";
default:
return "\u2022";
}
}
};
export {
TodoScanner
};