Skip to content

Commit 53e622c

Browse files
authored
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
1 parent b7d18c4 commit 53e622c

File tree

6 files changed

+170
-158
lines changed

6 files changed

+170
-158
lines changed

.eslintrc.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ module.exports = {
1616
// Stop ESLint from looking for a configuration file in parent folders
1717
root: true,
1818

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+
],
2026

2127
parser: 'babel-eslint',
2228
parserOptions: {
@@ -91,6 +97,9 @@ module.exports = {
9197
// You can disable this rule for code that isn't shipped (e.g. build scripts and tests).
9298
'no-for-of-loops/no-for-of-loops': ERROR,
9399

100+
// Prevent function declarations after return statements
101+
'no-function-declare-after-return/no-function-declare-after-return': ERROR,
102+
94103
// CUSTOM RULES
95104
// the second argument of warning/invariant should be a literal string
96105
'react-internal/no-primitive-constructors': ERROR,

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"eslint-plugin-flowtype": "^2.25.0",
5555
"eslint-plugin-jest": "^22.15.0",
5656
"eslint-plugin-no-for-of-loops": "^1.0.0",
57+
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
5758
"eslint-plugin-react": "^6.7.1",
5859
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
5960
"fbjs-scripts": "1.2.0",

packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js

+147-148
Original file line numberDiff line numberDiff line change
@@ -86,154 +86,6 @@ export default {
8686
return result;
8787
};
8888
}
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-
23789
/**
23890
* Visitor for both function expressions and arrow function expressions.
23991
*/
@@ -1251,6 +1103,153 @@ export default {
12511103
],
12521104
});
12531105
}
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+
};
12541253
},
12551254
};
12561255

packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ function removeChild(parent, child) {
6060

6161
const RCTUIManager = {
6262
__dumpHierarchyForJestTestsOnly: function() {
63-
return roots.map(tag => dumpSubtree(tag, 0)).join('\n');
64-
6563
function dumpSubtree(tag, indent) {
6664
const info = views.get(tag);
6765
let out = '';
@@ -73,6 +71,7 @@ const RCTUIManager = {
7371
}
7472
return out;
7573
}
74+
return roots.map(tag => dumpSubtree(tag, 0)).join('\n');
7675
},
7776
clearJSResponder: jest.fn(),
7877
createView: jest.fn(function createView(reactTag, viewName, rootTag, props) {

packages/react-native-renderer/src/legacy-events/SyntheticEvent.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,6 @@ addEventPoolingTo(SyntheticEvent);
259259
* @return {object} defineProperty object
260260
*/
261261
function getPooledWarningPropertyDefinition(propName, getVal) {
262-
const isFunction = typeof getVal === 'function';
263-
return {
264-
configurable: true,
265-
set: set,
266-
get: get,
267-
};
268-
269262
function set(val) {
270263
const action = isFunction ? 'setting the method' : 'setting the property';
271264
warn(action, 'This is effectively a no-op');
@@ -296,6 +289,12 @@ function getPooledWarningPropertyDefinition(propName, getVal) {
296289
);
297290
}
298291
}
292+
const isFunction = typeof getVal === 'function';
293+
return {
294+
configurable: true,
295+
set: set,
296+
get: get,
297+
};
299298
}
300299

301300
function createOrGetPooledEvent(

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -5407,6 +5407,11 @@ eslint-plugin-no-for-of-loops@^1.0.0:
54075407
resolved "https://registry.yarnpkg.com/eslint-plugin-no-for-of-loops/-/eslint-plugin-no-for-of-loops-1.0.1.tgz#2ee3732d50c642b8d1bfacedbfe3b28f86222369"
54085408
integrity sha512-uCotzBHt2W+HbLw2srRmqDJHOPbJGzeVLstKh8YyxS3ppduq2P50qdpJfHKoD+UGbnqA/zhy8NRgPH6p0y8bnA==
54095409

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+
54105415
eslint-plugin-no-unsanitized@3.1.2:
54115416
version "3.1.2"
54125417
resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-3.1.2.tgz#a54724e0b81d43279bb1f8f5e1d82c97da78c858"

0 commit comments

Comments
 (0)