1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
# frozen_string_literal: true
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`
#
module VariablesAreUsedAndDefined
class VariableUsage
attr_accessor :ast_node, :used_by, :declared_by, :path
def used?
!!@used_by
end
def declared?
!!@declared_by
end
end
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
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
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
# For FragmentSpreads:
# - find the context on the stack
# - mark the context as containing this spread
def on_fragment_spread(node, parent)
variable_context = @variable_context_stack.last
@spreads_for_context[variable_context] << node.name
super
end
# For VariableIdentifiers:
# - mark the variable as used
# - assign its AST node
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
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
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 = 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
# Determine all the error messages,
# Then push messages into the validation context
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
end
end
end
|