From 6fc81dfc7686badca5a6f955bbcb9228cf55c46e Mon Sep 17 00:00:00 2001 From: Risson Date: Wed, 9 Aug 2023 16:10:42 +0200 Subject: [PATCH] move module call to common interpreter --- .../interpreter/action_interpreter.ex | 64 ------------------- .../interpreter/common_interpreter.ex | 53 +++++++++------ .../interpreter/condition_interpreter.ex | 34 ++++++++-- .../interpreter/function_interpreter.ex | 52 +++++---------- .../contracts/interpreter/library.ex | 34 ++++++++-- .../library/{ => common}/contract.ex | 2 +- .../condition_interpreter_test.exs | 14 ++++ .../interpreter/function_interpreter_test.exs | 14 ++++ .../interpreter/library/contract_test.exs | 2 +- 9 files changed, 134 insertions(+), 135 deletions(-) rename lib/archethic/contracts/interpreter/library/{ => common}/contract.ex (98%) diff --git a/lib/archethic/contracts/interpreter/action_interpreter.ex b/lib/archethic/contracts/interpreter/action_interpreter.ex index 322a720645..093ffcc606 100644 --- a/lib/archethic/contracts/interpreter/action_interpreter.ex +++ b/lib/archethic/contracts/interpreter/action_interpreter.ex @@ -8,7 +8,6 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do alias Archethic.Contracts.Interpreter alias Archethic.Contracts.Interpreter.ASTHelper, as: AST alias Archethic.Contracts.Interpreter.CommonInterpreter - alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.Scope # # Module `Contract` is handled differently @@ -149,14 +148,6 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do # | .__/|_| \___| \_/\_/ \__,_|_|_|\_\ # |_| # ---------------------------------------------------------------------- - # autorize the use of Contract module - defp prewalk( - node = {:__aliases__, _, [atom: "Contract"]}, - acc - ) do - {node, acc} - end - # # autorize the use of modules whitelisted # defp prewalk(node = {:__aliases__, _, [atom: module_name]}, acc) # when module_name in @modules_whitelisted, @@ -177,61 +168,6 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do # | .__/ \___/|___/\__| \_/\_/ \__,_|_|_|\_\ # |_| # ---------------------------------------------------------------------- - # handle the Contract module - # here we have 2 things to do: - # - feed the `next_transaction` as the 1st function parameter - # - update the `next_transaction` in scope - defp postwalk( - node = - {{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, args}, - acc - ) do - absolute_module_atom = Archethic.Contracts.Interpreter.Library.Contract - - # check function exists - unless Library.function_exists?(absolute_module_atom, function_name) do - throw({:error, node, "unknown function"}) - end - - # check function is available with given arity - # (we add 1 to arity because we add the contract as 1st argument implicitely) - unless Library.function_exists?(absolute_module_atom, function_name, length(args) + 1) do - throw({:error, node, "invalid function arity"}) - end - - function_atom = String.to_existing_atom(function_name) - - # check the type of the args, and allow custom function call as args - unless absolute_module_atom.check_types(function_atom, args) do - throw({:error, node, "invalid function arguments"}) - end - - new_node = - quote do - # mark the next_tx as dirty - Scope.update_global(["next_transaction_changed"], fn _ -> true end) - - # call the function with the next_transaction as the 1st argument - # and update it in the scope - Scope.update_global( - ["next_transaction"], - &apply(unquote(absolute_module_atom), unquote(function_atom), [&1 | unquote(args)]) - ) - end - - {new_node, acc} - end - - # # handle modules whitelisted (but not Contract) - # defp postwalk( - # node = - # {{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, _function_name}]}, _, - # _args}, - # acc - # ) - # when module_name in @modules_whitelisted do - # {CommonInterpreter.module_call(node, common: false), acc} - # end # --------------- catch all ------------------- defp postwalk(node, acc) do diff --git a/lib/archethic/contracts/interpreter/common_interpreter.ex b/lib/archethic/contracts/interpreter/common_interpreter.ex index 8f7b75954b..34945358b6 100644 --- a/lib/archethic/contracts/interpreter/common_interpreter.ex +++ b/lib/archethic/contracts/interpreter/common_interpreter.ex @@ -10,8 +10,6 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.Scope - @modules_whitelisted Library.list_common_modules() - # ---------------------------------------------------------------------- # _ _ # _ __ _ __ _____ ____ _| | | __ @@ -96,9 +94,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do def prewalk(node = {:., _, [{:__aliases__, _, _}, _]}, acc), do: {node, acc} # whitelisted modules - def prewalk(node = {:__aliases__, _, [atom: module_name]}, acc) - when module_name in @modules_whitelisted, - do: {node, acc} + def prewalk(node = {:__aliases__, _, [atom: _module_name]}, acc), + do: {node, acc} # internal modules (Process/Scope/Kernel) def prewalk(node = {:__aliases__, _, [atom]}, acc) when is_atom(atom), do: {node, acc} @@ -290,11 +287,10 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # common modules call def postwalk( node = - {{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, _function_name}]}, _, + {{:., _meta, [{:__aliases__, _, [atom: _module_name]}, {:atom, _function_name}]}, _, _args}, acc - ) - when module_name in @modules_whitelisted do + ) do {module_call(node), acc} end @@ -375,25 +371,23 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do def module_call( node = - {{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args}, - opts \\ [] - ) do - absolute_module_name = - if Keyword.get(opts, :common, true) do - "Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}" - else - "Elixir.Archethic.Contracts.Interpreter.Library.#{module_name}" + {{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args} + ) + when is_binary(module_name) do + {absolute_module_atom, _} = + case Library.get_module(module_name) do + {:ok, module_atom, module_atom_impl} -> {module_atom, module_atom_impl} + {:error, message} -> throw({:error, node, message}) end - absolute_module_atom = Code.ensure_loaded!(String.to_existing_atom(absolute_module_name)) - # check function exists unless Library.function_exists?(absolute_module_atom, function_name) do throw({:error, node, "unknown function"}) end # check function is available with given arity - unless Library.function_exists?(absolute_module_atom, function_name, length(args)) do + unless Library.function_exists?(absolute_module_atom, function_name, length(args)) or + Library.function_exists?(absolute_module_atom, function_name, length(args) + 1) do throw({:error, node, "invalid function arity"}) end @@ -405,9 +399,26 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do throw({:error, node, "invalid function arguments"}) end - meta_with_alias = Keyword.put(meta, :alias, absolute_module_atom) + new_node = + if module_name == "Contract" do + quote do + # mark the next_tx as dirty + Scope.update_global(["next_transaction_changed"], fn _ -> true end) + + # call the function with the next_transaction as the 1st argument + # and update it in the scope + Scope.update_global( + ["next_transaction"], + &apply(unquote(absolute_module_atom), unquote(function_atom), [&1 | unquote(args)]) + ) + end + else + meta_with_alias = Keyword.put(meta, :alias, absolute_module_atom) + + {{:., meta, [{:__aliases__, meta_with_alias, [module_atom]}, function_atom]}, meta, args} + end - {{:., meta, [{:__aliases__, meta_with_alias, [module_atom]}, function_atom]}, meta, args} + new_node end # ---------------------------------------------------------------------- diff --git a/lib/archethic/contracts/interpreter/condition_interpreter.ex b/lib/archethic/contracts/interpreter/condition_interpreter.ex index 3612e50464..fd7f038be4 100644 --- a/lib/archethic/contracts/interpreter/condition_interpreter.ex +++ b/lib/archethic/contracts/interpreter/condition_interpreter.ex @@ -8,7 +8,6 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do alias Archethic.Contracts.Interpreter.ASTHelper, as: AST alias Archethic.Contracts.Interpreter - @modules_whitelisted Library.list_common_modules() @condition_fields Conditions.__struct__() |> Map.keys() |> Enum.reject(&(&1 == :__struct__)) @@ -125,6 +124,27 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do # | .__/|_| \___| \_/\_/ \__,_|_|_|\_\ # |_| # ---------------------------------------------------------------------- + + defp prewalk( + _subject, + node = + {{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, _}, + acc + ) do + {_absolute_module_atom, module_impl} = + case Library.get_module(module_name) do + {:ok, module_atom, module_atom_impl} -> {module_atom, module_atom_impl} + {:error, message} -> throw({:error, node, message}) + end + + function_atom = String.to_existing_atom(function_name) + + if module_impl.tagged_with?(function_atom, :write_contract), + do: throw({:error, node, "Write contract functions are not allowed in condition block"}) + + CommonInterpreter.prewalk(node, acc) + end + defp prewalk(_subject, node, acc) do CommonInterpreter.prewalk(node, acc) end @@ -182,15 +202,15 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do node = {{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args}, acc - ) - when module_name in @modules_whitelisted do + ) do # if function exist with arity => node arity = length(args) - absolute_module_atom = - String.to_existing_atom( - "Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}" - ) + {absolute_module_atom, _} = + case Library.get_module(module_name) do + {:ok, module_atom, module_atom_impl} -> {module_atom, module_atom_impl} + {:error, message} -> throw({:error, node, message}) + end new_node = cond do diff --git a/lib/archethic/contracts/interpreter/function_interpreter.ex b/lib/archethic/contracts/interpreter/function_interpreter.ex index 1c60350ced..32c5863b3d 100644 --- a/lib/archethic/contracts/interpreter/function_interpreter.ex +++ b/lib/archethic/contracts/interpreter/function_interpreter.ex @@ -1,5 +1,6 @@ defmodule Archethic.Contracts.Interpreter.FunctionInterpreter do @moduledoc false + alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter alias Archethic.Contracts.Interpreter.ASTHelper, as: AST alias Archethic.Contracts.Interpreter.Scope @@ -97,49 +98,30 @@ defmodule Archethic.Contracts.Interpreter.FunctionInterpreter do # | .__/|_| \___| \_/\_/ \__,_|_|_|\_\ # |_| # ---------------------------------------------------------------------- - # Ban access to Contract module defp prewalk( node = - {{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, _}, + {{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, _}, acc, - _visibility + is_internal? ) do - absolute_module_atom = - Code.ensure_loaded!( - String.to_existing_atom("Elixir.Archethic.Contracts.Interpreter.Library.Contract") - ) + {_absolute_module_atom, module_impl} = + case Library.get_module(module_name) do + {:ok, module_atom, module_atom_impl} -> {module_atom, module_atom_impl} + {:error, message} -> throw({:error, node, message}) + end - if absolute_module_atom.tagged_with?(String.to_atom(function_name), :write_contract) do - throw({:error, node, "Write contract functions are not allowed in custom functions"}) - end + function_atom = String.to_existing_atom(function_name) - CommonInterpreter.prewalk(node, acc) - end + if is_internal? do + if module_impl.tagged_with?(function_atom, :io), + do: throw({:error, node, "IO function calls not allowed in public functions"}) - defp prewalk( - node = - {{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, _}, - acc, - true - ) do - absolute_module_atom = - Code.ensure_loaded!( - String.to_existing_atom( - "Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}" - ) - ) - - absolute_module_atom = - try do - %Knigge.Options{default: module} = Knigge.options!(absolute_module_atom) - module - rescue - _ -> - absolute_module_atom + if module_impl.tagged_with?(function_atom, :write_contract), + do: throw({:error, node, "Write contract functions are not allowed in custom functions"}) + else + if module_impl.tagged_with?(function_atom, :write_contract) do + throw({:error, node, "Write contract functions are not allowed in custom functions"}) end - - if absolute_module_atom.tagged_with?(String.to_atom(function_name), :io) do - throw({:error, node, "IO function calls not allowed in public functions"}) end CommonInterpreter.prewalk(node, acc) diff --git a/lib/archethic/contracts/interpreter/library.ex b/lib/archethic/contracts/interpreter/library.ex index 6b5d84a27d..2c061753ba 100644 --- a/lib/archethic/contracts/interpreter/library.ex +++ b/lib/archethic/contracts/interpreter/library.ex @@ -32,13 +32,35 @@ defmodule Archethic.Contracts.Interpreter.Library do end @doc """ - Returns the list of common modules available. - - This function is also used to create the atoms of the modules + Gets a module and its imlpementation when there is one """ - def list_common_modules() do - [:Map, :List, :Regex, :Json, :Time, :Chain, :Crypto, :Token, :String, :Code, :Http] - |> Enum.map(&Atom.to_string/1) + @spec get_module(binary()) :: {:ok, module(), module()} | {:error, binary()} + def get_module(module_name) do + try do + case Code.ensure_loaded( + String.to_existing_atom( + "Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}" + ) + ) do + {:module, module_atom} -> + module_atom_impl = + try do + %Knigge.Options{default: module} = Knigge.options!(module_atom) + module + rescue + _ -> + module_atom + end + + {:ok, module_atom, module_atom_impl} + + _ -> + {:error, "Module #{module_name} not found"} + end + rescue + _ -> + {:error, "Module #{module_name} not found"} + end end # ---------------------------------------- diff --git a/lib/archethic/contracts/interpreter/library/contract.ex b/lib/archethic/contracts/interpreter/library/common/contract.ex similarity index 98% rename from lib/archethic/contracts/interpreter/library/contract.ex rename to lib/archethic/contracts/interpreter/library/common/contract.ex index 21f3e235c8..4997f028e6 100644 --- a/lib/archethic/contracts/interpreter/library/contract.ex +++ b/lib/archethic/contracts/interpreter/library/common/contract.ex @@ -1,4 +1,4 @@ -defmodule Archethic.Contracts.Interpreter.Library.Contract do +defmodule Archethic.Contracts.Interpreter.Library.Common.Contract do @moduledoc """ We are delegating to the legacy transaction statements. This is fine as long as we don't need to change anything. diff --git a/test/archethic/contracts/interpreter/condition_interpreter_test.exs b/test/archethic/contracts/interpreter/condition_interpreter_test.exs index 96cc624634..2a1e81b8d7 100644 --- a/test/archethic/contracts/interpreter/condition_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/condition_interpreter_test.exs @@ -102,6 +102,20 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert is_tuple(ast) && :ok == Macro.validate(ast) end + test "should not parse :write_contract functions" do + code = ~s""" + condition transaction: [ + uco_transfers: Contract.set_content "content" + ] + """ + + assert {:error, _, _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse([]) + end + test "parse custom functions" do code = ~s""" condition transaction: [ diff --git a/test/archethic/contracts/interpreter/function_interpreter_test.exs b/test/archethic/contracts/interpreter/function_interpreter_test.exs index 2f3ec78e53..5fcdc8c0c4 100644 --- a/test/archethic/contracts/interpreter/function_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/function_interpreter_test.exs @@ -107,6 +107,20 @@ defmodule Archethic.Contracts.Interpreter.FunctionInterpreterTest do |> FunctionInterpreter.parse([]) end + test "should return an error if module is unknown" do + code = ~S""" + export fun test_public do + Hello.world() + end + """ + + assert {:error, _, "Module Hello not found"} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + test "should be able to use IO functions in private function" do code = ~S""" fun test_private do diff --git a/test/archethic/contracts/interpreter/library/contract_test.exs b/test/archethic/contracts/interpreter/library/contract_test.exs index 5e12f37f55..cac2f814b4 100644 --- a/test/archethic/contracts/interpreter/library/contract_test.exs +++ b/test/archethic/contracts/interpreter/library/contract_test.exs @@ -7,7 +7,7 @@ defmodule Archethic.Contracts.Interpreter.Library.ContractTest do use ArchethicCase import ArchethicCase - alias Archethic.Contracts.Interpreter.Library.Contract + alias Archethic.Contracts.Interpreter.Library.Common.Contract alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData