UNPKG

@tanstack/eslint-plugin-query

Version:

ESLint plugin for TanStack Query

1,053 lines (1,029 loc) 37.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name8 in all) __defProp(target, name8, { get: all[name8], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { default: () => index_default, plugin: () => plugin }); module.exports = __toCommonJS(index_exports); // src/rules/exhaustive-deps/exhaustive-deps.rule.ts var import_utils4 = require("@typescript-eslint/utils"); // src/utils/ast-utils.ts var import_utils = require("@typescript-eslint/utils"); // src/utils/unique-by.ts function uniqueBy(arr, fn) { return arr.filter((x, i, a) => a.findIndex((y) => fn(x) === fn(y)) === i); } // src/utils/ast-utils.ts var ASTUtils = { isNodeOfOneOf(node, types) { return types.includes(node.type); }, isIdentifier(node) { return node.type === import_utils.AST_NODE_TYPES.Identifier; }, isIdentifierWithName(node, name8) { return ASTUtils.isIdentifier(node) && node.name === name8; }, isIdentifierWithOneOfNames(node, name8) { return ASTUtils.isIdentifier(node) && name8.includes(node.name); }, isProperty(node) { return node.type === import_utils.AST_NODE_TYPES.Property; }, isObjectExpression(node) { return node.type === import_utils.AST_NODE_TYPES.ObjectExpression; }, isPropertyWithIdentifierKey(node, key) { return ASTUtils.isProperty(node) && ASTUtils.isIdentifierWithName(node.key, key); }, findPropertyWithIdentifierKey(properties, key) { return properties.find( (x) => ASTUtils.isPropertyWithIdentifierKey(x, key) ); }, getNestedIdentifiers(node) { const identifiers = []; if (ASTUtils.isIdentifier(node)) { identifiers.push(node); } if ("arguments" in node) { node.arguments.forEach((x) => { identifiers.push(...ASTUtils.getNestedIdentifiers(x)); }); } if ("elements" in node) { node.elements.forEach((x) => { if (x !== null) { identifiers.push(...ASTUtils.getNestedIdentifiers(x)); } }); } if ("properties" in node) { node.properties.forEach((x) => { identifiers.push(...ASTUtils.getNestedIdentifiers(x)); }); } if ("expressions" in node) { node.expressions.forEach((x) => { identifiers.push(...ASTUtils.getNestedIdentifiers(x)); }); } if ("left" in node) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.left)); } if ("right" in node) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.right)); } if (node.type === import_utils.AST_NODE_TYPES.Property) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.value)); } if (node.type === import_utils.AST_NODE_TYPES.SpreadElement) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument)); } if (node.type === import_utils.AST_NODE_TYPES.MemberExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.object)); } if (node.type === import_utils.AST_NODE_TYPES.UnaryExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument)); } if (node.type === import_utils.AST_NODE_TYPES.ChainExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.expression)); } if (node.type === import_utils.AST_NODE_TYPES.TSNonNullExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.expression)); } if (node.type === import_utils.AST_NODE_TYPES.ArrowFunctionExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.body)); } if (node.type === import_utils.AST_NODE_TYPES.FunctionExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.body)); } if (node.type === import_utils.AST_NODE_TYPES.BlockStatement) { identifiers.push( ...node.body.map((body) => ASTUtils.getNestedIdentifiers(body)).flat() ); } if (node.type === import_utils.AST_NODE_TYPES.ReturnStatement && node.argument) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument)); } return identifiers; }, isAncestorIsCallee(identifier) { let previousNode = identifier; let currentNode = identifier.parent; while (currentNode !== void 0) { if (currentNode.type === import_utils.AST_NODE_TYPES.CallExpression && currentNode.callee === previousNode) { return true; } if (currentNode.type !== import_utils.AST_NODE_TYPES.MemberExpression) { return false; } previousNode = currentNode; currentNode = currentNode.parent; } return false; }, traverseUpOnly(identifier, allowedNodeTypes) { const parent = identifier.parent; if (parent !== void 0 && allowedNodeTypes.includes(parent.type)) { return ASTUtils.traverseUpOnly(parent, allowedNodeTypes); } return identifier; }, isDeclaredInNode(params) { const { functionNode, reference, scopeManager } = params; const scope = scopeManager.acquire(functionNode); if (scope === null) { return false; } return scope.set.has(reference.identifier.name); }, getExternalRefs(params) { const { scopeManager, sourceCode, node } = params; const scope = scopeManager.acquire(node); if (scope === null) { return []; } const references = scope.references.filter((x) => x.isRead() && !scope.set.has(x.identifier.name)).map((x) => { const referenceNode = ASTUtils.traverseUpOnly(x.identifier, [ import_utils.AST_NODE_TYPES.MemberExpression, import_utils.AST_NODE_TYPES.Identifier ]); return { variable: x, node: referenceNode, text: sourceCode.getText(referenceNode) }; }); const localRefIds = new Set( [...scope.set.values()].map((x) => sourceCode.getText(x.identifiers[0])) ); const externalRefs = references.filter( (x) => x.variable.resolved === null || !localRefIds.has(x.text) ); return uniqueBy(externalRefs, (x) => x.text).map((x) => x.variable); }, mapKeyNodeToText(node, sourceCode) { return sourceCode.getText( ASTUtils.traverseUpOnly(node, [ import_utils.AST_NODE_TYPES.MemberExpression, import_utils.AST_NODE_TYPES.TSNonNullExpression, import_utils.AST_NODE_TYPES.Identifier ]) ); }, mapKeyNodeToBaseText(node, sourceCode) { return ASTUtils.mapKeyNodeToText(node, sourceCode).replace( /(?:\?(\.)|!)/g, "$1" ); }, isValidReactComponentOrHookName(identifier) { return identifier !== null && identifier !== void 0 && /^(use|[A-Z])/.test(identifier.name); }, getFunctionAncestor(sourceCode, node) { for (const ancestor of sourceCode.getAncestors(node)) { if (ASTUtils.isNodeOfOneOf(ancestor, [ import_utils.AST_NODE_TYPES.FunctionDeclaration, import_utils.AST_NODE_TYPES.FunctionExpression, import_utils.AST_NODE_TYPES.ArrowFunctionExpression ])) { return ancestor; } if (ancestor.parent?.type === import_utils.AST_NODE_TYPES.VariableDeclarator && ancestor.parent.id.type === import_utils.AST_NODE_TYPES.Identifier && ASTUtils.isNodeOfOneOf(ancestor, [ import_utils.AST_NODE_TYPES.FunctionDeclaration, import_utils.AST_NODE_TYPES.FunctionExpression, import_utils.AST_NODE_TYPES.ArrowFunctionExpression ])) { return ancestor; } } return void 0; }, getReferencedExpressionByIdentifier(params) { const { node, context } = params; const sourceCode = context.sourceCode ?? context.getSourceCode(); const scope = context.sourceCode.getScope(node) ? sourceCode.getScope(node) : context.getScope(); const resolvedNode = scope.references.find((ref) => ref.identifier === node)?.resolved?.defs[0]?.node; if (resolvedNode?.type !== import_utils.AST_NODE_TYPES.VariableDeclarator) { return null; } return resolvedNode.init; }, getClosestVariableDeclarator(node) { let currentNode = node; while (currentNode.type !== import_utils.AST_NODE_TYPES.Program) { if (currentNode.type === import_utils.AST_NODE_TYPES.VariableDeclarator) { return currentNode; } currentNode = currentNode.parent; } return void 0; }, getNestedReturnStatements(node) { const returnStatements = []; if (node.type === import_utils.AST_NODE_TYPES.ReturnStatement) { returnStatements.push(node); } if ("body" in node && node.body !== void 0 && node.body !== null) { Array.isArray(node.body) ? node.body.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)); }) : returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.body) ); } if ("consequent" in node) { Array.isArray(node.consequent) ? node.consequent.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)); }) : returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.consequent) ); } if ("alternate" in node && node.alternate !== null) { Array.isArray(node.alternate) ? node.alternate.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)); }) : returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.alternate) ); } if ("cases" in node) { node.cases.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)); }); } if ("block" in node) { returnStatements.push(...ASTUtils.getNestedReturnStatements(node.block)); } if ("handler" in node && node.handler !== null) { returnStatements.push(...ASTUtils.getNestedReturnStatements(node.handler)); } if ("finalizer" in node && node.finalizer !== null) { returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.finalizer) ); } if ("expression" in node && node.expression !== true && node.expression !== false) { returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.expression) ); } if ("test" in node && node.test !== null) { returnStatements.push(...ASTUtils.getNestedReturnStatements(node.test)); } return returnStatements; } }; // src/utils/get-docs-url.ts var getDocsUrl = (ruleName) => `https://tanstack.com/query/latest/docs/eslint/${ruleName}`; // src/utils/detect-react-query-imports.ts var import_utils2 = require("@typescript-eslint/utils"); function detectTanstackQueryImports(create) { return (context, optionsWithDefault) => { const tanstackQueryImportSpecifiers = []; const helpers = { isSpecificTanstackQueryImport(node, source) { return !!tanstackQueryImportSpecifiers.find((specifier) => { if (specifier.type === import_utils2.TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.parent.type === import_utils2.TSESTree.AST_NODE_TYPES.ImportDeclaration && specifier.parent.source.value === source) { return node.name === specifier.local.name; } return false; }); }, isTanstackQueryImport(node) { return !!tanstackQueryImportSpecifiers.find((specifier) => { if (specifier.type === import_utils2.TSESTree.AST_NODE_TYPES.ImportSpecifier) { return node.name === specifier.local.name; } return false; }); } }; const detectionInstructions = { ImportDeclaration(node) { if (node.specifiers.length > 0 && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (node.importKind === "value" || node.importKind === void 0) && node.source.value.startsWith("@tanstack/") && node.source.value.endsWith("-query")) { tanstackQueryImportSpecifiers.push(...node.specifiers); } } }; const ruleInstructions = create(context, optionsWithDefault, helpers); const enhancedRuleInstructions = {}; const allKeys = new Set( Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)) ); allKeys.forEach((instruction) => { enhancedRuleInstructions[instruction] = (node) => { if (instruction in detectionInstructions) { detectionInstructions[instruction]?.(node); } const ruleInstruction = ruleInstructions[instruction]; if (ruleInstruction) { return ruleInstruction(node); } return void 0; }; }); return enhancedRuleInstructions; }; } // src/rules/exhaustive-deps/exhaustive-deps.utils.ts var import_utils3 = require("@typescript-eslint/utils"); var ExhaustiveDepsUtils = { isRelevantReference(params) { const { sourceCode, reference, scopeManager, node } = params; const component = ASTUtils.getFunctionAncestor(sourceCode, node); if (component === void 0) { return false; } if (!ASTUtils.isDeclaredInNode({ scopeManager, reference, functionNode: component })) { return false; } return reference.identifier.name !== "undefined" && reference.identifier.parent.type !== import_utils3.AST_NODE_TYPES.NewExpression && !ExhaustiveDepsUtils.isInstanceOfKind(reference.identifier.parent); }, isInstanceOfKind(node) { return node.type === import_utils3.AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof"; } }; // src/rules/exhaustive-deps/exhaustive-deps.rule.ts var QUERY_KEY = "queryKey"; var QUERY_FN = "queryFn"; var name = "exhaustive-deps"; var createRule = import_utils4.ESLintUtils.RuleCreator(getDocsUrl); var rule = createRule({ name, meta: { type: "problem", docs: { description: "Exhaustive deps rule for useQuery", recommended: "error" }, messages: { missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`, fixTo: "Fix to {{result}}" }, hasSuggestions: true, fixable: "code", schema: [] }, defaultOptions: [], create: detectTanstackQueryImports((context) => { return { Property: (node) => { if (!ASTUtils.isObjectExpression(node.parent) || !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)) { return; } const scopeManager = context.sourceCode.scopeManager; const queryKey = ASTUtils.findPropertyWithIdentifierKey( node.parent.properties, QUERY_KEY ); const queryFn = ASTUtils.findPropertyWithIdentifierKey( node.parent.properties, QUERY_FN ); if (scopeManager === null || queryKey === void 0 || queryFn === void 0 || !ASTUtils.isNodeOfOneOf(queryFn.value, [ import_utils4.AST_NODE_TYPES.ArrowFunctionExpression, import_utils4.AST_NODE_TYPES.FunctionExpression, import_utils4.AST_NODE_TYPES.ConditionalExpression ])) { return; } let queryKeyNode = queryKey.value; if (queryKeyNode.type === import_utils4.AST_NODE_TYPES.TSAsExpression && queryKeyNode.expression.type === import_utils4.AST_NODE_TYPES.ArrayExpression) { queryKeyNode = queryKeyNode.expression; } if (queryKeyNode.type === import_utils4.AST_NODE_TYPES.Identifier) { const expression = ASTUtils.getReferencedExpressionByIdentifier({ context, node: queryKeyNode }); if (expression?.type === import_utils4.AST_NODE_TYPES.ArrayExpression) { queryKeyNode = expression; } } const externalRefs = ASTUtils.getExternalRefs({ scopeManager, sourceCode: context.sourceCode, node: getQueryFnRelevantNode(queryFn) }); const relevantRefs = externalRefs.filter( (reference) => ExhaustiveDepsUtils.isRelevantReference({ sourceCode: context.sourceCode, reference, scopeManager, node: getQueryFnRelevantNode(queryFn) }) ); const existingKeys = ASTUtils.getNestedIdentifiers(queryKeyNode).map( (identifier) => ASTUtils.mapKeyNodeToBaseText(identifier, context.sourceCode) ); const missingRefs = relevantRefs.map((ref) => ({ ref, text: ASTUtils.mapKeyNodeToBaseText( ref.identifier, context.sourceCode ) })).filter(({ ref, text }) => { return !ref.isTypeReference && !ASTUtils.isAncestorIsCallee(ref.identifier) && !existingKeys.some((existingKey) => existingKey === text) && !existingKeys.includes(text.split(/[?.]/)[0] ?? ""); }).map(({ ref, text }) => ({ identifier: ref.identifier, text })); const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text); if (uniqueMissingRefs.length > 0) { const missingAsText = uniqueMissingRefs.map( (ref) => ASTUtils.mapKeyNodeToText(ref.identifier, context.sourceCode) ).join(", "); const queryKeyValue = context.sourceCode.getText(queryKeyNode); const existingWithMissing = queryKeyValue === "[]" ? `[${missingAsText}]` : queryKeyValue.replace(/\]$/, `, ${missingAsText}]`); const suggestions = []; if (queryKeyNode.type === import_utils4.AST_NODE_TYPES.ArrayExpression) { suggestions.push({ messageId: "fixTo", data: { result: existingWithMissing }, fix(fixer) { return fixer.replaceText(queryKeyNode, existingWithMissing); } }); } context.report({ node, messageId: "missingDeps", data: { deps: uniqueMissingRefs.map((ref) => ref.text).join(", ") }, suggest: suggestions }); } } }; }) }); function getQueryFnRelevantNode(queryFn) { if (queryFn.value.type !== import_utils4.AST_NODE_TYPES.ConditionalExpression) { return queryFn.value; } if (queryFn.value.consequent.type === import_utils4.AST_NODE_TYPES.Identifier && queryFn.value.consequent.name === "skipToken") { return queryFn.value.alternate; } return queryFn.value.consequent; } // src/rules/stable-query-client/stable-query-client.rule.ts var import_utils5 = require("@typescript-eslint/utils"); var name2 = "stable-query-client"; var createRule2 = import_utils5.ESLintUtils.RuleCreator(getDocsUrl); var rule2 = createRule2({ name: name2, meta: { type: "problem", docs: { description: "Makes sure that QueryClient is stable", recommended: "error" }, messages: { unstable: [ "QueryClient is not stable. It should be either extracted from the component or wrapped in React.useState.", "See https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable" ].join("\n"), fixTo: "Fix to {{result}}" }, hasSuggestions: true, fixable: "code", schema: [] }, defaultOptions: [], create: detectTanstackQueryImports((context, _, helpers) => { return { NewExpression: (node) => { if (node.callee.type !== import_utils5.AST_NODE_TYPES.Identifier || node.callee.name !== "QueryClient" || node.parent.type !== import_utils5.AST_NODE_TYPES.VariableDeclarator || !helpers.isSpecificTanstackQueryImport( node.callee, "@tanstack/react-query" )) { return; } const fnAncestor = ASTUtils.getFunctionAncestor( context.sourceCode, node ); const isReactServerComponent = fnAncestor?.async === true; if (!ASTUtils.isValidReactComponentOrHookName(fnAncestor?.id) || isReactServerComponent) { return; } context.report({ node: node.parent, messageId: "unstable", fix: (() => { const { parent } = node; if (parent.id.type !== import_utils5.AST_NODE_TYPES.Identifier) { return; } const sourceCode = context.sourceCode ?? context.getSourceCode(); const nodeText = sourceCode.getText(node); const variableName = parent.id.name; return (fixer) => { return fixer.replaceTextRange( [parent.range[0], parent.range[1]], `[${variableName}] = React.useState(() => ${nodeText})` ); }; })() }); } }; }) }); // src/rules/no-rest-destructuring/no-rest-destructuring.rule.ts var import_utils7 = require("@typescript-eslint/utils"); // src/rules/no-rest-destructuring/no-rest-destructuring.utils.ts var import_utils6 = require("@typescript-eslint/utils"); var NoRestDestructuringUtils = { isObjectRestDestructuring(node) { if (node.type !== import_utils6.AST_NODE_TYPES.ObjectPattern) { return false; } return node.properties.some((p) => p.type === import_utils6.AST_NODE_TYPES.RestElement); } }; // src/rules/no-rest-destructuring/no-rest-destructuring.rule.ts var name3 = "no-rest-destructuring"; var queryHooks = [ "useQuery", "useQueries", "useInfiniteQuery", "useSuspenseQuery", "useSuspenseQueries", "useSuspenseInfiniteQuery" ]; var createRule3 = import_utils7.ESLintUtils.RuleCreator(getDocsUrl); var rule3 = createRule3({ name: name3, meta: { type: "problem", docs: { description: "Disallows rest destructuring in queries", recommended: "warn" }, messages: { objectRestDestructure: `Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.` }, schema: [] }, defaultOptions: [], create: detectTanstackQueryImports((context, _, helpers) => { const queryResultVariables = /* @__PURE__ */ new Set(); return { CallExpression: (node) => { if (!ASTUtils.isIdentifierWithOneOfNames(node.callee, queryHooks) || node.parent.type !== import_utils7.AST_NODE_TYPES.VariableDeclarator || !helpers.isTanstackQueryImport(node.callee)) { return; } const returnValue = node.parent.id; if (node.callee.name !== "useQueries" && node.callee.name !== "useSuspenseQueries") { if (NoRestDestructuringUtils.isObjectRestDestructuring(returnValue)) { return context.report({ node: node.parent, messageId: "objectRestDestructure" }); } if (returnValue.type === import_utils7.AST_NODE_TYPES.Identifier) { queryResultVariables.add(returnValue.name); } return; } if (returnValue.type !== import_utils7.AST_NODE_TYPES.ArrayPattern) { if (returnValue.type === import_utils7.AST_NODE_TYPES.Identifier) { queryResultVariables.add(returnValue.name); } return; } returnValue.elements.forEach((queryResult) => { if (queryResult === null) { return; } if (NoRestDestructuringUtils.isObjectRestDestructuring(queryResult)) { context.report({ node: queryResult, messageId: "objectRestDestructure" }); } }); }, VariableDeclarator: (node) => { if (node.init?.type === import_utils7.AST_NODE_TYPES.Identifier && queryResultVariables.has(node.init.name) && NoRestDestructuringUtils.isObjectRestDestructuring(node.id)) { context.report({ node, messageId: "objectRestDestructure" }); } }, SpreadElement: (node) => { if (node.argument.type === import_utils7.AST_NODE_TYPES.Identifier && queryResultVariables.has(node.argument.name)) { context.report({ node, messageId: "objectRestDestructure" }); } } }; }) }); // src/rules/no-unstable-deps/no-unstable-deps.rule.ts var import_utils8 = require("@typescript-eslint/utils"); var name4 = "no-unstable-deps"; var reactHookNames = ["useEffect", "useCallback", "useMemo"]; var useQueryHookNames = [ "useQuery", "useSuspenseQuery", "useQueries", "useSuspenseQueries", "useInfiniteQuery", "useSuspenseInfiniteQuery" ]; var allHookNames = ["useMutation", ...useQueryHookNames]; var createRule4 = import_utils8.ESLintUtils.RuleCreator(getDocsUrl); var rule4 = createRule4({ name: name4, meta: { type: "problem", docs: { description: "Disallow putting the result of query hooks directly in a React hook dependency array", recommended: "error" }, messages: { noUnstableDeps: `The result of {{queryHook}} is not referentially stable, so don't pass it directly into the dependencies array of {{reactHook}}. Instead, destructure the return value of {{queryHook}} and pass the destructured values into the dependency array of {{reactHook}}.` }, schema: [] }, defaultOptions: [], create: detectTanstackQueryImports((context) => { const trackedVariables = {}; const hookAliasMap = {}; function getReactHook(node) { if (node.callee.type === "Identifier") { const calleeName = node.callee.name; if (reactHookNames.includes(calleeName) || calleeName in hookAliasMap) { return calleeName; } } else if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "React" && node.callee.property.type === "Identifier" && reactHookNames.includes(node.callee.property.name)) { return node.callee.property.name; } return void 0; } function collectVariableNames(pattern, queryHook) { if (pattern.type === import_utils8.AST_NODE_TYPES.Identifier) { trackedVariables[pattern.name] = queryHook; } } return { ImportDeclaration(node) { if (node.specifiers.length > 0 && node.importKind === "value" && node.source.value === "React") { node.specifiers.forEach((specifier) => { if (specifier.type === import_utils8.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils8.AST_NODE_TYPES.Identifier && reactHookNames.includes(specifier.imported.name)) { hookAliasMap[specifier.local.name] = specifier.imported.name; } }); } }, VariableDeclarator(node) { if (node.init !== null && node.init.type === import_utils8.AST_NODE_TYPES.CallExpression && node.init.callee.type === import_utils8.AST_NODE_TYPES.Identifier && allHookNames.includes(node.init.callee.name)) { collectVariableNames(node.id, node.init.callee.name); } }, CallExpression: (node) => { const reactHook = getReactHook(node); if (reactHook !== void 0 && node.arguments.length > 1 && node.arguments[1]?.type === import_utils8.AST_NODE_TYPES.ArrayExpression) { const depsArray = node.arguments[1].elements; depsArray.forEach((dep) => { if (dep !== null && dep.type === import_utils8.AST_NODE_TYPES.Identifier && trackedVariables[dep.name] !== void 0) { const queryHook = trackedVariables[dep.name]; context.report({ node: dep, messageId: "noUnstableDeps", data: { queryHook, reactHook } }); } }); } } }; }) }); // src/utils/create-property-order-rule.ts var import_utils9 = require("@typescript-eslint/utils"); // src/utils/sort-data-by-order.ts function sortDataByOrder(data, orderRules, key) { const getSubsetIndex = (item, subsets) => { for (let i = 0; i < subsets.length; i++) { if (subsets[i]?.includes(item)) { return i; } } return null; }; const orderSets = orderRules.reduce( (sets, [A, B]) => [...sets, A, B], [] ); const inOrderArray = data.filter( (item) => getSubsetIndex(item[key], orderSets) !== null ); let wasResorted = false; const sortedArray = inOrderArray.sort((a, b) => { const aKey = a[key], bKey = b[key]; const aSubsetIndex = getSubsetIndex(aKey, orderSets); const bSubsetIndex = getSubsetIndex(bKey, orderSets); if (aSubsetIndex !== null && bSubsetIndex !== null && aSubsetIndex !== bSubsetIndex) { return aSubsetIndex - bSubsetIndex; } return 0; }); const inOrderIterator = sortedArray.values(); const result = data.map((item) => { if (getSubsetIndex(item[key], orderSets) !== null) { const sortedItem = inOrderIterator.next().value; if (sortedItem[key] !== item[key]) { wasResorted = true; } return sortedItem; } return item; }); if (!wasResorted) { return null; } return result; } // src/utils/create-property-order-rule.ts var createRule5 = import_utils9.ESLintUtils.RuleCreator(getDocsUrl); function createPropertyOrderRule(options, targetFunctions, orderRules) { const targetFunctionSet = new Set(targetFunctions); function isTargetFunction(node) { return targetFunctionSet.has(node); } return createRule5({ ...options, create: detectTanstackQueryImports((context) => { return { CallExpression(node) { if (node.callee.type !== import_utils9.AST_NODE_TYPES.Identifier) { return; } const functions = node.callee.name; if (!isTargetFunction(functions)) { return; } const argument = node.arguments[0]; if (argument === void 0 || argument.type !== "ObjectExpression") { return; } const allProperties = argument.properties; if (allProperties.length < 2) { return; } const properties = allProperties.flatMap((p, index) => { if (p.type === import_utils9.AST_NODE_TYPES.Property && p.key.type === import_utils9.AST_NODE_TYPES.Identifier) { return { name: p.key.name, property: p }; } else return { name: `_property_${index}`, property: p }; }); const sortedProperties = sortDataByOrder( properties, orderRules, "name" ); if (sortedProperties === null) { return; } context.report({ node: argument, data: { function: node.callee.name }, messageId: "invalidOrder", fix(fixer) { const sourceCode = context.sourceCode; const reorderedText = sortedProperties.reduce( (sourceText, specifier, index) => { let textBetweenProperties = ""; if (index < allProperties.length - 1) { textBetweenProperties = sourceCode.getText().slice( allProperties[index].range[1], allProperties[index + 1].range[0] ); } return sourceText + sourceCode.getText(specifier.property) + textBetweenProperties; }, "" ); return fixer.replaceTextRange( [allProperties[0].range[0], allProperties.at(-1).range[1]], reorderedText ); } }); } }; }) }); } // src/rules/infinite-query-property-order/constants.ts var infiniteQueryFunctions = [ "infiniteQueryOptions", "useInfiniteQuery", "useSuspenseInfiniteQuery" ]; var sortRules = [ [["queryFn"], ["getPreviousPageParam", "getNextPageParam"]] ]; // src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts var name5 = "infinite-query-property-order"; var rule5 = createPropertyOrderRule( { name: name5, meta: { type: "problem", docs: { description: "Ensure correct order of inference sensitive properties for infinite queries", recommended: "error" }, messages: { invalidOrder: "Invalid order of properties for `{{function}}`." }, schema: [], hasSuggestions: true, fixable: "code" }, defaultOptions: [] }, infiniteQueryFunctions, sortRules ); // src/rules/no-void-query-fn/no-void-query-fn.rule.ts var import_utils10 = require("@typescript-eslint/utils"); var import_typescript = __toESM(require("typescript"), 1); var name6 = "no-void-query-fn"; var createRule6 = import_utils10.ESLintUtils.RuleCreator(getDocsUrl); var rule6 = createRule6({ name: name6, meta: { type: "problem", docs: { description: "Ensures queryFn returns a non-undefined value", recommended: "error" }, messages: { noVoidReturn: "queryFn must return a non-undefined value" }, schema: [] }, defaultOptions: [], create: detectTanstackQueryImports((context) => { return { Property(node) { if (!ASTUtils.isObjectExpression(node.parent) || !ASTUtils.isIdentifierWithName(node.key, "queryFn")) { return; } const parserServices = context.sourceCode.parserServices; if (!parserServices || !parserServices.esTreeNodeToTSNodeMap || !parserServices.program) { return; } const checker = parserServices.program.getTypeChecker(); const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.value); const type = checker.getTypeAtLocation(tsNode); if (type.getCallSignatures().length > 0) { const returnType = type.getCallSignatures()[0]?.getReturnType(); if (!returnType) { return; } if (isIllegalReturn(checker, returnType)) { context.report({ node: node.value, messageId: "noVoidReturn" }); } } } }; }) }); function isIllegalReturn(checker, type) { const awaited = checker.getAwaitedType(type); if (!awaited) return false; if (awaited.isUnion()) { return awaited.types.some((t) => isIllegalReturn(checker, t)); } return awaited.flags & (import_typescript.default.TypeFlags.Void | import_typescript.default.TypeFlags.Undefined) ? true : false; } // src/rules/mutation-property-order/constants.ts var mutationFunctions = ["useMutation"]; var sortRules2 = [[["onMutate"], ["onError", "onSettled"]]]; // src/rules/mutation-property-order/mutation-property-order.rule.ts var name7 = "mutation-property-order"; var rule7 = createPropertyOrderRule( { name: name7, meta: { type: "problem", docs: { description: "Ensure correct order of inference-sensitive properties in useMutation()", recommended: "error" }, messages: { invalidOrder: "Invalid order of properties for `{{function}}`." }, schema: [], hasSuggestions: true, fixable: "code" }, defaultOptions: [] }, mutationFunctions, sortRules2 ); // src/rules.ts var rules = { [name]: rule, [name2]: rule2, [name3]: rule3, [name4]: rule4, [name5]: rule5, [name6]: rule6, [name7]: rule7 }; // src/index.ts var plugin = { meta: { name: "@tanstack/eslint-plugin-query" }, configs: {}, rules }; Object.assign(plugin.configs, { recommended: { plugins: ["@tanstack/query"], rules: { "@tanstack/query/exhaustive-deps": "error", "@tanstack/query/no-rest-destructuring": "warn", "@tanstack/query/stable-query-client": "error", "@tanstack/query/no-unstable-deps": "error", "@tanstack/query/infinite-query-property-order": "error", "@tanstack/query/no-void-query-fn": "error", "@tanstack/query/mutation-property-order": "error" } }, "flat/recommended": [ { name: "tanstack/query/flat/recommended", plugins: { "@tanstack/query": plugin }, rules: { "@tanstack/query/exhaustive-deps": "error", "@tanstack/query/no-rest-destructuring": "warn", "@tanstack/query/stable-query-client": "error", "@tanstack/query/no-unstable-deps": "error", "@tanstack/query/infinite-query-property-order": "error", "@tanstack/query/no-void-query-fn": "error", "@tanstack/query/mutation-property-order": "error" } } ] }); var index_default = plugin; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { plugin }); module.exports = module.exports.default //# sourceMappingURL=index.cjs.map