source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-no-leaked-render.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 7.5 KB
Line 
1/**
2 * @fileoverview Prevent problematic leaked values from being rendered
3 * @author Mario Beltrán
4 */
5
6'use strict';
7
8const find = require('es-iterator-helpers/Iterator.prototype.find');
9const from = require('es-iterator-helpers/Iterator.from');
10
11const getText = require('../util/eslint').getText;
12const docsUrl = require('../util/docsUrl');
13const report = require('../util/report');
14const variableUtil = require('../util/variable');
15const testReactVersion = require('../util/version').testReactVersion;
16const isParenthesized = require('../util/ast').isParenthesized;
17
18//------------------------------------------------------------------------------
19// Rule Definition
20//------------------------------------------------------------------------------
21
22const messages = {
23 noPotentialLeakedRender: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
24};
25
26const COERCE_STRATEGY = 'coerce';
27const TERNARY_STRATEGY = 'ternary';
28const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY];
29const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression'];
30const TERNARY_INVALID_ALTERNATE_VALUES = [undefined, null, false];
31
32function trimLeftNode(node) {
33 // Remove double unary expression (boolean coercion), so we avoid trimming valid negations
34 if (node.type === 'UnaryExpression' && node.argument.type === 'UnaryExpression') {
35 return trimLeftNode(node.argument.argument);
36 }
37
38 return node;
39}
40
41function getIsCoerceValidNestedLogicalExpression(node) {
42 if (node.type === 'LogicalExpression') {
43 return getIsCoerceValidNestedLogicalExpression(node.left) && getIsCoerceValidNestedLogicalExpression(node.right);
44 }
45
46 return COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === node.type);
47}
48
49function extractExpressionBetweenLogicalAnds(node) {
50 if (node.type !== 'LogicalExpression') return [node];
51 if (node.operator !== '&&') return [node];
52 return [].concat(
53 extractExpressionBetweenLogicalAnds(node.left),
54 extractExpressionBetweenLogicalAnds(node.right)
55 );
56}
57
58function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) {
59 const rightSideText = getText(context, rightNode);
60
61 if (fixStrategy === COERCE_STRATEGY) {
62 const expressions = extractExpressionBetweenLogicalAnds(leftNode);
63 const newText = expressions.map((node) => {
64 let nodeText = getText(context, node);
65 if (isParenthesized(context, node)) {
66 nodeText = `(${nodeText})`;
67 }
68 if (node.parent && node.parent.type === 'ConditionalExpression' && node.parent.consequent.value === false) {
69 return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!'}${nodeText}`;
70 }
71 return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!!'}${nodeText}`;
72 }).join(' && ');
73
74 if (rightNode.parent && rightNode.parent.type === 'ConditionalExpression' && rightNode.parent.consequent.value === false) {
75 const consequentVal = rightNode.parent.consequent.raw || rightNode.parent.consequent.name;
76 const alternateVal = rightNode.parent.alternate.raw || rightNode.parent.alternate.name;
77 if (rightNode.parent.test && rightNode.parent.test.type === 'LogicalExpression') {
78 return fixer.replaceText(reportedNode, `${newText} ? ${consequentVal} : ${alternateVal}`);
79 }
80 return fixer.replaceText(reportedNode, `${newText} && ${alternateVal}`);
81 }
82
83 if (rightNode.type === 'ConditionalExpression' || rightNode.type === 'LogicalExpression') {
84 return fixer.replaceText(reportedNode, `${newText} && (${rightSideText})`);
85 }
86 if (rightNode.type === 'JSXElement') {
87 const rightSideTextLines = rightSideText.split('\n');
88 if (rightSideTextLines.length > 1) {
89 const rightSideTextLastLine = rightSideTextLines[rightSideTextLines.length - 1];
90 const indentSpacesStart = ' '.repeat(rightSideTextLastLine.search(/\S/));
91 const indentSpacesClose = ' '.repeat(rightSideTextLastLine.search(/\S/) - 2);
92 return fixer.replaceText(reportedNode, `${newText} && (\n${indentSpacesStart}${rightSideText}\n${indentSpacesClose})`);
93 }
94 }
95 if (rightNode.type === 'Literal') {
96 return null;
97 }
98 return fixer.replaceText(reportedNode, `${newText} && ${rightSideText}`);
99 }
100
101 if (fixStrategy === TERNARY_STRATEGY) {
102 let leftSideText = getText(context, trimLeftNode(leftNode));
103 if (isParenthesized(context, leftNode)) {
104 leftSideText = `(${leftSideText})`;
105 }
106 return fixer.replaceText(reportedNode, `${leftSideText} ? ${rightSideText} : null`);
107 }
108
109 throw new TypeError('Invalid value for "validStrategies" option');
110}
111
112/**
113 * @type {import('eslint').Rule.RuleModule}
114 */
115/** @type {import('eslint').Rule.RuleModule} */
116module.exports = {
117 meta: {
118 docs: {
119 description: 'Disallow problematic leaked values from being rendered',
120 category: 'Possible Errors',
121 recommended: false,
122 url: docsUrl('jsx-no-leaked-render'),
123 },
124
125 messages,
126
127 fixable: 'code',
128 schema: [
129 {
130 type: 'object',
131 properties: {
132 validStrategies: {
133 type: 'array',
134 items: {
135 enum: [
136 TERNARY_STRATEGY,
137 COERCE_STRATEGY,
138 ],
139 },
140 uniqueItems: true,
141 default: DEFAULT_VALID_STRATEGIES,
142 },
143 },
144 additionalProperties: false,
145 },
146 ],
147 },
148
149 create(context) {
150 const config = context.options[0] || {};
151 const validStrategies = new Set(config.validStrategies || DEFAULT_VALID_STRATEGIES);
152 const fixStrategy = find(from(validStrategies), () => true);
153
154 return {
155 'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
156 const leftSide = node.left;
157
158 const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS
159 .some((validExpression) => validExpression === leftSide.type);
160 if (validStrategies.has(COERCE_STRATEGY)) {
161 if (isCoerceValidLeftSide || getIsCoerceValidNestedLogicalExpression(leftSide)) {
162 return;
163 }
164 const leftSideVar = variableUtil.getVariableFromContext(context, node, leftSide.name);
165 if (leftSideVar) {
166 const leftSideValue = leftSideVar.defs
167 && leftSideVar.defs.length
168 && leftSideVar.defs[0].node.init
169 && leftSideVar.defs[0].node.init.value;
170 if (typeof leftSideValue === 'boolean') {
171 return;
172 }
173 }
174 }
175
176 if (testReactVersion(context, '>= 18') && leftSide.type === 'Literal' && leftSide.value === '') {
177 return;
178 }
179 report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
180 node,
181 fix(fixer) {
182 return ruleFixer(context, fixStrategy, fixer, node, leftSide, node.right);
183 },
184 });
185 },
186
187 'JSXExpressionContainer > ConditionalExpression'(node) {
188 if (validStrategies.has(TERNARY_STRATEGY)) {
189 return;
190 }
191
192 const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
193 const isJSXElementAlternate = node.alternate.type === 'JSXElement';
194 if (isValidTernaryAlternate || isJSXElementAlternate) {
195 return;
196 }
197
198 report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
199 node,
200 fix(fixer) {
201 return ruleFixer(context, fixStrategy, fixer, node, node.test, node.consequent);
202 },
203 });
204 },
205 };
206 },
207};
Note: See TracBrowser for help on using the repository browser.