UNPKG

eslint-plugin-preact-signal-patterns

Version:

ESLint rules for Preact Signals architectural patterns - promotes signal-passing convention and reactive component patterns

151 lines (132 loc) 4.54 kB
/** * Shared utilities for signal detection across ESLint rules */ /** * Creates a signal detector that can track and identify signal variables * @param {Object} context - ESLint rule context * @returns {Object} Object with signal detection methods */ function createSignalDetector(context) { // Track actual signal variables by their definitions const signalVariables = new Set(); /** * Track a variable as a signal * @param {string} name - Variable name to track */ function trackSignalVariable(name) { // Input validation: ensure name is a non-empty string if (typeof name === 'string' && name.trim().length > 0) { signalVariables.add(name.trim()); } } /** * Check if a node represents a signal variable * @param {Object} node - AST node to check * @returns {boolean} True if the node is a signal variable */ function isSignalVariable(node) { // Input validation: ensure node exists and has a name property if (!node || typeof node.name !== 'string' || node.name.trim().length === 0) { return false; } // First check if we've tracked this as a signal variable if (signalVariables.has(node.name)) { return true; } // Try to detect if variable comes from a Preact signal import or assignment const scope = context.getScope(); let currentScope = scope; while (currentScope) { const variable = currentScope.set.get(node.name); if (variable && variable.defs.length > 0) { const def = variable.defs[0]; // Check if it's imported from @preact/signals* if (def.type === 'ImportBinding' && def.node.source) { const importPath = def.node.source.value; if (typeof importPath === 'string' && importPath.includes('@preact/signals')) { trackSignalVariable(node.name); return true; } } // Check if it's assigned from a signal-creating function if (def.node && def.node.init) { const init = def.node.init; if (init.type === 'CallExpression' && init.callee) { const calleeName = init.callee.name; // Check for signal creation functions if (['signal', 'useSignal', 'useComputed', 'computed'].includes(calleeName)) { trackSignalVariable(node.name); return true; } } } break; } currentScope = currentScope.upper; } return false; } /** * Check if a node represents reading signal.value * @param {Object} node - AST node to check * @returns {boolean} True if it's a signal.value read operation */ function isSignalValueRead(node) { // Input validation: ensure node has the expected structure if (!node || node.type !== 'MemberExpression') { return false; } return ( node.property && node.property.type === 'Identifier' && node.property.name === 'value' && !node.computed && node.object && node.object.type === 'Identifier' && isSignalVariable(node.object) // Only check if it's actually a signal variable ); } /** * Check if a node is an assignment operation * @param {Object} node - AST node to check * @returns {boolean} True if it's an assignment */ function isAssignment(node) { // Input validation: ensure node exists and has a parent if (!node || !node.parent) { return false; } const parent = node.parent; return parent.type === 'AssignmentExpression' && parent.left === node; } /** * Get a visitor for tracking signal variable declarations * @returns {Object} ESLint visitor object */ function getSignalDeclarationVisitor() { return { VariableDeclarator(node) { // Input validation: ensure node has the expected structure if (!node || !node.init || node.init.type !== 'CallExpression' || !node.init.callee) { return; } const calleeName = node.init.callee.name; if (typeof calleeName === 'string' && ['signal', 'useSignal', 'useComputed', 'computed'].includes(calleeName)) { if (node.id && node.id.type === 'Identifier' && typeof node.id.name === 'string') { trackSignalVariable(node.id.name); } } } }; } return { trackSignalVariable, isSignalVariable, isSignalValueRead, isAssignment, getSignalDeclarationVisitor }; } module.exports = { createSignalDetector };