module GraphQL module StaticValidation # 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` # class VariablesAreUsedAndDefined include GraphQL::StaticValidation::Message::MessageHelper class VariableUsage attr_accessor :ast_node, :used_by, :declared_by def used? !!@used_by end def declared? !!@declared_by end end def variable_hash Hash.new {|h, k| h[k] = VariableUsage.new } end def validate(context) variable_usages_for_context = Hash.new {|hash, key| hash[key] = variable_hash } spreads_for_context = Hash.new {|hash, key| hash[key] = [] } variable_context_stack = [] # OperationDefinitions and FragmentDefinitions # both push themselves onto the context stack (and pop themselves off) push_variable_context_stack = -> (node, parent) { # initialize the hash of vars for this context: variable_usages_for_context[node] variable_context_stack.push(node) } pop_variable_context_stack = -> (node, parent) { variable_context_stack.pop } context.visitor[GraphQL::Language::Nodes::OperationDefinition] << push_variable_context_stack context.visitor[GraphQL::Language::Nodes::OperationDefinition] << -> (node, parent) { # mark variables as defined: var_hash = variable_usages_for_context[node] node.variables.each { |var| var_hash[var.name].declared_by = node } } context.visitor[GraphQL::Language::Nodes::OperationDefinition].leave << pop_variable_context_stack context.visitor[GraphQL::Language::Nodes::FragmentDefinition] << push_variable_context_stack context.visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << pop_variable_context_stack # For FragmentSpreads: # - find the context on the stack # - mark the context as containing this spread context.visitor[GraphQL::Language::Nodes::FragmentSpread] << -> (node, parent) { variable_context = variable_context_stack.last spreads_for_context[variable_context] << node.name } # For VariableIdentifiers: # - mark the variable as used # - assign its AST node context.visitor[GraphQL::Language::Nodes::VariableIdentifier] << -> (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 } context.visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) { 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, context) end } end private # 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`. 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, variables = fragment_definitions.find { |def_node, vars| def_node.name == spread_name } 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 end end follow_spreads(def_node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments) end end # Determine all the error messages, # Then push messages into the validation context def create_errors(node_variables, context) errors = [] # Declared but not used: errors += node_variables .select { |name, usage| usage.declared? && !usage.used? } .map { |var_name, usage| ["Variable $#{var_name} is declared by #{usage.declared_by.name} but not used", usage.declared_by] } # Used but not declared: errors += node_variables .select { |name, usage| usage.used? && !usage.declared? } .map { |var_name, usage| ["Variable $#{var_name} is used by #{usage.used_by.name} but not declared", usage.ast_node] } errors.each do |error_args| context.errors << message(*error_args) end end end end end