changed
.formatter.exs
|
@@ -10,7 10,8 @@ locals_without_parens = [
|
10
10
|
"{mix,.formatter}.exs",
|
11
11
|
"{config,lib}/**/*.{ex,exs}",
|
12
12
|
"test/**/*.{ex,exs}",
|
13
|
- "test_integration/**/*.{ex,exs}"
|
13
|
"test_integration/**/*.{ex,exs}",
|
14
|
"examples/*.{ex,exs}"
|
14
15
|
],
|
15
16
|
locals_without_parens: locals_without_parens,
|
16
17
|
export: [locals_without_parens: locals_without_parens]
|
changed
CHANGELOG.md
|
@@ -1,52 1,69 @@
|
1
1
|
# Changelog
|
2
2
|
|
3
|
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
3
|
This format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
4
|
|
5
|
## Unreleased
|
6
|
|
7
|
### Changed
|
8
|
|
9
|
* Pattern generation improvements:
|
10
|
* Multi-line strings will now generate both a heredoc and a single-line option.
|
11
|
|
12
|
### Removed
|
13
|
|
14
|
* No longer depend on `libgraph`.
|
4
15
|
|
5
16
|
## v0.3.0 (2023-04-10)
|
6
17
|
|
7
|
- It is now recommended to use Elixir v1.14.4 or later.
|
18
|
It is recommended to now use Elixir v1.14.4 or later.
|
8
19
|
|
9
|
- ### Breaking changes
|
20
|
### Added
|
10
21
|
|
11
|
- * [Core] `auto_assert` now uses `<-` when comparing against falsy values instead of `==`. Support for comparisons using `==` has been removed.
|
12
|
-
|
13
|
- ### Enhancements
|
14
|
-
|
15
|
- * [Core] Add three new auto-assertions:
|
22
|
* Add three new auto-assertions:
|
16
23
|
* [`auto_assert_raise`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_raise/3)
|
17
24
|
* [`auto_assert_receive`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_receive/2)
|
18
25
|
* [`auto_assert_received`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_received/1)
|
19
|
- * [Core] Add a `:force_update` option that forces re-generating assertion patterns even when they succeed. See the [Options documentation](https://hexdocs.pm/mneme/Mneme.html#module-options) for more info.
|
20
|
- * [Core] Existing auto-assertions will now run when `Mneme.start()` is not called, but new or failing auto-assertions will fail without prompting ([#32](https://github.com/zachallaun/mneme/issues/32)).
|
21
|
- * [Core] Pattern generation improvements:
|
26
|
* Add a `:force_update` option that forces re-generating assertion patterns even when they succeed. See the [Options documentation](https://hexdocs.pm/mneme/Mneme.html#module-options) for more info.
|
27
|
* Prompts will now show options that were overridden from the defaults.
|
28
|
* Mneme now prints a one-line summary at the end of the test run.
|
29
|
|
30
|
### Changed
|
31
|
|
32
|
* For falsy values, `auto_assert` now generates `<-` pattern matches instead of `==` value comparisons, which have been removed.
|
33
|
* Existing auto-assertions will now run when `Mneme.start()` is not called, but new or failing auto-assertions will fail without prompting ([#32](https://github.com/zachallaun/mneme/issues/32)).
|
34
|
* Pattern generation improvements:
|
22
35
|
* Ranges now use range syntax like `1..10` and `1..10//2` instead of generating a `%Range{}` struct.
|
23
|
- * [CLI] Prompts will now show options that were overridden from the defaults.
|
24
|
- * [CLI] Mneme now prints a one-line summary at the end of the test run.
|
25
36
|
|
26
|
- ### Fixes
|
37
|
### Removed
|
27
38
|
|
28
|
- * [Core] Fix a configuration precedence bug that caused options set in application config to always override module, describe, or test options.
|
29
|
- * [Core] Fix a compatibility issue with Ecto ~> 3.9.4 ([#34](https://github.com/zachallaun/mneme/issues/34)).
|
30
|
- * [CLI] Fix a confusing diff result that could occur with some binary operations ([#11](https://github.com/zachallaun/mneme/issues/11)).
|
31
|
- * [CLI] Preceding comments are no longer shown in diffs ([#26](https://github.com/zachallaun/mneme/issues/26)).
|
32
|
- * [CLI] Fix a number of diffing errors related to structs.
|
39
|
* **Breaking:** `auto_assert` no longer supports value comparisons using `==`.
|
40
|
|
41
|
### Fixed
|
42
|
|
43
|
* Fix a configuration precedence bug that caused options set in application config to always override module, describe, or test options.
|
44
|
* Fix a compatibility issue with Ecto ~> 3.9.4 ([#34](https://github.com/zachallaun/mneme/issues/34)).
|
45
|
* Fix a confusing diff result that could occur with some binary operations ([#11](https://github.com/zachallaun/mneme/issues/11)).
|
46
|
* Preceding comments are no longer shown in diffs ([#26](https://github.com/zachallaun/mneme/issues/26)).
|
47
|
* Fix a number of diffing errors related to structs.
|
33
48
|
|
34
49
|
## v0.2.7 (2023-03-29)
|
35
50
|
|
36
|
- ### Fixes
|
51
|
### Fixed
|
37
52
|
|
38
53
|
* Fix a crash related to escaped string interpolation characters ([#29](https://github.com/zachallaun/mneme/issues/29)).
|
39
54
|
|
40
55
|
## v0.2.6 (2023-03-27)
|
41
56
|
|
42
|
- ### Enhancements
|
57
|
### Added
|
43
58
|
|
44
59
|
* Auto-assertion prompts can now be skipped (`s`) in addition to accepted (`y`) or rejected (`n`). This allows the test clause to continue so that later assertions might be run, but fails the test run once the suite finishes.
|
45
|
- * Updated formatting for semantic diffs:
|
46
|
- * Diffs will be displayed side-by-side if terminal width allows. To always display diffs stacked, use the `diff_style: :stacked` option; see the "Configuration" section of the `Mneme` module doc for more.
|
47
|
- * Both `:stacked` and `:side_by_side` diffs have updated, more consistent formatting.
|
60
|
* Semantic diffs will now be displayed side-by-side if terminal width allows. To always display diffs stacked, use the `diff_style: :stacked` option; see the "Configuration" section of the `Mneme` module doc for more.
|
48
61
|
|
49
|
- ### Fixes
|
62
|
### Changed
|
63
|
|
64
|
* Semantic diff formatting has been improved for clarity.
|
65
|
|
66
|
### Fixed
|
50
67
|
|
51
68
|
* Don't overwrite test files if their content changes after starting the test run ([#23](https://github.com/zachallaun/mneme/issues/23)).
|
52
69
|
* Fix a crash that occurred when a value contained nested strings with newlines, e.g. `{:ok, "hello\nworld"}` ([#25](https://github.com/zachallaun/mneme/issues/25)).
|
|
@@ -54,13 71,13 @@ It is now recommended to use Elixir v1.14.4 or later.
|
54
71
|
|
55
72
|
## v0.2.4, v0.2.5 (2023-03-25)
|
56
73
|
|
57
|
- ### Fixes
|
74
|
### Fixed
|
58
75
|
|
59
76
|
* Remove unnecessary files from Hex package. This cuts the package size down drastically.
|
60
77
|
|
61
78
|
## v0.2.3 (2023-03-25)
|
62
79
|
|
63
|
- ### Fixes
|
80
|
### Fixed
|
64
81
|
|
65
82
|
* Fix diffing for certain sigil variations.
|
66
83
|
* Fix `dbg`-related error when running `MIX_ENV=test iex -S mix` ([#20](https://github.com/zachallaun/mneme/issues/20)).
|
|
@@ -68,27 85,27 @@ It is now recommended to use Elixir v1.14.4 or later.
|
68
85
|
|
69
86
|
## v0.2.2 (2023-03-20)
|
70
87
|
|
71
|
- ### Fixes
|
88
|
### Fixed
|
72
89
|
|
73
90
|
* Disable a semantic diffing optimization that caused poor diff results in certain cases, usually manifesting as incorrect branches being compared.
|
74
91
|
|
75
92
|
## v0.2.1 (2023-03-19)
|
76
93
|
|
77
|
- ### Enhancements
|
94
|
### Changed
|
78
95
|
|
79
96
|
* More consistent formatting between `:semantic` and `:text` diffs.
|
80
97
|
|
81
98
|
## v0.2.0 (2023-03-18)
|
82
99
|
|
83
|
- ### Breaking
|
84
|
-
|
85
|
- * Mneme now requires Elixir v1.14 or later.
|
86
|
-
|
87
|
- ### Enhancements
|
100
|
### Added
|
88
101
|
|
89
102
|
* Adds semantic diffs which selectively highlight only meaningful changes when updating an assertion. This can be disabled with the `diff: :text` option; see the "Configuration" section of the `Mneme` module doc for more.
|
90
103
|
|
91
|
- ### Fixes
|
104
|
### Changed
|
105
|
|
106
|
* **Breaking:** Mneme now requires Elixir v1.14 or later.
|
107
|
|
108
|
### Fixed
|
92
109
|
|
93
110
|
* Invalid options now cause a warning instead of crashing test process.
|
94
111
|
* Internal errors now show an error instead of crashing test process.
|
|
@@ -96,41 113,41 @@ It is now recommended to use Elixir v1.14.4 or later.
|
96
113
|
|
97
114
|
## v0.1.6 (2023-03-04)
|
98
115
|
|
99
|
- ### Enhancements
|
116
|
### Changed
|
100
117
|
|
101
118
|
* Improved compile-time error message when `auto_assert` is used outside of a `test` block ([#9](https://github.com/zachallaun/mneme/issues/9)).
|
102
119
|
|
103
120
|
## v0.1.5 (2023-02-25)
|
104
121
|
|
105
|
- ### Enhancements
|
122
|
### Changed
|
106
123
|
|
107
124
|
* More consistent handling of charlists: lists of integers will now generate themselves as a pattern as well as a charlist if the list is ASCII printable ([#6](https://github.com/zachallaun/mneme/issues/6)).
|
108
125
|
|
109
126
|
## v0.1.4 (2023-02-23)
|
110
127
|
|
111
|
- ### Fixes
|
128
|
### Fixed
|
112
129
|
|
113
130
|
* Fix a bug that could cause `auto_assert` expressions to revert to the previous value when using `Mneme.start(restart: true)` ([#7](https://github.com/zachallaun/mneme/issues/7)).
|
114
131
|
|
115
132
|
## v0.1.3 (2023-02-22)
|
116
133
|
|
117
|
- ### Enhancements
|
134
|
### Added
|
118
135
|
|
119
136
|
* Add a `:default_pattern` configuration option for auto-assertions which controls the pattern that should be selected by default when prompted.
|
120
137
|
|
121
|
- ### Fixes
|
138
|
### Fixed
|
122
139
|
|
123
140
|
* When converting an auto-assertion to an ExUnit assertion, select the identical pattern when the `:default_pattern` is `:infer` (set by default).
|
124
141
|
|
125
142
|
## v0.1.2 (2023-02-21)
|
126
143
|
|
127
|
- ### Enhancements
|
144
|
### Added
|
128
145
|
|
129
146
|
* Add a `:restart` option to `Mneme.start/1` to restart Mneme if called multiple times.
|
130
147
|
|
131
148
|
## v0.1.1 (2023-02-20)
|
132
149
|
|
133
|
- ### Enhancements
|
150
|
### Changed
|
134
151
|
|
135
152
|
* Dramatically reduce the performance gap between `auto_assert` and ExUnit's `assert`.
|
changed
hex_metadata.config
|
@@ -6,15 6,16 @@
|
6
6
|
[<<"lib">>,<<"lib/mneme">>,<<"lib/mneme/server.ex">>,<<"lib/mneme/diff.ex">>,
|
7
7
|
<<"lib/mneme/server">>,<<"lib/mneme/server/ex_unit_formatter.ex">>,
|
8
8
|
<<"lib/mneme/diff">>,<<"lib/mneme/diff/syntax_node.ex">>,
|
9
|
- <<"lib/mneme/diff/delta.ex">>,<<"lib/mneme/diff/formatter.ex">>,
|
10
|
- <<"lib/mneme/diff/ast.ex">>,<<"lib/mneme/diff/pathfinding.ex">>,
|
11
|
- <<"lib/mneme/diff/zipper.ex">>,<<"lib/mneme/options.ex">>,
|
9
|
<<"lib/mneme/diff/priority_queue.ex">>,<<"lib/mneme/diff/delta.ex">>,
|
10
|
<<"lib/mneme/diff/formatter.ex">>,<<"lib/mneme/diff/ast.ex">>,
|
11
|
<<"lib/mneme/diff/pathfinding.ex">>,<<"lib/mneme/diff/zipper.ex">>,
|
12
|
<<"lib/mneme/utils.ex">>,<<"lib/mneme/options.ex">>,
|
12
13
|
<<"lib/mneme/assertion.ex">>,<<"lib/mneme/prompter">>,
|
13
14
|
<<"lib/mneme/prompter/terminal.ex">>,<<"lib/mneme/prompter.ex">>,
|
14
15
|
<<"lib/mneme/patcher.ex">>,<<"lib/mneme/errors.ex">>,
|
15
16
|
<<"lib/mneme/assertion">>,<<"lib/mneme/assertion/pattern_builder.ex">>,
|
16
|
- <<"lib/mneme.ex">>,<<"priv">>,<<".formatter.exs">>,<<"mix.exs">>,
|
17
|
- <<"README.md">>,<<"CHANGELOG.md">>]}.
|
17
|
<<"lib/mneme/assertion/pattern.ex">>,<<"lib/mneme.ex">>,<<"priv">>,
|
18
|
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}.
|
18
19
|
{<<"licenses">>,[<<"MIT">>]}.
|
19
20
|
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/zachallaun/mneme">>}]}.
|
20
21
|
{<<"name">>,<<"mneme">>}.
|
|
@@ -38,10 39,5 @@
|
38
39
|
{<<"name">>,<<"rewrite">>},
|
39
40
|
{<<"optional">>,false},
|
40
41
|
{<<"repository">>,<<"hexpm">>},
|
41
|
- {<<"requirement">>,<<"~> 0.6.0">>}],
|
42
|
- [{<<"app">>,<<"libgraph">>},
|
43
|
- {<<"name">>,<<"libgraph">>},
|
44
|
- {<<"optional">>,false},
|
45
|
- {<<"repository">>,<<"hexpm">>},
|
46
|
- {<<"requirement">>,<<"~> 0.16.0">>}]]}.
|
47
|
- {<<"version">>,<<"0.3.0">>}.
|
42
|
{<<"requirement">>,<<"~> 0.6.0">>}]]}.
|
43
|
{<<"version">>,<<"0.3.1">>}.
|
changed
lib/mneme/assertion.ex
|
@@ -2,6 2,7 @@ defmodule Mneme.Assertion do
|
2
2
|
@moduledoc false
|
3
3
|
|
4
4
|
alias __MODULE__
|
5
|
alias Mneme.Assertion.Pattern
|
5
6
|
alias Mneme.Assertion.PatternBuilder
|
6
7
|
|
7
8
|
defstruct [
|
|
@@ -23,7 24,7 @@ defmodule Mneme.Assertion do
|
23
24
|
macro_ast: Macro.t(),
|
24
25
|
rich_ast: Macro.t(),
|
25
26
|
code: Macro.t(),
|
26
|
- patterns: [pattern],
|
27
|
patterns: [Pattern.t()],
|
27
28
|
pattern_idx: non_neg_integer(),
|
28
29
|
context: context
|
29
30
|
}
|
|
@@ -34,8 35,6 @@ defmodule Mneme.Assertion do
|
34
35
|
| :auto_assert_receive
|
35
36
|
| :auto_assert_received
|
36
37
|
|
37
|
- @type pattern :: {match :: Macro.t(), guard :: Macro.t() | nil, notes :: [String.t()]}
|
38
|
-
|
39
38
|
@type context :: %{
|
40
39
|
file: String.t(),
|
41
40
|
line: non_neg_integer(),
|
|
@@ -239,7 238,7 @@ defmodule Mneme.Assertion do
|
239
238
|
end
|
240
239
|
|
241
240
|
defp build_and_select_raise(%{value: %exception{} = e, macro_ast: macro_ast}, default) do
|
242
|
- patterns = [{{exception, nil}, nil, []}, {{exception, Exception.message(e)}, nil, []}]
|
241
|
patterns = [Pattern.new({exception, nil}), Pattern.new({exception, Exception.message(e)})]
|
243
242
|
|
244
243
|
case {default, macro_ast} do
|
245
244
|
{:infer, {_, _, [_, _, _]}} -> {patterns, 1}
|
|
@@ -273,12 272,8 @@ defmodule Mneme.Assertion do
|
273
272
|
|> Enum.map(&simplify_expr/1)
|
274
273
|
|
275
274
|
PatternBuilder.to_patterns(value, assertion.context)
|
276
|
- |> Enum.split_while(fn
|
277
|
- {pattern_expr, pattern_guard, _} ->
|
278
|
- !(simplify_expr(pattern_expr) == expr && simplify_expr(pattern_guard) == guard)
|
279
|
-
|
280
|
- _ ->
|
281
|
- true
|
275
|
|> Enum.split_while(fn %Pattern{expr: pattern_expr, guard: pattern_guard} ->
|
276
|
!(simplify_expr(pattern_expr) == expr && simplify_expr(pattern_guard) == guard)
|
282
277
|
end)
|
283
278
|
|> case do
|
284
279
|
{patterns, []} -> {patterns, 0}
|
|
@@ -331,11 326,6 @@ defmodule Mneme.Assertion do
|
331
326
|
"""
|
332
327
|
def pattern(%Assertion{pattern_idx: idx, patterns: patterns}), do: Enum.at(patterns, idx)
|
333
328
|
|
334
|
- @doc """
|
335
|
- Returns any notes associated with the current pattern.
|
336
|
- """
|
337
|
- def notes(%Assertion{} = assertion), do: assertion |> pattern() |> elem(2)
|
338
|
-
|
339
329
|
@doc """
|
340
330
|
Check whether the assertion struct represents the given AST node.
|
341
331
|
"""
|
|
@@ -350,12 340,12 @@ defmodule Mneme.Assertion do
|
350
340
|
Generates assertion code for the given target.
|
351
341
|
"""
|
352
342
|
def to_code(%Assertion{kind: :auto_assert_raise} = assertion, target) do
|
353
|
- {expr, _, _} = pattern(assertion)
|
343
|
%Pattern{expr: expr} = pattern(assertion)
|
354
344
|
build_call(target, assertion, {expr, nil})
|
355
345
|
end
|
356
346
|
|
357
347
|
def to_code(%Assertion{rich_ast: ast} = assertion, target) do
|
358
|
- {expr, guard, _} = pattern(assertion)
|
348
|
%Pattern{expr: expr, guard: guard} = pattern(assertion)
|
359
349
|
build_call(target, assertion, {block_with_line(expr, meta(ast)), guard})
|
360
350
|
end
|
361
351
|
|
|
@@ -613,8 603,6 @@ defmodule Mneme.Assertion do
|
613
603
|
string
|
614
604
|
|> String.replace("\n", "\\n")
|
615
605
|
|> String.replace("\#{", "\\\#{")
|
616
|
-
|
617
|
- # |> String.replace("\"", "\\\"")
|
618
606
|
end
|
619
607
|
|
620
608
|
defp meta({_, meta, _}), do: meta
|
added
lib/mneme/assertion/pattern.ex
|
@@ -0,0 1,21 @@
|
1
|
defmodule Mneme.Assertion.Pattern do
|
2
|
@moduledoc false
|
3
|
|
4
|
alias __MODULE__
|
5
|
|
6
|
@enforce_keys [:expr]
|
7
|
defstruct [:expr, guard: nil, notes: []]
|
8
|
|
9
|
@type t :: %Pattern{
|
10
|
expr: term(),
|
11
|
guard: Macro.t() | nil,
|
12
|
notes: [String.t()]
|
13
|
}
|
14
|
|
15
|
@doc """
|
16
|
Creates a new pattern.
|
17
|
"""
|
18
|
def new(expr, other_attrs \\ []) do
|
19
|
struct(Pattern, Keyword.put(other_attrs, :expr, expr))
|
20
|
end
|
21
|
end
|
changed
lib/mneme/assertion/pattern_builder.ex
|
@@ -2,16 2,18 @@ defmodule Mneme.Assertion.PatternBuilder do
|
2
2
|
@moduledoc false
|
3
3
|
|
4
4
|
alias Mneme.Assertion
|
5
|
alias Mneme.Assertion.Pattern
|
6
|
alias Mneme.Utils
|
5
7
|
|
6
8
|
@doc """
|
7
9
|
Builds pattern expressions from a runtime value.
|
8
10
|
"""
|
9
|
- @spec to_patterns(term(), Assertion.context()) :: [Assertion.pattern(), ...]
|
11
|
@spec to_patterns(term(), Assertion.context()) :: [Pattern.t(), ...]
|
10
12
|
def to_patterns(value, context) do
|
11
13
|
patterns = do_to_patterns(value, context)
|
12
14
|
|
13
15
|
case fetch_pinned(value, context) do
|
14
|
- {:ok, pin} -> [{pin, nil, []} | patterns]
|
16
|
{:ok, pin} -> [Pattern.new(pin) | patterns]
|
15
17
|
:error -> patterns
|
16
18
|
end
|
17
19
|
end
|
|
@@ -30,39 32,40 @@ defmodule Mneme.Assertion.PatternBuilder do
|
30
32
|
defp fetch_pinned(_, _), do: :error
|
31
33
|
|
32
34
|
defp do_to_patterns(int, context) when is_integer(int) do
|
33
|
- pattern = {{:__block__, with_meta([token: inspect(int)], context), [int]}, nil, []}
|
34
|
- [pattern]
|
35
|
{:__block__, with_meta([token: inspect(int)], context), [int]}
|
36
|
|> Pattern.new()
|
37
|
|> List.wrap()
|
35
38
|
end
|
36
39
|
|
37
|
- defp do_to_patterns(value, _context)
|
38
|
- when is_atom(value) or is_float(value) do
|
39
|
- [{value, nil, []}]
|
40
|
defp do_to_patterns(value, _context) when is_atom(value) or is_float(value) do
|
41
|
[Pattern.new(value)]
|
40
42
|
end
|
41
43
|
|
42
44
|
defp do_to_patterns(string, context) when is_binary(string) do
|
43
|
- block =
|
44
|
- cond do
|
45
|
- !String.printable?(string) ->
|
46
|
- {:<<>>, [], String.to_charlist(string)}
|
45
|
newlines = Utils.occurrences(string, ?\n)
|
47
46
|
|
48
|
- String.contains?(string, "\n") ->
|
49
|
- {:__block__, with_meta([delimiter: ~S(""")], context),
|
50
|
- [string |> escape() |> format_for_heredoc()]}
|
47
|
cond do
|
48
|
!String.printable?(string) ->
|
49
|
[Pattern.new({:<<>>, [], String.to_charlist(string)})]
|
51
50
|
|
52
|
- true ->
|
53
|
- {:__block__, with_meta([delimiter: ~S(")], context), [escape(string)]}
|
54
|
- end
|
51
|
newlines >= 2 ->
|
52
|
[heredoc_pattern(string, context), string_pattern(string, context)]
|
55
53
|
|
56
|
- [{block, nil, []}]
|
54
|
newlines >= 1 ->
|
55
|
[string_pattern(string, context), heredoc_pattern(string, context)]
|
56
|
|
57
|
true ->
|
58
|
[string_pattern(string, context)]
|
59
|
end
|
57
60
|
end
|
58
61
|
|
59
|
- defp do_to_patterns([], _), do: [{[], nil, []}]
|
62
|
defp do_to_patterns([], _), do: [Pattern.new([])]
|
60
63
|
|
61
64
|
defp do_to_patterns(list, context) when is_list(list) do
|
62
65
|
patterns = enum_to_patterns(list, context)
|
63
66
|
|
64
67
|
if List.ascii_printable?(list) do
|
65
|
- patterns [{list, nil, []}]
|
68
|
patterns [Pattern.new(list)]
|
66
69
|
else
|
67
70
|
patterns
|
68
71
|
end
|
|
@@ -72,20 75,19 @@ defmodule Mneme.Assertion.PatternBuilder do
|
72
75
|
tuple
|
73
76
|
|> Tuple.to_list()
|
74
77
|
|> enum_to_patterns(context)
|
75
|
- |> transform_patterns(&tuple_pattern/2, context)
|
78
|
|> Enum.map(&to_tuple_pattern(&1, context))
|
76
79
|
end
|
77
80
|
|
78
81
|
for {var_name, guard} <- [ref: :is_reference, pid: :is_pid, port: :is_port] do
|
79
82
|
defp do_to_patterns(value, context) when unquote(guard)(value) do
|
80
|
- guard_non_serializable(unquote(var_name), unquote(guard), value, context)
|
83
|
[guard_pattern(unquote(var_name), unquote(guard), value, context)]
|
81
84
|
end
|
82
85
|
end
|
83
86
|
|
84
87
|
for module <- [Range, Regex, DateTime, NaiveDateTime, Date, Time] do
|
85
88
|
defp do_to_patterns(%unquote(module){} = value, context) do
|
86
89
|
{call, meta, args} = value |> inspect() |> Code.string_to_quoted!()
|
87
|
- pattern = {{call, with_meta(meta, context), args}, nil, []}
|
88
|
- [pattern]
|
90
|
[Pattern.new({call, with_meta(meta, context), args})]
|
89
91
|
end
|
90
92
|
end
|
91
93
|
|
|
@@ -103,16 105,16 @@ defmodule Mneme.Assertion.PatternBuilder do
|
103
105
|
end
|
104
106
|
|
105
107
|
defp do_to_patterns(%{} = map, context) when map_size(map) == 0 do
|
106
|
- [map_pattern(context)]
|
108
|
[map_pattern([], context)]
|
107
109
|
end
|
108
110
|
|
109
111
|
defp do_to_patterns(%{} = map, context) do
|
110
112
|
patterns =
|
111
113
|
map
|
112
114
|
|> enum_to_patterns(context)
|
113
|
- |> transform_patterns(&map_pattern/2, context)
|
115
|
|> Enum.map(&to_map_pattern(&1, context))
|
114
116
|
|
115
|
- [map_pattern(context) | patterns]
|
117
|
[map_pattern([], context) | patterns]
|
116
118
|
end
|
117
119
|
|
118
120
|
defp struct_to_patterns(struct, map, context, extra_notes) do
|
|
@@ -121,15 123,7 @@ defmodule Mneme.Assertion.PatternBuilder do
|
121
123
|
map
|
122
124
|
|> Map.filter(fn {k, v} -> v != Map.get(empty, k) end)
|
123
125
|
|> to_patterns(context)
|
124
|
- |> transform_patterns(&struct_pattern(struct, &1, &2, extra_notes), context)
|
125
|
- end
|
126
|
-
|
127
|
- defp format_for_heredoc(string) when is_binary(string) do
|
128
|
- if String.ends_with?(string, "\n") do
|
129
|
- string
|
130
|
- else
|
131
|
- string <> "\\\n"
|
132
|
- end
|
126
|
|> Enum.map(&to_struct_pattern(struct, &1, context, extra_notes))
|
133
127
|
end
|
134
128
|
|
135
129
|
defp enum_to_patterns(values, context) do
|
|
@@ -169,48 163,47 @@ defmodule Mneme.Assertion.PatternBuilder do
|
169
163
|
|
170
164
|
defp combine_patterns(patterns, context) do
|
171
165
|
{exprs, {guard, notes}} =
|
172
|
- Enum.map_reduce(patterns, {nil, []}, fn {expr, g1, n1}, {g2, n2} ->
|
173
|
- {expr, {combine_guards(g1, g2, context), n1 n2}}
|
174
|
- end)
|
166
|
Enum.map_reduce(
|
167
|
patterns,
|
168
|
{nil, []},
|
169
|
fn %Pattern{expr: expr, guard: g1, notes: n1}, {g2, n2} ->
|
170
|
{expr, {combine_guards(g1, g2, context), n1 n2}}
|
171
|
end
|
172
|
)
|
175
173
|
|
176
|
- {exprs, guard, notes}
|
174
|
Pattern.new(exprs, guard: guard, notes: notes)
|
177
175
|
end
|
178
176
|
|
179
177
|
defp combine_guards(nil, guard, _context), do: guard
|
180
178
|
defp combine_guards(guard, nil, _context), do: guard
|
181
179
|
defp combine_guards(g1, g2, context), do: {:and, with_meta(context), [g2, g1]}
|
182
180
|
|
183
|
- defp guard_non_serializable(name, guard, value, context) do
|
181
|
defp guard_pattern(name, guard, value, context) do
|
184
182
|
var = make_var(name, context)
|
185
183
|
|
186
|
- pattern =
|
187
|
- {var, {guard, with_meta(context), [var]},
|
188
|
- ["Using guard for non-serializable value `#{inspect(value)}`"]}
|
189
|
-
|
190
|
- [pattern]
|
184
|
Pattern.new(var,
|
185
|
guard: {guard, with_meta(context), [var]},
|
186
|
notes: ["Using guard for non-serializable value `#{inspect(value)}`"]
|
187
|
)
|
191
188
|
end
|
192
189
|
|
193
190
|
defp make_var(name, context) do
|
194
191
|
{name, with_meta(context), nil}
|
195
192
|
end
|
196
193
|
|
197
|
- defp transform_patterns(patterns, transform, context) do
|
198
|
- Enum.map(patterns, &transform.(&1, context))
|
194
|
defp to_tuple_pattern(%Pattern{expr: [e1, e2]} = pattern, _context) do
|
195
|
%{pattern | expr: {e1, e2}}
|
199
196
|
end
|
200
197
|
|
201
|
- defp tuple_pattern({[e1, e2], guard, notes}, _context) do
|
202
|
- {{e1, e2}, guard, notes}
|
198
|
defp to_tuple_pattern(%Pattern{expr: exprs} = pattern, context) do
|
199
|
%{pattern | expr: {:{}, with_meta(context), exprs}}
|
203
200
|
end
|
204
201
|
|
205
|
- defp tuple_pattern({exprs, guard, notes}, context) do
|
206
|
- {{:{}, with_meta(context), exprs}, guard, notes}
|
202
|
defp to_map_pattern(%Pattern{expr: tuples} = pattern, context) do
|
203
|
%{pattern | expr: map_pattern(tuples, context).expr}
|
207
204
|
end
|
208
205
|
|
209
|
- defp map_pattern({tuples, guard, notes} \\ {[], nil, []}, context) do
|
210
|
- {{:%{}, with_meta(context), tuples}, guard, notes}
|
211
|
- end
|
212
|
-
|
213
|
- defp struct_pattern(struct, {map_expr, guard, notes}, context, extra_notes) do
|
206
|
defp to_struct_pattern(struct, map_pattern, context, extra_notes) do
|
214
207
|
{aliased, _} =
|
215
208
|
context
|
216
209
|
|> Map.get(:aliases, [])
|
|
@@ -218,8 211,8 @@ defmodule Mneme.Assertion.PatternBuilder do
|
218
211
|
|
219
212
|
aliases = aliased |> Module.split() |> Enum.map(&String.to_atom/1)
|
220
213
|
|
221
|
- {{:%, with_meta(context), [{:__aliases__, with_meta(context), aliases}, map_expr]}, guard,
|
222
|
- extra_notes notes}
|
214
|
{:%, with_meta(context), [{:__aliases__, with_meta(context), aliases}, map_pattern.expr]}
|
215
|
|> Pattern.new(guard: map_pattern.guard, notes: extra_notes map_pattern.notes)
|
223
216
|
end
|
224
217
|
|
225
218
|
defp ecto_schema?(module) do
|
|
@@ -272,6 265,29 @@ defmodule Mneme.Assertion.PatternBuilder do
|
272
265
|
end
|
273
266
|
end
|
274
267
|
|
268
|
defp map_pattern(tuples, context) do
|
269
|
Pattern.new({:%{}, with_meta(context), tuples})
|
270
|
end
|
271
|
|
272
|
defp string_pattern(string, context) do
|
273
|
Pattern.new({:__block__, with_meta([delimiter: ~S(")], context), [escape(string)]})
|
274
|
end
|
275
|
|
276
|
defp heredoc_pattern(string, context) do
|
277
|
Pattern.new(
|
278
|
{:__block__, with_meta([delimiter: ~S(""")], context),
|
279
|
[string |> escape() |> format_for_heredoc()]}
|
280
|
)
|
281
|
end
|
282
|
|
283
|
defp format_for_heredoc(string) when is_binary(string) do
|
284
|
if String.ends_with?(string, "\n") do
|
285
|
string
|
286
|
else
|
287
|
string <> "\\\n"
|
288
|
end
|
289
|
end
|
290
|
|
275
291
|
defp escape(string) when is_binary(string) do
|
276
292
|
String.replace(string, "\\", "\\\\")
|
277
293
|
end
|
changed
lib/mneme/diff/formatter.ex
|
@@ -3,6 3,15 @@ defmodule Mneme.Diff.Formatter do
|
3
3
|
|
4
4
|
alias Mneme.Diff
|
5
5
|
alias Mneme.Diff.Zipper
|
6
|
alias Mneme.Utils
|
7
|
|
8
|
@type fmt_instruction :: {op, bounds}
|
9
|
|
10
|
@type op :: :ins | :del | {:ins, :highlight} | {:del, :highlight}
|
11
|
|
12
|
@type bounds :: {start_bound :: bound, end_bound :: bound}
|
13
|
|
14
|
@type bound :: {line :: pos_integer(), column :: pos_integer()}
|
6
15
|
|
7
16
|
@re_newline ~r/\n|\r\n/
|
8
17
|
|
|
@@ -13,8 22,8 @@ defmodule Mneme.Diff.Formatter do
|
13
22
|
def highlight_lines(code, instructions) do
|
14
23
|
lines = code |> Owl.Data.lines() |> Enum.reverse()
|
15
24
|
[last_line | earlier_lines] = lines
|
16
|
- hl_instructions = denormalize_all(instructions)
|
17
|
- highlighted = highlight(hl_instructions, length(lines), last_line, earlier_lines)
|
25
|
fmt_instructions = to_fmt_instructions(instructions)
|
26
|
highlighted = highlight(fmt_instructions, length(lines), last_line, earlier_lines)
|
18
27
|
|
19
28
|
Enum.map(highlighted, fn
|
20
29
|
list when is_list(list) ->
|
|
@@ -25,6 34,15 @@ defmodule Mneme.Diff.Formatter do
|
25
34
|
end)
|
26
35
|
end
|
27
36
|
|
37
|
@spec fmt(op, bounds) :: fmt_instruction
|
38
|
defp fmt(op, {{l1, c1}, {l2, c2}} = bounds) when l2 >= l1 and c2 >= c1 do
|
39
|
{op, bounds}
|
40
|
end
|
41
|
|
42
|
defp fmt(_op, bounds) do
|
43
|
raise ArgumentError, "invalid bounds: #{inspect(bounds)}"
|
44
|
end
|
45
|
|
28
46
|
defp highlight(instructions, line_no, current_line, earlier_lines, line_acc \\ [], acc \\ [])
|
29
47
|
|
30
48
|
defp highlight([], _, current_line, earlier_lines, line_acc, acc) do
|
|
@@ -71,31 89,32 @@ defmodule Mneme.Diff.Formatter do
|
71
89
|
highlight(instructions, l - 1, next, rest_earlier, [], [[line | line_acc] | acc])
|
72
90
|
end
|
73
91
|
|
74
|
- defp denormalize_all(instructions) do
|
92
|
@spec to_fmt_instructions([Diff.instruction()]) :: [fmt_instruction]
|
93
|
defp to_fmt_instructions(instructions) do
|
75
94
|
instructions
|
76
95
|
|> Enum.flat_map(fn
|
77
96
|
{op, kind, zipper} ->
|
78
|
- denormalize(kind, op, Zipper.node(zipper), zipper)
|
97
|
to_fmt_instructions(kind, op, Zipper.node(zipper), zipper)
|
79
98
|
|
80
99
|
{op, :node, zipper, edit_script} ->
|
81
|
- denormalize_with_edit_script(op, Zipper.node(zipper), edit_script)
|
100
|
edit_script_to_fmt_instructions(op, Zipper.node(zipper), edit_script)
|
82
101
|
end)
|
83
102
|
|> Enum.sort_by(&elem(&1, 1), :desc)
|
84
103
|
end
|
85
104
|
|
86
|
- defp denormalize(:node, op, {:{}, tuple_meta, [left, right]} = node, _) do
|
105
|
defp to_fmt_instructions(:node, op, {:{}, tuple_meta, [left, right]} = node, _) do
|
87
106
|
case tuple_meta do
|
88
107
|
%{closing: _} ->
|
89
|
- [{op, bounds(node)}]
|
108
|
[fmt(op, bounds(node))]
|
90
109
|
|
91
110
|
_ ->
|
92
|
- {lc1, _} = bounds(left)
|
93
|
- {_, lc2} = bounds(right)
|
94
|
- [{op, {lc1, lc2}}]
|
111
|
{start_bound, _} = bounds(left)
|
112
|
{_, end_bound} = bounds(right)
|
113
|
[fmt(op, {start_bound, end_bound})]
|
95
114
|
end
|
96
115
|
end
|
97
116
|
|
98
|
- defp denormalize(:node, op, {:var, meta, var}, zipper) do
|
117
|
defp to_fmt_instructions(:node, op, {:var, meta, var}, zipper) do
|
99
118
|
var_bounds = bounds({:var, meta, var})
|
100
119
|
{{l, c}, {l2, c2}} = var_bounds
|
101
120
|
|
|
@@ -103,67 122,67 @@ defmodule Mneme.Diff.Formatter do
|
103
122
|
{:__aliases__, _, _} ->
|
104
123
|
case Zipper.right(zipper) do
|
105
124
|
nil ->
|
106
|
- [{op, {{l, c - 1}, {l2, c2}}}]
|
125
|
[fmt(op, {{l, c - 1}, {l2, c2}})]
|
107
126
|
|
108
127
|
_ ->
|
109
|
- [{op, {{l, c}, {l2, c2 1}}}]
|
128
|
[fmt(op, {{l, c}, {l2, c2 1}})]
|
110
129
|
end
|
111
130
|
|
112
131
|
_ ->
|
113
|
- [{op, var_bounds}]
|
132
|
[fmt(op, var_bounds)]
|
114
133
|
end
|
115
134
|
end
|
116
135
|
|
117
|
- defp denormalize(:node, _op, [], _zipper), do: []
|
136
|
defp to_fmt_instructions(:node, _op, [], _zipper), do: []
|
118
137
|
|
119
|
- defp denormalize(:node, op, node, _zipper) do
|
138
|
defp to_fmt_instructions(:node, op, node, _zipper) do
|
120
139
|
if bounds = bounds(node) do
|
121
|
- [{op, bounds}]
|
140
|
[fmt(op, bounds)]
|
122
141
|
else
|
123
142
|
[]
|
124
143
|
end
|
125
144
|
end
|
126
145
|
|
127
|
- defp denormalize(:delimiter, op, {:%, meta, [name, _]}, _) do
|
128
|
- [struct_start, struct_end] = denormalize_delimiter(op, meta, 1, 1)
|
146
|
defp to_fmt_instructions(:delimiter, op, {:%, meta, [name, _]}, _) do
|
147
|
[struct_start, struct_end] = delimiter_to_fmt_instructions(op, meta, 1, 1)
|
129
148
|
{_, {name_end_l, name_end_c}} = bounds(name)
|
130
149
|
|
131
150
|
[
|
132
151
|
struct_start,
|
133
|
- {op, {{name_end_l, name_end_c}, {name_end_l, name_end_c 1}}},
|
152
|
fmt(op, {{name_end_l, name_end_c}, {name_end_l, name_end_c 1}}),
|
134
153
|
struct_end
|
135
154
|
]
|
136
155
|
end
|
137
156
|
|
138
|
- defp denormalize(:delimiter, op, {:.., %{line: l, column: c}, [_, _]}, _) do
|
139
|
- [{op, {{l, c}, {l, c 2}}}]
|
157
|
defp to_fmt_instructions(:delimiter, op, {:.., %{line: l, column: c}, [_, _]}, _) do
|
158
|
[fmt(op, {{l, c}, {l, c 2}})]
|
140
159
|
end
|
141
160
|
|
142
|
- defp denormalize(:delimiter, op, {:"..//", %{line: l, column: c}, [_, range_end, _]}, _) do
|
161
|
defp to_fmt_instructions(:delimiter, op, {:"..//", %{line: l, column: c}, [_, range_end, _]}, _) do
|
143
162
|
{_, {l2, c2}} = bounds(range_end)
|
144
163
|
|
145
|
- [{op, {{l, c}, {l, c 2}}}, {op, {{l2, c2}, {l2, c2 2}}}]
|
164
|
[fmt(op, {{l, c}, {l, c 2}}), fmt(op, {{l2, c2}, {l2, c2 2}})]
|
146
165
|
end
|
147
166
|
|
148
|
- defp denormalize(:delimiter, op, {:"[]", meta, _}, _) do
|
149
|
- denormalize_delimiter(op, meta, 1, 1)
|
167
|
defp to_fmt_instructions(:delimiter, op, {:"[]", meta, _}, _) do
|
168
|
delimiter_to_fmt_instructions(op, meta, 1, 1)
|
150
169
|
end
|
151
170
|
|
152
|
- defp denormalize(:delimiter, op, {:{}, meta, _}, _) do
|
153
|
- denormalize_delimiter(op, meta, 1, 1)
|
171
|
defp to_fmt_instructions(:delimiter, op, {:{}, meta, _}, _) do
|
172
|
delimiter_to_fmt_instructions(op, meta, 1, 1)
|
154
173
|
end
|
155
174
|
|
156
|
- defp denormalize(:delimiter, op, {:%{}, meta, _}, zipper) do
|
175
|
defp to_fmt_instructions(:delimiter, op, {:%{}, meta, _}, zipper) do
|
157
176
|
case zipper |> Zipper.up() |> Zipper.node() do
|
158
177
|
{:%, %{line: l, column: c}, _} ->
|
159
|
- [{op, {{l, c}, {l, c 1}}} | denormalize_delimiter(op, meta, 1, 1)]
|
178
|
[fmt(op, {{l, c}, {l, c 1}}) | delimiter_to_fmt_instructions(op, meta, 1, 1)]
|
160
179
|
|
161
180
|
_ ->
|
162
|
- denormalize_delimiter(op, Map.update!(meta, :column, &(&1 - 1)), 2, 1)
|
181
|
delimiter_to_fmt_instructions(op, Map.update!(meta, :column, &(&1 - 1)), 2, 1)
|
163
182
|
end
|
164
183
|
end
|
165
184
|
|
166
|
- defp denormalize(
|
185
|
defp to_fmt_instructions(
|
167
186
|
:delimiter,
|
168
187
|
op,
|
169
188
|
{call, %{line: l, column: c, closing: %{line: l2, column: c2}}, _},
|
|
@@ -171,10 190,10 @@ defmodule Mneme.Diff.Formatter do
|
171
190
|
)
|
172
191
|
when is_atom(call) do
|
173
192
|
len = Macro.inspect_atom(:remote_call, call) |> String.length()
|
174
|
- [{op, {{l, c}, {l, c len 1}}}, {op, {{l2, c2}, {l2, c2 1}}}]
|
193
|
[fmt(op, {{l, c}, {l, c len 1}}), fmt(op, {{l2, c2}, {l2, c2 1}})]
|
175
194
|
end
|
176
195
|
|
177
|
- defp denormalize(
|
196
|
defp to_fmt_instructions(
|
178
197
|
:delimiter,
|
179
198
|
op,
|
180
199
|
{call, %{line: l, column: c}, _},
|
|
@@ -182,53 201,54 @@ defmodule Mneme.Diff.Formatter do
|
182
201
|
)
|
183
202
|
when is_atom(call) do
|
184
203
|
len = Macro.inspect_atom(:remote_call, call) |> String.length()
|
185
|
- [{op, {{l, c}, {l, c len}}}]
|
204
|
[fmt(op, {{l, c}, {l, c len}})]
|
186
205
|
end
|
187
206
|
|
188
|
- defp denormalize(
|
207
|
defp to_fmt_instructions(
|
189
208
|
:delimiter,
|
190
209
|
op,
|
191
210
|
{{:., _, [_left, right]}, %{closing: %{line: l2, column: c2}}, _},
|
192
211
|
_
|
193
212
|
) do
|
194
213
|
{_, {l, c}} = bounds(right)
|
195
|
- [{op, {{l, c}, {l, c 1}}}, {op, {{l2, c2}, {l2, c2 1}}}]
|
214
|
[fmt(op, {{l, c}, {l, c 1}}), fmt(op, {{l2, c2}, {l2, c2 1}})]
|
196
215
|
end
|
197
216
|
|
198
|
- defp denormalize(
|
217
|
defp to_fmt_instructions(
|
199
218
|
:delimiter,
|
200
219
|
op,
|
201
220
|
{{:., _, [var]}, %{closing: %{line: l2, column: c2}}, _},
|
202
221
|
_
|
203
222
|
) do
|
204
223
|
{_, {l, c}} = bounds(var)
|
205
|
- [{op, {{l, c}, {l, c 1}}}, {op, {{l2, c2}, {l2, c2 1}}}]
|
224
|
[fmt(op, {{l, c}, {l, c 1}}), fmt(op, {{l2, c2}, {l2, c2 1}})]
|
206
225
|
end
|
207
226
|
|
208
227
|
# Dot call delimiter with no parens, nothing to highlight
|
209
|
- defp denormalize(:delimiter, _op, {{:., _, _}, _, _}, _), do: []
|
228
|
defp to_fmt_instructions(:delimiter, _op, {{:., _, _}, _, _}, _), do: []
|
210
229
|
|
211
|
- defp denormalize(:delimiter, op, {atom, %{line: l, column: c}, _}, _) when is_atom(atom) do
|
230
|
defp to_fmt_instructions(:delimiter, op, {atom, %{line: l, column: c}, _}, _)
|
231
|
when is_atom(atom) do
|
212
232
|
len = Macro.inspect_atom(:remote_call, atom) |> String.length()
|
213
|
- [{op, {{l, c}, {l, c len}}}]
|
233
|
[fmt(op, {{l, c}, {l, c len}})]
|
214
234
|
end
|
215
235
|
|
216
236
|
# list literals and 2-tuples are structural only in the AST and cannot
|
217
237
|
# be highlighted
|
218
|
- defp denormalize(:delimiter, _op, list, _) when is_list(list), do: []
|
219
|
- defp denormalize(:delimiter, _op, {_, _}, _), do: []
|
238
|
defp to_fmt_instructions(:delimiter, _op, list, _) when is_list(list), do: []
|
239
|
defp to_fmt_instructions(:delimiter, _op, {_, _}, _), do: []
|
220
240
|
|
221
|
- defp denormalize_delimiter(op, meta, start_len, end_len) do
|
241
|
defp delimiter_to_fmt_instructions(op, meta, start_len, end_len) do
|
222
242
|
case meta do
|
223
243
|
%{line: l, column: c, closing: %{line: l2, column: c2}} ->
|
224
|
- [{op, {{l, c}, {l, c start_len}}}, {op, {{l2, c2}, {l2, c2 end_len}}}]
|
244
|
[fmt(op, {{l, c}, {l, c start_len}}), fmt(op, {{l2, c2}, {l2, c2 end_len}})]
|
225
245
|
|
226
246
|
_ ->
|
227
247
|
[]
|
228
248
|
end
|
229
249
|
end
|
230
250
|
|
231
|
- defp denormalize_with_edit_script(op, {_, meta, _}, edit_script) do
|
251
|
defp edit_script_to_fmt_instructions(op, {_, meta, _}, edit_script) do
|
232
252
|
%{line: l_start, column: c_start, delimiter: del} = meta
|
233
253
|
|
234
254
|
{l, c} =
|
|
@@ -247,17 267,18 @@ defmodule Mneme.Diff.Formatter do
|
247
267
|
{edit, s}, {l, c, c_start} ->
|
248
268
|
c2 =
|
249
269
|
if del == ~s(") do
|
250
|
- c String.length(s) occurrences(s, ?")
|
270
|
c String.length(s) Utils.occurrences(s, ?")
|
251
271
|
else
|
252
272
|
c String.length(s)
|
253
273
|
end
|
254
274
|
|
255
275
|
op = if edit == :eq, do: op, else: {op, :highlight}
|
256
|
- {[{op, {{l, c}, {l, c2}}}], {l, c2, c_start}}
|
276
|
|
277
|
{[fmt(op, {{l, c}, {l, c2}})], {l, c2, c_start}}
|
257
278
|
end)
|
258
279
|
|
259
|
- del_start = [{op, {{l_start, c_start}, {l_start, c_start String.length(del)}}}]
|
260
|
- del_end = [{op, {{l_end, c_end}, {l_end, c_end String.length(del)}}}]
|
280
|
del_start = [fmt(op, {{l_start, c_start}, {l_start, c_start String.length(del)}})]
|
281
|
del_end = [fmt(op, {{l_end, c_end}, {l_end, c_end String.length(del)}})]
|
261
282
|
del_start ops del_end
|
262
283
|
end
|
263
284
|
|
|
@@ -273,6 294,9 @@ defmodule Mneme.Diff.Formatter do
|
273
294
|
end)
|
274
295
|
end
|
275
296
|
|
297
|
@spec bounds(term()) :: bounds | nil
|
298
|
defp bounds(node)
|
299
|
|
276
300
|
defp bounds({:%{}, %{closing: %{line: l2, column: c2}, line: l, column: c}, _}) do
|
277
301
|
{{l, c - 1}, {l2, c2 1}}
|
278
302
|
end
|
|
@@ -295,7 319,7 @@ defmodule Mneme.Diff.Formatter do
|
295
319
|
end
|
296
320
|
|
297
321
|
defp bounds({:string, %{line: l, column: c, delimiter: ~s(")}, string}) do
|
298
|
- offset = 2 String.length(string) occurrences(string, ?")
|
322
|
offset = 2 String.length(string) Utils.occurrences(string, ?")
|
299
323
|
{{l, c}, {l, c offset}}
|
300
324
|
end
|
301
325
|
|
|
@@ -309,7 333,7 @@ defmodule Mneme.Diff.Formatter do
|
309
333
|
end
|
310
334
|
|
311
335
|
defp bounds({:charlist, %{line: l, column: c, delimiter: "'"}, string}) do
|
312
|
- offset = 2 String.length(string) occurrences(string, ?')
|
336
|
offset = 2 String.length(string) Utils.occurrences(string, ?')
|
313
337
|
{{l, c}, {l, c offset}}
|
314
338
|
end
|
315
339
|
|
|
@@ -400,9 424,4 @@ defmodule Mneme.Diff.Formatter do
|
400
424
|
|
401
425
|
defp tag(data, {:ins, :highlight}), do: Owl.Data.tag(data, [:bright, :green, :underline])
|
402
426
|
defp tag(data, {:del, :highlight}), do: Owl.Data.tag(data, [:bright, :red, :underline])
|
403
|
-
|
404
|
- defp occurrences(string, char, acc \\ 0)
|
405
|
- defp occurrences(<<char, rest::binary>>, char, acc), do: occurrences(rest, char, acc 1)
|
406
|
- defp occurrences(<<_, rest::binary>>, char, acc), do: occurrences(rest, char, acc)
|
407
|
- defp occurrences(<<>>, _char, acc), do: acc
|
408
427
|
end
|
changed
lib/mneme/diff/pathfinding.ex
|
@@ -1,9 1,11 @@
|
1
|
- # Derived from bitwalker/libgraph:
|
2
|
- # https://github.com/bitwalker/libgraph/blob/main/lib/graph/pathfinding.ex
|
3
|
-
|
4
1
|
defmodule Mneme.Diff.Pathfinding do
|
5
2
|
@moduledoc false
|
6
3
|
|
4
|
# Originally derived from libgraph:
|
5
|
# https://github.com/bitwalker/libgraph/blob/main/lib/graph/pathfinding.ex
|
6
|
|
7
|
alias Mneme.Diff.PriorityQueue
|
8
|
|
7
9
|
@doc """
|
8
10
|
Finds the shortest path between `v` and a target vertex.
|
9
11
|
|
|
@@ -19,7 21,6 @@ defmodule Mneme.Diff.Pathfinding do
|
19
21
|
def lazy_a_star(v, vertex_identifier, next_fun, hfun) do
|
20
22
|
v_id = vertex_identifier.(v)
|
21
23
|
known = %{v_id => v}
|
22
|
- tree = Graph.new(vertex_identifier: &Function.identity/1) |> Graph.add_vertex(v_id)
|
23
24
|
q = PriorityQueue.new()
|
24
25
|
|
25
26
|
with {:cont, vs_out} <- next_fun.(v) do
|
|
@@ -27,7 28,7 @@ defmodule Mneme.Diff.Pathfinding do
|
27
28
|
|
28
29
|
q
|
29
30
|
|> push_vertices(known, v_id, vs_out, hfun)
|
30
|
- |> do_lazy_bfs(known, tree, vertex_identifier, next_fun, hfun)
|
31
|
|> do_lazy_bfs(known, %{}, vertex_identifier, next_fun, hfun)
|
31
32
|
|> case do
|
32
33
|
{:ok, known, path} -> for(id <- path, do: Map.get(known, id))
|
33
34
|
:error -> nil
|
|
@@ -40,33 41,29 @@ defmodule Mneme.Diff.Pathfinding do
|
40
41
|
|
41
42
|
## Private
|
42
43
|
|
43
|
- defp do_lazy_bfs(q, known, tree, vertex_identifier, next_fun, hfun) do
|
44
|
defp do_lazy_bfs(q, known, history, vertex_identifier, next_fun, hfun) do
|
44
45
|
case PriorityQueue.pop(q) do
|
45
|
- {{:value, {v1_id, v2_id, acc_cost}}, q} ->
|
46
|
{:ok, {v1_id, v2_id, acc_cost}, q} ->
|
46
47
|
v2 = Map.fetch!(known, v2_id)
|
47
48
|
|
48
49
|
case next_fun.(v2) do
|
49
50
|
:halt ->
|
50
|
- {:ok, known, construct_path(v1_id, tree, [v2_id])}
|
51
|
{:ok, known, construct_path(history, v1_id, [v2_id])}
|
51
52
|
|
52
53
|
{:cont, vs_out} ->
|
53
|
- if Map.has_key?(tree.vertices, v2_id) do
|
54
|
- do_lazy_bfs(q, known, tree, vertex_identifier, next_fun, hfun)
|
54
|
if Map.has_key?(history, v2_id) do
|
55
|
do_lazy_bfs(q, known, history, vertex_identifier, next_fun, hfun)
|
55
56
|
else
|
56
57
|
{v2_out, known} = push_known(known, vs_out, vertex_identifier)
|
57
|
-
|
58
|
- tree =
|
59
|
- tree
|
60
|
- |> Graph.add_vertex(v2_id)
|
61
|
- |> Graph.add_edge(v2_id, v1_id)
|
58
|
history = Map.put(history, v2_id, v1_id)
|
62
59
|
|
63
60
|
q
|
64
61
|
|> push_vertices(known, v2_id, v2_out, hfun, acc_cost)
|
65
|
- |> do_lazy_bfs(known, tree, vertex_identifier, next_fun, hfun)
|
62
|
|> do_lazy_bfs(known, history, vertex_identifier, next_fun, hfun)
|
66
63
|
end
|
67
64
|
end
|
68
65
|
|
69
|
- {:empty, _} ->
|
66
|
:error ->
|
70
67
|
:error
|
71
68
|
end
|
72
69
|
end
|
|
@@ -87,12 84,12 @@ defmodule Mneme.Diff.Pathfinding do
|
87
84
|
end)
|
88
85
|
end
|
89
86
|
|
90
|
- defp construct_path(v_id, %Graph{out_edges: oe} = tree, path) do
|
87
|
defp construct_path(history, v_id, path) do
|
91
88
|
path = [v_id | path]
|
92
89
|
|
93
|
- case oe |> Map.get(v_id, MapSet.new()) |> MapSet.to_list() do
|
94
|
- [] -> path
|
95
|
- [next_id] -> construct_path(next_id, tree, path)
|
90
|
case history do
|
91
|
%{^v_id => next_id} -> construct_path(history, next_id, path)
|
92
|
_ -> path
|
96
93
|
end
|
97
94
|
end
|
98
95
|
end
|
added
lib/mneme/diff/priority_queue.ex
|
@@ -0,0 1,52 @@
|
1
|
defmodule Mneme.Diff.PriorityQueue do
|
2
|
@moduledoc false
|
3
|
|
4
|
@opaque t :: :gb_trees.tree()
|
5
|
|
6
|
@type priority :: non_neg_integer()
|
7
|
|
8
|
@doc """
|
9
|
Creates a new priority queue.
|
10
|
"""
|
11
|
@spec new() :: t
|
12
|
def new do
|
13
|
:gb_trees.empty()
|
14
|
end
|
15
|
|
16
|
@doc """
|
17
|
Push a new element into the queue with a given priority.
|
18
|
"""
|
19
|
@spec push(t, term(), priority) :: t
|
20
|
def push(pqueue, value, priority) do
|
21
|
case :gb_trees.lookup(priority, pqueue) do
|
22
|
:none ->
|
23
|
queue = :queue.new()
|
24
|
queue = :queue.in(value, queue)
|
25
|
:gb_trees.insert(priority, queue, pqueue)
|
26
|
|
27
|
{:value, queue} ->
|
28
|
new_queue = :queue.in(value, queue)
|
29
|
:gb_trees.update(priority, new_queue, pqueue)
|
30
|
end
|
31
|
end
|
32
|
|
33
|
@doc """
|
34
|
Pop an element out of the queue, returning `{:ok, value, queue}` or
|
35
|
`:error` if there is not an element to pop.
|
36
|
"""
|
37
|
@spec pop(t) :: {:ok, term(), t} | :error
|
38
|
def pop(pqueue) do
|
39
|
if :gb_trees.is_empty(pqueue) do
|
40
|
:error
|
41
|
else
|
42
|
{priority, queue, pqueue} = :gb_trees.take_smallest(pqueue)
|
43
|
{{:value, value}, queue} = :queue.out(queue)
|
44
|
|
45
|
if :queue.is_empty(queue) do
|
46
|
{:ok, value, pqueue}
|
47
|
else
|
48
|
{:ok, value, :gb_trees.insert(priority, queue, pqueue)}
|
49
|
end
|
50
|
end
|
51
|
end
|
52
|
end
|
changed
lib/mneme/prompter.ex
|
@@ -1,8 1,9 @@
|
1
1
|
defmodule Mneme.Prompter do
|
2
|
- # Behaviour controlling how to prompt acceptance or rejection of an
|
3
|
- # assertion patch. This behaviour may become public in the future.
|
4
2
|
@moduledoc false
|
5
3
|
|
4
|
# This module defines a behaviour and utilities for prompting a user
|
5
|
# when an assertion should be updated.
|
6
|
|
6
7
|
@type response :: :accept | :reject | :skip | :prev | :next
|
7
8
|
@type diff :: %{left: String.t(), right: String.t()}
|
8
9
|
@type options :: map()
|
changed
lib/mneme/prompter/terminal.ex
|
@@ -27,7 27,7 @@ defmodule Mneme.Prompter.Terminal do
|
27
27
|
|
28
28
|
@doc false
|
29
29
|
def message(%Assertion{} = assertion, counter, diff, opts) do
|
30
|
- notes = Assertion.notes(assertion)
|
30
|
notes = Assertion.pattern(assertion).notes
|
31
31
|
|
32
32
|
[
|
33
33
|
format_header(assertion, counter, opts),
|
added
lib/mneme/utils.ex
|
@@ -0,0 1,23 @@
|
1
|
defmodule Mneme.Utils do
|
2
|
@moduledoc false
|
3
|
|
4
|
@doc """
|
5
|
Returns the occurrences of a given character in a string.
|
6
|
|
7
|
## Examples
|
8
|
|
9
|
iex> occurrences("foo", ?o)
|
10
|
2
|
11
|
|
12
|
iex> occurrences("foo", ?z)
|
13
|
0
|
14
|
|
15
|
"""
|
16
|
def occurrences(string, char) when is_binary(string) and is_integer(char) do
|
17
|
occurrences(string, char, 0)
|
18
|
end
|
19
|
|
20
|
defp occurrences(<<char, rest::binary>>, char, acc), do: occurrences(rest, char, acc 1)
|
21
|
defp occurrences(<<_, rest::binary>>, char, acc), do: occurrences(rest, char, acc)
|
22
|
defp occurrences(<<>>, _char, acc), do: acc
|
23
|
end
|
changed
mix.exs
|
@@ -4,7 4,7 @@ defmodule Mneme.MixProject do
|
4
4
|
@app :mneme
|
5
5
|
@source_url "https://github.com/zachallaun/mneme"
|
6
6
|
|
7
|
- def version, do: "0.3.0"
|
7
|
def version, do: "0.3.1"
|
8
8
|
|
9
9
|
def project do
|
10
10
|
[
|
|
@@ -15,7 15,7 @@ defmodule Mneme.MixProject do
|
15
15
|
start_permanent: Mix.env() == :prod,
|
16
16
|
deps: deps(),
|
17
17
|
dialyzer: dialyzer(),
|
18
|
- test_coverage: [tool: ExCoveralls],
|
18
|
test_coverage: [tool: ExCoveralls, import_cover: "cover"],
|
19
19
|
aliases: aliases(),
|
20
20
|
preferred_cli_env: preferred_cli_env(),
|
21
21
|
|
|
@@ -41,10 41,13 @@ defmodule Mneme.MixProject do
|
41
41
|
{:nimble_options, "~> 1.0"},
|
42
42
|
{:sourceror, "~> 0.12"},
|
43
43
|
{:rewrite, "~> 0.6.0"},
|
44
|
- {:libgraph, "~> 0.16.0"},
|
45
44
|
|
46
|
- # Development
|
47
|
- {:excoveralls, "~> 0.15", only: :test},
|
45
|
# Development / Test
|
46
|
{:excoveralls,
|
47
|
github: "zachallaun/excoveralls", ref: "import-coverdata-improvements", only: :test},
|
48
|
# Go back to using the release version when https://github.com/parroty/excoveralls/pull/309
|
49
|
# is merged:
|
50
|
# {:excoveralls, "~> 0.15", only: :test},
|
48
51
|
{:ecto, "~> 3.9.4", only: :test},
|
49
52
|
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
|
50
53
|
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
|
|
@@ -59,14 62,21 @@ defmodule Mneme.MixProject do
|
59
62
|
|
60
63
|
defp aliases do
|
61
64
|
[
|
62
|
- t: "coveralls"
|
65
|
coveralls: [
|
66
|
&export_integration_coverage/1,
|
67
|
"coveralls --import-cover cover"
|
68
|
],
|
69
|
"coveralls.html": [
|
70
|
&export_integration_coverage/1,
|
71
|
"coveralls.html --import-cover cover"
|
72
|
]
|
63
73
|
]
|
64
74
|
end
|
65
75
|
|
66
76
|
defp preferred_cli_env do
|
67
77
|
[
|
68
78
|
coveralls: :test,
|
69
|
- t: :test
|
79
|
"coveralls.html": :test
|
70
80
|
]
|
71
81
|
end
|
72
82
|
|
|
@@ -112,4 122,8 @@ defmodule Mneme.MixProject do
|
112
122
|
]
|
113
123
|
]
|
114
124
|
end
|
125
|
|
126
|
defp export_integration_coverage(_) do
|
127
|
Application.put_env(:mneme, :export_integration_coverage, true)
|
128
|
end
|
115
129
|
end
|