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

nshkrdotcom / ElixirScope / e3c85b2dec2a55971a334d0061997410177723b4

28 May 2025 09:47AM UTC coverage: 59.568% (-4.1%) from 63.697%
e3c85b2dec2a55971a334d0061997410177723b4

push

github

NSHkr
Add new AST enhanced features with tests. 892 tests, 0 failures, 76 excluded

1752 of 3271 new or added lines in 17 files covered. (53.56%)

3 existing lines in 1 file now uncovered.

4797 of 8053 relevant lines covered (59.57%)

3798.33 hits per line

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

69.52
/lib/elixir_scope/ast_repository/parser.ex
1
defmodule ElixirScope.ASTRepository.Parser do
2
  @moduledoc """
3
  Enhanced AST parser that assigns unique node IDs to instrumentable AST nodes,
4
  extracts instrumentation points, and builds correlation indexes for runtime correlation.
5
  
6
  This module is the foundation for compile-time AST analysis with runtime correlation.
7
  """
8

9
  @doc """
10
  Assigns unique node IDs to instrumentable AST nodes.
11
  
12
  Instrumentable nodes include:
13
  - Function definitions (def, defp)
14
  - Pipe operations (|>)
15
  - Case statements
16
  - Try-catch blocks
17
  - Module attributes
18
  
19
  Returns {:ok, enhanced_ast} or {:error, reason}.
20
  """
21
  @spec assign_node_ids(Macro.t()) :: {:ok, Macro.t()} | {:error, term()}
22
  def assign_node_ids(nil), do: {:error, :empty_ast}
1✔
23
  def assign_node_ids(ast) when not is_tuple(ast), do: {:error, :invalid_ast}
×
24
  
25
  def assign_node_ids(ast) do
26
    try do
14✔
27
      {enhanced_ast, _counter} = assign_node_ids_recursive(ast, 0)
14✔
28
      {:ok, enhanced_ast}
29
    rescue
30
      error -> {:error, "Failed to assign node IDs: #{inspect(error)}"}
×
31
    end
32
  end
33

34
  @doc """
35
  Extracts instrumentation points from an enhanced AST.
36
  
37
  Returns {:ok, instrumentation_points} or {:error, reason}.
38
  """
39
  @spec extract_instrumentation_points(Macro.t()) :: {:ok, [map()]} | {:error, term()}
40
  def extract_instrumentation_points(ast) do
41
    try do
9✔
42
      points = extract_points_recursive(ast, [])
9✔
43
      {:ok, points}
44
    rescue
45
      error -> {:error, "Failed to extract instrumentation points: #{inspect(error)}"}
×
46
    end
47
  end
48

49
  @doc """
50
  Builds a correlation index from enhanced AST and instrumentation points.
51
  
52
  Returns {:ok, correlation_index} where correlation_index is a map of
53
  correlation_id -> ast_node_id.
54
  """
55
  @spec build_correlation_index(Macro.t(), [map()]) :: {:ok, map()} | {:error, term()}
56
  def build_correlation_index(_ast, instrumentation_points) do
57
    try do
4✔
58
      correlation_index = 
4✔
59
        instrumentation_points
60
        |> Enum.with_index()
61
        |> Enum.map(fn {point, index} ->
62
          correlation_id = generate_correlation_id(point, index)
29✔
63
          {correlation_id, point.ast_node_id}
29✔
64
        end)
65
        |> Map.new()
29✔
66
      
67
      {:ok, correlation_index}
68
    rescue
69
      error -> {:error, "Failed to build correlation index: #{inspect(error)}"}
×
70
    end
71
  end
72

73
  # Private functions
74

75
  defp assign_node_ids_recursive(ast, counter) do
76
    case ast do
1,506✔
77
      # Function definitions - always instrumentable
78
      {:def, meta, args} ->
79
        {new_meta, new_counter} = add_node_id_to_meta(meta, counter)
57✔
80
        {enhanced_args, final_counter} = assign_node_ids_to_args(args, new_counter)
57✔
81
        {{:def, new_meta, enhanced_args}, final_counter}
82
      
83
      {:defp, meta, args} ->
84
        {new_meta, new_counter} = add_node_id_to_meta(meta, counter)
35✔
85
        {enhanced_args, final_counter} = assign_node_ids_to_args(args, new_counter)
35✔
86
        {{:defp, new_meta, enhanced_args}, final_counter}
87
      
88
      # Pipe operations - instrumentable for data flow tracking
89
      {:|>, meta, args} ->
90
        {new_meta, new_counter} = add_node_id_to_meta(meta, counter)
18✔
91
        {enhanced_args, final_counter} = assign_node_ids_to_args(args, new_counter)
18✔
92
        {{:|>, new_meta, enhanced_args}, final_counter}
93
      
94
      # Case statements - instrumentable for control flow tracking
95
      {:case, meta, args} ->
96
        {new_meta, new_counter} = add_node_id_to_meta(meta, counter)
6✔
97
        {enhanced_args, final_counter} = assign_node_ids_to_args(args, new_counter)
6✔
98
        {{:case, new_meta, enhanced_args}, final_counter}
99
      
100
      # Try-catch blocks - instrumentable for error tracking
101
      {:try, meta, args} ->
102
        {new_meta, new_counter} = add_node_id_to_meta(meta, counter)
3✔
103
        {enhanced_args, final_counter} = assign_node_ids_to_args(args, new_counter)
3✔
104
        {{:try, new_meta, enhanced_args}, final_counter}
105
      
106
      # Module attributes - instrumentable for metadata tracking
107
      {:@, meta, args} ->
108
        {new_meta, new_counter} = add_node_id_to_meta(meta, counter)
3✔
109
        {enhanced_args, final_counter} = assign_node_ids_to_args(args, new_counter)
3✔
110
        {{:@, new_meta, enhanced_args}, final_counter}
111
      
112
      # Generic tuple with metadata - recurse into children
113
      {form, meta, args} when is_list(meta) ->
114
        {enhanced_args, new_counter} = assign_node_ids_to_args(args, counter)
630✔
115
        {{form, meta, enhanced_args}, new_counter}
116
      
117
      # Generic tuple without metadata - recurse into children
118
      {form, args} ->
119
        {enhanced_args, new_counter} = assign_node_ids_to_args(args, counter)
214✔
120
        {{form, enhanced_args}, new_counter}
121
      
122
      # Lists - recurse into elements
123
      list when is_list(list) ->
124
        {enhanced_list, new_counter} = 
166✔
125
          Enum.reduce(list, {[], counter}, fn item, {acc, cnt} ->
126
            {enhanced_item, new_cnt} = assign_node_ids_recursive(item, cnt)
165✔
127
            {[enhanced_item | acc], new_cnt}
128
          end)
129
        {Enum.reverse(enhanced_list), new_counter}
130
      
131
      # Atoms, numbers, strings, etc. - no enhancement needed
132
      other ->
374✔
133
        {other, counter}
134
    end
135
  end
136

137
  defp assign_node_ids_to_args(args, counter) when is_list(args) do
138
    Enum.reduce(args, {[], counter}, fn arg, {acc, cnt} ->
139
      {enhanced_arg, new_cnt} = assign_node_ids_recursive(arg, cnt)
867✔
140
      {[enhanced_arg | acc], new_cnt}
141
    end)
142
    |> then(fn {acc, cnt} -> {Enum.reverse(acc), cnt} end)
506✔
143
  end
144

145
  defp assign_node_ids_to_args(args, counter) do
146
    assign_node_ids_recursive(args, counter)
460✔
147
  end
148

149
  defp add_node_id_to_meta(meta, counter) do
150
    node_id = "ast_node_#{counter}_#{:erlang.unique_integer([:positive])}"
122✔
151
    new_meta = Keyword.put(meta, :ast_node_id, node_id)
122✔
152
    {new_meta, counter + 1}
153
  end
154

155
  defp extract_points_recursive(ast, acc) do
156
    case ast do
369✔
157
      # Module definitions - recurse into module body
158
      {:defmodule, _meta, [_module_name, [do: body]]} ->
159
        extract_points_recursive(body, acc)
8✔
160
      
161
      # Block structures - recurse into block contents
162
      {:__block__, _meta, statements} when is_list(statements) ->
163
        Enum.reduce(statements, acc, fn statement, statement_acc ->
7✔
164
          extract_points_recursive(statement, statement_acc)
58✔
165
        end)
166
      
167
      # Function definitions - match the actual AST structure
168
      {:def, meta, [{name, _meta2, args} | _rest]} when is_list(args) ->
169
        case Keyword.get(meta, :ast_node_id) do
35✔
170
          nil -> extract_from_children(ast, acc)
×
171
          node_id ->
172
            point = create_instrumentation_point(node_id, :function_entry, {name, length(args)}, meta, :public)
35✔
173
            extract_from_children(ast, [point | acc])
35✔
174
        end
175
      
176
      {:def, meta, [{name, _meta2, _args} | _rest]} ->
177
        case Keyword.get(meta, :ast_node_id) do
×
178
          nil -> extract_from_children(ast, acc)
×
179
          node_id ->
180
            point = create_instrumentation_point(node_id, :function_entry, {name, 0}, meta, :public)
×
181
            extract_from_children(ast, [point | acc])
×
182
        end
183
      
184
      {:defp, meta, [{name, _meta2, args} | _rest]} when is_list(args) ->
185
        case Keyword.get(meta, :ast_node_id) do
18✔
186
          nil -> extract_from_children(ast, acc)
×
187
          node_id ->
188
            point = create_instrumentation_point(node_id, :function_entry, {name, length(args)}, meta, :private)
18✔
189
            extract_from_children(ast, [point | acc])
18✔
190
        end
191
      
192
      {:defp, meta, [{name, _meta2, _args} | _rest]} ->
UNCOV
193
        case Keyword.get(meta, :ast_node_id) do
×
194
          nil -> extract_from_children(ast, acc)
×
195
          node_id ->
UNCOV
196
            point = create_instrumentation_point(node_id, :function_entry, {name, 0}, meta, :private)
×
UNCOV
197
            extract_from_children(ast, [point | acc])
×
198
        end
199
      
200
      # Pipe operations
201
      {:|>, meta, _args} ->
202
        case Keyword.get(meta, :ast_node_id) do
×
203
          nil -> extract_from_children(ast, acc)
×
204
          node_id ->
205
            point = create_instrumentation_point(node_id, :pipe_operation, nil, meta, :public)
×
206
            extract_from_children(ast, [point | acc])
×
207
        end
208
      
209
      # Case statements
210
      {:case, meta, _args} ->
211
        case Keyword.get(meta, :ast_node_id) do
×
212
          nil -> extract_from_children(ast, acc)
×
213
          node_id ->
214
            point = create_instrumentation_point(node_id, :case_statement, nil, meta, :public)
×
215
            extract_from_children(ast, [point | acc])
×
216
        end
217
      
218
      # Try-catch blocks
219
      {:try, meta, _args} ->
220
        case Keyword.get(meta, :ast_node_id) do
×
221
          nil -> extract_from_children(ast, acc)
×
222
          node_id ->
223
            point = create_instrumentation_point(node_id, :try_block, nil, meta, :public)
×
224
            extract_from_children(ast, [point | acc])
×
225
        end
226
      
227
      # Module attributes
228
      {:@, meta, _args} ->
229
        case Keyword.get(meta, :ast_node_id) do
1✔
230
          nil -> extract_from_children(ast, acc)
×
231
          node_id ->
232
            point = create_instrumentation_point(node_id, :module_attribute, nil, meta, :public)
1✔
233
            extract_from_children(ast, [point | acc])
1✔
234
        end
235
      
236
      # Recurse into other structures
237
      _ ->
238
        extract_from_children(ast, acc)
300✔
239
    end
240
  end
241

242
  defp extract_from_children(ast, acc) do
243
    case ast do
354✔
244
      {_form, _meta, args} when is_list(args) ->
245
        Enum.reduce(args, acc, fn child, child_acc ->
147✔
246
          extract_points_recursive(child, child_acc)
237✔
247
        end)
248
      
249
      {_form, args} when is_list(args) ->
250
        Enum.reduce(args, acc, fn child, child_acc ->
2✔
251
          extract_points_recursive(child, child_acc)
2✔
252
        end)
253
      
254
      list when is_list(list) ->
255
        Enum.reduce(list, acc, fn child, child_acc ->
60✔
256
          extract_points_recursive(child, child_acc)
55✔
257
        end)
258
      
259
      # Handle keyword lists (like [do: body])
260
      keyword_list when is_list(keyword_list) ->
261
        Enum.reduce(keyword_list, acc, fn
×
262
          {_key, value}, child_acc ->
263
            extract_points_recursive(value, child_acc)
×
264
          other, child_acc ->
265
            extract_points_recursive(other, child_acc)
×
266
        end)
267
      
268
      _ ->
269
        acc
145✔
270
    end
271
  end
272

273
  defp create_instrumentation_point(ast_node_id, type, function_info, meta, visibility) do
274
    base_point = %{
54✔
275
      ast_node_id: ast_node_id,
276
      type: determine_instrumentation_type(type, function_info, meta),
277
      function: function_info,
278
      visibility: visibility,
279
      has_pattern_matching: has_pattern_matching?(meta),
280
      has_guards: has_guards?(meta),
281
      line: Keyword.get(meta, :line),
282
      file: Keyword.get(meta, :file)
283
    }
284
    
285
    # Add type-specific metadata
286
    case type do
54✔
287
      :function_entry ->
288
        Map.merge(base_point, %{
53✔
289
          instrumentation_strategy: :function_tracing,
290
          capture_args: true,
291
          capture_return: true
292
        })
293
      
294
      :pipe_operation ->
295
        Map.merge(base_point, %{
×
296
          instrumentation_strategy: :data_flow_tracing,
297
          capture_input: true,
298
          capture_output: true
299
        })
300
      
301
      :case_statement ->
302
        Map.merge(base_point, %{
×
303
          instrumentation_strategy: :control_flow_tracing,
304
          capture_condition: true,
305
          capture_branches: true
306
        })
307
      
308
      _ ->
309
        base_point
1✔
310
    end
311
  end
312

313
  defp determine_instrumentation_type(:function_entry, {name, _arity}, meta) do
314
    cond do
53✔
315
      is_genserver_callback?(name) -> :genserver_callback
12✔
316
      is_phoenix_controller_action?(name, meta) -> :controller_action
41✔
317
      is_phoenix_live_view_callback?(name) -> :live_view_callback
35✔
318
      true -> :function_entry
35✔
319
    end
320
  end
321

322
  defp determine_instrumentation_type(type, _function_info, _meta), do: type
1✔
323

324
  defp has_pattern_matching?(_meta) do
54✔
325
    # This would require more sophisticated AST analysis
326
    # For now, we'll default to false and enhance later
327
    false
328
  end
329

330
  defp has_guards?(_meta) do
54✔
331
    # This would require more sophisticated AST analysis
332
    # For now, we'll default to false and enhance later
333
    false
334
  end
335

336
  defp is_genserver_callback?(name) do
337
    name in [:init, :handle_call, :handle_cast, :handle_info, :terminate, :code_change]
53✔
338
  end
339

340
  defp is_phoenix_controller_action?(name, _meta) do
341
    # Common Phoenix controller actions
342
    name in [:index, :show, :new, :create, :edit, :update, :delete]
41✔
343
  end
344

345
  defp is_phoenix_live_view_callback?(name) do
346
    name in [:mount, :handle_params, :handle_event, :handle_info, :render]
35✔
347
  end
348

349
  defp generate_correlation_id(point, index) do
350
    base = "corr_#{point.type}_#{index}"
29✔
351
    hash = :crypto.hash(:md5, "#{base}_#{point.ast_node_id}") |> Base.encode16(case: :lower)
29✔
352
    "#{base}_#{String.slice(hash, 0, 8)}"
29✔
353
  end
354
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

© 2026 Coveralls, Inc