• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

exercism / elixir-analyzer / 81bb2bd44c91598e28b337a76394d2632f36db5b

29 Sep 2025 12:19AM UTC coverage: 98.524%. Remained the same
81bb2bd44c91598e28b337a76394d2632f36db5b

push

github

web-flow
Add analyzer for gotta-snatch-em-all (#451)

* Add analyzer for gotta-snatch-em-all

* Fix space alignment & formatting

* Add tests for gotta snatch em all analyzer

* Fix wording & spelling mistakes

Co-authored-by: Jie <jie.gillet@gmail.com>

---------

Co-authored-by: Jie <jie.gillet@gmail.com>

868 of 881 relevant lines covered (98.52%)

16232.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.14
/lib/elixir_analyzer/exercise_test/feature.ex
1
defmodule ElixirAnalyzer.ExerciseTest.Feature do
2
  @moduledoc """
3
  Defines a `feature` macro that allows looking for specific snippets
4
  whose AST matches part of the AST of the solution.
5
  """
6

7
  alias ElixirAnalyzer.Comment
8
  alias ElixirAnalyzer.ExerciseTest.Feature.FeatureError
9

10
  @doc false
11
  defmacro __using__(_opts) do
12
    quote do
13
      import unquote(__MODULE__)
14

15
      @feature_tests []
16
    end
17
  end
18

19
  @doc """
20
  Store each feature in the @features attribute so we can compile them all at once later
21
  """
22
  defmacro feature(description, do: block) do
23
    feature_data = %{
172✔
24
      name: description,
25
      forms: []
26
    }
27

28
    :ok = validate_feature_block(block)
172✔
29
    {_, feature_data} = Macro.prewalk(block, feature_data, &gather_feature_data/2)
171✔
30

31
    # Check if feature forms are unique
32
    feature_data.forms
169✔
33
    |> Enum.reverse()
34
    |> Enum.with_index(1)
35
    |> Enum.sort()
36
    |> then(fn forms -> [forms, tl(forms)] end)
169✔
37
    |> Enum.zip_with(fn
169✔
38
      [{form, i}, {form, j}] ->
39
        raise FeatureError,
4✔
40
          message:
41
            "Forms number #{min(i, j)} and #{max(i, j)} of \"#{description}\" compile to the same value."
4✔
42

43
      _ ->
152✔
44
        :ok
45
    end)
46

47
    # made into a key-val list for better quoting
48
    feature_forms = Enum.sort(feature_data.forms)
165✔
49
    feature_data = Map.delete(feature_data, :forms)
165✔
50
    feature_data = Map.to_list(feature_data)
165✔
51

52
    unless Keyword.has_key?(feature_data, :comment) do
165✔
53
      raise "Comment must be defined for each feature test"
1✔
54
    end
55

56
    quote do
57
      # Check if the feature is unique
58
      case Enum.filter(@feature_tests, fn {data, forms} ->
59
             {Keyword.get(data, :find), Keyword.get(data, :depth), forms} ==
60
               {Keyword.get(unquote(feature_data), :find),
61
                Keyword.get(unquote(feature_data), :depth), unquote(Macro.escape(feature_forms))}
62
           end) do
63
        [{data, _forms} | _] ->
64
          raise FeatureError,
65
            message:
66
              "Features \"#{data[:name]}\" and \"#{unquote(description)}\" compile to the same value."
67

68
        _ ->
69
          @feature_tests [
70
            {unquote(feature_data), unquote(Macro.escape(feature_forms))} | @feature_tests
71
          ]
72
      end
73
    end
74
  end
75

76
  @supported_expressions [:comment, :type, :find, :suppress_if, :depth, :form]
77
  defp validate_feature_block({:__block__, _, args}) do
78
    Enum.each(args, fn {name, _, _} ->
172✔
79
      if name not in @supported_expressions do
823✔
80
        raise """
1✔
81
        Unsupported expression `#{name}`.
1✔
82
        The macro `feature` supports expressions: #{Enum.join(@supported_expressions, ", ")}.
83
        """
84
      end
85
    end)
86

87
    :ok
88
  end
89

90
  defp gather_feature_data({:type, _, [type]} = node, acc) do
91
    if not Comment.supported_type?(type) do
158✔
92
      raise """
1✔
93
      Unsupported type `#{type}`.
1✔
94
      The macro `feature` supports the following types: #{Enum.join(Comment.supported_types(), ", ")}.
95
      """
96
    end
97

98
    {node, put_in(acc, [:type], type)}
99
  end
100

101
  defp gather_feature_data({field, _, [f]} = node, acc)
328✔
102
       when field in [:comment, :find] do
103
    {node, put_in(acc, [field], f)}
104
  end
105

106
  defp gather_feature_data({:suppress_if, _, args} = node, acc) do
107
    case args do
2✔
108
      [name, condition] when condition in [:pass, :fail] ->
109
        acc = Map.update(acc, :suppress_if, [{name, condition}], &[{name, condition} | &1])
1✔
110
        {node, acc}
111

112
      _ ->
113
        raise """
1✔
114
        Invalid :suppress_if arguments. Arguments must have the form
115
          suppress_if "some check name", (:pass | :fail)
116
        """
117
    end
118
  end
119

120
  defp gather_feature_data({:depth, _, [f]} = node, acc) when is_integer(f) do
4✔
121
    {node, put_in(acc, [:depth], f)}
122
  end
123

124
  defp gather_feature_data({:form, _, [[do: form]]} = node, acc) do
125
    ast =
327✔
126
      Macro.prewalk(form, fn
127
        {name, _, param} -> {name, [:_ignore], param}
1,502✔
128
        node -> node
1,230✔
129
      end)
130

131
    {ast, block_params} =
327✔
132
      case ast do
133
        {:__block__, _, [param]} ->
×
134
          {param, false}
135

136
        {:__block__, _, [_ | _] = params} ->
1✔
137
          {params, length(params)}
138

139
        _ ->
326✔
140
          {ast, false}
141
      end
142

143
    {node,
144
     update_in(acc, [:forms], fn fs ->
327✔
145
       [[{:find_ast, ast}, {:block_params, block_params}] | fs]
146
     end)}
147
  end
148

149
  defp gather_feature_data(node, acc), do: {node, acc}
4,951✔
150
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc