| 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.htmlconst 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,	},};
 |