UNPKG

@ospm/eslint-plugin-react-signals-hooks

Version:

ESLint plugin for React Signals hooks - enforces best practices, performance optimizations, and integration patterns for @preact/signals-react usage in React projects

294 lines 12.5 kB
import { PerformanceOperations } from './performance-constants.js'; import { validatePerformanceOptions } from './validate-performance-options.js'; export const DEFAULT_PERFORMANCE_BUDGET = { maxTime: 50, // ms maxNodes: 2000, maxMemory: 50 * 1024 * 1024, // 50MB maxOperations: { [PerformanceOperations.signalAccess]: 1000, [PerformanceOperations.signalCheck]: 500, [PerformanceOperations.effectCheck]: 500, [PerformanceOperations.identifierResolution]: 1000, [PerformanceOperations.scopeLookup]: 1000, [PerformanceOperations.typeCheck]: 500, }, enableMetrics: false, logMetrics: false, }; export class PerformanceLimitExceededError extends Error { metric; limit; actual; constructor(metric, limit, actual) { super(`Performance limit exceeded: ${metric} (limit: ${limit}, actual: ${actual})`); this.metric = metric; this.limit = limit; this.actual = actual; this.name = 'PerformanceLimitExceededError'; } } export const performanceMetrics = new Map(); const phaseStack = []; export function startTracking(context, perfKey, budget, ruleName) { // Validate performance budget if provided if (typeof budget !== 'undefined') { const validation = validatePerformanceOptions(budget, perfKey); if (!validation.valid) { // Log validation errors but don't fail console.warn(`[${perfKey}] Invalid performance options:`, validation.errors.join('; ')); } } const startTime = performance.now(); const memoryAtStart = process.memoryUsage(); // Initialize all metrics with default values const initialMetrics = { startTime, nodeCount: 0, operationCounts: {}, filePath: context.filename, ruleName, perfBudget: budget, phaseDurations: {}, customMetrics: {}, exceededBudget: false, budgetExceededBy: 0, memoryUsage: memoryAtStart, memoryDelta: 0, }; performanceMetrics.set(perfKey, initialMetrics); // Start with an initial phase if metrics are enabled if (typeof budget !== 'undefined' && budget.enableMetrics === true) { startPhase(perfKey, 'total'); } return; } function incrementNodeCount(key, nodeType) { const metrics = performanceMetrics.get(key); if (!metrics) { return; } metrics.nodeCount++; if (typeof nodeType !== 'undefined') { // eslint-disable-next-line security/detect-object-injection metrics.operationCounts[nodeType] = // eslint-disable-next-line security/detect-object-injection (metrics.operationCounts[nodeType] ?? 0) + 1; } } export function trackOperation(key, operation, count = 1) { const metrics = performanceMetrics.get(key); if (!metrics) { return; } // eslint-disable-next-line security/detect-object-injection const newCount = (metrics.operationCounts[operation] ?? 0) + count; // eslint-disable-next-line security/detect-object-injection metrics.operationCounts[operation] = newCount; // eslint-disable-next-line security/detect-object-injection const operationLimit = metrics.perfBudget?.maxOperations?.[operation]; if (typeof operationLimit !== 'undefined' && newCount > operationLimit) { throw new PerformanceLimitExceededError(`Operation '${operation}' count`, operationLimit, newCount); } } export function startPhase(key, phaseName) { const metrics = performanceMetrics.get(key); if (!metrics) { return; } endPhase(key, phaseName); phaseStack.push({ key, startTime: performance.now() }); if (typeof metrics.phaseDurations === 'undefined') { metrics.phaseDurations = {}; } // eslint-disable-next-line security/detect-object-injection if (typeof metrics.phaseDurations[phaseName] === 'undefined') { // eslint-disable-next-line security/detect-object-injection metrics.phaseDurations[phaseName] = 0; } } export function endPhase(key, phaseName) { const metrics = performanceMetrics.get(key); if (typeof metrics === 'undefined' || typeof metrics.phaseDurations === 'undefined' || phaseStack.length === 0) { return; } const phase = phaseStack.pop(); if (typeof phase === 'undefined' || phase.key !== key) { if (phase) { phaseStack.push(phase); } return; } const duration = performance.now() - phase.startTime; // eslint-disable-next-line security/detect-object-injection metrics.phaseDurations[phaseName] = // eslint-disable-next-line security/detect-object-injection (metrics.phaseDurations[phaseName] ?? 0) + duration; } export function recordMetric(key, name, value) { const metrics = performanceMetrics.get(key); if (typeof metrics === 'undefined') { return; } if (typeof metrics.customMetrics === 'undefined') { metrics.customMetrics = {}; } // eslint-disable-next-line security/detect-object-injection metrics.customMetrics[name] = value; } export function stopTracking(key) { const metrics = performanceMetrics.get(key); if (typeof metrics === 'undefined') { return undefined; } if (phaseStack.some((phase) => { return phase.key === key; })) { endPhase(key, 'total'); } metrics.endTime = performance.now(); metrics.duration = metrics.endTime - metrics.startTime; if (typeof metrics.perfBudget?.maxTime !== 'undefined' && metrics.duration > metrics.perfBudget.maxTime) { metrics.exceededBudget = true; metrics.budgetExceededBy = metrics.duration - metrics.perfBudget.maxTime; if (metrics.perfBudget.logMetrics === true) { console.warn(`[${metrics.ruleName}] Performance budget exceeded: ` + `Time limit of ${metrics.perfBudget.maxTime}ms exceeded by ${metrics.budgetExceededBy.toFixed(2)}ms`); } } if (typeof metrics.perfBudget?.maxNodes !== 'undefined' && metrics.nodeCount > metrics.perfBudget.maxNodes) { metrics.exceededBudget = true; const exceededBy = metrics.nodeCount - metrics.perfBudget.maxNodes; if (metrics.perfBudget.logMetrics === true) { console.warn(`[${metrics.ruleName}] Performance budget exceeded: ` + `Node limit of ${metrics.perfBudget.maxNodes} exceeded by ${exceededBy} nodes`); } } const memoryUsage = process.memoryUsage(); if (typeof metrics.perfBudget?.maxMemory !== 'undefined' && memoryUsage.heapUsed > metrics.perfBudget.maxMemory) { metrics.exceededBudget = true; const exceededBy = memoryUsage.heapUsed - metrics.perfBudget.maxMemory; if (metrics.perfBudget.logMetrics === true) { console.warn(`[${metrics.ruleName}] Performance budget exceeded: ` + `Memory limit of ${formatBytes(metrics.perfBudget.maxMemory)} ` + `exceeded by ${formatBytes(exceededBy)}`); } } if (metrics.perfBudget) { const { maxTime, maxMemory, maxNodes } = metrics.perfBudget; if (typeof maxTime !== 'undefined' && metrics.duration > maxTime) { metrics.exceededBudget = true; metrics.budgetExceededBy = metrics.duration - maxTime; } if (typeof maxMemory !== 'undefined' && memoryUsage.heapUsed > maxMemory) { metrics.exceededBudget = true; } if (typeof maxNodes !== 'undefined' && metrics.nodeCount > maxNodes) { metrics.exceededBudget = true; } } if (metrics.perfBudget?.enableMetrics === true && metrics.perfBudget.logMetrics === true) { console.info(`[Performance Metrics] ${metrics.ruleName} (${metrics.filePath})`); console.info(` Duration: ${metrics.duration.toFixed(2)}ms`); console.info(` Node count: ${metrics.nodeCount}`); if (metrics.memoryUsage && 'heapUsed' in memoryUsage) { const endMemory = memoryUsage.heapUsed; console.info(` Memory usage: ${endMemory.toLocaleString()} bytes`); console.info(` Memory delta: ${(endMemory - metrics.memoryUsage.heapUsed).toLocaleString()} bytes`); } } performanceMetrics.delete(key); return metrics; } export function logMetrics(metrics) { if (typeof metrics.endTime === 'undefined') { return; } if (metrics.perfBudget?.enableMetrics === true && metrics.perfBudget.logMetrics === true) { const duration = (metrics.endTime - metrics.startTime).toFixed(2); const mem = metrics.memoryUsage ? `, Memory: ${formatBytes(metrics.memoryUsage.heapUsed)} / ${formatBytes(metrics.memoryUsage.heapTotal)}` : ''; const exceeded = metrics.exceededBudget === true ? ` [BUDGET EXCEEDED${typeof metrics.budgetExceededBy !== 'undefined' ? ` by ${metrics.budgetExceededBy.toFixed(2)}ms` : ''}]` : ''; console.info(`[Perf] ${metrics.ruleName}: Processed ${metrics.nodeCount} nodes in ${duration}ms${mem}${exceeded}`); } } function formatBytes(bytes, decimals = 2) { if (bytes === 0) { return '0 Bytes'; } const k = 1024; const i = Math.floor(Math.log(bytes) / Math.log(k)); // eslint-disable-next-line security/detect-object-injection return `${Number.parseFloat((bytes / k ** i).toFixed(decimals < 0 ? 0 : decimals))} ${['Bytes', 'KB', 'MB', 'GB'][i]}`; } export function createPerformanceTracker(key, budget) { const metrics = performanceMetrics.get(key); if (typeof metrics !== 'undefined') { // Store budget on the metrics object for later checking metrics.perfBudget = budget; } return { trackNode(node) { const currentMetrics = performanceMetrics.get(key); if (!currentMetrics) { return; } // Increment node count and track operation incrementNodeCount(key, node.type); // Initialize node type tracking if not exists if (!currentMetrics.nodeTypes) { currentMetrics.nodeTypes = new Map(); } // Increment count for this node type const currentCount = currentMetrics.nodeTypes.get(node.type) ?? 0; currentMetrics.nodeTypes.set(node.type, currentCount + 1); // Track node locations for debugging if location is available // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (node.loc !== null) { currentMetrics.nodeLocations ??= []; currentMetrics.nodeLocations.push({ type: node.type, start: node.loc.start, end: node.loc.end, }); } // Check node count budget if maxNodes is defined if (typeof budget?.maxNodes !== 'undefined' && currentMetrics.nodeCount > budget.maxNodes) { currentMetrics.exceededBudget = true; currentMetrics.budgetExceededBy = (currentMetrics.budgetExceededBy ?? 0) + 1; // Initialize and add to budget exceeded node types currentMetrics.budgetExceededNodeTypes ??= new Set(); currentMetrics.budgetExceededNodeTypes.add(node.type); } }, 'Program:exit'() { const metrics = stopTracking(key); if (typeof metrics !== 'undefined') { if (typeof budget?.maxTime !== 'undefined' && typeof metrics.endTime !== 'undefined' && typeof metrics.startTime !== 'undefined') { const duration = metrics.endTime - metrics.startTime; if (duration > budget.maxTime) { metrics.exceededBudget = true; metrics.budgetExceededBy = duration - budget.maxTime; } } if (typeof budget?.maxMemory !== 'undefined' && typeof metrics.memoryUsage !== 'undefined') { if (metrics.memoryUsage.heapUsed > budget.maxMemory) { metrics.exceededBudget = true; } } logMetrics(metrics); } }, }; } //# sourceMappingURL=performance.js.map