no_fragment_cycles.ex (4232B)
1 defmodule Absinthe.Phase.Document.Validation.NoFragmentCycles do 2 @moduledoc false 3 4 # Ensure that document doesn't have any fragment cycles that could 5 # result in a loop during execution. 6 # 7 # Note that if this phase fails, an error should immediately be given to 8 # the user. 9 10 alias Absinthe.{Blueprint, Phase} 11 12 use Absinthe.Phase 13 14 @doc """ 15 Run the validation. 16 """ 17 @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() 18 def run(input, options \\ []) do 19 do_run(input, Map.new(options)) 20 end 21 22 @spec do_run(Blueprint.t(), %{validation_result_phase: Phase.t()}) :: Phase.result_t() 23 def do_run(input, %{validation_result_phase: abort_phase}) do 24 {fragments, error_count} = check(input.fragments) 25 result = %{input | fragments: fragments} 26 27 if error_count > 0 do 28 {:jump, result, abort_phase} 29 else 30 {:ok, result} 31 end 32 end 33 34 # Check a list of fragments for cycles 35 @spec check([Blueprint.Document.Fragment.Named.t()]) :: 36 {[Blueprint.Document.Fragment.Named.t()], integer} 37 defp check(fragments) do 38 graph = :digraph.new([:cyclic]) 39 40 try do 41 with {fragments, 0} <- check(fragments, graph) do 42 fragments = Map.new(fragments, &{&1.name, &1}) 43 44 fragments = 45 graph 46 |> :digraph_utils.topsort() 47 |> Enum.reverse() 48 |> Enum.map(&Map.get(fragments, &1)) 49 |> Enum.reject(&is_nil/1) 50 51 {fragments, 0} 52 end 53 after 54 :digraph.delete(graph) 55 end 56 end 57 58 @spec check([Blueprint.Document.Fragment.Named.t()], :digraph.graph()) :: 59 {[Blueprint.Document.Fragment.Named.t()], integer} 60 defp check(fragments, graph) do 61 Enum.each(fragments, fn node -> Blueprint.prewalk(node, &vertex(&1, graph)) end) 62 63 {modified, error_count} = 64 Enum.reduce(fragments, {[], 0}, fn fragment, {processed, error_count} -> 65 errors_to_add = cycle_errors(fragment, :digraph.get_cycle(graph, fragment.name)) 66 fragment_with_errors = update_in(fragment.errors, &(errors_to_add ++ &1)) 67 {[fragment_with_errors | processed], error_count + length(errors_to_add)} 68 end) 69 70 {modified, error_count} 71 end 72 73 # Add a vertex modeling a fragment 74 @spec vertex(Blueprint.Document.Fragment.Named.t(), :digraph.graph()) :: 75 Blueprint.Document.Fragment.Named.t() 76 defp vertex(%Blueprint.Document.Fragment.Named{} = fragment, graph) do 77 :digraph.add_vertex(graph, fragment.name) 78 79 Blueprint.prewalk(fragment, fn 80 %Blueprint.Document.Fragment.Spread{} = spread -> 81 edge(fragment, spread, graph) 82 spread 83 84 node -> 85 node 86 end) 87 88 fragment 89 end 90 91 defp vertex(fragment, _graph) do 92 fragment 93 end 94 95 # Add an edge, modeling the relationship between two fragments 96 @spec edge( 97 Blueprint.Document.Fragment.Named.t(), 98 Blueprint.Document.Fragment.Spread.t(), 99 :digraph.graph() 100 ) :: true 101 defp edge(fragment, spread, graph) do 102 :digraph.add_vertex(graph, spread.name) 103 :digraph.add_edge(graph, fragment.name, spread.name) 104 true 105 end 106 107 # Generate an error for a cyclic reference 108 @spec cycle_errors(Blueprint.Document.Fragment.Named.t(), false | [String.t()]) :: [ 109 Phase.Error.t() 110 ] 111 defp cycle_errors(_, false) do 112 [] 113 end 114 115 defp cycle_errors(fragment, cycles) do 116 [cycle_error(fragment, error_message(fragment.name, cycles))] 117 end 118 119 @doc """ 120 Generate the error message. 121 """ 122 @spec error_message(String.t(), [String.t()]) :: String.t() 123 def error_message(fragment_name, [fragment_name]) do 124 ~s(Cannot spread fragment "#{fragment_name}" within itself.) 125 end 126 127 def error_message(fragment_name, [_fragment_name | cycles]) do 128 deps = Enum.map(cycles, &~s("#{&1}")) |> Enum.join(", ") 129 ~s(Cannot spread fragment "#{fragment_name}" within itself via #{deps}.) 130 end 131 132 # Generate the error for a fragment cycle 133 @spec cycle_error(Blueprint.Document.Fragment.Named.t(), String.t()) :: Phase.Error.t() 134 defp cycle_error(fragment, message) do 135 %Phase.Error{ 136 message: message, 137 phase: __MODULE__, 138 locations: [ 139 %{line: fragment.source_location.line, column: fragment.source_location.column} 140 ] 141 } 142 end 143 end