Skip to content

Instantly share code, notes, and snippets.

@seancribbs
Last active August 29, 2015 14:04
Show Gist options
  • Save seancribbs/2d19271c02d5017632f1 to your computer and use it in GitHub Desktop.
Save seancribbs/2d19271c02d5017632f1 to your computer and use it in GitHub Desktop.
%% @doc Reports on percentage of modules, functions and types that are
%% documented.
-module(edoc_stats).
-export([file/1, file/2, files/1, files/2, aggregate/1, report/1, report_files/1, report_files/2]).
-include_lib("xmerl/include/xmerl.hrl").
-import(xmerl_xpath, [string/2]).
-define(QUERY_FUNS, [ fun module_has_description/2
, fun functions_have_descriptions/2
, fun types_have_descriptions/2
]).
-record(stats, {
modules = 0,
types = 0,
exported_funs = 0,
private_funs = 0,
doc_errors = 0
}).
-type module_doc() :: {module, boolean() | error}.
%% Summary entry that describes whether the module is documented.
-type function_doc() :: {function, string(), export | private, boolean()}.
%% Summary entry that describes a module function.
-type type_doc() :: {type, string(), boolean()}.
%% Summary entry that describes a module type declaration.
-type summary() :: {module() | file:filename(), [module_doc() | function_doc() | type_doc()]}.
%% A summary of a module's documentation status.
-type stats() :: {Total::#stats{}, Undocumented::#stats{}}.
%% An aggregation of statistics about multiple modules' documentation.
%% @doc Reports on a list of files.
-spec report_files([file:filename()]) -> ok.
report_files(Filenames) ->
report_files(Filenames, []).
%% @doc Reports on a list of files.
-spec report_files([file:filename()], [proplist:property()]) -> ok.
report_files(Filenames, Options) ->
report(files(Filenames, Options)).
%% @doc Reports the documentation statistics across a list of module
%% summaries.
-spec report([summary()]) -> ok.
report(Modules) ->
{Total, Undoc} = aggregate(Modules),
TotalCount = lists:sum(tl(tuple_to_list(Total))),
UndocCount = lists:sum(tl(tuple_to_list(Undoc))),
print_summary("Modules:", #stats.modules, Total, Undoc),
print_summary("Types:", #stats.types, Total, Undoc),
print_summary("Exported functions:", #stats.exported_funs, Total, Undoc),
print_summary("Private functions:", #stats.private_funs, Total, Undoc),
if Total#stats.doc_errors > 0 ->
io:format("~-20s~4w~n", ["Errors:", Total#stats.doc_errors]);
true -> ok
end,
if TotalCount /= 0 ->
PercentDoc = (TotalCount - UndocCount) / TotalCount * 100.0,
io:format("~7.2f% documented~n", [PercentDoc]);
true ->
io:format("Nothing to report.~n")
end,
ok.
print_summary(_, Fn, Total, _) when element(Fn, Total) == 0 ->
ok;
print_summary(Title, Fn, Total, Undoc) ->
io:format("~-20s~4w ( ~4w undocumented)~n", [Title, element(Fn, Total), element(Fn, Undoc)]).
%% @doc Aggregates documentation statistics across a list of module
%% summaries. Used internally in report/1.
-spec aggregate([summary()]) -> stats().
aggregate(Modules) ->
lists:foldl(fun aggregate_module/2, {#stats{}, #stats{}}, Modules).
aggregate_module({_ModName, Contents}, Stats) ->
lists:foldl(fun aggregate_module_contents/2, Stats, Contents).
aggregate_module_contents({module, error}, {Total, Undoc}) ->
%% We count doc parse errors both as errors and as undocumented
%% modules. If we can't generate documentation from it, it should
%% count as undocumented.
aggregate_module_contents(
{module, false},
update_doc_counters(Total, Undoc, #stats.doc_errors, false));
aggregate_module_contents({module, Doc}, {Total, Undoc}) ->
update_doc_counters(Total, Undoc, #stats.modules, Doc);
aggregate_module_contents({function, _Name, export, Doc}, {Total, Undoc}) ->
update_doc_counters(Total, Undoc, #stats.exported_funs, Doc);
aggregate_module_contents({function, _Name, private, Doc}, {Total, Undoc}) ->
update_doc_counters(Total, Undoc, #stats.private_funs, Doc);
aggregate_module_contents({type, _Name, Doc}, {Total, Undoc}) ->
update_doc_counters(Total, Undoc, #stats.types, Doc).
update_doc_counters(Total, Undoc, Fn, Doc) ->
Count = element(Fn, Total) + 1,
UnDocCount = if not Doc -> element(Fn, Undoc) + 1;
true -> element(Fn, Undoc)
end,
{setelement(Fn, Total, Count), setelement(Fn, Undoc, UnDocCount)}.
%% @doc Reads a list of files and produces summaries.
%% @equiv files(Filenames, [])
%% @see file/1
-spec files([file:filename()]) -> [summary()].
files(Filenames) ->
files(Filenames, []).
%% @doc Reads a list of files and produces summaries.
-spec files([file:filename()], [proplists:property()]) -> [summary()].
files(Filenames, Options) ->
[ try
file(Filename, Options)
catch
_:_ ->
{Filename, [{module, error}]}
end || Filename <- Filenames ].
%% @doc Reads the documentation from a source file and returns a
%% summary. By default, this ignores hidden and private functions.
-spec file(file:filename()) -> summary().
file(Filename) ->
file(Filename, []).
%% @doc Reads the documentation from a source file and returns a
%% summary. `Options' are the same as options for edoc:get_doc/2.
%% @see edoc:get_doc/2
-spec file(file:filename(), [proplists:property()]) -> summary().
file(Filename, Options) ->
{Module, Doc} = edoc:get_doc(Filename, Options),
{Module,
lists:foldr(fun(F, Acc) ->
F(Doc, Acc)
end, [], ?QUERY_FUNS)}.
module_has_description(Doc, Acc) ->
Docs = string("/module/description//text()", Doc),
[{module, Docs /= []}|Acc].
functions_have_descriptions(Doc, Acc) ->
Functions = string("/module/functions/function", Doc),
[ function_has_description(F) || F <- Functions ] ++ Acc.
function_has_description(F) ->
[#xmlAttribute{value=FunName}] = string("attribute::name", F),
[#xmlAttribute{value=Arity}] = string("attribute::arity", F),
[#xmlAttribute{value=ExportedStr}] = string("attribute::exported", F),
Exported = if ExportedStr == "yes" -> export; true -> private end,
FullFunName = FunName ++ "/" ++ Arity,
case string("//description//text()", F) of
[] -> {function, FullFunName, Exported, false};
_ -> {function, FullFunName, Exported, true}
end.
types_have_descriptions(Doc, Acc) ->
Types = string("/module/typedecls/typedecl", Doc),
[ type_has_description(T) || T <- Types ] ++ Acc.
type_has_description(T) ->
[#xmlAttribute{value="type-"++RawTypeName}] = string("attribute::label", T),
Arity = length(string("//argtypes/node()", T)),
TypeName = RawTypeName ++ "/" ++ integer_to_list(Arity),
case string("//description//text()", T) of
[] -> {type, TypeName, false};
_ -> {type, TypeName, true}
end.
71> edoc_stats:file(Filename).
{riakc_counter,[{module,true},
{function,"new/0",export,true},
{function,"new/1",export,true},
{function,"new/2",export,true},
{function,"value/1",export,true},
{function,"increment/1",export,true},
{function,"increment/2",export,true},
{function,"decrement/1",export,true},
{function,"decrement/2",export,true},
{function,"to_op/1",export,true},
{function,"is_type/1",export,true},
{function,"type/0",export,true},
{function,"gen_type/0",private,false},
{function,"gen_op/0",private,false},
{type,"counter_op/0",false},
{type,"counter/0",false}]}
99> edoc_stats:report(Mods).
Modules: 6 ( 0 undocumented)
Types: 38 ( 38 undocumented)
Exported functions: 87 ( 2 undocumented)
Private functions: 40 ( 38 undocumented)
54.39% documented
ok
146> edoc_stats:report_files(["../edoc_stats.erl"], []).
Modules: 1 ( 0 undocumented)
Types: 5 ( 0 undocumented)
Exported functions: 8 ( 0 undocumented)
100.00% documented
147> edoc_stats:report_files(filelib:wildcard("src/*.erl"), []).
src/riakc_map.erl, function fold_ignore_noop/3: at line 216: `-quote ended unexpectedly at line 216
src/riakc_obj.erl: at line 100: warning: duplicated type metadata
src/riakc_pb_socket.erl: at line 1696: syntax error before: '{'
Modules: 8 ( 2 undocumented)
Types: 38 ( 28 undocumented)
Exported functions: 85 ( 2 undocumented)
Errors: 2
74.44% documented
ok
148> edoc_stats:report_files(filelib:wildcard("src/*.erl"), [private]).
src/riakc_map.erl, function fold_ignore_noop/3: at line 216: `-quote ended unexpectedly at line 216
src/riakc_obj.erl: at line 100: warning: duplicated type metadata
src/riakc_pb_socket.erl: at line 1696: syntax error before: '{'
Modules: 8 ( 2 undocumented)
Types: 38 ( 28 undocumented)
Exported functions: 87 ( 2 undocumented)
Private functions: 40 ( 38 undocumented)
Errors: 2
58.86% documented
ok
149> edoc_stats:report_files(filelib:wildcard("src/*.erl"), [hidden]).
src/riakc_map.erl, function fold_ignore_noop/3: at line 216: `-quote ended unexpectedly at line 216
src/riakc_obj.erl: at line 100: warning: duplicated type metadata
src/riakc_pb_socket.erl: at line 1696: syntax error before: '{'
Modules: 8 ( 2 undocumented)
Types: 38 ( 28 undocumented)
Exported functions: 85 ( 2 undocumented)
Errors: 2
74.44% documented
ok
150> edoc_stats:report_files(filelib:wildcard("src/*.erl"), [hidden, private]).
src/riakc_map.erl, function fold_ignore_noop/3: at line 216: `-quote ended unexpectedly at line 216
src/riakc_obj.erl: at line 100: warning: duplicated type metadata
src/riakc_pb_socket.erl: at line 1696: syntax error before: '{'
Modules: 8 ( 2 undocumented)
Types: 38 ( 28 undocumented)
Exported functions: 87 ( 2 undocumented)
Private functions: 40 ( 38 undocumented)
Errors: 2
58.86% documented
ok
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment