From 495a4cf85e6e810802e9de1b5f6efe730cb485c7 Mon Sep 17 00:00:00 2001 From: Johnny Zabala Date: Mon, 13 Jul 2020 20:53:24 -0400 Subject: [PATCH] [New] component detection: add componentWrapperFunctions setting Closes #2268. Co-authored-by: Johnny Zabala Co-authored-by: Landon Schropp --- CHANGELOG.md | 5 +++ README.md | 7 +++++ lib/util/Components.js | 39 ++++++++++++++++++----- tests/lib/rules/prop-types.js | 59 +++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a610df6244..8a24f0341d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Added +* component detection: add componentWrapperFunctions setting ([#2713][] @@jzabala @LandonSchropp) + +[#2713]: https://github.com/yannickcr/eslint-plugin-react/pull/2713 + ## [7.23.2] - 2021.04.08 ### Fixed diff --git a/README.md b/README.md index 503fc9e64d..ae2522d6d6 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,13 @@ You should also specify settings that will be shared across all the plugin rules {"property": "freeze", "object": "Object"}, {"property": "myFavoriteWrapper"} ], + "componentWrapperFunctions": [ + // The name 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"}, + {"property": "observer", "object": ""}, // sets `object` to whatever value `settings.react.pragma` is set to + ], "linkComponents": [ // Components used as alternatives to for linking, eg. "Hyperlink", diff --git a/lib/util/Components.js b/lib/util/Components.js index ff65e367aa..4cbd62e750 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -212,11 +212,28 @@ class Components { } } +function getWrapperFunctions(context, pragma) { + const componentWrapperFunctions = context.settings.componentWrapperFunctions || []; + + // eslint-disable-next-line arrow-body-style + return componentWrapperFunctions.map((wrapperFunction) => { + return typeof wrapperFunction === 'string' + ? {property: wrapperFunction} + : Object.assign({}, wrapperFunction, { + object: wrapperFunction.object === '' ? pragma : wrapperFunction.object + }); + }).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 = { @@ -597,14 +614,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 8c80feb15d..2475d85257 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -2515,6 +2515,26 @@ ruleTester.run('prop-types', rule, { } ` }, + { + code: ` + const SideMenu = styled( + ({ componentId }) => ( + + + + + + + ), + ); + SideMenu.propTypes = { + componentId: PropTypes.string.isRequired + } + `, + settings: { + componentWrapperFunctions: [{property: 'styled'}] + } + }, parsers.TS([ { code: ` @@ -6037,6 +6057,45 @@ ruleTester.run('prop-types', rule, { data: {name: 'name'} }] }, + { + 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' + }] + }, parsers.TS([ { code: `