UNPKG

eslint-plugin-solid

Version:
210 lines (198 loc) 8.33 kB
/** * FIXME: remove this comments and import when below issue is fixed. * This import is necessary for type generation due to a bug in the TypeScript compiler. * See: https://github.com/microsoft/TypeScript/issues/42873 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { TSESLint } from "@typescript-eslint/utils"; import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils"; import { getFunctionName, type FunctionNode } from "../utils"; import { getSourceCode } from "../compat"; const createRule = ESLintUtils.RuleCreator.withoutDocs; const isNothing = (node?: T.Node): boolean => { if (!node) { return true; } switch (node.type) { case "Literal": return ([null, undefined, false, ""] as Array<unknown>).includes(node.value); case "JSXFragment": return !node.children || node.children.every(isNothing); default: return false; } }; const getLineLength = (loc: T.SourceLocation) => loc.end.line - loc.start.line + 1; export default createRule({ meta: { type: "problem", docs: { description: "Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX.", url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/components-return-once.md", }, fixable: "code", schema: [], messages: { noEarlyReturn: "Solid components run once, so an early return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.", noConditionalReturn: "Solid components run once, so a conditional return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.", }, }, defaultOptions: [], create(context) { const functionStack: Array<{ /** switched to true by :exit if the current function is detected to be a component */ isComponent: boolean; lastReturn: T.ReturnStatement | undefined; earlyReturns: Array<T.ReturnStatement>; }> = []; const putIntoJSX = (node: T.Node): string => { const text = getSourceCode(context).getText(node); return node.type === "JSXElement" || node.type === "JSXFragment" ? text : `{${text}}`; }; const currentFunction = () => functionStack[functionStack.length - 1]; const onFunctionEnter = (node: FunctionNode) => { let lastReturn: T.ReturnStatement | undefined; if (node.body.type === "BlockStatement") { // find last statement, ignoring function/class/variable declarations (hoisting) const last = node.body.body.findLast((node) => !node.type.endsWith("Declaration")); // if it's a return, store it if (last && last.type === "ReturnStatement") { lastReturn = last; } } functionStack.push({ isComponent: false, lastReturn, earlyReturns: [] }); }; const onFunctionExit = (node: FunctionNode) => { if ( // "render props" aren't components getFunctionName(node)?.match(/^[a-z]/) || node.parent?.type === "JSXExpressionContainer" || // ignore createMemo(() => conditional JSX), report HOC(() => conditional JSX) (node.parent?.type === "CallExpression" && node.parent.arguments.some((n) => n === node) && !(node.parent.callee as T.Identifier).name?.match(/^[A-Z]/)) ) { currentFunction().isComponent = false; } if (currentFunction().isComponent) { // Warn on each early return currentFunction().earlyReturns.forEach((earlyReturn) => { context.report({ node: earlyReturn, messageId: "noEarlyReturn", }); }); const argument = currentFunction().lastReturn?.argument; if (argument?.type === "ConditionalExpression") { const sourceCode = getSourceCode(context); context.report({ node: argument.parent!, messageId: "noConditionalReturn", fix: (fixer) => { const { test, consequent, alternate } = argument; const conditions = [{ test, consequent }]; let fallback = alternate; while (fallback.type === "ConditionalExpression") { conditions.push({ test: fallback.test, consequent: fallback.consequent }); fallback = fallback.alternate; } if (conditions.length >= 2) { // we have a nested ternary, use <Switch><Match /></Switch> const fallbackStr = !isNothing(fallback) ? ` fallback={${sourceCode.getText(fallback)}}` : ""; return fixer.replaceText( argument, `<Switch${fallbackStr}>\n${conditions .map( ({ test, consequent }) => `<Match when={${sourceCode.getText(test)}}>${putIntoJSX( consequent )}</Match>` ) .join("\n")}\n</Switch>` ); } if (isNothing(consequent)) { // we have a single ternary and the consequent is nothing. Negate the condition and use a <Show>. return fixer.replaceText( argument, `<Show when={!(${sourceCode.getText(test)})}>${putIntoJSX(alternate)}</Show>` ); } if ( isNothing(fallback) || getLineLength(consequent.loc) >= getLineLength(alternate.loc) * 1.5 ) { // we have a standard ternary, and the alternate is a bit shorter in LOC than the consequent, which // should be enough to tell that it's logically a fallback instead of an equal branch. const fallbackStr = !isNothing(fallback) ? ` fallback={${sourceCode.getText(fallback)}}` : ""; return fixer.replaceText( argument, `<Show when={${sourceCode.getText(test)}}${fallbackStr}>${putIntoJSX( consequent )}</Show>` ); } // we have a standard ternary, but no signal from the user as to which branch is the "fallback" and // which is the children. Move the whole conditional inside a JSX fragment. return fixer.replaceText(argument, `<>${putIntoJSX(argument)}</>`); }, }); } else if (argument?.type === "LogicalExpression") { if (argument.operator === "&&") { const sourceCode = getSourceCode(context); // we have a `return condition && expression`--put that in a <Show /> context.report({ node: argument, messageId: "noConditionalReturn", fix: (fixer) => { const { left: test, right: consequent } = argument; return fixer.replaceText( argument, `<Show when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Show>` ); }, }); } else { // we have some other kind of conditional, warn context.report({ node: argument, messageId: "noConditionalReturn", }); } } } // Pop on exit functionStack.pop(); }; return { FunctionDeclaration: onFunctionEnter, FunctionExpression: onFunctionEnter, ArrowFunctionExpression: onFunctionEnter, "FunctionDeclaration:exit": onFunctionExit, "FunctionExpression:exit": onFunctionExit, "ArrowFunctionExpression:exit": onFunctionExit, JSXElement() { if (functionStack.length) { currentFunction().isComponent = true; } }, JSXFragment() { if (functionStack.length) { currentFunction().isComponent = true; } }, ReturnStatement(node) { if (functionStack.length && node !== currentFunction().lastReturn) { currentFunction().earlyReturns.push(node); } }, }; }, });