Current section
6 Versions
Jump to
Current section
6 Versions
Compare versions
6
files changed
+139
additions
-20
deletions
| @@ -28,6 +28,9 @@ whitelist = Loppers.special_forms ++ [ | |
| 28 28 | the `Loppers.Walk` module) |
| 29 29 | * Returns the AST-Fragment (including the line number if your compiler provides it) |
| 30 30 | so you can add squiggly lines to the editor at the right place. |
| 31 | + * Whitelist a module with functions with `{Application.Callbacks, :__all__}` |
| 32 | + * Whitelist a module with all child-modules and functions with `{Application.Callbacks, :__submodules_all__}` |
| 33 | + |
| 31 34 | |
| 32 35 | ## Installation |
| @@ -4,12 +4,11 @@ | |
| 4 4 | <<"Validate quoted Elixir AST against a function whitelist">>}. |
| 5 5 | {<<"elixir">>,<<"~> 1.4">>}. |
| 6 6 | {<<"files">>, |
| 7 | - [<<"lib/list.ex">>,<<"lib/loppers.ex">>,<<"lib/match.ex">>, |
| 8 | - <<"lib/validate.ex">>,<<"lib/walk.ex">>,<<"mix.exs">>,<<"README.md">>, |
| 7 | + [<<"lib">>,<<"lib/walk.ex">>,<<"lib/match.ex">>,<<"lib/list.ex">>, |
| 8 | + <<"lib/validate.ex">>,<<"lib/loppers.ex">>,<<"mix.exs">>,<<"README.md">>, |
| 9 9 | <<"LICENSE">>]}. |
| 10 10 | {<<"licenses">>,[<<"MIT">>]}. |
| 11 11 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/narrowtux/loppers">>}]}. |
| 12 | - {<<"maintainers">>,[<<"Moritz Schmale">>]}. |
| 13 12 | {<<"name">>,<<"loppers">>}. |
| 14 13 | {<<"requirements">>,[]}. |
| 15 | - {<<"version">>,<<"0.1.3">>}. |
| 14 | + {<<"version">>,<<"1.0.0">>}. |
| @@ -4,9 +4,10 @@ defmodule Loppers do | |
| 4 4 | @type error :: |
| 5 5 | {:not_allowed, ast :: term} |
| 6 6 | @type function_ref :: |
| 7 | - {module :: atom, :__all__} | |
| 7 | + {module :: atom, :__all__} | |
| 8 | + {module :: atom, :__submodules_all__} | |
| 8 9 | {module :: atom, function :: atom} | |
| 9 | - function :: atom |
| 10 | + (function :: atom) |
| 10 11 | |
| 11 12 | @type validate_option :: |
| 12 13 | {:whitelist, [function_ref]} | |
| @@ -42,7 +43,19 @@ defmodule Loppers do | |
| 42 43 | iex> blacklist = [{Enum, :map_reduce}] |
| 43 44 | iex> quoted = quote do Enum.map_reduce([], nil, &({&1, nil})) end |
| 44 45 | iex> Loppers.validate(quoted, [whitelist: whitelist, blacklist: blacklist]) |
| 45 | - {:error, [not_allowed: {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map_reduce]}, [], [[], nil, {:&, [], [{{:&, [], [1]}, nil}]}]}]} |
| 46 | + {:error, [ |
| 47 | + not_allowed: {{:., [parent_modules: []], |
| 48 | + [ |
| 49 | + {:__aliases__, [parent_modules: [], alias: false], [:Enum]}, |
| 50 | + :map_reduce |
| 51 | + ]}, [parent_modules: []], |
| 52 | + [ |
| 53 | + [], |
| 54 | + nil, |
| 55 | + {:&, [parent_modules: []], |
| 56 | + [{{:&, [parent_modules: []], [1]}, nil}]} |
| 57 | + ]} |
| 58 | + ]} |
| 46 59 | |
| 47 60 | ## Options |
| 48 61 | |
| @@ -55,6 +68,7 @@ defmodule Loppers do | |
| 55 68 | def validate(quoted, opts) do |
| 56 69 | {quoted, _acc} = Walk.walk(quoted, {%{}, [{Kernel, []}]}, &Walk.gen_meta/2) |
| 57 70 | {quoted, _acc} = Walk.walk(quoted, [], &Walk.module_functions/2) |
| 71 | + {quoted, _acc} = Walk.recurse(quoted, %{}, &Walk.track_parent_modules/2) |
| 58 72 | whitelist = Keyword.get(opts, :whitelist, nil) |
| 59 73 | blacklist = Keyword.get(opts, :blacklist, []) |
| 60 74 | acc = Validate.validate(quoted, [], fn {_, meta, _} = ast, acc -> |
| @@ -100,7 +114,8 @@ defmodule Loppers do | |
| 100 114 | [ |
| 101 115 | {Kernel, :@}, |
| 102 116 | {Kernel, :defmodule}, |
| 103 | - {Kernel, :def} |
| 117 | + {Kernel, :def}, |
| 118 | + :when |
| 104 119 | ] |
| 105 120 | end |
| 106 121 | |
| @@ -110,8 +125,15 @@ defmodule Loppers do | |
| 110 125 | Without those it's going to be hard to write any elixir code. |
| 111 126 | """ |
| 112 127 | def special_forms do |
| 113 | - [:functions, :macros] |
| 114 | - |> Enum.flat_map(&Kernel.SpecialForms.__info__/1) |
| 115 | - |> Keyword.keys() |
| 128 | + special_forms = |
| 129 | + [:functions, :macros] |
| 130 | + |> Enum.flat_map(&Kernel.SpecialForms.__info__/1) |
| 131 | + |> Keyword.keys() |
| 132 | + special_forms ++ [ |
| 133 | + :{}, |
| 134 | + :doc, |
| 135 | + :->, |
| 136 | + {Kernel, :is_binary} |
| 137 | + ] |
| 116 138 | end |
| 117 139 | end |
| @@ -13,6 +13,23 @@ defmodule Loppers.Match do | |
| 13 13 | # ABC.fun(1, 2, 3) |
| 14 14 | # {{:., [], [{:__aliases__, [alias: false], [:ABC]}, :fun]}, [], [1, 2, 3]} |
| 15 15 | |
| 16 | + # Handling :__submodules_all__ |
| 17 | + def matches?({{:., _dot_meta, [{:__aliases__, aliases, called_as}, _called_fn]}, fn_meta, _arguments}, {mod, :__submodules_all__}) do |
| 18 | + |
| 19 | + called_module = case Keyword.get(aliases, :alias, false) do |
| 20 | + false -> |
| 21 | + parent_modules = Keyword.get(fn_meta, :parent_modules, []) |
| 22 | + list_to_module(parent_modules ++ called_as) |
| 23 | + |
| 24 | + aliased_module -> |
| 25 | + aliased_module |
| 26 | + end |
| 27 | + |
| 28 | + # Check if "Module.Child" has the Prefix for "Module." |
| 29 | + String.starts_with?("#{called_module}", "#{mod}.") |
| 30 | + end |
| 31 | + |
| 32 | + |
| 16 33 | def matches?({{:., _dot_meta, [{:__aliases__, aliases, called_as}, called_fn]}, _fn_meta, _arguments}, {mod, list_fn}) |
| 17 34 | when called_fn == list_fn or list_fn == :__all__ do |
| 18 35 | called_module = case Keyword.get(aliases, :alias, false) do |
| @@ -40,7 +57,6 @@ defmodule Loppers.Match do | |
| 40 57 | mod == called_module && (list_fn == called_fn || list_fn == :__all__) |
| 41 58 | end |
| 42 59 | |
| 43 | - |
| 44 60 | def matches?(_, _) do |
| 45 61 | false |
| 46 62 | end |
| @@ -1,6 +1,11 @@ | |
| 1 1 | defmodule Loppers.Walk do |
| 2 2 | import Loppers.Match, only: [list_to_module: 1] |
| 3 3 | |
| 4 | + @doc """ |
| 5 | + Walking a given ast filling the accumulator using a walker-function. |
| 6 | + |
| 7 | + Walker signature needs to be `walker.(ast, acc)` |
| 8 | + """ |
| 4 9 | def walk({_, _, _} = ast, acc, walker) do |
| 5 10 | {{fun, meta, args}, acc} = walker.(ast, acc) |
| 6 11 | {args, _acc} = reduce_args(args, acc, walker) |
| @@ -9,11 +14,6 @@ defmodule Loppers.Walk do | |
| 9 14 | {ast, acc} |
| 10 15 | end |
| 11 16 | |
| 12 | - def walk([{key, {_, _, _} = call}], acc, walker) do |
| 13 | - {call, acc} = walk(call, acc, walker) |
| 14 | - {[{key, call}], acc} |
| 15 | - end |
| 16 | - |
| 17 17 | def walk(primitive, acc, _walker) |
| 18 18 | when is_atom(primitive) or |
| 19 19 | is_number(primitive) or |
| @@ -32,6 +32,7 @@ defmodule Loppers.Walk do | |
| 32 32 | reduce_args(list, acc, walker) |
| 33 33 | end |
| 34 34 | |
| 35 | + @doc "Helper to call walker callback for each args." |
| 35 36 | def reduce_args(nil, acc, _) do |
| 36 37 | {nil, acc} |
| 37 38 | end |
| @@ -39,6 +40,76 @@ defmodule Loppers.Walk do | |
| 39 40 | Enum.map_reduce(args, acc, &walk(&1, &2, walker)) |
| 40 41 | end |
| 41 42 | |
| 43 | + |
| 44 | + # Testing walk() or recurse() |
| 45 | + #def inspect(a, b), do: {a, b} |> IO.inspect(label: "inspect") |
| 46 | + |
| 47 | + |
| 48 | + |
| 49 | + def track_parent_modules({:defmodule, meta_defmodule, [{:__aliases__, aliases_meta, parent_modules} | rest]}, acc) do |
| 50 | + |
| 51 | + # Add defined module to parent_modules in acc for whole branch. |
| 52 | + |
| 53 | + ast = {:defmodule, meta_defmodule, [{:__aliases__, aliases_meta, parent_modules} | rest]} |
| 54 | + acc = Map.update(acc, :parent_modules, parent_modules, &(&1 ++ parent_modules)) |
| 55 | + |
| 56 | + {ast, acc} |
| 57 | + end |
| 58 | + |
| 59 | + def track_parent_modules({fun, meta, args}, acc) do |
| 60 | + |
| 61 | + # Add parent_modules to meta of function call from acc. |
| 62 | + meta = Keyword.put(meta, :parent_modules, Map.get(acc, :parent_modules, [])) |
| 63 | + |
| 64 | + {{fun, meta, args}, acc} |
| 65 | + end |
| 66 | + |
| 67 | + def track_parent_modules(ast, acc), do: {ast, acc} |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | + |
| 72 | + @doc """ |
| 73 | + Recursing into the AST. Difference to walk() is that the accumulator will only be changed for current branch. |
| 74 | + """ |
| 75 | + def recurse({_, _, _} = ast, acc, callback) do |
| 76 | + {{fun, meta, args}, acc} = callback.(ast, acc) |
| 77 | + |
| 78 | + {args, _acc} = recurse(args, acc, callback) |
| 79 | + {fun, _acc} = recurse(fun, acc, callback) |
| 80 | + |
| 81 | + {{fun, meta, args}, acc} |
| 82 | + end |
| 83 | + |
| 84 | + |
| 85 | + def recurse({key, value}, acc, callback) do |
| 86 | + {{key, value}, acc} = callback.({key, value}, acc) |
| 87 | + {key, _acc} = recurse(key, acc, callback) |
| 88 | + {value, _acc} = recurse(value, acc, callback) |
| 89 | + {{key, value}, acc} |
| 90 | + end |
| 91 | + |
| 92 | + |
| 93 | + def recurse(ast, acc, callback) when is_list(ast) do |
| 94 | + {Enum.map(ast, fn(ast_entry) -> |
| 95 | + ast_entry |> recurse(acc, callback) |> elem(0) |
| 96 | + end), acc} |
| 97 | + end |
| 98 | + |
| 99 | + def recurse(primitive, acc, callback) do |
| 100 | + callback.(primitive, acc) |
| 101 | + end |
| 102 | + |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | + |
| 107 | + |
| 108 | + |
| 109 | + |
| 110 | + @doc """ |
| 111 | + Walk callback for collecting metadata like aliases and imports. |
| 112 | + """ |
| 42 113 | def gen_meta({:alias, _, args} = ast, {aliases, imports}) do |
| 43 114 | {aka, opts} = case args do |
| 44 115 | [aka, opts] -> {aka, opts} |
| @@ -87,6 +158,9 @@ defmodule Loppers.Walk do | |
| 87 158 | |
| 88 159 | @defs ~w[def defp defmacro defmacrop]a |
| 89 160 | |
| 161 | + @doc """ |
| 162 | + Walk callback for collecting metadata like aliases and imports. |
| 163 | + """ |
| 90 164 | def module_functions({:defmodule, _, [_aliases, args]} = module, _current_functions) do |
| 91 165 | contents = case Keyword.get(args, :do, {:__block__, [], []}) do |
| 92 166 | {:__block__, _, contents} -> contents |
| @@ -98,7 +172,10 @@ defmodule Loppers.Walk do | |
| 98 172 | {defs, _, _} when defs in @defs -> true |
| 99 173 | _ -> false |
| 100 174 | end) |
| 101 | - |> Enum.map(fn {_, _, [{name, _, _} | _]} -> name end) |
| 175 | + |> Enum.map(fn |
| 176 | + {_def, _ctx1, [{:when, _ctx2, [{name, _ctx3, _args} | _]} | _]} -> name |
| 177 | + {_def, _ctx1, [{name, _ctx2, _args} | _]} -> name |
| 178 | + end) |
| 102 179 | |
| 103 180 | {module, functions} |
| 104 181 | end |
| @@ -116,6 +193,8 @@ defmodule Loppers.Walk do | |
| 116 193 | |
| 117 194 | def module_functions(ast, acc), do: {ast, acc} |
| 118 195 | |
| 196 | + |
| 197 | + @doc "Helper to check if a function is imported in a given module." |
| 119 198 | def function_imported?({module, opts}, function, arity) do |
| 120 199 | excluded = {function, arity} in Keyword.get(opts, :except, []) |
| 121 200 | included = case Keyword.get(opts, :only) do |
Loading more files…