Skip to content

Preserve column when translating typespecs #13101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member Author

@wojtekmach wojtekmach Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the moment the auto-generated @type t for protocols does not get the column so this test remains as is.

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