@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.
@@ -104,21 +104,6 @@ var ASTUtils = {
104
104
  }
105
105
  return identifiers;
106
106
  },
107
- isAncestorIsCallee(identifier) {
108
- let previousNode = identifier;
109
- let currentNode = identifier.parent;
110
- while (currentNode !== void 0) {
111
- if (currentNode.type === AST_NODE_TYPES.CallExpression && currentNode.callee === previousNode) {
112
- return true;
113
- }
114
- if (currentNode.type !== AST_NODE_TYPES.MemberExpression) {
115
- return false;
116
- }
117
- previousNode = currentNode;
118
- currentNode = currentNode.parent;
119
- }
120
- return false;
121
- },
122
107
  traverseUpOnly(identifier, allowedNodeTypes) {
123
108
  const parent = identifier.parent;
124
109
  if (parent !== void 0 && allowedNodeTypes.includes(parent.type)) {
@@ -339,7 +324,7 @@ var ExhaustiveDepsUtils = {
339
324
  const { sourceCode, reference, scopeManager, node, filename } = params;
340
325
  const component = ASTUtils.getFunctionAncestor(sourceCode, node);
341
326
  const queryFnScope = scopeManager.acquire(node);
342
- if (queryFnScope === null) {
327
+ if (queryFnScope === null || reference.isValueReference === false) {
343
328
  return false;
344
329
  }
345
330
  let currentScope = reference.resolved?.scope ?? null;
@@ -371,15 +356,57 @@ var ExhaustiveDepsUtils = {
371
356
  }
372
357
  return reference.identifier.name !== "undefined" && reference.identifier.parent.type !== AST_NODE_TYPES2.NewExpression && !ExhaustiveDepsUtils.isInstanceOfKind(reference.identifier.parent);
373
358
  },
374
- isInstanceOfKind(node) {
375
- return node.type === AST_NODE_TYPES2.BinaryExpression && node.operator === "instanceof";
359
+ /**
360
+ * Given required refs and existing queryKey entries, compute missing dependency paths
361
+ * respecting allowlisted variables and types.
362
+ */
363
+ computeFilteredMissingPaths(params) {
364
+ const {
365
+ requiredRefs,
366
+ allowlistedVariables,
367
+ existingRootIdentifiers,
368
+ existingFullPaths
369
+ } = params;
370
+ const missingPaths = /* @__PURE__ */ new Set();
371
+ for (const { root, path, allowlistedByType } of requiredRefs) {
372
+ if (existingRootIdentifiers.has(root)) continue;
373
+ if (allowlistedVariables.has(root)) continue;
374
+ if (existingFullPaths.has(path)) continue;
375
+ if (allowlistedByType) continue;
376
+ missingPaths.add(path);
377
+ }
378
+ for (const path of missingPaths) {
379
+ const root = path.split(".")[0];
380
+ if (root !== path && root !== void 0 && missingPaths.has(root)) {
381
+ missingPaths.delete(path);
382
+ }
383
+ }
384
+ return Array.from(missingPaths);
376
385
  },
386
+ /**
387
+ * Extract existing queryKey deps as root identifiers and full member paths.
388
+ */
377
389
  collectQueryKeyDeps(params) {
378
390
  const { sourceCode, scopeManager, queryKeyNode } = params;
379
- const deps = /* @__PURE__ */ new Set();
391
+ const roots = /* @__PURE__ */ new Set();
392
+ const paths = /* @__PURE__ */ new Set();
380
393
  const visitorKeys = sourceCode.visitorKeys;
381
- function add(identifier) {
382
- deps.add(ASTUtils.mapKeyNodeToBaseText(identifier, sourceCode));
394
+ function addRoot(name8) {
395
+ const cleaned = ExhaustiveDepsUtils.normalizeChain(name8);
396
+ roots.add(cleaned);
397
+ paths.add(cleaned);
398
+ }
399
+ function addFull(text) {
400
+ const cleaned = ExhaustiveDepsUtils.normalizeChain(text);
401
+ paths.add(cleaned);
402
+ }
403
+ function addRefPath(refPath) {
404
+ if (!refPath) return;
405
+ if (refPath.coversRootMembers) {
406
+ addRoot(refPath.root);
407
+ return;
408
+ }
409
+ addFull(refPath.path);
383
410
  }
384
411
  function visitChildren(node) {
385
412
  const keys = visitorKeys[node.type] ?? [];
@@ -401,9 +428,15 @@ var ExhaustiveDepsUtils = {
401
428
  function visit(node) {
402
429
  if (!node) return;
403
430
  switch (node.type) {
404
- case AST_NODE_TYPES2.Identifier:
405
- add(node);
431
+ case AST_NODE_TYPES2.Identifier: {
432
+ addRefPath(
433
+ ExhaustiveDepsUtils.computeRefPath({
434
+ identifier: node,
435
+ sourceCode
436
+ })
437
+ );
406
438
  return;
439
+ }
407
440
  case AST_NODE_TYPES2.ArrowFunctionExpression:
408
441
  case AST_NODE_TYPES2.FunctionExpression:
409
442
  for (const reference of ExhaustiveDepsUtils.collectExternalRefsInFunction(
@@ -412,9 +445,15 @@ var ExhaustiveDepsUtils = {
412
445
  scopeManager
413
446
  }
414
447
  )) {
415
- if (reference.identifier.type === AST_NODE_TYPES2.Identifier) {
416
- add(reference.identifier);
448
+ if (reference.identifier.type !== AST_NODE_TYPES2.Identifier) {
449
+ continue;
417
450
  }
451
+ addRefPath(
452
+ ExhaustiveDepsUtils.computeRefPath({
453
+ identifier: reference.identifier,
454
+ sourceCode
455
+ })
456
+ );
418
457
  }
419
458
  return;
420
459
  case AST_NODE_TYPES2.Property:
@@ -422,26 +461,96 @@ var ExhaustiveDepsUtils = {
422
461
  return;
423
462
  case AST_NODE_TYPES2.MemberExpression:
424
463
  if (node.parent.type === AST_NODE_TYPES2.CallExpression && node.parent.callee === node && node.object.type === AST_NODE_TYPES2.Identifier) {
425
- deps.add(node.object.name);
464
+ addRoot(node.object.name);
426
465
  } else {
427
466
  visit(node.object);
428
467
  }
429
468
  return;
430
469
  case AST_NODE_TYPES2.CallExpression:
431
470
  node.arguments.forEach((argument) => visit(argument));
432
- if (node.callee.type === AST_NODE_TYPES2.MemberExpression || node.callee.type === AST_NODE_TYPES2.ChainExpression || node.callee.type === AST_NODE_TYPES2.TSNonNullExpression) {
433
- visit(node.callee);
471
+ switch (node.callee.type) {
472
+ case AST_NODE_TYPES2.Identifier:
473
+ case AST_NODE_TYPES2.MemberExpression:
474
+ case AST_NODE_TYPES2.ChainExpression:
475
+ case AST_NODE_TYPES2.TSNonNullExpression:
476
+ visit(node.callee);
477
+ break;
434
478
  }
435
479
  return;
436
480
  }
437
481
  visitChildren(node);
438
482
  }
439
483
  visit(queryKeyNode);
440
- return deps;
484
+ return { roots, paths };
441
485
  },
442
486
  isNode(value) {
443
487
  return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
444
488
  },
489
+ /**
490
+ * Checks whether the resolved variable is allowlisted by its type annotation
491
+ */
492
+ variableIsAllowlistedByType(params) {
493
+ const { allowlistedTypes, variable } = params;
494
+ if (allowlistedTypes.size === 0) return false;
495
+ if (!variable) return false;
496
+ for (const id of variable.identifiers) {
497
+ if (id.typeAnnotation) {
498
+ const typeIdentifiers = /* @__PURE__ */ new Set();
499
+ ExhaustiveDepsUtils.collectTypeIdentifiers(
500
+ id.typeAnnotation.typeAnnotation,
501
+ typeIdentifiers
502
+ );
503
+ for (const typeIdentifier of typeIdentifiers) {
504
+ if (allowlistedTypes.has(typeIdentifier)) return true;
505
+ }
506
+ }
507
+ }
508
+ return false;
509
+ },
510
+ isInstanceOfKind(node) {
511
+ return node.type === AST_NODE_TYPES2.BinaryExpression && node.operator === "instanceof";
512
+ },
513
+ /**
514
+ * Normalizes a chain by removing optional chaining operators
515
+ *
516
+ * Example: `a?.b.c!` -> `a.b.c`
517
+ */
518
+ normalizeChain(text) {
519
+ return text.replace(/(?:\?(\.)|!)/g, "$1");
520
+ },
521
+ /**
522
+ * Computes the reference path for an identifier
523
+ *
524
+ * Example: `a.b.c!` -> `{ path: 'a.b.c', root: 'a' }`
525
+ */
526
+ computeRefPath(params) {
527
+ const { identifier, sourceCode } = params;
528
+ const fullChainNode = ASTUtils.traverseUpOnly(identifier, [
529
+ AST_NODE_TYPES2.MemberExpression,
530
+ AST_NODE_TYPES2.TSNonNullExpression,
531
+ AST_NODE_TYPES2.Identifier
532
+ ]);
533
+ const fullText = ExhaustiveDepsUtils.normalizeChain(
534
+ sourceCode.getText(fullChainNode)
535
+ );
536
+ const parent = fullChainNode.parent;
537
+ let dependencyPath = fullText;
538
+ let coversRootMembers = fullText === identifier.name;
539
+ if (parent && parent.type === AST_NODE_TYPES2.CallExpression && parent.callee === fullChainNode) {
540
+ const segments = fullText.split(".");
541
+ if (segments.length > 1) {
542
+ dependencyPath = segments.slice(0, -1).join(".");
543
+ }
544
+ coversRootMembers = false;
545
+ }
546
+ dependencyPath = dependencyPath.split(".")[0] === "" ? identifier.name : dependencyPath;
547
+ const root = dependencyPath.split(".")[0];
548
+ return {
549
+ path: dependencyPath,
550
+ root: root ?? identifier.name,
551
+ coversRootMembers: coversRootMembers && dependencyPath === root
552
+ };
553
+ },
445
554
  collectExternalRefsInFunction(params) {
446
555
  const { functionNode, scopeManager } = params;
447
556
  const functionScope = scopeManager.acquire(functionNode);
@@ -473,6 +582,52 @@ var ExhaustiveDepsUtils = {
473
582
  }
474
583
  collect(functionScope);
475
584
  return externalRefs;
585
+ },
586
+ /**
587
+ * Recursively collects type identifiers from a type annotation
588
+ */
589
+ collectTypeIdentifiers(typeNode, out) {
590
+ switch (typeNode.type) {
591
+ case AST_NODE_TYPES2.TSTypeReference: {
592
+ if (typeNode.typeName.type === AST_NODE_TYPES2.Identifier) {
593
+ out.add(typeNode.typeName.name);
594
+ }
595
+ break;
596
+ }
597
+ case AST_NODE_TYPES2.TSUnionType:
598
+ case AST_NODE_TYPES2.TSIntersectionType: {
599
+ typeNode.types.forEach(
600
+ (t) => ExhaustiveDepsUtils.collectTypeIdentifiers(t, out)
601
+ );
602
+ break;
603
+ }
604
+ case AST_NODE_TYPES2.TSArrayType: {
605
+ ExhaustiveDepsUtils.collectTypeIdentifiers(typeNode.elementType, out);
606
+ break;
607
+ }
608
+ case AST_NODE_TYPES2.TSTupleType: {
609
+ typeNode.elementTypes.forEach(
610
+ (et) => ExhaustiveDepsUtils.collectTypeIdentifiers(et, out)
611
+ );
612
+ break;
613
+ }
614
+ }
615
+ },
616
+ /**
617
+ * Gets the function expression nodes from a queryFn property, handling conditional expressions.
618
+ * When neither branch is skipToken, returns both branches so all deps are scanned.
619
+ */
620
+ getQueryFnNodes(queryFn) {
621
+ if (queryFn.value.type !== AST_NODE_TYPES2.ConditionalExpression) {
622
+ return [queryFn.value];
623
+ }
624
+ if (queryFn.value.consequent.type === AST_NODE_TYPES2.Identifier && queryFn.value.consequent.name === "skipToken") {
625
+ return [queryFn.value.alternate];
626
+ }
627
+ if (queryFn.value.alternate.type === AST_NODE_TYPES2.Identifier && queryFn.value.alternate.name === "skipToken") {
628
+ return [queryFn.value.consequent];
629
+ }
630
+ return [queryFn.value.consequent, queryFn.value.alternate];
476
631
  }
477
632
  };
478
633
 
@@ -495,22 +650,34 @@ var rule = createRule({
495
650
  },
496
651
  hasSuggestions: true,
497
652
  fixable: "code",
498
- schema: []
653
+ schema: [
654
+ {
655
+ type: "object",
656
+ properties: {
657
+ allowlist: {
658
+ type: "object",
659
+ properties: {
660
+ variables: { type: "array", items: { type: "string" } },
661
+ types: { type: "array", items: { type: "string" } }
662
+ },
663
+ additionalProperties: false
664
+ }
665
+ },
666
+ additionalProperties: false
667
+ }
668
+ ]
499
669
  },
500
670
  defaultOptions: [],
501
671
  create: detectTanstackQueryImports((context) => {
502
672
  return {
503
- Property: (node) => {
504
- if (!ASTUtils.isObjectExpression(node.parent) || !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)) {
505
- return;
506
- }
673
+ ObjectExpression: (node) => {
507
674
  const scopeManager = context.sourceCode.scopeManager;
508
675
  const queryKey = ASTUtils.findPropertyWithIdentifierKey(
509
- node.parent.properties,
676
+ node.properties,
510
677
  QUERY_KEY
511
678
  );
512
679
  const queryFn = ASTUtils.findPropertyWithIdentifierKey(
513
- node.parent.properties,
680
+ node.properties,
514
681
  QUERY_FN
515
682
  );
516
683
  if (scopeManager === null || queryKey === void 0 || queryFn === void 0 || !ASTUtils.isNodeOfOneOf(queryFn.value, [
@@ -524,73 +691,104 @@ var rule = createRule({
524
691
  queryKey.value,
525
692
  context
526
693
  );
527
- const externalRefs = ASTUtils.getExternalRefs({
528
- scopeManager,
529
- sourceCode: context.sourceCode,
530
- node: getQueryFnRelevantNode(queryFn)
531
- });
532
- const relevantRefs = externalRefs.filter(
533
- (reference) => ExhaustiveDepsUtils.isRelevantReference({
534
- sourceCode: context.sourceCode,
535
- reference,
694
+ const queryFnNodes = ExhaustiveDepsUtils.getQueryFnNodes(queryFn);
695
+ const externalRefs = queryFnNodes.flatMap(
696
+ (fnNode) => ASTUtils.getExternalRefs({
536
697
  scopeManager,
537
- node: getQueryFnRelevantNode(queryFn),
538
- filename: context.filename
698
+ sourceCode: context.sourceCode,
699
+ node: fnNode
539
700
  })
540
701
  );
702
+ const relevantRefs = externalRefs.filter(
703
+ (reference) => queryFnNodes.some(
704
+ (fnNode) => ExhaustiveDepsUtils.isRelevantReference({
705
+ sourceCode: context.sourceCode,
706
+ reference,
707
+ scopeManager,
708
+ node: fnNode,
709
+ filename: context.filename
710
+ })
711
+ )
712
+ );
713
+ const ruleOptions = context.options.at(0);
714
+ const allowlistedVariables = new Set(
715
+ ruleOptions?.allowlist?.variables ?? []
716
+ );
717
+ const allowlistedTypes = new Set(ruleOptions?.allowlist?.types ?? []);
718
+ const requiredRefs = relevantRefs.flatMap((ref) => {
719
+ if (ref.identifier.type !== AST_NODE_TYPES3.Identifier) return [];
720
+ const refPath = ExhaustiveDepsUtils.computeRefPath({
721
+ identifier: ref.identifier,
722
+ sourceCode: context.sourceCode
723
+ });
724
+ if (refPath === null) return [];
725
+ return [
726
+ {
727
+ ...refPath,
728
+ allowlistedByType: ExhaustiveDepsUtils.variableIsAllowlistedByType({
729
+ allowlistedTypes,
730
+ variable: ref.resolved ?? null
731
+ })
732
+ }
733
+ ];
734
+ });
735
+ if (requiredRefs.length === 0) return;
541
736
  const queryKeyDeps = ExhaustiveDepsUtils.collectQueryKeyDeps({
542
737
  sourceCode: context.sourceCode,
543
738
  scopeManager,
544
739
  queryKeyNode
545
740
  });
546
- const missingRefs = relevantRefs.map((ref) => ({
547
- ref,
548
- text: ASTUtils.isAncestorIsCallee(ref.identifier) ? ref.identifier.name : ASTUtils.mapKeyNodeToBaseText(
549
- ref.identifier,
550
- context.sourceCode
551
- )
552
- })).filter(({ ref, text }) => {
553
- return !ref.isTypeReference && !queryKeyDeps.has(text) && !queryKeyDeps.has(text.split(/[?.]/)[0] ?? "");
554
- }).map(({ ref, text }) => ({
555
- identifier: ref.identifier,
556
- text
557
- }));
558
- const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text);
559
- if (uniqueMissingRefs.length > 0) {
560
- const missingAsText = uniqueMissingRefs.map((ref) => ref.text).join(", ");
561
- const queryKeyValue = context.sourceCode.getText(queryKeyNode);
562
- const existingWithMissing = queryKeyValue === "[]" ? `[${missingAsText}]` : queryKeyValue.replace(/\]$/, `, ${missingAsText}]`);
563
- const suggestions = [];
564
- if (queryKeyNode.type === AST_NODE_TYPES3.ArrayExpression) {
565
- suggestions.push({
566
- messageId: "fixTo",
567
- data: { result: existingWithMissing },
568
- fix(fixer) {
569
- return fixer.replaceText(queryKeyNode, existingWithMissing);
570
- }
571
- });
572
- }
573
- context.report({
574
- node,
575
- messageId: "missingDeps",
576
- data: {
577
- deps: uniqueMissingRefs.map((ref) => ref.text).join(", ")
578
- },
579
- suggest: suggestions
580
- });
581
- }
741
+ const missingPaths = ExhaustiveDepsUtils.computeFilteredMissingPaths({
742
+ requiredRefs,
743
+ allowlistedVariables,
744
+ existingRootIdentifiers: queryKeyDeps.roots,
745
+ existingFullPaths: queryKeyDeps.paths
746
+ });
747
+ if (missingPaths.length === 0) return;
748
+ const missingAsText = missingPaths.join(", ");
749
+ const suggestions = buildSuggestions({
750
+ queryKeyNode,
751
+ missingPaths,
752
+ missingAsText,
753
+ sourceCode: context.sourceCode
754
+ });
755
+ context.report({
756
+ node,
757
+ messageId: "missingDeps",
758
+ data: { deps: missingAsText },
759
+ suggest: suggestions
760
+ });
582
761
  }
583
762
  };
584
763
  })
585
764
  });
586
- function getQueryFnRelevantNode(queryFn) {
587
- if (queryFn.value.type !== AST_NODE_TYPES3.ConditionalExpression) {
588
- return queryFn.value;
765
+ function buildSuggestions(params) {
766
+ const { queryKeyNode, missingPaths, missingAsText, sourceCode } = params;
767
+ if (queryKeyNode.type !== AST_NODE_TYPES3.ArrayExpression) {
768
+ return [];
589
769
  }
590
- if (queryFn.value.consequent.type === AST_NODE_TYPES3.Identifier && queryFn.value.consequent.name === "skipToken") {
591
- return queryFn.value.alternate;
770
+ const closingBracket = sourceCode.getLastToken(queryKeyNode);
771
+ if (!closingBracket) return [];
772
+ const existingElements = queryKeyNode.elements.filter((el) => el !== null).map((el) => sourceCode.getText(el));
773
+ const resultText = `[${[...existingElements, ...missingPaths].join(", ")}]`;
774
+ if (queryKeyNode.elements.length === 0) {
775
+ return [
776
+ {
777
+ messageId: "fixTo",
778
+ data: { result: resultText },
779
+ fix: (fixer) => fixer.replaceText(queryKeyNode, resultText)
780
+ }
781
+ ];
592
782
  }
593
- return queryFn.value.consequent;
783
+ const tokenBefore = sourceCode.getTokenBefore(closingBracket);
784
+ const separator = tokenBefore?.value === "," ? " " : ", ";
785
+ return [
786
+ {
787
+ messageId: "fixTo",
788
+ data: { result: resultText },
789
+ fix: (fixer) => fixer.insertTextBefore(closingBracket, `${separator}${missingAsText}`)
790
+ }
791
+ ];
594
792
  }
595
793
  function dereferenceVariablesAndTypeAssertions(queryKeyNode, context) {
596
794
  const visitedNodes = /* @__PURE__ */ new Set();
@@ -1134,4 +1332,4 @@ var rules = {
1134
1332
  export {
1135
1333
  rules
1136
1334
  };
1137
- //# sourceMappingURL=chunk-XO2SGL7P.js.map
1335
+ //# sourceMappingURL=chunk-XH4ABCYA.js.map