%%% @doc Utility functions -module(hank_utils). %% To allow erl_syntax:syntaxTree/0 type spec -elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z][a-z0-9]*_?)*$"}}]). -export([macro_arity/1, macro_name/1, parse_macro_name/1, macro_definition_name/1, attr_name/1, ast_has_attrs/2, node_has_attrs/2, attr_args/3, attr_args/2, attr_args_concrete/2, implements_behaviour/1, paths_match/2]). %% @doc Get the macro arity of given Node -spec macro_arity(erl_syntax:syntaxTree()) -> none | pos_integer(). macro_arity(Node) -> case erl_syntax:macro_arguments(Node) of none -> none; Args -> length(Args) end. %% @doc Get the parsed macro name of given Node -spec macro_name(erl_syntax:syntaxTree()) -> string(). macro_name(Node) -> parse_macro_name(erl_syntax:macro_name(Node)). %% @doc Parse the given Node macro name -spec parse_macro_name(erl_syntax:syntaxTree()) -> string(). parse_macro_name(Node) -> case erl_syntax:type(Node) of variable -> erl_syntax:variable_literal(Node); atom -> erl_syntax:atom_name(Node) end. %% @doc Get the macro definition name and arity of a given Macro Node. -spec macro_definition_name(erl_syntax:syntaxTree()) -> {string(), integer() | atom()}. macro_definition_name(Node) -> [MacroNameNode | _] = erl_syntax:attribute_arguments(Node), case erl_syntax:type(MacroNameNode) of application -> Operator = erl_syntax:application_operator(MacroNameNode), MacroName = parse_macro_name(Operator), MacroArity = length(erl_syntax:application_arguments(MacroNameNode)), {MacroName, MacroArity}; variable -> {erl_syntax:variable_literal(MacroNameNode), none}; atom -> {erl_syntax:atom_literal(MacroNameNode), none} end. %% @doc Macro dodging version of erl_syntax:attribute_name/1 -spec attr_name(erl_syntax:syntaxTree()) -> atom(). attr_name(Node) -> N = erl_syntax:attribute_name(Node), try erl_syntax:concrete(N) catch _:_ -> N end. %% @doc Whether the given AST nodes list %% has defined the given AttrNames attribute names or not -spec ast_has_attrs(erl_syntax:forms(), atom() | [atom()]) -> boolean(). ast_has_attrs(AST, AttrName) when not is_list(AttrName) -> ast_has_attrs(AST, [AttrName]); ast_has_attrs(AST, AttrNames) -> lists:any(fun(Node) -> node_has_attrs(Node, AttrNames) end, AST). %% @doc Whether the given Node node %% has defined the given AttrNames attribute names or not -spec node_has_attrs(erl_syntax:syntaxTree(), atom() | [atom()]) -> boolean(). node_has_attrs(Node, AttrName) when not is_list(AttrName) -> node_has_attrs(Node, [AttrName]); node_has_attrs(Node, AttrNames) -> erl_syntax:type(Node) == attribute andalso lists:member(attr_name(Node), AttrNames). %% @doc Extract attribute arguments from given AST nodes list %% whose attribute name is AttrName and apply MapFunc to every element -spec attr_args(erl_syntax:forms(), atom() | [atom()], function()) -> [term()]. attr_args(AST, AttrName, MapFunc) when not is_list(AttrName) -> attr_args(AST, [AttrName], MapFunc); attr_args(AST, AttrNames, MapFunc) -> [MapFunc(AttrArg) || Node <- AST, node_has_attrs(Node, AttrNames), AttrArg <- erl_syntax:attribute_arguments(Node)]. %% @doc Same as attr_args/3 but with a dummy default MapFunc -spec attr_args(erl_syntax:forms(), atom() | [atom()]) -> [term()]. attr_args(AST, AttrName) -> attr_args(AST, AttrName, fun(AttrArg) -> AttrArg end). %% @doc Same as attr_args/3 but calling erl_syntax:concrete/1 for each element -spec attr_args_concrete(erl_syntax:forms(), atom() | [atom()]) -> [term()]. attr_args_concrete(AST, AttrName) -> attr_args(AST, AttrName, fun erl_syntax:concrete/1). %% @doc Whether the given AST nodes list has behaviours implemented or not -spec implements_behaviour(erl_syntax:forms()) -> boolean(). implements_behaviour(AST) -> ast_has_attrs(AST, [behaviour, behavior]). %% @doc Whether one of the given paths is contained inside the other one or not. %% It doesn't matter which one is contained at which other. %% Verifies if FilePath and IncludePath refer both to the same file. %% Note that we can't just compare both filename:absname's here, since we %% don't really know what is the absolute path of the file referred by %% the include directive. -spec paths_match(string(), string()) -> boolean(). paths_match(IncludePath, IncludePath) -> % The path used in the include directive is exactly the file path true; paths_match(FilePath, IncludePath) -> % We remove relative paths because FilePath will not be a relative path and, % in any case, the paths will be relative to something that we don't know. % % Note that this might result in some false negatives. % For instance, Hank may think that lib/app1/include/header.hrl is used % if lib/app2/src/module.erl contains -include("header.hrl"). % when, in reality, module is including lib/app2/include/header.erl % That should be an extremely edge scenario and Hank never promised to find % ALL the dead code, anyway. It just promised that *if* it finds something, % that's dead code, 100% sure. compare_paths(clean_path(FilePath), clean_path(IncludePath)). %% @doc Whether one of the given paths is contained inside the other one or not %% It doesn't matter which one is contained at which other compare_paths({PathA, LenA}, {PathB, LenB}) when LenA > LenB -> PathB == string:find(PathA, PathB, trailing); compare_paths({PathA, _}, {PathB, _}) -> PathA == string:find(PathB, PathA, trailing); compare_paths(PathA, PathB) -> compare_paths({PathA, length(PathA)}, {PathB, length(PathB)}). %% @doc Remove backtrailing "../" and "./" from a given Path clean_path(Path) -> unicode:characters_to_list( string:replace( string:replace(Path, "../", "", all), "./", "", all)).