You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
9.0 KiB

  1. %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. %% -------------------------------------------------------------------
  4. %%
  5. %% rebar: Erlang Build Tools
  6. %%
  7. %% Copyright (c) 2010 Dave Smith (dizzyd@dizzyd.com)
  8. %%
  9. %% Permission is hereby granted, free of charge, to any person obtaining a copy
  10. %% of this software and associated documentation files (the "Software"), to deal
  11. %% in the Software without restriction, including without limitation the rights
  12. %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. %% copies of the Software, and to permit persons to whom the Software is
  14. %% furnished to do so, subject to the following conditions:
  15. %%
  16. %% The above copyright notice and this permission notice shall be included in
  17. %% all copies or substantial portions of the Software.
  18. %%
  19. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. %% THE SOFTWARE.
  26. %% -------------------------------------------------------------------
  27. %% @author Dave Smith <dizzyd@dizzyd.com>
  28. %% @doc rebar_dialyzer supports the following commands:
  29. %% <ul>
  30. %% <li>dialyze (essentially "dialyzer ebin" or "dialyzer --src src")</li>
  31. %% <li>build-plt (essentially "dialyzer --build_plt -r &lt;app_dirs&gt;")</li>
  32. %% <li>check-plt (essentially "dialyzer --check_plt")</li>
  33. %% </ul>
  34. %% A single option <code>plt</code> can be presented in the <code>dialyzer_opts</code>
  35. %% options in <code>rebar.config</code>. If it is present, it is used as the PLT for the
  36. %% supported commands. Should it not be present, then the default is <code>$HOME/.dialyzer_plt</code>.
  37. %% @reference <a href="http://user.it.uu.se/~kostis/Papers/bugs05.pdf">Experience from developing the Dialyzer:
  38. %% A static analysis tool detecting defects in Erlang applications</a>
  39. %% @reference <a href="http://user.it.uu.se/~kostis/Papers/contracts.pdf">A Language for Specifying Type
  40. %% Contracts in Erlang and its Interaction with Success Typings</a>
  41. %% @reference <a href="http://user.it.uu.se/~kostis/Papers/wrangler.pdf">Gradual Typing of Erlang
  42. %% Programs: A Wrangler Experience</a>
  43. %% @copyright 2010 Dave Smith
  44. %% -------------------------------------------------------------------
  45. -module(rebar_dialyzer).
  46. -export([dialyze/2,
  47. 'build-plt'/2,
  48. 'check-plt'/2]).
  49. -include("rebar.hrl").
  50. -type(warning() :: {atom(), {string(), integer()}, any()}).
  51. %% ===================================================================
  52. %% Public API
  53. %% ===================================================================
  54. %% @doc Perform static analysis on the contents of the ebin directory.
  55. %% @spec dialyze(Config::#config{}, File::string()) -> ok
  56. -spec dialyze(Config::#config{}, File::string()) -> ok.
  57. dialyze(Config, File) ->
  58. Plt = existing_plt_path(Config, File),
  59. case dialyzer:plt_info(Plt) of
  60. {ok, _} ->
  61. FromSrc = proplists:get_bool(src, rebar_config:get(Config,
  62. dialyzer_opts,
  63. [])),
  64. DialyzerOpts0 = case FromSrc of
  65. true ->
  66. [{files_rec, ["src"]}, {init_plt, Plt},
  67. {from, src_code}];
  68. false ->
  69. [{files_rec, ["ebin"]}, {init_plt, Plt}]
  70. end,
  71. WarnOpts = warnings(Config),
  72. DialyzerOpts = case WarnOpts of
  73. [] -> DialyzerOpts0;
  74. _ -> [{warnings, WarnOpts}|DialyzerOpts0]
  75. end,
  76. ?DEBUG("DialyzerOpts: ~p~n", [DialyzerOpts]),
  77. try dialyzer:run(DialyzerOpts) of
  78. Warnings -> output_warnings(Warnings)
  79. catch
  80. throw:{dialyzer_error, Reason} ->
  81. ?ABORT("~s~n", [Reason])
  82. end;
  83. {error, no_such_file} ->
  84. ?ABORT("The PLT ~s does not exist. Please perform the build-plt "
  85. "command to ~n"
  86. "produce the initial PLT. Be aware that this operation may "
  87. "take several minutes.~n", [Plt]);
  88. {error, read_error} ->
  89. ?ABORT("Unable to read PLT ~n~n", [Plt]);
  90. {error, not_valid} ->
  91. ?ABORT("The PLT ~s is not valid.~n", [Plt])
  92. end,
  93. ok.
  94. %% @doc Build the PLT.
  95. %% @spec 'build-plt'(Config::#config{}, File::string()) -> ok
  96. -spec 'build-plt'(Config::#config{}, File::string()) -> ok.
  97. 'build-plt'(Config, File) ->
  98. Plt = new_plt_path(Config, File),
  99. Apps = rebar_app_utils:app_applications(File),
  100. ?DEBUG("Build PLT ~s including following apps:~n~p~n", [Plt, Apps]),
  101. Warnings = dialyzer:run([{analysis_type, plt_build},
  102. {files_rec, app_dirs(Apps)},
  103. {output_plt, Plt}]),
  104. case Warnings of
  105. [] ->
  106. ?INFO("The built PLT can be found in ~s~n", [Plt]);
  107. _ ->
  108. output_warnings(Warnings)
  109. end,
  110. ok.
  111. %% @doc Check whether the PLT is up-to-date (rebuilding it if not).
  112. %% @spec 'check-plt'(Config::#config{}, File::string()) -> ok
  113. -spec 'check-plt'(Config::#config{}, File::string()) -> ok.
  114. 'check-plt'(Config, File) ->
  115. Plt = existing_plt_path(Config, File),
  116. try dialyzer:run([{analysis_type, plt_check}, {init_plt, Plt}]) of
  117. [] ->
  118. ?CONSOLE("The PLT ~s is up-to-date~n", [Plt]);
  119. _ ->
  120. %% @todo Determine whether this is the correct summary.
  121. ?CONSOLE("The PLT ~s is not up-to-date~n", [Plt])
  122. catch
  123. throw:{dialyzer_error, _Reason} ->
  124. ?CONSOLE("The PLT ~s is not valid.~n", [Plt])
  125. end,
  126. ok.
  127. %% ===================================================================
  128. %% Internal functions
  129. %% ===================================================================
  130. %% @doc Obtain the library paths for the supplied applications.
  131. %% @spec app_dirs(Apps::[atom()]) -> [string()]
  132. -spec app_dirs(Apps::[atom()]) -> [string()].
  133. app_dirs(Apps) ->
  134. [filename:join(Path, "ebin")
  135. || Path <- [code:lib_dir(App) || App <- Apps], erlang:is_list(Path)].
  136. %% @doc Render the warnings on the console.
  137. %% @spec output_warnings(Warnings::[warning()]) -> 'ok'
  138. -spec output_warnings(Warnings::[warning()]) -> 'ok'.
  139. output_warnings(Warnings) ->
  140. lists:foreach(fun(Warning) ->
  141. ?CONSOLE("~s", [dialyzer:format_warning(Warning)])
  142. end, Warnings).
  143. %% @doc If the plt option is present in rebar.config return its value, otherwise
  144. %% return $HOME/.dialyzer_plt.
  145. %% @spec new_plt_path(Config::#config{}, File::string()) -> string()
  146. -spec new_plt_path(Config::#config{}, File::string()) -> string().
  147. new_plt_path(Config, File) ->
  148. AppName = rebar_app_utils:app_name(File),
  149. DialyzerOpts = rebar_config:get(Config, dialyzer_opts, []),
  150. case proplists:get_value(plt, DialyzerOpts) of
  151. undefined ->
  152. filename:join(os:getenv("HOME"),
  153. "." ++ atom_to_list(AppName) ++ "_dialyzer_plt");
  154. Plt ->
  155. Plt
  156. end.
  157. %% @doc If the plt option is present in rebar.config and the file exists
  158. %% return its value or if ~/.AppName_dialyzer_plt exists return that.
  159. %% Otherwise return ~/.dialyzer_plt if it exists or abort.
  160. %% @spec existing_plt_path(Config::#config{}, File::string()) -> string()
  161. -spec existing_plt_path(Config::#config{}, File::string()) -> string().
  162. existing_plt_path(Config, File) ->
  163. AppName = rebar_app_utils:app_name(File),
  164. DialyzerOpts = rebar_config:get(Config, dialyzer_opts, []),
  165. Home = os:getenv("HOME"),
  166. case proplists:get_value(plt, DialyzerOpts) of
  167. undefined ->
  168. AppPlt = filename:join(Home, "." ++ atom_to_list(AppName)
  169. ++ "_dialyzer_plt"),
  170. case filelib:is_regular(AppPlt) of
  171. true ->
  172. AppPlt;
  173. false ->
  174. HomePlt = filename:join(Home, ".dialyzer_plt"),
  175. case filelib:is_regular(HomePlt) of
  176. true ->
  177. HomePlt;
  178. false ->
  179. ?ABORT("No PLT found~n", [])
  180. end
  181. end;
  182. [$~|[$/|Plt]] ->
  183. filename:join(Home,Plt);
  184. Plt ->
  185. Plt
  186. end.
  187. %% @doc If the warnings option is present in rebar.config return its value,
  188. %% otherwise return [].
  189. %% @spec warnings(Config::#config{}) -> list()
  190. -spec warnings(Config::#config{}) -> list().
  191. warnings(Config) ->
  192. DialyzerOpts = rebar_config:get(Config, dialyzer_opts, []),
  193. proplists:get_value(warnings, DialyzerOpts, []).