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