Skip to content

Commit eef61d2

Browse files
authored
Merge pull request #138 from rezkiy37/feature/no-use-state-initializer-functions
no-use-state-initializer-functions rule
2 parents c8533ab + 9420237 commit eef61d2

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

eslint-plugin-expensify/CONST.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ module.exports = {
3434
PREFER_TYPE_FEST_VALUE_OF: 'Prefer using `ValueOf` from `type-fest` to extract the type of the properties of an object.',
3535
PREFER_AT: 'Prefer using the `.at()` method for array element access.',
3636
PREFER_SHOULD_USE_NARROW_LAYOUT_INSTEAD_OF_IS_SMALL_SCREEN_WIDTH: 'Prefer using `shouldUseNarrowLayout` instead of `isSmallScreenWidth` from `useResponsiveLayout`.',
37+
NO_USE_STATE_INITIALIZER_CALL_FUNCTION:
38+
'Avoid calling a function directly in the useState initializer. Use an initializer function instead (a callback).',
3739
},
3840
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const message = require('./CONST').MESSAGE.NO_USE_STATE_INITIALIZER_CALL_FUNCTION;
2+
3+
module.exports = {
4+
meta: {
5+
type: 'problem', // This is a potential performance issue
6+
docs: {
7+
description:
8+
'Disallow direct function calls in useState initializer',
9+
category: 'Best Practices',
10+
recommended: false,
11+
},
12+
schema: [], // No options for this rule
13+
messages: {
14+
noDirectCall: message,
15+
},
16+
},
17+
create(context) {
18+
return {
19+
CallExpression(node) {
20+
// Return early if the function being called is not `useState`
21+
if (
22+
node.callee.type !== 'Identifier'
23+
|| node.callee.name !== 'useState'
24+
|| node.arguments.length === 0
25+
) {
26+
return;
27+
}
28+
29+
const firstArg = node.arguments[0];
30+
31+
// Return early if the first argument is not a function call, member expression with a function call, or conditional expression with function calls
32+
if (
33+
firstArg.type !== 'CallExpression'
34+
&& !(firstArg.type === 'MemberExpression' && firstArg.object.type === 'CallExpression')
35+
&& !(firstArg.type === 'ConditionalExpression'
36+
&& (firstArg.consequent.type === 'CallExpression' || firstArg.alternate.type === 'CallExpression'))
37+
) {
38+
return;
39+
}
40+
41+
context.report({
42+
node: firstArg,
43+
messageId: 'noDirectCall',
44+
});
45+
},
46+
47+
// Handle cases where the initializer is passed as a function reference
48+
VariableDeclarator(node) {
49+
if (
50+
!node.init
51+
|| node.init.type !== 'CallExpression'
52+
|| node.init.callee.name !== 'useState'
53+
|| node.init.arguments.length === 0
54+
) {
55+
return;
56+
}
57+
58+
const firstArg = node.init.arguments[0];
59+
60+
// Return early if the first argument is a function reference (valid case)
61+
if (
62+
firstArg.type === 'Identifier' // e.g., `getChatTabBrickRoad`
63+
|| (firstArg.type === 'ArrowFunctionExpression'
64+
&& firstArg.body.type !== 'CallExpression') // e.g., `() => getChatTabBrickRoad`
65+
) {
66+
return; // Valid case, do nothing
67+
}
68+
69+
// If it's a direct function call, member expression with a function call, or conditional expression with function calls, report it
70+
if (
71+
firstArg.type === 'CallExpression'
72+
|| (firstArg.type === 'MemberExpression' && firstArg.object.type === 'CallExpression')
73+
|| (firstArg.type === 'ConditionalExpression'
74+
&& (firstArg.consequent.type === 'CallExpression' || firstArg.alternate.type === 'CallExpression'))
75+
) {
76+
context.report({
77+
node: firstArg,
78+
messageId: 'noDirectCall',
79+
});
80+
}
81+
},
82+
};
83+
},
84+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const RuleTester = require('eslint').RuleTester;
2+
const rule = require('../no-use-state-initializer-functions');
3+
const message = require('../CONST').MESSAGE.NO_USE_STATE_INITIALIZER_CALL_FUNCTION;
4+
5+
const ruleTester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 6,
8+
sourceType: 'module',
9+
},
10+
});
11+
12+
ruleTester.run('no-use-state-initializer-functions', rule, {
13+
valid: [
14+
{
15+
// Calling a callback should be valid
16+
code: `
17+
useState(() => testFunc());
18+
`,
19+
},
20+
{
21+
// Calling a callback should be valid
22+
code: `
23+
useState(() => testFunc().value);
24+
`,
25+
},
26+
{
27+
// Calling a callback should be valid
28+
code: `
29+
useState(condition ? testFunc : testFunc);
30+
`,
31+
},
32+
{
33+
// Calling a callback should be valid
34+
code: `
35+
useState(condition ? () => testFunc() : () => testFunc());
36+
`,
37+
},
38+
],
39+
invalid: [
40+
{
41+
// Calling a function should be invalid
42+
code: `
43+
useState(testFunc());
44+
`,
45+
errors: [
46+
{
47+
message,
48+
},
49+
],
50+
},
51+
{
52+
// Calling a function should be invalid
53+
code: `
54+
useState(testFunc().value);
55+
`,
56+
errors: [
57+
{
58+
message,
59+
},
60+
],
61+
},
62+
{
63+
// Calling a function should be invalid
64+
code: `
65+
useState(condition ? testFunc() : testFunc());
66+
`,
67+
errors: [
68+
{
69+
message,
70+
},
71+
],
72+
},
73+
{
74+
// Calling a function should be invalid
75+
code: `
76+
useState(condition ? (() => testFunc())() : (() => testFunc())());
77+
`,
78+
errors: [
79+
{
80+
message,
81+
},
82+
],
83+
},
84+
],
85+
});

rules/expensify.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
}],
3030
}],
3131
'rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth': 'warn',
32+
'rulesdir/no-use-state-initializer-functions': 'error',
3233
},
3334
};
3435

0 commit comments

Comments
 (0)