1#!/usr/bin/env escript
2%% -*- erlang -*-
3%% %CopyrightBegin%
4%%
5%% Copyright Ericsson AB 2011-2016. All Rights Reserved.
6%%
7%% Licensed under the Apache License, Version 2.0 (the "License");
8%% you may not use this file except in compliance with the License.
9%% You may obtain a copy of the License at
10%%
11%%     http://www.apache.org/licenses/LICENSE-2.0
12%%
13%% Unless required by applicable law or agreed to in writing, software
14%% distributed under the License is distributed on an "AS IS" BASIS,
15%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16%% See the License for the specific language governing permissions and
17%% limitations under the License.
18%%
19%% %CopyrightEnd%
20
21%%% <script> [-I<dir>]... [-o<dir>] [-module Module] [File]
22%%%
23%%% Use EDoc and the layout module 'docgen_otp_specs' to create an XML file
24%%% containing Dialyzer types and specifications (-type, -spec).
25%%%
26%%% Options:
27%%%
28%%%  "-o<dir>" The output directory for the created file.
29%%%            Default is ".".
30%%%  "-I<dir>" Directory to be searched when including a file.
31%%%  "-module Module"
32%%%            Module name to use when there is no File argument.
33%%%            A empty specifications file will be created.
34%%%            Exactly one of -module Module and File must be given.
35%%%
36%%% The name of the generated file is "specs_<module>.xml". Its exact
37%%% format is not further described here.
38
39main(Args) ->
40    case catch parse(Args, [], ".", no_module) of
41        {ok, FileSpec, InclFs, Dir} ->
42            call_edoc(FileSpec, InclFs, Dir);
43        {error, Msg} ->
44            io:format("~s\n", [Msg]),
45            usage()
46    end.
47
48parse(["-o"++Dir | Opts], InclFs, _, Module) ->
49    parse(Opts, InclFs, Dir, Module);
50parse(["-I"++I | Opts], InclFs, Dir, Module) ->
51    parse(Opts, [I | InclFs], Dir, Module);
52parse(["-module", Module | Opts], InclFs, Dir, _) ->
53    parse(Opts, InclFs, Dir, Module);
54parse([File], InclFs, Dir, no_module) ->
55    {ok, {file, File}, lists:reverse(InclFs), Dir};
56parse([_], _, _, _) ->
57    {error, io_lib:format("Cannot have both -module option and file", [])};
58parse([], _, _, no_module) ->
59    {error, io_lib:format("Missing -module option or file", [])};
60parse([], InclFs, Dir, Module) ->
61    {ok, {module, Module}, lists:reverse(InclFs), Dir};
62parse(Args, _, _, _) ->
63    {error, io_lib:format("Bad arguments: ~p", [Args])}.
64
65usage() ->
66    io:format("usage:  ~s [-I<include_dir>]... [-o<out_dir>] "
67              "[-module <module>] [file]\n", [escript:script_name()]),
68    halt(1).
69
70call_edoc(FileSpec, InclFs, Dir) ->
71    ReadOpts = [{includes, InclFs}, {preprocess, true}],
72    ExtractOpts = [{report_missing_type, false}],
73    LayoutOpts = [{pretty_printer, erl_pp}, {layout, docgen_otp_specs}],
74    File = case FileSpec of
75               {file, File0} -> File0;
76               {module, Module0} -> Module0
77           end,
78    try
79        Fs = case FileSpec of
80                 {file, _} ->
81                     Fs0 = read_file(File, ReadOpts),
82                     clauses(Fs0);
83                 {module, Module} ->
84                     [{attribute,0,module,list_to_atom(Module)}]
85             end,
86        Doc = extract(File, Fs, ExtractOpts),
87        Text = edoc:layout(Doc, LayoutOpts),
88        ok = write_text(Text, File, Dir),
89        rename(Dir, File)
90    catch
91        _:_ ->
92            io:format("EDoc could not process file '~s'\n", [File]),
93            clean_up(Dir),
94            halt(3)
95    end.
96
97read_file(File, Opts) ->
98    edoc:read_source(File, Opts).
99
100extract(File, Forms, Opts) ->
101    Env = edoc_lib:get_doc_env([], [], _Opts=[]),
102    {_Module, Doc} = edoc_extract:source(Forms, File, Env, Opts),
103    Doc.
104
105clauses(Fs) ->
106    clauses(Fs, no).
107
108clauses([], no) ->
109    [];
110clauses([F | Fs], Spec) ->
111    case F of
112        {attribute,_,spec,_} ->
113            clauses(Fs, F);
114        {function,_,_N,_A,_Cls} when Spec =/= no->
115            {attribute,_,spec,{Name,FunTypes}} = Spec,
116            %% [throw({no,Name,{_N,_A}}) || Name =/= {_N,_A}],
117            %% EDoc doesn't care if a function appears more than once;
118            %% this is how overloaded specs are handled:
119            (lists:append([[setelement(4, Spec, {Name,[T]}),F] ||
120                              T <- FunTypes])
121             ++ clauses(Fs, no));
122        _ ->
123            [F | clauses(Fs, Spec)]
124    end.
125
126write_text(Text, File, Dir) ->
127    Base = filename:basename(File, ".erl"),
128    OutFile = filename:join(Dir, Base) ++ ".specs",
129    case file:write_file(OutFile, Text) of
130        ok ->
131            ok;
132        {error, R} ->
133            R1 = file:format_error(R),
134            io:format("could not write file '~s': ~s\n", [File, R1]),
135            halt(2)
136    end.
137
138rename(Dir, F) ->
139    Mod = filename:basename(F, ".erl"),
140    Old = filename:join(Dir, Mod ++ ".specs"),
141    New = filename:join(Dir, "specs_" ++ Mod ++ ".xml"),
142    case file:rename(Old, New) of
143        ok ->
144            ok;
145        {error, R} ->
146            R1 = file:format_error(R),
147            io:format("could not rename file '~s': ~s\n", [New, R1]),
148            halt(2)
149    end.
150
151clean_up(Dir) ->
152    _ = [file:delete(filename:join(Dir, F)) ||
153            F <- ["packages-frame.html",
154                  "overview-summary.html",
155                  "modules-frame.html",
156                  "index.html", "erlang.png", "edoc-info"]],
157    ok.
158