Last active
August 29, 2015 14:04
-
-
Save seancribbs/2d19271c02d5017632f1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
%% @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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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