pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
147 lines (146 loc) • 5.24 kB
JavaScript
/**
* Centralized generated/artifact detection.
*
* This module is intentionally lightweight and synchronous because it is used by
* hot-path source collection and dispatch role detection. It combines cheap
* path/name heuristics with optional header inspection for common generated-code
* banners.
*/
import * as fs from "node:fs";
import * as path from "node:path";
const DEFAULT_HEADER_BYTES = 4096;
const GENERATED_DIR_NAMES = new Set([
"generated",
"__generated__",
"codegen",
"__codegen__",
"generated-sources",
"generated_sources",
"openapi-generated",
".openapi-generator",
]);
const GENERATED_HEADER_PATTERNS = [
/@generated\b/i,
/@ts-nocheck\b/i,
/\bcode generated\b/i,
/\bdo not edit\b/i,
/\bautomatically generated\b/i,
/\bthis file was generated\b/i,
/\bgenerated by protoc\b/i,
/\bgenerated by graphql-code-generator\b/i,
/\bgenerated by openapi-generator\b/i,
/\bgenerated by swagger codegen\b/i,
];
const GENERATED_FILE_PATTERNS = [
/(^|[._-])generated([._-]|$)/i,
/(^|[._-])gen([._-])/i,
/_generated\.(go|rs|py|rb|java|kt|ts|tsx|js|jsx|mjs|cjs)$/i,
/_gen\.(go|rs|py|rb|java|kt|ts|tsx|js|jsx|mjs|cjs)$/i,
/\.pb\.(go|ts|tsx|js|jsx|mjs|cjs)$/i,
/_pb2(_grpc)?\.py$/i,
/\.sql\.(go|ts|js)$/i,
/_sqlc\.go$/i,
/\.g\.([jt]sx?|mjs|cjs|rs|go)$/i,
/\.min\.(js|mjs|cjs|css)$/i,
/\.(bundle|chunk)\.(js|mjs|cjs|css)$/i,
];
const DECLARATION_FILE_PATTERNS = [/\.d\.ts$/i, /\.d\.mts$/i, /\.d\.cts$/i];
function pathSegments(filePath) {
return path
.normalize(filePath)
.split(/[\\/]+/)
.filter(Boolean);
}
export function isGeneratedArtifactDirectoryName(dirName) {
return GENERATED_DIR_NAMES.has(dirName.trim().toLowerCase());
}
export function isDeclarationFile(filePath) {
const base = path.basename(filePath).toLowerCase();
return DECLARATION_FILE_PATTERNS.some((pattern) => pattern.test(base));
}
function hasGeneratedArtifactPath(filePath) {
const segments = pathSegments(filePath);
const dirSegments = segments.slice(0, -1);
if (dirSegments.some((segment) => isGeneratedArtifactDirectoryName(segment))) {
return true;
}
const base = path.basename(filePath);
return GENERATED_FILE_PATTERNS.some((pattern) => pattern.test(base));
}
function hasGeneratedArtifactContent(content) {
const header = content.slice(0, DEFAULT_HEADER_BYTES);
return GENERATED_HEADER_PATTERNS.some((pattern) => pattern.test(header));
}
// Memoize the generated-banner verdict per (path, mtimeMs, size, maxBytes).
// The 4 KB header read (openSync + readSync + closeSync) is ~70% of the
// per-file cost in `collectSourceFiles`, and the same files are re-scanned by
// multiple background scanners (todo, project-diagnostics, language-profile)
// every session/turn. Keying on mtime+size makes the memo self-invalidating:
// an edited file misses the cache and is re-read. A single stat replaces the
// open/read/close on a hit. Bounded to avoid unbounded growth on giant repos.
const HEADER_VERDICT_MEMO_CAP = 50_000;
const headerVerdictMemo = new Map();
/** Test-only: drop the header-verdict memo so fixtures don't leak across cases. */
export function _resetGeneratedArtifactCaches() {
headerVerdictMemo.clear();
}
function readFileHeader(filePath, maxBytes = DEFAULT_HEADER_BYTES) {
let fd;
try {
fd = fs.openSync(filePath, "r");
const buffer = Buffer.alloc(Math.max(0, maxBytes));
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
return buffer.toString("utf8", 0, bytesRead);
}
catch {
return undefined;
}
finally {
if (fd !== undefined) {
try {
fs.closeSync(fd);
}
catch {
/* ignore close errors */
}
}
}
}
/**
* Memoized form of "does this file's header contain a generated-code banner?".
* Returns `false` when the file cannot be stat'd/read (same fallback as the
* inline read path). Keyed on path + mtime + size so edits invalidate it.
*/
function fileHeaderLooksGenerated(filePath, maxBytes) {
let key;
try {
const st = fs.statSync(filePath);
key = `${st.mtimeMs}:${st.size}:${maxBytes}:${filePath}`;
}
catch {
return false;
}
const cached = headerVerdictMemo.get(key);
if (cached !== undefined)
return cached;
const header = readFileHeader(filePath, maxBytes);
const verdict = header !== undefined && hasGeneratedArtifactContent(header);
if (headerVerdictMemo.size >= HEADER_VERDICT_MEMO_CAP) {
headerVerdictMemo.clear();
}
headerVerdictMemo.set(key, verdict);
return verdict;
}
export function isGeneratedOrArtifact(filePath, options = {}) {
if (hasGeneratedArtifactPath(filePath))
return true;
if (options.includeDeclarations && isDeclarationFile(filePath))
return true;
if (options.content !== undefined) {
return hasGeneratedArtifactContent(options.content);
}
if (options.readContentHeader) {
return fileHeaderLooksGenerated(filePath, options.maxHeaderBytes ?? DEFAULT_HEADER_BYTES);
}
return false;
}