source: frontend/node_modules/eslint-plugin-react/lib/rules/jsx-no-constructed-context-values.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 13 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 8.6 KB
Line 
1/**
2 * @fileoverview Prevents jsx context provider values from taking values that
3 * will cause needless rerenders.
4 * @author Dylan Oshima
5 */
6
7'use strict';
8
9const Components = require('../util/Components');
10const docsUrl = require('../util/docsUrl');
11const getScope = require('../util/eslint').getScope;
12const report = require('../util/report');
13
14// ------------------------------------------------------------------------------
15// Helpers
16// ------------------------------------------------------------------------------
17
18// Recursively checks if an element is a construction.
19// A construction is a variable that changes identity every render.
20function isConstruction(node, callScope) {
21 switch (node.type) {
22 case 'Literal':
23 if (node.regex != null) {
24 return { type: 'regular expression', node };
25 }
26 return null;
27 case 'Identifier': {
28 const variableScoping = callScope.set.get(node.name);
29
30 if (variableScoping == null || variableScoping.defs == null) {
31 // If it's not in scope, we don't care.
32 return null; // Handled
33 }
34
35 // Gets the last variable identity
36 const variableDefs = variableScoping.defs;
37 const def = variableDefs[variableDefs.length - 1];
38 if (def != null
39 && def.type !== 'Variable'
40 && def.type !== 'FunctionName'
41 ) {
42 // Parameter or an unusual pattern. Bail out.
43 return null; // Unhandled
44 }
45
46 if (def.node.type === 'FunctionDeclaration') {
47 return { type: 'function declaration', node: def.node, usage: node };
48 }
49
50 const init = def.node.init;
51 if (init == null) {
52 return null;
53 }
54
55 const initConstruction = isConstruction(init, callScope);
56 if (initConstruction == null) {
57 return null;
58 }
59
60 return {
61 type: initConstruction.type,
62 node: initConstruction.node,
63 usage: node,
64 };
65 }
66 case 'ObjectExpression':
67 // Any object initialized inline will create a new identity
68 return { type: 'object', node };
69 case 'ArrayExpression':
70 return { type: 'array', node };
71 case 'ArrowFunctionExpression':
72 case 'FunctionExpression':
73 // Functions that are initialized inline will have a new identity
74 return { type: 'function expression', node };
75 case 'ClassExpression':
76 return { type: 'class expression', node };
77 case 'NewExpression':
78 // `const a = new SomeClass();` is a construction
79 return { type: 'new expression', node };
80 case 'ConditionalExpression':
81 return (isConstruction(node.consequent, callScope)
82 || isConstruction(node.alternate, callScope)
83 );
84 case 'LogicalExpression':
85 return (isConstruction(node.left, callScope)
86 || isConstruction(node.right, callScope)
87 );
88 case 'MemberExpression': {
89 const objConstruction = isConstruction(node.object, callScope);
90 if (objConstruction == null) {
91 return null;
92 }
93 return {
94 type: objConstruction.type,
95 node: objConstruction.node,
96 usage: node.object,
97 };
98 }
99 case 'JSXFragment':
100 return { type: 'JSX fragment', node };
101 case 'JSXElement':
102 return { type: 'JSX element', node };
103 case 'AssignmentExpression': {
104 const construct = isConstruction(node.right, callScope);
105 if (construct != null) {
106 return {
107 type: 'assignment expression',
108 node: construct.node,
109 usage: node,
110 };
111 }
112 return null;
113 }
114 case 'TypeCastExpression':
115 case 'TSAsExpression':
116 return isConstruction(node.expression, callScope);
117 default:
118 return null;
119 }
120}
121
122function isReactContext(context, node) {
123 let scope = getScope(context, node);
124 let variableScoping = null;
125 const contextName = node.name;
126
127 while (scope && !variableScoping) { // Walk up the scope chain to find the variable
128 variableScoping = scope.set.get(contextName);
129 scope = scope.upper;
130 }
131
132 if (!variableScoping) { // Context was not found in scope
133 return false;
134 }
135
136 // Get the variable's definition
137 const def = variableScoping.defs[0];
138
139 if (!def || def.node.type !== 'VariableDeclarator') {
140 return false;
141 }
142
143 const init = def.node.init; // Variable initializer
144
145 const isCreateContext = init
146 && init.type === 'CallExpression'
147 && (
148 (
149 init.callee.type === 'Identifier'
150 && init.callee.name === 'createContext'
151 ) || (
152 init.callee.type === 'MemberExpression'
153 && init.callee.object.name === 'React'
154 && init.callee.property.name === 'createContext'
155 )
156 );
157
158 return isCreateContext;
159}
160
161// ------------------------------------------------------------------------------
162// Rule Definition
163// ------------------------------------------------------------------------------
164
165const messages = {
166 withIdentifierMsg: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.",
167 withIdentifierMsgFunc: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.",
168 defaultMsg: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.',
169 defaultMsgFunc: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.',
170};
171
172/** @type {import('eslint').Rule.RuleModule} */
173module.exports = {
174 meta: {
175 docs: {
176 description: 'Disallows JSX context provider values from taking values that will cause needless rerenders',
177 category: 'Best Practices',
178 recommended: false,
179 url: docsUrl('jsx-no-constructed-context-values'),
180 },
181 messages,
182 schema: false,
183 },
184
185 // eslint-disable-next-line arrow-body-style
186 create: Components.detect((context, components, utils) => {
187 return {
188 JSXOpeningElement(node) {
189 const openingElementName = node.name;
190
191 if (openingElementName.type === 'JSXMemberExpression') {
192 const isJSXContext = openingElementName.property.name === 'Provider';
193 if (!isJSXContext) {
194 // Member is not Provider
195 return;
196 }
197 } else if (openingElementName.type === 'JSXIdentifier') {
198 const isJSXContext = isReactContext(context, openingElementName);
199 if (!isJSXContext) {
200 // Member is not context
201 return;
202 }
203 } else {
204 return;
205 }
206
207 // Contexts can take in more than just a value prop
208 // so we need to iterate through all of them
209 const jsxValueAttribute = node.attributes.find(
210 (attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'value'
211 );
212
213 if (jsxValueAttribute == null) {
214 // No value prop was passed
215 return;
216 }
217
218 const valueNode = jsxValueAttribute.value;
219 if (!valueNode) {
220 // attribute is a boolean shorthand
221 return;
222 }
223 if (valueNode.type !== 'JSXExpressionContainer') {
224 // value could be a literal
225 return;
226 }
227
228 const valueExpression = valueNode.expression;
229 const invocationScope = getScope(context, node);
230
231 // Check if the value prop is a construction
232 const constructInfo = isConstruction(valueExpression, invocationScope);
233 if (constructInfo == null) {
234 return;
235 }
236
237 if (!utils.getParentComponent(node)) {
238 return;
239 }
240
241 // Report found error
242 const constructType = constructInfo.type;
243 const constructNode = constructInfo.node;
244 const constructUsage = constructInfo.usage;
245 const data = {
246 type: constructType, nodeLine: constructNode.loc.start.line,
247 };
248 let messageId = 'defaultMsg';
249
250 // Variable passed to value prop
251 if (constructUsage != null) {
252 messageId = 'withIdentifierMsg';
253 data.usageLine = constructUsage.loc.start.line;
254 data.variableName = constructUsage.name;
255 }
256
257 // Type of expression
258 if (
259 constructType === 'function expression'
260 || constructType === 'function declaration'
261 ) {
262 messageId += 'Func';
263 }
264
265 report(context, messages[messageId], messageId, {
266 node: constructNode,
267 data,
268 });
269 },
270 };
271 }),
272};
Note: See TracBrowser for help on using the repository browser.