Skip to content

Commit

Permalink
Preserve column when translating typespecs (#13101)
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach authored and josevalim committed Nov 14, 2023
1 parent 901ec18 commit aef8673
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 51 deletions.
10 changes: 9 additions & 1 deletion lib/elixir/lib/code/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -420,5 +420,13 @@ defmodule Code.Typespec do
:error
end

defp meta(anno), do: [line: :erl_anno.line(anno)]
defp meta(anno) do
case :erl_anno.location(anno) do
{line, column} ->
[line: line, column: column]

line when is_integer(line) ->
[line: line]
end
end
end
92 changes: 51 additions & 41 deletions lib/elixir/lib/kernel/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -385,17 +385,17 @@ defmodule Kernel.Typespec do
compile_error(caller, error)
end

line = line(meta)
location = location(meta)
vars = Keyword.keys(guard)

{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{return, state} = typespec(return, vars, caller, state)
spec = {:type, line, :fun, [{:type, line, :product, args}, return]}
spec = {:type, location, :fun, [{:type, location, :product, args}, return]}

{spec, state} =
case guard_to_constraints(guard, vars, meta, caller, state) do
{[], state} -> {spec, state}
{constraints, state} -> {{:type, line, :bounded_fun, [spec, constraints]}, state}
{constraints, state} -> {{:type, location, :bounded_fun, [spec, constraints]}, state}
end

ensure_no_unused_local_vars!(caller, state.local_vars)
Expand Down Expand Up @@ -437,17 +437,17 @@ defmodule Kernel.Typespec do
defp ensure_not_default(_), do: :ok

defp guard_to_constraints(guard, vars, meta, caller, state) do
line = line(meta)
location = location(meta)

fun = fn
{_name, {:var, _, context}}, {constraints, state} when is_atom(context) ->
{constraints, state}

{name, type}, {constraints, state} ->
{spec, state} = typespec(type, vars, caller, state)
constraint = [{:atom, line, :is_subtype}, [{:var, line, name}, spec]]
constraint = [{:atom, location, :is_subtype}, [{:var, location, name}, spec]]
state = update_local_vars(state, name)
{[{:type, line, :constraint, constraint} | constraints], state}
{[{:type, location, :constraint, constraint} | constraints], state}
end

{constraints, state} = :lists.foldl(fun, {[], state}, guard)
Expand All @@ -456,21 +456,27 @@ defmodule Kernel.Typespec do

## To typespec conversion

defp line(meta) do
Keyword.get(meta, :line, 0)
defp location(meta) do
line = Keyword.get(meta, :line, 0)

if column = Keyword.get(meta, :column) do
{line, column}
else
line
end
end

# Handle unions
defp typespec({:|, meta, [_, _]} = exprs, vars, caller, state) do
exprs = collect_union(exprs)
{union, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, exprs)
{{:type, line(meta), :union, union}, state}
{{:type, location(meta), :union, union}, state}
end

# Handle binaries
defp typespec({:<<>>, meta, []}, _, _, state) do
line = line(meta)
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line, 0}]}, state}
location = location(meta)
{{:type, location, :binary, [{:integer, location, 0}, {:integer, location, 0}]}, state}
end

defp typespec(
Expand All @@ -480,14 +486,18 @@ defmodule Kernel.Typespec do
state
)
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
line = line(meta)
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
location = location(meta)

{{:type, location, :binary, [{:integer, location, 0}, {:integer, location(unit_meta), unit}]},
state}
end

defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
when is_atom(ctx) and is_integer(size) and size >= 0 do
line = line(meta)
{{:type, line, :binary, [{:integer, line(size_meta), size}, {:integer, line, 0}]}, state}
location = location(meta)

{{:type, location, :binary, [{:integer, location(size_meta), size}, {:integer, location, 0}]},
state}
end

defp typespec(
Expand All @@ -505,8 +515,8 @@ defmodule Kernel.Typespec do
)
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
size >= 0 and unit in 1..256 do
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
{{:type, line(meta), :binary, args}, state}
args = [{:integer, location(size_meta), size}, {:integer, location(unit_meta), unit}]
{{:type, location(meta), :binary, args}, state}
end

defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
Expand All @@ -519,25 +529,25 @@ defmodule Kernel.Typespec do

## Handle maps and structs
defp typespec({:map, meta, args}, _vars, _caller, state) when args == [] or is_atom(args) do
{{:type, line(meta), :map, :any}, state}
{{:type, location(meta), :map, :any}, state}
end

defp typespec({:%{}, meta, fields} = map, vars, caller, state) do
fun = fn
{{:required, meta2, [k]}, v}, state ->
{arg1, state} = typespec(k, vars, caller, state)
{arg2, state} = typespec(v, vars, caller, state)
{{:type, line(meta2), :map_field_exact, [arg1, arg2]}, state}
{{:type, location(meta2), :map_field_exact, [arg1, arg2]}, state}

{{:optional, meta2, [k]}, v}, state ->
{arg1, state} = typespec(k, vars, caller, state)
{arg2, state} = typespec(v, vars, caller, state)
{{:type, line(meta2), :map_field_assoc, [arg1, arg2]}, state}
{{:type, location(meta2), :map_field_assoc, [arg1, arg2]}, state}

{k, v}, state ->
{arg1, state} = typespec(k, vars, caller, state)
{arg2, state} = typespec(v, vars, caller, state)
{{:type, line(meta), :map_field_exact, [arg1, arg2]}, state}
{{:type, location(meta), :map_field_exact, [arg1, arg2]}, state}

{:|, _, [_, _]}, _state ->
error =
Expand All @@ -551,7 +561,7 @@ defmodule Kernel.Typespec do
end

{fields, state} = :lists.mapfoldl(fun, state, fields)
{{:type, line(meta), :map, fields}, state}
{{:type, location(meta), :map, fields}, state}
end

defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
Expand Down Expand Up @@ -644,7 +654,7 @@ defmodule Kernel.Typespec do
{right, state} = typespec(right, vars, caller, state)
:ok = validate_range(left, right, caller)

{{:type, line(meta), :range, [left, right]}, state}
{{:type, location(meta), :range, [left, right]}, state}
end

# Handle special forms
Expand All @@ -668,7 +678,7 @@ defmodule Kernel.Typespec do
pair -> pair
end

{{:type, line(meta), :fun, fun_args}, state}
{{:type, location(meta), :fun, fun_args}, state}
end

# Handle type operator
Expand All @@ -691,10 +701,10 @@ defmodule Kernel.Typespec do
# This may be generating an invalid typespec but we need to generate it
# to avoid breaking existing code that was valid but only broke Dialyzer
{right, state} = typespec(expr, vars, caller, state)
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
{{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state}

{right, state} ->
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
{{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state}
end
end

Expand Down Expand Up @@ -723,13 +733,13 @@ defmodule Kernel.Typespec do
{left, state} = typespec(left, vars, caller, state)
state = %{state | undefined_type_error_enabled?: true}
{right, state} = typespec(right, vars, caller, state)
{{:ann_type, line(meta), [left, right]}, state}
{{:ann_type, location(meta), [left, right]}, state}
end

# Handle unary ops
defp typespec({op, meta, [integer]}, _, _, state) when op in [:+, :-] and is_integer(integer) do
line = line(meta)
{{:op, line, op, {:integer, line, integer}}, state}
location = location(meta)
{{:op, location, op, {:integer, location, integer}}, state}
end

# Handle remote calls in the form of @module_attribute.type.
Expand Down Expand Up @@ -778,12 +788,12 @@ defmodule Kernel.Typespec do

# Handle tuples
defp typespec({:tuple, meta, []}, _vars, _caller, state) do
{{:type, line(meta), :tuple, :any}, state}
{{:type, location(meta), :tuple, :any}, state}
end

defp typespec({:{}, meta, t}, vars, caller, state) when is_list(t) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, t)
{{:type, line(meta), :tuple, args}, state}
{{:type, location(meta), :tuple, args}, state}
end

defp typespec({left, right}, vars, caller, state) do
Expand All @@ -799,7 +809,7 @@ defmodule Kernel.Typespec do
defp typespec({name, meta, atom}, vars, caller, state) when is_atom(atom) do
if :lists.member(name, vars) do
state = update_local_vars(state, name)
{{:var, line(meta), name}, state}
{{:var, location(meta), name}, state}
else
typespec({name, meta, []}, vars, caller, state)
end
Expand All @@ -814,7 +824,7 @@ defmodule Kernel.Typespec do

IO.warn(warning, caller)
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, line(meta), :string, args}, state}
{{:type, location(meta), :string, args}, state}
end

defp typespec({:nonempty_string, meta, args}, vars, caller, state) do
Expand All @@ -825,7 +835,7 @@ defmodule Kernel.Typespec do

IO.warn(warning, caller)
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, line(meta), :nonempty_string, args}, state}
{{:type, location(meta), :nonempty_string, args}, state}
end

defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
Expand Down Expand Up @@ -855,7 +865,7 @@ defmodule Kernel.Typespec do

defp typespec({:fun, meta, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, line(meta), :fun, args}, state}
{{:type, location(meta), :fun, args}, state}
end

defp typespec({:..., _meta, _args}, _vars, caller, _state) do
Expand All @@ -872,7 +882,7 @@ defmodule Kernel.Typespec do

case :erl_internal.is_type(name, arity) do
true ->
{{:type, line(meta), name, args}, state}
{{:type, location(meta), name, args}, state}

false ->
if state.undefined_type_error_enabled? and
Expand All @@ -890,7 +900,7 @@ defmodule Kernel.Typespec do
%{state | used_type_pairs: [{name, arity} | state.used_type_pairs]}
end

{{:user_type, line(meta), name, args}, state}
{{:user_type, location(meta), name, args}, state}
end
end

Expand Down Expand Up @@ -963,7 +973,7 @@ defmodule Kernel.Typespec do

defp remote_type({remote, meta, name, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:remote_type, line(meta), [remote, name, args]}, state}
{{:remote_type, location(meta), [remote, name, args]}, state}
end

defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
Expand Down Expand Up @@ -996,16 +1006,16 @@ defmodule Kernel.Typespec do
end

defp fn_args(meta, [{:..., _, _}], _vars, _caller, state) do
{{:type, line(meta), :any}, state}
{{:type, location(meta), :any}, state}
end

defp fn_args(meta, args, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, line(meta), :product, args}, state}
{{:type, location(meta), :product, args}, state}
end

defp variable({name, meta, args}) when is_atom(name) and is_atom(args) do
{:var, line(meta), name}
{:var, location(meta), name}
end

defp variable(expr), do: expr
Expand Down
8 changes: 6 additions & 2 deletions lib/elixir/test/elixir/protocol_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,12 @@ defmodule ProtocolTest do
end

test "protocol defines callbacks" do
assert [{:type, 13, :fun, args}] = get_callbacks(@sample_binary, :ok, 1)
assert args == [{:type, 13, :product, [{:user_type, 13, :t, []}]}, {:type, 13, :boolean, []}]
assert [{:type, {13, 19}, :fun, args}] = get_callbacks(@sample_binary, :ok, 1)

assert args == [
{:type, {13, 19}, :product, [{:user_type, {13, 16}, :t, []}]},
{:type, {13, 22}, :boolean, []}
]

assert [{:type, 23, :fun, args}] = get_callbacks(@with_any_binary, :ok, 1)
assert args == [{:type, 23, :product, [{:user_type, 23, :t, []}]}, {:type, 23, :term, []}]
Expand Down
20 changes: 13 additions & 7 deletions lib/elixir/test/elixir/typespec_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1582,23 +1582,29 @@ defmodule TypespecTest do
""")

[type: type] = types(:typespec_test_mod)
line = 5

assert Code.Typespec.type_to_quoted(type) ==
{:"::", [], [{:t, [], [{:x, [line: line], nil}]}, [{:x, [line: line], nil}]]}
{:"::", [],
[
{:t, [], [{:x, meta(5, 9), nil}]},
[{:x, meta(5, 20), nil}]
]}

[{{:f, 1}, [spec]}] = specs(:typespec_test_mod)
line = 7

assert Code.Typespec.spec_to_quoted(:f, spec) ==
{:when, [line: line],
{:when, meta(7, 8),
[
{:"::", [line: line],
[{:f, [line: line], [{:x, [line: line], nil}]}, {:x, [line: line], nil}]},
[x: {:var, [line: line], nil}]
{:"::", meta(7, 8),
[{:f, meta(7, 8), [{:x, meta(7, 9), nil}]}, {:x, meta(7, 15), nil}]},
[x: {:var, meta(7, 8), nil}]
]}
end

defp meta(line, column) do
[line: line, column: column]
end

defp erlc(context, module, code) do
dir = context.tmp_dir

Expand Down

0 comments on commit aef8673

Please sign in to comment.