292 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Erlang
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Erlang
		
	
	
		
			Executable File
		
	
	
| #!/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 Pivotal Software, Inc.
 | |
| %% Copyright (c) 2010-2015 Pivotal Software, 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]) ->
 | |
|     put({?MODULE, quiet}, lists:member("-q", Argv)),
 | |
|     put({?MODULE, no_filters}, lists:member("-X", Argv)),
 | |
| 
 | |
|     {ok, Cwd} = file:get_cwd(),
 | |
|     code:add_pathz(filename:join(Cwd, "ebin")),
 | |
|     LibDir = filename:join(Cwd, "lib"),
 | |
|     case filelib:is_dir(LibDir) of
 | |
|         false -> ok;
 | |
|         true  -> os:cmd("rm -rf " ++ LibDir)
 | |
|     end,
 | |
|     Rc = try
 | |
|              check(Cwd, PluginsDir, LibDir, checks())
 | |
|          catch
 | |
|              _:Err ->
 | |
|                  io:format(user, "failed: ~p~n", [Err]),
 | |
|                  1
 | |
|          end,
 | |
|     shutdown(Rc, LibDir).
 | |
| 
 | |
| shutdown(Rc, LibDir) ->
 | |
|     os:cmd("rm -rf " ++ LibDir),
 | |
|     erlang:halt(Rc).
 | |
| 
 | |
| check(Cwd, PluginsDir, LibDir, Checks) ->
 | |
|     {ok, Plugins} = file:list_dir(PluginsDir),
 | |
|     ok = file:make_dir(LibDir),
 | |
|     put({?MODULE, third_party}, []),
 | |
|     [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")),
 | |
|         case IsExternal of
 | |
|             true -> App = list_to_atom(hd(string:tokens(filename:basename(AppN),
 | |
|                                                         "-"))),
 | |
|                     report(info, "loading ~p~n", [App]),
 | |
|                     application:load(App),
 | |
|                     store_third_party(App);
 | |
|             _    -> 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(_) -> false 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()},
 | |
|     {"(E | \"(rabbit|amqp).*\":_/_ || \"gen_server2?\":call/2)",
 | |
|      "has 5 sec timeout in",
 | |
|      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) ->
 | |
|         Result = cleanup(AnalysisResult),
 | |
|         lists:foldl(fun(F, false) -> F(Result);
 | |
|                        (_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)]),
 | |
|     case length(Errors) > 0 of
 | |
|         true  -> 1;
 | |
|         false -> 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}), Level} of
 | |
|         {true,  error} -> do_report(lookup_prefix(Level), Fmt, Args);
 | |
|         {false, _}     -> do_report(lookup_prefix(Level), Fmt, Args);
 | |
|         _              -> ok
 | |
|     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),
 | |
|     AppModules = proplists:get_value(modules, AppConfig),
 | |
|     put({?MODULE, third_party}, AppModules ++ get({?MODULE, third_party})).
 | |
| 
 | |
| %% 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)).
 |