source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/no-unstable-nested-components.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: 14.8 KB
RevLine 
[d565449]1/**
2 * @fileoverview Prevent creating unstable components inside components
3 * @author Ari Perkkiö
4 */
5
6'use strict';
7
8const Components = require('../util/Components');
9const docsUrl = require('../util/docsUrl');
10const isCreateElement = require('../util/isCreateElement');
11const report = require('../util/report');
12
13// ------------------------------------------------------------------------------
14// Constants
15// ------------------------------------------------------------------------------
16
17const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.';
18const HOOK_REGEXP = /^use[A-Z0-9].*$/;
19
20// ------------------------------------------------------------------------------
21// Helpers
22// ------------------------------------------------------------------------------
23
24/**
25 * Generate error message with given parent component name
26 * @param {String} parentName Name of the parent component, if known
27 * @returns {String} Error message with parent component name
28 */
29function generateErrorMessageWithParentName(parentName) {
30 return `Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component${parentName ? ` “${parentName}” ` : ' '}and pass data as props.`;
31}
32
33/**
34 * Check whether given text starts with `render`. Comparison is case-sensitive.
35 * @param {String} text Text to validate
36 * @returns {Boolean}
37 */
38function startsWithRender(text) {
39 return (text || '').startsWith('render');
40}
41
42/**
43 * Get closest parent matching given matcher
44 * @param {ASTNode} node The AST node
45 * @param {Context} context eslint context
46 * @param {Function} matcher Method used to match the parent
47 * @returns {ASTNode} The matching parent node, if any
48 */
49function getClosestMatchingParent(node, context, matcher) {
50 if (!node || !node.parent || node.parent.type === 'Program') {
51 return;
52 }
53
54 if (matcher(node.parent, context)) {
55 return node.parent;
56 }
57
58 return getClosestMatchingParent(node.parent, context, matcher);
59}
60
61/**
62 * Matcher used to check whether given node is a `createElement` call
63 * @param {ASTNode} node The AST node
64 * @param {Context} context eslint context
65 * @returns {Boolean} True if node is a `createElement` call, false if not
66 */
67function isCreateElementMatcher(node, context) {
68 return (
69 node
70 && node.type === 'CallExpression'
71 && isCreateElement(context, node)
72 );
73}
74
75/**
76 * Matcher used to check whether given node is a `ObjectExpression`
77 * @param {ASTNode} node The AST node
78 * @returns {Boolean} True if node is a `ObjectExpression`, false if not
79 */
80function isObjectExpressionMatcher(node) {
81 return node && node.type === 'ObjectExpression';
82}
83
84/**
85 * Matcher used to check whether given node is a `JSXExpressionContainer`
86 * @param {ASTNode} node The AST node
87 * @returns {Boolean} True if node is a `JSXExpressionContainer`, false if not
88 */
89function isJSXExpressionContainerMatcher(node) {
90 return node && node.type === 'JSXExpressionContainer';
91}
92
93/**
94 * Matcher used to check whether given node is a `JSXAttribute` of `JSXExpressionContainer`
95 * @param {ASTNode} node The AST node
96 * @returns {Boolean} True if node is a `JSXAttribute` of `JSXExpressionContainer`, false if not
97 */
98function isJSXAttributeOfExpressionContainerMatcher(node) {
99 return (
100 node
101 && node.type === 'JSXAttribute'
102 && node.value
103 && node.value.type === 'JSXExpressionContainer'
104 );
105}
106
107/**
108 * Matcher used to check whether given node is an object `Property`
109 * @param {ASTNode} node The AST node
110 * @returns {Boolean} True if node is a `Property`, false if not
111 */
112function isPropertyOfObjectExpressionMatcher(node) {
113 return (
114 node
115 && node.parent
116 && node.parent.type === 'Property'
117 );
118}
119
120/**
121 * Matcher used to check whether given node is a `CallExpression`
122 * @param {ASTNode} node The AST node
123 * @returns {Boolean} True if node is a `CallExpression`, false if not
124 */
125function isCallExpressionMatcher(node) {
126 return node && node.type === 'CallExpression';
127}
128
129/**
130 * Check whether given node or its parent is directly inside `map` call
131 * ```jsx
132 * {items.map(item => <li />)}
133 * ```
134 * @param {ASTNode} node The AST node
135 * @returns {Boolean} True if node is directly inside `map` call, false if not
136 */
137function isMapCall(node) {
138 return (
139 node
140 && node.callee
141 && node.callee.property
142 && node.callee.property.name === 'map'
143 );
144}
145
146/**
147 * Check whether given node is `ReturnStatement` of a React hook
148 * @param {ASTNode} node The AST node
149 * @param {Context} context eslint context
150 * @returns {Boolean} True if node is a `ReturnStatement` of a React hook, false if not
151 */
152function isReturnStatementOfHook(node, context) {
153 if (
154 !node
155 || !node.parent
156 || node.parent.type !== 'ReturnStatement'
157 ) {
158 return false;
159 }
160
161 const callExpression = getClosestMatchingParent(node, context, isCallExpressionMatcher);
162 return (
163 callExpression
164 && callExpression.callee
165 && HOOK_REGEXP.test(callExpression.callee.name)
166 );
167}
168
169/**
170 * Check whether given node is declared inside a render prop
171 * ```jsx
172 * <Component renderFooter={() => <div />} />
173 * <Component>{() => <div />}</Component>
174 * ```
175 * @param {ASTNode} node The AST node
176 * @param {Context} context eslint context
177 * @returns {Boolean} True if component is declared inside a render prop, false if not
178 */
179function isComponentInRenderProp(node, context) {
180 if (
181 node
182 && node.parent
183 && node.parent.type === 'Property'
184 && node.parent.key
185 && startsWithRender(node.parent.key.name)
186 ) {
187 return true;
188 }
189
190 // Check whether component is a render prop used as direct children, e.g. <Component>{() => <div />}</Component>
191 if (
192 node
193 && node.parent
194 && node.parent.type === 'JSXExpressionContainer'
195 && node.parent.parent
196 && node.parent.parent.type === 'JSXElement'
197 ) {
198 return true;
199 }
200
201 const jsxExpressionContainer = getClosestMatchingParent(node, context, isJSXExpressionContainerMatcher);
202
203 // Check whether prop name indicates accepted patterns
204 if (
205 jsxExpressionContainer
206 && jsxExpressionContainer.parent
207 && jsxExpressionContainer.parent.type === 'JSXAttribute'
208 && jsxExpressionContainer.parent.name
209 && jsxExpressionContainer.parent.name.type === 'JSXIdentifier'
210 ) {
211 const propName = jsxExpressionContainer.parent.name.name;
212
213 // Starts with render, e.g. <Component renderFooter={() => <div />} />
214 if (startsWithRender(propName)) {
215 return true;
216 }
217
218 // Uses children prop explicitly, e.g. <Component children={() => <div />} />
219 if (propName === 'children') {
220 return true;
221 }
222 }
223
224 return false;
225}
226
227/**
228 * Check whether given node is declared directly inside a render property
229 * ```jsx
230 * const rows = { render: () => <div /> }
231 * <Component rows={ [{ render: () => <div /> }] } />
232 * ```
233 * @param {ASTNode} node The AST node
234 * @returns {Boolean} True if component is declared inside a render property, false if not
235 */
236function isDirectValueOfRenderProperty(node) {
237 return (
238 node
239 && node.parent
240 && node.parent.type === 'Property'
241 && node.parent.key
242 && node.parent.key.type === 'Identifier'
243 && startsWithRender(node.parent.key.name)
244 );
245}
246
247/**
248 * Resolve the component name of given node
249 * @param {ASTNode} node The AST node of the component
250 * @returns {String} Name of the component, if any
251 */
252function resolveComponentName(node) {
253 const parentName = node.id && node.id.name;
254 if (parentName) return parentName;
255
256 return (
257 node.type === 'ArrowFunctionExpression'
258 && node.parent
259 && node.parent.id
260 && node.parent.id.name
261 );
262}
263
264// ------------------------------------------------------------------------------
265// Rule Definition
266// ------------------------------------------------------------------------------
267
268/** @type {import('eslint').Rule.RuleModule} */
269module.exports = {
270 meta: {
271 docs: {
272 description: 'Disallow creating unstable components inside components',
273 category: 'Possible Errors',
274 recommended: false,
275 url: docsUrl('no-unstable-nested-components'),
276 },
277 schema: [{
278 type: 'object',
279 properties: {
280 customValidators: {
281 type: 'array',
282 items: {
283 type: 'string',
284 },
285 },
286 allowAsProps: {
287 type: 'boolean',
288 },
289 },
290 additionalProperties: false,
291 }],
292 },
293
294 create: Components.detect((context, components, utils) => {
295 const allowAsProps = context.options.some((option) => option && option.allowAsProps);
296
297 /**
298 * Check whether given node is declared inside class component's render block
299 * ```jsx
300 * class Component extends React.Component {
301 * render() {
302 * class NestedClassComponent extends React.Component {
303 * ...
304 * ```
305 * @param {ASTNode} node The AST node being checked
306 * @returns {Boolean} True if node is inside class component's render block, false if not
307 */
308 function isInsideRenderMethod(node) {
309 const parentComponent = utils.getParentComponent(node);
310
311 if (!parentComponent || parentComponent.type !== 'ClassDeclaration') {
312 return false;
313 }
314
315 return (
316 node
317 && node.parent
318 && node.parent.type === 'MethodDefinition'
319 && node.parent.key
320 && node.parent.key.name === 'render'
321 );
322 }
323
324 /**
325 * Check whether given node is a function component declared inside class component.
326 * Util's component detection fails to detect function components inside class components.
327 * ```jsx
328 * class Component extends React.Component {
329 * render() {
330 * const NestedComponent = () => <div />;
331 * ...
332 * ```
333 * @param {ASTNode} node The AST node being checked
334 * @returns {Boolean} True if given node a function component declared inside class component, false if not
335 */
336 function isFunctionComponentInsideClassComponent(node) {
337 const parentComponent = utils.getParentComponent(node);
338 const parentStatelessComponent = utils.getParentStatelessComponent(node);
339
340 return (
341 parentComponent
342 && parentStatelessComponent
343 && parentComponent.type === 'ClassDeclaration'
344 && utils.getStatelessComponent(parentStatelessComponent)
345 && utils.isReturningJSX(node)
346 );
347 }
348
349 /**
350 * Check whether given node is declared inside `createElement` call's props
351 * ```js
352 * React.createElement(Component, {
353 * footer: () => React.createElement("div", null)
354 * })
355 * ```
356 * @param {ASTNode} node The AST node
357 * @returns {Boolean} True if node is declare inside `createElement` call's props, false if not
358 */
359 function isComponentInsideCreateElementsProp(node) {
360 if (!components.get(node)) {
361 return false;
362 }
363
364 const createElementParent = getClosestMatchingParent(node, context, isCreateElementMatcher);
365
366 return (
367 createElementParent
368 && createElementParent.arguments
369 && createElementParent.arguments[1] === getClosestMatchingParent(node, context, isObjectExpressionMatcher)
370 );
371 }
372
373 /**
374 * Check whether given node is declared inside a component/object prop.
375 * ```jsx
376 * <Component footer={() => <div />} />
377 * { footer: () => <div /> }
378 * ```
379 * @param {ASTNode} node The AST node being checked
380 * @returns {Boolean} True if node is a component declared inside prop, false if not
381 */
382 function isComponentInProp(node) {
383 if (isPropertyOfObjectExpressionMatcher(node)) {
384 return utils.isReturningJSX(node);
385 }
386
387 const jsxAttribute = getClosestMatchingParent(node, context, isJSXAttributeOfExpressionContainerMatcher);
388
389 if (!jsxAttribute) {
390 return isComponentInsideCreateElementsProp(node);
391 }
392
393 return utils.isReturningJSX(node);
394 }
395
396 /**
397 * Check whether given node is a stateless component returning non-JSX
398 * ```jsx
399 * {{ a: () => null }}
400 * ```
401 * @param {ASTNode} node The AST node being checked
402 * @returns {Boolean} True if node is a stateless component returning non-JSX, false if not
403 */
404 function isStatelessComponentReturningNull(node) {
405 const component = utils.getStatelessComponent(node);
406
407 return component && !utils.isReturningJSX(component);
408 }
409
410 /**
411 * Check whether given node is a unstable nested component
412 * @param {ASTNode} node The AST node being checked
413 */
414 function validate(node) {
415 if (!node || !node.parent) {
416 return;
417 }
418
419 const isDeclaredInsideProps = isComponentInProp(node);
420
421 if (
422 !components.get(node)
423 && !isFunctionComponentInsideClassComponent(node)
424 && !isDeclaredInsideProps) {
425 return;
426 }
427
428 if (
429 // Support allowAsProps option
430 (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context)))
431
432 // Prevent reporting components created inside Array.map calls
433 || isMapCall(node)
434 || isMapCall(node.parent)
435
436 // Do not mark components declared inside hooks (or falsy '() => null' clean-up methods)
437 || isReturnStatementOfHook(node, context)
438
439 // Do not mark objects containing render methods
440 || isDirectValueOfRenderProperty(node)
441
442 // Prevent reporting nested class components twice
443 || isInsideRenderMethod(node)
444
445 // Prevent falsely reporting detected "components" which do not return JSX
446 || isStatelessComponentReturningNull(node)
447 ) {
448 return;
449 }
450
451 // Get the closest parent component
452 const parentComponent = getClosestMatchingParent(
453 node,
454 context,
455 (nodeToMatch) => components.get(nodeToMatch)
456 );
457
458 if (parentComponent) {
459 const parentName = resolveComponentName(parentComponent);
460
461 // Exclude lowercase parents, e.g. function createTestComponent()
462 // React-dom prevents creating lowercase components
463 if (parentName && parentName[0] === parentName[0].toLowerCase()) {
464 return;
465 }
466
467 let message = generateErrorMessageWithParentName(parentName);
468
469 // Add information about allowAsProps option when component is declared inside prop
470 if (isDeclaredInsideProps && !allowAsProps) {
471 message += COMPONENT_AS_PROPS_INFO;
472 }
473
474 report(context, message, null, {
475 node,
476 });
477 }
478 }
479
480 // --------------------------------------------------------------------------
481 // Public
482 // --------------------------------------------------------------------------
483
484 return {
485 FunctionDeclaration(node) { validate(node); },
486 ArrowFunctionExpression(node) { validate(node); },
487 FunctionExpression(node) { validate(node); },
488 ClassDeclaration(node) { validate(node); },
489 CallExpression(node) { validate(node); },
490 };
491 }),
492};
Note: See TracBrowser for help on using the repository browser.