noFragmentCyclesRule function

Visitor noFragmentCyclesRule(
  1. SDLValidationCtx context
)

No fragment cycles

The graph of fragment spreads must not form any cycles including spreading itself. Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data.

See https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles

Implementation

Visitor noFragmentCyclesRule(
  SDLValidationCtx context, // ASTValidationContext,
) {
  // Tracks already visited fragments to maintain O(N) and to ensure that cycles
  // are not redundantly reported.
  final visitedFrags = <String>{};

  // Array of AST nodes used to produce meaningful errors
  final spreadPath = <FragmentSpreadNode>[];

  // Position in the spread path
  final spreadPathIndexByName = <String, int?>{};

  // This does a straight-forward DFS to find cycles.
  // It does not terminate when a cycle was found but continues to explore
  // the graph to find all possible cycles.
  void detectCycleRecursive(FragmentDefinitionNode fragment) {
    if (visitedFrags.contains(fragment.name.value)) {
      return;
    }

    final fragmentName = fragment.name.value;
    visitedFrags.add(fragmentName);

    final spreadNodes = context.getFragmentSpreads(fragment.selectionSet);
    if (spreadNodes.isEmpty) {
      return;
    }

    spreadPathIndexByName[fragmentName] = spreadPath.length;

    for (final spreadNode in spreadNodes) {
      final spreadName = spreadNode.name.value;
      final cycleIndex = spreadPathIndexByName[spreadName];

      spreadPath.add(spreadNode);
      if (cycleIndex == null) {
        final spreadFragment = context.fragmentsMap[spreadName];
        if (spreadFragment != null) {
          detectCycleRecursive(spreadFragment);
        }
      } else {
        final cyclePath = spreadPath.slice(cycleIndex);
        final viaPath = spreadPath
            .slice(cycleIndex, spreadPath.length - 1)
            .map((s) => '"${s.name.value}"')
            .join(', ');

        context.reportError(
          GraphQLError(
            'Cannot spread fragment "${spreadName}" within itself' +
                (viaPath != '' ? ' via ${viaPath}.' : '.'),
            locations: [
              ...cyclePath.map(
                (e) => GraphQLErrorLocation.fromSourceLocation(
                  (e.span ?? e.name.span)!.start,
                ),
              )
            ],
            extensions: _noFragmentCyclesSpec.extensions(),
          ),
        );
      }
      spreadPath.removeLast();
    }

    spreadPathIndexByName[fragmentName] = null;
  }

  final visitor = TypedVisitor();

  visitor.add<OperationDefinitionNode>((_) => VisitBehavior.skipTree);
  visitor.add<FragmentDefinitionNode>((node) {
    detectCycleRecursive(node);
    return VisitBehavior.skipTree;
  });

  return visitor;
}