File: definition_dependencies.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (204 lines) | stat: -rw-r--r-- 8,143 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# frozen_string_literal: true
module GraphQL
  module StaticValidation
    # Track fragment dependencies for operations
    # and expose the fragment definitions which
    # are used by a given operation
    module DefinitionDependencies
      attr_reader :dependencies

      def initialize(*)
        super
        @defdep_node_paths = {}

        # { name => [node, ...] } pairs for fragments (although duplicate-named fragments are _invalid_, they are _possible_)
        @defdep_fragment_definitions = Hash.new{ |h, k| h[k] = [] }

        # This tracks dependencies from fragment to Node where it was used
        # { fragment_definition_name => [dependent_node, dependent_node]}
        @defdep_dependent_definitions = Hash.new { |h, k| h[k] = Set.new }

        # First-level usages of spreads within definitions
        # (When a key has an empty list as its value,
        #  we can resolve that key's dependents)
        # { definition_node => [node, node ...] }
        @defdep_immediate_dependencies = Hash.new { |h, k| h[k] = Set.new }

        # When we encounter a spread,
        # this node is the one who depends on it
        @defdep_current_parent = nil
      end

      def on_document(node, parent)
        node.definitions.each do |definition|
          if definition.is_a? GraphQL::Language::Nodes::FragmentDefinition
            @defdep_fragment_definitions[definition.name] << definition
          end
        end
        super
        @dependencies = dependency_map { |defn, spreads, frag|
          context.on_dependency_resolve_handlers.each { |h| h.call(defn, spreads, frag) }
        }
      end

      def on_operation_definition(node, prev_node)
        @defdep_node_paths[node.name] = NodeWithPath.new(node, context.path)
        @defdep_current_parent = node
        super
        @defdep_current_parent = nil
      end

      def on_fragment_definition(node, parent)
        @defdep_node_paths[node] = NodeWithPath.new(node, context.path)
        @defdep_current_parent = node
        super
        @defdep_current_parent = nil
      end

      def on_fragment_spread(node, parent)
        @defdep_node_paths[node] = NodeWithPath.new(node, context.path)

        # Track both sides of the dependency
        @defdep_dependent_definitions[node.name] << @defdep_current_parent
        @defdep_immediate_dependencies[@defdep_current_parent] << node
        super
      end

      # A map of operation definitions to an array of that operation's dependencies
      # @return [DependencyMap]
      def dependency_map(&block)
        @dependency_map ||= resolve_dependencies(&block)
      end

      # Map definition AST nodes to the definition AST nodes they depend on.
      # Expose circular dependencies.
      class DependencyMap
        # @return [Array<GraphQL::Language::Nodes::FragmentDefinition>]
        attr_reader :cyclical_definitions

        # @return [Hash<Node, Array<GraphQL::Language::Nodes::FragmentSpread>>]
        attr_reader :unmet_dependencies

        # @return [Array<GraphQL::Language::Nodes::FragmentDefinition>]
        attr_reader :unused_dependencies

        def initialize
          @dependencies = Hash.new { |h, k| h[k] = [] }
          @cyclical_definitions = []
          @unmet_dependencies = Hash.new { |h, k| h[k] = [] }
          @unused_dependencies = []
        end

        # @return [Array<GraphQL::Language::Nodes::AbstractNode>] dependencies for `definition_node`
        def [](definition_node)
          @dependencies[definition_node]
        end
      end

      class NodeWithPath
        extend Forwardable
        attr_reader :node, :path
        def initialize(node, path)
          @node = node
          @path = path
        end

        def_delegators :@node, :name, :eql?, :hash
      end

      private

      # Return a hash of { node => [node, node ... ]} pairs
      # Keys are top-level definitions
      # Values are arrays of flattened dependencies
      def resolve_dependencies
        dependency_map = DependencyMap.new
        # Don't allow the loop to run more times
        # than the number of fragments in the document
        max_loops = 0
        @defdep_fragment_definitions.each_value do |v|
          max_loops += v.size
        end

        loops = 0

        # Instead of tracking independent fragments _as you visit_,
        # determine them at the end. This way, we can treat fragments with the
        # same name as if they were the same name. If _any_ of the fragments
        # with that name has a dependency, we record it.
        independent_fragment_nodes = @defdep_fragment_definitions.values.flatten - @defdep_immediate_dependencies.keys
        visited_fragment_names = Set.new
        while fragment_node = independent_fragment_nodes.pop
          if visited_fragment_names.add?(fragment_node.name)
            # this is a new fragment name
          else
            # this is a duplicate fragment name
            next
          end
          loops += 1
          if loops > max_loops
            raise("Resolution loops exceeded the number of definitions; infinite loop detected. (Max: #{max_loops}, Current: #{loops})")
          end
          # Since it's independent, let's remove it from here.
          # That way, we can use the remainder to identify cycles
          @defdep_immediate_dependencies.delete(fragment_node)
          fragment_usages = @defdep_dependent_definitions[fragment_node.name]
          if fragment_usages.empty?
            # If we didn't record any usages during the visit,
            # then this fragment is unused.
            dependency_map.unused_dependencies << @defdep_node_paths[fragment_node]
          else
            fragment_usages.each do |definition_node|
              # Register the dependency AND second-order dependencies
              dependency_map[definition_node] << fragment_node
              dependency_map[definition_node].concat(dependency_map[fragment_node])
              # Since we've registered it, remove it from our to-do list
              deps = @defdep_immediate_dependencies[definition_node]
              # Can't find a way to _just_ delete from `deps` and return the deleted entries
              removed, remaining = deps.partition { |spread| spread.name == fragment_node.name }
              @defdep_immediate_dependencies[definition_node] = remaining
              if block_given?
                yield(definition_node, removed, fragment_node)
              end
              if remaining.empty? &&
                definition_node.is_a?(GraphQL::Language::Nodes::FragmentDefinition) &&
                definition_node.name != fragment_node.name
                # If all of this definition's dependencies have
                # been resolved, we can now resolve its
                # own dependents.
                #
                # But, it's possible to have a duplicate-named fragment here.
                # Skip it in that case
                independent_fragment_nodes << definition_node
              end
            end
          end
        end

        # If any dependencies were _unmet_
        # (eg, spreads with no corresponding definition)
        # then they're still in there
        @defdep_immediate_dependencies.each do |defn_node, deps|
          deps.each do |spread|
            if !@defdep_fragment_definitions.key?(spread.name)
              dependency_map.unmet_dependencies[@defdep_node_paths[defn_node]] << @defdep_node_paths[spread]
              deps.delete(spread)
            end
          end
          if deps.empty?
            @defdep_immediate_dependencies.delete(defn_node)
          end
        end

        # Anything left in @immediate_dependencies is cyclical
        cyclical_nodes = @defdep_immediate_dependencies.keys.map { |n| @defdep_node_paths[n] }
        # @immediate_dependencies also includes operation names, but we don't care about
        # those. They became nil when we looked them up on `@fragment_definitions`, so remove them.
        cyclical_nodes.compact!
        dependency_map.cyclical_definitions.concat(cyclical_nodes)

        dependency_map
      end
    end
  end
end