pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
103 lines (102 loc) • 3.83 kB
JavaScript
/**
* File Role Classification for pi-lens
*
* Classifies files by their structural role in the project so runners
* can skip checks that don't apply (e.g. no complexity checks on generated
* files, no logic-density checks on re-export barrels).
*/
import { basename, dirname } from "node:path";
import { isGeneratedOrArtifact } from "./generated-artifacts.js";
// --- Detection ---
const RE_EXPORT_LINE = /^export\s+(type\s+)?\*\s+from\s+|^export\s*\{[^}]*\}\s+from\s+/;
function isReExportBarrel(content) {
const lines = content
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("//") && !l.startsWith("*") && !l.startsWith("/*"));
return lines.length > 0 && lines.every((l) => RE_EXPORT_LINE.test(l));
}
function isTypeStub(content) {
const lines = content
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l &&
!l.startsWith("//") &&
!l.startsWith("*") &&
!l.startsWith("/*") &&
l !== "{" &&
l !== "}");
if (lines.length < 4)
return false;
const typeOnlyLines = lines.filter((l) => /^(export\s+)?(interface|type|abstract\s+class|declare\s+(class|function|const|var|let|module|namespace|enum))\s+/.test(l) ||
/^(import\s+type\s+|import\s+\{)/.test(l) ||
/^export\s+type\s+\{/.test(l));
return typeOnlyLines.length / lines.length >= 0.75;
}
/**
* Classify the structural role of a file. Pass `content` for higher
* accuracy (enables generated-code detection, barrel detection, etc.).
*/
export function detectFileRole(filePath, content) {
const base = basename(filePath).toLowerCase();
const dir = dirname(filePath).replace(/\\/g, "/").toLowerCase();
// --- Test ---
if (base.includes(".test.") ||
base.includes(".spec.") ||
base.startsWith("test_") ||
base.startsWith("spec_") ||
dir.includes("/__tests__/") ||
dir.includes("/test/") ||
dir.includes("/tests/") ||
dir.includes("/spec/") ||
dir.includes("/specs/") ||
/[/_-]tests?\/?$/.test(dir) ||
/[/_-]specs?\/?$/.test(dir))
return "test";
// --- Generated/artifact markers (path/name/header-based) ---
if (isGeneratedOrArtifact(filePath, { content, includeDeclarations: false })) {
return "generated";
}
// --- Migrations ---
// Normalize separators before regex so the pattern works on Windows too
const forwardDir = dir.replace(/\\/g, "/");
if (forwardDir.includes("/migrations/") ||
forwardDir.includes("/migration/") ||
/\/\d{4,}_[a-z]/.test(forwardDir + "/" + base))
return "migration";
// --- Config ---
if (base.endsWith(".config.ts") ||
base.endsWith(".config.js") ||
base.endsWith(".config.mjs") ||
base.endsWith(".config.cjs") ||
base === "vite.config.ts" ||
base === "jest.config.ts" ||
base === "jest.config.js" ||
base === "webpack.config.js" ||
base === "rollup.config.js" ||
base === "tailwind.config.js" ||
base === "tailwind.config.ts" ||
base === "next.config.js" ||
base === "next.config.mjs")
return "config";
// --- Type stubs ---
if (base.endsWith(".d.ts"))
return "stub";
if (content && isTypeStub(content))
return "stub";
// --- Init / barrel ---
const isIndexFile = base === "index.ts" ||
base === "index.tsx" ||
base === "index.js" ||
base === "index.mjs" ||
base === "index.cjs" ||
base === "index.jsx" ||
base === "__init__.py" ||
base === "mod.rs";
if (isIndexFile) {
if (content && isReExportBarrel(content))
return "re-export";
return "init";
}
return "source";
}