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