changed hex_metadata.config
 
@@ -5,9 5,11 @@
5
5
{<<"files">>,
6
6
[<<"lib">>,<<"lib/mneme">>,<<"lib/mneme/serializer.ex">>,
7
7
<<"lib/mneme/ex_unit_formatter.ex">>,<<"lib/mneme/server.ex">>,
8
- <<"lib/mneme/options.ex">>,<<"lib/mneme/assertion.ex">>,
9
- <<"lib/mneme/prompter">>,<<"lib/mneme/prompter/terminal.ex">>,
10
- <<"lib/mneme/prompter.ex">>,<<"lib/mneme/patcher.ex">>,<<"lib/mneme.ex">>,
8
<<"lib/mneme/utils.ex">>,<<"lib/mneme/options.ex">>,
9
<<"lib/mneme/assertion.ex">>,<<"lib/mneme/prompter">>,
10
<<"lib/mneme/prompter/terminal.ex">>,<<"lib/mneme/prompter.ex">>,
11
<<"lib/mneme/patcher.ex">>,<<"lib/mneme.ex">>,<<"priv">>,<<"priv/plts">>,
12
<<"priv/plts/dialyzer.plt.hash">>,<<"priv/plts/dialyzer.plt">>,
11
13
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>]}.
12
14
{<<"licenses">>,[<<"MIT">>]}.
13
15
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/zachallaun/mneme">>}]}.
 
@@ -32,5 34,5 @@
32
34
{<<"name">>,<<"rewrite">>},
33
35
{<<"optional">>,false},
34
36
{<<"repository">>,<<"hexpm">>},
35
- {<<"requirement">>,<<"0.4.0">>}]]}.
36
- {<<"version">>,<<"0.0.2">>}.
37
{<<"requirement">>,<<"~> 0.5.0">>}]]}.
38
{<<"version">>,<<"0.0.3">>}.
changed lib/mneme/assertion.ex
 
@@ -134,15 134,6 @@ defmodule Mneme.Assertion do
134
134
|> Map.put(:eval, code_for_eval(new_code, assertion.pattern))
135
135
end
136
136
137
- @doc """
138
- Format the assertion as a string.
139
- """
140
- def format(%Assertion{code: code}, opts) do
141
- code
142
- |> escape_newlines()
143
- |> Sourceror.to_string(opts)
144
- end
145
-
146
137
@doc """
147
138
Check whether the assertion struct represents the given AST node.
148
139
"""
 
@@ -167,31 158,55 @@ defmodule Mneme.Assertion do
167
158
def to_code(assertion, target) when target in [:auto_assert, :assert] do
168
159
case assertion.pattern do
169
160
{falsy, nil} when falsy in [nil, false] ->
170
- build_call(target, :compare, assertion.code, falsy, nil)
161
build_call(
162
target,
163
:compare,
164
assertion.code,
165
block_with_line(falsy, meta(assertion.code)),
166
nil
167
)
171
168
172
169
{expr, guard} ->
173
- build_call(target, :match, assertion.code, expr, guard)
170
build_call(
171
target,
172
:match,
173
assertion.code,
174
block_with_line(expr, meta(assertion.code)),
175
guard
176
)
174
177
end
175
178
end
176
179
180
# This gets around a bug in Elixir's `Code.Normalizer` prior to this
181
# PR being merged: https://github.com/elixir-lang/elixir/pull/12389
182
defp block_with_line({call, meta, args}, parent_meta) do
183
{call, Keyword.put(meta, :line, parent_meta[:line]), args}
184
end
185
186
defp block_with_line(value, parent_meta) do
187
{:__block__, [line: parent_meta[:line]], [value]}
188
end
189
177
190
defp build_call(:auto_assert, :compare, code, falsy_expr, nil) do
178
- {:auto_assert, [], [{:==, [], [value_expr(code), falsy_expr]}]}
191
{:auto_assert, meta(code), [{:==, meta(value_expr(code)), [value_expr(code), falsy_expr]}]}
179
192
end
180
193
181
194
defp build_call(:auto_assert, :match, code, expr, nil) do
182
- {:auto_assert, [], [{:<-, [], [expr, value_expr(code)]}]}
195
{:auto_assert, meta(code), [{:<-, meta(value_expr(code)), [expr, value_expr(code)]}]}
183
196
end
184
197
185
198
defp build_call(:auto_assert, :match, code, expr, guard) do
186
- {:auto_assert, [], [{:<-, [], [{:when, [], [expr, guard]}, value_expr(code)]}]}
199
{:auto_assert, meta(code),
200
[{:<-, meta(value_expr(code)), [{:when, [], [expr, guard]}, value_expr(code)]}]}
187
201
end
188
202
189
203
defp build_call(:assert, :compare, code, falsy, nil) do
190
- {:assert, [], [{:==, [], [value_expr(code), falsy]}]}
204
{:assert, meta(code), [{:==, meta(value_expr(code)), [value_expr(code), falsy]}]}
191
205
end
192
206
193
207
defp build_call(:assert, :match, code, expr, nil) do
194
- {:assert, [], [{:=, [], [normalize_heredoc(expr), value_expr(code)]}]}
208
{:assert, meta(code),
209
[{:=, meta(value_expr(code)), [normalize_heredoc(expr), value_expr(code)]}]}
195
210
end
196
211
197
212
defp build_call(:assert, :match, code, expr, guard) do
 
@@ -203,6 218,9 @@ defmodule Mneme.Assertion do
203
218
end
204
219
end
205
220
221
defp meta({_, meta, _}), do: meta
222
defp meta(_), do: []
223
206
224
defp code_for_eval(code, pattern) do
207
225
case pattern do
208
226
{falsy, nil} when falsy in [nil, false] ->
 
@@ -275,21 293,4 @@ defmodule Mneme.Assertion do
275
293
end
276
294
277
295
defp normalize_heredoc(expr), do: expr
278
-
279
- defp escape_newlines(code) when is_list(code) do
280
- Enum.map(code, &escape_newlines/1)
281
- end
282
-
283
- defp escape_newlines(code) do
284
- Sourceror.prewalk(code, fn
285
- {:__block__, meta, [string]} = quoted, state when is_binary(string) ->
286
- case meta[:delimiter] do
287
- "\"" -> {{:__block__, meta, [String.replace(string, "\n", "\\n")]}, state}
288
- _ -> {quoted, state}
289
- end
290
-
291
- quoted, state ->
292
- {quoted, state}
293
- end)
294
- end
295
296
end
changed lib/mneme/patcher.ex
 
@@ -1,140 1,112 @@
1
1
defmodule Mneme.Patcher do
2
2
@moduledoc false
3
3
4
- alias Rewrite.DotFormatter
4
alias Mneme.Assertion
5
5
alias Sourceror.Zipper
6
-
7
- defmodule FileResult do
8
- @moduledoc false
9
- defstruct [:file, :source, :ast, accepted: [], rejected: []]
10
- end
11
-
12
- defmodule SuiteResult do
13
- @moduledoc false
14
- defstruct [:format_opts, files: %{}, finalized: false]
15
- end
6
alias Rewrite.Project
7
alias Rewrite.Source
16
8
17
9
@doc """
18
10
Initialize patch state.
19
11
"""
20
12
def init do
21
- %SuiteResult{format_opts: DotFormatter.opts()}
13
Rewrite.Project.from_sources([])
14
end
15
16
@doc """
17
Load and cache and source and AST required by the context.
18
"""
19
def load_file!(%Project{} = project, %{file: file}) do
20
case Project.source(project, file) do
21
{:ok, _source} ->
22
project
23
24
:error ->
25
Project.update(project, Source.read!(file))
26
end
22
27
end
23
28
24
29
@doc """
25
30
Finalize all patches, writing all results to disk.
26
31
"""
27
- def finalize!(%SuiteResult{finalized: false} = state) do
28
- %{files: files, format_opts: format_opts} = state
29
-
30
- for {_, %FileResult{file: file, source: source, accepted: [_ | _] = patches}} <- files do
31
- patched_iodata =
32
- source
33
- |> Sourceror.patch_string(patches)
34
- |> Sourceror.parse_string!()
35
- |> Sourceror.to_string(format_opts)
36
-
37
- File.write!(file, [patched_iodata, "\n"])
38
- end
39
-
40
- %{state | finalized: true}
41
- end
42
-
43
- def finalize!(%SuiteResult{finalized: true} = state), do: state
32
def finalize!(project), do: Project.save(project)
44
33
45
34
@doc """
46
35
Run an assertion patch.
47
36
48
37
Returns `{result, patch_state}`.
49
38
"""
50
- def patch!(%SuiteResult{} = state, assertion, opts) do
51
- {patch, assertion} = patch_assertion(state, assertion, opts)
39
def patch!(%Project{} = project, assertion, opts) do
40
{source, assertion} = patch_assertion(project, assertion, opts)
52
41
53
- if accept_patch?(patch, opts) do
54
- {{:ok, assertion}, accept_patch(state, patch, assertion)}
42
if accept_change?(source, assertion, opts) do
43
{{:ok, assertion}, Project.update(project, source)}
55
44
else
56
- {:error, reject_patch(state, patch, assertion)}
45
{:error, project}
57
46
end
58
47
end
59
48
60
- @doc """
61
- Load and cache and source and AST required by the context.
62
- """
63
- def load_file!(%SuiteResult{} = state, %{file: file}) do
64
- case state.files[file] do
65
- nil -> register_file(state, file, File.read!(file))
66
- _ -> state
67
- end
49
defp patch_assertion(project, assertion, opts) do
50
source = Project.source!(project, assertion.context.file)
51
52
zipper =
53
source
54
|> Source.ast()
55
|> Zipper.zip()
56
|> Zipper.find(&Assertion.same?(assertion, &1))
57
58
# Hack: String serialization fix
59
# Sourceror's AST is richer than the one we get back from a macro call.
60
# String literals are in a :__block__ tuple and include a delimiter;
61
# we use this information when formatting to ensure that the same
62
# delimiters are used for output.
63
assertion =
64
assertion
65
|> Map.put(:code, Zipper.node(zipper))
66
|> Assertion.regenerate_code(opts.target)
67
68
new_zipper = zipper_update_with_meta(zipper, assertion.code)
69
70
ast = new_zipper |> Zipper.root() |> escape_newlines()
71
72
{Source.update(source, :mneme, ast: ast), assertion}
68
73
end
69
74
70
- @doc """
71
- Registers the source and AST for the given file and content.
72
- """
73
- def register_file(%SuiteResult{} = state, file, source) do
74
- case state.files[file] do
75
- nil ->
76
- file_result = %FileResult{
77
- file: file,
78
- source: source,
79
- ast: Sourceror.parse_string!(source)
80
- }
75
defp zipper_update_with_meta(zipper, {:__block__, _, [{call1, _, args1}, {call2, _, args2}]}) do
76
{_, meta, _} = Zipper.node(zipper)
77
call1_meta = Keyword.put(meta, :end_of_expression, newlines: 1)
81
78
82
- Map.update!(state, :files, &Map.put(&1, file, file_result))
83
-
84
- _ ->
85
- state
86
- end
79
zipper
80
|> Zipper.insert_left({call1, call1_meta, args1})
81
|> Zipper.insert_left({call2, meta, args2})
82
|> Zipper.remove()
87
83
end
88
84
89
- defp patch_assertion(%{files: files} = state, assertion, opts) do
90
- files
91
- |> Map.fetch!(assertion.context.file)
92
- |> Map.fetch!(:ast)
93
- |> Zipper.zip()
94
- |> Zipper.find(fn node -> Mneme.Assertion.same?(assertion, node) end)
95
- |> Zipper.node()
96
- |> create_patch(state, assertion, opts)
85
defp zipper_update_with_meta(zipper, {call, _, args}) do
86
Zipper.update(zipper, fn {_, meta, _} -> {call, meta, args} end)
97
87
end
98
88
99
- defp create_patch(node, %SuiteResult{format_opts: format_opts}, assertion, opts) do
100
- # HACK: String serialization fix
101
- # Sourceror's AST is richer than the one we get from the macro call.
102
- # In particular, string literals are in a :__block__ tuple and include
103
- # delimiter information. We use this when formatting to ensure that
104
- # the same delimiters are used.
105
- node = remove_comments(node)
106
- assertion = Map.put(assertion, :code, node)
107
-
108
- new_assertion = Mneme.Assertion.regenerate_code(assertion, opts.target)
109
-
110
- patch = %{
111
- change: Mneme.Assertion.format(new_assertion, format_opts),
112
- range: Sourceror.get_range(node),
113
- original: assertion,
114
- replacement: new_assertion,
115
- format_opts: format_opts
116
- }
117
-
118
- {patch, new_assertion}
89
defp accept_change?(source, assertion, %{action: :prompt, prompter: prompter}) do
90
prompter.prompt!(source, assertion)
119
91
end
120
92
121
- defp remove_comments({call, meta, body}) do
122
- meta = Keyword.drop(meta, [:leading_comments, :trailing_comments])
123
- {call, meta, body}
93
defp accept_change?(_, _, %{action: :accept}), do: true
94
defp accept_change?(_, _, %{action: :reject}), do: false
95
96
defp escape_newlines(code) when is_list(code) do
97
Enum.map(code, &escape_newlines/1)
124
98
end
125
99
126
- defp accept_patch?(patch, %{action: :prompt, prompter: prompter}) do
127
- prompter.prompt!(patch)
128
- end
100
defp escape_newlines(code) do
101
Sourceror.prewalk(code, fn
102
{:__block__, meta, [string]} = quoted, state when is_binary(string) ->
103
case meta[:delimiter] do
104
"\"" -> {{:__block__, meta, [String.replace(string, "\n", "\\n")]}, state}
105
_ -> {quoted, state}
106
end
129
107
130
- defp accept_patch?(_patch, %{action: :accept}), do: true
131
- defp accept_patch?(_patch, %{action: :reject}), do: false
132
-
133
- defp accept_patch(state, patch, assertion) do
134
- update_in(state.files[assertion.context.file].accepted, &[patch | &1])
135
- end
136
-
137
- defp reject_patch(state, patch, assertion) do
138
- update_in(state.files[assertion.context.file].rejected, &[patch | &1])
108
quoted, state ->
109
{quoted, state}
110
end)
139
111
end
140
112
end
changed lib/mneme/prompter.ex
 
@@ -1,7 1,7 @@
1
1
defmodule Mneme.Prompter do
2
- @moduledoc """
3
- Behaviour controlling how to prompt acceptance or rejection of an assertion patch.
4
- """
2
# Behaviour controlling how to prompt acceptance or rejection of an
3
# assertion patch. This behaviour may become public in the future.
4
@moduledoc false
5
5
6
- @callback prompt!(patch :: map()) :: boolean()
6
@callback prompt!(Rewrite.Source.t(), assertion :: term()) :: boolean()
7
7
end
changed lib/mneme/prompter/terminal.ex
 
@@ -1,24 1,16 @@
1
1
defmodule Mneme.Prompter.Terminal do
2
- @moduledoc """
3
- Default terminal-based prompter.
4
- """
2
@moduledoc false
5
3
6
4
@behaviour Mneme.Prompter
7
5
8
6
import Owl.Data, only: [tag: 2]
9
7
10
8
alias Mneme.Assertion
9
alias Rewrite.Source
11
10
12
11
@impl true
13
- def prompt!(patch) do
14
- %{
15
- original: %{type: type, context: context} = original,
16
- replacement: replacement,
17
- format_opts: format_opts
18
- } = patch
19
-
20
- original_source = Assertion.format(original, format_opts)
21
- replacement_source = Assertion.format(replacement, format_opts)
12
def prompt!(%Source{} = source, %Assertion{} = assertion) do
13
%{type: type, context: context, pattern_notes: notes} = assertion
22
14
23
15
prefix = tag("│ ", :light_black)
24
16
 
@@ -29,8 21,8 @@ defmodule Mneme.Prompter.Terminal do
29
21
"\n",
30
22
file_tag(context),
31
23
"\n\n",
32
- diff(original_source, replacement_source),
33
- notes_tag(replacement.pattern_notes)
24
diff(source),
25
notes_tag(notes)
34
26
]
35
27
|> Owl.Data.add_prefix(prefix)
36
28
 
@@ -43,15 35,20 @@ defmodule Mneme.Prompter.Terminal do
43
35
Owl.IO.confirm(message: prompt)
44
36
end
45
37
46
- defp diff(old, new) do
47
- Rewrite.TextDiff.format(old, new,
38
defp diff(source) do
39
Rewrite.TextDiff.format(
40
source |> Source.code(Source.version(source) - 1) |> eof_newline(),
41
source |> Source.code() |> eof_newline(),
48
42
line_numbers: false,
49
- before: 0,
50
- after: 0,
51
- format: [separator: " ", gutter: [eq: " ", ins: " ", del: " -", skip: "..."]]
43
format: [
44
separator: "",
45
gutter: [eq: " ", ins: " ", del: " - ", skip: "..."]
46
]
52
47
)
53
48
end
54
49
50
defp eof_newline(code), do: String.trim_trailing(code) <> "\n"
51
55
52
defp file_tag(%{file: file, line: line} = _context) do
56
53
path = Path.relative_to_cwd(file)
57
54
tag([path, ":", to_string(line)], :light_black)
added lib/mneme/utils.ex
 
@@ -0,0 1,11 @@
1
defmodule Mneme.Utils do
2
@moduledoc false
3
4
@doc """
5
Returns the formatter options for the given file.
6
"""
7
def formatter_opts(file \\ nil) do
8
{_formatter, opts} = Mix.Tasks.Format.formatter_for_file(file || __ENV__.file)
9
opts
10
end
11
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.0.2"
7
def version, do: "0.0.3"
8
8
9
9
def project do
10
10
[
 
@@ -40,7 40,7 @@ defmodule Mneme.MixProject do
40
40
{:owl, "~> 0.6.0"},
41
41
{:nimble_options, "~> 0.5.2"},
42
42
{:sourceror, "~> 0.12"},
43
- {:rewrite, "0.4.0"},
43
{:rewrite, "~> 0.5.0"},
44
44
45
45
# Development
46
46
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
 
@@ -84,7 84,7 @@ defmodule Mneme.MixProject do
84
84
defp dialyzer do
85
85
[
86
86
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
87
- plt_add_apps: [:mix],
87
plt_add_apps: [:mix, :ex_unit],
88
88
flags: [
89
89
:underspecs,
90
90
:extra_return,
unknown priv/plts/dialyzer.plt
CANNOT RENDER FILES LARGER THAN 1MB
added priv/plts/dialyzer.plt.hash
 
@@ -0,0 1 @@
1
�����MP�3����k�!��
\ No newline at end of file