Skip to content

Commit 3ddce46

Browse files
jenil94ljharb
authored andcommitted
[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
1 parent 1aab93d commit 3ddce46

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

lib/util/Components.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const doctrine = require('doctrine');
99
const arrayIncludes = require('array-includes');
10+
const values = require('object.values');
1011

1112
const variableUtil = require('./variable');
1213
const pragmaUtil = require('./pragma');
@@ -437,14 +438,85 @@ function componentRule(rule, context) {
437438
return prevNode;
438439
},
439440

441+
getComponentNameFromJSXElement(node) {
442+
if (node.type !== 'JSXElement') {
443+
return null;
444+
}
445+
if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
446+
return node.openingElement.name.name;
447+
}
448+
return null;
449+
},
450+
451+
/**
452+
*
453+
* @param {object} node
454+
* Getting the first JSX element's name.
455+
*/
456+
getNameOfWrappedComponent(node) {
457+
if (node.length < 1) {
458+
return null;
459+
}
460+
const body = node[0].body;
461+
if (!body) {
462+
return null;
463+
}
464+
if (body.type === 'JSXElement') {
465+
return this.getComponentNameFromJSXElement(body);
466+
}
467+
if (body.type === 'BlockStatement') {
468+
const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
469+
return jsxElement && this.getComponentNameFromJSXElement(jsxElement.argument);
470+
}
471+
return null;
472+
},
473+
474+
/**
475+
* Get the list of names of components created till now
476+
*/
477+
getDetectedComponents() {
478+
const list = components.list();
479+
return values(list).filter((val) => {
480+
if (val.node.type === 'ClassDeclaration') {
481+
return true;
482+
}
483+
if (
484+
val.node.type === 'ArrowFunctionExpression' &&
485+
val.node.parent &&
486+
val.node.parent.type === 'VariableDeclarator' &&
487+
val.node.parent.id
488+
) {
489+
return true;
490+
}
491+
return false;
492+
}).map((val) => {
493+
if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
494+
return val.node.id.name;
495+
});
496+
},
497+
498+
/**
499+
*
500+
* @param {object} node
501+
* It will check wheater memo/forwardRef is wrapping existing component or
502+
* creating a new one.
503+
*/
504+
nodeWrapsComponent(node) {
505+
const childComponent = this.getNameOfWrappedComponent(node.arguments);
506+
const componentList = this.getDetectedComponents();
507+
return childComponent && arrayIncludes(componentList, childComponent);
508+
},
509+
440510
isPragmaComponentWrapper(node) {
441511
if (!node || node.type !== 'CallExpression') {
442512
return false;
443513
}
444514
const propertyNames = ['forwardRef', 'memo'];
445515
const calleeObject = node.callee.object;
446516
if (calleeObject && node.callee.property) {
447-
return arrayIncludes(propertyNames, node.callee.property.name) && calleeObject.name === pragma;
517+
return arrayIncludes(propertyNames, node.callee.property.name) &&
518+
calleeObject.name === pragma &&
519+
!this.nodeWrapsComponent(node);
448520
}
449521
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
450522
},

tests/lib/rules/no-multi-comp.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,111 @@ ruleTester.run('no-multi-comp', rule, {
116116
options: [{
117117
ignoreStateless: true
118118
}]
119+
}, {
120+
code: `
121+
class StoreListItem extends React.PureComponent {
122+
// A bunch of stuff here
123+
}
124+
export default React.forwardRef((props, ref) => <StoreListItem {...props} forwardRef={ref} />);
125+
`,
126+
options: [{
127+
ignoreStateless: false
128+
}]
129+
}, {
130+
code: `
131+
class StoreListItem extends React.PureComponent {
132+
// A bunch of stuff here
133+
}
134+
export default React.forwardRef((props, ref) => {
135+
return <StoreListItem {...props} forwardRef={ref} />
136+
});
137+
`,
138+
options: [{
139+
ignoreStateless: false
140+
}]
141+
}, {
142+
code: `
143+
const HelloComponent = (props) => {
144+
return <div></div>;
145+
}
146+
export default React.forwardRef((props, ref) => <HelloComponent {...props} forwardRef={ref} />);
147+
`,
148+
options: [{
149+
ignoreStateless: false
150+
}]
151+
}, {
152+
code: `
153+
class StoreListItem extends React.PureComponent {
154+
// A bunch of stuff here
155+
}
156+
export default React.forwardRef(
157+
function myFunction(props, ref) {
158+
return <StoreListItem {...props} forwardedRef={ref} />;
159+
}
160+
);
161+
`,
162+
options: [{
163+
ignoreStateless: false
164+
}]
165+
}, {
166+
code: `
167+
class StoreListItem extends React.PureComponent {
168+
// A bunch of stuff here
169+
}
170+
export default React.forwardRef((props, ref) => <StoreListItem {...props} forwardRef={ref} />);
171+
`,
172+
options: [{
173+
ignoreStateless: true
174+
}]
175+
}, {
176+
code: `
177+
class StoreListItem extends React.PureComponent {
178+
// A bunch of stuff here
179+
}
180+
export default React.forwardRef((props, ref) => {
181+
return <StoreListItem {...props} forwardRef={ref} />
182+
});
183+
`,
184+
options: [{
185+
ignoreStateless: true
186+
}]
187+
}, {
188+
code: `
189+
const HelloComponent = (props) => {
190+
return <div></div>;
191+
}
192+
export default React.forwardRef((props, ref) => <HelloComponent {...props} forwardRef={ref} />);
193+
`,
194+
options: [{
195+
ignoreStateless: true
196+
}]
197+
}, {
198+
code: `
199+
const HelloComponent = (props) => {
200+
return <div></div>;
201+
}
202+
class StoreListItem extends React.PureComponent {
203+
// A bunch of stuff here
204+
}
205+
export default React.forwardRef(
206+
function myFunction(props, ref) {
207+
return <StoreListItem {...props} forwardedRef={ref} />;
208+
}
209+
);
210+
`,
211+
options: [{
212+
ignoreStateless: true
213+
}]
214+
}, {
215+
code: `
216+
const HelloComponent = (props) => {
217+
return <div></div>;
218+
}
219+
export default React.memo((props, ref) => <HelloComponent {...props} />);
220+
`,
221+
options: [{
222+
ignoreStateless: false
223+
}]
119224
}],
120225

121226
invalid: [{
@@ -207,5 +312,50 @@ ruleTester.run('no-multi-comp', rule, {
207312
message: 'Declare only one React component per file',
208313
line: 6
209314
}]
315+
}, {
316+
code: `
317+
class StoreListItem extends React.PureComponent {
318+
// A bunch of stuff here
319+
}
320+
export default React.forwardRef((props, ref) => <div><StoreListItem {...props} forwardRef={ref} /></div>);
321+
`,
322+
options: [{
323+
ignoreStateless: false
324+
}],
325+
parser: parsers.BABEL_ESLINT,
326+
errors: [{
327+
message: 'Declare only one React component per file',
328+
line: 5
329+
}]
330+
}, {
331+
code: `
332+
const HelloComponent = (props) => {
333+
return <div></div>;
334+
}
335+
const HelloComponent2 = React.forwardRef((props, ref) => <div></div>);
336+
`,
337+
options: [{
338+
ignoreStateless: false
339+
}],
340+
parser: parsers.BABEL_ESLINT,
341+
errors: [{
342+
message: 'Declare only one React component per file',
343+
line: 5
344+
}]
345+
}, {
346+
code: `
347+
const HelloComponent = (0, (props) => {
348+
return <div></div>;
349+
});
350+
const HelloComponent2 = React.forwardRef((props, ref) => <><HelloComponent></HelloComponent></>);
351+
`,
352+
options: [{
353+
ignoreStateless: false
354+
}],
355+
parser: parsers.BABEL_ESLINT,
356+
errors: [{
357+
message: 'Declare only one React component per file',
358+
line: 5
359+
}]
210360
}]
211361
});

0 commit comments

Comments
 (0)