1 | const {docsUrl} = require('../utilities');
|
2 |
|
3 | const MAX_RETURN_ELEMENTS = 2;
|
4 |
|
5 | module.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 |
|
74 | function 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 |
|
96 | function 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 |
|
116 | function isHook(node) {
|
117 | return /^use[A-Z0-9].*$/.test(node.id.name);
|
118 | }
|
119 |
|
120 | function isSpreadElement(node) {
|
121 | if (!node) {
|
122 | return false;
|
123 | }
|
124 | return (
|
125 | node.type === 'SpreadElement' || node.type === 'ExperimentalSpreadProperty'
|
126 | );
|
127 | }
|
128 |
|
129 | function 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 |
|
145 | function 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 |
|
156 | function guranteeArray(maybeArray) {
|
157 | return Array.isArray(maybeArray) ? maybeArray : [maybeArray];
|
158 | }
|