import ts from "typescript"; /** * Local type-aware rule: the left operand of `&&` in JSX must be entirely * boolean. * * `{value && }` renders its left operand when it is falsy and * non-boolean (notably `{count && }` 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;