UNPKG

5.08 kBJavaScriptView Raw
1'use strict';
2const getDocumentationUrl = require('./utils/get-documentation-url');
3const getReferences = require('./utils/get-references');
4
5const MESSAGE_ID_NAMED = 'named';
6const MESSAGE_ID_ANONYMOUS = 'anonymous';
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
101function checkNode(node, scopeManager) {
102 const scope = scopeManager.acquire(node);
103
104 if (!scope || isArrowFunctionWithThis(scope)) {
105 return true;
106 }
107
108 let parentNode = node.parent;
109 if (!parentNode) {
110 return true;
111 }
112
113 // Skip over junk like the block statement inside of a function declaration
114 // or the various pieces of an arrow function.
115
116 if (parentNode.type === 'VariableDeclarator') {
117 parentNode = parentNode.parent;
118 }
119
120 if (parentNode.type === 'VariableDeclaration') {
121 parentNode = parentNode.parent;
122 }
123
124 if (parentNode.type === 'BlockStatement') {
125 parentNode = parentNode.parent;
126 }
127
128 const parentScope = scopeManager.acquire(parentNode);
129 if (!parentScope || parentScope.type === 'global' || isReactHook(parentScope)) {
130 return true;
131 }
132
133 return checkReferences(scope, parentScope, scopeManager);
134}
135
136const create = context => {
137 const sourceCode = context.getSourceCode();
138 const {scopeManager} = sourceCode;
139
140 const functions = [];
141 let hasJsx = false;
142
143 return {
144 'ArrowFunctionExpression, FunctionDeclaration': node => functions.push(node),
145 JSXElement: () => {
146 // Turn off this rule if we see a JSX element because scope
147 // references does not include JSXElement nodes.
148 hasJsx = true;
149 },
150 ':matches(ArrowFunctionExpression, FunctionDeclaration):exit': node => {
151 if (!hasJsx && !checkNode(node, scopeManager)) {
152 const functionType = node.type === 'ArrowFunctionExpression' ? 'arrow function' : 'function';
153 let functionName = '';
154 if (node.id) {
155 functionName = node.id.name;
156 } else if (
157 node.parent &&
158 node.parent.type === 'VariableDeclarator' &&
159 node.parent.id &&
160 node.parent.id.type === 'Identifier'
161 ) {
162 functionName = node.parent.id.name;
163 }
164
165 context.report({
166 node,
167 messageId: functionName ? MESSAGE_ID_NAMED : MESSAGE_ID_ANONYMOUS,
168 data: {
169 functionType,
170 functionName
171 }
172 });
173 }
174
175 functions.pop();
176 if (functions.length === 0) {
177 hasJsx = false;
178 }
179 }
180 };
181};
182
183module.exports = {
184 create,
185 meta: {
186 type: 'suggestion',
187 docs: {
188 url: getDocumentationUrl(__filename)
189 },
190 messages: {
191 [MESSAGE_ID_NAMED]: 'Move {{functionType}} `{{functionName}}` to the outer scope.',
192 [MESSAGE_ID_ANONYMOUS]: 'Move {{functionType}} to the outer scope.'
193 }
194 }
195};