module GraphQL::StaticValidation::VariablesAreUsedAndDefined

The problem is

 - Variable $usage must be determined at the OperationDefinition level
 - You can't tell how fragments use variables until you visit FragmentDefinitions (which may be at the end of the document)

So, this validator includes some crazy logic to follow fragment spreads recursively, while avoiding infinite loops.

‘graphql-js` solves this problem by:

- re-visiting the AST for each validator
- allowing validators to say `followSpreads: true`

Public Class Methods

new(*) click to toggle source
Calls superclass method
# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 26
def initialize(*)
  super
  @variable_usages_for_context = Hash.new {|hash, key| hash[key] = Hash.new {|h, k| h[k] = VariableUsage.new } }
  @spreads_for_context = Hash.new {|hash, key| hash[key] = [] }
  @variable_context_stack = []
end

Public Instance Methods

on_document(node, parent) click to toggle source
Calls superclass method
# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 78
def on_document(node, parent)
  super
  fragment_definitions = @variable_usages_for_context.select { |key, value| key.is_a?(GraphQL::Language::Nodes::FragmentDefinition) }
  operation_definitions = @variable_usages_for_context.select { |key, value| key.is_a?(GraphQL::Language::Nodes::OperationDefinition) }

  operation_definitions.each do |node, node_variables|
    follow_spreads(node, node_variables, @spreads_for_context, fragment_definitions, [])
    create_errors(node_variables)
  end
end
on_fragment_definition(node, parent) click to toggle source
Calls superclass method
# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 48
def on_fragment_definition(node, parent)
  # initialize the hash of vars for this context:
  @variable_usages_for_context[node]
  @variable_context_stack.push(node)
  super
  @variable_context_stack.pop
end
on_fragment_spread(node, parent) click to toggle source

For FragmentSpreads:

- find the context on the stack
- mark the context as containing this spread
Calls superclass method
# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 59
def on_fragment_spread(node, parent)
  variable_context = @variable_context_stack.last
  @spreads_for_context[variable_context] << node.name
  super
end
on_operation_definition(node, parent) click to toggle source
Calls superclass method
# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 33
def on_operation_definition(node, parent)
  # initialize the hash of vars for this context:
  @variable_usages_for_context[node]
  @variable_context_stack.push(node)
  # mark variables as defined:
  var_hash = @variable_usages_for_context[node]
  node.variables.each { |var|
    var_usage = var_hash[var.name]
    var_usage.declared_by = node
    var_usage.path = context.path
  }
  super
  @variable_context_stack.pop
end
on_variable_identifier(node, parent) click to toggle source

For VariableIdentifiers:

- mark the variable as used
- assign its AST node
Calls superclass method
# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 68
def on_variable_identifier(node, parent)
  usage_context = @variable_context_stack.last
  declared_variables = @variable_usages_for_context[usage_context]
  usage = declared_variables[node.name]
  usage.used_by = usage_context
  usage.ast_node = node
  usage.path = context.path
  super
end

Private Instance Methods

create_errors(node_variables) click to toggle source

Determine all the error messages, Then push messages into the validation context

# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 124
def create_errors(node_variables)
  # Declared but not used:
  node_variables
    .select { |name, usage| usage.declared? && !usage.used? }
    .each { |var_name, usage|
      declared_by_error_name = usage.declared_by.name || "anonymous #{usage.declared_by.operation_type}"
      add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
        "Variable $#{var_name} is declared by #{declared_by_error_name} but not used",
        nodes: usage.declared_by,
        path: usage.path,
        name: var_name,
        error_type: VariablesAreUsedAndDefinedError::VIOLATIONS[:VARIABLE_NOT_USED]
      ))
    }

  # Used but not declared:
  node_variables
    .select { |name, usage| usage.used? && !usage.declared? }
    .each { |var_name, usage|
      used_by_error_name = usage.used_by.name || "anonymous #{usage.used_by.operation_type}"
      add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
        "Variable $#{var_name} is used by #{used_by_error_name} but not declared",
        nodes: usage.ast_node,
        path: usage.path,
        name: var_name,
        error_type: VariablesAreUsedAndDefinedError::VIOLATIONS[:VARIABLE_NOT_DEFINED]
      ))
    }
end
follow_spreads(node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments) click to toggle source

Follow spreads in ‘node`, looking them up from `spreads_for_context` and finding their match in `fragment_definitions`. Use those fragments to update {VariableUsage}s in `parent_variables`. Avoid infinite loops by skipping anything in `visited_fragments`.

# File lib/graphql/static_validation/rules/variables_are_used_and_defined.rb, line 94
def follow_spreads(node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments)
  spreads = spreads_for_context[node] - visited_fragments
  spreads.each do |spread_name|
    def_node = nil
    variables = nil
    # Implement `.find` by hand to avoid Ruby's internal allocations
    fragment_definitions.each do |frag_def_node, vars|
      if frag_def_node.name == spread_name
        def_node = frag_def_node
        variables = vars
        break
      end
    end

    next if !def_node
    visited_fragments << spread_name
    variables.each do |name, child_usage|
      parent_usage = parent_variables[name]
      if child_usage.used?
        parent_usage.ast_node   = child_usage.ast_node
        parent_usage.used_by    = child_usage.used_by
        parent_usage.path       = child_usage.path
      end
    end
    follow_spreads(def_node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments)
  end
end