shoc-frontend-new/eslint-rules/no-non-boolean-jsx-and.js
2026-07-24 11:32:58 -03:00

95 lines
2.9 KiB
JavaScript

import ts from "typescript";
/**
* Local type-aware rule: the left operand of `&&` in JSX must be entirely
* boolean.
*
* `{value && <Element />}` renders its left operand when it is falsy and
* non-boolean (notably `{count && <X />}` renders `0`), so the left operand
* must be `boolean` (or boolean literals) across the whole union. The rule
* asks the TypeScript checker for the type of the left operand and reports
* unless every union constituent is boolean-like.
*
* Type-aware by design: a selector that cannot see operand types would give
* false confidence rather than enforcement. If type services are unexpectedly
* unavailable in a governed TSX file, the rule fails closed (reports) instead
* of silently claiming the expression is safe.
*/
const booleanLikeFlags = ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral;
function isBooleanLikeType(type) {
return (type.flags & booleanLikeFlags) !== 0;
}
function isEntirelyBoolean(type) {
if (type.isUnion()) {
return type.types.every((constituent) => isBooleanLikeType(constituent));
}
return isBooleanLikeType(type);
}
const transparentExpressionTypes = new Set([
"ChainExpression",
"ParenthesizedExpression",
"TSAsExpression",
"TSNonNullExpression",
"TSTypeAssertion",
]);
function isInRenderedPosition(node) {
let current = node;
while (current.parent) {
const parent = current.parent;
if (parent.type === "JSXExpressionContainer") {
return parent.parent?.type !== "JSXAttribute";
}
if (parent.type === "LogicalExpression" || transparentExpressionTypes.has(parent.type)) {
current = parent;
continue;
}
return false;
}
return false;
}
const rule = {
meta: {
type: "problem",
docs: {
description: "Require the left operand of `&&` in JSX to be entirely boolean",
},
schema: [],
messages: {
nonBooleanLeft:
'The left operand of `&&` in JSX must be boolean. Non-boolean falsy operands (e.g. `0`, `""`) render into the DOM; coerce with `Boolean(...)` or `!!` before `&&`.',
typeServicesUnavailable:
"Type-aware boolean-safety check is unavailable for this JSX expression. This file must be part of a TypeScript project with type information so the rule can enforce safely.",
},
},
create(context) {
const services = context.sourceCode?.parserServices ?? context.parserServices;
return {
LogicalExpression(node) {
if (node.operator !== "&&") {
return;
}
if (!isInRenderedPosition(node)) {
return;
}
if (!services || services.program === null) {
context.report({ node, messageId: "typeServicesUnavailable" });
return;
}
const leftType = services.getTypeAtLocation(node.left);
if (!isEntirelyBoolean(leftType)) {
context.report({ node: node.left, messageId: "nonBooleanLeft" });
}
},
};
},
};
export default rule;