consistent-function-scoping.js 5.3 KB

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