eslint-plugin-sonarjs
Version:
116 lines (115 loc) • 4.38 kB
JavaScript
"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'));
}