class GraphQL::Analysis::AST::QueryComplexity

Public Class Methods

new(query) click to toggle source

State for the query complexity calculation:

  • ‘complexities_on_type` holds complexity scores for each type

Calls superclass method GraphQL::Analysis::AST::Analyzer::new
# File lib/graphql/analysis/ast/query_complexity.rb, line 9
def initialize(query)
  super
  @complexities_on_type_by_query = {}
end

Public Instance Methods

on_enter_field(node, parent, visitor) click to toggle source
# File lib/graphql/analysis/ast/query_complexity.rb, line 61
def on_enter_field(node, parent, visitor)
  # We don't want to visit fragment definitions,
  # we'll visit them when we hit the spreads instead
  return if visitor.visiting_fragment_definition?
  return if visitor.skipping?
  parent_type = visitor.parent_type_definition
  field_key = node.alias || node.name
  # Find the complexity calculation for this field --
  # if we're re-entering a selection, we'll already have one.
  # Otherwise, make a new one and store it.
  #
  # `node` and `visitor.field_definition` may appear from a cache,
  # but I think that's ok. If the arguments _didn't_ match,
  # then the query would have been rejected as invalid.
  complexities_on_type = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]

  complexity = complexities_on_type.last.scoped_children[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
  complexity.nodes.push(node)
  # Push it on the stack.
  complexities_on_type.push(complexity)
end
on_leave_field(node, parent, visitor) click to toggle source
# File lib/graphql/analysis/ast/query_complexity.rb, line 83
def on_leave_field(node, parent, visitor)
  # We don't want to visit fragment definitions,
  # we'll visit them when we hit the spreads instead
  return if visitor.visiting_fragment_definition?
  return if visitor.skipping?
  complexities_on_type = @complexities_on_type_by_query[visitor.query]
  complexities_on_type.pop
end
result() click to toggle source

Overide this method to use the complexity result

# File lib/graphql/analysis/ast/query_complexity.rb, line 15
def result
  max_possible_complexity
end

Private Instance Methods

applies_to?(query, left_scope, right_scope) click to toggle source
# File lib/graphql/analysis/ast/query_complexity.rb, line 161
def applies_to?(query, left_scope, right_scope)
  if left_scope == right_scope
    # This can happen when several branches are being analyzed together
    true
  else
    # Check if these two scopes have _any_ types in common.
    possible_right_types = query.possible_types(right_scope)
    possible_left_types = query.possible_types(left_scope)
    !(possible_right_types & possible_left_types).empty?
  end
end
field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil) click to toggle source

A hook which is called whenever a field’s max complexity is calculated. Override this method to capture individual field complexity details.

@param scoped_type_complexity [ScopedTypeComplexity] @param max_complexity [Numeric] Field’s maximum complexity including child complexity @param child_complexity [Numeric, nil] Field’s child complexity

# File lib/graphql/analysis/ast/query_complexity.rb, line 179
def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil)
end
max_possible_complexity() click to toggle source

@return [Integer]

# File lib/graphql/analysis/ast/query_complexity.rb, line 95
def max_possible_complexity
  @complexities_on_type_by_query.reduce(0) do |total, (query, complexities_on_type)|
    root_complexity = complexities_on_type.last
    # Use this entry point to calculate the total complexity
    total_complexity_for_query = merged_max_complexity_for_scopes(query, [root_complexity.scoped_children])
    total + total_complexity_for_query
  end
end
merged_max_complexity(query, children_for_scope) click to toggle source

@param children_for_scope [Array<Hash>] An array of ‘scoped_children` hashes (`{field_key => complexity}`) @return [Integer] Complexity value for all these selections in the current scope

# File lib/graphql/analysis/ast/query_complexity.rb, line 185
def merged_max_complexity(query, children_for_scope)
  all_keys = []
  children_for_scope.each do |c|
    all_keys.concat(c.keys)
  end
  all_keys.uniq!
  complexity_for_keys = {}

  all_keys.each do |child_key|
    scoped_children_for_key = nil
    complexity_for_key = nil
    children_for_scope.each do |children_hash|
      next unless children_hash.key?(child_key)

      complexity_for_key = children_hash[child_key]
      if complexity_for_key.terminal?
        # Assume that all terminals would return the same complexity
        # Since it's a terminal, its child complexity is zero.
        complexity = complexity_for_key.own_complexity(0)
        complexity_for_keys[child_key] = complexity

        field_complexity(complexity_for_key, max_complexity: complexity, child_complexity: nil)
      else
        scoped_children_for_key ||= []
        scoped_children_for_key << complexity_for_key.scoped_children
      end
    end

    next unless scoped_children_for_key

    child_complexity = merged_max_complexity_for_scopes(query, scoped_children_for_key)
    # This is the _last_ one we visited; assume it's representative.
    max_complexity = complexity_for_key.own_complexity(child_complexity)

    field_complexity(complexity_for_key, max_complexity: max_complexity, child_complexity: child_complexity)

    complexity_for_keys[child_key] = max_complexity
  end

  # Calculate the child complexity by summing the complexity of all selections
  complexity_for_keys.each_value.inject(0, &:+)
end
merged_max_complexity_for_scopes(query, scoped_children_hashes) click to toggle source

@param query [GraphQL::Query] Used for ‘query.possible_types` @param scoped_children_hashes [Array<Hash>] Array of scoped children hashes @return [Integer]

# File lib/graphql/analysis/ast/query_complexity.rb, line 107
def merged_max_complexity_for_scopes(query, scoped_children_hashes)
  # Figure out what scopes are possible here.
  # Use a hash, but ignore the values; it's just a fast way to work with the keys.
  all_scopes = {}
  scoped_children_hashes.each do |h|
    all_scopes.merge!(h)
  end

  # If an abstract scope is present, but _all_ of its concrete types
  # are also in the list, remove it from the list of scopes to check,
  # because every possible type is covered by a concrete type.
  # (That is, there are no remainder types to check.)
  prev_keys = all_scopes.keys
  prev_keys.each do |scope|
    next unless scope.kind.abstract?

    missing_concrete_types = query.possible_types(scope).select { |t| !all_scopes.key?(t) }
    # This concrete type is possible _only_ as a member of the abstract type.
    # So, attribute to it the complexity which belongs to the abstract type.
    missing_concrete_types.each do |concrete_scope|
      all_scopes[concrete_scope] = all_scopes[scope]
    end
    all_scopes.delete(scope)
  end

  # This will hold `{ type => int }` pairs, one for each possible branch
  complexity_by_scope = {}

  # For each scope,
  # find the lexical selections that might apply to it,
  # and gather them together into an array.
  # Then, treat the set of selection hashes
  # as a set and calculate the complexity for them as a unit
  all_scopes.each do |scope, _|
    # These will be the selections on `scope`
    children_for_scope = []
    scoped_children_hashes.each do |sc_h|
      sc_h.each do |inner_scope, children_hash|
        if applies_to?(query, scope, inner_scope)
          children_for_scope << children_hash
        end
      end
    end

    # Calculate the complexity for `scope`, merging all
    # possible lexical branches.
    complexity_value = merged_max_complexity(query, children_for_scope)
    complexity_by_scope[scope] = complexity_value
  end

  # Return the max complexity among all scopes
  complexity_by_scope.each_value.max
end