Skip to content

Commit c9c1ed5

Browse files
Require tail-call recursion in dna-encoding (#374)
1 parent 2b248b1 commit c9c1ed5

6 files changed

Lines changed: 575 additions & 4 deletions

File tree

lib/elixir_analyzer/constants.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ defmodule ElixirAnalyzer.Constants do
8484

8585
# DNA Encoding Comments
8686
dna_encoding_use_recursion: "elixir.dna-encoding.use_recursion",
87+
dna_encoding_use_tail_call_recursion: "elixir.dna-encoding.use_tail_call_recursion",
8788

8889
# File Sniffer Comments
8990
file_sniffer_use_pattern_matching: "elixir.file-sniffer.use_pattern_matching",

lib/elixir_analyzer/exercise_test/check_source/compiler.ex

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,21 @@ defmodule ElixirAnalyzer.ExerciseTest.CheckSource.Compiler do
2020

2121
quote do
2222
(fn %Source{} = source ->
23-
if unquote(check_function).(source) do
24-
{:pass, unquote(test_description)}
25-
else
26-
{:fail, unquote(test_description)}
23+
case unquote(check_function).(source) do
24+
true ->
25+
{:pass, unquote(test_description)}
26+
27+
false ->
28+
{:fail, unquote(test_description)}
29+
30+
{true, params} when is_map(params) ->
31+
{:pass, %{unquote(test_description) | params: params}}
32+
33+
{false, params} when is_map(params) ->
34+
{:fail, %{unquote(test_description) | params: params}}
35+
36+
_ ->
37+
raise "check must be a boolean or a tuple with a boolean and a map of parameters for the comment"
2738
end
2839
end).(unquote(code_source))
2940
end

lib/elixir_analyzer/test_suite/dna_encoding.ex

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule ElixirAnalyzer.TestSuite.DNAEncoding do
55

66
use ElixirAnalyzer.ExerciseTest
77
alias ElixirAnalyzer.Constants
8+
alias ElixirAnalyzer.Source
89

910
assert_no_call "does not call any Enum functions" do
1011
type :essential
@@ -29,4 +30,131 @@ defmodule ElixirAnalyzer.TestSuite.DNAEncoding do
2930
called_fn name: :for
3031
comment Constants.dna_encoding_use_recursion()
3132
end
33+
34+
check_source "uses tail call recursion" do
35+
type :essential
36+
suppress_if "does not call any Enum functions", :fail
37+
suppress_if "does not call any Stream functions", :fail
38+
suppress_if "does not call any List functions", :fail
39+
suppress_if "does not use list comprehensions", :fail
40+
comment Constants.dna_encoding_use_tail_call_recursion()
41+
42+
check(%Source{code_ast: code_ast}) do
43+
{_, tail_call_recursive_functions} =
44+
Macro.prewalk(
45+
code_ast,
46+
[],
47+
fn node, acc -> find_tail_call_recursive_functions(node, acc) end
48+
)
49+
50+
tail_call_recursive_functions = Enum.uniq(tail_call_recursive_functions)
51+
52+
{_, all_recursive_functions} =
53+
Macro.prewalk(
54+
code_ast,
55+
[],
56+
fn node, acc -> find_all_recursive_functions(node, acc) end
57+
)
58+
59+
all_recursive_functions = Enum.uniq(all_recursive_functions)
60+
61+
non_tail_call_recursive_functions = all_recursive_functions -- tail_call_recursive_functions
62+
63+
if non_tail_call_recursive_functions != [] || tail_call_recursive_functions == [] do
64+
{false,
65+
%{
66+
non_tail_call_recursive_functions:
67+
format_function_names(non_tail_call_recursive_functions),
68+
tail_call_recursive_functions: format_function_names(tail_call_recursive_functions)
69+
}}
70+
else
71+
true
72+
end
73+
end
74+
end
75+
76+
defp format_function_names(list) do
77+
if list == [] do
78+
"none"
79+
else
80+
Enum.map(list, fn {name, arity} -> "`#{name}/#{arity}`" end)
81+
|> Enum.join(", ")
82+
end
83+
end
84+
85+
defp find_tail_call_recursive_functions(node, acc) do
86+
acc =
87+
case node do
88+
{op, _meta1, [{:when, _meta2, [{fn_name, _meta3, args} | _]}, opts]}
89+
when op in [:def, :defp] ->
90+
check_if_function_tail_call_recursive(acc, fn_name, args, opts)
91+
92+
{op, _meta1, [{fn_name, _meta2, args}, opts]} when op in [:def, :defp] ->
93+
check_if_function_tail_call_recursive(acc, fn_name, args, opts)
94+
95+
_ ->
96+
acc
97+
end
98+
99+
{node, acc}
100+
end
101+
102+
defp check_if_function_tail_call_recursive(acc, fn_name, args, opts) do
103+
fn_arity = length(args)
104+
105+
last_call_in_function_def =
106+
case opts[:do] do
107+
{:__block, _, calls} when is_list(calls) ->
108+
List.last(calls)
109+
110+
calls when is_list(calls) ->
111+
List.last(calls)
112+
113+
call ->
114+
call
115+
end
116+
117+
case last_call_in_function_def do
118+
{^fn_name, _meta3, args} when length(args) == fn_arity ->
119+
[{fn_name, fn_arity} | acc]
120+
121+
_ ->
122+
acc
123+
end
124+
end
125+
126+
defp find_all_recursive_functions(node, acc) do
127+
acc =
128+
case node do
129+
{op, _meta1, [{:when, _meta2, [{fn_name, _meta3, args} | _]}, opts]}
130+
when op in [:def, :defp] ->
131+
check_if_function_recursive(acc, fn_name, args, opts)
132+
133+
{op, _meta1, [{fn_name, _meta2, args}, opts]} when op in [:def, :defp] ->
134+
check_if_function_recursive(acc, fn_name, args, opts)
135+
136+
_ ->
137+
acc
138+
end
139+
140+
{node, acc}
141+
end
142+
143+
defp check_if_function_recursive(acc, fn_name, args, opts) do
144+
fn_arity = length(args)
145+
146+
{_, any_nested_recursive_calls?} =
147+
Macro.prewalk(opts[:do], false, fn node, acc ->
148+
case node do
149+
{^fn_name, _, args} when length(args) == fn_arity -> {node, true}
150+
_ -> {node, acc}
151+
end
152+
end)
153+
154+
if any_nested_recursive_calls? do
155+
[{fn_name, fn_arity} | acc]
156+
else
157+
acc
158+
end
159+
end
32160
end

test/elixir_analyzer/exercise_test/check_source_test.exs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ defmodule ElixirAnalyzer.ExerciseTest.CheckSourceTest do
22
use ElixirAnalyzer.ExerciseTestCase,
33
exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.CheckSource
44

5+
alias ElixirAnalyzer.Submission
6+
57
test_exercise_analysis "empty module",
68
comments: ["always return false", "didn't use multiline"] do
79
~S"""
@@ -206,4 +208,33 @@ defmodule ElixirAnalyzer.ExerciseTest.CheckSourceTest do
206208
end
207209
end
208210
end
211+
212+
test "allows passing comment params" do
213+
code_string = ~S"""
214+
defmodule Fruits do
215+
@best_fruit = "banana"
216+
@best_fruit_for_breakfast = "banana"
217+
@best_fruit_for_a_snack = "banana"
218+
end
219+
"""
220+
221+
source =
222+
ElixirAnalyzer.ExerciseTestCase.find_source(
223+
ElixirAnalyzer.Support.AnalyzerVerification.CheckSource
224+
)
225+
226+
result =
227+
ElixirAnalyzer.Support.AnalyzerVerification.CheckSource.analyze(%Submission{
228+
source: %{source | code_string: code_string},
229+
analysis_module: ElixirAnalyzer.Support.AnalyzerVerification.CheckSource
230+
})
231+
232+
comment = Enum.find(result.comments, fn comment -> comment.comment == "even banana count" end)
233+
234+
assert comment == %{
235+
comment: "even banana count",
236+
params: %{banana_count: 3},
237+
type: :actionable
238+
}
239+
end
209240
end

0 commit comments

Comments
 (0)