NoFragmentCycles.js.flow 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. // @flow strict
  2. import { GraphQLError } from '../../error/GraphQLError';
  3. import { type ASTVisitor } from '../../language/visitor';
  4. import { type FragmentDefinitionNode } from '../../language/ast';
  5. import { type ASTValidationContext } from '../ValidationContext';
  6. export function cycleErrorMessage(
  7. fragName: string,
  8. spreadNames: $ReadOnlyArray<string>,
  9. ): string {
  10. const via = spreadNames.length ? ' via ' + spreadNames.join(', ') : '';
  11. return `Cannot spread fragment "${fragName}" within itself${via}.`;
  12. }
  13. export function NoFragmentCycles(context: ASTValidationContext): ASTVisitor {
  14. // Tracks already visited fragments to maintain O(N) and to ensure that cycles
  15. // are not redundantly reported.
  16. const visitedFrags = Object.create(null);
  17. // Array of AST nodes used to produce meaningful errors
  18. const spreadPath = [];
  19. // Position in the spread path
  20. const spreadPathIndexByName = Object.create(null);
  21. return {
  22. OperationDefinition: () => false,
  23. FragmentDefinition(node) {
  24. detectCycleRecursive(node);
  25. return false;
  26. },
  27. };
  28. // This does a straight-forward DFS to find cycles.
  29. // It does not terminate when a cycle was found but continues to explore
  30. // the graph to find all possible cycles.
  31. function detectCycleRecursive(fragment: FragmentDefinitionNode) {
  32. if (visitedFrags[fragment.name.value]) {
  33. return;
  34. }
  35. const fragmentName = fragment.name.value;
  36. visitedFrags[fragmentName] = true;
  37. const spreadNodes = context.getFragmentSpreads(fragment.selectionSet);
  38. if (spreadNodes.length === 0) {
  39. return;
  40. }
  41. spreadPathIndexByName[fragmentName] = spreadPath.length;
  42. for (const spreadNode of spreadNodes) {
  43. const spreadName = spreadNode.name.value;
  44. const cycleIndex = spreadPathIndexByName[spreadName];
  45. spreadPath.push(spreadNode);
  46. if (cycleIndex === undefined) {
  47. const spreadFragment = context.getFragment(spreadName);
  48. if (spreadFragment) {
  49. detectCycleRecursive(spreadFragment);
  50. }
  51. } else {
  52. const cyclePath = spreadPath.slice(cycleIndex);
  53. const fragmentNames = cyclePath.slice(0, -1).map(s => s.name.value);
  54. context.reportError(
  55. new GraphQLError(
  56. cycleErrorMessage(spreadName, fragmentNames),
  57. cyclePath,
  58. ),
  59. );
  60. }
  61. spreadPath.pop();
  62. }
  63. spreadPathIndexByName[fragmentName] = undefined;
  64. }
  65. }