eslint-plugin-react-query-must-invalidate-queries
Version:
ESLint plugin for React query keys to enforce queries invalidation after a mutation
136 lines (135 loc) • 7.95 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireMutationInvalidation = void 0;
const utils_1 = require("@typescript-eslint/utils");
const MUTATION_HOOK_NAME = "useMutation";
const ON_SUCCESS_PROP = "onSuccess";
const INVALIDATE_QUERIES_NAME = "invalidateQueries";
exports.requireMutationInvalidation = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/adelowo/eslint-plugin-react-query-keys/blob/main/docs/rules/${name}.md`)({
name: "require-mutation-invalidation",
meta: {
type: "problem",
docs: {
description: "Enforce calling invalidateQueries in useMutation onSuccess callbacks",
},
messages: {
missingInvalidation: "useMutation onSuccess callback must call invalidateQueries to ensure data consistency",
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
// Check if it's a useMutation call
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
node.callee.name === MUTATION_HOOK_NAME) {
// Look for options object in the arguments
const options = node.arguments.find((arg) => arg.type === utils_1.AST_NODE_TYPES.ObjectExpression);
if (!options) {
context.report({
node,
messageId: "missingInvalidation",
});
return;
}
// look for the onSuccess property
const onSuccessProp = options.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
"name" in prop.key &&
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
prop.key.name === ON_SUCCESS_PROP);
if (!onSuccessProp) {
context.report({
node,
messageId: "missingInvalidation",
});
return;
}
// keep a track of all variables that reference invalidateQueries method call
const invalidateQueriesRefs = new Set();
// does onSuccess callback contains invalidateQueries calls?
const hasInvalidateQueries = (node) => {
if (!node || typeof node !== 'object') {
return false;
}
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
// regular assignment
if (node.init?.type === utils_1.AST_NODE_TYPES.MemberExpression) {
if (node.init.property.type === utils_1.AST_NODE_TYPES.Identifier &&
node.init.property.name === INVALIDATE_QUERIES_NAME) {
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
invalidateQueriesRefs.add(node.id.name);
}
}
}
else if (node.init?.type === utils_1.AST_NODE_TYPES.Identifier &&
invalidateQueriesRefs.has(node.init.name)) {
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
invalidateQueriesRefs.add(node.id.name);
}
}
// object destructuring
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
node.id.properties.forEach((prop) => {
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
prop.key.name === INVALIDATE_QUERIES_NAME &&
prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
invalidateQueriesRefs.add(prop.value.name);
}
});
}
}
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
// this is a straight foward call to invalidateQueries on any object
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
node.callee.property.name === INVALIDATE_QUERIES_NAME) {
return true;
}
// a call to a variable that references invalidateQueries methods
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
invalidateQueriesRefs.has(node.callee.name)) {
return true;
}
}
// Handle awaited expressions
if (node.type === utils_1.AST_NODE_TYPES.AwaitExpression) {
return hasInvalidateQueries(node.argument);
}
// go through only specific AST node properties that could contain the invalidateQueries call
switch (node.type) {
case utils_1.AST_NODE_TYPES.BlockStatement:
return node.body.some(hasInvalidateQueries);
case utils_1.AST_NODE_TYPES.ExpressionStatement:
return hasInvalidateQueries(node.expression);
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
case utils_1.AST_NODE_TYPES.FunctionExpression:
return hasInvalidateQueries(node.body);
case utils_1.AST_NODE_TYPES.CallExpression:
return hasInvalidateQueries(node.callee) || node.arguments.some(hasInvalidateQueries);
case utils_1.AST_NODE_TYPES.MemberExpression:
return hasInvalidateQueries(node.object) || hasInvalidateQueries(node.property);
case utils_1.AST_NODE_TYPES.IfStatement:
return hasInvalidateQueries(node.consequent) || (node.alternate ? hasInvalidateQueries(node.alternate) : false);
case utils_1.AST_NODE_TYPES.TryStatement:
return hasInvalidateQueries(node.block) || (node.handler ? hasInvalidateQueries(node.handler.body) : false);
case utils_1.AST_NODE_TYPES.CatchClause:
return hasInvalidateQueries(node.body);
case utils_1.AST_NODE_TYPES.VariableDeclaration:
return node.declarations.some(hasInvalidateQueries);
default:
return false;
}
};
if (!hasInvalidateQueries(onSuccessProp.value)) {
context.report({
node: onSuccessProp,
messageId: "missingInvalidation",
});
}
}
},
};
},
});