Skip to content
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
6 changes: 3 additions & 3 deletions lib/gradient/elixir_expr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Gradient.ElixirExpr do
@doc """
Convert abstract expressions to Elixir code and format output with formatter.
"""
@spec pp_expr_format([expr()], keyword()) :: iodata()
@spec pp_expr_format(expr() | [expr()], keyword()) :: iodata()
def pp_expr_format(exprs, fmt_opts \\ []) do
exprs
|> pp_expr()
Expand All @@ -35,7 +35,7 @@ defmodule Gradient.ElixirExpr do
def pp_expr({:atom, _, val}) do
case Atom.to_string(val) do
"Elixir." <> mod -> mod
str -> ":" <> str
str -> ":\"" <> str <> "\""
end
end

Expand Down Expand Up @@ -420,7 +420,7 @@ defmodule Gradient.ElixirExpr do

if shortand_syntax do
{:atom, _, key} = key
Atom.to_string(key) <> ": " <> value
"\"" <> Atom.to_string(key) <> "\": " <> value
else
pp_expr(key) <> " => " <> value
end
Expand Down
109 changes: 82 additions & 27 deletions lib/gradient/elixir_fmt.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
defmodule Gradient.ElixirFmt do
@moduledoc """
Module that handles formatting and printing error messages produced by Gradient in Elixir.
Module that handles formatting and printing error messages produced by Gradualizer in Elixir.

Options:
- `ex_colors`: list of color options:
- {`use_colors`, boolean()}: - wheather to use the colors, default: true
- {`expression`, ansicode()}: color of the expressions, default: :yellow
- {`type`, ansicode()}: color of the types, default: :cyan
- {`underscored_line`, ansicode()}: color of the underscored line pointed the error in code, default: :red

- `ex_fmt_expr_fun`: function to pretty print an expression AST in Elixir `(abstract_expr()) -> iodata()`.

- `ex_fmt_type_fun`: function to pretty print an type AST in Elixir `(abstract_type() -> iodata())`.

- Gradualizer options, but some of them are overwritten by Gradient.
"""
@behaviour Gradient.Fmt

alias :gradualizer_fmt, as: FmtLib
alias Gradient.ElixirType
alias Gradient.ElixirExpr
alias Gradient.Types

@type colors_opts() :: [
use_colors: boolean(),
expression: IO.ANSI.ansicode(),
type: IO.ANSI.ansicode(),
underscored_line: IO.ANSI.ansicode()
]
@type options() :: [
ex_colors: colors_opts(),
ex_fmt_type_fun: (Types.abstract_type() -> iodata()),
ex_fmt_expr_fun: (Types.abstract_expr() -> iodata())
]

@default_colors [use_colors: true, expression: :yellow, type: :cyan, underscored_line: :red]

def print_errors(errors, opts) do
for {file, e} <- errors do
Expand All @@ -29,8 +57,9 @@ defmodule Gradient.ElixirFmt do
end

def format_error(error, opts) do
opts = Keyword.put_new(opts, :fmt_type_fun, &ElixirType.pretty_print/1)
opts = Keyword.put_new(opts, :fmt_expr_fun, &ElixirExpr.pp_expr/1)
opts = Keyword.put_new(opts, :color, false)
opts = Keyword.put_new(opts, :fmt_type_fun, pp_type_fun(opts))
opts = Keyword.put_new(opts, :fmt_expr_fun, pp_expr_fun(opts))
format_type_error(error, opts)
end

Expand Down Expand Up @@ -120,7 +149,7 @@ defmodule Gradient.ElixirFmt do
def format_expr_type_error(expression, actual_type, expected_type, opts) do
{inline_expr, fancy_expr} =
case try_highlight_in_context(expression, opts) do
{:error, _e} -> {" " <> pp_expr(expression, opts), ""}
{:error, _e} -> {[" " | pp_expr(expression, opts)], ""}
{:ok, fancy} -> {"", fancy}
end

Expand All @@ -145,34 +174,56 @@ defmodule Gradient.ElixirFmt do
end
end

def pp_expr(expression, opts) do
fmt = Keyword.get(opts, :fmt_expr_fun, &ElixirExpr.pp_expr/1)
def pp_expr_fun(opts) do
fmt = Keyword.get(opts, :ex_fmt_expr_fun, &ElixirExpr.pp_expr_format/1)
colors = get_colors_with_default(opts)
{:ok, use_colors} = Keyword.fetch(colors, :use_colors)
{:ok, expr_color} = Keyword.fetch(colors, :expression)

if Keyword.get(opts, :colors, true) do
IO.ANSI.blue() <> fmt.(expression) <> IO.ANSI.reset()
else
fmt.(expression)
fn expression ->
IO.ANSI.format([expr_color, fmt.(expression)], use_colors)
end
end

def pp_type(type, opts) do
fmt = Keyword.get(opts, :fmt_type_fun, &ElixirType.pretty_print/1)
def pp_type_fun(opts) do
fmt = Keyword.get(opts, :ex_fmt_type_fun, &ElixirType.pp_type_format/1)
colors = get_colors_with_default(opts)
{:ok, use_colors} = Keyword.fetch(colors, :use_colors)
{:ok, type_color} = Keyword.fetch(colors, :type)

if Keyword.get(opts, :colors, true) do
IO.ANSI.cyan() <> fmt.(type) <> IO.ANSI.reset()
else
fmt.(type)
fn type ->
IO.ANSI.format([type_color, fmt.(type)], use_colors)
end
end

def get_colors_with_default(opts) do
case Keyword.fetch(opts, :ex_colors) do
{:ok, colors} ->
colors ++ @default_colors

_ ->
@default_colors
end
end

def pp_expr(expression, opts) do
pp_expr_fun(opts).(expression)
end

def pp_type(type, opts) do
pp_type_fun(opts).(type)
end

@spec try_highlight_in_context(Types.abstract_expr(), options()) ::
{:ok, iodata()} | {:error, term()}
def try_highlight_in_context(expression, opts) do
forms = Keyword.get(opts, :forms)

with :ok <- has_location?(expression),
{:ok, path} <- get_ex_file_path(forms),
{:ok, code} <- File.read(path) do
code_lines = String.split(code, ~r/\R/)
{:ok, highlight_in_context(expression, code_lines)}
{:ok, highlight_in_context(expression, code_lines, opts)}
end
end

Expand All @@ -184,14 +235,14 @@ defmodule Gradient.ElixirFmt do
end
end

@spec highlight_in_context(tuple(), [String.t()]) :: String.t()
def highlight_in_context(expression, context) do
@spec highlight_in_context(tuple(), [String.t()], options()) :: iodata()
def highlight_in_context(expression, context, opts) do
line = elem(expression, 1)

context
|> Enum.with_index(1)
|> filter_context(line, 2)
|> underscore_line(line)
|> underscore_line(line, opts)
|> Enum.join("\n")
end

Expand All @@ -202,10 +253,19 @@ defmodule Gradient.ElixirFmt do
Enum.filter(lines, fn {_, number} -> number in range end)
end

def underscore_line(lines, line) do
def underscore_line(lines, line, opts) do
Enum.map(lines, fn {str, n} ->
if(n == line) do
IO.ANSI.underline() <> IO.ANSI.red() <> to_string(n) <> " " <> str <> IO.ANSI.reset()
colors = get_colors_with_default(opts)
{:ok, use_colors} = Keyword.fetch(colors, :use_colors)
{:ok, color} = Keyword.fetch(colors, :underscored_line)
line_str = to_string(n) <> " " <> str

[
IO.ANSI.underline(),
IO.ANSI.format_fragment([color, line_str], use_colors),
IO.ANSI.reset()
]
else
to_string(n) <> " " <> str
end
Expand All @@ -225,11 +285,6 @@ defmodule Gradient.ElixirFmt do
end
end

# defp warning_error_not_handled(error) do
# msg = "\nElixir formatter not exist for #{inspect(error, pretty: true)} using default \n"
# String.to_charlist(IO.ANSI.light_yellow() <> msg <> IO.ANSI.reset())
# end

@spec describe_expr(:gradualizer_type.abstract_expr()) :: binary()
def describe_expr({:atom, _, _}), do: "atom"
def describe_expr({:bc, _, _, _}), do: "binary comprehension"
Expand Down
22 changes: 15 additions & 7 deletions lib/gradient/elixir_type.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
defmodule Gradient.ElixirType do
@moduledoc """
Module to format types.

Seems that:
- record type
- constrained function type
are not used by Elixir so the pp support has not been added.
Convert the Erlang abstract types to the Elixir code.
"""

alias Gradient.ElixirFmt

@type abstract_type() :: Gradient.Types.abstract_type()

@doc """
Convert abstract type to Elixir code and format output with formatter.
"""
@spec pp_type_format(abstract_type(), keyword()) :: iodata()
def pp_type_format(type, fmt_opts \\ []) do
type
|> pretty_print()
|> Code.format_string!(fmt_opts)
end

@doc """
Take type and prepare a pretty string representation.
"""
Expand Down Expand Up @@ -84,7 +89,10 @@ defmodule Gradient.ElixirType do
end

def pretty_print({:atom, _, val}) do
":" <> Atom.to_string(val)
case Atom.to_string(val) do
"Elixir." <> mod -> mod
str -> ":\"" <> str <> "\""
end
end

def pretty_print({:integer, _, val}) do
Expand Down
1 change: 1 addition & 0 deletions lib/gradient/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Gradient.Types do
@type token :: tuple()
@type tokens :: [tuple()]
@type abstract_type :: :erl_parse.abstract_type()
@type abstract_expr :: :erl_parse.abstract_expr()
@type form ::
:erl_parse.abstract_clause()
| :erl_parse.abstract_expr()
Expand Down
30 changes: 15 additions & 15 deletions test/gradient/elixir_expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "fn {:ok, v} -> v; {:error, _} -> :error end" == actual
assert ~s(fn {:"ok", v} -> v; {:"error", _} -> :"error" end) == actual
end

test "binary comprehension" do
Expand Down Expand Up @@ -79,7 +79,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "receive do {:hello, msg} -> msg end" == actual
assert ~s(receive do {:"hello", msg} -> msg end) == actual
end

test "receive after" do
Expand All @@ -93,7 +93,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert ~s(receive do {:hello, msg} -> msg after 1000 -> "nothing happened" end) == actual
assert ~s(receive do {:"hello", msg} -> msg after 1000 -> "nothing happened" end) == actual
end

test "call pipe" do
Expand Down Expand Up @@ -123,7 +123,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "map = %{a: 12, b: 0}; case :maps.find(:a, map) do {:ok, a} -> case :maps.find(:b, map) do {:ok, b} -> a + b; _gen -> case _gen do :error -> 0; _gen -> raise {:with_clause, _gen} end end; _gen -> case _gen do :error -> 0; _gen -> raise {:with_clause, _gen} end end" ==
assert ~s(map = %{"a": 12, "b": 0}; case :maps.find(:"a", map\) do {:"ok", a} -> case :maps.find(:"b", map\) do {:"ok", b} -> a + b; _gen -> case _gen do :"error" -> 0; _gen -> raise {:"with_clause", _gen} end end; _gen -> case _gen do :"error" -> 0; _gen -> raise {:"with_clause", _gen} end end) ==
actual
end

Expand All @@ -140,7 +140,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert ~s(try do raise "ok"; catch :error, e -> IO.puts(Exception.format(:error, e, __STACKTRACE__\)\); reraise e, __STACKTRACE__ end) ==
assert ~s(try do raise "ok"; catch :"error", e -> IO.puts(Exception.format(:"error", e, __STACKTRACE__\)\); reraise e, __STACKTRACE__ end) ==
actual
end

Expand All @@ -155,7 +155,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert ~s(try do raise "oops"; catch :error, %RuntimeError{} = _ -> "Error!" end) ==
assert ~s(try do raise "oops"; catch :"error", %RuntimeError{} = _ -> "Error!" end) ==
actual
end

Expand All @@ -170,7 +170,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "try do :ok; catch :error, _ -> :ok end" == actual
assert ~s(try do :"ok"; catch :"error", _ -> :"ok" end) == actual
end

test "simple after try" do
Expand All @@ -184,7 +184,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "try do :ok; after :ok end" == actual
assert ~s(try do :"ok"; after :"ok" end) == actual
end

test "try guard" do
Expand Down Expand Up @@ -215,7 +215,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert ~s(try do throw "good"; :ok; else v when v == :ok -> :ok; v -> :nok; catch :error, %RuntimeError{} = e -> 11; e; :throw, val -> val; :throw, _ -> 0; after IO.puts("Cleaning!"\) end) ==
assert ~s(try do throw "good"; :"ok"; else v when v == :"ok" -> :"ok"; v -> :"nok"; catch :"error", %RuntimeError{} = e -> 11; e; :"throw", val -> val; :"throw", _ -> 0; after IO.puts("Cleaning!"\) end) ==
actual
end

Expand All @@ -235,7 +235,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "case {:ok, 10} do {:ok, v} when v > 0 and v > 1 or v < - 1 -> :ok; t when :erlang.is_tuple(t) -> :nok; _ -> :err end" ==
assert ~s(case {:"ok", 10} do {:"ok", v} when v > 0 and v > 1 or v < - 1 -> :"ok"; t when :erlang.is_tuple(t\) -> :"nok"; _ -> :"err" end) ==
actual
end

Expand All @@ -249,7 +249,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "case {:ok, 13} do {:ok, v} -> v; _err -> :error end" == actual
assert ~s(case {:"ok", 13} do {:"ok", v} -> v; _err -> :"error" end) == actual
end

test "if" do
Expand All @@ -263,7 +263,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "if :math.floor(1.9) == 1.0 do :ok else :error end" == actual
assert ~s(if :math.floor(1.9\) == 1.0 do :"ok" else :"error" end) == actual
end

test "unless" do
Expand All @@ -277,7 +277,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "if :math.floor(1.9) == 1.0 do :error else :ok end" == actual
assert ~s(if :math.floor(1.9\) == 1.0 do :"error" else :"ok" end) == actual
end

test "cond" do
Expand All @@ -296,7 +296,7 @@ defmodule Gradient.ElixirExprTest do
end
|> ElixirExpr.pp_expr()

assert "cond do true == false -> :ok; :math.floor(1.9) == 1.0 -> :ok; true -> :error end" ==
assert ~s(cond do true == false -> :"ok"; :math.floor\(1.9\) == 1.0 -> :"ok"; true -> :"error" end) ==
actual
end

Expand All @@ -322,7 +322,7 @@ defmodule Gradient.ElixirExprTest do
|> ElixirExpr.pp_expr()

assert ~s(try do if true do throw "good" else raise "oops" end;) <>
~s( catch :error, %RuntimeError{} = e -> 11; e; :throw, val -> 12; val end) ==
~s( catch :"error", %RuntimeError{} = e -> 11; e; :"throw", val -> 12; val end) ==
actual
end
end
Expand Down
Loading