123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- 'use strict';
- const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils');
- const getReferences = require('./utils/get-references.js');
- const {isNodeMatches} = require('./utils/is-node-matches.js');
- const MESSAGE_ID = 'consistent-function-scoping';
- const messages = {
- [MESSAGE_ID]: 'Move {{functionNameWithKind}} to the outer scope.',
- };
- const isSameScope = (scope1, scope2) =>
- scope1 && scope2 && (scope1 === scope2 || scope1.block === scope2.block);
- function checkReferences(scope, parent, scopeManager) {
- const hitReference = references => references.some(reference => {
- if (isSameScope(parent, reference.from)) {
- return true;
- }
- const {resolved} = reference;
- const [definition] = resolved.defs;
- // Skip recursive function name
- if (definition?.type === 'FunctionName' && resolved.name === definition.name.name) {
- return false;
- }
- return isSameScope(parent, resolved.scope);
- });
- const hitDefinitions = definitions => definitions.some(definition => {
- const scope = scopeManager.acquire(definition.node);
- return isSameScope(parent, scope);
- });
- // This check looks for neighboring function definitions
- const hitIdentifier = identifiers => identifiers.some(identifier => {
- // Only look at identifiers that live in a FunctionDeclaration
- if (
- !identifier.parent
- || identifier.parent.type !== 'FunctionDeclaration'
- ) {
- return false;
- }
- const identifierScope = scopeManager.acquire(identifier);
- // If we have a scope, the earlier checks should have worked so ignore them here
- /* c8 ignore next 3 */
- if (identifierScope) {
- return false;
- }
- const identifierParentScope = scopeManager.acquire(identifier.parent);
- /* c8 ignore next 3 */
- if (!identifierParentScope) {
- return false;
- }
- // Ignore identifiers from our own scope
- if (isSameScope(scope, identifierParentScope)) {
- return false;
- }
- // Look at the scope above the function definition to see if lives
- // next to the reference being checked
- return isSameScope(parent, identifierParentScope.upper);
- });
- return getReferences(scope)
- .map(({resolved}) => resolved)
- .filter(Boolean)
- .some(variable =>
- hitReference(variable.references)
- || hitDefinitions(variable.defs)
- || hitIdentifier(variable.identifiers),
- );
- }
- // https://reactjs.org/docs/hooks-reference.html
- const reactHooks = [
- 'useState',
- 'useEffect',
- 'useContext',
- 'useReducer',
- 'useCallback',
- 'useMemo',
- 'useRef',
- 'useImperativeHandle',
- 'useLayoutEffect',
- 'useDebugValue',
- ].flatMap(hookName => [hookName, `React.${hookName}`]);
- const isReactHook = scope =>
- scope.block?.parent?.callee
- && isNodeMatches(scope.block.parent.callee, reactHooks);
- const isArrowFunctionWithThis = scope =>
- scope.type === 'function'
- && scope.block?.type === 'ArrowFunctionExpression'
- && (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope)));
- const iifeFunctionTypes = new Set([
- 'FunctionExpression',
- 'ArrowFunctionExpression',
- ]);
- const isIife = node =>
- iifeFunctionTypes.has(node.type)
- && node.parent.type === 'CallExpression'
- && node.parent.callee === node;
- function checkNode(node, scopeManager) {
- const scope = scopeManager.acquire(node);
- if (!scope || isArrowFunctionWithThis(scope)) {
- return true;
- }
- let parentNode = node.parent;
- // Skip over junk like the block statement inside of a function declaration
- // or the various pieces of an arrow function.
- if (parentNode.type === 'VariableDeclarator') {
- parentNode = parentNode.parent;
- }
- if (parentNode.type === 'VariableDeclaration') {
- parentNode = parentNode.parent;
- }
- if (parentNode.type === 'BlockStatement') {
- parentNode = parentNode.parent;
- }
- const parentScope = scopeManager.acquire(parentNode);
- if (
- !parentScope
- || parentScope.type === 'global'
- || isReactHook(parentScope)
- || isIife(parentNode)
- ) {
- return true;
- }
- return checkReferences(scope, parentScope, scopeManager);
- }
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => {
- const {checkArrowFunctions} = {checkArrowFunctions: true, ...context.options[0]};
- const sourceCode = context.getSourceCode();
- const {scopeManager} = sourceCode;
- const functions = [];
- return {
- ':function'() {
- functions.push(false);
- },
- JSXElement() {
- // Turn off this rule if we see a JSX element because scope
- // references does not include JSXElement nodes.
- if (functions.length > 0) {
- functions[functions.length - 1] = true;
- }
- },
- ':function:exit'(node) {
- const currentFunctionHasJsx = functions.pop();
- if (currentFunctionHasJsx) {
- return;
- }
- if (node.type === 'ArrowFunctionExpression' && !checkArrowFunctions) {
- return;
- }
- if (checkNode(node, scopeManager)) {
- return;
- }
- return {
- node,
- loc: getFunctionHeadLocation(node, sourceCode),
- messageId: MESSAGE_ID,
- data: {
- functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
- },
- };
- },
- };
- };
- const schema = [
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- checkArrowFunctions: {
- type: 'boolean',
- default: true,
- },
- },
- },
- ];
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Move function definitions to the highest possible scope.',
- },
- schema,
- messages,
- },
- };
|