Skip to content

Commit

Permalink
[New] component detection: add componentWrapperFunctions setting
Browse files Browse the repository at this point in the history
Closes #2268.

Co-authored-by: Johnny Zabala <jzabala.s@gmail.com>
Co-authored-by: Landon Schropp <schroppl@gmail.com>
  • Loading branch information
2 people authored and ljharb committed Jul 14, 2020
1 parent 0d999ef commit 495a4cf
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions README.md
Expand Up @@ -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": "<pragma>"}, // sets `object` to whatever value `settings.react.pragma` is set to
],
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
Expand Down
39 changes: 31 additions & 8 deletions lib/util/Components.js
Expand Up @@ -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>' ? 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 = {
Expand Down Expand Up @@ -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))
);
});
},

/**
Expand Down
59 changes: 59 additions & 0 deletions tests/lib/rules/prop-types.js
Expand Up @@ -2515,6 +2515,26 @@ ruleTester.run('prop-types', rule, {
}
`
},
{
code: `
const SideMenu = styled(
({ componentId }) => (
<S.Container>
<S.Head />
<UserInfo />
<Separator />
<MenuList componentId={componentId} />
</S.Container>
),
);
SideMenu.propTypes = {
componentId: PropTypes.string.isRequired
}
`,
settings: {
componentWrapperFunctions: [{property: 'styled'}]
}
},
parsers.TS([
{
code: `
Expand Down Expand Up @@ -6037,6 +6057,45 @@ ruleTester.run('prop-types', rule, {
data: {name: 'name'}
}]
},
{
code: `
const SideMenu = observer(
({ componentId }) => (
<S.Container>
<S.Head />
<UserInfo />
<Separator />
<MenuList componentId={componentId} />
</S.Container>
),
);`,
settings: {
componentWrapperFunctions: ['observer']
},
errors: [{
message: '\'componentId\' is missing in props validation'
}]
},
{
code: `
const SideMenu = Mobx.observer(
({ id }) => (
<S.Container>
<S.Head />
<UserInfo />
<Separator />
<MenuList componentId={id} />
</S.Container>
),
);
`,
settings: {
componentWrapperFunctions: [{property: 'observer', object: 'Mobx'}]
},
errors: [{
message: '\'id\' is missing in props validation'
}]
},
parsers.TS([
{
code: `
Expand Down

0 comments on commit 495a4cf

Please sign in to comment.