erlex.ex (15622B)
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 pretty = 139 Enum.map_join([head | tail], "\n\n" <> joiner, fn contract -> 140 contract 141 |> to_charlist() 142 |> do_pretty_print_contract() 143 end) 144 145 joiner <> pretty 146 end 147 end 148 149 defp do_pretty_print_contract(contract) do 150 prefix = "@spec a" 151 suffix = "\ndef a() do\n :ok\nend" 152 pretty = pretty_print(contract) 153 154 """ 155 @spec a#{pretty} 156 def a() do 157 :ok 158 end 159 """ 160 |> format() 161 |> Enum.join("") 162 |> String.trim_leading(prefix) 163 |> String.trim_trailing(suffix) 164 |> String.replace("\n ", "\n") 165 end 166 167 @spec pretty_print_type(type :: String.t()) :: String.t() 168 def pretty_print_type(type) do 169 prefix = "@spec a(" 170 suffix = ") :: :ok\ndef a() do\n :ok\nend" 171 indented_suffix = ") ::\n :ok\ndef a() do\n :ok\nend" 172 pretty = pretty_print(type) 173 174 """ 175 @spec a(#{pretty}) :: :ok 176 def a() do 177 :ok 178 end 179 """ 180 |> format() 181 |> Enum.join("") 182 |> String.trim_leading(prefix) 183 |> String.trim_trailing(suffix) 184 |> String.trim_trailing(indented_suffix) 185 |> String.replace("\n ", "\n") 186 end 187 188 @spec pretty_print_args(args :: String.t()) :: String.t() 189 def pretty_print_args(args) do 190 prefix = "@spec a" 191 suffix = " :: :ok\ndef a() do\n :ok\nend" 192 pretty = pretty_print(args) 193 194 """ 195 @spec a#{pretty} :: :ok 196 def a() do 197 :ok 198 end 199 """ 200 |> format() 201 |> Enum.join("") 202 |> String.trim_leading(prefix) 203 |> String.trim_trailing(suffix) 204 |> String.replace("\n ", "\n") 205 end 206 207 defp do_pretty_print({:any}) do 208 "_" 209 end 210 211 defp do_pretty_print({:inner_any_function}) do 212 "(...)" 213 end 214 215 defp do_pretty_print({:any_function}) do 216 "(... -> any)" 217 end 218 219 defp do_pretty_print({:assignment, {:atom, atom}, value}) do 220 name = 221 atom 222 |> deatomize() 223 |> to_string() 224 |> strip_var_version() 225 226 "#{name} = #{do_pretty_print(value)}" 227 end 228 229 defp do_pretty_print({:atom, [:_]}) do 230 "_" 231 end 232 233 defp do_pretty_print({:atom, ['_']}) do 234 "_" 235 end 236 237 defp do_pretty_print({:atom, atom}) do 238 atomize(atom) 239 end 240 241 defp do_pretty_print({:binary_part, value, _, size}) do 242 "#{do_pretty_print(value)} :: #{do_pretty_print(size)}" 243 end 244 245 defp do_pretty_print({:binary_part, value, size}) do 246 "#{do_pretty_print(value)} :: #{do_pretty_print(size)}" 247 end 248 249 defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 8}}}]}) do 250 "binary()" 251 end 252 253 defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 1}}}]}) do 254 "bitstring()" 255 end 256 257 defp do_pretty_print({:binary, binary_parts}) do 258 binary_parts = Enum.map_join(binary_parts, ", ", &do_pretty_print/1) 259 "<<#{binary_parts}>>" 260 end 261 262 defp do_pretty_print({:binary, value, size}) do 263 "<<#{do_pretty_print(value)} :: #{do_pretty_print(size)}>>" 264 end 265 266 defp do_pretty_print({:byte_list, byte_list}) do 267 byte_list 268 |> Enum.into(<<>>, fn byte -> 269 <<byte::8>> 270 end) 271 |> inspect() 272 end 273 274 defp do_pretty_print({:contract, {:args, args}, {:return, return}, {:whens, whens}}) do 275 {printed_whens, when_names} = collect_and_print_whens(whens) 276 277 args = {:when_names, when_names, args} 278 return = {:when_names, when_names, return} 279 280 "(#{do_pretty_print(args)}) :: #{do_pretty_print(return)} when #{printed_whens}" 281 end 282 283 defp do_pretty_print({:contract, {:args, {:inner_any_function}}, {:return, return}}) do 284 "((...) -> #{do_pretty_print(return)})" 285 end 286 287 defp do_pretty_print({:contract, {:args, args}, {:return, return}}) do 288 "#{do_pretty_print(args)} :: #{do_pretty_print(return)}" 289 end 290 291 defp do_pretty_print({:function, {:contract, {:args, args}, {:return, return}}}) do 292 "(#{do_pretty_print(args)} -> #{do_pretty_print(return)})" 293 end 294 295 defp do_pretty_print({:int, int}) do 296 "#{to_string(int)}" 297 end 298 299 defp do_pretty_print({:list, :paren, items}) do 300 "(#{Enum.map_join(items, ", ", &do_pretty_print/1)})" 301 end 302 303 defp do_pretty_print( 304 {:list, :square, 305 [ 306 tuple: [ 307 {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, 308 {:atom, [:_]} 309 ] 310 ]} 311 ) do 312 "Keyword.t()" 313 end 314 315 defp do_pretty_print( 316 {:list, :square, 317 [ 318 tuple: [ 319 {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, 320 t 321 ] 322 ]} 323 ) do 324 "Keyword.t(#{do_pretty_print(t)})" 325 end 326 327 defp do_pretty_print({:list, :square, items}) do 328 "[#{Enum.map_join(items, ", ", &do_pretty_print/1)}]" 329 end 330 331 defp do_pretty_print({:map_entry, key, value}) do 332 "#{do_pretty_print(key)} => #{do_pretty_print(value)}" 333 end 334 335 defp do_pretty_print( 336 {:map, 337 [ 338 {:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}}, 339 {:map_entry, {:atom, [:_]}, {:atom, [:_]}} 340 ]} 341 ) do 342 "struct()" 343 end 344 345 defp do_pretty_print( 346 {:map, 347 [ 348 {:map_entry, {:atom, '\'__struct__\''}, 349 {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}}, 350 {:map_entry, {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, {:atom, [:_]}} 351 ]} 352 ) do 353 "struct()" 354 end 355 356 defp do_pretty_print( 357 {:map, 358 [ 359 {:map_entry, {:atom, '\'__struct__\''}, 360 {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}}, 361 {:map_entry, {:atom, [:_]}, {:atom, [:_]}} 362 ]} 363 ) do 364 "struct()" 365 end 366 367 defp do_pretty_print( 368 {:map, 369 [ 370 {:map_entry, {:atom, '\'__exception__\''}, {:atom, '\'true\''}}, 371 {:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}}, 372 {:map_entry, {:atom, [:_]}, {:atom, [:_]}} 373 ]} 374 ) do 375 "Exception.t()" 376 end 377 378 defp do_pretty_print({:map, map_keys}) do 379 %{struct_name: struct_name, entries: entries} = struct_parts(map_keys) 380 381 if struct_name do 382 "%#{struct_name}{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}" 383 else 384 "%{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}" 385 end 386 end 387 388 defp do_pretty_print({:named_type_with_appended_colon, named_type, type}) 389 when is_tuple(named_type) and is_tuple(type) do 390 case named_type do 391 {:atom, name} -> 392 name = 393 name 394 |> deatomize() 395 |> to_string() 396 |> strip_var_version() 397 398 "#{name}: #{do_pretty_print(type)}" 399 400 other -> 401 "#{do_pretty_print(other)}: #{do_pretty_print(type)}" 402 end 403 end 404 405 defp do_pretty_print({:named_type, named_type, type}) 406 when is_tuple(named_type) and is_tuple(type) do 407 case named_type do 408 {:atom, name} -> 409 name = 410 name 411 |> deatomize() 412 |> to_string() 413 |> strip_var_version() 414 415 "#{name} :: #{do_pretty_print(type)}" 416 417 other -> 418 "#{do_pretty_print(other)} :: #{do_pretty_print(type)}" 419 end 420 end 421 422 defp do_pretty_print({:named_type, named_type, type}) when is_tuple(named_type) do 423 case named_type do 424 {:atom, name = '\'Elixir' ++ _} -> 425 "#{atomize(name)}.#{deatomize(type)}()" 426 427 {:atom, name} -> 428 name = 429 name 430 |> deatomize() 431 |> to_string() 432 |> strip_var_version() 433 434 "#{name} :: #{deatomize(type)}()" 435 436 other -> 437 name = do_pretty_print(other) 438 "#{name} :: #{deatomize(type)}()" 439 end 440 end 441 442 defp do_pretty_print({nil}) do 443 "nil" 444 end 445 446 defp do_pretty_print({:pattern, pattern_items}) do 447 "#{Enum.map_join(pattern_items, ", ", &do_pretty_print/1)}" 448 end 449 450 defp do_pretty_print( 451 {:pipe_list, {:atom, ['f', 'a', 'l', 's', 'e']}, {:atom, ['t', 'r', 'u', 'e']}} 452 ) do 453 "boolean()" 454 end 455 456 defp do_pretty_print( 457 {:pipe_list, {:atom, '\'infinity\''}, 458 {:type_list, ['n', 'o', 'n', :_, 'n', 'e', 'g', :_, 'i', 'n', 't', 'e', 'g', 'e', 'r'], 459 {:list, :paren, []}}} 460 ) do 461 "timeout()" 462 end 463 464 defp do_pretty_print({:pipe_list, head, tail}) do 465 "#{do_pretty_print(head)} | #{do_pretty_print(tail)}" 466 end 467 468 defp do_pretty_print({:range, from, to}) do 469 "#{do_pretty_print(from)}..#{do_pretty_print(to)}" 470 end 471 472 defp do_pretty_print({:rest}) do 473 "..." 474 end 475 476 defp do_pretty_print({:size, size}) do 477 "size(#{do_pretty_print(size)})" 478 end 479 480 defp do_pretty_print({:tuple, tuple_items}) do 481 "{#{Enum.map_join(tuple_items, ", ", &do_pretty_print/1)}}" 482 end 483 484 defp do_pretty_print({:type, type}) do 485 "#{deatomize(type)}()" 486 end 487 488 defp do_pretty_print({:type, module, type}) do 489 module = do_pretty_print(module) 490 491 type = 492 if is_tuple(type) do 493 do_pretty_print(type) 494 else 495 deatomize(type) <> "()" 496 end 497 498 "#{module}.#{type}" 499 end 500 501 defp do_pretty_print({:type, module, type, inner_type}) do 502 "#{atomize(module)}.#{deatomize(type)}(#{do_pretty_print(inner_type)})" 503 end 504 505 defp do_pretty_print({:type_list, type, types}) do 506 "#{deatomize(type)}#{do_pretty_print(types)}" 507 end 508 509 defp do_pretty_print({:when_names, when_names, {:list, :paren, items}}) do 510 Enum.map_join(items, ", ", &format_when_names(do_pretty_print(&1), when_names)) 511 end 512 513 defp do_pretty_print({:when_names, when_names, item}) do 514 format_when_names(do_pretty_print(item), when_names) 515 end 516 517 defp format_when_names(item, when_names) do 518 trimmed = String.trim_leading(item, ":") 519 520 if trimmed in when_names do 521 downcase_first(trimmed) 522 else 523 item 524 end 525 end 526 527 defp collect_and_print_whens(whens) do 528 {pretty_names, when_names} = 529 Enum.reduce(whens, {[], []}, fn {_, when_name, type}, {prettys, whens} -> 530 pretty_name = 531 {:named_type_with_appended_colon, when_name, type} 532 |> do_pretty_print() 533 |> downcase_first() 534 535 {[pretty_name | prettys], [when_name | whens]} 536 end) 537 538 when_names = 539 when_names 540 |> Enum.map(fn {_, v} -> v |> atomize() |> String.trim_leading(":") end) 541 542 printed_whens = pretty_names |> Enum.reverse() |> Enum.join(", ") 543 544 {printed_whens, when_names} 545 end 546 547 defp downcase_first(string) do 548 {first, rest} = String.split_at(string, 1) 549 String.downcase(first) <> rest 550 end 551 552 defp atomize("Elixir." <> module_name) do 553 String.trim(module_name, "'") 554 end 555 556 defp atomize([char]) do 557 to_string(char) 558 end 559 560 defp atomize(atom) when is_list(atom) do 561 atom_string = 562 atom 563 |> deatomize() 564 |> to_string() 565 566 stripped = strip_var_version(atom_string) 567 568 if stripped == atom_string do 569 atomize(stripped) 570 else 571 stripped 572 end 573 end 574 575 defp atomize(<<atom>>) when is_number(atom) do 576 "#{atom}" 577 end 578 579 defp atomize(atom) do 580 atom = to_string(atom) 581 582 if String.starts_with?(atom, "_") do 583 atom 584 else 585 inspect(:"#{String.trim(atom, "'")}") 586 end 587 end 588 589 defp atom_part_to_string({:int, atom_part}), do: Integer.to_charlist(atom_part) 590 defp atom_part_to_string(atom_part), do: atom_part 591 592 defp strip_var_version(var_name) do 593 var_name 594 |> String.replace(~r/^V(.+)@\d+$/, "\\1") 595 |> String.replace(~r/^(.+)@\d+$/, "\\1") 596 end 597 598 defp struct_parts(map_keys) do 599 %{struct_name: struct_name, entries: entries} = 600 Enum.reduce(map_keys, %{struct_name: nil, entries: []}, &struct_part/2) 601 602 %{struct_name: struct_name, entries: Enum.reverse(entries)} 603 end 604 605 defp struct_part({:map_entry, {:atom, '\'__struct__\''}, {:atom, struct_name}}, struct_parts) do 606 struct_name = 607 struct_name 608 |> atomize() 609 |> String.trim("\"") 610 611 Map.put(struct_parts, :struct_name, struct_name) 612 end 613 614 defp struct_part(entry, struct_parts = %{entries: entries}) do 615 Map.put(struct_parts, :entries, [entry | entries]) 616 end 617 618 defp deatomize([:_, :_, '@', {:int, _}]) do 619 "_" 620 end 621 622 defp deatomize(chars) when is_list(chars) do 623 Enum.map(chars, fn char -> 624 char 625 |> deatomize_char() 626 |> atom_part_to_string() 627 end) 628 end 629 630 defp deatomize_char(char) when is_atom(char) do 631 Atom.to_string(char) 632 end 633 634 defp deatomize_char(char), do: char 635 end