UNPKG

4.97 kBJavaScriptView Raw
1'use strict';
2const {getFunctionHeadLocation, getFunctionNameWithKind} = require('eslint-utils');
3const getDocumentationUrl = require('./utils/get-documentation-url');
4const getReferences = require('./utils/get-references');
5
6const MESSAGE_ID = 'consistent-function-scoping';
7
8const isSameScope = (scope1, scope2) =>
9 scope1 && scope2 && (scope1 === scope2 || scope1.block === scope2.block);
10
11function checkReferences(scope, parent, scopeManager) {
12 const hitReference = references => references.some(reference => {
13 if (isSameScope(parent, reference.from)) {
14 return true;
15 }
16
17 const {resolved} = reference;
18 const [definition] = resolved.defs;
19
20 // Skip recursive function name
21 if (definition && definition.type === 'FunctionName' && resolved.name === definition.name.name) {
22 return false;
23 }
24
25 return isSameScope(parent, resolved.scope);
26 });
27
28 const hitDefinitions = definitions => definitions.some(definition => {
29 const scope = scopeManager.acquire(definition.node);
30 return isSameScope(parent, scope);
31 });
32
33 // This check looks for neighboring function definitions
34 const hitIdentifier = identifiers => identifiers.some(identifier => {
35 // Only look at identifiers that live in a FunctionDeclaration
36 if (
37 !identifier.parent ||
38 identifier.parent.type !== 'FunctionDeclaration'
39 ) {
40 return false;
41 }
42
43 const identifierScope = scopeManager.acquire(identifier);
44
45 // If we have a scope, the earlier checks should have worked so ignore them here
46 if (identifierScope) {
47 return false;
48 }
49
50 const identifierParentScope = scopeManager.acquire(identifier.parent);
51 if (!identifierParentScope) {
52 return false;
53 }
54
55 // Ignore identifiers from our own scope
56 if (isSameScope(scope, identifierParentScope)) {
57 return false;
58 }
59
60 // Look at the scope above the function definition to see if lives
61 // next to the reference being checked
62 return isSameScope(parent, identifierParentScope.upper);
63 });
64
65 return getReferences(scope)
66 .map(({resolved}) => resolved)
67 .filter(Boolean)
68 .some(variable =>
69 hitReference(variable.references) ||
70 hitDefinitions(variable.defs) ||
71 hitIdentifier(variable.identifiers)
72 );
73}
74
75// https://reactjs.org/docs/hooks-reference.html
76const reactHooks = new Set([
77 'useState',
78 'useEffect',
79 'useContext',
80 'useReducer',
81 'useCallback',
82 'useMemo',
83 'useRef',
84 'useImperativeHandle',
85 'useLayoutEffect',
86 'useDebugValue'
87]);
88const isReactHook = scope =>
89 scope.block &&
90 scope.block.parent &&
91 scope.block.parent.callee &&
92 scope.block.parent.callee.type === 'Identifier' &&
93 reactHooks.has(scope.block.parent.callee.name);
94
95const isArrowFunctionWithThis = scope =>
96 scope.type === 'function' &&
97 scope.block &&
98 scope.block.type === 'ArrowFunctionExpression' &&
99 (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope)));
100
101const iifeFunctionTypes = new Set([
102 'FunctionExpression',
103 'ArrowFunctionExpression'
104]);
105const isIife = node => node &&
106 iifeFunctionTypes.has(node.type) &&
107 node.parent &&
108 node.parent.type === 'CallExpression' &&
109 node.parent.callee === node;
110
111function checkNode(node, scopeManager) {
112 const scope = scopeManager.acquire(node);
113
114 if (!scope || isArrowFunctionWithThis(scope)) {
115 return true;
116 }
117
118 let parentNode = node.parent;
119 if (!parentNode) {
120 return true;
121 }
122
123 // Skip over junk like the block statement inside of a function declaration
124 // or the various pieces of an arrow function.
125
126 if (parentNode.type === 'VariableDeclarator') {
127 parentNode = parentNode.parent;
128 }
129
130 if (parentNode.type === 'VariableDeclaration') {
131 parentNode = parentNode.parent;
132 }
133
134 if (parentNode.type === 'BlockStatement') {
135 parentNode = parentNode.parent;
136 }
137
138 const parentScope = scopeManager.acquire(parentNode);
139 if (
140 !parentScope ||
141 parentScope.type === 'global' ||
142 isReactHook(parentScope) ||
143 isIife(parentNode)
144 ) {
145 return true;
146 }
147
148 return checkReferences(scope, parentScope, scopeManager);
149}
150
151const create = context => {
152 const sourceCode = context.getSourceCode();
153 const {scopeManager} = sourceCode;
154
155 const functions = [];
156 let hasJsx = false;
157
158 return {
159 'ArrowFunctionExpression, FunctionDeclaration': node => functions.push(node),
160 JSXElement: () => {
161 // Turn off this rule if we see a JSX element because scope
162 // references does not include JSXElement nodes.
163 hasJsx = true;
164 },
165 ':matches(ArrowFunctionExpression, FunctionDeclaration):exit': node => {
166 if (!hasJsx && !checkNode(node, scopeManager)) {
167 context.report({
168 node,
169 loc: getFunctionHeadLocation(node, sourceCode),
170 messageId: MESSAGE_ID,
171 data: {
172 functionNameWithKind: getFunctionNameWithKind(node)
173 }
174 });
175 }
176
177 functions.pop();
178 if (functions.length === 0) {
179 hasJsx = false;
180 }
181 }
182 };
183};
184
185module.exports = {
186 create,
187 meta: {
188 type: 'suggestion',
189 docs: {
190 url: getDocumentationUrl(__filename)
191 },
192 messages: {
193 [MESSAGE_ID]: 'Move {{functionNameWithKind}} to the outer scope.'
194 }
195 }
196};