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

808 lines (807 loc) 40.1 kB
/** biome-ignore-all assist/source/organizeImports: off */ import { ESLintUtils, AST_NODE_TYPES, } from "@typescript-eslint/utils"; import { buildNamedImport, getPreferredQuote, getPreferredSemicolon, } from "./utils/import-format.js"; import { PerformanceOperations } from "./utils/performance-constants.js"; import { endPhase, startPhase, recordMetric, startTracking, trackOperation, createPerformanceTracker, DEFAULT_PERFORMANCE_BUDGET, } from "./utils/performance.js"; import { buildSuffixRegex, hasSignalSuffix } from "./utils/suffix.js"; import { getRuleDocUrl } from "./utils/urls.js"; function getSeverity(messageId, options) { if (!options?.severity) { return "error"; } switch (messageId) { case "missingUseSignalsInComponent": { return options.severity.missingUseSignalsInComponent ?? "error"; } case "missingUseSignalsInCustomHook": { return options.severity.missingUseSignalsInCustomHook ?? "error"; } case "wrongUseSignalsArg": { return options.severity.wrongUseSignalsArg ?? "error"; } default: { return "error"; } } } function isSignalUsageLocal(node, suffixRegex) { if (node.type === AST_NODE_TYPES.ChainExpression) { return isSignalUsageLocal(node.expression, suffixRegex); } if (node.type === AST_NODE_TYPES.MemberExpression) { if (node.property.type === AST_NODE_TYPES.Identifier && (node.property.name === "value" || node.property.name === "peek")) { if (node.property.name === "peek" && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions (!node.parent || !((node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) || (node.parent.type === AST_NODE_TYPES.ChainExpression && node.parent.expression.type === AST_NODE_TYPES.CallExpression && node.parent.expression.callee === node)))) { return false; } let base = node.object; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition while (base && base.type === AST_NODE_TYPES.MemberExpression) { base = base.object; } // Unwrap chain at base as well // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition if (base && base.type === AST_NODE_TYPES.ChainExpression) { base = base.expression; } return ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions !!base && base.type === AST_NODE_TYPES.Identifier && hasSignalSuffix(base.name, suffixRegex)); } return false; } if (node.type === AST_NODE_TYPES.Identifier) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (!node.parent) { return false; } // Skip when part of a MemberExpression (handled above when accessing .value/.peek) if (node.parent.type === AST_NODE_TYPES.MemberExpression && node.parent.object === node) { return false; } // Skip import/export specifiers and type positions if (node.parent.type === AST_NODE_TYPES.ImportSpecifier || node.parent.type === AST_NODE_TYPES.ExportSpecifier || node.parent.type === AST_NODE_TYPES.TSTypeReference || node.parent.type === AST_NODE_TYPES.TSTypeAnnotation || node.parent.type === AST_NODE_TYPES.TSQualifiedName || node.parent.type === AST_NODE_TYPES.TSTypeParameter || node.parent.type === AST_NODE_TYPES.TSEnumMember || node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) { return false; } // Skip label and property key/name contexts if (node.parent.type === AST_NODE_TYPES.LabeledStatement || (node.parent.type === AST_NODE_TYPES.Property && node.parent.key === node && node.parent.computed === false) || node.parent.type === AST_NODE_TYPES.PropertyDefinition || (node.parent.type === AST_NODE_TYPES.MethodDefinition && node.parent.key === node)) { return false; } // Skip JSX identifier/name contexts if (node.parent.type === AST_NODE_TYPES.JSXIdentifier || node.parent.type === AST_NODE_TYPES.JSXAttribute || node.parent.type === AST_NODE_TYPES.JSXMemberExpression) { return false; } return hasSignalSuffix(node.name, suffixRegex); } return false; } // Helper: find or create a store declaration within the component body. // Reuses an existing `const <name> = useSignals(...)` if found; otherwise inserts // a unique declaration after directives and returns the chosen name. function findOrCreateStoreDeclaration(componentNode, fixer, fixes, context) { let storeName = "store"; let hasExistingStoreDecl = false; if ("body" in componentNode.body && Array.isArray(componentNode.body.body)) { for (const stmt of componentNode.body.body) { if (stmt.type !== AST_NODE_TYPES.VariableDeclaration) { continue; } for (const decl of stmt.declarations) { if (decl.id.type === AST_NODE_TYPES.Identifier && decl.init && ((decl.init.type === AST_NODE_TYPES.CallExpression && decl.init.callee.type === AST_NODE_TYPES.Identifier && decl.init.callee.name === "useSignals") || (decl.init.type === AST_NODE_TYPES.ChainExpression && decl.init.expression.type === AST_NODE_TYPES.CallExpression && decl.init.expression.callee.type === AST_NODE_TYPES.Identifier && decl.init.expression.callee.name === "useSignals"))) { storeName = decl.id.name; hasExistingStoreDecl = true; break; } } if (hasExistingStoreDecl) { break; } } } if (!hasExistingStoreDecl) { let __idx = 1; while ( // eslint-disable-next-line security/detect-non-literal-regexp new RegExp(`\\b(?:const|let|var)\\s+${storeName}\\b`).test(context.sourceCode.getText(componentNode.body))) { storeName = `store${__idx++}`; } } // Insert const store = useSignals(X) after directives if it doesn't exist if (!hasExistingStoreDecl) { let lastDirectiveEnd = null; if ("body" in componentNode.body && Array.isArray(componentNode.body.body)) { for (const stmt of componentNode.body.body) { if (stmt.type === AST_NODE_TYPES.ExpressionStatement && stmt.expression.type === AST_NODE_TYPES.Literal && typeof stmt.expression.value === "string") { lastDirectiveEnd = stmt.range[1]; continue; } break; } } const insertDeclPos = lastDirectiveEnd ?? componentNode.body.range[0] + 1; fixes.push(fixer.insertTextAfterRange([insertDeclPos, insertDeclPos], `\nconst ${storeName} = useSignals(${computeExpectedArg(componentNode)});\n`)); } return storeName; } // Helper: ensure there is a try/finally calling store.f(). If a try/finally // exists, append `${storeName}.f();` if not present; else wrap the body. function wrapBodyInTryFinally(componentNode, storeName, hasTryFinallyInCurrent, fixer, fixes, context) { if (hasTryFinallyInCurrent && "body" in componentNode.body && Array.isArray(componentNode.body.body)) { const tryWithFinally = componentNode.body.body.find((s) => { return s.type === AST_NODE_TYPES.TryStatement && s.finalizer != null; }); if (tryWithFinally?.finalizer) { const beforeClose = tryWithFinally.finalizer.range[1] - 1; if (!/\.f\s*\(\s*\)\s*;?/.test(context.sourceCode.getText(tryWithFinally.finalizer))) { fixes.push(fixer.insertTextBeforeRange([beforeClose, beforeClose], `\n${storeName}.f();\n`)); } } return; } // No try/finally exists. Wrap the remaining body in try/finally let directiveCount = 0; if ("body" in componentNode.body && Array.isArray(componentNode.body.body)) { for (const stmt of componentNode.body.body) { if (stmt.type === AST_NODE_TYPES.ExpressionStatement && stmt.expression.type === AST_NODE_TYPES.Literal && typeof stmt.expression.value === "string") { directiveCount++; continue; } break; } const firstNonDirective = componentNode.body.body[directiveCount]; const insertDeclPos = typeof firstNonDirective === "undefined" ? componentNode.body.range[0] + 1 : firstNonDirective.range[0]; fixes.push(fixer.insertTextBeforeRange([insertDeclPos, insertDeclPos], "try {\n")); } const beforeBodyClose = componentNode.body.range[1] - 1; fixes.push(fixer.insertTextBeforeRange([beforeBodyClose, beforeBodyClose], `\n} finally {\n${storeName}.f();\n}\n`)); } function isHook(node) { if (node?.type === AST_NODE_TYPES.FunctionDeclaration && typeof node.id?.name === "string") { return /^use[A-Z]/.test(node.id.name); } if ((node?.type === AST_NODE_TYPES.ArrowFunctionExpression || node?.type === AST_NODE_TYPES.FunctionExpression) && // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition node.parent && node.parent.type === AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES.Identifier) { return /^use[A-Z]/.test(node.parent.id.name); } return false; } function inferIsHook(n) { if (n.type === AST_NODE_TYPES.FunctionDeclaration && typeof n.id?.name === "string") { return /^use[A-Z]/.test(n.id.name); } if ((n.type === AST_NODE_TYPES.ArrowFunctionExpression || n.type === AST_NODE_TYPES.FunctionExpression) && // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition n.parent && n.parent.type === AST_NODE_TYPES.VariableDeclarator && n.parent.id.type === AST_NODE_TYPES.Identifier) { return /^use[A-Z]/.test(n.parent.id.name); } return false; } // Helper: is the node inside the current component's function body? function isInsideCurrentComponent(n, componentNode) { if (componentNode === null) { return false; } const [nStart, nEnd] = n.range; const [cStart, cEnd] = componentNode.range; return nStart >= cStart && nEnd <= cEnd; } // Helper: map component to expected useSignals(...) argument function computeExpectedArg(componentNode) { return inferIsHook(componentNode) ? 2 : 1; } function ensureUseSignalsImport(fixer, fixes, context) { const signalsImport = context.sourceCode.ast.body.find((n) => { return (n.type === AST_NODE_TYPES.ImportDeclaration && n.source.value === "@preact/signals-react/runtime"); }); if (typeof signalsImport === "undefined") { // Insert a fresh value import right after the last top-level import declaration const body = context.sourceCode.ast.body; const lastTopImport = (() => { let last; for (const stmt of body) { if (stmt.type === AST_NODE_TYPES.ImportDeclaration) { last = stmt; } else { break; } } return last; })(); const quote = getPreferredQuote(context.sourceCode); const semi = getPreferredSemicolon(context.sourceCode); const text = buildNamedImport("@preact/signals-react/runtime", ["useSignals"], quote, semi); if (typeof lastTopImport !== "undefined") { fixes.push(fixer.insertTextAfter(lastTopImport, text)); } else if (typeof body[0] !== "undefined") { fixes.push(fixer.insertTextBefore(body[0], text)); } else { // Empty file fallback fixes.push(fixer.insertTextAfterRange([0, 0], text)); } return; } if (!signalsImport.specifiers.some((s) => { return (s.type === AST_NODE_TYPES.ImportSpecifier && s.imported.type === AST_NODE_TYPES.Identifier && s.imported.name === "useSignals"); })) { const lastNamed = [...signalsImport.specifiers] .reverse() .find((s) => { return s.type === AST_NODE_TYPES.ImportSpecifier; }); if (typeof lastNamed !== "undefined" && // append only when this import is a value import (typeof signalsImport.importKind === "undefined" || signalsImport.importKind === "value")) { fixes.push(fixer.insertTextAfter(lastNamed, ", useSignals")); } else { const quote = getPreferredQuote(context.sourceCode); const semi = getPreferredSemicolon(context.sourceCode); fixes.push(fixer.insertTextAfter(signalsImport, `\n${buildNamedImport("@preact/signals-react/runtime", ["useSignals"], quote, semi)}`)); } } } let hasUseSignals = false; let hasSignalUsage = false; let componentName = ""; let componentNode = null; let isHookContext = false; let hasTryFinallyInCurrent = false; const ruleName = "require-use-signals"; export const requireUseSignalsRule = ESLintUtils.RuleCreator((name) => { return getRuleDocUrl(name); })({ name: ruleName, meta: { type: "problem", // Changed from 'suggestion' to 'problem' as missing useSignals() can break reactivity docs: { description: "Ensures that components and custom hooks using signals properly import and call the `useSignals()` hook. This hook is essential for signal reactivity in React components and hooks. The rule helps prevent subtle bugs by ensuring that any component or hook using signals has the necessary hook in place.", url: getRuleDocUrl(ruleName), }, hasSuggestions: true, messages: { missingUseSignalsInComponent: "Component '{{componentName}}' reads signals; call useSignals() to subscribe for updates", missingUseSignalsInCustomHook: "Custom hook '{{hookName}}' reads signals; call useSignals() to subscribe for updates", wrongUseSignalsArg: "'useSignals({{got}})' is not appropriate here; expected 'useSignals({{expected}})' when used with try/finally in a {{contextKind}}", }, schema: [ { type: "object", additionalProperties: false, properties: { ignoreComponents: { type: "array", items: { type: "string" }, description: "List of component names to ignore", }, wrapConciseArrows: { type: "boolean", default: false, description: "When true, transform concise arrow components/hooks to block bodies to insert useSignals try/finally during autofix.", }, severity: { type: "object", properties: { missingUseSignalsInComponent: { type: "string", enum: ["error", "warn", "off"], }, missingUseSignalsInCustomHook: { type: "string", enum: ["error", "warn", "off"], }, wrongUseSignalsArg: { type: "string", enum: ["error", "warn", "off"], }, }, additionalProperties: false, }, performance: { type: "object", properties: { maxTime: { type: "number", minimum: 1 }, maxMemory: { type: "number", minimum: 1 }, maxNodes: { type: "number", minimum: 1 }, enableMetrics: { type: "boolean" }, logMetrics: { type: "boolean" }, maxOperations: { type: "object", properties: Object.fromEntries(Object.entries(PerformanceOperations).map(([key]) => [ key, { type: "number", minimum: 1 }, ])), }, }, additionalProperties: false, }, suffix: { description: "Configurable suffix used to detect signal identifiers (default: 'Signal')", type: "string", default: "Signal", }, extraCreatorModules: { description: "Additional module specifiers that export signal/computed (in addition to '@preact/signals-react')", type: "array", items: { type: "string", minLength: 1 }, default: [], }, }, }, ], fixable: "code", }, defaultOptions: [ { ignoreComponents: [], suffix: "Signal", wrapConciseArrows: false, performance: DEFAULT_PERFORMANCE_BUDGET, }, ], create(context, [option]) { const perfKey = `${ruleName}:${context.filename}:${Date.now()}`; const suffixRegex = buildSuffixRegex(option?.suffix); // Only run this rule for TSX files (React components). Avoids leaking into non-React TS/JS files. if (!/\.tsx$/i.test(context.filename)) { return {}; } startPhase(perfKey, "ruleInit"); const perf = createPerformanceTracker(perfKey, option?.performance); if (option?.performance?.enableMetrics === true) { startTracking(context, perfKey, option.performance, ruleName); } if (option?.performance?.enableMetrics === true && option.performance.logMetrics === true) { console.info(`${ruleName}: Initializing rule for file: ${context.filename}`); console.info(`${ruleName}: Rule configuration:`, option); } recordMetric(perfKey, "config", { performance: { enableMetrics: option?.performance?.enableMetrics, logMetrics: option?.performance?.logMetrics, }, }); trackOperation(perfKey, PerformanceOperations.ruleInit); endPhase(perfKey, "ruleInit"); startPhase(perfKey, "ruleExecution"); const useSignalsLocalNames = new Set(["useSignals"]); const signalCreatorLocals = new Set(["signal"]); const computedCreatorLocals = new Set(["computed"]); const creatorNamespaces = new Set(); const creatorModules = new Set([ "@preact/signals-react", ...(Array.isArray(option?.extraCreatorModules) ? option.extraCreatorModules : []), ]); const signalVariables = new Set(); let nodeCount = 0; function shouldContinue() { nodeCount++; if (typeof option?.performance?.maxNodes === "number" && nodeCount > option.performance.maxNodes) { trackOperation(perfKey, PerformanceOperations.nodeBudgetExceeded); return false; } return true; } return { "*": (node) => { if (!shouldContinue()) { endPhase(perfKey, "recordMetrics"); return; } perf.trackNode(node); const dynamicOp = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition PerformanceOperations[`${node.type}Processing`] ?? PerformanceOperations.nodeProcessing; trackOperation(perfKey, dynamicOp); }, [AST_NODE_TYPES.FunctionDeclaration](node) { if (typeof node.id?.name !== "string") { return; } if (/^[A-Z]/.test(node.id.name)) { // React component componentName = node.id.name; componentNode = node; isHookContext = false; } else if (/^use[A-Z]/.test(node.id.name)) { // Custom hook componentName = node.id.name; componentNode = node; isHookContext = true; } else { return; } hasUseSignals = false; hasSignalUsage = false; hasTryFinallyInCurrent = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition node.body?.type === AST_NODE_TYPES.BlockStatement && node.body.body.some((s) => { return (s.type === AST_NODE_TYPES.TryStatement && s.finalizer != null); }); }, [AST_NODE_TYPES.ArrowFunctionExpression](node) { if (!(node.parent.type === AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES.Identifier && (/^[A-Z]/.test(node.parent.id.name) || /^use[A-Z]/.test(node.parent.id.name)))) { return; } componentName = node.parent.id.name; componentNode = node; isHookContext = /^use[A-Z]/.test(node.parent.id.name); hasUseSignals = false; hasSignalUsage = false; hasTryFinallyInCurrent = node.body.type === AST_NODE_TYPES.BlockStatement && node.body.body.some((s) => { return (s.type === AST_NODE_TYPES.TryStatement && s.finalizer != null); }); }, [AST_NODE_TYPES.FunctionExpression](node) { if (!(node.parent.type === AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES.Identifier && (/^[A-Z]/.test(node.parent.id.name) || /^use[A-Z]/.test(node.parent.id.name)))) { return; } componentName = node.parent.id.name; componentNode = node; isHookContext = /^use[A-Z]/.test(node.parent.id.name); hasUseSignals = false; hasSignalUsage = false; hasTryFinallyInCurrent = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition node.body.type === AST_NODE_TYPES.BlockStatement && node.body.body.some((s) => { return (s.type === AST_NODE_TYPES.TryStatement && s.finalizer != null); }); }, [AST_NODE_TYPES.ExportDefaultDeclaration](node) { if (node.declaration.type === AST_NODE_TYPES.FunctionDeclaration) { if (node.declaration.id && /^[A-Z]/.test(node.declaration.id.name)) { componentName = node.declaration.id.name; componentNode = node.declaration; isHookContext = false; hasUseSignals = false; hasSignalUsage = false; } else if (!node.declaration.id) { componentName = "default"; componentNode = node.declaration; isHookContext = false; hasUseSignals = false; hasSignalUsage = false; } } else if (node.declaration.type === AST_NODE_TYPES.ArrowFunctionExpression || node.declaration.type === AST_NODE_TYPES.FunctionExpression) { componentName = "default"; componentNode = node.declaration; isHookContext = false; hasUseSignals = false; hasSignalUsage = false; } }, [AST_NODE_TYPES.CallExpression](node) { if ((node.callee.type === AST_NODE_TYPES.Identifier && useSignalsLocalNames.has(node.callee.name)) || (node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "useSignals")) { if (isInsideCurrentComponent(node, componentNode)) { hasUseSignals = true; } else { return; } if (hasTryFinallyInCurrent) { const expected = isHookContext ? 2 : 1; if (node.arguments.length === 0) { if (getSeverity("wrongUseSignalsArg", option) !== "off") { context.report({ node, messageId: "wrongUseSignalsArg", data: { got: "none", expected: String(expected), contextKind: isHookContext ? "custom hook" : "component", }, fix(fixer) { // Insert expected argument before the closing parenthesis return fixer.insertTextBeforeRange([node.range[1] - 1, node.range[1] - 1], String(expected)); }, }); } } else if (node.arguments[0]?.type === AST_NODE_TYPES.Literal && typeof node.arguments[0].value === "number") { if (node.arguments[0].value !== expected && getSeverity("wrongUseSignalsArg", option) !== "off") { context.report({ node: node.arguments[0], messageId: "wrongUseSignalsArg", data: { got: String(node.arguments[0].value), expected: String(expected), contextKind: isHookContext ? "custom hook" : "component", }, fix(fixer) { if (typeof node.arguments[0] === "undefined") { return null; } return fixer.replaceText(node.arguments[0], String(expected)); }, }); } } } } }, [AST_NODE_TYPES.MemberExpression](node) { if (!isInsideCurrentComponent(node, componentNode)) { return; } if (isSignalUsageLocal(node, suffixRegex)) { hasSignalUsage = true; return; } if (!(node.property.type === AST_NODE_TYPES.Identifier && (node.property.name === "value" || node.property.name === "peek"))) { return; } let base = node.object; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition while (base && base.type === AST_NODE_TYPES.MemberExpression) { base = base.object; } if ( // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition base && base.type === AST_NODE_TYPES.Identifier && signalVariables.has(base.name)) { hasSignalUsage = true; } }, [AST_NODE_TYPES.VariableDeclarator](node) { if (!isInsideCurrentComponent(node, componentNode)) { return; } if (!(node.id.type === AST_NODE_TYPES.ObjectPattern && node.init)) { return; } const hasValueOrPeek = node.id.properties.some((p) => { return (p.type === AST_NODE_TYPES.Property && p.key.type === AST_NODE_TYPES.Identifier && (p.key.name === "value" || p.key.name === "peek")); }); if (!hasValueOrPeek) { return; } // Walk to base of init let base = node.init; // Unwrap optional chain wrapper // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition if (base && base.type === AST_NODE_TYPES.ChainExpression) { base = base.expression; } // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition while (base && base.type === AST_NODE_TYPES.MemberExpression) { base = base.object; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition if (base && base.type === AST_NODE_TYPES.ChainExpression) { base = base.expression; } } if ( // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition base && base.type === AST_NODE_TYPES.Identifier && (hasSignalSuffix(base.name, suffixRegex) || signalVariables.has(base.name))) { hasSignalUsage = true; } }, [AST_NODE_TYPES.Identifier](node) { if (!isInsideCurrentComponent(node, componentNode)) { return; } if (isSignalUsageLocal(node, suffixRegex)) { hasSignalUsage = true; return; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (!node.parent) { return; } if (node.parent.type === AST_NODE_TYPES.MemberExpression && node.parent.object === node) { return; } if (node.parent.type === AST_NODE_TYPES.ImportSpecifier || node.parent.type === AST_NODE_TYPES.ExportSpecifier || node.parent.type === AST_NODE_TYPES.TSTypeReference || node.parent.type === AST_NODE_TYPES.TSTypeAnnotation || node.parent.type === AST_NODE_TYPES.TSQualifiedName || node.parent.type === AST_NODE_TYPES.TSTypeParameter || node.parent.type === AST_NODE_TYPES.TSEnumMember || node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration || node.parent.type === AST_NODE_TYPES.LabeledStatement || (node.parent.type === AST_NODE_TYPES.Property && node.parent.key === node && node.parent.computed === false) || node.parent.type === AST_NODE_TYPES.PropertyDefinition || (node.parent.type === AST_NODE_TYPES.MethodDefinition && node.parent.key === node) || node.parent.type === AST_NODE_TYPES.JSXIdentifier || node.parent.type === AST_NODE_TYPES.JSXAttribute || node.parent.type === AST_NODE_TYPES.JSXMemberExpression) { return; } if (signalVariables.has(node.name)) { hasSignalUsage = true; } }, [AST_NODE_TYPES.Program](node) { for (const stmt of node.body) { if (stmt.type !== AST_NODE_TYPES.ImportDeclaration) { continue; } if (stmt.source.value === "@preact/signals-react/runtime") { for (const spec of stmt.specifiers) { if (spec.type === AST_NODE_TYPES.ImportSpecifier && spec.imported.type === AST_NODE_TYPES.Identifier && spec.imported.name === "useSignals") { useSignalsLocalNames.add(spec.local.name); } } } if (typeof stmt.source.value === "string" && creatorModules.has(stmt.source.value)) { for (const spec of stmt.specifiers) { if (spec.type === AST_NODE_TYPES.ImportSpecifier) { if (spec.imported.type === AST_NODE_TYPES.Identifier && spec.imported.name === "signal") { signalCreatorLocals.add(spec.local.name); } else if (spec.imported.type === AST_NODE_TYPES.Identifier && spec.imported.name === "computed") { computedCreatorLocals.add(spec.local.name); } } else if (spec.type === AST_NODE_TYPES.ImportNamespaceSpecifier) { creatorNamespaces.add(spec.local.name); } } } } }, [`${AST_NODE_TYPES.Program}:exit`]() { startPhase(perfKey, "programExit"); if (hasSignalUsage && !hasUseSignals && componentName && !new Set(context.options[0]?.ignoreComponents ?? []).has(componentName) && componentNode) { const missingId = isHook(componentNode) ? "missingUseSignalsInCustomHook" : "missingUseSignalsInComponent"; if (getSeverity(missingId, option) === "off") { return; } context.report({ node: componentNode, messageId: missingId, data: { componentName }, fix(fixer) { const fixes = []; if (!componentNode) { return null; } // Handle block bodies directly if ((componentNode.type === AST_NODE_TYPES.FunctionDeclaration || componentNode.type === AST_NODE_TYPES.FunctionExpression || componentNode.type === AST_NODE_TYPES.ArrowFunctionExpression) && componentNode.body.type === AST_NODE_TYPES.BlockStatement) { wrapBodyInTryFinally(componentNode, findOrCreateStoreDeclaration(componentNode, fixer, fixes, context), hasTryFinallyInCurrent, fixer, fixes, context); } else if (option?.wrapConciseArrows === true && componentNode.type === AST_NODE_TYPES.ArrowFunctionExpression && componentNode.body.type !== AST_NODE_TYPES.BlockStatement) { // Basic unique store name generation against current function text let storeName = "store"; let suffixIdx = 1; while ( // eslint-disable-next-line security/detect-non-literal-regexp new RegExp(`\\b${storeName}\\b`).test(context.sourceCode.getText(componentNode))) { storeName = `store${suffixIdx++}`; } // const quote = getPreferredQuote(context.sourceCode); const semi = getPreferredSemicolon(context.sourceCode); const newBlock = `{ const ${storeName} = useSignals(${computeExpectedArg(componentNode)})${semi} try { return ${context.sourceCode.getText(componentNode.body)}; } finally { ${storeName}.f()${semi} } }`; fixes.push(fixer.replaceText(componentNode.body, newBlock)); ensureUseSignalsImport(fixer, fixes, context); } ensureUseSignalsImport(fixer, fixes, context); return fixes.length > 0 ? fixes : null; }, }); } perf["Program:exit"](); endPhase(perfKey, "programExit"); }, }; }, }); //# sourceMappingURL=require-use-signals.js.map