Skip to content

Commit

Permalink
[new] no-multi-comp: Added handling for forwardRef and memo wra…
Browse files Browse the repository at this point in the history
…pping components declared in the same file

 - getComponentNameFromJSXElement returns null when node.type is not JSXElement
  • Loading branch information
jenil94 authored and ljharb committed Mar 1, 2019
1 parent 1aab93d commit 3ddce46
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 1 deletion.
74 changes: 73 additions & 1 deletion lib/util/Components.js
Expand Up @@ -7,6 +7,7 @@

const doctrine = require('doctrine');
const arrayIncludes = require('array-includes');
const values = require('object.values');

const variableUtil = require('./variable');
const pragmaUtil = require('./pragma');
Expand Down Expand Up @@ -437,14 +438,85 @@ function componentRule(rule, context) {
return prevNode;
},

getComponentNameFromJSXElement(node) {
if (node.type !== 'JSXElement') {
return null;
}
if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
return node.openingElement.name.name;
}
return null;
},

/**
*
* @param {object} node
* Getting the first JSX element's name.
*/
getNameOfWrappedComponent(node) {
if (node.length < 1) {
return null;
}
const body = node[0].body;
if (!body) {
return null;
}
if (body.type === 'JSXElement') {
return this.getComponentNameFromJSXElement(body);
}
if (body.type === 'BlockStatement') {
const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
return jsxElement && this.getComponentNameFromJSXElement(jsxElement.argument);
}
return null;
},

/**
* Get the list of names of components created till now
*/
getDetectedComponents() {
const list = components.list();
return values(list).filter((val) => {
if (val.node.type === 'ClassDeclaration') {
return true;
}
if (
val.node.type === 'ArrowFunctionExpression' &&
val.node.parent &&
val.node.parent.type === 'VariableDeclarator' &&
val.node.parent.id
) {
return true;
}
return false;
}).map((val) => {
if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
return val.node.id.name;
});
},

/**
*
* @param {object} node
* It will check wheater memo/forwardRef is wrapping existing component or
* creating a new one.
*/
nodeWrapsComponent(node) {
const childComponent = this.getNameOfWrappedComponent(node.arguments);
const componentList = this.getDetectedComponents();
return childComponent && arrayIncludes(componentList, childComponent);
},

isPragmaComponentWrapper(node) {
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;
return arrayIncludes(propertyNames, node.callee.property.name) &&
calleeObject.name === pragma &&
!this.nodeWrapsComponent(node);
}
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
},
Expand Down
150 changes: 150 additions & 0 deletions tests/lib/rules/no-multi-comp.js
Expand Up @@ -116,6 +116,111 @@ ruleTester.run('no-multi-comp', rule, {
options: [{
ignoreStateless: true
}]
}, {
code: `
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef((props, ref) => <StoreListItem {...props} forwardRef={ref} />);
`,
options: [{
ignoreStateless: false
}]
}, {
code: `
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef((props, ref) => {
return <StoreListItem {...props} forwardRef={ref} />
});
`,
options: [{
ignoreStateless: false
}]
}, {
code: `
const HelloComponent = (props) => {
return <div></div>;
}
export default React.forwardRef((props, ref) => <HelloComponent {...props} forwardRef={ref} />);
`,
options: [{
ignoreStateless: false
}]
}, {
code: `
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef(
function myFunction(props, ref) {
return <StoreListItem {...props} forwardedRef={ref} />;
}
);
`,
options: [{
ignoreStateless: false
}]
}, {
code: `
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef((props, ref) => <StoreListItem {...props} forwardRef={ref} />);
`,
options: [{
ignoreStateless: true
}]
}, {
code: `
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef((props, ref) => {
return <StoreListItem {...props} forwardRef={ref} />
});
`,
options: [{
ignoreStateless: true
}]
}, {
code: `
const HelloComponent = (props) => {
return <div></div>;
}
export default React.forwardRef((props, ref) => <HelloComponent {...props} forwardRef={ref} />);
`,
options: [{
ignoreStateless: true
}]
}, {
code: `
const HelloComponent = (props) => {
return <div></div>;
}
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef(
function myFunction(props, ref) {
return <StoreListItem {...props} forwardedRef={ref} />;
}
);
`,
options: [{
ignoreStateless: true
}]
}, {
code: `
const HelloComponent = (props) => {
return <div></div>;
}
export default React.memo((props, ref) => <HelloComponent {...props} />);
`,
options: [{
ignoreStateless: false
}]
}],

invalid: [{
Expand Down Expand Up @@ -207,5 +312,50 @@ ruleTester.run('no-multi-comp', rule, {
message: 'Declare only one React component per file',
line: 6
}]
}, {
code: `
class StoreListItem extends React.PureComponent {
// A bunch of stuff here
}
export default React.forwardRef((props, ref) => <div><StoreListItem {...props} forwardRef={ref} /></div>);
`,
options: [{
ignoreStateless: false
}],
parser: parsers.BABEL_ESLINT,
errors: [{
message: 'Declare only one React component per file',
line: 5
}]
}, {
code: `
const HelloComponent = (props) => {
return <div></div>;
}
const HelloComponent2 = React.forwardRef((props, ref) => <div></div>);
`,
options: [{
ignoreStateless: false
}],
parser: parsers.BABEL_ESLINT,
errors: [{
message: 'Declare only one React component per file',
line: 5
}]
}, {
code: `
const HelloComponent = (0, (props) => {
return <div></div>;
});
const HelloComponent2 = React.forwardRef((props, ref) => <><HelloComponent></HelloComponent></>);
`,
options: [{
ignoreStateless: false
}],
parser: parsers.BABEL_ESLINT,
errors: [{
message: 'Declare only one React component per file',
line: 5
}]
}]
});

0 comments on commit 3ddce46

Please sign in to comment.