# 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 # @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 = 0 # 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 class Visitor # If any hook returns this value, the {Visitor} stops visiting this # node right away # @deprecated Use `super` to continue the visit; or don't call it to halt. SKIP = :_skip 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 @visitors = {} @result = nil end # @return [GraphQL::Language::Nodes::Document] The document with any modifications applied attr_reader :result # Get a {NodeVisitor} for `node_class` # @param node_class [Class] The node class that you want to listen to # @return [NodeVisitor] # # @example Run a hook whenever you enter a new Field # visitor[GraphQL::Language::Nodes::Field] << ->(node, parent) { p "Here's a field" } # @deprecated see `on_` methods, like {#on_field} def [](node_class) @visitors[node_class] ||= NodeVisitor.new end # Visit `document` and all children, applying hooks as you go # @return [void] def visit result = on_node_with_modifications(@document, nil) @result = if result.is_a?(Array) result.first else # The node wasn't modified @document end end # Call the user-defined handler for `node`. def visit_node(node, parent) public_send(node.visit_method, node, parent) end # 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_methodes (or the base method?) # in your subclasses. # # For compatibility, it calls hook procs, too. # @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 on_abstract_node(node, parent) if node == DELETE_NODE # This might be passed to `super(DELETE_NODE, ...)` # by a user hook, don't want to keep visiting in that case. nil else # Run hooks if there are any new_node = node begin_hooks_ok = @visitors.empty? || begin_visit(new_node, parent) if begin_hooks_ok node.children.each do |child_node| new_child_and_node = on_node_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 @visitors.any? && end_visit(new_node, parent) if new_node.equal?(node) nil else [new_node, parent] end end end # We don't use `alias` here because it breaks `super` def self.make_visit_method(node_method) class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def #{node_method}(node, parent) child_mod = on_abstract_node(node, parent) # If visiting the children returned changes, continue passing those. child_mod || [node, parent] end RUBY end make_visit_method :on_argument make_visit_method :on_directive make_visit_method :on_directive_definition make_visit_method :on_directive_location make_visit_method :on_document make_visit_method :on_enum make_visit_method :on_enum_type_definition make_visit_method :on_enum_type_extension make_visit_method :on_enum_value_definition make_visit_method :on_field make_visit_method :on_field_definition make_visit_method :on_fragment_definition make_visit_method :on_fragment_spread make_visit_method :on_inline_fragment make_visit_method :on_input_object make_visit_method :on_input_object_type_definition make_visit_method :on_input_object_type_extension make_visit_method :on_input_value_definition make_visit_method :on_interface_type_definition make_visit_method :on_interface_type_extension make_visit_method :on_list_type make_visit_method :on_non_null_type make_visit_method :on_null_value make_visit_method :on_object_type_definition make_visit_method :on_object_type_extension make_visit_method :on_operation_definition make_visit_method :on_scalar_type_definition make_visit_method :on_scalar_type_extension make_visit_method :on_schema_definition make_visit_method :on_schema_extension make_visit_method :on_type_name make_visit_method :on_union_type_definition make_visit_method :on_union_type_extension make_visit_method :on_variable_definition make_visit_method :on_variable_identifier private # Run the hooks for `node`, and if the hooks return a copy of `node`, # copy `parent` so that it contains the copy of that node as a child, # then return the copies # If a non-array value is returned, consuming functions should ignore # said value def on_node_with_modifications(node, parent) new_node_and_new_parent = visit_node(node, 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 == 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 def begin_visit(node, parent) node_visitor = self[node.class] self.class.apply_hooks(node_visitor.enter, node, parent) end # Should global `leave` visitors come first or last? def end_visit(node, parent) node_visitor = self[node.class] self.class.apply_hooks(node_visitor.leave, node, parent) end # If one of the visitors returns SKIP, stop visiting this node def self.apply_hooks(hooks, node, parent) hooks.reduce(true) { |memo, proc| memo && (proc.call(node, parent) != SKIP) } end # Collect `enter` and `leave` hooks for classes in {GraphQL::Language::Nodes} # # Access {NodeVisitor}s via {GraphQL::Language::Visitor#[]} class NodeVisitor # @return [Array] Hooks to call when entering a node of this type attr_reader :enter # @return [Array] Hooks to call when leaving a node of this type attr_reader :leave def initialize @enter = [] @leave = [] end # Shorthand to add a hook to the {#enter} array # @param hook [Proc] A hook to add def <<(hook) enter << hook end end end end end