Skip to content

Commit

Permalink
Change get_range/1 to allow returning nil and add syntax corpus (#…
Browse files Browse the repository at this point in the history
…107)

* chore: add a corpus of syntax examples adapted from tree-sitter-elixir

* feat!: `get_range/1` returns `nil` if range cannot be calculated
  • Loading branch information
zachallaun committed Sep 13, 2023
1 parent 2eba98f commit eaf0cc6
Show file tree
Hide file tree
Showing 43 changed files with 2,066 additions and 98 deletions.
12 changes: 9 additions & 3 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
included: [
"lib/",
"src/",
"test/",
"test/"
],
excluded: [~r"/_build/", ~r"/deps/", ~r"/lib/sourceror/code/", ~r"/test/code/", "lib/sourceror/code.ex"]
},
excluded: [
~r"/_build/",
~r"/deps/",
~r"/lib/sourceror/code/",
~r"/test/corpus/",
"lib/sourceror/code.ex"
]
}
}
]
}
4 changes: 3 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ locals_without_parens = [
export: [
locals_without_parens: locals_without_parens
],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs:
["{mix,.formatter}.exs"] ++
(Path.wildcard("{config,lib,test}/**/*.{ex,exs}") -- Path.wildcard("test/corpus/**/*.ex"))
]
15 changes: 13 additions & 2 deletions lib/sourceror.ex
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,21 @@ defmodule Sourceror do
get_start_position(left, default)
end

def get_start_position({{:., _, [Kernel, :to_string]}, _, [left | _]}, default) do
get_start_position(left, default)
end

def get_start_position({{:., _, [List, :to_charlist]}, meta, _}, default) do
position = Keyword.take(meta, [:line, :column])
Keyword.merge(default, position)
end

def get_start_position({{:., _, [left | _]}, _, _}, default) do
get_start_position(left, default)
end

def get_start_position({_, meta, _}, default) do
position = Keyword.take(meta, [:line, :column])

Keyword.merge(default, position)
end

Expand Down Expand Up @@ -621,6 +629,9 @@ defmodule Sourceror do
The quoted expression must have at least line and column metadata, otherwise
it is not possible to calculate an accurate range, or to calculate it at all.
Additionally, certain syntax constructs desugar into ASTs without a
meaningful range. In these cases, `get_range/1` returns `nil`.
This function is most useful when used after `Sourceror.parse_string/1`,
before any kind of modification to the AST.
Expand Down Expand Up @@ -654,7 +665,7 @@ defmodule Sourceror do
...> |> Sourceror.get_range(include_comments: true)
%{start: [line: 1, column: 1], end: [line: 2, column: 11]}
"""
@spec get_range(Macro.t()) :: range
@spec get_range(Macro.t()) :: range | nil
def get_range(quoted, opts \\ []) do
Sourceror.Range.get_range(quoted, opts)
end
Expand Down
144 changes: 66 additions & 78 deletions lib/sourceror/range.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ defmodule Sourceror.Range do
String.split(string, ~r/\n|\r\n|\r/)
end

@spec get_range(Macro.t()) :: Sourceror.range() | nil
def get_range(quoted, opts \\ []) do
range = do_get_range(quoted)

if Keyword.get(opts, :include_comments, false) do
add_comments_to_range(range, quoted)
else
range
with %{} = range <- do_get_range(quoted) do
if Keyword.get(opts, :include_comments, false) do
add_comments_to_range(range, quoted)
else
range
end
end
end

Expand Down Expand Up @@ -51,16 +52,13 @@ defmodule Sourceror.Range do
}
end

@spec get_range(Macro.t()) :: Sourceror.range()
@spec do_get_range(Macro.t()) :: Sourceror.range() | nil
defp do_get_range(quoted)

# Module aliases starting with a non-atom or special form
# e.g. __MODULE__.Nested, @module.Nested, module().Nested
defp do_get_range({:__aliases__, meta, [{_, _, _} = first_segment | rest]}) do
%{start: start_pos} = do_get_range(first_segment)
%{end: end_pos} = do_get_range({:__aliases__, meta, rest})

%{start: start_pos, end: end_pos}
get_range_for_pair(first_segment, {:__aliases__, meta, rest})
end

# Module aliases
Expand Down Expand Up @@ -161,15 +159,14 @@ defmodule Sourceror.Range do
[{_, _, _} | _] ->
{first, rest} = List.pop_at(args, 0)
{last, _} = List.pop_at(rest, -1, first)

%{
start: get_range(first).start,
end: get_range(last).end
}
get_range_for_pair(first, last)

[charlist] when is_list(charlist) ->
string = List.to_string(charlist)
do_get_range({:__block__, meta, [string]})

[] ->
nil
end
end
end
Expand All @@ -188,27 +185,18 @@ defmodule Sourceror.Range do

# 2-tuples from keyword lists
defp do_get_range({left, right}) do
left_range = get_range(left)
right_range = get_range(right)

%{start: left_range.start, end: right_range.end}
get_range_for_pair(left, right)
end

# Handles arguments. Lists are always wrapped in `:__block__`, so the only case
# in which we can have a naked list is in partial keyword lists, as in `[:a, :b, c: d, e: f]`,
# or stabs like `:foo -> :bar`
defp do_get_range(list) when is_list(list) do
first_range = List.first(list) |> get_range()
start_pos = first_range.start

end_pos =
if last = List.last(list) do
get_range(last).end
else
first_range.end
end
defp do_get_range([first, _second | _] = list) do
get_range_for_pair(first, List.last(list))
end

%{start: start_pos, end: end_pos}
defp do_get_range([first]) do
get_range(first)
end

# Stabs without args
Expand All @@ -222,11 +210,8 @@ defmodule Sourceror.Range do

# Stabs with args
# a -> b
defp do_get_range({:->, _, [left_args, right]}) do
start_pos = get_range(left_args).start
end_pos = get_range(right).end

%{start: start_pos, end: end_pos}
defp do_get_range({:->, _, [left, right]}) do
get_range_for_pair(left, right)
end

# Argument capture syntax
Expand All @@ -245,10 +230,10 @@ defmodule Sourceror.Range do

# Unwrapped qualified calls
defp do_get_range({:., meta, [left, atom]}) when is_atom(atom) do
start_pos = get_range(left).start
atom_length = atom |> inspect() |> String.length()

%{start: start_pos, end: [line: meta[:line], column: meta[:column] + atom_length]}
with %{start: start_pos} <- get_range(left) do
atom_length = atom |> inspect() |> String.length()
%{start: start_pos, end: [line: meta[:line], column: meta[:column] + atom_length]}
end
end

# Access syntax
Expand Down Expand Up @@ -300,33 +285,28 @@ defmodule Sourceror.Range do

# Unary operators
defp do_get_range({op, meta, [arg]}) when is_unary_op(op) do
start_pos = Keyword.take(meta, [:line, :column])
arg_range = get_range(arg)
with %{end: end_pos} <- get_range(arg) do
start_pos = Keyword.take(meta, [:line, :column])

end_column =
if arg_range.end[:line] == meta[:line] do
arg_range.end[:column]
else
arg_range.end[:column] + String.length(to_string(op))
end
end_column =
if end_pos[:line] == meta[:line] do
end_pos[:column]
else
end_pos[:column] + String.length(to_string(op))
end

%{start: start_pos, end: [line: arg_range.end[:line], column: end_column]}
%{start: start_pos, end: [line: end_pos[:line], column: end_column]}
end
end

# Binary operators
defp do_get_range({op, _, [left, right]}) when is_binary_op(op) do
%{
start: get_range(left).start,
end: get_range(right).end
}
get_range_for_pair(left, right)
end

# Stepped ranges
defp do_get_range({:"..//", _, [left, _middle, right]}) do
%{
start: get_range(left).start,
end: get_range(right).end
}
get_range_for_pair(left, right)
end

# Bitstrings and interpolations
Expand Down Expand Up @@ -386,14 +366,17 @@ defmodule Sourceror.Range do
get_range_for_unqualified_call(quoted)
end

# Catch-all
defp do_get_range(_), do: nil

defp get_range_for_unqualified_call({_call, meta, args} = quoted) do
if Sourceror.has_closing_line?(quoted) do
get_range_for_node_with_closing_line(quoted)
else
start_pos = Keyword.take(meta, [:line, :column])
end_pos = get_range(List.last(args)).end

%{start: start_pos, end: end_pos}
with %{end: end_pos} <- get_range(List.last(args)) do
start_pos = Keyword.take(meta, [:line, :column])
%{start: start_pos, end: end_pos}
end
end
end

Expand All @@ -407,33 +390,31 @@ defmodule Sourceror.Range do
[left] -> {left, 0}
end

start_pos = get_range(left).start
identifier_pos = Keyword.take(meta, [:line, :column])
with %{start: start_pos} <- get_range(left) do
identifier_pos = Keyword.take(meta, [:line, :column])

parens_length =
if meta[:no_parens] do
0
else
2
end
parens_length =
if meta[:no_parens] do
0
else
2
end

end_pos = [
line: identifier_pos[:line],
column: identifier_pos[:column] + right_len + parens_length
]
end_pos = [
line: identifier_pos[:line],
column: identifier_pos[:column] + right_len + parens_length
]

%{start: start_pos, end: end_pos}
%{start: start_pos, end: end_pos}
end
end
end

defp get_range_for_qualified_call_with_arguments({{:., _, [left | _]}, _meta, args} = quoted) do
if Sourceror.has_closing_line?(quoted) do
get_range_for_node_with_closing_line(quoted)
else
start_pos = get_range(left).start
end_pos = get_range(List.last(args) || left).end

%{start: start_pos, end: end_pos}
get_range_for_pair(left, List.last(args) || left)
end
end

Expand Down Expand Up @@ -510,6 +491,13 @@ defmodule Sourceror.Range do
end
end

defp get_range_for_pair(left, right) do
with %{start: start_pos} <- get_range(left),
%{end: end_pos} <- get_range(right) do
%{start: start_pos, end: end_pos}
end
end

defp has_interpolations?(segments) do
Enum.any?(segments, fn segment ->
match?({:"::", _, _}, segment) or match?({{:., _, [Kernel, :to_string]}, _, _}, segment)
Expand Down
Loading

0 comments on commit eaf0cc6

Please sign in to comment.