UNPKG

3.2 kBJavaScriptView Raw
1const {docsUrl} = require('../utilities');
2
3const MAX_RETURN_ELEMENTS = 2;
4
5module.exports = {
6 meta: {
7 docs: {
8 description: 'Restrict the number of returned items from React hooks.',
9 category: 'Best Practices',
10 recommended: true,
11 uri: docsUrl('react-hooks-strict-return'),
12 },
13 messages: {
14 hooksStrictReturn:
15 'React hooks must return a tuple of two or fewer values or a single object.',
16 },
17 },
18
19 create(context) {
20 let inHook = 0;
21
22 return {
23 VariableDeclarator(node) {
24 if (!isHook(node)) {
25 return;
26 }
27
28 inHook++;
29 },
30 'VariableDeclarator:exit': function(node) {
31 if (!isHook(node)) {
32 return;
33 }
34
35 inHook--;
36 },
37 FunctionDeclaration(node) {
38 if (!isHook(node)) {
39 return;
40 }
41
42 inHook++;
43 },
44 'FunctionDeclaration:exit': function(node) {
45 if (!isHook(node)) {
46 return;
47 }
48
49 inHook--;
50 },
51 ReturnStatement(node) {
52 if (inHook === 0) {
53 return;
54 }
55 if (
56 !exceedsMaxReturnProperties(
57 node,
58 context.getScope(),
59 MAX_RETURN_ELEMENTS,
60 )
61 ) {
62 return;
63 }
64
65 context.report({
66 messageId: 'hooksStrictReturn',
67 node,
68 });
69 },
70 };
71 },
72};
73
74function exceedsMaxReturnProperties(node, scope, max) {
75 const {argument} = node;
76
77 if (argument === null) {
78 return false;
79 }
80
81 const {type, elements} = argument;
82
83 if (type !== 'ArrayExpression') {
84 return getProps(node, scope).length > max;
85 }
86
87 return (
88 elements &&
89 elements.reduce((acc, val) => {
90 const property = isSpreadElement(val) ? getProps(val, scope) : val;
91 return [...acc, ...guranteeArray(property)];
92 }, []).length > max
93 );
94}
95
96function getProps(node, scope) {
97 const {references} = getVariableByName(scope, node.argument.name) || {};
98
99 const properties =
100 references &&
101 references.reduce((acc, ref) => {
102 if (
103 ref.identifier &&
104 ref.identifier.parent &&
105 ref.identifier.parent.init &&
106 ref.identifier.parent.init.elements
107 ) {
108 return [...acc, ...ref.identifier.parent.init.elements];
109 }
110 return acc;
111 }, []);
112
113 return properties ? flatten(properties) : [];
114}
115
116function isHook(node) {
117 return /^use[A-Z0-9].*$/.test(node.id.name);
118}
119
120function isSpreadElement(node) {
121 if (!node) {
122 return false;
123 }
124 return (
125 node.type === 'SpreadElement' || node.type === 'ExperimentalSpreadProperty'
126 );
127}
128
129function getVariableByName(initialScope, name) {
130 let scope = initialScope;
131
132 while (scope) {
133 const variable = scope.set.get(name);
134
135 if (variable) {
136 return variable;
137 }
138
139 scope = scope.upper;
140 }
141
142 return null;
143}
144
145function flatten(arr) {
146 if (!Array.isArray(arr)) {
147 return arr;
148 }
149 return arr.reduce(function(flat, toFlatten) {
150 return flat.concat(
151 Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten,
152 );
153 }, []);
154}
155
156function guranteeArray(maybeArray) {
157 return Array.isArray(maybeArray) ? maybeArray : [maybeArray];
158}