Skip to content

Commit 7965c22

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 7965c22

File tree

2 files changed

+222
-1
lines changed

2 files changed

+222
-1
lines changed

lib/util/Components.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,14 +437,85 @@ function componentRule(rule, context) {
437437
return prevNode;
438438
},
439439

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

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)