UNPKG

@truenine/eslint9-config

Version:

ESLint 9 configuration package for Compose Client projects with TypeScript, Vue, and modern JavaScript support

119 lines (118 loc) 5.47 kB
//#region src/rules/code-style/guard-clause.ts const rule = { meta: { type: "suggestion", docs: { description: "Prefer guard clauses (early returns) to reduce nesting", recommended: false }, fixable: "code", schema: [{ type: "object", properties: { minStatements: { type: "number", default: 2 } }, additionalProperties: false }], messages: { preferGuardClause: "Prefer guard clause with early return to reduce nesting" } }, create(context) { const { sourceCode } = context; const minStatements = (context.options[0] ?? {}).minStatements ?? 2; function isFunctionBody(node) { const { parent } = node; return parent != null && [ "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression" ].includes(parent.type); } function invertCondition(conditionText) { const trimmed = conditionText.trim(); if (trimmed.startsWith("!") && !trimmed.startsWith("!=")) { const inner = trimmed.slice(1).trim(); return inner.startsWith("(") && inner.endsWith(")") ? inner.slice(1, -1) : inner; } if (trimmed.includes("===")) return trimmed.replace("===", "!=="); if (trimmed.includes("!==")) return trimmed.replace("!==", "==="); if (trimmed.includes("==") && !trimmed.includes("===")) return trimmed.replace("==", "!="); if (trimmed.includes("!=") && !trimmed.includes("!==")) return trimmed.replace("!=", "=="); if (trimmed.includes(">=")) return trimmed.replace(">=", "<"); if (trimmed.includes("<=")) return trimmed.replace("<=", ">"); if (trimmed.includes(">") && !trimmed.includes(">=")) return trimmed.replace(">", "<="); if (trimmed.includes("<") && !trimmed.includes("<=")) return trimmed.replace("<", ">="); if (/\.length\s*>\s*0/.test(trimmed)) return trimmed.replace(/\.length\s*>\s*0/, ".length === 0"); if (/\.length\s*===\s*0/.test(trimmed)) return trimmed.replace(/\.length\s*===\s*0/, ".length > 0"); return trimmed.includes("&&") || trimmed.includes("||") ? `!(${trimmed})` : `!${trimmed}`; } function getIndent(node) { return " ".repeat(node.loc?.start.column ?? 0); } function isMultiLine(node) { return node.loc != null && node.loc.start.line !== node.loc.end.line; } function formatReturnStatement(returnText, indent) { return returnText.includes("\n") ? `{\n${indent} ${returnText}\n${indent}}` : returnText; } return { IfStatement(node) { if (node.alternate != null || node.parent?.type !== "BlockStatement" || !isFunctionBody(node.parent) || node.consequent?.type !== "BlockStatement") return; const { parent } = node; if (parent.parent?.type === "IfStatement" && parent.parent.alternate === node) return; const blockBody = node.consequent.body; if (blockBody.length < minStatements) return; const parentBody = parent.body; const ifIndex = parentBody.indexOf(node); const statementsAfter = parentBody.slice(ifIndex + 1).filter((s) => s.type !== "EmptyStatement"); if (statementsAfter.length === 1 && statementsAfter[0]?.type === "ReturnStatement") { const returnNode = statementsAfter[0]; if (isMultiLine(returnNode)) return; const lastBlockStmt = blockBody.at(-1); context.report({ node, messageId: "preferGuardClause", fix(fixer) { const indent = getIndent(node); const returnText = sourceCode.getText(returnNode); const bodyText = blockBody.map((s) => sourceCode.getText(s)).join(`\n${indent}`); const formattedReturn = formatReturnStatement(returnText, indent); const result = lastBlockStmt?.type === "ReturnStatement" ? `if (${invertCondition(sourceCode.getText(node.test))}) ${formattedReturn}\n\n${indent}${bodyText}` : `if (${invertCondition(sourceCode.getText(node.test))}) ${formattedReturn}\n\n${indent}${bodyText}\n${indent}${returnText}`; const nodeRange = node.range; return fixer.replaceTextRange([nodeRange[0], returnNode.range[1]], result); } }); return; } if (statementsAfter.length !== 0) return; const lastStmt = blockBody.at(-1); if (lastStmt == null) return; const endsWithReturn = lastStmt.type === "ReturnStatement"; let defaultReturnText = "return"; const funcParent = parent.parent; if (funcParent?.returnType != null) { const returnTypeText = sourceCode.getText(funcParent.returnType); if (!/^\s*:\s*(?:void|undefined)\s*$/.test(returnTypeText)) if (endsWithReturn && lastStmt.argument != null) defaultReturnText = `return ${sourceCode.getText(lastStmt.argument)}`; else return; } context.report({ node, messageId: "preferGuardClause", fix(fixer) { const invertedCondition = invertCondition(sourceCode.getText(node.test)); const indent = getIndent(node); if (endsWithReturn) { const statementsWithoutReturn = blockBody.slice(0, -1); if (statementsWithoutReturn.length === 0) return fixer.replaceText(node, `if (${invertedCondition}) ${defaultReturnText}`); const bodyText = statementsWithoutReturn.map((s) => sourceCode.getText(s)).join(`\n${indent}`); return fixer.replaceText(node, `if (${invertedCondition}) ${defaultReturnText}\n\n${indent}${bodyText}`); } const bodyText = blockBody.map((s) => sourceCode.getText(s)).join(`\n${indent}`); return fixer.replaceText(node, `if (${invertedCondition}) ${defaultReturnText}\n\n${indent}${bodyText}`); } }); } }; } }; //#endregion export { rule as default }; //# sourceMappingURL=guard-clause.mjs.map