Skip to content

Commit d0ea6df

Browse files
authored
Merge pull request Expensify#150 from callstack-internal/VickyStash/disallow-empty-array-object-default-inside-useOnyx
Add rule to disallow inline empty object/array defaults inside the useOnyx hook
2 parents 9b8816c + b4f5759 commit d0ea6df

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @typedef {import('eslint').Rule.RuleModule} RuleModule
3+
*/
4+
5+
/** @type {RuleModule} */
6+
module.exports = {
7+
name: 'no-unstable-useOnyx-defaults',
8+
meta: {
9+
type: 'problem',
10+
docs: {
11+
description: 'Disallow inline array or object literals as default values in useOnyx. Use getEmptyArray(), getEmptyObject(), a constant, or a memoized value instead.',
12+
recommended: 'error',
13+
},
14+
schema: [],
15+
messages: {
16+
noEmptyArrayDefault: 'Avoid using empty array [] as default value in useOnyx. Use getEmptyArray<T>() instead.',
17+
noInlineArrayDefault: 'Avoid using array literal as default value in useOnyx. Use a memoized value or a constant instead.',
18+
noEmptyObjectDefault: 'Avoid using empty object {} as default value in useOnyx. Use getEmptyObject<T>() instead.',
19+
noInlineObjectDefault: 'Avoid using object literal as default value in useOnyx. Use a memoized value or a constant instead.',
20+
noUnstableIdentifierDefault: 'Default value must be memoized with useMemo or declared as a top-level constant.',
21+
},
22+
},
23+
create(context) {
24+
return {
25+
VariableDeclarator(node) {
26+
if (
27+
!node.init
28+
|| node.init.type !== 'CallExpression'
29+
|| node.init.callee.name !== 'useOnyx'
30+
) {
31+
return;
32+
}
33+
34+
// Check if the variable declaration is an array pattern (destructuring)
35+
if (node.id.type !== 'ArrayPattern') {
36+
return;
37+
}
38+
39+
node.id.elements.forEach((element) => {
40+
if (!element || element.type !== 'AssignmentPattern') {
41+
return;
42+
}
43+
44+
const defaultValue = element.right;
45+
46+
// Check for inline array default
47+
if (defaultValue.type === 'ArrayExpression') {
48+
const isEmpty = defaultValue.elements.length === 0;
49+
context.report({
50+
node: defaultValue,
51+
messageId: isEmpty ? 'noEmptyArrayDefault' : 'noInlineArrayDefault',
52+
});
53+
}
54+
55+
// Check for inline object default
56+
if (defaultValue.type === 'ObjectExpression') {
57+
const isEmpty = defaultValue.properties.length === 0;
58+
context.report({
59+
node: defaultValue,
60+
messageId: isEmpty ? 'noEmptyObjectDefault' : 'noInlineObjectDefault',
61+
});
62+
}
63+
64+
// If it's an Identifier, verify it's memoized or top-level const
65+
if (defaultValue.type === 'Identifier') {
66+
const scope = context.getScope();
67+
const variable = scope.set.get(defaultValue.name);
68+
69+
if (
70+
variable
71+
&& variable.defs.length > 0
72+
&& variable.defs[0].node.type === 'VariableDeclarator'
73+
) {
74+
const def = variable.defs[0].node;
75+
const init = def.init;
76+
77+
const isMemoized = init
78+
&& init.type === 'CallExpression'
79+
&& init.callee.type === 'Identifier'
80+
&& init.callee.name === 'useMemo';
81+
82+
const isTopLevelConst = variable.scope.type === 'module'
83+
&& variable.defs[0].parent.kind === 'const';
84+
85+
if (!isMemoized && !isTopLevelConst) {
86+
context.report({
87+
node: defaultValue,
88+
messageId: 'noUnstableIdentifierDefault',
89+
});
90+
}
91+
}
92+
}
93+
});
94+
},
95+
};
96+
},
97+
};
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const RuleTester = require('eslint').RuleTester;
2+
const rule = require('../no-unstable-useOnyx-defaults');
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: 'module',
8+
},
9+
});
10+
11+
ruleTester.run('no-unstable-useOnyx-defaults', rule, {
12+
valid: [
13+
{
14+
code: 'const [data = getEmptyObject()] = useOnyx(ONYXKEYS.DATA);',
15+
},
16+
{
17+
code: 'const [data = getEmptyArray()] = useOnyx(ONYXKEYS.DATA);',
18+
},
19+
{
20+
code: 'const [data] = useOnyx(ONYXKEYS.DATA);',
21+
},
22+
{
23+
code: 'const [data = null] = useOnyx(ONYXKEYS.DATA);',
24+
},
25+
{
26+
code: 'const [data = undefined] = useOnyx(ONYXKEYS.DATA);',
27+
},
28+
{
29+
code: 'const [data = false] = useOnyx(ONYXKEYS.DATA);',
30+
},
31+
{
32+
code: 'const [data = ""] = useOnyx(ONYXKEYS.DATA);',
33+
},
34+
{
35+
code: 'const [data = 0] = useOnyx(ONYXKEYS.DATA);',
36+
},
37+
{
38+
code: 'const data = useOnyx(ONYXKEYS.DATA);',
39+
},
40+
{
41+
code: `
42+
const CONST_VALUE = {key: "value"};
43+
function Component() {
44+
const [data = CONST_VALUE] = useOnyx(ONYXKEYS.DATA);
45+
}
46+
`,
47+
},
48+
{
49+
code: `
50+
function Component() {
51+
const memoizedValue = useMemo(() => ({key: "value"}), []);
52+
const [data = memoizedValue] = useOnyx(ONYXKEYS.DATA);
53+
}
54+
`,
55+
},
56+
],
57+
invalid: [
58+
{
59+
code: 'const [data = {}] = useOnyx(ONYXKEYS.DATA);',
60+
errors: [{
61+
messageId: 'noEmptyObjectDefault',
62+
}],
63+
},
64+
{
65+
code: 'const [data = []] = useOnyx(ONYXKEYS.DATA);',
66+
errors: [{
67+
messageId: 'noEmptyArrayDefault',
68+
}],
69+
},
70+
{
71+
code: 'const [data = {key: "value"}] = useOnyx(ONYXKEYS.DATA);',
72+
errors: [{
73+
messageId: 'noInlineObjectDefault',
74+
}],
75+
},
76+
{
77+
code: 'const [data = ["item"]] = useOnyx(ONYXKEYS.DATA);',
78+
errors: [{
79+
messageId: 'noInlineArrayDefault',
80+
}],
81+
},
82+
{
83+
code: `
84+
function Component() {
85+
const notMemoizedValue = {key: "value"};
86+
const [data = notMemoizedValue] = useOnyx(ONYXKEYS.DATA);
87+
}
88+
`,
89+
errors: [{messageId: 'noUnstableIdentifierDefault'}],
90+
},
91+
],
92+
});

rules/expensify.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = {
3131
}],
3232
'rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth': 'warn',
3333
'rulesdir/no-use-state-initializer-functions': 'error',
34+
'rulesdir/no-unstable-useOnyx-defaults': 'error',
3435
},
3536
};
3637

0 commit comments

Comments
 (0)