UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

147 lines (146 loc) • 5.24 kB
/** * 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; }