From a0dc6b9213a84d7a18013a38c9191e9a80426b10 Mon Sep 17 00:00:00 2001 From: morgan-coded <256248948+morgan-coded@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:56:17 -0500 Subject: [PATCH] jsx-no-leaked-render: preserve expression alternates --- lib/rules/jsx-no-leaked-render.js | 54 +++++-- tests/lib/rules/jsx-no-leaked-render.js | 180 ++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 9 deletions(-) diff --git a/lib/rules/jsx-no-leaked-render.js b/lib/rules/jsx-no-leaked-render.js index efe7018c21..42431419b0 100644 --- a/lib/rules/jsx-no-leaked-render.js +++ b/lib/rules/jsx-no-leaked-render.js @@ -7,14 +7,17 @@ const find = require('es-iterator-helpers/Iterator.prototype.find'); const from = require('es-iterator-helpers/Iterator.from'); +const arrayIncludes = require('array-includes'); -const getText = require('../util/eslint').getText; +const eslintUtil = require('../util/eslint'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); const variableUtil = require('../util/variable'); const testReactVersion = require('../util/version').testReactVersion; const isParenthesized = require('../util/ast').isParenthesized; +const getText = eslintUtil.getText; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -27,7 +30,39 @@ const COERCE_STRATEGY = 'coerce'; const TERNARY_STRATEGY = 'ternary'; const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY]; const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression']; -const TERNARY_INVALID_ALTERNATE_VALUES = [undefined, null, false]; +const TERNARY_INVALID_ALTERNATE_VALUES = [null, false]; + +function isGlobalUndefined(context, node) { + const variable = variableUtil.getVariableFromContext(context, node, node.name); + return !variable || !variable.defs || variable.defs.length === 0; +} + +function isInvalidTernaryAlternate(context, node) { + if (node.type === 'Identifier') { + return node.name === 'undefined' && isGlobalUndefined(context, node); + } + if (node.type === 'UnaryExpression') { + return node.operator === 'void' && node.argument.type === 'Literal'; + } + return node.type === 'Literal' && arrayIncludes(TERNARY_INVALID_ALTERNATE_VALUES, node.value); +} + +function isFalseLiteral(node) { + return node.type === 'Literal' && node.value === false; +} + +function getCoerceAlternateText(context, node) { + const nodeText = getText(context, node); + if ( + node.type === 'AssignmentExpression' + || node.type === 'ConditionalExpression' + || node.type === 'LogicalExpression' + || node.type === 'SequenceExpression' + ) { + return `(${nodeText})`; + } + return nodeText; +} function trimLeftNode(node) { // Remove double unary expression (boolean coercion), so we avoid trimming valid negations @@ -80,19 +115,22 @@ function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNod if (isParenthesized(context, node)) { nodeText = `(${nodeText})`; } - if (node.parent && node.parent.type === 'ConditionalExpression' && node.parent.consequent.value === false) { + if (node.parent && node.parent.type === 'ConditionalExpression' && isFalseLiteral(node.parent.consequent)) { return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!'}${nodeText}`; } return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!!'}${nodeText}`; }).join(' && '); - if (rightNode.parent && rightNode.parent.type === 'ConditionalExpression' && rightNode.parent.consequent.value === false) { + if (rightNode.parent && rightNode.parent.type === 'ConditionalExpression' && isFalseLiteral(rightNode.parent.consequent)) { const consequentVal = rightNode.parent.consequent.raw || rightNode.parent.consequent.name; - const alternateVal = rightNode.parent.alternate.raw || rightNode.parent.alternate.name; + const alternateVal = getCoerceAlternateText(context, rightNode.parent.alternate); if (rightNode.parent.test && rightNode.parent.test.type === 'LogicalExpression') { return fixer.replaceText(reportedNode, `${newText} ? ${consequentVal} : ${alternateVal}`); } - return fixer.replaceText(reportedNode, `${newText} && ${alternateVal}`); + return fixer.replaceText( + reportedNode, + `${newText} && ${alternateVal}` + ); } if (rightNode.type === 'ConditionalExpression' || rightNode.type === 'LogicalExpression') { @@ -211,9 +249,7 @@ module.exports = { return; } - const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1; - const isJSXElementAlternate = node.alternate.type === 'JSXElement'; - if (isValidTernaryAlternate || isJSXElementAlternate) { + if (!isInvalidTernaryAlternate(context, node.alternate) && !isFalseLiteral(node.consequent)) { return; } diff --git a/tests/lib/rules/jsx-no-leaked-render.js b/tests/lib/rules/jsx-no-leaked-render.js index aaedb60b7e..fc74ef92bc 100644 --- a/tests/lib/rules/jsx-no-leaked-render.js +++ b/tests/lib/rules/jsx-no-leaked-render.js @@ -126,6 +126,14 @@ ruleTester.run('jsx-no-leaked-render', rule, { `, options: [{ validStrategies: ['coerce', 'ternary'] }], }, + { + code: ` + function Component({ count, title }, undefined) { + return