|
| 1 | +defmodule NextLS.ASTHelpers.Variables do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + @scope_breaks ~w(defmodule defprotocol defimpl defdelegate fn if unless case cond for with receive try quote)a |
| 5 | + @defs_with_args ~w(def defp defmacro defmacrop)a |
| 6 | + @blocks ~w(do catch rescue after else)a |
| 7 | + @scope_ends [:->] ++ @scope_breaks ++ @defs_with_args |
| 8 | + |
| 9 | + @spec get_variable_definition(String.t(), {integer(), integer()}) :: {atom(), {Range.t(), Range.t()}} | nil |
| 10 | + def get_variable_definition(file, position) do |
| 11 | + file = File.read!(file) |
| 12 | + ast = Code.string_to_quoted!(file, columns: true) |
| 13 | + |
| 14 | + {_ast, %{vars: vars}} = |
| 15 | + Macro.traverse( |
| 16 | + ast, |
| 17 | + %{vars: [], symbols: %{}, sym_ranges: [], scope: []}, |
| 18 | + &prewalk/2, |
| 19 | + &postwalk/2 |
| 20 | + ) |
| 21 | + |
| 22 | + Enum.find_value(vars, fn %{name: name, sym_range: range, ref_range: ref_range} -> |
| 23 | + if position_in_range?(position, ref_range), do: {name, range}, else: nil |
| 24 | + end) |
| 25 | + end |
| 26 | + |
| 27 | + @spec list_variable_references(String.t(), {integer(), integer()}) :: [{atom(), {Range.t(), Range.t()}}] |
| 28 | + def list_variable_references(file, position) do |
| 29 | + file = File.read!(file) |
| 30 | + ast = Code.string_to_quoted!(file, columns: true) |
| 31 | + |
| 32 | + {_ast, %{vars: vars}} = |
| 33 | + Macro.traverse( |
| 34 | + ast, |
| 35 | + %{vars: [], symbols: %{}, sym_ranges: [], scope: []}, |
| 36 | + &prewalk/2, |
| 37 | + &postwalk/2 |
| 38 | + ) |
| 39 | + |
| 40 | + symbol = |
| 41 | + Enum.find_value(vars, fn %{name: name, sym_range: range, ref_range: ref_range} -> |
| 42 | + if position_in_range?(position, ref_range), do: {name, range}, else: nil |
| 43 | + end) |
| 44 | + |
| 45 | + position = |
| 46 | + case symbol do |
| 47 | + nil -> position |
| 48 | + {_, {line.._, column.._}} -> {line, column} |
| 49 | + end |
| 50 | + |
| 51 | + Enum.reduce(vars, [], fn val, acc -> |
| 52 | + if position_in_range?(position, val.sym_range) do |
| 53 | + [{val.name, val.ref_range} | acc] |
| 54 | + else |
| 55 | + acc |
| 56 | + end |
| 57 | + end) |
| 58 | + end |
| 59 | + |
| 60 | + # search symbols in function and macro definition args and increase scope |
| 61 | + defp prewalk({operation, meta, [args | _]} = ast, acc) when operation in @defs_with_args do |
| 62 | + acc = increase_scope_nesting(acc, meta[:line]) |
| 63 | + acc = find_symbols(args, acc) |
| 64 | + {ast, acc} |
| 65 | + end |
| 66 | + |
| 67 | + # special case for 'cond', don't search for symbols in left side of 'cond' clause |
| 68 | + defp prewalk({:->, meta, _} = ast, %{scope: ["cond" <> _ | _]} = acc) do |
| 69 | + acc = increase_scope_nesting(acc, meta[:line]) |
| 70 | + {ast, acc} |
| 71 | + end |
| 72 | + |
| 73 | + # search symbols in a left side of forward arrow clause and increase scope |
| 74 | + defp prewalk({:->, meta, [left, _right]} = ast, acc) do |
| 75 | + acc = increase_scope_nesting(acc, meta[:line]) |
| 76 | + acc = find_symbols(left, acc) |
| 77 | + {ast, acc} |
| 78 | + end |
| 79 | + |
| 80 | + # special case for 'cond' |
| 81 | + defp prewalk({:cond, meta, _args} = ast, acc) do |
| 82 | + acc = increase_scope_nesting(acc, "cond#{meta[:line]}") |
| 83 | + {ast, acc} |
| 84 | + end |
| 85 | + |
| 86 | + # increase scope on enter |
| 87 | + defp prewalk({operation, meta, _args} = ast, acc) when operation in @scope_breaks do |
| 88 | + acc = increase_scope_nesting(acc, meta[:line]) |
| 89 | + {ast, acc} |
| 90 | + end |
| 91 | + |
| 92 | + # special case for 'cond' |
| 93 | + defp prewalk({:do, _args} = ast, %{scope: ["cond" <> _ | _]} = acc) do |
| 94 | + acc = increase_scope_nesting(acc, "conddo") |
| 95 | + {ast, acc} |
| 96 | + end |
| 97 | + |
| 98 | + # increase scope on enter 'do/end' block |
| 99 | + defp prewalk({operation, _args} = ast, acc) when operation in @blocks do |
| 100 | + acc = increase_scope_nesting(acc, operation) |
| 101 | + {ast, acc} |
| 102 | + end |
| 103 | + |
| 104 | + # search symbols inside left side of a match or <- and fix processig sequence |
| 105 | + defp prewalk({operation, meta, [left, right]}, acc) when operation in [:=, :<-, :destructure] do |
| 106 | + acc = find_symbols(left, acc) |
| 107 | + {{operation, meta, [right, left]}, acc} |
| 108 | + end |
| 109 | + |
| 110 | + # exclude attribute macro from variable search |
| 111 | + defp prewalk({:@, _, _}, acc) do |
| 112 | + {nil, acc} |
| 113 | + end |
| 114 | + |
| 115 | + # find variable |
| 116 | + defp prewalk({name, meta, nil} = ast, acc) do |
| 117 | + range = calculate_range(name, meta[:line], meta[:column]) |
| 118 | + type = if range in acc.sym_ranges, do: :sym, else: :ref |
| 119 | + var = {type, name, range, acc.scope} |
| 120 | + |
| 121 | + acc = collect_var(acc, var) |
| 122 | + |
| 123 | + {ast, acc} |
| 124 | + end |
| 125 | + |
| 126 | + defp prewalk(ast, acc), do: {ast, acc} |
| 127 | + |
| 128 | + # decrease scope when exiting it |
| 129 | + defp postwalk({operation, _, _} = ast, acc) when operation in @scope_ends do |
| 130 | + acc = decrease_scope_nesting(acc) |
| 131 | + {ast, acc} |
| 132 | + end |
| 133 | + |
| 134 | + # decrease scope when exiting 'do/else' block |
| 135 | + defp postwalk({operation, _} = ast, acc) when operation in @blocks do |
| 136 | + acc = decrease_scope_nesting(acc) |
| 137 | + {ast, acc} |
| 138 | + end |
| 139 | + |
| 140 | + defp postwalk(ast, acc), do: {ast, acc} |
| 141 | + |
| 142 | + defp find_symbols(ast, acc) do |
| 143 | + {_ast, acc} = Macro.prewalk(ast, acc, &find_symbol/2) |
| 144 | + acc |
| 145 | + end |
| 146 | + |
| 147 | + defp find_symbol({operation, _, _}, acc) when operation in [:^, :unquote] do |
| 148 | + {nil, acc} |
| 149 | + end |
| 150 | + |
| 151 | + # exclude right side of 'when' from symbol search |
| 152 | + defp find_symbol({:when, _, [left, _right]}, acc) do |
| 153 | + {left, acc} |
| 154 | + end |
| 155 | + |
| 156 | + defp find_symbol({name, meta, nil} = ast, acc) do |
| 157 | + range = calculate_range(name, meta[:line], meta[:column]) |
| 158 | + acc = Map.update!(acc, :sym_ranges, &[range | &1]) |
| 159 | + {ast, acc} |
| 160 | + end |
| 161 | + |
| 162 | + defp find_symbol(ast, acc), do: {ast, acc} |
| 163 | + |
| 164 | + defp calculate_range(name, line, column) do |
| 165 | + length = name |> to_string() |> String.length() |
| 166 | + |
| 167 | + {line..line, column..(column + length - 1)} |
| 168 | + end |
| 169 | + |
| 170 | + defp position_in_range?({position_line, position_column}, {range_lines, range_columns}) do |
| 171 | + position_line in range_lines and position_column in range_columns |
| 172 | + end |
| 173 | + |
| 174 | + defp in_scope?(inner_scope, outer_scope) do |
| 175 | + outer = Enum.reverse(outer_scope) |
| 176 | + inner = Enum.reverse(inner_scope) |
| 177 | + List.starts_with?(inner, outer) |
| 178 | + end |
| 179 | + |
| 180 | + defp increase_scope_nesting(acc, identifier) do |
| 181 | + Map.update!(acc, :scope, &[to_string(identifier) | &1]) |
| 182 | + end |
| 183 | + |
| 184 | + defp decrease_scope_nesting(acc) do |
| 185 | + Map.update!(acc, :scope, &tl(&1)) |
| 186 | + end |
| 187 | + |
| 188 | + # add new symbol with scope |
| 189 | + defp collect_var(acc, {:sym, name, range, scope}) do |
| 190 | + symbol = %{ |
| 191 | + range: range, |
| 192 | + scope: scope |
| 193 | + } |
| 194 | + |
| 195 | + update_in(acc, [:symbols, name], fn |
| 196 | + nil -> [symbol] |
| 197 | + vals -> [symbol | vals] |
| 198 | + end) |
| 199 | + end |
| 200 | + |
| 201 | + # ignore reference which was not defined yet |
| 202 | + defp collect_var(%{symbols: symbols} = acc, {:ref, name, _, _}) when not is_map_key(symbols, name), do: acc |
| 203 | + |
| 204 | + # find symbol for current reference and save sym/ref pair |
| 205 | + # remove symbol scopes if reference is from outer scope |
| 206 | + defp collect_var(acc, {:ref, name, range, scope}) do |
| 207 | + case Enum.split_while(acc.symbols[name], &(not in_scope?(scope, &1.scope))) do |
| 208 | + {_, []} -> |
| 209 | + acc |
| 210 | + |
| 211 | + {_, symbols_in_scope} -> |
| 212 | + var_pair = %{ |
| 213 | + name: name, |
| 214 | + sym_range: hd(symbols_in_scope).range, |
| 215 | + ref_range: range |
| 216 | + } |
| 217 | + |
| 218 | + acc |
| 219 | + |> Map.update!(:vars, &[var_pair | &1]) |
| 220 | + |> Map.update!(:symbols, &%{&1 | name => symbols_in_scope}) |
| 221 | + end |
| 222 | + end |
| 223 | +end |
0 commit comments