[ESLint] Treat functions that don't capture anything as static (#14996)
* Treat functions that don't capture anything as static * Fix comment
Dan Abramov committed
Mar 5, 2019 at 21:07 UTC
fa5d4ee43b2b7145ab4013b86662e69ea64ab180
2 files changed
+674
-78
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+507
@@ -652,6 +652,206 @@ const tests = {
652
}
653
`,
654
},
655
+ {
656
+ code: `
657
+ function MyComponent(props) {
658
+ function handleNext1() {
659
+ console.log('hello');
660
+ }
661
+ const handleNext2 = () => {
662
+ console.log('hello');
663
+ };
664
+ let handleNext3 = function() {
665
+ console.log('hello');
666
+ };
667
+ useEffect(() => {
668
+ return Store.subscribe(handleNext1);
669
+ }, [handleNext1]);
670
+ useLayoutEffect(() => {
671
+ return Store.subscribe(handleNext2);
672
+ }, [handleNext2]);
673
+ useMemo(() => {
674
+ return Store.subscribe(handleNext3);
675
+ }, [handleNext3]);
676
+ }
677
+ `,
678
+ },
679
+ {
680
+ // Declaring handleNext is optional because
681
+ // it doesn't use anything in the function scope.
682
+ code: `
683
+ function MyComponent(props) {
684
+ function handleNext1() {
685
+ console.log('hello');
686
+ }
687
+ const handleNext2 = () => {
688
+ console.log('hello');
689
+ };
690
+ let handleNext3 = function() {
691
+ console.log('hello');
692
+ };
693
+ useEffect(() => {
694
+ return Store.subscribe(handleNext1);
695
+ }, []);
696
+ useLayoutEffect(() => {
697
+ return Store.subscribe(handleNext2);
698
+ }, []);
699
+ useMemo(() => {
700
+ return Store.subscribe(handleNext3);
701
+ }, []);
702
+ }
703
+ `,
704
+ },
705
+ {
706
+ // Declaring handleNext is optional because
707
+ // it doesn't use anything in the function scope.
708
+ code: `
709
+ function MyComponent(props) {
710
+ function handleNext() {
711
+ console.log('hello');
712
+ }
713
+ useEffect(() => {
714
+ return Store.subscribe(handleNext);
715
+ }, []);
716
+ useLayoutEffect(() => {
717
+ return Store.subscribe(handleNext);
718
+ }, []);
719
+ useMemo(() => {
720
+ return Store.subscribe(handleNext);
721
+ }, []);
722
+ }
723
+ `,
724
+ },
725
+ {
726
+ // Declaring handleNext is optional because
727
+ // everything they use is fully static.
728
+ code: `
729
+ function MyComponent(props) {
730
+ let [, setState] = useState();
731
+ let [, dispatch] = React.useReducer();
732
+
733
+ function handleNext1(value) {
734
+ let value2 = value * 100;
735
+ setState(value2);
736
+ console.log('hello');
737
+ }
738
+ const handleNext2 = (value) => {
739
+ setState(foo(value));
740
+ console.log('hello');
741
+ };
742
+ let handleNext3 = function(value) {
743
+ console.log(value);
744
+ dispatch({ type: 'x', value });
745
+ };
746
+ useEffect(() => {
747
+ return Store.subscribe(handleNext1);
748
+ }, []);
749
+ useLayoutEffect(() => {
750
+ return Store.subscribe(handleNext2);
751
+ }, []);
752
+ useMemo(() => {
753
+ return Store.subscribe(handleNext3);
754
+ }, []);
755
+ }
756
+ `,
757
+ },
758
+ {
759
+ code: `
760
+ function useInterval(callback, delay) {
761
+ const savedCallback = useRef();
762
+ useEffect(() => {
763
+ savedCallback.current = callback;
764
+ });
765
+ useEffect(() => {
766
+ function tick() {
767
+ savedCallback.current();
768
+ }
769
+ if (delay !== null) {
770
+ let id = setInterval(tick, delay);
771
+ return () => clearInterval(id);
772
+ }
773
+ }, [delay]);
774
+ }
775
+ `,
776
+ },
777
+ {
778
+ code: `
779
+ function Counter() {
780
+ const [count, setCount] = useState(0);
781
+
782
+ useEffect(() => {
783
+ let id = setInterval(() => {
784
+ setCount(c => c + 1);
785
+ }, 1000);
786
+ return () => clearInterval(id);
787
+ }, []);
788
+
789
+ return <h1>{count}</h1>;
790
+ }
791
+ `,
792
+ },
793
+ {
794
+ code: `
795
+ function Counter() {
796
+ const [count, setCount] = useState(0);
797
+
798
+ function tick() {
799
+ setCount(c => c + 1);
800
+ }
801
+
802
+ useEffect(() => {
803
+ let id = setInterval(() => {
804
+ tick();
805
+ }, 1000);
806
+ return () => clearInterval(id);
807
+ }, []);
808
+
809
+ return <h1>{count}</h1>;
810
+ }
811
+ `,
812
+ },
813
+ {
814
+ code: `
815
+ function Counter() {
816
+ const [count, dispatch] = useReducer((state, action) => {
817
+ if (action === 'inc') {
818
+ return state + 1;
819
+ }
820
+ }, 0);
821
+
822
+ useEffect(() => {
823
+ let id = setInterval(() => {
824
+ dispatch('inc');
825
+ }, 1000);
826
+ return () => clearInterval(id);
827
+ }, []);
828
+
829
+ return <h1>{count}</h1>;
830
+ }
831
+ `,
832
+ },
833
+ {
834
+ code: `
835
+ function Counter() {
836
+ const [count, dispatch] = useReducer((state, action) => {
837
+ if (action === 'inc') {
838
+ return state + 1;
839
+ }
840
+ }, 0);
841
+
842
+ const tick = () => {
843
+ dispatch('inc');
844
+ };
845
+
846
+ useEffect(() => {
847
+ let id = setInterval(tick, 1000);
848
+ return () => clearInterval(id);
849
+ }, []);
850
+
851
+ return <h1>{count}</h1>;
852
+ }
853
+ `,
854
+ },
855
],
856
invalid: [
857
{
@@ -2858,6 +3058,313 @@ const tests = {
3058
"because their mutation doesn't re-render the component.",
3059
],
3060
},
3061
+ {
3062
+ // Every almost-static function is tainted by a dynamic value.
3063
+ code: `
3064
+ function MyComponent(props) {
3065
+ let [, setState] = useState();
3066
+ let [, dispatch] = React.useReducer();
3067
+ let taint = props.foo;
3068
+
3069
+ function handleNext1(value) {
3070
+ let value2 = value * taint;
3071
+ setState(value2);
3072
+ console.log('hello');
3073
+ }
3074
+ const handleNext2 = (value) => {
3075
+ setState(taint(value));
3076
+ console.log('hello');
3077
+ };
3078
+ let handleNext3 = function(value) {
3079
+ setTimeout(() => console.log(taint));
3080
+ dispatch({ type: 'x', value });
3081
+ };
3082
+ useEffect(() => {
3083
+ return Store.subscribe(handleNext1);
3084
+ }, []);
3085
+ useLayoutEffect(() => {
3086
+ return Store.subscribe(handleNext2);
3087
+ }, []);
3088
+ useMemo(() => {
3089
+ return Store.subscribe(handleNext3);
3090
+ }, []);
3091
+ }
3092
+ `,
3093
+ output: `
3094
+ function MyComponent(props) {
3095
+ let [, setState] = useState();
3096
+ let [, dispatch] = React.useReducer();
3097
+ let taint = props.foo;
3098
+
3099
+ function handleNext1(value) {
3100
+ let value2 = value * taint;
3101
+ setState(value2);
3102
+ console.log('hello');
3103
+ }
3104
+ const handleNext2 = (value) => {
3105
+ setState(taint(value));
3106
+ console.log('hello');
3107
+ };
3108
+ let handleNext3 = function(value) {
3109
+ setTimeout(() => console.log(taint));
3110
+ dispatch({ type: 'x', value });
3111
+ };
3112
+ useEffect(() => {
3113
+ return Store.subscribe(handleNext1);
3114
+ }, [handleNext1]);
3115
+ useLayoutEffect(() => {
3116
+ return Store.subscribe(handleNext2);
3117
+ }, [handleNext2]);
3118
+ useMemo(() => {
3119
+ return Store.subscribe(handleNext3);
3120
+ }, [handleNext3]);
3121
+ }
3122
+ `,
3123
+ errors: [
3124
+ "React Hook useEffect has a missing dependency: 'handleNext1'. " +
3125
+ 'Either include it or remove the dependency array.',
3126
+ "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " +
3127
+ 'Either include it or remove the dependency array.',
3128
+ "React Hook useMemo has a missing dependency: 'handleNext3'. " +
3129
+ 'Either include it or remove the dependency array.',
3130
+ ],
3131
+ },
3132
+ {
3133
+ // Regression test
3134
+ code: `
3135
+ function MyComponent(props) {
3136
+ let [, setState] = useState();
3137
+ let [, dispatch] = React.useReducer();
3138
+ let taint = props.foo;
3139
+
3140
+ // Shouldn't affect anything
3141
+ function handleChange() {}
3142
+
3143
+ function handleNext1(value) {
3144
+ let value2 = value * taint;
3145
+ setState(value2);
3146
+ console.log('hello');
3147
+ }
3148
+ const handleNext2 = (value) => {
3149
+ setState(taint(value));
3150
+ console.log('hello');
3151
+ };
3152
+ let handleNext3 = function(value) {
3153
+ console.log(taint);
3154
+ dispatch({ type: 'x', value });
3155
+ };
3156
+ useEffect(() => {
3157
+ return Store.subscribe(handleNext1);
3158
+ }, []);
3159
+ useLayoutEffect(() => {
3160
+ return Store.subscribe(handleNext2);
3161
+ }, []);
3162
+ useMemo(() => {
3163
+ return Store.subscribe(handleNext3);
3164
+ }, []);
3165
+ }
3166
+ `,
3167
+ output: `
3168
+ function MyComponent(props) {
3169
+ let [, setState] = useState();
3170
+ let [, dispatch] = React.useReducer();
3171
+ let taint = props.foo;
3172
+
3173
+ // Shouldn't affect anything
3174
+ function handleChange() {}
3175
+
3176
+ function handleNext1(value) {
3177
+ let value2 = value * taint;
3178
+ setState(value2);
3179
+ console.log('hello');
3180
+ }
3181
+ const handleNext2 = (value) => {
3182
+ setState(taint(value));
3183
+ console.log('hello');
3184
+ };
3185
+ let handleNext3 = function(value) {
3186
+ console.log(taint);
3187
+ dispatch({ type: 'x', value });
3188
+ };
3189
+ useEffect(() => {
3190
+ return Store.subscribe(handleNext1);
3191
+ }, [handleNext1]);
3192
+ useLayoutEffect(() => {
3193
+ return Store.subscribe(handleNext2);
3194
+ }, [handleNext2]);
3195
+ useMemo(() => {
3196
+ return Store.subscribe(handleNext3);
3197
+ }, [handleNext3]);
3198
+ }
3199
+ `,
3200
+ errors: [
3201
+ "React Hook useEffect has a missing dependency: 'handleNext1'. " +
3202
+ 'Either include it or remove the dependency array.',
3203
+ "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " +
3204
+ 'Either include it or remove the dependency array.',
3205
+ "React Hook useMemo has a missing dependency: 'handleNext3'. " +
3206
+ 'Either include it or remove the dependency array.',
3207
+ ],
3208
+ },
3209
+ {
3210
+ // Regression test
3211
+ code: `
3212
+ function MyComponent(props) {
3213
+ let [, setState] = useState();
3214
+ let [, dispatch] = React.useReducer();
3215
+ let taint = props.foo;
3216
+
3217
+ // Shouldn't affect anything
3218
+ const handleChange = () => {};
3219
+
3220
+ function handleNext1(value) {
3221
+ let value2 = value * taint;
3222
+ setState(value2);
3223
+ console.log('hello');
3224
+ }
3225
+ const handleNext2 = (value) => {
3226
+ setState(taint(value));
3227
+ console.log('hello');
3228
+ };
3229
+ let handleNext3 = function(value) {
3230
+ console.log(taint);
3231
+ dispatch({ type: 'x', value });
3232
+ };
3233
+ useEffect(() => {
3234
+ return Store.subscribe(handleNext1);
3235
+ }, []);
3236
+ useLayoutEffect(() => {
3237
+ return Store.subscribe(handleNext2);
3238
+ }, []);
3239
+ useMemo(() => {
3240
+ return Store.subscribe(handleNext3);
3241
+ }, []);
3242
+ }
3243
+ `,
3244
+ output: `
3245
+ function MyComponent(props) {
3246
+ let [, setState] = useState();
3247
+ let [, dispatch] = React.useReducer();
3248
+ let taint = props.foo;
3249
+
3250
+ // Shouldn't affect anything
3251
+ const handleChange = () => {};
3252
+
3253
+ function handleNext1(value) {
3254
+ let value2 = value * taint;
3255
+ setState(value2);
3256
+ console.log('hello');
3257
+ }
3258
+ const handleNext2 = (value) => {
3259
+ setState(taint(value));
3260
+ console.log('hello');
3261
+ };
3262
+ let handleNext3 = function(value) {
3263
+ console.log(taint);
3264
+ dispatch({ type: 'x', value });
3265
+ };
3266
+ useEffect(() => {
3267
+ return Store.subscribe(handleNext1);
3268
+ }, [handleNext1]);
3269
+ useLayoutEffect(() => {
3270
+ return Store.subscribe(handleNext2);
3271
+ }, [handleNext2]);
3272
+ useMemo(() => {
3273
+ return Store.subscribe(handleNext3);
3274
+ }, [handleNext3]);
3275
+ }
3276
+ `,
3277
+ errors: [
3278
+ "React Hook useEffect has a missing dependency: 'handleNext1'. " +
3279
+ 'Either include it or remove the dependency array.',
3280
+ "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " +
3281
+ 'Either include it or remove the dependency array.',
3282
+ "React Hook useMemo has a missing dependency: 'handleNext3'. " +
3283
+ 'Either include it or remove the dependency array.',
3284
+ ],
3285
+ },
3286
+ {
3287
+ code: `
3288
+ function Counter() {
3289
+ let [count, setCount] = useState(0);
3290
+
3291
+ useEffect(() => {
3292
+ let id = setInterval(() => {
3293
+ setCount(count + 1);
3294
+ }, 1000);
3295
+ return () => clearInterval(id);
3296
+ }, []);
3297
+
3298
+ return <h1>{count}</h1>;
3299
+ }
3300
+ `,
3301
+ output: `
3302
+ function Counter() {
3303
+ let [count, setCount] = useState(0);
3304
+
3305
+ useEffect(() => {
3306
+ let id = setInterval(() => {
3307
+ setCount(count + 1);
3308
+ }, 1000);
3309
+ return () => clearInterval(id);
3310
+ }, [count]);
3311
+
3312
+ return <h1>{count}</h1>;
3313
+ }
3314
+ `,
3315
+ // TODO: ideally this should suggest useState updater form
3316
+ // since this code doesn't actually work.
3317
+ errors: [
3318
+ "React Hook useEffect has a missing dependency: 'count'. " +
3319
+ 'Either include it or remove the dependency array.',
3320
+ ],
3321
+ },
3322
+ {
3323
+ code: `
3324
+ function Counter() {
3325
+ const [count, setCount] = useState(0);
3326
+
3327
+ function tick() {
3328
+ setCount(count + 1);
3329
+ }
3330
+
3331
+ useEffect(() => {
3332
+ let id = setInterval(() => {
3333
+ tick();
3334
+ }, 1000);
3335
+ return () => clearInterval(id);
3336
+ }, []);
3337
+
3338
+ return <h1>{count}</h1>;
3339
+ }
3340
+ `,
3341
+ output: `
3342
+ function Counter() {
3343
+ const [count, setCount] = useState(0);
3344
+
3345
+ function tick() {
3346
+ setCount(count + 1);
3347
+ }
3348
+
3349
+ useEffect(() => {
3350
+ let id = setInterval(() => {
3351
+ tick();
3352
+ }, 1000);
3353
+ return () => clearInterval(id);
3354
+ }, [tick]);
3355
+
3356
+ return <h1>{count}</h1>;
3357
+ }
3358
+ `,
3359
+ // TODO: ideally this should suggest useState updater form
3360
+ // since this code doesn't actually work. The autofix could
3361
+ // at least avoid suggesting 'tick' since it's obviously
3362
+ // always different, and thus useless.
3363
+ errors: [
3364
+ "React Hook useEffect has a missing dependency: 'tick'. " +
3365
+ 'Either include it or remove the dependency array.',
3366
+ ],
3367
+ },
3368
],
3369
};
3370
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+167
-78
@@ -34,6 +34,22 @@ export default {
34
: undefined;
35
const options = {additionalHooks};
36
37
+ // Should be shared between visitors.
38
+ let staticKnownValueCache = new WeakMap();
39
+ let functionWithoutCapturedValueCache = new WeakMap();
40
+ function memoizeWithWeakMap(fn, map) {
41
+ return function(arg) {
42
+ if (map.has(arg)) {
43
+ // to verify cache hits:
44
+ // console.log(arg.name)
45
+ return map.get(arg);
46
+ }
47
+ const result = fn(arg);
48
+ map.set(arg, result);
49
+ return result;
50
+ };
51
+ }
52
+
53
return {
54
FunctionExpression: visitFunctionExpression,
55
ArrowFunctionExpression: visitFunctionExpression,
@@ -105,6 +121,153 @@ export default {
121
componentScope = currentScope;
122
}
123
124
+ // Next we'll define a few helpers that helps us
125
+ // tell if some values don't have to be declared as deps.
126
+
127
+ // Some are known to be static based on Hook calls.
128
+ // const [state, setState] = useState() / React.useState()
129
+ // ^^^ true for this reference
130
+ // const [state, dispatch] = useReducer() / React.useReducer()
131
+ // ^^^ true for this reference
132
+ // const ref = useRef()
133
+ // ^^^ true for this reference
134
+ // False for everything else.
135
+ function isStaticKnownHookValue(resolved) {
136
+ if (!Array.isArray(resolved.defs)) {
137
+ return false;
138
+ }
139
+ const def = resolved.defs[0];
140
+ if (def == null) {
141
+ return false;
142
+ }
143
+ // Look for `let stuff = ...`
144
+ if (def.node.type !== 'VariableDeclarator') {
145
+ return false;
146
+ }
147
+ const init = def.node.init;
148
+ if (init == null) {
149
+ return false;
150
+ }
151
+ // Detect primitive constants
152
+ // const foo = 42
153
+ const declaration = def.node.parent;
154
+ if (
155
+ declaration.kind === 'const' &&
156
+ init.type === 'Literal' &&
157
+ (typeof init.value === 'string' ||
158
+ typeof init.value === 'number' ||
159
+ init.value === null)
160
+ ) {
161
+ // Definitely static
162
+ return true;
163
+ }
164
+ // Detect known Hook calls
165
+ // const [_, setState] = useState()
166
+ if (init.type !== 'CallExpression') {
167
+ return false;
168
+ }
169
+ let callee = init.callee;
170
+ // Step into `= React.something` initializer.
171
+ if (
172
+ callee.type === 'MemberExpression' &&
173
+ callee.object.name === 'React' &&
174
+ callee.property != null &&
175
+ !callee.computed
176
+ ) {
177
+ callee = callee.property;
178
+ }
179
+ if (callee.type !== 'Identifier') {
180
+ return false;
181
+ }
182
+ const id = def.node.id;
183
+ if (callee.name === 'useRef' && id.type === 'Identifier') {
184
+ // useRef() return value is static.
185
+ return true;
186
+ } else if (callee.name === 'useState' || callee.name === 'useReducer') {
187
+ // Only consider second value in initializing tuple static.
188
+ if (
189
+ id.type === 'ArrayPattern' &&
190
+ id.elements.length === 2 &&
191
+ Array.isArray(resolved.identifiers) &&
192
+ // Is second tuple value the same reference we're checking?
193
+ id.elements[1] === resolved.identifiers[0]
194
+ ) {
195
+ return true;
196
+ }
197
+ }
198
+ // By default assume it's dynamic.
199
+ return false;
200
+ }
201
+
202
+ // Some are just functions that don't reference anything dynamic.
203
+ function isFunctionWithoutCapturedValues(resolved) {
204
+ if (!Array.isArray(resolved.defs)) {
205
+ return false;
206
+ }
207
+ const def = resolved.defs[0];
208
+ if (def == null) {
209
+ return false;
210
+ }
211
+ if (def.node == null || def.node.id == null) {
212
+ return false;
213
+ }
214
+ // Search the direct component subscopes for
215
+ // top-level function definitions matching this reference.
216
+ const fnNode = def.node;
217
+ let childScopes = componentScope.childScopes;
218
+ let fnScope = null;
219
+ let i;
220
+ for (i = 0; i < childScopes.length; i++) {
221
+ let childScope = childScopes[i];
222
+ let childScopeBlock = childScope.block;
223
+ if (
224
+ // function handleChange() {}
225
+ (fnNode.type === 'FunctionDeclaration' &&
226
+ childScopeBlock === fnNode) ||
227
+ // const handleChange = () => {}
228
+ // const handleChange = function() {}
229
+ (fnNode.type === 'VariableDeclarator' &&
230
+ childScopeBlock.parent === fnNode)
231
+ ) {
232
+ // Found it!
233
+ fnScope = childScope;
234
+ break;
235
+ }
236
+ }
237
+ if (fnScope == null) {
238
+ return false;
239
+ }
240
+ // Does this function capture any values
241
+ // that are in pure scopes (aka render)?
242
+ for (i = 0; i < fnScope.through.length; i++) {
243
+ const ref = fnScope.through[i];
244
+ if (ref.resolved == null) {
245
+ continue;
246
+ }
247
+ if (
248
+ pureScopes.has(ref.resolved.scope) &&
249
+ // Static values are fine though,
250
+ // although we won't check functions deeper.
251
+ !memoizedIsStaticKnownHookValue(ref.resolved)
252
+ ) {
253
+ return false;
254
+ }
255
+ }
256
+ // If we got here, this function doesn't capture anything
257
+ // from render--or everything it captures is known static.
258
+ return true;
259
+ }
260
+
261
+ // Remember such values. Avoid re-running extra checks on them.
262
+ const memoizedIsStaticKnownHookValue = memoizeWithWeakMap(
263
+ isStaticKnownHookValue,
264
+ staticKnownValueCache,
265
+ );
266
+ const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(
267
+ isFunctionWithoutCapturedValues,
268
+ functionWithoutCapturedValueCache,
269
+ );
270
+
271
// These are usually mistaken. Collect them.
272
const currentRefsInEffectCleanup = new Map();
273
@@ -172,7 +335,10 @@ export default {
335
// Add the dependency to a map so we can make sure it is referenced
336
// again in our dependencies array. Remember whether it's static.
337
if (!dependencies.has(dependency)) {
175
- const isStatic = isDefinitelyStaticDependency(reference);
338
+ const resolved = reference.resolved;
339
+ const isStatic =
340
+ memoizedIsStaticKnownHookValue(resolved) ||
341
+ memoizedIsFunctionWithoutCapturedValues(resolved);
342
dependencies.set(dependency, {
343
isStatic,
344
reference,
@@ -778,83 +944,6 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
944
}
945
}
946
781
-// const [state, setState] = useState() / React.useState()
782
-// ^^^ true for this reference
783
-// const [state, dispatch] = useReducer() / React.useReducer()
784
-// ^^^ true for this reference
785
-// const ref = useRef()
786
-// ^^^ true for this reference
787
-// False for everything else.
788
-function isDefinitelyStaticDependency(reference) {
789
- // This function is written defensively because I'm not sure about corner cases.
790
- // TODO: we can strengthen this if we're sure about the types.
791
- const resolved = reference.resolved;
792
- if (resolved == null || !Array.isArray(resolved.defs)) {
793
- return false;
794
- }
795
- const def = resolved.defs[0];
796
- if (def == null) {
797
- return false;
798
- }
799
- // Look for `let stuff = ...`
800
- if (def.node.type !== 'VariableDeclarator') {
801
- return false;
802
- }
803
- const init = def.node.init;
804
- if (init == null) {
805
- return false;
806
- }
807
- // Detect primitive constants
808
- // const foo = 42
809
- const declaration = def.node.parent;
810
- if (
811
- declaration.kind === 'const' &&
812
- init.type === 'Literal' &&
813
- (typeof init.value === 'string' ||
814
- typeof init.value === 'number' ||
815
- init.value === null)
816
- ) {
817
- // Definitely static
818
- return true;
819
- }
820
- // Detect known Hook calls
821
- // const [_, setState] = useState()
822
- if (init.type !== 'CallExpression') {
823
- return false;
824
- }
825
- let callee = init.callee;
826
- // Step into `= React.something` initializer.
827
- if (
828
- callee.type === 'MemberExpression' &&
829
- callee.object.name === 'React' &&
830
- callee.property != null &&
831
- !callee.computed
832
- ) {
833
- callee = callee.property;
834
- }
835
- if (callee.type !== 'Identifier') {
836
- return false;
837
- }
838
- const id = def.node.id;
839
- if (callee.name === 'useRef' && id.type === 'Identifier') {
840
- // useRef() return value is static.
841
- return true;
842
- } else if (callee.name === 'useState' || callee.name === 'useReducer') {
843
- // Only consider second value in initializing tuple static.
844
- if (
845
- id.type === 'ArrayPattern' &&
846
- id.elements.length === 2 &&
847
- Array.isArray(reference.resolved.identifiers) &&
848
- // Is second tuple value the same reference we're checking?
849
- id.elements[1] === reference.resolved.identifiers[0]
850
- ) {
851
- return true;
852
- }
853
- }
854
- // By default assume it's dynamic.
855
- return false;
856
-}
857
-
947
/**
948
* ESLint won't assign node.parent to references from context.getScope()
949
*