@tanstack/eslint-plugin-query 5.94.5 → 5.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -131,21 +131,6 @@ var ASTUtils = {
131
131
  }
132
132
  return identifiers;
133
133
  },
134
- isAncestorIsCallee(identifier) {
135
- let previousNode = identifier;
136
- let currentNode = identifier.parent;
137
- while (currentNode !== void 0) {
138
- if (currentNode.type === import_utils.AST_NODE_TYPES.CallExpression && currentNode.callee === previousNode) {
139
- return true;
140
- }
141
- if (currentNode.type !== import_utils.AST_NODE_TYPES.MemberExpression) {
142
- return false;
143
- }
144
- previousNode = currentNode;
145
- currentNode = currentNode.parent;
146
- }
147
- return false;
148
- },
149
134
  traverseUpOnly(identifier, allowedNodeTypes) {
150
135
  const parent = identifier.parent;
151
136
  if (parent !== void 0 && allowedNodeTypes.includes(parent.type)) {
@@ -370,7 +355,7 @@ var ExhaustiveDepsUtils = {
370
355
  const { sourceCode, reference, scopeManager, node, filename } = params;
371
356
  const component = ASTUtils.getFunctionAncestor(sourceCode, node);
372
357
  const queryFnScope = scopeManager.acquire(node);
373
- if (queryFnScope === null) {
358
+ if (queryFnScope === null || reference.isValueReference === false) {
374
359
  return false;
375
360
  }
376
361
  let currentScope = ((_a = reference.resolved) == null ? void 0 : _a.scope) ?? null;
@@ -402,15 +387,57 @@ var ExhaustiveDepsUtils = {
402
387
  }
403
388
  return reference.identifier.name !== "undefined" && reference.identifier.parent.type !== import_utils3.AST_NODE_TYPES.NewExpression && !ExhaustiveDepsUtils.isInstanceOfKind(reference.identifier.parent);
404
389
  },
405
- isInstanceOfKind(node) {
406
- return node.type === import_utils3.AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof";
390
+ /**
391
+ * Given required refs and existing queryKey entries, compute missing dependency paths
392
+ * respecting allowlisted variables and types.
393
+ */
394
+ computeFilteredMissingPaths(params) {
395
+ const {
396
+ requiredRefs,
397
+ allowlistedVariables,
398
+ existingRootIdentifiers,
399
+ existingFullPaths
400
+ } = params;
401
+ const missingPaths = /* @__PURE__ */ new Set();
402
+ for (const { root, path, allowlistedByType } of requiredRefs) {
403
+ if (existingRootIdentifiers.has(root)) continue;
404
+ if (allowlistedVariables.has(root)) continue;
405
+ if (existingFullPaths.has(path)) continue;
406
+ if (allowlistedByType) continue;
407
+ missingPaths.add(path);
408
+ }
409
+ for (const path of missingPaths) {
410
+ const root = path.split(".")[0];
411
+ if (root !== path && root !== void 0 && missingPaths.has(root)) {
412
+ missingPaths.delete(path);
413
+ }
414
+ }
415
+ return Array.from(missingPaths);
407
416
  },
417
+ /**
418
+ * Extract existing queryKey deps as root identifiers and full member paths.
419
+ */
408
420
  collectQueryKeyDeps(params) {
409
421
  const { sourceCode, scopeManager, queryKeyNode } = params;
410
- const deps = /* @__PURE__ */ new Set();
422
+ const roots = /* @__PURE__ */ new Set();
423
+ const paths = /* @__PURE__ */ new Set();
411
424
  const visitorKeys = sourceCode.visitorKeys;
412
- function add(identifier) {
413
- deps.add(ASTUtils.mapKeyNodeToBaseText(identifier, sourceCode));
425
+ function addRoot(name8) {
426
+ const cleaned = ExhaustiveDepsUtils.normalizeChain(name8);
427
+ roots.add(cleaned);
428
+ paths.add(cleaned);
429
+ }
430
+ function addFull(text) {
431
+ const cleaned = ExhaustiveDepsUtils.normalizeChain(text);
432
+ paths.add(cleaned);
433
+ }
434
+ function addRefPath(refPath) {
435
+ if (!refPath) return;
436
+ if (refPath.coversRootMembers) {
437
+ addRoot(refPath.root);
438
+ return;
439
+ }
440
+ addFull(refPath.path);
414
441
  }
415
442
  function visitChildren(node) {
416
443
  const keys = visitorKeys[node.type] ?? [];
@@ -432,9 +459,15 @@ var ExhaustiveDepsUtils = {
432
459
  function visit(node) {
433
460
  if (!node) return;
434
461
  switch (node.type) {
435
- case import_utils3.AST_NODE_TYPES.Identifier:
436
- add(node);
462
+ case import_utils3.AST_NODE_TYPES.Identifier: {
463
+ addRefPath(
464
+ ExhaustiveDepsUtils.computeRefPath({
465
+ identifier: node,
466
+ sourceCode
467
+ })
468
+ );
437
469
  return;
470
+ }
438
471
  case import_utils3.AST_NODE_TYPES.ArrowFunctionExpression:
439
472
  case import_utils3.AST_NODE_TYPES.FunctionExpression:
440
473
  for (const reference of ExhaustiveDepsUtils.collectExternalRefsInFunction(
@@ -443,9 +476,15 @@ var ExhaustiveDepsUtils = {
443
476
  scopeManager
444
477
  }
445
478
  )) {
446
- if (reference.identifier.type === import_utils3.AST_NODE_TYPES.Identifier) {
447
- add(reference.identifier);
479
+ if (reference.identifier.type !== import_utils3.AST_NODE_TYPES.Identifier) {
480
+ continue;
448
481
  }
482
+ addRefPath(
483
+ ExhaustiveDepsUtils.computeRefPath({
484
+ identifier: reference.identifier,
485
+ sourceCode
486
+ })
487
+ );
449
488
  }
450
489
  return;
451
490
  case import_utils3.AST_NODE_TYPES.Property:
@@ -453,26 +492,96 @@ var ExhaustiveDepsUtils = {
453
492
  return;
454
493
  case import_utils3.AST_NODE_TYPES.MemberExpression:
455
494
  if (node.parent.type === import_utils3.AST_NODE_TYPES.CallExpression && node.parent.callee === node && node.object.type === import_utils3.AST_NODE_TYPES.Identifier) {
456
- deps.add(node.object.name);
495
+ addRoot(node.object.name);
457
496
  } else {
458
497
  visit(node.object);
459
498
  }
460
499
  return;
461
500
  case import_utils3.AST_NODE_TYPES.CallExpression:
462
501
  node.arguments.forEach((argument) => visit(argument));
463
- if (node.callee.type === import_utils3.AST_NODE_TYPES.MemberExpression || node.callee.type === import_utils3.AST_NODE_TYPES.ChainExpression || node.callee.type === import_utils3.AST_NODE_TYPES.TSNonNullExpression) {
464
- visit(node.callee);
502
+ switch (node.callee.type) {
503
+ case import_utils3.AST_NODE_TYPES.Identifier:
504
+ case import_utils3.AST_NODE_TYPES.MemberExpression:
505
+ case import_utils3.AST_NODE_TYPES.ChainExpression:
506
+ case import_utils3.AST_NODE_TYPES.TSNonNullExpression:
507
+ visit(node.callee);
508
+ break;
465
509
  }
466
510
  return;
467
511
  }
468
512
  visitChildren(node);
469
513
  }
470
514
  visit(queryKeyNode);
471
- return deps;
515
+ return { roots, paths };
472
516
  },
473
517
  isNode(value) {
474
518
  return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
475
519
  },
520
+ /**
521
+ * Checks whether the resolved variable is allowlisted by its type annotation
522
+ */
523
+ variableIsAllowlistedByType(params) {
524
+ const { allowlistedTypes, variable } = params;
525
+ if (allowlistedTypes.size === 0) return false;
526
+ if (!variable) return false;
527
+ for (const id of variable.identifiers) {
528
+ if (id.typeAnnotation) {
529
+ const typeIdentifiers = /* @__PURE__ */ new Set();
530
+ ExhaustiveDepsUtils.collectTypeIdentifiers(
531
+ id.typeAnnotation.typeAnnotation,
532
+ typeIdentifiers
533
+ );
534
+ for (const typeIdentifier of typeIdentifiers) {
535
+ if (allowlistedTypes.has(typeIdentifier)) return true;
536
+ }
537
+ }
538
+ }
539
+ return false;
540
+ },
541
+ isInstanceOfKind(node) {
542
+ return node.type === import_utils3.AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof";
543
+ },
544
+ /**
545
+ * Normalizes a chain by removing optional chaining operators
546
+ *
547
+ * Example: `a?.b.c!` -> `a.b.c`
548
+ */
549
+ normalizeChain(text) {
550
+ return text.replace(/(?:\?(\.)|!)/g, "$1");
551
+ },
552
+ /**
553
+ * Computes the reference path for an identifier
554
+ *
555
+ * Example: `a.b.c!` -> `{ path: 'a.b.c', root: 'a' }`
556
+ */
557
+ computeRefPath(params) {
558
+ const { identifier, sourceCode } = params;
559
+ const fullChainNode = ASTUtils.traverseUpOnly(identifier, [
560
+ import_utils3.AST_NODE_TYPES.MemberExpression,
561
+ import_utils3.AST_NODE_TYPES.TSNonNullExpression,
562
+ import_utils3.AST_NODE_TYPES.Identifier
563
+ ]);
564
+ const fullText = ExhaustiveDepsUtils.normalizeChain(
565
+ sourceCode.getText(fullChainNode)
566
+ );
567
+ const parent = fullChainNode.parent;
568
+ let dependencyPath = fullText;
569
+ let coversRootMembers = fullText === identifier.name;
570
+ if (parent && parent.type === import_utils3.AST_NODE_TYPES.CallExpression && parent.callee === fullChainNode) {
571
+ const segments = fullText.split(".");
572
+ if (segments.length > 1) {
573
+ dependencyPath = segments.slice(0, -1).join(".");
574
+ }
575
+ coversRootMembers = false;
576
+ }
577
+ dependencyPath = dependencyPath.split(".")[0] === "" ? identifier.name : dependencyPath;
578
+ const root = dependencyPath.split(".")[0];
579
+ return {
580
+ path: dependencyPath,
581
+ root: root ?? identifier.name,
582
+ coversRootMembers: coversRootMembers && dependencyPath === root
583
+ };
584
+ },
476
585
  collectExternalRefsInFunction(params) {
477
586
  const { functionNode, scopeManager } = params;
478
587
  const functionScope = scopeManager.acquire(functionNode);
@@ -504,6 +613,52 @@ var ExhaustiveDepsUtils = {
504
613
  }
505
614
  collect(functionScope);
506
615
  return externalRefs;
616
+ },
617
+ /**
618
+ * Recursively collects type identifiers from a type annotation
619
+ */
620
+ collectTypeIdentifiers(typeNode, out) {
621
+ switch (typeNode.type) {
622
+ case import_utils3.AST_NODE_TYPES.TSTypeReference: {
623
+ if (typeNode.typeName.type === import_utils3.AST_NODE_TYPES.Identifier) {
624
+ out.add(typeNode.typeName.name);
625
+ }
626
+ break;
627
+ }
628
+ case import_utils3.AST_NODE_TYPES.TSUnionType:
629
+ case import_utils3.AST_NODE_TYPES.TSIntersectionType: {
630
+ typeNode.types.forEach(
631
+ (t) => ExhaustiveDepsUtils.collectTypeIdentifiers(t, out)
632
+ );
633
+ break;
634
+ }
635
+ case import_utils3.AST_NODE_TYPES.TSArrayType: {
636
+ ExhaustiveDepsUtils.collectTypeIdentifiers(typeNode.elementType, out);
637
+ break;
638
+ }
639
+ case import_utils3.AST_NODE_TYPES.TSTupleType: {
640
+ typeNode.elementTypes.forEach(
641
+ (et) => ExhaustiveDepsUtils.collectTypeIdentifiers(et, out)
642
+ );
643
+ break;
644
+ }
645
+ }
646
+ },
647
+ /**
648
+ * Gets the function expression nodes from a queryFn property, handling conditional expressions.
649
+ * When neither branch is skipToken, returns both branches so all deps are scanned.
650
+ */
651
+ getQueryFnNodes(queryFn) {
652
+ if (queryFn.value.type !== import_utils3.AST_NODE_TYPES.ConditionalExpression) {
653
+ return [queryFn.value];
654
+ }
655
+ if (queryFn.value.consequent.type === import_utils3.AST_NODE_TYPES.Identifier && queryFn.value.consequent.name === "skipToken") {
656
+ return [queryFn.value.alternate];
657
+ }
658
+ if (queryFn.value.alternate.type === import_utils3.AST_NODE_TYPES.Identifier && queryFn.value.alternate.name === "skipToken") {
659
+ return [queryFn.value.consequent];
660
+ }
661
+ return [queryFn.value.consequent, queryFn.value.alternate];
507
662
  }
508
663
  };
509
664
 
@@ -526,22 +681,35 @@ var rule = createRule({
526
681
  },
527
682
  hasSuggestions: true,
528
683
  fixable: "code",
529
- schema: []
684
+ schema: [
685
+ {
686
+ type: "object",
687
+ properties: {
688
+ allowlist: {
689
+ type: "object",
690
+ properties: {
691
+ variables: { type: "array", items: { type: "string" } },
692
+ types: { type: "array", items: { type: "string" } }
693
+ },
694
+ additionalProperties: false
695
+ }
696
+ },
697
+ additionalProperties: false
698
+ }
699
+ ]
530
700
  },
531
701
  defaultOptions: [],
532
702
  create: detectTanstackQueryImports((context) => {
533
703
  return {
534
- Property: (node) => {
535
- if (!ASTUtils.isObjectExpression(node.parent) || !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)) {
536
- return;
537
- }
704
+ ObjectExpression: (node) => {
705
+ var _a, _b;
538
706
  const scopeManager = context.sourceCode.scopeManager;
539
707
  const queryKey = ASTUtils.findPropertyWithIdentifierKey(
540
- node.parent.properties,
708
+ node.properties,
541
709
  QUERY_KEY
542
710
  );
543
711
  const queryFn = ASTUtils.findPropertyWithIdentifierKey(
544
- node.parent.properties,
712
+ node.properties,
545
713
  QUERY_FN
546
714
  );
547
715
  if (scopeManager === null || queryKey === void 0 || queryFn === void 0 || !ASTUtils.isNodeOfOneOf(queryFn.value, [
@@ -555,73 +723,104 @@ var rule = createRule({
555
723
  queryKey.value,
556
724
  context
557
725
  );
558
- const externalRefs = ASTUtils.getExternalRefs({
559
- scopeManager,
560
- sourceCode: context.sourceCode,
561
- node: getQueryFnRelevantNode(queryFn)
562
- });
563
- const relevantRefs = externalRefs.filter(
564
- (reference) => ExhaustiveDepsUtils.isRelevantReference({
565
- sourceCode: context.sourceCode,
566
- reference,
726
+ const queryFnNodes = ExhaustiveDepsUtils.getQueryFnNodes(queryFn);
727
+ const externalRefs = queryFnNodes.flatMap(
728
+ (fnNode) => ASTUtils.getExternalRefs({
567
729
  scopeManager,
568
- node: getQueryFnRelevantNode(queryFn),
569
- filename: context.filename
730
+ sourceCode: context.sourceCode,
731
+ node: fnNode
570
732
  })
571
733
  );
734
+ const relevantRefs = externalRefs.filter(
735
+ (reference) => queryFnNodes.some(
736
+ (fnNode) => ExhaustiveDepsUtils.isRelevantReference({
737
+ sourceCode: context.sourceCode,
738
+ reference,
739
+ scopeManager,
740
+ node: fnNode,
741
+ filename: context.filename
742
+ })
743
+ )
744
+ );
745
+ const ruleOptions = context.options.at(0);
746
+ const allowlistedVariables = new Set(
747
+ ((_a = ruleOptions == null ? void 0 : ruleOptions.allowlist) == null ? void 0 : _a.variables) ?? []
748
+ );
749
+ const allowlistedTypes = new Set(((_b = ruleOptions == null ? void 0 : ruleOptions.allowlist) == null ? void 0 : _b.types) ?? []);
750
+ const requiredRefs = relevantRefs.flatMap((ref) => {
751
+ if (ref.identifier.type !== import_utils4.AST_NODE_TYPES.Identifier) return [];
752
+ const refPath = ExhaustiveDepsUtils.computeRefPath({
753
+ identifier: ref.identifier,
754
+ sourceCode: context.sourceCode
755
+ });
756
+ if (refPath === null) return [];
757
+ return [
758
+ {
759
+ ...refPath,
760
+ allowlistedByType: ExhaustiveDepsUtils.variableIsAllowlistedByType({
761
+ allowlistedTypes,
762
+ variable: ref.resolved ?? null
763
+ })
764
+ }
765
+ ];
766
+ });
767
+ if (requiredRefs.length === 0) return;
572
768
  const queryKeyDeps = ExhaustiveDepsUtils.collectQueryKeyDeps({
573
769
  sourceCode: context.sourceCode,
574
770
  scopeManager,
575
771
  queryKeyNode
576
772
  });
577
- const missingRefs = relevantRefs.map((ref) => ({
578
- ref,
579
- text: ASTUtils.isAncestorIsCallee(ref.identifier) ? ref.identifier.name : ASTUtils.mapKeyNodeToBaseText(
580
- ref.identifier,
581
- context.sourceCode
582
- )
583
- })).filter(({ ref, text }) => {
584
- return !ref.isTypeReference && !queryKeyDeps.has(text) && !queryKeyDeps.has(text.split(/[?.]/)[0] ?? "");
585
- }).map(({ ref, text }) => ({
586
- identifier: ref.identifier,
587
- text
588
- }));
589
- const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text);
590
- if (uniqueMissingRefs.length > 0) {
591
- const missingAsText = uniqueMissingRefs.map((ref) => ref.text).join(", ");
592
- const queryKeyValue = context.sourceCode.getText(queryKeyNode);
593
- const existingWithMissing = queryKeyValue === "[]" ? `[${missingAsText}]` : queryKeyValue.replace(/\]$/, `, ${missingAsText}]`);
594
- const suggestions = [];
595
- if (queryKeyNode.type === import_utils4.AST_NODE_TYPES.ArrayExpression) {
596
- suggestions.push({
597
- messageId: "fixTo",
598
- data: { result: existingWithMissing },
599
- fix(fixer) {
600
- return fixer.replaceText(queryKeyNode, existingWithMissing);
601
- }
602
- });
603
- }
604
- context.report({
605
- node,
606
- messageId: "missingDeps",
607
- data: {
608
- deps: uniqueMissingRefs.map((ref) => ref.text).join(", ")
609
- },
610
- suggest: suggestions
611
- });
612
- }
773
+ const missingPaths = ExhaustiveDepsUtils.computeFilteredMissingPaths({
774
+ requiredRefs,
775
+ allowlistedVariables,
776
+ existingRootIdentifiers: queryKeyDeps.roots,
777
+ existingFullPaths: queryKeyDeps.paths
778
+ });
779
+ if (missingPaths.length === 0) return;
780
+ const missingAsText = missingPaths.join(", ");
781
+ const suggestions = buildSuggestions({
782
+ queryKeyNode,
783
+ missingPaths,
784
+ missingAsText,
785
+ sourceCode: context.sourceCode
786
+ });
787
+ context.report({
788
+ node,
789
+ messageId: "missingDeps",
790
+ data: { deps: missingAsText },
791
+ suggest: suggestions
792
+ });
613
793
  }
614
794
  };
615
795
  })
616
796
  });
617
- function getQueryFnRelevantNode(queryFn) {
618
- if (queryFn.value.type !== import_utils4.AST_NODE_TYPES.ConditionalExpression) {
619
- return queryFn.value;
797
+ function buildSuggestions(params) {
798
+ const { queryKeyNode, missingPaths, missingAsText, sourceCode } = params;
799
+ if (queryKeyNode.type !== import_utils4.AST_NODE_TYPES.ArrayExpression) {
800
+ return [];
620
801
  }
621
- if (queryFn.value.consequent.type === import_utils4.AST_NODE_TYPES.Identifier && queryFn.value.consequent.name === "skipToken") {
622
- return queryFn.value.alternate;
802
+ const closingBracket = sourceCode.getLastToken(queryKeyNode);
803
+ if (!closingBracket) return [];
804
+ const existingElements = queryKeyNode.elements.filter((el) => el !== null).map((el) => sourceCode.getText(el));
805
+ const resultText = `[${[...existingElements, ...missingPaths].join(", ")}]`;
806
+ if (queryKeyNode.elements.length === 0) {
807
+ return [
808
+ {
809
+ messageId: "fixTo",
810
+ data: { result: resultText },
811
+ fix: (fixer) => fixer.replaceText(queryKeyNode, resultText)
812
+ }
813
+ ];
623
814
  }
624
- return queryFn.value.consequent;
815
+ const tokenBefore = sourceCode.getTokenBefore(closingBracket);
816
+ const separator = (tokenBefore == null ? void 0 : tokenBefore.value) === "," ? " " : ", ";
817
+ return [
818
+ {
819
+ messageId: "fixTo",
820
+ data: { result: resultText },
821
+ fix: (fixer) => fixer.insertTextBefore(closingBracket, `${separator}${missingAsText}`)
822
+ }
823
+ ];
625
824
  }
626
825
  function dereferenceVariablesAndTypeAssertions(queryKeyNode, context) {
627
826
  const visitedNodes = /* @__PURE__ */ new Set();