├── test ├── test_helper.exs └── muzak │ ├── config_test.exs │ ├── mutations_test.exs │ ├── mutators │ ├── constants │ │ ├── numbers_test.exs │ │ └── strings_test.exs │ └── functions │ │ └── rename_test.exs │ ├── formatter_test.exs │ └── runner_test.exs ├── .tool-versions ├── .formatter.exs ├── lib ├── muzak.ex ├── mix │ └── tasks │ │ └── muzak.ex └── muzak │ ├── mutators │ ├── functions │ │ └── rename.ex │ ├── constants │ │ ├── strings.ex │ │ └── numbers.ex │ └── mutator.ex │ ├── config.ex │ ├── mutations.ex │ ├── runner.ex │ ├── formatter.ex │ └── code │ └── formatter.ex ├── .gitignore ├── README.md ├── docs ├── mutators.md ├── muzak.md └── why_mutation_testing.md ├── mix.exs ├── mix.lock └── LICENSE /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 25.1.1 2 | elixir 1.14.1-otp-25 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/muzak.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak do 2 | @moduledoc false 3 | 4 | def run(args) do 5 | args 6 | |> Muzak.Config.setup() 7 | |> Muzak.Runner.run_test_loop() 8 | |> Muzak.Formatter.print_report() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/mix/tasks/muzak.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Muzak do 2 | @moduledoc false 3 | 4 | @shortdoc "Run mutation tests" 5 | use Mix.Task 6 | 7 | @preferred_cli_env :test 8 | 9 | @impl true 10 | def run(args) do 11 | Muzak.run(args) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/muzak/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.ConfigTest do 2 | use ExUnit.Case, async: true 3 | 4 | describe "setup/1" do 5 | test "throws an exception when the file passed to `--only` doesn't exist" do 6 | path = "/does/not/exist.ex" 7 | args = ["--only", path] 8 | msg = "file `#{path}` passed as argument to `--only` does not exist" 9 | assert_raise RuntimeError, msg, fn -> Muzak.Config.setup(args) end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | /apps/**/_build/ 4 | 5 | # If you run "mix test --cover", coverage assets end up here. 6 | /cover/ 7 | 8 | # The directory Mix downloads your dependencies sources to. 9 | /deps/ 10 | /apps/**/deps/ 11 | 12 | # Where third-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | 24 | # Ignore package tarball (built via "mix hex.build"). 25 | muzak-*.tar 26 | 27 | /apps/**/.elixir_ls 28 | .elixir_ls/ 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Muzak 2 | 3 | Mutation testing for Elixir! 4 | 5 | ## Getting Started 6 | 7 | To get started with mutation testing, first add `muzak` as a dependency in your `mix.exs` file and 8 | set the `preferred_cli_env` for `muzak` to `test`: 9 | 10 | ```elixir 11 | defmodule MyApp.Mixfile do 12 | def project do 13 | [ 14 | # ... 15 | preferred_cli_env: [muzak: :test] 16 | ] 17 | end 18 | 19 | # ... 20 | 21 | defp deps do 22 | [ 23 | # ... 24 | {:muzak, "~> 1.1", only: :test} 25 | ] 26 | end 27 | end 28 | ``` 29 | 30 | You're now ready to get started! 31 | 32 | ```bash 33 | $ mix deps.get 34 | $ mix muzak 35 | ``` 36 | 37 | Muzak will then randomly generate up to 1000 mutations in your application and run your test suite 38 | against each of them. If your application contains more than 1000 possible mutations, then you may 39 | see different results for subsequent runs. 40 | -------------------------------------------------------------------------------- /lib/muzak/mutators/functions/rename.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Functions.Rename do 2 | @moduledoc false 3 | # Mutator for changing the names of modules 4 | 5 | use Muzak.Mutators.Mutator 6 | 7 | def mutate(ast, name_fun) do 8 | Macro.postwalk(ast, [], fn node, acc -> 9 | case mutate(node, ast, name_fun) do 10 | :no_mutation -> {node, acc} 11 | mutation -> {node, [mutation | acc]} 12 | end 13 | end) 14 | |> elem(1) 15 | end 16 | 17 | @ops [:def, :defp, :defmacro, :defmacrop] 18 | 19 | @doc false 20 | def mutate({op, meta, [{_, fun_meta, args}, impl]} = node, ast, name_fun) when op in @ops do 21 | name = String.to_atom(name_fun.(:string)) 22 | mutation = {op, meta, [{name, fun_meta, args}, impl]} 23 | replacement = replace_node(ast, node, mutation) 24 | %{mutated_ast: replacement, line: meta[:line], example_format: :line} 25 | end 26 | 27 | def mutate(_, _, _) do 28 | :no_mutation 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /docs/mutators.md: -------------------------------------------------------------------------------- 1 | # Included Mutators 2 | 3 | Muzak includes only three mutators - `Constants.Numbers`, `Constants.Strings` and 4 | `Functions.Rename`. While this is indeed rather limited, it should give some examples of the 5 | benefits of mutation testing. 6 | 7 | ## `Constants.Numbers` 8 | 9 | Mutates any `Integer` or `Float` literals. 10 | 11 | #### Original File 12 | ``` 13 | def do_math(num), do: num + 2 - 0.38 14 | ``` 15 | 16 | #### Generated mutations 17 | ``` 18 | def do_math(num), do: num + 327_237_729 - 0.38 19 | ``` 20 | 21 | ``` 22 | def do_math(num), do: num + 2 - 392_763.216_279 23 | ``` 24 | 25 | ## `Constants.Strings` 26 | 27 | Mutates any string literals. 28 | 29 | #### Original File 30 | ``` 31 | def append(str), do: str <> " added on " <> " a string." 32 | ``` 33 | 34 | #### Generated mutations 35 | ``` 36 | def append(str), do: str <> "random_string" <> " a string." 37 | ``` 38 | 39 | ``` 40 | def append(str), do: str <> " added on " <> "random_string" 41 | ``` 42 | 43 | ## `Functions.Rename` 44 | 45 | Renames any function or macro definition. 46 | 47 | #### Original File 48 | ``` 49 | def add_one(int), do: do_add_one(int) 50 | 51 | defp do_add_one(int), do: int + 1 52 | ``` 53 | 54 | #### Generated mutations 55 | ``` 56 | def random_function_name(int), do: do_add_one(int) 57 | 58 | defp do_add_one(int), do: int + 1 59 | ``` 60 | 61 | ``` 62 | def add_one(int), do: do_add_one(int) 63 | 64 | defp random_function_name(int), do: int + 1 65 | ``` 66 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.1.1" 5 | 6 | @source_url "https://github.com/devonestes/muzak" 7 | @homepage_url "https://devonestes.com/muzak" 8 | 9 | def project do 10 | [ 11 | app: :muzak, 12 | description: "Mutation testing for Elixir", 13 | elixir: "~> 1.10", 14 | version: @version, 15 | source_url: @source_url, 16 | homepage_url: @homepage_url, 17 | elixirc_paths: ["lib"], 18 | start_permanent: false, 19 | deps: [{:assertions, ">= 0.0.0", only: :test}, {:ex_doc, ">= 0.0.0", only: :dev}], 20 | docs: docs(), 21 | package: [ 22 | maintainers: ["Devon Estes"], 23 | licenses: ["CC-BY-NC-ND-4.0"], 24 | links: %{ 25 | "GitHub" => @source_url, 26 | "Website" => @homepage_url 27 | }, 28 | files: ["lib", "mix.exs", "LICENSE"] 29 | ] 30 | ] 31 | end 32 | 33 | def application, do: [extra_applications: [:logger]] 34 | 35 | defp docs() do 36 | [ 37 | main: "muzak", 38 | api_reference: false, 39 | source_ref: @version, 40 | extras: [ 41 | "docs/muzak.md": [filename: "muzak", title: "Muzak"], 42 | "docs/mutators.md": [filename: "mutators", title: "Included mutators"], 43 | "docs/why_mutation_testing.md": [ 44 | filename: "what_is_mutation_testing", 45 | title: "What is mutation testing?" 46 | ] 47 | ], 48 | authors: ["Devon Estes"] 49 | ] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "assertions": {:hex, :assertions, "0.18.1", "a7e1e0c65dacac7d6c010345f2831e63e1897ba0c028c608fc508677c662d3d4", [:mix], [], "hexpm", "3b0479514b8f5ef5cb2a193202e792f60f87554a72d3b9c6fd791da0d81ae1e1"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 4 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 5 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 8 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 9 | } 10 | -------------------------------------------------------------------------------- /lib/muzak/mutators/constants/strings.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Constants.Strings do 2 | @moduledoc false 3 | 4 | # Mutator changing all hard coded strings. 5 | 6 | use Muzak.Mutators.Mutator 7 | 8 | def mutate(ast, name_fun) do 9 | ast 10 | |> strip_typespecs() 11 | |> Macro.postwalk([], fn node, acc -> 12 | case mutate(node, ast, name_fun) do 13 | :no_mutation -> {node, acc} 14 | mutations -> {node, mutations ++ acc} 15 | end 16 | end) 17 | |> elem(1) 18 | |> Enum.uniq() 19 | end 20 | 21 | def mutate({:__block__, meta, [arg]} = node, ast, rand_func) when is_binary(arg) do 22 | string = rand_func.(:string) 23 | 24 | replacement = {:__block__, meta, [string]} 25 | 26 | [ 27 | %{ 28 | mutated_ast: replace_node(ast, node, replacement), 29 | line: meta[:line], 30 | example_format: :line 31 | } 32 | ] 33 | end 34 | 35 | def mutate({:<<>>, meta, args} = node, ast, rand_func) when is_list(args) do 36 | args 37 | |> Enum.with_index() 38 | |> Enum.reduce([], fn 39 | {arg, idx}, acc when is_binary(arg) -> 40 | replacment_args = List.replace_at(args, idx, rand_func.(:string)) 41 | replacement = {:<<>>, meta, replacment_args} 42 | 43 | [ 44 | %{ 45 | mutated_ast: replace_node(ast, node, replacement), 46 | line: meta[:line], 47 | example_format: :line 48 | } 49 | | acc 50 | ] 51 | 52 | _, acc -> 53 | acc 54 | end) 55 | end 56 | 57 | def mutate(_, _, _) do 58 | :no_mutation 59 | end 60 | 61 | @to_strip [:doc, :moduledoc, :typedoc] 62 | defp strip_typespecs(ast) do 63 | Macro.postwalk(ast, fn node -> 64 | if match?({:@, _, [{op, _, _} | _]} when op in @to_strip, node) do 65 | {:@, [], []} 66 | else 67 | node 68 | end 69 | end) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/muzak/mutators/constants/numbers.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Constants.Numbers do 2 | @moduledoc false 3 | 4 | # Mutator changing all hard coded numbers. 5 | 6 | use Muzak.Mutators.Mutator 7 | 8 | def mutate(ast, name_fun) do 9 | tagged = tag_nodes(ast) 10 | 11 | tagged 12 | |> strip_typespecs() 13 | |> Macro.postwalk([], fn node, acc -> 14 | case mutate(node, tagged, name_fun) do 15 | :no_mutation -> {node, acc} 16 | mutation -> {node, [mutation | acc]} 17 | end 18 | end) 19 | |> elem(1) 20 | |> Enum.uniq() 21 | end 22 | 23 | def mutate({:__block__, meta, [arg]} = node, ast, rand_func) when is_number(arg) do 24 | number = arg + rand_func.(:number) 25 | 26 | token = 27 | if is_integer(number) do 28 | number 29 | |> to_string() 30 | |> String.graphemes() 31 | |> Enum.reverse() 32 | |> Enum.chunk_every(3, 3, []) 33 | |> Enum.map(&Enum.reverse/1) 34 | |> Enum.intersperse("_") 35 | |> Enum.reverse() 36 | |> to_string() 37 | else 38 | to_string(number) 39 | end 40 | 41 | replacement = {:__block__, Keyword.put(meta, :token, token), [number]} 42 | 43 | %{ 44 | mutated_ast: strip_unique_tags(replace_node(ast, node, replacement)), 45 | line: meta[:line], 46 | example_format: :line 47 | } 48 | end 49 | 50 | def mutate(_, _, _) do 51 | :no_mutation 52 | end 53 | 54 | @to_strip [:type, :spec, :callback, :impl] 55 | defp strip_typespecs(ast) do 56 | Macro.postwalk(ast, fn node -> 57 | if match?({:@, _, [{op, _, _} | _]} when op in @to_strip, node) do 58 | {:@, [], []} 59 | else 60 | node 61 | end 62 | end) 63 | end 64 | 65 | defp tag_nodes(ast) do 66 | ast 67 | |> Macro.postwalk(0, fn 68 | {op, meta, args} = node, tag when is_ast_op(node) -> 69 | {{op, Keyword.put(meta, :unique_tag, tag), args}, tag + 1} 70 | 71 | node, tag -> 72 | {node, tag} 73 | end) 74 | |> elem(0) 75 | end 76 | 77 | def strip_unique_tags(ast) do 78 | Macro.postwalk(ast, fn 79 | {op, meta, args} = node when is_ast_op(node) -> 80 | {op, Keyword.delete(meta, :unique_tag), args} 81 | 82 | node -> 83 | node 84 | end) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/muzak/mutations_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.MutationsTest do 2 | use Assertions.Case, async: true 3 | 4 | alias Muzak.{Mutations, Mutators.Functions.Rename, Mutators.Constants.Numbers} 5 | 6 | describe "mutate_file/2" do 7 | test "applies all possible mutations to the given file for the given mutators" do 8 | file = """ 9 | defmodule TestModule do 10 | def test_fun(var) do 11 | var == :ok 12 | end 13 | end 14 | """ 15 | 16 | expected = %{ 17 | file: 18 | String.trim_trailing(""" 19 | defmodule TestModule do 20 | def randomatom(var) do 21 | var == :ok 22 | end 23 | end 24 | """), 25 | line: 2, 26 | original: ["def", " ", "test_fun", "(", "", "var", "", ")", " do"], 27 | mutation: ["def", " ", "randomatom", "(", "", "var", "", ")", " do"], 28 | original_file: file, 29 | path: "path/to/file.ex" 30 | } 31 | 32 | {file, "path/to/file.ex", Enum.to_list(1..10)} 33 | |> Mutations.mutate_file([Rename], fn _ -> "randomatom" end) 34 | |> assert_lists_equal([expected]) 35 | end 36 | 37 | test "returns multiple mutations on a single line" do 38 | file = """ 39 | defmodule TestModule do 40 | def test_fun(var) do 41 | var in [1, 2] 42 | end 43 | end 44 | """ 45 | 46 | expected1 = %{ 47 | file: 48 | String.trim_trailing(""" 49 | defmodule TestModule do 50 | def test_fun(var) do 51 | var in [32_780, 2] 52 | end 53 | end 54 | """), 55 | line: 3, 56 | mutation: ["var", " in ", "[", "", "32_780", ",", " ", "2", "", "]"], 57 | original: ["var", " in ", "[", "", "1", ",", " ", "2", "", "]"], 58 | original_file: file, 59 | path: "path/to/file.ex" 60 | } 61 | 62 | expected2 = %{ 63 | file: 64 | String.trim_trailing(""" 65 | defmodule TestModule do 66 | def test_fun(var) do 67 | var in [1, 32_781] 68 | end 69 | end 70 | """), 71 | line: 3, 72 | mutation: ["var", " in ", "[", "", "1", ",", " ", "32_781", "", "]"], 73 | original: ["var", " in ", "[", "", "1", ",", " ", "2", "", "]"], 74 | original_file: file, 75 | path: "path/to/file.ex" 76 | } 77 | 78 | {file, "path/to/file.ex", [1, 2, 3, 4, 5]} 79 | |> Mutations.mutate_file([Numbers], &name_fun/1) 80 | |> assert_lists_equal([expected1, expected2]) 81 | end 82 | end 83 | 84 | defp name_fun(_), do: 32_779 85 | end 86 | -------------------------------------------------------------------------------- /test/muzak/mutators/constants/numbers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Constants.NumbersTest do 2 | use Assertions.Case, async: true 3 | 4 | alias Muzak.{Code.Formatter, Mutators.Mutator, Mutators.Constants.Numbers} 5 | 6 | describe "mutate/2" do 7 | test "returns all expected mutations" do 8 | ast = 9 | Formatter.to_ast(""" 10 | defmodule Tester do 11 | @type thing() :: 3 | 4 12 | 13 | @spec test_fun(1 | 2) :: number() 14 | def test_fun(arg) do 15 | 1 + arg - 3.02 / -2 + 0 16 | [name: 0, other: 1] 17 | [9, 8] 18 | end 19 | end 20 | """) 21 | 22 | node1 = {:__block__, [token: "1", line: 6], [1]} 23 | mutation1 = {:__block__, [token: "2", line: 6], [2]} 24 | 25 | node2 = {:__block__, [token: "3.02", line: 6], [3.02]} 26 | mutation2 = {:__block__, [token: "4.02", line: 6], [4.02]} 27 | 28 | node3 = {:__block__, [token: "2", line: 6], [2]} 29 | mutation3 = {:__block__, [token: "3", line: 6], [3]} 30 | 31 | node4 = {:__block__, [token: "0", line: 6], [0]} 32 | mutation4 = {:__block__, [token: "1", line: 6], [1]} 33 | 34 | node5 = {:__block__, [token: "1", line: 7], [1]} 35 | mutation5 = {:__block__, [token: "2", line: 7], [2]} 36 | 37 | node6 = {:__block__, [token: "0", line: 7], [0]} 38 | mutation6 = {:__block__, [token: "1", line: 7], [1]} 39 | 40 | node7 = {:__block__, [token: "9", line: 8], [9]} 41 | mutation7 = {:__block__, [token: "10", line: 8], [10]} 42 | 43 | node8 = {:__block__, [token: "8", line: 8], [8]} 44 | mutation8 = {:__block__, [token: "9", line: 8], [9]} 45 | 46 | ast 47 | |> Numbers.mutate(fn _ -> 1 end) 48 | |> assert_lists_equal([ 49 | %{ 50 | mutated_ast: Mutator.replace_node(ast, node1, mutation1), 51 | line: 6, 52 | example_format: :line 53 | }, 54 | %{ 55 | mutated_ast: Mutator.replace_node(ast, node2, mutation2), 56 | line: 6, 57 | example_format: :line 58 | }, 59 | %{ 60 | mutated_ast: Mutator.replace_node(ast, node3, mutation3), 61 | line: 6, 62 | example_format: :line 63 | }, 64 | %{ 65 | mutated_ast: Mutator.replace_node(ast, node4, mutation4), 66 | line: 6, 67 | example_format: :line 68 | }, 69 | %{ 70 | mutated_ast: Mutator.replace_node(ast, node5, mutation5), 71 | line: 7, 72 | example_format: :line 73 | }, 74 | %{ 75 | mutated_ast: Mutator.replace_node(ast, node6, mutation6), 76 | line: 7, 77 | example_format: :line 78 | }, 79 | %{ 80 | mutated_ast: Mutator.replace_node(ast, node7, mutation7), 81 | line: 8, 82 | example_format: :line 83 | }, 84 | %{ 85 | mutated_ast: Mutator.replace_node(ast, node8, mutation8), 86 | line: 8, 87 | example_format: :line 88 | } 89 | ]) 90 | end 91 | end 92 | 93 | describe "mutate/3" do 94 | test "returns the correct result (mutation expected part 1)" do 95 | ast = 96 | Formatter.to_ast(""" 97 | defmodule Tester do 98 | def test_fun(arg) do 99 | 1 + arg - 3.02 / -2 + 0 100 | [name: 0..1] 101 | end 102 | end 103 | """) 104 | 105 | node = {:__block__, [token: "1", line: 3], [1]} 106 | 107 | mutation = {:__block__, [token: "792_763", line: 3], [792_763]} 108 | 109 | assert Numbers.mutate(node, ast, fn _ -> 792_762 end) == %{ 110 | mutated_ast: Mutator.replace_node(ast, node, mutation), 111 | line: 3, 112 | example_format: :line 113 | } 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/muzak/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Config do 2 | @moduledoc false 3 | 4 | # Everything around configuration and setup of the application 5 | # 6 | # This is like all side effects, so I'm not even going to try and test it, nor should anything 7 | # else go in this module 8 | 9 | @switches [ 10 | mutations: :integer, 11 | seed: :integer, 12 | only: :string, 13 | profile: :string, 14 | min_coverage: :float 15 | ] 16 | @ex_unit_option_keys [:include, :seed, :max_failures, :formatters, :exclude, :only] 17 | 18 | alias Muzak.{Formatter, Mutations} 19 | 20 | @doc false 21 | # The function to do all our config setup 22 | def setup(args) do 23 | Application.ensure_started(:logger) 24 | Mix.Task.run("compile", args) 25 | Mix.Task.run("app.start", args) 26 | Application.ensure_loaded(:ex_unit) 27 | opts = get_opts(args) 28 | Code.put_compiler_option(:ignore_module_conflict, true) 29 | Formatter.start_link(opts) 30 | 31 | {matched_test_files, test_paths, ex_unit_opts} = configure_ex_unit(opts) 32 | 33 | {matched_test_files, test_paths, ex_unit_opts, Mutations.generate_mutations(opts), opts} 34 | end 35 | 36 | defp get_opts(args) do 37 | {cli_opts, _} = OptionParser.parse!(args, strict: @switches) 38 | 39 | {file_opts, _} = 40 | if File.exists?(".muzak.exs") do 41 | Code.eval_file(".muzak.exs") 42 | else 43 | {%{default: []}, nil} 44 | end 45 | 46 | file_opts = 47 | case cli_opts[:profile] do 48 | nil -> Map.get(file_opts, :default) 49 | profile -> Map.get(file_opts, String.to_atom(profile)) 50 | end 51 | 52 | seed = 53 | 0..9 54 | |> Stream.cycle() 55 | |> Enum.take(600) 56 | |> Enum.take_random(6) 57 | |> Enum.join() 58 | |> String.to_integer() 59 | 60 | debug_config = 61 | if System.get_env("DEBUG") do 62 | [formatters: [ExUnit.CLIFormatter]] 63 | else 64 | [formatters: []] 65 | end 66 | 67 | opts = 68 | [ 69 | mutations: 1_000, 70 | autorun: false, 71 | max_failures: 1, 72 | seed: seed 73 | ] 74 | |> Keyword.merge(debug_config) 75 | |> Keyword.merge(file_opts) 76 | |> Keyword.merge(cli_opts) 77 | 78 | if Keyword.has_key?(opts, :only) do 79 | only = opts[:only] 80 | 81 | {file, line} = 82 | case String.split(only, ":") do 83 | [file, line] -> {file, [String.to_integer(line)]} 84 | _ -> {only, nil} 85 | end 86 | 87 | unless File.exists?(file) do 88 | raise("file `#{file}` passed as argument to `--only` does not exist") 89 | end 90 | 91 | Keyword.put(opts, :mutation_filter, fn _ -> [{file, line}] end) 92 | else 93 | opts 94 | end 95 | end 96 | 97 | def configure_ex_unit(opts) do 98 | shell = Mix.shell() 99 | project = Mix.Project.config() 100 | test_paths = project[:test_paths] || default_test_paths() 101 | test_pattern = project[:test_pattern] || "*_test.exs" 102 | ex_unit_opts = Keyword.take(opts, @ex_unit_option_keys) 103 | 104 | ExUnit.configure(ex_unit_opts) 105 | Enum.each(test_paths, &require_test_helper(shell, &1)) 106 | ExUnit.configure(ex_unit_opts) 107 | 108 | {Mix.Utils.extract_files(test_paths, test_pattern), test_paths, ex_unit_opts} 109 | end 110 | 111 | defp require_test_helper(shell, dir) do 112 | file = Path.join(dir, "test_helper.exs") 113 | 114 | if File.exists?(file) do 115 | Code.unrequire_files([file]) 116 | Code.require_file(file) 117 | else 118 | Mix.shell(shell) 119 | Mix.raise("Cannot run tests because test helper file #{inspect(file)} does not exist") 120 | end 121 | end 122 | 123 | defp default_test_paths do 124 | if File.dir?("test") do 125 | ["test"] 126 | else 127 | [] 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/muzak/mutators/mutator.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Mutator do 2 | @moduledoc false 3 | # Defines the behaviour for a mutator 4 | 5 | @callback mutate(Macro.t(), Macro.t()) :: 6 | :no_mutation 7 | | %{ 8 | mutated_ast: Macro.t(), 9 | line: pos_integer(), 10 | example_format: :line | {:block, pos_integer()} 11 | } 12 | 13 | defmacro __using__(_opts) do 14 | quote do 15 | @behaviour Muzak.Mutators.Mutator 16 | 17 | defguard is_ast_op(tuple) 18 | when tuple_size(tuple) == 3 and 19 | is_atom(elem(tuple, 0)) and 20 | is_list(elem(tuple, 1)) and 21 | is_list(elem(tuple, 2)) 22 | 23 | import Muzak.Mutators.Mutator, only: [replace_node: 3, random_string: 1] 24 | 25 | @doc false 26 | def mutate(ast) do 27 | mutate(ast, &random_string/1) 28 | end 29 | 30 | @impl true 31 | @doc false 32 | def mutate(node, ast) when not is_function(ast) do 33 | mutate(node, ast, &random_string/1) 34 | end 35 | end 36 | end 37 | 38 | defguard is_ast_op(tuple) 39 | when tuple_size(tuple) == 3 and 40 | is_atom(elem(tuple, 0)) and 41 | is_list(elem(tuple, 1)) and 42 | is_list(elem(tuple, 2)) 43 | 44 | @doc false 45 | def to_ast_and_state(string) do 46 | Muzak.Code.Formatter.to_forms_and_state(string) 47 | end 48 | 49 | @doc false 50 | def replace_node(ast, node, replacement) do 51 | Macro.postwalk(ast, fn 52 | ^node -> replacement 53 | other_node -> other_node 54 | end) 55 | end 56 | 57 | @random_string ?a..?z |> Stream.cycle() |> Enum.take(520) |> Enum.take_random(10) |> to_string() 58 | @random_number 0..9 59 | |> Stream.cycle() 60 | |> Enum.take(520) 61 | |> Enum.take_random(2) 62 | |> Enum.join() 63 | |> String.to_integer() 64 | 65 | @doc false 66 | def random_string(), do: random_string(:string) 67 | 68 | @doc false 69 | def random_string(:string), do: @random_string 70 | def random_string(:number), do: @random_number 71 | 72 | def line_from_ast(original_file, line) when is_binary(original_file) do 73 | original_file 74 | |> String.split("\n") 75 | |> Enum.at(line - 1) 76 | end 77 | 78 | def line_from_ast(ast, original_file, line) do 79 | line = map_original_line(original_file, line) 80 | 81 | ast 82 | |> Macro.to_string() 83 | |> Code.format_string!() 84 | |> to_string() 85 | |> String.split("\n") 86 | |> Enum.at(line - 1) 87 | end 88 | 89 | defp map_original_line(original_file, original_line) do 90 | {_, node_to_match} = 91 | original_file 92 | |> Code.string_to_quoted!() 93 | |> Macro.postwalk(nil, fn 94 | {_, meta, _} = node, acc -> 95 | if meta[:line] == original_line do 96 | {node, escape_node(node)} 97 | else 98 | {node, acc} 99 | end 100 | 101 | node, acc -> 102 | {node, acc} 103 | end) 104 | 105 | {_, line} = 106 | original_file 107 | |> Code.string_to_quoted!() 108 | |> Macro.to_string() 109 | |> Code.string_to_quoted!() 110 | |> Macro.postwalk(nil, fn 111 | {_, meta, _} = node, nil -> 112 | if node_to_match == escape_node(node) do 113 | {node, meta[:line]} 114 | else 115 | {node, nil} 116 | end 117 | 118 | node, acc -> 119 | {node, acc} 120 | end) 121 | 122 | line 123 | end 124 | 125 | defp escape_node({op, _, args}) do 126 | {escape_node(op), [], escape_node(args)} 127 | end 128 | 129 | defp escape_node(node) when is_list(node) do 130 | Enum.map(node, &escape_node/1) 131 | end 132 | 133 | defp escape_node(node) when is_tuple(node) do 134 | node 135 | |> Tuple.to_list() 136 | |> Enum.map(&escape_node/1) 137 | |> List.to_tuple() 138 | end 139 | 140 | defp escape_node(node) do 141 | node 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /docs/muzak.md: -------------------------------------------------------------------------------- 1 | # Muzak 2 | 3 | Muzak is a basic mutation testing library for Elixir. It is the limited, open source version of 4 | [Muzak Pro](#muzak-pro). If you're not familiar with mutation testing, you can learn more about 5 | it in the [guide here](why_mutation_testing.md). 6 | 7 | ## Getting started 8 | 9 | To get started with mutation testing, first add `muzak` as a dependency in your `mix.exs` file and 10 | set the `preferred_cli_env` for `muzak` to `test`: 11 | 12 | ``` 13 | defmodule MyApp.Mixfile do 14 | def project do 15 | [ 16 | # ... 17 | preferred_cli_env: [muzak: :test] 18 | ] 19 | end 20 | 21 | # ... 22 | 23 | defp deps do 24 | [ 25 | # ... 26 | {:muzak, "~> 1.0", only: :test} 27 | ] 28 | end 29 | end 30 | ``` 31 | 32 | You're now ready to get started! 33 | 34 | ```bash 35 | $ mix deps.get 36 | $ mix muzak 37 | ``` 38 | 39 | Muzak will then randomly generate up to 1000 mutations in your application and run your test suite 40 | against each of them. If your application contains more than 1000 possible mutations, then you may 41 | see different results for subsequent runs. 42 | 43 | ## Configuration 44 | 45 | Configuration in Muzak is limited, but you can do quite a bit with it. If you require additional 46 | configuration options, [Muzak Pro](#muzak-pro) will likely meet all those needs. 47 | 48 | ### CLI Flags 49 | 50 | There are several CLI flags that can be passed to modify the behavior of Muzak as needed. CLI 51 | flags will always override any configuration set in `.muzak.exs`. 52 | 53 | * `--mutations`: The number of mutations to generate. Example: `mix muzak --mutations 30` 54 | * `--seed`: The seed used for randomization. Example: `mix muzak --seed 976276` 55 | * `--profile`: The profile in `.muzak.exs` to use. Example: `mix muzak --profile ci` 56 | * `--min-coverage`: The minimum percentage (0.0-100/0) of mutations that must be found for a run 57 | to be considered "passing" and to exit with a 0 status code. Example: 58 | `mix muzak --min-coverage 85.5` 59 | * `--only`: Restrict mutation generation to a single file or a single line. Examples: `mix muzak 60 | --only path/to/file.ex` or `mix muzak --only path/to/file.ex:12` 61 | 62 | ### `.muzak.exs` Configuration File 63 | 64 | Most of the time you'll want to save your configuration in a `.muzak.exs` file. In this file, 65 | you can create profiles with different sets of configuration for different uses (such as when 66 | running in CI or locally by a developer). Each of these profiles can contain the following keys: 67 | 68 | * `:min_coverage`: The minimum percentage (0.0-100.0) of mutations that must be found for a run 69 | to be considered "passing" and to exit with a 0 status code. Defaults to `0.0` 70 | * `:mutation_filter`: An arity 1 function used to filter files for mutation generation. 71 | This function must return a list of tuples, where the first element in the tuple is the 72 | path to the file, and the second element is `nil` or a list of integers representing line 73 | numbers. The argument passed to the function is a list of the files in your application as 74 | set in the `elixirc_paths` key in your Mix project. 75 | - `{"path/to/file.ex", nil}` will make all possible mutations on all lines in the file. 76 | - `{"path/to/file.ex", [1, 2, 3]}` will make all possible mutations but only on lines 77 | 1, 2 and 3 in the file. 78 | 79 | A `.muzak.exs` file might look something like this: 80 | 81 | ``` 82 | %{ 83 | default: [ 84 | mutation_filter: fn all_project_files -> 85 | all_project_files 86 | |> Enum.reject(&String.starts_with?(&1, "test/")) 87 | |> Enum.filter(&String.ends_with?(&1, ".ex")) 88 | |> Enum.map(&{&1, nil}) 89 | end 90 | ], 91 | ci: [ 92 | mutation_filter: fn all_project_files -> 93 | all_project_files 94 | |> Enum.reject(&String.starts_with?(&1, "test/")) 95 | |> Enum.filter(&String.ends_with?(&1, ".ex")) 96 | |> Enum.map(&{&1, nil}) 97 | end, 98 | min_coverage: 85.5 99 | ] 100 | } 101 | ``` 102 | 103 | ## Muzak Pro 104 | 105 | Muzak Pro is the full-featured, paid version of Muzak. It includes: 106 | 107 | * No limit on the number of generated mutations 108 | * Over a dozen additional mutators 109 | * Far more configuration options 110 | * Parallel execution by spawning multiple BEAM nodes (_experimental_) 111 | * "Analysis Mode" to identify potentially low-value or duplicate tests in your test suite (_coming soon_) 112 | * Enhanced reporting, including HTML reports (_coming soon_) 113 | 114 | Muzak Pro costs $29/month, and [is available now](https://devonestes.com/muzak). 115 | -------------------------------------------------------------------------------- /docs/why_mutation_testing.md: -------------------------------------------------------------------------------- 1 | # What is Mutation Testing? 2 | 3 | Mutation testing is the process of programmatically introducing bugs to an application by mutating 4 | the application's source code and then running that application's test suite. 5 | 6 | ## What can mutation testing do for me? 7 | 8 | Mutation testing can serve several goals: 9 | 10 | 1. Identifying untested code paths 11 | 2. Identifying unused code paths 12 | 3. Identifying tests that never fail 13 | 4. Identifying tests that always fail together (duplicate coverage) 14 | 5. Identifying tests that run slowly and rarely fail (what some consider "low value" tests) 15 | 16 | All of those benefits put together means that mutation testing can help us "test our tests," both 17 | for any tests that might be missing, and to help us reduce time spent running unnecessary or low 18 | value tests. 19 | 20 | ## But isn't mutation testing slow? 21 | 22 | Yes, in its most basic form mutation testing is rather slow. You can make thousands of mutations 23 | in a reasonably sized application, and for each of those mutations you might potentially need to 24 | run your entire test suite. If there were no optimizations to how mutation testing was run, this 25 | would indeed take an unreasonably long time! 26 | 27 | To combat this, Muzak limits the number of mutations generated in each run to 25, and by default 28 | it will stop the test suite at the first failure for each mutation. This, combined with Elixir's 29 | easy ability to run asynchronous tests, makes mutation testing a relatively quick process, but at 30 | the cost of poor coverage for the application that's being tested. 31 | 32 | There are many better ways of reducing mutation testing run time that don't sacrifice nearly as 33 | much mutation coverage, though, and those options are all available in [Muzak 34 | Pro](muzak.md#muzak-pro). 35 | 36 | ## Can I get an example? 37 | 38 | Sure! Imagine that we have the below module: 39 | 40 | ``` 41 | defmodule Authorization do 42 | def user_can_modify?(user, resource) do 43 | user.role in [:admin, :owner] or user.id in resource.member_ids 44 | end 45 | end 46 | ``` 47 | 48 | And for that module, we have these tests: 49 | 50 | ``` 51 | defmodule AuthorizationTest do 52 | use ExUnit.Case, async: true 53 | 54 | describe "user_can_modify?/2" do 55 | test "returns true if the user is a member" do 56 | user = %{id: 1, role: :reader} 57 | resource = %{member_ids: [1]} 58 | assert Authorization.user_can_modify?(user, resource) 59 | end 60 | 61 | test "returns true if the user is an admin" do 62 | user = %{id: 1, role: :admin} 63 | resource = %{member_ids: []} 64 | assert Authorization.user_can_modify?(user, resource) 65 | end 66 | 67 | test "returns false if the user isn't an admin, owner or member" do 68 | user = %{id: 1, role: :reader} 69 | resource = %{member_ids: []} 70 | refute Authorization.user_can_modify?(user, resource) 71 | end 72 | end 73 | end 74 | ``` 75 | 76 | All those tests will pass, and we also have 100% test coverage (if we're measuring by lines of 77 | code executed during our test suite). However, it is still possible to break that code in a way 78 | that _won't_ trigger a failing test by applying the following mutation: 79 | 80 | ``` 81 | # original 82 | user.role in [:admin, :owner] or user.id in resource.member_ids 83 | 84 | # mutation 85 | user.role in [:admin, :random_atom] or user.id in resource.member_ids 86 | ``` 87 | 88 | To resolve this, we would add a test that would catch this mutation: 89 | 90 | ``` 91 | test "returns true if the user is an owner" do 92 | user = %{id: 1, role: :owner} 93 | resource = %{member_ids: []} 94 | assert Authorization.user_can_modify?(user, resource) 95 | end 96 | ``` 97 | 98 | This is just one very simple example of how mutation testing can help, but when it's used 99 | regularly in real applications it can make a _huge_ difference. 100 | 101 | ## Should I try to get 100% coverage? 102 | 103 | No - and in fact, it may not even be possible! There are things that we can't test with a single 104 | automated test suite, such as code that's conditionally executed when running on a given operating 105 | system or in a given environment. There's also the case of "equivalent mutants," where the 106 | mutation that's made satisfies all conditions in the application and therefore doesn't produce a 107 | failing test because the mutation is technically valid. This _might_ mean that your application 108 | could be improved in some way, but this is often more of a hassle than it's worth. 109 | 110 | Of course test coverage needs vary from application to application, but 95% coverage is a 111 | reasonable goal that most applications can achieve. 112 | -------------------------------------------------------------------------------- /test/muzak/formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.FormatterTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Muzak.Formatter 5 | 6 | alias ExUnit.CaptureIO 7 | 8 | describe "do_print_report/1" do 9 | test "prints the right thing for a :noop" do 10 | assert CaptureIO.capture_io(fn -> 11 | Formatter.do_print_report({:noop, 123, 123_000, 0.0, [seed: 632_719]}) 12 | end) == """ 13 | 14 | 15 | Finished in 0.1 seconds (0.00s async, 0.1s sync) 16 | Something went wrong - tests did not run for a later set of mutations! 17 | 18 | """ 19 | end 20 | 21 | test "prints the right thing for :no_tests_run" do 22 | assert CaptureIO.capture_io(fn -> 23 | Formatter.do_print_report({:no_tests_run, 123, 123_000, 0.0, [seed: 632_719]}) 24 | end) == """ 25 | 26 | 27 | Finished in 0.1 seconds (0.00s async, 0.1s sync) 28 | Something went wrong - no tests were selected to be run! 29 | 30 | """ 31 | end 32 | 33 | test "prints the right thing for successful cases" do 34 | assert CaptureIO.capture_io(fn -> 35 | Formatter.do_print_report({[], 123, 123_000, 0.0, [seed: 632_719]}) 36 | end) == """ 37 | 38 | 39 | Finished in 0.1 seconds (0.00s async, 0.1s sync) 40 | \e[32m123 run - 0 mutations survived\e[0m 41 | 42 | Randomized with seed 632719 43 | """ 44 | end 45 | 46 | test "prints the right thing for failures cases" do 47 | line1 = ["def", " ", "test_fun", "(", "", "var", "", ")", " do"] 48 | line2 = [" ", "var", " ==", " ", ":ok"] 49 | line3 = ["", "end"] 50 | mutation = [" ", "raise", " ", "\"", "Exception introduced by Muzak", "\""] 51 | 52 | original = line1 ++ ["\n"] ++ line2 ++ ["\n"] ++ line3 53 | mutation = line1 ++ ["\n"] ++ mutation ++ ["\n"] ++ line2 ++ ["\n"] ++ line3 54 | 55 | failures = [ 56 | %{ 57 | line: 72, 58 | original: ["if", " ", "var", " ==", " ", ":ok"], 59 | mutation: ["if", " ", "var", " !=", " ", ":ok"], 60 | path: "path/to/file.ex" 61 | }, 62 | %{ 63 | line: 81, 64 | original: ["if", " ", "var", " ==", " ", ":ok"], 65 | mutation: ["if", " ", "var", " ==", " ", ":error"], 66 | path: "path/to/file.ex" 67 | }, 68 | %{ 69 | line: 87, 70 | original: ["{", "", ":ok", ",", " response", "}", " ", "->"], 71 | mutation: ["{", "", ":error", ",", " response", "}", " ", "->"], 72 | path: "path/to/other/file.ex" 73 | }, 74 | %{ 75 | line: 90, 76 | original: ["{", "", ":ok", ",", " ", "\"", "thing", "\"", "}", " ", "->"], 77 | mutation: ["{", "", ":ok", ",", " ", "\"", "random_string", "\"", "}", " ", "->"], 78 | path: "path/to/other/file.ex" 79 | }, 80 | %{ 81 | line: 99, 82 | original: original, 83 | mutation: mutation, 84 | path: "path/to/other/file.ex" 85 | } 86 | ] 87 | 88 | first = 89 | String.trim(""" 90 | \e[31mpath/to/file.ex:72\e[0m 91 | \e[36moriginal: \e[0mif var =\e[32m=\e[0m :ok 92 | \e[36mmutation: \e[0mif var \e[31m!\e[0m= :ok 93 | """) 94 | 95 | second = 96 | String.trim(""" 97 | \e[31mpath/to/file.ex:81\e[0m 98 | \e[36moriginal: \e[0mif var == \e[32m:ok\e[0m 99 | \e[36mmutation: \e[0mif var == \e[31m:error\e[0m 100 | """) 101 | 102 | third = 103 | String.trim(""" 104 | \e[31mpath/to/other/file.ex:87\e[0m 105 | \e[36moriginal: \e[0m{\e[32m:ok\e[0m, response} -> 106 | \e[36mmutation: \e[0m{\e[31m:error\e[0m, response} -> 107 | """) 108 | 109 | fourth = 110 | String.trim(""" 111 | \e[31mpath/to/other/file.ex:90\e[0m 112 | \e[36moriginal: \e[0m{:ok, \"\e[32mthing\e[0m\"} -> 113 | \e[36mmutation: \e[0m{:ok, \"\e[31mrandom_string\e[0m\"} -> 114 | """) 115 | 116 | fifth = 117 | String.trim(""" 118 | \e[31mpath/to/other/file.ex:99\e[0m 119 | \e[36moriginal: \e[0mdef test_fun(var) do 120 | var == :ok 121 | end 122 | \e[36mmutation: \e[0mdef test_fun(var) do 123 | \e[31mraise "Exception introduced by Muzak" 124 | \e[0mvar == :ok 125 | end 126 | """) 127 | 128 | summary = 129 | String.trim(""" 130 | Finished in 0.1 seconds (0.00s async, 0.1s sync) 131 | \e[31m123 mutations run 132 | 5 survived 98.04% of mutations were found\e[0m 133 | 134 | Randomized with seed 632719 135 | """) 136 | 137 | assert CaptureIO.capture_io(fn -> 138 | Formatter.do_print_report({failures, 123, 123_000, 98.04, [seed: 632_719]}) 139 | end) == """ 140 | 141 | 142 | #{first} 143 | 144 | #{second} 145 | 146 | #{third} 147 | 148 | #{fourth} 149 | 150 | #{fifth} 151 | 152 | 153 | #{summary} 154 | """ 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /test/muzak/runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.RunnerTest do 2 | use ExUnit.Case 3 | 4 | alias Muzak.Runner 5 | 6 | defmodule SuccessCompiler do 7 | def require_and_run(matched_test_files) do 8 | send(Application.get_env(:muzak, :test_pid), {:require_and_run, matched_test_files}) 9 | {:ok, %{failures: 1, total: 1}} 10 | end 11 | end 12 | 13 | defmodule FailureCompiler do 14 | def require_and_run(matched_test_files) do 15 | send(Application.get_env(:muzak, :test_pid), {:require_and_run, matched_test_files}) 16 | {:ok, %{failures: 0, total: 1}} 17 | end 18 | end 19 | 20 | defmodule Formatter do 21 | def start_link(test_pid) do 22 | pid = spawn(fn -> receive_loop(test_pid) end) 23 | Process.register(pid, Muzak.Formatter) 24 | {:ok, pid} 25 | end 26 | 27 | def receive_loop(test_pid) do 28 | receive do 29 | msg -> 30 | send(test_pid, {__MODULE__, msg}) 31 | receive_loop(test_pid) 32 | end 33 | end 34 | 35 | def child_spec(_) do 36 | %{ 37 | id: __MODULE__, 38 | start: {__MODULE__, :start_link, [self()]}, 39 | type: :worker, 40 | restart: :temporary, 41 | shutdown: 500 42 | } 43 | end 44 | end 45 | 46 | setup do 47 | formatter = Process.whereis(Muzak.Formatter) 48 | System.put_env("DEBUG", "1") 49 | Application.put_env(:muzak, :test_pid, self()) 50 | 51 | on_exit(fn -> 52 | if is_pid(formatter) do 53 | Process.register(formatter, Muzak.Formatter) 54 | end 55 | 56 | System.delete_env("DEBUG") 57 | end) 58 | 59 | start_supervised!(Formatter) 60 | 61 | :ok 62 | end 63 | 64 | describe "run_test_loop/1" do 65 | test "doesn't run tests if the mutation can't compile" do 66 | ExUnit.CaptureIO.capture_io(fn -> 67 | test_files = [:a, :b] 68 | test_paths = [:c, :d] 69 | opts = [:e, :f] 70 | 71 | mutations = [ 72 | %{ 73 | original_file: "{1, 2}", 74 | file: "{1, 2", 75 | path: "/to/file.ex", 76 | original: "", 77 | mutation: "", 78 | line: 1 79 | } 80 | ] 81 | 82 | test_info = {test_files, test_paths, opts, mutations, []} 83 | 84 | assert {[], 1, num, 100.0, []} = 85 | Runner.run_test_loop(test_info, &SuccessCompiler.require_and_run/1) 86 | 87 | assert num > 1 88 | 89 | assert_receive {Formatter, {:"Mutating file", :nonode@nohost}} 90 | assert_receive {Formatter, {:"Mutation failed to compile", :nonode@nohost}} 91 | assert_receive {Formatter, {:"Original file compiled", :nonode@nohost}} 92 | assert_receive {Formatter, {:"Files unrequired", :nonode@nohost}} 93 | assert_receive {Formatter, {"Running mutation 1 of 1", :nonode@nohost}} 94 | assert_receive {Formatter, {:success, :nonode@nohost}} 95 | assert_receive {Formatter, {_, :nonode@nohost}} 96 | refute_receive _ 97 | end) 98 | end 99 | 100 | test "calls the ExUnit compiler correctly when the file can be compiled" do 101 | ExUnit.CaptureIO.capture_io(fn -> 102 | test_files = [:a, :b] 103 | test_paths = [:c, :d] 104 | opts = [:e, :f] 105 | 106 | mutations = [ 107 | %{ 108 | path: "path/to/file.ex", 109 | mutation: [""], 110 | original: [""], 111 | original_file: "{1, 2, 3}", 112 | file: "{3, 2, 1}", 113 | line: 1 114 | } 115 | ] 116 | 117 | test_info = {test_files, test_paths, opts, mutations, []} 118 | 119 | assert {[], 1, num, 100.0, []} = 120 | Runner.run_test_loop(test_info, &SuccessCompiler.require_and_run/1) 121 | 122 | assert num > 1 123 | 124 | assert_receive {Formatter, {:"Mutating file", :nonode@nohost}} 125 | assert_receive {Formatter, {:"Mutating completed", :nonode@nohost}} 126 | assert_receive {Formatter, {:"Tests starting", :nonode@nohost}} 127 | assert_receive {:require_and_run, ^test_files} 128 | assert_receive {Formatter, {:"Tests finished", :nonode@nohost}} 129 | assert_receive {Formatter, {:"Original file compiled", :nonode@nohost}} 130 | assert_receive {Formatter, {:"Files unrequired", :nonode@nohost}} 131 | assert_receive {Formatter, {:success, :nonode@nohost}} 132 | assert_receive {Formatter, {"Running mutation 1 of 1", :nonode@nohost}} 133 | assert_receive {Formatter, {_, :nonode@nohost}} 134 | refute_receive _ 135 | end) 136 | end 137 | 138 | test "sends the right messages when there is a failure" do 139 | ExUnit.CaptureIO.capture_io(fn -> 140 | test_files = [:a, :b] 141 | test_paths = [:c, :d] 142 | opts = [:e, :f] 143 | 144 | mutations = [ 145 | %{ 146 | path: "path/to/file.ex", 147 | original: [""], 148 | mutation: [""], 149 | original_file: "{1, 2, 3}", 150 | file: "{3, 2, 1}", 151 | line: 1 152 | } 153 | ] 154 | 155 | test_info = {test_files, test_paths, opts, mutations, []} 156 | 157 | assert {^mutations, 1, num, 0.0, []} = 158 | Runner.run_test_loop(test_info, &FailureCompiler.require_and_run/1) 159 | 160 | assert num > 1 161 | 162 | assert_receive {Formatter, {:"Mutating file", :nonode@nohost}} 163 | assert_receive {Formatter, {:"Mutating completed", :nonode@nohost}} 164 | assert_receive {Formatter, {:"Tests starting", :nonode@nohost}} 165 | assert_receive {:require_and_run, ^test_files} 166 | assert_receive {Formatter, {:"Tests finished", :nonode@nohost}} 167 | assert_receive {Formatter, {:"Original file compiled", :nonode@nohost}} 168 | assert_receive {Formatter, {:"Files unrequired", :nonode@nohost}} 169 | assert_receive {Formatter, {:failure, :nonode@nohost}} 170 | assert_receive {Formatter, {"Running mutation 1 of 1", :nonode@nohost}} 171 | assert_receive {Formatter, {_, :nonode@nohost}} 172 | refute_receive _ 173 | end) 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /test/muzak/mutators/constants/strings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Constants.StringsTest do 2 | use Assertions.Case, async: true 3 | 4 | alias Muzak.{Code.Formatter, Mutators.Mutator, Mutators.Constants.Strings} 5 | 6 | describe "mutate/2" do 7 | test "returns all expected mutations" do 8 | ast = Formatter.to_ast(~s| 9 | defmodule Tester do 10 | @doc "a string doc that shouldn't mutate" 11 | def heredoc() do 12 | """ 13 | Heredoc 14 | """ <> "other string" 15 | end 16 | end 17 | |) 18 | 19 | ast 20 | |> Strings.mutate(fn _ -> "random_string" end) 21 | |> assert_lists_equal([ 22 | %{ 23 | example_format: :line, 24 | line: 7, 25 | mutated_ast: { 26 | :defmodule, 27 | [do: [line: 2], end: [line: 9], line: 2], 28 | [ 29 | {:__aliases__, [{:last, [line: 2]}, {:line, 2}], [:Tester]}, 30 | [ 31 | { 32 | {:__block__, [line: 2], [:do]}, 33 | { 34 | :__block__, 35 | [], 36 | [ 37 | { 38 | :@, 39 | [end_of_expression: [newlines: 1, line: 3], line: 3], 40 | [ 41 | {:doc, [line: 3], 42 | [ 43 | {:__block__, [delimiter: "\"", line: 3], 44 | ["a string doc that shouldn't mutate"]} 45 | ]} 46 | ] 47 | }, 48 | { 49 | :def, 50 | [do: [line: 4], end: [line: 8], line: 4], 51 | [ 52 | {:heredoc, [closing: [line: 4], line: 4], []}, 53 | [ 54 | { 55 | {:__block__, [line: 4], [:do]}, 56 | { 57 | :<>, 58 | [line: 7], 59 | [ 60 | {:__block__, 61 | [{:delimiter, "\"\"\""}, {:indentation, 12}, {:line, 5}], 62 | ["Heredoc\n"]}, 63 | {:__block__, [delimiter: "\"", line: 7], ["random_string"]} 64 | ] 65 | } 66 | } 67 | ] 68 | ] 69 | } 70 | ] 71 | } 72 | } 73 | ] 74 | ] 75 | } 76 | }, 77 | %{ 78 | example_format: :line, 79 | line: 5, 80 | mutated_ast: { 81 | :defmodule, 82 | [do: [line: 2], end: [line: 9], line: 2], 83 | [ 84 | {:__aliases__, [{:last, [line: 2]}, {:line, 2}], [:Tester]}, 85 | [ 86 | { 87 | {:__block__, [line: 2], [:do]}, 88 | { 89 | :__block__, 90 | [], 91 | [ 92 | { 93 | :@, 94 | [end_of_expression: [newlines: 1, line: 3], line: 3], 95 | [ 96 | {:doc, [line: 3], 97 | [ 98 | {:__block__, [delimiter: "\"", line: 3], 99 | ["a string doc that shouldn't mutate"]} 100 | ]} 101 | ] 102 | }, 103 | { 104 | :def, 105 | [do: [line: 4], end: [line: 8], line: 4], 106 | [ 107 | {:heredoc, [closing: [line: 4], line: 4], []}, 108 | [ 109 | { 110 | {:__block__, [line: 4], [:do]}, 111 | { 112 | :<>, 113 | [line: 7], 114 | [ 115 | {:__block__, 116 | [{:delimiter, "\"\"\""}, {:indentation, 12}, {:line, 5}], 117 | ["random_string"]}, 118 | {:__block__, [delimiter: "\"", line: 7], ["other string"]} 119 | ] 120 | } 121 | } 122 | ] 123 | ] 124 | } 125 | ] 126 | } 127 | } 128 | ] 129 | ] 130 | } 131 | } 132 | ]) 133 | end 134 | end 135 | 136 | describe "mutate/3" do 137 | test "returns the correct result (mutation expected part 1)" do 138 | ast = 139 | Formatter.to_ast(""" 140 | defmodule Tester do 141 | def test_fun(arg) do 142 | "putting it #\{arg}" <> "other string" 143 | end 144 | end 145 | """) 146 | 147 | node = 148 | {:<<>>, [delimiter: "\"", line: 3], 149 | [ 150 | "putting it ", 151 | {:"::", [line: 3], 152 | [ 153 | {{:., [line: 3], [Kernel, :to_string]}, [closing: [line: 3], line: 3], 154 | [{:arg, [line: 3], nil}]}, 155 | {:binary, [line: 3], nil} 156 | ]} 157 | ]} 158 | 159 | mutation = 160 | {:<<>>, [delimiter: "\"", line: 3], 161 | [ 162 | "random_string", 163 | {:"::", [line: 3], 164 | [ 165 | {{:., [line: 3], [Kernel, :to_string]}, [closing: [line: 3], line: 3], 166 | [{:arg, [line: 3], nil}]}, 167 | {:binary, [line: 3], nil} 168 | ]} 169 | ]} 170 | 171 | assert Strings.mutate(node, ast, fn _ -> "random_string" end) == [ 172 | %{ 173 | mutated_ast: Mutator.replace_node(ast, node, mutation), 174 | line: 3, 175 | example_format: :line 176 | } 177 | ] 178 | end 179 | 180 | test "returns the correct result (mutation expected part 2)" do 181 | ast = 182 | Formatter.to_ast(""" 183 | defmodule Tester do 184 | def test_fun(arg) do 185 | "putting it #\{arg}" <> "other string" 186 | end 187 | end 188 | """) 189 | 190 | node = {:__block__, [delimiter: "\"", line: 3], ["other string"]} 191 | 192 | mutation = {:__block__, [delimiter: "\"", line: 3], ["random_string"]} 193 | 194 | assert Strings.mutate(node, ast, fn _ -> "random_string" end) == [ 195 | %{ 196 | mutated_ast: Mutator.replace_node(ast, node, mutation), 197 | line: 3, 198 | example_format: :line 199 | } 200 | ] 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /lib/muzak/mutations.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutations do 2 | @moduledoc false 3 | 4 | alias Muzak.Code.Formatter 5 | 6 | @mutators [ 7 | Muzak.Mutators.Constants.Numbers, 8 | Muzak.Mutators.Constants.Strings, 9 | Muzak.Mutators.Functions.Rename 10 | ] 11 | 12 | @doc false 13 | def generate_mutations(opts) do 14 | opts[:mutation_filter] 15 | |> get_files() 16 | |> generate_mutations(@mutators, opts[:mutations], opts[:seed]) 17 | end 18 | 19 | @doc false 20 | def mutate_file(info, mutators) do 21 | mutate_file(info, mutators, 0) 22 | end 23 | 24 | def mutate_file(info, mutators, seed) when is_integer(seed) do 25 | mutate_file(info, mutators, seed, &Muzak.Mutators.Mutator.random_string/1) 26 | end 27 | 28 | def mutate_file(info, mutators, name_fun) do 29 | mutate_file(info, mutators, 0, name_fun) 30 | end 31 | 32 | def mutate_file(info, nil, seed, name_fun) do 33 | mutate_file(info, @mutators, seed, name_fun) 34 | end 35 | 36 | def mutate_file({file, path, lines}, mutators, seed, name_fun) do 37 | {ast, state} = Formatter.to_forms_and_state(file) 38 | 39 | mutators 40 | |> shuffle(seed) 41 | |> Enum.map(&Task.async(fn -> mutate(&1, file, lines, path, ast, state, name_fun) end)) 42 | |> Enum.flat_map(&Task.await(&1, :infinity)) 43 | |> List.flatten() 44 | |> shuffle(seed) 45 | end 46 | 47 | defp get_files(exclude) do 48 | Mix.Project.config() 49 | |> Keyword.get(:elixirc_paths) 50 | |> Enum.flat_map(&ls_r/1) 51 | |> Enum.filter(&String.ends_with?(&1, ".ex")) 52 | |> filter(exclude) 53 | |> Enum.map(&read_file/1) 54 | end 55 | 56 | @stream_opts [ 57 | timeout: :infinity, 58 | ordered: false, 59 | max_concurrency: System.schedulers_online() * 2 60 | ] 61 | defp generate_mutations(files, mutators, num_mutations, seed) do 62 | files 63 | |> shuffle(seed) 64 | |> Task.async_stream(&mutate_file(&1, mutators, seed), @stream_opts) 65 | |> Enum.reduce_while([], &reduce_mutations(&1, &2, num_mutations)) 66 | end 67 | 68 | defp reduce_mutations(_, acc, num_mutations) when length(acc) >= num_mutations, 69 | do: {:halt, Enum.take(acc, num_mutations)} 70 | 71 | defp reduce_mutations({:ok, result}, acc, _), 72 | do: {:cont, result |> Enum.uniq() |> Enum.reduce(acc, &[&1 | &2])} 73 | 74 | defp reduce_mutations(_, acc, _), do: {:cont, acc} 75 | 76 | defp read_file({path, lines}) do 77 | file = path |> Path.expand() |> File.read!() |> Code.format_string!() |> to_string() 78 | {file, path, lines} 79 | end 80 | 81 | defp shuffle(list, seed) do 82 | :rand.seed(:exrop, {seed, seed, seed}) 83 | Enum.shuffle(list) 84 | end 85 | 86 | defp filter(files, func) when is_function(func, 1), do: func.(files) 87 | defp filter(files, _), do: Enum.map(files, &{&1, nil}) 88 | 89 | defp mutate(mutator, file, lines, path, ast, state, name_fun) do 90 | ast 91 | |> mutator.mutate(name_fun) 92 | |> Enum.reduce([], &add_mutation(&1, &2, lines)) 93 | |> Enum.uniq() 94 | |> Enum.map(&expand_mutation(&1, file, path, state)) 95 | end 96 | 97 | defp add_mutation(mutation, mutations, nil), do: [mutation | mutations] 98 | 99 | defp add_mutation(mutation, mutations, lines) do 100 | if mutation.line in lines, do: [mutation | mutations], else: mutations 101 | end 102 | 103 | defp expand_mutation(mutation, original_file, path, state) do 104 | original_algebra = Formatter.to_algebra(original_file) 105 | mutated_file = Formatter.to_algebra(mutation.mutated_ast, state) 106 | 107 | {mutated, original} = get_lines(mutated_file, original_algebra, mutation.example_format) 108 | 109 | %{ 110 | line: mutation.line, 111 | path: path, 112 | original_file: original_file, 113 | file: to_string(mutated_file), 114 | mutation: mutated, 115 | original: original 116 | } 117 | end 118 | 119 | defp get_lines(file, original_file, :line) do 120 | chunk_fun = fn element, acc -> 121 | if String.starts_with?(element, "\n") do 122 | element = String.replace_leading(element, "\n\n", "\n") 123 | [newline | spaces] = String.graphemes(element) 124 | {:cont, Enum.reverse(acc), [newline, Enum.join(spaces)]} 125 | else 126 | if match?(["\n", _], acc) do 127 | {:cont, [element | tl(acc)]} 128 | else 129 | {:cont, [element | acc]} 130 | end 131 | end 132 | end 133 | 134 | after_fun = fn 135 | [] -> {:cont, []} 136 | acc -> {:cont, Enum.reverse(acc), []} 137 | end 138 | 139 | file = Enum.chunk_while(file, [], chunk_fun, after_fun) 140 | original_file = Enum.chunk_while(original_file, [], chunk_fun, after_fun) 141 | 142 | {original, mutated} = 143 | original_file 144 | |> Enum.zip(file) 145 | |> Enum.filter(fn {original, mutated} -> original != mutated end) 146 | |> Enum.reduce({[], []}, fn {original, mutated}, {original_acc, mutated_acc} -> 147 | {[original | original_acc], [mutated | mutated_acc]} 148 | end) 149 | 150 | {_, original} = 151 | original 152 | |> Enum.reverse() 153 | |> Enum.reduce({nil, []}, &trim/2) 154 | 155 | {_, mutated} = 156 | mutated 157 | |> Enum.reverse() 158 | |> Enum.reduce({nil, []}, &trim/2) 159 | 160 | {List.flatten(mutated), List.flatten(original)} 161 | end 162 | 163 | defp get_lines(file, original_file, {:block, original_lines, mutated_lines}) do 164 | chunk_fun = fn element, acc -> 165 | if String.starts_with?(element, "\n") do 166 | element = String.replace_leading(element, "\n\n", "\n") 167 | [newline | spaces] = String.graphemes(element) 168 | {:cont, Enum.reverse(acc), [newline, Enum.join(spaces)]} 169 | else 170 | if match?(["\n", _], acc) do 171 | {:cont, [element | tl(acc)]} 172 | else 173 | {:cont, [element | acc]} 174 | end 175 | end 176 | end 177 | 178 | after_fun = fn 179 | [] -> {:cont, []} 180 | acc -> {:cont, Enum.reverse(acc), []} 181 | end 182 | 183 | file = Enum.chunk_while(file, [], chunk_fun, after_fun) 184 | 185 | original_file = Enum.chunk_while(original_file, [], chunk_fun, after_fun) 186 | 187 | start_idx = 188 | file 189 | |> Enum.zip(original_file) 190 | |> Enum.find_index(fn {mutated, original} -> mutated != original end) 191 | |> Kernel.-(1) 192 | 193 | original = 194 | Enum.slice(original_file, start_idx..(start_idx + original_lines - 1)) 195 | |> Enum.intersperse(["\n"]) 196 | 197 | mutated = 198 | Enum.slice(file, start_idx..(start_idx + mutated_lines - 1)) 199 | |> Enum.intersperse(["\n"]) 200 | 201 | {_, original} = Enum.reduce(original, {nil, []}, &trim/2) 202 | {_, mutated} = Enum.reduce(mutated, {nil, []}, &trim/2) 203 | 204 | {List.flatten(Enum.reverse(mutated)), List.flatten(Enum.reverse(original))} 205 | end 206 | 207 | defp trim([maybe_space | rest] = line, {nil, lines}) do 208 | if String.starts_with?(maybe_space, " ") do 209 | {String.codepoints(maybe_space), [rest | lines]} 210 | else 211 | {String.codepoints("😀"), [line | lines]} 212 | end 213 | end 214 | 215 | defp trim([maybe_space | rest], {pad, lines}) do 216 | first = 217 | maybe_space 218 | |> String.codepoints() 219 | |> Kernel.--(pad) 220 | |> Enum.join() 221 | 222 | {pad, [[first | rest] | lines]} 223 | end 224 | 225 | defp ls_r(path) do 226 | if File.dir?(path) do 227 | path 228 | |> File.ls!() 229 | |> Enum.map(&Path.join(path, &1)) 230 | |> Enum.flat_map(&ls_r/1) 231 | else 232 | [path] 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/muzak/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Runner do 2 | @moduledoc false 3 | 4 | # All the code to actually run the tests and such 5 | 6 | alias Muzak.{Config, Formatter} 7 | 8 | @doc false 9 | def run_test_loop({_, _, _, mutations, opts} = test_info, runner \\ &require_and_run/1) do 10 | num_mutations = length(mutations) 11 | 12 | IO.puts("Beginning mutation testing - #{num_mutations} mutations generated\n") 13 | 14 | start = System.monotonic_time(:microsecond) 15 | 16 | results = 17 | mutations 18 | |> Enum.with_index() 19 | |> Enum.reduce([], fn {mutation, idx}, acc -> 20 | print("Running mutation #{idx + 1} of #{num_mutations}") 21 | 22 | mutation 23 | |> run_mutation(test_info, runner, opts) 24 | |> handle_result(acc) 25 | end) 26 | 27 | finish_time = System.monotonic_time(:microsecond) - start 28 | 29 | success_percentage = 30 | if num_mutations > 0 do 31 | num_failures = length(results) 32 | success_percentage = Float.round((1 - num_failures / num_mutations) * 100, 2) 33 | 34 | if success_percentage < Keyword.get(opts, :min_coverage, 100.0) do 35 | System.at_exit(fn _ -> exit({:shutdown, 1}) end) 36 | end 37 | 38 | success_percentage 39 | else 40 | 100.0 41 | end 42 | 43 | {results, num_mutations, finish_time, success_percentage, opts} 44 | end 45 | 46 | @doc false 47 | defp run_mutation(mutation_info, test_info, runner, opts) do 48 | restart_apps(opts) 49 | 50 | fn -> 51 | cleanup_processes() 52 | 53 | print(""" 54 | 55 | Starting mutation at #{mutation_info.path}:#{mutation_info.line} 56 | 57 | <<<<<<< ORIGINAL 58 | #{mutation_info.original} 59 | ======= 60 | #{mutation_info.mutation} 61 | >>>>>>> MUTATION 62 | 63 | """) 64 | 65 | results = 66 | with :ok <- compile_mutation(mutation_info), 67 | :ok <- compile_dependencies(mutation_info) do 68 | run_tests(mutation_info, test_info, runner) 69 | end 70 | 71 | recompile_original(mutation_info.original_file) 72 | {results, mutation_info} 73 | end 74 | |> run_silent() 75 | |> print_result() 76 | end 77 | 78 | @apps_to_keep [ 79 | # OTP basic apps 80 | 81 | :compiler, 82 | :erts, 83 | :kernel, 84 | :sasl, 85 | :stdlib, 86 | :os_mon, 87 | :asn1, 88 | :crypto, 89 | :diameter, 90 | :eldap, 91 | :erl_interface, 92 | :ftp, 93 | :inets, 94 | :jinterface, 95 | :megaco, 96 | :public_key, 97 | :ssh, 98 | :ssl, 99 | :tftp, 100 | :wx, 101 | :xmerl, 102 | :logger, 103 | :parsetools, 104 | :runtime_tools, 105 | :hipe, 106 | 107 | # Elixir basic apps 108 | 109 | :elixir, 110 | :mix, 111 | :hex, 112 | :muzak 113 | ] 114 | 115 | defp restart_apps(opts) do 116 | Application.stop(Mix.Project.config()[:app]) 117 | 118 | apps_to_keep = 119 | if System.get_env("MUZAK_TESTS") do 120 | @apps_to_keep ++ [:ex_unit] 121 | else 122 | @apps_to_keep 123 | end 124 | 125 | for {dep, _, _} <- Application.started_applications(), dep not in apps_to_keep do 126 | Application.stop(dep) 127 | end 128 | 129 | Mix.Task.reenable("app.start") 130 | Mix.Task.run("app.start") 131 | 132 | unless System.get_env("MUZAK_TESTS") do 133 | Config.configure_ex_unit(opts) 134 | Application.ensure_started(:ex_unit) 135 | end 136 | end 137 | 138 | defp cleanup_processes() do 139 | Code.purge_compiler_modules() 140 | 141 | # This is a really weird hack because some files were stuck as being already required, and so 142 | # we entered the compilation queue but never actually made it out of the queue. 143 | # 144 | # We should ask Jose what's going on here and how to _not_ do this. 145 | :sys.replace_state(:elixir_code_server, &put_elem(&1, 1, %{})) 146 | 147 | # We also need to update the ExUnit.Server, which I would love to not have to do, but looks 148 | # like we need to. 149 | :sys.replace_state(ExUnit.Server, fn _ -> 150 | %{ 151 | async_modules: [], 152 | loaded: System.monotonic_time(), 153 | sync_modules: [], 154 | waiting: nil 155 | } 156 | end) 157 | 158 | for pid <- Process.list(), 159 | [links: [], monitors: [], dictionary: dict] <- [ 160 | Process.info(pid, [:links, :monitors, :dictionary]) 161 | ] do 162 | case {Keyword.get(dict, :"$initial_call"), Keyword.get(dict, :elixir_compiler_pid)} do 163 | # When the compiler has an error it can orphan processes, so we're cleaning them up here 164 | {_, compiler_pid} when is_pid(compiler_pid) -> 165 | Process.exit(pid, :kill) 166 | 167 | # When ExUnit finishes, it leaves some processes orphaned, so we're cleaning them up 168 | # here before we begin again 169 | {{_, f, _}, _} -> 170 | if f |> Atom.to_string() |> String.starts_with?("-test") do 171 | Process.exit(pid, :kill) 172 | end 173 | 174 | _ -> 175 | :ok 176 | end 177 | end 178 | end 179 | 180 | defp print_result({{:ok, %{failures: 0, total: total}}, _} = result) when total > 0 do 181 | print(:failure) 182 | result 183 | end 184 | 185 | defp print_result(result) do 186 | print(:success) 187 | result 188 | end 189 | 190 | defp handle_result({{:ok, %{failures: 0, total: t}}, info}, acc) when t > 0, do: [info | acc] 191 | defp handle_result(_, acc), do: acc 192 | 193 | defp compile_mutation(mutation_info) do 194 | print(:"Mutating file") 195 | 196 | try do 197 | if hd(mutation_info.original) == "defmodule" do 198 | [_, _ | module_info] = mutation_info.original 199 | [_ | module_info] = Enum.reverse(module_info) 200 | module = (module_info ++ ["Elixir"]) |> Enum.reverse() |> Module.concat() 201 | 202 | path = 203 | Enum.find_value(:code.all_loaded(), fn {mod, path} -> 204 | if mod == module do 205 | path 206 | end 207 | end) 208 | 209 | :code.purge(module) 210 | :code.delete(module) 211 | File.rm!(path) 212 | end 213 | 214 | Code.compile_string(mutation_info.file) 215 | print(:"Mutating completed") 216 | :ok 217 | rescue 218 | _ -> 219 | print(:"Mutation failed to compile") 220 | :compilation_error 221 | end 222 | end 223 | 224 | defp compile_dependencies(mutation_info) do 225 | try do 226 | sources = 227 | Mix.Project.manifest_path() 228 | |> Path.join("compile.elixir") 229 | |> File.read!() 230 | |> :erlang.binary_to_term() 231 | |> case do 232 | {_, _, sources} -> sources 233 | {_, _, sources, _} -> sources 234 | end 235 | 236 | sources 237 | |> Enum.find_value(fn {_, path, _, _, _, _, _, _, _, modules} -> 238 | if path == mutation_info.path, do: modules, else: false 239 | end) 240 | |> case do 241 | [] -> 242 | print(:"No modules defined in file") 243 | 244 | [_ | _] = modules_defined -> 245 | sources 246 | |> Enum.reduce([], fn {_, path, _, module_dependencies, _, _, _, _, _, _}, acc -> 247 | if Enum.any?(module_dependencies, &(&1 in modules_defined)), 248 | do: [path | acc], 249 | else: acc 250 | end) 251 | |> case do 252 | [] -> 253 | print(:"No dependencies of mutated file to compile") 254 | :ok 255 | 256 | paths -> 257 | print(:"Compiling dependencies of mutated file") 258 | Enum.each(paths, &Code.compile_file/1) 259 | print(:"Compiling dependencies of mutated file completed") 260 | :ok 261 | end 262 | 263 | _ -> 264 | :ok 265 | end 266 | rescue 267 | _ -> 268 | print(:"Compiling dependencies of mutated file failed") 269 | :compilation_error 270 | end 271 | end 272 | 273 | defp run_tests(_, {test_files, _, _, _, _}, runner) do 274 | print(:"Tests starting") 275 | parent = self() 276 | spawn(fn -> send(parent, {:__muzak_test_run_results, runner.(test_files)}) end) 277 | 278 | receive do 279 | {:__muzak_test_run_results, results} -> 280 | print(:"Tests finished") 281 | results 282 | end 283 | end 284 | 285 | defp require_and_run(matched_test_files) do 286 | task = ExUnit.async_run() 287 | 288 | try do 289 | case Kernel.ParallelCompiler.require(matched_test_files, []) do 290 | {:ok, _, _} -> 291 | ExUnit.Server.modules_loaded() 292 | {:ok, ExUnit.await_run(task)} 293 | 294 | {:error, _, _} -> 295 | Task.shutdown(task, :brutal_kill) 296 | {:ok, :compile_error} 297 | end 298 | catch 299 | _, _ -> 300 | Task.shutdown(task, :brutal_kill) 301 | {:ok, :compile_error} 302 | end 303 | end 304 | 305 | defp recompile_original(original_file) do 306 | Code.compile_string(original_file) 307 | print(:"Original file compiled") 308 | 309 | Code.unrequire_files(Code.required_files()) 310 | print(:"Files unrequired") 311 | end 312 | 313 | defp run_silent(function) do 314 | if System.get_env("DEBUG") do 315 | function.() 316 | else 317 | me = self() 318 | 319 | ExUnit.CaptureLog.capture_log(fn -> 320 | ExUnit.CaptureIO.capture_io(:standard_io, fn -> 321 | ExUnit.CaptureIO.capture_io(:standard_error, fn -> 322 | send(me, function.()) 323 | end) 324 | end) 325 | end) 326 | 327 | receive do 328 | response -> response 329 | end 330 | end 331 | end 332 | 333 | defp print(msg) do 334 | send(Formatter, {msg, node()}) 335 | end 336 | end 337 | -------------------------------------------------------------------------------- /lib/muzak/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Formatter do 2 | @moduledoc false 3 | 4 | # Contains all the stuff for formatting and printing of reports 5 | 6 | require ExUnit.Assertions 7 | 8 | alias Inspect.Algebra 9 | 10 | alias ExUnit.Diff 11 | 12 | @no_value :ex_unit_no_meaningful_value 13 | 14 | @counter_padding " " 15 | 16 | @doc false 17 | # The start_link for our printer process 18 | def start_link(opts) do 19 | group_leader = Process.group_leader() 20 | 21 | fn -> print_loop(group_leader, opts) end 22 | |> spawn_link() 23 | |> Process.register(__MODULE__) 24 | end 25 | 26 | @doc false 27 | def print_report(report) do 28 | send(__MODULE__, {:finished, self()}) 29 | 30 | receive do 31 | :done -> do_print_report(report) 32 | end 33 | end 34 | 35 | @doc false 36 | def do_print_report({:noop, _, time, _, _}) do 37 | print_time(time) 38 | IO.puts("Something went wrong - tests did not run for a later set of mutations!\n") 39 | end 40 | 41 | def do_print_report({:no_tests_run, _, time, _, _}) do 42 | print_time(time) 43 | IO.puts("Something went wrong - no tests were selected to be run!\n") 44 | end 45 | 46 | def do_print_report({[], num_mutations, time, _, opts}) do 47 | print_time(time) 48 | IO.puts([IO.ANSI.green(), "#{num_mutations} run - 0 mutations survived", IO.ANSI.reset()]) 49 | IO.puts("\nRandomized with seed #{opts[:seed]}") 50 | end 51 | 52 | def do_print_report({surviving_mutations, num_mutations, time, success_percentage, opts}) do 53 | failure_info = 54 | surviving_mutations 55 | |> Enum.sort_by(&{&1.path, &1.line}) 56 | |> Enum.reverse() 57 | |> Enum.reduce([], fn mutation_info, acc -> 58 | exception = 59 | try do 60 | ExUnit.Assertions.assert(mutation_info.mutation == mutation_info.original) 61 | rescue 62 | e -> e 63 | end 64 | 65 | case exception do 66 | true -> 67 | IO.warn(""" 68 | Original and mutation were the same - something went wrong! 69 | file: #{mutation_info.path}:#{mutation_info.line} 70 | """) 71 | 72 | acc 73 | 74 | _ -> 75 | exception = %{ 76 | exception 77 | | message: "#{mutation_info.path}:#{mutation_info.line}", 78 | expr: @no_value 79 | } 80 | 81 | ["\n", format_exception(exception) | acc] 82 | end 83 | end) 84 | 85 | IO.puts("") 86 | IO.puts(failure_info) 87 | print_time(time, "") 88 | 89 | msg = 90 | "#{num_mutations} mutations run\n#{length(surviving_mutations)} survived " <> 91 | "#{success_percentage}% of mutations were found" 92 | 93 | IO.puts([IO.ANSI.red(), msg, IO.ANSI.reset()]) 94 | IO.puts("\nRandomized with seed #{opts[:seed]}") 95 | end 96 | 97 | defp print_loop(group_leader, opts) do 98 | receive do 99 | {:finished, caller} -> 100 | send(caller, :done) 101 | 102 | {:success, _} -> 103 | unless opts[:format] == "progress_bar" do 104 | msg = IO.iodata_to_binary([IO.ANSI.green(), ".", IO.ANSI.reset()]) 105 | IO.write(group_leader, msg) 106 | end 107 | 108 | print_loop(group_leader, opts) 109 | 110 | {:failure, _} -> 111 | unless opts[:format] == "progress_bar" do 112 | msg = IO.iodata_to_binary([IO.ANSI.red(), "F", IO.ANSI.reset()]) 113 | IO.write(group_leader, msg) 114 | end 115 | 116 | print_loop(group_leader, opts) 117 | 118 | {:timeout, _} -> 119 | unless opts[:format] == "progress_bar" do 120 | msg = IO.iodata_to_binary([IO.ANSI.yellow(), "*", IO.ANSI.reset()]) 121 | IO.write(group_leader, msg) 122 | end 123 | 124 | print_loop(group_leader, opts) 125 | 126 | {msg, _} -> 127 | if System.get_env("DEBUG") do 128 | IO.puts(group_leader, msg) 129 | end 130 | 131 | print_loop(group_leader, opts) 132 | 133 | msg -> 134 | if System.get_env("DEBUG") do 135 | IO.inspect(msg, label: "#{Node.self()}") 136 | end 137 | 138 | print_loop(group_leader, opts) 139 | end 140 | end 141 | 142 | defp print_time(time, space \\ "\n") do 143 | IO.puts(space) 144 | 145 | time 146 | |> ExUnit.Formatter.format_time(nil) 147 | |> IO.puts() 148 | end 149 | 150 | defp format_exception(struct) do 151 | formatter = &formatter(&1, &2, %{colors: colors()}) 152 | 153 | label_padding_size = 10 154 | padding_size = label_padding_size + byte_size(@counter_padding) 155 | 156 | formatted = 157 | [ 158 | note: format_message(struct.message, formatter) 159 | ] 160 | |> Kernel.++(format_context(struct, formatter, padding_size, :infinity)) 161 | |> format_meta(formatter, @counter_padding, label_padding_size) 162 | |> IO.iodata_to_binary() 163 | 164 | formatted 165 | end 166 | 167 | defp format_meta(fields, formatter, padding, padding_size) do 168 | for {label, value} <- fields, has_value?(value) do 169 | [padding, format_label(label, formatter, padding_size), value, "\n"] 170 | end 171 | end 172 | 173 | defp format_label(:note, _formatter, _padding_size), do: "" 174 | 175 | defp format_label(label, formatter, padding_size) do 176 | formatter.(:extra_info, String.pad_trailing("#{label}:", padding_size)) 177 | end 178 | 179 | defp inspect_multiline(expr, padding_size, width) do 180 | expr 181 | |> Algebra.to_doc(%Inspect.Opts{width: width}) 182 | |> Algebra.group() 183 | |> Algebra.nest(padding_size) 184 | |> Algebra.format(width) 185 | end 186 | 187 | defp format_sides(left, right, context, formatter, padding_size, width) do 188 | inspect = &inspect_multiline(&1, padding_size, width) 189 | 190 | case format_diff(left, right, context, formatter) do 191 | {result, _env} -> 192 | left = list_to_code(result.left, :delete) 193 | 194 | right = list_to_code(result.right, :insert) 195 | 196 | {left, right} 197 | 198 | nil -> 199 | {if_value(left, &code_multiline(&1, padding_size)), if_value(right, inspect)} 200 | end 201 | end 202 | 203 | defp format_diff(left, right, context, _) do 204 | if has_value?(left) and has_value?(right) do 205 | find_diff(left, right, context) 206 | end 207 | end 208 | 209 | defp find_diff(left, right, context) do 210 | task = Task.async(Diff, :compute, [left, right, context]) 211 | 212 | case Task.yield(task, 1500) || Task.shutdown(task, :brutal_kill) do 213 | {:ok, diff} -> diff 214 | nil -> nil 215 | end 216 | end 217 | 218 | defp format_context( 219 | %{left: left, right: right, context: context}, 220 | formatter, 221 | padding_size, 222 | width 223 | ) do 224 | {left, right} = format_sides(left, right, context, formatter, padding_size, width) 225 | [original: right, mutation: left] 226 | end 227 | 228 | defp has_value?(value) do 229 | value != @no_value 230 | end 231 | 232 | defp if_value(value, fun) do 233 | if has_value?(value) do 234 | fun.(value) 235 | else 236 | value 237 | end 238 | end 239 | 240 | defp code_multiline(expr, padding_size) do 241 | pad_multiline(Macro.to_string(expr), padding_size) 242 | end 243 | 244 | defp pad_multiline(expr, padding_size) when is_binary(expr) do 245 | padding = String.duplicate(" ", padding_size) 246 | String.replace(expr, "\n", "\n" <> padding) 247 | end 248 | 249 | defp format_message(value, formatter) do 250 | value = String.replace(value, "\n", "\n" <> @counter_padding) 251 | formatter.(:error_info, value) 252 | end 253 | 254 | defp formatter(:diff_enabled?, _, %{colors: colors}), do: colors[:enabled] 255 | 256 | defp formatter(:error_info, msg, config), do: colorize(:red, msg, config) 257 | 258 | defp formatter(:extra_info, msg, config), do: colorize(:cyan, msg, config) 259 | 260 | defp formatter(:location_info, msg, config), do: colorize([:bright, :black], msg, config) 261 | 262 | defp formatter(:diff_delete, doc, config), do: colorize_doc(:diff_delete, doc, config) 263 | 264 | defp formatter(:diff_delete_whitespace, doc, config), 265 | do: colorize_doc(:diff_delete_whitespace, doc, config) 266 | 267 | defp formatter(:diff_insert, doc, config), do: colorize_doc(:diff_insert, doc, config) 268 | 269 | defp formatter(:diff_insert_whitespace, doc, config), 270 | do: colorize_doc(:diff_insert_whitespace, doc, config) 271 | 272 | defp formatter(:blame_diff, msg, %{colors: colors} = config) do 273 | if colors[:enabled] do 274 | colorize(:red, msg, config) 275 | else 276 | "-" <> msg <> "-" 277 | end 278 | end 279 | 280 | defp formatter(_, msg, _config), do: msg 281 | 282 | defp colorize(escape, string, %{colors: colors}) do 283 | if colors[:enabled] do 284 | [escape, string, :reset] 285 | |> IO.ANSI.format_fragment(true) 286 | |> IO.iodata_to_binary() 287 | else 288 | string 289 | end 290 | end 291 | 292 | defp colorize_doc(escape, doc, %{colors: colors}) do 293 | if colors[:enabled] do 294 | Inspect.Algebra.color(doc, escape, %Inspect.Opts{syntax_colors: colors}) 295 | else 296 | doc 297 | end 298 | end 299 | 300 | @default_colors [ 301 | diff_delete: :red, 302 | diff_delete_whitespace: IO.ANSI.color_background(2, 0, 0), 303 | diff_insert: :green, 304 | diff_insert_whitespace: IO.ANSI.color_background(0, 2, 0) 305 | ] 306 | defp colors() do 307 | Keyword.put(@default_colors, :enabled, IO.ANSI.enabled?()) 308 | end 309 | 310 | defp list_to_code(diff, atom) do 311 | after_fun = fn 312 | [] -> {:cont, []} 313 | {block, meta, acc} -> {:cont, {block, meta, Enum.reverse(acc)}, []} 314 | end 315 | 316 | code = 317 | if atom == :delete do 318 | "\e[31m" 319 | else 320 | "\e[32m" 321 | end 322 | 323 | result = 324 | diff 325 | |> Enum.flat_map(&flatten(&1, nil, nil)) 326 | |> List.flatten() 327 | |> Enum.chunk_while({:__block__, [], []}, &do_chunk/2, after_fun) 328 | |> tl() 329 | |> Enum.map(fn {_, meta, strings} -> 330 | if meta[:diff] do 331 | "#{code}#{to_string(strings)}\e[0m" 332 | else 333 | to_string(strings) 334 | end 335 | end) 336 | |> to_string() 337 | |> String.replace(~r/\\"(?!.*\\")/, "\"") 338 | |> String.replace(~r/\\"(?!.*\\")/, "\"") 339 | |> String.replace("\\n", "\n") 340 | |> String.replace("\n", "\n ") 341 | 342 | result 343 | end 344 | 345 | defp flatten({block, meta, args}, _, _) when is_list(args) do 346 | Enum.map(args, &flatten(&1, block, meta)) 347 | end 348 | 349 | defp flatten(string, block, meta) when is_binary(string) do 350 | {block, meta, string} 351 | end 352 | 353 | defp do_chunk({block, meta, string}, {block, meta, strings}) do 354 | {:cont, {block, meta, [string | strings]}} 355 | end 356 | 357 | defp do_chunk({block, new_meta, string}, {block, meta, strings}) do 358 | {:cont, {block, meta, Enum.reverse(strings)}, {block, new_meta, [string]}} 359 | end 360 | end 361 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License 2 | 3 | By exercising the Licensed Rights (defined below), You accept and agree 4 | to be bound by the terms and conditions of this Creative Commons 5 | Attribution-NonCommercial-NoDerivatives 4.0 International Public 6 | License ("Public License"). To the extent this Public License may be 7 | interpreted as a contract, You are granted the Licensed Rights in 8 | consideration of Your acceptance of these terms and conditions, and the 9 | Licensor grants You such rights in consideration of benefits the 10 | Licensor receives from making the Licensed Material available under 11 | these terms and conditions. 12 | 13 | 14 | Section 1 -- Definitions. 15 | 16 | a. Adapted Material means material subject to Copyright and Similar 17 | Rights that is derived from or based upon the Licensed Material 18 | and in which the Licensed Material is translated, altered, 19 | arranged, transformed, or otherwise modified in a manner requiring 20 | permission under the Copyright and Similar Rights held by the 21 | Licensor. For purposes of this Public License, where the Licensed 22 | Material is a musical work, performance, or sound recording, 23 | Adapted Material is always produced where the Licensed Material is 24 | synched in timed relation with a moving image. 25 | 26 | b. Copyright and Similar Rights means copyright and/or similar rights 27 | closely related to copyright including, without limitation, 28 | performance, broadcast, sound recording, and Sui Generis Database 29 | Rights, without regard to how the rights are labeled or 30 | categorized. For purposes of this Public License, the rights 31 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 32 | Rights. 33 | 34 | c. Effective Technological Measures means those measures that, in the 35 | absence of proper authority, may not be circumvented under laws 36 | fulfilling obligations under Article 11 of the WIPO Copyright 37 | Treaty adopted on December 20, 1996, and/or similar international 38 | agreements. 39 | 40 | d. Exceptions and Limitations means fair use, fair dealing, and/or 41 | any other exception or limitation to Copyright and Similar Rights 42 | that applies to Your use of the Licensed Material. 43 | 44 | e. Licensed Material means the artistic or literary work, database, 45 | or other material to which the Licensor applied this Public 46 | License. 47 | 48 | f. Licensed Rights means the rights granted to You subject to the 49 | terms and conditions of this Public License, which are limited to 50 | all Copyright and Similar Rights that apply to Your use of the 51 | Licensed Material and that the Licensor has authority to license. 52 | 53 | g. Licensor means the individual(s) or entity(ies) granting rights 54 | under this Public License. 55 | 56 | h. NonCommercial means not primarily intended for or directed towards 57 | commercial advantage or monetary compensation. For purposes of 58 | this Public License, the exchange of the Licensed Material for 59 | other material subject to Copyright and Similar Rights by digital 60 | file-sharing or similar means is NonCommercial provided there is 61 | no payment of monetary compensation in connection with the 62 | exchange. 63 | 64 | i. Share means to provide material to the public by any means or 65 | process that requires permission under the Licensed Rights, such 66 | as reproduction, public display, public performance, distribution, 67 | dissemination, communication, or importation, and to make material 68 | available to the public including in ways that members of the 69 | public may access the material from a place and at a time 70 | individually chosen by them. 71 | 72 | j. Sui Generis Database Rights means rights other than copyright 73 | resulting from Directive 96/9/EC of the European Parliament and of 74 | the Council of 11 March 1996 on the legal protection of databases, 75 | as amended and/or succeeded, as well as other essentially 76 | equivalent rights anywhere in the world. 77 | 78 | k. You means the individual or entity exercising the Licensed Rights 79 | under this Public License. Your has a corresponding meaning. 80 | 81 | 82 | Section 2 -- Scope. 83 | 84 | a. License grant. 85 | 86 | 1. Subject to the terms and conditions of this Public License, 87 | the Licensor hereby grants You a worldwide, royalty-free, 88 | non-sublicensable, non-exclusive, irrevocable license to 89 | exercise the Licensed Rights in the Licensed Material to: 90 | 91 | a. reproduce and Share the Licensed Material, in whole or 92 | in part, for NonCommercial purposes only; and 93 | 94 | b. produce and reproduce, but not Share, Adapted Material 95 | for NonCommercial purposes only. 96 | 97 | 2. Exceptions and Limitations. For the avoidance of doubt, where 98 | Exceptions and Limitations apply to Your use, this Public 99 | License does not apply, and You do not need to comply with 100 | its terms and conditions. 101 | 102 | 3. Term. The term of this Public License is specified in Section 103 | 6(a). 104 | 105 | 4. Media and formats; technical modifications allowed. The 106 | Licensor authorizes You to exercise the Licensed Rights in 107 | all media and formats whether now known or hereafter created, 108 | and to make technical modifications necessary to do so. The 109 | Licensor waives and/or agrees not to assert any right or 110 | authority to forbid You from making technical modifications 111 | necessary to exercise the Licensed Rights, including 112 | technical modifications necessary to circumvent Effective 113 | Technological Measures. For purposes of this Public License, 114 | simply making modifications authorized by this Section 2(a) 115 | (4) never produces Adapted Material. 116 | 117 | 5. Downstream recipients. 118 | 119 | a. Offer from the Licensor -- Licensed Material. Every 120 | recipient of the Licensed Material automatically 121 | receives an offer from the Licensor to exercise the 122 | Licensed Rights under the terms and conditions of this 123 | Public License. 124 | 125 | b. No downstream restrictions. You may not offer or impose 126 | any additional or different terms or conditions on, or 127 | apply any Effective Technological Measures to, the 128 | Licensed Material if doing so restricts exercise of the 129 | Licensed Rights by any recipient of the Licensed 130 | Material. 131 | 132 | 6. No endorsement. Nothing in this Public License constitutes or 133 | may be construed as permission to assert or imply that You 134 | are, or that Your use of the Licensed Material is, connected 135 | with, or sponsored, endorsed, or granted official status by, 136 | the Licensor or others designated to receive attribution as 137 | provided in Section 3(a)(1)(A)(i). 138 | 139 | b. Other rights. 140 | 141 | 1. Moral rights, such as the right of integrity, are not 142 | licensed under this Public License, nor are publicity, 143 | privacy, and/or other similar personality rights; however, to 144 | the extent possible, the Licensor waives and/or agrees not to 145 | assert any such rights held by the Licensor to the limited 146 | extent necessary to allow You to exercise the Licensed 147 | Rights, but not otherwise. 148 | 149 | 2. Patent and trademark rights are not licensed under this 150 | Public License. 151 | 152 | 3. To the extent possible, the Licensor waives any right to 153 | collect royalties from You for the exercise of the Licensed 154 | Rights, whether directly or through a collecting society 155 | under any voluntary or waivable statutory or compulsory 156 | licensing scheme. In all other cases the Licensor expressly 157 | reserves any right to collect such royalties, including when 158 | the Licensed Material is used other than for NonCommercial 159 | purposes. 160 | 161 | 162 | Section 3 -- License Conditions. 163 | 164 | Your exercise of the Licensed Rights is expressly made subject to the 165 | following conditions. 166 | 167 | a. Attribution. 168 | 169 | 1. If You Share the Licensed Material, You must: 170 | 171 | a. retain the following if it is supplied by the Licensor 172 | with the Licensed Material: 173 | 174 | i. identification of the creator(s) of the Licensed 175 | Material and any others designated to receive 176 | attribution, in any reasonable manner requested by 177 | the Licensor (including by pseudonym if 178 | designated); 179 | 180 | ii. a copyright notice; 181 | 182 | iii. a notice that refers to this Public License; 183 | 184 | iv. a notice that refers to the disclaimer of 185 | warranties; 186 | 187 | v. a URI or hyperlink to the Licensed Material to the 188 | extent reasonably practicable; 189 | 190 | b. indicate if You modified the Licensed Material and 191 | retain an indication of any previous modifications; and 192 | 193 | c. indicate the Licensed Material is licensed under this 194 | Public License, and include the text of, or the URI or 195 | hyperlink to, this Public License. 196 | 197 | For the avoidance of doubt, You do not have permission under 198 | this Public License to Share Adapted Material. 199 | 200 | 2. You may satisfy the conditions in Section 3(a)(1) in any 201 | reasonable manner based on the medium, means, and context in 202 | which You Share the Licensed Material. For example, it may be 203 | reasonable to satisfy the conditions by providing a URI or 204 | hyperlink to a resource that includes the required 205 | information. 206 | 207 | 3. If requested by the Licensor, You must remove any of the 208 | information required by Section 3(a)(1)(A) to the extent 209 | reasonably practicable. 210 | 211 | 212 | Section 4 -- Sui Generis Database Rights. 213 | 214 | Where the Licensed Rights include Sui Generis Database Rights that 215 | apply to Your use of the Licensed Material: 216 | 217 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 218 | to extract, reuse, reproduce, and Share all or a substantial 219 | portion of the contents of the database for NonCommercial purposes 220 | only and provided You do not Share Adapted Material; 221 | 222 | b. if You include all or a substantial portion of the database 223 | contents in a database in which You have Sui Generis Database 224 | Rights, then the database in which You have Sui Generis Database 225 | Rights (but not its individual contents) is Adapted Material; and 226 | 227 | c. You must comply with the conditions in Section 3(a) if You Share 228 | all or a substantial portion of the contents of the database. 229 | 230 | For the avoidance of doubt, this Section 4 supplements and does not 231 | replace Your obligations under this Public License where the Licensed 232 | Rights include other Copyright and Similar Rights. 233 | 234 | 235 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 236 | 237 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 238 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 239 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 240 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 241 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 242 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 243 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 244 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 245 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 246 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 247 | 248 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 249 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 250 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 251 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 252 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 253 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 254 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 255 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 256 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 257 | 258 | c. The disclaimer of warranties and limitation of liability provided 259 | above shall be interpreted in a manner that, to the extent 260 | possible, most closely approximates an absolute disclaimer and 261 | waiver of all liability. 262 | 263 | 264 | Section 6 -- Term and Termination. 265 | 266 | a. This Public License applies for the term of the Copyright and 267 | Similar Rights licensed here. However, if You fail to comply with 268 | this Public License, then Your rights under this Public License 269 | terminate automatically. 270 | 271 | b. Where Your right to use the Licensed Material has terminated under 272 | Section 6(a), it reinstates: 273 | 274 | 1. automatically as of the date the violation is cured, provided 275 | it is cured within 30 days of Your discovery of the 276 | violation; or 277 | 278 | 2. upon express reinstatement by the Licensor. 279 | 280 | For the avoidance of doubt, this Section 6(b) does not affect any 281 | right the Licensor may have to seek remedies for Your violations 282 | of this Public License. 283 | 284 | c. For the avoidance of doubt, the Licensor may also offer the 285 | Licensed Material under separate terms or conditions or stop 286 | distributing the Licensed Material at any time; however, doing so 287 | will not terminate this Public License. 288 | 289 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 290 | License. 291 | 292 | 293 | Section 7 -- Other Terms and Conditions. 294 | 295 | a. The Licensor shall not be bound by any additional or different 296 | terms or conditions communicated by You unless expressly agreed. 297 | 298 | b. Any arrangements, understandings, or agreements regarding the 299 | Licensed Material not stated herein are separate from and 300 | independent of the terms and conditions of this Public License. 301 | 302 | 303 | Section 8 -- Interpretation. 304 | 305 | a. For the avoidance of doubt, this Public License does not, and 306 | shall not be interpreted to, reduce, limit, restrict, or impose 307 | conditions on any use of the Licensed Material that could lawfully 308 | be made without permission under this Public License. 309 | 310 | b. To the extent possible, if any provision of this Public License is 311 | deemed unenforceable, it shall be automatically reformed to the 312 | minimum extent necessary to make it enforceable. If the provision 313 | cannot be reformed, it shall be severed from this Public License 314 | without affecting the enforceability of the remaining terms and 315 | conditions. 316 | 317 | c. No term or condition of this Public License will be waived and no 318 | failure to comply consented to unless expressly agreed to by the 319 | Licensor. 320 | 321 | d. Nothing in this Public License constitutes or may be interpreted 322 | as a limitation upon, or waiver of, any privileges and immunities 323 | that apply to the Licensor or You, including from the legal 324 | processes of any jurisdiction or authority. 325 | 326 | ======================================================================= 327 | 328 | Creative Commons is not a party to its public licenses. 329 | Notwithstanding, Creative Commons may elect to apply one of 330 | its public licenses to material it publishes and in those instances 331 | will be considered the “Licensor.” The text of the Creative Commons 332 | public licenses is dedicated to the public domain under the CC0 Public 333 | Domain Dedication. Except for the limited purpose of indicating that 334 | material is shared under a Creative Commons public license or as 335 | otherwise permitted by the Creative Commons policies published at 336 | creativecommons.org/policies, Creative Commons does not authorize the 337 | use of the trademark "Creative Commons" or any other trademark or logo 338 | of Creative Commons without its prior written consent including, 339 | without limitation, in connection with any unauthorized modifications 340 | to any of its public licenses or any other arrangements, 341 | understandings, or agreements concerning use of licensed material. For 342 | the avoidance of doubt, this paragraph does not form part of the 343 | public licenses. 344 | 345 | Creative Commons may be contacted at creativecommons.org. 346 | -------------------------------------------------------------------------------- /test/muzak/mutators/functions/rename_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Mutators.Functions.RenameTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Muzak.{Code.Formatter, Mutators.Functions.Rename} 5 | 6 | @ast Formatter.to_ast(""" 7 | defmodule Tester do 8 | defmacrop priv_macro(arg) do 9 | quote do 10 | priv_fun(arg) 11 | end 12 | end 13 | 14 | defmacro my_macro(arg) do 15 | quote do 16 | my_fun(arg) 17 | end 18 | end 19 | 20 | def my_fun(arg), do: priv_fun(arg) 21 | 22 | defp priv_fun(arg) do 23 | arg + 1 24 | end 25 | end 26 | """) 27 | 28 | describe "mutate/2" do 29 | test "changes the name of functions and macros (part 1)" do 30 | node = 31 | {:defmacrop, 32 | [ 33 | end_of_expression: [newlines: 2, line: 6], 34 | do: [line: 2], 35 | end: [line: 6], 36 | line: 2 37 | ], 38 | [ 39 | {:priv_macro, [closing: [line: 2], line: 2], [{:arg, [line: 2], nil}]}, 40 | [ 41 | {{:__block__, [line: 2], [:do]}, 42 | {:quote, [do: [line: 3], end: [line: 5], line: 3], 43 | [ 44 | [ 45 | {{:__block__, [line: 3], [:do]}, 46 | {:priv_fun, [closing: [line: 4], line: 4], [{:arg, [line: 4], nil}]}} 47 | ] 48 | ]}} 49 | ] 50 | ]} 51 | 52 | assert Rename.mutate(node, @ast, &name_fun/1) == %{ 53 | example_format: :line, 54 | line: 2, 55 | mutated_ast: { 56 | :defmodule, 57 | [do: [line: 1], end: [line: 19], line: 1], 58 | [ 59 | {:__aliases__, [{:last, [line: 1]}, {:line, 1}], [:Tester]}, 60 | [ 61 | { 62 | {:__block__, [line: 1], [:do]}, 63 | { 64 | :__block__, 65 | [], 66 | [ 67 | { 68 | :defmacrop, 69 | [ 70 | end_of_expression: [newlines: 2, line: 6], 71 | do: [line: 2], 72 | end: [line: 6], 73 | line: 2 74 | ], 75 | [ 76 | {:random_function, [closing: [line: 2], line: 2], 77 | [{:arg, [line: 2], nil}]}, 78 | [ 79 | { 80 | {:__block__, [line: 2], [:do]}, 81 | { 82 | :quote, 83 | [do: [line: 3], end: [line: 5], line: 3], 84 | [ 85 | [ 86 | { 87 | {:__block__, [line: 3], [:do]}, 88 | {:priv_fun, [closing: [line: 4], line: 4], 89 | [{:arg, [line: 4], nil}]} 90 | } 91 | ] 92 | ] 93 | } 94 | } 95 | ] 96 | ] 97 | }, 98 | { 99 | :defmacro, 100 | [ 101 | end_of_expression: [newlines: 2, line: 12], 102 | do: [line: 8], 103 | end: [line: 12], 104 | line: 8 105 | ], 106 | [ 107 | {:my_macro, [closing: [line: 8], line: 8], 108 | [{:arg, [line: 8], nil}]}, 109 | [ 110 | { 111 | {:__block__, [line: 8], [:do]}, 112 | { 113 | :quote, 114 | [do: [line: 9], end: [line: 11], line: 9], 115 | [ 116 | [ 117 | { 118 | {:__block__, [line: 9], [:do]}, 119 | {:my_fun, [closing: [line: 10], line: 10], 120 | [{:arg, [line: 10], nil}]} 121 | } 122 | ] 123 | ] 124 | } 125 | } 126 | ] 127 | ] 128 | }, 129 | { 130 | :def, 131 | [end_of_expression: [newlines: 2, line: 14], line: 14], 132 | [ 133 | {:my_fun, [closing: [line: 14], line: 14], 134 | [{:arg, [line: 14], nil}]}, 135 | [ 136 | { 137 | {:__block__, [{:format, :keyword}, {:line, 14}], [:do]}, 138 | {:priv_fun, [closing: [line: 14], line: 14], 139 | [{:arg, [line: 14], nil}]} 140 | } 141 | ] 142 | ] 143 | }, 144 | { 145 | :defp, 146 | [do: [line: 16], end: [line: 18], line: 16], 147 | [ 148 | {:priv_fun, [closing: [line: 16], line: 16], 149 | [{:arg, [line: 16], nil}]}, 150 | [ 151 | { 152 | {:__block__, [line: 16], [:do]}, 153 | {:+, [line: 17], 154 | [ 155 | {:arg, [line: 17], nil}, 156 | {:__block__, [token: "1", line: 17], [1]} 157 | ]} 158 | } 159 | ] 160 | ] 161 | } 162 | ] 163 | } 164 | } 165 | ] 166 | ] 167 | } 168 | } 169 | end 170 | 171 | test "changes the name of functions and macros (part 2)" do 172 | node = 173 | {:defmacro, 174 | [ 175 | end_of_expression: [newlines: 2, line: 12], 176 | do: [line: 8], 177 | end: [line: 12], 178 | line: 8 179 | ], 180 | [ 181 | {:my_macro, [closing: [line: 8], line: 8], [{:arg, [line: 8], nil}]}, 182 | [ 183 | {{:__block__, [line: 8], [:do]}, 184 | {:quote, [do: [line: 9], end: [line: 11], line: 9], 185 | [ 186 | [ 187 | {{:__block__, [line: 9], [:do]}, 188 | {:my_fun, [closing: [line: 10], line: 10], [{:arg, [line: 10], nil}]}} 189 | ] 190 | ]}} 191 | ] 192 | ]} 193 | 194 | assert Rename.mutate(node, @ast, &name_fun/1) == %{ 195 | example_format: :line, 196 | line: 8, 197 | mutated_ast: { 198 | :defmodule, 199 | [do: [line: 1], end: [line: 19], line: 1], 200 | [ 201 | {:__aliases__, [{:last, [line: 1]}, {:line, 1}], [:Tester]}, 202 | [ 203 | { 204 | {:__block__, [line: 1], [:do]}, 205 | { 206 | :__block__, 207 | [], 208 | [ 209 | { 210 | :defmacrop, 211 | [ 212 | end_of_expression: [newlines: 2, line: 6], 213 | do: [line: 2], 214 | end: [line: 6], 215 | line: 2 216 | ], 217 | [ 218 | {:priv_macro, [closing: [line: 2], line: 2], 219 | [{:arg, [line: 2], nil}]}, 220 | [ 221 | { 222 | {:__block__, [line: 2], [:do]}, 223 | { 224 | :quote, 225 | [do: [line: 3], end: [line: 5], line: 3], 226 | [ 227 | [ 228 | { 229 | {:__block__, [line: 3], [:do]}, 230 | {:priv_fun, [closing: [line: 4], line: 4], 231 | [{:arg, [line: 4], nil}]} 232 | } 233 | ] 234 | ] 235 | } 236 | } 237 | ] 238 | ] 239 | }, 240 | { 241 | :defmacro, 242 | [ 243 | end_of_expression: [newlines: 2, line: 12], 244 | do: [line: 8], 245 | end: [line: 12], 246 | line: 8 247 | ], 248 | [ 249 | {:random_function, [closing: [line: 8], line: 8], 250 | [{:arg, [line: 8], nil}]}, 251 | [ 252 | { 253 | {:__block__, [line: 8], [:do]}, 254 | { 255 | :quote, 256 | [do: [line: 9], end: [line: 11], line: 9], 257 | [ 258 | [ 259 | { 260 | {:__block__, [line: 9], [:do]}, 261 | {:my_fun, [closing: [line: 10], line: 10], 262 | [{:arg, [line: 10], nil}]} 263 | } 264 | ] 265 | ] 266 | } 267 | } 268 | ] 269 | ] 270 | }, 271 | { 272 | :def, 273 | [end_of_expression: [newlines: 2, line: 14], line: 14], 274 | [ 275 | {:my_fun, [closing: [line: 14], line: 14], 276 | [{:arg, [line: 14], nil}]}, 277 | [ 278 | { 279 | {:__block__, [{:format, :keyword}, {:line, 14}], [:do]}, 280 | {:priv_fun, [closing: [line: 14], line: 14], 281 | [{:arg, [line: 14], nil}]} 282 | } 283 | ] 284 | ] 285 | }, 286 | { 287 | :defp, 288 | [do: [line: 16], end: [line: 18], line: 16], 289 | [ 290 | {:priv_fun, [closing: [line: 16], line: 16], 291 | [{:arg, [line: 16], nil}]}, 292 | [ 293 | { 294 | {:__block__, [line: 16], [:do]}, 295 | {:+, [line: 17], 296 | [ 297 | {:arg, [line: 17], nil}, 298 | {:__block__, [token: "1", line: 17], [1]} 299 | ]} 300 | } 301 | ] 302 | ] 303 | } 304 | ] 305 | } 306 | } 307 | ] 308 | ] 309 | } 310 | } 311 | end 312 | 313 | test "changes the name of functions and macros (part 3)" do 314 | node = 315 | {:def, [end_of_expression: [newlines: 2, line: 14], line: 14], 316 | [ 317 | {:my_fun, [closing: [line: 14], line: 14], [{:arg, [line: 14], nil}]}, 318 | [ 319 | {{:__block__, [format: :keyword, line: 14], [:do]}, 320 | {:priv_fun, [closing: [line: 14], line: 14], [{:arg, [line: 14], nil}]}} 321 | ] 322 | ]} 323 | 324 | assert Rename.mutate(node, @ast, &name_fun/1) == %{ 325 | example_format: :line, 326 | line: 14, 327 | mutated_ast: { 328 | :defmodule, 329 | [do: [line: 1], end: [line: 19], line: 1], 330 | [ 331 | {:__aliases__, [{:last, [line: 1]}, {:line, 1}], [:Tester]}, 332 | [ 333 | { 334 | {:__block__, [line: 1], [:do]}, 335 | { 336 | :__block__, 337 | [], 338 | [ 339 | { 340 | :defmacrop, 341 | [ 342 | end_of_expression: [newlines: 2, line: 6], 343 | do: [line: 2], 344 | end: [line: 6], 345 | line: 2 346 | ], 347 | [ 348 | {:priv_macro, [closing: [line: 2], line: 2], 349 | [{:arg, [line: 2], nil}]}, 350 | [ 351 | { 352 | {:__block__, [line: 2], [:do]}, 353 | { 354 | :quote, 355 | [do: [line: 3], end: [line: 5], line: 3], 356 | [ 357 | [ 358 | { 359 | {:__block__, [line: 3], [:do]}, 360 | {:priv_fun, [closing: [line: 4], line: 4], 361 | [{:arg, [line: 4], nil}]} 362 | } 363 | ] 364 | ] 365 | } 366 | } 367 | ] 368 | ] 369 | }, 370 | { 371 | :defmacro, 372 | [ 373 | end_of_expression: [newlines: 2, line: 12], 374 | do: [line: 8], 375 | end: [line: 12], 376 | line: 8 377 | ], 378 | [ 379 | {:my_macro, [closing: [line: 8], line: 8], 380 | [{:arg, [line: 8], nil}]}, 381 | [ 382 | { 383 | {:__block__, [line: 8], [:do]}, 384 | { 385 | :quote, 386 | [do: [line: 9], end: [line: 11], line: 9], 387 | [ 388 | [ 389 | { 390 | {:__block__, [line: 9], [:do]}, 391 | {:my_fun, [closing: [line: 10], line: 10], 392 | [{:arg, [line: 10], nil}]} 393 | } 394 | ] 395 | ] 396 | } 397 | } 398 | ] 399 | ] 400 | }, 401 | { 402 | :def, 403 | [end_of_expression: [newlines: 2, line: 14], line: 14], 404 | [ 405 | {:random_function, [closing: [line: 14], line: 14], 406 | [{:arg, [line: 14], nil}]}, 407 | [ 408 | { 409 | {:__block__, [{:format, :keyword}, {:line, 14}], [:do]}, 410 | {:priv_fun, [closing: [line: 14], line: 14], 411 | [{:arg, [line: 14], nil}]} 412 | } 413 | ] 414 | ] 415 | }, 416 | { 417 | :defp, 418 | [do: [line: 16], end: [line: 18], line: 16], 419 | [ 420 | {:priv_fun, [closing: [line: 16], line: 16], 421 | [{:arg, [line: 16], nil}]}, 422 | [ 423 | { 424 | {:__block__, [line: 16], [:do]}, 425 | {:+, [line: 17], 426 | [ 427 | {:arg, [line: 17], nil}, 428 | {:__block__, [token: "1", line: 17], [1]} 429 | ]} 430 | } 431 | ] 432 | ] 433 | } 434 | ] 435 | } 436 | } 437 | ] 438 | ] 439 | } 440 | } 441 | end 442 | 443 | test "changes the name of functions and macros (part 4)" do 444 | node = 445 | {:defp, [do: [line: 16], end: [line: 18], line: 16], 446 | [ 447 | {:priv_fun, [closing: [line: 16], line: 16], [{:arg, [line: 16], nil}]}, 448 | [ 449 | {{:__block__, [line: 16], [:do]}, 450 | {:+, [line: 17], 451 | [ 452 | {:arg, [line: 17], nil}, 453 | {:__block__, [token: "1", line: 17], [1]} 454 | ]}} 455 | ] 456 | ]} 457 | 458 | assert Rename.mutate(node, @ast, &name_fun/1) == %{ 459 | example_format: :line, 460 | line: 16, 461 | mutated_ast: { 462 | :defmodule, 463 | [do: [line: 1], end: [line: 19], line: 1], 464 | [ 465 | {:__aliases__, [{:last, [line: 1]}, {:line, 1}], [:Tester]}, 466 | [ 467 | { 468 | {:__block__, [line: 1], [:do]}, 469 | { 470 | :__block__, 471 | [], 472 | [ 473 | { 474 | :defmacrop, 475 | [ 476 | end_of_expression: [newlines: 2, line: 6], 477 | do: [line: 2], 478 | end: [line: 6], 479 | line: 2 480 | ], 481 | [ 482 | {:priv_macro, [closing: [line: 2], line: 2], 483 | [{:arg, [line: 2], nil}]}, 484 | [ 485 | { 486 | {:__block__, [line: 2], [:do]}, 487 | { 488 | :quote, 489 | [do: [line: 3], end: [line: 5], line: 3], 490 | [ 491 | [ 492 | { 493 | {:__block__, [line: 3], [:do]}, 494 | {:priv_fun, [closing: [line: 4], line: 4], 495 | [{:arg, [line: 4], nil}]} 496 | } 497 | ] 498 | ] 499 | } 500 | } 501 | ] 502 | ] 503 | }, 504 | { 505 | :defmacro, 506 | [ 507 | end_of_expression: [newlines: 2, line: 12], 508 | do: [line: 8], 509 | end: [line: 12], 510 | line: 8 511 | ], 512 | [ 513 | {:my_macro, [closing: [line: 8], line: 8], 514 | [{:arg, [line: 8], nil}]}, 515 | [ 516 | { 517 | {:__block__, [line: 8], [:do]}, 518 | { 519 | :quote, 520 | [do: [line: 9], end: [line: 11], line: 9], 521 | [ 522 | [ 523 | { 524 | {:__block__, [line: 9], [:do]}, 525 | {:my_fun, [closing: [line: 10], line: 10], 526 | [{:arg, [line: 10], nil}]} 527 | } 528 | ] 529 | ] 530 | } 531 | } 532 | ] 533 | ] 534 | }, 535 | { 536 | :def, 537 | [end_of_expression: [newlines: 2, line: 14], line: 14], 538 | [ 539 | {:my_fun, [closing: [line: 14], line: 14], 540 | [{:arg, [line: 14], nil}]}, 541 | [ 542 | { 543 | {:__block__, [{:format, :keyword}, {:line, 14}], [:do]}, 544 | {:priv_fun, [closing: [line: 14], line: 14], 545 | [{:arg, [line: 14], nil}]} 546 | } 547 | ] 548 | ] 549 | }, 550 | { 551 | :defp, 552 | [do: [line: 16], end: [line: 18], line: 16], 553 | [ 554 | {:random_function, [closing: [line: 16], line: 16], 555 | [{:arg, [line: 16], nil}]}, 556 | [ 557 | { 558 | {:__block__, [line: 16], [:do]}, 559 | {:+, [line: 17], 560 | [ 561 | {:arg, [line: 17], nil}, 562 | {:__block__, [token: "1", line: 17], [1]} 563 | ]} 564 | } 565 | ] 566 | ] 567 | } 568 | ] 569 | } 570 | } 571 | ] 572 | ] 573 | } 574 | } 575 | end 576 | end 577 | 578 | defp name_fun(_), do: "random_function" 579 | end 580 | -------------------------------------------------------------------------------- /lib/muzak/code/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Muzak.Code.Formatter do 2 | @moduledoc false 3 | 4 | # This is mostly copied over from Elixir core and just modified a bit to expose some of the 5 | # internals that I needed. 6 | 7 | import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4] 8 | 9 | @double_quote "\"" 10 | @double_heredoc "\"\"\"" 11 | @single_quote "'" 12 | @single_heredoc "'''" 13 | @newlines 2 14 | @min_line 0 15 | @max_line 9_999_999 16 | @empty empty() 17 | @ampersand_prec Code.Identifier.unary_op(:&) |> elem(1) 18 | 19 | # Operators that do not have space between operands 20 | @no_space_binary_operators [:..] 21 | 22 | # Operators that do not have newline between operands (as well as => and keywords) 23 | @no_newline_binary_operators [:\\, :in] 24 | 25 | # Left associative operators that start on the next line in case of breaks (always pipes) 26 | @pipeline_operators [:|>, :~>>, :<<~, :~>, :<~, :<~>, :"<|>"] 27 | 28 | # Right associative operators that start on the next line in case of breaks 29 | @right_new_line_before_binary_operators [:|, :when] 30 | 31 | # Operators that are logical cannot be mixed without parens 32 | @required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and] 33 | 34 | # Operators with next break fits. = and :: do not consider new lines though 35 | @next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"] 36 | 37 | # Operators that always require parens on operands when they are the parent 38 | @required_parens_on_binary_operands [ 39 | :|>, 40 | :<<<, 41 | :>>>, 42 | :<~, 43 | :~>, 44 | :<<~, 45 | :~>>, 46 | :<~>, 47 | :"<|>", 48 | :in, 49 | :++, 50 | :--, 51 | :.., 52 | :<> 53 | ] 54 | 55 | @locals_without_parens [ 56 | # Special forms 57 | alias: 1, 58 | alias: 2, 59 | case: 2, 60 | cond: 1, 61 | for: :*, 62 | import: 1, 63 | import: 2, 64 | quote: 1, 65 | quote: 2, 66 | receive: 1, 67 | require: 1, 68 | require: 2, 69 | try: 1, 70 | with: :*, 71 | 72 | # Kernel 73 | def: 1, 74 | def: 2, 75 | defp: 1, 76 | defp: 2, 77 | defguard: 1, 78 | defguardp: 1, 79 | defmacro: 1, 80 | defmacro: 2, 81 | defmacrop: 1, 82 | defmacrop: 2, 83 | defmodule: 2, 84 | defdelegate: 2, 85 | defexception: 1, 86 | defoverridable: 1, 87 | defstruct: 1, 88 | destructure: 2, 89 | raise: 1, 90 | raise: 2, 91 | reraise: 2, 92 | reraise: 3, 93 | if: 2, 94 | unless: 2, 95 | use: 1, 96 | use: 2, 97 | 98 | # Stdlib, 99 | defrecord: 2, 100 | defrecord: 3, 101 | defrecordp: 2, 102 | defrecordp: 3, 103 | 104 | # Testing 105 | assert: 1, 106 | assert: 2, 107 | assert_in_delta: 3, 108 | assert_in_delta: 4, 109 | assert_raise: 2, 110 | assert_raise: 3, 111 | assert_receive: 1, 112 | assert_receive: 2, 113 | assert_receive: 3, 114 | assert_received: 1, 115 | assert_received: 2, 116 | doctest: 1, 117 | doctest: 2, 118 | refute: 1, 119 | refute: 2, 120 | refute_in_delta: 3, 121 | refute_in_delta: 4, 122 | refute_receive: 1, 123 | refute_receive: 2, 124 | refute_receive: 3, 125 | refute_received: 1, 126 | refute_received: 2, 127 | setup: 1, 128 | setup: 2, 129 | setup_all: 1, 130 | setup_all: 2, 131 | test: 1, 132 | test: 2, 133 | 134 | # Mix config 135 | config: 2, 136 | config: 3, 137 | import_config: 1 138 | ] 139 | 140 | @do_end_keywords [:rescue, :catch, :else, :after] 141 | 142 | # Converts `string` to an algebra document. 143 | # See `Code.format_string!/2` for the list of options. 144 | @doc false 145 | def to_forms_and_state(string, opts \\ []) when is_binary(string) and is_list(opts) do 146 | file = Keyword.get(opts, :file, "nofile") 147 | line = Keyword.get(opts, :line, 1) 148 | charlist = String.to_charlist(string) 149 | 150 | Process.put(:code_formatter_comments, []) 151 | 152 | tokenizer_options = [ 153 | unescape: false, 154 | preserve_comments: &preserve_comments/5, 155 | warn_on_unnecessary_quotes: false 156 | ] 157 | 158 | parser_options = [ 159 | literal_encoder: &{:ok, {:__block__, &2, [&1]}}, 160 | token_metadata: true 161 | ] 162 | 163 | tokenizer = 164 | if function_exported?(:elixir, :string_to_tokens, 5) do 165 | &apply(:elixir, :string_to_tokens, [&1, &2, 1, &3, &4]) 166 | else 167 | &apply(:elixir, :string_to_tokens, [&1, &2, &3, &4]) 168 | end 169 | 170 | with {:ok, tokens} <- tokenizer.(charlist, line, file, tokenizer_options), 171 | {:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, parser_options) do 172 | state = 173 | Process.get(:code_formatter_comments) 174 | |> Enum.reverse() 175 | |> gather_comments() 176 | |> state(opts) 177 | 178 | {forms, state} 179 | end 180 | after 181 | Process.delete(:code_formatter_comments) 182 | end 183 | 184 | def to_ast(string, opts \\ []) do 185 | string |> to_forms_and_state(opts) |> elem(0) 186 | end 187 | 188 | def to_string(forms, state), do: forms |> to_algebra(state) |> to_string() 189 | 190 | def to_algebra(string) do 191 | {forms, state} = to_forms_and_state(string) 192 | to_algebra(forms, state) 193 | end 194 | 195 | def to_algebra(forms, state) do 196 | {doc, _} = block_to_algebra(forms, state, @min_line, @max_line) 197 | doc |> Inspect.Algebra.format(@max_line) 198 | end 199 | 200 | defp state(comments, opts) do 201 | force_do_end_blocks = Keyword.get(opts, :force_do_end_blocks, false) 202 | 203 | rename_deprecated_at = 204 | if version = opts[:rename_deprecated_at] do 205 | case Version.parse(version) do 206 | {:ok, parsed} -> 207 | parsed 208 | 209 | :error -> 210 | raise ArgumentError, 211 | "invalid version #{inspect(version)} given to :rename_deprecated_at" 212 | end 213 | end 214 | 215 | locals_without_parens = 216 | Keyword.get(opts, :locals_without_parens, []) ++ @locals_without_parens 217 | 218 | %{ 219 | force_do_end_blocks: force_do_end_blocks, 220 | locals_without_parens: locals_without_parens, 221 | operand_nesting: 2, 222 | rename_deprecated_at: rename_deprecated_at, 223 | comments: comments 224 | } 225 | end 226 | 227 | # Code comment handling 228 | 229 | defp preserve_comments(line, _column, tokens, comment, rest) do 230 | comments = Process.get(:code_formatter_comments) 231 | comment = {line, {previous_eol(tokens), next_eol(rest, 0)}, format_comment(comment, [])} 232 | Process.put(:code_formatter_comments, [comment | comments]) 233 | end 234 | 235 | defp next_eol('\s' ++ rest, count), do: next_eol(rest, count) 236 | defp next_eol('\t' ++ rest, count), do: next_eol(rest, count) 237 | defp next_eol('\n' ++ rest, count), do: next_eol(rest, count + 1) 238 | defp next_eol('\r\n' ++ rest, count), do: next_eol(rest, count + 1) 239 | defp next_eol(_, count), do: count 240 | 241 | defp previous_eol([{token, {_, _, count}} | _]) 242 | when token in [:eol, :",", :";"] and count > 0 do 243 | count 244 | end 245 | 246 | defp previous_eol([]), do: 1 247 | defp previous_eol(_), do: nil 248 | 249 | defp format_comment('##' ++ rest, acc), do: format_comment([?# | rest], [?# | acc]) 250 | 251 | defp format_comment('#!', acc), do: reverse_to_string(acc, '#!') 252 | defp format_comment('#! ' ++ _ = rest, acc), do: reverse_to_string(acc, rest) 253 | defp format_comment('#!' ++ rest, acc), do: reverse_to_string(acc, [?#, ?!, ?\s, rest]) 254 | 255 | defp format_comment('#', acc), do: reverse_to_string(acc, '#') 256 | defp format_comment('# ' ++ _ = rest, acc), do: reverse_to_string(acc, rest) 257 | defp format_comment('#' ++ rest, acc), do: reverse_to_string(acc, [?#, ?\s, rest]) 258 | 259 | defp reverse_to_string(acc, prefix) do 260 | acc |> Enum.reverse(prefix) |> List.to_string() 261 | end 262 | 263 | # If there is a no new line before, we can't gather all followup comments. 264 | defp gather_comments([{line, {nil, next_eol}, doc} | comments]) do 265 | comment = {line, {@newlines, next_eol}, doc} 266 | [comment | gather_comments(comments)] 267 | end 268 | 269 | defp gather_comments([{line, {previous_eol, next_eol}, doc} | comments]) do 270 | {next_eol, comments, doc} = gather_followup_comments(line + 1, next_eol, comments, doc) 271 | comment = {line, {previous_eol, next_eol}, doc} 272 | [comment | gather_comments(comments)] 273 | end 274 | 275 | defp gather_comments([]) do 276 | [] 277 | end 278 | 279 | defp gather_followup_comments(line, _, [{line, {previous_eol, next_eol}, text} | comments], doc) 280 | when previous_eol != nil do 281 | gather_followup_comments(line + 1, next_eol, comments, line(doc, text)) 282 | end 283 | 284 | defp gather_followup_comments(_line, next_eol, comments, doc) do 285 | {next_eol, comments, doc} 286 | end 287 | 288 | # Special AST nodes from compiler feedback. 289 | 290 | defp quoted_to_algebra({{:special, :clause_args}, _meta, [args]}, _context, state) do 291 | {doc, state} = clause_args_to_algebra(args, state) 292 | {group(doc), state} 293 | end 294 | 295 | defp quoted_to_algebra({{:special, :bitstring_segment}, _meta, [arg, last]}, _context, state) do 296 | bitstring_segment_to_algebra({arg, -1}, state, last) 297 | end 298 | 299 | defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do 300 | {var |> Atom.to_string() |> string(), state} 301 | end 302 | 303 | defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do 304 | cond do 305 | entries == [] -> 306 | {"<<>>", state} 307 | 308 | not interpolated?(entries) -> 309 | bitstring_to_algebra(meta, entries, state) 310 | 311 | meta[:delimiter] == ~s["""] -> 312 | {doc, state} = 313 | entries 314 | |> prepend_heredoc_line() 315 | |> interpolation_to_algebra(:heredoc, state, @double_heredoc, @double_heredoc) 316 | 317 | {force_unfit(doc), state} 318 | 319 | true -> 320 | interpolation_to_algebra(entries, @double_quote, state, @double_quote, @double_quote) 321 | end 322 | end 323 | 324 | defp quoted_to_algebra( 325 | {{:., _, [List, :to_charlist]}, meta, [entries]} = quoted, 326 | context, 327 | state 328 | ) do 329 | cond do 330 | not list_interpolated?(entries) -> 331 | remote_to_algebra(quoted, context, state) 332 | 333 | meta[:delimiter] == ~s['''] -> 334 | {doc, state} = 335 | entries 336 | |> prepend_heredoc_line() 337 | |> list_interpolation_to_algebra(:heredoc, state, @single_heredoc, @single_heredoc) 338 | 339 | {force_unfit(doc), state} 340 | 341 | true -> 342 | list_interpolation_to_algebra(entries, @single_quote, state, @single_quote, @single_quote) 343 | end 344 | end 345 | 346 | defp quoted_to_algebra( 347 | {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted, 348 | context, 349 | state 350 | ) do 351 | if interpolated?(entries) do 352 | interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote) 353 | else 354 | remote_to_algebra(quoted, context, state) 355 | end 356 | end 357 | 358 | # foo[bar] 359 | defp quoted_to_algebra({{:., _, [Access, :get]}, meta, [target | args]}, _context, state) do 360 | {target_doc, state} = remote_target_to_algebra(target, state) 361 | {call_doc, state} = list_to_algebra(meta, args, state) 362 | {concat(target_doc, call_doc), state} 363 | end 364 | 365 | # %Foo{} 366 | # %name{foo: 1} 367 | # %name{bar | foo: 1} 368 | defp quoted_to_algebra({:%, _, [name, {:%{}, meta, args}]}, _context, state) do 369 | {name_doc, state} = quoted_to_algebra(name, :parens_arg, state) 370 | map_to_algebra(meta, name_doc, args, state) 371 | end 372 | 373 | # %{foo: 1} 374 | # %{foo => bar} 375 | # %{name | foo => bar} 376 | defp quoted_to_algebra({:%{}, meta, args}, _context, state) do 377 | map_to_algebra(meta, @empty, args, state) 378 | end 379 | 380 | # {} 381 | # {1, 2} 382 | defp quoted_to_algebra({:{}, meta, args}, _context, state) do 383 | tuple_to_algebra(meta, args, :flex_break, state) 384 | end 385 | 386 | defp quoted_to_algebra({:__block__, meta, [{left, right}]}, _context, state) do 387 | tuple_to_algebra(meta, [left, right], :flex_break, state) 388 | end 389 | 390 | defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do 391 | case meta[:delimiter] do 392 | ~s['''] -> 393 | string = list |> List.to_string() |> escape_heredoc() 394 | {@single_heredoc |> concat(string) |> concat(@single_heredoc) |> force_unfit(), state} 395 | 396 | ~s['] -> 397 | string = list |> List.to_string() |> escape_string(@single_quote) 398 | {@single_quote |> concat(string) |> concat(@single_quote), state} 399 | 400 | _other -> 401 | list_to_algebra(meta, list, state) 402 | end 403 | end 404 | 405 | defp quoted_to_algebra({:__block__, meta, [string]}, _context, state) when is_binary(string) do 406 | if meta[:delimiter] == ~s["""] do 407 | string = escape_heredoc(string) 408 | {@double_heredoc |> concat(string) |> concat(@double_heredoc) |> force_unfit(), state} 409 | else 410 | string = escape_string(string, @double_quote) 411 | {@double_quote |> concat(string) |> concat(@double_quote), state} 412 | end 413 | end 414 | 415 | defp quoted_to_algebra({:__block__, _, [atom]}, _context, state) when is_atom(atom) do 416 | {atom_to_algebra(atom), state} 417 | end 418 | 419 | defp quoted_to_algebra({:__block__, meta, [integer]}, _context, state) 420 | when is_integer(integer) do 421 | {integer_to_algebra(Keyword.fetch!(meta, :token)), state} 422 | end 423 | 424 | defp quoted_to_algebra({:__block__, meta, [float]}, _context, state) when is_float(float) do 425 | {float_to_algebra(Keyword.fetch!(meta, :token)), state} 426 | end 427 | 428 | defp quoted_to_algebra( 429 | {:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]}, 430 | context, 431 | state 432 | ) do 433 | {doc, state} = local_to_algebra(:unquote_splicing, meta, args, context, state) 434 | {wrap_in_parens(doc), state} 435 | end 436 | 437 | defp quoted_to_algebra({:__block__, _meta, [arg]}, context, state) do 438 | quoted_to_algebra(arg, context, state) 439 | end 440 | 441 | defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do 442 | {"nil", state} 443 | end 444 | 445 | defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do 446 | {block, state} = block_to_algebra(block, state, line(meta), closing_line(meta)) 447 | {surround("(", block, ")"), state} 448 | end 449 | 450 | defp quoted_to_algebra({:__aliases__, _meta, [head | tail]}, context, state) do 451 | {doc, state} = 452 | if is_atom(head) do 453 | {Atom.to_string(head), state} 454 | else 455 | quoted_to_algebra_with_parens_if_operator(head, context, state) 456 | end 457 | 458 | {Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1))), state} 459 | end 460 | 461 | # &1 462 | # &local(&1) 463 | # &local/1 464 | # &Mod.remote/1 465 | # & &1 466 | # & &1 + &2 467 | defp quoted_to_algebra({:&, _, [arg]}, context, state) do 468 | capture_to_algebra(arg, context, state) 469 | end 470 | 471 | defp quoted_to_algebra({:@, meta, [arg]}, context, state) do 472 | module_attribute_to_algebra(meta, arg, context, state) 473 | end 474 | 475 | # not(left in right) 476 | # left not in right 477 | defp quoted_to_algebra({:not, meta, [{:in, _, [left, right]} = arg]}, context, state) do 478 | %{rename_deprecated_at: since} = state 479 | 480 | # TODO: Remove metadata and always rewrite to left not in right in Elixir v2.0. 481 | if meta[:operator] == :"not in" || (since && Version.match?(since, "~> 1.5")) do 482 | binary_op_to_algebra(:in, "not in", meta, left, right, context, state) 483 | else 484 | unary_op_to_algebra(:not, meta, arg, context, state) 485 | end 486 | end 487 | 488 | defp quoted_to_algebra({:fn, meta, [_ | _] = clauses}, _context, state) do 489 | anon_fun_to_algebra(clauses, line(meta), closing_line(meta), state, eol?(meta)) 490 | end 491 | 492 | defp quoted_to_algebra({fun, meta, args}, context, state) when is_atom(fun) and is_list(args) do 493 | with :error <- maybe_sigil_to_algebra(fun, meta, args, state), 494 | :error <- maybe_unary_op_to_algebra(fun, meta, args, context, state), 495 | :error <- maybe_binary_op_to_algebra(fun, meta, args, context, state), 496 | do: local_to_algebra(fun, meta, args, context, state) 497 | end 498 | 499 | defp quoted_to_algebra({_, _, args} = quoted, context, state) when is_list(args) do 500 | remote_to_algebra(quoted, context, state) 501 | end 502 | 503 | # (left -> right) 504 | defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do 505 | type_fun_to_algebra(clauses, @max_line, @min_line, state) 506 | end 507 | 508 | # [keyword: :list] (inner part) 509 | # %{:foo => :bar} (inner part) 510 | defp quoted_to_algebra(list, context, state) when is_list(list) do 511 | many_args_to_algebra(list, state, "ed_to_algebra(&1, context, &2)) 512 | end 513 | 514 | # keyword: :list 515 | # key => value 516 | defp quoted_to_algebra({left_arg, right_arg}, context, state) do 517 | {left, op, right, state} = 518 | if keyword_key?(left_arg) do 519 | {left, state} = 520 | case left_arg do 521 | {:__block__, _, [atom]} when is_atom(atom) -> 522 | key = 523 | case classify(atom) do 524 | type when type in [:callable_local, :callable_operator, :not_callable] -> 525 | IO.iodata_to_binary([Atom.to_string(atom), ?:]) 526 | 527 | _ -> 528 | IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:]) 529 | end 530 | 531 | {string(key), state} 532 | 533 | {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} -> 534 | interpolation_to_algebra(entries, @double_quote, state, "\"", "\":") 535 | end 536 | 537 | {right, state} = quoted_to_algebra(right_arg, context, state) 538 | {left, "", right, state} 539 | else 540 | {left, state} = quoted_to_algebra(left_arg, context, state) 541 | {right, state} = quoted_to_algebra(right_arg, context, state) 542 | left = wrap_in_parens_if_binary_operator(left, left_arg) 543 | {left, " =>", right, state} 544 | end 545 | 546 | doc = 547 | with_next_break_fits(next_break_fits?(right_arg, state), right, fn right -> 548 | concat(group(left), group(nest(glue(op, group(right)), 2, :break))) 549 | end) 550 | 551 | {doc, state} 552 | end 553 | 554 | ## Blocks 555 | 556 | def block_to_algebra(type_fun, state, min_line \\ @min_line, max_line \\ @max_line) 557 | 558 | def block_to_algebra([{:->, _, _} | _] = type_fun, state, min_line, max_line) do 559 | type_fun_to_algebra(type_fun, min_line, max_line, state) 560 | end 561 | 562 | def block_to_algebra({:__block__, _, []}, state, min_line, max_line) do 563 | block_args_to_algebra([], min_line, max_line, state) 564 | end 565 | 566 | def block_to_algebra({:__block__, _, [_, _ | _] = args}, state, min_line, max_line) do 567 | block_args_to_algebra(args, min_line, max_line, state) 568 | end 569 | 570 | def block_to_algebra(block, state, min_line, max_line) do 571 | block_args_to_algebra([block], min_line, max_line, state) 572 | end 573 | 574 | defp block_args_to_algebra(args, min_line, max_line, state) do 575 | quoted_to_algebra = fn {kind, meta, _} = arg, _args, state -> 576 | newlines = meta[:end_of_expression][:newlines] || 1 577 | {doc, state} = quoted_to_algebra(arg, :block, state) 578 | {{doc, block_next_line(kind), newlines}, state} 579 | end 580 | 581 | {args_docs, _comments?, state} = 582 | quoted_to_algebra_with_comments(args, [], min_line, max_line, state, quoted_to_algebra) 583 | 584 | case args_docs do 585 | [] -> {@empty, state} 586 | [line] -> {line, state} 587 | lines -> {lines |> Enum.reduce(&line(&2, &1)) |> force_unfit(), state} 588 | end 589 | end 590 | 591 | defp block_next_line(:@), do: @empty 592 | defp block_next_line(_), do: break("") 593 | 594 | ## Operators 595 | 596 | defp maybe_unary_op_to_algebra(fun, meta, args, context, state) do 597 | with [arg] <- args, 598 | {_, _} <- Code.Identifier.unary_op(fun) do 599 | unary_op_to_algebra(fun, meta, arg, context, state) 600 | else 601 | _ -> :error 602 | end 603 | end 604 | 605 | defp unary_op_to_algebra(op, _meta, arg, context, state) do 606 | {doc, state} = quoted_to_algebra(arg, force_many_args_or_operand(context, :operand), state) 607 | 608 | # not and ! are nestable, all others are not. 609 | doc = 610 | case arg do 611 | {^op, _, [_]} when op in [:!, :not] -> doc 612 | _ -> wrap_in_parens_if_operator(doc, arg) 613 | end 614 | 615 | # not requires a space unless the doc was wrapped in parens. 616 | op_string = 617 | if op == :not do 618 | "not " 619 | else 620 | Atom.to_string(op) 621 | end 622 | 623 | {concat(op_string, doc), state} 624 | end 625 | 626 | defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do 627 | with [left, right] <- args, 628 | {_, _} <- Code.Identifier.binary_op(fun) do 629 | binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state) 630 | else 631 | _ -> :error 632 | end 633 | end 634 | 635 | # There are five kinds of operators. 636 | # 637 | # 1. no space binary operators, for example, 1..2 638 | # 2. no newline binary operators, for example, left in right 639 | # 3. strict newlines before a left precedent operator, for example, foo |> bar |> baz 640 | # 4. strict newlines before a right precedent operator, for example, foo when bar when baz 641 | # 5. flex newlines after the operator, for example, foo ++ bar ++ baz 642 | # 643 | # Cases 1, 2 and 5 are handled fairly easily by relying on the 644 | # operator precedence and making sure nesting is applied only once. 645 | # 646 | # Cases 3 and 4 are the complex ones, as it requires passing the 647 | # strict or flex mode around. 648 | defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state) do 649 | %{operand_nesting: nesting} = state 650 | binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) 651 | end 652 | 653 | defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting) 654 | when op in @right_new_line_before_binary_operators do 655 | op_info = Code.Identifier.binary_op(op) 656 | op_string = op_string <> " " 657 | left_context = left_op_context(context) 658 | right_context = right_op_context(context) 659 | 660 | min_line = 661 | case left_arg do 662 | {_, left_meta, _} -> line(left_meta) 663 | _ -> line(meta) 664 | end 665 | 666 | {operands, max_line} = 667 | unwrap_right(right_arg, op, meta, right_context, [{{:root, left_context}, left_arg}]) 668 | 669 | operand_to_algebra = fn 670 | {{:root, context}, arg}, _args, state -> 671 | {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2) 672 | {{doc, @empty, 1}, state} 673 | 674 | {{kind, context}, arg}, _args, state -> 675 | {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, kind, 0) 676 | doc = doc |> nest_by_length(op_string) |> force_keyword(arg) 677 | {{concat(op_string, doc), @empty, 1}, state} 678 | end 679 | 680 | {doc, state} = 681 | operand_to_algebra_with_comments( 682 | operands, 683 | meta, 684 | min_line, 685 | max_line, 686 | state, 687 | operand_to_algebra 688 | ) 689 | 690 | if keyword?(right_arg) and context in [:parens_arg, :no_parens_arg] do 691 | {wrap_in_parens(doc), state} 692 | else 693 | {doc, state} 694 | end 695 | end 696 | 697 | defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting) 698 | when op in @pipeline_operators do 699 | op_info = Code.Identifier.binary_op(op) 700 | left_context = left_op_context(context) 701 | right_context = right_op_context(context) 702 | max_line = line(meta) 703 | 704 | {pipes, min_line} = 705 | unwrap_pipes(left_arg, meta, left_context, [{{op, right_context}, right_arg}]) 706 | 707 | operand_to_algebra = fn 708 | {{:root, context}, arg}, _args, state -> 709 | {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2) 710 | {{doc, @empty, 1}, state} 711 | 712 | {{op, context}, arg}, _args, state -> 713 | op_info = Code.Identifier.binary_op(op) 714 | op_string = Atom.to_string(op) <> " " 715 | {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0) 716 | {{concat(op_string, doc), @empty, 1}, state} 717 | end 718 | 719 | operand_to_algebra_with_comments(pipes, meta, min_line, max_line, state, operand_to_algebra) 720 | end 721 | 722 | defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do 723 | op_info = Code.Identifier.binary_op(op) 724 | left_context = left_op_context(context) 725 | right_context = right_op_context(context) 726 | 727 | {left, state} = 728 | binary_operand_to_algebra(left_arg, left_context, state, op, op_info, :left, 2) 729 | 730 | {right, state} = 731 | binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0) 732 | 733 | doc = 734 | cond do 735 | op in @no_space_binary_operators -> 736 | concat(concat(group(left), op_string), group(right)) 737 | 738 | op in @no_newline_binary_operators -> 739 | op_string = " " <> op_string <> " " 740 | concat(concat(group(left), op_string), group(right)) 741 | 742 | true -> 743 | eol? = eol?(meta) 744 | 745 | next_break_fits? = 746 | op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol? 747 | 748 | with_next_break_fits(next_break_fits?, right, fn right -> 749 | op_string = " " <> op_string 750 | right = nest(glue(op_string, group(right)), nesting, :break) 751 | right = if eol?, do: force_unfit(right), else: right 752 | concat(group(left), group(right)) 753 | end) 754 | end 755 | 756 | {doc, state} 757 | end 758 | 759 | # TODO: We can remove this workaround once we remove 760 | # ?rearrange_uop from the parser on v2.0. 761 | # (! left) in right 762 | # (not left) in right 763 | defp binary_operand_to_algebra( 764 | {:__block__, _, [{op, meta, [arg]}]}, 765 | context, 766 | state, 767 | :in, 768 | _parent_info, 769 | :left, 770 | _nesting 771 | ) 772 | when op in [:not, :!] do 773 | {doc, state} = unary_op_to_algebra(op, meta, arg, context, state) 774 | {wrap_in_parens(doc), state} 775 | end 776 | 777 | defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do 778 | {parent_assoc, parent_prec} = parent_info 779 | 780 | with {op, meta, [left, right]} <- operand, 781 | op_info = Code.Identifier.binary_op(op), 782 | {_assoc, prec} <- op_info do 783 | op_string = Atom.to_string(op) 784 | 785 | cond do 786 | # If the operator has the same precedence as the parent and is on 787 | # the correct side, we respect the nesting rule to avoid multiple 788 | # nestings. This only applies for left associativity or same operator. 789 | parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) -> 790 | binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting) 791 | 792 | # If the parent requires parens or the precedence is inverted or 793 | # it is in the wrong side, then we *need* parenthesis. 794 | (parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or 795 | (op in @required_parens_logical_binary_operands and 796 | parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or 797 | (parent_prec == prec and parent_assoc != side) -> 798 | {operand, state} = 799 | binary_op_to_algebra(op, op_string, meta, left, right, context, state, 2) 800 | 801 | {wrap_in_parens(operand), state} 802 | 803 | # Otherwise, we rely on precedence but also nest. 804 | true -> 805 | binary_op_to_algebra(op, op_string, meta, left, right, context, state, 2) 806 | end 807 | else 808 | {:&, _, [arg]} 809 | when not is_integer(arg) and side == :left 810 | when not is_integer(arg) and parent_assoc == :left and parent_prec > @ampersand_prec -> 811 | {doc, state} = quoted_to_algebra(operand, context, state) 812 | {wrap_in_parens(doc), state} 813 | 814 | _ -> 815 | quoted_to_algebra(operand, context, state) 816 | end 817 | end 818 | 819 | defp unwrap_pipes({op, meta, [left, right]}, _meta, context, acc) 820 | when op in @pipeline_operators do 821 | left_context = left_op_context(context) 822 | right_context = right_op_context(context) 823 | unwrap_pipes(left, meta, left_context, [{{op, right_context}, right} | acc]) 824 | end 825 | 826 | defp unwrap_pipes(left, meta, context, acc) do 827 | min_line = 828 | case left do 829 | {_, meta, _} -> line(meta) 830 | _ -> line(meta) 831 | end 832 | 833 | {[{{:root, context}, left} | acc], min_line} 834 | end 835 | 836 | defp unwrap_right({op, meta, [left, right]}, op, _meta, context, acc) do 837 | left_context = left_op_context(context) 838 | right_context = right_op_context(context) 839 | unwrap_right(right, op, meta, right_context, [{{:left, left_context}, left} | acc]) 840 | end 841 | 842 | defp unwrap_right(right, _op, meta, context, acc) do 843 | acc = [{{:right, context}, right} | acc] 844 | {Enum.reverse(acc), line(meta)} 845 | end 846 | 847 | defp operand_to_algebra_with_comments(operands, meta, min_line, max_line, state, fun) do 848 | {docs, comments?, state} = 849 | quoted_to_algebra_with_comments(operands, [], min_line, max_line, state, fun) 850 | 851 | if comments? or eol?(meta) do 852 | {docs |> Enum.reduce(&line(&2, &1)) |> force_unfit(), state} 853 | else 854 | {docs |> Enum.reduce(&glue(&2, &1)), state} 855 | end 856 | end 857 | 858 | ## Module attributes 859 | 860 | # @Foo 861 | # @Foo.Bar 862 | defp module_attribute_to_algebra(_meta, {:__aliases__, _, [_, _ | _]} = quoted, _context, state) do 863 | {doc, state} = quoted_to_algebra(quoted, :parens_arg, state) 864 | {concat(concat("@(", doc), ")"), state} 865 | end 866 | 867 | # @foo bar 868 | # @foo(bar) 869 | defp module_attribute_to_algebra(meta, {name, call_meta, [_] = args} = expr, context, state) 870 | when is_atom(name) and name not in [:__block__, :__aliases__] do 871 | if classify(name) == :callable_local do 872 | {{call_doc, state}, wrap_in_parens?} = 873 | call_args_to_algebra(args, call_meta, context, :skip_unless_many_args, false, state) 874 | 875 | doc = 876 | "@#{name}" 877 | |> string() 878 | |> concat(call_doc) 879 | 880 | doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc 881 | {doc, state} 882 | else 883 | unary_op_to_algebra(:@, meta, expr, context, state) 884 | end 885 | end 886 | 887 | # @foo 888 | # @(foo.bar()) 889 | defp module_attribute_to_algebra(meta, quoted, context, state) do 890 | unary_op_to_algebra(:@, meta, quoted, context, state) 891 | end 892 | 893 | ## Capture operator 894 | 895 | defp capture_to_algebra(integer, _context, state) when is_integer(integer) do 896 | {"&" <> Integer.to_string(integer), state} 897 | end 898 | 899 | defp capture_to_algebra(arg, context, state) do 900 | {doc, state} = capture_target_to_algebra(arg, context, state) 901 | 902 | if doc |> format_to_string() |> String.starts_with?("&") do 903 | {concat("& ", doc), state} 904 | else 905 | {concat("&", doc), state} 906 | end 907 | end 908 | 909 | defp capture_target_to_algebra( 910 | {:/, _, [{{:., _, [target, fun]}, _, []}, {:__block__, _, [arity]}]}, 911 | _context, 912 | state 913 | ) 914 | when is_atom(fun) and is_integer(arity) do 915 | {target_doc, state} = remote_target_to_algebra(target, state) 916 | fun = remote_fun_to_algebra(target, fun, arity, state) 917 | {target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state} 918 | end 919 | 920 | defp capture_target_to_algebra( 921 | {:/, _, [{name, _, var_context}, {:__block__, _, [arity]}]}, 922 | _context, 923 | state 924 | ) 925 | when is_atom(name) and is_atom(var_context) and is_integer(arity) do 926 | {string("#{name}/#{arity}"), state} 927 | end 928 | 929 | defp capture_target_to_algebra(arg, context, state) do 930 | {doc, state} = quoted_to_algebra(arg, context, state) 931 | {wrap_in_parens_if_operator(doc, arg), state} 932 | end 933 | 934 | ## Calls (local, remote and anonymous) 935 | 936 | # expression.{arguments} 937 | defp remote_to_algebra({{:., _, [target, :{}]}, meta, args}, _context, state) do 938 | {target_doc, state} = remote_target_to_algebra(target, state) 939 | {call_doc, state} = tuple_to_algebra(meta, args, :break, state) 940 | {concat(concat(target_doc, "."), call_doc), state} 941 | end 942 | 943 | # expression.(arguments) 944 | defp remote_to_algebra({{:., _, [target]}, meta, args}, context, state) do 945 | {target_doc, state} = remote_target_to_algebra(target, state) 946 | 947 | {{call_doc, state}, wrap_in_parens?} = 948 | call_args_to_algebra(args, meta, context, :skip_if_do_end, true, state) 949 | 950 | doc = concat(concat(target_doc, "."), call_doc) 951 | doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc 952 | {doc, state} 953 | end 954 | 955 | # Mod.function() 956 | # var.function 957 | # expression.function(arguments) 958 | defp remote_to_algebra({{:., _, [target, fun]}, meta, args}, context, state) 959 | when is_atom(fun) do 960 | {target_doc, state} = remote_target_to_algebra(target, state) 961 | fun = remote_fun_to_algebra(target, fun, length(args), state) 962 | remote_doc = target_doc |> concat(".") |> concat(string(fun)) 963 | 964 | if args == [] and not remote_target_is_a_module?(target) and not meta?(meta, :closing) do 965 | {remote_doc, state} 966 | else 967 | {{call_doc, state}, wrap_in_parens?} = 968 | call_args_to_algebra(args, meta, context, :skip_if_do_end, true, state) 969 | 970 | doc = concat(remote_doc, call_doc) 971 | doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc 972 | {doc, state} 973 | end 974 | end 975 | 976 | # call(call)(arguments) 977 | defp remote_to_algebra({target, meta, args}, context, state) do 978 | {target_doc, state} = quoted_to_algebra(target, :no_parens_arg, state) 979 | 980 | {{call_doc, state}, wrap_in_parens?} = 981 | call_args_to_algebra(args, meta, context, :required, true, state) 982 | 983 | doc = concat(target_doc, call_doc) 984 | doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc 985 | {doc, state} 986 | end 987 | 988 | defp remote_target_is_a_module?(target) do 989 | case target do 990 | {:__MODULE__, _, context} when is_atom(context) -> true 991 | {:__block__, _, [atom]} when is_atom(atom) -> true 992 | {:__aliases__, _, _} -> true 993 | _ -> false 994 | end 995 | end 996 | 997 | defp remote_fun_to_algebra(target, fun, arity, state) do 998 | %{rename_deprecated_at: since} = state 999 | 1000 | atom_target = 1001 | case since && target do 1002 | {:__aliases__, _, [alias | _] = aliases} when is_atom(alias) -> 1003 | Module.concat(aliases) 1004 | 1005 | {:__block__, _, [atom]} when is_atom(atom) -> 1006 | atom 1007 | 1008 | _ -> 1009 | nil 1010 | end 1011 | 1012 | with {fun, requirement} <- deprecated(atom_target, fun, arity), 1013 | true <- Version.match?(since, requirement) do 1014 | fun 1015 | else 1016 | _ -> inspect_as_function(fun) 1017 | end 1018 | end 1019 | 1020 | # We can only rename functions in the same module because 1021 | # introducing a new module may be wrong due to aliases. 1022 | defp deprecated(Enum, :partition, 2), do: {"split_with", "~> 1.4"} 1023 | defp deprecated(Code, :unload_files, 2), do: {"unrequire_files", "~> 1.7"} 1024 | defp deprecated(Code, :loaded_files, 2), do: {"required_files", "~> 1.7"} 1025 | defp deprecated(Kernel.ParallelCompiler, :files, 2), do: {"compile", "~> 1.6"} 1026 | defp deprecated(Kernel.ParallelCompiler, :files_to_path, 2), do: {"compile_to_path", "~> 1.6"} 1027 | defp deprecated(_, _, _), do: :error 1028 | 1029 | defp remote_target_to_algebra({:fn, _, [_ | _]} = quoted, state) do 1030 | # This change is not semantically required but for beautification. 1031 | {doc, state} = quoted_to_algebra(quoted, :no_parens_arg, state) 1032 | {wrap_in_parens(doc), state} 1033 | end 1034 | 1035 | defp remote_target_to_algebra(quoted, state) do 1036 | quoted_to_algebra_with_parens_if_operator(quoted, :no_parens_arg, state) 1037 | end 1038 | 1039 | # function(arguments) 1040 | defp local_to_algebra(fun, meta, args, context, state) when is_atom(fun) do 1041 | skip_parens = 1042 | cond do 1043 | meta?(meta, :closing) -> :skip_if_only_do_end 1044 | local_without_parens?(fun, args, state) -> :skip_unless_many_args 1045 | true -> :skip_if_do_end 1046 | end 1047 | 1048 | {{call_doc, state}, wrap_in_parens?} = 1049 | call_args_to_algebra(args, meta, context, skip_parens, true, state) 1050 | 1051 | doc = 1052 | fun 1053 | |> Atom.to_string() 1054 | |> string() 1055 | |> concat(call_doc) 1056 | 1057 | doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc 1058 | {doc, state} 1059 | end 1060 | 1061 | # parens may be one of: 1062 | # 1063 | # * :skip_unless_many_args - skips parens unless we are the argument context 1064 | # * :skip_if_only_do_end - skip parens if we are do-end and the only arg 1065 | # * :skip_if_do_end - skip parens if we are do-end 1066 | # * :required - never skip parens 1067 | # 1068 | defp call_args_to_algebra([], meta, _context, _parens, _list_to_keyword?, state) do 1069 | {args_doc, _join, state} = 1070 | args_to_algebra_with_comments([], meta, false, :none, :break, state, &{&1, &2}) 1071 | 1072 | {{surround("(", args_doc, ")"), state}, false} 1073 | end 1074 | 1075 | defp call_args_to_algebra(args, meta, context, parens, list_to_keyword?, state) do 1076 | {rest, last} = split_last(args) 1077 | 1078 | if blocks = do_end_blocks(meta, last, state) do 1079 | {call_doc, state} = 1080 | case rest do 1081 | [] when parens == :required -> 1082 | {"() do", state} 1083 | 1084 | [] -> 1085 | {" do", state} 1086 | 1087 | _ -> 1088 | no_parens? = parens not in [:required, :skip_if_only_do_end] 1089 | call_args_to_algebra_no_blocks(meta, rest, no_parens?, list_to_keyword?, " do", state) 1090 | end 1091 | 1092 | {blocks_doc, state} = do_end_blocks_to_algebra(blocks, state) 1093 | call_doc = call_doc |> concat(blocks_doc) |> line("end") |> force_unfit() 1094 | {{call_doc, state}, context in [:no_parens_arg, :no_parens_one_arg]} 1095 | else 1096 | no_parens? = 1097 | parens == :skip_unless_many_args and 1098 | context in [:block, :operand, :no_parens_one_arg, :parens_one_arg] 1099 | 1100 | res = 1101 | call_args_to_algebra_no_blocks(meta, args, no_parens?, list_to_keyword?, @empty, state) 1102 | 1103 | {res, false} 1104 | end 1105 | end 1106 | 1107 | defp call_args_to_algebra_no_blocks(meta, args, skip_parens?, list_to_keyword?, extra, state) do 1108 | {left, right} = split_last(args) 1109 | {keyword?, right} = last_arg_to_keyword(right, list_to_keyword?, skip_parens?, state.comments) 1110 | 1111 | context = 1112 | if left == [] and not keyword? do 1113 | if skip_parens?, do: :no_parens_one_arg, else: :parens_one_arg 1114 | else 1115 | if skip_parens?, do: :no_parens_arg, else: :parens_arg 1116 | end 1117 | 1118 | args = if keyword?, do: left ++ right, else: left ++ [right] 1119 | many_eol? = match?([_, _ | _], args) and eol?(meta) 1120 | no_generators? = no_generators?(args) 1121 | to_algebra_fun = "ed_to_algebra(&1, context, &2) 1122 | 1123 | {args_doc, next_break_fits?, state} = 1124 | if left != [] and keyword? and no_generators? do 1125 | join = if force_args?(left) or many_eol?, do: :line, else: :break 1126 | 1127 | {left_doc, _join, state} = 1128 | args_to_algebra_with_comments( 1129 | left, 1130 | Keyword.delete(meta, :closing), 1131 | skip_parens?, 1132 | :force_comma, 1133 | join, 1134 | state, 1135 | to_algebra_fun 1136 | ) 1137 | 1138 | join = if force_args?(right) or force_args?(args) or many_eol?, do: :line, else: :break 1139 | 1140 | {right_doc, _join, state} = 1141 | args_to_algebra_with_comments(right, meta, false, :none, join, state, to_algebra_fun) 1142 | 1143 | right_doc = apply(Inspect.Algebra, join, []) |> concat(right_doc) 1144 | 1145 | args_doc = 1146 | if skip_parens? do 1147 | left_doc 1148 | |> concat(next_break_fits(group(right_doc, :inherit), :enabled)) 1149 | |> nest(:cursor, :break) 1150 | else 1151 | right_doc = 1152 | right_doc 1153 | |> nest(2, :break) 1154 | |> concat(break("")) 1155 | |> group(:inherit) 1156 | |> next_break_fits(:enabled) 1157 | 1158 | concat(nest(left_doc, 2, :break), right_doc) 1159 | end 1160 | 1161 | {args_doc, true, state} 1162 | else 1163 | join = if force_args?(args) or many_eol?, do: :line, else: :break 1164 | next_break_fits? = join == :break and next_break_fits?(right, state) 1165 | last_arg_mode = if next_break_fits?, do: :next_break_fits, else: :none 1166 | 1167 | {args_doc, _join, state} = 1168 | args_to_algebra_with_comments( 1169 | args, 1170 | meta, 1171 | skip_parens?, 1172 | last_arg_mode, 1173 | join, 1174 | state, 1175 | to_algebra_fun 1176 | ) 1177 | 1178 | # If we have a single argument, then we won't have an option to break 1179 | # before the "extra" part, so we ungroup it and build it later. 1180 | args_doc = ungroup_if_group(args_doc) 1181 | 1182 | args_doc = 1183 | if skip_parens? do 1184 | nest(args_doc, :cursor, :break) 1185 | else 1186 | nest(args_doc, 2, :break) |> concat(break("")) 1187 | end 1188 | 1189 | {args_doc, next_break_fits?, state} 1190 | end 1191 | 1192 | doc = 1193 | cond do 1194 | left != [] and keyword? and skip_parens? and no_generators? -> 1195 | " " 1196 | |> concat(args_doc) 1197 | |> nest(2) 1198 | |> concat(extra) 1199 | |> group() 1200 | 1201 | skip_parens? -> 1202 | " " 1203 | |> concat(args_doc) 1204 | |> concat(extra) 1205 | |> group() 1206 | 1207 | true -> 1208 | "(" 1209 | |> concat(break("")) 1210 | |> nest(2, :break) 1211 | |> concat(args_doc) 1212 | |> concat(")") 1213 | |> concat(extra) 1214 | |> group() 1215 | end 1216 | 1217 | if next_break_fits? do 1218 | {next_break_fits(doc, :disabled), state} 1219 | else 1220 | {doc, state} 1221 | end 1222 | end 1223 | 1224 | defp local_without_parens?(fun, args, %{locals_without_parens: locals_without_parens}) do 1225 | length = length(args) 1226 | 1227 | length > 0 and 1228 | Enum.any?(locals_without_parens, fn {key, val} -> 1229 | key == fun and (val == :* or val == length) 1230 | end) 1231 | end 1232 | 1233 | defp no_generators?(args) do 1234 | not Enum.any?(args, &match?({:<-, _, [_, _]}, &1)) 1235 | end 1236 | 1237 | defp do_end_blocks(meta, [{{:__block__, _, [:do]}, _} | rest] = blocks, state) do 1238 | if meta?(meta, :do) or can_force_do_end_blocks?(rest, state) do 1239 | blocks 1240 | |> Enum.map(fn {{:__block__, meta, [key]}, value} -> {key, line(meta), value} end) 1241 | |> do_end_blocks_with_range(end_line(meta)) 1242 | end 1243 | end 1244 | 1245 | defp do_end_blocks(_, _, _), do: nil 1246 | 1247 | defp can_force_do_end_blocks?(rest, state) do 1248 | state.force_do_end_blocks and 1249 | Enum.all?(rest, fn {{:__block__, _, [key]}, _} -> key in @do_end_keywords end) 1250 | end 1251 | 1252 | defp do_end_blocks_with_range([{key1, line1, value1}, {_, line2, _} = h | t], end_line) do 1253 | [{key1, line1, line2, value1} | do_end_blocks_with_range([h | t], end_line)] 1254 | end 1255 | 1256 | defp do_end_blocks_with_range([{key, line, value}], end_line) do 1257 | [{key, line, end_line, value}] 1258 | end 1259 | 1260 | defp do_end_blocks_to_algebra([{:do, line, end_line, value} | blocks], state) do 1261 | {acc, state} = do_end_block_to_algebra(@empty, line, end_line, value, state) 1262 | 1263 | Enum.reduce(blocks, {acc, state}, fn {key, line, end_line, value}, {acc, state} -> 1264 | {doc, state} = do_end_block_to_algebra(Atom.to_string(key), line, end_line, value, state) 1265 | {line(acc, doc), state} 1266 | end) 1267 | end 1268 | 1269 | defp do_end_block_to_algebra(key_doc, line, end_line, value, state) do 1270 | case clauses_to_algebra(value, line, end_line, state) do 1271 | {@empty, state} -> {key_doc, state} 1272 | {value_doc, state} -> {key_doc |> line(value_doc) |> nest(2), state} 1273 | end 1274 | end 1275 | 1276 | ## Interpolation 1277 | 1278 | defp list_interpolated?(entries) do 1279 | Enum.all?(entries, fn 1280 | {{:., _, [Kernel, :to_string]}, _, [_]} -> true 1281 | entry when is_binary(entry) -> true 1282 | _ -> false 1283 | end) 1284 | end 1285 | 1286 | defp interpolated?(entries) do 1287 | Enum.all?(entries, fn 1288 | {:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true 1289 | entry when is_binary(entry) -> true 1290 | _ -> false 1291 | end) 1292 | end 1293 | 1294 | defp prepend_heredoc_line([entry | entries]) when is_binary(entry) do 1295 | ["\n" <> entry | entries] 1296 | end 1297 | 1298 | defp prepend_heredoc_line(entries) do 1299 | ["\n" | entries] 1300 | end 1301 | 1302 | defp list_interpolation_to_algebra([entry | entries], escape, state, acc, last) 1303 | when is_binary(entry) do 1304 | acc = concat(acc, escape_string(entry, escape)) 1305 | list_interpolation_to_algebra(entries, escape, state, acc, last) 1306 | end 1307 | 1308 | defp list_interpolation_to_algebra([entry | entries], escape, state, acc, last) do 1309 | {{:., _, [Kernel, :to_string]}, meta, [quoted]} = entry 1310 | {doc, state} = block_to_algebra(quoted, state, line(meta), closing_line(meta)) 1311 | doc = surround("\#{", doc, "}") 1312 | list_interpolation_to_algebra(entries, escape, state, concat(acc, doc), last) 1313 | end 1314 | 1315 | defp list_interpolation_to_algebra([], _escape, state, acc, last) do 1316 | {concat(acc, last), state} 1317 | end 1318 | 1319 | defp interpolation_to_algebra([entry | entries], escape, state, acc, last) 1320 | when is_binary(entry) do 1321 | acc = concat(acc, escape_string(entry, escape)) 1322 | interpolation_to_algebra(entries, escape, state, acc, last) 1323 | end 1324 | 1325 | defp interpolation_to_algebra([entry | entries], escape, state, acc, last) do 1326 | {:"::", _, [{{:., _, [Kernel, :to_string]}, meta, [quoted]}, {:binary, _, _}]} = entry 1327 | {doc, state} = block_to_algebra(quoted, state, line(meta), closing_line(meta)) 1328 | doc = surround("\#{", doc, "}") 1329 | interpolation_to_algebra(entries, escape, state, concat(acc, doc), last) 1330 | end 1331 | 1332 | defp interpolation_to_algebra([], _escape, state, acc, last) do 1333 | {concat(acc, last), state} 1334 | end 1335 | 1336 | ## Sigils 1337 | 1338 | defp maybe_sigil_to_algebra(fun, meta, args, state) do 1339 | with <<"sigil_", name>> <- Atom.to_string(fun), 1340 | [{:<<>>, _, entries}, modifiers] when is_list(modifiers) <- args, 1341 | opening_delimiter when not is_nil(opening_delimiter) <- meta[:delimiter] do 1342 | doc = <> 1343 | 1344 | if opening_delimiter in [@double_heredoc, @single_heredoc] do 1345 | closing_delimiter = concat(opening_delimiter, List.to_string(modifiers)) 1346 | 1347 | {doc, state} = 1348 | entries 1349 | |> prepend_heredoc_line() 1350 | |> interpolation_to_algebra(:heredoc, state, doc, closing_delimiter) 1351 | 1352 | {force_unfit(doc), state} 1353 | else 1354 | escape = closing_sigil_delimiter(opening_delimiter) 1355 | closing_delimiter = concat(escape, List.to_string(modifiers)) 1356 | interpolation_to_algebra(entries, escape, state, doc, closing_delimiter) 1357 | end 1358 | else 1359 | _ -> 1360 | :error 1361 | end 1362 | end 1363 | 1364 | defp closing_sigil_delimiter("("), do: ")" 1365 | defp closing_sigil_delimiter("["), do: "]" 1366 | defp closing_sigil_delimiter("{"), do: "}" 1367 | defp closing_sigil_delimiter("<"), do: ">" 1368 | defp closing_sigil_delimiter(other) when other in ["\"", "'", "|", "/"], do: other 1369 | 1370 | ## Bitstrings 1371 | 1372 | defp bitstring_to_algebra(meta, args, state) do 1373 | last = length(args) - 1 1374 | join = if eol?(meta), do: :line, else: :flex_break 1375 | to_algebra_fun = &bitstring_segment_to_algebra(&1, &2, last) 1376 | 1377 | {args_doc, join, state} = 1378 | args 1379 | |> Enum.with_index() 1380 | |> args_to_algebra_with_comments(meta, false, :none, join, state, to_algebra_fun) 1381 | 1382 | if join == :flex_break do 1383 | {"<<" |> concat(args_doc) |> nest(2) |> concat(">>") |> group(), state} 1384 | else 1385 | {surround("<<", args_doc, ">>"), state} 1386 | end 1387 | end 1388 | 1389 | defp bitstring_segment_to_algebra({{:<-, meta, [left, right]}, i}, state, last) do 1390 | left = {{:special, :bitstring_segment}, meta, [left, last]} 1391 | {doc, state} = quoted_to_algebra({:<-, meta, [left, right]}, :parens_arg, state) 1392 | {bitstring_wrap_parens(doc, i, last), state} 1393 | end 1394 | 1395 | defp bitstring_segment_to_algebra({{:"::", _, [segment, spec]}, i}, state, last) do 1396 | {doc, state} = quoted_to_algebra(segment, :parens_arg, state) 1397 | {spec, state} = bitstring_spec_to_algebra(spec, state) 1398 | 1399 | spec = wrap_in_parens_if_inspected_atom(spec) 1400 | spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec 1401 | 1402 | doc = 1403 | doc 1404 | |> bitstring_wrap_parens(i, -1) 1405 | |> concat("::") 1406 | |> concat(spec) 1407 | 1408 | {doc, state} 1409 | end 1410 | 1411 | defp bitstring_segment_to_algebra({segment, i}, state, last) do 1412 | {doc, state} = quoted_to_algebra(segment, :parens_arg, state) 1413 | {bitstring_wrap_parens(doc, i, last), state} 1414 | end 1415 | 1416 | defp bitstring_spec_to_algebra({op, _, [left, right]}, state) when op in [:-, :*] do 1417 | {left, state} = bitstring_spec_to_algebra(left, state) 1418 | {right, state} = quoted_to_algebra_with_parens_if_operator(right, :parens_arg, state) 1419 | {concat(concat(left, Atom.to_string(op)), right), state} 1420 | end 1421 | 1422 | defp bitstring_spec_to_algebra(spec, state) do 1423 | quoted_to_algebra_with_parens_if_operator(spec, :parens_arg, state) 1424 | end 1425 | 1426 | defp bitstring_wrap_parens(doc, i, last) when i == 0 or i == last do 1427 | string = format_to_string(doc) 1428 | 1429 | if (i == 0 and String.starts_with?(string, ["~", "<<"])) or 1430 | (i == last and String.ends_with?(string, [">>"])) do 1431 | wrap_in_parens(doc) 1432 | else 1433 | doc 1434 | end 1435 | end 1436 | 1437 | defp bitstring_wrap_parens(doc, _, _), do: doc 1438 | 1439 | ## Literals 1440 | 1441 | defp list_to_algebra(meta, args, state) do 1442 | join = if eol?(meta), do: :line, else: :break 1443 | fun = "ed_to_algebra(&1, :parens_arg, &2) 1444 | 1445 | {args_doc, _join, state} = 1446 | args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) 1447 | 1448 | {surround("[", args_doc, "]"), state} 1449 | end 1450 | 1451 | defp map_to_algebra(meta, name_doc, [{:|, _, [left, right]}], state) do 1452 | join = if eol?(meta), do: :line, else: :break 1453 | fun = "ed_to_algebra(&1, :parens_arg, &2) 1454 | {left_doc, state} = fun.(left, state) 1455 | 1456 | {right_doc, _join, state} = 1457 | args_to_algebra_with_comments(right, meta, false, :none, join, state, fun) 1458 | 1459 | args_doc = 1460 | left_doc 1461 | |> wrap_in_parens_if_binary_operator(left) 1462 | |> glue(concat("| ", nest(right_doc, 2))) 1463 | 1464 | name_doc = "%" |> concat(name_doc) |> concat("{") 1465 | {surround(name_doc, args_doc, "}"), state} 1466 | end 1467 | 1468 | defp map_to_algebra(meta, name_doc, args, state) do 1469 | join = if eol?(meta), do: :line, else: :break 1470 | fun = "ed_to_algebra(&1, :parens_arg, &2) 1471 | 1472 | {args_doc, _join, state} = 1473 | args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) 1474 | 1475 | name_doc = "%" |> concat(name_doc) |> concat("{") 1476 | {surround(name_doc, args_doc, "}"), state} 1477 | end 1478 | 1479 | defp tuple_to_algebra(meta, args, join, state) do 1480 | join = if eol?(meta), do: :line, else: join 1481 | fun = "ed_to_algebra(&1, :parens_arg, &2) 1482 | 1483 | {args_doc, join, state} = 1484 | args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) 1485 | 1486 | if join == :flex_break do 1487 | {"{" |> concat(args_doc) |> nest(1) |> concat("}") |> group(), state} 1488 | else 1489 | {surround("{", args_doc, "}"), state} 1490 | end 1491 | end 1492 | 1493 | defp atom_to_algebra(atom) when atom in [nil, true, false] do 1494 | Atom.to_string(atom) 1495 | end 1496 | 1497 | defp atom_to_algebra(atom) do 1498 | string = Atom.to_string(atom) 1499 | 1500 | iodata = 1501 | case classify(atom) do 1502 | type when type in [:callable_local, :callable_operator, :not_callable] -> 1503 | [?:, string] 1504 | 1505 | _ -> 1506 | [?:, ?", String.replace(string, "\"", "\\\""), ?"] 1507 | end 1508 | 1509 | iodata |> IO.iodata_to_binary() |> string() 1510 | end 1511 | 1512 | defp integer_to_algebra(text) do 1513 | case text do 1514 | <> -> 1515 | "0x" <> String.upcase(rest) 1516 | 1517 | <> = digits when base in [?b, ?o] -> 1518 | digits 1519 | 1520 | <> = char -> 1521 | char 1522 | 1523 | decimal -> 1524 | insert_underscores(decimal) 1525 | end 1526 | end 1527 | 1528 | defp float_to_algebra(text) do 1529 | [int_part, decimal_part] = :binary.split(text, ".") 1530 | decimal_part = String.downcase(decimal_part) 1531 | insert_underscores(int_part) <> "." <> decimal_part 1532 | end 1533 | 1534 | defp insert_underscores(digits) do 1535 | cond do 1536 | digits =~ "_" -> 1537 | digits 1538 | 1539 | byte_size(digits) >= 6 -> 1540 | digits 1541 | |> String.to_charlist() 1542 | |> Enum.reverse() 1543 | |> Enum.chunk_every(3) 1544 | |> Enum.intersperse('_') 1545 | |> List.flatten() 1546 | |> Enum.reverse() 1547 | |> List.to_string() 1548 | 1549 | true -> 1550 | digits 1551 | end 1552 | end 1553 | 1554 | defp escape_heredoc(string) do 1555 | heredoc_to_algebra(["" | String.split(string, "\n")]) 1556 | end 1557 | 1558 | defp escape_string(string, :heredoc) do 1559 | heredoc_to_algebra(String.split(string, "\n")) 1560 | end 1561 | 1562 | defp escape_string(string, escape) when is_binary(escape) do 1563 | string 1564 | |> String.replace(escape, "\\" <> escape) 1565 | |> String.split("\n") 1566 | |> Enum.reverse() 1567 | |> Enum.map(&string/1) 1568 | |> Enum.reduce(&concat(&1, concat(nest(line(), :reset), &2))) 1569 | end 1570 | 1571 | defp heredoc_to_algebra([string]) do 1572 | string(string) 1573 | end 1574 | 1575 | defp heredoc_to_algebra(["" | rest]) do 1576 | rest 1577 | |> heredoc_line() 1578 | |> concat(heredoc_to_algebra(rest)) 1579 | end 1580 | 1581 | defp heredoc_to_algebra([string | rest]) do 1582 | string 1583 | |> string() 1584 | |> concat(heredoc_line(rest)) 1585 | |> concat(heredoc_to_algebra(rest)) 1586 | end 1587 | 1588 | defp heredoc_line(["", _ | _]), do: nest(line(), :reset) 1589 | defp heredoc_line(_), do: line() 1590 | 1591 | defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do 1592 | min_line = line(meta) 1593 | max_line = closing_line(meta) 1594 | 1595 | arg_to_algebra = fn arg, args, state -> 1596 | {doc, state} = fun.(arg, state) 1597 | 1598 | doc = 1599 | case args do 1600 | [_ | _] -> concat_to_last_group(doc, ",") 1601 | [] when last_arg_mode == :force_comma -> concat_to_last_group(doc, ",") 1602 | [] when last_arg_mode == :next_break_fits -> next_break_fits(doc, :enabled) 1603 | [] when last_arg_mode == :none -> doc 1604 | end 1605 | 1606 | {{doc, @empty, 1}, state} 1607 | end 1608 | 1609 | # If skipping parens, we cannot extract the comments of the first 1610 | # argument as there is no place to move them to, so we handle it now. 1611 | {args, acc, state} = 1612 | case args do 1613 | [head | tail] when skip_parens? -> 1614 | {doc_triplet, state} = arg_to_algebra.(head, tail, state) 1615 | {tail, [doc_triplet], state} 1616 | 1617 | _ -> 1618 | {args, [], state} 1619 | end 1620 | 1621 | {args_docs, comments?, state} = 1622 | quoted_to_algebra_with_comments(args, acc, min_line, max_line, state, arg_to_algebra) 1623 | 1624 | cond do 1625 | args_docs == [] -> 1626 | {@empty, :empty, state} 1627 | 1628 | join == :line or comments? -> 1629 | {args_docs |> Enum.reduce(&line(&2, &1)) |> force_unfit(), :line, state} 1630 | 1631 | join == :break -> 1632 | {args_docs |> Enum.reduce(&glue(&2, &1)), :break, state} 1633 | 1634 | join == :flex_break -> 1635 | {args_docs |> Enum.reduce(&flex_glue(&2, &1)), :flex_break, state} 1636 | end 1637 | end 1638 | 1639 | ## Anonymous functions 1640 | 1641 | # fn -> block end 1642 | defp anon_fun_to_algebra( 1643 | [{:->, meta, [[], body]}] = clauses, 1644 | _min_line, 1645 | max_line, 1646 | state, 1647 | _multi_clauses_style 1648 | ) do 1649 | min_line = line(meta) 1650 | {body_doc, state} = block_to_algebra(body, state, min_line, max_line) 1651 | 1652 | doc = 1653 | "fn ->" 1654 | |> glue(body_doc) 1655 | |> nest(2) 1656 | |> glue("end") 1657 | |> maybe_force_clauses(clauses) 1658 | |> group() 1659 | 1660 | {doc, state} 1661 | end 1662 | 1663 | # fn x -> y end 1664 | # fn x -> 1665 | # y 1666 | # end 1667 | defp anon_fun_to_algebra( 1668 | [{:->, meta, [args, body]}] = clauses, 1669 | _min_line, 1670 | max_line, 1671 | state, 1672 | false = _multi_clauses_style 1673 | ) do 1674 | min_line = line(meta) 1675 | {args_doc, state} = clause_args_to_algebra(args, min_line, state) 1676 | {body_doc, state} = block_to_algebra(body, state, min_line, max_line) 1677 | 1678 | head = 1679 | args_doc 1680 | |> ungroup_if_group() 1681 | |> concat(" ->") 1682 | |> nest(:cursor) 1683 | |> group() 1684 | 1685 | doc = 1686 | "fn " 1687 | |> concat(head) 1688 | |> glue(body_doc) 1689 | |> nest(2) 1690 | |> glue("end") 1691 | |> maybe_force_clauses(clauses) 1692 | |> group() 1693 | 1694 | {doc, state} 1695 | end 1696 | 1697 | # fn 1698 | # args1 -> 1699 | # block1 1700 | # args2 -> 1701 | # block2 1702 | # end 1703 | defp anon_fun_to_algebra(clauses, min_line, max_line, state, _multi_clauses_style) do 1704 | {clauses_doc, state} = clauses_to_algebra(clauses, min_line, max_line, state) 1705 | {"fn" |> line(clauses_doc) |> nest(2) |> line("end") |> force_unfit(), state} 1706 | end 1707 | 1708 | ## Type functions 1709 | 1710 | # (() -> block) 1711 | defp type_fun_to_algebra([{:->, meta, [[], body]}] = clauses, _min_line, max_line, state) do 1712 | min_line = line(meta) 1713 | {body_doc, state} = block_to_algebra(body, state, min_line, max_line) 1714 | 1715 | doc = 1716 | "(() -> " 1717 | |> concat(nest(body_doc, :cursor)) 1718 | |> concat(")") 1719 | |> maybe_force_clauses(clauses) 1720 | |> group() 1721 | 1722 | {doc, state} 1723 | end 1724 | 1725 | # (x -> y) 1726 | # (x -> 1727 | # y) 1728 | defp type_fun_to_algebra([{:->, meta, [args, body]}] = clauses, _min_line, max_line, state) do 1729 | min_line = line(meta) 1730 | {args_doc, state} = clause_args_to_algebra(args, min_line, state) 1731 | {body_doc, state} = block_to_algebra(body, state, min_line, max_line) 1732 | 1733 | doc = 1734 | args_doc 1735 | |> ungroup_if_group() 1736 | |> concat(" ->") 1737 | |> group() 1738 | |> concat(break() |> concat(body_doc) |> nest(2)) 1739 | |> wrap_in_parens() 1740 | |> maybe_force_clauses(clauses) 1741 | |> group() 1742 | 1743 | {doc, state} 1744 | end 1745 | 1746 | # ( 1747 | # args1 -> 1748 | # block1 1749 | # args2 -> 1750 | # block2 1751 | # ) 1752 | defp type_fun_to_algebra(clauses, min_line, max_line, state) do 1753 | {clauses_doc, state} = clauses_to_algebra(clauses, min_line, max_line, state) 1754 | {"(" |> line(clauses_doc) |> nest(2) |> line(")") |> force_unfit(), state} 1755 | end 1756 | 1757 | ## Clauses 1758 | 1759 | defp maybe_force_clauses(doc, clauses) do 1760 | if Enum.any?(clauses, fn {:->, meta, _} -> eol?(meta) end) do 1761 | force_unfit(doc) 1762 | else 1763 | doc 1764 | end 1765 | end 1766 | 1767 | defp clauses_to_algebra([{:->, _, _} | _] = clauses, min_line, max_line, state) do 1768 | [clause | clauses] = add_max_line_to_last_clause(clauses, max_line) 1769 | {clause_doc, state} = clause_to_algebra(clause, min_line, state) 1770 | 1771 | {clauses_doc, state} = 1772 | Enum.reduce(clauses, {clause_doc, state}, fn clause, {doc_acc, state_acc} -> 1773 | {clause_doc, state_acc} = clause_to_algebra(clause, min_line, state_acc) 1774 | 1775 | doc_acc = 1776 | doc_acc 1777 | |> concat(maybe_empty_line()) 1778 | |> line(clause_doc) 1779 | 1780 | {doc_acc, state_acc} 1781 | end) 1782 | 1783 | {clauses_doc |> maybe_force_clauses([clause | clauses]) |> group(), state} 1784 | end 1785 | 1786 | defp clauses_to_algebra(other, min_line, max_line, state) do 1787 | case block_to_algebra(other, state, min_line, max_line) do 1788 | {@empty, state} -> {@empty, state} 1789 | {doc, state} -> {group(doc), state} 1790 | end 1791 | end 1792 | 1793 | defp clause_to_algebra({:->, meta, [[], body]}, _min_line, state) do 1794 | {body_doc, state} = block_to_algebra(body, state, line(meta), closing_line(meta)) 1795 | {"() ->" |> glue(body_doc) |> nest(2), state} 1796 | end 1797 | 1798 | defp clause_to_algebra({:->, meta, [args, body]}, min_line, state) do 1799 | %{operand_nesting: nesting} = state 1800 | 1801 | state = %{state | operand_nesting: nesting + 2} 1802 | {args_doc, state} = clause_args_to_algebra(args, min_line, state) 1803 | 1804 | state = %{state | operand_nesting: nesting} 1805 | {body_doc, state} = block_to_algebra(body, state, min_line, closing_line(meta)) 1806 | 1807 | doc = 1808 | args_doc 1809 | |> ungroup_if_group() 1810 | |> concat(" ->") 1811 | |> group() 1812 | |> concat(break() |> concat(body_doc) |> nest(2)) 1813 | 1814 | {doc, state} 1815 | end 1816 | 1817 | defp add_max_line_to_last_clause([{op, meta, args}], max_line) do 1818 | [{op, [closing: [line: max_line]] ++ meta, args}] 1819 | end 1820 | 1821 | defp add_max_line_to_last_clause([clause | clauses], max_line) do 1822 | [clause | add_max_line_to_last_clause(clauses, max_line)] 1823 | end 1824 | 1825 | defp clause_args_to_algebra(args, min_line, state) do 1826 | arg_to_algebra = fn arg, _args, state -> 1827 | {doc, state} = clause_args_to_algebra(arg, state) 1828 | {{doc, @empty, 1}, state} 1829 | end 1830 | 1831 | {args_docs, comments?, state} = 1832 | quoted_to_algebra_with_comments([args], [], min_line, @min_line, state, arg_to_algebra) 1833 | 1834 | if comments? do 1835 | {Enum.reduce(args_docs, &line(&2, &1)), state} 1836 | else 1837 | {Enum.reduce(args_docs, &glue(&2, &1)), state} 1838 | end 1839 | end 1840 | 1841 | # fn a, b, c when d -> e end 1842 | defp clause_args_to_algebra([{:when, meta, args}], state) do 1843 | {args, right} = split_last(args) 1844 | left = {{:special, :clause_args}, meta, [args]} 1845 | binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state) 1846 | end 1847 | 1848 | # fn () -> e end 1849 | defp clause_args_to_algebra([], state) do 1850 | {"()", state} 1851 | end 1852 | 1853 | # fn a, b, c -> e end 1854 | defp clause_args_to_algebra(args, state) do 1855 | many_args_to_algebra(args, state, "ed_to_algebra(&1, :no_parens_arg, &2)) 1856 | end 1857 | 1858 | ## Quoted helpers for comments 1859 | 1860 | defp quoted_to_algebra_with_comments(args, acc, min_line, max_line, state, fun) do 1861 | {pre_comments, state} = 1862 | get_and_update_in(state.comments, fn comments -> 1863 | Enum.split_while(comments, fn {line, _, _} -> line <= min_line end) 1864 | end) 1865 | 1866 | {docs, comments?, state} = 1867 | each_quoted_to_algebra_with_comments(args, acc, max_line, state, false, fun) 1868 | 1869 | {docs, comments?, update_in(state.comments, &(pre_comments ++ &1))} 1870 | end 1871 | 1872 | defp each_quoted_to_algebra_with_comments([], acc, max_line, state, comments?, _fun) do 1873 | {acc, comments, comments?} = extract_comments_before(max_line, acc, state.comments, comments?) 1874 | args_docs = merge_algebra_with_comments(Enum.reverse(acc), @empty) 1875 | {args_docs, comments?, %{state | comments: comments}} 1876 | end 1877 | 1878 | defp each_quoted_to_algebra_with_comments([arg | args], acc, max_line, state, comments?, fun) do 1879 | {doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line}) 1880 | 1881 | {acc, comments, comments?} = 1882 | extract_comments_before(doc_start, acc, state.comments, comments?) 1883 | 1884 | {doc_triplet, state} = fun.(arg, args, %{state | comments: comments}) 1885 | 1886 | {acc, comments, comments?} = 1887 | extract_comments_trailing(doc_start, doc_end, acc, state.comments, comments?) 1888 | 1889 | acc = [adjust_trailing_newlines(doc_triplet, doc_end, comments) | acc] 1890 | state = %{state | comments: comments} 1891 | each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun) 1892 | end 1893 | 1894 | defp extract_comments_before(max, acc, [{line, _, _} = comment | rest], _) when line < max do 1895 | {_, {previous, next}, doc} = comment 1896 | acc = [{doc, @empty, next} | add_previous_to_acc(acc, previous)] 1897 | extract_comments_before(max, acc, rest, true) 1898 | end 1899 | 1900 | defp extract_comments_before(_max, acc, rest, comments?) do 1901 | {acc, rest, comments?} 1902 | end 1903 | 1904 | defp add_previous_to_acc([{doc, next_line, newlines} | acc], previous) when newlines < previous, 1905 | do: [{doc, next_line, previous} | acc] 1906 | 1907 | defp add_previous_to_acc(acc, _previous), 1908 | do: acc 1909 | 1910 | defp extract_comments_trailing(min, max, acc, [{line, _, doc_comment} | rest], _) 1911 | when line >= min and line <= max do 1912 | acc = [{doc_comment, @empty, 1} | acc] 1913 | extract_comments_trailing(min, max, acc, rest, true) 1914 | end 1915 | 1916 | defp extract_comments_trailing(_min, _max, acc, rest, comments?) do 1917 | {acc, rest, comments?} 1918 | end 1919 | 1920 | # If the document is immediately followed by comment which is followed by newlines, 1921 | # its newlines wouldn't have considered the comment, so we need to adjust it. 1922 | defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [{line, _, _} | _]) 1923 | when newlines > 1 and line == doc_end + 1 do 1924 | {doc, next_line, 1} 1925 | end 1926 | 1927 | defp adjust_trailing_newlines(doc_triplet, _, _), do: doc_triplet 1928 | 1929 | defp traverse_line({expr, meta, args}, {min, max}) do 1930 | acc = 1931 | case Keyword.fetch(meta, :line) do 1932 | {:ok, line} -> {min(line, min), max(line, max)} 1933 | :error -> {min, max} 1934 | end 1935 | 1936 | traverse_line(args, traverse_line(expr, acc)) 1937 | end 1938 | 1939 | defp traverse_line({left, right}, acc) do 1940 | traverse_line(right, traverse_line(left, acc)) 1941 | end 1942 | 1943 | defp traverse_line(args, acc) when is_list(args) do 1944 | Enum.reduce(args, acc, &traverse_line/2) 1945 | end 1946 | 1947 | defp traverse_line(_, acc) do 1948 | acc 1949 | end 1950 | 1951 | # Below are the rules for line rendering in the formatter: 1952 | # 1953 | # 1. respect the user's choice 1954 | # 2. and add empty lines around expressions that take multiple lines 1955 | # (except for module attributes) 1956 | # 3. empty lines are collapsed as to not exceed more than one 1957 | # 1958 | defp merge_algebra_with_comments([{doc, next_line, newlines} | docs], left) do 1959 | right = if newlines >= @newlines, do: line(), else: next_line 1960 | 1961 | doc = 1962 | if left != @empty do 1963 | concat(left, doc) 1964 | else 1965 | doc 1966 | end 1967 | 1968 | doc = 1969 | if docs != [] and right != @empty do 1970 | concat(doc, concat(collapse_lines(2), right)) 1971 | else 1972 | doc 1973 | end 1974 | 1975 | [group(doc) | merge_algebra_with_comments(docs, right)] 1976 | end 1977 | 1978 | defp merge_algebra_with_comments([], _) do 1979 | [] 1980 | end 1981 | 1982 | ## Quoted helpers 1983 | 1984 | defp left_op_context(context), do: force_many_args_or_operand(context, :parens_arg) 1985 | defp right_op_context(context), do: force_many_args_or_operand(context, :operand) 1986 | 1987 | defp force_many_args_or_operand(:no_parens_one_arg, _choice), do: :no_parens_arg 1988 | defp force_many_args_or_operand(:parens_one_arg, _choice), do: :parens_arg 1989 | defp force_many_args_or_operand(:no_parens_arg, _choice), do: :no_parens_arg 1990 | defp force_many_args_or_operand(:parens_arg, _choice), do: :parens_arg 1991 | defp force_many_args_or_operand(:operand, choice), do: choice 1992 | defp force_many_args_or_operand(:block, choice), do: choice 1993 | 1994 | defp quoted_to_algebra_with_parens_if_operator(ast, context, state) do 1995 | {doc, state} = quoted_to_algebra(ast, context, state) 1996 | {wrap_in_parens_if_operator(doc, ast), state} 1997 | end 1998 | 1999 | # TODO: We can remove this workaround once we remove 2000 | # ?rearrange_uop from the parser on v2.0. 2001 | defp wrap_in_parens_if_operator(doc, {:__block__, _, [expr]}) do 2002 | wrap_in_parens_if_operator(doc, expr) 2003 | end 2004 | 2005 | defp wrap_in_parens_if_operator(doc, quoted) do 2006 | if operator?(quoted) and not module_attribute_read?(quoted) and not integer_capture?(quoted) do 2007 | wrap_in_parens(doc) 2008 | else 2009 | doc 2010 | end 2011 | end 2012 | 2013 | defp wrap_in_parens_if_binary_operator(doc, quoted) do 2014 | if binary_operator?(quoted) do 2015 | wrap_in_parens(doc) 2016 | else 2017 | doc 2018 | end 2019 | end 2020 | 2021 | defp wrap_in_parens_if_inspected_atom(":" <> _ = doc) do 2022 | "(" <> doc <> ")" 2023 | end 2024 | 2025 | defp wrap_in_parens_if_inspected_atom(doc) do 2026 | doc 2027 | end 2028 | 2029 | defp wrap_in_parens(doc) do 2030 | concat(concat("(", nest(doc, :cursor)), ")") 2031 | end 2032 | 2033 | defp many_args_to_algebra([arg | args], state, fun) do 2034 | Enum.reduce(args, fun.(arg, state), fn arg, {doc_acc, state_acc} -> 2035 | {arg_doc, state_acc} = fun.(arg, state_acc) 2036 | {glue(concat(doc_acc, ","), arg_doc), state_acc} 2037 | end) 2038 | end 2039 | 2040 | defp module_attribute_read?({:@, _, [{var, _, var_context}]}) 2041 | when is_atom(var) and is_atom(var_context) do 2042 | classify(var) == :callable_local 2043 | end 2044 | 2045 | defp module_attribute_read?(_), do: false 2046 | 2047 | defp integer_capture?({:&, _, [integer]}) when is_integer(integer), do: true 2048 | defp integer_capture?(_), do: false 2049 | 2050 | defp operator?(quoted) do 2051 | unary_operator?(quoted) or binary_operator?(quoted) 2052 | end 2053 | 2054 | defp binary_operator?(quoted) do 2055 | case quoted do 2056 | {op, _, [_, _]} when is_atom(op) -> 2057 | Code.Identifier.binary_op(op) != :error 2058 | 2059 | _ -> 2060 | false 2061 | end 2062 | end 2063 | 2064 | defp unary_operator?(quoted) do 2065 | case quoted do 2066 | {op, _, [_]} when is_atom(op) -> 2067 | Code.Identifier.unary_op(op) != :error 2068 | 2069 | _ -> 2070 | false 2071 | end 2072 | end 2073 | 2074 | defp with_next_break_fits(condition, doc, fun) do 2075 | if condition do 2076 | doc 2077 | |> next_break_fits(:enabled) 2078 | |> fun.() 2079 | |> next_break_fits(:disabled) 2080 | else 2081 | fun.(doc) 2082 | end 2083 | end 2084 | 2085 | defp next_break_fits?({:{}, meta, _args}, state) do 2086 | eol_or_comments?(meta, state) 2087 | end 2088 | 2089 | defp next_break_fits?({:__block__, meta, [{_, _}]}, state) do 2090 | eol_or_comments?(meta, state) 2091 | end 2092 | 2093 | defp next_break_fits?({:<<>>, meta, [_ | _] = entries}, state) do 2094 | meta[:delimiter] == ~s["""] or 2095 | (not interpolated?(entries) and eol_or_comments?(meta, state)) 2096 | end 2097 | 2098 | defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do 2099 | meta[:delimiter] == ~s['''] 2100 | end 2101 | 2102 | defp next_break_fits?({{:., _, [_left, :{}]}, _, _}, _state) do 2103 | true 2104 | end 2105 | 2106 | defp next_break_fits?({:__block__, meta, [string]}, _state) when is_binary(string) do 2107 | meta[:delimiter] == ~s["""] 2108 | end 2109 | 2110 | defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do 2111 | meta[:delimiter] != ~s['] 2112 | end 2113 | 2114 | defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do 2115 | true 2116 | end 2117 | 2118 | defp next_break_fits?({fun, meta, args}, _state) when is_atom(fun) and is_list(args) do 2119 | meta[:delimiter] in [@double_heredoc, @single_heredoc] and 2120 | fun |> Atom.to_string() |> String.starts_with?("sigil_") 2121 | end 2122 | 2123 | defp next_break_fits?({{:__block__, _, [atom]}, expr}, state) when is_atom(atom) do 2124 | next_break_fits?(expr, state) 2125 | end 2126 | 2127 | defp next_break_fits?(_, _state) do 2128 | false 2129 | end 2130 | 2131 | defp eol_or_comments?(meta, %{comments: comments}) do 2132 | eol?(meta) or 2133 | ( 2134 | min_line = line(meta) 2135 | max_line = closing_line(meta) 2136 | Enum.any?(comments, fn {line, _, _} -> line > min_line and line < max_line end) 2137 | ) 2138 | end 2139 | 2140 | # A literal list is a keyword or (... -> ...) 2141 | defp last_arg_to_keyword([_ | _] = arg, _list_to_keyword?, _skip_parens?, _comments) do 2142 | {keyword?(arg), arg} 2143 | end 2144 | 2145 | # This is a list of tuples, it can be converted to keywords. 2146 | defp last_arg_to_keyword( 2147 | {:__block__, meta, [[_ | _] = arg]} = block, 2148 | true, 2149 | skip_parens?, 2150 | comments 2151 | ) do 2152 | cond do 2153 | not keyword?(arg) -> 2154 | {false, block} 2155 | 2156 | skip_parens? -> 2157 | block_line = line(meta) 2158 | {{_, arg_meta, _}, _} = hd(arg) 2159 | first_line = line(arg_meta) 2160 | 2161 | case Enum.drop_while(comments, fn {line, _, _} -> line <= block_line end) do 2162 | [{line, _, _} | _] when line <= first_line -> 2163 | {false, block} 2164 | 2165 | _ -> 2166 | {true, arg} 2167 | end 2168 | 2169 | true -> 2170 | {true, arg} 2171 | end 2172 | end 2173 | 2174 | # Otherwise we don't have a keyword. 2175 | defp last_arg_to_keyword(arg, _list_to_keyword?, _skip_parens?, _comments) do 2176 | {false, arg} 2177 | end 2178 | 2179 | defp force_args?(args) do 2180 | match?([_, _ | _], args) and force_args?(args, MapSet.new()) 2181 | end 2182 | 2183 | defp force_args?([[arg | _] | args], lines) do 2184 | force_args?([arg | args], lines) 2185 | end 2186 | 2187 | defp force_args?([arg | args], lines) do 2188 | line = 2189 | case arg do 2190 | {{_, meta, _}, _} -> line(meta) 2191 | {_, meta, _} -> line(meta) 2192 | end 2193 | 2194 | if MapSet.member?(lines, line) do 2195 | false 2196 | else 2197 | force_args?(args, MapSet.put(lines, line)) 2198 | end 2199 | end 2200 | 2201 | defp force_args?([], _lines), do: true 2202 | 2203 | defp force_keyword(doc, arg) do 2204 | if force_args?(arg), do: force_unfit(doc), else: doc 2205 | end 2206 | 2207 | defp keyword?([{_, _} | list]), do: keyword?(list) 2208 | defp keyword?(rest), do: rest == [] 2209 | 2210 | defp keyword_key?({:__block__, meta, [atom]}) when is_atom(atom), 2211 | do: meta[:format] == :keyword 2212 | 2213 | defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, meta, [{:<<>>, _, _}, :utf8]}), 2214 | do: meta[:format] == :keyword 2215 | 2216 | defp keyword_key?(_), 2217 | do: false 2218 | 2219 | defp eol?(meta) do 2220 | Keyword.get(meta, :newlines, 0) > 0 2221 | end 2222 | 2223 | defp meta?(meta, key) do 2224 | is_list(meta[key]) 2225 | end 2226 | 2227 | defp line(meta) do 2228 | meta[:line] || @max_line 2229 | end 2230 | 2231 | defp end_line(meta) do 2232 | meta[:end][:line] || @min_line 2233 | end 2234 | 2235 | defp closing_line(meta) do 2236 | meta[:closing][:line] || @min_line 2237 | end 2238 | 2239 | ## Algebra helpers 2240 | 2241 | # Relying on the inner document is brittle and error prone. 2242 | # It would be best if we had a mechanism to apply this. 2243 | defp concat_to_last_group({:doc_cons, left, right}, concat) do 2244 | {:doc_cons, left, concat_to_last_group(right, concat)} 2245 | end 2246 | 2247 | defp concat_to_last_group({:doc_group, group, mode}, concat) do 2248 | {:doc_group, {:doc_cons, group, concat}, mode} 2249 | end 2250 | 2251 | defp concat_to_last_group(other, concat) do 2252 | {:doc_cons, other, concat} 2253 | end 2254 | 2255 | defp ungroup_if_group({:doc_group, group, _mode}), do: group 2256 | defp ungroup_if_group(other), do: other 2257 | 2258 | defp format_to_string(doc) do 2259 | doc |> Inspect.Algebra.format(:infinity) |> IO.iodata_to_binary() 2260 | end 2261 | 2262 | defp maybe_empty_line() do 2263 | nest(break(""), :reset) 2264 | end 2265 | 2266 | defp surround(left, doc, right) do 2267 | if doc == @empty do 2268 | concat(left, right) 2269 | else 2270 | group(glue(nest(glue(left, "", doc), 2, :break), "", right)) 2271 | end 2272 | end 2273 | 2274 | defp nest_by_length(doc, string) do 2275 | nest(doc, String.length(string)) 2276 | end 2277 | 2278 | defp split_last(list) do 2279 | {left, [right]} = Enum.split(list, -1) 2280 | {left, right} 2281 | end 2282 | 2283 | defp classify(atom) when is_atom(atom) do 2284 | charlist = Atom.to_charlist(atom) 2285 | 2286 | cond do 2287 | atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] -> 2288 | :not_callable 2289 | 2290 | atom in [:"::", :"//"] -> 2291 | :not_atomable 2292 | 2293 | Code.Identifier.unary_op(atom) != :error or Code.Identifier.binary_op(atom) != :error -> 2294 | :callable_operator 2295 | 2296 | valid_alias?(charlist) -> 2297 | :alias 2298 | 2299 | true -> 2300 | case :elixir_config.identifier_tokenizer().tokenize(charlist) do 2301 | {kind, _acc, [], _, _, special} -> 2302 | if kind == :identifier and not :lists.member(?@, special) do 2303 | :callable_local 2304 | else 2305 | :not_callable 2306 | end 2307 | 2308 | _ -> 2309 | :other 2310 | end 2311 | end 2312 | end 2313 | 2314 | defp valid_alias?('Elixir' ++ rest), do: valid_alias_piece?(rest) 2315 | defp valid_alias?(_other), do: false 2316 | 2317 | defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z, 2318 | do: valid_alias_piece?(trim_leading_while_valid_identifier(rest)) 2319 | 2320 | defp valid_alias_piece?([]), do: true 2321 | defp valid_alias_piece?(_other), do: false 2322 | 2323 | defp trim_leading_while_valid_identifier([char | rest]) 2324 | when char >= ?a and char <= ?z 2325 | when char >= ?A and char <= ?Z 2326 | when char >= ?0 and char <= ?9 2327 | when char == ?_ do 2328 | trim_leading_while_valid_identifier(rest) 2329 | end 2330 | 2331 | defp trim_leading_while_valid_identifier(other) do 2332 | other 2333 | end 2334 | 2335 | defp inspect_as_function(atom) when is_atom(atom) do 2336 | binary = Atom.to_string(atom) 2337 | 2338 | case classify(atom) do 2339 | type when type in [:callable_local, :callable_operator, :not_atomable] -> 2340 | binary 2341 | 2342 | type -> 2343 | escaped = 2344 | if type in [:not_callable, :alias] do 2345 | binary 2346 | else 2347 | elem(escape(binary, ?"), 0) 2348 | end 2349 | 2350 | IO.iodata_to_binary([?", escaped, ?"]) 2351 | end 2352 | end 2353 | 2354 | def escape(other, char, count \\ :infinity, fun \\ &escape_map/1) do 2355 | escape(other, char, count, [], fun) 2356 | end 2357 | 2358 | defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do 2359 | {acc, binary} 2360 | end 2361 | 2362 | defp escape(<>, char, count, acc, fun) do 2363 | escape(t, char, decrement(count), [acc | [?\\, char]], fun) 2364 | end 2365 | 2366 | defp escape(<>, char, count, acc, fun) do 2367 | escape(t, char, decrement(count), [acc | '\\\#{'], fun) 2368 | end 2369 | 2370 | defp escape(<>, char, count, acc, fun) do 2371 | escaped = if value = fun.(h), do: value, else: escape_char(h) 2372 | escape(t, char, decrement(count), [acc | escaped], fun) 2373 | end 2374 | 2375 | defp escape(<>, char, count, acc, fun) do 2376 | escape(t, char, decrement(count), [acc | ['\\x', to_hex(a), to_hex(b)]], fun) 2377 | end 2378 | 2379 | defp escape(<<>>, _char, _count, acc, _fun) do 2380 | {acc, <<>>} 2381 | end 2382 | 2383 | defp escape_char(0), do: '\\0' 2384 | 2385 | defp escape_char(65279), do: '\\uFEFF' 2386 | 2387 | defp escape_char(char) 2388 | when char in 0x20..0x7E 2389 | when char in 0xA0..0xD7FF 2390 | when char in 0xE000..0xFFFD 2391 | when char in 0x10000..0x10FFFF do 2392 | <> 2393 | end 2394 | 2395 | defp escape_char(char) when char < 0x100 do 2396 | <> = <> 2397 | ['\\x', to_hex(a), to_hex(b)] 2398 | end 2399 | 2400 | defp escape_char(char) when char < 0x10000 do 2401 | <> = <> 2402 | ['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}] 2403 | end 2404 | 2405 | defp escape_char(char) when char < 0x1000000 do 2406 | <> = <> 2407 | ['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}] 2408 | end 2409 | 2410 | defp escape_map(?\a), do: '\\a' 2411 | defp escape_map(?\b), do: '\\b' 2412 | defp escape_map(?\d), do: '\\d' 2413 | defp escape_map(?\e), do: '\\e' 2414 | defp escape_map(?\f), do: '\\f' 2415 | defp escape_map(?\n), do: '\\n' 2416 | defp escape_map(?\r), do: '\\r' 2417 | defp escape_map(?\t), do: '\\t' 2418 | defp escape_map(?\v), do: '\\v' 2419 | defp escape_map(?\\), do: '\\\\' 2420 | defp escape_map(_), do: false 2421 | 2422 | @compile {:inline, to_hex: 1, decrement: 1} 2423 | defp to_hex(c) when c in 0..9, do: ?0 + c 2424 | defp to_hex(c) when c in 10..15, do: ?A + c - 10 2425 | 2426 | defp decrement(:infinity), do: :infinity 2427 | defp decrement(counter), do: counter - 1 2428 | end 2429 | --------------------------------------------------------------------------------