Repository: sindresorhus/eslint-plugin-unicorn (JavaScript)

Issue:
False negative? for unicorn/consistent-function-scoping
I like this new rule and I've made change to my code. Here's one WIP snippet where it proved to be problematic though.

```ts
type Data<T> = T extends 'error' ? Error : Record<string, unknown> | unknown[]

type Method = 'info' | 'error'

export function createLogger(name: string) {
    // Two lint errors are on the next line.
    const log = <T extends Method>(method: T) => (data: Data<T>) => {
        try {
            // eslint-disable-next-line no-console
            console[method](JSON.stringify({ name, data }))
        } catch (error) {
            console.error(error)
        }
    }

    return {
        info: log('info'),
        error: log('error'),
    }
}
```

```
6:17  error  Move arrow function to the outer scope  unicorn/consistent-function-scoping
6:50  error  Move arrow function to the outer scope  unicorn/consistent-function-scoping
```

Could this be a bug in `@typescript-eslint`?

@typescript-eslint/eslint-plugin@2.3.0
@typescript-eslint/parser@2.3.0
eslint-plugin-unicorn@11.0.0
Is this a `consistent-function-scoping` bug?
I was working on #387 , and found our code 

https://github.com/sindresorhus/eslint-plugin-unicorn/blob/92f2da10fd97841e8d674c17f397c39c24bf9fe2/test/integration/test.js#L57-L65

is reported by `consistent-function-scoping`.

This code looks good to me, is this a bug or did I miss something?

/cc @MrHen 


Discussion:
Yeah, that doesn't look right. ~~I'll try to take a look at it tomorrow tonight.~~
I have a very similar case.

```ts
test('it works', async function(assert) {
  function assertHeader(assertions) {
    for (const [key, value] of Object.entries(assertions)) {
      assert.strictEqual(
        native[key],
        value,
        `'native.${key}' was updated to '${value}'.`
      );
    }
  }

   // ...
});
```

If I lift the `assert` reference out of the `for` loop, the false negative vanishes.

```diff
test('it works', async function(assert) {
  function assertHeader(assertions) {
+   assert;
    for (const [key, value] of Object.entries(assertions)) {
      assert.strictEqual(
        native[key],
        value,
        `'native.${key}' was updated to '${value}'.`
      );
    }
  }

   // ...
});
```

I did not look at the rule source code yet, but it looks like only the root function body scope is checked for references.
Have the similar issue at line 2 with the following:

```ts
export function a(x: number) {
  const b = (y: number) => (z: number): number => x + y + z;
  return b(1)(2);
}
```
I am seeing this issue in a .js file.

![Screen Shot 2020-10-14 at 2 00 49 PM](https://user-images.githubusercontent.com/7051557/96044913-04fa7100-0e26-11eb-9c7e-0986d6fddec5.png)


"@typescript-eslint/eslint-plugin": "4.4.1",
 "@typescript-eslint/parser": "4.4.1",
 "eslint-plugin-unicorn": "22.0.0",

Looks like #372 at first glance
Definitely looks similar to #372 but I'll keep the use case around just to be sure.
Seems like another case for an issue:
```js
export const canStepForward = ([X, Y]) => ([x, y]) => direction => {
  switch (direction) {
    case 0:
      return y !== 0
    case 1:
      return x !== X - 1
    case 2:
      return y !== Y - 1
    case 3:
      return x !== 0
    default:
      throw new Error('unknown direction')
  }
}
```

> `1:43`  `error`  Move arrow function to the outer scope  `unicorn/consistent-function-scoping`

`1:43` points to second function, e.g. `([x, y]) => direction =>`

Seems like currying is not handled properly because refactoring to this removes ESLint error:
```js
export const canStepForward = ([X, Y]) => {
  return ([x, y]) => direction => {
    switch (direction) { ... }
  }
}
```



Patch:
diff --git a/rules/consistent-function-scoping.js b/rules/consistent-function-scoping.js
index 6a96d281c6..3cff85bbed 100644
--- a/rules/consistent-function-scoping.js
+++ b/rules/consistent-function-scoping.js
@@ -4,12 +4,16 @@ const getDocumentationUrl = require('./utils/get-documentation-url');
 const MESSAGE_ID_ARROW = 'ArrowFunctionExpression';
 const MESSAGE_ID_FUNCTION = 'FunctionDeclaration';
 
+const getReferences = scope => scope.references.concat(
+	...scope.childScopes.map(scope => getReferences(scope))
+);
+
 function checkReferences(scope, parent, scopeManager) {
 	if (!scope) {
 		return false;
 	}
 
-	const {references} = scope;
+	const references = getReferences(scope);
 	if (!references || references.length === 0) {
 		return false;
 	}


Test patch:
diff --git a/test/consistent-function-scoping.js b/test/consistent-function-scoping.js
index ad5eb3cb68..e2eee2a121 100644
--- a/test/consistent-function-scoping.js
+++ b/test/consistent-function-scoping.js
@@ -4,17 +4,19 @@ import {outdent} from 'outdent';
 import rule from '../rules/consistent-function-scoping';
 
 const ruleTester = avaRuleTester(test, {
-	env: {
-		es6: true
-	},
 	parserOptions: {
-		ecmaVersion: 2018,
+		sourceType: 'module',
+		ecmaVersion: 2020,
 		ecmaFeatures: {
 			jsx: true
 		}
 	}
 });
 
+const typescriptRuleTester = avaRuleTester(test, {
+	parser: require.resolve('@typescript-eslint/parser')
+});
+
 const arrowError = {
 	ruleId: 'consistent-function-scoping',
 	messageId: 'ArrowFunctionExpression'
@@ -188,6 +190,35 @@ ruleTester.run('consistent-function-scoping', rule, {
 				}
 				return Bar;
 			};
+		`,
+		// #391
+		outdent`
+			const enrichErrors = (packageName, cliArgs, f) => async (...args) => {
+				try {
+					return await f(...args);
+				} catch (error) {
+					error.packageName = packageName;
+					error.cliArgs = cliArgs;
+					throw error;
+				}
+			};
+		`,
+		// #391 comment https://github.com/sindresorhus/eslint-plugin-unicorn/issues/391#issuecomment-536916771
+		outdent`
+			export const canStepForward = ([X, Y]) => ([x, y]) => direction => {
+				switch (direction) {
+					case 0:
+						return y !== 0
+					case 1:
+						return x !== X - 1
+					case 2:
+						return y !== Y - 1
+					case 3:
+						return x !== 0
+					default:
+						throw new Error('unknown direction')
+				}
+			}
 		`
 	],
 	invalid: [
@@ -309,3 +340,54 @@ ruleTester.run('consistent-function-scoping', rule, {
 		}
 	]
 });
+
+typescriptRuleTester.run('consistent-function-scoping', rule, {
+	valid: [
+		// #372
+		outdent`
+			type Data<T> = T extends 'error' ? Error : Record<string, unknown> | unknown[]
+
+			type Method = 'info' | 'error'
+
+			export function createLogger(name: string) {
+					// Two lint errors are on the next line.
+					const log = <T extends Method>(method: T) => (data: Data<T>) => {
+							try {
+									// eslint-disable-next-line no-console
+									console[method](JSON.stringify({ name, data }))
+							} catch (error) {
+									console.error(error)
+							}
+					}
+
+					return {
+							info: log('info'),
+							error: log('error'),
+					}
+			}
+		`,
+		// #372 comment https://github.com/sindresorhus/eslint-plugin-unicorn/issues/372#issuecomment-546915612
+		outdent`
+			test('it works', async function(assert) {
+				function assertHeader(assertions) {
+					for (const [key, value] of Object.entries(assertions)) {
+						assert.strictEqual(
+							native[key],
+							value
+						);
+					}
+				}
+
+				// ...
+			});
+		`,
+		// #372 comment https://github.com/sindresorhus/eslint-plugin-unicorn/issues/372#issuecomment-546915612
+		outdent`
+			export function a(x: number) {
+				const b = (y: number) => (z: number): number => x + y + z;
+				return b(1)(2);
+			}
+		`
+	],
+	invalid: []
+});
diff --git a/test/lint/lint.js b/test/lint/lint.js
index 5eaa3d459a..b7d7b99fab 100755
--- a/test/lint/lint.js
+++ b/test/lint/lint.js
@@ -11,10 +11,6 @@ const unicornRules = new Map(Object.entries(unicorn.rules));
 
 const cli = new CLIEngine({
 	baseConfig: recommended,
-	rules: {
-		// TODO: remove this override, when #391 is fixed
-		'unicorn/consistent-function-scoping': 'off'
-	},
 	useEslintrc: false,
 	fix
 });