Skip to content

Commit

Permalink
Ignore module-level calls in assert_call (#409)
Browse files Browse the repository at this point in the history
* Add failing tests

* Adjust existing tests

* Ignore module-level calls in assert_call

* Add tests for dna-encoding
  • Loading branch information
angelikatyborska authored Nov 21, 2023
1 parent f1c76d0 commit bf1d881
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 25 deletions.
76 changes: 57 additions & 19 deletions lib/elixir_analyzer/exercise_test/assert_call/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
acc = %{
in_module: nil,
in_function_def: nil,
in_function_modules: %{},
in_macro_def: nil,
in_function_or_macro_modules: %{},
modules_in_scope: %{[:Kernel] => Kernel.module_info(:exports)},
found_called: false,
called_fn: called_fn,
Expand Down Expand Up @@ -91,9 +92,17 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
|> track_imports(node)

cond do
module_def?(node) -> {node, %{acc | in_module: extract_module_name(node)}}
function_def?(node) -> {node, %{acc | in_function_def: extract_function_name(node)}}
true -> {node, acc}
module_def?(node) ->
{node, %{acc | in_module: extract_module_name(node)}}

function_def?(node) ->
{node, %{acc | in_function_def: extract_function_or_macro_name(node)}}

macro_def?(node) ->
{node, %{acc | in_macro_def: extract_function_or_macro_name(node)}}

true ->
{node, acc}
end
end

Expand All @@ -105,9 +114,17 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
{node, acc} = find(node, acc)

cond do
module_def?(node) -> {node, %{acc | in_module: nil}}
function_def?(node) -> {node, %{acc | in_function_def: nil, in_function_modules: %{}}}
true -> {node, acc}
module_def?(node) ->
{node, %{acc | in_module: nil}}

function_def?(node) ->
{node, %{acc | in_function_def: nil, in_function_or_macro_modules: %{}}}

module_def?(node) ->
{node, %{acc | in_macro_def: nil, in_function_or_macro_modules: %{}}}

true ->
{node, acc}
end
end

Expand All @@ -122,22 +139,28 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
%{
in_module: module,
modules_in_scope: modules_in_scope,
in_function_modules: in_function_modules,
in_function_or_macro_modules: in_function_or_macro_modules,
called_fn: called_fn,
calling_fn: calling_fn,
in_function_def: name,
function_call_tree: tree
} = acc
) do
modules = Map.merge(modules_in_scope, in_function_modules)
modules = Map.merge(modules_in_scope, in_function_or_macro_modules)

acc = track_all_functions(acc, node)

match_called_fn? =
matching_function_call?(node, called_fn, modules, module) and
not in_function?({module, name}, called_fn)

match_calling_fn? = in_function?({module, name}, calling_fn) or is_nil(calling_fn)
match_calling_fn? =
if calling_fn do
in_function?({module, name}, calling_fn)
else
# in any calling function or macro? (ignoring module-level calls)
acc.in_function_def || acc.in_macro_def
end

cond do
match_called_fn? and match_calling_fn? ->
Expand Down Expand Up @@ -279,14 +302,25 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
def function_def?(_node), do: false

@doc """
get the name of a function from a function definition node
node is a macro definition
"""
def extract_function_name({def_type, _, [{:when, _, [{name, _, _} | _]}, [{:do, _} | _]]})
when is_atom(name) and def_type in ~w[def defp]a,
def macro_def?({def_type, _, [_, [{:do, _} | _]]}) when def_type in ~w[defmacro defmacrop]a do
true
end

def macro_def?(_node), do: false

@doc """
get the name of a function or macro from a definition node
"""
def extract_function_or_macro_name(
{def_type, _, [{:when, _, [{name, _, _} | _]}, [{:do, _} | _]]}
)
when is_atom(name) and def_type in ~w[def defp defmacro defmacrop]a,
do: name

def extract_function_name({def_type, _, [{name, _, _}, [{:do, _} | _]]})
when is_atom(name) and def_type in ~w[def defp]a,
def extract_function_or_macro_name({def_type, _, [{name, _, _}, [{:do, _} | _]]})
when is_atom(name) and def_type in ~w[def defp defmacro defmacrop]a,
do: name

@doc """
Expand Down Expand Up @@ -441,8 +475,12 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
# track modules
defp track_modules(acc, module_paths) do
Enum.reduce(module_paths, acc, fn {alias, full_path}, acc ->
if acc.in_function_def,
do: %{acc | in_function_modules: Map.put(acc.in_function_modules, alias, full_path)},
if acc.in_function_def || acc.in_macro_def,
do: %{
acc
| in_function_or_macro_modules:
Map.put(acc.in_function_or_macro_modules, alias, full_path)
},
else: %{acc | modules_in_scope: Map.put(acc.modules_in_scope, alias, full_path)}
end)
end
Expand All @@ -454,12 +492,12 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
in_module: module,
in_function_def: name,
modules_in_scope: modules_in_scope,
in_function_modules: in_function_modules
in_function_or_macro_modules: in_function_or_macro_modules
} = acc,
{_, _, args} = function
)
when not is_nil(name) and is_list(args) do
module_aliases = Map.merge(modules_in_scope, in_function_modules)
module_aliases = Map.merge(modules_in_scope, in_function_or_macro_modules)

{module_called, name_called} =
case function do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.ErlangTest do
def function() do
end

:rand.normal()
defp not_the_function(), do: :rand.normal()
end,
defmodule AssertCallVerification do
def function() do
import :rand, except: [normal: 0]
end

:rand.normal()
defp not_the_function(), do: :rand.normal()
end
]
end
Expand Down Expand Up @@ -121,7 +121,7 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.ErlangTest do
def function() do
end

:rand.uniform()
defp not_the_function(), do: :rand.uniform()
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
defmodule ElixirAnalyzer.ExerciseTest.AssertCall.ModuleLevelCallTest do
use ElixirAnalyzer.ExerciseTestCase,
exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.ModuleLevelCall

test_exercise_analysis "calls in function bodies count", comments: [] do
[
defmodule AssertCallVerification do
def main_function() do
[0, 1]
|> Enum.map(&(&1 + 1))
|> List.to_tuple()
|> elem(0)
end
end,
defmodule AssertCallVerification do
defp main_function() do
[0, 1]
|> Enum.map(&(&1 + 1))
|> List.to_tuple()
|> elem(0)
end
end
]
end

test_exercise_analysis "calls in macro bodies count if no calling_fn specified",
comments: ["didn't find any call to List.to_tuple/1 from main_function/0"] do
[
defmodule AssertCallVerification do
defmacro main_function() do
[0, 1]
|> Enum.map(&(&1 + 1))
|> List.to_tuple()
|> elem(0)
end
end,
defmodule AssertCallVerification do
defmacrop main_function() do
[0, 1]
|> Enum.map(&(&1 + 1))
|> List.to_tuple()
|> elem(0)
end
end
]
end

test_exercise_analysis "module level calls do not count",
comments: [
"didn't find any call to Enum.map/2 from anywhere",
"didn't find any call to List.to_tuple/1 from main_function/0"
] do
[
defmodule AssertCallVerification do
def main_function() do
1
end
end,
defmodule AssertCallVerification do
{@one, @two} = List.to_tuple([1, 2])

def main_function() do
@one
end
end,
defmodule AssertCallVerification do
{@one, @two} =
[0, 1]
|> Enum.map(&(&1 + 1))
|> List.to_tuple()

def main_function() do
@one
end
end
]
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,13 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.ModuleTrackingTest do
[
defmodule AssertCallVerification do
import MyModule
Custom.my_function()

def function(), do: Custom.my_function()
end,
defmodule AssertCallVerification do
import MyModule.Custom, only: [my_function: 0]
my_function()

def function(), do: my_function()
end
]
end
Expand Down
90 changes: 90 additions & 0 deletions test/elixir_analyzer/test_suite/dna_encoding_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,51 @@ defmodule ElixirAnalyzer.ExerciseTest.DNAEncodingTest do
]
end

test_exercise_analysis "allows the usage of Enum module for metaprogramming (outside of function bodies)",
comments: [] do
[
defmodule DNA do
@mapping %{
?\s => 0b0000,
?A => 0b0001,
?C => 0b0010,
?G => 0b0100,
?T => 0b1000
}
Enum.each(@mapping, fn {char, num} ->
def encode_nucleotide(unquote(char)), do: unquote(num)
end)

Enum.each(@mapping, fn {char, num} ->
def decode_nucleotide(unquote(num)), do: unquote(char)
end)

def encode(dna) do
do_encode(dna, <<>>)
end

defp do_encode([], acc), do: acc

defp do_encode([n | rest], acc) do
do_encode(rest, <<acc::bitstring, encode_nucleotide(n)::4>>)
end

def decode(dna) do
do_decode(dna, [])
end

defp do_decode(<<>>, acc), do: acc |> reverse()

defp do_decode(<<n::4, rest::bitstring>>, acc),
do: do_decode(rest, [decode_nucleotide(n) | acc])

defp reverse(l), do: do_reverse(l, [])
defp do_reverse([], acc), do: acc
defp do_reverse([h | t], acc), do: do_reverse(t, [h | acc])
end
]
end

test_exercise_analysis "detects the usage of the Stream module",
comments: [Constants.dna_encoding_use_recursion()] do
[
Expand Down Expand Up @@ -270,6 +315,51 @@ defmodule ElixirAnalyzer.ExerciseTest.DNAEncodingTest do
]
end

test_exercise_analysis "allows the usage of list comprehensions for metaprogramming (outside of function bodies)",
comments: [] do
[
defmodule DNA do
@mapping %{
?\s => 0b0000,
?A => 0b0001,
?C => 0b0010,
?G => 0b0100,
?T => 0b1000
}
for {char, num} <- @mapping do
def encode_nucleotide(unquote(char)), do: unquote(num)
end

for {char, num} <- @mapping do
def decode_nucleotide(unquote(num)), do: unquote(char)
end

def encode(dna) do
do_encode(dna, <<>>)
end

defp do_encode([], acc), do: acc

defp do_encode([n | rest], acc) do
do_encode(rest, <<acc::bitstring, encode_nucleotide(n)::4>>)
end

def decode(dna) do
do_decode(dna, [])
end

defp do_decode(<<>>, acc), do: acc |> reverse()

defp do_decode(<<n::4, rest::bitstring>>, acc),
do: do_decode(rest, [decode_nucleotide(n) | acc])

defp reverse(l), do: do_reverse(l, [])
defp do_reverse([], acc), do: acc
defp do_reverse([h | t], acc), do: do_reverse(t, [h | acc])
end
]
end

test_exercise_analysis "recursive solutions but no tail call recursion",
comments: [ElixirAnalyzer.Constants.dna_encoding_use_tail_call_recursion()] do
[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.ModuleLevelCall do
@moduledoc """
This is an exercise analyzer extension module to test assert_call calling a function from
outside any function/macro bodies.
"""

use ElixirAnalyzer.ExerciseTest

assert_call "find a call to Enum.map" do
type :informative
called_fn module: Enum, name: :map
comment "didn't find any call to Enum.map/2 from anywhere"
end

assert_call "find a call to List.to_tuple from main_function" do
type :informative
called_fn module: List, name: :to_tuple
calling_fn module: AssertCallVerification, name: :main_function
comment "didn't find any call to List.to_tuple/1 from main_function/0"
end
end

0 comments on commit bf1d881

Please sign in to comment.