diff options
author | Tim Watson <tim@rabbitmq.com> | 2012-06-21 10:45:34 +0100 |
---|---|---|
committer | Tim Watson <tim@rabbitmq.com> | 2012-06-21 10:45:34 +0100 |
commit | 58351e5fa31ff2267d912913df12d126af886d1d (patch) | |
tree | da93f1f919b944e7f8b07fba351041729ef24dcb /check_xref | |
parent | 8439d39f7ab1666517467c9d07624001a605cdd6 (diff) | |
download | rabbitmq-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-x | check_xref | 290 |
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)). |