summaryrefslogtreecommitdiff
path: root/check_xref
diff options
context:
space:
mode:
authorTim Watson <tim@rabbitmq.com>2012-06-21 10:45:34 +0100
committerTim Watson <tim@rabbitmq.com>2012-06-21 10:45:34 +0100
commit58351e5fa31ff2267d912913df12d126af886d1d (patch)
treeda93f1f919b944e7f8b07fba351041729ef24dcb /check_xref
parent8439d39f7ab1666517467c9d07624001a605cdd6 (diff)
downloadrabbitmq-server-58351e5fa31ff2267d912913df12d126af886d1d.tar.gz
Run additional xref checks in the broker when plugins-src is available.
Output full paths and line numbers for analysis. Differentiate between warnings and error where appropriate.
Diffstat (limited to 'check_xref')
-rwxr-xr-xcheck_xref290
1 files changed, 290 insertions, 0 deletions
diff --git a/check_xref b/check_xref
new file mode 100755
index 00000000..70b922c1
--- /dev/null
+++ b/check_xref
@@ -0,0 +1,290 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+-mode(compile).
+
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (the "License"); you may not use this file except in
+%% compliance with the License. You may obtain a copy of the License
+%% at http://www.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and
+%% limitations under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developer of the Original Code is VMware, Inc.
+%% Copyright (c) 2010-2012 VMware, Inc. All rights reserved.
+%%
+
+main(["-h"]) ->
+ io:format("usage: check_xref PluginDirectory (options)~n"
+ "options:~n"
+ " -q - quiet mode (only prints errors)~n"
+ " -X - disables all filters~n");
+main([PluginsDir|Argv]) ->
+ Quiet = lists:member("-q", Argv),
+ if Quiet == true -> put({?MODULE, quiet}, true);
+ true -> ok
+ end,
+
+ DisableFilters = lists:member("-X", Argv),
+ if DisableFilters == true -> put({?MODULE, no_filters}, true);
+ true -> ok
+ end,
+
+ {ok, Cwd} = file:get_cwd(),
+ code:add_pathz(filename:join(Cwd, "ebin")),
+ LibDir = filename:join(Cwd, "lib"),
+
+ try
+ check(Cwd, PluginsDir, LibDir, checks())
+ after
+ %% TODO: bootstrap file_handle_cache and use
+ %% rabbit_file:recursive_delete instead of this...
+ _ = os:cmd("rm -rf " ++ LibDir)
+ end.
+
+check(Cwd, PluginsDir, LibDir, Checks) ->
+ {ok, Plugins} = file:list_dir(PluginsDir),
+ ok = file:make_dir(LibDir),
+ [begin
+ Source = filename:join(PluginsDir, Plugin),
+ Target = filename:join(LibDir, Plugin),
+ IsExternal = external_dependency(Plugin),
+ AppN = case IsExternal of
+ true -> filename:join(LibDir, unmangle_name(Plugin));
+ false -> filename:join(LibDir,
+ filename:basename(Plugin, ".ez"))
+ end,
+
+ report(info, "mkdir -p ~s~n", [Target]),
+ filelib:ensure_dir(Target),
+
+ report(info, "cp ~s ~s~n", [Source, Target]),
+ {ok, _} = file:copy(Source, Target),
+
+ report(info, "unzip -d ~s ~s~n", [LibDir, Target]),
+ {ok, _} = zip:unzip(Target, [{cwd, LibDir}]),
+
+ UnpackDir = filename:join(LibDir, filename:basename(Target, ".ez")),
+ report(info, "mv ~s ~s~n", [UnpackDir, AppN]),
+ ok = file:rename(UnpackDir, AppN),
+
+ code:add_patha(filename:join(AppN, "ebin")),
+ if IsExternal =:= true ->
+ App = list_to_atom(hd(string:tokens(filename:basename(AppN), "-"))),
+ report(info, "loading ~p~n", [App]),
+ application:load(App),
+ store_third_party(App);
+ true ->
+ ok
+ end
+ end || Plugin <- Plugins,
+ lists:suffix(".ez", Plugin)],
+
+ RabbitAppEbin = filename:join([LibDir, "rabbit", "ebin"]),
+ filelib:ensure_dir(filename:join(RabbitAppEbin, "foo")),
+ {ok, Beams} = file:list_dir("ebin"),
+ [{ok, _} = file:copy(filename:join("ebin", Beam),
+ filename:join(RabbitAppEbin, Beam)) || Beam <- Beams],
+ xref:start(?MODULE),
+ xref:set_default(?MODULE, [{verbose, false}, {warnings, false}]),
+ xref:set_library_path(?MODULE, code:get_path()),
+ xref:add_release(?MODULE, Cwd, {name, rabbit}),
+ store_unresolved_calls(),
+ Results = lists:flatten([perform_analysis(Q) || Q <- Checks]),
+ report(Results).
+
+%%
+%% Analysis
+%%
+
+perform_analysis({Query, Description, Severity}) ->
+ perform_analysis({Query, Description, Severity, fun(_) -> return end});
+perform_analysis({Query, Description, Severity, Filter}) ->
+ report_progress("Checking whether any code ~s "
+ "(~s)~n", [Description, Query]),
+ case analyse(Query) of
+ {ok, Analysis} ->
+ [filter(Result, Filter) ||
+ Result <- process_analysis(Query, Description,
+ Severity, Analysis)];
+ {error, Module, Reason} ->
+ {analysis_error, {Module, Reason}}
+ end.
+
+partition(Results) ->
+ lists:partition(fun({{_, L}, _}) -> L =:= error end, Results).
+
+analyse(Query) when is_atom(Query) ->
+ xref:analyse(?MODULE, Query, [{verbose, false}]);
+analyse(Query) when is_list(Query) ->
+ xref:q(?MODULE, Query).
+
+process_analysis(Query, Tag, Severity, Analysis) when is_atom(Query) ->
+ [{{Tag, Severity}, MFA} || MFA <- Analysis];
+process_analysis(Query, Tag, Severity, Analysis) when is_list(Query) ->
+ [{{Tag, Severity}, Result} || Result <- Analysis].
+
+checks() ->
+ [{"(XXL)(Lin) ((XC - UC) || (XU - X - B))",
+ "has call to undefined function(s)",
+ error, filters()},
+ {"(Lin) (L - LU)", "has unused local function(s)",
+ error, filters()},
+ {"(Lin) (LU * (X - XU))",
+ "has exported function(s) only used locally",
+ warning, filters()},
+ {"(Lin) (DF * (XU + LU))", "used deprecated function(s)",
+ warning, filters()}].
+% {"(Lin) (X - XU)", "possibly unused export",
+% warning, fun filter_unused/1}].
+
+%%
+%% noise filters (can be disabled with -X) - strip uninteresting analyses
+%%
+
+filter(Result, Filter) ->
+ case Filter(Result) of
+ false -> Result;
+ true -> [] %% NB: this gets flattened out later on....
+ end.
+
+filters() ->
+ case get({?MODULE, no_filters}) of
+ true -> fun(_) -> false end;
+ _ -> filter_chain([fun is_unresolved_call/1, fun is_callback/1,
+ fun is_unused/1, fun is_irrelevant/1])
+ end.
+
+filter_chain(FnChain) ->
+ fun(AnalysisResult) ->
+ lists:foldl(fun(F, false) -> F(cleanup(AnalysisResult));
+ (_F, true) -> true
+ end, false, FnChain)
+ end.
+
+cleanup({{_, _},{{{{_,_,_}=MFA1,_},{{_,_,_}=MFA2,_}},_}}) -> {MFA1, MFA2};
+cleanup({{_, _},{{{_,_,_}=MFA1,_},{{_,_,_}=MFA2,_}}}) -> {MFA1, MFA2};
+cleanup({{_, _},{{_,_,_}=MFA1,{_,_,_}=MFA2},_}) -> {MFA1, MFA2};
+cleanup({{_, _},{{_,_,_}=MFA1,{_,_,_}=MFA2}}) -> {MFA1, MFA2};
+cleanup({{_, _}, {_,_,_}=MFA}) -> MFA;
+cleanup({{_, _}, {{_,_,_}=MFA,_}}) -> MFA;
+cleanup({{_,_,_}=MFA, {_,_,_}}) -> MFA;
+cleanup({{_,_,_}=MFA, {_,_,_},_}) -> MFA;
+cleanup(Other) -> Other.
+
+is_irrelevant({{M,_,_}, {_,_,_}}) ->
+ is_irrelevant(M);
+is_irrelevant({M,_,_}) ->
+ is_irrelevant(M);
+is_irrelevant(Mod) when is_atom(Mod) ->
+ lists:member(Mod, get({?MODULE, third_party})).
+
+is_unused({{_,_,_}=MFA, {_,_,_}}) ->
+ is_unused(MFA);
+is_unused({M,_F,_A}) ->
+ lists:suffix("_tests", atom_to_list(M));
+is_unused(_) ->
+ false.
+
+is_unresolved_call({_, F, A}) ->
+ UC = get({?MODULE, unresolved_calls}),
+ sets:is_element({'$M_EXPR', F, A}, UC);
+is_unresolved_call(_) ->
+ false.
+
+%% TODO: cache this....
+is_callback({M,_,_}=MFA) ->
+ Attributes = M:module_info(attributes),
+ Behaviours = proplists:append_values(behaviour, Attributes),
+ {_, Callbacks} = lists:foldl(fun acc_behaviours/2, {M, []}, Behaviours),
+ lists:member(MFA, Callbacks);
+is_callback(_) ->
+ false.
+
+acc_behaviours(B, {M, CB}=Acc) ->
+ case catch(B:behaviour_info(callbacks)) of
+ [{_,_}|_]=Callbacks ->
+ {M, CB ++ [{M, F, A} || {F,A} <- Callbacks]};
+ _ ->
+ Acc
+ end.
+
+%%
+%% reporting/output
+%%
+
+report(Results) ->
+ [report_failures(F) || F <- Results],
+ {Errors, Warnings} = partition(Results),
+ report(info, "Completed: ~p errors, ~p warnings~n",
+ [length(Errors), length(Warnings)]),
+ if length(Errors) > 0 -> halt(1);
+ true -> halt(0)
+ end.
+
+report_failures({analysis_error, {Mod, Reason}}) ->
+ report(error, "~s:0 Analysis Error: ~p~n", [source_file(Mod), Reason]);
+report_failures({{Tag, Level}, {{{{M,_,_},L},{{M2,F2,A2},_}},_}}) ->
+ report(Level, "~s:~w ~s ~p:~p/~p~n",
+ [source_file(M), L, Tag, M2, F2, A2]);
+report_failures({{Tag, Level}, {{M,F,A},L}}) ->
+ report(Level, "~s:~w ~s ~p:~p/~p~n", [source_file(M), L, Tag, M, F, A]);
+report_failures({{Tag, Level}, {M,F,A}}) ->
+ report(Level, "~s:unknown ~s ~p:~p/~p~n", [source_file(M), Tag, M, F, A]);
+report_failures(Term) ->
+ report(error, "Ignoring ~p~n", [Term]),
+ ok.
+
+report_progress(Fmt, Args) ->
+ report(info, Fmt, Args).
+
+report(Level, Fmt, Args) ->
+ case get({?MODULE, quiet}) of
+ true -> if Level=:=error -> do_report(lookup_prefix(Level), Fmt, Args);
+ true -> ok
+ end;
+ _ -> do_report(lookup_prefix(Level), Fmt, Args)
+ end.
+
+do_report(Prefix, Fmt, Args) ->
+ io:format(Prefix ++ Fmt, Args).
+
+lookup_prefix(error) -> "ERROR: ";
+lookup_prefix(warning) -> "WARNING: ";
+lookup_prefix(info) -> "INFO: ".
+
+source_file(M) ->
+ proplists:get_value(source, M:module_info(compile)).
+
+%%
+%% setup/code-path/file-system ops
+%%
+
+store_third_party(App) ->
+ {ok, AppConfig} = application:get_all_key(App),
+ case get({?MODULE, third_party}) of
+ undefined ->
+ put({?MODULE, third_party},
+ proplists:get_value(modules, AppConfig));
+ Modules ->
+ put({?MODULE, third_party},
+ proplists:get_value(modules, AppConfig) ++ Modules)
+ end.
+
+%% TODO: this ought not to be maintained in such a fashion
+external_dependency(Path) ->
+ lists:any(fun(P) -> lists:prefix(P, Path) end,
+ ["mochiweb", "webmachine", "rfc4627", "eldap"]).
+
+unmangle_name(Path) ->
+ [Name, Vsn|_] = re:split(Path, "-", [{return, list}]),
+ string:join([Name, Vsn], "-").
+
+store_unresolved_calls() ->
+ {ok, UCFull} = analyse("UC"),
+ UC = [MFA || {_, {_,_,_}=MFA} <- UCFull],
+ put({?MODULE, unresolved_calls}, sets:from_list(UC)).