From 7965c223088149ccfd592a454a1bb75bd25e6ced Mon Sep 17 00:00:00 2001 From: Jenil Date: Fri, 1 Mar 2019 18:16:42 +0530 Subject: [PATCH] [new] `no-multi-comp`: Added handling for `forwardRef` and `memo` wrapping components declared in the same file - getComponentNameFromJSXElement returns null when node.type is not JSXElement --- lib/util/Components.js | 73 ++++++++++++++- tests/lib/rules/no-multi-comp.js | 150 +++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) diff --git a/lib/util/Components.js b/lib/util/Components.js index 04d36be260..f0ef41df2b 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -437,6 +437,75 @@ 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 Object.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; @@ -444,7 +513,9 @@ function componentRule(rule, context) { 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); }, diff --git a/tests/lib/rules/no-multi-comp.js b/tests/lib/rules/no-multi-comp.js index 9728276c80..eafbbf554b 100644 --- a/tests/lib/rules/no-multi-comp.js +++ b/tests/lib/rules/no-multi-comp.js @@ -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) => ); + `, + options: [{ + ignoreStateless: false + }] + }, { + code: ` + class StoreListItem extends React.PureComponent { + // A bunch of stuff here + } + export default React.forwardRef((props, ref) => { + return + }); + `, + options: [{ + ignoreStateless: false + }] + }, { + code: ` + const HelloComponent = (props) => { + return
; + } + export default React.forwardRef((props, ref) => ); + `, + options: [{ + ignoreStateless: false + }] + }, { + code: ` + class StoreListItem extends React.PureComponent { + // A bunch of stuff here + } + export default React.forwardRef( + function myFunction(props, ref) { + return ; + } + ); + `, + options: [{ + ignoreStateless: false + }] + }, { + code: ` + class StoreListItem extends React.PureComponent { + // A bunch of stuff here + } + export default React.forwardRef((props, ref) => ); + `, + options: [{ + ignoreStateless: true + }] + }, { + code: ` + class StoreListItem extends React.PureComponent { + // A bunch of stuff here + } + export default React.forwardRef((props, ref) => { + return + }); + `, + options: [{ + ignoreStateless: true + }] + }, { + code: ` + const HelloComponent = (props) => { + return
; + } + export default React.forwardRef((props, ref) => ); + `, + options: [{ + ignoreStateless: true + }] + }, { + code: ` + const HelloComponent = (props) => { + return
; + } + class StoreListItem extends React.PureComponent { + // A bunch of stuff here + } + export default React.forwardRef( + function myFunction(props, ref) { + return ; + } + ); + `, + options: [{ + ignoreStateless: true + }] + }, { + code: ` + const HelloComponent = (props) => { + return
; + } + export default React.memo((props, ref) => ); + `, + options: [{ + ignoreStateless: false + }] }], invalid: [{ @@ -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) =>
); + `, + options: [{ + ignoreStateless: false + }], + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Declare only one React component per file', + line: 5 + }] + }, { + code: ` + const HelloComponent = (props) => { + return
; + } + const HelloComponent2 = React.forwardRef((props, ref) =>
); + `, + options: [{ + ignoreStateless: false + }], + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Declare only one React component per file', + line: 5 + }] + }, { + code: ` + const HelloComponent = (0, (props) => { + return
; + }); + const HelloComponent2 = React.forwardRef((props, ref) => <>); + `, + options: [{ + ignoreStateless: false + }], + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Declare only one React component per file', + line: 5 + }] }] });