Skip to content

Commit 495a4cf

Browse files
jzabalaLandonSchropp
authored andcommitted
[New] component detection: add componentWrapperFunctions setting
Closes #2268. Co-authored-by: Johnny Zabala <jzabala.s@gmail.com> Co-authored-by: Landon Schropp <schroppl@gmail.com>
1 parent 0d999ef commit 495a4cf

File tree

4 files changed

+102
-8
lines changed

4 files changed

+102
-8
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
55

66
## Unreleased
77

8+
### Added
9+
* component detection: add componentWrapperFunctions setting ([#2713][] @@jzabala @LandonSchropp)
10+
11+
[#2713]: https://github.com/yannickcr/eslint-plugin-react/pull/2713
12+
813
## [7.23.2] - 2021.04.08
914

1015
### Fixed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ You should also specify settings that will be shared across all the plugin rules
5252
{"property": "freeze", "object": "Object"},
5353
{"property": "myFavoriteWrapper"}
5454
],
55+
"componentWrapperFunctions": [
56+
// 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.
57+
"observer", // `property`
58+
{"property": "styled"} // `object` is optional
59+
{"property": "observer", "object": "Mobx"},
60+
{"property": "observer", "object": "<pragma>"}, // sets `object` to whatever value `settings.react.pragma` is set to
61+
],
5562
"linkComponents": [
5663
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
5764
"Hyperlink",

lib/util/Components.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,28 @@ class Components {
212212
}
213213
}
214214

215+
function getWrapperFunctions(context, pragma) {
216+
const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
217+
218+
// eslint-disable-next-line arrow-body-style
219+
return componentWrapperFunctions.map((wrapperFunction) => {
220+
return typeof wrapperFunction === 'string'
221+
? {property: wrapperFunction}
222+
: Object.assign({}, wrapperFunction, {
223+
object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object
224+
});
225+
}).concat([
226+
{property: 'forwardRef', object: pragma},
227+
{property: 'memo', object: pragma}
228+
]);
229+
}
230+
215231
function componentRule(rule, context) {
216232
const createClass = pragmaUtil.getCreateClassFromContext(context);
217233
const pragma = pragmaUtil.getFromContext(context);
218234
const sourceCode = context.getSourceCode();
219235
const components = new Components();
236+
const wrapperFunctions = getWrapperFunctions(context, pragma);
220237

221238
// Utilities for component detection
222239
const utils = {
@@ -597,14 +614,20 @@ function componentRule(rule, context) {
597614
if (!node || node.type !== 'CallExpression') {
598615
return false;
599616
}
600-
const propertyNames = ['forwardRef', 'memo'];
601-
const calleeObject = node.callee.object;
602-
if (calleeObject && node.callee.property) {
603-
return arrayIncludes(propertyNames, node.callee.property.name)
604-
&& calleeObject.name === pragma
605-
&& !this.nodeWrapsComponent(node);
606-
}
607-
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
617+
618+
return wrapperFunctions.some((wrapperFunction) => {
619+
if (node.callee.type === 'MemberExpression') {
620+
return wrapperFunction.object
621+
&& wrapperFunction.object === node.callee.object.name
622+
&& wrapperFunction.property === node.callee.property.name
623+
&& !this.nodeWrapsComponent(node);
624+
}
625+
return wrapperFunction.property === node.callee.name
626+
&& (!wrapperFunction.object
627+
// Functions coming from the current pragma need special handling
628+
|| (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node.callee.name))
629+
);
630+
});
608631
},
609632

610633
/**

tests/lib/rules/prop-types.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,6 +2515,26 @@ ruleTester.run('prop-types', rule, {
25152515
}
25162516
`
25172517
},
2518+
{
2519+
code: `
2520+
const SideMenu = styled(
2521+
({ componentId }) => (
2522+
<S.Container>
2523+
<S.Head />
2524+
<UserInfo />
2525+
<Separator />
2526+
<MenuList componentId={componentId} />
2527+
</S.Container>
2528+
),
2529+
);
2530+
SideMenu.propTypes = {
2531+
componentId: PropTypes.string.isRequired
2532+
}
2533+
`,
2534+
settings: {
2535+
componentWrapperFunctions: [{property: 'styled'}]
2536+
}
2537+
},
25182538
parsers.TS([
25192539
{
25202540
code: `
@@ -6037,6 +6057,45 @@ ruleTester.run('prop-types', rule, {
60376057
data: {name: 'name'}
60386058
}]
60396059
},
6060+
{
6061+
code: `
6062+
const SideMenu = observer(
6063+
({ componentId }) => (
6064+
<S.Container>
6065+
<S.Head />
6066+
<UserInfo />
6067+
<Separator />
6068+
<MenuList componentId={componentId} />
6069+
</S.Container>
6070+
),
6071+
);`,
6072+
settings: {
6073+
componentWrapperFunctions: ['observer']
6074+
},
6075+
errors: [{
6076+
message: '\'componentId\' is missing in props validation'
6077+
}]
6078+
},
6079+
{
6080+
code: `
6081+
const SideMenu = Mobx.observer(
6082+
({ id }) => (
6083+
<S.Container>
6084+
<S.Head />
6085+
<UserInfo />
6086+
<Separator />
6087+
<MenuList componentId={id} />
6088+
</S.Container>
6089+
),
6090+
);
6091+
`,
6092+
settings: {
6093+
componentWrapperFunctions: [{property: 'observer', object: 'Mobx'}]
6094+
},
6095+
errors: [{
6096+
message: '\'id\' is missing in props validation'
6097+
}]
6098+
},
60406099
parsers.TS([
60416100
{
60426101
code: `

0 commit comments

Comments
 (0)