├── .check.exs ├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ ├── pulls.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── code-of-conduct.md ├── config └── config.exs ├── dialyzer.ignore_warnings.exs ├── lib └── erlex.ex ├── mix.exs ├── mix.lock ├── priv └── plts │ └── .gitkeep ├── src ├── lexer.xrl └── parser.yrl └── test ├── literals_pretty_print_test.exs ├── pretty_print_test.exs └── test_helper.exs /.check.exs: -------------------------------------------------------------------------------- 1 | [ 2 | parallel: true, 3 | skipped: true, 4 | 5 | tools: [ 6 | {:unused_deps, command: "mix deps.unlock --check-unused"}, 7 | {:credo, "mix credo --strict --format oneline"}, 8 | {:compiler, "mix compile --warnings-as-errors"}, 9 | {:sobelow, false} 10 | ] 11 | ] 12 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # config/.credo.exs 2 | %{ 3 | configs: [ 4 | %{ 5 | name: "default", 6 | files: %{ 7 | included: ["lib/"], 8 | excluded: [ 9 | "mix.exs", 10 | "_build/", 11 | ".git/" 12 | ] 13 | }, 14 | color: true, 15 | checks: [ 16 | {Credo.Check.Consistency.ExceptionNames, false}, 17 | {Credo.Check.Design.TagTODO, false}, 18 | {Credo.Check.Readability.MaxLineLength, false}, 19 | {Credo.Check.Readability.PreferImplicitTry, false}, 20 | {Credo.Check.Refactor.ABCSize}, 21 | {Credo.Check.Refactor.PipeChainStart}, 22 | ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/pulls.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | name: ci 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | elixir: [1.10.3, 1.11.0] 12 | otp: [22.3, 23.1] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Elixir 16 | uses: erlef/setup-beam@v1 17 | with: 18 | elixir-version: ${{ matrix.elixir }} 19 | otp-version: ${{ matrix.otp }} 20 | - name: Restore dependencies cache 21 | uses: actions/cache@v2 22 | with: 23 | path: deps 24 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }} 27 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix 28 | - name: Restore Dialyzer PLT cache 29 | uses: actions/cache@v2 30 | with: 31 | path: priv/plts 32 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt-${{ hashFiles('**/priv/plts/dialyzer.plt.hash') }} 33 | restore-keys: | 34 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt-${{ hashFiles('**/priv/plts/dialyzer.plt.hash') }} 35 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt 36 | - name: Install dependencies 37 | run: mix deps.get 38 | - name: mix check 39 | run: mix check 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | name: Release 12 | strategy: 13 | matrix: 14 | otp: [23] 15 | elixir: [1.11.0] 16 | env: 17 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp}} 23 | elixir-version: ${{matrix.elixir}} 24 | - name: Restore dependencies cache 25 | uses: actions/cache@v2 26 | with: 27 | path: deps 28 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-release-deps-${{ hashFiles('**/mix.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-release-deps-${{ hashFiles('**/mix.lock') }} 31 | ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-release-deps 32 | - run: mix deps.get 33 | - run: mix hex.publish --yes 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /cover/ 3 | /deps/ 4 | /doc/ 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | erlex-*.tar 9 | .elixir_ls 10 | src/parser.erl 11 | src/lexer.erl 12 | 13 | /priv/plts/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ## 0.2.6 - 2020-03-09 9 | ### Added 10 | - Replace << parsing to fix nexted pattern matching binary bugs discovered from Phoenix 1.4.15. 11 | 12 | ## 0.2.5 - 2019-09-19 13 | ### Added 14 | - Handle Erlang variables better with casing in guards. 15 | 16 | ## 0.2.4 - 2019-07-18 17 | ### Added 18 | - Handle names in whens more properly. 19 | 20 | ## 0.2.3 - 2019-07-16 21 | ### Added 22 | - Handle single letter atoms in constracts. 23 | 24 | ## 0.2.2 - 2019-06-07 25 | ### Added 26 | - Handle pretty printing binary strings better. 27 | 28 | ## 0.2.1 - 2019-02-11 29 | ### Added 30 | - Handle another struct pretty print. 31 | 32 | ## 0.2.0 - 2019-02-01 33 | ### Added 34 | - Handle scientific notation. 35 | - Handle numbered differently, fixes bug. 36 | - Handle ... contracts. 37 | - Add another struct pretty print layer. 38 | 39 | ## 0.1.6 - 2018-11-06 40 | ### Added 41 | - Add ability to pretty print exception struct as Exception.t(). 42 | 43 | ## 0.1.5 - 2018-09-17 44 | ### Added 45 | - Add ability to pretty print numbered named types 46 | 47 | ## 0.1.4 - 2018-09-17 48 | ### Added 49 | - Add ability to parse whens in specs. 50 | 51 | ## 0.1.3 - 2018-09-10 52 | ### Added 53 | - Add ability to parse empty tuples 54 | - Bump ex_doc 55 | 56 | ## 0.1.2 - 2018-08-26 57 | ### Added 58 | - Add ability to deal with when clauses in contracts 59 | 60 | ## 0.1.1 - 2018-08-21 61 | ### Added 62 | - Add newline characters in multihead contracts 63 | 64 | ## 0.1.0 - 2018-07-22 65 | ### Added 66 | - Changelog, CI, docs 67 | - Initial functionality and tests 68 | 69 | [Unreleased]: https://github.com/asummers/erlex/compare/v0.2.1...HEAD 70 | [0.2.0...0.2.1]: https://github.com/asummers/erlex/compare/v0.2.0...v0.2.1 71 | [0.1.6...0.2.0]: https://github.com/asummers/erlex/compare/v0.1.6...v0.2.0 72 | [0.1.5...0.1.6]: https://github.com/asummers/erlex/compare/v0.1.5...v0.1.6 73 | [0.1.4...0.1.5]: https://github.com/asummers/erlex/compare/v0.1.4...v0.1.5 74 | [0.1.3...0.1.4]: https://github.com/asummers/erlex/compare/v0.1.3...v0.1.4 75 | [0.1.2...0.1.3]: https://github.com/asummers/erlex/compare/v0.1.2...v0.1.3 76 | [0.1.1...0.1.2]: https://github.com/asummers/erlex/compare/v0.1.1...v0.1.2 77 | [0.1.0...0.1.1]: https://github.com/asummers/erlex/compare/v0.1.0...v0.1.1 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Erlex 2 | 3 | Thank you for considering contributing to Erlex! No matter how big or 4 | small, every contribution helps make this a better project. We 5 | absolutely appreciate any and all help. 6 | 7 | ## Issues 8 | 9 | Please file issues [here](https://github.com/asummers/erlex/issues) 10 | 11 | ## Documentation 12 | 13 | Documentation is the gift that keeps on giving. If you notice 14 | something even as small as a typo or an ambiguous sentence, please 15 | file an issue or submit a pull request! 16 | 17 | ## Development Contributions 18 | 19 | Take a look at the open issues 20 | [here](https://github.com/asummers/erlex/issues) and post that you 21 | plan on working on an issue, so we can discuss approaches and make 22 | sure there isn't duplicated work happening. 23 | 24 | Before submitting a pull request, make sure all of the following pass: 25 | 26 | ``` 27 | mix deps.get 28 | mix check 29 | ``` 30 | 31 | Steps: 32 | 1) Fork it! 33 | 2) Create a new branch off of master in your fork and do your work 34 | 3) Squash your commits to a single commit. If you don't know how to do 35 | this, that's okay. We will work through it in pull request. 36 | 4) Make sure any documentation has been updated and all checks are passing. 37 | 5) Submit a pull request. Thank you! :heart: 38 | 39 | ## Code of Conduct 40 | 41 | Be sure to read and follow the [code of conduct](https://github.com/asummers/erlex/blob/master/code-of-conduct.md). 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2013-2018 Andrew Summers and contributors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Hex version badge](https://img.shields.io/hexpm/v/erlex.svg)](https://hex.pm/packages/erlex) 2 | [![Hex docs badge](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/erlex/) 3 | [![Total download badge](https://img.shields.io/hexpm/dt/erlex.svg)](https://hex.pm/packages/erlex) 4 | [![License badge](https://img.shields.io/hexpm/l/erlex.svg)](https://github.com/asummers/erlex/blob/master/LICENSE.md) 5 | [![Build status badge](https://img.shields.io/circleci/project/github/asummers/erlex/master.svg)](https://circleci.com/gh/asummers/erlex/tree/master) 6 | [![Code coverage badge](https://img.shields.io/codecov/c/github/asummers/erlex/master.svg)](https://codecov.io/gh/asummers/erlex/branch/master) 7 | [![Last Updated badge](https://img.shields.io/github/last-commit/asummers/erlex.svg)](https://github.com/asummers/erlex/commits/master) 8 | 9 | # Erlex 10 | 11 | Convert Erlang style structs and error messages to equivalent Elixir. 12 | 13 | Useful for pretty printing things like Dialyzer errors and Observer 14 | state. NOTE: Because this code calls the Elixir formatter, it requires 15 | Elixir 1.6+. 16 | 17 | ## Documentation 18 | [Hex Docs](https://hexdocs.pm/erlex). 19 | 20 | ## Changelog 21 | 22 | Check out the [Changelog](https://github.com/asummers/erlex/blob/master/CHANGELOG.md). 23 | 24 | ## Installation 25 | 26 | The package can be installed from Hex by adding `erlex` to your list 27 | of dependencies in `mix.exs`: 28 | 29 | ```elixir 30 | def deps do 31 | [ 32 | {:erlex, "~> 0.2"}, 33 | ] 34 | end 35 | ``` 36 | 37 | ## Usage 38 | 39 | Invoke `Erlex.pretty_print/1` with the input string. 40 | 41 | ```elixir 42 | iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map()) -> 'Elixir.Plug.Conn':t()" 43 | iex> Erlex.pretty_print(str) 44 | (Plug.Conn.t(), binary() | atom(), Keyword.t() | map()) :: Plug.Conn.t() 45 | ``` 46 | 47 | While the lion's share of the work is done via invoking 48 | `Erlex.pretty_print/1`, other higher order functions exist for further 49 | formatting certain messages by running through the Elixir formatter. 50 | Because we know the previous example is a type, we can invoke the 51 | `Erlex.pretty_print_contract/1` function, which would format that 52 | appropriately for very long lines. 53 | 54 | ```elixir 55 | iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map(), map() | atom(), non_neg_integer(), binary(), binary(), binary(), binary(), binary()) -> 'Elixir.Plug.Conn':t()" 56 | iex> Erlex.pretty_print_contract(str) 57 | ( 58 | Plug.Conn.t(), 59 | binary() | atom(), 60 | Keyword.t() | map(), 61 | map() | atom(), 62 | non_neg_integer(), 63 | binary(), 64 | binary(), 65 | binary(), 66 | binary(), 67 | binary() 68 | ) :: Plug.Conn.t() 69 | ``` 70 | 71 | ## Contributing 72 | 73 | We welcome contributions of all kinds! To get started, click [here](https://github.com/asummers/erlex/blob/master/CONTRIBUTING.md). 74 | 75 | ## Code of Conduct 76 | 77 | Be sure to read and follow the [code of conduct](https://github.com/asummers/erlex/blob/master/code-of-conduct.md). 78 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /dialyzer.ignore_warnings.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {"lib/erlex.ex", :no_return, 63}, 3 | {"lib/erlex.ex", :no_return, 96}, 4 | {"lib/erlex.ex", :no_return, 139}, 5 | {"lib/erlex.ex", :no_return, 149}, 6 | {"lib/erlex.ex", :no_return, 168}, 7 | {"lib/erlex.ex", :no_return, 189}, 8 | ] 9 | -------------------------------------------------------------------------------- /lib/erlex.ex: -------------------------------------------------------------------------------- 1 | defmodule Erlex do 2 | @moduledoc """ 3 | Convert Erlang style structs and error messages to equivalent Elixir. 4 | 5 | Lexes and parses the Erlang output, then runs through pretty 6 | printer. 7 | 8 | ## Usage 9 | 10 | Invoke `Erlex.pretty_print/1` wuth the input string. 11 | 12 | ```elixir 13 | iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map()) -> 'Elixir.Plug.Conn':t()" 14 | iex> Erlex.pretty_print(str) 15 | (Plug.Conn.t(), binary() | atom(), Keyword.t() | map()) :: Plug.Conn.t() 16 | ``` 17 | 18 | While the lion's share of the work is done via invoking 19 | `Erlex.pretty_print/1`, other higher order functions exist for further 20 | formatting certain messages by running through the Elixir formatter. 21 | Because we know the previous example is a type, we can invoke the 22 | `Erlex.pretty_print_contract/1` function, which would format that 23 | appropriately for very long lines. 24 | 25 | ```elixir 26 | iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map(), map() | atom(), non_neg_integer(), binary(), binary(), binary(), binary(), binary()) -> 'Elixir.Plug.Conn':t()" 27 | iex> Erlex.pretty_print_contract(str) 28 | ( 29 | Plug.Conn.t(), 30 | binary() | atom(), 31 | Keyword.t() | map(), 32 | map() | atom(), 33 | non_neg_integer(), 34 | binary(), 35 | binary(), 36 | binary(), 37 | binary(), 38 | binary() 39 | ) :: Plug.Conn.t() 40 | ``` 41 | """ 42 | 43 | defp lex(str) do 44 | try do 45 | {:ok, tokens, _} = :lexer.string(str) 46 | tokens 47 | rescue 48 | _ -> 49 | throw({:error, :lexing, str}) 50 | end 51 | end 52 | 53 | defp parse(tokens) do 54 | try do 55 | {:ok, [first | _]} = :parser.parse(tokens) 56 | first 57 | rescue 58 | _ -> 59 | throw({:error, :parsing, tokens}) 60 | end 61 | end 62 | 63 | defp format(code) do 64 | try do 65 | Code.format_string!(code) 66 | rescue 67 | _ -> 68 | throw({:error, :formatting, code}) 69 | end 70 | end 71 | 72 | @spec pretty_print_infix(infix :: String.t()) :: String.t() 73 | def pretty_print_infix('=:='), do: "===" 74 | def pretty_print_infix('=/='), do: "!==" 75 | def pretty_print_infix('/='), do: "!=" 76 | def pretty_print_infix('=<'), do: "<=" 77 | def pretty_print_infix(infix), do: to_string(infix) 78 | 79 | @spec pretty_print(str :: String.t()) :: String.t() 80 | def pretty_print(str) do 81 | parsed = 82 | str 83 | |> to_charlist() 84 | |> lex() 85 | |> parse() 86 | 87 | try do 88 | do_pretty_print(parsed) 89 | rescue 90 | _ -> 91 | throw({:error, :pretty_printing, parsed}) 92 | end 93 | end 94 | 95 | @spec pretty_print_pattern(pattern :: String.t()) :: String.t() 96 | def pretty_print_pattern('pattern ' ++ rest) do 97 | pretty_print_type(rest) 98 | end 99 | 100 | def pretty_print_pattern(pattern) do 101 | pretty_print_type(pattern) 102 | end 103 | 104 | @spec pretty_print_contract( 105 | contract :: String.t(), 106 | module :: String.t(), 107 | function :: String.t() 108 | ) :: String.t() 109 | def pretty_print_contract(contract, module, function) do 110 | [head | tail] = 111 | contract 112 | |> to_string() 113 | |> String.split(";") 114 | 115 | head = 116 | head 117 | |> String.trim_leading(to_string(module)) 118 | |> String.trim_leading(":") 119 | |> String.trim_leading(to_string(function)) 120 | 121 | [head | tail] 122 | |> Enum.join(";") 123 | |> pretty_print_contract() 124 | end 125 | 126 | @spec pretty_print_contract(contract :: String.t()) :: String.t() 127 | def pretty_print_contract(contract) do 128 | [head | tail] = 129 | contract 130 | |> to_string() 131 | |> String.split(";") 132 | 133 | if Enum.empty?(tail) do 134 | do_pretty_print_contract(head) 135 | else 136 | joiner = "Contract head:\n" 137 | 138 | joiner <> Enum.map_join([head | tail], "\n\n" <> joiner, &do_pretty_print_contract/1) 139 | end 140 | end 141 | 142 | defp do_pretty_print_contract(contract) do 143 | prefix = "@spec a" 144 | suffix = "\ndef a() do\n :ok\nend" 145 | pretty = pretty_print(contract) 146 | 147 | """ 148 | @spec a#{pretty} 149 | def a() do 150 | :ok 151 | end 152 | """ 153 | |> format() 154 | |> Enum.join("") 155 | |> String.trim_leading(prefix) 156 | |> String.trim_trailing(suffix) 157 | |> String.replace("\n ", "\n") 158 | end 159 | 160 | @spec pretty_print_type(type :: String.t()) :: String.t() 161 | def pretty_print_type(type) do 162 | prefix = "@spec a(" 163 | suffix = ") :: :ok\ndef a() do\n :ok\nend" 164 | indented_suffix = ") ::\n :ok\ndef a() do\n :ok\nend" 165 | pretty = pretty_print(type) 166 | 167 | """ 168 | @spec a(#{pretty}) :: :ok 169 | def a() do 170 | :ok 171 | end 172 | """ 173 | |> format() 174 | |> Enum.join("") 175 | |> String.trim_leading(prefix) 176 | |> String.trim_trailing(suffix) 177 | |> String.trim_trailing(indented_suffix) 178 | |> String.replace("\n ", "\n") 179 | end 180 | 181 | @spec pretty_print_args(args :: String.t()) :: String.t() 182 | def pretty_print_args(args) do 183 | prefix = "@spec a" 184 | suffix = " :: :ok\ndef a() do\n :ok\nend" 185 | pretty = pretty_print(args) 186 | 187 | """ 188 | @spec a#{pretty} :: :ok 189 | def a() do 190 | :ok 191 | end 192 | """ 193 | |> format() 194 | |> Enum.join("") 195 | |> String.trim_leading(prefix) 196 | |> String.trim_trailing(suffix) 197 | |> String.replace("\n ", "\n") 198 | end 199 | 200 | defp do_pretty_print({:any}) do 201 | "_" 202 | end 203 | 204 | defp do_pretty_print({:inner_any_function}) do 205 | "(...)" 206 | end 207 | 208 | defp do_pretty_print({:any_function}) do 209 | "(... -> any)" 210 | end 211 | 212 | defp do_pretty_print({:assignment, {:atom, atom}, value}) do 213 | "#{normalize_name(atom)} = #{do_pretty_print(value)}" 214 | end 215 | 216 | defp do_pretty_print({:atom, [:_]}) do 217 | "_" 218 | end 219 | 220 | defp do_pretty_print({:atom, ['_']}) do 221 | "_" 222 | end 223 | 224 | defp do_pretty_print({:atom, atom}) do 225 | atomize(atom) 226 | end 227 | 228 | defp do_pretty_print({:binary_part, value, _, size}) do 229 | "#{do_pretty_print(value)} :: #{do_pretty_print(size)}" 230 | end 231 | 232 | defp do_pretty_print({:binary_part, value, size}) do 233 | "#{do_pretty_print(value)} :: #{do_pretty_print(size)}" 234 | end 235 | 236 | defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 8}}}]}) do 237 | "binary()" 238 | end 239 | 240 | defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 1}}}]}) do 241 | "bitstring()" 242 | end 243 | 244 | defp do_pretty_print({:binary, binary_parts}) do 245 | binary_parts = Enum.map_join(binary_parts, ", ", &do_pretty_print/1) 246 | "<<#{binary_parts}>>" 247 | end 248 | 249 | defp do_pretty_print({:binary, value, size}) do 250 | "<<#{do_pretty_print(value)} :: #{do_pretty_print(size)}>>" 251 | end 252 | 253 | defp do_pretty_print({:byte_list, byte_list}) do 254 | byte_list 255 | |> Enum.into(<<>>, fn byte -> 256 | <> 257 | end) 258 | |> inspect() 259 | end 260 | 261 | defp do_pretty_print({:contract, {:args, args}, {:return, return}, {:whens, whens}}) do 262 | {printed_whens, when_names} = collect_and_print_whens(whens) 263 | 264 | args = {:when_names, when_names, args} 265 | return = {:when_names, when_names, return} 266 | 267 | "(#{do_pretty_print(args)}) :: #{do_pretty_print(return)} when #{printed_whens}" 268 | end 269 | 270 | defp do_pretty_print({:contract, {:args, {:inner_any_function}}, {:return, return}}) do 271 | "((...) -> #{do_pretty_print(return)})" 272 | end 273 | 274 | defp do_pretty_print({:contract, {:args, args}, {:return, return}}) do 275 | "#{do_pretty_print(args)} :: #{do_pretty_print(return)}" 276 | end 277 | 278 | defp do_pretty_print({:function, {:contract, {:args, args}, {:return, return}}}) do 279 | "(#{do_pretty_print(args)} -> #{do_pretty_print(return)})" 280 | end 281 | 282 | defp do_pretty_print({:int, int}) do 283 | "#{to_string(int)}" 284 | end 285 | 286 | defp do_pretty_print({:list, :paren, items}) do 287 | "(#{Enum.map_join(items, ", ", &do_pretty_print/1)})" 288 | end 289 | 290 | defp do_pretty_print( 291 | {:list, :square, 292 | [ 293 | tuple: [ 294 | {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, 295 | {:atom, [:_]} 296 | ] 297 | ]} 298 | ) do 299 | "Keyword.t()" 300 | end 301 | 302 | defp do_pretty_print( 303 | {:list, :square, 304 | [ 305 | tuple: [ 306 | {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, 307 | t 308 | ] 309 | ]} 310 | ) do 311 | "Keyword.t(#{do_pretty_print(t)})" 312 | end 313 | 314 | defp do_pretty_print({:list, :square, items}) do 315 | "[#{Enum.map_join(items, ", ", &do_pretty_print/1)}]" 316 | end 317 | 318 | defp do_pretty_print({:map_entry, key, value}) do 319 | "#{do_pretty_print(key)} => #{do_pretty_print(value)}" 320 | end 321 | 322 | defp do_pretty_print( 323 | {:map, 324 | [ 325 | {:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}}, 326 | {:map_entry, {:atom, [:_]}, {:atom, [:_]}} 327 | ]} 328 | ) do 329 | "struct()" 330 | end 331 | 332 | defp do_pretty_print( 333 | {:map, 334 | [ 335 | {:map_entry, {:atom, '\'__struct__\''}, 336 | {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}}, 337 | {:map_entry, {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, {:atom, [:_]}} 338 | ]} 339 | ) do 340 | "struct()" 341 | end 342 | 343 | defp do_pretty_print( 344 | {:map, 345 | [ 346 | {:map_entry, {:atom, '\'__struct__\''}, 347 | {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}}, 348 | {:map_entry, {:atom, [:_]}, {:atom, [:_]}} 349 | ]} 350 | ) do 351 | "struct()" 352 | end 353 | 354 | defp do_pretty_print( 355 | {:map, 356 | [ 357 | {:map_entry, {:atom, '\'__exception__\''}, {:atom, '\'true\''}}, 358 | {:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}}, 359 | {:map_entry, {:atom, [:_]}, {:atom, [:_]}} 360 | ]} 361 | ) do 362 | "Exception.t()" 363 | end 364 | 365 | defp do_pretty_print({:map, map_keys}) do 366 | case struct_parts(map_keys) do 367 | %{name: name, entries: [{:map_entry, {:atom, [:_]}, {:atom, [:_]}}]} -> 368 | "%#{name}{}" 369 | 370 | %{name: name, entries: entries} -> 371 | "%#{name}{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}" 372 | end 373 | end 374 | 375 | defp do_pretty_print({:named_type_with_appended_colon, named_type, type}) 376 | when is_tuple(named_type) and is_tuple(type) do 377 | case named_type do 378 | {:atom, name} -> 379 | "#{normalize_name(name)}: #{do_pretty_print(type)}" 380 | 381 | other -> 382 | "#{do_pretty_print(other)}: #{do_pretty_print(type)}" 383 | end 384 | end 385 | 386 | defp do_pretty_print({:named_type, named_type, type}) 387 | when is_tuple(named_type) and is_tuple(type) do 388 | case named_type do 389 | {:atom, name} -> 390 | "#{normalize_name(name)} :: #{do_pretty_print(type)}" 391 | 392 | other -> 393 | "#{do_pretty_print(other)} :: #{do_pretty_print(type)}" 394 | end 395 | end 396 | 397 | defp do_pretty_print({:named_type, named_type, type}) when is_tuple(named_type) do 398 | case named_type do 399 | {:atom, name = '\'Elixir' ++ _} -> 400 | "#{atomize(name)}.#{deatomize(type)}()" 401 | 402 | {:atom, name} -> 403 | "#{normalize_name(name)} :: #{deatomize(type)}()" 404 | 405 | other -> 406 | name = do_pretty_print(other) 407 | "#{name} :: #{deatomize(type)}()" 408 | end 409 | end 410 | 411 | defp do_pretty_print({nil}) do 412 | "nil" 413 | end 414 | 415 | defp do_pretty_print({:pattern, pattern_items}) do 416 | "#{Enum.map_join(pattern_items, ", ", &do_pretty_print/1)}" 417 | end 418 | 419 | defp do_pretty_print( 420 | {:pipe_list, {:atom, ['f', 'a', 'l', 's', 'e']}, {:atom, ['t', 'r', 'u', 'e']}} 421 | ) do 422 | "boolean()" 423 | end 424 | 425 | defp do_pretty_print( 426 | {:pipe_list, {:atom, '\'infinity\''}, 427 | {:type_list, ['n', 'o', 'n', :_, 'n', 'e', 'g', :_, 'i', 'n', 't', 'e', 'g', 'e', 'r'], 428 | {:list, :paren, []}}} 429 | ) do 430 | "timeout()" 431 | end 432 | 433 | defp do_pretty_print({:pipe_list, head, tail}) do 434 | "#{do_pretty_print(head)} | #{do_pretty_print(tail)}" 435 | end 436 | 437 | defp do_pretty_print({:range, from, to}) do 438 | "#{do_pretty_print(from)}..#{do_pretty_print(to)}" 439 | end 440 | 441 | defp do_pretty_print({:rest}) do 442 | "..." 443 | end 444 | 445 | defp do_pretty_print({:size, size}) do 446 | "size(#{do_pretty_print(size)})" 447 | end 448 | 449 | defp do_pretty_print({:tuple, tuple_items}) do 450 | "{#{Enum.map_join(tuple_items, ", ", &do_pretty_print/1)}}" 451 | end 452 | 453 | defp do_pretty_print({:type, type}) do 454 | "#{deatomize(type)}()" 455 | end 456 | 457 | defp do_pretty_print({:type, module, type}) do 458 | module = do_pretty_print(module) 459 | 460 | type = 461 | if is_tuple(type) do 462 | do_pretty_print(type) 463 | else 464 | deatomize(type) <> "()" 465 | end 466 | 467 | "#{module}.#{type}" 468 | end 469 | 470 | defp do_pretty_print({:type, module, type, inner_type}) do 471 | "#{atomize(module)}.#{deatomize(type)}(#{do_pretty_print(inner_type)})" 472 | end 473 | 474 | defp do_pretty_print({:type_list, type, types}) do 475 | "#{deatomize(type)}#{do_pretty_print(types)}" 476 | end 477 | 478 | defp do_pretty_print({:when_names, when_names, {:list, :paren, items}}) do 479 | Enum.map_join(items, ", ", &format_when_names(do_pretty_print(&1), when_names)) 480 | end 481 | 482 | defp do_pretty_print({:when_names, when_names, item}) do 483 | format_when_names(do_pretty_print(item), when_names) 484 | end 485 | 486 | defp format_when_names(item, when_names) do 487 | trimmed = String.trim_leading(item, ":") 488 | 489 | if trimmed in when_names do 490 | downcase_first(trimmed) 491 | else 492 | item 493 | end 494 | end 495 | 496 | defp collect_and_print_whens(whens) do 497 | {pretty_names, when_names} = 498 | Enum.reduce(whens, {[], []}, fn {_, when_name, type}, {prettys, whens} -> 499 | pretty_name = 500 | {:named_type_with_appended_colon, when_name, type} 501 | |> do_pretty_print() 502 | |> downcase_first() 503 | 504 | {[pretty_name | prettys], [when_name | whens]} 505 | end) 506 | 507 | when_names = 508 | Enum.map(when_names, fn {_, name} -> 509 | name 510 | |> atomize() 511 | |> String.trim_leading(":") 512 | end) 513 | 514 | printed_whens = 515 | pretty_names 516 | |> Enum.reverse() 517 | |> Enum.join(", ") 518 | 519 | {printed_whens, when_names} 520 | end 521 | 522 | defp downcase_first(string) do 523 | {first, rest} = String.split_at(string, 1) 524 | String.downcase(first) <> rest 525 | end 526 | 527 | defp atomize("Elixir." <> module_name) do 528 | String.trim(module_name, "'") 529 | end 530 | 531 | defp atomize([char]) do 532 | to_string(char) 533 | end 534 | 535 | defp atomize(atom) when is_list(atom) do 536 | atom_string = 537 | atom 538 | |> deatomize() 539 | |> to_string() 540 | 541 | stripped = strip_var_version(atom_string) 542 | 543 | if stripped == atom_string do 544 | atomize(stripped) 545 | else 546 | stripped 547 | end 548 | end 549 | 550 | defp atomize(<>) when is_number(number) do 551 | to_string(number) 552 | end 553 | 554 | defp atomize(atom) do 555 | atom = to_string(atom) 556 | 557 | if String.starts_with?(atom, "_") do 558 | atom 559 | else 560 | atom 561 | |> String.trim("'") 562 | |> String.to_atom() 563 | |> inspect() 564 | end 565 | end 566 | 567 | defp atom_part_to_string({:int, atom_part}), do: Integer.to_charlist(atom_part) 568 | defp atom_part_to_string(atom_part), do: atom_part 569 | 570 | defp strip_var_version(var_name) do 571 | var_name 572 | |> String.replace(~r/^V(.+)@\d+$/, "\\1") 573 | |> String.replace(~r/^(.+)@\d+$/, "\\1") 574 | end 575 | 576 | defp struct_parts(map_keys) do 577 | %{name: name, entries: entries} = 578 | Enum.reduce(map_keys, %{name: "", entries: []}, &struct_part/2) 579 | 580 | %{name: name, entries: Enum.reverse(entries)} 581 | end 582 | 583 | defp struct_part({:map_entry, {:atom, '\'__struct__\''}, {:atom, name}}, struct_parts) do 584 | name = 585 | name 586 | |> atomize() 587 | |> String.trim("\"") 588 | 589 | Map.put(struct_parts, :name, name) 590 | end 591 | 592 | defp struct_part(entry, struct_parts = %{entries: entries}) do 593 | Map.put(struct_parts, :entries, [entry | entries]) 594 | end 595 | 596 | defp deatomize([:_, :_, '@', {:int, _}]) do 597 | "_" 598 | end 599 | 600 | defp deatomize(chars) when is_list(chars) do 601 | Enum.map(chars, fn char -> 602 | char 603 | |> deatomize_char() 604 | |> atom_part_to_string() 605 | end) 606 | end 607 | 608 | defp deatomize_char(char) when is_atom(char) do 609 | Atom.to_string(char) 610 | end 611 | 612 | defp deatomize_char(char), do: char 613 | 614 | defp normalize_name(name) do 615 | name 616 | |> deatomize() 617 | |> to_string() 618 | |> strip_var_version() 619 | end 620 | end 621 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Erlex.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.2.6" 5 | @repo_url "https://github.com/asummers/erlex" 6 | 7 | def project do 8 | [ 9 | app: :erlex, 10 | version: @version, 11 | elixir: "~> 1.6", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | description: description(), 15 | package: package(), 16 | docs: docs(), 17 | dialyzer: dialyzer() 18 | ] 19 | end 20 | 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | defp dialyzer do 28 | [ 29 | plt_add_apps: [:mix, :erts, :kernel, :stdlib], 30 | flags: ["-Wunmatched_returns", "-Werror_handling", "-Wrace_conditions", "-Wno_opaque"], 31 | ignore_warnings: "dialyzer.ignore_warnings.exs", 32 | plt_core_path: "priv/plts", 33 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"} 34 | ] 35 | end 36 | 37 | defp deps do 38 | [ 39 | {:credo, "~> 1.0", only: :dev, runtime: false}, 40 | {:dialyxir, "1.0.0-rc.3", only: :dev, runtime: false, override: true}, 41 | {:ex_check, "~> 0.12.0", only: :dev, runtime: false}, 42 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 43 | ] 44 | end 45 | 46 | defp description do 47 | """ 48 | Convert Erlang style structs and error messages to equivalent Elixir. 49 | """ 50 | end 51 | 52 | defp docs() do 53 | [ 54 | main: "readme", 55 | source_url: @repo_url, 56 | source_ref: @version, 57 | homepage_url: @repo_url, 58 | extras: ["CHANGELOG.md", "README.md"] 59 | ] 60 | end 61 | 62 | defp package do 63 | [ 64 | files: [ 65 | "lib", 66 | "mix.exs", 67 | "README.md", 68 | "LICENSE.md", 69 | "src/lexer.xrl", 70 | "src/parser.yrl" 71 | ], 72 | maintainers: ["Andrew Summers"], 73 | licenses: ["Apache-2.0"], 74 | links: %{ 75 | "Changelog" => "https://hexdocs.pm/erlex/changelog.html", 76 | "GitHub" => @repo_url 77 | } 78 | ] 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm", "ddde98a783cec47d1b0ffaf5a0d5fbe09e90e1ac2d28452eefa6f72aac072c5a"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 6 | "ex_check": {:hex, :ex_check, "0.12.0", "c0e2919ecc06afeaf62c52d64f3d91bd4bc7dd8deaac5f84becb6278888c967a", [:mix], [], "hexpm", "cfafa8ef97c2596d45a1f19b5794cb5c7f700f25d164d3c9f8d7ec17ee67cf42"}, 7 | "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"}, 8 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 9 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 10 | "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"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 12 | } 13 | -------------------------------------------------------------------------------- /priv/plts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asummers/erlex/0548765838a08583c83d72d208fde112104a3d2b/priv/plts/.gitkeep -------------------------------------------------------------------------------- /src/lexer.xrl: -------------------------------------------------------------------------------- 1 | Definitions. 2 | 3 | WHITESPACE=[\s\t\r\n]+ 4 | SCIENTIFIC_NOTATION = -?[0-9]+\.[0-9]+e-?[0-9]+ 5 | INT = -?[0-9]+ 6 | REST = \.\.\. 7 | RANGE = \.\. 8 | ATOM = \'[^']+\' 9 | WHEN = \swhen\s 10 | 11 | Rules. 12 | 13 | {WHITESPACE} : skip_token. 14 | 15 | {REST} : {token, {'...', TokenLine}}. 16 | {WHEN} : {token, {'when', TokenLine}}. 17 | fun\( : {token, {'fun(', TokenLine}}. 18 | \* : {token, {'*', TokenLine}}. 19 | \[ : {token, {'[', TokenLine}}. 20 | \] : {token, {']', TokenLine}}. 21 | \( : {token, {'(', TokenLine}}. 22 | \) : {token, {')', TokenLine}}. 23 | \{ : {token, {'{', TokenLine}}. 24 | \} : {token, {'}', TokenLine}}. 25 | \# : {token, {'#', TokenLine}}. 26 | \| : {token, {'|', TokenLine}}. 27 | _ : {token, {'_', TokenLine}}. 28 | \:\: : {token, {'::', TokenLine}}. 29 | \: : {token, {':', TokenLine}}. 30 | \:\= : {token, {':=', TokenLine}}. 31 | \=\> : {token, {'=>', TokenLine}}. 32 | \-\> : {token, {'->', TokenLine}}. 33 | \| : {token, {'|', TokenLine}}. 34 | \< : {token, {'<', TokenLine}}. 35 | \> : {token, {'>', TokenLine}}. 36 | \' : {token, {'\'', TokenLine}}. 37 | , : {token, {',', TokenLine}}. 38 | \= : {token, {'=', TokenLine}}. 39 | {RANGE} : {token, {'..', TokenLine}}. 40 | {SCIENTIFIC_NOTATION} : {token, {int, TokenLine, TokenChars}}. 41 | {INT} : {token, {int, TokenLine, list_to_integer(TokenChars)}}. 42 | {ATOM} : {token, {atom_full, TokenLine, TokenChars}}. 43 | . : {token, {atom_part, TokenLine, TokenChars}}. 44 | 45 | Erlang code. 46 | -------------------------------------------------------------------------------- /src/parser.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals 2 | 3 | assignment 4 | atom 5 | binary binary_items binary_part 6 | byte 7 | byte_list byte_items 8 | contract 9 | document 10 | function 11 | integer 12 | list 13 | map map_items map_entry 14 | pattern 15 | pipe_list 16 | range 17 | rest 18 | tuple 19 | type 20 | value_items 21 | values value. 22 | 23 | Terminals 24 | 25 | atom_part atom_full 26 | int 27 | '(' ')' 28 | '[' ']' 29 | '_' 30 | '\'' 31 | ',' 32 | '#' '{' '}' 33 | ':=' '=>' 34 | 'fun(' '->' 35 | '|' 36 | '..' 37 | '::' 38 | ':' 39 | '...' 40 | '<' '>' 41 | '*' 42 | 'when' 43 | '='. 44 | 45 | Rootsymbol document. 46 | 47 | document -> values : '$1'. 48 | 49 | values -> value : ['$1']. 50 | values -> value values : ['$1'] ++ '$2'. 51 | 52 | value -> '\'' value '\'' : '$2'. 53 | value -> assignment : '$1'. 54 | value -> atom : {atom, '$1'}. 55 | value -> binary : '$1'. 56 | value -> byte_list : '$1'. 57 | value -> contract : '$1'. 58 | value -> function : '$1'. 59 | value -> integer : '$1'. 60 | value -> list : '$1'. 61 | value -> map : '$1'. 62 | value -> pattern : '$1'. 63 | value -> pipe_list : '$1'. 64 | value -> range : '$1'. 65 | value -> rest : '$1'. 66 | value -> tuple : '$1'. 67 | value -> type : '$1'. 68 | 69 | binary -> '<' '<' '>' '>' : {binary, []}. 70 | binary -> '<' '<' binary_items '>' '>' : {binary, '$3'}. 71 | binary -> '<' '<' value_items '>' '>' : {binary, '$3'}. 72 | 73 | pattern -> '<' value_items '>' : {pattern, '$2'}. 74 | 75 | tuple -> '{' '}' : {tuple, []}. 76 | tuple -> '{' value_items '}' : {tuple, '$2'}. 77 | 78 | byte_list -> '#' '{' '}' '#' : {byte_list, []}. 79 | byte_list -> '#' '{' byte_items '}' '#' : {byte_list, '$3'}. 80 | 81 | list -> '(' ')' : {list, paren, []}. 82 | list -> '(' value_items ')' : {list, paren, '$2'}. 83 | list -> '[' ']' : {list, square, []}. 84 | list -> '[' value_items ']' : {list, square, '$2'}. 85 | 86 | map -> '#' '{' '}' : {map, []}. 87 | map -> '#' '{' map_items '}' : {map, '$3'}. 88 | 89 | map_entry -> value ':=' value : {map_entry, '$1', '$3'}. 90 | map_entry -> value '=>' value : {map_entry, '$1', '$3'}. 91 | 92 | function -> 'fun(' ')' : {any_function}. 93 | function -> 'fun(' '...' ')' : {inner_any_function}. 94 | function -> 'fun(' contract ')' : {function, '$2'}. 95 | 96 | binary_part -> '_' ':' value : {binary_part, {any}, '$3'}. 97 | binary_part -> '_' ':' '_' '*' value : {binary_part, {any}, {any}, {size, '$5'}}. 98 | 99 | assignment -> value '=' value : {assignment, '$1', '$3'}. 100 | 101 | byte -> '#' '<' int '>' '(' int ',' int ',' atom ',' '[' atom ',' atom ']' ')' : unwrap('$3'). 102 | 103 | contract -> list '->' value when value_items : {contract, {args, '$1'}, {return, '$3'}, {whens, '$5'}}. 104 | contract -> list '->' value : {contract, {args, '$1'}, {return, '$3'}}. 105 | contract -> function '->' value : {contract, {args, '$1'}, {return, '$3'}}. 106 | 107 | integer -> int : {int, unwrap('$1')}. 108 | 109 | pipe_list -> value '|' value : {pipe_list, '$1', '$3'}. 110 | 111 | range -> integer '..' integer : {range, '$1', '$3'}. 112 | 113 | rest -> '...' : {rest}. 114 | 115 | atom -> atom_full : unwrap('$1'). 116 | atom -> atom_part : [unwrap('$1')]. 117 | atom -> '_' : ['_']. 118 | atom -> atom integer : '$1' ++ ['$2']. 119 | atom -> atom atom : '$1' ++ '$2'. 120 | 121 | type -> atom ':' type : {type, {atom, '$1'}, '$3'}. 122 | type -> atom '::' value : {named_type, {atom, '$1'}, '$3'}. 123 | type -> atom list : {type_list, '$1', '$2'}. 124 | 125 | binary_items -> binary_part : ['$1']. 126 | binary_items -> binary_part ',' binary_items : ['$1'] ++ '$3'. 127 | 128 | byte_items -> byte : ['$1']. 129 | byte_items -> byte ',' byte_items : ['$1'] ++ '$3'. 130 | 131 | map_items -> map_entry : ['$1']. 132 | map_items -> map_entry ',' map_items : ['$1'] ++ '$3'. 133 | 134 | value_items -> value : ['$1']. 135 | value_items -> value ',' value_items : ['$1'] ++ '$3'. 136 | 137 | Erlang code. 138 | 139 | unwrap({_,_,V}) -> V. 140 | -------------------------------------------------------------------------------- /test/literals_pretty_print_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Erlex.Test.LiteralsPretyPrintTest do 2 | use ExUnit.Case 3 | 4 | test "nil is parsed appropriately" do 5 | input = "nil" 6 | pretty_printed = Erlex.pretty_print(input) 7 | 8 | expected_output = "nil" 9 | assert pretty_printed == expected_output 10 | end 11 | 12 | test "true is parsed appropriately" do 13 | input = "true" 14 | pretty_printed = Erlex.pretty_print(input) 15 | 16 | expected_output = "true" 17 | assert pretty_printed == expected_output 18 | end 19 | 20 | test "false is parsed appropriately" do 21 | input = "false" 22 | pretty_printed = Erlex.pretty_print(input) 23 | 24 | expected_output = "false" 25 | assert pretty_printed == expected_output 26 | end 27 | 28 | test "empty square lists are pretty printed appropriately" do 29 | input = ~S""" 30 | [] 31 | """ 32 | 33 | pretty_printed = Erlex.pretty_print(input) 34 | 35 | expected_output = "[]" 36 | assert pretty_printed == expected_output 37 | end 38 | 39 | test "empty maps are pretty printed appropriately" do 40 | input = ~S""" 41 | #{} 42 | """ 43 | 44 | pretty_printed = Erlex.pretty_print(input) 45 | 46 | expected_output = "%{}" 47 | assert pretty_printed == expected_output 48 | end 49 | 50 | test "empty tuples are pretty printed appropriately" do 51 | input = ~S""" 52 | {} 53 | """ 54 | 55 | pretty_printed = Erlex.pretty_print(input) 56 | 57 | expected_output = "{}" 58 | assert pretty_printed == expected_output 59 | end 60 | 61 | test "maps are pretty printed appropriately" do 62 | input = ~S"#{'halted':='true'}" 63 | 64 | pretty_printed = Erlex.pretty_print(input) 65 | 66 | expected_output = "%{:halted => true}" 67 | assert pretty_printed == expected_output 68 | end 69 | 70 | test "structs are pretty printed appropriately" do 71 | input = ~S"#{'halted':='true', '__struct__':='Elixir.Plug.Conn'}" 72 | 73 | pretty_printed = Erlex.pretty_print(input) 74 | 75 | expected_output = "%Plug.Conn{:halted => true}" 76 | assert pretty_printed == expected_output 77 | end 78 | 79 | test "ranges are pretty printed appropriately" do 80 | input = "1..5" 81 | pretty_printed = Erlex.pretty_print(input) 82 | 83 | expected_output = "1..5" 84 | assert pretty_printed == expected_output 85 | end 86 | 87 | test "zero arg functions in contract are pretty printed appropriately" do 88 | input = "() -> atom()" 89 | pretty_printed = Erlex.pretty_print(input) 90 | 91 | expected_output = "() :: atom()" 92 | assert pretty_printed == expected_output 93 | end 94 | 95 | test "empty binary is pretty printed appropriately" do 96 | input = "<<>>" 97 | pretty_printed = Erlex.pretty_print(input) 98 | 99 | expected_output = "<<>>" 100 | assert pretty_printed == expected_output 101 | end 102 | 103 | test "sized binary is pretty printed appropriately" do 104 | input = "<<_:64>>" 105 | pretty_printed = Erlex.pretty_print(input) 106 | 107 | expected_output = "<<_ :: 64>>" 108 | assert pretty_printed == expected_output 109 | end 110 | 111 | test "unit binary is pretty printed appropriately" do 112 | input = "<<_:_*12>>" 113 | pretty_printed = Erlex.pretty_print(input) 114 | 115 | expected_output = "<<_ :: size(12)>>" 116 | assert pretty_printed == expected_output 117 | end 118 | 119 | test "sized list binary is pretty printed appropriately" do 120 | input = "<<_:64,_:_*8>>" 121 | pretty_printed = Erlex.pretty_print(input) 122 | 123 | expected_output = "<<_ :: 64, _ :: size(8)>>" 124 | assert pretty_printed == expected_output 125 | end 126 | 127 | test "binary as first value in pattern" do 128 | input = "<<<_:8,_:_*1>>,'false'>" 129 | 130 | pretty_printed = Erlex.pretty_print(input) 131 | 132 | assert pretty_printed == "<<_ :: 8, _ :: size(1)>>, false" 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /test/pretty_print_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Erlex.Test.PretyPrintTest do 2 | use ExUnit.Case 3 | 4 | test "simple atoms are pretty printed appropriately" do 5 | input = "'ok'" 6 | pretty_printed = Erlex.pretty_print(input) 7 | 8 | expected_output = ":ok" 9 | assert pretty_printed == expected_output 10 | end 11 | 12 | test "integers are pretty printed appropriately" do 13 | input = "1" 14 | pretty_printed = Erlex.pretty_print(input) 15 | 16 | expected_output = "1" 17 | assert pretty_printed == expected_output 18 | end 19 | 20 | test "module names are pretty printed appropriately" do 21 | input = "Elixir.Plug.Conn" 22 | pretty_printed = Erlex.pretty_print(input) 23 | 24 | expected_output = "Plug.Conn" 25 | assert pretty_printed == expected_output 26 | end 27 | 28 | test "module types are pretty printed appropriately" do 29 | input = "'Elixir.Plug.Conn':t()" 30 | pretty_printed = Erlex.pretty_print(input) 31 | 32 | expected_output = "Plug.Conn.t()" 33 | assert pretty_printed == expected_output 34 | end 35 | 36 | test "atom types are pretty printed appropriately" do 37 | input = "atom()" 38 | pretty_printed = Erlex.pretty_print(input) 39 | 40 | expected_output = "atom()" 41 | assert pretty_printed == expected_output 42 | end 43 | 44 | test "or'd simple types are pretty printed appropriately" do 45 | input = "binary() | integer()" 46 | pretty_printed = Erlex.pretty_print(input) 47 | 48 | expected_output = "binary() | integer()" 49 | assert pretty_printed == expected_output 50 | end 51 | 52 | test "or'd mixed types are pretty printed appropriately" do 53 | input = "'Elixir.Keyword':t() | map() | nil" 54 | pretty_printed = Erlex.pretty_print(input) 55 | 56 | expected_output = "Keyword.t() | map() | nil" 57 | assert pretty_printed == expected_output 58 | end 59 | 60 | test "or'd mixed types function signatures are pretty printed appropriately" do 61 | input = 62 | "('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map()) -> 'Elixir.Plug.Conn':t()" 63 | 64 | pretty_printed = Erlex.pretty_print(input) 65 | 66 | expected_output = "(Plug.Conn.t(), binary() | atom(), Keyword.t() | map()) :: Plug.Conn.t()" 67 | assert pretty_printed == expected_output 68 | end 69 | 70 | test "named values are pretty printed appropriately" do 71 | input = "data::'Elixir.MyApp.Data':t()" 72 | 73 | pretty_printed = Erlex.pretty_print(input) 74 | 75 | expected_output = "data :: MyApp.Data.t()" 76 | assert pretty_printed == expected_output 77 | end 78 | 79 | test "structs with any arrows are pretty printed appropriately" do 80 | input = ~S"#{'halted':='true', '__struct__':='Elixir.Plug.Conn', _ => _}" 81 | 82 | pretty_printed = Erlex.pretty_print(input) 83 | 84 | expected_output = "%Plug.Conn{:halted => true, _ => _}" 85 | assert pretty_printed == expected_output 86 | end 87 | 88 | test "one arg tuples are pretty printed appropriately" do 89 | input = "{'ok'}" 90 | pretty_printed = Erlex.pretty_print(input) 91 | 92 | expected_output = "{:ok}" 93 | assert pretty_printed == expected_output 94 | end 95 | 96 | test "three arg tuples are parsed appropriately" do 97 | input = "{'ok', 'error', 'ok'}" 98 | pretty_printed = Erlex.pretty_print(input) 99 | 100 | expected_output = "{:ok, :error, :ok}" 101 | assert pretty_printed == expected_output 102 | end 103 | 104 | test "erlang function calls are pretty printed appropriately" do 105 | input = 106 | "([supervisor:child_spec() | {module(),term()} | module()],[init_option()]) -> {'ok',tuple()}" 107 | 108 | pretty_printed = Erlex.pretty_print(input) 109 | 110 | expected_output = 111 | "([:supervisor.child_spec() | {module(), term()} | module()], [init_option()]) :: {:ok, tuple()}" 112 | 113 | assert pretty_printed == expected_output 114 | end 115 | 116 | test "patterns get pretty printed appropriately" do 117 | input = 'pattern {\'ok\', Vuser@1}' 118 | pretty_printed = Erlex.pretty_print_pattern(input) 119 | 120 | expected_output = "{:ok, user}" 121 | assert pretty_printed == expected_output 122 | end 123 | 124 | test "an assignment gets pretty printed appropriately" do 125 | input = ~S""" 126 | Vconn@1 = #{ 127 | 'params':=#{#{ 128 | #<105>(8, 1, 'integer', ['unsigned', 'big']), 129 | #<110>(8, 1, 'integer', ['unsigned', 'big']), 130 | #<99>(8, 1, 'integer', ['unsigned', 'big']), 131 | #<108>(8, 1, 'integer', ['unsigned', 'big']), 132 | #<117>(8, 1, 'integer', ['unsigned', 'big']), 133 | #<100>(8, 1, 'integer', ['unsigned', 'big']), 134 | #<101>(8, 1, 'integer', ['unsigned', 'big'])}# :='nil'}} 135 | """ 136 | 137 | pretty_printed = Erlex.pretty_print_pattern(input) 138 | 139 | expected_output = 140 | String.trim(""" 141 | conn = %{:params => %{"include" => nil}} 142 | """) 143 | 144 | assert pretty_printed == expected_output 145 | end 146 | 147 | test "zero arg functions are pretty printed appropriately" do 148 | input = "fun(() -> 1)" 149 | pretty_printed = Erlex.pretty_print(input) 150 | 151 | expected_output = "(() -> 1)" 152 | assert pretty_printed == expected_output 153 | end 154 | 155 | test "mixed number/atom atoms are pretty printed appropriately" do 156 | input = ~S"(#{'is_over_13?':=_}) -> 'ok'" 157 | pretty_printed = Erlex.pretty_print(input) 158 | 159 | expected_output = "(%{:is_over_13? => _}) :: :ok" 160 | assert pretty_printed == expected_output 161 | end 162 | 163 | test "tokenized characters are pretty printed appropriately" do 164 | input = ~S"'<' | '<=' | '>' | '>=' | 'fun('" 165 | pretty_printed = Erlex.pretty_print(input) 166 | 167 | expected_output = ":< | :<= | :> | :>= | :\"fun(\"" 168 | assert pretty_printed == expected_output 169 | end 170 | 171 | test "V# names are pretty printed appropriately" do 172 | input = ~S"'Elixir.Module.V1.Foo'" 173 | pretty_printed = Erlex.pretty_print(input) 174 | 175 | expected_output = "Module.V1.Foo" 176 | assert pretty_printed == expected_output 177 | end 178 | 179 | test "empty paren lists are pretty printed appropriately" do 180 | input = ~S""" 181 | () 182 | """ 183 | 184 | pretty_printed = Erlex.pretty_print(input) 185 | 186 | expected_output = "()" 187 | assert pretty_printed == expected_output 188 | end 189 | 190 | test "integers in maps are pretty printed appropriately" do 191 | input = ~S""" 192 | #{'source':={[any()] | 98971880 | map()}} 193 | """ 194 | 195 | pretty_printed = Erlex.pretty_print(input) 196 | 197 | expected_output = "%{:source => {[any()] | 98971880 | map()}}" 198 | assert pretty_printed == expected_output 199 | end 200 | 201 | test "any functions are pretty printed appropriately" do 202 | input = ~S""" 203 | fun() 204 | """ 205 | 206 | pretty_printed = Erlex.pretty_print(input) 207 | 208 | expected_output = "(... -> any)" 209 | assert pretty_printed == expected_output 210 | end 211 | 212 | test "inner types are printed appropriately" do 213 | input = ~S""" 214 | 'Elixir.MapSet':t('Elixir.MapSet':t(_)) 215 | """ 216 | 217 | pretty_printed = Erlex.pretty_print(input) 218 | 219 | expected_output = "MapSet.t(MapSet.t(_))" 220 | assert pretty_printed == expected_output 221 | end 222 | 223 | test "modules with numbers are pretty printed appropriately" do 224 | input = 'Elixir.Project.Resources.Components.V1.Actions' 225 | 226 | pretty_printed = Erlex.pretty_print(input) 227 | 228 | expected_output = "Project.Resources.Components.V1.Actions" 229 | assert pretty_printed == expected_output 230 | end 231 | 232 | test "semicolons are pretty printed appropriately" do 233 | input = ~S""" 234 | 'Elixir.Module.V1.Ac(tion.Hel(pers':foo_bar(any(),'nil') -> 'nil' 235 | ; ('Elixir.Ecto.Queryable':t(),'Elixir.String':t()) -> 'Elixir.String':t() 236 | """ 237 | 238 | pretty_printed = 239 | input 240 | |> to_charlist() 241 | |> Erlex.pretty_print_contract( 242 | "'Elixir.Module.V1.Ac(tion.Hel(pers'", 243 | "foo_bar" 244 | ) 245 | 246 | expected_output = 247 | "Contract head:\n(any(), nil) :: nil\n\nContract head:\n(Ecto.Queryable.t(), String.t()) :: String.t()" 248 | 249 | assert pretty_printed == expected_output 250 | end 251 | 252 | test "elixir SSA numbered variables get pretty printed appropriately" do 253 | input = '_money@1' 254 | 255 | pretty_printed = Erlex.pretty_print(input) 256 | 257 | expected_output = "_money" 258 | assert pretty_printed == expected_output 259 | end 260 | 261 | test "that patterns do not error when calling pretty_print_type/1 " do 262 | input = ~S""" 263 | <'few' | 'many' | 'one' | 'other' | 'two' | {'error',{'Elixir.Cldr.InvalidLanguageTag',<<_:64,_:_*8>>} | {'Elixir.Cldr.UnknownPluralRules',<<_:64,_:_*8>>}},non_neg_integer()> 264 | """ 265 | 266 | assert Erlex.pretty_print_type(input) 267 | end 268 | 269 | test "named SSA variables with @s get pretty printed appropriately" do 270 | input = ~S""" 271 | (_number@1::#{'__struct__':='Elixir.Decimal', 'sign':=_, _=>_}) 272 | """ 273 | 274 | pretty_printed = Erlex.pretty_print(input) 275 | 276 | expected_output = "(_number :: %Decimal{:sign => _, _ => _})" 277 | assert pretty_printed == expected_output 278 | end 279 | 280 | test "multiple SSA variables are pretty printed appropriately" do 281 | input = ~S""" 282 | ('start',Vcompile@1::map(),Vruntime@1::map()) 283 | """ 284 | 285 | pretty_printed = Erlex.pretty_print(input) 286 | 287 | expected_output = "(:start, compile :: map(), runtime :: map())" 288 | assert pretty_printed == expected_output 289 | end 290 | 291 | test "named parts in specs are pretty printed appropriately" do 292 | input = ~S""" 293 | Vcompile@1::map() 294 | """ 295 | 296 | pretty_printed = Erlex.pretty_print(input) 297 | 298 | expected_output = "compile :: map()" 299 | assert pretty_printed == expected_output 300 | end 301 | 302 | test "named atoms in specs are pretty printed appropriately" do 303 | input = ~S""" 304 | (Vrules_page_html@1::'nil') 305 | """ 306 | 307 | pretty_printed = Erlex.pretty_print(input) 308 | 309 | expected_output = "(rules_page_html :: nil)" 310 | assert pretty_printed == expected_output 311 | end 312 | 313 | test "tuple assigns are pretty printed appropriately" do 314 | input = ~S""" 315 | (Vres@1::{'error',_}) 316 | """ 317 | 318 | pretty_printed = Erlex.pretty_print(input) 319 | 320 | expected_output = "(res :: {:error, _})" 321 | assert pretty_printed == expected_output 322 | end 323 | 324 | test "binary and list assigns are pretty printed appropriately" do 325 | input = ~S""" 326 | (Vurl@1::<<_:272>>,Vreal_payload@1::{'multipart',[any()]},Vheaders@1::[{[45 | 65 | 85 | 101 | 103 | 110 | 114 | 115 | 116,...],[1..255,...]},...],[{'follow_redirect','true'},...]) 327 | """ 328 | 329 | pretty_printed = Erlex.pretty_print(input) 330 | 331 | expected_output = 332 | "(url :: <<_ :: 272>>, real_payload :: {:multipart, [any()]}, headers :: [{[45 | 65 | 85 | 101 | 103 | 110 | 114 | 115 | 116, ...], [1..255, ...]}, ...], [{:follow_redirect, true}, ...])" 333 | 334 | assert pretty_printed == expected_output 335 | end 336 | 337 | test "when clauses in contracts pretty print appropriately" do 338 | input = ~S""" 339 | (Atom,Encoding) -> binary() when Atom :: atom(), Encoding :: 'latin1' | 'utf8' 340 | """ 341 | 342 | pretty_printed = Erlex.pretty_print(input) 343 | 344 | expected_output = "(atom, encoding) :: binary() when atom: atom(), encoding: :latin1 | :utf8" 345 | 346 | assert pretty_printed == expected_output 347 | end 348 | 349 | test "binary transformation layer pretty prints appropriately" do 350 | input = ~S""" 351 | <<_:_*8>> 352 | """ 353 | 354 | pretty_printed = Erlex.pretty_print(input) 355 | 356 | expected_output = "binary()" 357 | assert pretty_printed == expected_output 358 | end 359 | 360 | test "bitstring transformation layer pretty prints appropriately" do 361 | input = ~S""" 362 | <<_:_*1>> 363 | """ 364 | 365 | pretty_printed = Erlex.pretty_print(input) 366 | 367 | expected_output = "bitstring()" 368 | assert pretty_printed == expected_output 369 | end 370 | 371 | test "boolean transformation layer pretty prints appropriately" do 372 | input = ~S""" 373 | false | true 374 | """ 375 | 376 | pretty_printed = Erlex.pretty_print(input) 377 | 378 | expected_output = "boolean()" 379 | assert pretty_printed == expected_output 380 | end 381 | 382 | test "struct transformation layer pretty prints appropriately" do 383 | input = ~S""" 384 | #{'__struct__':=_, _=>_} 385 | """ 386 | 387 | pretty_printed = Erlex.pretty_print(input) 388 | 389 | expected_output = "struct()" 390 | assert pretty_printed == expected_output 391 | end 392 | 393 | test "struct atom transformation layer pretty prints appropriately" do 394 | input = ~S""" 395 | #{'__struct__':=atom(), _=>_} 396 | """ 397 | 398 | pretty_printed = Erlex.pretty_print(input) 399 | 400 | expected_output = "struct()" 401 | assert pretty_printed == expected_output 402 | end 403 | 404 | test "struct atom filtering blanks prints appropriately" do 405 | input = ~S""" 406 | #{'__struct__':='Elixir.SomeStruct', _=>_} 407 | """ 408 | 409 | pretty_printed = Erlex.pretty_print(input) 410 | 411 | expected_output = "%SomeStruct{}" 412 | assert pretty_printed == expected_output 413 | end 414 | 415 | test "struct with atom key transformation layer pretty prints appropriately" do 416 | input = ~S""" 417 | #{'__struct__':=atom(), atom()=>_} 418 | """ 419 | 420 | pretty_printed = Erlex.pretty_print(input) 421 | 422 | expected_output = "struct()" 423 | assert pretty_printed == expected_output 424 | end 425 | 426 | test "whens in contracts pretty prints appropriately" do 427 | input = ~S""" 428 | (one,any(),'Elixir.Keyword':t()) -> 'ok' when one :: key() 429 | """ 430 | 431 | pretty_printed = Erlex.pretty_print_contract(input) 432 | 433 | expected_output = "(one, any(), Keyword.t()) :: :ok when one: key()" 434 | assert pretty_printed == expected_output 435 | end 436 | 437 | test "underscore in when type variable prints appropriately" do 438 | input = ~S""" 439 | (This_one, any()) -> 'ok' when This_one :: key() 440 | """ 441 | 442 | pretty_printed = Erlex.pretty_print_contract(input) 443 | 444 | expected_output = "(this_one, any()) :: :ok when this_one: key()" 445 | assert pretty_printed == expected_output 446 | end 447 | 448 | test "mixed case in when type variable prints appropriately" do 449 | input = ~S""" 450 | (ThisOne, any()) -> 'ok' when ThisOne :: key() 451 | """ 452 | 453 | pretty_printed = Erlex.pretty_print_contract(input) 454 | 455 | expected_output = "(thisOne, any()) :: :ok when thisOne: key()" 456 | assert pretty_printed == expected_output 457 | end 458 | 459 | test "whens with single letter type variables prints appropriately" do 460 | input = ~S""" 461 | () -> a when a :: atom() 462 | """ 463 | 464 | pretty_printed = assert Erlex.pretty_print_contract(input) 465 | 466 | expected_output = "() :: a when a: atom()" 467 | assert pretty_printed == expected_output 468 | end 469 | 470 | test "type variable and atom argument types pretty print appropriately" do 471 | input = ~S""" 472 | (km, s, 'fnord') -> km when km :: term(), s :: atom() 473 | """ 474 | 475 | pretty_printed = assert Erlex.pretty_print_contract(input) 476 | 477 | expected_output = "(km, s, :fnord) :: km when km: term(), s: atom()" 478 | assert pretty_printed == expected_output 479 | end 480 | 481 | test "timeout transformation layer pretty prints appropriately" do 482 | input = ~S""" 483 | 'infinity' | non_neg_integer() 484 | """ 485 | 486 | pretty_printed = Erlex.pretty_print(input) 487 | 488 | expected_output = "timeout()" 489 | assert pretty_printed == expected_output 490 | end 491 | 492 | test "Keyword.t() transformation layer pretty prints appropriately" do 493 | input = ~S""" 494 | [{atom(), _}] 495 | """ 496 | 497 | pretty_printed = Erlex.pretty_print(input) 498 | 499 | expected_output = "Keyword.t()" 500 | assert pretty_printed == expected_output 501 | end 502 | 503 | test "Keyword.t(integer()) transformation layer pretty prints appropriately" do 504 | input = ~S""" 505 | [{atom(), integer()}] 506 | """ 507 | 508 | pretty_printed = Erlex.pretty_print(input) 509 | 510 | expected_output = "Keyword.t(integer())" 511 | assert pretty_printed == expected_output 512 | end 513 | 514 | test "contracts with semicolons are pretty printed appropriately" do 515 | input = ~S""" 516 | ('nil','Elixir.Dnsimple.Events.HostCreateRequested':t()) -> {'ok',{'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()}} ; ({'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()},'Elixir.Dnsimple.Events.HostCreateSucceeded':t()) -> {'ok',{'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()}} 517 | """ 518 | 519 | pretty_printed = Erlex.pretty_print_contract(input) 520 | 521 | expected_output = 522 | "Contract head:\n(nil, Dnsimple.Events.HostCreateRequested.t()) ::\n {:ok, {Dnsimple.Models.Host.t(), Dnsimple.Models.Order.t()}}\n\nContract head:\n(\n {Dnsimple.Models.Host.t(), Dnsimple.Models.Order.t()},\n Dnsimple.Events.HostCreateSucceeded.t()\n) :: {:ok, {Dnsimple.Models.Host.t(), Dnsimple.Models.Order.t()}}" 523 | 524 | assert pretty_printed == expected_output 525 | end 526 | 527 | test "numbered named types are pretty printed appropriately" do 528 | input = ~S""" 529 | __@1::<<_:1>> 530 | """ 531 | 532 | pretty_printed = Erlex.pretty_print_args(input) 533 | 534 | expected_output = "_ :: <<_::1>>" 535 | assert pretty_printed == expected_output 536 | end 537 | 538 | test "exceptions are pretty printed appropriately" do 539 | input = ~S""" 540 | (__@5::#{'__exception__':='true', '__struct__':=_, _=>_}) 541 | """ 542 | 543 | pretty_printed = Erlex.pretty_print_contract(input) 544 | 545 | expected_output = "(_ :: Exception.t())" 546 | assert pretty_printed == expected_output 547 | end 548 | 549 | test "multiple _ variables are pretty printed appropriately" do 550 | input = ~S""" 551 | (_@1::any(),_@2::any(),[]) 552 | """ 553 | 554 | pretty_printed = Erlex.pretty_print_contract(input) 555 | 556 | expected_output = "(_ :: any(), _ :: any(), [])" 557 | assert pretty_printed == expected_output 558 | end 559 | 560 | test "scientific notation numbers are pretty printed appropriately" do 561 | input = ~S""" 562 | (1.0e-3) 563 | """ 564 | 565 | pretty_printed = Erlex.pretty_print_contract(input) 566 | 567 | expected_output = "(1.0e-3)" 568 | assert pretty_printed == expected_output 569 | end 570 | 571 | test "ellipsed inner functions are pretty printed appropriately" do 572 | input = ~S""" 573 | (graph::'Elixir.Scenic.Graph':t(),id::any(),action::fun(...) -> 'Elixir.Scenic.Primitive':t()) -> 'Elixir.Scenic.Graph':t() 574 | """ 575 | 576 | pretty_printed = Erlex.pretty_print_contract(input) 577 | 578 | expected_output = 579 | "(graph :: Scenic.Graph.t(), id :: any(), action :: (... -> Scenic.Primitive.t())) ::\n Scenic.Graph.t()" 580 | 581 | assert pretty_printed == expected_output 582 | end 583 | 584 | test "binaries are pretty printed appropriately" do 585 | input = ~S""" 586 | (<<114,111,108,101,115,95,117,115,101,114,115>>) 587 | """ 588 | 589 | pretty_printed = Erlex.pretty_print(input) 590 | 591 | assert pretty_printed == "(<<114, 111, 108, 101, 115, 95, 117, 115, 101, 114, 115>>)" 592 | end 593 | end 594 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------