diff --git a/README.md b/README.md index 3a06ef705d..cac2b31e14 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ You should also specify settings that will be shared across all the plugin rules {"property": "freeze", "object": "Object"}, {"property": "myFavoriteWrapper"} ], + "componentWrapperFunctions": [ + // The names of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped. + "observer", // `property` + {"property": "styled"} // `object` is optional + {"property": "observer", "object": "Mobx"}, + ], "linkComponents": [ // Components used as alternatives to for linking, eg. "Hyperlink", diff --git a/lib/util/Components.js b/lib/util/Components.js index deb26dda56..ad821059c4 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -213,11 +213,27 @@ class Components { } } +function getWrapperFunctions(context, pragma) { + const componentWrapperFunctions = context.settings.componentWrapperFunctions || []; + + return componentWrapperFunctions.map((wrapperFunction) => { + wrapperFunction = typeof wrapperFunction === 'string' ? {property: wrapperFunction} : Object.assign({}, wrapperFunction); + if (wrapperFunction.object === '') { + wrapperFunction.object = pragma; + } + return wrapperFunction; + }).concat([ + {property: 'forwardRef', object: pragma}, + {property: 'memo', object: pragma} + ]); +} + function componentRule(rule, context) { const createClass = pragmaUtil.getCreateClassFromContext(context); const pragma = pragmaUtil.getFromContext(context); const sourceCode = context.getSourceCode(); const components = new Components(); + const wrapperFunctions = getWrapperFunctions(context, pragma); // Utilities for component detection const utils = { @@ -594,14 +610,20 @@ function componentRule(rule, context) { if (!node || node.type !== 'CallExpression') { return false; } - const propertyNames = ['forwardRef', 'memo']; - const calleeObject = node.callee.object; - if (calleeObject && node.callee.property) { - return arrayIncludes(propertyNames, node.callee.property.name) - && calleeObject.name === pragma - && !this.nodeWrapsComponent(node); - } - return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name); + + return wrapperFunctions.some((wrapperFunction) => { + if (node.callee.type === 'MemberExpression') { + return wrapperFunction.object + && wrapperFunction.object === node.callee.object.name + && wrapperFunction.property === node.callee.property.name + && !this.nodeWrapsComponent(node); + } + return wrapperFunction.property === node.callee.name + && (!wrapperFunction.object + // Functions coming from the current pragma need special handling + || (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node.callee.name)) + ); + }); }, /** diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 0089d1c812..2be5086597 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -2569,6 +2569,26 @@ ruleTester.run('prop-types', rule, { return null; }`, parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + const SideMenu = styled( + ({ componentId }) => ( + + + + + + + ), + ); + SideMenu.propTypes = { + componentId: PropTypes.string.isRequired + } + `, + settings: { + componentWrapperFunctions: [{property: 'styled'}] + } } ], @@ -5161,6 +5181,45 @@ ruleTester.run('prop-types', rule, { errors: [{ message: '\'value\' is missing in props validation' }] + }, + { + code: ` + const SideMenu = observer( + ({ componentId }) => ( + + + + + + + ), + );`, + settings: { + componentWrapperFunctions: ['observer'] + }, + errors: [{ + message: '\'componentId\' is missing in props validation' + }] + }, + { + code: ` + const SideMenu = Mobx.observer( + ({ id }) => ( + + + + + + + ), + ); + `, + settings: { + componentWrapperFunctions: [{property: 'observer', object: 'Mobx'}] + }, + errors: [{ + message: '\'id\' is missing in props validation' + }] } ] });