eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
304 lines (276 loc) • 8.6 kB
JavaScript
/**
* @fileoverview Prevent problematic leaked values from being rendered
* @author Mario Beltrán
*/
;
const find = require('es-iterator-helpers/Iterator.prototype.find');
const from = require('es-iterator-helpers/Iterator.from');
const getText = require('../util/eslint').getText;
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const variableUtil = require('../util/variable');
const testReblendVersion = require('../util/version').testReblendVersion;
const isParenthesized = require('../util/ast').isParenthesized;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const messages = {
noPotentialLeakedRender:
'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
};
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];
function trimLeftNode(node) {
// Remove double unary expression (boolean coercion), so we avoid trimming valid negations
if (
node.type === 'UnaryExpression' &&
node.argument.type === 'UnaryExpression'
) {
return trimLeftNode(node.argument.argument);
}
return node;
}
function getIsCoerceValidNestedLogicalExpression(node) {
if (node.type === 'LogicalExpression') {
return (
getIsCoerceValidNestedLogicalExpression(node.left) &&
getIsCoerceValidNestedLogicalExpression(node.right)
);
}
return COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some(
validExpression => validExpression === node.type
);
}
function extractExpressionBetweenLogicalAnds(node) {
if (node.type !== 'LogicalExpression') return [node];
if (node.operator !== '&&') return [node];
return [].concat(
extractExpressionBetweenLogicalAnds(node.left),
extractExpressionBetweenLogicalAnds(node.right)
);
}
function ruleFixer(
context,
fixStrategy,
fixer,
reportedNode,
leftNode,
rightNode
) {
const rightSideText = getText(context, rightNode);
if (fixStrategy === COERCE_STRATEGY) {
const expressions = extractExpressionBetweenLogicalAnds(leftNode);
const newText = expressions
.map(node => {
let nodeText = getText(context, node);
if (isParenthesized(context, node)) {
nodeText = `(${nodeText})`;
}
if (
node.parent &&
node.parent.type === 'ConditionalExpression' &&
node.parent.consequent.value === false
) {
return `${
getIsCoerceValidNestedLogicalExpression(node) ? '' : '!'
}${nodeText}`;
}
return `${
getIsCoerceValidNestedLogicalExpression(node) ? '' : '!!'
}${nodeText}`;
})
.join(' && ');
if (
rightNode.parent &&
rightNode.parent.type === 'ConditionalExpression' &&
rightNode.parent.consequent.value === false
) {
const consequentVal =
rightNode.parent.consequent.raw || rightNode.parent.consequent.name;
const alternateVal =
rightNode.parent.alternate.raw || rightNode.parent.alternate.name;
if (
rightNode.parent.test &&
rightNode.parent.test.type === 'LogicalExpression'
) {
return fixer.replaceText(
reportedNode,
`${newText} ? ${consequentVal} : ${alternateVal}`
);
}
return fixer.replaceText(reportedNode, `${newText} && ${alternateVal}`);
}
if (
rightNode.type === 'ConditionalExpression' ||
rightNode.type === 'LogicalExpression'
) {
return fixer.replaceText(
reportedNode,
`${newText} && (${rightSideText})`
);
}
if (rightNode.type === 'JSXElement') {
const rightSideTextLines = rightSideText.split('\n');
if (rightSideTextLines.length > 1) {
const rightSideTextLastLine =
rightSideTextLines[rightSideTextLines.length - 1];
const indentSpacesStart = ' '.repeat(
rightSideTextLastLine.search(/\S/)
);
const indentSpacesClose = ' '.repeat(
rightSideTextLastLine.search(/\S/) - 2
);
return fixer.replaceText(
reportedNode,
`${newText} && (\n${indentSpacesStart}${rightSideText}\n${indentSpacesClose})`
);
}
}
if (rightNode.type === 'Literal') {
return null;
}
return fixer.replaceText(reportedNode, `${newText} && ${rightSideText}`);
}
if (fixStrategy === TERNARY_STRATEGY) {
let leftSideText = getText(context, trimLeftNode(leftNode));
if (isParenthesized(context, leftNode)) {
leftSideText = `(${leftSideText})`;
}
return fixer.replaceText(
reportedNode,
`${leftSideText} ? ${rightSideText} : null`
);
}
throw new TypeError('Invalid value for "validStrategies" option');
}
/**
* @type {import('eslint').Rule.RuleModule}
*/
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow problematic leaked values from being rendered',
category: 'Possible Errors',
recommended: false,
url: docsUrl('jsx-no-leaked-render'),
},
messages,
fixable: 'code',
schema: [
{
type: 'object',
properties: {
validStrategies: {
type: 'array',
items: {
enum: [TERNARY_STRATEGY, COERCE_STRATEGY],
},
uniqueItems: true,
default: DEFAULT_VALID_STRATEGIES,
},
},
additionalProperties: false,
},
],
},
create(context) {
const config = context.options[0] || {};
const validStrategies = new Set(
config.validStrategies || DEFAULT_VALID_STRATEGIES
);
const fixStrategy = find(from(validStrategies), () => true);
return {
'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
const leftSide = node.left;
const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some(
validExpression => validExpression === leftSide.type
);
if (validStrategies.has(COERCE_STRATEGY)) {
if (
isCoerceValidLeftSide ||
getIsCoerceValidNestedLogicalExpression(leftSide)
) {
return;
}
const variables = variableUtil.variablesInScope(context, node);
const leftSideVar = variableUtil.getVariable(
variables,
leftSide.name
);
if (leftSideVar) {
const leftSideValue =
leftSideVar.defs &&
leftSideVar.defs.length &&
leftSideVar.defs[0].node.init &&
leftSideVar.defs[0].node.init.value;
if (typeof leftSideValue === 'boolean') {
return;
}
}
}
if (
testReblendVersion(context, '>= 18') &&
leftSide.type === 'Literal' &&
leftSide.value === ''
) {
return;
}
report(
context,
messages.noPotentialLeakedRender,
'noPotentialLeakedRender',
{
node,
fix(fixer) {
return ruleFixer(
context,
fixStrategy,
fixer,
node,
leftSide,
node.right
);
},
}
);
},
'JSXExpressionContainer > ConditionalExpression'(node) {
if (validStrategies.has(TERNARY_STRATEGY)) {
return;
}
const isValidTernaryAlternate =
TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
const isJSXElementAlternate = node.alternate.type === 'JSXElement';
if (isValidTernaryAlternate || isJSXElementAlternate) {
return;
}
report(
context,
messages.noPotentialLeakedRender,
'noPotentialLeakedRender',
{
node,
fix(fixer) {
return ruleFixer(
context,
fixStrategy,
fixer,
node,
node.test,
node.consequent
);
},
}
);
},
};
},
};