source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/no-unstable-nested-components.js@ 0c6b92a

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

Pred finalna verzija

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