eslint-plugin-react-hooks 2.4.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -34,6 +34,17 @@ Then add it to your ESLint configuration:
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Or use the recommended config:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
{
|
|
41
|
+
"extends": [
|
|
42
|
+
// ...
|
|
43
|
+
"plugin:react-hooks/recommended"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
37
48
|
## Valid and Invalid Examples
|
|
38
49
|
|
|
39
50
|
Please refer to the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation and the [Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#what-exactly-do-the-lint-rules-enforce) to learn more about this rule.
|
|
@@ -16,7 +16,6 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
|
19
|
-
|
|
20
19
|
/**
|
|
21
20
|
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
|
22
21
|
* character to exclude identifiers like "user".
|
|
@@ -35,10 +34,9 @@ function isHook(node) {
|
|
|
35
34
|
if (node.type === 'Identifier') {
|
|
36
35
|
return isHookName(node.name);
|
|
37
36
|
} else if (node.type === 'MemberExpression' && !node.computed && isHook(node.property)) {
|
|
38
|
-
// Only consider React.useFoo() to be namespace hooks for now to avoid false positives.
|
|
39
|
-
// We can expand this check later.
|
|
40
37
|
var obj = node.object;
|
|
41
|
-
|
|
38
|
+
var isPascalCaseNameSpace = /^[A-Z].*/;
|
|
39
|
+
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
|
|
42
40
|
} else {
|
|
43
41
|
return false;
|
|
44
42
|
}
|
|
@@ -100,7 +98,16 @@ function isInsideComponentOrHook(node) {
|
|
|
100
98
|
return false;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
var
|
|
101
|
+
var RulesOfHooks = {
|
|
102
|
+
meta: {
|
|
103
|
+
type: 'problem',
|
|
104
|
+
docs: {
|
|
105
|
+
description: 'enforces the Rules of Hooks',
|
|
106
|
+
category: 'Possible Errors',
|
|
107
|
+
recommended: true,
|
|
108
|
+
url: 'https://reactjs.org/docs/hooks-rules.html'
|
|
109
|
+
}
|
|
110
|
+
},
|
|
104
111
|
create: function (context) {
|
|
105
112
|
var codePathReactHooksMapStack = [];
|
|
106
113
|
var codePathSegmentStack = [];
|
|
@@ -151,51 +158,48 @@ var RuleOfHooks = {
|
|
|
151
158
|
* Populates `cyclic` with cyclic segments.
|
|
152
159
|
*/
|
|
153
160
|
|
|
154
|
-
function countPathsFromStart(segment) {
|
|
161
|
+
function countPathsFromStart(segment, pathHistory) {
|
|
155
162
|
var cache = countPathsFromStart.cache;
|
|
156
|
-
var paths = cache.get(segment.id);
|
|
157
|
-
//
|
|
163
|
+
var paths = cache.get(segment.id);
|
|
164
|
+
var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
|
|
165
|
+
// We need to fill `cyclic` with all segments inside cycle
|
|
158
166
|
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
var _didIteratorError = false;
|
|
166
|
-
var _iteratorError = undefined;
|
|
167
|
+
if (pathList.has(segment.id)) {
|
|
168
|
+
var pathArray = [].concat(pathList);
|
|
169
|
+
var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
|
|
170
|
+
var _iteratorNormalCompletion = true;
|
|
171
|
+
var _didIteratorError = false;
|
|
172
|
+
var _iteratorError = undefined;
|
|
167
173
|
|
|
174
|
+
try {
|
|
175
|
+
for (var _iterator = cyclicSegments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
176
|
+
var cyclicSegment = _step.value;
|
|
177
|
+
cyclic.add(cyclicSegment);
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
_didIteratorError = true;
|
|
181
|
+
_iteratorError = err;
|
|
182
|
+
} finally {
|
|
168
183
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
countPathsFromStart(prevSegment);
|
|
184
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
185
|
+
_iterator.return();
|
|
172
186
|
}
|
|
173
|
-
} catch (err) {
|
|
174
|
-
_didIteratorError = true;
|
|
175
|
-
_iteratorError = err;
|
|
176
187
|
} finally {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
_iterator.return();
|
|
180
|
-
}
|
|
181
|
-
} finally {
|
|
182
|
-
if (_didIteratorError) {
|
|
183
|
-
throw _iteratorError;
|
|
184
|
-
}
|
|
188
|
+
if (_didIteratorError) {
|
|
189
|
+
throw _iteratorError;
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
|
-
|
|
188
|
-
return 0;
|
|
189
192
|
}
|
|
190
|
-
} // We have a cached `paths`. Return it.
|
|
191
193
|
|
|
194
|
+
return 0;
|
|
195
|
+
} // add the current segment to pathList
|
|
192
196
|
|
|
193
|
-
if (paths !== undefined) {
|
|
194
|
-
return paths;
|
|
195
|
-
} // Compute `paths` and cache it. Guarding against cycles.
|
|
196
197
|
|
|
198
|
+
pathList.add(segment.id); // We have a cached `paths`. Return it.
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
if (paths !== undefined) {
|
|
201
|
+
return paths;
|
|
202
|
+
}
|
|
199
203
|
|
|
200
204
|
if (codePath.thrownSegments.includes(segment)) {
|
|
201
205
|
paths = 0;
|
|
@@ -209,8 +213,8 @@ var RuleOfHooks = {
|
|
|
209
213
|
|
|
210
214
|
try {
|
|
211
215
|
for (var _iterator2 = segment.prevSegments[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
|
|
212
|
-
var
|
|
213
|
-
paths += countPathsFromStart(
|
|
216
|
+
var prevSegment = _step2.value;
|
|
217
|
+
paths += countPathsFromStart(prevSegment, pathList);
|
|
214
218
|
}
|
|
215
219
|
} catch (err) {
|
|
216
220
|
_didIteratorError2 = true;
|
|
@@ -261,51 +265,48 @@ var RuleOfHooks = {
|
|
|
261
265
|
*/
|
|
262
266
|
|
|
263
267
|
|
|
264
|
-
function countPathsToEnd(segment) {
|
|
268
|
+
function countPathsToEnd(segment, pathHistory) {
|
|
265
269
|
var cache = countPathsToEnd.cache;
|
|
266
|
-
var paths = cache.get(segment.id);
|
|
267
|
-
//
|
|
270
|
+
var paths = cache.get(segment.id);
|
|
271
|
+
var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
|
|
272
|
+
// We need to fill `cyclic` with all segments inside cycle
|
|
268
273
|
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
var _didIteratorError3 = false;
|
|
276
|
-
var _iteratorError3 = undefined;
|
|
274
|
+
if (pathList.has(segment.id)) {
|
|
275
|
+
var pathArray = Array.from(pathList);
|
|
276
|
+
var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
|
|
277
|
+
var _iteratorNormalCompletion3 = true;
|
|
278
|
+
var _didIteratorError3 = false;
|
|
279
|
+
var _iteratorError3 = undefined;
|
|
277
280
|
|
|
281
|
+
try {
|
|
282
|
+
for (var _iterator3 = cyclicSegments[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
|
|
283
|
+
var cyclicSegment = _step3.value;
|
|
284
|
+
cyclic.add(cyclicSegment);
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
_didIteratorError3 = true;
|
|
288
|
+
_iteratorError3 = err;
|
|
289
|
+
} finally {
|
|
278
290
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
countPathsToEnd(nextSegment);
|
|
291
|
+
if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
|
|
292
|
+
_iterator3.return();
|
|
282
293
|
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
_didIteratorError3 = true;
|
|
285
|
-
_iteratorError3 = err;
|
|
286
294
|
} finally {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
_iterator3.return();
|
|
290
|
-
}
|
|
291
|
-
} finally {
|
|
292
|
-
if (_didIteratorError3) {
|
|
293
|
-
throw _iteratorError3;
|
|
294
|
-
}
|
|
295
|
+
if (_didIteratorError3) {
|
|
296
|
+
throw _iteratorError3;
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
|
-
|
|
298
|
-
return 0;
|
|
299
299
|
}
|
|
300
|
-
} // We have a cached `paths`. Return it.
|
|
301
300
|
|
|
301
|
+
return 0;
|
|
302
|
+
} // add the current segment to pathList
|
|
302
303
|
|
|
303
|
-
if (paths !== undefined) {
|
|
304
|
-
return paths;
|
|
305
|
-
} // Compute `paths` and cache it. Guarding against cycles.
|
|
306
304
|
|
|
305
|
+
pathList.add(segment.id); // We have a cached `paths`. Return it.
|
|
307
306
|
|
|
308
|
-
|
|
307
|
+
if (paths !== undefined) {
|
|
308
|
+
return paths;
|
|
309
|
+
}
|
|
309
310
|
|
|
310
311
|
if (codePath.thrownSegments.includes(segment)) {
|
|
311
312
|
paths = 0;
|
|
@@ -319,8 +320,8 @@ var RuleOfHooks = {
|
|
|
319
320
|
|
|
320
321
|
try {
|
|
321
322
|
for (var _iterator4 = segment.nextSegments[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
|
|
322
|
-
var
|
|
323
|
-
paths += countPathsToEnd(
|
|
323
|
+
var nextSegment = _step4.value;
|
|
324
|
+
paths += countPathsToEnd(nextSegment, pathList);
|
|
324
325
|
}
|
|
325
326
|
} catch (err) {
|
|
326
327
|
_didIteratorError4 = true;
|
|
@@ -570,26 +571,29 @@ var RuleOfHooks = {
|
|
|
570
571
|
message: message
|
|
571
572
|
});
|
|
572
573
|
}
|
|
573
|
-
} else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) {
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
574
|
+
} else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) {
|
|
575
|
+
// Custom message for hooks inside a class
|
|
576
|
+
var _message = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'in a class component. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
|
|
577
|
+
|
|
578
|
+
context.report({
|
|
579
|
+
node: hook,
|
|
580
|
+
message: _message
|
|
581
|
+
});
|
|
578
582
|
} else if (codePathFunctionName) {
|
|
579
583
|
// Custom message if we found an invalid function name.
|
|
580
|
-
var
|
|
584
|
+
var _message2 = "React Hook \"" + context.getSource(hook) + "\" is called in " + ("function \"" + context.getSource(codePathFunctionName) + "\" ") + 'that is neither a React function component nor a custom ' + 'React Hook function.';
|
|
581
585
|
|
|
582
586
|
context.report({
|
|
583
587
|
node: hook,
|
|
584
|
-
message:
|
|
588
|
+
message: _message2
|
|
585
589
|
});
|
|
586
590
|
} else if (codePathNode.type === 'Program') {
|
|
587
591
|
// These are dangerous if you have inline requires enabled.
|
|
588
|
-
var
|
|
592
|
+
var _message3 = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'at the top level. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
|
|
589
593
|
|
|
590
594
|
context.report({
|
|
591
595
|
node: hook,
|
|
592
|
-
message:
|
|
596
|
+
message: _message3
|
|
593
597
|
});
|
|
594
598
|
} else {
|
|
595
599
|
// Assume in all other cases the user called a hook in some
|
|
@@ -598,11 +602,11 @@ var RuleOfHooks = {
|
|
|
598
602
|
// enough in the common case that the incorrect message in
|
|
599
603
|
// uncommon cases doesn't matter.
|
|
600
604
|
if (isSomewhereInsideComponentOrHook) {
|
|
601
|
-
var
|
|
605
|
+
var _message4 = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
|
|
602
606
|
|
|
603
607
|
context.report({
|
|
604
608
|
node: hook,
|
|
605
|
-
message:
|
|
609
|
+
message: _message4
|
|
606
610
|
});
|
|
607
611
|
}
|
|
608
612
|
}
|
|
@@ -717,15 +721,26 @@ function last(array) {
|
|
|
717
721
|
|
|
718
722
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
|
719
723
|
|
|
720
|
-
|
|
721
724
|
var ExhaustiveDeps = {
|
|
722
725
|
meta: {
|
|
726
|
+
type: 'suggestion',
|
|
727
|
+
docs: {
|
|
728
|
+
description: 'verifies the list of dependencies for Hooks like useEffect and similar',
|
|
729
|
+
category: 'Best Practices',
|
|
730
|
+
recommended: true,
|
|
731
|
+
url: 'https://github.com/facebook/react/issues/14920'
|
|
732
|
+
},
|
|
733
|
+
fixable: 'code',
|
|
723
734
|
schema: [{
|
|
724
735
|
type: 'object',
|
|
725
736
|
additionalProperties: false,
|
|
737
|
+
enableDangerousAutofixThisMayCauseInfiniteLoops: false,
|
|
726
738
|
properties: {
|
|
727
739
|
additionalHooks: {
|
|
728
740
|
type: 'string'
|
|
741
|
+
},
|
|
742
|
+
enableDangerousAutofixThisMayCauseInfiniteLoops: {
|
|
743
|
+
type: 'boolean'
|
|
729
744
|
}
|
|
730
745
|
}
|
|
731
746
|
}]
|
|
@@ -733,9 +748,25 @@ var ExhaustiveDeps = {
|
|
|
733
748
|
create: function (context) {
|
|
734
749
|
// Parse the `additionalHooks` regex.
|
|
735
750
|
var additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined;
|
|
751
|
+
var enableDangerousAutofixThisMayCauseInfiniteLoops = context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops || false;
|
|
736
752
|
var options = {
|
|
737
|
-
additionalHooks: additionalHooks
|
|
738
|
-
|
|
753
|
+
additionalHooks: additionalHooks,
|
|
754
|
+
enableDangerousAutofixThisMayCauseInfiniteLoops: enableDangerousAutofixThisMayCauseInfiniteLoops
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
function reportProblem(problem) {
|
|
758
|
+
if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
|
|
759
|
+
// Used to enable legacy behavior. Dangerous.
|
|
760
|
+
// Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
|
|
761
|
+
if (Array.isArray(problem.suggest) && problem.suggest.length > 0) {
|
|
762
|
+
problem.fix = problem.suggest[0].fix;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
context.report(problem);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
var scopeManager = context.getSourceCode().scopeManager; // Should be shared between visitors.
|
|
739
770
|
|
|
740
771
|
var setStateCallSites = new WeakMap();
|
|
741
772
|
var stateVariables = new WeakSet();
|
|
@@ -757,41 +788,31 @@ var ExhaustiveDeps = {
|
|
|
757
788
|
}
|
|
758
789
|
|
|
759
790
|
return {
|
|
760
|
-
|
|
761
|
-
ArrowFunctionExpression: visitFunctionExpression
|
|
791
|
+
CallExpression: visitCallExpression
|
|
762
792
|
};
|
|
763
|
-
/**
|
|
764
|
-
* Visitor for both function expressions and arrow function expressions.
|
|
765
|
-
*/
|
|
766
793
|
|
|
767
|
-
function
|
|
768
|
-
|
|
769
|
-
if (node.type !== 'FunctionExpression' && node.type !== 'ArrowFunctionExpression' || node.parent.type !== 'CallExpression') {
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
794
|
+
function visitCallExpression(node) {
|
|
795
|
+
var callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
|
|
772
796
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (node.parent.arguments[callbackIndex] !== node) {
|
|
797
|
+
if (callbackIndex === -1) {
|
|
798
|
+
// Not a React Hook call that needs deps.
|
|
776
799
|
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
800
|
+
}
|
|
779
801
|
|
|
780
|
-
var
|
|
802
|
+
var callback = node.arguments[callbackIndex];
|
|
803
|
+
var reactiveHook = node.callee;
|
|
781
804
|
var reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
|
|
782
|
-
var
|
|
805
|
+
var declaredDependenciesNode = node.arguments[callbackIndex + 1];
|
|
806
|
+
var isEffect = /Effect($|[^a-z])/g.test(reactiveHookName); // Check the declared dependencies for this reactive hook. If there is no
|
|
783
807
|
// second argument then the reactive callback will re-run on every render.
|
|
784
808
|
// So no need to check for dependency inclusion.
|
|
785
809
|
|
|
786
|
-
var depsIndex = callbackIndex + 1;
|
|
787
|
-
var declaredDependenciesNode = node.parent.arguments[depsIndex];
|
|
788
|
-
|
|
789
810
|
if (!declaredDependenciesNode && !isEffect) {
|
|
790
811
|
// These are only used for optimization.
|
|
791
812
|
if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') {
|
|
792
813
|
// TODO: Can this have a suggestion?
|
|
793
|
-
|
|
794
|
-
node:
|
|
814
|
+
reportProblem({
|
|
815
|
+
node: reactiveHook,
|
|
795
816
|
message: "React Hook " + reactiveHookName + " does nothing when called with " + "only one argument. Did you forget to pass an array of " + "dependencies?"
|
|
796
817
|
});
|
|
797
818
|
}
|
|
@@ -799,15 +820,120 @@ var ExhaustiveDeps = {
|
|
|
799
820
|
return;
|
|
800
821
|
}
|
|
801
822
|
|
|
823
|
+
switch (callback.type) {
|
|
824
|
+
case 'FunctionExpression':
|
|
825
|
+
case 'ArrowFunctionExpression':
|
|
826
|
+
visitFunctionWithDependencies(callback, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
|
|
827
|
+
return;
|
|
828
|
+
// Handled
|
|
829
|
+
|
|
830
|
+
case 'Identifier':
|
|
831
|
+
if (!declaredDependenciesNode) {
|
|
832
|
+
// No deps, no problems.
|
|
833
|
+
return; // Handled
|
|
834
|
+
} // The function passed as a callback is not written inline.
|
|
835
|
+
// But perhaps it's in the dependencies array?
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
if (declaredDependenciesNode.elements && declaredDependenciesNode.elements.some(function (el) {
|
|
839
|
+
return el.type === 'Identifier' && el.name === callback.name;
|
|
840
|
+
})) {
|
|
841
|
+
// If it's already in the list of deps, we don't care because
|
|
842
|
+
// this is valid regardless.
|
|
843
|
+
return; // Handled
|
|
844
|
+
} // We'll do our best effort to find it, complain otherwise.
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
var variable = context.getScope().set.get(callback.name);
|
|
848
|
+
|
|
849
|
+
if (variable == null || variable.defs == null) {
|
|
850
|
+
// If it's not in scope, we don't care.
|
|
851
|
+
return; // Handled
|
|
852
|
+
} // The function passed as a callback is not written inline.
|
|
853
|
+
// But it's defined somewhere in the render scope.
|
|
854
|
+
// We'll do our best effort to find and check it, complain otherwise.
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
var def = variable.defs[0];
|
|
858
|
+
|
|
859
|
+
if (!def || !def.node) {
|
|
860
|
+
break; // Unhandled
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (def.type !== 'Variable' && def.type !== 'FunctionName') {
|
|
864
|
+
// Parameter or an unusual pattern. Bail out.
|
|
865
|
+
break; // Unhandled
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
switch (def.node.type) {
|
|
869
|
+
case 'FunctionDeclaration':
|
|
870
|
+
// useEffect(() => { ... }, []);
|
|
871
|
+
visitFunctionWithDependencies(def.node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
|
|
872
|
+
return;
|
|
873
|
+
// Handled
|
|
874
|
+
|
|
875
|
+
case 'VariableDeclarator':
|
|
876
|
+
var init = def.node.init;
|
|
877
|
+
|
|
878
|
+
if (!init) {
|
|
879
|
+
break; // Unhandled
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
switch (init.type) {
|
|
883
|
+
// const effectBody = () => {...};
|
|
884
|
+
// useEffect(effectBody, []);
|
|
885
|
+
case 'ArrowFunctionExpression':
|
|
886
|
+
case 'FunctionExpression':
|
|
887
|
+
// We can inspect this function as if it were inline.
|
|
888
|
+
visitFunctionWithDependencies(init, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
|
|
889
|
+
return;
|
|
890
|
+
// Handled
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
break;
|
|
894
|
+
// Unhandled
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
break;
|
|
898
|
+
// Unhandled
|
|
899
|
+
|
|
900
|
+
default:
|
|
901
|
+
// useEffect(generateEffectBody(), []);
|
|
902
|
+
reportProblem({
|
|
903
|
+
node: reactiveHook,
|
|
904
|
+
message: "React Hook " + reactiveHookName + " received a function whose dependencies " + "are unknown. Pass an inline function instead."
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
// Handled
|
|
908
|
+
} // Something unusual. Fall back to suggesting to add the body itself as a dep.
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
reportProblem({
|
|
912
|
+
node: reactiveHook,
|
|
913
|
+
message: "React Hook " + reactiveHookName + " has a missing dependency: '" + callback.name + "'. " + "Either include it or remove the dependency array.",
|
|
914
|
+
suggest: [{
|
|
915
|
+
desc: "Update the dependencies array to be: [" + callback.name + "]",
|
|
916
|
+
fix: function (fixer) {
|
|
917
|
+
return fixer.replaceText(declaredDependenciesNode, "[" + callback.name + "]");
|
|
918
|
+
}
|
|
919
|
+
}]
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Visitor for both function expressions and arrow function expressions.
|
|
924
|
+
*/
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) {
|
|
802
928
|
if (isEffect && node.async) {
|
|
803
|
-
|
|
929
|
+
reportProblem({
|
|
804
930
|
node: node,
|
|
805
931
|
message: "Effect callbacks are synchronous to prevent race conditions. " + "Put the async function inside:\n\n" + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching'
|
|
806
932
|
});
|
|
807
933
|
} // Get the current scope.
|
|
808
934
|
|
|
809
935
|
|
|
810
|
-
var scope =
|
|
936
|
+
var scope = scopeManager.acquire(node); // Find all our "pure scopes". On every re-render of a component these
|
|
811
937
|
// pure scopes may have changes to the variables declared within. So all
|
|
812
938
|
// variables used in our reactive hook callback but declared in a pure
|
|
813
939
|
// scope need to be listed as dependencies of our reactive hook callback.
|
|
@@ -870,6 +996,10 @@ var ExhaustiveDeps = {
|
|
|
870
996
|
|
|
871
997
|
if (init == null) {
|
|
872
998
|
return false;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
while (init.type === 'TSAsExpression') {
|
|
1002
|
+
init = init.expression;
|
|
873
1003
|
} // Detect primitive constants
|
|
874
1004
|
// const foo = 42
|
|
875
1005
|
|
|
@@ -1170,7 +1300,7 @@ var ExhaustiveDeps = {
|
|
|
1170
1300
|
return;
|
|
1171
1301
|
}
|
|
1172
1302
|
|
|
1173
|
-
|
|
1303
|
+
reportProblem({
|
|
1174
1304
|
node: dependencyNode.parent.property,
|
|
1175
1305
|
message: "The ref value '" + dependency + ".current' will likely have " + "changed by the time this effect cleanup function runs. If " + "this ref points to a node rendered by React, copy " + ("'" + dependency + ".current' to a variable inside the effect, and ") + "use that variable in the cleanup function."
|
|
1176
1306
|
});
|
|
@@ -1185,7 +1315,7 @@ var ExhaustiveDeps = {
|
|
|
1185
1315
|
}
|
|
1186
1316
|
|
|
1187
1317
|
staleAssignments.add(key);
|
|
1188
|
-
|
|
1318
|
+
reportProblem({
|
|
1189
1319
|
node: writeExpr,
|
|
1190
1320
|
message: "Assignments to the '" + key + "' variable from inside React Hook " + (context.getSource(reactiveHook) + " will be lost after each ") + "render. To preserve the value over time, store it in a useRef " + "Hook and keep the mutable value in the '.current' property. " + "Otherwise, you can move this variable directly inside " + (context.getSource(reactiveHook) + ".")
|
|
1191
1321
|
});
|
|
@@ -1262,8 +1392,8 @@ var ExhaustiveDeps = {
|
|
|
1262
1392
|
}),
|
|
1263
1393
|
_suggestedDependencies = _collectRecommendatio.suggestedDependencies;
|
|
1264
1394
|
|
|
1265
|
-
|
|
1266
|
-
node:
|
|
1395
|
+
reportProblem({
|
|
1396
|
+
node: reactiveHook,
|
|
1267
1397
|
message: "React Hook " + reactiveHookName + " contains a call to '" + setStateInsideEffectWithoutDeps + "'. " + "Without a list of dependencies, this can lead to an infinite chain of updates. " + "To fix this, pass [" + _suggestedDependencies.join(', ') + ("] as a second argument to the " + reactiveHookName + " Hook."),
|
|
1268
1398
|
suggest: [{
|
|
1269
1399
|
desc: "Add dependencies array: [" + _suggestedDependencies.join(', ') + "]",
|
|
@@ -1284,7 +1414,7 @@ var ExhaustiveDeps = {
|
|
|
1284
1414
|
// If the declared dependencies are not an array expression then we
|
|
1285
1415
|
// can't verify that the user provided the correct dependencies. Tell
|
|
1286
1416
|
// the user this in an error.
|
|
1287
|
-
|
|
1417
|
+
reportProblem({
|
|
1288
1418
|
node: declaredDependenciesNode,
|
|
1289
1419
|
message: "React Hook " + context.getSource(reactiveHook) + " was passed a " + 'dependency list that is not an array literal. This means we ' + "can't statically verify whether you've passed the correct " + 'dependencies.'
|
|
1290
1420
|
});
|
|
@@ -1297,7 +1427,7 @@ var ExhaustiveDeps = {
|
|
|
1297
1427
|
|
|
1298
1428
|
|
|
1299
1429
|
if (declaredDependencyNode.type === 'SpreadElement') {
|
|
1300
|
-
|
|
1430
|
+
reportProblem({
|
|
1301
1431
|
node: declaredDependencyNode,
|
|
1302
1432
|
message: "React Hook " + context.getSource(reactiveHook) + " has a spread " + "element in its dependency array. This means we can't " + "statically verify whether you've passed the " + 'correct dependencies.'
|
|
1303
1433
|
});
|
|
@@ -1314,18 +1444,18 @@ var ExhaustiveDeps = {
|
|
|
1314
1444
|
if (/Unsupported node type/.test(error.message)) {
|
|
1315
1445
|
if (declaredDependencyNode.type === 'Literal') {
|
|
1316
1446
|
if (dependencies.has(declaredDependencyNode.value)) {
|
|
1317
|
-
|
|
1447
|
+
reportProblem({
|
|
1318
1448
|
node: declaredDependencyNode,
|
|
1319
1449
|
message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + "because it never changes. " + ("Did you mean to include " + declaredDependencyNode.value + " in the array instead?")
|
|
1320
1450
|
});
|
|
1321
1451
|
} else {
|
|
1322
|
-
|
|
1452
|
+
reportProblem({
|
|
1323
1453
|
node: declaredDependencyNode,
|
|
1324
1454
|
message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + 'because it never changes. You can safely remove it.'
|
|
1325
1455
|
});
|
|
1326
1456
|
}
|
|
1327
1457
|
} else {
|
|
1328
|
-
|
|
1458
|
+
reportProblem({
|
|
1329
1459
|
node: declaredDependencyNode,
|
|
1330
1460
|
message: "React Hook " + context.getSource(reactiveHook) + " has a " + "complex expression in the dependency array. " + 'Extract it to a separate variable so it can be statically checked.'
|
|
1331
1461
|
});
|
|
@@ -1370,6 +1500,7 @@ var ExhaustiveDeps = {
|
|
|
1370
1500
|
missingDependencies = _collectRecommendatio2.missingDependencies,
|
|
1371
1501
|
duplicateDependencies = _collectRecommendatio2.duplicateDependencies;
|
|
1372
1502
|
|
|
1503
|
+
var suggestedDeps = suggestedDependencies;
|
|
1373
1504
|
var problemCount = duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size;
|
|
1374
1505
|
|
|
1375
1506
|
if (problemCount === 0) {
|
|
@@ -1411,7 +1542,7 @@ var ExhaustiveDeps = {
|
|
|
1411
1542
|
// Should we suggest removing effect deps as an appropriate fix too?
|
|
1412
1543
|
|
|
1413
1544
|
|
|
1414
|
-
|
|
1545
|
+
reportProblem({
|
|
1415
1546
|
// TODO: Why not report this at the dependency site?
|
|
1416
1547
|
node: fn.node,
|
|
1417
1548
|
message: message,
|
|
@@ -1428,7 +1559,7 @@ var ExhaustiveDeps = {
|
|
|
1428
1559
|
|
|
1429
1560
|
|
|
1430
1561
|
if (!isEffect && missingDependencies.size > 0) {
|
|
1431
|
-
|
|
1562
|
+
suggestedDeps = collectRecommendations({
|
|
1432
1563
|
dependencies: dependencies,
|
|
1433
1564
|
declaredDependencies: [],
|
|
1434
1565
|
// Pretend we don't know
|
|
@@ -1452,7 +1583,7 @@ var ExhaustiveDeps = {
|
|
|
1452
1583
|
}
|
|
1453
1584
|
|
|
1454
1585
|
if (areDeclaredDepsAlphabetized()) {
|
|
1455
|
-
|
|
1586
|
+
suggestedDeps.sort();
|
|
1456
1587
|
}
|
|
1457
1588
|
|
|
1458
1589
|
function getWarningMessage(deps, singlePrefix, label, fixVerb) {
|
|
@@ -1675,15 +1806,15 @@ var ExhaustiveDeps = {
|
|
|
1675
1806
|
}
|
|
1676
1807
|
}
|
|
1677
1808
|
|
|
1678
|
-
|
|
1809
|
+
reportProblem({
|
|
1679
1810
|
node: declaredDependenciesNode,
|
|
1680
1811
|
message: "React Hook " + context.getSource(reactiveHook) + " has " + ( // To avoid a long message, show the next actionable item.
|
|
1681
1812
|
getWarningMessage(missingDependencies, 'a', 'missing', 'include') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + extraWarning,
|
|
1682
1813
|
suggest: [{
|
|
1683
|
-
desc: "Update the dependencies array to be: [" +
|
|
1814
|
+
desc: "Update the dependencies array to be: [" + suggestedDeps.join(', ') + "]",
|
|
1684
1815
|
fix: function (fixer) {
|
|
1685
1816
|
// TODO: consider preserving the comments or formatting?
|
|
1686
|
-
return fixer.replaceText(declaredDependenciesNode, "[" +
|
|
1817
|
+
return fixer.replaceText(declaredDependenciesNode, "[" + suggestedDeps.join(', ') + "]");
|
|
1687
1818
|
}
|
|
1688
1819
|
}]
|
|
1689
1820
|
});
|
|
@@ -1983,6 +2114,8 @@ function scanForDeclaredBareFunctions(_ref8) {
|
|
|
1983
2114
|
function getDependency(node) {
|
|
1984
2115
|
if (node.parent.type === 'MemberExpression' && node.parent.object === node && node.parent.property.name !== 'current' && !node.parent.computed && !(node.parent.parent != null && node.parent.parent.type === 'CallExpression' && node.parent.parent.callee === node.parent)) {
|
|
1985
2116
|
return getDependency(node.parent);
|
|
2117
|
+
} else if (node.type === 'MemberExpression' && node.parent && node.parent.type === 'AssignmentExpression') {
|
|
2118
|
+
return node.object;
|
|
1986
2119
|
} else {
|
|
1987
2120
|
return node;
|
|
1988
2121
|
}
|
|
@@ -2025,7 +2158,7 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
|
|
|
2025
2158
|
var node = getNodeWithoutReactNamespace(calleeNode);
|
|
2026
2159
|
|
|
2027
2160
|
if (node.type !== 'Identifier') {
|
|
2028
|
-
return
|
|
2161
|
+
return -1;
|
|
2029
2162
|
}
|
|
2030
2163
|
|
|
2031
2164
|
switch (node.name) {
|
|
@@ -2041,7 +2174,9 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
|
|
|
2041
2174
|
return 1;
|
|
2042
2175
|
|
|
2043
2176
|
default:
|
|
2044
|
-
if (node === calleeNode &&
|
|
2177
|
+
if (node === calleeNode && node.name.match(/use.+Effect/)) {
|
|
2178
|
+
return 0;
|
|
2179
|
+
} else if (node === calleeNode && options && options.additionalHooks) {
|
|
2045
2180
|
// Allow the user to provide a regular expression which enables the lint to
|
|
2046
2181
|
// target custom reactive hooks.
|
|
2047
2182
|
var name;
|
|
@@ -2146,17 +2281,21 @@ function isAncestorNodeOf(a, b) {
|
|
|
2146
2281
|
return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
|
|
2147
2282
|
}
|
|
2148
2283
|
|
|
2284
|
+
var configs = {
|
|
2285
|
+
recommended: {
|
|
2286
|
+
plugins: ['react-hooks'],
|
|
2287
|
+
rules: {
|
|
2288
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
2289
|
+
'react-hooks/exhaustive-deps': 'warn'
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2149
2293
|
var rules = {
|
|
2150
|
-
'rules-of-hooks':
|
|
2294
|
+
'rules-of-hooks': RulesOfHooks,
|
|
2151
2295
|
'exhaustive-deps': ExhaustiveDeps
|
|
2152
2296
|
};
|
|
2153
2297
|
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
});
|
|
2157
|
-
|
|
2158
|
-
var eslintPluginReactHooks = src;
|
|
2159
|
-
|
|
2160
|
-
module.exports = eslintPluginReactHooks;
|
|
2298
|
+
exports.configs = configs;
|
|
2299
|
+
exports.rules = rules;
|
|
2161
2300
|
})();
|
|
2162
2301
|
}
|
|
@@ -7,46 +7,51 @@
|
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
'use strict';function
|
|
11
|
-
function
|
|
12
|
-
function
|
|
10
|
+
'use strict';function U(a){if("Identifier"===a.type)return/^use[A-Z0-9].*$/.test(a.name);if("MemberExpression"===a.type&&!a.computed&&U(a.property)){a=a.object;var c=/^[A-Z].*/;return"Identifier"===a.type&&c.test(a.name)}return!1}function aa(a){return"Identifier"===a.type?!/^[a-z]/.test(a.name):!1}function ca(a,c){return a.name===c||"MemberExpression"===a.type&&"React"===a.object.name&&a.property.name===c}function fa(a){return!!(a.parent&&a.parent.callee&&ca(a.parent.callee,"forwardRef"))}
|
|
11
|
+
function ha(a){return!!(a.parent&&a.parent.callee&&ca(a.parent.callee,"memo"))}function ia(a){for(;a;){var c=ja(a);if(c&&(aa(c)||U(c))||fa(a)||ha(a))return!0;a=a.parent}return!1}
|
|
12
|
+
function ja(a){if("FunctionDeclaration"===a.type||"FunctionExpression"===a.type&&a.id)return a.id;if("FunctionExpression"===a.type||"ArrowFunctionExpression"===a.type)return"VariableDeclarator"===a.parent.type&&a.parent.init===a?a.parent.id:"AssignmentExpression"===a.parent.type&&a.parent.right===a&&"="===a.parent.operator?a.parent.left:"Property"!==a.parent.type||a.parent.value!==a||a.parent.computed?"AssignmentPattern"!==a.parent.type||a.parent.right!==a||a.parent.computed?void 0:a.parent.left:
|
|
13
13
|
a.parent.key}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
!0})});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function na(a
|
|
21
|
-
function Y(a
|
|
22
|
-
function pa(a){
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
(a.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
14
|
+
function ka(a){function c(){return{isRequired:!1,isSatisfiedRecursively:!1,hasRequiredNodesBelow:!1,children:new Map}}function e(a,b){b=b.split(".");var k=!0,f=!1,d=void 0;try{for(var e=b[Symbol.iterator](),h;!(k=(h=e.next()).done);k=!0){var n=h.value,z=a.children.get(n);z||(z=c(),a.children.set(n,z));a=z}}catch(G){f=!0,d=G}finally{try{k||null==e.return||e.return()}finally{if(f)throw d;}}return a}function h(a,c,b){c=c.split(".");var k=!0,f=!1,d=void 0;try{for(var e=c[Symbol.iterator](),h;!(k=
|
|
15
|
+
(h=e.next()).done);k=!0){var z=a.children.get(h.value);if(!z)break;b(z);a=z}}catch(G){f=!0,d=G}finally{try{k||null==e.return||e.return()}finally{if(f)throw d;}}}function f(a,c,b,d){a.children.forEach(function(a,k){var e=d(k);a.isSatisfiedRecursively?a.hasRequiredNodesBelow&&b.add(e):a.isRequired?c.add(e):f(a,c,b,function(a){return e+"."+a})})}var H=a.dependencies,p=a.declaredDependencies,u=a.optionalDependencies,x=a.externalDependencies,y=a.isEffect,I=c();H.forEach(function(a,c){e(I,c).isRequired=
|
|
16
|
+
!0;h(I,c,function(a){a.hasRequiredNodesBelow=!0})});p.forEach(function(a){e(I,a.key).isSatisfiedRecursively=!0});u.forEach(function(a){e(I,a).isSatisfiedRecursively=!0});a=new Set;var d=new Set;f(I,a,d,function(a){return a});var b=[],z=new Set,A=new Set;p.forEach(function(a){a=a.key;d.has(a)?-1===b.indexOf(a)?b.push(a):A.add(a):!y||a.endsWith(".current")||x.has(a)?z.add(a):-1===b.indexOf(a)&&b.push(a)});a.forEach(function(a){b.push(a)});return{suggestedDependencies:b,unnecessaryDependencies:z,duplicateDependencies:A,
|
|
17
|
+
missingDependencies:a}}
|
|
18
|
+
function la(a){var c=a.declaredDependenciesNode,e=a.componentScope,h=a.scope;return a.declaredDependencies.map(function(a){a=e.set.get(a.key);if(null==a)return null;var c=a.defs[0];return null==c?null:"Variable"===c.type&&"VariableDeclarator"===c.node.type&&null!=c.node.init&&("ArrowFunctionExpression"===c.node.init.type||"FunctionExpression"===c.node.init.type)||"FunctionName"===c.type&&"FunctionDeclaration"===c.node.type?a:null}).filter(Boolean).map(function(a){var e=a.defs[0];a:{for(var f=!1,u=
|
|
19
|
+
0;u<a.references.length;u++){var x=a.references[u];if(x.writeExpr)if(f){a=!0;break a}else{f=!0;continue}for(var y=x.from;y!==h&&null!=y;)y=y.upper;if(y!==h&&!ma(c,x.identifier)){a=!0;break a}}a=!1}return{fn:e,suggestUseCallback:a}})}
|
|
20
|
+
function na(a){return"MemberExpression"!==a.parent.type||a.parent.object!==a||"current"===a.parent.property.name||a.parent.computed||null!=a.parent.parent&&"CallExpression"===a.parent.parent.type&&a.parent.parent.callee===a.parent?"MemberExpression"===a.type&&a.parent&&"AssignmentExpression"===a.parent.type?a.object:a:na(a.parent)}
|
|
21
|
+
function Y(a){if("Identifier"===a.type)return a.name;if("MemberExpression"!==a.type||a.computed)throw Error("Unsupported node type: "+a.type);var c=Y(a.object);a=Y(a.property);return c+"."+a}function oa(a){return"MemberExpression"!==a.type||"Identifier"!==a.object.type||"React"!==a.object.name||"Identifier"!==a.property.type||a.computed?a:a.property}
|
|
22
|
+
function pa(a,c){var e=oa(a);if("Identifier"!==e.type)return-1;switch(e.name){case "useEffect":case "useLayoutEffect":case "useCallback":case "useMemo":return 0;case "useImperativeHandle":return 1;default:if(e===a&&e.name.match(/use.+Effect/))return 0;if(e===a&&c&&c.additionalHooks){try{var h=Y(e)}catch(f){if(/Unsupported node type/.test(f.message))return 0;throw f;}return c.additionalHooks.test(h)?0:-1}return-1}}
|
|
23
|
+
function qa(a,c){for(var e=[a],h=null;e.length;){h=e.shift();if("Identifier"===h.type&&h.name===c.name&&h.range[0]===c.range[0]&&h.range[1]===c.range[1])return h;if(ma(h,c)){a=0;for(var f=Object.entries(h);a<f.length;a++){var H=f[a],p=H[1];"parent"!==H[0]&&(ra(p)?(p.parent=h,e.push(p)):Array.isArray(p)&&p.forEach(function(a){ra(a)&&(a.parent=h,e.push(a))}))}}}return null}
|
|
24
|
+
function sa(a){for(var c="",e=0;e<a.length;e++)c+=a[e],0===e&&2===a.length?c+=" and ":e===a.length-2&&2<a.length?c+=", and ":e<a.length-1&&(c+=", ");return c}function ra(a){return"object"===typeof a&&null!==a&&!Array.isArray(a)&&"string"===typeof a.type}function ma(a,c){return a.range[0]<=c.range[0]&&a.range[1]>=c.range[1]}exports.configs={recommended:{plugins:["react-hooks"],rules:{"react-hooks/rules-of-hooks":"error","react-hooks/exhaustive-deps":"warn"}}};
|
|
25
|
+
exports.rules={"rules-of-hooks":{meta:{type:"problem",docs:{description:"enforces the Rules of Hooks",category:"Possible Errors",recommended:!0,url:"https://reactjs.org/docs/hooks-rules.html"}},create:function(a){var c=[],e=[];return{onCodePathSegmentStart:function(a){return e.push(a)},onCodePathSegmentEnd:function(){return e.pop()},onCodePathStart:function(){return c.push(new Map)},onCodePathEnd:function(e,f){function h(a,c){var b=h.cache,d=b.get(a.id);c=new Set(c);if(c.has(a.id)){b=[].concat(c);
|
|
26
|
+
a=b.slice(b.indexOf(a.id)+1);b=!0;d=!1;var g=void 0;try{for(var m=a[Symbol.iterator](),B;!(b=(B=m.next()).done);b=!0)y.add(B.value)}catch(J){d=!0,g=J}finally{try{b||null==m.return||m.return()}finally{if(d)throw g;}}return 0}c.add(a.id);if(void 0!==d)return d;if(e.thrownSegments.includes(a))d=0;else if(0===a.prevSegments.length)d=1;else{d=0;m=!0;B=!1;var L=void 0;try{g=a.prevSegments[Symbol.iterator]();for(var M;!(m=(M=g.next()).done);m=!0)d+=h(M.value,c)}catch(J){B=!0,L=J}finally{try{m||null==
|
|
27
|
+
g.return||g.return()}finally{if(B)throw L;}}}a.reachable&&0===d?b.delete(a.id):b.set(a.id,d);return d}function p(a,c){var b=p.cache,d=b.get(a.id);c=new Set(c);if(c.has(a.id)){b=Array.from(c);a=b.slice(b.indexOf(a.id)+1);b=!0;d=!1;var g=void 0;try{for(var m=a[Symbol.iterator](),B;!(b=(B=m.next()).done);b=!0)y.add(B.value)}catch(J){d=!0,g=J}finally{try{b||null==m.return||m.return()}finally{if(d)throw g;}}return 0}c.add(a.id);if(void 0!==d)return d;if(e.thrownSegments.includes(a))d=0;else if(0===
|
|
28
|
+
a.nextSegments.length)d=1;else{d=0;m=!0;B=!1;var L=void 0;try{g=a.nextSegments[Symbol.iterator]();for(var M;!(m=(M=g.next()).done);m=!0)d+=p(M.value,c)}catch(J){B=!0,L=J}finally{try{m||null==g.return||g.return()}finally{if(B)throw L;}}}b.set(a.id,d);return d}function u(a){var c=u.cache,b=c.get(a.id);if(null===b)return Infinity;if(void 0!==b)return b;c.set(a.id,null);if(0===a.prevSegments.length)b=1;else{b=Infinity;var d=!0,g=!1,m=void 0;try{for(var B=a.prevSegments[Symbol.iterator](),L;!(d=
|
|
29
|
+
(L=B.next()).done);d=!0){var e=u(L.value);e<b&&(b=e)}}catch(J){g=!0,m=J}finally{try{d||null==B.return||B.return()}finally{if(g)throw m;}}b+=1}c.set(a.id,b);return b}var x=c.pop();if(0!==x.size){var y=new Set;h.cache=new Map;p.cache=new Map;u.cache=new Map;var I=p(e.initialSegment),d=ja(f),b=ia(f),z=d?aa(d)||U(d):fa(f)||ha(f),A=Infinity,k=!0,n=!1,T=void 0;try{for(var q=e.finalSegments[Symbol.iterator](),F;!(k=(F=q.next()).done);k=!0){var C=F.value;if(C.reachable){var w=u(C);w<A&&(A=w)}}}catch(E){n=
|
|
30
|
+
!0,T=E}finally{try{k||null==q.return||q.return()}finally{if(n)throw T;}}k=!0;n=!1;T=void 0;try{for(var ba=x[Symbol.iterator](),Z;!(k=(Z=ba.next()).done);k=!0){var G=Z.value,t=G[0],W=G[1];if(t.reachable){var Q=0===t.nextSegments.length?A<=u(t):A<u(t),O=h(t)*p(t),N=y.has(t.id);x=!0;q=!1;F=void 0;try{for(var l=W[Symbol.iterator](),D;!(x=(D=l.next()).done);x=!0){var v=D.value;N&&a.report({node:v,message:'React Hook "'+a.getSource(v)+'" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.'});
|
|
31
|
+
if(z){if(!N&&O!==I){var R='React Hook "'+a.getSource(v)+'" is called conditionally. React Hooks must be called in the exact same order in every component render.'+(Q?" Did you accidentally call a React Hook after an early return?":"");a.report({node:v,message:R})}}else if(f.parent&&("MethodDefinition"===f.parent.type||"ClassProperty"===f.parent.type)&&f.parent.value===f){var S='React Hook "'+a.getSource(v)+'" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.';
|
|
32
|
+
a.report({node:v,message:S})}else if(d){var K='React Hook "'+a.getSource(v)+'" is called in function "'+(a.getSource(d)+'" that is neither a React function component nor a custom React Hook function.');a.report({node:v,message:K})}else if("Program"===f.type){var X='React Hook "'+a.getSource(v)+'" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.';a.report({node:v,message:X})}else if(b){var P='React Hook "'+a.getSource(v)+'" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.';
|
|
33
|
+
a.report({node:v,message:P})}}}catch(E){q=!0,F=E}finally{try{x||null==l.return||l.return()}finally{if(q)throw F;}}}}}catch(E){n=!0,T=E}finally{try{k||null==ba.return||ba.return()}finally{if(n)throw T;}}}},CallExpression:function(a){if(U(a.callee)){var f=c[c.length-1],h=e[e.length-1],p=f.get(h);p||(p=[],f.set(h,p));p.push(a.callee)}}}}},"exhaustive-deps":{meta:{type:"suggestion",docs:{description:"verifies the list of dependencies for Hooks like useEffect and similar",category:"Best Practices",recommended:!0,
|
|
34
|
+
url:"https://github.com/facebook/react/issues/14920"},fixable:"code",schema:[{type:"object",additionalProperties:!1,enableDangerousAutofixThisMayCauseInfiniteLoops:!1,properties:{additionalHooks:{type:"string"},enableDangerousAutofixThisMayCauseInfiniteLoops:{type:"boolean"}}}]},create:function(a){function c(c){f&&Array.isArray(c.suggest)&&0<c.suggest.length&&(c.fix=c.suggest[0].fix);a.report(c)}function e(a,c){return function(b){if(c.has(b))return c.get(b);var d=a(b);c.set(b,d);return d}}function h(d,
|
|
35
|
+
b,h,f,k){function z(a){var g=!0,c=!1,b=void 0;try{for(var e=a.references[Symbol.iterator](),h;!(g=(h=e.next()).done);g=!0){var f=h.value;if(f.resolved&&F.has(f.resolved.scope)){var p=qa(d,f.identifier),l=na(p),A=Y(l),q;if(q=k&&"Identifier"===l.type&&"MemberExpression"===l.parent.type&&!l.parent.computed&&"Identifier"===l.parent.property.type&&"current"===l.parent.property.name){for(var n=f.from,u=!1;n.block!==d;)"function"===n.type&&(u=null!=n.block.parent&&"ReturnStatement"===n.block.parent.type),
|
|
36
|
+
n=n.upper;q=u}q&&G.set(A,{reference:f,dependencyNode:l});var r=f.resolved.defs[0];if(null!=r&&(null==r.node||r.node.init!==d.parent)&&"TypeParameter"!==r.type)if(t.has(A))t.get(A).references.push(f);else{var w=f.resolved,x=H(w)||Z(w);t.set(A,{isStatic:x,references:[f]})}}}}catch(ea){c=!0,b=ea}finally{try{g||null==e.return||e.return()}finally{if(c)throw b;}}g=!0;c=!1;b=void 0;try{for(var v=a.childScopes[Symbol.iterator](),y;!(g=(y=v.next()).done);g=!0)z(y.value)}catch(ea){c=!0,b=ea}finally{try{g||
|
|
37
|
+
null==v.return||v.return()}finally{if(c)throw b;}}}function A(a,c,b,d){return 0===a.size?null:(1<a.size?"":c+" ")+b+" "+(1<a.size?"dependencies":"dependency")+": "+sa(Array.from(a).sort().map(function(a){return"'"+a+"'"}))+(". Either "+d+" "+(1<a.size?"them":"it")+" or remove the dependency array.")}k&&d.async&&c({node:d,message:"Effect callbacks are synchronous to prevent race conditions. Put the async function inside:\n\nuseEffect(() => {\n async function fetchData() {\n // You can await here\n const response = await MyAPI.getData(someId);\n // ...\n }\n fetchData();\n}, [someId]); // Or [] if effect doesn't need props or state\n\nLearn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching"});
|
|
38
|
+
for(var q=p.acquire(d),F=new Set,C=null,w=q.upper;w;){F.add(w);if("function"===w.type)break;w=w.upper}if(w){C=w;var H=e(function(a){if(!Array.isArray(a.defs))return!1;var c=a.defs[0];if(null==c||"VariableDeclarator"!==c.node.type)return!1;var b=c.node.init;if(null==b)return!1;for(;"TSAsExpression"===b.type;)b=b.expression;var g=c.node.parent;if(null==g&&(qa(C.block,c.node.id),g=c.node.parent,null==g))return!1;if("const"===g.kind&&"Literal"===b.type&&("string"===typeof b.value||"number"===typeof b.value||
|
|
39
|
+
null===b.value))return!0;if("CallExpression"!==b.type)return!1;b=b.callee;"MemberExpression"!==b.type||"React"!==b.object.name||null==b.property||b.computed||(b=b.property);if("Identifier"!==b.type)return!1;c=c.node.id;b=b.name;if("useRef"===b&&"Identifier"===c.type)return!0;if(("useState"===b||"useReducer"===b)&&"ArrayPattern"===c.type&&2===c.elements.length&&Array.isArray(a.identifiers)){if(c.elements[1]===a.identifiers[0]){if("useState"===b)for(a=a.references,b=0;b<a.length;b++)u.set(a[b].identifier,
|
|
40
|
+
c.elements[0]);return!0}if(c.elements[0]===a.identifiers[0]&&"useState"===b)for(a=a.references,c=0;c<a.length;c++)x.add(a[c].identifier)}return!1},y),Z=e(function(a){if(!Array.isArray(a.defs))return!1;a=a.defs[0];if(null==a||null==a.node||null==a.node.id)return!1;var c=a.node,b=C.childScopes;a=null;var g;for(g=0;g<b.length;g++){var d=b[g],e=d.block;if("FunctionDeclaration"===c.type&&e===c||"VariableDeclarator"===c.type&&e.parent===c){a=d;break}}if(null==a)return!1;for(g=0;g<a.through.length;g++)if(c=
|
|
41
|
+
a.through[g],null!=c.resolved&&F.has(c.resolved.scope)&&!H(c.resolved))return!1;return!0},I),G=new Map,t=new Map;z(q);G.forEach(function(a,b){var g=a.dependencyNode;a=a.reference.resolved.references;for(var d=!1,e=0;e<a.length;e++){var m=a[e].identifier.parent;if(null!=m&&"MemberExpression"===m.type&&!m.computed&&"Identifier"===m.property.type&&"current"===m.property.name&&"AssignmentExpression"===m.parent.type&&m.parent.left===m){d=!0;break}}d||c({node:g.parent.property,message:"The ref value '"+
|
|
42
|
+
b+".current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy '"+(b+".current' to a variable inside the effect, and use that variable in the cleanup function.")})});var W=new Set,Q=new Set;t.forEach(function(b,d){var g=b.references;b.isStatic&&Q.add(d);g.forEach(function(b){b.writeExpr&&(b=b.writeExpr,W.has(d)||(W.add(d),c({node:b,message:"Assignments to the '"+d+"' variable from inside React Hook "+(a.getSource(h)+" will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside ")+
|
|
43
|
+
(a.getSource(h)+".")})))})});if(!(0<W.size))if(b){var O=[],N=new Set;"ArrayExpression"!==b.type?c({node:b,message:"React Hook "+a.getSource(h)+" was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies."}):b.elements.forEach(function(b){if(null!==b)if("SpreadElement"===b.type)c({node:b,message:"React Hook "+a.getSource(h)+" has a spread element in its dependency array. This means we can't statically verify whether you've passed the correct dependencies."});
|
|
44
|
+
else{try{var d=Y(b)}catch(M){if(/Unsupported node type/.test(M.message)){"Literal"===b.type?t.has(b.value)?c({node:b,message:"The "+b.raw+" literal is not a valid dependency because it never changes. Did you mean to include "+(b.value+" in the array instead?")}):c({node:b,message:"The "+b.raw+" literal is not a valid dependency because it never changes. You can safely remove it."}):c({node:b,message:"React Hook "+a.getSource(h)+" has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked."});
|
|
45
|
+
return}throw M;}for(var g=b;"MemberExpression"===g.type;)g=g.object;var e=!C.through.some(function(a){return a.identifier===g});O.push({key:d,node:b});e||N.add(d)}});var l=ka({dependencies:t,declaredDependencies:O,optionalDependencies:Q,externalDependencies:N,isEffect:k});w=l.unnecessaryDependencies;var D=l.missingDependencies,v=l.duplicateDependencies,R=l.suggestedDependencies;if(0===v.size+D.size+w.size)la({declaredDependencies:O,declaredDependenciesNode:b,componentScope:C,scope:q}).forEach(function(a){var d=
|
|
46
|
+
a.fn;a=a.suggestUseCallback;var g="The '"+d.name.name+"' function makes the dependencies of "+(f+" Hook (at line "+b.loc.start.line+") change on every render.");g=a?g+(" To fix this, wrap the '"+(d.name.name+"' definition into its own useCallback() Hook.")):g+(" Move it inside the "+f+" callback. Alternatively, wrap the '"+(d.name.name+"' definition into its own useCallback() Hook."));var e;a&&"Variable"===d.type&&(e=[{desc:"Wrap the '"+d.name.name+"' definition into its own useCallback() Hook.",
|
|
47
|
+
fix:function(a){return[a.insertTextBefore(d.node.init,"useCallback("),a.insertTextAfter(d.node.init,")")]}}]);c({node:d.node,message:g,suggest:e})});else{!k&&0<D.size&&(R=ka({dependencies:t,declaredDependencies:[],optionalDependencies:Q,externalDependencies:N,isEffect:k}).suggestedDependencies);(function(){if(0===O.length)return!0;var a=O.map(function(a){return a.key}),b=a.slice().sort();return a.join(",")===b.join(",")})()&&R.sort();l="";if(0<w.size){var S=null;Array.from(w.keys()).forEach(function(a){null===
|
|
48
|
+
S&&a.endsWith(".current")&&(S=a)});if(null!==S)l=" Mutable values like '"+S+"' aren't valid dependencies because mutating them doesn't re-render the component.";else if(0<N.size){var K=Array.from(N)[0];q.set.has(K)||(l=" Outer scope values like '"+K+"' aren't valid dependencies because mutating them doesn't re-render the component.")}}if(!l&&D.has("props")){q=t.get("props");if(null==q)return;q=q.references;if(!Array.isArray(q))return;K=!0;for(var X=0;X<q.length;X++){var P=qa(C.block,q[X].identifier);
|
|
49
|
+
if(!P){K=!1;break}P=P.parent;if(null==P){K=!1;break}if("MemberExpression"!==P.type){K=!1;break}}K&&(l=" However, 'props' will change when *any* prop changes, so the preferred fix is to destructure the 'props' object outside of the "+(f+" call and refer to those specific props inside ")+(a.getSource(h)+"."))}if(!l&&0<D.size){var E=null;D.forEach(function(a){if(!E){var b=C.set.get(a),c=t.get(a);if(c.references[0].resolved===b&&(b=b.defs[0],null!=b&&null!=b.name&&"Parameter"===b.type)){b=!1;for(var d,
|
|
50
|
+
e=0;e<c.references.length;e++)if(d=c.references[e].identifier,null!=d&&null!=d.parent&&"CallExpression"===d.parent.type&&d.parent.callee===d){b=!0;break}b&&(E=a)}}});null!==E&&(l=" If '"+E+"' changes too often, find the parent component that defines it and wrap that definition in useCallback.")}if(!l&&0<D.size){var r=null;D.forEach(function(a){if(null===r)for(var b=t.get(a).references,c,d,e=0;e<b.length;e++){c=b[e].identifier;for(d=c.parent;null!=d&&d!==C.block;){if("CallExpression"===d.type){var g=
|
|
51
|
+
u.get(d.callee);if(null!=g){g.name===a?r={missingDep:a,setter:d.callee.name,form:"updater"}:x.has(c)?r={missingDep:a,setter:d.callee.name,form:"reducer"}:(c=b[e].resolved,null!=c&&(c=c.defs[0],null!=c&&"Parameter"===c.type&&(r={missingDep:a,setter:d.callee.name,form:"inlineReducer"})));break}}d=d.parent}if(null!==r)break}});if(null!==r)switch(r.form){case "reducer":l=" You can also replace multiple useState variables with useReducer if '"+(r.setter+"' needs the current value of '")+(r.missingDep+
|
|
52
|
+
"'.");break;case "inlineReducer":l=" If '"+r.setter+"' needs the current value of '"+(r.missingDep+"', you can also switch to useReducer instead of useState and read '")+(r.missingDep+"' in the reducer.");break;case "updater":l=" You can also do a functional update '"+r.setter+"("+r.missingDep.substring(0,1)+" => ...)' if you only need '"+r.missingDep+"' in the '"+(r.setter+"' call.");break;default:throw Error("Unknown case.");}}c({node:b,message:"React Hook "+a.getSource(h)+" has "+(A(D,"a","missing",
|
|
53
|
+
"include")||A(w,"an","unnecessary","exclude")||A(v,"a","duplicate","omit"))+l,suggest:[{desc:"Update the dependencies array to be: ["+R.join(", ")+"]",fix:function(a){return a.replaceText(b,"["+R.join(", ")+"]")}}]})}}else{var V=null;t.forEach(function(a,b){V||a.references.forEach(function(a){if(!V&&u.has(a.identifier)){for(a=a.from;"function"!==a.type;)a=a.upper;a.block===d&&(V=b)}})});if(V){var da=ka({dependencies:t,declaredDependencies:[],optionalDependencies:Q,externalDependencies:new Set,isEffect:!0}).suggestedDependencies;
|
|
54
|
+
c({node:h,message:"React Hook "+f+" contains a call to '"+V+"'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass ["+da.join(", ")+("] as a second argument to the "+f+" Hook."),suggest:[{desc:"Add dependencies array: ["+da.join(", ")+"]",fix:function(a){return a.insertTextAfter(d,", ["+da.join(", ")+"]")}}]})}}}}var f=a.options&&a.options[0]&&a.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops||!1,H={additionalHooks:a.options&&a.options[0]&&
|
|
55
|
+
a.options[0].additionalHooks?new RegExp(a.options[0].additionalHooks):void 0,enableDangerousAutofixThisMayCauseInfiniteLoops:f},p=a.getSourceCode().scopeManager,u=new WeakMap,x=new WeakSet,y=new WeakMap,I=new WeakMap;return{CallExpression:function(d){var b=pa(d.callee,H);if(-1!==b){var e=d.arguments[b],f=d.callee,k=oa(f).name,n=d.arguments[b+1];d=/Effect($|[^a-z])/g.test(k);if(n||d){switch(e.type){case "FunctionExpression":case "ArrowFunctionExpression":h(e,n,f,k,d);return;case "Identifier":if(!n||
|
|
56
|
+
n.elements&&n.elements.some(function(a){return"Identifier"===a.type&&a.name===e.name}))return;b=a.getScope().set.get(e.name);if(null==b||null==b.defs)return;b=b.defs[0];if(!b||!b.node)break;if("Variable"!==b.type&&"FunctionName"!==b.type)break;switch(b.node.type){case "FunctionDeclaration":h(b.node,n,f,k,d);return;case "VariableDeclarator":if(b=b.node.init)switch(b.type){case "ArrowFunctionExpression":case "FunctionExpression":h(b,n,f,k,d);return}}break;default:c({node:f,message:"React Hook "+k+" received a function whose dependencies are unknown. Pass an inline function instead."});
|
|
57
|
+
return}c({node:f,message:"React Hook "+k+" has a missing dependency: '"+e.name+"'. Either include it or remove the dependency array.",suggest:[{desc:"Update the dependencies array to be: ["+e.name+"]",fix:function(a){return a.replaceText(n,"["+e.name+"]")}}]})}else"useMemo"!==k&&"useCallback"!==k||c({node:f,message:"React Hook "+k+" does nothing when called with only one argument. Did you forget to pass an array of dependencies?"})}}}}}};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-hooks",
|
|
3
3
|
"description": "ESLint rules for React Hooks",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/facebook/react.git",
|
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
"url": "https://github.com/facebook/react/issues"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
28
|
+
"node": ">=10"
|
|
29
29
|
},
|
|
30
30
|
"homepage": "https://reactjs.org/",
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@typescript-eslint/parser": "^2.26.0"
|
|
33
36
|
}
|
|
34
37
|
}
|