@samitouri / QOS-React / commits / fa5d4ee43b

[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 *