|
| 1 | +%% ------------------------------------------------------------------- |
| 2 | +%% This module is basically copied from rebar's rebar_xref, which in turn |
| 3 | +%% borrows heavily from rebar's http://github.com/etnt/eCheck project as |
| 4 | +%% written by Torbjorn Tornkvist <[email protected]>, Daniel Luna |
| 5 | +%% <[email protected]> and others. |
| 6 | +%% ------------------------------------------------------------------- |
| 7 | +-module(xref_runner). |
| 8 | + |
| 9 | + |
| 10 | +-type check() :: undefined_function_calls |
| 11 | + | undefined_functions |
| 12 | + | locals_not_used |
| 13 | + | exports_not_used |
| 14 | + | deprecated_function_calls |
| 15 | + | deprecated_functions. |
| 16 | + |
| 17 | +-type xref_default() :: {builtins | recurse | verbose | warnings, boolean()}. |
| 18 | + |
| 19 | +-type config() :: #{ extra_paths => [file:name_all()] |
| 20 | + , xref_defaults => [xref_default()] |
| 21 | + , dirs => [file:name_all()] |
| 22 | + }. |
| 23 | +-type warning() :: #{ filename => file:name_all() |
| 24 | + , line => non_neg_integer() |
| 25 | + , source => mfa() |
| 26 | + , target => mfa() |
| 27 | + }. |
| 28 | + |
| 29 | +-export_type([check/0, xref_default/0, config/0, warning/0]). |
| 30 | +-export([check/2]). |
| 31 | + |
| 32 | +-spec check(check(), config()) -> [warning()]. |
| 33 | +check(Check, Config) -> |
| 34 | + XrefDefaults = maps:get(xref_defaults, Config, []), |
| 35 | + Dirs = maps:get(dirs, Config, ["ebin"]), |
| 36 | + |
| 37 | + {ok, Xref} = xref:start(?MODULE), |
| 38 | + try |
| 39 | + ok = xref:set_library_path(Xref, code_path(Config)), |
| 40 | + |
| 41 | + xref:set_default(Xref, XrefDefaults), |
| 42 | + |
| 43 | + lists:foreach( |
| 44 | + fun(Dir) -> |
| 45 | + {ok, _} = xref:add_directory(Xref, Dir) |
| 46 | + end, Dirs), |
| 47 | + |
| 48 | + {ok, Results} = xref:analyze(Xref, Check), |
| 49 | + |
| 50 | + FilteredResults = filter_xref_results(Check, Results), |
| 51 | + |
| 52 | + [result_to_warning(Result) || Result <- FilteredResults] |
| 53 | + after |
| 54 | + stopped = xref:stop(Xref) |
| 55 | + end. |
| 56 | + |
| 57 | +%% =================================================================== |
| 58 | +%% Internal functions |
| 59 | +%% =================================================================== |
| 60 | + |
| 61 | +code_path(Config) -> |
| 62 | + ExtraPaths = maps:get(extra_paths, Config, []), |
| 63 | + [P || P <- code:get_path() ++ ExtraPaths, filelib:is_dir(P)]. |
| 64 | + |
| 65 | +filter_xref_results(Check, Results) -> |
| 66 | + SourceModules = |
| 67 | + lists:usort([source_module(Result) || Result <- Results]), |
| 68 | + |
| 69 | + Ignores = |
| 70 | + lists:flatmap( |
| 71 | + fun(Module) -> get_ignorelist(Module, Check) end, SourceModules), |
| 72 | + |
| 73 | + [Result || Result <- Results, |
| 74 | + not lists:member(parse_xref_result(Result), Ignores)]. |
| 75 | + |
| 76 | +source_module({Mt, _Ft, _At}) -> Mt; |
| 77 | +source_module({{Ms, _Fs, _As}, _Target}) -> Ms. |
| 78 | + |
| 79 | +%% |
| 80 | +%% Ignore behaviour functions, and explicitly marked functions |
| 81 | +%% |
| 82 | +%% Functions can be ignored by using |
| 83 | +%% -ignore_xref([{F, A}, {M, F, A}...]). |
| 84 | +get_ignorelist(Mod, Check) -> |
| 85 | + %% Get ignore_xref attribute and combine them in one list |
| 86 | + Attributes = |
| 87 | + try |
| 88 | + Mod:module_info(attributes) |
| 89 | + catch |
| 90 | + _Class:_Error -> [] |
| 91 | + end, |
| 92 | + |
| 93 | + IgnoreXref = |
| 94 | + [mfa(Mod, Value) || {ignore_xref, Values} <- Attributes, Value <- Values], |
| 95 | + |
| 96 | + BehaviourCallbacks = get_behaviour_callbacks(Check, Mod, Attributes), |
| 97 | + |
| 98 | + %% And create a flat {M,F,A} list |
| 99 | + IgnoreXref ++ BehaviourCallbacks. |
| 100 | + |
| 101 | +get_behaviour_callbacks(exports_not_used, Mod, Attributes) -> |
| 102 | + Behaviours = [Value || {behaviour, Values} <- Attributes, Value <- Values], |
| 103 | + [{Mod, {Mod, F, A}} |
| 104 | + || B <- Behaviours, {F, A} <- B:behaviour_info(callbacks)]; |
| 105 | +get_behaviour_callbacks(_Check, _Mod, _Attributes) -> |
| 106 | + []. |
| 107 | + |
| 108 | +mfa(M, {F, A}) -> {M, {M, F, A}}; |
| 109 | +mfa(M, MFA) -> {M, MFA}. |
| 110 | + |
| 111 | +parse_xref_result({{SM, _, _}, MFAt}) -> {SM, MFAt}; |
| 112 | +parse_xref_result({TM, _, _} = MFAt) -> {TM, MFAt}. |
| 113 | + |
| 114 | +result_to_warning({MFASource, MFATarget}) -> |
| 115 | + {Filename, Line} = get_source(MFASource), |
| 116 | + #{ filename => Filename |
| 117 | + , line => Line |
| 118 | + , source => MFASource |
| 119 | + , target => MFATarget |
| 120 | + }; |
| 121 | +result_to_warning(MFA) -> |
| 122 | + {Filename, Line} = get_source(MFA), |
| 123 | + #{ filename => Filename |
| 124 | + , line => Line |
| 125 | + , source => MFA |
| 126 | + }. |
| 127 | + |
| 128 | +%% |
| 129 | +%% Given a MFA, find the file and LOC where it's defined. Note that |
| 130 | +%% xref doesn't work if there is no abstract_code, so we can avoid |
| 131 | +%% being too paranoid here. |
| 132 | +%% |
| 133 | +get_source({M, F, A}) -> |
| 134 | + case code:get_object_code(M) of |
| 135 | + error -> {"", 0}; |
| 136 | + {M, Bin, _} -> find_function_source(M, F, A, Bin) |
| 137 | + end. |
| 138 | + |
| 139 | +find_function_source(M, F, A, Bin) -> |
| 140 | + AbstractCode = beam_lib:chunks(Bin, [abstract_code]), |
| 141 | + {ok, {M, [{abstract_code, {raw_abstract_v1, Code}}]}} = AbstractCode, |
| 142 | + |
| 143 | + %% Extract the original source filename from the abstract code |
| 144 | + [{attribute, 1, file, {Source, _}} | _] = Code, |
| 145 | + |
| 146 | + %% Extract the line number for a given function def |
| 147 | + Fn = [E || E <- Code, |
| 148 | + element(1, E) == function, |
| 149 | + element(3, E) == F, |
| 150 | + element(4, E) == A], |
| 151 | + |
| 152 | + case Fn of |
| 153 | + [{function, Line, F, _, _}] -> {Source, Line}; |
| 154 | + %% do not crash if functions are exported, even though they |
| 155 | + %% are not in the source. |
| 156 | + %% parameterized modules add new/1 and instance/1 for example. |
| 157 | + [] -> {Source, 0} |
| 158 | + end. |
0 commit comments