Skip to content

Commit b5e82ea

Browse files
committed
[Fix]: only detect functions that return JSX somewhere as components
1 parent ee4bad3 commit b5e82ea

File tree

3 files changed

+157
-40
lines changed

3 files changed

+157
-40
lines changed

lib/util/Components.js

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -419,33 +419,23 @@ function componentRule(rule, context) {
419419
);
420420
},
421421

422-
/**
423-
* Check if the node is returning null
424-
*
425-
* @param {ASTNode} ASTnode The AST node being checked
426-
* @returns {Boolean} True if the node is returning null, false if not
427-
*/
428-
isReturningNull(ASTnode) {
429-
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
430-
const property = nodeAndProperty.property;
431-
const node = nodeAndProperty.node;
432-
433-
if (!node) {
434-
return false;
435-
}
436-
437-
return node[property] && node[property].value === null;
438-
},
439-
440422
/**
441423
* Check if the node is returning JSX or null
442424
*
443425
* @param {ASTNode} ASTNode The AST node being checked
444426
* @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
445427
* @returns {Boolean} True if the node is returning JSX or null, false if not
446428
*/
447-
isReturningJSXOrNull(ASTNode, strict) {
448-
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
429+
isReturningJSXSomewhere(ASTNode, strict) {
430+
let returnStatements = [];
431+
// Arrow functions without block statements don't have return statements.
432+
if (ASTNode.type === 'ArrowFunctionExpression' && ASTNode.body.type !== 'BlockStatement') {
433+
returnStatements.push(ASTNode);
434+
} else {
435+
returnStatements = astUtil.findAllReturnStatements(ASTNode.body);
436+
}
437+
438+
return returnStatements.some((node) => utils.isReturningJSX(node, strict));
449439
},
450440

451441
getPragmaComponentWrapper(node) {
@@ -613,7 +603,8 @@ function componentRule(rule, context) {
613603
case 'AssignmentExpression':
614604
case 'Property':
615605
case 'ReturnStatement':
616-
case 'ExportDefaultDeclaration': {
606+
case 'ExportDefaultDeclaration':
607+
case 'ArrowFunctionExpression': {
617608
return true;
618609
}
619610
case 'SequenceExpression': {
@@ -635,19 +626,19 @@ function componentRule(rule, context) {
635626
if (
636627
node.type === 'FunctionDeclaration'
637628
&& isFirstLetterCapitalized(node.id.name)
638-
&& utils.isReturningJSXOrNull(node)
629+
&& utils.isReturningJSXSomewhere(node)
639630
) {
640631
return node;
641632
}
642633

643634
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
644-
if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
635+
if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXSomewhere(node)) {
645636
if (isFirstLetterCapitalized(node.parent.id.name)) {
646637
return node;
647638
}
648639
return undefined;
649640
}
650-
if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) {
641+
if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXSomewhere(node)) {
651642
return node;
652643
}
653644

lib/util/ast.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,41 @@ function unwrapTSAsExpression(node) {
201201
return node;
202202
}
203203

204+
function findAllReturnStatements(ASTNode) {
205+
const returnStatements = [];
206+
function findReturnStatements(node) {
207+
if (node) {
208+
if (Array.isArray(node)) {
209+
node.forEach((n) => findReturnStatements(n));
210+
} else {
211+
// eslint-disable-next-line default-case
212+
switch (node.type) {
213+
case 'ReturnStatement':
214+
returnStatements.push(node);
215+
break;
216+
case 'IfStatement':
217+
findReturnStatements(node.consequent);
218+
findReturnStatements(node.alternate);
219+
break;
220+
case 'SwitchStatement':
221+
findReturnStatements(node.cases);
222+
break;
223+
case 'SwitchCase':
224+
findReturnStatements(node.consequent);
225+
break;
226+
case 'BlockStatement':
227+
findReturnStatements(node.body);
228+
break;
229+
}
230+
}
231+
}
232+
}
233+
findReturnStatements(ASTNode);
234+
return returnStatements;
235+
}
236+
204237
module.exports = {
238+
findAllReturnStatements,
205239
findReturnStatement,
206240
getFirstNodeInLine,
207241
getPropertyName,

tests/lib/rules/no-this-in-sfc.js

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,42 @@ ruleTester.run('no-this-in-sfc', rule, {
151151
};
152152
};`,
153153
parser: parsers.BABEL_ESLINT
154+
}, {
155+
code: `
156+
export const prepareLogin = new ValidatedMethod({
157+
name: "user.prepare",
158+
validate: new SimpleSchema({
159+
}).validator(),
160+
run({ remember }) {
161+
if (Meteor.isServer) {
162+
const connectionId = this.connection.id;
163+
return Methods.prepareLogin(connectionId, remember);
164+
}
165+
return null;
166+
},
167+
});
168+
`
169+
}, {
170+
// functions returning null are not necessarily components.
171+
code: `
172+
class Foo {
173+
bar() {
174+
function Bar(){
175+
return () => {
176+
this.something();
177+
return null;
178+
}
179+
}
180+
}
181+
}`
182+
}, {
183+
code: `
184+
function Foo(props) {
185+
if (this.props.foo) {
186+
something();
187+
}
188+
return null;
189+
}`
154190
}],
155191
invalid: [{
156192
code: `
@@ -193,15 +229,6 @@ ruleTester.run('no-this-in-sfc', rule, {
193229
return null;
194230
}`,
195231
errors: [{message: ERROR_MESSAGE}]
196-
}, {
197-
code: `
198-
function Foo(props) {
199-
if (this.props.foo) {
200-
something();
201-
}
202-
return null;
203-
}`,
204-
errors: [{message: ERROR_MESSAGE}]
205232
}, {
206233
code: 'const Foo = (props) => <span>{this.props.foo}</span>',
207234
errors: [{message: ERROR_MESSAGE}]
@@ -219,16 +246,81 @@ ruleTester.run('no-this-in-sfc', rule, {
219246
errors: [{message: ERROR_MESSAGE}, {message: ERROR_MESSAGE}]
220247
}, {
221248
code: `
222-
class Foo {
223-
bar() {
224-
function Bar(){
225-
return () => {
226-
this.something();
227-
return null;
249+
export default class {
250+
renderFooter = () => {
251+
return () => (
252+
<div>{this.value}</div>
253+
);
254+
}
255+
}
256+
`,
257+
errors: [{message: ERROR_MESSAGE}],
258+
parser: parsers.BABEL_ESLINT
259+
}, {
260+
code: `
261+
export default class {
262+
renderFooter = () => () => (
263+
<div>{this.value}</div>
264+
);
265+
}
266+
`,
267+
errors: [{message: ERROR_MESSAGE}],
268+
parser: parsers.BABEL_ESLINT
269+
}, {
270+
code: `
271+
class Foo {
272+
bar() {
273+
function Bar(){
274+
return () => {
275+
this.something();
276+
return null || <div />;
277+
}
228278
}
229279
}
230280
}
231-
}`,
281+
`,
282+
errors: [{message: ERROR_MESSAGE}]
283+
}, {
284+
code: `
285+
class Foo {
286+
bar() {
287+
function Bar(){
288+
return () => {
289+
if (this.something()) {
290+
return <div />
291+
}
292+
return null;
293+
}
294+
}
295+
}
296+
}
297+
`,
232298
errors: [{message: ERROR_MESSAGE}]
299+
}, {
300+
code: `
301+
export default class {
302+
renderFooter = () => () => this.value ? (
303+
<div>{this.value}</div>
304+
) : null;
305+
}
306+
`,
307+
errors: [
308+
{message: ERROR_MESSAGE, line: 3},
309+
{message: ERROR_MESSAGE, line: 4}
310+
],
311+
parser: parsers.BABEL_ESLINT
312+
}, {
313+
code: `
314+
export default class {
315+
renderFooter = () => () => this.value && (
316+
<div>{this.value}</div>
317+
);
318+
}
319+
`,
320+
errors: [
321+
{message: ERROR_MESSAGE, line: 3},
322+
{message: ERROR_MESSAGE, line: 4}
323+
],
324+
parser: parsers.BABEL_ESLINT
233325
}]
234326
});

0 commit comments

Comments
 (0)