line_push/node_modules/eslint-plugin-unicorn/rules/consistent-function-scoping.js
2022-07-17 13:16:16 +08:00

196 lines
5.0 KiB
JavaScript

'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const getReferences = require('./utils/get-references');
const MESSAGE_ID_NAMED = 'named';
const MESSAGE_ID_ANONYMOUS = 'anonymous';
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 && 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
if (identifierScope) {
return false;
}
const identifierParentScope = scopeManager.acquire(identifier.parent);
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 = new Set([
'useState',
'useEffect',
'useContext',
'useReducer',
'useCallback',
'useMemo',
'useRef',
'useImperativeHandle',
'useLayoutEffect',
'useDebugValue'
]);
const isReactHook = scope =>
scope.block &&
scope.block.parent &&
scope.block.parent.callee &&
scope.block.parent.callee.type === 'Identifier' &&
reactHooks.has(scope.block.parent.callee.name);
const isArrowFunctionWithThis = scope =>
scope.type === 'function' &&
scope.block &&
scope.block.type === 'ArrowFunctionExpression' &&
(scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope)));
function checkNode(node, scopeManager) {
const scope = scopeManager.acquire(node);
if (!scope || isArrowFunctionWithThis(scope)) {
return true;
}
let parentNode = node.parent;
if (!parentNode) {
return true;
}
// 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)) {
return true;
}
return checkReferences(scope, parentScope, scopeManager);
}
const create = context => {
const sourceCode = context.getSourceCode();
const {scopeManager} = sourceCode;
const functions = [];
let hasJsx = false;
return {
'ArrowFunctionExpression, FunctionDeclaration': node => functions.push(node),
JSXElement: () => {
// Turn off this rule if we see a JSX element because scope
// references does not include JSXElement nodes.
hasJsx = true;
},
':matches(ArrowFunctionExpression, FunctionDeclaration):exit': node => {
if (!hasJsx && !checkNode(node, scopeManager)) {
const functionType = node.type === 'ArrowFunctionExpression' ? 'arrow function' : 'function';
let functionName = '';
if (node.id) {
functionName = node.id.name;
} else if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.id &&
node.parent.id.type === 'Identifier'
) {
functionName = node.parent.id.name;
}
context.report({
node,
messageId: functionName ? MESSAGE_ID_NAMED : MESSAGE_ID_ANONYMOUS,
data: {
functionType,
functionName
}
});
}
functions.pop();
if (functions.length === 0) {
hasJsx = false;
}
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
messages: {
[MESSAGE_ID_NAMED]: 'Move {{functionType}} `{{functionName}}` to the outer scope.',
[MESSAGE_ID_ANONYMOUS]: 'Move {{functionType}} to the outer scope.'
}
}
};