class GraphQL::StaticValidation::VariablesAreUsedAndDefined
- allowing validators to say ‘followSpreads: true`
- re-visiting the AST for each validator
`graphql-js` solves this problem by:
So, this validator includes some crazy logic to follow fragment spreads recursively, while avoiding infinite loops.
- You can’t tell how fragments use variables until you visit FragmentDefinitions (which may be at the end of the document)
- Variable usage must be determined at the OperationDefinition level
The problem is
def create_errors(node_variables, context)
Determine all the error messages,
def create_errors(node_variables, context) # Declared but not used: node_variables .select { |name, usage| usage.declared? && !usage.used? } .each { |var_name, usage| context.errors << message("Variable $#{var_name} is declared by #{usage.declared_by.name} but not used", usage.declared_by, path: usage.path) } # Used but not declared: node_variables .select { |name, usage| usage.used? && !usage.declared? } .each { |var_name, usage| context.errors << message("Variable $#{var_name} is used by #{usage.used_by.name} but not declared", usage.ast_node, path: usage.path) } end
def follow_spreads(node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments)
Use those fragments to update {VariableUsage}s in `parent_variables`.
Follow spreads in `node`, looking them up from `spreads_for_context` and finding their match in `fragment_definitions`.
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 parent_usage.path = child_usage.path end end follow_spreads(def_node, parent_variables, spreads_for_context, fragment_definitions, visited_fragments) end end
def validate(context)
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_usage = var_hash[var.name] var_usage.declared_by = node var_usage.path = context.path } } 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 usage.path = context.path } 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
def variable_hash
def variable_hash Hash.new {|h, k| h[k] = VariableUsage.new } end