File: visitor.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 (293 lines) | stat: -rw-r--r-- 11,334 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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# frozen_string_literal: true
module GraphQL
  module Language
    # Depth-first traversal through the tree, calling hooks at each stop.
    #
    # @example Create a visitor counting certain field names
    #   class NameCounter < GraphQL::Language::Visitor
    #     def initialize(document, field_name)
    #       super(document)
    #       @field_name = field_name
    #       @count = 0
    #     end
    #
    #     attr_reader :count
    #
    #     def on_field(node, parent)
    #       # if this field matches our search, increment the counter
    #       if node.name == @field_name
    #         @count += 1
    #       end
    #       # Continue visiting subfields:
    #       super
    #     end
    #   end
    #
    #   # Initialize a visitor
    #   visitor = NameCounter.new(document, "name")
    #   # Run it
    #   visitor.visit
    #   # Check the result
    #   visitor.count
    #   # => 3
    #
    # @see GraphQL::Language::StaticVisitor for a faster visitor that doesn't support modifying the document
    class Visitor
      class DeleteNode; end

      # When this is returned from a visitor method,
      # Then the `node` passed into the method is removed from `parent`'s children.
      DELETE_NODE = DeleteNode.new

      def initialize(document)
        @document = document
        @result = nil
      end

      # @return [GraphQL::Language::Nodes::Document] The document with any modifications applied
      attr_reader :result

      # Visit `document` and all children
      # @return [void]
      def visit
        # `@document` may be any kind of node:
        visit_method = :"#{@document.visit_method}_with_modifications"
        result = public_send(visit_method, @document, nil)
        @result = if result.is_a?(Array)
          result.first
        else
          # The node wasn't modified
          @document
        end
      end

      def on_document_children(document_node)
        new_node = document_node
        document_node.children.each do |child_node|
          visit_method = :"#{child_node.visit_method}_with_modifications"
          new_child_and_node = public_send(visit_method, child_node, new_node)
          # Reassign `node` in case the child hook makes a modification
          if new_child_and_node.is_a?(Array)
            new_node = new_child_and_node[1]
          end
        end
        new_node
      end

      def on_field_children(new_node)
        new_node.arguments.each do |arg_node| # rubocop:disable Development/ContextIsPassedCop
          new_child_and_node = on_argument_with_modifications(arg_node, new_node)
          # Reassign `node` in case the child hook makes a modification
          if new_child_and_node.is_a?(Array)
            new_node = new_child_and_node[1]
          end
        end
        new_node = visit_directives(new_node)
        new_node = visit_selections(new_node)
        new_node
      end

      def visit_directives(new_node)
        new_node.directives.each do |dir_node|
          new_child_and_node = on_directive_with_modifications(dir_node, new_node)
          # Reassign `node` in case the child hook makes a modification
          if new_child_and_node.is_a?(Array)
            new_node = new_child_and_node[1]
          end
        end
        new_node
      end

      def visit_selections(new_node)
        new_node.selections.each do |selection|
          new_child_and_node = case selection
          when GraphQL::Language::Nodes::Field
            on_field_with_modifications(selection, new_node)
          when GraphQL::Language::Nodes::InlineFragment
            on_inline_fragment_with_modifications(selection, new_node)
          when GraphQL::Language::Nodes::FragmentSpread
            on_fragment_spread_with_modifications(selection, new_node)
          else
            raise ArgumentError, "Invariant: unexpected field selection #{selection.class} (#{selection.inspect})"
          end
          # Reassign `node` in case the child hook makes a modification
          if new_child_and_node.is_a?(Array)
            new_node = new_child_and_node[1]
          end
        end
        new_node
      end

      def on_fragment_definition_children(new_node)
        new_node = visit_directives(new_node)
        new_node = visit_selections(new_node)
        new_node
      end

      alias :on_inline_fragment_children :on_fragment_definition_children

      def on_operation_definition_children(new_node)
        new_node.variables.each do |arg_node|
          new_child_and_node = on_variable_definition_with_modifications(arg_node, new_node)
          # Reassign `node` in case the child hook makes a modification
          if new_child_and_node.is_a?(Array)
            new_node = new_child_and_node[1]
          end
        end
        new_node = visit_directives(new_node)
        new_node = visit_selections(new_node)
        new_node
      end

      def on_argument_children(new_node)
        new_node.children.each do |value_node|
          new_child_and_node = case value_node
          when Language::Nodes::VariableIdentifier
            on_variable_identifier_with_modifications(value_node, new_node)
          when Language::Nodes::InputObject
            on_input_object_with_modifications(value_node, new_node)
          when Language::Nodes::Enum
            on_enum_with_modifications(value_node, new_node)
          when Language::Nodes::NullValue
            on_null_value_with_modifications(value_node, new_node)
          else
            raise ArgumentError, "Invariant: unexpected argument value node #{value_node.class} (#{value_node.inspect})"
          end
          # Reassign `node` in case the child hook makes a modification
          if new_child_and_node.is_a?(Array)
            new_node = new_child_and_node[1]
          end
        end
        new_node
      end

      # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time

      # We don't use `alias` here because it breaks `super`
      def self.make_visit_methods(ast_node_class)
        node_method = ast_node_class.visit_method
        children_of_type = ast_node_class.children_of_type
        child_visit_method = :"#{node_method}_children"

        class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
          # The default implementation for visiting an AST node.
          # It doesn't _do_ anything, but it continues to visiting the node's children.
          # To customize this hook, override one of its make_visit_methods (or the base method?)
          # in your subclasses.
          #
          # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
          # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
          # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`.
          def #{node_method}(node, parent)
            if node.equal?(DELETE_NODE)
              # This might be passed to `super(DELETE_NODE, ...)`
              # by a user hook, don't want to keep visiting in that case.
              [node, parent]
            else
              new_node = node
              #{
                if method_defined?(child_visit_method)
                  "new_node = #{child_visit_method}(new_node)"
                elsif children_of_type
                  children_of_type.map do |child_accessor, child_class|
                    "node.#{child_accessor}.each do |child_node|
                      new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node)
                      # Reassign `node` in case the child hook makes a modification
                      if new_child_and_node.is_a?(Array)
                        new_node = new_child_and_node[1]
                      end
                    end"
                  end.join("\n")
                else
                  ""
                end
              }

              if new_node.equal?(node)
                [node, parent]
              else
                [new_node, parent]
              end
            end
          end

          def #{node_method}_with_modifications(node, parent)
            new_node_and_new_parent = #{node_method}(node, parent)
            apply_modifications(node, parent, new_node_and_new_parent)
          end
        RUBY
      end

      [
        Language::Nodes::Argument,
        Language::Nodes::Directive,
        Language::Nodes::DirectiveDefinition,
        Language::Nodes::DirectiveLocation,
        Language::Nodes::Document,
        Language::Nodes::Enum,
        Language::Nodes::EnumTypeDefinition,
        Language::Nodes::EnumTypeExtension,
        Language::Nodes::EnumValueDefinition,
        Language::Nodes::Field,
        Language::Nodes::FieldDefinition,
        Language::Nodes::FragmentDefinition,
        Language::Nodes::FragmentSpread,
        Language::Nodes::InlineFragment,
        Language::Nodes::InputObject,
        Language::Nodes::InputObjectTypeDefinition,
        Language::Nodes::InputObjectTypeExtension,
        Language::Nodes::InputValueDefinition,
        Language::Nodes::InterfaceTypeDefinition,
        Language::Nodes::InterfaceTypeExtension,
        Language::Nodes::ListType,
        Language::Nodes::NonNullType,
        Language::Nodes::NullValue,
        Language::Nodes::ObjectTypeDefinition,
        Language::Nodes::ObjectTypeExtension,
        Language::Nodes::OperationDefinition,
        Language::Nodes::ScalarTypeDefinition,
        Language::Nodes::ScalarTypeExtension,
        Language::Nodes::SchemaDefinition,
        Language::Nodes::SchemaExtension,
        Language::Nodes::TypeName,
        Language::Nodes::UnionTypeDefinition,
        Language::Nodes::UnionTypeExtension,
        Language::Nodes::VariableDefinition,
        Language::Nodes::VariableIdentifier,
      ].each do |ast_node_class|
        make_visit_methods(ast_node_class)
      end

      # rubocop:enable Development/NoEvalCop

      private

      def apply_modifications(node, parent, new_node_and_new_parent)
        if new_node_and_new_parent.is_a?(Array)
          new_node = new_node_and_new_parent[0]
          new_parent = new_node_and_new_parent[1]
          if new_node.is_a?(Nodes::AbstractNode) && !node.equal?(new_node)
            # The user-provided hook returned a new node.
            new_parent = new_parent && new_parent.replace_child(node, new_node)
            return new_node, new_parent
          elsif new_node.equal?(DELETE_NODE)
            # The user-provided hook requested to remove this node
            new_parent = new_parent && new_parent.delete_child(node)
            return nil, new_parent
          elsif new_node_and_new_parent.none? { |n| n == nil || n.class < Nodes::AbstractNode }
            # The user-provided hook returned an array of who-knows-what
            # return nil here to signify that no changes should be made
            nil
          else
            new_node_and_new_parent
          end
        else
          # The user-provided hook didn't make any modifications.
          # In fact, the hook might have returned who-knows-what, so
          # ignore the return value and use the original values.
          new_node_and_new_parent
        end
      end
    end
  end
end