123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- 'use strict';
- const Components = require('../util/Components');
- const docsUrl = require('../util/docsUrl');
- const isCreateElement = require('../util/isCreateElement');
- const report = require('../util/report');
- const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.';
- const HOOK_REGEXP = /^use[A-Z0-9].*$/;
- function generateErrorMessageWithParentName(parentName) {
- 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.`;
- }
- function startsWithRender(text) {
- return (text || '').startsWith('render');
- }
- function getClosestMatchingParent(node, context, matcher) {
- if (!node || !node.parent || node.parent.type === 'Program') {
- return;
- }
- if (matcher(node.parent, context)) {
- return node.parent;
- }
- return getClosestMatchingParent(node.parent, context, matcher);
- }
- function isCreateElementMatcher(node, context) {
- return (
- node
- && node.type === 'CallExpression'
- && isCreateElement(node, context)
- );
- }
- function isObjectExpressionMatcher(node) {
- return node && node.type === 'ObjectExpression';
- }
- function isJSXExpressionContainerMatcher(node) {
- return node && node.type === 'JSXExpressionContainer';
- }
- function isJSXAttributeOfExpressionContainerMatcher(node) {
- return (
- node
- && node.type === 'JSXAttribute'
- && node.value
- && node.value.type === 'JSXExpressionContainer'
- );
- }
- function isPropertyOfObjectExpressionMatcher(node) {
- return (
- node
- && node.parent
- && node.parent.type === 'Property'
- );
- }
- function isCallExpressionMatcher(node) {
- return node && node.type === 'CallExpression';
- }
- function isMapCall(node) {
- return (
- node
- && node.callee
- && node.callee.property
- && node.callee.property.name === 'map'
- );
- }
- function isReturnStatementOfHook(node, context) {
- if (
- !node
- || !node.parent
- || node.parent.type !== 'ReturnStatement'
- ) {
- return false;
- }
- const callExpression = getClosestMatchingParent(node, context, isCallExpressionMatcher);
- return (
- callExpression
- && callExpression.callee
- && HOOK_REGEXP.test(callExpression.callee.name)
- );
- }
- function isComponentInRenderProp(node, context) {
- if (
- node
- && node.parent
- && node.parent.type === 'Property'
- && node.parent.key
- && startsWithRender(node.parent.key.name)
- ) {
- return true;
- }
-
- if (
- node
- && node.parent
- && node.parent.type === 'JSXExpressionContainer'
- && node.parent.parent
- && node.parent.parent.type === 'JSXElement'
- ) {
- return true;
- }
- const jsxExpressionContainer = getClosestMatchingParent(node, context, isJSXExpressionContainerMatcher);
-
- if (
- jsxExpressionContainer
- && jsxExpressionContainer.parent
- && jsxExpressionContainer.parent.type === 'JSXAttribute'
- && jsxExpressionContainer.parent.name
- && jsxExpressionContainer.parent.name.type === 'JSXIdentifier'
- ) {
- const propName = jsxExpressionContainer.parent.name.name;
-
- if (startsWithRender(propName)) {
- return true;
- }
-
- if (propName === 'children') {
- return true;
- }
- }
- return false;
- }
- function isDirectValueOfRenderProperty(node) {
- return (
- node
- && node.parent
- && node.parent.type === 'Property'
- && node.parent.key
- && node.parent.key.type === 'Identifier'
- && startsWithRender(node.parent.key.name)
- );
- }
- function resolveComponentName(node) {
- const parentName = node.id && node.id.name;
- if (parentName) return parentName;
- return (
- node.type === 'ArrowFunctionExpression'
- && node.parent
- && node.parent.id
- && node.parent.id.name
- );
- }
- module.exports = {
- meta: {
- docs: {
- description: 'Disallow creating unstable components inside components',
- category: 'Possible Errors',
- recommended: false,
- url: docsUrl('no-unstable-nested-components'),
- },
- schema: [{
- type: 'object',
- properties: {
- customValidators: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- allowAsProps: {
- type: 'boolean',
- },
- },
- additionalProperties: false,
- }],
- },
- create: Components.detect((context, components, utils) => {
- const allowAsProps = context.options.some((option) => option && option.allowAsProps);
-
- function isInsideRenderMethod(node) {
- const parentComponent = utils.getParentComponent();
- if (!parentComponent || parentComponent.type !== 'ClassDeclaration') {
- return false;
- }
- return (
- node
- && node.parent
- && node.parent.type === 'MethodDefinition'
- && node.parent.key
- && node.parent.key.name === 'render'
- );
- }
-
- function isFunctionComponentInsideClassComponent(node) {
- const parentComponent = utils.getParentComponent();
- const parentStatelessComponent = utils.getParentStatelessComponent();
- return (
- parentComponent
- && parentStatelessComponent
- && parentComponent.type === 'ClassDeclaration'
- && utils.getStatelessComponent(parentStatelessComponent)
- && utils.isReturningJSX(node)
- );
- }
-
- function isComponentInsideCreateElementsProp(node) {
- if (!components.get(node)) {
- return false;
- }
- const createElementParent = getClosestMatchingParent(node, context, isCreateElementMatcher);
- return (
- createElementParent
- && createElementParent.arguments
- && createElementParent.arguments[1] === getClosestMatchingParent(node, context, isObjectExpressionMatcher)
- );
- }
-
- function isComponentInProp(node) {
- if (isPropertyOfObjectExpressionMatcher(node)) {
- return utils.isReturningJSX(node);
- }
- const jsxAttribute = getClosestMatchingParent(node, context, isJSXAttributeOfExpressionContainerMatcher);
- if (!jsxAttribute) {
- return isComponentInsideCreateElementsProp(node);
- }
- return utils.isReturningJSX(node);
- }
-
- function isStatelessComponentReturningNull(node) {
- const component = utils.getStatelessComponent(node);
- return component && !utils.isReturningJSX(component);
- }
-
- function validate(node) {
- if (!node || !node.parent) {
- return;
- }
- const isDeclaredInsideProps = isComponentInProp(node);
- if (
- !components.get(node)
- && !isFunctionComponentInsideClassComponent(node)
- && !isDeclaredInsideProps) {
- return;
- }
- if (
-
- (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context)))
-
- || isMapCall(node)
- || isMapCall(node.parent)
-
- || isReturnStatementOfHook(node, context)
-
- || isDirectValueOfRenderProperty(node)
-
- || isInsideRenderMethod(node)
-
- || isStatelessComponentReturningNull(node)
- ) {
- return;
- }
-
- const parentComponent = getClosestMatchingParent(
- node,
- context,
- (nodeToMatch) => components.get(nodeToMatch)
- );
- if (parentComponent) {
- const parentName = resolveComponentName(parentComponent);
-
-
- if (parentName && parentName[0] === parentName[0].toLowerCase()) {
- return;
- }
- let message = generateErrorMessageWithParentName(parentName);
-
- if (isDeclaredInsideProps && !allowAsProps) {
- message += COMPONENT_AS_PROPS_INFO;
- }
- report(context, message, null, {
- node,
- });
- }
- }
-
-
-
- return {
- FunctionDeclaration(node) { validate(node); },
- ArrowFunctionExpression(node) { validate(node); },
- FunctionExpression(node) { validate(node); },
- ClassDeclaration(node) { validate(node); },
- CallExpression(node) { validate(node); },
- };
- }),
- };
|