defmodule Uptight.Assertions.Diff do @moduledoc false # A Diff struct and functions. # # The Diff struct contains the fields `:equivalent?`, `:left`, `:right`. # The `:equivalent?` field represents if the `:left` and `:right` side are # equivalents and contain no diffs. The `:left` and `:right` represent the sides # of the comparison and contain ASTs with some special metas: `:diff` and # `:diff_container`. # # When meta `:diff` is `true`, the AST inside of it has no equivalent on the # other side and should be rendered in a different color. If the AST is a # literal and doesn't contain meta, the `:diff` meta will be placed in a # wrapping block. alias Code.Identifier alias Inspect.Algebra defstruct equivalent?: true, left: nil, right: nil @doc """ Returns the diff between `left` and `right` and env after the comparison. The `left` side can be a literal or an AST, the `right` should always be a value. The `context` should be `{:match, pins}` for pattern matching and `==` and `===` for comparison cases. """ def compute(left, right, context) do diff(left, right, context_to_env(context)) end defp context_to_env({:match, pins}), do: %{pins: Map.new(pins), context: :match, current_vars: %{}} defp context_to_env(op) when op in [:==, :===], do: %{pins: %{}, context: op, current_vars: %{}} # Main entry point for recursive diff defp diff(left, right, %{context: :match} = env), do: diff_quoted(left, right, env) defp diff(left, right, env), do: diff_value(left, right, env) # diff quoted defp diff_quoted({:_, _, context} = left, right, env) when is_atom(context) do diff_right = escape(right) diff = %__MODULE__{equivalent?: true, left: left, right: diff_right} {diff, env} end defp diff_quoted({:^, _, [{name, _, context}]} = left, right, env) when is_atom(name) and is_atom(context) do diff_pin(left, right, env) end defp diff_quoted({name, _, context} = left, right, env) when is_atom(name) and is_atom(context) and name not in [:__MODULE__, :__DIR__, :__STACKTRACE__, :__ENV__, :__CALLER__] do diff_var(left, right, env) end defp diff_quoted({:-, _, [number]}, right, env) when is_number(number) do diff_quoted(-number, right, env) end defp diff_quoted({:+, _, [number]}, right, env) when is_number(number) do diff_quoted(number, right, env) end defp diff_quoted({:++, meta, [prefix, suffix]}, right, env) when is_list(right) do case prefix do {_, [expanded: expanded] ++ _, _} -> diff_maybe_improper_list({:++, meta, [expanded, suffix]}, right, env) _ -> diff_maybe_improper_list({:++, meta, [prefix, suffix]}, right, env) end end defp diff_quoted({:{}, _, left}, right, env) when is_tuple(right) do diff_tuple(left, Tuple.to_list(right), env) end defp diff_quoted({_, _} = left, right, env) when is_tuple(right) do diff_tuple(Tuple.to_list(left), Tuple.to_list(right), env) end defp diff_quoted({:%, _, [struct, {:%{}, _, kw}]}, %{} = right, env) when is_list(kw) do diff_quoted_struct([__struct__: struct] ++ kw, right, env) end defp diff_quoted({:%{}, _, kw}, %{} = right, env) when is_list(kw) do diff_quoted_struct(kw, right, env) end defp diff_quoted({:<>, _, [literal, _]} = left, right, env) when is_binary(literal) and is_binary(right) do diff_string_concat(left, right, env) end defp diff_quoted({:when, _, [_, _]} = left, right, env) do diff_guard(left, right, env) end defp diff_quoted({_, [{:expanded, expanded} | _], _} = left, right, env) do macro = Macro.update_meta(left, &Keyword.delete(&1, :expanded)) diff_macro(macro, expanded, right, env) end defp diff_quoted(left, right, env) when is_list(left) and is_list(right) do diff_maybe_list(left, right, env) end defp diff_quoted(left, right, env) when is_atom(left) or is_number(left) or is_reference(left) or is_pid(left) or is_function(left) or is_binary(left) do diff_value(left, right, env) end defp diff_quoted(left, right, %{context: :match} = env) do diff_left = update_diff_meta(left, true) diff_right = escape(right) |> update_diff_meta(true) diff = %__MODULE__{equivalent?: false, left: diff_left, right: diff_right} {diff, env} end ## diff_value defp diff_value(literal, literal, env) when is_atom(literal) or is_number(literal) or is_reference(literal) or is_pid(literal) or is_function(literal) do {%__MODULE__{equivalent?: true, left: literal, right: literal}, env} end defp diff_value(left, right, %{context: :==} = env) when left == right and is_number(left) do {%__MODULE__{equivalent?: true, left: left, right: right}, env} end defp diff_value(left, right, env) when is_number(left) and is_number(right) do diff_number(left, right, env) end defp diff_value(left, right, env) when is_list(left) and is_list(right) do diff_maybe_list(left, right, env) end defp diff_value(left, right, env) when is_tuple(left) and is_tuple(right) do diff_tuple(Tuple.to_list(left), Tuple.to_list(right), env) end defp diff_value(%left_struct{} = left, %right_struct{} = right, env) do diff_struct( left, Map.to_list(left), right, left_struct, right_struct, env ) end defp diff_value(%{} = left, %{} = right, env) do diff_map(Map.to_list(left), right, maybe_struct(left), maybe_struct(right), env) end defp diff_value(left, right, env) when is_binary(left) and is_binary(right) do diff_string(left, right, ?", env) end defp diff_value(left, right, env) do diff_left = escape(left) |> update_diff_meta(true) diff_right = escape(right) |> update_diff_meta(true) diff = %__MODULE__{equivalent?: false, left: diff_left, right: diff_right} {diff, env} end # Macros defp diff_macro(macro, expanded, right, env) do {diff, post_env} = diff(expanded, right, env) diff_left = update_diff_meta(macro, !diff.equivalent?) {%{diff | left: diff_left}, post_env} end # Guards defp diff_guard({:when, _, [expression, clause]}, right, env) do {diff_expression, post_env} = diff_quoted(expression, right, env) {guard_clause, guard_equivalent?} = if diff_expression.equivalent? do bindings = Map.merge(post_env.pins, post_env.current_vars) diff_guard_clause(clause, Map.to_list(bindings)) else {clause, false} end diff = %__MODULE__{ diff_expression | left: {:when, [], [diff_expression.left, guard_clause]}, equivalent?: guard_equivalent? } {diff, post_env} end defp diff_guard_clause({op, _, [clause1, clause2]}, bindings) when op in [:when, :or, :and] do {diff_clause1, clause1_equivalent?} = diff_guard_clause(clause1, bindings) {diff_clause2, clause2_equivalent?} = diff_guard_clause(clause2, bindings) equivalent? = case op do :and -> clause1_equivalent? and clause2_equivalent? _other -> clause1_equivalent? or clause2_equivalent? end diff = {op, [], [diff_clause1, diff_clause2]} {diff, equivalent?} end defp diff_guard_clause(quoted, bindings) do expanded = Macro.prewalk(quoted, fn {_, [{:expanded, expanded} | _], _} -> expanded other -> other end) {equivalent?, _bindings} = Code.eval_quoted(expanded, bindings) {update_diff_meta(quoted, !equivalent?), equivalent?} end # Pins defp diff_pin({:^, _, [var]} = pin, right, %{pins: pins} = env) do identifier = var_context(var) %{^identifier => pin_value} = pins {diff, post_env} = diff_value(pin_value, right, env) diff_left = update_diff_meta(pin, not diff.equivalent?) {%{diff | left: diff_left}, post_env} end # Vars defp diff_var({name, meta, context} = left, right, env) do identifier = {name, meta[:counter] || context} case env.current_vars do %{^identifier => ^right} -> diff_right = escape(right) diff = %__MODULE__{equivalent?: true, left: left, right: diff_right} {diff, env} %{^identifier => _} -> diff_left = update_diff_meta(left, true) diff_right = escape(right) |> update_diff_meta(true) diff = %__MODULE__{equivalent?: false, left: diff_left, right: diff_right} {diff, env} current_vars = %{} -> updated_vars = Map.put(current_vars, identifier, right) diff_right = escape(right) diff = %__MODULE__{equivalent?: true, left: left, right: diff_right} {diff, %{env | current_vars: updated_vars}} end end # Tuples defp diff_tuple(list_left, list_right, env) do diff_tuple(list_left, list_right, true, [], [], env) end defp diff_tuple([left | tleft], [right | tright], acc_equivalent?, acc_left, acc_right, env) do {diff, env} = diff(left, right, env) acc_equivalent? = acc_equivalent? and diff.equivalent? acc_left = [diff.left | acc_left] acc_right = [diff.right | acc_right] diff_tuple(tleft, tright, acc_equivalent?, acc_left, acc_right, env) end defp diff_tuple(remaining_left, remaining_right, acc_equivalent?, acc_left, acc_right, env) do remaining_left = Enum.map(remaining_left, &(&1 |> maybe_escape(env) |> update_diff_meta(true))) remaining_right = Enum.map(remaining_right, &(&1 |> escape() |> update_diff_meta(true))) equivalent? = acc_equivalent? and remaining_left == [] and remaining_right == [] diff_left = {:{}, [], Enum.reverse(acc_left, remaining_left)} diff_right = {:{}, [], Enum.reverse(acc_right, remaining_right)} {%__MODULE__{equivalent?: equivalent?, left: diff_left, right: diff_right}, env} end # Lists defp diff_maybe_list([], [], env) do {%__MODULE__{equivalent?: true, left: [], right: []}, env} end defp diff_maybe_list(left, right, env) do if List.ascii_printable?(left) and List.ascii_printable?(right) do diff_string(List.to_string(left), List.to_string(right), ?', env) else diff_maybe_improper_list(left, right, env) end end # Compare two lists, removing all the operators (`|` and `++`) from the left # side before and adding them back in the end. Improper lists on the left side # are handled as quoted expressions. Improper lists on the right side are # handled as runtime improper lists. defp diff_maybe_improper_list(left, right, env) do {parsed_left, improper_left, operators_left, length_left} = split_left_list(left, 0, env.context) {parsed_right, improper_right} = split_right_list(right, length_left, []) {parsed_diff, parsed_post_env} = myers_difference_list(parsed_left, parsed_right, env) {improper_diff, improper_post_env} = diff_improper(improper_left, improper_right, parsed_post_env) diff = merge_diff(parsed_diff, improper_diff, fn left1, left2, right1, right2 -> improper_left = cond do improper_left != [] -> {:improper, left2} improper_right != [] -> :tail true -> :nothing end left = rebuild_left_list(left1, improper_left, operators_left, env) right = rebuild_right_list(right1, right2) {left, right} end) {diff, improper_post_env} end defp diff_improper([], right, env) when is_list(right) do equivalent? = right == [] right = right |> escape() |> update_diff_meta(not equivalent?) {%__MODULE__{equivalent?: equivalent?, right: right, left: []}, env} end defp diff_improper(left, right, env) do diff(left, right, env) end defp split_right_list([head | tail], length, acc) when length > 0, do: split_right_list(tail, length - 1, [head | acc]) defp split_right_list(rest, _length, acc), do: {Enum.reverse(acc), rest} defp rebuild_right_list(left, right) do left = Enum.reverse(left) case extract_diff_meta(right) do {[_ | _] = list, _diff?} -> # Inner was escaped, diffs are inside rebuild_maybe_improper(list, left, & &1) {list, diff?} -> # Outer was escaped, move diffs and escape inside list |> unescape() |> rebuild_maybe_improper(left, &(&1 |> escape() |> update_diff_meta(diff?))) end end defp rebuild_maybe_improper([head | tail], acc, fun), do: rebuild_maybe_improper(tail, [fun.(head) | acc], fun) defp rebuild_maybe_improper([], acc, _fun), do: Enum.reverse(acc) defp rebuild_maybe_improper(other, [prev | acc], fun), do: Enum.reverse([{:|, [], [prev, fun.(other)]} | acc]) defp split_left_list([], _index, _context) do {[], [], nil, 0} end defp split_left_list({:++, _, [left, right]}, _index, :match) do {parsed_left, [], operators_left, length_left} = split_left_list(left, 0, :match) case split_left_list(right, 0, :match) do {:improper, improper} -> operators = {:++, length_left, [operators_left, nil]} {parsed_left, improper, operators, length_left} {parsed_right, improper_right, operators_right, length_right} -> operators = {:++, length_left, [operators_left, operators_right]} length = length_right + length_left {parsed_left ++ parsed_right, improper_right, operators, length} end end defp split_left_list([{:|, _, [head, tail]}], index, :match) do case split_left_list(tail, 0, :match) do {:improper, improper} -> operator = {:|, index, [nil]} {[head], improper, operator, 1} {parsed_tail, improper_tail, operators_tail, length_tail} -> operators = {:|, index, [operators_tail]} {[head | parsed_tail], improper_tail, operators, length_tail + 1} end end defp split_left_list([head | tail], index, context) do case split_left_list(tail, index + 1, context) do {:improper, improper} -> operator = {:|, index, [nil]} {[head], improper, operator, 1} {parsed_tail, improper_tail, operators_tail, length_tail} -> {[head | parsed_tail], improper_tail, operators_tail, length_tail + 1} end end defp split_left_list(element, _index, _) do {:improper, element} end defp rebuild_left_list([], {:improper, improper}, _operators = nil, _env), do: improper defp rebuild_left_list(list, _, _operators = nil, _env), do: list defp rebuild_left_list(list, :tail, {:|, index, [operators]}, env) do {left, [head | tail]} = Enum.split(list, index) rebuilt_tail = rebuild_left_list(tail, :nothing, operators, env) rebuilt_tail = rebuilt_tail |> update_diff_meta(true) left ++ [{:|, [], [head, rebuilt_tail]}] end defp rebuild_left_list(list, improper, {:|, index, [operators]}, env) do {left, [head | tail]} = Enum.split(list, index) rebuilt_tail = rebuild_left_list(tail, improper, operators, env) left ++ [{:|, [], [head, rebuilt_tail]}] end defp rebuild_left_list(list, improper, {:++, index, operators}, env) do [operators_left, operators_right] = operators {left, right} = Enum.split(list, index) rebuilt_left = rebuild_left_list(left, :nothing, operators_left, env) rebuilt_right = rebuild_left_list(right, improper, operators_right, env) {:++, [], [rebuilt_left, rebuilt_right]} end defp myers_difference_list(left, right, env) do path = {0, left, right, {[], [], env}} find_diff(0, length(left) + length(right), [path]) end defp find_diff(envelope, max, paths) do case each_diagonal(-envelope, envelope, paths, []) do {:done, {edit1, edit2, env}} -> list_script_to_diff(Enum.reverse(edit1), Enum.reverse(edit2), true, [], [], env) {:next, paths} -> find_diff(envelope + 1, max, paths) end end defp each_diagonal(diag, limit, _paths, next_paths) when diag > limit do {:next, Enum.reverse(next_paths)} end defp each_diagonal(diag, limit, paths, next_paths) do {path, rest} = proceed_path(diag, limit, paths) case follow_snake(path) do {:cont, path} -> each_diagonal(diag + 2, limit, rest, [path | next_paths]) {:done, edits} -> {:done, edits} end end defp proceed_path(0, 0, [path]), do: {path, []} defp proceed_path(diag, limit, [path | _] = paths) when diag == -limit do {move_down(path), paths} end defp proceed_path(diag, limit, [path]) when diag == limit do {move_right(path), []} end defp proceed_path(_diag, _limit, [path1, path2 | rest]) do if elem(path1, 0) > elem(path2, 0) do {move_right(path1), [path2 | rest]} else {move_down(path2), [path2 | rest]} end end defp move_right({y, list1, [elem2 | rest2], {edit1, edit2, env}}) do {y, list1, rest2, {edit1, [{:ins, elem2} | edit2], env}} end defp move_right({y, list1, [], edits}) do {y, list1, [], edits} end defp move_down({y, [elem1 | rest1], list2, {edit1, edit2, env}}) do {y + 1, rest1, list2, {[{:del, elem1} | edit1], edit2, env}} end defp move_down({y, [], list2, edits}) do {y + 1, [], list2, edits} end defp follow_snake({y, [elem1 | rest1], [elem2 | rest2], {edit1, edit2, env}} = path) do {diff, post_env} = diff(elem1, elem2, env) if diff.equivalent? do new_edit1 = [{:eq, diff.left} | edit1] new_edit2 = [{:eq, diff.right} | edit2] follow_snake({y + 1, rest1, rest2, {new_edit1, new_edit2, post_env}}) else {:cont, path} end end defp follow_snake({_y, [], [], edits}) do {:done, edits} end defp follow_snake(path) do {:cont, path} end defp list_script_to_diff([], [], equivalent?, left, right, env) do diff = %__MODULE__{ equivalent?: equivalent?, left: Enum.reverse(left), right: Enum.reverse(right) } {diff, env} end defp list_script_to_diff( [{:del, elem1} | rest1], [{:ins, elem2} | rest2], equivalent?, left, right, env ) do {diff, env} = diff(elem1, elem2, env) equivalent? = equivalent? and diff.equivalent? list_script_to_diff(rest1, rest2, equivalent?, [diff.left | left], [diff.right | right], env) end defp list_script_to_diff([{:del, elem1} | rest1], rest2, _, left, right, env) do diff_left = elem1 |> maybe_escape(env) |> update_diff_meta(true) list_script_to_diff(rest1, rest2, false, [diff_left | left], right, env) end defp list_script_to_diff(rest1, [{:ins, elem2} | rest2], _, left, right, env) do diff_right = elem2 |> escape() |> update_diff_meta(true) list_script_to_diff(rest1, rest2, false, left, [diff_right | right], env) end defp list_script_to_diff( [{:eq, elem1} | rest1], [{:eq, elem2} | rest2], equivalent?, left, right, env ) do list_script_to_diff(rest1, rest2, equivalent?, [elem1 | left], [elem2 | right], env) end # Maps # Compare items based on the keys of `left_items` and add the `:diff` meta to # the element that it wasn't able to compare. defp diff_map(left_items, right, struct1, struct2, env) do {equivalent?, left, right, env} = diff_map_by_key(left_items, right, env) left = build_map_or_struct(left, struct1) right = build_map_or_struct(right, struct2) {%__MODULE__{equivalent?: equivalent?, left: left, right: right}, env} end defp diff_map_by_key(items, right, env) do {acc_equivalent?, acc_left, acc_right, pending_left, pending_right, env} = Enum.reduce(items, {true, [], [], [], right, env}, fn {left_key, left_value}, {acc_equivalent?, acc_left, acc_right, pending_left, pending_right, env} -> right_key = literal_key(left_key, env) case pending_right do %{^right_key => right_value} -> pending_right = Map.delete(pending_right, right_key) {diff, env} = diff(left_value, right_value, env) acc_equivalent? = acc_equivalent? and diff.equivalent? acc_left = [{maybe_escape(left_key, env), diff.left} | acc_left] acc_right = [{escape(right_key), diff.right} | acc_right] {acc_equivalent?, acc_left, acc_right, pending_left, pending_right, env} %{} -> pair = {maybe_escape(left_key, env), maybe_escape(left_value, env)} pair_diff = update_diff_meta(pair, true) {false, acc_left, acc_right, [pair_diff | pending_left], pending_right, env} end end) # It may be a struct, so make sure we convert it to a list before calling Enum pending_right = Map.to_list(pending_right) {pending_right, equivalent?} = if env.context == :match do {Enum.map(pending_right, &escape_pair/1), acc_equivalent?} else pending_right = Enum.map(pending_right, &(&1 |> escape_pair() |> update_diff_meta(true))) {pending_right, acc_equivalent? and pending_right == []} end left = Enum.sort(acc_left) ++ Enum.sort(pending_left) right = Enum.sort(acc_right) ++ Enum.sort(pending_right) {equivalent?, left, right, env} end defp literal_key({:^, _, [var]}, %{pins: pins}) do identifier = var_context(var) %{^identifier => pin_value} = pins pin_value end defp literal_key(literal, _env) do literal end # Structs defp diff_quoted_struct(kw, right, env) do struct1 = kw[:__struct__] left = load_struct(kw[:__struct__]) if left && Enum.all?(kw, fn {k, _} -> Map.has_key?(left, k) end) do with true <- Macro.quoted_literal?(kw), {eval_kw, []} <- safe_eval(kw) do diff_quoted_struct(struct!(left, eval_kw), kw, right, struct1, env) else _ -> diff_map(kw, right, struct1, maybe_struct(right), env) end else diff_map(kw, right, nil, maybe_struct(right), env) end end defp diff_quoted_struct(left, kw, %struct2{} = right, struct1, env) do diff_struct(left, kw, right, struct1, struct2, env) end defp diff_quoted_struct(_left, kw, right, struct1, env) do diff_map(kw, right, struct1, nil, env) end defp diff_struct(left, kw, right, struct1, struct2, env) do with true <- Inspect.impl_for(left) not in [Inspect.Any, Inspect.Map], {:ok, inspect_left} <- safe_inspect(left), {:ok, inspect_right} <- safe_inspect(right) do if inspect_left != inspect_right do diff_string(inspect_left, inspect_right, :none, env) else # If they are equivalent, still use their inspected form case diff_map(kw, right, struct1, struct2, env) do {%{equivalent?: true}, ctx} -> left = block_diff_container([inspect_left], :none) right = block_diff_container([inspect_right], :none) {%__MODULE__{equivalent?: true, left: left, right: right}, ctx} diff_ctx -> diff_ctx end end else _ -> diff_map(kw, right, struct1, struct2, env) end end defp load_struct(struct) do if is_atom(struct) and struct != nil and Code.ensure_loaded?(struct) and function_exported?(struct, :__struct__, 0) do struct.__struct__ end end defp maybe_struct(%name{}), do: name defp maybe_struct(_), do: nil defp build_map_or_struct(items, nil) do {:%{}, [], items} end defp build_map_or_struct(items, _struct) do {struct, items} = pop_struct(items, []) {:%, [], [struct, {:%{}, [], items}]} end defp pop_struct([{:__block__, meta, [{:__struct__, struct}]} | tail], acc), do: {{:__block__, meta, [struct]}, Enum.reverse(acc, tail)} defp pop_struct([{:__struct__, struct} | tail], acc), do: {struct, Enum.reverse(acc, tail)} defp pop_struct([head | rest], acc), do: pop_struct(rest, [head | acc]) # Strings defp diff_string(left, right, delimiter, env) do {escaped_left, _} = Code.Identifier.escape(left, delimiter) {escaped_right, _} = Code.Identifier.escape(right, delimiter) left = IO.iodata_to_binary(escaped_left) right = IO.iodata_to_binary(escaped_right) diff = cond do diff_string?(left, right) -> String.myers_difference(left, right) |> string_script_to_diff(delimiter, true, [], []) left == right -> string_script_to_diff([eq: left], delimiter, true, [], []) true -> string_script_to_diff([del: left, ins: right], delimiter, true, [], []) end {diff, env} end # Concat all the literals on `left` and split `right` based on the size of # that, comparing them and the remaining AST from `left` and the remaining # string from `right`. defp diff_string_concat(left, right, env) do {parsed_left, quoted, indexes, parsed_left_length} = parse_string(left) diff_string_concat(parsed_left, quoted, indexes, parsed_left_length, right, env) end defp diff_string_concat(left, nil, indexes, _left_length, right, env) do {parsed_diff, parsed_post_env} = diff_string(left, right, ?", env) left_diff = rebuild_concat_string(parsed_diff.left, nil, indexes) diff = %__MODULE__{parsed_diff | left: left_diff} {diff, parsed_post_env} end defp diff_string_concat(left, quoted, indexes, left_length, right, env) do {parsed_right, continue_right} = String.split_at(right, left_length) {parsed_diff, parsed_post_env} = diff_string(left, parsed_right, ?", env) {quoted_diff, quoted_post_env} = diff(quoted, continue_right, parsed_post_env) diff = merge_diff(parsed_diff, quoted_diff, fn left1, left2, right1, right2 -> new_left = rebuild_concat_string(left1, left2, indexes) new_right = rebuild_split_strings(right1, right2) {new_left, new_right} end) {diff, quoted_post_env} end defp diff_string?(left, right) do String.bag_distance(left, right) > 0.4 end defp parse_string({:<>, _, [literal, rest]}) when is_binary(literal) do {parsed, quoted, indexes, parsed_length} = parse_string(rest) literal_length = String.length(literal) length = literal_length + parsed_length {literal <> parsed, quoted, [literal_length | indexes], length} end defp parse_string(literal) when is_binary(literal) do {literal, nil, [], String.length(literal)} end defp parse_string(pattern) do {"", pattern, [], 0} end defp rebuild_split_strings(left, "") do left end defp rebuild_split_strings({:__block__, meta, left_list}, {:__block__, _, right_list}) do {:__block__, meta, left_list ++ right_list} end defp rebuild_split_strings({:__block__, meta, left_list}, right) do {:__block__, meta, left_list ++ [right]} end defp rebuild_concat_string(literal, nil, []) do literal end defp rebuild_concat_string(_literal, quoted, []) do quoted end defp rebuild_concat_string(literal, quoted, [index | rest]) do {next, continue} = next_concat_result(literal, index) rebuilt_right = rebuild_concat_string(continue, quoted, rest) {:<>, [], [next, rebuilt_right]} end defp next_concat_result({:__block__, [{:diff_container, _} | _] = meta, list}, index) do {next, continue} = next_concat_result(list, index) {{:__block__, meta, next}, {:__block__, meta, continue}} end defp next_concat_result([head | tail], index) do {string, diff_meta?} = extract_diff_meta(head) length = String.length(string) cond do length > index -> {next, continue} = String.split_at(string, index) next = [update_diff_meta(next, diff_meta?)] continue = [update_diff_meta(continue, diff_meta?) | tail] {next, continue} length < index -> {next, continue} = next_concat_result(tail, index - length) {[head | next], continue} true -> {[head], tail} end end defp block_diff_container(contents, :none), do: {:__block__, [], contents} defp block_diff_container(contents, container), do: {:__block__, [diff_container: container], contents} defp string_script_to_diff([], delimiter, equivalent?, left, right) do left = block_diff_container(Enum.reverse(left), delimiter) right = block_diff_container(Enum.reverse(right), delimiter) %__MODULE__{equivalent?: equivalent?, left: left, right: right} end defp string_script_to_diff([{:eq, string} | tail], delimiter, equivalent?, left, right) do string_script_to_diff(tail, delimiter, equivalent?, [string | left], [string | right]) end defp string_script_to_diff([{:del, string} | tail], delimiter, _equivalent?, left, right) do string_script_to_diff(tail, delimiter, false, [update_diff_meta(string, true) | left], right) end defp string_script_to_diff([{:ins, string} | tail], delimiter, _equivalent?, left, right) do string_script_to_diff(tail, delimiter, false, left, [update_diff_meta(string, true) | right]) end # Numbers defp diff_number(left, right, env) do diff_string(inspect(left), inspect(right), :none, env) end # Algebra @doc """ Converts a diff to an algebra document. """ def to_algebra(quoted, diff_wrapper) do wrap_on_diff(quoted, &safe_to_algebra/2, diff_wrapper) end defp safe_to_algebra({:__block__, meta, list}, diff_wrapper) do content_docs = Enum.map(list, &string_to_algebra(&1, diff_wrapper)) if container = meta[:diff_container] do delimiter = to_string([container]) Algebra.concat([delimiter] ++ content_docs ++ [delimiter]) else Algebra.concat(content_docs) end end defp safe_to_algebra(list, diff_wrapper) when is_list(list) do container_to_algebra("[", list, "]", diff_wrapper, select_list_item_algebra(list)) end defp safe_to_algebra({op, _, [left, right]}, diff_wrapper) when op in [:<>, :++, :|, :when, :and, :or] do to_algebra(left, diff_wrapper) |> Algebra.concat(" #{op} ") |> Algebra.concat(to_algebra(right, diff_wrapper)) end defp safe_to_algebra({:{}, _, args}, diff_wrapper) do container_to_algebra("{", args, "}", diff_wrapper, &to_algebra/2) end defp safe_to_algebra({a, b}, diff_wrapper) do container_to_algebra("{", [a, b], "}", diff_wrapper, &to_algebra/2) end defp safe_to_algebra({:%, _, [{:_, _, _}, {:%{}, _, list}]}, diff_wrapper) do open = Algebra.concat(["%", "_", "{"]) container_to_algebra(open, list, "}", diff_wrapper, select_map_item_to_algebra(list)) end defp safe_to_algebra({:%, _, [struct, {:%{}, _, list}]}, diff_wrapper) do open = Algebra.concat(["%", struct_to_algebra(struct, diff_wrapper), "{"]) container_to_algebra(open, list, "}", diff_wrapper, select_map_item_to_algebra(list)) end defp safe_to_algebra({:%{}, _, list}, diff_wrapper) do container_to_algebra("%{", list, "}", diff_wrapper, select_map_item_to_algebra(list)) end defp safe_to_algebra({_, _, _} = quoted, _diff_wrapper) do Macro.to_string(quoted) end defp safe_to_algebra({escaped}, _diff_wrapper) do inspect(escaped) end defp safe_to_algebra(literal, _diff_wrapper) do inspect(literal) end def string_to_algebra(quoted, diff_wrapper) do wrap_on_diff(quoted, &safe_string_to_algebra/2, diff_wrapper) end def safe_string_to_algebra(literal, _diff_wrapper) do literal end defp keyword_to_algebra(quoted, diff_wrapper) do wrap_on_diff(quoted, &safe_keyword_to_algebra/2, diff_wrapper) end defp safe_keyword_to_algebra({:{}, _, [key, value]}, diff_wrapper) do keyword_to_algebra({key, value}, diff_wrapper) end defp safe_keyword_to_algebra({key, value}, diff_wrapper) do key_to_algebra(key, diff_wrapper) |> Algebra.concat(" ") |> Algebra.concat(to_algebra(value, diff_wrapper)) end defp key_to_algebra(quoted, diff_wrapper) do wrap_on_diff(quoted, &safe_key_to_algebra/2, diff_wrapper) end defp safe_key_to_algebra(key, _diff_wrapper) do Identifier.inspect_as_key(key) end defp map_item_to_algebra(quoted, diff_wrapper) do wrap_on_diff(quoted, &safe_map_item_to_algebra/2, diff_wrapper) end defp safe_map_item_to_algebra({:{}, _, [key, value]}, diff_wrapper) do safe_map_item_to_algebra({key, value}, diff_wrapper) end defp safe_map_item_to_algebra({key, value}, diff_wrapper) do to_algebra(key, diff_wrapper) |> Algebra.concat(" => ") |> Algebra.concat(to_algebra(value, diff_wrapper)) end defp container_to_algebra(open, list, close, diff_wrapper, item_to_algebra) do docs = list |> Enum.map(&item_to_algebra.(&1, diff_wrapper)) |> Algebra.fold_doc(&join_docs/2) open |> Algebra.glue("", docs) |> Algebra.nest(2) |> Algebra.glue("", close) |> Algebra.group() end defp join_docs(doc1, doc2) do doc1 |> Algebra.concat(",") |> Algebra.glue(doc2) end defp struct_to_algebra(quoted, diff_wrapper) do wrap_on_diff(quoted, &safe_struct_to_algebra/2, diff_wrapper) end defp safe_struct_to_algebra(name, _diff_wrapper) do Code.Identifier.inspect_as_atom(name) end defp select_list_item_algebra(list) do short? = Enum.all?(list, &keyword?/1) if short?, do: &keyword_to_algebra/2, else: &to_algebra/2 end defp select_map_item_to_algebra(list) do short? = Enum.all?(list, &keyword?/1) if short?, do: &keyword_to_algebra/2, else: &map_item_to_algebra/2 end defp wrap_on_diff(quoted, fun, wrapper) do case extract_diff_meta(quoted) do {expr, true} -> fun.(expr, & &1) |> wrapper.() {expr, false} -> fun.(expr, wrapper) end end # Diff helpers # The left side is only escaped if it is a value defp maybe_escape(other, %{context: :match}), do: other defp maybe_escape(other, _env), do: escape(other) # We escape it by wrapping it in one element tuple which is not valid AST defp escape(other) when is_list(other) or is_tuple(other), do: {other} defp escape(other), do: other defp escape_pair({key, value}), do: {escape(key), escape(value)} defp unescape({other}), do: other defp unescape(other), do: other defp merge_diff(%__MODULE__{} = result1, %__MODULE__{} = result2, fun) do {left, right} = fun.(result1.left, result2.left, result1.right, result2.right) %__MODULE__{ equivalent?: result1.equivalent? && result2.equivalent?, left: left, right: right } end defp update_diff_meta({left, meta, right}, false) when is_list(meta), do: {left, Keyword.delete(meta, :diff), right} defp update_diff_meta({left, meta, right}, true) when is_list(meta), do: {left, Keyword.put(meta, :diff, true), right} defp update_diff_meta(literal, false), do: literal defp update_diff_meta(literal, true), do: {:__block__, [diff: true], [literal]} defp extract_diff_meta({:__block__, [diff: true], [literal]}), do: {literal, true} defp extract_diff_meta({left, meta, right}), do: {{left, meta, right}, !!meta[:diff]} defp extract_diff_meta(other), do: {other, false} defp keyword?(quoted) do {pair, _} = extract_diff_meta(quoted) safe_keyword?(pair) end defp safe_keyword?({key, _value}), do: key_is_atom?(key) defp safe_keyword?({:{}, _meta, [key, _value]}), do: key_is_atom?(key) defp safe_keyword?(_other), do: false defp key_is_atom?(quoted) do {key, _} = extract_diff_meta(quoted) is_atom(key) end defp var_context({name, meta, context}) do {name, meta[:counter] || context} end defp safe_eval(expr) do Code.eval_quoted(expr, []) rescue _ -> :error end defp safe_inspect(value) do {:ok, inspect(value, safe: false)} rescue _ -> :error end end