# frozen_string_literal: true module GraphQL module StaticValidation class BaseVisitor < GraphQL::Language::Visitor def initialize(document, context) @path = [] @object_types = [] @directives = [] @field_definitions = [] @argument_definitions = [] @directive_definitions = [] @context = context @schema = context.schema super(document) end # This will be overwritten by {InternalRepresentation::Rewrite} if it's included def rewrite_document nil end attr_reader :context # @return [Array] Types whose scope we've entered attr_reader :object_types # @return [Array] The nesting of the current position in the AST def path @path.dup end # Build a class to visit the AST and perform validation, # or use a pre-built class if rules is `ALL_RULES` or empty. # @param rules [Array] # @param rewrite [Boolean] if `false`, don't include rewrite # @return [Class] A class for validating `rules` during visitation def self.including_rules(rules, rewrite: true) if rules.empty? if rewrite NoValidateVisitor else # It's not doing _anything?!?_ BaseVisitor end elsif rules == ALL_RULES if rewrite DefaultVisitor else InterpreterVisitor end else visitor_class = Class.new(self) do include(GraphQL::StaticValidation::DefinitionDependencies) end rules.reverse_each do |r| # If it's a class, it gets attached later. if !r.is_a?(Class) visitor_class.include(r) end end if rewrite visitor_class.include(GraphQL::InternalRepresentation::Rewrite) end visitor_class.include(ContextMethods) visitor_class end end module ContextMethods def on_operation_definition(node, parent) object_type = @schema.root_type_for_operation(node.operation_type) push_type(object_type) @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}") super @object_types.pop @path.pop end def on_fragment_definition(node, parent) on_fragment_with_type(node) do @path.push("fragment #{node.name}") super end end def on_inline_fragment(node, parent) on_fragment_with_type(node) do @path.push("...#{node.type ? " on #{node.type.to_query_string}" : ""}") super end end def on_field(node, parent) parent_type = @object_types.last field_definition = @schema.get_field(parent_type, node.name, @context.query.context) @field_definitions.push(field_definition) if !field_definition.nil? next_object_type = field_definition.type.unwrap push_type(next_object_type) else push_type(nil) end @path.push(node.alias || node.name) super @field_definitions.pop @object_types.pop @path.pop end def on_directive(node, parent) directive_defn = @schema.directives[node.name] @directive_definitions.push(directive_defn) super @directive_definitions.pop end def on_argument(node, parent) argument_defn = if (arg = @argument_definitions.last) arg_type = arg.type.unwrap if arg_type.kind.input_object? @context.warden.get_argument(arg_type, node.name) else nil end elsif (directive_defn = @directive_definitions.last) @context.warden.get_argument(directive_defn, node.name) elsif (field_defn = @field_definitions.last) @context.warden.get_argument(field_defn, node.name) else nil end @argument_definitions.push(argument_defn) @path.push(node.name) super @argument_definitions.pop @path.pop end def on_fragment_spread(node, parent) @path.push("... #{node.name}") super @path.pop end def on_input_object(node, parent) arg_defn = @argument_definitions.last if arg_defn && arg_defn.type.list? @path.push(parent.children.index(node)) super @path.pop else super end end # @return [GraphQL::BaseType] The current object type def type_definition @object_types.last end # @return [GraphQL::BaseType] The type which the current type came from def parent_type_definition @object_types[-2] end # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one def field_definition @field_definitions.last end # @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one def directive_definition @directive_definitions.last end # @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one def argument_definition # Don't get the _last_ one because that's the current one. # Get the second-to-last one, which is the parent of the current one. @argument_definitions[-2] end private def on_fragment_with_type(node) object_type = if node.type @context.warden.get_type(node.type.name) else @object_types.last end push_type(object_type) yield(node) @object_types.pop @path.pop end def push_type(t) @object_types.push(t) end end private def add_error(error, path: nil) if @context.too_many_errors? throw :too_many_validation_errors end error.path ||= (path || @path.dup) context.errors << error end end end end