UNPKG

eslint-plugin-sonarjs

Version:
116 lines (115 loc) • 4.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getJsxShortCircuitNodes = getJsxShortCircuitNodes; exports.someRenderedJsxDescendant = someRenderedJsxDescendant; const ast_js_1 = require("./ast.js"); function getJsxShortCircuitNodes(logicalExpression) { if (logicalExpression.parent?.type === 'JSXExpressionContainer') { return flattenJsxShortCircuitNodes(logicalExpression, logicalExpression); } else { return null; } } function flattenJsxShortCircuitNodes(root, node) { if (node.type === 'ConditionalExpression' || (node.type === 'LogicalExpression' && node.operator !== root.operator)) { return null; } else if (node.type === 'LogicalExpression') { const leftNodes = flattenJsxShortCircuitNodes(root, node.left); const rightNodes = flattenJsxShortCircuitNodes(root, node.right); if (leftNodes == null || rightNodes == null) { return null; } return [...leftNodes, node, ...rightNodes]; } else { return []; } } /** * Checks whether a JSX element has a matching descendant in a rendered child position. * * This follows JSX children, expressions, conditionals, arrays, TypeScript wrappers, and * map/flatMap callbacks. It deliberately excludes JSX attributes, non-rendering callbacks, * and bare function children. */ function someRenderedJsxDescendant(element, predicate, shouldPrune = () => false) { // Grows while iterating: every visited position appends its own rendered positions. const pending = renderedChildPositions(element); for (const current of pending) { if (current.type === 'JSXElement') { if (predicate(current)) { return true; } if (shouldPrune(current)) { continue; } } pending.push(...renderedChildPositions(current)); } return false; } function renderedChildPositions(node) { const unwrapped = (0, ast_js_1.unwrapTypeScriptExpression)(node); if (unwrapped !== node) { return [unwrapped]; } switch (node.type) { case 'JSXElement': case 'JSXFragment': return [...node.children]; case 'JSXExpressionContainer': return node.expression.type === 'JSXEmptyExpression' ? [] : [node.expression]; case 'ConditionalExpression': return [node.consequent, node.alternate]; case 'LogicalExpression': return renderedLogicalExpressionPositions(node); case 'ArrayExpression': return node.elements.filter(isArrayElement); case 'ChainExpression': return [node.expression]; case 'CallExpression': return isRenderingArrayMappingCall(node) ? node.arguments.filter(isRenderingCallback) : []; case 'ArrowFunctionExpression': case 'FunctionExpression': // Only a function that something invokes renders; a bare `{() => <B />}` does not. return isArgumentOfRenderingCall(node) ? [node.body] : []; case 'BlockStatement': return [...node.body]; case 'ReturnStatement': return node.argument === null ? [] : [node.argument]; case 'IfStatement': return node.alternate === null ? [node.consequent] : [node.consequent, node.alternate]; default: return []; } } function renderedLogicalExpressionPositions(node) { if (node.operator === '&&') { return [node.right]; } if (node.operator === '||' || node.operator === '??') { return [node.left, node.right]; } return []; } function isArrayElement(element) { return element !== null; } function isRenderingCallback(node) { return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'; } function isArgumentOfRenderingCall(node) { const parent = node.parent; return (parent?.type === 'CallExpression' && parent.arguments.includes(node) && isRenderingArrayMappingCall(parent)); } function isRenderingArrayMappingCall(node) { return (node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property.type === 'Identifier' && (node.callee.property.name === 'map' || node.callee.property.name === 'flatMap')); }