tslint-react-hooks
Version:
TSLint rule that enforces the Rules of Hooks
151 lines (150 loc) • 6.99 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReactHooksNestingWalker = void 0;
const tslint_1 = require("tslint");
const typescript_1 = require("typescript");
const is_hook_call_1 = require("./is-hook-call");
const error_messages_1 = require("./error-messages");
const is_binary_conditional_expression_1 = require("./is-binary-conditional-expression");
const is_component_or_hook_identifier_1 = require("./is-component-or-hook-identifier");
const is_react_component_decorator_1 = require("./is-react-component-decorator");
const find_ancestor_function_1 = require("./find-ancestor-function");
const function_node_1 = require("./function-node");
const find_closest_ancestor_node_1 = require("./find-closest-ancestor-node");
const options_1 = require("./options");
class ReactHooksNestingWalker extends tslint_1.RuleWalker {
constructor() {
super(...arguments);
this.functionsWithReturnStatements = new Set();
this.ruleOptions = (0, options_1.parseRuleOptions)(this.getOptions());
}
visitCallExpression(node) {
if ((0, is_hook_call_1.isHookCall)(node, this.ruleOptions)) {
this.visitHookAncestor(node, node.parent);
}
super.visitCallExpression(node);
}
visitReturnStatement(node) {
const parentFunction = (0, find_ancestor_function_1.findAncestorFunction)(node);
if (parentFunction) {
this.functionsWithReturnStatements.add(parentFunction);
}
super.visitReturnStatement(node);
}
visitHookAncestor(hookNode, ancestor) {
/**
* Fail for:
* * if statements
* * conditional expressions (binary and ternary)
* * switch statements
* * iterations statements
* * classes
*/
if ((0, typescript_1.isIfStatement)(ancestor) ||
(0, typescript_1.isSwitchStatement)(ancestor) ||
(0, typescript_1.isConditionalExpression)(ancestor) ||
(0, is_binary_conditional_expression_1.isBinaryConditionalExpression)(ancestor) ||
(0, typescript_1.isSourceFile)(ancestor) ||
(0, typescript_1.isClassDeclaration)(ancestor)) {
this.addFailureAtNode(hookNode, error_messages_1.ERROR_MESSAGES[ancestor.kind]);
return;
}
else if ((0, typescript_1.isIterationStatement)(ancestor, false)) {
this.addFailureAtNode(hookNode, error_messages_1.ERROR_MESSAGES.iterationStatement);
return;
}
/**
* Finish visiting ancestors if the following node is encountered:
* * `FunctionDeclaration`
* * `VariableDeclaration`
* * `FunctionExpression`
*
* If it not is a hook or a component, fail, as hooks cannot be nested inside arbitrary
* functions.
*/
if ((0, typescript_1.isFunctionDeclaration)(ancestor)) {
/**
* Allow using hooks inside functions that are hooks or components.
*
* ```ts
* function useCustomHook() {
* useEffect();
* }
*
* function MyComponent() {
* useEffect();
* }
* ```
*/
if (this.functionsWithReturnStatements.has(ancestor)) {
const closestReturnStatementOrFunctionNode = (0, find_closest_ancestor_node_1.findClosestAncestorNode)(hookNode, (node) => (0, typescript_1.isReturnStatement)(node) || (0, function_node_1.isFunctionNode)(node));
if (closestReturnStatementOrFunctionNode &&
!(0, typescript_1.isReturnStatement)(closestReturnStatementOrFunctionNode)) {
this.addFailureAtNode(hookNode, error_messages_1.ERROR_MESSAGES.hookAfterEarlyReturn);
}
}
if (ancestor.name && (0, is_component_or_hook_identifier_1.isComponentOrHookIdentifier)(ancestor.name)) {
return;
}
// Disallow using hooks inside other kinds of functions
this.addFailureAtNode(hookNode, error_messages_1.ERROR_MESSAGES.invalidFunctionDeclaration);
return;
}
else if ((0, typescript_1.isArrowFunction)(ancestor) || (0, typescript_1.isFunctionExpression)(ancestor)) {
/**
* Allow declaring custom hooks and components using arrow functions and function expressions
*
* ```ts
* const useCustomHook = () => {
* useEffect();
* }
*
* const MyComponent = function() {
* useEffect();
* }
*
* const MyComponent = function MyComponent() {
* useEffect();
* }
* ```
*/
/**
* REFACTOR: Use a shared implementation for all types of functions.
* The logic below is duplicated for function declarations.
*/
if (this.functionsWithReturnStatements.has(ancestor)) {
const closestReturnStatementOrFunctionNode = (0, find_closest_ancestor_node_1.findClosestAncestorNode)(hookNode, (node) => (0, typescript_1.isReturnStatement)(node) || (0, function_node_1.isFunctionNode)(node));
if (closestReturnStatementOrFunctionNode &&
!(0, typescript_1.isReturnStatement)(closestReturnStatementOrFunctionNode)) {
this.addFailureAtNode(hookNode, error_messages_1.ERROR_MESSAGES.hookAfterEarlyReturn);
}
}
/**
* Detect using hooks inside named function expressions
*/
if (ancestor.name && (0, is_component_or_hook_identifier_1.isComponentOrHookIdentifier)(ancestor.name)) {
return;
}
/**
* Detect that an unnamed function expression is a component or a hook
*/
if ((0, typescript_1.isVariableDeclaration)(ancestor.parent) &&
(0, typescript_1.isIdentifier)(ancestor.parent.name) &&
(0, is_component_or_hook_identifier_1.isComponentOrHookIdentifier)(ancestor.parent.name)) {
return;
}
/**
* Allow using hooks when the function is passed to `React.memo` or `React.forwardRef`
*/
if ((0, typescript_1.isCallExpression)(ancestor.parent) &&
(0, is_react_component_decorator_1.isReactComponentDecorator)(ancestor.parent.expression)) {
return;
}
// Disallow using hooks inside other kinds of functions
this.addFailureAtNode(hookNode, error_messages_1.ERROR_MESSAGES.invalidFunctionExpression);
return;
}
this.visitHookAncestor(hookNode, ancestor.parent);
}
}
exports.ReactHooksNestingWalker = ReactHooksNestingWalker;