├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── gradualixir.ex └── mix │ └── tasks │ ├── compile.gradualixir.ex │ └── gradualizer.ex ├── mix.exs ├── mix.lock ├── priv └── prelude │ └── list.specs.erl └── test ├── gradualixir_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | gradualixir-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradualixir 2 | 3 | Mix integration with [Gradualizer](https://github.com/josefs/Gradualizer). 4 | 5 | Gradualizer is an enforced static typing checker based on BEAM specs. 6 | 7 | ## Installation 8 | 9 | The package can currently be installed from github by adding `gradualixir` 10 | to your list of dependencies in `mix.exs`: 11 | 12 | ```elixir 13 | def deps do 14 | [ 15 | {:gradualixir, github: "overminddl1/gradualixir", ref: "master"} 16 | ] 17 | end 18 | ``` 19 | 20 | ## Usage 21 | 22 | Once added to a mix project dependencies and `mix deps.get` is run to acquire the dependencies, then just run the `mix gradualizer` command to check the files on the existing project, such as: 23 | 24 | ```zsh 25 | ╰─➤ mix gradualizer 26 | /home/overminddl1/elixir/gradualixir/_build/dev/lib/gradualixir/ebin/Elixir.Gradualixir.beam: The binary [{bin_element,0,{string,0,"*.beam"},default,default}] on line 0 does not have type t() 27 | /home/overminddl1/elixir/gradualixir/_build/dev/lib/gradualixir/ebin/Elixir.Mix.Tasks.Gradualizer.beam: The binary [{bin_element,0,{string,0,"*.beam"},default,default}] on line 0 does not have type t() 28 | ``` 29 | 30 | Currently Gradualizer is in early development and it's only output is currently just to `stdout` so the syntax is currently in Erlang format. Hope to parse it out in short order however. 31 | 32 | ### Options 33 | 34 | The currently supported options are: 35 | 36 | * `--gradualize-preload` will preload all beam files known to the erlang runtime and all known paths to the gradualizer db process, otherwise it looks to load them on-demand. 37 | * `--no-compile` will not compile the project before running gradualizer. 38 | * `--quiet` will silence all shell output, will still set the return code to the shell however. 39 | 40 | In addition it takes 0 or more position rest arguments: 41 | 42 | * 0 arguments -> Gradualize just the current project's BEAM files but no consolidated files, this is the same as passing in `:project-ebin`. 43 | * A single `:all` argument will gradualize all BEAM files **everywhere** known to the system, do note that a lot of erlang and elixir specs may not be 'good' for such a purpose, but it's great for testing and reporting and fixing bugs to them! 44 | * A single `:project-ebin` argument will gradualize the non-consolidated BEAM files of the current project. This is the default value if no arguments. 45 | * A single `:project` argument will gradualize all BEAM files of the current project, including consolidated files (which Elixir does not generate very cleanly so expect errors in those). 46 | * A single `:deps` argument will gradualize all BEAM files of the current project and all dependencies of the current project, essentially everything but the OTP and Elixir itself. 47 | * Or 1 or more arguments of the paths to specific BEAM files to gradualize. 48 | 49 | ## Name? 50 | 51 | Gradualixir is to Gradualizer as Dialyxir is to Dialyzer, and I hope Gradualizer will get a usable interface on par to that of Dialyzer for tool use in time. :-) 52 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :gradualixir, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:gradualixir, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/gradualixir.ex: -------------------------------------------------------------------------------- 1 | defmodule Gradualixir do 2 | @moduledoc """ 3 | Documentation for Gradualixir. 4 | """ 5 | 6 | @def_opts [print_file: true, specs_override: 'priv/prelude'] 7 | 8 | @doc """ 9 | Examples speak for themselves 10 | 11 | ## Examples 12 | 13 | ### Gradualizing a beam file 14 | 15 | iex> beamfile = "_build/test/lib/gradualixir/ebin/Elixir.Gradualixir.beam" 16 | iex> Gradualixir.gradualize(beamfile) 17 | :ok 18 | 19 | ### Gradualizing many beam files, with format option 20 | 21 | iex> beamfiles = Path.wildcard("_build/test/lib/gradualixir/ebin/*.beam") 22 | iex> Gradualixir.gradualize(beamfiles, fmt_location: :brief) 23 | :ok 24 | 25 | """ 26 | @spec gradualize(binary() | list(charlist()) | list(binary())) :: 27 | :ok | :error | list(error :: any()) 28 | def gradualize(files, opts \\ []) 29 | 30 | def gradualize([_ | _] = files, opts) do 31 | if opts[:preload] do 32 | :gradualizer_db.import_beam_files(get_beam_files(opts)) 33 | end 34 | 35 | files 36 | |> Enum.reduce(:ok, fn file, result -> 37 | file 38 | |> to_charlist() 39 | |> safe_type_check_file(opts) 40 | |> merge_results(result) 41 | end) 42 | end 43 | 44 | def gradualize(file, opts) when is_binary(file) do 45 | gradualize([file], opts) 46 | end 47 | 48 | def gradualize(:all, opts) do 49 | get_beam_files(opts) 50 | |> gradualize(opts) 51 | end 52 | 53 | def get_beam_files(opts) do 54 | opts 55 | |> get_base_paths() 56 | |> Enum.flat_map(fn path -> 57 | path 58 | |> Path.join("*.beam") 59 | |> Path.wildcard() 60 | end) 61 | end 62 | 63 | defp get_base_paths(opts) do 64 | case opts[:ebin_root] do 65 | nil -> :code.get_path() 66 | paths -> List.wrap(paths) 67 | end 68 | end 69 | 70 | defp safe_type_check_file(file, opts) do 71 | :gradualizer.type_check_file(file, @def_opts ++ opts) 72 | else 73 | :ok -> :ok 74 | :nok -> :error 75 | [] -> :ok 76 | [_ | _] = errors -> {:error, errors} 77 | rescue 78 | e in _ -> 79 | {e, stack} = Exception.blame(:error, e, __STACKTRACE__) 80 | 81 | IO.puts(""" 82 | 83 | ********************************* 84 | Report this error to Gradualizer: 85 | 86 | #{Exception.format(:error, e, stack)} 87 | 88 | """) 89 | 90 | :error 91 | end 92 | 93 | defp merge_results(:ok, acc), do: acc 94 | defp merge_results(:error, _acc), do: :error 95 | defp merge_results({:error, errors}, :ok), do: errors 96 | defp merge_results({:error, errors}, acc), do: acc ++ errors 97 | end 98 | -------------------------------------------------------------------------------- /lib/mix/tasks/compile.gradualixir.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Gradualixir do 2 | use Mix.Task 3 | 4 | @spec run(OptionParser.argv()) :: :ok 5 | def run(args) do 6 | {opts, _, _} = 7 | OptionParser.parse(args, 8 | switches: [ 9 | verbose: :boolean 10 | ] 11 | ) 12 | 13 | verbose = opts[:verbose] 14 | 15 | if(verbose, do: IO.puts("Gradualizing project...")) 16 | if(verbose, do: IO.puts("Gradualizing complete")) 17 | 18 | :ok 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mix/tasks/gradualizer.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Gradualizer do 2 | @shortdoc "Runs Gradualizer with default or project defined flags" 3 | 4 | @moduledoc """ 5 | """ 6 | 7 | use Mix.Task 8 | 9 | @command_options [ 10 | gradualize_preload: :boolean, 11 | no_compile: :boolean, 12 | quiet: :boolean, 13 | verbose: :boolean 14 | ] 15 | 16 | @spec run(OptionParser.argv()) :: :ok | :error 17 | def run(args) do 18 | {opts, gargs, _ignored_flags} = OptionParser.parse(args, strict: @command_options) 19 | original_shell = Mix.shell() 20 | if opts[:quiet], do: Mix.shell(Mix.Shell.Quiet) 21 | opts = Keyword.delete(opts, :quiet) 22 | 23 | result = 24 | if Mix.Project.get() do 25 | unless opts[:no_compile], do: Mix.Project.compile([]) 26 | run_gradualizer(opts, gargs) 27 | else 28 | IO.puts("No mix project found") 29 | :error 30 | end 31 | 32 | Mix.shell(original_shell) 33 | result 34 | end 35 | 36 | defp get_beams(type) 37 | defp get_beams([":all"]), do: :all 38 | defp get_beams([":project-ebin"]), do: get_beams([]) 39 | 40 | defp get_beams([]) do 41 | Mix.Project.umbrella?() 42 | |> if do 43 | Mix.Project.apps_paths() 44 | |> Map.keys() 45 | |> Enum.map(fn app -> 46 | [Mix.Project.build_path(), "lib", to_string(app), "ebin", "*.beam"] 47 | |> Path.join() 48 | |> Path.wildcard() 49 | end) 50 | else 51 | [Mix.Project.compile_path(), "*.beam"] 52 | |> Path.join() 53 | |> Path.wildcard() 54 | end 55 | |> List.flatten() 56 | end 57 | 58 | defp get_beams([":project"]) do 59 | Mix.Project.umbrella?() 60 | |> if do 61 | Mix.Project.apps_paths() 62 | |> Map.keys() 63 | |> Enum.map(fn app -> 64 | [Mix.Project.build_path(), "lib", to_string(app), "consolidated", "*.beam"] 65 | |> Path.join() 66 | |> Path.wildcard() 67 | end) 68 | else 69 | [Mix.Project.compile_path(), "..", "consolidated", "*.beam"] 70 | |> Path.join() 71 | |> Path.wildcard() 72 | end 73 | |> List.flatten(get_beams([])) 74 | end 75 | 76 | defp get_beams([":deps"]) do 77 | [Mix.Project.build_path(), "**", "*.beam"] 78 | |> Path.join() 79 | |> Path.wildcard() 80 | end 81 | 82 | defp get_beams(paths) do 83 | paths 84 | |> Enum.map(&Path.expand/1) 85 | |> Enum.flat_map(fn path -> 86 | cond do 87 | String.ends_with?(path, ".beam") -> 88 | Path.wildcard(path) 89 | 90 | path -> 91 | [path, "**", "*.beam"] 92 | |> Path.join() 93 | |> Path.wildcard() 94 | end 95 | end) 96 | end 97 | 98 | defp run_gradualizer(opts, gargs) do 99 | files = get_beams(gargs) 100 | 101 | opts[:verbose] && IO.puts("\n\nBeam Files to parse:") 102 | opts[:verbose] && Enum.each(files, &IO.puts/1) 103 | 104 | opts[:verbose] && IO.puts("\n\nGradualizing:") 105 | 106 | args = [ 107 | preload: (opts[:gradualizer_preload] && true) || false 108 | ] 109 | 110 | Gradualixir.gradualize(files, args) 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Gradualixir.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gradualixir, 7 | version: "0.2.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | dialyzer: [plt_add_apps: [:mix]] 12 | ] 13 | end 14 | 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:dialyxir, "~> 1.0.0-rc.4", only: [:dev, :test], runtime: false}, 24 | {:gradualizer, github: "josefs/Gradualizer", ref: "master", manager: :rebar3} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"}, 4 | "gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "0b4c6b0b78741bb77f1226db7f316bdf1b38787f", [ref: "master"]}, 5 | } 6 | -------------------------------------------------------------------------------- /priv/prelude/list.specs.erl: -------------------------------------------------------------------------------- 1 | -module('Elixir.List'). 2 | 3 | -type deep_list(A) :: [A | deep_list(A)]. 4 | 5 | -spec flatten(deep_list(A)) -> [A]. 6 | -spec flatten(deep_list(A), [A]) -> [A]. 7 | -------------------------------------------------------------------------------- /test/gradualixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GradualixirTest do 2 | use ExUnit.Case 3 | doctest Gradualixir 4 | 5 | test "greets the world" do 6 | # assert Gradualixir.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------