1Code.require_file("../../test_helper.exs", __DIR__)
2
3defmodule Mix.Tasks.FormatTest do
4  use MixTest.Case
5
6  import ExUnit.CaptureIO
7
8  defmodule FormatWithDepsApp do
9    def project do
10      [
11        app: :format_with_deps,
12        version: "0.1.0",
13        deps: [{:my_dep, "0.1.0", path: "deps/my_dep"}]
14      ]
15    end
16  end
17
18  test "formats the given files", context do
19    in_tmp(context.test, fn ->
20      File.write!("a.ex", """
21      foo bar
22      """)
23
24      Mix.Tasks.Format.run(["a.ex"])
25
26      assert File.read!("a.ex") == """
27             foo(bar)
28             """
29    end)
30  end
31
32  test "formats the given pattern", context do
33    in_tmp(context.test, fn ->
34      File.write!("a.ex", """
35      foo bar
36      """)
37
38      Mix.Tasks.Format.run(["*.ex", "a.ex"])
39
40      assert File.read!("a.ex") == """
41             foo(bar)
42             """
43    end)
44  end
45
46  test "is a no-op if the file is already formatted", context do
47    in_tmp(context.test, fn ->
48      File.write!("a.ex", """
49      foo(bar)
50      """)
51
52      File.touch!("a.ex", {{2010, 1, 1}, {0, 0, 0}})
53      Mix.Tasks.Format.run(["a.ex"])
54      assert File.stat!("a.ex").mtime == {{2010, 1, 1}, {0, 0, 0}}
55    end)
56  end
57
58  test "does not write file to disk on dry-run", context do
59    in_tmp(context.test, fn ->
60      File.write!("a.ex", """
61      foo bar
62      """)
63
64      Mix.Tasks.Format.run(["a.ex", "--dry-run"])
65
66      assert File.read!("a.ex") == """
67             foo bar
68             """
69    end)
70  end
71
72  test "reads file from stdin and prints to stdout", context do
73    in_tmp(context.test, fn ->
74      File.write!("a.ex", """
75      foo bar
76      """)
77
78      output =
79        capture_io("foo( )", fn ->
80          Mix.Tasks.Format.run(["a.ex", "-"])
81        end)
82
83      assert output == """
84             foo()
85             """
86
87      assert File.read!("a.ex") == """
88             foo(bar)
89             """
90    end)
91  end
92
93  test "reads file from stdin and prints to stdout with formatter", context do
94    in_tmp(context.test, fn ->
95      File.write!(".formatter.exs", """
96      [locals_without_parens: [foo: 1]]
97      """)
98
99      output =
100        capture_io("foo :bar", fn ->
101          Mix.Tasks.Format.run(["-"])
102        end)
103
104      assert output == """
105             foo :bar
106             """
107    end)
108  end
109
110  test "checks if file is formatted with --check-formatted", context do
111    in_tmp(context.test, fn ->
112      File.write!("a.ex", """
113      foo bar
114      """)
115
116      assert_raise Mix.Error, ~r"mix format failed due to --check-formatted", fn ->
117        Mix.Tasks.Format.run(["a.ex", "--check-formatted"])
118      end
119
120      assert File.read!("a.ex") == """
121             foo bar
122             """
123
124      assert Mix.Tasks.Format.run(["a.ex"]) == :ok
125      assert Mix.Tasks.Format.run(["a.ex", "--check-formatted"]) == :ok
126
127      assert File.read!("a.ex") == """
128             foo(bar)
129             """
130    end)
131  end
132
133  test "checks if stdin is formatted with --check-formatted" do
134    assert_raise Mix.Error, ~r"mix format failed due to --check-formatted", fn ->
135      capture_io("foo( )", fn ->
136        Mix.Tasks.Format.run(["--check-formatted", "-"])
137      end)
138    end
139
140    output =
141      capture_io("foo()\n", fn ->
142        Mix.Tasks.Format.run(["--check-formatted", "-"])
143      end)
144
145    assert output == ""
146  end
147
148  test "uses inputs and configuration from .formatter.exs", context do
149    in_tmp(context.test, fn ->
150      File.write!(".formatter.exs", """
151      [
152        inputs: ["a.ex"],
153        locals_without_parens: [foo: 1]
154      ]
155      """)
156
157      File.write!("a.ex", """
158      foo bar baz
159      """)
160
161      Mix.Tasks.Format.run([])
162
163      assert File.read!("a.ex") == """
164             foo bar(baz)
165             """
166    end)
167  end
168
169  test "expands patterns in inputs from .formatter.exs", context do
170    in_tmp(context.test, fn ->
171      File.write!(".formatter.exs", """
172      [
173        inputs: ["{a,.b}.ex"]
174      ]
175      """)
176
177      File.write!("a.ex", """
178      foo bar
179      """)
180
181      File.write!(".b.ex", """
182      foo bar
183      """)
184
185      Mix.Tasks.Format.run([])
186
187      assert File.read!("a.ex") == """
188             foo(bar)
189             """
190
191      assert File.read!(".b.ex") == """
192             foo(bar)
193             """
194    end)
195  end
196
197  defmodule Elixir.SigilWPlugin do
198    @behaviour Mix.Tasks.Format
199
200    def features(opts) do
201      assert opts[:from_formatter_exs] == :yes
202      [sigils: [:W]]
203    end
204
205    def format(contents, opts) do
206      assert opts[:from_formatter_exs] == :yes
207      assert opts[:sigil] == :W
208      assert opts[:modifiers] == 'abc'
209      contents |> String.split(~r/\s/) |> Enum.join("\n")
210    end
211  end
212
213  test "uses sigil plugins from .formatter.exs", context do
214    in_tmp(context.test, fn ->
215      File.write!(".formatter.exs", """
216      [
217        inputs: ["a.ex"],
218        plugins: [SigilWPlugin],
219        from_formatter_exs: :yes
220      ]
221      """)
222
223      File.write!("a.ex", """
224      if true do
225        ~W'''
226        foo bar baz
227        '''abc
228      end
229      """)
230
231      Mix.Tasks.Format.run([])
232
233      assert File.read!("a.ex") == """
234             if true do
235               ~W'''
236               foo
237               bar
238               baz
239               '''abc
240             end
241             """
242    end)
243  end
244
245  defmodule Elixir.ExtensionWPlugin do
246    @behaviour Mix.Tasks.Format
247
248    def features(opts) do
249      assert opts[:from_formatter_exs] == :yes
250      [extensions: ~w(.w)]
251    end
252
253    def format(contents, opts) do
254      assert opts[:from_formatter_exs] == :yes
255      assert opts[:extension] == ".w"
256      contents |> String.split(~r/\s/) |> Enum.join("\n")
257    end
258  end
259
260  test "uses extension plugins from .formatter.exs", context do
261    in_tmp(context.test, fn ->
262      File.write!(".formatter.exs", """
263      [
264        inputs: ["a.w"],
265        plugins: [ExtensionWPlugin],
266        from_formatter_exs: :yes
267      ]
268      """)
269
270      File.write!("a.w", """
271      foo bar baz
272      """)
273
274      Mix.Tasks.Format.run([])
275
276      assert File.read!("a.w") == """
277             foo
278             bar
279             baz
280             """
281    end)
282  end
283
284  test "uses inputs and configuration from --dot-formatter", context do
285    in_tmp(context.test, fn ->
286      File.write!("custom_formatter.exs", """
287      [
288        inputs: ["a.ex"],
289        locals_without_parens: [foo: 1]
290      ]
291      """)
292
293      File.write!("a.ex", """
294      foo bar baz
295      """)
296
297      Mix.Tasks.Format.run(["--dot-formatter", "custom_formatter.exs"])
298
299      assert File.read!("a.ex") == """
300             foo bar(baz)
301             """
302    end)
303  end
304
305  test "can read exported configuration from subdirectories", context do
306    in_tmp(context.test, fn ->
307      File.write!(".formatter.exs", """
308      [subdirectories: ["lib"]]
309      """)
310
311      File.mkdir_p!("lib")
312
313      File.write!("lib/.formatter.exs", """
314      [inputs: "a.ex", locals_without_parens: [my_fun: 2]]
315      """)
316
317      {formatter, formatter_opts} = Mix.Tasks.Format.formatter_for_file("lib/extra/a.ex")
318      assert Keyword.get(formatter_opts, :locals_without_parens) == [my_fun: 2]
319      assert formatter.("my_fun 1, 2") == "my_fun 1, 2\n"
320
321      File.write!("lib/a.ex", """
322      my_fun :foo, :bar
323      other_fun :baz
324      """)
325
326      Mix.Tasks.Format.run([])
327
328      assert File.read!("lib/a.ex") == """
329             my_fun :foo, :bar
330             other_fun(:baz)
331             """
332
333      Mix.Tasks.Format.run(["lib/a.ex"])
334
335      assert File.read!("lib/a.ex") == """
336             my_fun :foo, :bar
337             other_fun(:baz)
338             """
339
340      # No caching without a project
341      manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter")
342      refute File.regular?(manifest_path)
343
344      # Caching with a project
345      Mix.Project.push(__MODULE__.FormatWithDepsApp)
346      Mix.Tasks.Format.run(["lib/a.ex"])
347      manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter")
348      assert File.regular?(manifest_path)
349
350      # Let's check that the manifest gets updated if it's stale.
351      File.touch!(manifest_path, {{2010, 1, 1}, {0, 0, 0}})
352
353      Mix.Tasks.Format.run(["lib/a.ex"])
354      assert File.stat!(manifest_path).mtime > {{2010, 1, 1}, {0, 0, 0}}
355    end)
356  end
357
358  test "can read exported configuration from dependencies", context do
359    in_tmp(context.test, fn ->
360      Mix.Project.push(__MODULE__.FormatWithDepsApp)
361
362      File.write!(".formatter.exs", """
363      [import_deps: [:my_dep]]
364      """)
365
366      File.write!("a.ex", """
367      my_fun :foo, :bar
368      """)
369
370      File.mkdir_p!("deps/my_dep/")
371
372      File.write!("deps/my_dep/.formatter.exs", """
373      [export: [locals_without_parens: [my_fun: 2]]]
374      """)
375
376      Mix.Tasks.Format.run(["a.ex"])
377
378      assert File.read!("a.ex") == """
379             my_fun :foo, :bar
380             """
381
382      manifest_path = Path.join(Mix.Project.manifest_path(), "cached_dot_formatter")
383      assert File.regular?(manifest_path)
384
385      # Let's check that the manifest gets updated if it's stale.
386      File.touch!(manifest_path, {{2010, 1, 1}, {0, 0, 0}})
387
388      {_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("a.ex")
389      assert [my_fun: 2] = Keyword.get(formatter_opts, :locals_without_parens)
390
391      Mix.Tasks.Format.run(["a.ex"])
392      assert File.stat!(manifest_path).mtime > {{2010, 1, 1}, {0, 0, 0}}
393    end)
394  end
395
396  test "can read exported configuration from dependencies and subdirectories", context do
397    in_tmp(context.test, fn ->
398      Mix.Project.push(__MODULE__.FormatWithDepsApp)
399
400      File.mkdir_p!("deps/my_dep/")
401
402      File.write!("deps/my_dep/.formatter.exs", """
403      [export: [locals_without_parens: [my_fun: 2]]]
404      """)
405
406      File.mkdir_p!("lib/sub")
407      File.mkdir_p!("lib/not_used_and_wont_raise")
408
409      File.write!(".formatter.exs", """
410      [subdirectories: ["lib"]]
411      """)
412
413      File.write!("lib/.formatter.exs", """
414      [subdirectories: ["*"]]
415      """)
416
417      File.write!("lib/sub/.formatter.exs", """
418      [inputs: "a.ex", import_deps: [:my_dep]]
419      """)
420
421      File.write!("lib/sub/a.ex", """
422      my_fun :foo, :bar
423      other_fun :baz
424      """)
425
426      Mix.Tasks.Format.run([])
427
428      assert File.read!("lib/sub/a.ex") == """
429             my_fun :foo, :bar
430             other_fun(:baz)
431             """
432
433      Mix.Tasks.Format.run(["lib/sub/a.ex"])
434
435      assert File.read!("lib/sub/a.ex") == """
436             my_fun :foo, :bar
437             other_fun(:baz)
438             """
439
440      # Update .formatter.exs, check that file is updated
441      File.write!("lib/sub/.formatter.exs", """
442      [inputs: "a.ex"]
443      """)
444
445      File.touch!("lib/sub/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}})
446      Mix.Tasks.Format.run([])
447
448      assert File.read!("lib/sub/a.ex") == """
449             my_fun(:foo, :bar)
450             other_fun(:baz)
451             """
452
453      # Add a new entry to "lib" and it also gets picked.
454      File.mkdir_p!("lib/extra")
455
456      File.write!("lib/extra/.formatter.exs", """
457      [inputs: "a.ex", locals_without_parens: [other_fun: 1]]
458      """)
459
460      File.write!("lib/extra/a.ex", """
461      my_fun :foo, :bar
462      other_fun :baz
463      """)
464
465      File.touch!("lib/extra/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}})
466      Mix.Tasks.Format.run([])
467
468      {_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("lib/extra/a.ex")
469      assert [other_fun: 1] = Keyword.get(formatter_opts, :locals_without_parens)
470
471      assert File.read!("lib/extra/a.ex") == """
472             my_fun(:foo, :bar)
473             other_fun :baz
474             """
475    end)
476  end
477
478  test "validates subdirectories in :subdirectories", context do
479    in_tmp(context.test, fn ->
480      File.write!(".formatter.exs", """
481      [subdirectories: "oops"]
482      """)
483
484      message = "Expected :subdirectories to return a list of directories, got: \"oops\""
485      assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end
486
487      File.write!(".formatter.exs", """
488      [subdirectories: ["lib"]]
489      """)
490
491      File.mkdir_p!("lib")
492
493      File.write!("lib/.formatter.exs", """
494      []
495      """)
496
497      message = "Expected :inputs or :subdirectories key in lib/.formatter.exs"
498      assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end
499    end)
500  end
501
502  test "validates dependencies in :import_deps", context do
503    in_tmp(context.test, fn ->
504      Mix.Project.push(__MODULE__.FormatWithDepsApp)
505
506      File.write!(".formatter.exs", """
507      [import_deps: [:my_dep]]
508      """)
509
510      message =
511        "Unavailable dependency :my_dep given to :import_deps in the formatter configuration. " <>
512          "The dependency cannot be found in the file system, please run \"mix deps.get\" and try again"
513
514      assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end
515
516      File.write!(".formatter.exs", """
517      [import_deps: [:nonexistent_dep]]
518      """)
519
520      message =
521        "Unknown dependency :nonexistent_dep given to :import_deps in the formatter configuration. " <>
522          "The dependency is not listed in your mix.exs for environment :dev"
523
524      assert_raise Mix.Error, message, fn -> Mix.Tasks.Format.run([]) end
525    end)
526  end
527
528  test "prints an error on conflicting .formatter.exs files", context do
529    in_tmp(context.test, fn ->
530      File.write!(".formatter.exs", """
531      [inputs: "lib/**/*.{ex,exs}", subdirectories: ["lib", "foo"]]
532      """)
533
534      File.mkdir_p!("lib")
535
536      File.write!("lib/.formatter.exs", """
537      [inputs: "a.ex", locals_without_parens: [my_fun: 2]]
538      """)
539
540      File.mkdir_p!("foo")
541
542      File.write!("foo/.formatter.exs", """
543      [inputs: "../lib/a.ex", locals_without_parens: [my_fun: 2]]
544      """)
545
546      File.write!("lib/a.ex", """
547      my_fun :foo, :bar
548      other_fun :baz
549      """)
550
551      Mix.Tasks.Format.run([])
552
553      message1 =
554        "Both .formatter.exs and lib/.formatter.exs specify the file lib/a.ex in their " <>
555          ":inputs option. To resolve the conflict, the configuration in .formatter.exs " <>
556          "will be ignored. Please change the list of :inputs in one of the formatter files " <>
557          "so only one of them matches lib/a.ex"
558
559      message2 =
560        "Both lib/.formatter.exs and foo/.formatter.exs specify the file lib/a.ex in their " <>
561          ":inputs option. To resolve the conflict, the configuration in lib/.formatter.exs " <>
562          "will be ignored. Please change the list of :inputs in one of the formatter files " <>
563          "so only one of them matches lib/a.ex"
564
565      assert_received {:mix_shell, :error, [^message1]}
566      assert_received {:mix_shell, :error, [^message2]}
567    end)
568  end
569
570  test "raises on invalid arguments", context do
571    in_tmp(context.test, fn ->
572      assert_raise Mix.Error, ~r"Expected one or more files\/patterns to be given", fn ->
573        Mix.Tasks.Format.run([])
574      end
575
576      assert_raise Mix.Error, ~r"Could not find a file to format", fn ->
577        Mix.Tasks.Format.run(["unknown.whatever"])
578      end
579    end)
580  end
581
582  test "raises SyntaxError when parsing invalid source file", context do
583    in_tmp(context.test, fn ->
584      File.write!("a.ex", """
585      defmodule <%= module %>.Bar do end
586      """)
587
588      assert_raise SyntaxError, ~r"a.ex:1:13: syntax error before: '='", fn ->
589        Mix.Tasks.Format.run(["a.ex"])
590      end
591
592      assert_received {:mix_shell, :error, ["mix format failed for file: a.ex"]}
593    end)
594  end
595
596  test "raises SyntaxError when parsing invalid stdin", context do
597    in_tmp(context.test, fn ->
598      assert_raise SyntaxError, ~r"stdin:1:13: syntax error before: '='", fn ->
599        capture_io("defmodule <%= module %>.Bar do end", fn ->
600          Mix.Tasks.Format.run(["-"])
601        end)
602      end
603
604      assert_received {:mix_shell, :error, ["mix format failed for stdin"]}
605    end)
606  end
607end
608