lib/graphql/internal_representation/rewrite.rb



module GraphQL
  module InternalRepresentation
    # Convert an AST into a tree of {InternalRepresentation::Node}s
    #
    # This rides along with {StaticValidation}, building a tree of nodes.
    #
    # However, if any errors occurred during validation, the resulting tree is bogus.
    #  (For example, `nil` could have been pushed instead of a type.)
    class Rewrite
      include GraphQL::Language

      # @return [Hash<String => InternalRepresentation::Node>] internal representation of each query root (operation, fragment)
      attr_reader :operations

      def initialize
        # { String => Node } Tracks the roots of the query
        @operations = {}
        @fragments = {}
        # [String...] fragments which don't have fragments inside them
        @independent_fragments = []
        # Tracks the current node during traversal
        # Stack<InternalRepresentation::Node>
        @nodes = []
        # This tracks dependencies from fragment to Node where it was used
        # { frag_name => [dependent_node, dependent_node]}
        @fragment_spreads = Hash.new { |h, k| h[k] = []}
        # [Nodes::Directive ... ] directive affecting the current scope
        @parent_directives = []
      end

      def validate(context)
        visitor = context.visitor

        visitor[Nodes::OperationDefinition].enter << -> (ast_node, prev_ast_node) {
          node = Node.new(
            return_type: context.type_definition && context.type_definition.unwrap,
            ast_node: ast_node,
            name: ast_node.name,
            parent: nil,
          )
          @nodes.push(node)
          @operations[ast_node.name] = node
        }

        visitor[Nodes::Field].enter << -> (ast_node, prev_ast_node) {
          parent_node = @nodes.last
          node_name = ast_node.alias || ast_node.name
          # This node might not be novel, eg inside an inline fragment
          # but it could contain new type information, which is captured below.
          # (StaticValidation ensures that merging fields is fair game)
          node = parent_node.children[node_name] ||= begin
            Node.new(
              return_type: context.type_definition && context.type_definition.unwrap,
              ast_node: ast_node,
              name: node_name,
              definition_name: ast_node.name,
              parent: parent_node,
            )
          end
          object_type = context.parent_type_definition.unwrap
          node.definitions[object_type] = context.field_definition
          @nodes.push(node)
          @parent_directives.push([])
        }

        visitor[Nodes::InlineFragment].enter << -> (ast_node, prev_ast_node) {
          @parent_directives.push([])
        }

        visitor[Nodes::Directive].enter << -> (ast_node, prev_ast_node) {
          # It could be a query error where a directive is somewhere it shouldn't be
          if @parent_directives.any?
            @parent_directives.last << Node.new(
              name: ast_node.name,
              definition_name: ast_node.name,
              ast_node: ast_node,
              definitions: [context.directive_definition],
              # This isn't used, the directive may have many parents in the case of inline fragment
              parent: nil,
            )
          end
        }

        visitor[Nodes::FragmentSpread].enter << -> (ast_node, prev_ast_node) {
          parent_node = @nodes.last
          # Record _both sides_ of the dependency
          spread_node = Node.new(
            parent: parent_node,
            name: ast_node.name,
            ast_node: ast_node,
          )
          # The parent node has a reference to the fragment
          parent_node.spreads.push(spread_node)
          # And keep a reference from the fragment to the parent node
          @fragment_spreads[ast_node.name].push(parent_node)
          @nodes.push(spread_node)
          @parent_directives.push([])
        }

        visitor[Nodes::FragmentDefinition].enter << -> (ast_node, prev_ast_node) {
          node = Node.new(
            parent: nil,
            name: ast_node.name,
            return_type: context.type_definition,
            ast_node: ast_node,
          )
          @nodes.push(node)
          @fragments[ast_node.name] = node
        }

        visitor[Nodes::InlineFragment].leave  << -> (ast_node, prev_ast_node) {
          @parent_directives.pop
        }

        visitor[Nodes::FragmentSpread].leave  << -> (ast_node, prev_ast_node) {
          # Capture any directives that apply to this spread
          # so that they can be applied to fields when
          # the fragment is merged in later
          spread_node = @nodes.pop
          spread_node.directives.merge(@parent_directives.flatten)
          @parent_directives.pop
        }

        visitor[Nodes::FragmentDefinition].leave << -> (ast_node, prev_ast_node) {
          # This fragment doesn't depend on any others,
          # we should save it as the starting point for dependency resolution
          frag_node = @nodes.pop
          if !any_fragment_spreads?(frag_node)
            @independent_fragments << frag_node
          end
        }

        visitor[Nodes::OperationDefinition].leave << -> (ast_node, prev_ast_node) {
          @nodes.pop
        }

        visitor[Nodes::Field].leave << -> (ast_node, prev_ast_node) {
          # Pop this field's node
          # and record any directives that were visited
          # during this field & before it (eg, inline fragments)
          field_node = @nodes.pop
          field_node.directives.merge(@parent_directives.flatten)
          @parent_directives.pop
        }

        visitor[Nodes::Document].leave << -> (ast_node, prev_ast_node) {
          # Resolve fragment dependencies. Start with fragments with no
          # dependencies and work along the spreads.
          while fragment_node = @independent_fragments.pop
            fragment_usages = @fragment_spreads[fragment_node.name]
            while dependent_node = fragment_usages.pop
              # remove self from dependent_node.spreads
              rejected_spread_nodes = dependent_node.spreads.select { |spr| spr.name == fragment_node.name }
              rejected_spread_nodes.each { |r_node| dependent_node.spreads.delete(r_node) }

              # resolve the dependency (merge into dependent node)
              deep_merge(dependent_node, fragment_node, rejected_spread_nodes.first.directives)
              owner = dependent_node.owner
              if owner.ast_node.is_a?(Nodes::FragmentDefinition) && !any_fragment_spreads?(owner)
                @independent_fragments.push(owner)
              end
            end
          end
        }
      end

      private

      # Merge the chilren from `fragment_node` into `parent_node`. Merge `directives` into each of those fields.
      def deep_merge(parent_node, fragment_node, directives)
        fragment_node.children.each do |name, child_node|
          deep_merge_child(parent_node, name, child_node, directives)
        end
      end

      # Merge `node` into `parent_node`'s children, as `name`, applying `extra_directives`
      def deep_merge_child(parent_node, name, node, extra_directives)
        child_node = parent_node.children[name] ||= node.dup
        child_node.definitions.merge!(node.definitions)
        node.children.each do |merge_child_name, merge_child_node|
          deep_merge_child(child_node, merge_child_name, merge_child_node, [])
        end
        child_node.directives.merge(extra_directives)
      end

      # return true if node or _any_ children have a fragment spread
      def any_fragment_spreads?(node)
        node.spreads.any? || node.children.any? { |name, node| any_fragment_spreads?(node) }
      end
    end
  end
end