Skip to content

Commit 46ba1b4

Browse files
New common check: Helper functions should be private (#207)
* Implement check * Add Source to read and hold exemploid files * Add tests for missing/corrupted example files * Send proper file name to compiler warnings * Read practice exercice example files in test cases * Add tests, fix old tests with public helpers * update submodule * mix format and mix credo * Typos and formatting * Refactor, remove undocumented :file and :module parameters * Add tests for private helpers * Change check to consider arity * Rework check test to show comment details * Update externel test CI and submodule * typo * Update submodule with cleaned up exemploids * Arguments to _, show first failure * Hide functions after @doc false or @impl true * Revert "Hide functions after @doc false or @impl true" This reverts commit 6e5c3a7. * Annotate AST to keep track of modules Co-authored-by: Angelika Tyborska <[email protected]>
1 parent 4cf4840 commit 46ba1b4

File tree

28 files changed

+931
-171
lines changed

28 files changed

+931
-171
lines changed

.github/workflows/elixir_test_external.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@ jobs:
1010
image: hexpm/elixir:1.12.1-erlang-24.0.1-ubuntu-focal-20210325
1111

1212
steps:
13-
- uses: actions/checkout@v1
13+
- name: Install git
14+
run: |
15+
apt-get update
16+
apt-get install -y git
17+
18+
- name: Checkout repository and submodules
19+
uses: actions/checkout@v2
20+
with:
21+
submodules: recursive
22+
23+
- name: Update submodules
24+
run: |
25+
git submodule update --recursive --remote
26+
1427
- name: Install Dependencies
1528
run: |
1629
mix local.rebar --force
1730
mix local.hex --force
1831
mix deps.get
32+
1933
- name: Run Tests
2034
run: mix test --only external

elixir

Submodule elixir updated 92 files

lib/elixir_analyzer.ex

Lines changed: 58 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule ElixirAnalyzer do
99
alias ElixirAnalyzer.Constants
1010
alias ElixirAnalyzer.Submission
1111
alias ElixirAnalyzer.Comment
12+
alias ElixirAnalyzer.Source
1213

1314
import ElixirAnalyzer.Summary, only: [summary: 2]
1415

@@ -85,8 +86,6 @@ defmodule ElixirAnalyzer do
8586
defaults = [
8687
{:exercise, exercise},
8788
{:path, input_path},
88-
{:file, nil},
89-
{:module, nil},
9089
{:output_path, output_path},
9190
{:output_file, @output_file},
9291
{:exercise_config, default_exercise_config()},
@@ -100,22 +99,28 @@ defmodule ElixirAnalyzer do
10099
# Do init work
101100
# -read config, create the initial Submission struct
102101
defp init(params) do
103-
submission = %Submission{
102+
source = %Source{
104103
path: params.path,
105-
code_path: nil,
106-
code_file: nil,
104+
slug: params.exercise
105+
}
106+
107+
submission = %Submission{
108+
source: source,
107109
analysis_module: nil
108110
}
109111

110112
try do
111113
Logger.debug("Getting the exercise config")
112114
exercise_config = params.exercise_config[params.exercise]
113-
{code_path, code_file, exemplar_path, analysis_module} = do_init(params, exercise_config)
115+
116+
{code_path, exercise_type, exemploid_path, analysis_module} =
117+
do_init(params, exercise_config)
114118

115119
Logger.debug("Initialization successful",
116120
path: params.path,
117121
code_path: code_path,
118-
exemplar_path: exemplar_path,
122+
exercise_type: exercise_type,
123+
exemploid_path: exemploid_path,
119124
analysis_module: analysis_module
120125
)
121126

@@ -128,11 +133,16 @@ defmodule ElixirAnalyzer do
128133
Logger.info("Exercise test suite '#{m}' found and loaded.")
129134
end
130135

136+
source = %{
137+
source
138+
| code_path: code_path,
139+
exercise_type: exercise_type,
140+
exemploid_path: exemploid_path
141+
}
142+
131143
%{
132144
submission
133-
| code_path: code_path,
134-
code_file: code_file,
135-
exemplar_path: exemplar_path,
145+
| source: source,
136146
analysis_module: analysis_module
137147
}
138148
rescue
@@ -159,99 +169,78 @@ defmodule ElixirAnalyzer do
159169
end
160170
end
161171

162-
# When file is nil, pull code params from config file
163-
defp do_init(%{file: nil} = params, exercise_config) do
172+
defp do_init(params, exercise_config) do
164173
meta_config = Path.join(params.path, @meta_config) |> File.read!() |> Jason.decode!()
165174
relative_code_path = meta_config["files"]["solution"] |> hd()
166-
full_code_path = Path.join(params.path, relative_code_path)
175+
code_path = Path.join(params.path, relative_code_path)
167176

168-
code_path = Path.dirname(full_code_path)
169-
code_file = Path.basename(full_code_path)
170-
171-
exemplar_path =
172-
case meta_config["files"]["exemplar"] do
173-
[path | _] -> Path.join(params.path, path)
174-
_ -> nil
177+
{exercise_type, exemploid_path} =
178+
case meta_config["files"] do
179+
%{"exemplar" => [path | _]} -> {:concept, Path.join(params.path, path)}
180+
%{"example" => [path | _]} -> {:practice, Path.join(params.path, path)}
175181
end
176182

177-
{code_path, code_file, exemplar_path,
183+
{code_path, exercise_type, exemploid_path,
178184
exercise_config[:analyzer_module] || ElixirAnalyzer.TestSuite.Default}
179185
end
180186

181-
# Else, use passed in params to analyze
182-
defp do_init(params, _exercise_config) do
183-
{
184-
params.path,
185-
params.file,
186-
String.to_existing_atom("ElixirAnalyzer.ExerciseTest.#{params.module}")
187-
}
188-
end
189-
190187
# Check
191188
# - check if the file exists
192189
# - read in the code
193-
# - check if there is an exemplar
194-
# - read in the exemplar
195-
# - parse the exemplar into an AST
190+
# - check if there is an exemploid
191+
# - read in the exemploid
192+
# - parse the exemploid into an AST
196193
defp check(%Submission{halted: true} = submission, _params) do
197194
Logger.warning("Check not performed, halted previously")
198195
submission
199196
end
200197

201-
defp check(%Submission{} = submission, _params) do
202-
with path_to_code <- Path.join(submission.code_path, submission.code_file),
203-
:ok <- Logger.info("Attempting to read code file", code_file_path: path_to_code),
204-
{:code_read, {:ok, code_str}} <- {:code_read, File.read(path_to_code)},
205-
:ok <- Logger.info("Code file read successfully"),
206-
submission <- %{submission | code: code_str},
207-
:ok <- Logger.info("Check if exemplar exists", exemplar_path: submission.exemplar_path),
208-
{:exemplar_exists, submission, exemplar_path} when not is_nil(exemplar_path) <-
209-
{:exemplar_exists, submission, submission.exemplar_path},
210-
:ok <-
211-
Logger.info("Exemplar file exists, attempting to read", exemplar_path: exemplar_path),
212-
{:exemplar_read, submission, {:ok, exemplar_code}} <-
213-
{:exemplar_read, submission, File.read(exemplar_path)},
214-
:ok <- Logger.info("Exemplar file read successfully, attempting to parse"),
215-
{:exemplar_ast, submission, {:ok, exemplar_code}} <-
216-
{:exemplar_ast, submission, Code.string_to_quoted(exemplar_code)},
217-
:ok <- Logger.info("Exemplar file parsed successfully"),
218-
submission <- %{submission | exemplar_code: exemplar_code} do
219-
submission
198+
defp check(%Submission{source: source} = submission, _params) do
199+
Logger.info("Attempting to read code file", code_file_path: source.code_path)
200+
201+
with {:code_read, {:ok, code_string}} <- {:code_read, File.read(source.code_path)},
202+
source <- %{source | code_string: code_string},
203+
Logger.info("Code file read successfully"),
204+
Logger.info("Attempting to read exemploid", exemploid_path: source.exemploid_path),
205+
{:exemploid_read, _, {:ok, exemploid_string}} <-
206+
{:exemploid_read, source, File.read(source.exemploid_path)},
207+
Logger.info("Exemploid file read successfully, attempting to parse"),
208+
{:exemploid_ast, _, {:ok, exemploid_ast}} <-
209+
{:exemploid_ast, source, Code.string_to_quoted(exemploid_string)} do
210+
Logger.info("Exemploid file parsed successfully")
211+
source = %{source | exemploid_string: exemploid_string, exemploid_ast: exemploid_ast}
212+
%{submission | source: source}
220213
else
221214
{:code_read, {:error, reason}} ->
222215
Logger.warning("TestSuite halted: Code file not found. Reason: #{reason}",
223-
path: submission.path,
224-
file_name: submission.code_file
216+
path: source.path,
217+
code_path: source.code_path
225218
)
226219

227220
submission
228221
|> Submission.halt()
229222
|> Submission.append_comment(%Comment{
230223
comment: Constants.general_file_not_found(),
231224
params: %{
232-
"file_name" => submission.code_file,
233-
"path" => submission.path
225+
"file_name" => Path.basename(source.code_path),
226+
"path" => source.path
234227
},
235228
type: :essential
236229
})
237230

238-
{:exemplar_exists, submission, nil} ->
239-
Logger.info("There is no exemplar file for this exercise")
240-
submission
241-
242-
{:exemplar_read, submission, {:error, reason}} ->
243-
Logger.warning("Exemplar file not found. Reason: #{reason}",
244-
exemplar_path: submission.exemplar_path
231+
{:exemploid_read, source, {:error, reason}} ->
232+
Logger.warning("Exemploid file not found. Reason: #{reason}",
233+
exemploid_path: source.exemploid_path
245234
)
246235

247-
submission
236+
%{submission | source: source}
248237

249-
{:exemplar_ast, submission, {:error, reason}} ->
250-
Logger.warning("Exemplar file could not be parsed. Reason: #{inspect(reason)}",
251-
exemplar_code: submission.exemplar_code
238+
{:exemploid_ast, source, {:error, reason}} ->
239+
Logger.warning("Exemploid file could not be parsed. Reason: #{inspect(reason)}",
240+
exemploid_path: source.exemploid_path
252241
)
253242

254-
submission
243+
%{submission | source: source}
255244
end
256245
end
257246

@@ -267,7 +256,7 @@ defmodule ElixirAnalyzer do
267256

268257
submission =
269258
submission
270-
|> submission.analysis_module.analyze(submission.code, submission.exemplar_code)
259+
|> submission.analysis_module.analyze(submission.source)
271260
|> Submission.set_analyzed(true)
272261

273262
Logger.info("Analyzing code complete")

lib/elixir_analyzer/constants.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ defmodule ElixirAnalyzer.Constants do
3535
solution_no_integer_literal: "elixir.solution.no_integer_literal",
3636
solution_boilerplate_comment: "elixir.solution.boilerplate_comment",
3737
solution_todo_comment: "elixir.solution.todo_comment",
38+
solution_private_helper_functions: "elixir.solution.private_helper_functions",
3839

3940
# Concept exercises
4041

lib/elixir_analyzer/exercise_test.ex

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule ElixirAnalyzer.ExerciseTest do
99
alias ElixirAnalyzer.Submission
1010
alias ElixirAnalyzer.Constants
1111
alias ElixirAnalyzer.Comment
12+
alias ElixirAnalyzer.Source
1213

1314
@doc false
1415
defmacro __using__(opts) do
@@ -21,7 +22,7 @@ defmodule ElixirAnalyzer.ExerciseTest do
2122

2223
import unquote(__MODULE__)
2324
@before_compile unquote(__MODULE__)
24-
@dialyzer no_match: {:do_analyze, 4}
25+
@dialyzer no_match: {:do_analyze, 2}
2526
end
2627
end
2728

@@ -38,7 +39,7 @@ defmodule ElixirAnalyzer.ExerciseTest do
3839

3940
# placeholders for submission code
4041
code_ast = quote do: code_ast
41-
code_as_string = quote do: code_as_string
42+
code_string = quote do: code_string
4243

4344
# compile each feature to a test
4445
feature_tests = Enum.map(feature_test_data, &FeatureCompiler.compile(&1, code_ast))
@@ -48,28 +49,34 @@ defmodule ElixirAnalyzer.ExerciseTest do
4849

4950
# compile each check_source to a test
5051
check_source_tests =
51-
Enum.map(check_source_data, &CheckSourceCompiler.compile(&1, code_as_string))
52+
Enum.map(check_source_data, &CheckSourceCompiler.compile(&1, code_string))
5253

5354
quote do
54-
@spec analyze(Submission.t(), String.t(), nil | Macro.t()) :: Submission.t()
55-
def analyze(%Submission{} = submission, code_as_string, exemplar_ast) do
56-
case Code.string_to_quoted(code_as_string) do
55+
@spec analyze(Submission.t(), Source.t()) :: Submission.t()
56+
def analyze(%Submission{} = submission, %Source{code_string: code_string} = source) do
57+
case Code.string_to_quoted(code_string) do
5758
{:ok, code_ast} ->
58-
do_analyze(submission, code_ast, code_as_string, exemplar_ast)
59+
source = %{source | code_ast: code_ast}
60+
do_analyze(submission, source)
5961

6062
{:error, e} ->
6163
append_analysis_failure(submission, e)
6264
end
6365
end
6466

65-
defp do_analyze(%Submission{} = submission, code_ast, code_as_string, exemplar_ast)
66-
when is_binary(code_as_string) do
67+
defp do_analyze(
68+
%Submission{} = submission,
69+
%Source{
70+
code_string: code_string,
71+
code_ast: code_ast
72+
} = source
73+
) do
6774
results =
6875
Enum.concat([
6976
unquote(feature_tests),
7077
unquote(assert_call_tests),
7178
unquote(check_source_tests),
72-
CommonChecks.run(code_ast, code_as_string, exemplar_ast)
79+
CommonChecks.run(source)
7380
])
7481
|> filter_suppressed_results()
7582

lib/elixir_analyzer/exercise_test/common_checks.ex

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ defmodule ElixirAnalyzer.ExerciseTest.CommonChecks do
1212
BooleanFunctions,
1313
ExemplarComparison,
1414
Indentation,
15+
PrivateHelperFunctions,
1516
Comments
1617
}
1718

1819
alias ElixirAnalyzer.Comment
20+
alias ElixirAnalyzer.Source
1921

2022
# CommonChecks that use feature or assert_call should be called here
2123
defmacro __using__(_opts) do
@@ -27,18 +29,25 @@ defmodule ElixirAnalyzer.ExerciseTest.CommonChecks do
2729
end
2830
end
2931

30-
@spec run(Macro.t(), String.t(), nil | Macro.t()) :: [{:pass | :fail | :skip, %Comment{}}]
31-
def run(code_ast, code_as_string, exemplar_ast) when is_binary(code_as_string) do
32+
@spec run(Source.t()) :: [{:pass | :fail | :skip, %Comment{}}]
33+
def run(%Source{
34+
code_path: code_path,
35+
code_ast: code_ast,
36+
code_string: code_string,
37+
exercise_type: type,
38+
exemploid_ast: exemploid_ast
39+
}) do
3240
[
3341
FunctionNames.run(code_ast),
3442
VariableNames.run(code_ast),
3543
ModuleAttributeNames.run(code_ast),
3644
ModulePascalCase.run(code_ast),
37-
CompilerWarnings.run(code_ast),
45+
CompilerWarnings.run(code_path, code_ast),
3846
BooleanFunctions.run(code_ast),
39-
ExemplarComparison.run(code_ast, exemplar_ast),
40-
Indentation.run(code_ast, code_as_string),
41-
Comments.run(code_ast, code_as_string)
47+
ExemplarComparison.run(code_ast, type, exemploid_ast),
48+
Indentation.run(code_ast, code_string),
49+
PrivateHelperFunctions.run(code_ast, exemploid_ast),
50+
Comments.run(code_ast, code_string)
4251
]
4352
|> List.flatten()
4453
end

lib/elixir_analyzer/exercise_test/common_checks/compiler_warnings.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.CompilerWarnings do
55
alias ElixirAnalyzer.Constants
66
alias ElixirAnalyzer.Comment
77

8-
def run(code_ast) do
8+
def run(code_path, code_ast) do
99
import ExUnit.CaptureIO
1010
Application.put_env(:elixir, :ansi_enabled, false)
1111

1212
warnings =
1313
capture_io(:stderr, fn ->
1414
try do
15-
Code.compile_quoted(code_ast)
15+
Code.compile_quoted(code_ast, Path.basename(code_path))
1616
|> Enum.each(fn {module, _binary} ->
1717
:code.delete(module)
1818
:code.purge(module)

0 commit comments

Comments
 (0)