@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
JavaScript
//#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