Fix instances of function declaration after return (#19733)
* Add ESLint plugin to check for any function declare after return * Refactor code to move function declarations before return and fix failing lint
Bhumij Gupta committed
Sep 1, 2020 at 18:25 UTC
53e622ca7f643cc9d18c9c4896b68ab14549e292
6 files changed
+170
-158
.eslintrc.js
+10
-1
@@ -16,7 +16,13 @@ module.exports = {
16
// Stop ESLint from looking for a configuration file in parent folders
17
root: true,
18
19
- plugins: ['jest', 'no-for-of-loops', 'react', 'react-internal'],
19
+ plugins: [
20
+ 'jest',
21
+ 'no-for-of-loops',
22
+ 'no-function-declare-after-return',
23
+ 'react',
24
+ 'react-internal',
25
+ ],
26
27
parser: 'babel-eslint',
28
parserOptions: {
@@ -91,6 +97,9 @@ module.exports = {
97
// You can disable this rule for code that isn't shipped (e.g. build scripts and tests).
98
'no-for-of-loops/no-for-of-loops': ERROR,
99
100
+ // Prevent function declarations after return statements
101
+ 'no-function-declare-after-return/no-function-declare-after-return': ERROR,
102
+
103
// CUSTOM RULES
104
// the second argument of warning/invariant should be a literal string
105
'react-internal/no-primitive-constructors': ERROR,
package.json
+1
@@ -54,6 +54,7 @@
54
"eslint-plugin-flowtype": "^2.25.0",
55
"eslint-plugin-jest": "^22.15.0",
56
"eslint-plugin-no-for-of-loops": "^1.0.0",
57
+ "eslint-plugin-no-function-declare-after-return": "^1.0.0",
58
"eslint-plugin-react": "^6.7.1",
59
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
60
"fbjs-scripts": "1.2.0",
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+147
-148
@@ -86,154 +86,6 @@ export default {
86
return result;
87
};
88
}
89
-
90
- return {
91
- CallExpression: visitCallExpression,
92
- };
93
-
94
- function visitCallExpression(node) {
95
- const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
96
- if (callbackIndex === -1) {
97
- // Not a React Hook call that needs deps.
98
- return;
99
- }
100
- const callback = node.arguments[callbackIndex];
101
- const reactiveHook = node.callee;
102
- const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
103
- const declaredDependenciesNode = node.arguments[callbackIndex + 1];
104
- const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
105
-
106
- // Check the declared dependencies for this reactive hook. If there is no
107
- // second argument then the reactive callback will re-run on every render.
108
- // So no need to check for dependency inclusion.
109
- if (!declaredDependenciesNode && !isEffect) {
110
- // These are only used for optimization.
111
- if (
112
- reactiveHookName === 'useMemo' ||
113
- reactiveHookName === 'useCallback'
114
- ) {
115
- // TODO: Can this have a suggestion?
116
- reportProblem({
117
- node: reactiveHook,
118
- message:
119
- `React Hook ${reactiveHookName} does nothing when called with ` +
120
- `only one argument. Did you forget to pass an array of ` +
121
- `dependencies?`,
122
- });
123
- }
124
- return;
125
- }
126
-
127
- switch (callback.type) {
128
- case 'FunctionExpression':
129
- case 'ArrowFunctionExpression':
130
- visitFunctionWithDependencies(
131
- callback,
132
- declaredDependenciesNode,
133
- reactiveHook,
134
- reactiveHookName,
135
- isEffect,
136
- );
137
- return; // Handled
138
- case 'Identifier':
139
- if (!declaredDependenciesNode) {
140
- // No deps, no problems.
141
- return; // Handled
142
- }
143
- // The function passed as a callback is not written inline.
144
- // But perhaps it's in the dependencies array?
145
- if (
146
- declaredDependenciesNode.elements &&
147
- declaredDependenciesNode.elements.some(
148
- el => el && el.type === 'Identifier' && el.name === callback.name,
149
- )
150
- ) {
151
- // If it's already in the list of deps, we don't care because
152
- // this is valid regardless.
153
- return; // Handled
154
- }
155
- // We'll do our best effort to find it, complain otherwise.
156
- const variable = context.getScope().set.get(callback.name);
157
- if (variable == null || variable.defs == null) {
158
- // If it's not in scope, we don't care.
159
- return; // Handled
160
- }
161
- // The function passed as a callback is not written inline.
162
- // But it's defined somewhere in the render scope.
163
- // We'll do our best effort to find and check it, complain otherwise.
164
- const def = variable.defs[0];
165
- if (!def || !def.node) {
166
- break; // Unhandled
167
- }
168
- if (def.type !== 'Variable' && def.type !== 'FunctionName') {
169
- // Parameter or an unusual pattern. Bail out.
170
- break; // Unhandled
171
- }
172
- switch (def.node.type) {
173
- case 'FunctionDeclaration':
174
- // useEffect(() => { ... }, []);
175
- visitFunctionWithDependencies(
176
- def.node,
177
- declaredDependenciesNode,
178
- reactiveHook,
179
- reactiveHookName,
180
- isEffect,
181
- );
182
- return; // Handled
183
- case 'VariableDeclarator':
184
- const init = def.node.init;
185
- if (!init) {
186
- break; // Unhandled
187
- }
188
- switch (init.type) {
189
- // const effectBody = () => {...};
190
- // useEffect(effectBody, []);
191
- case 'ArrowFunctionExpression':
192
- case 'FunctionExpression':
193
- // We can inspect this function as if it were inline.
194
- visitFunctionWithDependencies(
195
- init,
196
- declaredDependenciesNode,
197
- reactiveHook,
198
- reactiveHookName,
199
- isEffect,
200
- );
201
- return; // Handled
202
- }
203
- break; // Unhandled
204
- }
205
- break; // Unhandled
206
- default:
207
- // useEffect(generateEffectBody(), []);
208
- reportProblem({
209
- node: reactiveHook,
210
- message:
211
- `React Hook ${reactiveHookName} received a function whose dependencies ` +
212
- `are unknown. Pass an inline function instead.`,
213
- });
214
- return; // Handled
215
- }
216
-
217
- // Something unusual. Fall back to suggesting to add the body itself as a dep.
218
- reportProblem({
219
- node: reactiveHook,
220
- message:
221
- `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` +
222
- `Either include it or remove the dependency array.`,
223
- suggest: [
224
- {
225
- desc: `Update the dependencies array to be: [${callback.name}]`,
226
- fix(fixer) {
227
- return fixer.replaceText(
228
- declaredDependenciesNode,
229
- `[${callback.name}]`,
230
- );
231
- },
232
- },
233
- ],
234
- });
235
- }
236
-
89
/**
90
* Visitor for both function expressions and arrow function expressions.
91
*/
@@ -1251,6 +1103,153 @@ export default {
1103
],
1104
});
1105
}
1106
+
1107
+ function visitCallExpression(node) {
1108
+ const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
1109
+ if (callbackIndex === -1) {
1110
+ // Not a React Hook call that needs deps.
1111
+ return;
1112
+ }
1113
+ const callback = node.arguments[callbackIndex];
1114
+ const reactiveHook = node.callee;
1115
+ const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
1116
+ const declaredDependenciesNode = node.arguments[callbackIndex + 1];
1117
+ const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
1118
+
1119
+ // Check the declared dependencies for this reactive hook. If there is no
1120
+ // second argument then the reactive callback will re-run on every render.
1121
+ // So no need to check for dependency inclusion.
1122
+ if (!declaredDependenciesNode && !isEffect) {
1123
+ // These are only used for optimization.
1124
+ if (
1125
+ reactiveHookName === 'useMemo' ||
1126
+ reactiveHookName === 'useCallback'
1127
+ ) {
1128
+ // TODO: Can this have a suggestion?
1129
+ reportProblem({
1130
+ node: reactiveHook,
1131
+ message:
1132
+ `React Hook ${reactiveHookName} does nothing when called with ` +
1133
+ `only one argument. Did you forget to pass an array of ` +
1134
+ `dependencies?`,
1135
+ });
1136
+ }
1137
+ return;
1138
+ }
1139
+
1140
+ switch (callback.type) {
1141
+ case 'FunctionExpression':
1142
+ case 'ArrowFunctionExpression':
1143
+ visitFunctionWithDependencies(
1144
+ callback,
1145
+ declaredDependenciesNode,
1146
+ reactiveHook,
1147
+ reactiveHookName,
1148
+ isEffect,
1149
+ );
1150
+ return; // Handled
1151
+ case 'Identifier':
1152
+ if (!declaredDependenciesNode) {
1153
+ // No deps, no problems.
1154
+ return; // Handled
1155
+ }
1156
+ // The function passed as a callback is not written inline.
1157
+ // But perhaps it's in the dependencies array?
1158
+ if (
1159
+ declaredDependenciesNode.elements &&
1160
+ declaredDependenciesNode.elements.some(
1161
+ el => el && el.type === 'Identifier' && el.name === callback.name,
1162
+ )
1163
+ ) {
1164
+ // If it's already in the list of deps, we don't care because
1165
+ // this is valid regardless.
1166
+ return; // Handled
1167
+ }
1168
+ // We'll do our best effort to find it, complain otherwise.
1169
+ const variable = context.getScope().set.get(callback.name);
1170
+ if (variable == null || variable.defs == null) {
1171
+ // If it's not in scope, we don't care.
1172
+ return; // Handled
1173
+ }
1174
+ // The function passed as a callback is not written inline.
1175
+ // But it's defined somewhere in the render scope.
1176
+ // We'll do our best effort to find and check it, complain otherwise.
1177
+ const def = variable.defs[0];
1178
+ if (!def || !def.node) {
1179
+ break; // Unhandled
1180
+ }
1181
+ if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1182
+ // Parameter or an unusual pattern. Bail out.
1183
+ break; // Unhandled
1184
+ }
1185
+ switch (def.node.type) {
1186
+ case 'FunctionDeclaration':
1187
+ // useEffect(() => { ... }, []);
1188
+ visitFunctionWithDependencies(
1189
+ def.node,
1190
+ declaredDependenciesNode,
1191
+ reactiveHook,
1192
+ reactiveHookName,
1193
+ isEffect,
1194
+ );
1195
+ return; // Handled
1196
+ case 'VariableDeclarator':
1197
+ const init = def.node.init;
1198
+ if (!init) {
1199
+ break; // Unhandled
1200
+ }
1201
+ switch (init.type) {
1202
+ // const effectBody = () => {...};
1203
+ // useEffect(effectBody, []);
1204
+ case 'ArrowFunctionExpression':
1205
+ case 'FunctionExpression':
1206
+ // We can inspect this function as if it were inline.
1207
+ visitFunctionWithDependencies(
1208
+ init,
1209
+ declaredDependenciesNode,
1210
+ reactiveHook,
1211
+ reactiveHookName,
1212
+ isEffect,
1213
+ );
1214
+ return; // Handled
1215
+ }
1216
+ break; // Unhandled
1217
+ }
1218
+ break; // Unhandled
1219
+ default:
1220
+ // useEffect(generateEffectBody(), []);
1221
+ reportProblem({
1222
+ node: reactiveHook,
1223
+ message:
1224
+ `React Hook ${reactiveHookName} received a function whose dependencies ` +
1225
+ `are unknown. Pass an inline function instead.`,
1226
+ });
1227
+ return; // Handled
1228
+ }
1229
+
1230
+ // Something unusual. Fall back to suggesting to add the body itself as a dep.
1231
+ reportProblem({
1232
+ node: reactiveHook,
1233
+ message:
1234
+ `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` +
1235
+ `Either include it or remove the dependency array.`,
1236
+ suggest: [
1237
+ {
1238
+ desc: `Update the dependencies array to be: [${callback.name}]`,
1239
+ fix(fixer) {
1240
+ return fixer.replaceText(
1241
+ declaredDependenciesNode,
1242
+ `[${callback.name}]`,
1243
+ );
1244
+ },
1245
+ },
1246
+ ],
1247
+ });
1248
+ }
1249
+
1250
+ return {
1251
+ CallExpression: visitCallExpression,
1252
+ };
1253
},
1254
};
1255
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js
+1
-2
@@ -60,8 +60,6 @@ function removeChild(parent, child) {
60
61
const RCTUIManager = {
62
__dumpHierarchyForJestTestsOnly: function() {
63
- return roots.map(tag => dumpSubtree(tag, 0)).join('\n');
64
-
63
function dumpSubtree(tag, indent) {
64
const info = views.get(tag);
65
let out = '';
@@ -73,6 +71,7 @@ const RCTUIManager = {
71
}
72
return out;
73
}
74
+ return roots.map(tag => dumpSubtree(tag, 0)).join('\n');
75
},
76
clearJSResponder: jest.fn(),
77
createView: jest.fn(function createView(reactTag, viewName, rootTag, props) {
packages/react-native-renderer/src/legacy-events/SyntheticEvent.js
+6
-7
@@ -259,13 +259,6 @@ addEventPoolingTo(SyntheticEvent);
259
* @return {object} defineProperty object
260
*/
261
function getPooledWarningPropertyDefinition(propName, getVal) {
262
- const isFunction = typeof getVal === 'function';
263
- return {
264
- configurable: true,
265
- set: set,
266
- get: get,
267
- };
268
-
262
function set(val) {
263
const action = isFunction ? 'setting the method' : 'setting the property';
264
warn(action, 'This is effectively a no-op');
@@ -296,6 +289,12 @@ function getPooledWarningPropertyDefinition(propName, getVal) {
289
);
290
}
291
}
292
+ const isFunction = typeof getVal === 'function';
293
+ return {
294
+ configurable: true,
295
+ set: set,
296
+ get: get,
297
+ };
298
}
299
300
function createOrGetPooledEvent(
yarn.lock
+5
@@ -5407,6 +5407,11 @@ eslint-plugin-no-for-of-loops@^1.0.0:
5407
resolved "https://registry.yarnpkg.com/eslint-plugin-no-for-of-loops/-/eslint-plugin-no-for-of-loops-1.0.1.tgz#2ee3732d50c642b8d1bfacedbfe3b28f86222369"
5408
integrity sha512-uCotzBHt2W+HbLw2srRmqDJHOPbJGzeVLstKh8YyxS3ppduq2P50qdpJfHKoD+UGbnqA/zhy8NRgPH6p0y8bnA==
5409
5410
+eslint-plugin-no-function-declare-after-return@^1.0.0:
5411
+ version "1.0.0"
5412
+ resolved "https://registry.yarnpkg.com/eslint-plugin-no-function-declare-after-return/-/eslint-plugin-no-function-declare-after-return-1.0.0.tgz#ddf01d71d27b37ced61ccb295c6ac0708d87b209"
5413
+ integrity sha512-/w6tuSK4kYSwHSlPtf36u/rQOG8YVPgl8a0bh4rV/ElZNaUGYOquY/hp4jaCnEmPta0aTpAkFEmSi6ZTd7wCEQ==
5414
+
5415
eslint-plugin-no-unsanitized@3.1.2:
5416
version "3.1.2"
5417
resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-3.1.2.tgz#a54724e0b81d43279bb1f8f5e1d82c97da78c858"