collector.ex (4295B)
1 defmodule Credo.Check.Consistency.SpaceAroundOperators.Collector do 2 @moduledoc false 3 4 use Credo.Check.Consistency.Collector 5 6 import Credo.Check.Consistency.SpaceAroundOperators.SpaceHelper, 7 only: [ 8 operator?: 1, 9 space_between?: 2, 10 no_space_between?: 2, 11 usually_no_space_before?: 3, 12 usually_no_space_after?: 3 13 ] 14 15 def collect_matches(source_file, _params) do 16 source_file 17 |> Credo.Code.to_tokens() 18 |> traverse_tokens(&record_spaces(&1, &2, &3, &4), %{}) 19 end 20 21 def find_locations_not_matching(expected, source_file) do 22 source_file 23 |> Credo.Code.to_tokens() 24 |> traverse_tokens(&record_not_matching(expected, &1, &2, &3, &4), []) 25 |> Enum.reverse() 26 end 27 28 defp traverse_tokens(tokens, callback, acc) do 29 tokens 30 |> skip_specs_types_captures_and_binary_patterns() 31 |> case do 32 [prev | [current | [next | rest]]] -> 33 acc = 34 if operator?(current) do 35 callback.(prev, current, next, acc) 36 else 37 acc 38 end 39 40 traverse_tokens([current | [next | rest]], callback, acc) 41 42 _ -> 43 acc 44 end 45 end 46 47 defp skip_specs_types_captures_and_binary_patterns([{:at_op, {line, _, _}, :@} | tokens]) do 48 case tokens do 49 # @spec - drop whole line 50 [{:identifier, _, :spec} | tokens] -> 51 drop_while_on_line(tokens, line) 52 53 # @type - drop whole line 54 [{:identifier, _, :type} | tokens] -> 55 drop_while_on_line(tokens, line) 56 57 tokens -> 58 tokens 59 end 60 end 61 62 defp skip_specs_types_captures_and_binary_patterns([{:capture_op, _, _} | tokens]) do 63 drop_while_in_fun_capture(tokens) 64 end 65 66 # When quoting &//2 (which captures the / operator via &fun_name/2), the 67 # {:capture_op, _, :&} token becomes an {:identifier, _, :&} token ... 68 defp skip_specs_types_captures_and_binary_patterns([ 69 {:identifier, _, :&} | [{:identifier, _, :/} | tokens] 70 ]) do 71 drop_while_in_fun_capture(tokens) 72 end 73 74 defp skip_specs_types_captures_and_binary_patterns([{:"<<", _} | tokens]) do 75 drop_while_in_binary_pattern(tokens) 76 end 77 78 defp skip_specs_types_captures_and_binary_patterns(tokens), do: tokens 79 80 defp drop_while_on_line(tokens, line) do 81 Enum.drop_while(tokens, fn 82 {_, {^line, _, _}} -> true 83 {_, {^line, _, _}, _} -> true 84 _ -> false 85 end) 86 end 87 88 defp drop_while_in_fun_capture(tokens) do 89 Enum.drop_while(tokens, fn 90 # :erlang_module 91 {:atom, _, _} -> 92 true 93 94 # ElixirModule (Elixir >= 1.6.0) 95 {:alias, _, _} -> 96 true 97 98 # ElixirModule 99 {:aliases, _, _} -> 100 true 101 102 # function_name 103 {:identifier, _, _} -> 104 true 105 106 # unquote 107 {:paren_identifier, _, :unquote} -> 108 true 109 110 # @module_attribute 111 {:at_op, _, _} -> 112 true 113 114 {:mult_op, _, :/} -> 115 true 116 117 {:., _} -> 118 true 119 120 {:"(", _} -> 121 true 122 123 {:")", _} -> 124 true 125 126 _ -> 127 false 128 end) 129 end 130 131 defp drop_while_in_binary_pattern(tokens) do 132 Enum.drop_while(tokens, fn 133 :">>" -> 134 false 135 136 _ -> 137 true 138 end) 139 end 140 141 defp record_spaces(prev, current, next, acc) do 142 acc 143 |> increment(:with_space, with_space?(prev, current, next)) 144 |> increment(:without_space, without_space?(prev, current, next)) 145 end 146 147 defp increment(map, key, matches) do 148 if matches do 149 Map.update(map, key, 1, &(&1 + 1)) 150 else 151 map 152 end 153 end 154 155 defp record_not_matching(expected, prev, current, next, acc) do 156 match_found = 157 case expected do 158 :with_space -> 159 without_space?(prev, current, next) 160 161 :without_space -> 162 with_space?(prev, current, next) 163 end 164 165 if match_found do 166 {_, {line_no, column, _}, trigger} = current 167 168 [[line_no: line_no, column: column, trigger: trigger] | acc] 169 else 170 acc 171 end 172 end 173 174 defp with_space?(prev, op, next) do 175 space_between?(prev, op) || space_between?(op, next) 176 end 177 178 defp without_space?(prev, op, next) do 179 (!usually_no_space_before?(prev, op, next) && no_space_between?(prev, op)) || 180 (!usually_no_space_after?(prev, op, next) && no_space_between?(op, next) && 181 !(elem(next, 0) == :eol)) 182 end 183 end