analysis.ex (4815B)
1 defmodule Absinthe.Phase.Document.Complexity.Analysis do 2 @moduledoc false 3 4 # Analyses document complexity. 5 6 alias Absinthe.{Blueprint, Phase, Complexity, Type} 7 8 use Absinthe.Phase 9 10 @default_complexity 1 11 12 @doc """ 13 Run complexity analysis. 14 """ 15 @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() 16 def run(input, options \\ []) do 17 if Keyword.get(options, :analyze_complexity, false) do 18 do_run(input, options) 19 else 20 {:ok, input} 21 end 22 end 23 24 defp do_run(input, options) do 25 info = info_boilerplate(input, options) 26 fragments = process_fragments(input, info) 27 fun = &handle_node(&1, info, fragments) 28 {:ok, Blueprint.postwalk(input, fun)} 29 end 30 31 defp process_fragments(input, info) do 32 Enum.reduce(input.fragments, %{}, fn fragment, processed -> 33 fun = &handle_node(&1, info, processed) 34 fragment = Blueprint.postwalk(fragment, fun) 35 Map.put(processed, fragment.name, fragment) 36 end) 37 end 38 39 def handle_node(%Blueprint.Document.Fragment.Spread{name: name} = node, _info, fragments) do 40 fragment = Map.fetch!(fragments, name) 41 %{node | complexity: fragment.complexity} 42 end 43 44 def handle_node( 45 %Blueprint.Document.Fragment.Named{selections: fields} = node, 46 _info, 47 _fragments 48 ) do 49 %{node | complexity: sum_complexity(fields)} 50 end 51 52 def handle_node( 53 %Blueprint.Document.Fragment.Inline{selections: fields} = node, 54 _info, 55 _fragments 56 ) do 57 %{node | complexity: sum_complexity(fields)} 58 end 59 60 def handle_node( 61 %Blueprint.Document.Field{ 62 complexity: nil, 63 selections: fields, 64 argument_data: args, 65 schema_node: schema_node 66 } = node, 67 info, 68 _fragments 69 ) do 70 # NOTE: 71 # This really should be more nuanced. If this particular field's schema node 72 # is a union type, right now the complexity of: 73 # thisField { 74 # ... User { a b c} 75 # ... Dog { x y z } 76 # } 77 # would be the complexity of `|a, b, c, x, y, z|` despite the fact that it is 78 # impossible for `a, b, c` to also happen with `x, y, z` 79 # 80 # However, if this schema node is an interface type things get complicated quickly. 81 # You would have to evaluate the complexity for every possible type which can get 82 # pretty unwieldy. For now, simple types it is. 83 child_complexity = sum_complexity(fields) 84 85 schema_node = %{ 86 schema_node 87 | complexity: Type.function(schema_node, :complexity) 88 } 89 90 case field_complexity(schema_node, args, child_complexity, info, node) do 91 complexity when is_integer(complexity) and complexity >= 0 -> 92 %{node | complexity: complexity} 93 94 other -> 95 raise Absinthe.AnalysisError, field_value_error(node, other) 96 end 97 end 98 99 def handle_node(%Blueprint.Document.Operation{complexity: nil, selections: fields} = node, _, _) do 100 %{node | complexity: sum_complexity(fields)} 101 end 102 103 def handle_node(node, _, _) do 104 node 105 end 106 107 defp field_complexity(%{complexity: nil}, _, child_complexity, _, _) do 108 @default_complexity + child_complexity 109 end 110 111 defp field_complexity(%{complexity: complexity}, arg, child_complexity, _, _) 112 when is_function(complexity, 2) do 113 complexity.(arg, child_complexity) 114 end 115 116 defp field_complexity(%{complexity: complexity}, arg, child_complexity, info, node) 117 when is_function(complexity, 3) do 118 info = struct(Complexity, Map.put(info, :definition, node)) 119 complexity.(arg, child_complexity, info) 120 end 121 122 defp field_complexity(%{complexity: {mod, fun}}, arg, child_complexity, info, node) do 123 info = struct(Complexity, Map.put(info, :definition, node)) 124 apply(mod, fun, [arg, child_complexity, info]) 125 end 126 127 defp field_complexity(%{complexity: complexity}, _, _, _, _) do 128 complexity 129 end 130 131 defp field_value_error(field, value) do 132 """ 133 Invalid value returned from complexity analyzer. 134 135 Analyzing field: 136 137 #{field.name} 138 139 Defined at: 140 141 #{field.schema_node.__reference__.location.file}:#{ 142 field.schema_node.__reference__.location.line 143 } 144 145 Got value: 146 147 #{inspect(value)} 148 149 The complexity value must be a non negative integer. 150 """ 151 end 152 153 defp sum_complexity(fields) do 154 Enum.reduce(fields, 0, &sum_complexity/2) 155 end 156 157 defp sum_complexity(%{complexity: complexity}, acc) when is_nil(complexity) do 158 @default_complexity + acc 159 end 160 161 defp sum_complexity(%{complexity: complexity}, acc) when is_integer(complexity) do 162 complexity + acc 163 end 164 165 # Execution context data that's common to all fields 166 defp info_boilerplate(bp_root, options) do 167 %{ 168 context: options[:context] || %{}, 169 root_value: options[:root_value] || %{}, 170 schema: bp_root.schema 171 } 172 end 173 end