Skip to content

Commit a546444

Browse files
consistent-function-scoping: Fix inconsistent behavior (#2748)
1 parent a121aca commit a546444

File tree

3 files changed

+176
-11
lines changed

3 files changed

+176
-11
lines changed

docs/rules/consistent-function-scoping.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ export function doFoo(foo) {
2020
return doBar;
2121
}
2222

23+
function doFoo(foo) {
24+
const doBar = bar => {
25+
return bar === 'bar';
26+
};
27+
}
28+
29+
function doFoo() {
30+
// Does not capture anything from the scope, can be moved to the outer scope
31+
return bar => bar === 'bar';
32+
}
33+
34+
// Arrow functions in return statements are now also flagged
35+
export function someAction() {
36+
return dispatch => dispatch({type: 'SOME_TYPE'});
37+
}
38+
```
39+
40+
```js
2341
//
2442
function doBar(bar) {
2543
return bar === 'bar';
@@ -55,6 +73,12 @@ export function doFoo(foo) {
5573

5674
return doBar;
5775
}
76+
77+
const doBar = bar => bar === 'bar';
78+
79+
export function doFoo() {
80+
return doBar;
81+
}
5882
```
5983

6084
## Options

rules/consistent-function-scoping.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,43 @@ const isIife = node =>
108108
&& node.parent.type === 'CallExpression'
109109
&& node.parent.callee === node;
110110

111+
// Helper to walk up the chain to find the first non-arrow ancestor
112+
function getNonArrowAncestor(node) {
113+
let ancestor = node;
114+
while (ancestor && ancestor.type === 'ArrowFunctionExpression') {
115+
ancestor = ancestor.parent;
116+
}
117+
118+
return ancestor;
119+
}
120+
121+
// Helper to skip over a chain of ArrowFunctionExpression nodes
122+
function skipArrowFunctionChain(node) {
123+
let current = node;
124+
while (current.type === 'ArrowFunctionExpression') {
125+
current = current.parent;
126+
}
127+
128+
return current;
129+
}
130+
131+
function handleNestedArrowFunctions(parentNode, node) {
132+
// Skip over arrow function expressions when they are parents and we came from a ReturnStatement
133+
// This handles nested arrow functions: return next => action => { ... }
134+
// But only when we're in a return statement context
135+
if (parentNode.type === 'ArrowFunctionExpression' && node.type === 'ArrowFunctionExpression') {
136+
const ancestor = getNonArrowAncestor(parentNode);
137+
if (ancestor && ancestor.type === 'ReturnStatement') {
138+
parentNode = skipArrowFunctionChain(parentNode);
139+
if (parentNode.type === 'ReturnStatement') {
140+
parentNode = parentNode.parent;
141+
}
142+
}
143+
}
144+
145+
return parentNode;
146+
}
147+
111148
function checkNode(node, scopeManager) {
112149
const scope = scopeManager.acquire(node);
113150

@@ -128,7 +165,15 @@ function checkNode(node, scopeManager) {
128165
parentNode = parentNode.parent;
129166
}
130167

131-
if (parentNode.type === 'BlockStatement') {
168+
// Only skip ReturnStatement for arrow functions
169+
// Regular function expressions have different semantics and shouldn't be moved
170+
if (parentNode?.type === 'ReturnStatement' && node.type === 'ArrowFunctionExpression') {
171+
parentNode = parentNode.parent;
172+
}
173+
174+
parentNode = handleNestedArrowFunctions(parentNode, node);
175+
176+
if (parentNode?.type === 'BlockStatement') {
132177
parentNode = parentNode.parent;
133178
}
134179

test/consistent-function-scoping.js

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,6 @@ test({
137137
bar =>
138138
foo + bar;
139139
`,
140-
outdent`
141-
const doFoo = () => {
142-
return bar => bar;
143-
}
144-
`,
145-
outdent`
146-
function doFoo() {
147-
return bar => bar;
148-
}
149-
`,
150140
outdent`
151141
const doFoo = foo => {
152142
const doBar = bar => {
@@ -380,6 +370,27 @@ test({
380370
inner();
381371
}
382372
`,
373+
// #931 - valid cases: arrow functions that capture variables from outer scope
374+
outdent`
375+
function createHandler(config) {
376+
return (event) => {
377+
return config.handle(event);
378+
};
379+
}
380+
`,
381+
outdent`
382+
export function middleware(store) {
383+
return next => action => {
384+
store.dispatch(action);
385+
return next(action);
386+
};
387+
}
388+
`,
389+
outdent`
390+
function createThunk(type) {
391+
return (dispatch) => dispatch({ type });
392+
}
393+
`,
383394
// Should ignore functions inside arrow functions
384395
{
385396
code: outdent`
@@ -807,6 +818,91 @@ test({
807818
errors: [createError('function \'inner\'')],
808819
options: [{checkArrowFunctions: false}],
809820
},
821+
// #931 - arrow functions returned from function declarations should be flagged
822+
{
823+
code: outdent`
824+
export function someAction() {
825+
return (dispatch) => dispatch({ type: 'SOME_TYPE' });
826+
}
827+
`,
828+
errors: [createError('arrow function')],
829+
},
830+
{
831+
code: outdent`
832+
function outer() {
833+
return bar => bar;
834+
}
835+
`,
836+
errors: [createError('arrow function')],
837+
},
838+
{
839+
code: outdent`
840+
const outer = function() {
841+
return bar => bar;
842+
};
843+
`,
844+
errors: [createError('arrow function')],
845+
},
846+
{
847+
code: outdent`
848+
export function thunk() {
849+
return async (dispatch, getState) => {
850+
return dispatch({ type: 'ACTION' });
851+
};
852+
}
853+
`,
854+
errors: [createError('async arrow function')],
855+
},
856+
{
857+
code: outdent`
858+
function middleware() {
859+
return next => action => {
860+
return next(action);
861+
};
862+
}
863+
`,
864+
errors: [
865+
createError('arrow function'),
866+
createError('arrow function'),
867+
],
868+
},
869+
{
870+
code: outdent`
871+
const doFoo = () => {
872+
return bar => bar;
873+
}
874+
`,
875+
errors: [createError('arrow function')],
876+
},
877+
// Additional test cases for edge cases
878+
{
879+
code: outdent`
880+
class TestClass {
881+
method() {
882+
return x => x * 2;
883+
}
884+
}
885+
`,
886+
errors: [createError('arrow function')],
887+
},
888+
{
889+
code: outdent`
890+
const obj = {
891+
method() {
892+
return y => y + 1;
893+
}
894+
};
895+
`,
896+
errors: [createError('arrow function')],
897+
},
898+
{
899+
code: outdent`
900+
export default function() {
901+
return z => z.toString();
902+
}
903+
`,
904+
errors: [createError('arrow function')],
905+
},
810906
],
811907
});
812908

0 commit comments

Comments
 (0)